diff --git a/local/recipes/kde/kf6-kcmutils/source/CMakeLists.txt b/local/recipes/kde/kf6-kcmutils/source/CMakeLists.txt index b553483ede..f45e081592 100644 --- a/local/recipes/kde/kf6-kcmutils/source/CMakeLists.txt +++ b/local/recipes/kde/kf6-kcmutils/source/CMakeLists.txt @@ -117,6 +117,7 @@ find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) +find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) # shall we use DBus? # enabled per default on Linux & BSD systems diff --git a/local/recipes/kde/kf6-kcolorscheme/source/CMakeLists.txt b/local/recipes/kde/kf6-kcolorscheme/source/CMakeLists.txt index 0409716918..dcf832d81f 100644 --- a/local/recipes/kde/kf6-kcolorscheme/source/CMakeLists.txt +++ b/local/recipes/kde/kf6-kcolorscheme/source/CMakeLists.txt @@ -97,6 +97,7 @@ find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) +find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) set(EXCLUDE_DEPRECATED_BEFORE_AND_AT 0 CACHE STRING "Control the range of deprecated API excluded from the build [default=0].") diff --git a/local/recipes/kde/kf6-kcompletion/source/CMakeLists.txt b/local/recipes/kde/kf6-kcompletion/source/CMakeLists.txt index d1d4881da0..b89fb79814 100644 --- a/local/recipes/kde/kf6-kcompletion/source/CMakeLists.txt +++ b/local/recipes/kde/kf6-kcompletion/source/CMakeLists.txt @@ -86,6 +86,7 @@ find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) +find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(KF6Codecs ${KF_DEP_VERSION} REQUIRED) find_package(KF6Config ${KF_DEP_VERSION} REQUIRED) diff --git a/local/recipes/kde/kf6-kconfigwidgets/source/CMakeLists.txt b/local/recipes/kde/kf6-kconfigwidgets/source/CMakeLists.txt index ffa4f2adbf..6283228df0 100644 --- a/local/recipes/kde/kf6-kconfigwidgets/source/CMakeLists.txt +++ b/local/recipes/kde/kf6-kconfigwidgets/source/CMakeLists.txt @@ -89,6 +89,7 @@ find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) +find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) # shall we use DBus? # enabled per default on Linux & BSD systems diff --git a/local/recipes/kde/kf6-kdeclarative/source/CMakeLists.txt b/local/recipes/kde/kf6-kdeclarative/source/CMakeLists.txt index 47458d1663..0ab0c43803 100644 --- a/local/recipes/kde/kf6-kdeclarative/source/CMakeLists.txt +++ b/local/recipes/kde/kf6-kdeclarative/source/CMakeLists.txt @@ -32,7 +32,7 @@ find_package(KF6GuiAddons ${KF_DEP_VERSION} REQUIRED) if(NOT WIN32 AND NOT APPLE AND NOT ANDROID AND NOT REDOX) -######################################################## find_package(KF6GlobalAccel ${KF_DEP_VERSION} REQUIRED) +######################################################### find_package(KF6GlobalAccel ${KF_DEP_VERSION} REQUIRED) set(HAVE_KGLOBALACCEL TRUE) else() set(HAVE_KGLOBALACCEL FALSE) diff --git a/local/recipes/kde/kf6-kiconthemes/source/CMakeLists.txt b/local/recipes/kde/kf6-kiconthemes/source/CMakeLists.txt index 9b516742e0..56acc842b5 100644 --- a/local/recipes/kde/kf6-kiconthemes/source/CMakeLists.txt +++ b/local/recipes/kde/kf6-kiconthemes/source/CMakeLists.txt @@ -108,6 +108,7 @@ find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) +find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6Svg ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE) # shall we use DBus? diff --git a/local/recipes/kde/kf6-kjobwidgets/source/CMakeLists.txt b/local/recipes/kde/kf6-kjobwidgets/source/CMakeLists.txt index f3d8f2dacb..17f0a21925 100644 --- a/local/recipes/kde/kf6-kjobwidgets/source/CMakeLists.txt +++ b/local/recipes/kde/kf6-kjobwidgets/source/CMakeLists.txt @@ -75,6 +75,7 @@ find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) +find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) if(NOT WIN32 AND NOT APPLE AND NOT ANDROID AND NOT HAIKU) option(WITH_X11 "Build with support for QX11Info::appUserTime()" ON) diff --git a/local/recipes/kde/kf6-knotifications/source/src/notifications_interface.cpp b/local/recipes/kde/kf6-knotifications/source/src/notifications_interface.cpp index bc0ce4ec67..8c42029459 100644 --- a/local/recipes/kde/kf6-knotifications/source/src/notifications_interface.cpp +++ b/local/recipes/kde/kf6-knotifications/source/src/notifications_interface.cpp @@ -9,7 +9,7 @@ * before re-generating it. */ -#include "/home/kellito/Builds/RedBear-OS/local/recipes/kde/kf6-knotifications/source/src/notifications_interface.h" +#include "/mnt/data/Builds/RedBear-OS/local/recipes/kde/kf6-knotifications/source/src/notifications_interface.h" /* * Implementation of interface class OrgFreedesktopNotificationsInterface diff --git a/local/recipes/kde/kf6-ksvg/source.tar b/local/recipes/kde/kf6-ksvg/source.tar index f0ae5e6c27..464cf0d060 100644 Binary files a/local/recipes/kde/kf6-ksvg/source.tar and b/local/recipes/kde/kf6-ksvg/source.tar differ diff --git a/local/recipes/kde/kf6-ksvg/source/.git-blame-ignore-revs b/local/recipes/kde/kf6-ksvg/source/.git-blame-ignore-revs new file mode 100644 index 0000000000..cc102c7e78 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/.git-blame-ignore-revs @@ -0,0 +1,6 @@ +#clang-format +08d4f6335bcd60306b243f4fd53d1ea60a995a06 +#clang-tidy +3120de70f25378cad1faf0ee4e96ac506006b953 +# re-run of clang-format +30f46ade664d7a9492817befa838297aecc44506 diff --git a/local/recipes/kde/kf6-ksvg/source/.gitignore b/local/recipes/kde/kf6-ksvg/source/.gitignore new file mode 100644 index 0000000000..31ded7dc90 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/.gitignore @@ -0,0 +1,28 @@ +# 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/ +cmake-build-debug* +.idea +/.clang-format +/compile_commands.json +.clangd +.cache diff --git a/local/recipes/kde/kf6-ksvg/source/.gitlab-ci.yml b/local/recipes/kde/kf6-ksvg/source/.gitlab-ci.yml new file mode 100644 index 0000000000..9adc353983 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/.gitlab-ci.yml @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2020 Volker Krause +# SPDX-License-Identifier: CC0-1.0 + +include: + - project: sysadmin/ci-utilities + file: + - /gitlab-templates/linux-qt6.yml + - /gitlab-templates/linux-qt6-next.yml + - /gitlab-templates/freebsd-qt6.yml + - /gitlab-templates/windows-qt6.yml + - /gitlab-templates/android-qt6.yml + - /gitlab-templates/alpine-qt6.yml + - /gitlab-templates/xml-lint.yml + - /gitlab-templates/yaml-lint.yml diff --git a/local/recipes/kde/kf6-ksvg/source/.kde-ci.yml b/local/recipes/kde/kf6-ksvg/source/.kde-ci.yml new file mode 100644 index 0000000000..25a157c3b9 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/.kde-ci.yml @@ -0,0 +1,16 @@ +Dependencies: + - 'on': ['Linux', 'FreeBSD', 'Windows', 'macOS', 'Android'] + 'require': + 'frameworks/extra-cmake-modules': '@same' + 'frameworks/karchive': '@same' + 'frameworks/kconfig': '@same' + 'frameworks/kcoreaddons': '@same' + 'frameworks/kguiaddons': '@same' + 'frameworks/kirigami': '@same' + 'frameworks/kcolorscheme': '@same' + +Options: + test-before-installing: True + cppcheck-ignore-files: ['templates/'] + require-passing-tests-on: ['Linux', 'FreeBSD', 'Windows', 'macOS'] + enable-lsan: True diff --git a/local/recipes/kde/kf6-ksvg/source/CMakeLists.txt b/local/recipes/kde/kf6-ksvg/source/CMakeLists.txt new file mode 100644 index 0000000000..f6badb25a8 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/CMakeLists.txt @@ -0,0 +1,116 @@ +cmake_minimum_required(VERSION 3.29) + +set(KF_VERSION "6.10.0") # handled by release scripts +set(KF_DEP_VERSION "6.10.0") # handled by release scripts +project(KSvg VERSION ${KF_VERSION}) + +# ECM setup +include(FeatureSummary) +find_package(ECM 6.10.0 NO_MODULE) +set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://commits.kde.org/extra-cmake-modules") +feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES) + +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) + +include(KDEInstallDirs) +include(KDECMakeSettings) +include(KDEGitCommitHooks) +include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) + +include(ECMGenerateExportHeader) +include(ECMGenerateHeaders) +include(CMakePackageConfigHelpers) +include(ECMSetupVersion) +include(ECMQtDeclareLoggingCategory) +include(KDEPackageAppTemplates) +include(ECMGenerateQmlTypes) +include(ECMMarkNonGuiExecutable) +include(ECMDeprecationSettings) +include(ECMQmlModule) +include(ECMGenerateQDoc) + +ecm_setup_version(PROJECT + VARIABLE_PREFIX KSVG + VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/ksvg_version.h" + PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF6SvgConfigVersion.cmake" + SOVERSION 6) + +################# now find all used packages ################# + +set (REQUIRED_QT_VERSION 6.9.0) + +find_package(Qt6 ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE COMPONENTS Quick Gui Qml Svg QuickControls2) + +find_package(KF6 ${KF_DEP_VERSION} REQUIRED + COMPONENTS + Archive # svgz + Config # rects cache + ColorScheme + CoreAddons + GuiAddons # KImageCache + KirigamiPlatform # Kirigami.Theme +) + +######################################################################### + +ecm_set_disabled_deprecation_versions( + QT 6.11.0 + KF 6.23.0 +) + +#add_definitions(-Wno-deprecated) + +######################################################################### + +option(BUILD_TOOLS "Build and install KSVG tools." OFF) + +option(BUILD_COVERAGE "Build Plasma Frameworks with gcov support" OFF) + +if(BUILD_COVERAGE) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -lgcov") +endif() + +# make ksvg_version.h available +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +################# list the subdirectories ################# +add_subdirectory(src) + +if (BUILD_TESTING) + add_subdirectory(autotests) +endif() + +################ create PlasmaConfig.cmake and install it ########################### + +# create a Config.cmake and a ConfigVersion.cmake file and install them + +set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF6Svg") + +configure_package_config_file( + "${CMAKE_CURRENT_SOURCE_DIR}/KF6SvgConfig.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/KF6SvgConfig.cmake" + INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR} + PATH_VARS CMAKE_INSTALL_PREFIX +) + +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/KF6SvgConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/KF6SvgConfigVersion.cmake" + DESTINATION "${CMAKECONFIG_INSTALL_DIR}" + COMPONENT Devel +) + +install(EXPORT KF6SvgTargets + DESTINATION "${CMAKECONFIG_INSTALL_DIR}" + FILE KF6SvgTargets.cmake + NAMESPACE KF6:: + COMPONENT Devel) + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/ksvg_version.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/KSvg COMPONENT Devel ) + +include(ECMFeatureSummary) +ecm_feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) + +kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT) diff --git a/local/recipes/kde/kf6-ksvg/source/KF6SvgConfig.cmake.in b/local/recipes/kde/kf6-ksvg/source/KF6SvgConfig.cmake.in new file mode 100644 index 0000000000..2a36a526c3 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/KF6SvgConfig.cmake.in @@ -0,0 +1,16 @@ +@PACKAGE_INIT@ + +# Any changes in this ".cmake" file will be overwritten by CMake, the source is the ".cmake.in" file. + +include("${CMAKE_CURRENT_LIST_DIR}/KF6SvgTargets.cmake") + +set(KSvg_INSTALL_PREFIX "@PACKAGE_CMAKE_INSTALL_PREFIX@") + +set(KSvg_LIBRARIES KF6::Svg) + +include(CMakeFindDependencyMacro) +find_dependency(Qt6Gui "@REQUIRED_QT_VERSION@") + +include("${CMAKE_CURRENT_LIST_DIR}/KF6SvgTargets.cmake") + +@PACKAGE_SETUP_AUTOMOC_VARIABLES@ diff --git a/local/recipes/kde/kf6-ksvg/source/LICENSES/BSD-2-Clause.txt b/local/recipes/kde/kf6-ksvg/source/LICENSES/BSD-2-Clause.txt new file mode 100644 index 0000000000..2d2bab1127 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/LICENSES/BSD-2-Clause.txt @@ -0,0 +1,22 @@ +Copyright (c) . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/local/recipes/kde/kf6-ksvg/source/LICENSES/BSD-3-Clause.txt b/local/recipes/kde/kf6-ksvg/source/LICENSES/BSD-3-Clause.txt new file mode 100644 index 0000000000..0741db789e --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/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/local/recipes/kde/kf6-ksvg/source/LICENSES/CC0-1.0.txt b/local/recipes/kde/kf6-ksvg/source/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000000..0e259d42c9 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/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/local/recipes/kde/kf6-ksvg/source/LICENSES/GPL-2.0-only.txt b/local/recipes/kde/kf6-ksvg/source/LICENSES/GPL-2.0-only.txt new file mode 100644 index 0000000000..0f3d6411da --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/LICENSES/GPL-2.0-only.txt @@ -0,0 +1,319 @@ +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. + + + +Copyright (C)< yyyy> + +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. + +, 1 April 1989 Ty Coon, President of Vice This General +Public License does not permit incorporating your program into proprietary +programs. If your program is a subroutine library, you may consider it more +useful to permit linking proprietary applications with the library. If this +is what you want to do, use the GNU Lesser General Public License instead +of this License. diff --git a/local/recipes/kde/kf6-ksvg/source/LICENSES/GPL-2.0-or-later.txt b/local/recipes/kde/kf6-ksvg/source/LICENSES/GPL-2.0-or-later.txt new file mode 100644 index 0000000000..1d80ac3653 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/LICENSES/GPL-2.0-or-later.txt @@ -0,0 +1,319 @@ +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. + + + +Copyright (C) + +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. + +, 1 April 1989 Ty Coon, President of Vice This General +Public License does not permit incorporating your program into proprietary +programs. If your program is a subroutine library, you may consider it more +useful to permit linking proprietary applications with the library. If this +is what you want to do, use the GNU Lesser General Public License instead +of this License. diff --git a/local/recipes/kde/kf6-ksvg/source/LICENSES/GPL-3.0-only.txt b/local/recipes/kde/kf6-ksvg/source/LICENSES/GPL-3.0-only.txt new file mode 100644 index 0000000000..e142a525bd --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/LICENSES/GPL-3.0-only.txt @@ -0,0 +1,625 @@ +GNU GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and +other kinds of works. + +The licenses for most software and other practical works are designed to take +away your freedom to share and change the works. By contrast, the GNU General +Public License is intended to guarantee your freedom to share and change all +versions of a program--to make sure it remains free software for all its users. +We, the Free Software Foundation, use the GNU General Public License for most +of our software; it applies also to any other work released this way by its +authors. 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 them 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 prevent others from denying you these rights +or asking you to surrender the rights. Therefore, you have certain responsibilities +if you distribute copies of the software, or if you modify it: responsibilities +to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must pass on to the recipients the same freedoms that you received. +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. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert +copyright on the software, and (2) offer you this License giving you legal +permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that +there is no warranty for this free software. For both users' and authors' +sake, the GPL requires that modified versions be marked as changed, so that +their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified +versions of the software inside them, although the manufacturer can do so. +This is fundamentally incompatible with the aim of protecting users' freedom +to change the software. The systematic pattern of such abuse occurs in the +area of products for individuals to use, which is precisely where it is most +unacceptable. Therefore, we have designed this version of the GPL to prohibit +the practice for those products. If such problems arise substantially in other +domains, we stand ready to extend this provision to those domains in future +versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States +should not allow patents to restrict development and use of software on general-purpose +computers, but in those that do, we wish to avoid the special danger that +patents applied to a free program could make it effectively proprietary. To +prevent this, the GPL assures that patents cannot be used to render the program +non-free. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, +such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. +Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals +or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in +a fashion requiring copyright permission, other than the making of an exact +copy. The resulting work is called a "modified version" of the earlier work +or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the +Program. + +To "propagate" a work means to do anything with it that, without permission, +would make you directly or secondarily liable for infringement under applicable +copyright law, except executing it on a computer or modifying a private copy. +Propagation includes copying, distribution (with or without modification), +making available to the public, and in some countries other activities as +well. + +To "convey" a work means any kind of propagation that enables other parties +to make or receive copies. Mere interaction with a user through a computer +network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the +extent that it includes a convenient and prominently visible feature that +(1) displays an appropriate copyright notice, and (2) tells the user that +there is no warranty for the work (except to the extent that warranties are +provided), that licensees may convey the work under this License, and how +to view a copy of this License. If the interface presents a list of user commands +or options, such as a menu, a prominent item in the list meets this criterion. + + 1. Source Code. + +The "source code" for a work means the preferred form of the work for making +modifications to it. "Object code" means any non-source form of a work. + +A "Standard Interface" means an interface that either is an official standard +defined by a recognized standards body, or, in the case of interfaces specified +for a particular programming language, one that is widely used among developers +working in that language. + +The "System Libraries" of an executable work include anything, other than +the work as a whole, that (a) is included in the normal form of packaging +a Major Component, but which is not part of that Major Component, and (b) +serves only to enable use of the work with that Major Component, or to implement +a Standard Interface for which an implementation is available to the public +in source code form. A "Major Component", in this context, means a major essential +component (kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to produce +the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source +code needed to generate, install, and (for an executable work) run the object +code and to modify the work, including scripts to control those activities. +However, it does not include the work's System Libraries, or general-purpose +tools or generally available free programs which are used unmodified in performing +those activities but which are not part of the work. For example, Corresponding +Source includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically linked +subprograms that the work is specifically designed to require, such as by +intimate data communication or control flow between those subprograms and +other parts of the work. + +The Corresponding Source need not include anything that users can regenerate +automatically from other parts of the Corresponding Source. + + The Corresponding Source for a work in source code form is that same work. + + 2. Basic Permissions. + +All rights granted under this License are granted for the term of copyright +on the Program, and are irrevocable provided the stated conditions are met. +This License explicitly affirms your unlimited permission to run the unmodified +Program. The output from running a covered work is covered by this License +only if the output, given its content, constitutes a covered work. This License +acknowledges your rights of fair use or other equivalent, as provided by copyright +law. + +You may make, run and propagate covered works that you do not convey, without +conditions so long as your license otherwise remains in force. You may convey +covered works to others for the sole purpose of having them make modifications +exclusively for you, or provide you with facilities for running those works, +provided that you comply with the terms of this License in conveying all material +for which you do not control copyright. Those thus making or running the covered +works for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of your copyrighted +material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions +stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological measure +under any applicable law fulfilling obligations under article 11 of the WIPO +copyright treaty adopted on 20 December 1996, or similar laws prohibiting +or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention +of technological measures to the extent such circumvention is effected by +exercising rights under this License with respect to the covered work, and +you disclaim any intention to limit operation or modification of the work +as a means of enforcing, against the work's users, your or third parties' +legal rights to forbid circumvention of technological measures. + + 4. Conveying Verbatim Copies. + +You may convey 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; keep intact all notices stating +that this License and any non-permissive terms added in accord with section +7 apply to the code; keep intact all notices of the absence of any warranty; +and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you +may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to produce +it from the Program, in the form of source code under the terms of section +4, provided that you also meet all of these conditions: + +a) The work must carry prominent notices stating that you modified it, and +giving a relevant date. + +b) The work must carry prominent notices stating that it is released under +this License and any conditions added under section 7. This requirement modifies +the requirement in section 4 to "keep intact all notices". + +c) You must license the entire work, as a whole, under this License to anyone +who comes into possession of a copy. This License will therefore apply, along +with any applicable section 7 additional terms, to the whole of the work, +and all its parts, regardless of how they are packaged. This License gives +no permission to license the work in any other way, but it does not invalidate +such permission if you have separately received it. + +d) If the work has interactive user interfaces, each must display Appropriate +Legal Notices; however, if the Program has interactive interfaces that do +not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, +which are not by their nature extensions of the covered work, and which are +not combined with it such as to form a larger program, in or on a volume of +a storage or distribution medium, is called an "aggregate" if the compilation +and its resulting copyright are not used to limit the access or legal rights +of the compilation's users beyond what the individual works permit. Inclusion +of a covered work in an aggregate does not cause this License to apply to +the other parts of the aggregate. + + 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of sections +4 and 5, provided that you also convey the machine-readable Corresponding +Source under the terms of this License, in one of these ways: + +a) Convey the object code in, or embodied in, a physical product (including +a physical distribution medium), accompanied by the Corresponding Source fixed +on a durable physical medium customarily used for software interchange. + +b) Convey the object code in, or embodied in, a physical product (including +a physical distribution medium), accompanied by a written offer, valid for +at least three years and valid for as long as you offer spare parts or customer +support for that product model, to give anyone who possesses the object code +either (1) a copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical medium customarily +used for software interchange, for a price no more than your reasonable cost +of physically performing this conveying of source, or (2) access to copy the +Corresponding Source from a network server at no charge. + +c) Convey individual copies of the object code with a copy of the written +offer to provide the Corresponding Source. This alternative is allowed only +occasionally and noncommercially, and only if you received the object code +with such an offer, in accord with subsection 6b. + +d) Convey the object code by offering access from a designated place (gratis +or for a charge), and offer equivalent access to the Corresponding Source +in the same way through the same place at no further charge. You need not +require recipients to copy the Corresponding Source along with the object +code. If the place to copy the object code is a network server, the Corresponding +Source may be on a different server (operated by you or a third party) that +supports equivalent copying facilities, provided you maintain clear directions +next to the object code saying where to find the Corresponding Source. Regardless +of what server hosts the Corresponding Source, you remain obligated to ensure +that it is available for as long as needed to satisfy these requirements. + +e) Convey the object code using peer-to-peer transmission, provided you inform +other peers where the object code and Corresponding Source of the work are +being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from +the Corresponding Source as a System Library, need not be included in conveying +the object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible +personal property which is normally used for personal, family, or household +purposes, or (2) anything designed or sold for incorporation into a dwelling. +In determining whether a product is a consumer product, doubtful cases shall +be resolved in favor of coverage. For a particular product received by a particular +user, "normally used" refers to a typical or common use of that class of product, +regardless of the status of the particular user or of the way in which the +particular user actually uses, or expects or is expected to use, the product. +A product is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent the +only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, +authorization keys, or other information required to install and execute modified +versions of a covered work in that User Product from a modified version of +its Corresponding Source. The information must suffice to ensure that the +continued functioning of the modified object code is in no case prevented +or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically +for use in, a User Product, and the conveying occurs as part of a transaction +in which the right of possession and use of the User Product is transferred +to the recipient in perpetuity or for a fixed term (regardless of how the +transaction is characterized), the Corresponding Source conveyed under this +section must be accompanied by the Installation Information. But this requirement +does not apply if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has been installed +in ROM). + +The requirement to provide Installation Information does not include a requirement +to continue to provide support service, warranty, or updates for a work that +has been modified or installed by the recipient, or for the User Product in +which it has been modified or installed. Access to a network may be denied +when the modification itself materially and adversely affects the operation +of the network or violates the rules and protocols for communication across +the network. + +Corresponding Source conveyed, and Installation Information provided, in accord +with this section must be in a format that is publicly documented (and with +an implementation available to the public in source code form), and must require +no special password or key for unpacking, reading or copying. + + 7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this License +by making exceptions from one or more of its conditions. Additional permissions +that are applicable to the entire Program shall be treated as though they +were included in this License, to the extent that they are valid under applicable +law. If additional permissions apply only to part of the Program, that part +may be used separately under those permissions, but the entire Program remains +governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any +additional permissions from that copy, or from any part of it. (Additional +permissions may be written to require their own removal in certain cases when +you modify the work.) You may place additional permissions on material, added +by you to a covered work, for which you have or can give appropriate copyright +permission. + +Notwithstanding any other provision of this License, for material you add +to a covered work, you may (if authorized by the copyright holders of that +material) supplement the terms of this License with terms: + +a) Disclaiming warranty or limiting liability differently from the terms of +sections 15 and 16 of this License; or + +b) Requiring preservation of specified reasonable legal notices or author +attributions in that material or in the Appropriate Legal Notices displayed +by works containing it; or + +c) Prohibiting misrepresentation of the origin of that material, or requiring +that modified versions of such material be marked in reasonable ways as different +from the original version; or + +d) Limiting the use for publicity purposes of names of licensors or authors +of the material; or + +e) Declining to grant rights under trademark law for use of some trade names, +trademarks, or service marks; or + +f) Requiring indemnification of licensors and authors of that material by +anyone who conveys the material (or modified versions of it) with contractual +assumptions of liability to the recipient, for any liability that these contractual +assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered "further restrictions" +within the meaning of section 10. If the Program as you received it, or any +part of it, contains a notice stating that it is governed by this License +along with a term that is a further restriction, you may remove that term. +If a license document contains a further restriction but permits relicensing +or conveying under this License, you may add to a covered work material governed +by the terms of that license document, provided that the further restriction +does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, +in the relevant source files, a statement of the additional terms that apply +to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form +of a separately written license, or stated as exceptions; the above requirements +apply either way. + + 8. Termination. + +You may not propagate or modify a covered work except as expressly provided +under this License. Any attempt otherwise to propagate or modify it is void, +and will automatically terminate your rights under this License (including +any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from +a particular copyright holder is reinstated (a) provisionally, unless and +until the copyright holder explicitly and finally terminates your license, +and (b) permanently, if the copyright holder fails to notify you of the violation +by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently +if the copyright holder notifies you of the violation by some reasonable means, +this is the first time you have received notice of violation of this License +(for any work) from that copyright holder, and you cure the violation prior +to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses +of parties who have received copies or rights from you under this License. +If your rights have been terminated and not permanently reinstated, you do +not qualify to receive new licenses for the same material under section 10. + + 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy +of the Program. Ancillary propagation of a covered work occurring solely as +a consequence of using peer-to-peer transmission to receive a copy likewise +does not require acceptance. However, nothing other than this License grants +you permission to propagate or modify any covered work. These actions infringe +copyright if you do not accept this License. Therefore, by modifying or propagating +a covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives +a license from the original licensors, to run, modify and propagate that work, +subject to this License. You are not responsible for enforcing compliance +by third parties with this License. + +An "entity transaction" is a transaction transferring control of an organization, +or substantially all assets of one, or subdividing an organization, or merging +organizations. If propagation of a covered work results from an entity transaction, +each party to that transaction who receives a copy of the work also receives +whatever licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the Corresponding +Source of the work from the predecessor in interest, if the predecessor has +it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights +granted or affirmed under this License. For example, you may not impose a +license fee, royalty, or other charge for exercise of rights granted under +this License, and you may not initiate litigation (including a cross-claim +or counterclaim in a lawsuit) alleging that any patent claim is infringed +by making, using, selling, offering for sale, or importing the Program or +any portion of it. + + 11. Patents. + +A "contributor" is a copyright holder who authorizes use under this License +of the Program or a work on which the Program is based. The work thus licensed +is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or controlled +by the contributor, whether already acquired or hereafter acquired, that would +be infringed by some manner, permitted by this License, of making, using, +or selling its contributor version, but do not include claims that would be +infringed only as a consequence of further modification of the contributor +version. For purposes of this definition, "control" includes the right to +grant patent sublicenses in a manner consistent with the requirements of this +License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent +license under the contributor's essential patent claims, to make, use, sell, +offer for sale, import and otherwise run, modify and propagate the contents +of its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement +or commitment, however denominated, not to enforce a patent (such as an express +permission to practice a patent or covenant not to sue for patent infringement). +To "grant" such a patent license to a party means to make such an agreement +or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the +Corresponding Source of the work is not available for anyone to copy, free +of charge and under the terms of this License, through a publicly available +network server or other readily accessible means, then you must either (1) +cause the Corresponding Source to be so available, or (2) arrange to deprive +yourself of the benefit of the patent license for this particular work, or +(3) arrange, in a manner consistent with the requirements of this License, +to extend the patent license to downstream recipients. "Knowingly relying" +means you have actual knowledge that, but for the patent license, your conveying +the covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that country +that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, +you convey, or propagate by procuring conveyance of, a covered work, and grant +a patent license to some of the parties receiving the covered work authorizing +them to use, propagate, modify or convey a specific copy of the covered work, +then the patent license you grant is automatically extended to all recipients +of the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope +of its coverage, prohibits the exercise of, or is conditioned on the non-exercise +of one or more of the rights that are specifically granted under this License. +You may not convey a covered work if you are a party to an arrangement with +a third party that is in the business of distributing software, under which +you make payment to the third party based on the extent of your activity of +conveying the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by you +(or copies made from those copies), or (b) primarily for and in connection +with specific products or compilations that contain the covered work, unless +you entered into that arrangement, or that patent license was granted, prior +to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied +license or other defenses to infringement that may otherwise be available +to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + +If 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 convey a covered work so as +to satisfy simultaneously your obligations under this License and any other +pertinent obligations, then as a consequence you may not convey it at all. +For example, if you agree to terms that obligate you to collect a royalty +for further conveying from those to whom you convey the Program, the only +way you could satisfy both those terms and this License would be to refrain +entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have permission to +link or combine any covered work with a work licensed under version 3 of the +GNU Affero General Public License into a single combined work, and to convey +the resulting work. The terms of this License will continue to apply to the +part which is the covered work, but the special requirements of the GNU Affero +General Public License, section 13, concerning interaction through a network +will apply to the combination as such. + + 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the +GNU 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 +that a certain numbered version of the GNU General Public License "or any +later version" applies to it, you have the option of following the terms and +conditions either of that numbered version or of any later version published +by the Free Software Foundation. If the Program does not specify a version +number of the GNU General Public License, you may choose any version ever +published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of +the GNU General Public License can be used, that proxy's public statement +of acceptance of a version permanently authorizes you to choose that version +for the Program. + +Later license versions may give you additional or different permissions. However, +no additional obligations are imposed on any author or copyright holder as +a result of your choosing to follow a later version. + + 15. Disclaimer of Warranty. + +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. + + 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL +ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 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. + + 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot +be given local legal effect according to their terms, reviewing courts shall +apply local law that most closely approximates an absolute waiver of all civil +liability in connection with the Program, unless a warranty or assumption +of liability accompanies a copy of the Program in return for a fee. 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 state the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + + + +Copyright (C) + +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 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like +this when it starts in an interactive mode: + + Copyright (C) + +This program 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, your program's commands might +be different; for a GUI interface, you would use an "about box". + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. For +more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General Public +License instead of this License. But first, please read . diff --git a/local/recipes/kde/kf6-ksvg/source/LICENSES/LGPL-2.0-or-later.txt b/local/recipes/kde/kf6-ksvg/source/LICENSES/LGPL-2.0-or-later.txt new file mode 100644 index 0000000000..5c96471aaf --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/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/local/recipes/kde/kf6-ksvg/source/LICENSES/LGPL-2.1-only.txt b/local/recipes/kde/kf6-ksvg/source/LICENSES/LGPL-2.1-only.txt new file mode 100644 index 0000000000..130dffb311 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/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/local/recipes/kde/kf6-ksvg/source/LICENSES/LGPL-2.1-or-later.txt b/local/recipes/kde/kf6-ksvg/source/LICENSES/LGPL-2.1-or-later.txt new file mode 100644 index 0000000000..04bb156e77 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/LICENSES/LGPL-2.1-or-later.txt @@ -0,0 +1,468 @@ +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. + + + +Copyright (C) + +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/local/recipes/kde/kf6-ksvg/source/LICENSES/LGPL-3.0-only.txt b/local/recipes/kde/kf6-ksvg/source/LICENSES/LGPL-3.0-only.txt new file mode 100644 index 0000000000..bd405afbef --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/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/local/recipes/kde/kf6-ksvg/source/LICENSES/LicenseRef-KDE-Accepted-GPL.txt b/local/recipes/kde/kf6-ksvg/source/LICENSES/LicenseRef-KDE-Accepted-GPL.txt new file mode 100644 index 0000000000..60a2dffc9c --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/LICENSES/LicenseRef-KDE-Accepted-GPL.txt @@ -0,0 +1,12 @@ +This library 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 3 of +the license or (at your option) at any later version that is +accepted by the membership of KDE e.V. (or its successor +approved by the membership of KDE e.V.), which shall act as a +proxy as defined in Section 14 of version 3 of the license. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. diff --git a/local/recipes/kde/kf6-ksvg/source/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt b/local/recipes/kde/kf6-ksvg/source/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt new file mode 100644 index 0000000000..232b3c5da1 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt @@ -0,0 +1,12 @@ +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the license or (at your option) any later version +that is accepted by the membership of KDE e.V. (or its successor +approved by the membership of KDE e.V.), which shall act as a +proxy as defined in Section 6 of version 3 of the license. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. diff --git a/local/recipes/kde/kf6-ksvg/source/LICENSES/LicenseRef-Qt-Commercial.txt b/local/recipes/kde/kf6-ksvg/source/LICENSES/LicenseRef-Qt-Commercial.txt new file mode 100644 index 0000000000..11e00c7426 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/LICENSES/LicenseRef-Qt-Commercial.txt @@ -0,0 +1,7 @@ +Commercial License Usage +Licensees holding valid commercial Qt licenses may use this file in +accordance with the commercial license agreement provided with the +Software or, alternatively, in accordance with the terms contained in +a written agreement between you and The Qt Company. For licensing terms +and conditions see https://www.qt.io/terms-conditions. For further +information use the contact form at https://www.qt.io/contact-us. diff --git a/local/recipes/kde/kf6-ksvg/source/LICENSES/Qt-LGPL-exception-1.1.txt b/local/recipes/kde/kf6-ksvg/source/LICENSES/Qt-LGPL-exception-1.1.txt new file mode 100644 index 0000000000..d0f532e9e1 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/LICENSES/Qt-LGPL-exception-1.1.txt @@ -0,0 +1,21 @@ +The Qt Company Qt LGPL Exception version 1.1 + +As an additional permission to the GNU Lesser General Public License version 2.1, the object code form of a "work that uses the Library" may incorporate material from a header file that is part of the Library. You may distribute such object code under terms of your choice, provided that: + + (i) the header files of the Library have not been modified; and + + (ii) the incorporated material is limited to numerical parameters, data structure layouts, accessors, macros, inline functions and templates; and + + (iii) you comply with the terms of Section 6 of the GNU Lesser General Public License version 2.1. + +Moreover, you may apply this exception to a modified version of the Library, provided that such modification does not involve copying material from the Library into the modified Library's header files unless such material is limited to + + (i) numerical parameters; + + (ii) data structure layouts; + + (iii) accessors; and + + (iv) small macros, templates and inline functions of five lines or less in length. + +Furthermore, you are not required to apply this additional permission to a modified version of the Library. diff --git a/local/recipes/kde/kf6-ksvg/source/README.md b/local/recipes/kde/kf6-ksvg/source/README.md new file mode 100644 index 0000000000..8e4254718d --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/README.md @@ -0,0 +1,43 @@ +# KSvg +A library for rendering SVG-based themes with stylesheet re-coloring and on-disk caching. + +## Introduction +KSvg provides both C++ classes and QtQuick components to render svgs based on image packs. +Compared to plain QSvg, it caches the rendered images on disk with KImageCache, and can re-color +properly crafted svg shapes that include internal stylesheets. + +The default behavior is to re-color with the application palette, making it possible to create UI elements in SVG. + +## C++ +In C++ there are 3 main classes, usable also in QWidget applications with a QPainter-compatible API: + +* ``ImageSet``: Used to tell the other classes where to find the SVG files: by default, SVG "themes" + will be searched in the application data dir (share/application_name/theme_name) +* ``Svg``: Class to used to render Svg files: it loads a file with ``setImagePath`` using relative paths + to the theme specified in ``ImageSet`` +* ``FrameSvg``: A subclass of Svg used to render 9-patch images, such as Buttons, where you want to stretch + only the central area but not the edges + +## QML +Import QML bindings with ``import org.kde.ksvg 1.0 as KSvg``. + +ImageSet is exported directly to QML which makes it possible to set the theme from QML. + +Svg and FrameSvg have corresponding items, ``SvgItem`` and ``FrameSvgItem``, which inherit from QQuickItem +and will paint their associated ``Svg`` or ``FrameSvg`` stretched to their full geometry. + +QML code example: + +``` +FrameSvgItem { + // This resolves to a file like /usr/share/myapp/mytheme/widgets/button.svgz + imagePath: "widgets/button" + prefix: "pressed" +} +``` + +## More documentation +Assume the theme filesystem hierarchy used by the Plasma shell, but the general concepts apply everywhere: + +* https://develop.kde.org/docs/plasma/theme/theme-elements/ +* https://develop.kde.org/docs/plasma/theme/quickstart/ diff --git a/local/recipes/kde/kf6-ksvg/source/autotests/CMakeLists.txt b/local/recipes/kde/kf6-ksvg/source/autotests/CMakeLists.txt new file mode 100644 index 0000000000..2a8a93cf4f --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/autotests/CMakeLists.txt @@ -0,0 +1,31 @@ +find_package(Qt6Test ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE) +set_package_properties(Qt6Test PROPERTIES PURPOSE "Required for tests") + +set(EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR}) +remove_definitions(-DQT_NO_CAST_FROM_ASCII -DQT_STRICT_ITERATORS -DQT_NO_CAST_FROM_BYTEARRAY -DQT_NO_KEYWORDS) + +include(ECMMarkAsTest) +include(ECMAddTests) + +MACRO(KSVG_UNIT_TESTS) + FOREACH(_testname ${ARGN}) + set(libs Qt6::Qml Qt6::Test KF6::Svg + KF6::Archive KF6::CoreAddons KF6::ConfigGui KF6::ColorScheme) + if(QT_QTOPENGL_FOUND) + list(APPEND libs Qt6::OpenGL) + endif() + ecm_add_test(${_testname}.cpp + LINK_LIBRARIES ${libs} + NAME_PREFIX "plasmasvg-") + qt_add_resources(${_testname} "images" + FILES "data/background.svgz") + target_include_directories(${_testname} PRIVATE ) + ENDFOREACH(_testname) +ENDMACRO(KSVG_UNIT_TESTS) + +KSVG_UNIT_TESTS( + framesvgtest + imagesettest + svgtest +) + diff --git a/local/recipes/kde/kf6-ksvg/source/autotests/data/background.svgz b/local/recipes/kde/kf6-ksvg/source/autotests/data/background.svgz new file mode 100644 index 0000000000..70a668aa31 Binary files /dev/null and b/local/recipes/kde/kf6-ksvg/source/autotests/data/background.svgz differ diff --git a/local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/test_old_metadata_format_theme/metadata.desktop b/local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/test_old_metadata_format_theme/metadata.desktop new file mode 100644 index 0000000000..68686d69e4 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/test_old_metadata_format_theme/metadata.desktop @@ -0,0 +1,19 @@ +[Desktop Entry] +Name=Plasma test theme + + +X-KDE-PluginInfo-Author=KDE Visual Design Group +X-KDE-PluginInfo-Email=kde-artists@kde.org +X-KDE-PluginInfo-Name=default +X-KDE-PluginInfo-Version=5.20 +X-KDE-PluginInfo-Website=https://plasma.kde.org +X-KDE-PluginInfo-Category= +X-KDE-PluginInfo-License=LGPL +X-KDE-PluginInfo-EnabledByDefault=true +X-Plasma-API=5.0 + +[ContrastEffect] +enabled=true +contrast=0.23 +intensity=2.0 +saturation=1.7 diff --git a/local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/colors b/local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/colors new file mode 100644 index 0000000000..a7e74c131a --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/colors @@ -0,0 +1,120 @@ +[ColorEffects:Disabled] +Color=56,56,56 +ColorAmount=0 +ColorEffect=0 +ContrastAmount=0.65 +ContrastEffect=1 +IntensityAmount=0.1 +IntensityEffect=2 + +[ColorEffects:Inactive] +ChangeSelectionColor=true +Color=112,111,110 +ColorAmount=0.025 +ColorEffect=2 +ContrastAmount=0.1 +ContrastEffect=2 +Enable=false +IntensityAmount=0 +IntensityEffect=0 + +[Colors:Button] +BackgroundAlternate=224,223,222 +BackgroundNormal=239,240,241 +DecorationFocus=30,146,255 +DecorationHover=61,174,230 +ForegroundActive=246,116,0 +ForegroundInactive=175,176,179 +ForegroundLink=61,174,230 +ForegroundNegative=237,21,22 +ForegroundNeutral=201,206,60 +ForegroundNormal=49,54,59 +ForegroundPositive=17,209,23 +ForegroundVisited=61,174,230 + +[Colors:Selection] +BackgroundAlternate=48,138,183 +BackgroundNormal=61,174,230 +DecorationFocus=30,146,255 +DecorationHover=61,174,230 +ForegroundActive=246,116,0 +ForegroundInactive=146,204,230 +ForegroundLink=252,252,252 +ForegroundNegative=237,21,21 +ForegroundNeutral=201,206,59 +ForegroundNormal=252,252,252 +ForegroundPositive=17,209,22 +ForegroundVisited=252,252,252 + +[Colors:Tooltip] +BackgroundAlternate=196,224,255 +BackgroundNormal=239,240,241 +DecorationFocus=30,146,255 +DecorationHover=61,174,230 +ForegroundActive=246,116,0 +ForegroundInactive=175,176,179 +ForegroundLink=61,174,230 +ForegroundNegative=237,21,21 +ForegroundNeutral=201,206,59 +ForegroundNormal=49,54,59 +ForegroundPositive=17,209,22 +ForegroundVisited=61,174,230 + +[Colors:View] +BackgroundAlternate=248,247,246 +BackgroundNormal=252,252,252 +DecorationFocus=30,146,255 +DecorationHover=61,174,230 +ForegroundActive=246,116,0 +ForegroundInactive=175,176,179 +ForegroundLink=61,174,230 +ForegroundNegative=237,21,23 +ForegroundNeutral=201,206,61 +ForegroundNormal=49,54,59 +ForegroundPositive=17,209,24 +ForegroundVisited=61,174,230 + +[Colors:Window] +BackgroundAlternate=218,217,216 +BackgroundNormal=239,240,241 +DecorationFocus=30,146,255 +DecorationHover=61,174,230 +ForegroundActive=246,116,0 +ForegroundInactive=175,176,179 +ForegroundLink=61,174,230 +ForegroundNegative=237,21,21 +ForegroundNeutral=201,206,59 +#A very glaring red to check those colors are actually applied +ForegroundNormal=255,54,59 +ForegroundPositive=17,209,22 +ForegroundVisited=61,174,230 + +[Colors:Complementary] +BackgroundAlternate=59,64,69 +BackgroundNormal=49,54,59 +DecorationFocus=40,146,255 +DecorationHover=71,174,230 +ForegroundActive=246,116,20 +ForegroundInactive=185,176,179 +ForegroundLink=71,174,230 +ForegroundNegative=237,21,24 +ForegroundNeutral=201,206,62 +ForegroundNormal=239,240,241 +ForegroundPositive=17,209,25 +ForegroundVisited=71,174,230 + +[General] +ColorScheme=Breeze +Name=Breeze +shadeSortColumn=true + +[KDE] +contrast=7 + +[WM] +activeBackground=61,174,230 +activeBlend=252,252,252 +activeForeground=252,252,252 +inactiveBackground=123,124,126 +inactiveBlend=123,124,126 +inactiveForeground=252,252,252 diff --git a/local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/element.svg b/local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/element.svg new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/element.svg @@ -0,0 +1 @@ + diff --git a/local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/metadata.json b/local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/metadata.json new file mode 100644 index 0000000000..88a8e6c986 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/metadata.json @@ -0,0 +1,18 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "kde-artists@kde.org", + "Name": "KDE Visual Design Group" + } + ], + "Category": "", + "EnabledByDefault": true, + "Id": "default", + "License": "LGPL", + "Name": "Plasma test theme", + "Version": "5.20", + "Website": "https://plasma.kde.org" + }, + "X-Plasma-API": "5.0" +} diff --git a/local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/opaque/element.svg b/local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/opaque/element.svg new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/opaque/element.svg @@ -0,0 +1 @@ + diff --git a/local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/plasmarc b/local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/plasmarc new file mode 100644 index 0000000000..a31148a73c --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/autotests/data/plasma/desktoptheme/testtheme/plasmarc @@ -0,0 +1,5 @@ +[ContrastEffect] +enabled=true +contrast=0.23 +intensity=2.0 +saturation=1.7 diff --git a/local/recipes/kde/kf6-ksvg/source/autotests/framesvgtest.cpp b/local/recipes/kde/kf6-ksvg/source/autotests/framesvgtest.cpp new file mode 100644 index 0000000000..2e1163074a --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/autotests/framesvgtest.cpp @@ -0,0 +1,131 @@ +/* + SPDX-FileCopyrightText: 2014 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "framesvgtest.h" +#include +#include + +void copyDirectory(const QString &srcDir, const QString &dstDir) +{ + QDir targetDir(dstDir); + QDirIterator it(srcDir, QDir::Filters(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Name), QDirIterator::Subdirectories); + while (it.hasNext()) { + it.next(); + QString path = it.filePath(); + QString relDestPath = path.last(it.filePath().length() - srcDir.length() - 1); + if (it.fileInfo().isDir()) { + QVERIFY(targetDir.mkpath(relDestPath)); + } else { + QVERIFY(QFile::copy(path, dstDir % '/' % relDestPath)); + } + } +} + +void FrameSvgTest::initTestCase() +{ + QStandardPaths::setTestModeEnabled(true); + + m_themeDir = QDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) % '/' % "plasma"); + m_themeDir.removeRecursively(); + + copyDirectory(QFINDTESTDATA("data/plasma"), m_themeDir.absolutePath()); + + m_cacheDir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); + m_cacheDir.removeRecursively(); + + m_frameSvg = new KSvg::FrameSvg; + m_frameSvg->setImagePath(QFINDTESTDATA("data/background.svgz")); + QVERIFY(m_frameSvg->isValid()); +} + +void FrameSvgTest::cleanupTestCase() +{ + delete m_frameSvg; + + m_themeDir.removeRecursively(); + m_cacheDir.removeRecursively(); +} + +void FrameSvgTest::margins() +{ + QCOMPARE(m_frameSvg->marginSize(KSvg::FrameSvg::LeftMargin), (qreal)26); + QCOMPARE(m_frameSvg->marginSize(KSvg::FrameSvg::TopMargin), (qreal)26); + QCOMPARE(m_frameSvg->marginSize(KSvg::FrameSvg::RightMargin), (qreal)26); + QCOMPARE(m_frameSvg->marginSize(KSvg::FrameSvg::BottomMargin), (qreal)26); +} + +void FrameSvgTest::contentsRect() +{ + m_frameSvg->resizeFrame(QSize(100, 100)); + QCOMPARE(m_frameSvg->contentsRect(), QRectF(26, 26, 48, 48)); +} + +void FrameSvgTest::repaintBlocked() +{ + // check the properties to be correct even if set during a repaint blocked transaction + m_frameSvg->setRepaintBlocked(true); + QVERIFY(m_frameSvg->isRepaintBlocked()); + + m_frameSvg->setElementPrefix("prefix"); + m_frameSvg->setEnabledBorders(KSvg::FrameSvg::TopBorder | KSvg::FrameSvg::LeftBorder); + m_frameSvg->resizeFrame(QSizeF(100, 100)); + + m_frameSvg->setRepaintBlocked(false); + + QCOMPARE(m_frameSvg->prefix(), QString("prefix")); + QCOMPARE(m_frameSvg->enabledBorders(), KSvg::FrameSvg::TopBorder | KSvg::FrameSvg::LeftBorder); + QCOMPARE(m_frameSvg->frameSize(), QSizeF(100, 100)); +} + +void FrameSvgTest::setImageSet() +{ + // Should not crash + + KSvg::FrameSvg *frameSvg = new KSvg::FrameSvg; + frameSvg->setImagePath("widgets/background"); + frameSvg->setImageSet(new KSvg::ImageSet("breeze-light", {}, this)); + frameSvg->framePixmap(); + frameSvg->setImageSet(new KSvg::ImageSet("breeze-dark", {}, this)); + frameSvg->framePixmap(); + delete frameSvg; + + frameSvg = new KSvg::FrameSvg; + frameSvg->setImagePath("widgets/background"); + frameSvg->setImageSet(new KSvg::ImageSet("breeze-light", {}, this)); + frameSvg->framePixmap(); + frameSvg->setImageSet(new KSvg::ImageSet("breeze-dark", {}, this)); + frameSvg->framePixmap(); + + frameSvg->setImageSet(new KSvg::ImageSet("testtheme", "plasma/desktoptheme", this)); + QCOMPARE(frameSvg->color(KSvg::Svg::Text), QColor(255, 54, 59)); + + delete frameSvg; +} + +void FrameSvgTest::resizeMask() +{ + m_frameSvg->resizeFrame(QSize(100, 100)); + QCOMPARE(m_frameSvg->alphaMask().size(), QSize(100, 100)); + m_frameSvg->resizeFrame(QSize(50, 50)); + QCOMPARE(m_frameSvg->alphaMask().size(), QSize(50, 50)); + m_frameSvg->resizeFrame(QSize(100, 100)); + QCOMPARE(m_frameSvg->alphaMask().size(), QSize(100, 100)); +} + +void FrameSvgTest::loadQrc() +{ + KSvg::FrameSvg *frameSvg = new KSvg::FrameSvg; + frameSvg->setImageSet(new KSvg::ImageSet("testtheme", "plasma/desktoptheme", this)); + frameSvg->setImagePath(QStringLiteral("qrc:/data/background.svgz")); + QVERIFY(frameSvg->isValid()); + // An external image is colored as well + QCOMPARE(frameSvg->color(KSvg::Svg::Text), QColor(255, 54, 59)); + delete frameSvg; +} + +QTEST_MAIN(FrameSvgTest) + +#include "moc_framesvgtest.cpp" diff --git a/local/recipes/kde/kf6-ksvg/source/autotests/framesvgtest.h b/local/recipes/kde/kf6-ksvg/source/autotests/framesvgtest.h new file mode 100644 index 0000000000..5063eca71d --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/autotests/framesvgtest.h @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2014 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef FRAMESVGTEST_H +#define FRAMESVGTEST_H + +#include + +#include "ksvg/framesvg.h" + +class FrameSvgTest : public QObject +{ + Q_OBJECT + +public Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + +private Q_SLOTS: + void margins(); + void contentsRect(); + void setImageSet(); + void repaintBlocked(); + void resizeMask(); + void loadQrc(); + +private: + KSvg::FrameSvg *m_frameSvg; + QDir m_themeDir; + QDir m_cacheDir; +}; + +#endif diff --git a/local/recipes/kde/kf6-ksvg/source/autotests/imagesettest.cpp b/local/recipes/kde/kf6-ksvg/source/autotests/imagesettest.cpp new file mode 100644 index 0000000000..72852d0b2e --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/autotests/imagesettest.cpp @@ -0,0 +1,114 @@ +/* + * SPDX-FileCopyrightText: 2025 Nicolas Fella + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "imageset.h" + +#include +#include +#include + +using namespace Qt::Literals; + +class ImageSetTest : public QObject +{ + Q_OBJECT + +public Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + +private Q_SLOTS: + void testBasePath(); + void testSelectors(); + void testHasImage(); + void testFilePath(); + +private: + QDir m_themeDir; +}; + +void copyDirectory(const QString &srcDir, const QString &dstDir) +{ + QDir targetDir(dstDir); + QDirIterator it(srcDir, QDir::Filters(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Name), QDirIterator::Subdirectories); + while (it.hasNext()) { + it.next(); + QString path = it.filePath(); + QString relDestPath = path.last(it.filePath().length() - srcDir.length() - 1); + if (it.fileInfo().isDir()) { + QVERIFY(targetDir.mkpath(relDestPath)); + } else { + QVERIFY(QFile::copy(path, dstDir % '/' % relDestPath)); + } + } +} + +void ImageSetTest::initTestCase() +{ + QStandardPaths::setTestModeEnabled(true); + + m_themeDir = QDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) % '/' % "plasma"); + m_themeDir.removeRecursively(); + + copyDirectory(QFINDTESTDATA("data/plasma"), m_themeDir.absolutePath()); +} + +void ImageSetTest::cleanupTestCase() +{ + m_themeDir.removeRecursively(); +} + +void ImageSetTest::testBasePath() +{ + KSvg::ImageSet set("testtheme", "plasma/desktoptheme"); + QCOMPARE(set.imageSetName(), "testtheme"); + QCOMPARE(set.basePath(), "plasma/desktoptheme/"); + + set.setImageSetName("test_old_metadata_format_theme"); + QCOMPARE(set.imageSetName(), "test_old_metadata_format_theme"); + QCOMPARE(set.basePath(), "plasma/desktoptheme/"); +} + +void ImageSetTest::testSelectors() +{ + KSvg::ImageSet set("testtheme", "plasma/desktoptheme"); + + QVERIFY(set.imagePath(u"element"_s).endsWith(u"plasma/desktoptheme/testtheme/element.svg")); + + QSignalSpy spy(&set, &KSvg::ImageSet::imageSetChanged); + + set.setSelectors({u"opaque"_s}); + QVERIFY(spy.wait()); + QCOMPARE(spy.count(), 1); + QCOMPARE(spy[0][0], "testtheme"); + QVERIFY(set.imagePath(u"element"_s).endsWith(u"plasma/desktoptheme/testtheme/opaque/element.svg")); + + set.setSelectors({}); + QVERIFY(spy.wait()); + QCOMPARE(spy.count(), 2); + QCOMPARE(spy[1][0], "testtheme"); + QVERIFY(set.imagePath(u"element"_s).endsWith(u"plasma/desktoptheme/testtheme/element.svg")); +} + +void ImageSetTest::testHasImage() +{ + KSvg::ImageSet set("testtheme", "plasma/desktoptheme"); + + QVERIFY(set.currentImageSetHasImage(u"element"_s)); + QVERIFY(!set.currentImageSetHasImage(u"banana"_s)); +} + +void ImageSetTest::testFilePath() +{ + KSvg::ImageSet set("testtheme", "plasma/desktoptheme"); + + QVERIFY(set.filePath(u"plasmarc"_s).endsWith(u"plasma/desktoptheme/testtheme/plasmarc")); + QVERIFY(set.filePath(u"does_not_exist"_s).isEmpty()); +} + +QTEST_MAIN(ImageSetTest) + +#include "imagesettest.moc" diff --git a/local/recipes/kde/kf6-ksvg/source/autotests/svgtest.cpp b/local/recipes/kde/kf6-ksvg/source/autotests/svgtest.cpp new file mode 100644 index 0000000000..88a9e80423 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/autotests/svgtest.cpp @@ -0,0 +1,155 @@ +/* + * SPDX-FileCopyrightText: 2026 Nicolas Fella + * + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include "svg.h" + +#include +#include +#include + +#include + +using namespace Qt::Literals; + +class SvgTest : public QObject +{ + Q_OBJECT + +public Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + +private Q_SLOTS: + void testSize(); + void testElements(); + void testColors(); + +private: + KSvg::Svg *m_svg; + QDir m_themeDir; + QDir m_cacheDir; +}; + +void copyDirectory(const QString &srcDir, const QString &dstDir) +{ + QDir targetDir(dstDir); + QDirIterator it(srcDir, QDir::Filters(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Name), QDirIterator::Subdirectories); + while (it.hasNext()) { + it.next(); + QString path = it.filePath(); + QString relDestPath = path.last(it.filePath().length() - srcDir.length() - 1); + if (it.fileInfo().isDir()) { + QVERIFY(targetDir.mkpath(relDestPath)); + } else { + QVERIFY(QFile::copy(path, dstDir % '/' % relDestPath)); + } + } +} + +void SvgTest::initTestCase() +{ + QStandardPaths::setTestModeEnabled(true); + + m_themeDir = QDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) % '/' % "plasma"); + m_themeDir.removeRecursively(); + + copyDirectory(QFINDTESTDATA("data/plasma"), m_themeDir.absolutePath()); + + m_cacheDir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); + m_cacheDir.removeRecursively(); + + m_svg = new KSvg::Svg; + m_svg->setImagePath(QFINDTESTDATA("data/background.svgz")); + QVERIFY(m_svg->isValid()); +} + +void SvgTest::cleanupTestCase() +{ + m_themeDir.removeRecursively(); +} + +void SvgTest::testSize() +{ + QSignalSpy spy(m_svg, &KSvg::Svg::sizeChanged); + + QCOMPARE(m_svg->size(), QSizeF(148, 148)); + + m_svg->resize(500, 500); + QCOMPARE(spy.count(), 1); + QCOMPARE(m_svg->size(), QSizeF(500, 500)); + + m_svg->resize(); + QCOMPARE(spy.count(), 2); + QCOMPARE(m_svg->size(), QSizeF(148, 148)); +} + +void SvgTest::testElements() +{ + QVERIFY(m_svg->hasElement("center")); + QVERIFY(m_svg->hasElement("left")); + QVERIFY(m_svg->hasElement("right")); + QVERIFY(m_svg->hasElement("top")); + QVERIFY(m_svg->hasElement("bottom")); + QVERIFY(m_svg->hasElement("topleft")); + QVERIFY(m_svg->hasElement("topright")); + QVERIFY(m_svg->hasElement("bottomleft")); + QVERIFY(m_svg->hasElement("bottomright")); + + QVERIFY(m_svg->hasElement("prefix-left")); + QVERIFY(m_svg->hasElement("prefix-right")); + QVERIFY(m_svg->hasElement("prefix-top")); + QVERIFY(m_svg->hasElement("prefix-bottom")); + QVERIFY(m_svg->hasElement("prefix-topleft")); + QVERIFY(m_svg->hasElement("prefix-topright")); + QVERIFY(m_svg->hasElement("prefix-bottomleft")); + QVERIFY(m_svg->hasElement("prefix-bottomright")); + + QVERIFY(m_svg->hasElement("hint-left-margin")); + QVERIFY(m_svg->hasElement("hint-right-margin")); + QVERIFY(m_svg->hasElement("hint-top-margin")); + QVERIFY(m_svg->hasElement("hint-bottom-margin")); + + QVERIFY(!m_svg->hasElement("banana")); + + QCOMPARE(m_svg->elementSize("left"), QSizeF(35, 26)); + + QCOMPARE(m_svg->elementRect("left").toRect(), QRect(8, 71, 35, 26)); +} + +void SvgTest::testColors() +{ + KColorScheme windowColors(QPalette::Normal, KColorScheme::Window); + KColorScheme selectionColors(QPalette::Active, KColorScheme::Selection); + KColorScheme viewColors(QPalette::Active, KColorScheme::View); + + QCOMPARE(m_svg->color(KSvg::Svg::Text), windowColors.foreground(KColorScheme::NormalText)); + + m_svg->setStatus(KSvg::Svg::Selected); + QCOMPARE(m_svg->color(KSvg::Svg::Text), selectionColors.foreground(KColorScheme::NormalText)); + + m_svg->setStatus(KSvg::Svg::Inactive); + QCOMPARE(m_svg->color(KSvg::Svg::Text), selectionColors.foreground(KColorScheme::InactiveText)); + + m_svg->setStatus(KSvg::Svg::Normal); + m_svg->setColorSet(KSvg::Svg::View); + QCOMPARE(m_svg->color(KSvg::Svg::Text), viewColors.foreground(KColorScheme::NormalText)); + + m_svg->setStatus(KSvg::Svg::Selected); + QCOMPARE(m_svg->color(KSvg::Svg::Text), selectionColors.foreground(KColorScheme::NormalText)); + + m_svg->setStatus(KSvg::Svg::Normal); + + m_svg->setColor(KSvg::Svg::Text, QColor("#123456")); + QCOMPARE(m_svg->color(KSvg::Svg::Text), QColor("#123456")); + + m_svg->clearColorOverrides(); + + QCOMPARE(m_svg->color(KSvg::Svg::Text), viewColors.foreground(KColorScheme::NormalText)); +} + +QTEST_MAIN(SvgTest) + +#include "svgtest.moc" diff --git a/local/recipes/kde/kf6-ksvg/source/metainfo.yaml b/local/recipes/kde/kf6-ksvg/source/metainfo.yaml new file mode 100644 index 0000000000..b9fd3748d7 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/metainfo.yaml @@ -0,0 +1,19 @@ +description: A library for rendering SVG-based themes with stylesheet re-coloring and on-disk caching +tier: 3 +type: solution +platforms: + - name: Linux + - name: FreeBSD + - name: Windows + - name: macOS + - name: Android +portingAid: false +deprecated: false +release: true +libraries: + - cmake: KF6::Svg +cmakename: KF6Svg + +public_lib: true +group: Frameworks +subgroup: Tier 3 diff --git a/local/recipes/kde/kf6-ksvg/source/po/ar/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/ar/libksvg6.po new file mode 100644 index 0000000000..4fc3e90a47 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/ar/libksvg6.po @@ -0,0 +1,28 @@ +# Copyright (C) 2024 This file is copyright: +# This file is distributed under the same license as the ksvg package. +# +# SPDX-FileCopyrightText: 2024 Zayed Al-Saidi +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2024-02-12 12:10+0400\n" +"Last-Translator: Zayed Al-Saidi \n" +"Language-Team: ar\n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "زايد السعيدي" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "zayed.alsaidi@gmail.com" diff --git a/local/recipes/kde/kf6-ksvg/source/po/ast/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/ast/libksvg6.po new file mode 100644 index 0000000000..088cf8ea8e --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/ast/libksvg6.po @@ -0,0 +1,28 @@ +# Copyright (C) 2023 This file is copyright: +# This file is distributed under the same license as the ksvg package. +# +# SPDX-FileCopyrightText: 2023 Enol P. +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-10-25 10:01+0200\n" +"Last-Translator: Enol P. \n" +"Language-Team: \n" +"Language: ast\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 23.08.2\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Softastur" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "alministradores@softastur.org" diff --git a/local/recipes/kde/kf6-ksvg/source/po/bg/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/bg/libksvg6.po new file mode 100644 index 0000000000..352ccc3aeb --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/bg/libksvg6.po @@ -0,0 +1,30 @@ +# Bulgarian translations for ksvg package. +# Copyright (C) 2023 This file is copyright: +# This file is distributed under the same license as the ksvg package. +# +# Automatically generated, 2023. +# Mincho Kondarev , 2023. +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-08-09 09:53+0200\n" +"Last-Translator: Mincho Kondarev \n" +"Language-Team: \n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 23.04.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Минчо Кондарев" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "mkondarev@yahoo.de" diff --git a/local/recipes/kde/kf6-ksvg/source/po/ca/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/ca/libksvg6.po new file mode 100644 index 0000000000..d15146b8d5 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/ca/libksvg6.po @@ -0,0 +1,30 @@ +# Translation of libksvg6.po to Catalan +# Copyright (C) 2023 This_file_is_part_of_KDE +# This file is distributed under the license LGPL version 2.1 or +# version 3 or later versions approved by the membership of KDE e.V. +# +# SPDX-FileCopyrightText: 2023 Josep M. Ferrer +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-07-13 10:41+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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 22.12.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Josep M. Ferrer" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "txemaq@gmail.com" diff --git a/local/recipes/kde/kf6-ksvg/source/po/ca@valencia/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/ca@valencia/libksvg6.po new file mode 100644 index 0000000000..6f52a5abed --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/ca@valencia/libksvg6.po @@ -0,0 +1,30 @@ +# Translation of libksvg6.po to Catalan (Valencian) +# Copyright (C) 2023 This_file_is_part_of_KDE +# This file is distributed under the license LGPL version 2.1 or +# version 3 or later versions approved by the membership of KDE e.V. +# +# SPDX-FileCopyrightText: 2023 Josep M. Ferrer +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-07-13 10:41+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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 22.12.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Josep M. Ferrer" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "txemaq@gmail.com" diff --git a/local/recipes/kde/kf6-ksvg/source/po/cs/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/cs/libksvg6.po new file mode 100644 index 0000000000..2946273e8a --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/cs/libksvg6.po @@ -0,0 +1,28 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the ksvg package. +# Vit Pelcak , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-08-07 17:01+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 23.04.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Vít Pelčák" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "vit@pelcak.org" diff --git a/local/recipes/kde/kf6-ksvg/source/po/de/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/de/libksvg6.po new file mode 100644 index 0000000000..0ae375cfc9 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/de/libksvg6.po @@ -0,0 +1,28 @@ +# German translations for ksvg package. +# Copyright (C) 2023 This file is copyright: +# This file is distributed under the same license as the ksvg package. +# SPDX-FileCopyrightText: 2023 Frederik Schwarzer +msgid "" +msgstr "" +"Project-Id-Version: libksvg6\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-07-24 21:19+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 23.11.70\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Deutsches KDE-Übersetzungsteam" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "kde-i18n-de@kde.org" diff --git a/local/recipes/kde/kf6-ksvg/source/po/en_GB/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/en_GB/libksvg6.po new file mode 100644 index 0000000000..c64f453f9a --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/en_GB/libksvg6.po @@ -0,0 +1,28 @@ +# Copyright (C) 2024 This file is copyright: +# This file is distributed under the same license as the ksvg package. +# +# SPDX-FileCopyrightText: 2024 Steve Allewell +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2024-05-20 19:22+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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 24.02.2\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Steve Allewell" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "steve.allewell@gmail.com" diff --git a/local/recipes/kde/kf6-ksvg/source/po/eo/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/eo/libksvg6.po new file mode 100644 index 0000000000..f156a5c091 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/eo/libksvg6.po @@ -0,0 +1,28 @@ +# translation of libksvg6.pot to esperanto +# Copyright (C) 2023 Free Software Foundation, Inc. +# This file is distributed under the same license as the ksvg package. +# Oliver Kellogg \n" +"Language-Team: Esperanto \n" +"Language: eo\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Oliver Kellogg" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "olivermkellogg@gmail.com" diff --git a/local/recipes/kde/kf6-ksvg/source/po/es/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/es/libksvg6.po new file mode 100644 index 0000000000..e902297c01 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/es/libksvg6.po @@ -0,0 +1,30 @@ +# Spanish translations for libksvg5.po package. +# Copyright (C) 2023 This file is copyright: +# This file is distributed under the same license as the ksvg package. +# +# Automatically generated, 2023. +# Eloy Cuadra , 2023. +msgid "" +msgstr "" +"Project-Id-Version: libksvg5\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-07-15 00:31+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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 23.04.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Eloy Cuadra" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "ecuadra@eloihr.net" diff --git a/local/recipes/kde/kf6-ksvg/source/po/eu/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/eu/libksvg6.po new file mode 100644 index 0000000000..4a1bcfbf95 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/eu/libksvg6.po @@ -0,0 +1,31 @@ +# Translation for libksvg5.po to Euskara/Basque (eu) +# Copyright (C) 2023 This file is copyright: +# This file is distributed under the same license as the ksvg package. +# KDE euskaratzeko proiektuko arduraduna . +# +# Translators: +# Iñigo Salvador Azurmendi , 2023. +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-07-15 08:51+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" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 23.04.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Iñigo Salvador Azurmendi" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "xalba@ni.eus" diff --git a/local/recipes/kde/kf6-ksvg/source/po/fi/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/fi/libksvg6.po new file mode 100644 index 0000000000..f26bf5e8ec --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/fi/libksvg6.po @@ -0,0 +1,27 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the ksvg package. +# Tommi Nieminen , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-07-24 21:33+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" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Tommi Nieminen" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "translator@legisign.org" diff --git a/local/recipes/kde/kf6-ksvg/source/po/fr/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/fr/libksvg6.po new file mode 100644 index 0000000000..b757ea8edb --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/fr/libksvg6.po @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2023 Xavier Besnard +# SPDX-FileCopyrightText: 2023 Xavier Besnard +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-12-20 09:33+0100\n" +"Last-Translator: Xavier Besnard \n" +"Language-Team: French \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Lokalize 23.08.4\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Xavier Besnard" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "xavier.besnard@kde.org" diff --git a/local/recipes/kde/kf6-ksvg/source/po/ga/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/ga/libksvg6.po new file mode 100644 index 0000000000..65a69c72f7 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/ga/libksvg6.po @@ -0,0 +1,29 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the ksvg package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Irish Gaelic \n" +"Language: ga\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n < 11 ? " +"3 : 4\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "" diff --git a/local/recipes/kde/kf6-ksvg/source/po/gl/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/gl/libksvg6.po new file mode 100644 index 0000000000..e69e290d97 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/gl/libksvg6.po @@ -0,0 +1,28 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the ksvg package. +# Adrián Chaves (Gallaecio) , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-07-29 07:52+0200\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 23.04.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Adrian Chaves (Gallaecio)" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "adrian@chaves.gal" diff --git a/local/recipes/kde/kf6-ksvg/source/po/he/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/he/libksvg6.po new file mode 100644 index 0000000000..0d04982887 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/he/libksvg6.po @@ -0,0 +1,29 @@ +# Copyright (C) 2024 This file is copyright: +# This file is distributed under the same license as the ksvg package. +# +# SPDX-FileCopyrightText: 2024 Yaron Shahrabani +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2024-03-19 07:50+0200\n" +"Last-Translator: Yaron Shahrabani \n" +"Language-Team: צוות התרגום של KDE ישראל\n" +"Language: he\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && " +"n % 10 == 0) ? 2 : 3));\n" +"X-Generator: Lokalize 23.08.5\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "צוות התרגום של KDE ישראל" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "kde-l10n-he@kde.org" diff --git a/local/recipes/kde/kf6-ksvg/source/po/hi/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/hi/libksvg6.po new file mode 100644 index 0000000000..fd4057dec2 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/hi/libksvg6.po @@ -0,0 +1,28 @@ +# Hindi translations for ksvg package. +# Copyright (C) 2024 This file is copyright: +# This file is distributed under the same license as the ksvg package. +# Kali , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2024-12-15 17:36+0530\n" +"Last-Translator: Kali \n" +"Language-Team: Hindi \n" +"Language: hi\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" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "तुम्हारे नाम" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "आपके ईमेल" diff --git a/local/recipes/kde/kf6-ksvg/source/po/hu/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/hu/libksvg6.po new file mode 100644 index 0000000000..93d4de7eb0 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/hu/libksvg6.po @@ -0,0 +1,28 @@ +# Copyright (C) 2024 This file is copyright: +# This file is distributed under the same license as the ksvg package. +# +# SPDX-FileCopyrightText: 2024 Kristof Kiszel +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2024-01-07 22:36+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" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Lokalize 23.08.4\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Kiszel Kristóf" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "ulysses@fsf.hu" diff --git a/local/recipes/kde/kf6-ksvg/source/po/ia/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/ia/libksvg6.po new file mode 100644 index 0000000000..9103c96f7e --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/ia/libksvg6.po @@ -0,0 +1,28 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the ksvg package. +# +# giovanni , 2023. +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-08-13 15:13+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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 21.12.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Giovanni Sora" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "g.sora@tiscali.it" diff --git a/local/recipes/kde/kf6-ksvg/source/po/is/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/is/libksvg6.po new file mode 100644 index 0000000000..c44c6b05b9 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/is/libksvg6.po @@ -0,0 +1,28 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the ksvg package. +# +# Sveinn í Felli , 2024. +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2024-02-19 09:43+0000\n" +"Last-Translator: Sveinn í Felli \n" +"Language-Team: Icelandic\n" +"Language: is\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 22.04.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Sveinn í Felli" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "sv1@fellsnet.is" diff --git a/local/recipes/kde/kf6-ksvg/source/po/it/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/it/libksvg6.po new file mode 100644 index 0000000000..bc672dec9a --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/it/libksvg6.po @@ -0,0 +1,28 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the ksvg package. +# Vincenzo Reale , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-07-15 23:42+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 23.04.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Vincenzo Reale" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "smart2128vr@gmail.com" diff --git a/local/recipes/kde/kf6-ksvg/source/po/ja/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/ja/libksvg6.po new file mode 100644 index 0000000000..2e872e0b36 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/ja/libksvg6.po @@ -0,0 +1,25 @@ +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-07-14 21:55-0700\n" +"Last-Translator: Japanese KDE translation team \n" +"Language-Team: Japanese \n" +"Language: ja\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: kde4\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "" diff --git a/local/recipes/kde/kf6-ksvg/source/po/ka/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/ka/libksvg6.po new file mode 100644 index 0000000000..9d4f06a562 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/ka/libksvg6.po @@ -0,0 +1,29 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the ksvg package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-07-14 05:42+0200\n" +"Last-Translator: Temuri Doghonadze \n" +"Language-Team: Georgian \n" +"Language: ka\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.3.2\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Temuri Doghonadze" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "Temuri.doghonadze@gmail.com" diff --git a/local/recipes/kde/kf6-ksvg/source/po/ko/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/ko/libksvg6.po new file mode 100644 index 0000000000..7b75ca7a77 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/ko/libksvg6.po @@ -0,0 +1,28 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the ksvg package. +# Shinjo Park , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-07-25 01:00+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 22.12.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "박신조" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "kde@peremen.name" diff --git a/local/recipes/kde/kf6-ksvg/source/po/lt/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/lt/libksvg6.po new file mode 100644 index 0000000000..bcc8568036 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/lt/libksvg6.po @@ -0,0 +1,30 @@ +# Lithuanian translations for ksvg package. +# Copyright (C) 2024 This file is copyright: +# This file is distributed under the same license as the ksvg package. +# Automatically generated, 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2026-02-25 01:00+0200\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: lt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n%10>=2 && (n%100<10 || n" +"%100>=20) ? 1 : n%10==0 || (n%100>10 && n%100<20) ? 2 : 3);\n" +"X-Generator: Poedit 3.8\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Moo" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "<>" diff --git a/local/recipes/kde/kf6-ksvg/source/po/nl/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/nl/libksvg6.po new file mode 100644 index 0000000000..1ed5c05a28 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/nl/libksvg6.po @@ -0,0 +1,28 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the ksvg package. +# +# Freek de Kruijf , 2023. +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-07-13 12:27+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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 23.04.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Freek de Kruijf - 2023" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "freekdekruijf@kde.nl" diff --git a/local/recipes/kde/kf6-ksvg/source/po/nn/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/nn/libksvg6.po new file mode 100644 index 0000000000..ddcac43215 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/nn/libksvg6.po @@ -0,0 +1,30 @@ +# Translation of libksvg6 to Norwegian Nynorsk +# +# Karl Ove Hufthammer , 2023. +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-07-29 12:34+0200\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 23.04.3\n" +"X-Environment: kde\n" +"X-Accelerator-Marker: &\n" +"X-Text-Markup: kde4\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Karl Ove Hufthammer" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "karl@huftis.org" diff --git a/local/recipes/kde/kf6-ksvg/source/po/pl/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/pl/libksvg6.po new file mode 100644 index 0000000000..d4e5a505db --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/pl/libksvg6.po @@ -0,0 +1,28 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the ksvg package. +# +# Łukasz Wojniłowicz , 2023. +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-09-24 08:17+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" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2);\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Łukasz Wojniłowicz" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "lukasz.wojnilowicz@gmail.com" diff --git a/local/recipes/kde/kf6-ksvg/source/po/pt_BR/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/pt_BR/libksvg6.po new file mode 100644 index 0000000000..311f0c3cce --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/pt_BR/libksvg6.po @@ -0,0 +1,28 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the ksvg package. +# +# Luiz Fernando Ranghetti , 2023. +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-07-25 10:06-0300\n" +"Last-Translator: Luiz Fernando Ranghetti \n" +"Language-Team: Brazilian Portuguese \n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Lokalize 22.12.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Luiz Fernando Ranghetti" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "elchevive@opensuse.org" diff --git a/local/recipes/kde/kf6-ksvg/source/po/ro/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/ro/libksvg6.po new file mode 100644 index 0000000000..909ec744eb --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/ro/libksvg6.po @@ -0,0 +1,29 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the ksvg package. +# Sergiu Bivol , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2024-02-18 14:48+0000\n" +"Last-Translator: Sergiu Bivol \n" +"Language-Team: Romanian \n" +"Language: ro\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < " +"20)) ? 1 : 2;\n" +"X-Generator: Lokalize 21.12.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Sergiu Bivol" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "sergiu@cip.md" diff --git a/local/recipes/kde/kf6-ksvg/source/po/ru/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/ru/libksvg6.po new file mode 100644 index 0000000000..371da8abd9 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/ru/libksvg6.po @@ -0,0 +1,29 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the ksvg package. +# +# Alexander Yavorsky , 2024. +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2024-01-05 22:09+0300\n" +"Last-Translator: Alexander Yavorsky \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" +"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 21.08.3\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Александр Яворский" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "kekcuha@gmail.com" diff --git a/local/recipes/kde/kf6-ksvg/source/po/sa/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/sa/libksvg6.po new file mode 100644 index 0000000000..ee9fc7b6f2 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/sa/libksvg6.po @@ -0,0 +1,29 @@ +# Copyright (C) 2024 This file is copyright: +# This file is distributed under the same license as the ksvg package. +# +# SPDX-FileCopyrightText: 2024 kali +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2024-12-24 20:39+0530\n" +"Last-Translator: kali \n" +"Language-Team: Sanskrit \n" +"Language: sa\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 24.08.2\n" +"X-Poedit-SourceCharset: UTF-8\n" +"Plural-Forms: nplurals=2; plural=(n>2);\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "श्रीकान्त् कलवार्" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "skkalwar999@gmail.com" diff --git a/local/recipes/kde/kf6-ksvg/source/po/sk/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/sk/libksvg6.po new file mode 100644 index 0000000000..ef030c6386 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/sk/libksvg6.po @@ -0,0 +1,26 @@ +# translation of libksvg6.po to Slovak +# SPDX-FileCopyrightText: 2023 Roman Paholík +msgid "" +msgstr "" +"Project-Id-Version: libksvg6\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-12-09 09:08+0100\n" +"Last-Translator: Roman Paholik \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 23.08.3\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Roman Paholík" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "wizzardsk@gmail.com" diff --git a/local/recipes/kde/kf6-ksvg/source/po/sl/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/sl/libksvg6.po new file mode 100644 index 0000000000..5b62d5f1ac --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/sl/libksvg6.po @@ -0,0 +1,29 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the ksvg package. +# +# Matjaž Jeran , 2023. +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-07-14 06:33+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" +"X-Generator: Lokalize 23.04.2\n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n" +"%100==4 ? 3 : 0);\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Matjaž Jeran" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "matjaz.jeran@amis.net" diff --git a/local/recipes/kde/kf6-ksvg/source/po/sv/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/sv/libksvg6.po new file mode 100644 index 0000000000..ef39bbb721 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/sv/libksvg6.po @@ -0,0 +1,28 @@ +# Copyright (C) 2024 This file is copyright: +# This file is distributed under the same license as the ksvg package. +# +# SPDX-FileCopyrightText: 2024 Stefan Asserhäll +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2024-06-21 14:32+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" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Lokalize 23.08.5\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Stefan Asserhäll" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "stefan.asserhall@gmail.com" diff --git a/local/recipes/kde/kf6-ksvg/source/po/tr/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/tr/libksvg6.po new file mode 100644 index 0000000000..6ad03df6d6 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/tr/libksvg6.po @@ -0,0 +1,28 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the ksvg package. +# +# Emir SARI , 2023. +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-07-13 13:03+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 23.07.70\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Emir SARI" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "emir_sari@icloud.com" diff --git a/local/recipes/kde/kf6-ksvg/source/po/uk/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/uk/libksvg6.po new file mode 100644 index 0000000000..383a1e6882 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/uk/libksvg6.po @@ -0,0 +1,29 @@ +# Copyright (C) YEAR This file is copyright: +# This file is distributed under the same license as the ksvg package. +# +# Yuri Chornoivan , 2023. +msgid "" +msgstr "" +"Project-Id-Version: ksvg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2023-07-13 08:52+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" +"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" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Юрій Чорноіван" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "yurchor@ukr.net" diff --git a/local/recipes/kde/kf6-ksvg/source/po/zh_CN/libksvg6.po b/local/recipes/kde/kf6-ksvg/source/po/zh_CN/libksvg6.po new file mode 100644 index 0000000000..1a1a6adc0e --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/po/zh_CN/libksvg6.po @@ -0,0 +1,28 @@ +msgid "" +msgstr "" +"Project-Id-Version: kdeorg\n" +"Report-Msgid-Bugs-To: https://bugs.kde.org\n" +"POT-Creation-Date: 2025-11-17 11:49+0000\n" +"PO-Revision-Date: 2024-04-22 15:58\n" +"Last-Translator: \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" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Crowdin-Project: kdeorg\n" +"X-Crowdin-Project-ID: 269464\n" +"X-Crowdin-Language: zh-CN\n" +"X-Crowdin-File: /kf6-trunk/messages/ksvg/libksvg6.pot\n" +"X-Crowdin-File-ID: 44419\n" + +#, kde-format +msgctxt "NAME OF TRANSLATORS" +msgid "Your names" +msgstr "Tyson Tan" + +#, kde-format +msgctxt "EMAIL OF TRANSLATORS" +msgid "Your emails" +msgstr "tds00@qq.com" diff --git a/local/recipes/kde/kf6-ksvg/source/src/CMakeLists.txt b/local/recipes/kde/kf6-ksvg/source/src/CMakeLists.txt new file mode 100644 index 0000000000..dd6c20caad --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/CMakeLists.txt @@ -0,0 +1,12 @@ +add_subdirectory(ksvg) +add_subdirectory(declarativeimports) + +if (BUILD_TOOLS) + add_subdirectory(tools) +endif() + +ecm_qt_install_logging_categories( + EXPORT KSVG + FILE ksvg.categories + DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR} +) diff --git a/local/recipes/kde/kf6-ksvg/source/src/Messages.sh b/local/recipes/kde/kf6-ksvg/source/src/Messages.sh new file mode 100644 index 0000000000..ce1937a33b --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/Messages.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +# Invoke the extractrc script on all .ui, .rc, and .kcfg files in the sources. +# The results are stored in a pseudo .cpp file to be picked up by xgettext. +lst=`find . -name \*.rc -o -name \*.ui -o -name \*.kcfg` +if [ -n "$lst" ] ; then + $EXTRACTRC $lst >> rc.cpp +fi + +# Run xgettext to extract strings from all source files. +$XGETTEXT `find . -name \*.cpp -o -name \*.h -o -name \*.qml` -o $podir/libksvg6.pot diff --git a/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/CMakeLists.txt b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/CMakeLists.txt new file mode 100644 index 0000000000..ea16617575 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/CMakeLists.txt @@ -0,0 +1,30 @@ +ecm_add_qml_module(corebindingsplugin URI "org.kde.ksvg" VERSION 1.0 DEPENDENCIES QtQuick GENERATE_PLUGIN_SOURCE) + +target_sources(corebindingsplugin PRIVATE + svgitem.cpp + framesvgitem.cpp + managedtexturenode.cpp + imagetexturescache.cpp + types.h +) + +target_link_libraries(corebindingsplugin PRIVATE + Qt6::Quick + Qt6::Qml + Qt6::Svg + KF6::Svg + KF6::ColorScheme + KF6::KirigamiPlatform + KF6::GuiAddons +) + +ecm_qt_declare_logging_category(corebindingsplugin + HEADER debug_p.h + IDENTIFIER LOG_KSVGQML + CATEGORY_NAME kf.svg + DESCRIPTION "KSvg QML plugin" +) + +ecm_finalize_qml_module(corebindingsplugin DESTINATION ${KDE_INSTALL_QMLDIR}) + +ecm_generate_qdoc(corebindingsplugin ksvgqml.qdocconf) \ No newline at end of file diff --git a/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/framesvgitem.cpp b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/framesvgitem.cpp new file mode 100644 index 0000000000..dddf7a39a9 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/framesvgitem.cpp @@ -0,0 +1,787 @@ +/* + SPDX-FileCopyrightText: 2010 Marco Martin + SPDX-FileCopyrightText: 2014 David Edmundson + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "framesvgitem.h" + +#include "imagetexturescache.h" +#include "managedtexturenode.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include //floor() + +#include +#include + +namespace KSvg +{ +Q_GLOBAL_STATIC(ImageTexturesCache, s_cache) + +class FrameNode : public QSGNode +{ +public: + FrameNode(const QString &prefix, FrameSvg *svg) + : QSGNode() + , leftWidth(0) + , rightWidth(0) + , topHeight(0) + , bottomHeight(0) + { + if (svg->enabledBorders() & FrameSvg::LeftBorder) { + leftWidth = svg->elementSize(prefix % QLatin1String("left")).width(); + } + if (svg->enabledBorders() & FrameSvg::RightBorder) { + rightWidth = svg->elementSize(prefix % QLatin1String("right")).width(); + } + if (svg->enabledBorders() & FrameSvg::TopBorder) { + topHeight = svg->elementSize(prefix % QLatin1String("top")).height(); + } + if (svg->enabledBorders() & FrameSvg::BottomBorder) { + bottomHeight = svg->elementSize(prefix % QLatin1String("bottom")).height(); + } + } + + QRect contentsRect(const QSize &size) const + { + const QSize contentSize(size.width() - leftWidth - rightWidth, size.height() - topHeight - bottomHeight); + + return QRect(QPoint(leftWidth, topHeight), contentSize); + } + +private: + int leftWidth; + int rightWidth; + int topHeight; + int bottomHeight; +}; + +class FrameItemNode : public ManagedTextureNode +{ +public: + enum FitMode { + // render SVG at native resolution then stretch it in openGL + FastStretch, + // on resize re-render the part of the frame from the SVG + Stretch, + Tile, + }; + + FrameItemNode(FrameSvgItem *frameSvg, FrameSvg::EnabledBorders borders, FitMode fitMode, QSGNode *parent) + : ManagedTextureNode() + , m_frameSvg(frameSvg) + , m_border(borders) + , m_fitMode(fitMode) + { + parent->appendChildNode(this); + + if (m_fitMode == Tile) { + if (m_border == FrameSvg::TopBorder || m_border == FrameSvg::BottomBorder || m_border == FrameSvg::NoBorder) { + static_cast(material())->setHorizontalWrapMode(QSGTexture::Repeat); + static_cast(opaqueMaterial())->setHorizontalWrapMode(QSGTexture::Repeat); + } + if (m_border == FrameSvg::LeftBorder || m_border == FrameSvg::RightBorder || m_border == FrameSvg::NoBorder) { + static_cast(material())->setVerticalWrapMode(QSGTexture::Repeat); + static_cast(opaqueMaterial())->setVerticalWrapMode(QSGTexture::Repeat); + } + } + + if (m_fitMode == Tile || m_fitMode == FastStretch) { + QString elementId = m_frameSvg->frameSvg()->actualPrefix() + FrameSvgHelpers::borderToElementId(m_border); + m_elementNativeSize = m_frameSvg->frameSvg()->elementSize(elementId).toSize(); + + if (m_elementNativeSize.isEmpty()) { + // if the default element is empty, we can avoid the slower tiling path + // this also avoids a divide by 0 error + m_fitMode = FastStretch; + } + + updateTexture(m_elementNativeSize, elementId); + } + } + + void updateTexture(const QSize &size, const QString &elementId) + { + QQuickWindow::CreateTextureOptions options; + if (m_fitMode != Tile) { + options = QQuickWindow::TextureCanUseAtlas; + } + setTexture(s_cache->loadTexture(m_frameSvg->window(), m_frameSvg->frameSvg()->image(size, elementId), options)); + } + + void reposition(const QRect &frameGeometry, QSize &fullSize) + { + QRectF nodeRect = FrameSvgHelpers::sectionRect(m_border, frameGeometry, fullSize); + + // ensure we're not passing a weird rectangle to updateTexturedRectGeometry + if (!nodeRect.isValid() || nodeRect.isEmpty()) { + nodeRect = QRect(); + } + + // the position of the relevant texture within this texture ID. + // for atlas' this will only be a small part of the texture + QRectF textureRect; + + if (m_fitMode == Tile) { + textureRect = QRectF(0, 0, 1, 1); // we can never be in an atlas for tiled images. + + // if tiling horizontally + if (m_border == FrameSvg::TopBorder || m_border == FrameSvg::BottomBorder || m_border == FrameSvg::NoBorder) { + // cmp. CSS3's border-image-repeat: "repeat", though with first tile not centered, but aligned to left + textureRect.setWidth((qreal)nodeRect.width() / m_elementNativeSize.width()); + } + // if tiling vertically + if (m_border == FrameSvg::LeftBorder || m_border == FrameSvg::RightBorder || m_border == FrameSvg::NoBorder) { + // cmp. CSS3's border-image-repeat: "repeat", though with first tile not centered, but aligned to top + textureRect.setHeight((qreal)nodeRect.height() / m_elementNativeSize.height()); + } + } else if (m_fitMode == Stretch) { + QString prefix = m_frameSvg->frameSvg()->actualPrefix(); + + QString elementId = prefix + FrameSvgHelpers::borderToElementId(m_border); + + // re-render the SVG at new size + updateTexture(nodeRect.size().toSize(), elementId); + textureRect = texture()->normalizedTextureSubRect(); + } else if (texture()) { // for fast stretch. + textureRect = texture()->normalizedTextureSubRect(); + } + + QSGGeometry::updateTexturedRectGeometry(geometry(), nodeRect, textureRect); + markDirty(QSGNode::DirtyGeometry); + } + +private: + FrameSvgItem *m_frameSvg; + FrameSvg::EnabledBorders m_border; + QSize m_elementNativeSize; + FitMode m_fitMode; +}; + +FrameSvgItemMargins::FrameSvgItemMargins(KSvg::FrameSvg *frameSvg, QObject *parent) + : QObject(parent) + , m_frameSvg(frameSvg) + , m_fixed(false) + , m_inset(false) +{ + // qDebug() << "margins at: " << left() << top() << right() << bottom(); +} + +qreal FrameSvgItemMargins::left() const +{ + if (m_fixed) { + return m_frameSvg->fixedMarginSize(FrameSvg::LeftMargin); + } else if (m_inset) { + return m_frameSvg->insetSize(FrameSvg::LeftMargin); + } else { + return m_frameSvg->marginSize(FrameSvg::LeftMargin); + } +} + +qreal FrameSvgItemMargins::top() const +{ + if (m_fixed) { + return m_frameSvg->fixedMarginSize(FrameSvg::TopMargin); + } else if (m_inset) { + return m_frameSvg->insetSize(FrameSvg::TopMargin); + } else { + return m_frameSvg->marginSize(FrameSvg::TopMargin); + } +} + +qreal FrameSvgItemMargins::right() const +{ + if (m_fixed) { + return m_frameSvg->fixedMarginSize(FrameSvg::RightMargin); + } else if (m_inset) { + return m_frameSvg->insetSize(FrameSvg::RightMargin); + } else { + return m_frameSvg->marginSize(FrameSvg::RightMargin); + } +} + +qreal FrameSvgItemMargins::bottom() const +{ + if (m_fixed) { + return m_frameSvg->fixedMarginSize(FrameSvg::BottomMargin); + } else if (m_inset) { + return m_frameSvg->insetSize(FrameSvg::BottomMargin); + } else { + return m_frameSvg->marginSize(FrameSvg::BottomMargin); + } +} + +qreal FrameSvgItemMargins::horizontal() const +{ + return left() + right(); +} + +qreal FrameSvgItemMargins::vertical() const +{ + return top() + bottom(); +} + +QList FrameSvgItemMargins::margins() const +{ + qreal left; + qreal top; + qreal right; + qreal bottom; + m_frameSvg->getMargins(left, top, right, bottom); + return {left, top, right, bottom}; +} + +void FrameSvgItemMargins::update() +{ + Q_EMIT marginsChanged(); +} + +void FrameSvgItemMargins::setFixed(bool fixed) +{ + if (fixed == m_fixed) { + return; + } + + m_fixed = fixed; + Q_EMIT marginsChanged(); +} + +bool FrameSvgItemMargins::isFixed() const +{ + return m_fixed; +} + +void FrameSvgItemMargins::setInset(bool inset) +{ + if (inset == m_inset) { + return; + } + + m_inset = inset; + Q_EMIT marginsChanged(); +} + +bool FrameSvgItemMargins::isInset() const +{ + return m_inset; +} + +FrameSvgItem::FrameSvgItem(QQuickItem *parent) + : QQuickItem(parent) + , m_margins(nullptr) + , m_fixedMargins(nullptr) + , m_insetMargins(nullptr) + , m_textureChanged(false) + , m_sizeChanged(false) + , m_fastPath(true) +{ + m_frameSvg = new KSvg::FrameSvg(this); + + setFlag(QQuickItem::ItemHasContents, true); + setFlag(ItemHasContents, true); + connect(m_frameSvg, &FrameSvg::repaintNeeded, this, &FrameSvgItem::doUpdate); + connect(m_frameSvg, &Svg::fromCurrentImageSetChanged, this, &FrameSvgItem::fromCurrentImageSetChanged); + connect(m_frameSvg, &Svg::statusChanged, this, &FrameSvgItem::statusChanged); +} + +FrameSvgItem::~FrameSvgItem() +{ +} + +class CheckMarginsChange +{ +public: + CheckMarginsChange(QList &oldMargins, FrameSvgItemMargins *marginsObject) + : m_oldMargins(oldMargins) + , m_marginsObject(marginsObject) + { + } + + ~CheckMarginsChange() + { + const QList oldMarginsBefore = m_oldMargins; + m_oldMargins = m_marginsObject ? m_marginsObject->margins() : QList(); + + if (m_marginsObject && oldMarginsBefore != m_oldMargins) { + m_marginsObject->update(); + } + } + +private: + QList &m_oldMargins; + FrameSvgItemMargins *const m_marginsObject; +}; + +void FrameSvgItem::setImagePath(const QString &path) +{ + if (m_frameSvg->imagePath() == path) { + return; + } + + CheckMarginsChange checkMargins(m_oldMargins, m_margins); + CheckMarginsChange checkFixedMargins(m_oldFixedMargins, m_fixedMargins); + CheckMarginsChange checkInsetMargins(m_oldInsetMargins, m_insetMargins); + + updateDevicePixelRatio(); + m_frameSvg->setImagePath(path); + + if (implicitWidth() <= 0) { + setImplicitWidth(m_frameSvg->marginSize(KSvg::FrameSvg::LeftMargin) + m_frameSvg->marginSize(KSvg::FrameSvg::RightMargin)); + } + + if (implicitHeight() <= 0) { + setImplicitHeight(m_frameSvg->marginSize(KSvg::FrameSvg::TopMargin) + m_frameSvg->marginSize(KSvg::FrameSvg::BottomMargin)); + } + + Q_EMIT imagePathChanged(); + + if (isComponentComplete()) { + applyPrefixes(); + + m_frameSvg->resizeFrame(size()); + m_textureChanged = true; + update(); + } +} + +QString FrameSvgItem::imagePath() const +{ + return m_frameSvg->imagePath(); +} + +void FrameSvgItem::setPrefix(const QVariant &prefixes) +{ + QStringList prefixList; + // is this a simple string? + if (prefixes.canConvert()) { + prefixList << prefixes.toString(); + } else if (prefixes.canConvert()) { + prefixList = prefixes.toStringList(); + } + + if (m_prefixes == prefixList) { + return; + } + + CheckMarginsChange checkMargins(m_oldMargins, m_margins); + CheckMarginsChange checkFixedMargins(m_oldFixedMargins, m_fixedMargins); + CheckMarginsChange checkInsetMargins(m_oldInsetMargins, m_insetMargins); + + m_prefixes = prefixList; + applyPrefixes(); + + if (implicitWidth() <= 0) { + setImplicitWidth(m_frameSvg->marginSize(KSvg::FrameSvg::LeftMargin) + m_frameSvg->marginSize(KSvg::FrameSvg::RightMargin)); + } + + if (implicitHeight() <= 0) { + setImplicitHeight(m_frameSvg->marginSize(KSvg::FrameSvg::TopMargin) + m_frameSvg->marginSize(KSvg::FrameSvg::BottomMargin)); + } + + Q_EMIT prefixChanged(); + + if (isComponentComplete()) { + m_frameSvg->resizeFrame(QSizeF(width(), height())); + m_textureChanged = true; + update(); + } +} + +QVariant FrameSvgItem::prefix() const +{ + return m_prefixes; +} + +QString FrameSvgItem::usedPrefix() const +{ + return m_frameSvg->prefix(); +} + +FrameSvgItemMargins *FrameSvgItem::margins() +{ + if (!m_margins) { + m_margins = new FrameSvgItemMargins(m_frameSvg, this); + } + return m_margins; +} + +FrameSvgItemMargins *FrameSvgItem::fixedMargins() +{ + if (!m_fixedMargins) { + m_fixedMargins = new FrameSvgItemMargins(m_frameSvg, this); + m_fixedMargins->setFixed(true); + } + return m_fixedMargins; +} + +FrameSvgItemMargins *FrameSvgItem::inset() +{ + if (!m_insetMargins) { + m_insetMargins = new FrameSvgItemMargins(m_frameSvg, this); + m_insetMargins->setInset(true); + } + return m_insetMargins; +} + +bool FrameSvgItem::fromCurrentImageSet() const +{ + return m_frameSvg->fromCurrentImageSet(); +} + +void FrameSvgItem::setStatus(KSvg::Svg::Status status) +{ + m_frameSvg->setStatus(status); +} + +KSvg::Svg::Status FrameSvgItem::status() const +{ + return m_frameSvg->status(); +} + +void FrameSvgItem::setEnabledBorders(const KSvg::FrameSvg::EnabledBorders borders) +{ + if (m_frameSvg->enabledBorders() == borders) { + return; + } + + CheckMarginsChange checkMargins(m_oldMargins, m_margins); + + m_frameSvg->setEnabledBorders(borders); + Q_EMIT enabledBordersChanged(); + m_textureChanged = true; + update(); +} + +KSvg::FrameSvg::EnabledBorders FrameSvgItem::enabledBorders() const +{ + return m_frameSvg->enabledBorders(); +} + +void FrameSvgItem::setColorSet(KSvg::Svg::ColorSet colorSet) +{ + if (m_frameSvg->colorSet() == colorSet) { + return; + } + + m_frameSvg->setColorSet(colorSet); + m_textureChanged = true; + + update(); +} + +KSvg::Svg::ColorSet FrameSvgItem::colorSet() const +{ + return m_frameSvg->colorSet(); +} + +bool FrameSvgItem::hasElementPrefix(const QString &prefix) const +{ + return m_frameSvg->hasElementPrefix(prefix); +} + +bool FrameSvgItem::hasElement(const QString &elementName) const +{ + return m_frameSvg->hasElement(elementName); +} + +QRegion FrameSvgItem::mask() const +{ + return m_frameSvg->mask(); +} + +int FrameSvgItem::minimumDrawingHeight() const +{ + return m_frameSvg->minimumDrawingHeight(); +} + +int FrameSvgItem::minimumDrawingWidth() const +{ + return m_frameSvg->minimumDrawingWidth(); +} + +void FrameSvgItem::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + const bool isComponentComplete = this->isComponentComplete(); + if (isComponentComplete) { + m_frameSvg->resizeFrame(newGeometry.size()); + m_sizeChanged = true; + } + + QQuickItem::geometryChange(newGeometry, oldGeometry); + + // the above only triggers updatePaintNode, so we have to inform subscribers + // about the potential change of the mask explicitly here + if (isComponentComplete) { + Q_EMIT maskChanged(); + } +} + +void FrameSvgItem::doUpdate() +{ + if (m_frameSvg->isRepaintBlocked()) { + return; + } + + CheckMarginsChange checkMargins(m_oldMargins, m_margins); + CheckMarginsChange checkFixedMargins(m_oldFixedMargins, m_fixedMargins); + CheckMarginsChange checkInsetMargins(m_oldInsetMargins, m_insetMargins); + + // if the theme changed, the available prefix may have changed as well + applyPrefixes(); + + if (implicitWidth() <= 0) { + setImplicitWidth(m_frameSvg->marginSize(KSvg::FrameSvg::LeftMargin) + m_frameSvg->marginSize(KSvg::FrameSvg::RightMargin)); + } + + if (implicitHeight() <= 0) { + setImplicitHeight(m_frameSvg->marginSize(KSvg::FrameSvg::TopMargin) + m_frameSvg->marginSize(KSvg::FrameSvg::BottomMargin)); + } + + QString prefix = m_frameSvg->actualPrefix(); + bool hasOverlay = (!prefix.startsWith(QLatin1String("mask-")) // + && m_frameSvg->hasElement(prefix % QLatin1String("overlay"))); + bool hasComposeOverBorder = m_frameSvg->hasElement(prefix % QLatin1String("hint-compose-over-border")) + && m_frameSvg->hasElement(QLatin1String("mask-") % prefix % QLatin1String("center")); + m_fastPath = !hasOverlay && !hasComposeOverBorder; + + // Software rendering (at time of writing Qt5.10) doesn't seem to like our + // tiling/stretching in the 9-tiles. + // Also when using QPainter it's arguably faster to create and cache pixmaps + // of the whole frame, which is what the slow path does + if (QQuickWindow::sceneGraphBackend() == QLatin1String("software")) { + m_fastPath = false; + } + m_textureChanged = true; + + update(); + + Q_EMIT maskChanged(); + Q_EMIT repaintNeeded(); +} + +KSvg::FrameSvg *FrameSvgItem::frameSvg() const +{ + return m_frameSvg; +} + +QSGNode *FrameSvgItem::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *) +{ + if (!window() || !m_frameSvg // + || (!m_frameSvg->hasElementPrefix(m_frameSvg->actualPrefix()) // + && !m_frameSvg->hasElementPrefix(m_frameSvg->prefix()))) { + delete oldNode; + return nullptr; + } + + const QSGTexture::Filtering filtering = smooth() ? QSGTexture::Linear : QSGTexture::Nearest; + + if (m_fastPath) { + if (m_textureChanged) { + delete oldNode; + oldNode = nullptr; + } + + if (!oldNode) { + QString prefix = m_frameSvg->actualPrefix(); + oldNode = new FrameNode(prefix, m_frameSvg); + + bool tileCenter = (m_frameSvg->hasElement(QStringLiteral("hint-tile-center")) // + || m_frameSvg->hasElement(prefix % QLatin1String("hint-tile-center"))); + bool stretchBorders = (m_frameSvg->hasElement(QStringLiteral("hint-stretch-borders")) // + || m_frameSvg->hasElement(prefix % QLatin1String("hint-stretch-borders"))); + FrameItemNode::FitMode borderFitMode = stretchBorders ? FrameItemNode::Stretch : FrameItemNode::Tile; + FrameItemNode::FitMode centerFitMode = tileCenter ? FrameItemNode::Tile : FrameItemNode::Stretch; + + new FrameItemNode(this, FrameSvg::NoBorder, centerFitMode, oldNode); + if (enabledBorders() & (FrameSvg::TopBorder | FrameSvg::LeftBorder)) { + new FrameItemNode(this, FrameSvg::TopBorder | FrameSvg::LeftBorder, FrameItemNode::FastStretch, oldNode); + } + if (enabledBorders() & (FrameSvg::TopBorder | FrameSvg::RightBorder)) { + new FrameItemNode(this, FrameSvg::TopBorder | FrameSvg::RightBorder, FrameItemNode::FastStretch, oldNode); + } + if (enabledBorders() & FrameSvg::TopBorder) { + new FrameItemNode(this, FrameSvg::TopBorder, borderFitMode, oldNode); + } + if (enabledBorders() & FrameSvg::BottomBorder) { + new FrameItemNode(this, FrameSvg::BottomBorder, borderFitMode, oldNode); + } + if (enabledBorders() & (FrameSvg::BottomBorder | FrameSvg::LeftBorder)) { + new FrameItemNode(this, FrameSvg::BottomBorder | FrameSvg::LeftBorder, FrameItemNode::FastStretch, oldNode); + } + if (enabledBorders() & (FrameSvg::BottomBorder | FrameSvg::RightBorder)) { + new FrameItemNode(this, FrameSvg::BottomBorder | FrameSvg::RightBorder, FrameItemNode::FastStretch, oldNode); + } + if (enabledBorders() & FrameSvg::LeftBorder) { + new FrameItemNode(this, FrameSvg::LeftBorder, borderFitMode, oldNode); + } + if (enabledBorders() & FrameSvg::RightBorder) { + new FrameItemNode(this, FrameSvg::RightBorder, borderFitMode, oldNode); + } + + m_sizeChanged = true; + m_textureChanged = false; + } + + QSGNode *node = oldNode->firstChild(); + while (node) { + static_cast(node)->setFiltering(filtering); + node = node->nextSibling(); + } + + if (m_sizeChanged) { + FrameNode *frameNode = static_cast(oldNode); + QSize frameSize(width(), height()); + QRect geometry = frameNode->contentsRect(frameSize); + QSGNode *node = oldNode->firstChild(); + while (node) { + static_cast(node)->reposition(geometry, frameSize); + node = node->nextSibling(); + } + + m_sizeChanged = false; + } + } else { + ManagedTextureNode *textureNode = dynamic_cast(oldNode); + if (!textureNode) { + delete oldNode; + textureNode = new ManagedTextureNode; + m_textureChanged = true; // force updating the texture on our newly created node + oldNode = textureNode; + } + textureNode->setFiltering(filtering); + + if ((m_textureChanged || m_sizeChanged) || textureNode->texture()->textureSize() != m_frameSvg->size()) { + QImage image = m_frameSvg->framePixmap().toImage(); + textureNode->setTexture(s_cache->loadTexture(window(), image)); + textureNode->setRect(0, 0, width(), height()); + + m_textureChanged = false; + m_sizeChanged = false; + } + } + + return oldNode; +} + +void FrameSvgItem::classBegin() +{ + QQuickItem::classBegin(); + m_frameSvg->setRepaintBlocked(true); +} + +void FrameSvgItem::componentComplete() +{ + m_kirigamiTheme = qobject_cast(qmlAttachedPropertiesObject(this, true)); + if (!m_kirigamiTheme) { + qCWarning(LOG_KSVGQML) << "no theme!" << qmlAttachedPropertiesObject(this, true) << this; + return; + } + + auto checkApplyTheme = [this]() { + if (!m_frameSvg->imageSet()->filePath(QStringLiteral("colors")).isEmpty()) { + m_frameSvg->clearCache(); + m_frameSvg->clearColorOverrides(); + } + }; + auto applyTheme = [this]() { + if (!m_frameSvg->imageSet()->filePath(QStringLiteral("colors")).isEmpty()) { + m_frameSvg->clearCache(); + m_frameSvg->clearColorOverrides(); + + return; + } + m_frameSvg->setColors( + {{Svg::Text, m_kirigamiTheme->textColor()}, + {Svg::Background, m_kirigamiTheme->backgroundColor()}, + {Svg::Highlight, m_kirigamiTheme->highlightColor()}, + {Svg::HighlightedText, m_kirigamiTheme->highlightedTextColor()}, + {Svg::PositiveText, m_kirigamiTheme->positiveTextColor()}, + {Svg::NeutralText, m_kirigamiTheme->neutralTextColor()}, + {Svg::NegativeText, m_kirigamiTheme->negativeTextColor()}, + {Svg::Frame, KColorUtils::mix(m_kirigamiTheme->backgroundColor(), m_kirigamiTheme->textColor(), KColorScheme::frameContrast())}}); + }; + + applyTheme(); + connect(m_kirigamiTheme, &Kirigami::Platform::PlatformTheme::frameContrastChanged, this, applyTheme); + connect(m_kirigamiTheme, &Kirigami::Platform::PlatformTheme::colorsChanged, this, applyTheme); + connect(m_frameSvg->imageSet(), &ImageSet::imageSetChanged, this, checkApplyTheme); + connect(m_frameSvg, &Svg::imageSetChanged, this, checkApplyTheme); + + CheckMarginsChange checkMargins(m_oldMargins, m_margins); + CheckMarginsChange checkFixedMargins(m_oldFixedMargins, m_fixedMargins); + CheckMarginsChange checkInsetMargins(m_oldInsetMargins, m_insetMargins); + + QQuickItem::componentComplete(); + m_frameSvg->resizeFrame(size()); + m_frameSvg->setRepaintBlocked(false); + m_textureChanged = true; +} + +void FrameSvgItem::updateDevicePixelRatio() +{ + const auto newDevicePixelRatio = std::max(1.0, (window() ? window()->devicePixelRatio() : qApp->devicePixelRatio())); + if (newDevicePixelRatio != m_frameSvg->devicePixelRatio()) { + m_frameSvg->setDevicePixelRatio(newDevicePixelRatio); + m_textureChanged = true; + } +} + +void FrameSvgItem::applyPrefixes() +{ + if (m_frameSvg->imagePath().isEmpty()) { + return; + } + + const QString oldPrefix = m_frameSvg->prefix(); + + if (m_prefixes.isEmpty()) { + m_frameSvg->setElementPrefix(QString()); + if (oldPrefix != m_frameSvg->prefix()) { + Q_EMIT usedPrefixChanged(); + } + return; + } + + bool found = false; + for (const QString &prefix : std::as_const(m_prefixes)) { + if (m_frameSvg->hasElementPrefix(prefix)) { + m_frameSvg->setElementPrefix(prefix); + found = true; + break; + } + } + if (!found) { + // this setElementPrefix is done to keep the same behavior as before, when it was a simple string + m_frameSvg->setElementPrefix(m_prefixes.constLast()); + } + if (oldPrefix != m_frameSvg->prefix()) { + Q_EMIT usedPrefixChanged(); + } +} + +void FrameSvgItem::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) +{ + if (change == ItemSceneChange && value.window) { + updateDevicePixelRatio(); + } else if (change == QQuickItem::ItemDevicePixelRatioHasChanged) { + updateDevicePixelRatio(); + } + + QQuickItem::itemChange(change, value); +} + +} // KSvg namespace + +#include "moc_framesvgitem.cpp" diff --git a/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/framesvgitem.h b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/framesvgitem.h new file mode 100644 index 0000000000..2ff6a01e3a --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/framesvgitem.h @@ -0,0 +1,313 @@ +/* + SPDX-FileCopyrightText: 2010 Marco Martin + SPDX-FileCopyrightText: 2014 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef FRAMESVGITEM_P +#define FRAMESVGITEM_P + +#include +#include + +#include + +#include + +namespace Kirigami +{ +namespace Platform +{ +class PlatformTheme; +} +}; + +namespace KSvg +{ +class FrameSvg; + +class FrameSvgItemMargins : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("FrameSvgItemMargins are read-only properties of FrameSvgItem") + + Q_PROPERTY(qreal left READ left NOTIFY marginsChanged) + Q_PROPERTY(qreal top READ top NOTIFY marginsChanged) + Q_PROPERTY(qreal right READ right NOTIFY marginsChanged) + Q_PROPERTY(qreal bottom READ bottom NOTIFY marginsChanged) + Q_PROPERTY(qreal horizontal READ horizontal NOTIFY marginsChanged) + Q_PROPERTY(qreal vertical READ vertical NOTIFY marginsChanged) + +public: + FrameSvgItemMargins(KSvg::FrameSvg *frameSvg, QObject *parent = nullptr); + + qreal left() const; + qreal top() const; + qreal right() const; + qreal bottom() const; + qreal horizontal() const; + qreal vertical() const; + + /// returns a vector with left, top, right, bottom + QList margins() const; + + void setFixed(bool fixed); + bool isFixed() const; + + void setInset(bool inset); + bool isInset() const; + +public Q_SLOTS: + void update(); + +Q_SIGNALS: + void marginsChanged(); + +private: + FrameSvg *m_frameSvg; + bool m_fixed; + bool m_inset; +}; + +/*! + * \qmltype FrameSvgItem + * \inqmlmodule org.kde.ksvg + * + * \brief An SVG Item with borders. + */ +class FrameSvgItem : public QQuickItem +{ + Q_OBJECT + QML_ELEMENT + Q_INTERFACES(QQmlParserStatus) + + /*! + * \brief This property specifies the relative path of the SVG in the theme. + * + * Example: "widgets/background" + * + * \qmlproperty string FrameSvgItem::imagePath + */ + Q_PROPERTY(QString imagePath READ imagePath WRITE setImagePath NOTIFY imagePathChanged) + + /*! + * This property holds the prefix for the SVG. + * prefix for the 9-piece SVG, like "pushed" or "normal" for a button. + * see https://techbase.kde.org/Development/Tutorials/Plasma5/ThemeDetails + * for a list of paths and prefixes + * It can also be an array of strings, specifying a fallback chain in case + * the first element isn't found in the theme, eg ["toolbutton-normal", "normal"] + * so it's easy to keep backwards compatibility with old themes + * (Note: fallback chain is supported only \since 5.32) + * + * \qmlproperty variant FrameSvgItem::prefix + */ + Q_PROPERTY(QVariant prefix READ prefix WRITE setPrefix NOTIFY prefixChanged) + + /*! + * \brief This property holds the actual prefix that was used, if a fallback + * chain array was set as "prefix". + * + * \since 5.34 + * \qmlproperty string FrameSvgItem::usedPrefix + */ + Q_PROPERTY(QString usedPrefix READ usedPrefix NOTIFY usedPrefixChanged) + + /*! + * \qmlproperty qreal FrameSvgItem::margins.left + * \qmlproperty qreal FrameSvgItem::margins.top + * \qmlproperty qreal FrameSvgItem::margins.right + * \qmlproperty qreal FrameSvgItem::margins.bottom + * \qmlproperty qreal FrameSvgItem::margins.horizontal + * \qmlproperty qreal FrameSvgItem::margins.vertical + * + * \brief This property holds the frame's margins. + */ + Q_PROPERTY(KSvg::FrameSvgItemMargins *margins READ margins CONSTANT) + + /*! + * \qmlproperty qreal FrameSvgItem::fixedMargins.left + * \qmlproperty qreal FrameSvgItem::fixedMargins.top + * \qmlproperty qreal FrameSvgItem::fixedMargins.right + * \qmlproperty qreal FrameSvgItem::fixedMargins.bottom + * \qmlproperty qreal FrameSvgItem::fixedMargins.horizontal + * \qmlproperty qreal FrameSvgItem::fixedMargins.vertical + * + * \brief This property holds the fixed margins of the frame which are used + * regardless of any margins being enabled or not. + */ + Q_PROPERTY(KSvg::FrameSvgItemMargins *fixedMargins READ fixedMargins CONSTANT) + + /*! + * \qmlproperty qreal FrameSvgItem::inset.left + * \qmlproperty qreal FrameSvgItem::inset.top + * \qmlproperty qreal FrameSvgItem::inset.right + * \qmlproperty qreal FrameSvgItem::inset.bottom + * \qmlproperty qreal FrameSvgItem::inset.horizontal + * \qmlproperty qreal FrameSvgItem::inset.vertical + * + * \brief This property holds the frame's inset. + * + * \since 5.77 + */ + Q_PROPERTY(KSvg::FrameSvgItemMargins *inset READ inset CONSTANT) + + /*! + * \brief This property specifies which borders are shown. + * \sa KSvg::FrameSvg::EnabledBorder + * \qmlproperty flags FrameSvgItem::enabledBorders + */ + Q_PROPERTY(KSvg::FrameSvg::EnabledBorders enabledBorders READ enabledBorders WRITE setEnabledBorders NOTIFY enabledBordersChanged) + + /*! + * \brief This property holds whether the current SVG is present in + * the currently-used theme and no fallback is involved. + * + * \qmlproperty bool FrameSvgItem::fromCurrentImageSet + */ + Q_PROPERTY(bool fromCurrentImageSet READ fromCurrentImageSet NOTIFY fromCurrentImageSetChanged) + + /*! + * \brief This property specifies the SVG's status. + * + * Depending on the specified status, the SVG will use different colors: + * * Normal: text's color is textColor, and background color is + * backgroundColor. + * * Selected: text color becomes highlightedText and background color is + * changed to highlightColor. + * + * \sa Kirigami::PlatformTheme + * \sa KSvg::Svg::status + * \since 5.23 + * \qmlproperty enum FrameSvgItem::status + */ + Q_PROPERTY(KSvg::Svg::Status status READ status WRITE setStatus NOTIFY statusChanged) + + /*! + * \brief This property holds the mask that contains the SVG's painted areas. + * \since 5.58 + * \qmlproperty QRegion FrameSvgItem::mask + */ + Q_PROPERTY(QRegion mask READ mask NOTIFY maskChanged) + + /*! + * \brief This property holds the minimum height required to correctly draw + * this SVG. + * + * \since 5.101 + * \qmlproperty int FrameSvgItem::minimumDrawingHeight + */ + Q_PROPERTY(int minimumDrawingHeight READ minimumDrawingHeight NOTIFY repaintNeeded) + + /*! + * \brief This property holds the minimum width required to correctly draw + * this SVG. + * + * \since 5.101 + * \qmlproperty int FrameSvgItem::minimumDrawingWidth + */ + Q_PROPERTY(int minimumDrawingWidth READ minimumDrawingWidth NOTIFY repaintNeeded) + +public: + /*! + * \qmlmethod bool FrameSvgItem::hasElementPrefix(string prefix) + * + * Returns whether the svg has the necessary elements with the given prefix + * to draw a frame. + * + * \a prefix the given prefix we want to check if drawable + */ + Q_INVOKABLE bool hasElementPrefix(const QString &prefix) const; + + /*! + * \qmlmethod bool FrameSvgItem::hasElement(string elementName) + * + * Returns whether the SVG includes the given element. + * + * This is a convenience function that forwards to hasElement(). + * + * \sa KSvg::Svg::hasElement() + */ + Q_INVOKABLE bool hasElement(const QString &elementName) const; + + FrameSvgItem(QQuickItem *parent = nullptr); + ~FrameSvgItem() override; + + void setImagePath(const QString &path); + QString imagePath() const; + + void setPrefix(const QVariant &prefix); + QVariant prefix() const; + + QString usedPrefix() const; + + void setEnabledBorders(const KSvg::FrameSvg::EnabledBorders borders); + KSvg::FrameSvg::EnabledBorders enabledBorders() const; + + void setColorSet(KSvg::Svg::ColorSet colorSet); + KSvg::Svg::ColorSet colorSet() const; + + FrameSvgItemMargins *margins(); + FrameSvgItemMargins *fixedMargins(); + FrameSvgItemMargins *inset(); + + bool fromCurrentImageSet() const; + + void setStatus(KSvg::Svg::Status status); + KSvg::Svg::Status status() const; + int minimumDrawingHeight() const; + int minimumDrawingWidth() const; + + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; + + QRegion mask() const; + + /* + * Only to be used from inside this library, is not intended to be invokable + */ + KSvg::FrameSvg *frameSvg() const; + + QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *) override; + + void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data) override; + +protected: + void classBegin() override; + void componentComplete() override; + +Q_SIGNALS: + void imagePathChanged(); + void prefixChanged(); + void enabledBordersChanged(); + void fromCurrentImageSetChanged(); + void repaintNeeded(); + void statusChanged(); + void usedPrefixChanged(); + void maskChanged(); + +private Q_SLOTS: + void doUpdate(); + +private: + void updateDevicePixelRatio(); + void applyPrefixes(); + + KSvg::FrameSvg *m_frameSvg; + Kirigami::Platform::PlatformTheme *m_kirigamiTheme; + FrameSvgItemMargins *m_margins; + FrameSvgItemMargins *m_fixedMargins; + FrameSvgItemMargins *m_insetMargins; + // logged margins to check for changes + QList m_oldMargins; + QList m_oldFixedMargins; + QList m_oldInsetMargins; + QStringList m_prefixes; + bool m_textureChanged; + bool m_sizeChanged; + bool m_fastPath; +}; + +} + +#endif diff --git a/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/imagetexturescache.cpp b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/imagetexturescache.cpp new file mode 100644 index 0000000000..34f410f7a1 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/imagetexturescache.cpp @@ -0,0 +1,58 @@ +/* + SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "imagetexturescache.h" +#include + +typedef QHash>> TexturesCache; + +class ImageTexturesCachePrivate +{ +public: + TexturesCache cache; +}; + +ImageTexturesCache::ImageTexturesCache() + : d(new ImageTexturesCachePrivate) +{ +} + +ImageTexturesCache::~ImageTexturesCache() +{ +} + +QSharedPointer ImageTexturesCache::loadTexture(QQuickWindow *window, const QImage &image, QQuickWindow::CreateTextureOptions options) +{ + qint64 id = image.cacheKey(); + QSharedPointer texture = d->cache.value(id).value(window).toStrongRef(); + + 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 = QSharedPointer(window->createTextureFromImage(image, options), cleanAndDelete); + (d->cache)[id][window] = texture.toWeakRef(); + } + + // 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 = QSharedPointer(window->createTextureFromImage(image, options)); + } + + return texture; +} + +QSharedPointer ImageTexturesCache::loadTexture(QQuickWindow *window, const QImage &image) +{ + return loadTexture(window, image, QQuickWindow::CreateTextureOptions()); +} diff --git a/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/imagetexturescache.h b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/imagetexturescache.h new file mode 100644 index 0000000000..910c5b5026 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/imagetexturescache.h @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef IMAGETEXTURESCACHE_H +#define IMAGETEXTURESCACHE_H + +#include +#include + +class QImage; +class QSGTexture; +class ImageTexturesCachePrivate; + +/* + * Helps to manage textures by creating images and reference counts them. + * + * Use this class as a factory for textures, when creating them from a QImage + * instance. Keeps track of all the created textures in a map between the + * QImage::cacheKey() and the cached texture until it gets de-referenced. + * + * \sa ManagedTextureNode + */ +class ImageTexturesCache +{ +public: + ImageTexturesCache(); + ~ImageTexturesCache(); + + /*! + * Returns the texture for a given window and image. + * + * If image id is the same as one already provided before, we will not + * create a new texture, and will instead return a shared pointer to the existing texture. + */ + QSharedPointer loadTexture(QQuickWindow *window, const QImage &image, QQuickWindow::CreateTextureOptions options); + + QSharedPointer loadTexture(QQuickWindow *window, const QImage &image); + +private: + QScopedPointer d; +}; + +#endif // IMAGETEXTURESCACHE_H diff --git a/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/ksvgqml.qdoc b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/ksvgqml.qdoc new file mode 100644 index 0000000000..59a4e97be9 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/ksvgqml.qdoc @@ -0,0 +1,6 @@ +/*! + \qmlmodule org.kde.ksvg + \title KSvg QML Types + \ingroup qmlmodules + \brief A QML module for rendering SVG-based themes with stylesheet re-coloring and on-disk caching. +*/ diff --git a/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/ksvgqml.qdocconf b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/ksvgqml.qdocconf new file mode 100644 index 0000000000..23b8bb7647 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/ksvgqml.qdocconf @@ -0,0 +1,38 @@ +include($KDE_DOCS/global/qt-module-defaults.qdocconf) + +project = KSvgQml +description = A library for rendering SVG-based themes with stylesheet re-coloring and on-disk caching. + +documentationinheaders = true + +headerdirs += . +sourcedirs += . + +outputformats = HTML + +navigation.landingpage = "KSvg" + +depends += \ + kde \ + qtcore \ + qtqml \ + qtquickcontrols \ + ksvg \ + kirigami \ + kirigamiplatform + +qhp.projects = KSvgQml + +qhp.KSvgQml.file = ksvgqml.qhp +qhp.KSvgQml.namespace = org.kde.ksvgqml.$QT_VERSION_TAG +qhp.KSvgQml.virtualFolder = ksvgqml +qhp.KSvgQml.indexTitle = KSvg QML +qhp.KSvgQml.indexRoot = + +qhp.KSvgQml.subprojects = qmltypes +qhp.KSvgQml.subprojects.qmltypes.title = QML Types +qhp.KSvgQml.subprojects.qmltypes.indexTitle = KSvg QML Types +qhp.KSvgQml.subprojects.qmltypes.selectors = qmltype +qhp.KSvgQml.subprojects.qmltypes.sortPages = true + +tagfile = ksvgqml.tags diff --git a/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/managedtexturenode.cpp b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/managedtexturenode.cpp new file mode 100644 index 0000000000..e701feddbf --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/managedtexturenode.cpp @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "managedtexturenode.h" + +ManagedTextureNode::ManagedTextureNode() +{ +} + +void ManagedTextureNode::setTexture(QSharedPointer texture) +{ + m_texture = texture; + QSGSimpleTextureNode::setTexture(texture.data()); +} diff --git a/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/managedtexturenode.h b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/managedtexturenode.h new file mode 100644 index 0000000000..e0b6dde598 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/managedtexturenode.h @@ -0,0 +1,27 @@ +/* + SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef MANAGEDTEXTURENODE_H +#define MANAGEDTEXTURENODE_H + +#include +#include +#include +#include + +class ManagedTextureNode : public QSGSimpleTextureNode +{ + Q_DISABLE_COPY(ManagedTextureNode) +public: + ManagedTextureNode(); + + void setTexture(QSharedPointer texture); + +private: + QSharedPointer m_texture; +}; + +#endif diff --git a/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/svgitem.cpp b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/svgitem.cpp new file mode 100644 index 0000000000..fd37ebd9fb --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/svgitem.cpp @@ -0,0 +1,297 @@ +/* + SPDX-FileCopyrightText: 2010 Marco Martin + SPDX-FileCopyrightText: 2014 David Edmundson + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "svgitem.h" + +#include +#include +#include +#include + +#include "ksvg/svg.h" + +#include "managedtexturenode.h" + +#include +#include +#include +#include + +namespace KSvg +{ +SvgItem::SvgItem(QQuickItem *parent) + : QQuickItem(parent) + , m_textureChanged(false) +{ + m_svg = new KSvg::Svg(this); + setFlag(QQuickItem::ItemHasContents, true); + + connect(m_svg, &Svg::repaintNeeded, this, &SvgItem::updateNeeded); + connect(m_svg, &Svg::repaintNeeded, this, &SvgItem::naturalSizeChanged); + connect(m_svg, &Svg::sizeChanged, this, &SvgItem::naturalSizeChanged); + connect(m_svg, &Svg::repaintNeeded, this, &SvgItem::elementRectChanged); + connect(m_svg, &Svg::sizeChanged, this, &SvgItem::elementRectChanged); +} + +SvgItem::~SvgItem() +{ + // Make sure to not call anything on m_svg when this is shutting down + // Kirigami::PlatformTheme will lose its window at that point so will + // emit colorschanged, which we shouldn't react to during destructor + disconnect(m_kirigamiTheme, nullptr, this, nullptr); +} + +void SvgItem::componentComplete() +{ + m_kirigamiTheme = qobject_cast(qmlAttachedPropertiesObject(this, true)); + if (!m_kirigamiTheme) { + qCWarning(LOG_KSVGQML) << "No theme!" << qmlAttachedPropertiesObject(this, true) << this; + return; + } + + auto checkApplyTheme = [this]() { + if (!m_svg->imageSet()->filePath(QStringLiteral("colors")).isEmpty()) { + m_svg->clearColorOverrides(); + } + }; + auto applyTheme = [this]() { + if (!m_svg) { + return; + } + if (!m_svg->imageSet()->filePath(QStringLiteral("colors")).isEmpty()) { + m_svg->clearColorOverrides(); + return; + } + m_svg->setColors({{Svg::Text, m_kirigamiTheme->textColor()}, + {Svg::Background, m_kirigamiTheme->backgroundColor()}, + {Svg::Highlight, m_kirigamiTheme->highlightColor()}, + {Svg::HighlightedText, m_kirigamiTheme->highlightedTextColor()}, + {Svg::PositiveText, m_kirigamiTheme->positiveTextColor()}, + {Svg::NeutralText, m_kirigamiTheme->neutralTextColor()}, + {Svg::NegativeText, m_kirigamiTheme->negativeTextColor()}, + {Svg::Frame, KColorUtils::mix(m_kirigamiTheme->backgroundColor(), m_kirigamiTheme->textColor(), KColorScheme::frameContrast())}}); + }; + applyTheme(); + connect(m_kirigamiTheme, &Kirigami::Platform::PlatformTheme::frameContrastChanged, this, applyTheme); + connect(m_kirigamiTheme, &Kirigami::Platform::PlatformTheme::colorsChanged, this, applyTheme); + connect(m_svg->imageSet(), &ImageSet::imageSetChanged, this, checkApplyTheme); + connect(m_svg, &Svg::imageSetChanged, this, checkApplyTheme); + + QQuickItem::componentComplete(); +} + +void SvgItem::setImagePath(const QString &path) +{ + if (!m_svg || m_svg->imagePath() == path) { + return; + } + + updateDevicePixelRatio(); + m_svg->setImagePath(path); + + Q_EMIT imagePathChanged(); + + if (isComponentComplete()) { + update(); + } +} + +QString SvgItem::imagePath() const +{ + return m_svg->imagePath(); +} + +void SvgItem::setElementId(const QString &elementID) +{ + if (elementID == m_elementID) { + return; + } + + if (implicitWidth() <= 0) { + setImplicitWidth(naturalSize().width()); + } + if (implicitHeight() <= 0) { + setImplicitHeight(naturalSize().height()); + } + + m_elementID = elementID; + Q_EMIT elementIdChanged(); + Q_EMIT naturalSizeChanged(); + Q_EMIT elementRectChanged(); + + scheduleImageUpdate(); +} + +QString SvgItem::elementId() const +{ + return m_elementID; +} + +void SvgItem::setSvg(KSvg::Svg *svg) +{ + if (m_svg) { + disconnect(m_svg.data(), nullptr, this, nullptr); + } + m_svg = svg; + updateDevicePixelRatio(); + + if (svg) { + connect(svg, &Svg::repaintNeeded, this, &SvgItem::updateNeeded); + connect(svg, &Svg::repaintNeeded, this, &SvgItem::naturalSizeChanged); + connect(svg, &Svg::repaintNeeded, this, &SvgItem::elementRectChanged); + connect(svg, &Svg::sizeChanged, this, &SvgItem::naturalSizeChanged); + connect(svg, &Svg::sizeChanged, this, &SvgItem::elementRectChanged); + } + + if (implicitWidth() <= 0) { + setImplicitWidth(naturalSize().width()); + } + if (implicitHeight() <= 0) { + setImplicitHeight(naturalSize().height()); + } + + scheduleImageUpdate(); + + Q_EMIT svgChanged(); + Q_EMIT naturalSizeChanged(); + Q_EMIT elementRectChanged(); + Q_EMIT imagePathChanged(); +} + +KSvg::Svg *SvgItem::svg() const +{ + return m_svg.data(); +} + +QSizeF SvgItem::naturalSize() const +{ + if (!m_svg) { + return QSizeF(); + } else if (!m_elementID.isEmpty()) { + return m_svg->elementSize(m_elementID); + } + + return m_svg->size(); +} + +QRectF SvgItem::elementRect() const +{ + if (!m_svg) { + return QRectF(); + } else if (!m_elementID.isEmpty()) { + return m_svg->elementRect(m_elementID); + } + + return QRectF(QPointF(0, 0), m_svg->size()); +} + +QSGNode *SvgItem::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData) +{ + Q_UNUSED(updatePaintNodeData); + if (!window() || !m_svg) { + delete oldNode; + return nullptr; + } + + // this is more than just an optimization, uploading a null image to QSGAtlasTexture causes a crash + if (width() == 0.0 || height() == 0.0) { + delete oldNode; + return nullptr; + } + + ManagedTextureNode *textureNode = static_cast(oldNode); + if (!textureNode) { + textureNode = new ManagedTextureNode; + m_textureChanged = true; + } + + // TODO use a heuristic to work out when to redraw + // if !m_smooth and size is approximate simply change the textureNode.rect without + // updating the material + + if (m_textureChanged || textureNode->texture()->textureSize() != QSize(width(), height())) { + // despite having a valid size sometimes we still get a null QImage from KSvg::Svg + // loading a null texture to an atlas fatals + // Dave E fixed this in Qt in 5.3.something onwards but we need this for now + if (m_image.isNull()) { + delete textureNode; + return nullptr; + } + + QSharedPointer texture(window()->createTextureFromImage(m_image, QQuickWindow::TextureCanUseAtlas)); + textureNode->setTexture(texture); + m_textureChanged = false; + + textureNode->setRect(0, 0, width(), height()); + } + + textureNode->setFiltering(smooth() ? QSGTexture::Linear : QSGTexture::Nearest); + + return textureNode; +} + +void SvgItem::updateNeeded() +{ + if (implicitWidth() <= 0) { + setImplicitWidth(naturalSize().width()); + } + if (implicitHeight() <= 0) { + setImplicitHeight(naturalSize().height()); + } + scheduleImageUpdate(); +} + +void SvgItem::scheduleImageUpdate() +{ + polish(); + update(); +} + +void SvgItem::updatePolish() +{ + QQuickItem::updatePolish(); + + if (m_svg) { + // setContainsMultipleImages has to be done there since m_svg can be shared with somebody else + m_textureChanged = true; + m_svg->setContainsMultipleImages(!m_elementID.isEmpty()); + m_image = m_svg->image(QSize(width(), height()), m_elementID); + } +} + +void SvgItem::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + if (newGeometry.size() != oldGeometry.size() && newGeometry.isValid()) { + scheduleImageUpdate(); + } + + QQuickItem::geometryChange(newGeometry, oldGeometry); +} + +void SvgItem::updateDevicePixelRatio() +{ + const auto newDevicePixelRatio = std::max(1.0, (window() ? window()->devicePixelRatio() : qApp->devicePixelRatio())); + if (newDevicePixelRatio != m_svg->devicePixelRatio()) { + m_svg->setDevicePixelRatio(newDevicePixelRatio); + m_textureChanged = true; + } +} + +void SvgItem::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) +{ + if (change == ItemSceneChange && value.window) { + updateDevicePixelRatio(); + } else if (change == QQuickItem::ItemDevicePixelRatioHasChanged) { + updateDevicePixelRatio(); + } + + QQuickItem::itemChange(change, value); +} + +} // KSvg namespace + +#include "moc_svgitem.cpp" diff --git a/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/svgitem.h b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/svgitem.h new file mode 100644 index 0000000000..9ff50c99ec --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/svgitem.h @@ -0,0 +1,136 @@ +/* + SPDX-FileCopyrightText: 2010 Marco Martin + SPDX-FileCopyrightText: 2014 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef SVGITEM_P +#define SVGITEM_P + +#include +#include + +#include + +namespace Kirigami +{ +namespace Platform +{ +class PlatformTheme; +} +}; + +namespace KSvg +{ +class Svg; + +/*! + * \qmltype SvgItem + * \inqmlmodule org.kde.ksvg + * + * \brief Displays an SVG or an element from an SVG file. + */ +class SvgItem : public QQuickItem +{ + Q_OBJECT + QML_ELEMENT + + /*! + * \brief This property specifies the relative path of the Svg in the theme. + * + * Example: "widgets/background" + * + * \qmlproperty string SvgItem::imagePath + */ + Q_PROPERTY(QString imagePath READ imagePath WRITE setImagePath NOTIFY imagePathChanged) + + /*! + * \brief This property specifies the sub-element of the SVG to be + * rendered. + * + * If this is empty, the whole SVG document will be rendered. + * + * \qmlproperty string SvgItem::elementId + */ + Q_PROPERTY(QString elementId READ elementId WRITE setElementId NOTIFY elementIdChanged) + + /*! + * \brief This property holds the SVG's natural, unscaled size. + * + * This is useful if a pixel-perfect rendering of outlines is needed. + * + * \qmlproperty size SvgItem::naturalSize + */ + Q_PROPERTY(QSizeF naturalSize READ naturalSize NOTIFY naturalSizeChanged) + + /*! + * \brief This property holds the rectangle of the selected elementId + * relative to the unscaled size of the SVG document. + * + * Note that this property will holds the entire SVG if element id is not + * selected. + * + * \qmlproperty rect SvgItem::elementRect + */ + Q_PROPERTY(QRectF elementRect READ elementRect NOTIFY elementRectChanged) + + /*! + * \brief This property holds the internal SVG instance. + * + * Usually, specifying just the imagePath is enough. Use this if you have + * many items taking the same SVG as source, and you want to share the + * internal SVG object. + * + * \qmlproperty KSvg::Svg SvgItem::svg + */ + Q_PROPERTY(KSvg::Svg *svg READ svg WRITE setSvg NOTIFY svgChanged) + +public: + explicit SvgItem(QQuickItem *parent = nullptr); + ~SvgItem() override; + + void setImagePath(const QString &path); + QString imagePath() const; + + void setElementId(const QString &elementID); + QString elementId() const; + + void setSvg(KSvg::Svg *svg); + KSvg::Svg *svg() const; + + QSizeF naturalSize() const; + + QRectF elementRect() const; + + QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData) override; + + void itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data) override; + +protected: + void componentComplete() override; + +Q_SIGNALS: + void imagePathChanged(); + void elementIdChanged(); + void svgChanged(); + void naturalSizeChanged(); + void elementRectChanged(); + +protected Q_SLOTS: + void updateNeeded(); + +private: + void updateDevicePixelRatio(); + void scheduleImageUpdate(); + void updatePolish() override; + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; + + QPointer m_svg; + Kirigami::Platform::PlatformTheme *m_kirigamiTheme; + QString m_elementID; + QImage m_image; + bool m_textureChanged; +}; +} + +#endif diff --git a/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/types.h b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/types.h new file mode 100644 index 0000000000..4005e2d230 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/declarativeimports/types.h @@ -0,0 +1,60 @@ +/* + SPDX-FileCopyrightText: 2023 Nicolas Fella + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KSVG_TYPES_H +#define KSVG_TYPES_H + +#include + +#include + +#include +#include + +struct PlatformThemeForeign { + Q_GADGET + QML_ANONYMOUS + QML_FOREIGN(Kirigami::Platform::PlatformTheme) +}; + +/*! + * \qmltype Svg + * \inqmlmodule org.kde.ksvg + * \nativetype KSvg::Svg + */ +struct SvgForeign { + Q_GADGET + QML_ELEMENT + QML_NAMED_ELEMENT(Svg) + QML_FOREIGN(KSvg::Svg) +}; + +/*! + * \qmltype FrameSvg + * \inqmlmodule org.kde.ksvg + * \nativetype KSvg::FrameSvg + */ +struct FrameSvgForeign { + Q_GADGET + QML_ELEMENT + QML_NAMED_ELEMENT(FrameSvg) + QML_FOREIGN(KSvg::FrameSvg) +}; + +/*! + * \qmltype ImageSet + * \inqmlmodule org.kde.ksvg + * \nativetype KSvg::ImageSet + */ +struct ImageSetForeign { + Q_GADGET + QML_ELEMENT + QML_SINGLETON + QML_NAMED_ELEMENT(ImageSet) + QML_FOREIGN(KSvg::ImageSet) +}; + +#endif diff --git a/local/recipes/kde/kf6-ksvg/source/src/ksvg/.krazy b/local/recipes/kde/kf6-ksvg/source/src/ksvg/.krazy new file mode 100644 index 0000000000..587e2fcca6 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/ksvg/.krazy @@ -0,0 +1,2 @@ +EXTRA defines,kdebug,qenums,tipsandthis +SKIP /widgets/template\.h diff --git a/local/recipes/kde/kf6-ksvg/source/src/ksvg/CMakeLists.txt b/local/recipes/kde/kf6-ksvg/source/src/ksvg/CMakeLists.txt new file mode 100644 index 0000000000..72d5c3ebb3 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/ksvg/CMakeLists.txt @@ -0,0 +1,94 @@ + + +# Consumer's include dir which has to be explicitly used to make headers of this lib visible to documented includes +# Results in duplicate of prefix-dir & C++ namespace below, but part of different things, so by design: +# //class header files +set(KSVG_INSTALL_INCLUDEDIR "${KDE_INSTALL_INCLUDEDIR_KF}/KSvg") + +add_library(KF6Svg) +add_library(KF6::Svg ALIAS KF6Svg) + +qt_extract_metatypes(KF6Svg) + +set_target_properties(KF6Svg PROPERTIES + VERSION ${KSVG_VERSION} + SOVERSION ${KSVG_SOVERSION} + EXPORT_NAME Svg +) + +target_sources(KF6Svg PRIVATE + framesvg.cpp + svg.cpp + imageset.cpp + private/imageset_p.cpp +) + +ecm_qt_declare_logging_category(KF6Svg + HEADER debug_p.h + IDENTIFIER LOG_KSVG + CATEGORY_NAME kf.svg + DESCRIPTION "KSvg lib" + EXPORT KSVG +) + +ecm_generate_export_header(KF6Svg + EXPORT_FILE_NAME ksvg/ksvg_export.h + BASE_NAME KSvg + GROUP_BASE_NAME KF + VERSION ${KF_VERSION} + USE_VERSION_HEADER + DEPRECATED_BASE_VERSION 0 + EXCLUDE_DEPRECATED_BEFORE_AND_AT ${EXCLUDE_DEPRECATED_BEFORE_AND_AT} + DEPRECATION_VERSIONS 6.21 +) + +target_link_libraries(KF6Svg +PUBLIC + Qt6::Gui + KF6::ConfigCore +PRIVATE + Qt6::Svg + KF6::Archive + KF6::CoreAddons + KF6::GuiAddons #kimagecache + KF6::ConfigCore + KF6::ColorScheme +) + +set(KSvg_BUILD_INCLUDE_DIRS + ${CMAKE_CURRENT_SOURCE_DIR}/.. + ${CMAKE_CURRENT_BINARY_DIR}/KSvg +) +target_include_directories(KF6Svg + PUBLIC + "$" + INTERFACE + "$" +) + +########### install files ############### +ecm_generate_headers(KSvg_CamelCase_HEADERS + HEADER_NAMES + FrameSvg + Svg + ImageSet + REQUIRED_HEADERS KSvg_namespaced_HEADERS + PREFIX KSvg +) + +install( + FILES ${KSvg_namespaced_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/ksvg/ksvg_export.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/KSvg/ksvg # C++ namespace + COMPONENT Devel +) + +install( + FILES ${KSvg_CamelCase_HEADERS} + DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/KSvg/KSvg # C++ namespace + COMPONENT Devel +) + +install(TARGETS KF6Svg EXPORT KF6SvgTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS}) + +ecm_generate_qdoc(KF6Svg ksvg.qdocconf) diff --git a/local/recipes/kde/kf6-ksvg/source/src/ksvg/README b/local/recipes/kde/kf6-ksvg/source/src/ksvg/README new file mode 100644 index 0000000000..12a63647c2 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/ksvg/README @@ -0,0 +1,22 @@ +KSvg + +This directory contains the classes making up KSvg, which provides rendering api for Svg files, +and support for the themes used in the Plasma Desktop. + +Domain specific sets of functionality, e.g. for network awareness or sensors, +are not found here but as Applet, Wallpaper, +ContainmentActions, Containment and other plugins. + +Commit Guidelines: +* If your patch is not an obvious or trivial bug fix, have it peer reviewed + by another Frameworks developer; https://phabricator.kde.org is your friend :) + +* All code MUST follow the KDE Frameworks coding style, as found at: + https://techbase.kde.org/Policies/Frameworks_Coding_Style + +* All new public API MUST have apidox written before committing and must go + through an API review with another Frameworks developer. We have to maintain + binary compatibility, remember! + +Unit tests are next to godliness. + diff --git a/local/recipes/kde/kf6-ksvg/source/src/ksvg/framesvg.cpp b/local/recipes/kde/kf6-ksvg/source/src/ksvg/framesvg.cpp new file mode 100644 index 0000000000..ff9a62c71c --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/ksvg/framesvg.cpp @@ -0,0 +1,1110 @@ +/* + SPDX-FileCopyrightText: 2008-2010 Aaron Seigo + SPDX-FileCopyrightText: 2008-2010 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "framesvg.h" +#include "private/framesvg_p.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "debug_p.h" +#include "imageset.h" +#include "private/framesvg_helpers.h" +#include "private/imageset_p.h" +#include "private/svg_p.h" + +namespace KSvg +{ +QHash>> FrameSvgPrivate::s_sharedFrames; + +// Any attempt to generate a frame whose width or height is larger than this +// will be rejected +static const int MAX_FRAME_SIZE = 100000; + +FrameData::~FrameData() +{ + FrameSvgPrivate::s_sharedFrames[imageSet].remove(cacheId); +} + +FrameSvg::FrameSvg(QObject *parent) + : Svg(parent) + , d(new FrameSvgPrivate(this)) +{ + connect(this, &FrameSvg::colorSetChanged, this, [this]() { + if (!d->repaintBlocked) { + d->updateFrameData(Svg::d->lastModified); + } + }); + + connect(this, &FrameSvg::repaintNeeded, this, std::bind(&FrameSvgPrivate::updateNeeded, d)); +} + +FrameSvg::~FrameSvg() +{ + delete d; +} + +void FrameSvg::setImagePath(const QString &path) +{ + if (path == imagePath()) { + return; + } + + clearCache(); + + setContainsMultipleImages(true); + Svg::d->setImagePath(path); + if (!d->repaintBlocked) { + d->updateFrameData(Svg::d->lastModified); + } +} + +void FrameSvg::setEnabledBorders(const EnabledBorders borders) +{ + if (borders == d->enabledBorders) { + return; + } + + d->enabledBorders = borders; + + if (!d->repaintBlocked) { + d->updateFrameData(Svg::d->lastModified); + } +} + +FrameSvg::EnabledBorders FrameSvg::enabledBorders() const +{ + return d->enabledBorders; +} + +void FrameSvg::setElementPrefix(KSvg::FrameSvg::LocationPrefix location) +{ + switch (location) { + case TopEdge: + setElementPrefix(QStringLiteral("north")); + break; + case BottomEdge: + setElementPrefix(QStringLiteral("south")); + break; + case LeftEdge: + setElementPrefix(QStringLiteral("west")); + break; + case RightEdge: + setElementPrefix(QStringLiteral("east")); + break; + default: + setElementPrefix(QString()); + break; + } + + d->location = location; +} + +void FrameSvg::setElementPrefix(const QString &prefix) +{ + if (prefix.isEmpty() || !hasElement(prefix % QLatin1String("-center"))) { + d->prefix.clear(); + } else { + d->prefix = prefix; + if (!d->prefix.isEmpty()) { + d->prefix += QLatin1Char('-'); + } + } + d->requestedPrefix = prefix; + + d->location = FrameSvg::Floating; + + if (!d->repaintBlocked) { + d->updateFrameData(Svg::d->lastModified); + } +} + +bool FrameSvg::hasElementPrefix(const QString &prefix) const +{ + // for now it simply checks if a center element exists, + // because it could make sense for certain themes to not have all the elements + if (prefix.isEmpty()) { + return hasElement(QStringLiteral("center")); + } + if (prefix.endsWith(QLatin1Char('-'))) { + return hasElement(prefix % QLatin1String("center")); + } + + return hasElement(prefix % QLatin1String("-center")); +} + +bool FrameSvg::hasElementPrefix(KSvg::FrameSvg::LocationPrefix location) const +{ + switch (location) { + case TopEdge: + return hasElementPrefix(QStringLiteral("north")); + case BottomEdge: + return hasElementPrefix(QStringLiteral("south")); + case LeftEdge: + return hasElementPrefix(QStringLiteral("west")); + case RightEdge: + return hasElementPrefix(QStringLiteral("east")); + default: + return hasElementPrefix(QString()); + } +} + +QString FrameSvg::prefix() +{ + return d->requestedPrefix; +} + +void FrameSvg::resizeFrame(const QSizeF &size) +{ + if (imagePath().isEmpty()) { + return; + } + + if (size.isEmpty()) { +#ifndef NDEBUG + // qCDebug(LOG_KSVG) << "Invalid size" << size; +#endif + return; + } + + if (d->frame && size.toSize() == d->frame->frameSize) { + return; + } + d->pendingFrameSize = size.toSize(); + + if (!d->repaintBlocked) { + d->updateFrameData(Svg::d->lastModified, FrameSvgPrivate::UpdateFrame); + } +} + +QSizeF FrameSvg::frameSize() const +{ + if (!d->frame) { + return QSizeF(-1, -1); + } else { + return d->frameSize(d->frame.data()); + } +} + +qreal FrameSvg::marginSize(const FrameSvg::MarginEdge edge) const +{ + if (!d->frame) { + return .0; + } + + if (d->frame->noBorderPadding) { + return .0; + } + + switch (edge) { + case FrameSvg::TopMargin: + return d->frame->topMargin; + + case FrameSvg::LeftMargin: + return d->frame->leftMargin; + + case FrameSvg::RightMargin: + return d->frame->rightMargin; + + // KSvg::BottomMargin + default: + return d->frame->bottomMargin; + } +} + +qreal FrameSvg::insetSize(const FrameSvg::MarginEdge edge) const +{ + if (!d->frame) { + return .0; + } + + if (d->frame->noBorderPadding) { + return .0; + } + + switch (edge) { + case FrameSvg::TopMargin: + return d->frame->insetTopMargin; + + case FrameSvg::LeftMargin: + return d->frame->insetLeftMargin; + + case FrameSvg::RightMargin: + return d->frame->insetRightMargin; + + // KSvg::BottomMargin + default: + return d->frame->insetBottomMargin; + } +} + +qreal FrameSvg::fixedMarginSize(const FrameSvg::MarginEdge edge) const +{ + if (!d->frame) { + return .0; + } + + if (d->frame->noBorderPadding) { + return .0; + } + + switch (edge) { + case FrameSvg::TopMargin: + return d->frame->fixedTopMargin; + + case FrameSvg::LeftMargin: + return d->frame->fixedLeftMargin; + + case FrameSvg::RightMargin: + return d->frame->fixedRightMargin; + + // KSvg::BottomMargin + default: + return d->frame->fixedBottomMargin; + } +} + +void FrameSvg::getMargins(qreal &left, qreal &top, qreal &right, qreal &bottom) const +{ + if (!d->frame || d->frame->noBorderPadding) { + left = top = right = bottom = 0; + return; + } + + top = d->frame->topMargin; + left = d->frame->leftMargin; + right = d->frame->rightMargin; + bottom = d->frame->bottomMargin; +} + +QMarginsF FrameSvg::margins() const +{ + if (!d->frame || d->frame->noBorderPadding) { + return QMarginsF(); + } else { + return QMarginsF(d->frame->leftMargin, d->frame->topMargin, d->frame->rightMargin, d->frame->bottomMargin); + } +} + +void FrameSvg::getFixedMargins(qreal &left, qreal &top, qreal &right, qreal &bottom) const +{ + if (!d->frame || d->frame->noBorderPadding) { + left = top = right = bottom = 0; + return; + } + + top = d->frame->fixedTopMargin; + left = d->frame->fixedLeftMargin; + right = d->frame->fixedRightMargin; + bottom = d->frame->fixedBottomMargin; +} + +QMarginsF FrameSvg::fixedMargins() const +{ + if (!d->frame || d->frame->noBorderPadding) { + return QMarginsF(); + } else { + return QMarginsF(d->frame->fixedLeftMargin, d->frame->fixedTopMargin, d->frame->fixedRightMargin, d->frame->fixedBottomMargin); + } +} + +void FrameSvg::getInset(qreal &left, qreal &top, qreal &right, qreal &bottom) const +{ + if (!d->frame || d->frame->noBorderPadding) { + left = top = right = bottom = 0; + return; + } + + top = d->frame->insetTopMargin; + left = d->frame->insetLeftMargin; + right = d->frame->insetRightMargin; + bottom = d->frame->insetBottomMargin; +} + +QMarginsF FrameSvg::insets() const +{ + if (!d->frame || d->frame->noBorderPadding) { + return QMarginsF(); + } else { + return QMarginsF(d->frame->insetLeftMargin, d->frame->insetTopMargin, d->frame->insetRightMargin, d->frame->insetBottomMargin); + } +} + +QRectF FrameSvg::contentsRect() const +{ + if (d->frame) { + QRectF rect(QPoint(0, 0), d->frame->frameSize); + return rect.adjusted(d->frame->leftMargin, d->frame->topMargin, -d->frame->rightMargin, -d->frame->bottomMargin); + } else { + return QRectF(); + } +} + +QPixmap FrameSvg::alphaMask() const +{ + // FIXME: the distinction between overlay and + return d->alphaMask(); +} + +QRegion FrameSvg::mask() const +{ + QRegion result; + if (!d->frame) { + return result; + } + + size_t id = qHash(d->cacheId(d->frame.data(), QString()), SvgRectsCache::s_seed); + + QRegion *obj = d->frame->cachedMasks.object(id); + + if (!obj) { + QPixmap alphaMask = d->alphaMask(); + const qreal dpr = alphaMask.devicePixelRatio(); + + // region should always be in logical pixels, resize pixmap to be in the logical sizes + if (alphaMask.devicePixelRatio() != 1.0) { + alphaMask = alphaMask.scaled(alphaMask.width() / dpr, alphaMask.height() / dpr); + } + + // mask() of a QPixmap without alpha Channel will be null + // but if our mask has no lpha at all, we want instead consider the entire area as the mask + if (alphaMask.hasAlphaChannel()) { + obj = new QRegion(QBitmap(alphaMask.mask())); + } else { + obj = new QRegion(alphaMask.rect()); + } + + result = *obj; + d->frame->cachedMasks.insert(id, obj); + } else { + result = *obj; + } + return result; +} + +void FrameSvg::setCacheAllRenderedFrames(bool cache) +{ + if (d->cacheAll && !cache) { + clearCache(); + } + + d->cacheAll = cache; +} + +bool FrameSvg::cacheAllRenderedFrames() const +{ + return d->cacheAll; +} + +void FrameSvg::clearCache() +{ + if (d->frame) { + d->frame->cachedBackground = QPixmap(); + d->frame->cachedMasks.clear(); + } + if (d->maskFrame) { + d->maskFrame->cachedBackground = QPixmap(); + d->maskFrame->cachedMasks.clear(); + } +} + +QPixmap FrameSvg::framePixmap() +{ + if (d->frame->cachedBackground.isNull()) { + d->generateBackground(d->frame); + } + + return d->frame->cachedBackground; +} + +void FrameSvg::paintFrame(QPainter *painter, const QRectF &target, const QRectF &source) +{ + if (d->frame->cachedBackground.isNull()) { + d->generateBackground(d->frame); + if (d->frame->cachedBackground.isNull()) { + return; + } + } + + painter->drawPixmap(target, d->frame->cachedBackground, source.isValid() ? source : target); +} + +void FrameSvg::paintFrame(QPainter *painter, const QPointF &pos) +{ + if (d->frame->cachedBackground.isNull()) { + d->generateBackground(d->frame); + if (d->frame->cachedBackground.isNull()) { + return; + } + } + + painter->drawPixmap(pos, d->frame->cachedBackground); +} + +int FrameSvg::minimumDrawingHeight() +{ + if (d->frame) { + return d->frame->fixedTopHeight + d->frame->fixedBottomHeight; + } + return 0; +} + +int FrameSvg::minimumDrawingWidth() +{ + if (d->frame) { + return d->frame->fixedRightWidth + d->frame->fixedLeftWidth; + } + return 0; +} + +// #define DEBUG_FRAMESVG_CACHE +FrameSvgPrivate::~FrameSvgPrivate() = default; + +QPixmap FrameSvgPrivate::alphaMask() +{ + QString maskPrefix; + + if (q->hasElement(QLatin1String("mask-") % prefix % QLatin1String("center"))) { + maskPrefix = QStringLiteral("mask-"); + } + + if (maskPrefix.isNull()) { + if (frame->cachedBackground.isNull()) { + generateBackground(frame); + } + return frame->cachedBackground; + } + + // We are setting the prefix only temporary to generate + // the needed mask image + const QString maskRequestedPrefix = requestedPrefix.isEmpty() ? QStringLiteral("mask") : maskPrefix % requestedPrefix; + maskPrefix = maskPrefix % prefix; + + if (!maskFrame) { + maskFrame = lookupOrCreateMaskFrame(frame, maskPrefix, maskRequestedPrefix); + if (!maskFrame->cachedBackground.isNull()) { + return maskFrame->cachedBackground; + } + updateSizes(maskFrame); + generateBackground(maskFrame); + return maskFrame->cachedBackground; + } + + const bool shouldUpdate = (maskFrame->enabledBorders != frame->enabledBorders // + || maskFrame->frameSize != frameSize(frame.data()) // + || maskFrame->imagePath != frame->imagePath); + if (shouldUpdate) { + maskFrame = lookupOrCreateMaskFrame(frame, maskPrefix, maskRequestedPrefix); + if (!maskFrame->cachedBackground.isNull()) { + return maskFrame->cachedBackground; + } + updateSizes(maskFrame); + } + + if (maskFrame->cachedBackground.isNull()) { + generateBackground(maskFrame); + } + + return maskFrame->cachedBackground; +} + +QSharedPointer +FrameSvgPrivate::lookupOrCreateMaskFrame(const QSharedPointer &frame, const QString &maskPrefix, const QString &maskRequestedPrefix) +{ + const size_t key = qHash(cacheId(frame.data(), maskPrefix)); + QSharedPointer mask = s_sharedFrames[q->imageSet()->d].value(key); + + // See if we can find a suitable candidate in the shared frames. + // If there is one, use it. + if (mask) { + return mask; + } + + mask.reset(new FrameData(*frame.data())); + mask->prefix = maskPrefix; + mask->requestedPrefix = maskRequestedPrefix; + mask->imageSet = q->imageSet()->d; + mask->imagePath = frame->imagePath; + mask->enabledBorders = frame->enabledBorders; + mask->frameSize = frameSize(frame).toSize(); + mask->cacheId = key; + mask->lastModified = frame->lastModified; + s_sharedFrames[q->imageSet()->d].insert(key, mask); + + return mask; +} + +void FrameSvgPrivate::generateBackground(const QSharedPointer &frame) +{ + if (!frame->cachedBackground.isNull() || !q->hasElementPrefix(frame->prefix)) { + return; + } + + const size_t id = qHash(cacheId(frame.data(), frame->prefix)); + + bool frameCached = !frame->cachedBackground.isNull(); + bool overlayCached = false; + + const bool overlayAvailable = !frame->prefix.startsWith(QLatin1String("mask-")) && q->hasElement(frame->prefix % QLatin1String("overlay")); + QPixmap overlay; + if (q->isUsingRenderingCache()) { + frameCached = q->imageSet()->d->findInCache(QString::number(id), frame->cachedBackground, frame->lastModified) && !frame->cachedBackground.isNull(); + if (frameCached) { + frame->cachedBackground.setDevicePixelRatio(q->devicePixelRatio()); + } + + if (overlayAvailable) { + const size_t overlayId = qHash(cacheId(frame.data(), frame->prefix % QLatin1String("overlay"))); + overlayCached = q->imageSet()->d->findInCache(QString::number(overlayId), overlay, frame->lastModified) && !overlay.isNull(); + if (overlayCached) { + overlay.setDevicePixelRatio(q->devicePixelRatio()); + } + } + } + + if (!frameCached) { + generateFrameBackground(frame); + } + + // Overlays + QSizeF overlaySize; + QPointF actualOverlayPos = QPointF(0, 0); + if (overlayAvailable && !overlayCached) { + overlaySize = q->elementSize(frame->prefix % QLatin1String("overlay")).toSize(); + + if (q->hasElement(frame->prefix % QLatin1String("hint-overlay-pos-right"))) { + actualOverlayPos.setX(frame->frameSize.width() - overlaySize.width()); + } else if (q->hasElement(frame->prefix % QLatin1String("hint-overlay-pos-bottom"))) { + actualOverlayPos.setY(frame->frameSize.height() - overlaySize.height()); + // Stretched or Tiled? + } else if (q->hasElement(frame->prefix % QLatin1String("hint-overlay-stretch"))) { + overlaySize = frameSize(frame).toSize(); + } else { + if (q->hasElement(frame->prefix % QLatin1String("hint-overlay-tile-horizontal"))) { + overlaySize.setWidth(frameSize(frame).width()); + } + if (q->hasElement(frame->prefix % QLatin1String("hint-overlay-tile-vertical"))) { + overlaySize.setHeight(frameSize(frame).height()); + } + } + + // Only search for alphaMask if we actually have a mask element, because + // alphamask will fallback using the background image itself as mask, which + // can call generateBackground(), leading to an infinite recursion + // see BUG:510157 + if (q->hasElement(QLatin1String("mask-") % prefix % QLatin1String("center"))) { + overlay = alphaMask(); + } else { + overlay = QPixmap(overlaySize.toSize()); + overlay.fill(Qt::white); + } + QPainter overlayPainter(&overlay); + overlayPainter.setCompositionMode(QPainter::CompositionMode_SourceIn); + // Tiling? + if (q->hasElement(frame->prefix % QLatin1String("hint-overlay-tile-horizontal")) + || q->hasElement(frame->prefix % QLatin1String("hint-overlay-tile-vertical"))) { + QSizeF s = q->size().toSize(); + q->resize(q->elementSize(frame->prefix % QLatin1String("overlay"))); + + overlayPainter.drawTiledPixmap(QRectF(QPointF(0, 0), overlaySize), q->pixmap(frame->prefix % QLatin1String("overlay"))); + q->resize(s); + } else { + q->paint(&overlayPainter, QRectF(actualOverlayPos, overlaySize), frame->prefix % QLatin1String("overlay")); + } + + overlayPainter.end(); + } + + if (!frameCached) { + cacheFrame(frame->prefix, frame->cachedBackground, overlayCached ? overlay : QPixmap()); + } + + if (!overlay.isNull()) { + QPainter p(&frame->cachedBackground); + p.setCompositionMode(QPainter::CompositionMode_SourceOver); + p.drawPixmap(actualOverlayPos, overlay, QRectF(actualOverlayPos, overlaySize)); + } +} + +void FrameSvgPrivate::generateFrameBackground(const QSharedPointer &frame) +{ + // qCDebug(LOG_KSVG) << "generating background"; + const QSizeF size = frameSize(frame) * q->devicePixelRatio(); + + if (!size.isValid()) { +#ifndef NDEBUG + // qCDebug(LOG_KSVG) << "Invalid frame size" << size; +#endif + return; + } + if (size.width() >= MAX_FRAME_SIZE || size.height() >= MAX_FRAME_SIZE) { + qCWarning(LOG_KSVG) << "Not generating frame background for a size whose width or height is more than" << MAX_FRAME_SIZE << size; + return; + } + + // Don't cut away pieces of the frame + frame->cachedBackground = QPixmap(QSize(std::ceil(size.width()), std::ceil(size.height()))); + frame->cachedBackground.fill(Qt::transparent); + QPainter p(&frame->cachedBackground); + p.setCompositionMode(QPainter::CompositionMode_Source); + p.setRenderHint(QPainter::SmoothPixmapTransform); + + QRectF contentRect = contentGeometry(frame, size); + paintCenter(p, frame, contentRect, size); + + paintCorner(p, frame, FrameSvg::LeftBorder | FrameSvg::TopBorder, contentRect); + paintCorner(p, frame, FrameSvg::RightBorder | FrameSvg::TopBorder, contentRect); + paintCorner(p, frame, FrameSvg::LeftBorder | FrameSvg::BottomBorder, contentRect); + paintCorner(p, frame, FrameSvg::RightBorder | FrameSvg::BottomBorder, contentRect); + + // Sides + const qreal leftHeight = q->elementSize(frame->prefix % QLatin1String("left")).height(); + paintBorder(p, frame, FrameSvg::LeftBorder, QSizeF(frame->leftWidth, leftHeight) * q->devicePixelRatio(), contentRect); + const qreal rightHeight = q->elementSize(frame->prefix % QLatin1String("right")).height(); + paintBorder(p, frame, FrameSvg::RightBorder, QSizeF(frame->rightWidth, rightHeight) * q->devicePixelRatio(), contentRect); + + const qreal topWidth = q->elementSize(frame->prefix % QLatin1String("top")).width(); + paintBorder(p, frame, FrameSvg::TopBorder, QSizeF(topWidth, frame->topHeight) * q->devicePixelRatio(), contentRect); + const qreal bottomWidth = q->elementSize(frame->prefix % QLatin1String("bottom")).width(); + paintBorder(p, frame, FrameSvg::BottomBorder, QSizeF(bottomWidth, frame->bottomHeight) * q->devicePixelRatio(), contentRect); + p.end(); + + // Set the devicePixelRatio only at the end, drawing all happened in device pixels + frame->cachedBackground.setDevicePixelRatio(q->devicePixelRatio()); +} + +QRectF FrameSvgPrivate::contentGeometry(const QSharedPointer &frame, const QSizeF &size) const +{ + const QSizeF contentSize(size.width() - frame->leftWidth * q->devicePixelRatio() - frame->rightWidth * q->devicePixelRatio(), + size.height() - frame->topHeight * q->devicePixelRatio() - frame->bottomHeight * q->devicePixelRatio()); + QRectF contentRect(QPointF(0, 0), contentSize); + if (frame->enabledBorders & FrameSvg::LeftBorder && q->hasElement(frame->prefix % QLatin1String("left"))) { + contentRect.translate(frame->leftWidth * q->devicePixelRatio(), 0); + } + + // Corners + if (frame->enabledBorders & FrameSvg::TopBorder && q->hasElement(frame->prefix % QLatin1String("top"))) { + contentRect.translate(0, frame->topHeight * q->devicePixelRatio()); + } + return contentRect; +} + +void FrameSvgPrivate::updateFrameData(uint lastModified, UpdateType updateType) +{ + auto fd = frame; + uint newKey = 0; + + if (fd) { + const uint oldKey = fd->cacheId; + + const QString oldPath = fd->imagePath; + const FrameSvg::EnabledBorders oldBorders = fd->enabledBorders; + const QSizeF currentSize = fd->frameSize; + const int oldColorSet = fd->colorSet; + const auto oldColors = fd->colorOverrides; + + fd->enabledBorders = enabledBorders; + fd->frameSize = pendingFrameSize; + fd->imagePath = q->imagePath(); + fd->colorSet = q->colorSet(); + fd->colorOverrides = q->colorOverrides(); + + newKey = qHash(cacheId(fd.data(), prefix)); + + // reset frame to old values + fd->enabledBorders = oldBorders; + fd->frameSize = currentSize; + fd->imagePath = oldPath; + fd->colorSet = oldColorSet; + fd->colorOverrides = oldColors; + + // FIXME: something more efficient than string comparison? + if (oldKey == newKey) { + return; + } + + // qCDebug(LOG_KSVG) << "looking for" << newKey; + auto newFd = FrameSvgPrivate::s_sharedFrames[q->imageSet()->d].value(newKey); + if (newFd) { + // qCDebug(LOG_KSVG) << "FOUND IT!" << newFd->refcount; + // we've found a match, use that one + Q_ASSERT(newKey == newFd.lock()->cacheId); + frame = newFd; + return; + } + + fd.reset(new FrameData(*fd)); + } else { + fd.reset(new FrameData(q, QString())); + } + + frame = fd; + fd->prefix = prefix; + fd->requestedPrefix = requestedPrefix; + // updateSizes(); + fd->enabledBorders = enabledBorders; + fd->frameSize = pendingFrameSize; + fd->imagePath = q->imagePath(); + fd->colorSet = q->colorSet(); + fd->colorOverrides = q->colorOverrides(); + fd->lastModified = lastModified; + // was fd just created empty now? + if (newKey == 0) { + newKey = qHash(cacheId(fd.data(), prefix)); + } + + // we know it isn't in s_sharedFrames due to the check above, so insert it now + FrameSvgPrivate::s_sharedFrames[q->imageSet()->d].insert(newKey, fd); + fd->cacheId = newKey; + fd->imageSet = q->imageSet()->d; + if (updateType == UpdateFrameAndMargins) { + updateAndSignalSizes(); + } else { + updateSizes(frame); + } +} + +void FrameSvgPrivate::paintCenter(QPainter &p, const QSharedPointer &frame, const QRectF &contentRect, const QSizeF &fullSize) +{ + // fullSize and contentRect are in device pixels + if (!contentRect.isEmpty()) { + const QString centerElementId = frame->prefix % QLatin1String("center"); + if (frame->tileCenter) { + QSizeF centerTileSize = q->elementSize(centerElementId); + QPixmap center(centerTileSize.toSize()); + center.fill(Qt::transparent); + + QPainter centerPainter(¢er); + centerPainter.setCompositionMode(QPainter::CompositionMode_Source); + q->paint(¢erPainter, QRectF(QPointF(0, 0), centerTileSize), centerElementId); + + if (frame->composeOverBorder) { + p.drawTiledPixmap(QRectF(QPointF(0, 0), fullSize), center); + } else { + p.drawTiledPixmap(FrameSvgHelpers::sectionRect(FrameSvg::NoBorder, contentRect, fullSize * q->devicePixelRatio()), center); + } + } else { + if (frame->composeOverBorder) { + q->paint(&p, QRectF(QPointF(0, 0), fullSize), centerElementId); + } else { + q->paint(&p, FrameSvgHelpers::sectionRect(FrameSvg::NoBorder, contentRect, fullSize * q->devicePixelRatio()), centerElementId); + } + } + } + + if (frame->composeOverBorder) { + p.setCompositionMode(QPainter::CompositionMode_DestinationIn); + p.drawPixmap(QRectF(QPointF(0, 0), fullSize), alphaMask(), QRectF(QPointF(0, 0), alphaMask().size())); + p.setCompositionMode(QPainter::CompositionMode_SourceOver); + } +} + +void FrameSvgPrivate::paintBorder(QPainter &p, + const QSharedPointer &frame, + const FrameSvg::EnabledBorders borders, + const QSizeF &size, + const QRectF &contentRect) const +{ + // size and contentRect are in device pixels + QString side = frame->prefix % FrameSvgHelpers::borderToElementId(borders); + if (frame->enabledBorders & borders && q->hasElement(side) && !size.isEmpty()) { + if (frame->stretchBorders) { + q->paint(&p, FrameSvgHelpers::sectionRect(borders, contentRect, frame->frameSize * q->devicePixelRatio()), side); + } else { + QSize grownSize(std::ceil(size.width()), std::ceil(size.height())); + QPixmap px(grownSize); + // QPixmap px(QSize(std::ceil(size.width()), std::ceil(size.height()))); + px.fill(Qt::transparent); + + QPainter sidePainter(&px); + sidePainter.setCompositionMode(QPainter::CompositionMode_Source); + // A QRect as we have to exactly fill a QPixmap of integer size, prefer going slightly outside it to not have empty edges in the pixmap to tile + q->paint(&sidePainter, QRect(QPoint(0, 0), grownSize), side); + + // We are composing QPixmaps here, so all objects with integer size + // Rounding the position and ceiling the size is the way that gives better tiled results + auto r = FrameSvgHelpers::sectionRect(borders, contentRect, frame->frameSize * q->devicePixelRatio()); + r.setTopLeft(r.topLeft().toPoint()); + r.setSize(QSizeF(std::ceil(r.size().width()), std::ceil(r.size().height()))); + + p.drawTiledPixmap(r, px); + } + } +} + +void FrameSvgPrivate::paintCorner(QPainter &p, const QSharedPointer &frame, KSvg::FrameSvg::EnabledBorders border, const QRectF &contentRect) const +{ + // contentRect is in device pixels + // Draw the corner only if both borders in both directions are enabled. + if ((frame->enabledBorders & border) != border) { + return; + } + const QString corner = frame->prefix % FrameSvgHelpers::borderToElementId(border); + if (q->hasElement(corner)) { + auto r = FrameSvgHelpers::sectionRect(border, contentRect, frame->frameSize * q->devicePixelRatio()); + // We are composing QPixmaps here, so all objects with integer size + // Rounding the position and ceiling the size is the way that gives better tiled results + r.setTopLeft(r.topLeft().toPoint()); + r.setSize(QSizeF(std::ceil(r.size().width()), std::ceil(r.size().height()))); + q->paint(&p, r.toRect(), corner); + } +} + +SvgPrivate::CacheId FrameSvgPrivate::cacheId(FrameData *frame, const QString &prefixToSave) const +{ + std::vector parts; + const auto colors = frame->colorOverrides.values(); + for (const QColor &c : std::as_const(colors)) { + parts.push_back(::qHash(c.red())); + parts.push_back(::qHash(c.green())); + parts.push_back(::qHash(c.blue())); + parts.push_back(::qHash(c.alpha())); + } + const size_t colorsHash = qHashRange(parts.begin(), parts.end(), SvgRectsCache::s_seed); + + const QSize size = frameSize(frame).toSize(); + return SvgPrivate::CacheId{double(size.width()), + double(size.height()), + frame->imagePath, + prefixToSave, + q->status(), + q->devicePixelRatio(), + frame->colorSet, + colorsHash, + (uint)frame->enabledBorders, + q->Svg::d->lastModified}; +} + +void FrameSvgPrivate::cacheFrame(const QString &prefixToSave, const QPixmap &background, const QPixmap &overlay) +{ + if (!q->isUsingRenderingCache()) { + return; + } + + // insert background + if (!frame) { + return; + } + + const size_t id = qHash(cacheId(frame.data(), prefixToSave)); + + // qCDebug(LOG_KSVG)<<"Saving to cache frame"<imageSet()->d->insertIntoCache(QString::number(id), background, QString::number((qint64)q, 16) % prefixToSave); + + if (!overlay.isNull()) { + // insert overlay + const size_t overlayId = qHash(cacheId(frame.data(), frame->prefix % QLatin1String("overlay"))); + q->imageSet()->d->insertIntoCache(QString::number(overlayId), overlay, QString::number((qint64)q, 16) % prefixToSave % QLatin1String("overlay")); + } +} + +void FrameSvgPrivate::updateSizes(FrameData *frame) const +{ + // qCDebug(LOG_KSVG) << "!!!!!!!!!!!!!!!!!!!!!! updating sizes" << prefix; + Q_ASSERT(frame); + + QSizeF s = q->size(); + q->resize(); + if (!frame->cachedBackground.isNull()) { + frame->cachedBackground = QPixmap(); + } + + // This function needs to do a lot of string creation, since we have four + // sides with matching margins and insets. Rather than creating a new string + // every time for these, create a single buffer that can contain a full + // element name and pass that around using views, so we save a lot of + // allocations. + QString nameBuffer; + const auto offset = frame->prefix.length(); + nameBuffer.reserve(offset + 30); + nameBuffer.append(frame->prefix); + + // This uses UTF16 literals to avoid having to create QLatin1String and then + // converting that to a QString temporary for the replace operation. + // Additionally, we use a template parameter to provide us the compile-time + // length of the literal so we don't need to calculate that. + auto createName = [&nameBuffer, offset](const char16_t (&name)[length]) { + nameBuffer.replace(offset, length - 1, reinterpret_cast(name), length); + return QStringView(nameBuffer).mid(0, offset + length - 1); + }; + + // This has the same size regardless the border is enabled or not + frame->fixedTopHeight = q->elementSize(createName(u"top")).height(); + + if (auto topMargin = q->elementRect(createName(u"hint-top-margin")); topMargin.isValid()) { + frame->fixedTopMargin = topMargin.height(); + } else { + frame->fixedTopMargin = frame->fixedTopHeight; + } + + // The same, but its size depends from the margin being enabled + if (frame->enabledBorders & FrameSvg::TopBorder) { + frame->topMargin = frame->fixedTopMargin; + frame->topHeight = frame->fixedTopHeight; + } else { + frame->topMargin = frame->topHeight = 0; + } + + if (auto topInset = q->elementRect(createName(u"hint-top-inset")); topInset.isValid()) { + frame->insetTopMargin = topInset.height(); + } else { + frame->insetTopMargin = -1; + } + + frame->fixedLeftWidth = q->elementSize(createName(u"left")).width(); + + if (auto leftMargin = q->elementRect(createName(u"hint-left-margin")); leftMargin.isValid()) { + frame->fixedLeftMargin = leftMargin.width(); + } else { + frame->fixedLeftMargin = frame->fixedLeftWidth; + } + + if (frame->enabledBorders & FrameSvg::LeftBorder) { + frame->leftMargin = frame->fixedLeftMargin; + frame->leftWidth = frame->fixedLeftWidth; + } else { + frame->leftMargin = frame->leftWidth = 0; + } + + if (auto leftInset = q->elementRect(createName(u"hint-left-inset")); leftInset.isValid()) { + frame->insetLeftMargin = leftInset.width(); + } else { + frame->insetLeftMargin = -1; + } + + frame->fixedRightWidth = q->elementSize(createName(u"right")).width(); + + if (auto rightMargin = q->elementRect(createName(u"hint-right-margin")); rightMargin.isValid()) { + frame->fixedRightMargin = rightMargin.width(); + } else { + frame->fixedRightMargin = frame->fixedRightWidth; + } + + if (frame->enabledBorders & FrameSvg::RightBorder) { + frame->rightMargin = frame->fixedRightMargin; + frame->rightWidth = frame->fixedRightWidth; + } else { + frame->rightMargin = frame->rightWidth = 0; + } + + if (auto rightInset = q->elementRect(createName(u"hint-right-inset")); rightInset.isValid()) { + frame->insetRightMargin = rightInset.width(); + } else { + frame->insetRightMargin = -1; + } + + frame->fixedBottomHeight = q->elementSize(createName(u"bottom")).height(); + + if (auto bottomMargin = q->elementRect(createName(u"hint-bottom-margin")); bottomMargin.isValid()) { + frame->fixedBottomMargin = bottomMargin.height(); + } else { + frame->fixedBottomMargin = frame->fixedBottomHeight; + } + + if (frame->enabledBorders & FrameSvg::BottomBorder) { + frame->bottomMargin = frame->fixedBottomMargin; + frame->bottomHeight = frame->fixedBottomHeight; + } else { + frame->bottomMargin = frame->bottomHeight = 0; + } + + if (auto bottomInset = q->elementRect(createName(u"hint-bottom-inset")); bottomInset.isValid()) { + frame->insetBottomMargin = bottomInset.height(); + } else { + frame->insetBottomMargin = -1; + } + + static const QString maskPrefix = QStringLiteral("mask-"); + static const QString hintTileCenter = QStringLiteral("hint-tile-center"); + static const QString hintNoBorderPadding = QStringLiteral("hint-no-border-padding"); + static const QString hintStretchBorders = QStringLiteral("hint-stretch-borders"); + + frame->composeOverBorder = (q->hasElement(createName(u"hint-compose-over-border")) && q->hasElement(maskPrefix % createName(u"center"))); + + // since it's rectangular, topWidth and bottomWidth must be the same + // the ones that don't have a frame->prefix is for retrocompatibility + frame->tileCenter = (q->hasElement(hintTileCenter) || q->hasElement(createName(u"hint-tile-center"))); + frame->noBorderPadding = (q->hasElement(hintNoBorderPadding) || q->hasElement(createName(u"hint-no-border-padding"))); + frame->stretchBorders = (q->hasElement(hintStretchBorders) || q->hasElement(createName(u"hint-stretch-borders"))); + q->resize(s); +} + +void FrameSvgPrivate::updateNeeded() +{ + q->setElementPrefix(requestedPrefix); + // frame not created yet? + if (!frame) { + return; + } + q->clearCache(); + updateSizes(frame); +} + +void FrameSvgPrivate::updateAndSignalSizes() +{ + // frame not created yet? + if (!frame) { + return; + } + updateSizes(frame); + Q_EMIT q->repaintNeeded(); +} + +QSizeF FrameSvgPrivate::frameSize(FrameData *frame) const +{ + if (!frame) { + return QSizeF(); + } + + if (!frame->frameSize.isValid()) { + updateSizes(frame); + frame->frameSize = q->size().toSize(); + } + + return frame->frameSize; +} + +QString FrameSvg::actualPrefix() const +{ + return d->prefix; +} + +bool FrameSvg::isRepaintBlocked() const +{ + return d->repaintBlocked; +} + +void FrameSvg::setRepaintBlocked(bool blocked) +{ + d->repaintBlocked = blocked; + + if (!blocked) { + d->updateFrameData(Svg::d->lastModified); + } +} + +void FrameSvg::colorOverridesChange() +{ + if (!d->repaintBlocked) { + d->updateFrameData(Svg::d->lastModified); + } +} + +} // KSvg namespace + +#include "moc_framesvg.cpp" diff --git a/local/recipes/kde/kf6-ksvg/source/src/ksvg/framesvg.h b/local/recipes/kde/kf6-ksvg/source/src/ksvg/framesvg.h new file mode 100644 index 0000000000..15c914f867 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/ksvg/framesvg.h @@ -0,0 +1,512 @@ +/* + SPDX-FileCopyrightText: 2008 Aaron Seigo + SPDX-FileCopyrightText: 2008 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KSVG_FRAMESVG_H +#define KSVG_FRAMESVG_H + +#include +#include + +#include + +#include + +class QPainter; +class QPoint; +class QPointF; +class QRect; +class QRectF; +class QSize; +class QSizeF; +class QMatrix; + +namespace KSvg +{ +class FrameSvgPrivate; + +/*! + * \class KSvg::FrameSvg + * \inheaderfile KSvg/FrameSvg + * \inmodule KSvg + * + * \brief Provides an SVG with borders. + * + * When using SVG images for a background of an object that may change + * its aspect ratio, such as a dialog, simply scaling a single image + * may not be enough. + * + * FrameSvg allows SVGs to provide several elements for borders as well + * as a central element, each of which are scaled individually. These elements + * should be named: + * \list + * \li \c center - the central element, which will be scaled in both directions + * \li \c top - the top border; the height is fixed, but it will be scaled + * horizontally to the same width as \c center + * \li \c bottom - the bottom border; scaled in the same way as \c top + * \li \c left - the left border; the width is fixed, but it will be scaled + * vertically to the same height as \c center + * \li \c right - the right border; scaled in the same way as \c left + * \li \c topleft - fixed size; must be the same height as \c top and the same + * width as \c left + * \li \c bottomleft, \c topright, \c bottomright - similar to \c topleft + * \endlist + * + * \c center must exist, but all the others are optional. \c topleft and + * \c topright will be ignored if \c top does not exist, and similarly for + * \c bottomleft and \c bottomright. + * + * \sa KSvg::Svg + **/ +class KSVG_EXPORT FrameSvg : public Svg +{ + Q_OBJECT + + /*! + * \property KSvg::FrameSvg::enabledBorders + */ + Q_PROPERTY(EnabledBorders enabledBorders READ enabledBorders WRITE setEnabledBorders) + +public: + /*! + * \enum KSvg::FrameSvg::EnabledBorder + * + * \brief This flag enum specifies which borders should be drawn. + * + * \value NoBorder + * \value TopBorder + * \value BottomBorder + * \value LeftBorder + * \value RightBorder + * \value AllBorders + */ + enum EnabledBorder { + NoBorder = 0, + TopBorder = 1, + BottomBorder = 2, + LeftBorder = 4, + RightBorder = 8, + AllBorders = TopBorder | BottomBorder | LeftBorder | RightBorder, + }; + Q_DECLARE_FLAGS(EnabledBorders, EnabledBorder) + Q_FLAG(EnabledBorders) + + // TODO: merge those two? + /*! + * \enum KSvg::FrameSvg::LocationPrefix + * + * \value Floating Free floating. + * \value TopEdge Along the top of the screen. + * \value BottomEdge Along the bottom of the screen. + * \value LeftEdge Along the left side of the screen. + * \value RightEdge Along the right side of the screen. + */ + enum LocationPrefix { + Floating = 0, + TopEdge, + BottomEdge, + LeftEdge, + RightEdge, + }; + Q_ENUM(LocationPrefix) + + /*! + * \enum KSvg::FrameSvg::MarginEdge + * + * \value TopMargin The top margin. + * \value BottomMargin The bottom margin. + * \value LeftMargin The left margin. + * \value RightMargin The right margin. + */ + enum MarginEdge { + TopMargin = 0, + BottomMargin, + LeftMargin, + RightMargin, + }; + Q_ENUM(MarginEdge) + + /*! + * Constructs a new FrameSvg that paints the proper named subelements + * as borders. It may also be used as a regular KSvg::Svg object + * for direct access to elements in the Svg. + * + * \a parent options QObject to parent this to + * + * \sa KSvg::Theme + */ + explicit FrameSvg(QObject *parent = nullptr); + ~FrameSvg() override; + + /*! + * Loads a new Svg + * \a imagePath the new file + */ + Q_INVOKABLE void setImagePath(const QString &path) override; + + /*! + * \brief This method sets which borders should be painted. + * + * \a borders the borders we want to paint + * + * \sa EnabledBorder + */ + void setEnabledBorders(const EnabledBorders borders); + + /*! + * \brief This is a convenience method to get the enabled borders. + * Returns what borders are painted + */ + EnabledBorders enabledBorders() const; + + /*! + * \brief This method resizes the frame, maintaining the same border size. + * + * \a size the new size of the frame + */ + Q_INVOKABLE void resizeFrame(const QSizeF &size); + + /*! + * Returns the size of the frame + */ + Q_INVOKABLE QSizeF frameSize() const; + + /*! + * + * \brief This method returns the margin size for the given edge. + * + * Note that \c 0 will be returned if the given margin is disabled. + * + * The margins specify the spacing between the contents and the SVG bounding rect. + * + * If you don't care about the border being on or off, use + * fixedMarginSize() instead. + * + * \a edge the margin edge we want, top, bottom, left or right + * + * Returns the margin size + */ + Q_INVOKABLE qreal marginSize(const FrameSvg::MarginEdge edge) const; + + /*! + * \brief This is a convenience method that extracts the size of the four + * margins and saves their size into the passed variables. + * + * The margins specify the spacing between the contents and the SVG bounding rect. + * + * If you don't care about the borders being on or off, use + * getFixedMargins() instead. + * + * \a left left margin size + * + * \a top top margin size + * + * \a right right margin size + * + * \a bottom bottom margin size + */ + Q_INVOKABLE void getMargins(qreal &left, qreal &top, qreal &right, qreal &bottom) const; + + /*! + * Returns the margin extents. The margins specify the spacing between the contents and the SVG + * bounding rect. + * + * If you don't care about the borders being on or off, use fixedMargins() instead. + * + * \since 6.21 + */ + QMarginsF margins() const; + + /*! + * \brief This method returns the margin size for the specified edge. + * + * The margins specify the spacing between the contents and the SVG bounding rect. + * + * Compared to marginSize(), this does not depend on whether the border is + * enabled or not. + * + * \a edge the margin edge we want, top, bottom, left or right + * Returns the margin size + */ + Q_INVOKABLE qreal fixedMarginSize(const FrameSvg::MarginEdge edge) const; + + /*! + * Returns the margin extents. The margins specify the spacing between the contents and the SVG + * bounding rect. Compared to margins(), this does not depend on whether a border is enabled or not. + * + * \since 6.21 + */ + QMarginsF fixedMargins() const; + + /*! + * \brief This is a convenience method that extracts the size of the four + * margins and saves their size into the passed variables. + * + * The margins specify the spacing between the contents and the SVG bounding rect. + * + * Compared to getMargins(), this doesn't depend on whether the borders are + * enabled or not. + * + * \a left left margin size + * + * \a top top margin size + * + * \a right right margin size + * + * \a bottom bottom margin size + */ + Q_INVOKABLE void getFixedMargins(qreal &left, qreal &top, qreal &right, qreal &bottom) const; + + /*! + * \brief This method returns the insets margin size for the specified edge. + * + * The insets specify the spacing between the borders and the SVG bounding rect. For example, + * that space may include things such as drop shadows. + * + * \a edge the margin edge we want, top, bottom, left or right + * + * Returns the margin size + * \since 5.77 + */ + Q_INVOKABLE qreal insetSize(const FrameSvg::MarginEdge edge) const; + + /*! + * \brief This is a convenience method that extracts the size of the four + * inset margins and saves their size into the passed variables. + * + * The insets specify the spacing between the borders and the SVG bounding rect. For example, + * that space may include things such as drop shadows. + * + * \a left left margin size + * + * \a top top margin size + * + * \a right right margin size + * + * \a bottom bottom margin size + * \since 5.77 + */ + Q_INVOKABLE void getInset(qreal &left, qreal &top, qreal &right, qreal &bottom) const; + + /*! + * Returns the inset extents. The insets specify the spacing between the borders and the SVG + * bounding rect. For example, that space may include things such as drop shadows. + * + * \since 6.21 + */ + QMarginsF insets() const; + + /*! + * \brief This method returns the rectangle of the center element, taking + * the margins into account. + */ + Q_INVOKABLE QRectF contentsRect() const; + + /*! + * \brief This method sets the prefix to 'north', + * 'south', 'west' and 'east' when the location is TopEdge, BottomEdge, + * LeftEdge and RightEdge, respectively. Clears the prefix in other cases. + * + * The prefix must exist in the SVG document, which means that this can only + * be called successfully after setImagePath is called. + * + * \a location location in the UI this frame will be drawn + * + * \sa setElementPrefix + */ + Q_INVOKABLE void setElementPrefix(KSvg::FrameSvg::LocationPrefix location); + + /*! + * \brief This method sets the prefix for the SVG elements to be used for + * painting. + * + * For example, if prefix is 'active', then instead of using the 'top' + * element of the SVG file to paint the top border, the 'active-top' element + * will be used. The same goes for other SVG elements. + * + * If the elements with prefixes are not present, the default ones are used. + * (for the sake of speed, the test is present only for the 'center' element) + * + * Setting the prefix manually resets the location to Floating. + * + * The prefix must exist in the SVG document, which means that this can only be + * called successfully after setImagePath is called. + * + * \a prefix prefix for the SVG elements that make up the frame + */ + Q_INVOKABLE void setElementPrefix(const QString &prefix); + + /*! + * \brief This method returns whether the SVG has the necessary elements + * with the given prefix to draw a frame. + * + * \a prefix the given prefix we want to check if drawable (can have trailing '-' since 5.59) + */ + Q_INVOKABLE bool hasElementPrefix(const QString &prefix) const; + + /*! + * \brief This is an overloaded method provided for convenience that is + * equivalent to hasElementPrefix("north"), hasElementPrefix("south") + * hasElementPrefix("west") and hasElementPrefix("east"). + * + * Returns true if the svg has the necessary elements with the given prefix + * to draw a frame. + * + * \a location the given prefix we want to check if drawable + */ + Q_INVOKABLE bool hasElementPrefix(KSvg::FrameSvg::LocationPrefix location) const; + + /*! + * \brief This method returns the prefix for SVG elements of the FrameSvg + * (including a '-' at the end if not empty). + * + * Returns the prefix + * \sa actualPrefix() + */ + Q_INVOKABLE QString prefix(); + + /*! + * \brief This method returns a mask that tightly contains the fully opaque + * areas of the SVG. + * + * Returns a region of opaque areas + */ + Q_INVOKABLE QRegion mask() const; + + /*! + * \brief This method returns a pixmap whose alpha channel is the opacity of + * the frame. It may be the frame itself or a special frame with the + * "mask-" prefix. + */ + QPixmap alphaMask() const; + + /*! + * \brief This method sets whether saving all the rendered prefixes in a + * cache or not. + * + * \a cache whether to use the cache. + */ + Q_INVOKABLE void setCacheAllRenderedFrames(bool cache); + + /*! + * \brief This method returns whether all the different prefixes should be + * kept in a cache when rendered. + */ + Q_INVOKABLE bool cacheAllRenderedFrames() const; + + /*! + * \brief This method deletes the internal cache. + * + * Calling this method frees memory. Use this if you want to switch the + * rendered element and you don't plan to switch back to the previous one + * for a long time. + * + * This only works if setUsingRenderingCache(\c true) has been called. + * + * \sa KSvg::Svg::setUsingRenderingCache() + */ + Q_INVOKABLE void clearCache(); + + /*! + * \brief This method returns a pixmap of the SVG represented by this + * object. + * + * Returns a QPixmap of the rendered SVG + */ + Q_INVOKABLE QPixmap framePixmap(); + + /*! + * \brief This method paints the loaded SVG with the elements that + * represents the border. + * + * \a painter the QPainter to use + * + * \a target the target rectangle on the paint device + * + * \a source the portion rectangle of the source image + */ + Q_INVOKABLE void paintFrame(QPainter *painter, const QRectF &target, const QRectF &source = QRectF()); + + /*! + * \brief This method paints the loaded SVG with the elements that + * represents the border. + * + * This is an overloaded member provided for convenience + * + * \a painter the QPainter to use + * + * \a pos where to paint the svg + */ + Q_INVOKABLE void paintFrame(QPainter *painter, const QPointF &pos = QPointF(0, 0)); + + /*! + * \brief This method returns the prefix that is actually being used + * (including a '-' at the end if not empty). + * + * \sa prefix() + */ + QString actualPrefix() const; + + /*! + * \brief This method returns whether we are in a transaction of many + * changes at once. + * + * This is used to restrict rebuilding generated graphics for each change + * made. + * + * \since 5.31 + */ + bool isRepaintBlocked() const; + + /*! + * \brief This method sets whether we should block rebuilding generated + * graphics for each change made. + * + * Setting this to \c true will block rebuilding the generated graphics for + * each change made and will do these changes in blocks instead. + * + * How to use this method: + * When making several changes at once to the frame properties--such as + * prefix, enabled borders, and size--set this property to true to avoid + * regenerating the graphics for each change. Set it to false again after + * applying all required changes. + * + * Note that any change will not be visible in the painted frame while this + * property is set to true. + * \since 5.31 + */ + void setRepaintBlocked(bool blocked); + + /*! + * \brief This method returns the minimum height required to correctly draw + * this SVG. + * + * \since 5.101 + */ + Q_INVOKABLE int minimumDrawingHeight(); + + /*! + * \brief This method returns the minimum width required to correctly draw + * this SVG. + * + * \since 5.101 + */ + Q_INVOKABLE int minimumDrawingWidth(); + +private: + // Never call this from an inline function + void colorOverridesChange(); + + FrameSvgPrivate *const d; + friend class FrameData; + friend class Svg; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(FrameSvg::EnabledBorders) + +} // KSvg namespace + +#endif // multiple inclusion guard diff --git a/local/recipes/kde/kf6-ksvg/source/src/ksvg/imageset.cpp b/local/recipes/kde/kf6-ksvg/source/src/ksvg/imageset.cpp new file mode 100644 index 0000000000..45ee9cb818 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/ksvg/imageset.cpp @@ -0,0 +1,271 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "imageset.h" +#include "private/imageset_p.h" +#include "private/svg_p.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "debug_p.h" + +namespace KSvg +{ +ImageSet::ImageSet(QObject *parent) + : QObject(parent) +{ + if (!ImageSetPrivate::globalImageSet) { + ImageSetPrivate::globalImageSet = new ImageSetPrivate(QString()); + if (QCoreApplication::instance()) { + connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, ImageSetPrivate::globalImageSet, &ImageSetPrivate::onAppExitCleanup); + } + } + ImageSetPrivate::globalImageSet->ref.ref(); + d = ImageSetPrivate::globalImageSet; + + connect(d, &ImageSetPrivate::imageSetChanged, this, &ImageSet::imageSetChanged); +} + +ImageSet::ImageSet(const QString &imageSetName, const QString &basePath, QObject *parent) + : QObject(parent) +{ + auto &priv = ImageSetPrivate::themes[imageSetName]; + if (!priv) { + priv = new ImageSetPrivate(basePath); + if (QCoreApplication::instance()) { + connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, priv, &ImageSetPrivate::onAppExitCleanup); + } + } + + priv->ref.ref(); + d = priv; + + // turn off caching so we don't accidentally trigger unnecessary disk activity at this point + bool useCache = d->cacheImageSet; + d->cacheImageSet = false; + if (!basePath.isEmpty()) { + d->basePath = basePath; + if (!d->basePath.endsWith(u'/')) { + d->basePath += u'/'; + } + } + d->setImageSetName(imageSetName, false); + d->cacheImageSet = useCache; + connect(d, &ImageSetPrivate::imageSetChanged, this, &ImageSet::imageSetChanged); +} + +ImageSet::~ImageSet() +{ + if (d == ImageSetPrivate::globalImageSet) { + if (!d->ref.deref()) { + disconnect(ImageSetPrivate::globalImageSet, nullptr, this, nullptr); + delete ImageSetPrivate::globalImageSet; + ImageSetPrivate::globalImageSet = nullptr; + d = nullptr; + } + } else { + if (!d->ref.deref()) { + ImageSetPrivate::themes.remove(d->imageSetName); + delete d; + } + } +} + +void ImageSet::setBasePath(const QString &basePath) +{ + if (d->basePath == basePath) { + return; + } + + d->basePath = basePath; + if (!d->basePath.endsWith(u'/')) { + d->basePath += u'/'; + } + + // Don't use scheduleImageSetChangeNotification as we want things happening immediately there, + // we don't want in the client code to be setting things like the svg size right after thing just to + // be reset right after in an async fashion + d->discardCache(PixmapCache | SvgElementsCache); + d->cachesToDiscard = NoCache; + + Q_EMIT basePathChanged(basePath); + Q_EMIT imageSetChanged(d->imageSetName); +} + +QString ImageSet::basePath() const +{ + return d->basePath; +} + +void ImageSet::setSelectors(const QStringList &selectors) +{ + d->selectors = selectors; + d->scheduleImageSetChangeNotification(PixmapCache | SvgElementsCache); +} + +QStringList ImageSet::selectors() const +{ + return d->selectors; +} + +void ImageSet::setImageSetName(const QString &imageSetName) +{ + if (d->imageSetName == imageSetName) { + return; + } + + if (d != ImageSetPrivate::globalImageSet) { + const QString basePath = d->basePath; + + disconnect(QCoreApplication::instance(), nullptr, d, nullptr); + if (!d->ref.deref()) { + delete ImageSetPrivate::themes.take(d->imageSetName); + } + + auto &priv = ImageSetPrivate::themes[imageSetName]; + if (!priv) { + priv = new ImageSetPrivate(basePath); + if (QCoreApplication::instance()) { + connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, priv, &ImageSetPrivate::onAppExitCleanup); + } + } + priv->ref.ref(); + d = priv; + connect(d, &ImageSetPrivate::imageSetChanged, this, &ImageSet::imageSetChanged); + } + + d->setImageSetName(imageSetName, true); +} + +QString ImageSet::imageSetName() const +{ + return d->imageSetName; +} + +QString ImageSet::imagePath(const QString &name) const +{ + // look for a compressed svg file in the theme + if (name.contains(QLatin1String("../")) || name.isEmpty()) { + // we don't support relative paths + // qCDebug(LOG_KSVG) << "ImageSet says: bad image path " << name; + return QString(); + } + + const QString svgzName = name % QLatin1String(".svgz"); + QString path = d->findInImageSet(svgzName, d->imageSetName); + + if (path.isEmpty()) { + // try for an uncompressed svg file + const QString svgName = name % QLatin1String(".svg"); + path = d->findInImageSet(svgName, d->imageSetName); + + // search in fallback themes if necessary + for (int i = 0; path.isEmpty() && i < d->fallbackImageSets.count(); ++i) { + if (d->imageSetName == d->fallbackImageSets[i]) { + continue; + } + + // try a compressed svg file in the fallback theme + path = d->findInImageSet(svgzName, d->fallbackImageSets[i]); + + if (path.isEmpty()) { + // try an uncompressed svg file in the fallback theme + path = d->findInImageSet(svgName, d->fallbackImageSets[i]); + } + } + } + + return path; +} + +QString ImageSet::filePath(const QString &name) const +{ + // look for a compressed svg file in the theme + if (name.contains(QLatin1String("../")) || name.isEmpty()) { + // we don't support relative paths + // qCDebug(LOG_KSVG) << "ImageSet says: bad image path " << name; + return QString(); + } + + QString path = d->findInImageSet(name, d->imageSetName); + + if (path.isEmpty()) { + // search in fallback themes if necessary + for (int i = 0; path.isEmpty() && i < d->fallbackImageSets.count(); ++i) { + if (d->imageSetName == d->fallbackImageSets[i]) { + continue; + } + + path = d->findInImageSet(name, d->fallbackImageSets[i]); + } + } + + return path; +} + +bool ImageSet::currentImageSetHasImage(const QString &name) const +{ + if (name.contains(QLatin1String("../"))) { + // we don't support relative paths + return false; + } + + QString path = d->findInImageSet(name % QLatin1String(".svgz"), d->imageSetName); + if (path.isEmpty()) { + path = d->findInImageSet(name % QLatin1String(".svg"), d->imageSetName); + } + return path.contains(d->basePath % d->imageSetName); +} + +#if KSVG_BUILD_DEPRECATED_SINCE(6, 21) +void ImageSet::setUseGlobalSettings(bool useGlobal) +{ + if (d->useGlobal == useGlobal) { + return; + } + + d->useGlobal = useGlobal; + d->cfg = KConfigGroup(); + d->imageSetName.clear(); +} +#endif + +#if KSVG_BUILD_DEPRECATED_SINCE(6, 21) +bool ImageSet::useGlobalSettings() const +{ + return d->useGlobal; +} +#endif + +#if KSVG_BUILD_DEPRECATED_SINCE(6, 21) +void ImageSet::setCacheLimit(int kbytes) +{ + d->cacheSize = kbytes; + delete d->pixmapCache; + d->pixmapCache = nullptr; +} +#endif + +#if KSVG_BUILD_DEPRECATED_SINCE(6, 21) +KPluginMetaData ImageSet::metadata() const +{ + return d->pluginMetaData; +} +#endif +} + +#include "moc_imageset.cpp" diff --git a/local/recipes/kde/kf6-ksvg/source/src/ksvg/imageset.h b/local/recipes/kde/kf6-ksvg/source/src/ksvg/imageset.h new file mode 100644 index 0000000000..820aafc0d3 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/ksvg/imageset.h @@ -0,0 +1,245 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Aaron Seigo + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KSVG_IMAGESET_H +#define KSVG_IMAGESET_H + +#include +#include + +#include +#include + +class KPluginMetaData; + +namespace KSvg +{ +class ImageSetPrivate; +class SvgPrivate; + +// TODO: move in the plasma part the watching and regeneration of icon themes + +/*! + * \class KSvg::ImageSet + * \inheaderfile KSvg/ImageSet + * \inmodule KSvg + * + * \brief Interface to the Svg image set. + * + * KSvg::ImageSet provides access to a common and standardized set of graphic + * elements stored in SVG format. This allows artists to create single packages + * of SVGs that will affect the look and feel of all workspace components. + * + * KSvg::Svg uses KSvg::ImageSet internally to locate and load the appropriate + * SVG data. Alternatively, KSvg::ImageSet can be used directly to retrieve + * file system paths to SVGs by name. + */ +class KSVG_EXPORT ImageSet : public QObject +{ + Q_OBJECT + + /*! + * \property KSvg::ImageSet::imageSetName + */ + Q_PROPERTY(QString imageSetName READ imageSetName WRITE setImageSetName NOTIFY imageSetChanged) + + /*! + * \property KSvg::ImageSet::basePath + */ + Q_PROPERTY(QString basePath READ basePath WRITE setBasePath NOTIFY imageSetChanged) + +#if KSVG_ENABLE_DEPRECATED_SINCE(6, 21) + /*! + * \property KSvg::ImageSet::useGlobalSettings + * \deprecated[6.21] Not used. + */ + Q_PROPERTY(bool useGlobalSettings READ useGlobalSettings NOTIFY imageSetChanged) +#endif + +public: + /*! + * Default constructor. + * + * \a parent the parent object + */ + explicit ImageSet(QObject *parent = nullptr); + + /*! + * \brief This method constructs a theme which will be a custom theme + * instance of imageSetName. + * + * \a imageSetName the name of the theme to create + * \a basePath base path for the theme to look for svgs. if empty, the default will be used. + * \a parent the parent object + */ + explicit ImageSet(const QString &imageSetName, const QString &basePath = {}, QObject *parent = nullptr); + + ~ImageSet() override; + + /*! + * \brief This method sets a base path for the theme to look for SVGs. + * + * It can be a path relative to QStandardPaths::GenericDataLocation or an + * absolute path. + * + * \a basePath the base path + */ + void setBasePath(const QString &basePath); + + /*! + * \brief This method returns the base path of the theme where the SVGs will + * be looked for. + */ + QString basePath() const; + + /*! + * \brief This method sets the file selectors. + * + * The theme can have different svgs with the same name for different + * situations and platforms. The Plasma desktop for instance uses "opaque" + * or "translucent" based on presence of compositing and KWin blur effects. + * Other uses may be platform, like android-specific graphics. + * + * \a selectors selectors in order of preference + */ + void setSelectors(const QStringList &selectors); + + /*! + * \brief This method returns the current selectors in order of preference. + */ + QStringList selectors() const; + + /*! + * \brief This method sets the current theme. + */ + void setImageSetName(const QString &imageSetName); + + /*! + * \brief This method returns the name of the current theme. + */ + QString imageSetName() const; + + /*! + * \brief This method returns the path for an SVG image in the current + * theme. + * + * \a name the name of the file in the theme directory (without the + * ".svg" part or a leading slash). + * + * Returns the full path to the requested file for the current theme + */ + QString imagePath(const QString &name) const; + + /*! + * \brief This method returns the path for a generic file in the current + * theme. + * + * The theme can also ship any generic file, such as configuration files. + * + * \a name the name of the file in the theme directory (without a + * leading slash) + * + * Returns the full path to the requested file for the current theme + */ + QString filePath(const QString &name) const; + + /*! + * \brief This method checks whether this theme contains an image with the + * given name. + * + * \a name the name of the file in the theme directory (without the + * ".svg" part or a leading slash) + * + * Returns true if the image exists for this theme + */ + bool currentImageSetHasImage(const QString &name) const; + +#if KSVG_ENABLE_DEPRECATED_SINCE(6, 21) + /*! + * \brief This method sets whether the theme should follow the global + * settings or use application-specific settings. + * + * \a useGlobal pass in true to follow the global settings + * + * \deprecated[6.20] Not used + */ + KSVG_DEPRECATED_VERSION(6, 21, "Not used") + void setUseGlobalSettings(bool useGlobal); +#endif + +#if KSVG_ENABLE_DEPRECATED_SINCE(6, 21) + /*! + * \brief This method returns whether the global settings are followed. + * + * If application-specific settings are being used, it returns \c false. + * + * \deprecated[6.21] Not used + */ + KSVG_DEPRECATED_VERSION(6, 21, "Not used") + bool useGlobalSettings() const; +#endif + +#if KSVG_ENABLE_DEPRECATED_SINCE(6, 21) + /*! + * \brief This method sets the maximum size of the cache (in kilobytes). + * + * If cache gets bigger than the limit, then some entries are removed. + * Setting cache limit to 0 disables automatic cache size limiting. + * + * Note that the cleanup might not be done immediately, so the cache might + * temporarily (for a few seconds) grow bigger than the limit. + * + * \deprecated[6.21] Not used + **/ + KSVG_DEPRECATED_VERSION(6, 21, "Not used") + void setCacheLimit(int kbytes); +#endif + +#if KSVG_ENABLE_DEPRECATED_SINCE(6, 21) + /*! + * \brief This method returns the plugin metadata for this theme. + * + * Metadata contains information such as name, description, author, website, + * and url. + * + * \deprecated[6.21] Not used + */ + KSVG_DEPRECATED_VERSION(6, 21, "Not used") + KPluginMetaData metadata() const; +#endif + +Q_SIGNALS: + /*! + * \brief This signal is emitted when the user makes changes to the theme. + * + * Rendered images, colors, etc. should be updated at this point. However, + * SVGs should *not* be repainted in response to this signal; connect to + * Svg::repaintNeeded() instead for that, as SVG objects need repainting not + * only when imageSetChanged() is emitted; moreover SVG objects connect to + * and respond appropriately to imageSetChanged() internally, emitting + * Svg::repaintNeeded() at an appropriate time. + */ + void imageSetChanged(const QString &imageSetName); + + /*! + * \brief This signal is emitted when the user changes the base path of the + * image set. + */ + void basePathChanged(const QString &basePath); + +private: + friend class SvgPrivate; + friend class Svg; + friend class FrameSvg; + friend class FrameSvgPrivate; + friend class ImageSetPrivate; + ImageSetPrivate *d; +}; + +} // KSvg namespace + +#endif // multiple inclusion guard diff --git a/local/recipes/kde/kf6-ksvg/source/src/ksvg/ksvg-index.qdoc b/local/recipes/kde/kf6-ksvg/source/src/ksvg/ksvg-index.qdoc new file mode 100644 index 0000000000..24910aa6bc --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/ksvg/ksvg-index.qdoc @@ -0,0 +1,21 @@ +/*! + \page ksvg-index.html + \title KSvg + + A library for rendering SVG-based themes with stylesheet re-coloring and on-disk caching. + + \section1 Using the Module + + \include {module-use.qdocinc} {using the c++ api} + + \section2 Building with CMake + + \include {module-use.qdocinc} {building with cmake} {KF6} {Svg} {KF6::Svg} + + \section1 API Reference + + \list + \li \l{KSvg C++ Classes} + \li \l{KSvg QML Types} + \endlist +*/ diff --git a/local/recipes/kde/kf6-ksvg/source/src/ksvg/ksvg.qdoc b/local/recipes/kde/kf6-ksvg/source/src/ksvg/ksvg.qdoc new file mode 100644 index 0000000000..82c2a630ec --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/ksvg/ksvg.qdoc @@ -0,0 +1,9 @@ +/*! + \module KSvg + \title KSvg C++ Classes + \ingroup modules + \cmakepackage KF6 + \cmakecomponent Svg + + \brief A library for rendering SVG-based themes with stylesheet re-coloring and on-disk caching. +*/ diff --git a/local/recipes/kde/kf6-ksvg/source/src/ksvg/ksvg.qdocconf b/local/recipes/kde/kf6-ksvg/source/src/ksvg/ksvg.qdocconf new file mode 100644 index 0000000000..3896eb61ba --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/ksvg/ksvg.qdocconf @@ -0,0 +1,35 @@ +include($KDE_DOCS/global/qt-module-defaults.qdocconf) + +project = KSvg +description = A library for rendering SVG-based themes with stylesheet re-coloring and on-disk caching. + +documentationinheaders = true + +headerdirs += . +sourcedirs += . + +outputformats = HTML + +navigation.landingpage = "KSvg" + +depends += \ + kde \ + qtcore \ + qtgui \ + ksvgqml + +qhp.projects = KSvg + +qhp.KSvg.file = ksvg.qhp +qhp.KSvg.namespace = org.kde.ksvg.$QT_VERSION_TAG +qhp.KSvg.virtualFolder = ksvg +qhp.KSvg.indexTitle = KSvg +qhp.KSvg.indexRoot = + +qhp.KSvg.subprojects = classes +qhp.KSvg.subprojects.classes.title = C++ Classes +qhp.KSvg.subprojects.classes.indexTitle = KSvg C++ Classes +qhp.KSvg.subprojects.classes.selectors = class fake:headerfile +qhp.KSvg.subprojects.classes.sortPages = true + +tagfile = ksvg.tags diff --git a/local/recipes/kde/kf6-ksvg/source/src/ksvg/private/framesvg_helpers.h b/local/recipes/kde/kf6-ksvg/source/src/ksvg/private/framesvg_helpers.h new file mode 100644 index 0000000000..f8e536ef6c --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/ksvg/private/framesvg_helpers.h @@ -0,0 +1,83 @@ +/* + SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KSVG_FRAMESVG_HELPERS_H +#define KSVG_FRAMESVG_HELPERS_H + +#include "framesvg.h" + +namespace KSvg +{ +namespace FrameSvgHelpers +{ +/* + * Returns the element id name for said borders + */ +QString borderToElementId(FrameSvg::EnabledBorders borders) +{ + if (borders == FrameSvg::NoBorder) { + return QStringLiteral("center"); + } else if (borders == FrameSvg::TopBorder) { + return QStringLiteral("top"); + } else if (borders == FrameSvg::BottomBorder) { + return QStringLiteral("bottom"); + } else if (borders == FrameSvg::LeftBorder) { + return QStringLiteral("left"); + } else if (borders == FrameSvg::RightBorder) { + return QStringLiteral("right"); + } else if (borders == (FrameSvg::TopBorder | FrameSvg::LeftBorder)) { + return QStringLiteral("topleft"); + } else if (borders == (FrameSvg::TopBorder | FrameSvg::RightBorder)) { + return QStringLiteral("topright"); + } else if (borders == (FrameSvg::BottomBorder | FrameSvg::LeftBorder)) { + return QStringLiteral("bottomleft"); + } else if (borders == (FrameSvg::BottomBorder | FrameSvg::RightBorder)) { + return QStringLiteral("bottomright"); + } else { + qWarning() << "unrecognized border" << borders; + } + return QString(); +} + +/* + * Returns the suggested geometry for the borders given a fullSize frame size and a contentRect + */ +QRectF sectionRect(KSvg::FrameSvg::EnabledBorders borders, const QRectF &contentRect, const QSizeF &fullSize) +{ + // don't use QRect corner methods here, they have semantics that might come as unexpected. + // prefer constructing the points explicitly. e.g. from QRect::topRight docs: + // Note that for historical reasons this function returns QPoint(left() + width() -1, top()). + + if (borders == FrameSvg::NoBorder) { + return contentRect; + } else if (borders == FrameSvg::TopBorder) { + return QRectF(QPointF(contentRect.left(), 0), QSizeF(contentRect.width(), contentRect.top())); + } else if (borders == FrameSvg::BottomBorder) { + return QRectF(QPointF(contentRect.left(), contentRect.bottom()), QSizeF(contentRect.width(), fullSize.height() - contentRect.bottom())); + } else if (borders == FrameSvg::LeftBorder) { + return QRectF(QPointF(0, contentRect.top()), QSizeF(contentRect.left(), contentRect.height())); + } else if (borders == FrameSvg::RightBorder) { + return QRectF(QPointF(contentRect.right(), contentRect.top()), QSizeF(fullSize.width() - contentRect.right(), contentRect.height())); + } else if (borders == (FrameSvg::TopBorder | FrameSvg::LeftBorder)) { + return QRectF(QPointF(0, 0), QSizeF(contentRect.left(), contentRect.top())); + } else if (borders == (FrameSvg::TopBorder | FrameSvg::RightBorder)) { + return QRectF(QPointF(contentRect.right(), 0), QSizeF(fullSize.width() - contentRect.right(), contentRect.top())); + } else if (borders == (FrameSvg::BottomBorder | FrameSvg::LeftBorder)) { + return QRectF(QPointF(0, contentRect.bottom()), QSizeF(contentRect.left(), fullSize.height() - contentRect.bottom())); + } else if (borders == (FrameSvg::BottomBorder | FrameSvg::RightBorder)) { + return QRectF(QPointF(contentRect.right(), contentRect.bottom()), + QSizeF(fullSize.width() - contentRect.right(), fullSize.height() - contentRect.bottom())); + } else { + qWarning() << "unrecognized border" << borders; + } + return QRectF(); +} + +} + +} + +#endif diff --git a/local/recipes/kde/kf6-ksvg/source/src/ksvg/private/framesvg_p.h b/local/recipes/kde/kf6-ksvg/source/src/ksvg/private/framesvg_p.h new file mode 100644 index 0000000000..6ce057d46d --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/ksvg/private/framesvg_p.h @@ -0,0 +1,204 @@ +/* + SPDX-FileCopyrightText: 2008 Aaron Seigo + SPDX-FileCopyrightText: 2009 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KSVG_FRAMESVG_P_H +#define KSVG_FRAMESVG_P_H + +#include +#include +#include + +#include + +#include + +#include "framesvg.h" +#include "svg_p.h" + +namespace KSvg +{ +class FrameData +{ +public: + FrameData(FrameSvg *svg, const QString &p) + : imagePath(svg->imagePath()) + , prefix(p) + , enabledBorders(FrameSvg::AllBorders) + , frameSize(-1, -1) + , topHeight(0) + , leftWidth(0) + , rightWidth(0) + , bottomHeight(0) + , topMargin(0) + , leftMargin(0) + , rightMargin(0) + , bottomMargin(0) + , noBorderPadding(false) + , stretchBorders(false) + , tileCenter(false) + , composeOverBorder(false) + , imageSet(nullptr) + { + } + + FrameData(const FrameData &other) + : imagePath(other.imagePath) + , prefix(other.prefix) + , enabledBorders(other.enabledBorders) + , cachedMasks(MAX_CACHED_MASKS) + , frameSize(other.frameSize) + , topHeight(0) + , leftWidth(0) + , rightWidth(0) + , bottomHeight(0) + , topMargin(0) + , leftMargin(0) + , rightMargin(0) + , bottomMargin(0) + , noBorderPadding(false) + , stretchBorders(false) + , tileCenter(false) + , composeOverBorder(false) + , imageSet(nullptr) + { + } + + ~FrameData(); + + QString imagePath; + QString prefix; + QString requestedPrefix; + int colorSet = 0; + QMap colorOverrides; + FrameSvg::EnabledBorders enabledBorders; + QPixmap cachedBackground; + QCache cachedMasks; + static const int MAX_CACHED_MASKS = 10; + uint lastModified = 0; + + // Those sizes are in logical pixels + QSizeF frameSize; + uint cacheId; + + // measures + qreal topHeight; + qreal leftWidth; + qreal rightWidth; + qreal bottomHeight; + + // margins, are equal to the measures by default + qreal topMargin; + qreal leftMargin; + qreal rightMargin; + qreal bottomMargin; + + // measures + qreal fixedTopHeight; + qreal fixedLeftWidth; + qreal fixedRightWidth; + qreal fixedBottomHeight; + + // margins, are equal to the measures by default + qreal fixedTopMargin; + qreal fixedLeftMargin; + qreal fixedRightMargin; + qreal fixedBottomMargin; + + // margins, we only have the hqreal for insets + qreal insetTopMargin; + qreal insetLeftMargin; + qreal insetRightMargin; + qreal insetBottomMargin; + + // size of the svg where the size of the "center" + // element is contentWidth x contentHeight + bool noBorderPadding : 1; + bool stretchBorders : 1; + bool tileCenter : 1; + bool composeOverBorder : 1; + + KSvg::ImageSetPrivate *imageSet; +}; + +class FrameSvgPrivate +{ +public: + FrameSvgPrivate(FrameSvg *psvg) + : q(psvg) + , overlayPos(0, 0) + , enabledBorders(FrameSvg::AllBorders) + , cacheAll(false) + , repaintBlocked(false) + { + } + + ~FrameSvgPrivate(); + + QPixmap alphaMask(); + + enum UpdateType { + UpdateFrame, + UpdateFrameAndMargins, + }; + + void generateBackground(const QSharedPointer &frame); + void generateFrameBackground(const QSharedPointer &); + SvgPrivate::CacheId cacheId(FrameData *frame, const QString &prefixToUse) const; + void cacheFrame(const QString &prefixToSave, const QPixmap &background, const QPixmap &overlay); + void updateSizes(FrameData *frame) const; + void updateSizes(const QSharedPointer &frame) const + { + return updateSizes(frame.data()); + } + void updateNeeded(); + void updateAndSignalSizes(); + QSizeF frameSize(const QSharedPointer &frame) const + { + return frameSize(frame.data()); + } + QSizeF frameSize(FrameData *frame) const; + + // paintBorder, paintCorder and paintCenter sizes are in device pixels + void paintBorder(QPainter &p, + const QSharedPointer &frame, + KSvg::FrameSvg::EnabledBorders border, + const QSizeF &originalSize, + const QRectF &output) const; + void paintCorner(QPainter &p, const QSharedPointer &frame, KSvg::FrameSvg::EnabledBorders border, const QRectF &output) const; + void paintCenter(QPainter &p, const QSharedPointer &frame, const QRectF &contentRect, const QSizeF &fullSize); + + QRectF contentGeometry(const QSharedPointer &frame, const QSizeF &size) const; + void updateFrameData(uint lastModified, UpdateType updateType = UpdateFrameAndMargins); + QSharedPointer lookupOrCreateMaskFrame(const QSharedPointer &frame, const QString &maskPrefix, const QString &maskRequestedPrefix); + + FrameSvg::LocationPrefix location = FrameSvg::Floating; + QString prefix; + // sometimes the prefix we requested is not available, so prefix will be empty + // keep track of the requested one anyways, we'll try again when the theme changes + QString requestedPrefix; + + FrameSvg *const q; + + QPointF overlayPos; + + QSharedPointer frame; + QSharedPointer maskFrame; + + // those can differ from frame->enabledBorders if we are in a transition + FrameSvg::EnabledBorders enabledBorders; + // this can differ from frame->frameSize if we are in a transition + QSizeF pendingFrameSize; + + static QHash>> s_sharedFrames; + + bool cacheAll : 1; + bool repaintBlocked : 1; +}; + +} + +#endif diff --git a/local/recipes/kde/kf6-ksvg/source/src/ksvg/private/imageset_p.cpp b/local/recipes/kde/kf6-ksvg/source/src/ksvg/private/imageset_p.cpp new file mode 100644 index 0000000000..b16ce7dd8c --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/ksvg/private/imageset_p.cpp @@ -0,0 +1,741 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Aaron Seigo + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "imageset_p.h" +#include "debug_p.h" +#include "framesvg.h" +#include "framesvg_p.h" +#include "svg_p.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#if defined(Q_OS_LINUX) +#include +#endif + +#define DEFAULT_CACHE_SIZE 16384 // value is from the old kconfigxt default value + +namespace KSvg +{ +const char ImageSetPrivate::defaultImageSet[] = "default"; + +ImageSetPrivate *ImageSetPrivate::globalImageSet = nullptr; +QHash ImageSetPrivate::themes = QHash(); +using QSP = QStandardPaths; + +QString configFileForImageSet(const QString &basePath, const QString &theme) +{ + const QString baseName = basePath % theme; + QString configPath = QSP::locate(QSP::GenericDataLocation, baseName + QLatin1String("/config")); + if (!configPath.isEmpty()) { + return configPath; + } + QString metadataPath = QSP::locate(QSP::GenericDataLocation, baseName + QLatin1String("/metadata.desktop")); + return metadataPath; +} + +#if KSVG_BUILD_DEPRECATED_SINCE(6, 21) +KPluginMetaData metaDataForImageSet(const QString &basePath, const QString &theme) +{ + QString packageBasePath = basePath % theme; + QDir dir; + if (!dir.exists(packageBasePath)) { + packageBasePath = QSP::locate(QSP::GenericDataLocation, basePath % theme, QSP::LocateDirectory); + } + if (packageBasePath.isEmpty()) { + qWarning(LOG_KSVG) << "Could not locate KSvg image set" << theme << "in" << basePath << "using search path" + << QSP::standardLocations(QSP::GenericDataLocation); + return {}; + } + if (QFileInfo::exists(packageBasePath + QLatin1String("/metadata.json"))) { + return KPluginMetaData::fromJsonFile(packageBasePath + QLatin1String("/metadata.json")); + } else if (QFileInfo::exists(packageBasePath + QLatin1String("/metadata.desktop"))) { + QString metadataPath = packageBasePath + QLatin1String("/metadata.desktop"); + KConfigGroup cg(KSharedConfig::openConfig(packageBasePath + QLatin1String("/metadata.desktop"), KConfig::SimpleConfig), + QStringLiteral("Desktop Entry")); + QJsonObject obj = {}; + for (const QString &key : cg.keyList()) { + obj[key] = cg.readEntry(key); + } + qWarning(LOG_KSVG) << "The theme" << theme + << "uses the legacy metadata.desktop. Consider contacting the author and asking them update it to use the newer JSON format."; + return KPluginMetaData(obj, packageBasePath + QLatin1String("/metadata.desktop")); + } else { + qCWarning(LOG_KSVG) << "Could not locate metadata for theme" << theme; + return {}; + } +} +#endif + +ImageSetPrivate::ImageSetPrivate(const QString &_basePath, QObject *parent) + : QObject(parent) + , basePath(_basePath) + , colorScheme(QPalette::Active, KColorScheme::Window, KSharedConfigPtr(nullptr)) + , selectionColorScheme(QPalette::Active, KColorScheme::Selection, KSharedConfigPtr(nullptr)) + , buttonColorScheme(QPalette::Active, KColorScheme::Button, KSharedConfigPtr(nullptr)) + , viewColorScheme(QPalette::Active, KColorScheme::View, KSharedConfigPtr(nullptr)) + , complementaryColorScheme(QPalette::Active, KColorScheme::Complementary, KSharedConfigPtr(nullptr)) + , headerColorScheme(QPalette::Active, KColorScheme::Header, KSharedConfigPtr(nullptr)) + , tooltipColorScheme(QPalette::Active, KColorScheme::Tooltip, KSharedConfigPtr(nullptr)) + , pixmapCache(nullptr) + , cacheSize(DEFAULT_CACHE_SIZE) + , cachesToDiscard(NoCache) +#if KSVG_BUILD_DEPRECATED_SINCE(6, 21) + , useGlobal(true) +#endif + , cacheImageSet(true) +{ + if (basePath.isEmpty()) { + const QString org = QCoreApplication::organizationName(); + if (!org.isEmpty()) { + basePath += u'/' + org; + } + const QString appName = QCoreApplication::applicationName(); + if (!appName.isEmpty()) { + basePath += u'/' + appName; + } + if (basePath.isEmpty()) { + basePath = QStringLiteral("ksvg"); + } + basePath += u"/svgtheme/"; + } + + pixmapSaveTimer = new QTimer(this); + pixmapSaveTimer->setSingleShot(true); + pixmapSaveTimer->setInterval(600); + QObject::connect(pixmapSaveTimer, &QTimer::timeout, this, &ImageSetPrivate::scheduledCacheUpdate); + + updateNotificationTimer = new QTimer(this); + updateNotificationTimer->setSingleShot(true); + updateNotificationTimer->setInterval(100); + QObject::connect(updateNotificationTimer, &QTimer::timeout, this, &ImageSetPrivate::notifyOfChanged); + + QCoreApplication::instance()->installEventFilter(this); + +#if defined(Q_OS_LINUX) + struct sysinfo x; + if (sysinfo(&x) == 0) { + bootTime = QDateTime::currentSecsSinceEpoch() - x.uptime; + qCDebug(LOG_KSVG) << "ImageSetPrivate: Using boot time value" << bootTime; + } else { + // Should never happen, but just in case, fallback to a sane value + bootTime = QDateTime::currentSecsSinceEpoch(); + qCWarning(LOG_KSVG) << "ImageSetPrivate: Failed to get uptime from sysinfo. Using current time as boot time" << bootTime; + } +#endif +} + +ImageSetPrivate::~ImageSetPrivate() +{ + FrameSvgPrivate::s_sharedFrames.remove(this); + delete pixmapCache; +} + +bool ImageSetPrivate::useCache() +{ + bool cachesTooOld = false; + + if (cacheImageSet && !pixmapCache) { + if (cacheSize == 0) { + cacheSize = DEFAULT_CACHE_SIZE; + } + QString cacheFile = QLatin1String("plasma_theme_") + imageSetName; + + // clear any cached values from the previous theme cache + themeVersion.clear(); + + const QString themeMetadataPath = configFileForImageSet(basePath, imageSetName); + const QString cacheFileBase = cacheFile + QLatin1String("*.kcache"); + + QString currentCacheFileName; + if (!themeMetadataPath.isEmpty()) { + // now we record the theme version, if we can + const KPluginMetaData data = metaDataForImageSet(basePath, imageSetName); + if (data.isValid()) { + themeVersion = data.version(); + } + if (!themeVersion.isEmpty()) { + cacheFile += QLatin1String("_v") + themeVersion; + currentCacheFileName = cacheFile + QLatin1String(".kcache"); + } + } + + // now we check for, and remove if necessary, old caches + QDir cacheDir(QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation)); + cacheDir.setNameFilters(QStringList({cacheFileBase})); + + const auto files = cacheDir.entryInfoList(); + for (const QFileInfo &file : files) { + if (currentCacheFileName.isEmpty() // + || !file.absoluteFilePath().endsWith(currentCacheFileName)) { + QFile::remove(file.absoluteFilePath()); + } + } + + // now we do a sanity check: if the metadata.desktop file is newer than the cache, drop the cache + if (!themeMetadataPath.isEmpty()) { + // now we check to see if the theme metadata file itself is newer than the pixmap cache + // this is done before creating the pixmapCache object since that can change the mtime + // on the cache file + + // FIXME: when using the system colors, if they change while the application is not running + // the cache should be dropped; we need a way to detect system color change when the + // application is not running. + // check for expired cache + const QString cacheFilePath = + QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1Char('/') + cacheFile + QLatin1String(".kcache"); + if (!cacheFilePath.isEmpty()) { + const QFileInfo cacheFileInfo(cacheFilePath); + const QFileInfo metadataFileInfo(themeMetadataPath); + + cachesTooOld = (cacheFileInfo.lastModified().toSecsSinceEpoch() < metadataFileInfo.lastModified().toSecsSinceEpoch()); + } + } + + pixmapCache = new KImageCache(cacheFile, cacheSize * 1024); + pixmapCache->setEvictionPolicy(KSharedDataCache::EvictLeastRecentlyUsed); + + if (cachesTooOld) { + discardCache(PixmapCache | SvgElementsCache); + } + } + + return cacheImageSet; +} + +void ImageSetPrivate::onAppExitCleanup() +{ + pixmapsToCache.clear(); + delete pixmapCache; + pixmapCache = nullptr; + cacheImageSet = false; +} + +QString ImageSetPrivate::imagePath(const QString &theme, const QString &type, const QString &image) +{ + QString subdir = basePath % theme % type % image; + + if (QFileInfo::exists(subdir)) { + return subdir; + } else { + return QStandardPaths::locate(QStandardPaths::GenericDataLocation, subdir); + } +} + +QString ImageSetPrivate::findInImageSet(const QString &image, const QString &theme, bool cache) +{ + if (cache) { + auto it = discoveries.constFind(image); + if (it != discoveries.constEnd()) { + return it.value(); + } + } + + QString search; + + // TODO: use also QFileSelector::allSelectors? + // TODO: check if the theme supports selectors starting with + + for (const QString &type : std::as_const(selectors)) { + search = imagePath(theme, QLatin1Char('/') % type % QLatin1Char('/'), image); + if (!search.isEmpty()) { + break; + } + } + + // not found in selectors + if (search.isEmpty()) { + search = imagePath(theme, QStringLiteral("/"), image); + } + + if (cache && !search.isEmpty()) { + discoveries.insert(image, search); + } + + return search; +} + +void ImageSetPrivate::discardCache(CacheTypes caches) +{ + if (caches & PixmapCache) { + pixmapsToCache.clear(); + pixmapSaveTimer->stop(); + if (pixmapCache) { + pixmapCache->clear(); + } + } else { + // This deletes the object but keeps the on-disk cache for later use + delete pixmapCache; + pixmapCache = nullptr; + } + + cachedSvgStyleSheets.clear(); + cachedSelectedSvgStyleSheets.clear(); + cachedInactiveSvgStyleSheets.clear(); + + if (caches & SvgElementsCache) { + discoveries.clear(); + } +} + +void ImageSetPrivate::scheduledCacheUpdate() +{ + if (useCache()) { + QHashIterator it(pixmapsToCache); + while (it.hasNext()) { + it.next(); + pixmapCache->insertPixmap(idsToCache[it.key()], it.value()); + } + } + + pixmapsToCache.clear(); + keysToCache.clear(); + idsToCache.clear(); +} + +void ImageSetPrivate::colorsChanged() +{ + // in the case the theme follows the desktop settings, refetch the colorschemes + // and discard the svg pixmap cache + if (!colors) { + KSharedConfig::openConfig()->reparseConfiguration(); + } + colorScheme = KColorScheme(QPalette::Active, KColorScheme::Window, colors); + buttonColorScheme = KColorScheme(QPalette::Active, KColorScheme::Button, colors); + viewColorScheme = KColorScheme(QPalette::Active, KColorScheme::View, colors); + selectionColorScheme = KColorScheme(QPalette::Active, KColorScheme::Selection, colors); + complementaryColorScheme = KColorScheme(QPalette::Active, KColorScheme::Complementary, colors); + headerColorScheme = KColorScheme(QPalette::Active, KColorScheme::Header, colors); + tooltipColorScheme = KColorScheme(QPalette::Active, KColorScheme::Tooltip, colors); + scheduleImageSetChangeNotification(PixmapCache | SvgElementsCache); +} + +void ImageSetPrivate::scheduleImageSetChangeNotification(CacheTypes caches) +{ + cachesToDiscard |= caches; + updateNotificationTimer->start(); +} + +void ImageSetPrivate::notifyOfChanged() +{ + // qCDebug(LOG_KSVG) << cachesToDiscard; + discardCache(cachesToDiscard); + cachesToDiscard = NoCache; + Q_EMIT imageSetChanged(imageSetName); +} + +QColor ImageSetPrivate::namedColor(Svg::StyleSheetColor colorName, const KSvg::Svg *svg) +{ + const KSvg::Svg::Status status = svg->status(); + KColorScheme *currentScheme = nullptr; + + switch (KColorScheme::ColorSet(svg->colorSet())) { + case KColorScheme::Button: + currentScheme = &buttonColorScheme; + break; + case KColorScheme::View: + currentScheme = &viewColorScheme; + break; + case KColorScheme::Complementary: + currentScheme = &complementaryColorScheme; + break; + case KColorScheme::Header: + currentScheme = &headerColorScheme; + break; + case KColorScheme::Tooltip: + currentScheme = &tooltipColorScheme; + break; + default: + currentScheme = &colorScheme; + } + + switch (colorName) { + case Svg::Text: + switch (status) { + case Svg::Status::Selected: + return selectionColorScheme.foreground(KColorScheme::NormalText).color(); + case Svg::Status::Inactive: + return currentScheme->foreground(KColorScheme::InactiveText).color(); + default: + return currentScheme->foreground(KColorScheme::NormalText).color(); + } + case Svg::Background: + if (status == Svg::Status::Selected) { + return selectionColorScheme.background(KColorScheme::NormalBackground).color(); + } else { + return colorScheme.background(KColorScheme::NormalBackground).color(); + } + case Svg::Highlight: + return selectionColorScheme.background(KColorScheme::NormalBackground).color(); + case Svg::HighlightedText: + return selectionColorScheme.foreground(KColorScheme::NormalText).color(); + case Svg::PositiveText: + return currentScheme->foreground(KColorScheme::PositiveText).color(); + case Svg::NeutralText: + return currentScheme->foreground(KColorScheme::NeutralText).color(); + case Svg::NegativeText: + return currentScheme->foreground(KColorScheme::NegativeText).color(); + + case Svg::ButtonText: + if (status == Svg::Status::Selected) { + return selectionColorScheme.foreground(KColorScheme::NormalText).color(); + } else { + return buttonColorScheme.foreground(KColorScheme::NormalText).color(); + } + case Svg::ButtonBackground: + if (status == Svg::Status::Selected) { + return selectionColorScheme.background(KColorScheme::NormalBackground).color(); + } else { + return buttonColorScheme.background(KColorScheme::NormalBackground).color(); + } + case Svg::ButtonHover: + return buttonColorScheme.decoration(KColorScheme::HoverColor).color(); + case Svg::ButtonFocus: + return buttonColorScheme.decoration(KColorScheme::FocusColor).color(); + case Svg::ButtonHighlightedText: + return selectionColorScheme.foreground(KColorScheme::NormalText).color(); + case Svg::ButtonPositiveText: + return buttonColorScheme.foreground(KColorScheme::PositiveText).color(); + case Svg::ButtonNeutralText: + return buttonColorScheme.foreground(KColorScheme::NeutralText).color(); + case Svg::ButtonNegativeText: + return buttonColorScheme.foreground(KColorScheme::NegativeText).color(); + + case Svg::ViewText: + if (status == Svg::Status::Selected) { + return selectionColorScheme.foreground(KColorScheme::NormalText).color(); + } else { + return viewColorScheme.foreground(KColorScheme::NormalText).color(); + } + case Svg::ViewBackground: + if (status == Svg::Status::Selected) { + return selectionColorScheme.background(KColorScheme::NormalBackground).color(); + } else { + return viewColorScheme.background(KColorScheme::NormalBackground).color(); + } + case Svg::ViewHover: + return viewColorScheme.decoration(KColorScheme::HoverColor).color(); + case Svg::ViewFocus: + return viewColorScheme.decoration(KColorScheme::FocusColor).color(); + case Svg::ViewHighlightedText: + return selectionColorScheme.foreground(KColorScheme::NormalText).color(); + case Svg::ViewPositiveText: + return viewColorScheme.foreground(KColorScheme::PositiveText).color(); + case Svg::ViewNeutralText: + return viewColorScheme.foreground(KColorScheme::NeutralText).color(); + case Svg::ViewNegativeText: + return viewColorScheme.foreground(KColorScheme::NegativeText).color(); + + case Svg::TooltipText: + if (status == Svg::Status::Selected) { + return selectionColorScheme.foreground(KColorScheme::NormalText).color(); + } else { + return tooltipColorScheme.foreground(KColorScheme::NormalText).color(); + } + case Svg::TooltipBackground: + if (status == Svg::Status::Selected) { + return selectionColorScheme.background(KColorScheme::NormalBackground).color(); + } else { + return tooltipColorScheme.background(KColorScheme::NormalBackground).color(); + } + case Svg::TooltipHover: + return tooltipColorScheme.decoration(KColorScheme::HoverColor).color(); + case Svg::TooltipFocus: + return tooltipColorScheme.decoration(KColorScheme::FocusColor).color(); + case Svg::TooltipHighlightedText: + return selectionColorScheme.foreground(KColorScheme::NormalText).color(); + case Svg::TooltipPositiveText: + return tooltipColorScheme.foreground(KColorScheme::PositiveText).color(); + case Svg::TooltipNeutralText: + return tooltipColorScheme.foreground(KColorScheme::NeutralText).color(); + case Svg::TooltipNegativeText: + return tooltipColorScheme.foreground(KColorScheme::NegativeText).color(); + + case Svg::ComplementaryText: + if (status == Svg::Status::Selected) { + return selectionColorScheme.foreground(KColorScheme::NormalText).color(); + } else { + return complementaryColorScheme.foreground(KColorScheme::NormalText).color(); + } + case Svg::ComplementaryBackground: + if (status == Svg::Status::Selected) { + return selectionColorScheme.background(KColorScheme::NormalBackground).color(); + } else { + return complementaryColorScheme.background(KColorScheme::NormalBackground).color(); + } + case Svg::ComplementaryHover: + return complementaryColorScheme.decoration(KColorScheme::HoverColor).color(); + case Svg::ComplementaryFocus: + return complementaryColorScheme.decoration(KColorScheme::FocusColor).color(); + case Svg::ComplementaryHighlightedText: + return selectionColorScheme.foreground(KColorScheme::NormalText).color(); + case Svg::ComplementaryPositiveText: + return complementaryColorScheme.foreground(KColorScheme::PositiveText).color(); + case Svg::ComplementaryNeutralText: + return complementaryColorScheme.foreground(KColorScheme::NeutralText).color(); + case Svg::ComplementaryNegativeText: + return complementaryColorScheme.foreground(KColorScheme::NegativeText).color(); + + case Svg::HeaderText: + if (status == Svg::Status::Selected) { + return selectionColorScheme.foreground(KColorScheme::NormalText).color(); + } else { + return headerColorScheme.foreground(KColorScheme::NormalText).color(); + } + case Svg::HeaderBackground: + if (status == Svg::Status::Selected) { + return selectionColorScheme.background(KColorScheme::NormalBackground).color(); + } else { + return headerColorScheme.background(KColorScheme::NormalBackground).color(); + } + case Svg::HeaderHover: + return headerColorScheme.decoration(KColorScheme::HoverColor).color(); + case Svg::HeaderFocus: + return headerColorScheme.decoration(KColorScheme::FocusColor).color(); + case Svg::HeaderHighlightedText: + return selectionColorScheme.foreground(KColorScheme::NormalText).color(); + case Svg::HeaderPositiveText: + return headerColorScheme.foreground(KColorScheme::PositiveText).color(); + case Svg::HeaderNeutralText: + return headerColorScheme.foreground(KColorScheme::NeutralText).color(); + case Svg::HeaderNegativeText: + return headerColorScheme.foreground(KColorScheme::NegativeText).color(); + case Svg::Frame: + return KColorUtils::mix(currentScheme->background().color(), currentScheme->foreground().color(), KColorScheme::frameContrast()); + default: + return {}; + } +} + +const QString ImageSetPrivate::svgStyleSheet(KSvg::Svg *svg) +{ + const KSvg::Svg::Status status = svg->status(); + const KColorScheme::ColorSet colorSet = KColorScheme::ColorSet(svg->colorSet()); + const bool useCache = svg->d->colorOverrides.isEmpty(); + QString stylesheet; + if (useCache) { + stylesheet = (status == Svg::Status::Selected) + ? cachedSelectedSvgStyleSheets.value(colorSet) + : (status == Svg::Status::Inactive ? cachedInactiveSvgStyleSheets.value(colorSet) : cachedSvgStyleSheets.value(colorSet)); + } + + if (stylesheet.isEmpty()) { + const QList namedColors({Svg::Text, + Svg::Background, + Svg::Highlight, + Svg::HighlightedText, + Svg::PositiveText, + Svg::NeutralText, + Svg::NegativeText, + + Svg::ButtonText, + Svg::ButtonBackground, + Svg::ButtonHover, + Svg::ButtonFocus, + Svg::ButtonHighlightedText, + Svg::ButtonPositiveText, + Svg::ButtonNeutralText, + Svg::ButtonNegativeText, + + Svg::ViewText, + Svg::ViewBackground, + Svg::ViewHover, + Svg::ViewFocus, + Svg::ViewHighlightedText, + Svg::ViewPositiveText, + Svg::ViewNeutralText, + Svg::ViewNegativeText, + + Svg::TooltipText, + Svg::TooltipBackground, + Svg::TooltipHover, + Svg::TooltipFocus, + Svg::TooltipHighlightedText, + Svg::TooltipPositiveText, + Svg::TooltipNeutralText, + Svg::TooltipNegativeText, + + Svg::ComplementaryText, + Svg::ComplementaryBackground, + Svg::ComplementaryHover, + Svg::ComplementaryFocus, + Svg::ComplementaryHighlightedText, + Svg::ComplementaryPositiveText, + Svg::ComplementaryNeutralText, + Svg::ComplementaryNegativeText, + + Svg::HeaderText, + Svg::HeaderBackground, + Svg::HeaderHover, + Svg::HeaderFocus, + Svg::HeaderHighlightedText, + Svg::HeaderPositiveText, + Svg::HeaderNeutralText, + Svg::HeaderNegativeText, + Svg::Frame}); + const QString skel = QStringLiteral(".ColorScheme-%1{color:%2;}"); + const QMetaEnum metaEnum = QMetaEnum::fromType(); + + for (const auto colorName : std::as_const(namedColors)) { + stylesheet += skel.arg(QString::fromUtf8(metaEnum.valueToKey(colorName)), svg->color(colorName).name()); + } + + if (status == Svg::Status::Selected) { + cachedSelectedSvgStyleSheets.insert(colorSet, stylesheet); + } else if (status == Svg::Status::Inactive) { + cachedInactiveSvgStyleSheets.insert(colorSet, stylesheet); + } else { + cachedSvgStyleSheets.insert(colorSet, stylesheet); + } + } + + return stylesheet; +} + +bool ImageSetPrivate::findInCache(const QString &key, QPixmap &pix, unsigned int lastModified) +{ + if (!useCache()) { + return false; + } + + qint64 cacheLastModifiedTime = uint(pixmapCache->lastModifiedTime().toSecsSinceEpoch()); + if (lastModified > cacheLastModifiedTime) { + qCDebug(LOG_KSVG) << "ImageSetPrivate::findInCache: lastModified > cacheLastModifiedTime for" << key; + return false; + } +#if defined(Q_OS_LINUX) + // If the timestamp is the UNIX epoch (0) then we compare against the boot time instead. + // This is notably the case on ostree based systems such as Fedora Kinoite. + if (lastModified == 0 && bootTime > cacheLastModifiedTime) { + qCDebug(LOG_KSVG) << "ImageSetPrivate::findInCache: lastModified == 0 && bootTime > cacheLastModifiedTime for" << key; + return false; + } +#else + if (lastModified == 0) { + qCWarning(LOG_KSVG) << "findInCache with a lastModified timestamp of 0 is deprecated"; + return false; + } +#endif + + // qCDebug(LOG_KSVG) << "ImageSetPrivate::findInCache: using cache for" << key; + const QString id = keysToCache.value(key); + const auto it = pixmapsToCache.constFind(id); + if (it != pixmapsToCache.constEnd()) { + pix = *it; + return !pix.isNull(); + } + + QPixmap temp; + if (pixmapCache->findPixmap(key, &temp) && !temp.isNull()) { + pix = temp; + return true; + } + + return false; +} + +void ImageSetPrivate::insertIntoCache(const QString &key, const QPixmap &pix) +{ + if (useCache()) { + pixmapCache->insertPixmap(key, pix); + } +} + +void ImageSetPrivate::insertIntoCache(const QString &key, const QPixmap &pix, const QString &id) +{ + if (useCache()) { + // Remove old key -> id mapping first + if (auto key = idsToCache.find(id); key != idsToCache.end()) { + keysToCache.remove(*key); + } + pixmapsToCache[id] = pix; + keysToCache[key] = id; + idsToCache[id] = key; + + // always start timer in pixmapSaveTimer's thread + QMetaObject::invokeMethod(pixmapSaveTimer, "start", Qt::QueuedConnection); + } +} + +void ImageSetPrivate::setImageSetName(const QString &tempImageSetName, bool emitChanged) +{ + QString theme = tempImageSetName; + if (theme.isEmpty() || theme == imageSetName) { + // let's try and get the default theme at least + if (imageSetName.isEmpty()) { + theme = QLatin1String(ImageSetPrivate::defaultImageSet); + } else { + return; + } + } + + KPluginMetaData data = metaDataForImageSet(basePath, theme); + if (!data.isValid()) { + data = metaDataForImageSet(basePath, QStringLiteral("default")); + if (!data.isValid()) { + return; + } + + theme = QLatin1String(ImageSetPrivate::defaultImageSet); + } + + // check again as ImageSetPrivate::defaultImageSet might be empty + if (imageSetName == theme) { + return; + } + + imageSetName = theme; + + // load the color scheme config + const QString colorsFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, basePath % theme % QLatin1String("/colors")); + // qCDebug(LOG_KSVG) << "we're going for..." << colorsFile << "*******************"; + + if (colorsFile.isEmpty()) { + colors = nullptr; + } else { + colors = KSharedConfig::openConfig(colorsFile); + } + + colorScheme = KColorScheme(QPalette::Active, KColorScheme::Window, colors); + selectionColorScheme = KColorScheme(QPalette::Active, KColorScheme::Selection, colors); + buttonColorScheme = KColorScheme(QPalette::Active, KColorScheme::Button, colors); + viewColorScheme = KColorScheme(QPalette::Active, KColorScheme::View, colors); + complementaryColorScheme = KColorScheme(QPalette::Active, KColorScheme::Complementary, colors); + headerColorScheme = KColorScheme(QPalette::Active, KColorScheme::Header, colors); + tooltipColorScheme = KColorScheme(QPalette::Active, KColorScheme::Tooltip, colors); + + fallbackImageSets = {QLatin1String(ImageSetPrivate::defaultImageSet)}; + +#if KSVG_BUILD_DEPRECATED_SINCE(6, 21) + pluginMetaData = metaDataForImageSet(basePath, theme); +#endif + + if (emitChanged) { + scheduleImageSetChangeNotification(PixmapCache | SvgElementsCache); + } +} + +bool ImageSetPrivate::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == QCoreApplication::instance()) { + if (event->type() == QEvent::ApplicationPaletteChange) { + colorsChanged(); + } + } + return QObject::eventFilter(watched, event); +} +} + +#include "moc_imageset_p.cpp" diff --git a/local/recipes/kde/kf6-ksvg/source/src/ksvg/private/imageset_p.h b/local/recipes/kde/kf6-ksvg/source/src/ksvg/private/imageset_p.h new file mode 100644 index 0000000000..a8e2c1bb79 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/ksvg/private/imageset_p.h @@ -0,0 +1,174 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Aaron Seigo + SPDX-FileCopyrightText: 2013 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KSVG_IMAGESET_P_H +#define KSVG_IMAGESET_P_H + +#include "imageset.h" +#include "svg.h" +#include + +#include +#include +#include +#include +#include +#include + +#include + +namespace KSvg +{ +class ImageSet; + +enum CacheType { + NoCache = 0, + PixmapCache = 1, + SvgElementsCache = 2, +}; +Q_DECLARE_FLAGS(CacheTypes, CacheType) +Q_DECLARE_OPERATORS_FOR_FLAGS(CacheTypes) + +class ImageSetPrivate : public QObject, public QSharedData +{ + Q_OBJECT + +public: + explicit ImageSetPrivate(const QString &basePath, QObject *parent = nullptr); + ~ImageSetPrivate() override; + + QString imagePath(const QString &theme, const QString &type, const QString &image); + QString findInImageSet(const QString &image, const QString &theme, bool cache = true); + void discardCache(CacheTypes caches); + void scheduleImageSetChangeNotification(CacheTypes caches); + bool useCache(); + void setImageSetName(const QString &themeName, bool emitChanged); + + QColor namedColor(Svg::StyleSheetColor colorName, const KSvg::Svg *svg); + const QString svgStyleSheet(KSvg::Svg *svg); + + /*! + * Check if a pixmap already exists in the cache and compare the last modified + * timestamp of the file with the last modified date of the one in the cache to make sure + * the cache is still valid. + * + * On Linux systems only, if lastModified is not provided or set to 0, then this function + * uses the boot time as a reference instead. This is notably the case on ostree based + * systems such as Fedora Kinoite. + * + * TODO: timestamp shouldn't be user-provided + * + * \param key the name to use in the cache for this image + * \param pix the pixmap object to populate with the resulting data if found + * \param lastModified the timestamp of the file which will be compared with the last + * modified time of the entry in the cache + * + * @note Since KF 5.75, a lastModified value of 0 is deprecated on non-Linux systems. If + * used, it will now always return false. Use a proper file timestamp instead + * so modification can be properly tracked. + * + * Returns true when pixmap was found and loaded from cache, false otherwise + **/ + bool findInCache(const QString &key, QPixmap &pix, unsigned int lastModified); + + /*! + * Insert specified pixmap into the cache. + * If the cache already contains pixmap with the specified key then it is + * overwritten. + * + * \param key the name to use in the cache for this pixmap + * \param pix the pixmap data to store in the cache + **/ + void insertIntoCache(const QString &key, const QPixmap &pix); + + /*! + * Insert specified pixmap into the cache. + * If the cache already contains pixmap with the specified key then it is + * overwritten. + * The actual insert is delayed for optimization reasons and the id + * parameter is used to discard repeated inserts in the delay time, useful + * when for instance the graphics to insert comes from a quickly resizing + * object: the frames between the start and destination sizes aren't + * useful in the cache and just cause overhead. + * + * \param key the name to use in the cache for this pixmap + * \param pix the pixmap data to store in the cache + * \param id a name that identifies the caller class of this function in an unique fashion. + * This is needed to limit disk writes of the cache. + * If an image with the same id changes quickly, + * only the last size where insertIntoCache was called is actually stored on disk + **/ + void insertIntoCache(const QString &key, const QPixmap &pix, const QString &id); + + void colorsChanged(); + +public Q_SLOTS: + void scheduledCacheUpdate(); + void onAppExitCleanup(); + void notifyOfChanged(); + +Q_SIGNALS: + void imageSetChanged(const QString &imageSetName); + +public: + bool eventFilter(QObject *watched, QEvent *event) override; + + static const char defaultImageSet[]; + static const char themeRcFile[]; + + // Ref counting of ImageSetPrivate instances + static ImageSetPrivate *globalImageSet; + static QHash themes; + + QString imageSetName = QStringLiteral("default"); + QString basePath; +#if KSVG_BUILD_DEPRECATED_SINCE(6, 21) + KPluginMetaData pluginMetaData; +#endif + QList fallbackImageSets; + KSharedConfigPtr colors; + KColorScheme colorScheme; + KColorScheme selectionColorScheme; + KColorScheme buttonColorScheme; + KColorScheme viewColorScheme; + KColorScheme complementaryColorScheme; + KColorScheme headerColorScheme; + KColorScheme tooltipColorScheme; + QStringList selectors; + KConfigGroup cfg; + KImageCache *pixmapCache; + QHash pixmapsToCache; + QHash keysToCache; + QHash idsToCache; + QHash cachedSvgStyleSheets; + QHash cachedSelectedSvgStyleSheets; + QHash cachedInactiveSvgStyleSheets; + QHash discoveries; + QTimer *pixmapSaveTimer; + QTimer *updateNotificationTimer; + unsigned cacheSize; + CacheTypes cachesToDiscard; + QString themeVersion; + +#if defined(Q_OS_LINUX) + // Store boot time to be able to compare it to the lastModifiedTime when the timestamp + // of files is the UNIX epoch. + time_t bootTime = 0; +#endif + +#if KSVG_BUILD_DEPRECATED_SINCE(6, 21) + bool useGlobal : 1; +#endif + + bool cacheImageSet : 1; +}; + +} + +#endif + +extern const QString s; diff --git a/local/recipes/kde/kf6-ksvg/source/src/ksvg/private/svg_p.h b/local/recipes/kde/kf6-ksvg/source/src/ksvg/private/svg_p.h new file mode 100644 index 0000000000..3b81fd520d --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/ksvg/private/svg_p.h @@ -0,0 +1,179 @@ +/* + SPDX-FileCopyrightText: 2006-2010 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KSVG_SVG_P_H +#define KSVG_SVG_P_H + +#include "svg.h" + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace KSvg +{ +class SharedSvgRenderer : public QSvgRenderer, public QSharedData +{ + Q_OBJECT +public: + typedef QExplicitlySharedDataPointer Ptr; + + explicit SharedSvgRenderer(QObject *parent = nullptr); + SharedSvgRenderer(const QString &filename, const QString &styleSheet, QHash &interestingElements, QObject *parent = nullptr); + + SharedSvgRenderer(const QByteArray &contents, const QString &styleSheet, QHash &interestingElements, QObject *parent = nullptr); + + void reload(); + +private: + bool load(const QByteArray &contents, const QString &styleSheet, QHash &interestingElements); + + QString m_filename; + QString m_styleSheet; + QHash m_interestingElements; +}; + +class SvgPrivate +{ +public: + struct CacheId { + double width; + double height; + QString filePath; + QString elementName; + int status; + double scaleFactor; + int colorSet; + size_t styleSheet; // TODO: use that + uint extraFlags; // Not used here, used for enabledborders in FrameSvg + uint lastModified; + }; + + SvgPrivate(Svg *svg); + ~SvgPrivate(); + + size_t paletteId(const QPalette &palette, const QColor &positive, const QColor &neutral, const QColor &negative) const; + + // This function is meant for the rects cache + CacheId cacheId(QStringView elementId) const; + + // This function is meant for the pixmap cache + QString cachePath(const QString &path, const QSize &size) const; + + bool setImagePath(const QString &imagePath); + + ImageSet *actualImageSet(); + + QPixmap findInCache(const QString &elementId, qreal ratio, const QSizeF &s = QSizeF()); + + void createRenderer(); + void eraseRenderer(); + + QRectF elementRect(QStringView elementId); + QRectF findAndCacheElementRect(QStringView elementId); + + // Following two are utility functions to snap rendered elements to the pixel grid + // to and from are always 0 <= val <= 1 + qreal closestDistance(qreal to, qreal from); + + QRectF makeUniform(const QRectF &orig, const QRectF &dst); + + // Slots + void imageSetChanged(); + void colorsChanged(); + + static std::shared_mutex s_renderersLock; + static QHash s_renderers; + static QPointer s_systemColorsCache; + + Svg *q; + QPointer theme; + SharedSvgRenderer::Ptr renderer; + QString themePath; + QString path; + QSizeF size; + QSizeF naturalSize; + QChar styleCrc; + // We need colorOverrides.values() to have a stable order + QMap colorOverrides; + QString stylesheetOverride; + KColorScheme::ColorSet colorSet = KColorScheme::Window; + unsigned int lastModified; + qreal devicePixelRatio; + Svg::Status status; + QMetaObject::Connection imageSetChangedConnection; + + bool multipleImages : 1; + bool themed : 1; + bool fromCurrentImageSet : 1; + bool cacheRendering : 1; + bool themeFailed : 1; +}; + +class SvgRectsCache : public QObject +{ + Q_OBJECT +public: + SvgRectsCache(QObject *parent = nullptr); + + static SvgRectsCache *instance(); + + void insert(SvgPrivate::CacheId cacheId, const QRectF &rect, unsigned int lastModified); + void insert(size_t id, const QString &filePath, const QRectF &rect, unsigned int lastModified); + // Those 2 methods are the same, the second uses the integer id produced by hashed CacheId + bool findElementRect(SvgPrivate::CacheId cacheId, QRectF &rect); + bool findElementRect(size_t id, QStringView filePath, QRectF &rect); + + bool loadImageFromCache(const QString &path, uint lastModified); + void dropImageFromCache(const QString &path); + + void setNaturalSize(const QString &path, const QSizeF &size); + QSizeF naturalSize(const QString &path); + + QList sizeHintsForId(const QString &path, const QString &id); + void insertSizeHintForId(const QString &path, const QString &id, const QSizeF &size); + + QString iconThemePath(); + void setIconThemePath(const QString &path); + + QStringList cachedKeysForPath(const QString &path) const; + + unsigned int lastModifiedTimeFromCache(const QString &filePath); + + void updateLastModified(const QString &filePath, unsigned int lastModified); + + static const size_t s_seed; + +Q_SIGNALS: + void lastModifiedChanged(const QString &filePath, unsigned int lastModified); + +private: + QTimer *m_configSyncTimer = nullptr; + QString m_iconThemePath; + KSharedConfigPtr m_svgElementsCache; + /* + * We are indexing in the hash cache ids by their "digested" size_t out of qHash(CacheId) + * because we need to serialize it and unserialize it to a config file, + * which is more efficient to do that with the size_t directly rather than a CacheId struct serialization + */ + QHash m_localRectCache; + QHash> m_invalidElements; + QHash> m_sizeHintsForId; + QHash m_lastModifiedTimes; +}; +} + +size_t qHash(const KSvg::SvgPrivate::CacheId &id, size_t seed = 0); + +#endif diff --git a/local/recipes/kde/kf6-ksvg/source/src/ksvg/svg.cpp b/local/recipes/kde/kf6-ksvg/source/src/ksvg/svg.cpp new file mode 100644 index 0000000000..46c5ac3863 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/ksvg/svg.cpp @@ -0,0 +1,1209 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Aaron Seigo + SPDX-FileCopyrightText: 2008-2010 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "svg.h" +#include "framesvg.h" +#include "private/imageset_p.h" +#include "private/svg_p.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "debug_p.h" +#include "imageset.h" + +size_t qHash(const KSvg::SvgPrivate::CacheId &id, size_t seed) +{ + std::array parts = { + ::qHash(id.width), + ::qHash(id.height), + ::qHash(id.elementName), + ::qHash(id.filePath), + ::qHash(id.status), + ::qHash(id.scaleFactor), + ::qHash(id.colorSet), + ::qHash(id.styleSheet), + ::qHash(id.extraFlags), + ::qHash(id.lastModified), + }; + return qHashRange(parts.begin(), parts.end(), seed); +} + +size_t qHash(const QList &colors, size_t seed) +{ + std::vector parts; + for (const QColor &c : std::as_const(colors)) { + parts.push_back(::qHash(c.red())); + parts.push_back(::qHash(c.green())); + parts.push_back(::qHash(c.blue())); + parts.push_back(::qHash(c.alpha())); + } + return qHashRange(parts.begin(), parts.end(), seed); +} + +namespace KSvg +{ +class SvgRectsCacheSingleton +{ +public: + SvgRectsCache self; +}; + +Q_GLOBAL_STATIC(SvgRectsCacheSingleton, privateSvgRectsCacheSelf) + +const size_t SvgRectsCache::s_seed = 0x9e3779b9; + +SharedSvgRenderer::SharedSvgRenderer(QObject *parent) + : QSvgRenderer(parent) +{ +} + +SharedSvgRenderer::SharedSvgRenderer(const QString &filename, const QString &styleSheet, QHash &interestingElements, QObject *parent) + : QSvgRenderer(parent) +{ + KCompressionDevice file(filename, KCompressionDevice::GZip); + if (!file.open(QIODevice::ReadOnly)) { + return; + } + m_filename = filename; + m_styleSheet = styleSheet; + m_interestingElements = interestingElements; + load(file.readAll(), styleSheet, interestingElements); +} + +SharedSvgRenderer::SharedSvgRenderer(const QByteArray &contents, const QString &styleSheet, QHash &interestingElements, QObject *parent) + : QSvgRenderer(parent) +{ + load(contents, styleSheet, interestingElements); +} + +void SharedSvgRenderer::reload() +{ + KCompressionDevice file(m_filename, KCompressionDevice::GZip); + if (!file.open(QIODevice::ReadOnly)) { + return; + } + + load(file.readAll(), m_styleSheet, m_interestingElements); +} + +bool SharedSvgRenderer::load(const QByteArray &contents, const QString &styleSheet, QHash &interestingElements) +{ + // Apply the style sheet. + if (!styleSheet.isEmpty() && contents.contains("current-color-scheme")) { + QByteArray processedContents; + processedContents.reserve(contents.size()); + QXmlStreamReader reader(contents); + + QBuffer buffer(&processedContents); + buffer.open(QIODevice::WriteOnly); + QXmlStreamWriter writer(&buffer); + bool foundStyleSheet = false; + while (!reader.atEnd()) { + reader.readNext(); + if (!foundStyleSheet // + && reader.tokenType() == QXmlStreamReader::StartElement // + && reader.qualifiedName() == QLatin1String("style") + && reader.attributes().value(QLatin1String("id")) == QLatin1String("current-color-scheme")) { + writer.writeStartElement(QLatin1String("style")); + writer.writeAttributes(reader.attributes()); + writer.writeCharacters(styleSheet); + writer.writeEndElement(); + while (reader.tokenType() != QXmlStreamReader::EndElement) { + reader.readNext(); + } + foundStyleSheet = true; + } else if (reader.tokenType() != QXmlStreamReader::Invalid && !reader.isWhitespace() && !reader.isComment()) { + writer.writeCurrentToken(reader); + } + } + buffer.close(); + if (!QSvgRenderer::load(processedContents)) { + return false; + } + } else if (!QSvgRenderer::load(contents)) { + return false; + } + + // Search the SVG to find and store all ids that contain size hints. + const QString contentsAsString(QString::fromLatin1(contents)); + static const QRegularExpression idExpr(QLatin1String("id\\s*?=\\s*?(['\"])(\\d+?-\\d+?-.*?)\\1")); + Q_ASSERT(idExpr.isValid()); + + auto matchIt = idExpr.globalMatch(contentsAsString); + while (matchIt.hasNext()) { + auto match = matchIt.next(); + QString elementId = match.captured(2); + + QRectF elementRect = boundsOnElement(elementId); + if (elementRect.isValid()) { + interestingElements.insert(elementId, elementRect); + } + } + + return true; +} + +SvgRectsCache::SvgRectsCache(QObject *parent) + : QObject(parent) +{ + const QString svgElementsFile = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1Char('/') + QStringLiteral("ksvg-elements"); + m_svgElementsCache = KSharedConfig::openConfig(svgElementsFile, KConfig::SimpleConfig); + + m_configSyncTimer = new QTimer(this); + m_configSyncTimer->setSingleShot(true); + m_configSyncTimer->setInterval(5000); + connect(m_configSyncTimer, &QTimer::timeout, this, [this]() { + m_svgElementsCache->sync(); + }); +} + +SvgRectsCache *SvgRectsCache::instance() +{ + return &privateSvgRectsCacheSelf()->self; +} + +void SvgRectsCache::insert(KSvg::SvgPrivate::CacheId cacheId, const QRectF &rect, unsigned int lastModified) +{ + insert(qHash(cacheId, SvgRectsCache::s_seed), cacheId.filePath, rect, lastModified); +} + +void SvgRectsCache::insert(size_t id, const QString &filePath, const QRectF &rect, unsigned int lastModified) +{ + const unsigned int savedTime = lastModifiedTimeFromCache(filePath); + + if (savedTime == lastModified && m_localRectCache.contains(id)) { + return; + } + + m_localRectCache.insert(id, rect); + + KConfigGroup imageGroup(m_svgElementsCache, filePath); + + if (rect.isValid()) { + imageGroup.writeEntry(QString::number(id), rect); + } else { + m_invalidElements[filePath] << id; + imageGroup.writeEntry("Invalidelements", m_invalidElements[filePath].values()); + } + + QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start)); + + if (savedTime != lastModified) { + m_lastModifiedTimes[filePath] = lastModified; + imageGroup.writeEntry("LastModified", lastModified); + Q_EMIT lastModifiedChanged(filePath, lastModified); + } +} + +bool SvgRectsCache::findElementRect(KSvg::SvgPrivate::CacheId cacheId, QRectF &rect) +{ + return findElementRect(qHash(cacheId, SvgRectsCache::s_seed), cacheId.filePath, rect); +} + +bool SvgRectsCache::findElementRect(size_t id, QStringView filePath, QRectF &rect) +{ + auto it = m_localRectCache.find(id); + + if (it == m_localRectCache.end()) { + auto elements = m_invalidElements.value(filePath.toString()); + if (elements.contains(id)) { + rect = QRectF(); + return true; + } + return false; + } + + rect = *it; + + return true; +} + +bool SvgRectsCache::loadImageFromCache(const QString &path, uint lastModified) +{ + if (path.isEmpty()) { + return false; + } + + KConfigGroup imageGroup(m_svgElementsCache, path); + + unsigned int savedTime = lastModifiedTimeFromCache(path); + + // Reload even if is older, to support downgrades + if (lastModified != savedTime) { + imageGroup.deleteGroup(); + QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start)); + return false; + } + + auto &elements = m_invalidElements[path]; + if (elements.isEmpty()) { + auto list = imageGroup.readEntry("Invalidelements", QList()); + m_invalidElements[path] = QSet(list.begin(), list.end()); + + for (const auto &key : imageGroup.keyList()) { + bool ok = false; + uint keyUInt = key.toUInt(&ok); + if (ok) { + const QRectF rect = imageGroup.readEntry(key, QRectF()); + m_localRectCache.insert(keyUInt, rect); + } + } + } + return true; +} + +void SvgRectsCache::dropImageFromCache(const QString &path) +{ + KConfigGroup imageGroup(m_svgElementsCache, path); + imageGroup.deleteGroup(); + QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start)); +} + +QList SvgRectsCache::sizeHintsForId(const QString &path, const QString &id) +{ + const QString pathId = path % id; + + auto it = m_sizeHintsForId.constFind(pathId); + if (it == m_sizeHintsForId.constEnd()) { + KConfigGroup imageGroup(m_svgElementsCache, path); + const QStringList &encoded = imageGroup.readEntry(id, QStringList()); + QList sizes; + for (const auto &token : encoded) { + const auto &parts = token.split(QLatin1Char('x')); + if (parts.size() != 2) { + continue; + } + QSize size = QSize(parts[0].toDouble(), parts[1].toDouble()); + if (!size.isEmpty()) { + sizes << size; + } + } + m_sizeHintsForId[pathId] = sizes; + return sizes; + } + + return *it; +} + +void SvgRectsCache::insertSizeHintForId(const QString &path, const QString &id, const QSizeF &size) +{ + // TODO: need to make this more efficient + auto sizeListToString = [](const QList &list) { + QString ret; + for (const auto &s : list) { + ret += QString::number(s.width()) % QLatin1Char('x') % QString::number(s.height()) % QLatin1Char(','); + } + return ret; + }; + m_sizeHintsForId[path % id].append(size); + KConfigGroup imageGroup(m_svgElementsCache, path); + imageGroup.writeEntry(id, sizeListToString(m_sizeHintsForId[path % id])); + QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start)); +} + +QString SvgRectsCache::iconThemePath() +{ + if (!m_iconThemePath.isEmpty()) { + return m_iconThemePath; + } + + KConfigGroup imageGroup(m_svgElementsCache, QStringLiteral("General")); + m_iconThemePath = imageGroup.readEntry(QStringLiteral("IconThemePath"), QString()); + + return m_iconThemePath; +} + +void SvgRectsCache::setIconThemePath(const QString &path) +{ + m_iconThemePath = path; + KConfigGroup imageGroup(m_svgElementsCache, QStringLiteral("General")); + imageGroup.writeEntry(QStringLiteral("IconThemePath"), path); + QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start)); +} + +void SvgRectsCache::setNaturalSize(const QString &path, const QSizeF &size) +{ + KConfigGroup imageGroup(m_svgElementsCache, path); + + // FIXME: needs something faster, perhaps even sprintf + imageGroup.writeEntry(QStringLiteral("NaturalSize"), size); + QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start)); +} + +QSizeF SvgRectsCache::naturalSize(const QString &path) +{ + KConfigGroup imageGroup(m_svgElementsCache, path); + + // FIXME: needs something faster, perhaps even sprintf + return imageGroup.readEntry(QStringLiteral("NaturalSize"), QSizeF()); +} + +QStringList SvgRectsCache::cachedKeysForPath(const QString &path) const +{ + KConfigGroup imageGroup(m_svgElementsCache, path); + QStringList list = imageGroup.keyList(); + QStringList filtered; + + std::copy_if(list.begin(), list.end(), std::back_inserter(filtered), [](const QString element) { + bool ok; + element.toLong(&ok); + return ok; + }); + return filtered; +} + +unsigned int SvgRectsCache::lastModifiedTimeFromCache(const QString &filePath) +{ + const auto &i = m_lastModifiedTimes.constFind(filePath); + if (i != m_lastModifiedTimes.constEnd()) { + return i.value(); + } + + KConfigGroup imageGroup(m_svgElementsCache, filePath); + const unsigned int savedTime = imageGroup.readEntry("LastModified", 0); + m_lastModifiedTimes[filePath] = savedTime; + return savedTime; +} + +void SvgRectsCache::updateLastModified(const QString &filePath, unsigned int lastModified) +{ + KConfigGroup imageGroup(m_svgElementsCache, filePath); + const unsigned int savedTime = lastModifiedTimeFromCache(filePath); + + if (savedTime != lastModified) { + m_lastModifiedTimes[filePath] = lastModified; + imageGroup.writeEntry("LastModified", lastModified); + QMetaObject::invokeMethod(m_configSyncTimer, qOverload<>(&QTimer::start)); + Q_EMIT lastModifiedChanged(filePath, lastModified); + } +} + +SvgPrivate::SvgPrivate(Svg *svg) + : q(svg) + , renderer(nullptr) + , styleCrc(0) + , lastModified(0) + , devicePixelRatio(1.0) + , status(Svg::Status::Normal) + , multipleImages(false) + , themed(false) + , fromCurrentImageSet(false) + , cacheRendering(true) + , themeFailed(false) +{ +} + +SvgPrivate::~SvgPrivate() +{ + eraseRenderer(); +} + +size_t SvgPrivate::paletteId(const QPalette &palette, const QColor &positive, const QColor &neutral, const QColor &negative) const +{ + std::array parts = { + ::qHash(palette.cacheKey()), + ::qHash(positive.rgba()), + ::qHash(neutral.rgba()), + ::qHash(negative.rgba()), + }; + return qHashRange(parts.begin(), parts.end(), SvgRectsCache::s_seed); +} + +// This function is meant for the rects cache +SvgPrivate::CacheId SvgPrivate::cacheId(QStringView elementId) const +{ + auto idSize = size.isValid() && size != naturalSize ? size : QSizeF{-1.0, -1.0}; + return CacheId{idSize.width(), idSize.height(), path, elementId.toString(), status, devicePixelRatio, -1, 0, 0, lastModified}; +} + +// This function is meant for the pixmap cache +QString SvgPrivate::cachePath(const QString &id, const QSize &size) const +{ + std::vector parts; + const auto colors = colorOverrides.values(); + for (const QColor &c : std::as_const(colors)) { + parts.push_back(::qHash(c.red())); + parts.push_back(::qHash(c.green())); + parts.push_back(::qHash(c.blue())); + parts.push_back(::qHash(c.alpha())); + } + const size_t colorsHash = qHashRange(parts.begin(), parts.end(), SvgRectsCache::s_seed); + + auto cacheId = CacheId{double(size.width()), double(size.height()), path, id, status, devicePixelRatio, colorSet, colorsHash, 0, lastModified}; + return QString::number(qHash(cacheId, SvgRectsCache::s_seed)); +} + +bool SvgPrivate::setImagePath(const QString &imagePath) +{ + QString actualPath = imagePath; + bool isAbsoluteFile = QDir::isAbsolutePath(actualPath); + if (imagePath.startsWith(QLatin1String("file://"))) { + // length of file:// + actualPath.remove(0, 7); + isAbsoluteFile = true; + } + // If someone using the QML API uses Qt.resolvedUrl to get the absolute path inside of a QRC, + // the URI will look something like qrc:/artwork/file.svg + // In order for file IO to work it needs to be reformatted it needs to be :/artwork/file.svg + if (imagePath.startsWith(QLatin1String("qrc:/"))) { + actualPath.replace(QLatin1String("qrc:/"), QLatin1String(":/")); + isAbsoluteFile = true; + } + + bool isThemed = !actualPath.isEmpty() && !isAbsoluteFile; + + // lets check to see if we're already set to this file + if (isThemed == themed && ((themed && themePath == actualPath) || (!themed && path == actualPath))) { + return false; + } + + eraseRenderer(); + + // if we don't have any path right now and are going to set one, + // then lets not schedule a repaint because we are just initializing! + bool updateNeeded = true; //! path.isEmpty() || !themePath.isEmpty(); + + QObject::disconnect(imageSetChangedConnection); + + themed = isThemed; + path.clear(); + themePath.clear(); + + bool oldfromCurrentImageSet = fromCurrentImageSet; + fromCurrentImageSet = isThemed && actualImageSet()->currentImageSetHasImage(imagePath); + + if (fromCurrentImageSet != oldfromCurrentImageSet) { + Q_EMIT q->fromCurrentImageSetChanged(fromCurrentImageSet); + } + + if (themed) { + themePath = actualPath; + path = actualImageSet()->imagePath(themePath); + themeFailed = path.isEmpty(); + imageSetChangedConnection = QObject::connect(actualImageSet(), &ImageSet::imageSetChanged, q, [this]() { + imageSetChanged(); + }); + } else if (QFileInfo::exists(actualPath)) { + imageSetChangedConnection = QObject::connect(actualImageSet(), &ImageSet::imageSetChanged, q, [this]() { + imageSetChanged(); + }); + path = actualPath; + } else { +#ifndef NDEBUG + // qCDebug(LOG_KSVG) << "file '" << path << "' does not exist!"; +#endif + } + + QDateTime lastModifiedDate; + if (!path.isEmpty()) { + const QFileInfo info(path); + lastModifiedDate = info.lastModified(); + + lastModified = lastModifiedDate.toSecsSinceEpoch(); + + const bool imageWasCached = SvgRectsCache::instance()->loadImageFromCache(path, lastModified); + + if (!imageWasCached) { + std::shared_lock lock(s_renderersLock); + auto i = s_renderers.constBegin(); + while (i != s_renderers.constEnd()) { + if (i.key().contains(path)) { + i.value()->reload(); + } + i++; + } + } + } + + // also images with absolute path needs to have a natural size initialized, + // even if looks a bit weird using ImageSet to store non-themed stuff + if ((themed && !path.isEmpty() && lastModifiedDate.isValid()) || QFileInfo::exists(actualPath)) { + naturalSize = SvgRectsCache::instance()->naturalSize(path); + if (naturalSize.isEmpty()) { + createRenderer(); + naturalSize = renderer->defaultSize(); + SvgRectsCache::instance()->setNaturalSize(path, naturalSize); + } + } + + q->resize(); + Q_EMIT q->imagePathChanged(); + + return updateNeeded; +} + +ImageSet *SvgPrivate::actualImageSet() +{ + if (!theme) { + theme = new KSvg::ImageSet(q); + } + + return theme.data(); +} + +QPixmap SvgPrivate::findInCache(const QString &elementId, qreal ratio, const QSizeF &s) +{ + QSize size; + QString actualElementId; + + // Look at the size hinted elements and try to find the smallest one with an + // identical aspect ratio. + if (s.isValid() && !elementId.isEmpty()) { + const QList elementSizeHints = SvgRectsCache::instance()->sizeHintsForId(path, elementId); + + if (!elementSizeHints.isEmpty()) { + QSizeF bestFit(-1, -1); + + for (const auto &hint : elementSizeHints) { + if (hint.width() >= s.width() * ratio && hint.height() >= s.height() * ratio + && (!bestFit.isValid() || (bestFit.width() * bestFit.height()) > (hint.width() * hint.height()))) { + bestFit = hint; + } + } + + if (bestFit.isValid()) { + actualElementId = QString::number(bestFit.width()) % QLatin1Char('-') % QString::number(bestFit.height()) % QLatin1Char('-') % elementId; + } + } + } + + if (elementId.isEmpty() || !q->hasElement(actualElementId)) { + actualElementId = elementId; + } + + if (elementId.isEmpty() || (multipleImages && s.isValid())) { + size = s.toSize() * ratio; + } else { + size = elementRect(actualElementId).size().toSize() * ratio; + } + + if (size.isEmpty()) { + return QPixmap(); + } + + const QString id = cachePath(actualElementId, size); + + QPixmap p; + if (cacheRendering && lastModified == SvgRectsCache::instance()->lastModifiedTimeFromCache(path) && actualImageSet()->d->findInCache(id, p, lastModified)) { + p.setDevicePixelRatio(ratio); + // qCDebug(LOG_PLASMA) << "found cached version of " << id << p.size(); + return p; + } + + createRenderer(); + + QRectF finalRect = makeUniform(renderer->boundsOnElement(actualElementId), QRect(QPoint(0, 0), size)); + + // don't alter the pixmap size or it won't match up properly to, e.g., FrameSvg elements + // makeUniform should never change the size so much that it gains or loses a whole pixel + p = QPixmap(size); + + p.fill(Qt::transparent); + QPainter renderPainter(&p); + + if (actualElementId.isEmpty()) { + renderer->render(&renderPainter, finalRect); + } else { + renderer->render(&renderPainter, actualElementId, finalRect); + } + + renderPainter.end(); + p.setDevicePixelRatio(ratio); + + if (cacheRendering) { + actualImageSet()->d->insertIntoCache(id, p, QString::number((qint64)q, 16) % QLatin1Char('_') % actualElementId); + } + + SvgRectsCache::instance()->updateLastModified(path, lastModified); + + return p; +} + +void SvgPrivate::createRenderer() +{ + if (renderer) { + return; + } + + if (themed && path.isEmpty() && !themeFailed) { + if (path.isEmpty()) { + path = actualImageSet()->imagePath(themePath); + themeFailed = path.isEmpty(); + if (themeFailed) { + qCWarning(LOG_KSVG) << "No image path found for" << themePath; + } + } + } + + QString styleSheet; + if (!colorOverrides.isEmpty()) { + if (stylesheetOverride.isEmpty()) { + stylesheetOverride = actualImageSet()->d->svgStyleSheet(q); + } + styleSheet = stylesheetOverride; + } else { + styleSheet = actualImageSet()->d->svgStyleSheet(q); + } + + styleCrc = qChecksum(QByteArrayView(styleSheet.toUtf8().constData(), styleSheet.size())); + + // If the KSVG lives on the main thread (the majority case) share a renderer + // For other uses we should not as QSvgRenderer is not thread safe. + // We cannot compare the current thread as this might be hit from the render thread in updatePaintNode for KSVG objects living on the main thread + if (q->thread() == qGuiApp->thread()) { + std::shared_lock lock(s_renderersLock); + QHash::const_iterator it = s_renderers.constFind(styleCrc + path); + + if (it != s_renderers.constEnd()) { + renderer = it.value(); + if (size == QSizeF()) { + size = renderer->defaultSize(); + } + return; + } + } + + if (path.isEmpty()) { + renderer = new SharedSvgRenderer(); + } else { + QHash interestingElements; + renderer = new SharedSvgRenderer(path, styleSheet, interestingElements); + + // Add interesting elements to the theme's rect cache. + QHashIterator i(interestingElements); + + QRegularExpression sizeHintedKeyExpr(QStringLiteral("^(\\d+)-(\\d+)-(.+)$")); + + while (i.hasNext()) { + i.next(); + const QString &elementId = i.key(); + QString originalId = i.key(); + const QRectF &elementRect = i.value(); + + originalId.replace(sizeHintedKeyExpr, QStringLiteral("\\3")); + SvgRectsCache::instance()->insertSizeHintForId(path, originalId, elementRect.size().toSize()); + + const CacheId cacheId{.width = -1.0, + .height = -1.0, + .filePath = path, + .elementName = elementId, + .status = status, + .scaleFactor = devicePixelRatio, + .colorSet = -1, + .styleSheet = 0, + .extraFlags = 0, + .lastModified = lastModified}; + SvgRectsCache::instance()->insert(cacheId, elementRect, lastModified); + } + } + + { + std::unique_lock lock(s_renderersLock); + s_renderers[styleCrc + path] = renderer; + if (size == QSizeF()) { + size = renderer->defaultSize(); + } + } +} + +void SvgPrivate::eraseRenderer() +{ + if (renderer && renderer->ref.loadRelaxed() == 2) { + // this and the cache reference it + std::unique_lock lock(s_renderersLock); + s_renderers.erase(s_renderers.find(styleCrc + path)); + } + + renderer = nullptr; + styleCrc = QChar(0); +} + +QRectF SvgPrivate::elementRect(QStringView elementId) +{ + if (themed && path.isEmpty()) { + if (themeFailed) { + return QRectF(); + } + + path = actualImageSet()->imagePath(themePath); + themeFailed = path.isEmpty(); + + if (themeFailed) { + return QRectF(); + } + } + + if (path.isEmpty()) { + return QRectF(); + } + + QRectF rect; + const CacheId cacheId = SvgPrivate::cacheId(elementId); + bool found = SvgRectsCache::instance()->findElementRect(cacheId, rect); + // This is a corner case where we are *sure* the element is not valid + if (!found) { + rect = findAndCacheElementRect(elementId); + } + + return rect; +} + +QRectF SvgPrivate::findAndCacheElementRect(QStringView elementId) +{ + // we need to check the id before createRenderer(), otherwise it may generate a different id compared to the previous cacheId)( call + const CacheId cacheId = SvgPrivate::cacheId(elementId); + + createRenderer(); + + auto elementIdString = elementId.toString(); + + // This code will usually never be run because createRenderer already caches all the boundingRect in the elements in the svg + QRectF elementRect = renderer->elementExists(elementIdString) + ? renderer->transformForElement(elementIdString).map(renderer->boundsOnElement(elementIdString)).boundingRect() + : QRectF(); + + naturalSize = renderer->defaultSize(); + + qreal dx = size.width() / renderer->defaultSize().width(); + qreal dy = size.height() / renderer->defaultSize().height(); + + elementRect = QRectF(elementRect.x() * dx, elementRect.y() * dy, elementRect.width() * dx, elementRect.height() * dy); + SvgRectsCache::instance()->insert(cacheId, elementRect, lastModified); + + return elementRect; +} + +bool Svg::eventFilter(QObject *watched, QEvent *event) +{ + return QObject::eventFilter(watched, event); +} + +// Following two are utility functions to snap rendered elements to the pixel grid +// to and from are always 0 <= val <= 1 +qreal SvgPrivate::closestDistance(qreal to, qreal from) +{ + qreal a = to - from; + if (qFuzzyCompare(to, from)) { + return 0; + } else if (to > from) { + qreal b = to - from - 1; + return (qAbs(a) > qAbs(b)) ? b : a; + } else { + qreal b = 1 + to - from; + return (qAbs(a) > qAbs(b)) ? b : a; + } +} + +QRectF SvgPrivate::makeUniform(const QRectF &orig, const QRectF &dst) +{ + if (qFuzzyIsNull(orig.x()) || qFuzzyIsNull(orig.y())) { + return dst; + } + + QRectF res(dst); + qreal div_w = dst.width() / orig.width(); + qreal div_h = dst.height() / orig.height(); + + qreal div_x = dst.x() / orig.x(); + qreal div_y = dst.y() / orig.y(); + + // horizontal snap + if (!qFuzzyIsNull(div_x) && !qFuzzyCompare(div_w, div_x)) { + qreal rem_orig = orig.x() - (floor(orig.x())); + qreal rem_dst = dst.x() - (floor(dst.x())); + qreal offset = closestDistance(rem_dst, rem_orig); + res.translate(offset + offset * div_w, 0); + res.setWidth(res.width() + offset); + } + // vertical snap + if (!qFuzzyIsNull(div_y) && !qFuzzyCompare(div_h, div_y)) { + qreal rem_orig = orig.y() - (floor(orig.y())); + qreal rem_dst = dst.y() - (floor(dst.y())); + qreal offset = closestDistance(rem_dst, rem_orig); + res.translate(0, offset + offset * div_h); + res.setHeight(res.height() + offset); + } + + return res; +} + +void SvgPrivate::imageSetChanged() +{ + if (q->imagePath().isEmpty()) { + return; + } + + QString currentPath = themed ? themePath : path; + themePath.clear(); + eraseRenderer(); + setImagePath(currentPath); + q->resize(); + + // qCDebug(LOG_KSVG) << themePath << ">>>>>>>>>>>>>>>>>> theme changed"; + Q_EMIT q->repaintNeeded(); + Q_EMIT q->imageSetChanged(q->imageSet()); +} + +void SvgPrivate::colorsChanged() +{ + eraseRenderer(); + qCDebug(LOG_KSVG) << "repaint needed from colorsChanged"; + + Q_EMIT q->repaintNeeded(); +} + +std::shared_mutex SvgPrivate::s_renderersLock; +QHash SvgPrivate::s_renderers; +QPointer SvgPrivate::s_systemColorsCache; + +Svg::Svg(QObject *parent) + : QObject(parent) + , d(new SvgPrivate(this)) +{ + connect(SvgRectsCache::instance(), &SvgRectsCache::lastModifiedChanged, this, [this](const QString &filePath, unsigned int lastModified) { + if (d->lastModified != lastModified && filePath == d->path) { + d->lastModified = lastModified; + Q_EMIT repaintNeeded(); + } + }); +} + +Svg::~Svg() +{ + delete d; +} + +void Svg::setDevicePixelRatio(qreal ratio) +{ + if (FrameSvg *f = qobject_cast(this)) { + f->clearCache(); + } + + d->devicePixelRatio = ratio; + + Q_EMIT repaintNeeded(); +} + +qreal Svg::devicePixelRatio() const +{ + return d->devicePixelRatio; +} + +QPixmap Svg::pixmap(const QString &elementID) +{ + if (elementID.isNull() || d->multipleImages) { + return d->findInCache(elementID, d->devicePixelRatio, size()); + } else { + return d->findInCache(elementID, d->devicePixelRatio); + } +} + +QImage Svg::image(const QSize &size, const QString &elementID) +{ + QPixmap pix(d->findInCache(elementID, d->devicePixelRatio, size)); + return pix.toImage(); +} + +void Svg::paint(QPainter *painter, const QPointF &point, const QString &elementID) +{ + Q_ASSERT(painter->device()); + const qreal ratio = painter->device()->devicePixelRatio(); + QPixmap pix((elementID.isNull() || d->multipleImages) ? d->findInCache(elementID, ratio, size()) : d->findInCache(elementID, ratio)); + + if (pix.isNull()) { + return; + } + + painter->drawPixmap(QRectF(point, size()), pix, QRectF(QPointF(0, 0), pix.size())); +} + +void Svg::paint(QPainter *painter, int x, int y, const QString &elementID) +{ + paint(painter, QPointF(x, y), elementID); +} + +void Svg::paint(QPainter *painter, const QRectF &rect, const QString &elementID) +{ + Q_ASSERT(painter->device()); + const qreal ratio = painter->device()->devicePixelRatio(); + QPixmap pix(d->findInCache(elementID, ratio, rect.size())); + + painter->drawPixmap(rect, pix, QRect(QPoint(0, 0), pix.size())); +} + +void Svg::paint(QPainter *painter, int x, int y, int width, int height, const QString &elementID) +{ + Q_ASSERT(painter->device()); + const qreal ratio = painter->device()->devicePixelRatio(); + QPixmap pix(d->findInCache(elementID, ratio, QSizeF(width, height))); + painter->drawPixmap(x, y, pix, 0, 0, pix.size().width(), pix.size().height()); +} + +QSizeF Svg::size() const +{ + if (d->size.isEmpty()) { + d->size = d->naturalSize; + } + + return {std::round(d->size.width()), std::round(d->size.height())}; +} + +void Svg::resize(qreal width, qreal height) +{ + resize(QSize(width, height)); +} + +void Svg::resize(const QSizeF &size) +{ + if (qFuzzyCompare(size.width(), d->size.width()) && qFuzzyCompare(size.height(), d->size.height())) { + return; + } + + d->size = size; + Q_EMIT sizeChanged(); +} + +void Svg::resize() +{ + if (qFuzzyCompare(d->naturalSize.width(), d->size.width()) && qFuzzyCompare(d->naturalSize.height(), d->size.height())) { + return; + } + + d->size = d->naturalSize; + Q_EMIT sizeChanged(); +} + +QSizeF Svg::elementSize(const QString &elementId) const +{ + const QSizeF s = d->elementRect(elementId).size(); + return {std::round(s.width()), std::round(s.height())}; +} + +QSizeF Svg::elementSize(QStringView elementId) const +{ + const QSizeF s = d->elementRect(elementId).size(); + return {std::round(s.width()), std::round(s.height())}; +} + +QRectF Svg::elementRect(const QString &elementId) const +{ + return d->elementRect(elementId); +} + +QRectF Svg::elementRect(QStringView elementId) const +{ + return d->elementRect(elementId); +} + +bool Svg::hasElement(const QString &elementId) const +{ + return hasElement(QStringView(elementId)); +} + +bool Svg::hasElement(QStringView elementId) const +{ + if (elementId.isEmpty() || (d->path.isNull() && d->themePath.isNull())) { + return false; + } + + return d->elementRect(elementId).isValid(); +} + +bool Svg::isValid() const +{ + if (d->path.isNull() && d->themePath.isNull()) { + return false; + } + + // try very hard to avoid creation of a parser + QSizeF naturalSize = SvgRectsCache::instance()->naturalSize(d->path); + if (!naturalSize.isEmpty()) { + return true; + } + + if (d->path.isEmpty() || !QFileInfo::exists(d->path)) { + return false; + } + d->createRenderer(); + return d->renderer->isValid(); +} + +void Svg::setContainsMultipleImages(bool multiple) +{ + d->multipleImages = multiple; +} + +bool Svg::containsMultipleImages() const +{ + return d->multipleImages; +} + +void Svg::setImagePath(const QString &svgFilePath) +{ + if (d->setImagePath(svgFilePath)) { + Q_EMIT repaintNeeded(); + } +} + +QString Svg::imagePath() const +{ + return d->themed ? d->themePath : d->path; +} + +void Svg::setUsingRenderingCache(bool useCache) +{ + d->cacheRendering = useCache; + Q_EMIT repaintNeeded(); +} + +bool Svg::isUsingRenderingCache() const +{ + return d->cacheRendering; +} + +bool Svg::fromCurrentImageSet() const +{ + return d->fromCurrentImageSet; +} + +void Svg::setImageSet(KSvg::ImageSet *theme) +{ + if (!theme || theme == d->theme.data()) { + return; + } + + if (d->theme) { + disconnect(d->theme.data(), nullptr, this, nullptr); + } + + d->theme = theme; + connect(theme, SIGNAL(imageSetChanged(QString)), this, SLOT(imageSetChanged())); + d->imageSetChanged(); +} + +ImageSet *Svg::imageSet() const +{ + return d->actualImageSet(); +} + +void Svg::setStatus(KSvg::Svg::Status status) +{ + if (status == d->status) { + return; + } + + d->status = status; + d->eraseRenderer(); + Q_EMIT statusChanged(status); + Q_EMIT repaintNeeded(); +} + +Svg::Status Svg::status() const +{ + return d->status; +} + +void Svg::setColorSet(KSvg::Svg::ColorSet colorSet) +{ + const KColorScheme::ColorSet convertedSet = KColorScheme::ColorSet(colorSet); + if (convertedSet == d->colorSet) { + return; + } + + d->colorSet = convertedSet; + d->eraseRenderer(); + Q_EMIT colorSetChanged(colorSet); + Q_EMIT repaintNeeded(); +} + +Svg::ColorSet Svg::colorSet() const +{ + return Svg::ColorSet(d->colorSet); +} + +QColor Svg::color(StyleSheetColor colorName) const +{ + auto it = d->colorOverrides.constFind(colorName); + if (it != d->colorOverrides.constEnd()) { + return *it; + } + return d->actualImageSet()->d->namedColor(colorName, this); +} + +void Svg::setColor(StyleSheetColor colorName, const QColor &color) +{ + setColors({ + {colorName, color}, + }); +} + +void Svg::setColors(const QMap &colors) +{ + bool changed = false; + for (const auto &[colorName, color] : colors.asKeyValueRange()) { + if (d->colorOverrides.value(colorName) != color) { + changed = true; + + if (color.isValid()) { + d->colorOverrides[colorName] = color; + } else { + d->colorOverrides.remove(colorName); + } + } + } + + if (!changed) { + return; + } + + d->stylesheetOverride.clear(); + + d->eraseRenderer(); + + // Ideally, there should be a signal but adding a new signal can make ksvg users crash in QML cache code. + if (auto frameSvg = qobject_cast(this)) { + frameSvg->colorOverridesChange(); + } + + Q_EMIT repaintNeeded(); +} + +QMap Svg::colorOverrides() const +{ + return d->colorOverrides; +} + +void Svg::clearColorOverrides() +{ + d->colorOverrides.clear(); + d->stylesheetOverride.clear(); + d->eraseRenderer(); + if (auto frameSvg = qobject_cast(this)) { + frameSvg->colorOverridesChange(); + } + Q_EMIT repaintNeeded(); +} + +} // KSvg namespace + +#include "moc_svg.cpp" +#include "private/moc_svg_p.cpp" diff --git a/local/recipes/kde/kf6-ksvg/source/src/ksvg/svg.h b/local/recipes/kde/kf6-ksvg/source/src/ksvg/svg.h new file mode 100644 index 0000000000..7f1dbecc13 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/ksvg/svg.h @@ -0,0 +1,725 @@ +/* + SPDX-FileCopyrightText: 2006-2007 Aaron Seigo + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef KSVG_SVG_H +#define KSVG_SVG_H + +#include +#include + +#include +#include + +class QPainter; +class QPoint; +class QPointF; +class QRect; +class QRectF; +class QSize; +class QSizeF; +class QMatrix; + +/*! + * \namespace KSvg + * \inmodule KSvg + * \brief The KSvg namespace. + */ +namespace KSvg +{ +class FrameSvgPrivate; +class SvgPrivate; + +/*! + * \class KSvg::Svg + * \inheaderfile KSvg/Svg + * \inmodule KSvg + * + * \brief A theme aware image-centric SVG class. + * + * KSvg::Svg provides a class for rendering SVG images to a QPainter in a + * convenient manner. Unless an absolute path to a file is provided, it loads + * the SVG document using KSvg::ImageSet. It also provides a number of internal + * optimizations to help lower the cost of painting SVGs, such as caching. + * + * \sa KSvg::FrameSvg + **/ +class KSVG_EXPORT Svg : public QObject +{ + Q_OBJECT + + /*! + * \property KSvg::Svg::size + */ + Q_PROPERTY(QSizeF size READ size WRITE resize NOTIFY sizeChanged) + + /*! + * \property KSvg::Svg::multipleImages + */ + Q_PROPERTY(bool multipleImages READ containsMultipleImages WRITE setContainsMultipleImages) + + /*! + * \property KSvg::Svg::imagePath + */ + Q_PROPERTY(QString imagePath READ imagePath WRITE setImagePath NOTIFY imagePathChanged) + + /*! + * \property KSvg::Svg::usingRenderingCache + */ + Q_PROPERTY(bool usingRenderingCache READ isUsingRenderingCache WRITE setUsingRenderingCache) + + /*! + * \property KSvg::Svg::fromCurrentImageSet + */ + Q_PROPERTY(bool fromCurrentImageSet READ fromCurrentImageSet NOTIFY fromCurrentImageSetChanged) + + /*! + * \property KSvg::Svg::status + */ + Q_PROPERTY(KSvg::Svg::Status status READ status WRITE setStatus NOTIFY statusChanged) + + /*! + * \property KSvg::Svg::colorSet + */ + Q_PROPERTY(KSvg::Svg::ColorSet colorSet READ colorSet WRITE setColorSet NOTIFY colorSetChanged) + +public: + /*! + * \enum KSvg::Svg::Status + * \value Normal + * \value Selected + * \value Inactive + */ + enum Status { + Normal = 0, + Selected, + Inactive, + }; + Q_ENUM(Status) + + // FIXME? Those are copied from KColorScheme because is needed to make it a Q_ENUM + /*! + * \enum KSvg::Svg::ColorSet + * + * \value View + * \value Window + * \value Button + * \value Selection + * \value Tooltip + * \value Complementary + * \value Header + */ + enum ColorSet { + View, + Window, + Button, + Selection, + Tooltip, + Complementary, + Header + }; + Q_ENUM(ColorSet) + + /*! + * \enum KSvg::Svg::StyleSheetColor + * \value Text + * \value Background + * \value Highlight + * \value HighlightedText + * \value PositiveText + * \value NeutralText + * \value NegativeText + * \value ButtonText + * \value ButtonBackground + * \value ButtonHover + * \value ButtonFocus + * \value ButtonHighlightedText + * \value ButtonPositiveText + * \value ButtonNeutralText + * \value ButtonNegativeText + * \value ViewText + * \value ViewBackground + * \value ViewHover + * \value ViewFocus + * \value ViewHighlightedText + * \value ViewPositiveText + * \value ViewNeutralText + * \value ViewNegativeText + * \value TooltipText + * \value TooltipBackground + * \value TooltipHover + * \value TooltipFocus + * \value TooltipHighlightedText + * \value TooltipPositiveText + * \value TooltipNeutralText + * \value TooltipNegativeText + * \value ComplementaryText + * \value ComplementaryBackground + * \value ComplementaryHover + * \value ComplementaryFocus + * \value ComplementaryHighlightedText + * \value ComplementaryPositiveText + * \value ComplementaryNeutralText + * \value ComplementaryNegativeText + * \value HeaderText + * \value HeaderBackground + * \value HeaderHover + * \value HeaderFocus + * \value HeaderHighlightedText + * \value HeaderPositiveText + * \value HeaderNeutralText + * \value HeaderNegativeText + * \value Frame + */ + enum StyleSheetColor { + Text, + Background, + Highlight, + HighlightedText, + PositiveText, + NeutralText, + NegativeText, + + ButtonText, + ButtonBackground, + ButtonHover, + ButtonFocus, + ButtonHighlightedText, + ButtonPositiveText, + ButtonNeutralText, + ButtonNegativeText, + + ViewText, + ViewBackground, + ViewHover, + ViewFocus, + ViewHighlightedText, + ViewPositiveText, + ViewNeutralText, + ViewNegativeText, + + TooltipText, + TooltipBackground, + TooltipHover, + TooltipFocus, + TooltipHighlightedText, + TooltipPositiveText, + TooltipNeutralText, + TooltipNegativeText, + + ComplementaryText, + ComplementaryBackground, + ComplementaryHover, + ComplementaryFocus, + ComplementaryHighlightedText, + ComplementaryPositiveText, + ComplementaryNeutralText, + ComplementaryNegativeText, + + HeaderText, + HeaderBackground, + HeaderHover, + HeaderFocus, + HeaderHighlightedText, + HeaderPositiveText, + HeaderNeutralText, + HeaderNegativeText, + Frame + }; + Q_ENUM(StyleSheetColor); + + /*! + * \brief This method constructs an SVG object that implicitly shares and + * caches rendering. + * + * Unlike QSvgRenderer, which this class uses internally, + * KSvg::Svg represents an image generated from an SVG. As such, it has a + * related size and transform matrix (the latter being provided by the + * painter used to paint the image). + * + * The size is initialized to be the SVG's native size. + * + * \a parent options QObject to parent this to + * + * \sa KSvg::ImageSet + */ + explicit Svg(QObject *parent = nullptr); + ~Svg() override; + + /*! + * \brief This method sets the device pixel ratio for the Svg. + * + * This is the ratio between image pixels and device-independent pixels. The + * SVG will produce pixmaps scaled by devicePixelRatio, but all the sizes + * and element rects will not be altered. The default value is 1.0 and the + * scale will be done rounded to the floor integer. + * + * Setting it to something higher will make all the elements of this SVG + * appear bigger. + */ + void setDevicePixelRatio(qreal factor); + + /*! + * \brief This method returns the device pixel ratio for this Svg. + */ + qreal devicePixelRatio() const; + + /*! + * \brief This method returns a pixmap of the SVG represented by this + * object. + * + * The size of the pixmap will be the size of this Svg object (size()) if + * containsMultipleImages is \c true; otherwise, it will be the size of the + * requested element after the whole SVG has been scaled to size(). + * + * \a elementID the ID string of the element to render, or an empty + * string for the whole SVG (the default) + * + * Returns a QPixmap of the rendered SVG + */ + Q_INVOKABLE QPixmap pixmap(const QString &elementID = QString()); + + /*! + * \brief This method returns an image of the SVG represented by this + * object. + * + * The size of the image will be the size of this Svg object (size()) if + * containsMultipleImages is \c true; otherwise, it will be the size of the + * requested element after the whole SVG has been scaled to size(). + * + * \a elementID the ID string of the element to render, or an empty + * string for the whole SVG (the default) + * + * Returns a QPixmap of the rendered SVG + */ + Q_INVOKABLE QImage image(const QSize &size, const QString &elementID = QString()); + + /*! + * \brief This method paints all or part of the SVG represented by this + * object. + * + * The size of the painted area will be the size of this Svg object (size()) + * if containsMultipleImages is \c true; otherwise, it will be the size of + * the requested element after the whole SVG has been scaled to size(). + * + * \a painter the QPainter to use + * + * \a point the position to start drawing; the entire svg will be + * drawn starting at this point. + * + * \a elementID the ID string of the element to render, or an empty + * string for the whole SVG (the default) + */ + Q_INVOKABLE void paint(QPainter *painter, const QPointF &point, const QString &elementID = QString()); + + /*! + * \brief This method paints all or part of the SVG represented by this + * object. + * + * The size of the painted area will be the size of this Svg object (size()) + * if containsMultipleImages is \c true; otherwise, it will be the size of + * the requested element after the whole SVG has been scaled to size(). + * + * \a painter the QPainter to use + * + * \a x the horizontal coordinate to start painting from + * + * \a y the vertical coordinate to start painting from + * + * \a elementID the ID string of the element to render, or an empty + * string for the whole SVG (the default) + */ + Q_INVOKABLE void paint(QPainter *painter, int x, int y, const QString &elementID = QString()); + + /*! + * \brief This method paints all or part of the SVG represented by this + * object. + * + * \a painter the QPainter to use + * + * \a rect the rect to draw into; if smaller than the current size + * the drawing is starting at this point. + * + * \a elementID the ID string of the element to render, or an empty + * string for the whole SVG (the default) + */ + Q_INVOKABLE void paint(QPainter *painter, const QRectF &rect, const QString &elementID = QString()); + + /*! + * \brief This method paints all or part of the SVG represented by this + * object. + * + * \a painter the QPainter to use + * + * \a x the horizontal coordinate to start painting from + * + * \a y the vertical coordinate to start painting from + * + * \a width the width of the element to draw + * + * \a height the height of the element do draw + * + * \a elementID the ID string of the element to render, or an empty + * string for the whole SVG (the default) + */ + Q_INVOKABLE void paint(QPainter *painter, int x, int y, int width, int height, const QString &elementID = QString()); + + /*! + * \brief This method returns the size of the SVG. + * + * If the SVG has been resized with resize(), that size will be returned; + * otherwise, the natural size of the SVG will be returned. + * + * If containsMultipleImages is \c true, each element of the SVG will be + * rendered at this size by default. + * + * Returns the current size of the SVG + **/ + QSizeF size() const; + + /*! + * \brief This method resizes the rendered image. + * + * Rendering will actually take place on the next call to paint. + * + * If containsMultipleImages is \c true, each element of the SVG will be + * rendered at this size by default; otherwise, the entire image will be + * scaled to this size and each element will be scaled appropriately. + * + * \a width the new width + * + * \a height the new height + **/ + Q_INVOKABLE void resize(qreal width, qreal height); + + /*! + * \brief This method resizes the rendered image. + * + * Rendering will actually take place on the next call to paint. + * + * If containsMultipleImages is \c true, each element of the SVG will be + * rendered at this size by default; otherwise, the entire image will be + * scaled to this size and each element will be scaled appropriately. + * + * \a size the new size of the image + **/ + Q_INVOKABLE void resize(const QSizeF &size); + + /*! + * \brief This method resizes the rendered image to the natural size of the + * SVG. + * + * Rendering will actually take place on the next call to paint. + **/ + Q_INVOKABLE void resize(); + + /*! + * \brief This method returns the size of a given element. + * + * This is the size of the element with ID \a elementId after the SVG + * has been scaled (see resize()). Note that this is unaffected by + * the containsMultipleImages property. + * + * \a elementId the id of the element to check + * + * Returns the size of a given element, given the current size of the SVG + **/ + Q_INVOKABLE QSizeF elementSize(const QString &elementId) const; + + QSizeF elementSize(QStringView elementId) const; + + /*! + * \brief This method returns the bounding rect of a given element. + * + * This is the bounding rect of the element with ID \a elementId after the + * SVG has been scaled (see resize()). Note that this is unaffected by the + * containsMultipleImages property. + * + * \a elementId the id of the element to check + * + * Returns the current rect of a given element, given the current size of the SVG + **/ + Q_INVOKABLE QRectF elementRect(const QString &elementId) const; + + /*! + * + */ + QRectF elementRect(QStringView elementId) const; + + /*! + * \brief This method checks whether an element exists in the loaded SVG. + * + * \a elementId the id of the element to check for + * + * Returns \c true if the element is defined in the SVG, otherwise \c false + **/ + Q_INVOKABLE bool hasElement(const QString &elementId) const; + + /*! + * + */ + bool hasElement(QStringView elementId) const; + + /*! + * \brief This method checks whether this object is backed by a valid SVG + * file. + * + * This method can be expensive as it causes disk access. + * + * Returns \c true if the SVG file exists and the document is valid, + * otherwise \c false. + **/ + Q_INVOKABLE bool isValid() const; + + /*! + * \brief This method sets whether the SVG contains a single image or + * multiple ones. + * + * If this is set to \c true, the SVG will be treated as a collection of + * related images, rather than a consistent drawing. + * + * In particular, when individual elements are rendered, this affects + * whether the elements are resized to size() by default. See paint() and + * pixmap(). + * + * \sa paint() + * \sa pixmap() + * + * \a multiple true if the svg contains multiple images + */ + void setContainsMultipleImages(bool multiple); + + /*! + * \brief This method returns whether the SVG contains multiple images. + * + * If this is \c true, the SVG will be treated as a collection of related + * images, rather than a consistent drawing. + * + * Returns \c true if the SVG will be treated as containing multiple images, + * \c false if it will be treated as a coherent image. + */ + bool containsMultipleImages() const; + + /*! + * \brief This method sets the SVG file to render. + * + * Relative paths are looked for in the current Svg theme, and should not + * include the file extension (.svg and .svgz files will be searched for). + * include the file extension; files with the .svg and .svgz extensions will be + * found automatically. + * + * \sa ImageSet::imagePath() + * + * If the parent object of this Svg is a KSvg::Applet, relative paths will + * be searched for in the applet's package first. + * + * \a svgFilePath either an absolute path to an SVG file, or an image + * name. + */ + virtual void setImagePath(const QString &svgFilePath); + + /*! + * \brief This method returns the SVG file to render. + * + * If this SVG is themed, this will be a relative path, and will not + * include a file extension. + * + * Returns either an absolute path to an SVG file, or an image name + * \sa ImageSet::imagePath() + */ + QString imagePath() const; + + /*! + * \brief This method sets whether or not to cache the results of rendering + * to pixmaps. + * + * If the SVG is resized and re-rendered often (and does not keep using the + * same small set of pixmap dimensions), then it may be less efficient to do + * disk caching. A good example might be a progress meter that uses an Svg + * object to paint itself: the meter will be changing often enough, with + * enough unpredictability and without re-use of the previous pixmaps to + * not get a gain from caching. + * + * Most Svg objects should use the caching feature, however. + * Therefore, the default is to use the render cache. + * + * \a useCache true to cache rendered pixmaps + * \since 4.3 + */ + void setUsingRenderingCache(bool useCache); + + /*! + * Whether the rendering cache is being used. + * + * \brief This method returns whether the Svg object is using caching for + * rendering results. + * + * \since 4.3 + */ + bool isUsingRenderingCache() const; + + /*! + * \brief This method returns whether the current theme has this SVG, + * without having to fall back to the default theme. + * + * Returns true if the svg is loaded from the current theme + * \sa ImageSet::currentImageSetHasImage + */ + bool fromCurrentImageSet() const; + + /*! + * \brief This method sets the KSvg::ImageSet to use with this Svg object. + * + * By default, Svg objects use KSvg::ImageSet::default(). + * + * This determines how relative image paths are interpreted. + * + * \a theme the theme object to use + * \since 4.3 + */ + void setImageSet(KSvg::ImageSet *theme); + + /*! + * \brief This method returns the KSvg::ImageSet used by this Svg object. + * + * This determines how relative image paths are interpreted. + * + * Returns the theme used by this Svg + */ + ImageSet *imageSet() const; + + /*! + * \brief This method sets the image in a selected status. + * + * SVGs can be colored with system color themes. If \a status is selected, + * \c TextColor will become \c HighlightedText color, and \c BackgroundColor will + * become \c HighlightColor. This can be used to make SVG-based graphics such + * as symbolic icons look correct together. Supported statuses are \c Normal + * and \c Selected. + * \since 5.23 + */ + void setStatus(Svg::Status status); + + /*! + * \brief This method returns the Svg object's status. + * \since 5.23 + */ + Svg::Status status() const; + + /*! + * \brief This method sets a color set for the SVG. + * Set a color set for the Svg. + * if the Svg uses stylesheets and has elements + * that are either \c TextColor or \c BackgroundColor class, + * make them use \c ButtonTextColor / \c ButtonBackgroundColor + * or \c ViewTextColor / \c ViewBackgroundColor + */ + void setColorSet(ColorSet colorSet); + + /*! + * Returns the color set for this Svg + */ + KSvg::Svg::ColorSet colorSet() const; + + /*! + * + */ + QColor color(StyleSheetColor colorName) const; + + /*! + * + */ + void setColor(StyleSheetColor colorName, const QColor &color); + + /*! + * Sets the specified stylesheet \a colors. + * + * \sa color(), colorOverrides(), clearColorOverrides() + * \since 6.18 + */ + void setColors(const QMap &colors); + + /*! + * Returns the color overrides. + * + * \since 6.18 + */ + QMap colorOverrides() const; + + /*! + * + */ + void clearColorOverrides(); + +Q_SIGNALS: + /*! + * \brief This signal is emitted whenever the SVG data has changed in such a + * way that a repaint is required. + * + * Any usage of an SVG object that does the painting itself must connect to + * this signal and respond by updating the painting. Note that connecting to + * ImageSet::imageSetChanged is incorrect in such a use case as the SVG + * itself may not be updated yet nor may theme change be the only case when + * a repaint is needed. Also note that classes or QML code which take Svg + * objects as parameters for their own painting all respond to this signal + * so that in those cases manually responding to the signal is unnecessary; + * ONLY when direct, manual painting with an Svg object is done in + * application code is this signal used. + */ + void repaintNeeded(); + + /*! + * \brief This signal is emitted whenever the size has changed. + * \sa resize() + */ + void sizeChanged(); + + /*! + * \brief This signal is emitted whenever the image path has changed. + */ + void imagePathChanged(); + + /*! + * \brief This signal is emitted whenever the color hint has changed. + */ + void colorHintChanged(); + + /*! + * \brief This signal is emitted when the value of fromCurrentImageSet() + * has changed. + */ + void fromCurrentImageSetChanged(bool fromCurrentImageSet); + + /*! + * \brief This signal is emitted when the status has changed. + */ + void statusChanged(KSvg::Svg::Status status); + + /*! + * \brief This signal is emitted when the color set has changed. + */ + void colorSetChanged(KSvg::Svg::ColorSet colorSet); + + /*! + * \brief This signal is emitted when the image set has changed. + */ + void imageSetChanged(ImageSet *imageSet); + +private: + SvgPrivate *const d; + bool eventFilter(QObject *watched, QEvent *event) override; + + Q_PRIVATE_SLOT(d, void imageSetChanged()) + Q_PRIVATE_SLOT(d, void colorsChanged()) + + friend class SvgPrivate; + friend class FrameSvgPrivate; + friend class FrameSvg; + friend class ImageSetPrivate; +}; + +} // KSvg namespace + +#endif // multiple inclusion guard diff --git a/local/recipes/kde/kf6-ksvg/source/src/tools/CMakeLists.txt b/local/recipes/kde/kf6-ksvg/source/src/tools/CMakeLists.txt new file mode 100644 index 0000000000..f096af4385 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/tools/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(split-plasma-svgs) diff --git a/local/recipes/kde/kf6-ksvg/source/src/tools/apply-stylesheet.sh b/local/recipes/kde/kf6-ksvg/source/src/tools/apply-stylesheet.sh new file mode 100755 index 0000000000..516e5c66b4 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/tools/apply-stylesheet.sh @@ -0,0 +1,287 @@ +#!/bin/bash + +PARSED_OPTIONS=$(getopt -n "$0" -o hf: --long "file:,TextFrom:,TextTo:,BackgroundFrom:,BackgroundTo:,HighlightFrom:,HighlightTo:,ViewTextFrom:,ViewTextTo:,ViewBackgroundFrom:,ViewBackgroundTo:,ViewHoverFrom:,ViewHoverTo:,ViewFocusFrom:,ViewFocusTo:,ButtonTextFrom:,ButtonTextTo:,ButtonBackgroundFrom:,ButtonBackgroundTo:,ButtonHoverFrom:,ButtonHoverTo:,ButtonFocusFrom:,ButtonFocusTo:" -- "$@") + +if [ $? -ne 0 ]; +then + exit 1 +fi + +eval set -- "$PARSED_OPTIONS" + +textFrom=\#31363b +backgroundFrom=\#eff0f1 +highlightFrom=\#3daee9 +viewTextFrom=\#31363b +viewBackgroundFrom=\#fcfcfc +viewHoverFrom=\#93cee9 +viewFocusFrom=\#3daee9 +buttonTextFrom=\#31363b +buttonBackgroundFrom=\#eff0f1 +buttonHoverFrom=\#93cee9 +buttonFocusFrom=\#3daee9 + +textTo=\#31363b +backgroundTo=\#eff0f1 +highlightTo=\#3daee9 +viewTextTo=\#31363b +viewBackgroundTo=\#fcfcfc +viewHoverTo=\#93cee9 +viewFocusTo=\#3daee9 +buttonTextTo=\#31363b +buttonBackgroundTo=\#eff0f1 +buttonHoverTo=\#93cee9 +buttonFocusTo=\#3daee9 + +file='' + +while true; +do + case "$1" in + + -h|--help) + echo "usage $0 [-h|options] -f file.svgz" + echo "Where options can be:" + echo " --TextFrom=color html encoded color to replace with the ColorScheme-Text from the stylesheet" + echo " --TextTo=color html encoded that the ColorScheme-Text class will have" + echo + echo " --BackgroundFrom=color html encoded color to replace with the ColorScheme-Background from the stylesheet" + echo " --BackgroundTo=color html encoded that the ColorScheme-Background class will have" + echo + echo " --HighlightFrom=color html encoded color to replace with the ColorScheme-Highlight from the stylesheet" + echo " --HighlightTo=color html encoded that the ColorScheme-Highlight class will have" + echo + echo " --ViewTextFrom=color html encoded color to replace with the ColorScheme-ViewText from the stylesheet" + echo " --ViewTextTo=color html encoded that the ColorScheme-ViewText class will have" + echo + echo " --ViewBackgroundFrom=color html encoded color to replace with the ColorScheme-ViewBackground from the stylesheet" + echo " --ViewBackgroundTo=color html encoded that the ColorScheme-ViewBackground class will have" + echo + echo " --ViewHoverFrom=color html encoded color to replace with the ColorScheme-ViewHover from the stylesheet" + echo " --ViewHoverTo=color html encoded that the ColorScheme-ViewHover class will have" + echo + echo " --ViewFocusFrom=color html encoded color to replace with the ColorScheme-ViewFocus from the stylesheet" + echo " --ViewFocusTo=color html encoded that the ColorScheme-ViewFocus class will have" + echo + echo " --ButtonTextFrom=color html encoded color to replace with the ColorScheme-ButtonText from the stylesheet" + echo " --ButtonTextTo=color html encoded that the ColorScheme-ButtonText class will have" + echo + echo " --ButtonBackgroundFrom=color html encoded color to replace with the ColorScheme-ButtonBackground from the stylesheet" + echo " --ButtonBackgroundTo=color html encoded that the ColorScheme-ButtonBackground class will have" + echo + echo " --ButtonHoverFrom=color html encoded color to replace with the ColorScheme-ButtonHover from the stylesheet" + echo " --ButtonHoverTo=color html encoded that the ColorScheme-ButtonHover class will have" + echo + echo " --ButtonFocusFrom=color html encoded color to replace with the ColorScheme-ButtonFocus from the stylesheet" + echo " --ButtonFocusTo=color html encoded that the ColorScheme-ButtonFocus class will have" + echo + echo "All the colors have default values conformant to the Breeze color palette" + echo + exit + shift;; + + --TextFrom) + textFrom=$2 + shift 2;; + --TextTo) + textTo=$2 + shift 2;; + + --BackgroundFrom) + backgroundFrom=$2 + shift 2;; + --BackgroundTo) + backgroundTo=$2 + shift 2;; + + --HighlightFrom) + highlightFrom=$2 + shift 2;; + --HighlightTo) + highlightTo=$2 + shift 2;; + + --ViewTextFrom) + viewTextFrom=$2 + shift 2;; + --ViewTextTo) + viewTextTo=$2 + shift 2;; + + --ViewBackgroundFrom) + viewBackgroundFrom=$2 + shift 2;; + --ViewBackgroundTo) + viewBackgroundTo=$2 + shift 2;; + + --ViewHoverFrom) + viewHoverFrom=$2 + shift 2;; + --ViewHoverTo) + viewHoverTo=$2 + shift 2;; + + --ViewFocusFrom) + viewFocusFrom=$2 + shift 2;; + --ViewFocusTo) + viewFocusTo=$2 + shift 2;; + + --ButtonTextFrom) + buttonTextFrom=$2 + shift 2;; + --ButtonTextTo) + buttonTextTo=$2 + shift 2;; + + --ButtonBackgroundFrom) + buttonBackgroundFrom=$2 + shift 2;; + --ButtonBackgroundTo) + buttonBackgroundTo=$2 + shift 2;; + + --ButtonHoverFrom) + buttonHoverFrom=$2 + shift 2;; + --ButtonHoverTo) + buttonHoverTo=$2 + shift 2;; + + --ButtonFocusFrom) + buttonFocusFrom=$2 + shift 2;; + --ButtonFocusTo) + buttonFocusTo=$2 + shift 2;; + + -f|--file) + file=`echo $2 | cut -d'.' --complement -f2-` + shift 2;; + + --) + shift + break;; + esac +done + + +if [ -z "$file" ]; + then echo missing svg file + exit 1 +fi + +isSvgz=0 + +if [ ! -f $file.svgz ] && [ ! -f $file.svg ]; then + echo "you must specify a valid svg" + exit 1 +fi + +if [ -f $file.svgz ]; then + isSvgz=1 +fi + + +if [ $isSvgz = 1 ]; then + mv $file.svgz $file.svg.gz + gunzip $file.svg.gz +fi + +echo Processing $file + +stylesheet=" + .ColorScheme-Text { + color:$textTo; + } + .ColorScheme-Background { + color:$backgroundTo; + } + .ColorScheme-Highlight { + color:$highlightTo; + } + .ColorScheme-ViewText { + color:$viewTextTo; + } + .ColorScheme-ViewBackground { + color:$viewBackgroundTo; + } + .ColorScheme-ViewHover { + color:$viewHoverTo; + } + .ColorScheme-ViewFocus{ + color:$viewFocusTo; + } + .ColorScheme-ButtonText { + color:$buttonTextTo; + } + .ColorScheme-ButtonBackground { + color:$buttonBackgroundTo; + } + .ColorScheme-ButtonHover { + color:$buttonHoverTo; + } + .ColorScheme-ButtonFocus{ + color:$buttonFocusTo; + } + " +colors=($textFrom $backgroundFrom $highlightFrom $viewTextFrom $viewBackgroundFrom $viewHoverFrom $viewFocusFrom $buttonTextFrom $buttonBackgroundFrom $buttonHoverFrom $buttonFocusFrom) +colorNames=(ColorScheme-Text ColorScheme-Background ColorScheme-Highlight ColorScheme-ViewText ColorScheme-ViewBackground ColorScheme-ViewHover ColorScheme-ViewFocus ColorScheme-ButtonText ColorScheme-ButtonBackground ColorScheme-ButtonHover ColorScheme-ButtonFocus) + +reorderXslt=' + + + + + + + + + + + + + + + + + + + + +' +echo $reorderXslt > transform.xsl + +if grep -q '"current-color-scheme"' $file.svg; then + echo replacing the stylesheet + xmlstarlet ed --update "/svg:svg/svg:defs/_:style" -v "$stylesheet" $file.svg > temp.svg +else + echo adding the stylesheet +xmlstarlet ed --subnode "/svg:svg/svg:defs" -t elem -n "style" -v "$stylesheet"\ + --subnode "/svg:svg/svg:defs/style" -t attr -n "type" -v "text/css"\ + --subnode "/svg:svg/svg:defs/style" -t attr -n "id" -v "current-color-scheme" $file.svg > temp.svg +fi + +xmlstarlet tr transform.xsl temp.svg > temp2.svg +mv temp2.svg temp.svg + +for i in ${!colors[@]} +do + xmlstarlet ed --subnode "//*/*[contains(@style, '${colors[i]}') and not (@class)]" -t attr -n "class" -v "${colorNames[i]}" temp.svg > temp2.svg + + mv temp2.svg temp.svg + + sed -i 's/\(style=".*\)fill:'${colors[i]}'/\1fill:currentColor/g' temp.svg + sed -i 's/\(style=".*\)stop-color:'${colors[i]}'/\1stop-color:currentColor/g' temp.svg +done + +rm transform.xsl + +mv temp.svg $file.svg +if [ $isSvgz = 1 ]; then + gzip -n $file.svg + mv $file.svg.gz $file.svgz +fi diff --git a/local/recipes/kde/kf6-ksvg/source/src/tools/currentColorFillFix.sh b/local/recipes/kde/kf6-ksvg/source/src/tools/currentColorFillFix.sh new file mode 100755 index 0000000000..80fa2402a0 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/tools/currentColorFillFix.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +if [ $# -ne 1 ]; + then echo Usage: $0 file.svgz + exit 1 +fi + +if [ ! -f $1 ]; then + echo "you must specify a valid svg" + exit 1 +fi + + +file=`echo $1 | cut -d'.' --complement -f2-` +mv $1 $file.svg.gz +gunzip $file.svg.gz + +echo Processing $file + +/usr/bin/perl -p -i -e "s/color:#[^;]*;(.*)fill:currentColor/\1fill:currentColor/g" $file.svg + +gzip -n $file.svg +mv $file.svg.gz $file.svgz \ No newline at end of file diff --git a/local/recipes/kde/kf6-ksvg/source/src/tools/inkscape extensions/plasmarename.inx b/local/recipes/kde/kf6-ksvg/source/src/tools/inkscape extensions/plasmarename.inx new file mode 100644 index 0000000000..dc5102e455 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/tools/inkscape extensions/plasmarename.inx @@ -0,0 +1,15 @@ + + + PlasmaRename + notmart.filter.plasmarename + + + all + + + + + + diff --git a/local/recipes/kde/kf6-ksvg/source/src/tools/inkscape extensions/plasmarename.py b/local/recipes/kde/kf6-ksvg/source/src/tools/inkscape extensions/plasmarename.py new file mode 100644 index 0000000000..f3f4733de5 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/tools/inkscape extensions/plasmarename.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 + +''' +SPDX-FileCopyrightText: 2009 Marco Martin + +SPDX-License-Identifier: GPL-2.0-or-later +''' + +import inkex + +import pathmodifier + + +class PlasmaNamesEffect(pathmodifier.PathModifier): + """ + Renames 9 selected elements as a plasma theme frame + """ + + def add_arguments(self, pars): + pars.add_argument('--prefix', type=str, default='World', help='Prefix of the selected elements') + + def effect(self): + # Get script's "--prefix" option value. + prefix = self.options.prefix + + # 9 elements: is a frame. 4 elements: is a border hint + positions = [] + if len(self.svg.selection) == 9: + positions = ['topleft', 'left', 'bottomleft', 'top', 'center', 'bottom', 'topright', 'right', 'bottomright'] + elif len(self.svg.selection) == 4: + positions = ['hint-left-margin', 'hint-top-margin', 'hint-bottom-margin', 'hint-right-margin'] + else: + raise inkex.AbortExtension('This extension requires 4 or 9 selected elements.') + + # some heuristics to normalize the values, find the least coords and size + minX = 9999 + minY = 9999 + minWidth = 9999 + minHeight = 9999 + for node in self.svg.selection.values(): + bbox = node.bounding_box() + minX = min(minX, bbox.x.minimum) + minY = min(minY, bbox.y.minimum) + minWidth = min(minWidth, bbox.width) + minHeight = min(minHeight, bbox.height) + + nodedictionary = {} + for node in self.svg.selection.values(): + bbox = node.bounding_box() + x = bbox.x.minimum/minWidth - minX + y = bbox.y.minimum/minHeight - minY + nodedictionary[x*1000 + y] = node + + i = 0 + for _, node in sorted(nodedictionary.items()): + if prefix: + name = '%s-%s' % (prefix, positions[i]) + else: + name = '%s' % (positions[i]) + node.set('id', name) + i += 1 + + +if __name__ == '__main__': + PlasmaNamesEffect().run() diff --git a/local/recipes/kde/kf6-ksvg/source/src/tools/split-plasma-svgs/CMakeLists.txt b/local/recipes/kde/kf6-ksvg/source/src/tools/split-plasma-svgs/CMakeLists.txt new file mode 100644 index 0000000000..13d06d66c6 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/tools/split-plasma-svgs/CMakeLists.txt @@ -0,0 +1,16 @@ +add_executable(split-plasma-svgs) + +target_sources(split-plasma-svgs PRIVATE + split-plasma-svgs.cpp +) + +target_link_libraries(split-plasma-svgs +PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Svg + KF6::Archive + KF6::CoreAddons + KF6::Svg +) + diff --git a/local/recipes/kde/kf6-ksvg/source/src/tools/split-plasma-svgs/split-plasma-svgs.cpp b/local/recipes/kde/kf6-ksvg/source/src/tools/split-plasma-svgs/split-plasma-svgs.cpp new file mode 100644 index 0000000000..721f6df1ca --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/src/tools/split-plasma-svgs/split-plasma-svgs.cpp @@ -0,0 +1,321 @@ +/* SPDX-FileCopyrightText: 2023 Noah Davis + * SPDX-License-Identifier: LGPL-2.0-or-later + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Qt::Literals::StringLiterals; // for ""_L1 + +static KSvg::Svg s_ksvg; +static QSvgRenderer s_renderer; + +// https://developer.mozilla.org/en-US/docs/Web/SVG/Element#renderable_elements +static const QStringList s_renderableElements = {"a"_L1, + "circle"_L1, + "ellipse"_L1, + "foreignObject"_L1, + "g"_L1, + "image"_L1, + "line"_L1, + "path"_L1, + "polygon"_L1, + "polyline"_L1, + "rect"_L1, // excluding + "switch"_L1, + "symbol"_L1, + "text"_L1, + "textPath"_L1, + "tspan"_L1, + "use"_L1}; + +QString joinedStrings(const QStringList &strings) +{ + return strings.join("\", \""_L1).prepend("\""_L1).append("\""_L1); +} + +// Translate the current element to (0,0) if possible. +// FIXME: Does not necessarily translate to (0,0) in one go. +void writeElementTranslation(QXmlStreamReader &reader, QXmlStreamWriter &writer, qreal dx, qreal dy) +{ + if ((qIsFinite(dx) && dx != 0) || (qIsFinite(dy) && dy != 0)) { + writer.writeStartElement(reader.qualifiedName()); // The thing reader has currently read. + auto attributes = reader.attributes(); + bool wasTranslated = false; + QString svgTranslate = "translate(%1,%2)"_L1.arg(QString::number(dx), QString::number(dy)); + for (int i = 0; i < attributes.size(); ++i) { + if (attributes[i].qualifiedName() == "transform"_L1) { + auto svgTransform = attributes[i].value().toString(); + if (!svgTransform.isEmpty()) { + svgTransform += " "_L1; + } + attributes[i] = {"transform"_L1, svgTransform + svgTranslate}; + wasTranslated = true; + } + writer.writeAttribute(attributes[i]); + } + if (!wasTranslated) { + writer.writeAttribute("transform"_L1, svgTranslate); + } + } else { + writer.writeCurrentToken(reader); // The thing reader has currently read. + } +} + +QMap splitSvg(const QString &inputArg, const QByteArray &inputContents) +{ + s_renderer.load(inputContents); + QMap outputMap; // filename, contents + QXmlStreamReader reader(inputContents); + reader.setNamespaceProcessing(false); + + QString stylesheet; + + while (!reader.atEnd() && !reader.hasError()) { + reader.readNextStartElement(); + if (reader.hasError()) { + break; + } + + const auto qualifiedName = reader.qualifiedName(); + const auto attributes = reader.attributes(); + QString id = attributes.value("id"_L1).toString(); + + // Skip elements without IDs since they aren't icons. + // Make sure you don't miss children when you make the output contents though. + // Also skip hints and groups with the layer1 ID + if (id.isEmpty() || id.startsWith("hint-"_L1) || (qualifiedName == "g"_L1 && id == "layer1"_L1)) { + continue; + } + + // Some SVGs have multiple stylesheets. + // They really shouldn't, but that's just how it is sometimes. + // The last stylesheet with the correct ID is the one we will use. + static const auto s_stylesheetId = "current-color-scheme"_L1; + if (qualifiedName == "style"_L1 && id == s_stylesheetId) { + reader.readNext(); + auto text = reader.text(); + if (!text.isEmpty()) { + stylesheet = text.toString(); + } + continue; + } + + // ignore non-renderable elements + if (!s_renderableElements.contains(qualifiedName)) { + continue; + } + + // NOTE: Does not include its own transform. + QTransform transform = s_renderer.transformForElement(id); + QRectF mappedRect = transform.mapRect(s_renderer.boundsOnElement(id)); + + // Skip invisible renderable elements. + if (mappedRect.isEmpty()) { + continue; + } + + QString outputFilename = id + ".svg"_L1; + QByteArray outputContents; + QXmlStreamWriter writer(&outputContents); + // Start writing document + writer.setAutoFormatting(true); + writer.writeStartDocument(); + + // + writer.writeStartElement("svg"_L1); + writer.writeDefaultNamespace("http://www.w3.org/2000/svg"_L1); + writer.writeNamespace("http://www.w3.org/1999/xlink"_L1, "xlink"_L1); + writer.writeNamespace("http://creativecommons.org/ns#"_L1, "cc"_L1); + writer.writeNamespace("http://purl.org/dc/elements/1.1/"_L1, "dc"_L1); + writer.writeNamespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#"_L1, "rdf"_L1); + writer.writeNamespace("http://www.inkscape.org/namespaces/inkscape"_L1, "inkscape"_L1); + writer.writeNamespace("http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"_L1, "sodipodi"_L1); + writer.writeAttribute("width"_L1, QString::number(mappedRect.width())); + writer.writeAttribute("height"_L1, QString::number(mappedRect.height())); + + // + + // Translation via parent + auto dx = -mappedRect.x(); + auto dy = -mappedRect.y(); + writeElementTranslation(reader, writer, dx, dy); + + // Write contents until we're no longer writing the current element or any of its children. + int depth = 0; + while (depth >= 0 && !reader.atEnd() && !reader.hasError()) { + reader.readNext(); + if (reader.isStartElement()) { + ++depth; + } + if (reader.isEndElement()) { + --depth; + } + writer.writeCurrentToken(reader); + } + + if (reader.hasError()) { + qWarning() << inputArg << "has an error:" << reader.errorString(); + break; + } + + writer.writeEndElement(); + // + + writer.writeEndDocument(); + + if (!outputFilename.isEmpty() && !outputContents.isEmpty()) { + outputMap.insert(outputFilename, outputContents); + } + } + return outputMap; +} + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + + KAboutData aboutData(app.applicationName(), + app.applicationName(), + "1.0"_L1, + "Splits Plasma/KSVG SVGs into individual SVGs"_L1, + KAboutLicense::LGPL_V2, + "2023 Noah Davis"_L1); + aboutData.addAuthor("Noah Davis"_L1, {}, "noahadvs@gmail.com"_L1); + KAboutData::setApplicationData(aboutData); + + QCommandLineParser commandLineParser; + commandLineParser.addPositionalArgument("inputs"_L1, "Input files (separated by spaces)"_L1, "inputs..."_L1); + commandLineParser.addPositionalArgument("output"_L1, + "Output folder (optional, must exist). The default output folder is the current working directory."_L1, + "[output]"_L1); + aboutData.setupCommandLine(&commandLineParser); + + commandLineParser.process(app); + aboutData.processCommandLine(&commandLineParser); + + const QStringList &positionalArguments = commandLineParser.positionalArguments(); + if (positionalArguments.isEmpty()) { + qWarning() << "The arguments are missing."; + return 1; + } + + QFileInfo lastArgInfo(positionalArguments.last()); + if (positionalArguments.size() == 1 && lastArgInfo.isDir()) { + qWarning() << "Input file arguments are missing."; + return 1; + } + + QDir outputDir = lastArgInfo.isDir() ? lastArgInfo.absoluteFilePath() : QDir::currentPath(); + QFileInfo outputDirInfo(outputDir.absolutePath()); + if (!outputDirInfo.isWritable()) { + // Using the arg instead of just path or filename so the user sees what they typed. + auto output = lastArgInfo.isDir() ? positionalArguments.last() : QDir::currentPath(); + qWarning() << output << "is not a writable output folder."; + return 1; + } + + QStringList inputArgs; + QStringList ignoredArgs; + for (int i = 0; i < positionalArguments.size() - lastArgInfo.isDir(); ++i) { + if (!QFileInfo::exists(positionalArguments[i])) { + ignoredArgs << positionalArguments[i]; + continue; + } + inputArgs << positionalArguments[i]; + } + + if (inputArgs.isEmpty()) { + qWarning() << "None of the input files could be found."; + return 1; + } + + if (!ignoredArgs.isEmpty()) { + // Using the arg instead of path or filename so the user sees what they typed. + qWarning() << "The following input files could not be found:"; + qWarning().noquote() << joinedStrings(ignoredArgs); + } + + bool wasAnyFileWritten = false; + for (const QString &inputArg : inputArgs) { + QFileInfo inputInfo(inputArg); + + const QString &absoluteInputPath = inputInfo.absoluteFilePath(); + // Avoid reading from a theme with relative paths by accident. + s_ksvg.setImagePath(absoluteInputPath); + if (!s_ksvg.isValid()) { + qWarning() << inputArg << "is not a valid Plasma theme SVG."; + continue; + } + + KCompressionDevice inputFile(absoluteInputPath, KCompressionDevice::GZip); + if (!inputFile.open(QIODevice::ReadOnly)) { + qWarning() << inputArg << "could not be read."; + continue; + } + const auto outputMap = splitSvg(inputArg, inputFile.readAll()); + inputFile.close(); + + if (outputMap.isEmpty()) { + qWarning() << inputArg << "could not be split."; + continue; + } + + const auto outputSubDirPath = outputDir.absoluteFilePath(inputInfo.baseName()); + outputDir.mkpath(outputSubDirPath); + QDir outputSubDir(outputSubDirPath); + QStringList unwrittenFiles; + QStringList invalidSvgs; + for (auto it = outputMap.cbegin(); it != outputMap.cend(); ++it) { + const QString &key = it.key(); + const QByteArray &value = it.value(); + if (key.isEmpty() || value.isEmpty()) { + unwrittenFiles << key; + continue; + } + const auto absoluteOutputPath = outputSubDir.absoluteFilePath(key); + QFile outputFile(absoluteOutputPath); + if (!outputFile.open(QIODevice::WriteOnly)) { + unwrittenFiles << key; + continue; + } + wasAnyFileWritten |= outputFile.write(value); + outputFile.close(); + s_renderer.load(absoluteOutputPath); + if (!s_renderer.isValid()) { + // Write it even if it isn't valid so that the user can examine the output. + invalidSvgs << key; + } + } + if (unwrittenFiles.size() == outputMap.size()) { + qWarning().nospace() << "No files could be written for " << inputArg << "."; + } else if (!unwrittenFiles.isEmpty()) { + qWarning().nospace() << "The following files could not be written for " << inputArg << ":"; + qWarning().noquote() << joinedStrings(unwrittenFiles); + } + if (!invalidSvgs.isEmpty()) { + qWarning().nospace() << "The following files written for " << inputArg << " are not valid SVGs:"; + qWarning().noquote() << joinedStrings(invalidSvgs); + } + } + + return wasAnyFileWritten ? 0 : 1; +} diff --git a/local/recipes/kde/kf6-ksvg/source/tests/frames.qml b/local/recipes/kde/kf6-ksvg/source/tests/frames.qml new file mode 100644 index 0000000000..3edea960ad --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/tests/frames.qml @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2020 David Edmundson + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + + +import QtQuick 2.0 +import QtQuick.Layouts 1.1 + +import org.kde.plasma.core as PlasmaCore + +Item +{ + width: 500 + height: 500 + + + + Grid { + anchors.fill: parent + columns: 3 + + Repeater { + model: ["widgets/background", + "widgets/panel-background", + "opaque/widgets/panel-background", + "widgets/tooltip", + "opaque/widgets/tooltip" + ] + + delegate: PlasmaCore.FrameSvgItem { + width: 100 + height: 100 + imagePath: modelData + } + } + } +} + diff --git a/local/recipes/kde/kf6-ksvg/source/tests/selected_svg.qml b/local/recipes/kde/kf6-ksvg/source/tests/selected_svg.qml new file mode 100644 index 0000000000..43ee95ef69 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/tests/selected_svg.qml @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2016 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.2 +import QtQuick.Controls 2.15 as Controls +import org.kde.ksvg 1.0 as KSvg + +KSvg.FrameSvgItem { + id: root + imagePath: "widgets/background" + state: KSvg.Svg.Normal + width: 600 + height: 800 + + Column { + anchors.centerIn: parent + spacing: 4 + + Controls.Button { + text: "Switch Selected State" + onClicked: root.state = (root.state == KSvg.Svg.Selected ? KSvg.Svg.Normal : KSvg.Svg.Selected) + } + + KSvg.SvgItem { + svg: KSvg.Svg { + id: svg + imagePath: "icons/phone" + state: root.state + } + } + } +} diff --git a/local/recipes/kde/kf6-ksvg/source/tests/shadows.qml b/local/recipes/kde/kf6-ksvg/source/tests/shadows.qml new file mode 100644 index 0000000000..dfdaf4a086 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/tests/shadows.qml @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2021 Arjen Hiemstra + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import org.kde.ksvg 1.0 as KSvg + +Rectangle { + color: "white" + width: 600 + height: 600 + + GridLayout { + anchors.fill: parent + columns: 3 + + Repeater { + model: [ + "shadow-topleft", + "shadow-top", + "shadow-topright", + "shadow-left", + "shadow-middle", + "shadow-right", + "shadow-bottomleft", + "shadow-bottom", + "shadow-bottomright" + ] + + KSvg.SvgItem { + elementId: modelData + + svg: KSvg.Svg { + imagePath: "dialogs/background" + } + } + } + } +} + diff --git a/local/recipes/kde/kf6-ksvg/source/tests/testborders.qml b/local/recipes/kde/kf6-ksvg/source/tests/testborders.qml new file mode 100644 index 0000000000..715730f549 --- /dev/null +++ b/local/recipes/kde/kf6-ksvg/source/tests/testborders.qml @@ -0,0 +1,93 @@ +/* + SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + + +import QtQuick 2.0 +import QtQuick.Layouts 1.1 +import QtQuick.Controls 2.15 + +import org.kde.plasma.core as PlasmaCore + +Item +{ + width: 500 + height: 500 + + PlasmaCore.FrameSvgItem { + id: theItem + + imagePath: "widgets/background" + anchors { + fill: parent + margins: 10 + } + + Button { + text: "left" + checkable: true + checked: true + anchors { + horizontalCenterOffset: -50 + centerIn: parent + } + onClicked: { + if (checked) + theItem.enabledBorders |= PlasmaCore.FrameSvg.LeftBorder; + else + theItem.enabledBorders &=~PlasmaCore.FrameSvg.LeftBorder; + } + } + Button { + text: "right" + checkable: true + checked: true + + anchors { + horizontalCenterOffset: 50 + centerIn: parent + } + onClicked: { + if (checked) + theItem.enabledBorders |= PlasmaCore.FrameSvg.RightBorder; + else + theItem.enabledBorders &=~PlasmaCore.FrameSvg.RightBorder; + } + } + Button { + text: "top" + checkable: true + checked: true + + anchors { + verticalCenterOffset: -50 + centerIn: parent + } + onClicked: { + if (checked) + theItem.enabledBorders |= PlasmaCore.FrameSvg.TopBorder; + else + theItem.enabledBorders &=~PlasmaCore.FrameSvg.TopBorder; + } + } + Button { + text: "bottom" + checkable: true + checked: true + + anchors { + verticalCenterOffset: 50 + centerIn: parent + } + onClicked: { + if (checked) + theItem.enabledBorders |= PlasmaCore.FrameSvg.BottomBorder; + else + theItem.enabledBorders &=~PlasmaCore.FrameSvg.BottomBorder; + } + } + } +} + diff --git a/local/recipes/kde/kf6-ktextwidgets/source/CMakeLists.txt b/local/recipes/kde/kf6-ktextwidgets/source/CMakeLists.txt index 0e64d935dd..536e248b54 100644 --- a/local/recipes/kde/kf6-ktextwidgets/source/CMakeLists.txt +++ b/local/recipes/kde/kf6-ktextwidgets/source/CMakeLists.txt @@ -90,6 +90,7 @@ find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) +find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED) if (WITH_TEXT_TO_SPEECH) find_package(Qt6 ${REQUIRED_QT_VERSION} CONFIG REQUIRED TextToSpeech) diff --git a/local/recipes/kde/kf6-kxmlgui/source/src/kswitchlanguagedialog_p.cpp b/local/recipes/kde/kf6-kxmlgui/source/src/kswitchlanguagedialog_p.cpp index 8e3a00a714..031d5ed9b3 100644 --- a/local/recipes/kde/kf6-kxmlgui/source/src/kswitchlanguagedialog_p.cpp +++ b/local/recipes/kde/kf6-kxmlgui/source/src/kswitchlanguagedialog_p.cpp @@ -74,10 +74,10 @@ void initializeLanguages() // Ideally setting the LANGUAGE would change the default QLocale too // but unfortunately this is too late since the QCoreApplication constructor // already created a QLocale at this stage so we need to set the reset it -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // by triggering the creation and destruction of a QSystemLocale +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // by triggering the creation and destruction of a QSystemLocale // this is highly dependent on Qt internals, so may break, but oh well -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// QSystemLocale *dummy = new QSystemLocale(); -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// delete dummy; +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// QSystemLocale *dummy = new QSystemLocale(); +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// delete dummy; } } diff --git a/local/recipes/kde/kf6-solid/source/CMakeLists.txt b/local/recipes/kde/kf6-solid/source/CMakeLists.txt index c689125951..5f5556bc24 100644 --- a/local/recipes/kde/kf6-solid/source/CMakeLists.txt +++ b/local/recipes/kde/kf6-solid/source/CMakeLists.txt @@ -78,7 +78,7 @@ set_package_properties(PList PROPERTIES if (CMAKE_SYSTEM_NAME MATCHES Linux) # Used by the UDisks backend on Linux - ###########################################################################find_package(LibMount) + ############################################################################find_package(LibMount) set_package_properties(LibMount PROPERTIES TYPE REQUIRED) endif() diff --git a/local/recipes/kde/kwin/source/.gitignore b/local/recipes/kde/kwin/source/.gitignore new file mode 100644 index 0000000000..b0e04b182d --- /dev/null +++ b/local/recipes/kde/kwin/source/.gitignore @@ -0,0 +1,30 @@ +# 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* +.idea +.clang-format +/build*/ +/compile_commands.json +.clangd +/cmake-build* +.cache +.vscode +.qmlls.ini diff --git a/local/recipes/kde/kwin/source/.gitlab-ci.yml b/local/recipes/kde/kwin/source/.gitlab-ci.yml new file mode 100644 index 0000000000..4c78b846a5 --- /dev/null +++ b/local/recipes/kde/kwin/source/.gitlab-ci.yml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: None +# SPDX-License-Identifier: CC0-1.0 + +include: + - project: sysadmin/ci-utilities + file: + - /gitlab-templates/linux-qt6.yml + # - /gitlab-templates/linux-qt6-next.yml + - /gitlab-templates/freebsd-qt6.yml + - /gitlab-templates/xml-lint.yml + - /gitlab-templates/yaml-lint.yml + - /gitlab-templates/qml-lint.yml + - /gitlab-templates/cppcheck.yml + +suse_tumbleweed_qt611_reduced_featureset: + extends: suse_tumbleweed_qt611 + script: + - git config --global --add safe.directory $CI_PROJECT_DIR + - python3 -u ci-utilities/run-ci-build.py --project $CI_PROJECT_NAME --branch $CI_COMMIT_REF_NAME --platform Linux --extra-cmake-args="-DKWIN_BUILD_KCMS=OFF -DKWIN_BUILD_SCREENLOCKER=OFF -DKWIN_BUILD_TABBOX=OFF -DKWIN_BUILD_ACTIVITIES=OFF -DKWIN_BUILD_RUNNERS=OFF -DKWIN_BUILD_NOTIFICATIONS=OFF -DKWIN_BUILD_GLOBALSHORTCUTS=OFF -DKWIN_BUILD_X11=OFF -DKWIN_BUILD_EIS=OFF -DKWIN_BUILD_QACCESSIBILITYCLIENT=OFF" --skip-publishing --only-build diff --git a/local/recipes/kde/kwin/source/.kde-ci.yml b/local/recipes/kde/kwin/source/.kde-ci.yml new file mode 100644 index 0000000000..82cb960039 --- /dev/null +++ b/local/recipes/kde/kwin/source/.kde-ci.yml @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: None +# SPDX-License-Identifier: CC0-1.0 + +Dependencies: + - 'on': ['@all'] + 'require': + 'frameworks/breeze-icons': '@latest-kf6' + 'frameworks/extra-cmake-modules': '@latest-kf6' + 'frameworks/kcmutils': '@latest-kf6' + 'frameworks/kconfig': '@latest-kf6' + 'frameworks/kcoreaddons': '@latest-kf6' + 'frameworks/kcrash': '@latest-kf6' + 'frameworks/kdeclarative': '@latest-kf6' + 'frameworks/kdoctools': '@latest-kf6' + 'frameworks/kglobalaccel': '@latest-kf6' + 'frameworks/ki18n': '@latest-kf6' + 'frameworks/kidletime': '@latest-kf6' + 'frameworks/knewstuff': '@latest-kf6' + 'frameworks/knotifications': '@latest-kf6' + 'frameworks/kpackage': '@latest-kf6' + 'frameworks/kservice': '@latest-kf6' + 'frameworks/ksvg': '@latest-kf6' + 'frameworks/kwidgetsaddons': '@latest-kf6' + 'frameworks/kwindowsystem': '@latest-kf6' + 'frameworks/kxmlgui': '@latest-kf6' + 'frameworks/kcolorscheme': '@latest-kf6' + 'libraries/libqaccessibilityclient': '@latest-kf6' + 'libraries/plasma-wayland-protocols': '@latest-kf6' + 'plasma/plasma-activities': '@same' + 'plasma/kdecoration': '@same' + 'plasma/kglobalacceld': '@same' + 'plasma/knighttime': '@same' + 'plasma/kpipewire': '@same' + 'plasma/kscreenlocker': '@same' + 'plasma/kwayland': '@same' + 'third-party/wayland': '@latest' + 'third-party/wayland-protocols': '@latest' + +RuntimeDependencies: + - 'on': ['@all'] + 'require': + 'frameworks/kirigami': '@latest-kf6' + 'plasma/aurorae': '@same' + 'plasma/breeze': '@same' + 'plasma/libplasma': '@same' + 'plasma/plasma-workspace': '@same' # kscreenlocker needs it + +Options: + ctest-arguments: '--repeat until-pass:3 --timeout 90' + use-ccache: True + cppcheck-arguments: '--enable=warning,style,performance --library=kwin-cppcheck.xml --suppress-xml=cppcheck-suppressions.xml' + cppcheck-ignore-files: + - autotests + require-passing-tests-on: ['Linux/Qt6'] + run-qmllint: True diff --git a/local/recipes/kde/kwin/source/CMakeLists.txt b/local/recipes/kde/kwin/source/CMakeLists.txt new file mode 100644 index 0000000000..326ea13df7 --- /dev/null +++ b/local/recipes/kde/kwin/source/CMakeLists.txt @@ -0,0 +1,538 @@ +cmake_minimum_required(VERSION 3.16) + +set(PROJECT_VERSION "6.6.5") # Handled by release scripts +project(KWin VERSION ${PROJECT_VERSION}) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(PROJECT_DEP_VERSION "6.6.5") +set(QT_MIN_VERSION "6.10.0") +set(KF6_MIN_VERSION "6.10.0") +set(KDE_COMPILERSETTINGS_LEVEL "5.82") + +find_package(ECM ${KF6_MIN_VERSION} REQUIRED NO_MODULE) + +# where to look first for cmake modules, before ${CMAKE_ROOT}/Modules/ is checked +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules ${ECM_MODULE_PATH}) + +include(CMakeDependentOption) +include(CMakePackageConfigHelpers) +include(FeatureSummary) +include(WriteBasicConfigVersionFile) +include(GenerateExportHeader) +include(CheckCXXSourceCompiles) +include(CheckCXXCompilerFlag) +include(CheckIncludeFile) +include(CheckIncludeFiles) +include(CheckSymbolExists) + +include(KDEInstallDirs) +include(KDECMakeSettings) +include(KDECompilerSettings NO_POLICY_SCOPE) +include(KDEClangFormat) +include(KDEGitCommitHooks) + +include(ECMFindQmlModule) +include(ECMInstallIcons) +include(ECMOptionalAddSubdirectory) +include(ECMConfiguredInstall) +include(ECMQtDeclareLoggingCategory) +include(ECMSetupQtPluginMacroNames) +include(ECMSetupVersion) +##include(ECMQmlModule) +include(ECMGenerateQmlTypes) +include(ECMDeprecationSettings) +include(ECMGenerateQDoc) + +option(KWIN_BUILD_DECORATIONS "Enable building of KWin decorations." ON) +option(KWIN_BUILD_KCMS "Enable building of KWin configuration modules." ON) +option(KWIN_BUILD_NOTIFICATIONS "Enable building of KWin with knotifications support" ON) +option(KWIN_BUILD_SCREENLOCKER "Enable building of KWin lockscreen functionality" ON) +option(KWIN_BUILD_TABBOX "Enable building of KWin Tabbox functionality" ON) +option(KWIN_BUILD_X11 "Enable building X11 common code and Xwayland support" ON) +option(KWIN_BUILD_GLOBALSHORTCUTS "Enable building of KWin with global shortcuts support" ON) +option(KWIN_BUILD_RUNNERS "Enable building of KWin with krunner support" ON) + +find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS + Concurrent + Core + Core5Compat + DBus + Quick + WaylandClient + Widgets + Svg +) + +if (Qt6Gui_VERSION VERSION_GREATER_EQUAL "6.10.0") + find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE) +endif() + +find_package(Qt6Test ${QT_MIN_VERSION} CONFIG QUIET) +set_package_properties(Qt6Test PROPERTIES + PURPOSE "Required for tests" + TYPE OPTIONAL +) +add_feature_info("Qt6Test" Qt6Test_FOUND "Required for building tests") +if (NOT Qt6Test_FOUND) + set(BUILD_TESTING OFF CACHE BOOL "Build the testing tree.") +endif() + +if (BUILD_TESTING) + find_package(KPipeWire) + + if (Qt6WaylandClient_VERSION VERSION_GREATER_EQUAL "6.10.0") + find_package(Qt6WaylandClientPrivate ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE) + endif() +endif() + +# required frameworks by Core +find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS + Auth + ColorScheme + Config + CoreAddons + Crash + DBusAddons + GlobalAccel + GuiAddons + I18n + IdleTime + Package + Service + Svg + WidgetsAddons + WindowSystem +) +# required frameworks by config modules +if(KWIN_BUILD_KCMS) + find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS + Declarative + KCMUtils + NewStuff + Service + XmlGui + ) +endif() + +if(KWIN_BUILD_TABBOX AND KWIN_BUILD_KCMS) + find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS + WidgetsAddons + ) +endif() + +find_package(KNightTime ${PROJECT_DEP_VERSION} CONFIG) +set_package_properties(KNightTime PROPERTIES + PURPOSE "Needed for Night Light" + TYPE REQUIRED +) + +find_package(Threads) +set_package_properties(Threads PROPERTIES + PURPOSE "Needed for VirtualTerminal support in KWin Wayland" + TYPE REQUIRED +) + +find_package(KWayland ${PROJECT_DEP_VERSION} CONFIG) +set_package_properties(KWayland PROPERTIES + PURPOSE "Required to build wayland platform plugin and tests" + TYPE REQUIRED +) + +# optional frameworks +find_package(PlasmaActivities ${PROJECT_DEP_VERSION} CONFIG) +set_package_properties(PlasmaActivities PROPERTIES + PURPOSE "Enable building of KWin with kactivities support" + TYPE OPTIONAL +) +add_feature_info("PlasmaActivities" PlasmaActivities_FOUND "Enable building of KWin with kactivities support") + +find_package(KF6DocTools ${KF6_MIN_VERSION} CONFIG) +set_package_properties(KF6DocTools PROPERTIES + PURPOSE "Enable building documentation" + TYPE OPTIONAL +) +add_feature_info("KF6DocTools" KF6DocTools_FOUND "Enable building documentation") + +find_package(KF6Kirigami ${KF6_MIN_VERSION} CONFIG) +set_package_properties(KF6Kirigami PROPERTIES + DESCRIPTION "A QtQuick based components set" + PURPOSE "Required at runtime for several QML effects" + TYPE RUNTIME +) +find_package(Plasma ${PROJECT_DEP_VERSION} CONFIG) +set_package_properties(Plasma PROPERTIES + DESCRIPTION "A QtQuick based components set" + PURPOSE "Required at runtime for several QML effects" + TYPE RUNTIME +) + +find_package(KDecoration3 ${PROJECT_DEP_VERSION} CONFIG REQUIRED) + +if (KWIN_BUILD_DECORATIONS) + find_package(Breeze ${PROJECT_DEP_VERSION} CONFIG) + set_package_properties(Breeze PROPERTIES + TYPE RUNTIME + PURPOSE "For setting the default window decoration plugin" + ) + + find_package(Aurorae ${PROJECT_DEP_VERSION} CONFIG) + set_package_properties(Aurorae PROPERTIES + TYPE RUNTIME + PURPOSE "Provides support for decorations downloaded from the internet" + ) +endif() + +find_package(EGL) +set_package_properties(EGL PROPERTIES + TYPE REQUIRED + PURPOSE "Required to build KWin with EGL support" +) + +find_package(epoxy 1.3) +set_package_properties(epoxy PROPERTIES + DESCRIPTION "libepoxy" + URL "https://github.com/anholt/libepoxy" + TYPE REQUIRED + PURPOSE "OpenGL dispatch library" +) + +find_package(Wayland 1.24) +set_package_properties(Wayland PROPERTIES + TYPE REQUIRED + PURPOSE "Required for building KWin with Wayland support" +) + +find_package(WaylandProtocols 1.45) +set_package_properties(WaylandProtocols PROPERTIES + TYPE REQUIRED + PURPOSE "Collection of Wayland protocols that add functionality not available in the Wayland core protocol" + URL "https://gitlab.freedesktop.org/wayland/wayland-protocols/" +) +if (WaylandProtocols_VERSION VERSION_GREATER_EQUAL 1.47) + set(HAVE_WAYLAND_PROTOCOLS_147 1) +else() + set(HAVE_WAYLAND_PROTOCOLS_147 0) +endif() + +find_package(PlasmaWaylandProtocols 1.20.0 CONFIG) +set_package_properties(PlasmaWaylandProtocols PROPERTIES + TYPE REQUIRED + PURPOSE "Collection of Plasma-specific Wayland protocols" + URL "https://invent.kde.org/libraries/plasma-wayland-protocols/" +) + +find_package(XKB 0.7.0) +set_package_properties(XKB PROPERTIES + TYPE REQUIRED + PURPOSE "Required for building KWin with Wayland support" +) +if (XKB_VERSION VERSION_GREATER_EQUAL 1.5.0) + set(HAVE_XKBCOMMON_NO_SECURE_GETENV 1) +else() + set(HAVE_XKBCOMMON_NO_SECURE_GETENV 0) +endif() + +find_package(Canberra REQUIRED) + +if (KWIN_BUILD_X11) + pkg_check_modules(XKBX11 IMPORTED_TARGET xkbcommon-x11 REQUIRED) + add_feature_info(XKBX11 XKBX11_FOUND "Required for handling keyboard events in X11 backend") + + # All the required XCB components + find_package(XCB 1.10 REQUIRED COMPONENTS + COMPOSITE + CURSOR + DRI3 + ICCCM + KEYSYMS + PRESENT + RANDR + RENDER + RES + SHAPE + SHM + SYNC + XCB + XFIXES + XINERAMA + XINPUT + ) + set_package_properties(XCB PROPERTIES TYPE REQUIRED) + + find_package(X11_XCB) + set_package_properties(X11_XCB PROPERTIES + PURPOSE "Required for building X11 windowed backend of kwin_wayland" + TYPE OPTIONAL + ) + + find_package(Xwayland 23.1.0) + set_package_properties(Xwayland PROPERTIES + URL "https://x.org" + DESCRIPTION "Xwayland X server" + TYPE RUNTIME + PURPOSE "Needed for running kwin_wayland" + ) + set(HAVE_XWAYLAND_ENABLE_EI_PORTAL ${Xwayland_HAVE_ENABLE_EI_PORTAL}) + + # for kwin internal things + set(HAVE_X11_XCB ${X11_XCB_FOUND}) + + find_package(X11) + set_package_properties(X11 PROPERTIES + DESCRIPTION "X11 libraries" + URL "https://www.x.org" + TYPE REQUIRED + ) + + # Scripts to run on XWayland startup + set(XWAYLAND_SESSION_SCRIPTS "/etc/xdg/Xwayland-session.d") +endif() + +find_package(Libinput 1.28) +set_package_properties(Libinput PROPERTIES TYPE REQUIRED PURPOSE "Required for input handling on Wayland.") + +find_package(Libeis-1.0 1.4) +set_package_properties(Libeis-1.0 PROPERTIES TYPE OPTIONAL PURPOSE "Required for emulated input handling.") + +find_package(UDev) +set_package_properties(UDev PROPERTIES + URL "https://www.freedesktop.org/wiki/Software/systemd/" + DESCRIPTION "Linux device library." + TYPE REQUIRED + PURPOSE "Required for input handling on Wayland." +) + +find_package(Libdrm 2.4.118) +set_package_properties(Libdrm PROPERTIES TYPE REQUIRED PURPOSE "Required for drm output on Wayland.") +if (Libdrm_VERSION VERSION_GREATER_EQUAL 2.4.127) + set(HAVE_LIBDRM_FAUX 1) +else() + set(HAVE_LIBDRM_FAUX 0) +endif() + + +find_package(gbm) +set_package_properties(gbm PROPERTIES TYPE REQUIRED PURPOSE "Required for egl output of drm backend.") +if (gbm_VERSION VERSION_GREATER_EQUAL 21.1) + set(HAVE_GBM_BO_GET_FD_FOR_PLANE 1) +else() + set(HAVE_GBM_BO_GET_FD_FOR_PLANE 0) +endif() +if (gbm_VERSION VERSION_GREATER_EQUAL 21.3) + set(HAVE_GBM_BO_CREATE_WITH_MODIFIERS2 1) +else() + set(HAVE_GBM_BO_CREATE_WITH_MODIFIERS2 0) +endif() + +pkg_check_modules(Libxcvt IMPORTED_TARGET libxcvt>=0.1.1 REQUIRED) +add_feature_info(Libxcvt Libxcvt_FOUND "Required for generating modes in the drm backend") + +add_feature_info("XInput" X11_Xi_FOUND "Required for poll-free mouse cursor updates") +set(HAVE_X11_XINPUT ${X11_Xinput_FOUND}) + +find_package(lcms2) +set_package_properties(lcms2 PROPERTIES + DESCRIPTION "Small-footprint color management engine" + URL "http://www.littlecms.com" + TYPE REQUIRED + PURPOSE "Required for the color management system" +) + +find_package(hwdata) +set_package_properties(hwdata PROPERTIES + TYPE RUNTIME + PURPOSE "Runtime-only dependency needed for mapping monitor hardware vendor IDs to full names" + URL "https://github.com/vcrhonek/hwdata" +) + +find_package(QAccessibilityClient6 CONFIG) +set_package_properties(QAccessibilityClient6 PROPERTIES + URL "https://commits.kde.org/libqaccessibilityclient" + DESCRIPTION "KDE client-side accessibility library" + TYPE OPTIONAL + PURPOSE "Required to enable accessibility features" +) + +pkg_check_modules(libsystemd IMPORTED_TARGET libsystemd) +add_feature_info(libsystemd libsystemd_FOUND "Required for setting up the service watchdog") + +if(KWIN_BUILD_GLOBALSHORTCUTS) + find_package(KGlobalAccelD REQUIRED) +endif() + +pkg_check_modules(libdisplayinfo REQUIRED IMPORTED_TARGET libdisplay-info>=0.2.0) +add_feature_info(libdisplayinfo libdisplayinfo_FOUND "EDID and DisplayID library: https://gitlab.freedesktop.org/emersion/libdisplay-info") + +pkg_check_modules(PipeWire IMPORTED_TARGET libpipewire-0.3>=1.0.9) +add_feature_info(PipeWire PipeWire_FOUND "Required for Wayland screencasting") + +if (KWIN_BUILD_NOTIFICATIONS) + find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS Notifications) +endif() + +if (KWIN_BUILD_SCREENLOCKER) + find_package(KScreenLocker CONFIG) + set_package_properties(KScreenLocker PROPERTIES + TYPE REQUIRED + PURPOSE "For screenlocker integration in kwin_wayland" + ) +endif() + +pkg_check_modules(libevdev IMPORTED_TARGET libevdev) +add_feature_info(libevdev libevdev_FOUND "Required for game controller support") + +ecm_find_qmlmodule(QtQuick 2.3) +ecm_find_qmlmodule(QtQuick.Controls 2.15) +ecm_find_qmlmodule(QtQuick.Layouts 1.3) +ecm_find_qmlmodule(QtQuick.Window 2.1) +ecm_find_qmlmodule(org.kde.kquickcontrolsaddons 2.0) +ecm_find_qmlmodule(org.kde.plasma.core 2.0) +ecm_find_qmlmodule(org.kde.plasma.components 2.0) + +cmake_dependent_option(KWIN_BUILD_ACTIVITIES "Enable building of KWin with kactivities support" ON "PlasmaActivities_FOUND" OFF) +cmake_dependent_option(KWIN_BUILD_EIS "Enable building KWin with libeis support" ON "Libeis-1.0_FOUND" OFF) +cmake_dependent_option(KWIN_BUILD_QACCESSIBILITYCLIENT "Enable building KWin with libqaccessibilitysupport" ON "QAccessibilityClient6_FOUND" OFF) +cmake_dependent_option(KWIN_BUILD_GAMECONTROLLER "Enable building of KWin Game Controller functionality" ON "libevdev_FOUND" OFF) + +include_directories(BEFORE + ${CMAKE_CURRENT_BINARY_DIR}/src/wayland + ${CMAKE_CURRENT_BINARY_DIR}/src + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +check_symbol_exists(SCHED_RESET_ON_FORK "sched.h" HAVE_SCHED_RESET_ON_FORK) +add_feature_info("SCHED_RESET_ON_FORK" + HAVE_SCHED_RESET_ON_FORK + "Required for running kwin_wayland with real-time scheduling") + +# clang < 16 does not support ranges and compiling KWin will fail in the middle of the build. +# clang 14 is still the default clang version on KDE Neon and Clazy is build with it +check_cxx_source_compiles(" +#include +#include + +int main() { + std::vector numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + auto even_numbers = numbers | std::views::filter([](int n) { return n % 2 == 0; }); + auto squared_numbers = even_numbers | std::views::transform([](int n) { return n * n; }); + return 0; +} +" HAS_RANGES_SUPPORT) +if(NOT HAS_RANGES_SUPPORT) + message(FATAL_ERROR "Compiler does not support C++20 ranges") +endif() + +check_cxx_source_compiles(" +#include +#include +#include + +int main() { + const int size = 10; + int fd = memfd_create(\"test\", MFD_CLOEXEC | MFD_ALLOW_SEALING); + ftruncate(fd, size); + fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE | F_SEAL_SEAL); + mmap(nullptr, size, PROT_WRITE, MAP_SHARED, fd, 0); + return 0; +}" HAVE_MEMFD) + +check_cxx_compiler_flag(-Wno-unused-parameter COMPILER_UNUSED_PARAMETER_SUPPORTED) +if (COMPILER_UNUSED_PARAMETER_SUPPORTED) + add_compile_options(-Wno-unused-parameter) +endif() + +add_definitions( + -DQT_NO_KEYWORDS + -DQT_USE_QSTRINGBUILDER + -DQT_NO_URL_CAST_FROM_STRING + -DQT_NO_CAST_TO_ASCII + -DQT_NO_FOREACH + + # Prevent EGL headers from including platform headers, in particular Xlib.h. + -DMESA_EGL_NO_X11_HEADERS + -DEGL_NO_X11 + -DEGL_NO_PLATFORM_SPECIFIC_TYPES +) + +ecm_set_disabled_deprecation_versions(QT 5.15 + KF 6.9.0 +) + +ecm_setup_version(${PROJECT_VERSION} + VARIABLE_PREFIX KWIN + PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KWinConfigVersion.cmake" + SOVERSION 6 +) + +ecm_setup_qtplugin_macro_names( + JSON_ARG2 + "KWIN_EFFECT_FACTORY" + JSON_ARG3 + "KWIN_EFFECT_FACTORY_ENABLED" + "KWIN_EFFECT_FACTORY_SUPPORTED" + JSON_ARG4 + "KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED" + CONFIG_CODE_VARIABLE + PACKAGE_SETUP_KWINEFFECTS_AUTOMOC_VARIABLES +) + +if (KF6DocTools_FOUND) + add_subdirectory(doc) + kdoctools_install(po) +endif() + +add_subdirectory(data) +add_subdirectory(kconf_update) +add_subdirectory(src) + +if (BUILD_TESTING) + if (Qt6WaylandClient_VERSION VERSION_GREATER_EQUAL "6.10.0") + find_package(Qt6WaylandClientPrivate ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE) + endif() + + add_subdirectory(autotests) + add_subdirectory(tests) +endif() + +# add clang-format target for all our real source files +file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES *.cpp *.h) +kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES}) +kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT) + +feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) + +set(CMAKECONFIG_DBUS_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KWinDBusInterface") +configure_package_config_file(KWinDBusInterfaceConfig.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/KWinDBusInterfaceConfig.cmake" + PATH_VARS KDE_INSTALL_DBUSINTERFACEDIR + INSTALL_DESTINATION ${CMAKECONFIG_DBUS_INSTALL_DIR}) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/KWinDBusInterfaceConfig.cmake + DESTINATION ${CMAKECONFIG_DBUS_INSTALL_DIR}) + +set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KWin") + +configure_package_config_file( + "${CMAKE_CURRENT_SOURCE_DIR}/KWinConfig.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/KWinConfig.cmake" + INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR} +) + +install( + FILES + cmake/modules/FindLibdrm.cmake + "${CMAKE_CURRENT_BINARY_DIR}/KWinConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/KWinConfigVersion.cmake" + DESTINATION "${CMAKECONFIG_INSTALL_DIR}" + COMPONENT Devel +) + +install(EXPORT KWinTargets DESTINATION "${CMAKECONFIG_INSTALL_DIR}" FILE KWinTargets.cmake NAMESPACE KWin:: ) + +ecm_install_configured_files(INPUT plasma-kwin_wayland.service.in @ONLY + DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}) + + +#ki18n_install(po) diff --git a/local/recipes/kde/kwin/source/CONTRIBUTING.md b/local/recipes/kde/kwin/source/CONTRIBUTING.md new file mode 100644 index 0000000000..c82f5d5726 --- /dev/null +++ b/local/recipes/kde/kwin/source/CONTRIBUTING.md @@ -0,0 +1,134 @@ +# Contributing to KWin + +## Chatting + +Come on by and ask about anything you run into when hacking on KWin! + +KWin's Matrix room on our instance is located here: https://matrix.to/#/#kwin:kde.org. +You can grab an Matrix account at https://webchat.kde.org/ if you don't already have one from us or another provider. + +The Matrix room is bridged to `#kde-kwin` on Libera, allowing IRC users to access it. + +## What Needs Doing + +There's a large amount of bugs open for KWin on our [Bugzilla instance](https://bugs.kde.org/describecomponents.cgi?product=kwin). + +## Where Stuff Is + +Everything codewise for KWin itself is located in the `src` directory. + +### Settings Pages / KCMs + +All the settings pages for KWin found in System Settings are located in `src/kcmkwin`. + +### Default Decorations + +The Breeze decorations theme is not located in the KWin repository, and is in fact part of the [Breeze repository here](https://invent.kde.org/plasma/breeze), in `kdecoration`. + +### Tab Switcher + +The default visual appearance of the tab switcher is located in `src/tabbox/switchers`. + +Other window switchers usually shipped by default are located in [Plasma Addons](https://invent.kde.org/plasma/kdeplasma-addons), located in the `kwin/windowswitchers` directory. + +### Window Management + +Most window management stuff (layouting, movement, properties, communication between client<->server) is defined in files ending with `client`, such as `x11client.cpp` and `xdgshellclient.cpp`. + +### Window Effects + +Window effects are located in `src/plugins`, one effect plugin per folder. Folder `src/plugins/private` contains the plugin (`org.kde.kwin.private.effects`) that exposes layouting properties and `WindowHeap.qml` for QML effects. Not everything here is an effect as exposed in the configuration UI, such as the colour picker in `src/plugins/colorpicker`. + +Of note, the Effects QML engine is shared with the Scripting components (see `src/scripting`). + +### Scripting API + +Many objects in KWin are exposed directly to the scripting API; scriptable properties are marked with Q_PROPERTY and functions that scripts can invoke on them. + +Other scripting stuff is located in `src/scripting`. + +## Conventions + +### Coding Conventions + +KWin's coding conventions are located [here](doc/coding-conventions.md). + +KWin additionally follows [KDE's Frameworks Coding Style](https://community.kde.org/Policies/Frameworks_Coding_Style). + +### Commits + +We usually use this convention for commits in KWin: + +``` +component/subcomponent: Do a thing + +This is a body of the commit message, +elaborating on why we're doing thing. +``` + +While this isn't a hard rule, it's appreciated for easy scanning of commits by their messages. + +## Contributing + +KWin uses KDE's GitLab instance for submitting code. + +You can read about the [KDE workflow here](https://community.kde.org/Infrastructure/GitLab). + +## Running KWin From Source + +KWin uses CMake like most KDE projects, so you can build it like this: + +```bash +mkdir _build +cd _build +cmake .. +make +``` + +People hacking on much KDE software may want to set up [kdesrc-build](https://invent.kde.org/sdk/kdesrc-build). + +Once built, you can either install it over your system KWin (not recommended) or run it from the build directory directly. + +Running it from your build directory looks like this: +```bash +# from the root of your build directory + +source prefix.sh +cd bin + +# starts nested session: with console + +env QT_PLUGIN_PATH="$(pwd)":"$QT_PLUGIN_PATH" dbus-run-session ./kwin_wayland --xwayland konsole + +``` + +QT_PLUGIN_PATH tells Qt to load KWin's plugins from the build directory, and not from your system KWin. + +The dbus-run-session is needed to prevent the nested KWin instance from conflicting with your session KWin instance when exporting objects onto the bus, or with stuff like global shortcuts. + +If you need to run a whole Wayland plasma session, you should install a development session by first building [plasma-workspace](https://invent.kde.org/plasma/plasma-workspace) and executing the `login-sessions/install-sessions.sh` in the build directory. This can be done using kdesrc-build. + +```bash +kdesrc-build plasma-workspace +# assuming the root directory for kdesrc-build is ~/kde +bash ~/kde/build/plasma-workspace/login-sessions/install-sessions.sh +``` +Then you can select the develop session in the sddm login screen. + +You can look up the current boot kwin log via `journalctl --user-unit plasma-kwin_wayland --boot 0`. + +## Using A Debugger + +Trying to attach a debugger to a running KWin instance from within itself will likely be the last thing you do in the session, as KWin will freeze until you resume it from your debugger, which you need KWin to interact with. + +Instead, either attach a debugger to a nested KWin instance or debug over SSH. + +## Tests + +KWin has a series of unit tests and integration tests that ensure everything is running as expected. + +If you're adding substantial new code, it's expected that you'll write tests for it to ensure that it's working as expected. + +If you're fixing a bug, it's appreciated, but not expected, that you add a test case for the bug you fix. + +You can read more about [KWin's testing infrastructure here](doc/TESTING.md). diff --git a/local/recipes/kde/kwin/source/KWinDBusInterfaceConfig.cmake.in b/local/recipes/kde/kwin/source/KWinDBusInterfaceConfig.cmake.in new file mode 100644 index 0000000000..12a2ae37e1 --- /dev/null +++ b/local/recipes/kde/kwin/source/KWinDBusInterfaceConfig.cmake.in @@ -0,0 +1,10 @@ +@PACKAGE_INIT@ + +set(KWIN_INTERFACE "@PACKAGE_KDE_INSTALL_DBUSINTERFACEDIR@/org.kde.KWin.xml") +set(KWIN_COMPOSITING_INTERFACE "@PACKAGE_KDE_INSTALL_DBUSINTERFACEDIR@/org.kde.kwin.Compositing.xml") +set(KWIN_EFFECTS_INTERFACE "@PACKAGE_KDE_INSTALL_DBUSINTERFACEDIR@/org.kde.kwin.Effects.xml") +set(KWIN_VIRTUALKEYBOARD_INTERFACE "@PACKAGE_KDE_INSTALL_DBUSINTERFACEDIR@/org.kde.kwin.VirtualKeyboard.xml") +set(KWIN_TABLETMODE_INTERFACE "@PACKAGE_KDE_INSTALL_DBUSINTERFACEDIR@/org.kde.KWin.TabletModeManager.xml") +set(KWIN_INPUTDEVICE_INTERFACE "@PACKAGE_KDE_INSTALL_DBUSINTERFACEDIR@/org.kde.kwin.InputDevice.xml") +set(KWIN_NIGHTLIGHT_INTERFACE "@PACKAGE_KDE_INSTALL_DBUSINTERFACEDIR@/org.kde.KWin.NightLight.xml") +set(KWIN_WAYLAND_BIN_PATH "@CMAKE_INSTALL_FULL_BINDIR@/kwin_wayland") diff --git a/local/recipes/kde/kwin/source/LICENSES/BSD-3-Clause.txt b/local/recipes/kde/kwin/source/LICENSES/BSD-3-Clause.txt new file mode 100644 index 0000000000..0741db789e --- /dev/null +++ b/local/recipes/kde/kwin/source/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/local/recipes/kde/kwin/source/LICENSES/CC0-1.0.txt b/local/recipes/kde/kwin/source/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000000..0e259d42c9 --- /dev/null +++ b/local/recipes/kde/kwin/source/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/local/recipes/kde/kwin/source/LICENSES/GPL-2.0-only.txt b/local/recipes/kde/kwin/source/LICENSES/GPL-2.0-only.txt new file mode 100644 index 0000000000..0f3d6411da --- /dev/null +++ b/local/recipes/kde/kwin/source/LICENSES/GPL-2.0-only.txt @@ -0,0 +1,319 @@ +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. + + + +Copyright (C)< yyyy> + +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. + +, 1 April 1989 Ty Coon, President of Vice This General +Public License does not permit incorporating your program into proprietary +programs. If your program is a subroutine library, you may consider it more +useful to permit linking proprietary applications with the library. If this +is what you want to do, use the GNU Lesser General Public License instead +of this License. diff --git a/local/recipes/kde/kwin/source/LICENSES/GPL-2.0-or-later.txt b/local/recipes/kde/kwin/source/LICENSES/GPL-2.0-or-later.txt new file mode 100644 index 0000000000..1d80ac3653 --- /dev/null +++ b/local/recipes/kde/kwin/source/LICENSES/GPL-2.0-or-later.txt @@ -0,0 +1,319 @@ +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. + + + +Copyright (C) + +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. + +, 1 April 1989 Ty Coon, President of Vice This General +Public License does not permit incorporating your program into proprietary +programs. If your program is a subroutine library, you may consider it more +useful to permit linking proprietary applications with the library. If this +is what you want to do, use the GNU Lesser General Public License instead +of this License. diff --git a/local/recipes/kde/kwin/source/LICENSES/GPL-3.0-only.txt b/local/recipes/kde/kwin/source/LICENSES/GPL-3.0-only.txt new file mode 100644 index 0000000000..e142a525bd --- /dev/null +++ b/local/recipes/kde/kwin/source/LICENSES/GPL-3.0-only.txt @@ -0,0 +1,625 @@ +GNU GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and +other kinds of works. + +The licenses for most software and other practical works are designed to take +away your freedom to share and change the works. By contrast, the GNU General +Public License is intended to guarantee your freedom to share and change all +versions of a program--to make sure it remains free software for all its users. +We, the Free Software Foundation, use the GNU General Public License for most +of our software; it applies also to any other work released this way by its +authors. 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 them 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 prevent others from denying you these rights +or asking you to surrender the rights. Therefore, you have certain responsibilities +if you distribute copies of the software, or if you modify it: responsibilities +to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must pass on to the recipients the same freedoms that you received. +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. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert +copyright on the software, and (2) offer you this License giving you legal +permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that +there is no warranty for this free software. For both users' and authors' +sake, the GPL requires that modified versions be marked as changed, so that +their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified +versions of the software inside them, although the manufacturer can do so. +This is fundamentally incompatible with the aim of protecting users' freedom +to change the software. The systematic pattern of such abuse occurs in the +area of products for individuals to use, which is precisely where it is most +unacceptable. Therefore, we have designed this version of the GPL to prohibit +the practice for those products. If such problems arise substantially in other +domains, we stand ready to extend this provision to those domains in future +versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States +should not allow patents to restrict development and use of software on general-purpose +computers, but in those that do, we wish to avoid the special danger that +patents applied to a free program could make it effectively proprietary. To +prevent this, the GPL assures that patents cannot be used to render the program +non-free. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, +such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. +Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals +or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in +a fashion requiring copyright permission, other than the making of an exact +copy. The resulting work is called a "modified version" of the earlier work +or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the +Program. + +To "propagate" a work means to do anything with it that, without permission, +would make you directly or secondarily liable for infringement under applicable +copyright law, except executing it on a computer or modifying a private copy. +Propagation includes copying, distribution (with or without modification), +making available to the public, and in some countries other activities as +well. + +To "convey" a work means any kind of propagation that enables other parties +to make or receive copies. Mere interaction with a user through a computer +network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the +extent that it includes a convenient and prominently visible feature that +(1) displays an appropriate copyright notice, and (2) tells the user that +there is no warranty for the work (except to the extent that warranties are +provided), that licensees may convey the work under this License, and how +to view a copy of this License. If the interface presents a list of user commands +or options, such as a menu, a prominent item in the list meets this criterion. + + 1. Source Code. + +The "source code" for a work means the preferred form of the work for making +modifications to it. "Object code" means any non-source form of a work. + +A "Standard Interface" means an interface that either is an official standard +defined by a recognized standards body, or, in the case of interfaces specified +for a particular programming language, one that is widely used among developers +working in that language. + +The "System Libraries" of an executable work include anything, other than +the work as a whole, that (a) is included in the normal form of packaging +a Major Component, but which is not part of that Major Component, and (b) +serves only to enable use of the work with that Major Component, or to implement +a Standard Interface for which an implementation is available to the public +in source code form. A "Major Component", in this context, means a major essential +component (kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to produce +the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source +code needed to generate, install, and (for an executable work) run the object +code and to modify the work, including scripts to control those activities. +However, it does not include the work's System Libraries, or general-purpose +tools or generally available free programs which are used unmodified in performing +those activities but which are not part of the work. For example, Corresponding +Source includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically linked +subprograms that the work is specifically designed to require, such as by +intimate data communication or control flow between those subprograms and +other parts of the work. + +The Corresponding Source need not include anything that users can regenerate +automatically from other parts of the Corresponding Source. + + The Corresponding Source for a work in source code form is that same work. + + 2. Basic Permissions. + +All rights granted under this License are granted for the term of copyright +on the Program, and are irrevocable provided the stated conditions are met. +This License explicitly affirms your unlimited permission to run the unmodified +Program. The output from running a covered work is covered by this License +only if the output, given its content, constitutes a covered work. This License +acknowledges your rights of fair use or other equivalent, as provided by copyright +law. + +You may make, run and propagate covered works that you do not convey, without +conditions so long as your license otherwise remains in force. You may convey +covered works to others for the sole purpose of having them make modifications +exclusively for you, or provide you with facilities for running those works, +provided that you comply with the terms of this License in conveying all material +for which you do not control copyright. Those thus making or running the covered +works for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of your copyrighted +material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions +stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological measure +under any applicable law fulfilling obligations under article 11 of the WIPO +copyright treaty adopted on 20 December 1996, or similar laws prohibiting +or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention +of technological measures to the extent such circumvention is effected by +exercising rights under this License with respect to the covered work, and +you disclaim any intention to limit operation or modification of the work +as a means of enforcing, against the work's users, your or third parties' +legal rights to forbid circumvention of technological measures. + + 4. Conveying Verbatim Copies. + +You may convey 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; keep intact all notices stating +that this License and any non-permissive terms added in accord with section +7 apply to the code; keep intact all notices of the absence of any warranty; +and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you +may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to produce +it from the Program, in the form of source code under the terms of section +4, provided that you also meet all of these conditions: + +a) The work must carry prominent notices stating that you modified it, and +giving a relevant date. + +b) The work must carry prominent notices stating that it is released under +this License and any conditions added under section 7. This requirement modifies +the requirement in section 4 to "keep intact all notices". + +c) You must license the entire work, as a whole, under this License to anyone +who comes into possession of a copy. This License will therefore apply, along +with any applicable section 7 additional terms, to the whole of the work, +and all its parts, regardless of how they are packaged. This License gives +no permission to license the work in any other way, but it does not invalidate +such permission if you have separately received it. + +d) If the work has interactive user interfaces, each must display Appropriate +Legal Notices; however, if the Program has interactive interfaces that do +not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, +which are not by their nature extensions of the covered work, and which are +not combined with it such as to form a larger program, in or on a volume of +a storage or distribution medium, is called an "aggregate" if the compilation +and its resulting copyright are not used to limit the access or legal rights +of the compilation's users beyond what the individual works permit. Inclusion +of a covered work in an aggregate does not cause this License to apply to +the other parts of the aggregate. + + 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of sections +4 and 5, provided that you also convey the machine-readable Corresponding +Source under the terms of this License, in one of these ways: + +a) Convey the object code in, or embodied in, a physical product (including +a physical distribution medium), accompanied by the Corresponding Source fixed +on a durable physical medium customarily used for software interchange. + +b) Convey the object code in, or embodied in, a physical product (including +a physical distribution medium), accompanied by a written offer, valid for +at least three years and valid for as long as you offer spare parts or customer +support for that product model, to give anyone who possesses the object code +either (1) a copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical medium customarily +used for software interchange, for a price no more than your reasonable cost +of physically performing this conveying of source, or (2) access to copy the +Corresponding Source from a network server at no charge. + +c) Convey individual copies of the object code with a copy of the written +offer to provide the Corresponding Source. This alternative is allowed only +occasionally and noncommercially, and only if you received the object code +with such an offer, in accord with subsection 6b. + +d) Convey the object code by offering access from a designated place (gratis +or for a charge), and offer equivalent access to the Corresponding Source +in the same way through the same place at no further charge. You need not +require recipients to copy the Corresponding Source along with the object +code. If the place to copy the object code is a network server, the Corresponding +Source may be on a different server (operated by you or a third party) that +supports equivalent copying facilities, provided you maintain clear directions +next to the object code saying where to find the Corresponding Source. Regardless +of what server hosts the Corresponding Source, you remain obligated to ensure +that it is available for as long as needed to satisfy these requirements. + +e) Convey the object code using peer-to-peer transmission, provided you inform +other peers where the object code and Corresponding Source of the work are +being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from +the Corresponding Source as a System Library, need not be included in conveying +the object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible +personal property which is normally used for personal, family, or household +purposes, or (2) anything designed or sold for incorporation into a dwelling. +In determining whether a product is a consumer product, doubtful cases shall +be resolved in favor of coverage. For a particular product received by a particular +user, "normally used" refers to a typical or common use of that class of product, +regardless of the status of the particular user or of the way in which the +particular user actually uses, or expects or is expected to use, the product. +A product is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent the +only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, +authorization keys, or other information required to install and execute modified +versions of a covered work in that User Product from a modified version of +its Corresponding Source. The information must suffice to ensure that the +continued functioning of the modified object code is in no case prevented +or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically +for use in, a User Product, and the conveying occurs as part of a transaction +in which the right of possession and use of the User Product is transferred +to the recipient in perpetuity or for a fixed term (regardless of how the +transaction is characterized), the Corresponding Source conveyed under this +section must be accompanied by the Installation Information. But this requirement +does not apply if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has been installed +in ROM). + +The requirement to provide Installation Information does not include a requirement +to continue to provide support service, warranty, or updates for a work that +has been modified or installed by the recipient, or for the User Product in +which it has been modified or installed. Access to a network may be denied +when the modification itself materially and adversely affects the operation +of the network or violates the rules and protocols for communication across +the network. + +Corresponding Source conveyed, and Installation Information provided, in accord +with this section must be in a format that is publicly documented (and with +an implementation available to the public in source code form), and must require +no special password or key for unpacking, reading or copying. + + 7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this License +by making exceptions from one or more of its conditions. Additional permissions +that are applicable to the entire Program shall be treated as though they +were included in this License, to the extent that they are valid under applicable +law. If additional permissions apply only to part of the Program, that part +may be used separately under those permissions, but the entire Program remains +governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any +additional permissions from that copy, or from any part of it. (Additional +permissions may be written to require their own removal in certain cases when +you modify the work.) You may place additional permissions on material, added +by you to a covered work, for which you have or can give appropriate copyright +permission. + +Notwithstanding any other provision of this License, for material you add +to a covered work, you may (if authorized by the copyright holders of that +material) supplement the terms of this License with terms: + +a) Disclaiming warranty or limiting liability differently from the terms of +sections 15 and 16 of this License; or + +b) Requiring preservation of specified reasonable legal notices or author +attributions in that material or in the Appropriate Legal Notices displayed +by works containing it; or + +c) Prohibiting misrepresentation of the origin of that material, or requiring +that modified versions of such material be marked in reasonable ways as different +from the original version; or + +d) Limiting the use for publicity purposes of names of licensors or authors +of the material; or + +e) Declining to grant rights under trademark law for use of some trade names, +trademarks, or service marks; or + +f) Requiring indemnification of licensors and authors of that material by +anyone who conveys the material (or modified versions of it) with contractual +assumptions of liability to the recipient, for any liability that these contractual +assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered "further restrictions" +within the meaning of section 10. If the Program as you received it, or any +part of it, contains a notice stating that it is governed by this License +along with a term that is a further restriction, you may remove that term. +If a license document contains a further restriction but permits relicensing +or conveying under this License, you may add to a covered work material governed +by the terms of that license document, provided that the further restriction +does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, +in the relevant source files, a statement of the additional terms that apply +to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form +of a separately written license, or stated as exceptions; the above requirements +apply either way. + + 8. Termination. + +You may not propagate or modify a covered work except as expressly provided +under this License. Any attempt otherwise to propagate or modify it is void, +and will automatically terminate your rights under this License (including +any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from +a particular copyright holder is reinstated (a) provisionally, unless and +until the copyright holder explicitly and finally terminates your license, +and (b) permanently, if the copyright holder fails to notify you of the violation +by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently +if the copyright holder notifies you of the violation by some reasonable means, +this is the first time you have received notice of violation of this License +(for any work) from that copyright holder, and you cure the violation prior +to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses +of parties who have received copies or rights from you under this License. +If your rights have been terminated and not permanently reinstated, you do +not qualify to receive new licenses for the same material under section 10. + + 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy +of the Program. Ancillary propagation of a covered work occurring solely as +a consequence of using peer-to-peer transmission to receive a copy likewise +does not require acceptance. However, nothing other than this License grants +you permission to propagate or modify any covered work. These actions infringe +copyright if you do not accept this License. Therefore, by modifying or propagating +a covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives +a license from the original licensors, to run, modify and propagate that work, +subject to this License. You are not responsible for enforcing compliance +by third parties with this License. + +An "entity transaction" is a transaction transferring control of an organization, +or substantially all assets of one, or subdividing an organization, or merging +organizations. If propagation of a covered work results from an entity transaction, +each party to that transaction who receives a copy of the work also receives +whatever licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the Corresponding +Source of the work from the predecessor in interest, if the predecessor has +it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights +granted or affirmed under this License. For example, you may not impose a +license fee, royalty, or other charge for exercise of rights granted under +this License, and you may not initiate litigation (including a cross-claim +or counterclaim in a lawsuit) alleging that any patent claim is infringed +by making, using, selling, offering for sale, or importing the Program or +any portion of it. + + 11. Patents. + +A "contributor" is a copyright holder who authorizes use under this License +of the Program or a work on which the Program is based. The work thus licensed +is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or controlled +by the contributor, whether already acquired or hereafter acquired, that would +be infringed by some manner, permitted by this License, of making, using, +or selling its contributor version, but do not include claims that would be +infringed only as a consequence of further modification of the contributor +version. For purposes of this definition, "control" includes the right to +grant patent sublicenses in a manner consistent with the requirements of this +License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent +license under the contributor's essential patent claims, to make, use, sell, +offer for sale, import and otherwise run, modify and propagate the contents +of its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement +or commitment, however denominated, not to enforce a patent (such as an express +permission to practice a patent or covenant not to sue for patent infringement). +To "grant" such a patent license to a party means to make such an agreement +or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the +Corresponding Source of the work is not available for anyone to copy, free +of charge and under the terms of this License, through a publicly available +network server or other readily accessible means, then you must either (1) +cause the Corresponding Source to be so available, or (2) arrange to deprive +yourself of the benefit of the patent license for this particular work, or +(3) arrange, in a manner consistent with the requirements of this License, +to extend the patent license to downstream recipients. "Knowingly relying" +means you have actual knowledge that, but for the patent license, your conveying +the covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that country +that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, +you convey, or propagate by procuring conveyance of, a covered work, and grant +a patent license to some of the parties receiving the covered work authorizing +them to use, propagate, modify or convey a specific copy of the covered work, +then the patent license you grant is automatically extended to all recipients +of the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope +of its coverage, prohibits the exercise of, or is conditioned on the non-exercise +of one or more of the rights that are specifically granted under this License. +You may not convey a covered work if you are a party to an arrangement with +a third party that is in the business of distributing software, under which +you make payment to the third party based on the extent of your activity of +conveying the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by you +(or copies made from those copies), or (b) primarily for and in connection +with specific products or compilations that contain the covered work, unless +you entered into that arrangement, or that patent license was granted, prior +to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied +license or other defenses to infringement that may otherwise be available +to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + +If 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 convey a covered work so as +to satisfy simultaneously your obligations under this License and any other +pertinent obligations, then as a consequence you may not convey it at all. +For example, if you agree to terms that obligate you to collect a royalty +for further conveying from those to whom you convey the Program, the only +way you could satisfy both those terms and this License would be to refrain +entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have permission to +link or combine any covered work with a work licensed under version 3 of the +GNU Affero General Public License into a single combined work, and to convey +the resulting work. The terms of this License will continue to apply to the +part which is the covered work, but the special requirements of the GNU Affero +General Public License, section 13, concerning interaction through a network +will apply to the combination as such. + + 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the +GNU 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 +that a certain numbered version of the GNU General Public License "or any +later version" applies to it, you have the option of following the terms and +conditions either of that numbered version or of any later version published +by the Free Software Foundation. If the Program does not specify a version +number of the GNU General Public License, you may choose any version ever +published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of +the GNU General Public License can be used, that proxy's public statement +of acceptance of a version permanently authorizes you to choose that version +for the Program. + +Later license versions may give you additional or different permissions. However, +no additional obligations are imposed on any author or copyright holder as +a result of your choosing to follow a later version. + + 15. Disclaimer of Warranty. + +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. + + 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL +ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 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. + + 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot +be given local legal effect according to their terms, reviewing courts shall +apply local law that most closely approximates an absolute waiver of all civil +liability in connection with the Program, unless a warranty or assumption +of liability accompanies a copy of the Program in return for a fee. 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 state the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + + + +Copyright (C) + +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 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like +this when it starts in an interactive mode: + + Copyright (C) + +This program 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, your program's commands might +be different; for a GUI interface, you would use an "about box". + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. For +more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General Public +License instead of this License. But first, please read . diff --git a/local/recipes/kde/kwin/source/LICENSES/GPL-3.0-or-later.txt b/local/recipes/kde/kwin/source/LICENSES/GPL-3.0-or-later.txt new file mode 100644 index 0000000000..e142a525bd --- /dev/null +++ b/local/recipes/kde/kwin/source/LICENSES/GPL-3.0-or-later.txt @@ -0,0 +1,625 @@ +GNU GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The GNU General Public License is a free, copyleft license for software and +other kinds of works. + +The licenses for most software and other practical works are designed to take +away your freedom to share and change the works. By contrast, the GNU General +Public License is intended to guarantee your freedom to share and change all +versions of a program--to make sure it remains free software for all its users. +We, the Free Software Foundation, use the GNU General Public License for most +of our software; it applies also to any other work released this way by its +authors. 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 them 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 prevent others from denying you these rights +or asking you to surrender the rights. Therefore, you have certain responsibilities +if you distribute copies of the software, or if you modify it: responsibilities +to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or +for a fee, you must pass on to the recipients the same freedoms that you received. +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. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert +copyright on the software, and (2) offer you this License giving you legal +permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that +there is no warranty for this free software. For both users' and authors' +sake, the GPL requires that modified versions be marked as changed, so that +their problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified +versions of the software inside them, although the manufacturer can do so. +This is fundamentally incompatible with the aim of protecting users' freedom +to change the software. The systematic pattern of such abuse occurs in the +area of products for individuals to use, which is precisely where it is most +unacceptable. Therefore, we have designed this version of the GPL to prohibit +the practice for those products. If such problems arise substantially in other +domains, we stand ready to extend this provision to those domains in future +versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States +should not allow patents to restrict development and use of software on general-purpose +computers, but in those that do, we wish to avoid the special danger that +patents applied to a free program could make it effectively proprietary. To +prevent this, the GPL assures that patents cannot be used to render the program +non-free. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, +such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. +Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals +or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in +a fashion requiring copyright permission, other than the making of an exact +copy. The resulting work is called a "modified version" of the earlier work +or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the +Program. + +To "propagate" a work means to do anything with it that, without permission, +would make you directly or secondarily liable for infringement under applicable +copyright law, except executing it on a computer or modifying a private copy. +Propagation includes copying, distribution (with or without modification), +making available to the public, and in some countries other activities as +well. + +To "convey" a work means any kind of propagation that enables other parties +to make or receive copies. Mere interaction with a user through a computer +network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the +extent that it includes a convenient and prominently visible feature that +(1) displays an appropriate copyright notice, and (2) tells the user that +there is no warranty for the work (except to the extent that warranties are +provided), that licensees may convey the work under this License, and how +to view a copy of this License. If the interface presents a list of user commands +or options, such as a menu, a prominent item in the list meets this criterion. + + 1. Source Code. + +The "source code" for a work means the preferred form of the work for making +modifications to it. "Object code" means any non-source form of a work. + +A "Standard Interface" means an interface that either is an official standard +defined by a recognized standards body, or, in the case of interfaces specified +for a particular programming language, one that is widely used among developers +working in that language. + +The "System Libraries" of an executable work include anything, other than +the work as a whole, that (a) is included in the normal form of packaging +a Major Component, but which is not part of that Major Component, and (b) +serves only to enable use of the work with that Major Component, or to implement +a Standard Interface for which an implementation is available to the public +in source code form. A "Major Component", in this context, means a major essential +component (kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to produce +the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source +code needed to generate, install, and (for an executable work) run the object +code and to modify the work, including scripts to control those activities. +However, it does not include the work's System Libraries, or general-purpose +tools or generally available free programs which are used unmodified in performing +those activities but which are not part of the work. For example, Corresponding +Source includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically linked +subprograms that the work is specifically designed to require, such as by +intimate data communication or control flow between those subprograms and +other parts of the work. + +The Corresponding Source need not include anything that users can regenerate +automatically from other parts of the Corresponding Source. + + The Corresponding Source for a work in source code form is that same work. + + 2. Basic Permissions. + +All rights granted under this License are granted for the term of copyright +on the Program, and are irrevocable provided the stated conditions are met. +This License explicitly affirms your unlimited permission to run the unmodified +Program. The output from running a covered work is covered by this License +only if the output, given its content, constitutes a covered work. This License +acknowledges your rights of fair use or other equivalent, as provided by copyright +law. + +You may make, run and propagate covered works that you do not convey, without +conditions so long as your license otherwise remains in force. You may convey +covered works to others for the sole purpose of having them make modifications +exclusively for you, or provide you with facilities for running those works, +provided that you comply with the terms of this License in conveying all material +for which you do not control copyright. Those thus making or running the covered +works for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of your copyrighted +material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions +stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological measure +under any applicable law fulfilling obligations under article 11 of the WIPO +copyright treaty adopted on 20 December 1996, or similar laws prohibiting +or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention +of technological measures to the extent such circumvention is effected by +exercising rights under this License with respect to the covered work, and +you disclaim any intention to limit operation or modification of the work +as a means of enforcing, against the work's users, your or third parties' +legal rights to forbid circumvention of technological measures. + + 4. Conveying Verbatim Copies. + +You may convey 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; keep intact all notices stating +that this License and any non-permissive terms added in accord with section +7 apply to the code; keep intact all notices of the absence of any warranty; +and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you +may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to produce +it from the Program, in the form of source code under the terms of section +4, provided that you also meet all of these conditions: + +a) The work must carry prominent notices stating that you modified it, and +giving a relevant date. + +b) The work must carry prominent notices stating that it is released under +this License and any conditions added under section 7. This requirement modifies +the requirement in section 4 to "keep intact all notices". + +c) You must license the entire work, as a whole, under this License to anyone +who comes into possession of a copy. This License will therefore apply, along +with any applicable section 7 additional terms, to the whole of the work, +and all its parts, regardless of how they are packaged. This License gives +no permission to license the work in any other way, but it does not invalidate +such permission if you have separately received it. + +d) If the work has interactive user interfaces, each must display Appropriate +Legal Notices; however, if the Program has interactive interfaces that do +not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, +which are not by their nature extensions of the covered work, and which are +not combined with it such as to form a larger program, in or on a volume of +a storage or distribution medium, is called an "aggregate" if the compilation +and its resulting copyright are not used to limit the access or legal rights +of the compilation's users beyond what the individual works permit. Inclusion +of a covered work in an aggregate does not cause this License to apply to +the other parts of the aggregate. + + 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of sections +4 and 5, provided that you also convey the machine-readable Corresponding +Source under the terms of this License, in one of these ways: + +a) Convey the object code in, or embodied in, a physical product (including +a physical distribution medium), accompanied by the Corresponding Source fixed +on a durable physical medium customarily used for software interchange. + +b) Convey the object code in, or embodied in, a physical product (including +a physical distribution medium), accompanied by a written offer, valid for +at least three years and valid for as long as you offer spare parts or customer +support for that product model, to give anyone who possesses the object code +either (1) a copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical medium customarily +used for software interchange, for a price no more than your reasonable cost +of physically performing this conveying of source, or (2) access to copy the +Corresponding Source from a network server at no charge. + +c) Convey individual copies of the object code with a copy of the written +offer to provide the Corresponding Source. This alternative is allowed only +occasionally and noncommercially, and only if you received the object code +with such an offer, in accord with subsection 6b. + +d) Convey the object code by offering access from a designated place (gratis +or for a charge), and offer equivalent access to the Corresponding Source +in the same way through the same place at no further charge. You need not +require recipients to copy the Corresponding Source along with the object +code. If the place to copy the object code is a network server, the Corresponding +Source may be on a different server (operated by you or a third party) that +supports equivalent copying facilities, provided you maintain clear directions +next to the object code saying where to find the Corresponding Source. Regardless +of what server hosts the Corresponding Source, you remain obligated to ensure +that it is available for as long as needed to satisfy these requirements. + +e) Convey the object code using peer-to-peer transmission, provided you inform +other peers where the object code and Corresponding Source of the work are +being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from +the Corresponding Source as a System Library, need not be included in conveying +the object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible +personal property which is normally used for personal, family, or household +purposes, or (2) anything designed or sold for incorporation into a dwelling. +In determining whether a product is a consumer product, doubtful cases shall +be resolved in favor of coverage. For a particular product received by a particular +user, "normally used" refers to a typical or common use of that class of product, +regardless of the status of the particular user or of the way in which the +particular user actually uses, or expects or is expected to use, the product. +A product is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent the +only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, +authorization keys, or other information required to install and execute modified +versions of a covered work in that User Product from a modified version of +its Corresponding Source. The information must suffice to ensure that the +continued functioning of the modified object code is in no case prevented +or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically +for use in, a User Product, and the conveying occurs as part of a transaction +in which the right of possession and use of the User Product is transferred +to the recipient in perpetuity or for a fixed term (regardless of how the +transaction is characterized), the Corresponding Source conveyed under this +section must be accompanied by the Installation Information. But this requirement +does not apply if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has been installed +in ROM). + +The requirement to provide Installation Information does not include a requirement +to continue to provide support service, warranty, or updates for a work that +has been modified or installed by the recipient, or for the User Product in +which it has been modified or installed. Access to a network may be denied +when the modification itself materially and adversely affects the operation +of the network or violates the rules and protocols for communication across +the network. + +Corresponding Source conveyed, and Installation Information provided, in accord +with this section must be in a format that is publicly documented (and with +an implementation available to the public in source code form), and must require +no special password or key for unpacking, reading or copying. + + 7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this License +by making exceptions from one or more of its conditions. Additional permissions +that are applicable to the entire Program shall be treated as though they +were included in this License, to the extent that they are valid under applicable +law. If additional permissions apply only to part of the Program, that part +may be used separately under those permissions, but the entire Program remains +governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any +additional permissions from that copy, or from any part of it. (Additional +permissions may be written to require their own removal in certain cases when +you modify the work.) You may place additional permissions on material, added +by you to a covered work, for which you have or can give appropriate copyright +permission. + +Notwithstanding any other provision of this License, for material you add +to a covered work, you may (if authorized by the copyright holders of that +material) supplement the terms of this License with terms: + +a) Disclaiming warranty or limiting liability differently from the terms of +sections 15 and 16 of this License; or + +b) Requiring preservation of specified reasonable legal notices or author +attributions in that material or in the Appropriate Legal Notices displayed +by works containing it; or + +c) Prohibiting misrepresentation of the origin of that material, or requiring +that modified versions of such material be marked in reasonable ways as different +from the original version; or + +d) Limiting the use for publicity purposes of names of licensors or authors +of the material; or + +e) Declining to grant rights under trademark law for use of some trade names, +trademarks, or service marks; or + +f) Requiring indemnification of licensors and authors of that material by +anyone who conveys the material (or modified versions of it) with contractual +assumptions of liability to the recipient, for any liability that these contractual +assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered "further restrictions" +within the meaning of section 10. If the Program as you received it, or any +part of it, contains a notice stating that it is governed by this License +along with a term that is a further restriction, you may remove that term. +If a license document contains a further restriction but permits relicensing +or conveying under this License, you may add to a covered work material governed +by the terms of that license document, provided that the further restriction +does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, +in the relevant source files, a statement of the additional terms that apply +to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form +of a separately written license, or stated as exceptions; the above requirements +apply either way. + + 8. Termination. + +You may not propagate or modify a covered work except as expressly provided +under this License. Any attempt otherwise to propagate or modify it is void, +and will automatically terminate your rights under this License (including +any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from +a particular copyright holder is reinstated (a) provisionally, unless and +until the copyright holder explicitly and finally terminates your license, +and (b) permanently, if the copyright holder fails to notify you of the violation +by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently +if the copyright holder notifies you of the violation by some reasonable means, +this is the first time you have received notice of violation of this License +(for any work) from that copyright holder, and you cure the violation prior +to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses +of parties who have received copies or rights from you under this License. +If your rights have been terminated and not permanently reinstated, you do +not qualify to receive new licenses for the same material under section 10. + + 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy +of the Program. Ancillary propagation of a covered work occurring solely as +a consequence of using peer-to-peer transmission to receive a copy likewise +does not require acceptance. However, nothing other than this License grants +you permission to propagate or modify any covered work. These actions infringe +copyright if you do not accept this License. Therefore, by modifying or propagating +a covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives +a license from the original licensors, to run, modify and propagate that work, +subject to this License. You are not responsible for enforcing compliance +by third parties with this License. + +An "entity transaction" is a transaction transferring control of an organization, +or substantially all assets of one, or subdividing an organization, or merging +organizations. If propagation of a covered work results from an entity transaction, +each party to that transaction who receives a copy of the work also receives +whatever licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the Corresponding +Source of the work from the predecessor in interest, if the predecessor has +it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights +granted or affirmed under this License. For example, you may not impose a +license fee, royalty, or other charge for exercise of rights granted under +this License, and you may not initiate litigation (including a cross-claim +or counterclaim in a lawsuit) alleging that any patent claim is infringed +by making, using, selling, offering for sale, or importing the Program or +any portion of it. + + 11. Patents. + +A "contributor" is a copyright holder who authorizes use under this License +of the Program or a work on which the Program is based. The work thus licensed +is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or controlled +by the contributor, whether already acquired or hereafter acquired, that would +be infringed by some manner, permitted by this License, of making, using, +or selling its contributor version, but do not include claims that would be +infringed only as a consequence of further modification of the contributor +version. For purposes of this definition, "control" includes the right to +grant patent sublicenses in a manner consistent with the requirements of this +License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent +license under the contributor's essential patent claims, to make, use, sell, +offer for sale, import and otherwise run, modify and propagate the contents +of its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement +or commitment, however denominated, not to enforce a patent (such as an express +permission to practice a patent or covenant not to sue for patent infringement). +To "grant" such a patent license to a party means to make such an agreement +or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the +Corresponding Source of the work is not available for anyone to copy, free +of charge and under the terms of this License, through a publicly available +network server or other readily accessible means, then you must either (1) +cause the Corresponding Source to be so available, or (2) arrange to deprive +yourself of the benefit of the patent license for this particular work, or +(3) arrange, in a manner consistent with the requirements of this License, +to extend the patent license to downstream recipients. "Knowingly relying" +means you have actual knowledge that, but for the patent license, your conveying +the covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that country +that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, +you convey, or propagate by procuring conveyance of, a covered work, and grant +a patent license to some of the parties receiving the covered work authorizing +them to use, propagate, modify or convey a specific copy of the covered work, +then the patent license you grant is automatically extended to all recipients +of the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope +of its coverage, prohibits the exercise of, or is conditioned on the non-exercise +of one or more of the rights that are specifically granted under this License. +You may not convey a covered work if you are a party to an arrangement with +a third party that is in the business of distributing software, under which +you make payment to the third party based on the extent of your activity of +conveying the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by you +(or copies made from those copies), or (b) primarily for and in connection +with specific products or compilations that contain the covered work, unless +you entered into that arrangement, or that patent license was granted, prior +to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied +license or other defenses to infringement that may otherwise be available +to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + +If 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 convey a covered work so as +to satisfy simultaneously your obligations under this License and any other +pertinent obligations, then as a consequence you may not convey it at all. +For example, if you agree to terms that obligate you to collect a royalty +for further conveying from those to whom you convey the Program, the only +way you could satisfy both those terms and this License would be to refrain +entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have permission to +link or combine any covered work with a work licensed under version 3 of the +GNU Affero General Public License into a single combined work, and to convey +the resulting work. The terms of this License will continue to apply to the +part which is the covered work, but the special requirements of the GNU Affero +General Public License, section 13, concerning interaction through a network +will apply to the combination as such. + + 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the +GNU 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 +that a certain numbered version of the GNU General Public License "or any +later version" applies to it, you have the option of following the terms and +conditions either of that numbered version or of any later version published +by the Free Software Foundation. If the Program does not specify a version +number of the GNU General Public License, you may choose any version ever +published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of +the GNU General Public License can be used, that proxy's public statement +of acceptance of a version permanently authorizes you to choose that version +for the Program. + +Later license versions may give you additional or different permissions. However, +no additional obligations are imposed on any author or copyright holder as +a result of your choosing to follow a later version. + + 15. Disclaimer of Warranty. + +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. + + 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL +ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 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. + + 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot +be given local legal effect according to their terms, reviewing courts shall +apply local law that most closely approximates an absolute waiver of all civil +liability in connection with the Program, unless a warranty or assumption +of liability accompanies a copy of the Program in return for a fee. 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 state the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + + + +Copyright (C) + +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 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like +this when it starts in an interactive mode: + + Copyright (C) + +This program 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, your program's commands might +be different; for a GUI interface, you would use an "about box". + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. For +more information on this, and how to apply and follow the GNU GPL, see . + +The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General Public +License instead of this License. But first, please read . diff --git a/local/recipes/kde/kwin/source/LICENSES/LGPL-2.0-only.txt b/local/recipes/kde/kwin/source/LICENSES/LGPL-2.0-only.txt new file mode 100644 index 0000000000..5c96471aaf --- /dev/null +++ b/local/recipes/kde/kwin/source/LICENSES/LGPL-2.0-only.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/local/recipes/kde/kwin/source/LICENSES/LGPL-2.0-or-later.txt b/local/recipes/kde/kwin/source/LICENSES/LGPL-2.0-or-later.txt new file mode 100644 index 0000000000..5c96471aaf --- /dev/null +++ b/local/recipes/kde/kwin/source/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/local/recipes/kde/kwin/source/LICENSES/LGPL-2.1-only.txt b/local/recipes/kde/kwin/source/LICENSES/LGPL-2.1-only.txt new file mode 100644 index 0000000000..130dffb311 --- /dev/null +++ b/local/recipes/kde/kwin/source/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/local/recipes/kde/kwin/source/LICENSES/LGPL-2.1-or-later.txt b/local/recipes/kde/kwin/source/LICENSES/LGPL-2.1-or-later.txt new file mode 100644 index 0000000000..c9aa53018e --- /dev/null +++ b/local/recipes/kde/kwin/source/LICENSES/LGPL-2.1-or-later.txt @@ -0,0 +1,175 @@ +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/local/recipes/kde/kwin/source/LICENSES/LGPL-3.0-only.txt b/local/recipes/kde/kwin/source/LICENSES/LGPL-3.0-only.txt new file mode 100644 index 0000000000..bd405afbef --- /dev/null +++ b/local/recipes/kde/kwin/source/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/local/recipes/kde/kwin/source/LICENSES/LicenseRef-KDE-Accepted-GPL.txt b/local/recipes/kde/kwin/source/LICENSES/LicenseRef-KDE-Accepted-GPL.txt new file mode 100644 index 0000000000..60a2dffc9c --- /dev/null +++ b/local/recipes/kde/kwin/source/LICENSES/LicenseRef-KDE-Accepted-GPL.txt @@ -0,0 +1,12 @@ +This library 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 3 of +the license or (at your option) at any later version that is +accepted by the membership of KDE e.V. (or its successor +approved by the membership of KDE e.V.), which shall act as a +proxy as defined in Section 14 of version 3 of the license. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. diff --git a/local/recipes/kde/kwin/source/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt b/local/recipes/kde/kwin/source/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt new file mode 100644 index 0000000000..232b3c5da1 --- /dev/null +++ b/local/recipes/kde/kwin/source/LICENSES/LicenseRef-KDE-Accepted-LGPL.txt @@ -0,0 +1,12 @@ +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the license or (at your option) any later version +that is accepted by the membership of KDE e.V. (or its successor +approved by the membership of KDE e.V.), which shall act as a +proxy as defined in Section 6 of version 3 of the license. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. diff --git a/local/recipes/kde/kwin/source/LICENSES/MIT.txt b/local/recipes/kde/kwin/source/LICENSES/MIT.txt new file mode 100644 index 0000000000..204b93da48 --- /dev/null +++ b/local/recipes/kde/kwin/source/LICENSES/MIT.txt @@ -0,0 +1,19 @@ +MIT License Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/local/recipes/kde/kwin/source/Mainpage.dox b/local/recipes/kde/kwin/source/Mainpage.dox new file mode 100644 index 0000000000..c0d4e69bd4 --- /dev/null +++ b/local/recipes/kde/kwin/source/Mainpage.dox @@ -0,0 +1,19 @@ +/** @mainpage KWin + +KWin is the KDE window manager. + +@authors +Matthias Ettrich \
+Lubos Lunak \ + +@maintainers +Lubos Lunak \ + +@licenses +@gpl + + +*/ + +// DOXYGEN_SET_PROJECT_NAME = KWin +// vim:ts=4:sw=4:expandtab:filetype=doxygen diff --git a/local/recipes/kde/kwin/source/README.md b/local/recipes/kde/kwin/source/README.md new file mode 100644 index 0000000000..572888de19 --- /dev/null +++ b/local/recipes/kde/kwin/source/README.md @@ -0,0 +1,50 @@ +# KWin + +KWin is an easy to use, but flexible, compositor for Wayland on Linux. Its primary usage is in conjunction with a Desktop Shell (e.g. KDE Plasma Desktop). KWin is designed to go out of the way; users should not notice that they use a window manager at all. Nevertheless KWin provides a steep learning curve for advanced features, which are available, if they do not conflict with the primary mission. KWin does not have a dedicated targeted user group, but follows the targeted user group of the Desktop Shell using KWin as it's window manager. + +## KWin is not... + + * a standalone Wayland compositor (c.f. labwc, sway) and does not provide any functionality belonging to a Desktop Shell. + * a replacement for window managers designed for use with a specific Desktop Shell (e.g. GNOME Shell) + * a minimalistic window manager + * designed for use with network transparency, though it is possible (with e.g. waypipe). + +# Contributing to KWin + +Please refer to the [contributing document](CONTRIBUTING.md) for everything you need to know to get started contributing to KWin. + +# Contacting KWin development team + + * mailing list: [kwin@kde.org](https://mail.kde.org/mailman/listinfo/kwin) + * IRC: #kde-kwin on irc.libera.chat + * Matrix: [#kwin:kde.org](https://go.kde.org/matrix/#/#kwin:kde.org) + +# Support +## Application Developer +If you are an application developer having questions regarding windowing systems (either X11 or Wayland) please do not hesitate to contact us. Preferable through our mailing list. Ideally subscribe to the mailing list, so that your mail doesn't get stuck in the moderation queue. + +## End user +Please contact the support channels of your Linux distribution for user support. The KWin development team does not provide end user support. + +# Reporting bugs + +Please use [KDE's bugtracker](https://bugs.kde.org) and report for [product KWin](https://bugs.kde.org/enter_bug.cgi?product=kwin). + +## Guidelines for new features + +A new Feature can only be added to KWin if: + + * it does not violate the primary missions as stated at the start of this document + * it does not introduce instabilities + * it is maintained, that is bugs are fixed in a timely manner (second next minor release) if it is not a corner case. + * it works together with all existing features + * it supports both single and multi screen + * it adds a significant advantage + * it is feature complete, that is supports at least all useful features from competitive implementations + * it is not a special case for a small user group + * it does not increase code complexity significantly + * it does not affect KWin's license (GPLv2+) + +All new added features are under probation, that is if any of the non-functional requirements as listed above do not hold true in the next two feature releases, the added feature will be removed again. + +The same non functional requirements hold true for any kind of plugins (effects, scripts, etc.). It is suggested to use scripted plugins and distribute them separately. diff --git a/local/recipes/kde/kwin/source/autotests/CMakeLists.txt b/local/recipes/kde/kwin/source/autotests/CMakeLists.txt new file mode 100644 index 0000000000..680a773171 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/CMakeLists.txt @@ -0,0 +1,286 @@ +add_definitions(-DKWIN_UNIT_TEST) +remove_definitions(-DQT_USE_QSTRINGBUILDER) +add_subdirectory(effect) +add_subdirectory(integration) +add_subdirectory(libinput) +add_subdirectory(wayland) +# drm autotests are broken on FreeBSD for yet unknown reasons +# As the test isn't doing anything platform specific, only run it on Linux +if(CMAKE_SYSTEM_NAME MATCHES "Linux") + add_subdirectory(drm) +endif() + +######################################################## +# Test WindowPaintData +######################################################## +set(testWindowPaintData_SRCS test_window_paint_data.cpp) +add_executable(testWindowPaintData ${testWindowPaintData_SRCS}) +target_link_libraries(testWindowPaintData kwin Qt::Widgets Qt::Test ) +add_test(NAME kwin-testWindowPaintData COMMAND testWindowPaintData) +ecm_mark_as_test(testWindowPaintData) + +######################################################## +# Test VirtualDesktopManager +######################################################## +set(testVirtualDesktops_SRCS + ../src/virtualdesktops.cpp + test_virtual_desktops.cpp +) +add_executable(testVirtualDesktops ${testVirtualDesktops_SRCS}) + +target_link_libraries(testVirtualDesktops + kwin + + Qt::Test + Qt::Widgets + + KF6::ConfigCore + KF6::GlobalAccel + KF6::I18n + KF6::WindowSystem +) +add_test(NAME kwin-testVirtualDesktops COMMAND testVirtualDesktops) +ecm_mark_as_test(testVirtualDesktops) + +######################################################## +# Test ClientMachine +######################################################## +if(KWIN_BUILD_X11) + set(testClientMachine_SRCS + ../src/client_machine.cpp + test_client_machine.cpp + xcb_scaling_mock.cpp + ) + add_executable(testClientMachine ${testClientMachine_SRCS}) + set_target_properties(testClientMachine PROPERTIES COMPILE_DEFINITIONS "NO_NONE_WINDOW") + + target_link_libraries(testClientMachine + Qt::Concurrent + Qt::GuiPrivate + Qt::Test + Qt::Widgets + + KF6::ConfigCore + KF6::WindowSystem + + XCB::XCB + XCB::XFIXES + + ${X11_X11_LIB} # to make jenkins happy + ) + add_test(NAME kwin-testClientMachine COMMAND testClientMachine) + ecm_mark_as_test(testClientMachine) + + ######################################################## + # Test XcbWrapper + ######################################################## + add_executable(testXcbWrapper test_xcb_wrapper.cpp xcb_scaling_mock.cpp) + + target_link_libraries(testXcbWrapper + Qt::GuiPrivate + Qt::Test + Qt::Widgets + + KF6::ConfigCore + KF6::WindowSystem + + XCB::XCB + ) + add_test(NAME kwin-testXcbWrapper COMMAND testXcbWrapper) + ecm_mark_as_test(testXcbWrapper) + + add_executable(testXcbSizeHints test_xcb_size_hints.cpp xcb_scaling_mock.cpp) + set_target_properties(testXcbSizeHints PROPERTIES COMPILE_DEFINITIONS "NO_NONE_WINDOW") + target_link_libraries(testXcbSizeHints + Qt::GuiPrivate + Qt::Test + Qt::Widgets + + KF6::ConfigCore + KF6::WindowSystem + + XCB::ICCCM + XCB::XCB + ) + add_test(NAME kwin-testXcbSizeHints COMMAND testXcbSizeHints) + ecm_mark_as_test(testXcbSizeHints) + + ######################################################## + # Test XcbWindow + ######################################################## + add_executable(testXcbWindow test_xcb_window.cpp xcb_scaling_mock.cpp) + + target_link_libraries(testXcbWindow + Qt::GuiPrivate + Qt::Test + Qt::Widgets + + KF6::ConfigCore + KF6::WindowSystem + + XCB::XCB + ) + add_test(NAME kwin-testXcbWindow COMMAND testXcbWindow) + ecm_mark_as_test(testXcbWindow) +endif() + +######################################################## +# Test OnScreenNotification +######################################################## +set(testOnScreenNotification_SRCS + ../src/input_event_spy.cpp + ../src/onscreennotification.cpp + onscreennotificationtest.cpp +) +add_executable(testOnScreenNotification ${testOnScreenNotification_SRCS}) + +target_link_libraries(testOnScreenNotification + Qt::DBus + Qt::Quick + Qt::Test + Qt::Widgets # QAction include + + KF6::ConfigCore +) + +add_test(NAME kwin-testOnScreenNotification COMMAND testOnScreenNotification) +ecm_mark_as_test(testOnScreenNotification) + +######################################################## +# Test Gestures +######################################################## +set(testGestures_SRCS + ../src/gestures.cpp + test_gestures.cpp +) +add_executable(testGestures ${testGestures_SRCS}) + +target_link_libraries(testGestures + Qt::Test + Qt::Widgets +) + +add_test(NAME kwin-testGestures COMMAND testGestures) +ecm_mark_as_test(testGestures) + +set(testOpenGLContextAttributeBuilder_SRCS + ../src/opengl/abstract_opengl_context_attribute_builder.cpp + ../src/opengl/egl_context_attribute_builder.cpp + opengl_context_attribute_builder_test.cpp +) + +add_executable(testOpenGLContextAttributeBuilder ${testOpenGLContextAttributeBuilder_SRCS}) +target_link_libraries(testOpenGLContextAttributeBuilder epoxy::epoxy Qt::Test) +add_test(NAME kwin-testOpenGLContextAttributeBuilder COMMAND testOpenGLContextAttributeBuilder) +ecm_mark_as_test(testOpenGLContextAttributeBuilder) + +set(testXkb_SRCS + ../src/xkb.cpp + test_xkb.cpp +) +qt_add_dbus_interface(testXkb_SRCS ${CMAKE_SOURCE_DIR}/src/org.freedesktop.DBus.Properties.xml dbusproperties_interface) +add_executable(testXkb ${testXkb_SRCS}) +target_link_libraries(testXkb + kwin + + Qt::Gui + Qt::GuiPrivate + Qt::Test + Qt::Widgets + + KF6::ConfigCore + KF6::WindowSystem + + XKB::XKB +) +add_test(NAME kwin-testXkb COMMAND testXkb) +ecm_mark_as_test(testXkb) + +######################################################## +# Test FTrace +######################################################## +add_executable(testFtrace test_ftrace.cpp) +target_link_libraries(testFtrace + Qt::Test + kwin +) +add_test(NAME kwin-testFtrace COMMAND testFtrace) +ecm_mark_as_test(testFtrace) + +######################################################## +# Test KWin Utils +######################################################## +add_executable(testUtils test_utils.cpp) +target_link_libraries(testUtils + Qt::Test + kwin +) +add_test(NAME kwin-testUtils COMMAND testUtils) +ecm_mark_as_test(testUtils) + +######################################################## +# Test OutputTransform +######################################################## +add_executable(testOutputTransform output_transform_test.cpp) +target_link_libraries(testOutputTransform + Qt::Test + kwin +) +add_test(NAME kwin-testOutputTransform COMMAND testOutputTransform) +ecm_mark_as_test(testOutputTransform) + +######################################################## +# Test Colorspace +######################################################## +add_executable(testColorspaces test_colorspaces.cpp) +target_link_libraries(testColorspaces + Qt::Test + kwin + lcms2::lcms2 +) +add_test(NAME kwin-testColorspaces COMMAND testColorspaces) +ecm_mark_as_test(testColorspaces) + +######################################################## +# Test UInt32Serial +######################################################## +add_executable(testSerial serial_test.cpp) +target_link_libraries(testSerial + Qt::Test + kwin +) +add_test(NAME kwin-testSerial COMMAND testSerial) +ecm_mark_as_test(testSerial) + +######################################################## +# Test Box +######################################################## +add_executable(testRect test_rect.cpp) +target_link_libraries(testRect + Qt::Test + kwin +) +add_test(NAME kwin-testRect COMMAND testRect) +ecm_mark_as_test(testRect) + +######################################################## +# Test BoxF +######################################################## +add_executable(testRectF test_rectf.cpp) +target_link_libraries(testRectF + Qt::Test + kwin +) +add_test(NAME kwin-testRectF COMMAND testRectF) +ecm_mark_as_test(testRectF) + +######################################################## +# Test Region +######################################################## +add_executable(testRegion test_region.cpp) +target_link_libraries(testRegion + Qt::Test + kwin +) +add_test(NAME kwin-testRegion COMMAND testRegion) +ecm_mark_as_test(testRegion) diff --git a/local/recipes/kde/kwin/source/autotests/data/Framework 13.icc b/local/recipes/kde/kwin/source/autotests/data/Framework 13.icc new file mode 100644 index 0000000000..23e604a6a4 Binary files /dev/null and b/local/recipes/kde/kwin/source/autotests/data/Framework 13.icc differ diff --git a/local/recipes/kde/kwin/source/autotests/data/Samsung CRG49 Shaper Matrix.icc b/local/recipes/kde/kwin/source/autotests/data/Samsung CRG49 Shaper Matrix.icc new file mode 100644 index 0000000000..1b969e05ff Binary files /dev/null and b/local/recipes/kde/kwin/source/autotests/data/Samsung CRG49 Shaper Matrix.icc differ diff --git a/local/recipes/kde/kwin/source/autotests/drm/CMakeLists.txt b/local/recipes/kde/kwin/source/autotests/drm/CMakeLists.txt new file mode 100644 index 0000000000..bc271fe9b0 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/drm/CMakeLists.txt @@ -0,0 +1,57 @@ +set(mockDRM_SRCS + mock_drm.cpp + ../../src/backends/drm/drm_abstract_output.cpp + ../../src/backends/drm/drm_backend.cpp + ../../src/backends/drm/drm_blob.cpp + ../../src/backends/drm/drm_buffer.cpp + ../../src/backends/drm/drm_colorop.cpp + ../../src/backends/drm/drm_commit.cpp + ../../src/backends/drm/drm_commit_thread.cpp + ../../src/backends/drm/drm_connector.cpp + ../../src/backends/drm/drm_crtc.cpp + ../../src/backends/drm/drm_egl_backend.cpp + ../../src/backends/drm/drm_egl_layer.cpp + ../../src/backends/drm/drm_egl_layer_surface.cpp + ../../src/backends/drm/drm_gpu.cpp + ../../src/backends/drm/drm_layer.cpp + ../../src/backends/drm/drm_logging.cpp + ../../src/backends/drm/drm_object.cpp + ../../src/backends/drm/drm_output.cpp + ../../src/backends/drm/drm_pipeline.cpp + ../../src/backends/drm/drm_pipeline_legacy.cpp + ../../src/backends/drm/drm_plane.cpp + ../../src/backends/drm/drm_property.cpp + ../../src/backends/drm/drm_qpainter_backend.cpp + ../../src/backends/drm/drm_qpainter_layer.cpp + ../../src/backends/drm/drm_virtual_egl_layer.cpp + ../../src/backends/drm/drm_virtual_output.cpp +) + +include_directories(${Libdrm_INCLUDE_DIRS}) + +add_library(LibDrmTest STATIC ${mockDRM_SRCS}) +target_link_libraries(LibDrmTest + Qt::Gui + Qt::Widgets + KF6::ConfigCore + KF6::WindowSystem + KF6::CoreAddons + KF6::I18n + PkgConfig::Libxcvt + gbm::gbm + Libdrm::Libdrm + kwin +) +target_include_directories(LibDrmTest + PUBLIC + ../../src + ../../src/backends/drm/ +) + +######################################################## +# Tests +######################################################## +add_executable(testMockDrm mockDrmTest.cpp) +target_link_libraries(testMockDrm LibDrmTest Qt::Test) +add_test(NAME kwin-testMockDrm COMMAND testMockDrm) +ecm_mark_as_test(testMockDrm) diff --git a/local/recipes/kde/kwin/source/autotests/drm/mock_drm.cpp b/local/recipes/kde/kwin/source/autotests/drm/mock_drm.cpp new file mode 100644 index 0000000000..3edeed1234 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/drm/mock_drm.cpp @@ -0,0 +1,1290 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "mock_drm.h" + +#include +extern "C" { +#include +} +#include + +#include + +#include +#include + +// Mock impls + +static QMap s_gpus; + +static MockGpu *getGpu(int fd) +{ + return s_gpus[fd]; +} + +MockGpu::MockGpu(int fd, const QString &devNode, int numCrtcs, int gammaSize) + : fd(fd) + , devNode(devNode) +{ + s_gpus.insert(fd, this); + for (int i = 0; i < numCrtcs; i++) { + const auto &plane = std::make_shared(this, PlaneType::Primary, i); + crtcs << std::make_shared(this, plane, i, gammaSize); + planes << plane; + } + deviceCaps.insert(MOCKDRM_DEVICE_CAP_ATOMIC, 1); + deviceCaps.insert(DRM_CAP_DUMB_BUFFER, 1); + deviceCaps.insert(DRM_CAP_PRIME, DRM_PRIME_CAP_IMPORT | DRM_PRIME_CAP_EXPORT); + deviceCaps.insert(DRM_CAP_ASYNC_PAGE_FLIP, 0); + deviceCaps.insert(DRM_CAP_ADDFB2_MODIFIERS, 1); +} + +MockGpu::~MockGpu() +{ + s_gpus.remove(fd); +} + +MockPropertyBlob *MockGpu::getBlob(uint32_t id) const +{ + auto it = std::find_if(propertyBlobs.begin(), propertyBlobs.end(), [id](const auto &propBlob) { + return propBlob->id == id; + }); + return it == propertyBlobs.end() ? nullptr : it->get(); +} + +MockConnector *MockGpu::findConnector(uint32_t id) const +{ + auto it = std::find_if(connectors.constBegin(), connectors.constEnd(), [id](const auto &c){return c->id == id;}); + return it == connectors.constEnd() ? nullptr : (*it).get(); +} + +MockCrtc *MockGpu::findCrtc(uint32_t id) const +{ + auto it = std::find_if(crtcs.constBegin(), crtcs.constEnd(), [id](const auto &c){return c->id == id;}); + return it == crtcs.constEnd() ? nullptr : (*it).get(); +} + +MockPlane *MockGpu::findPlane(uint32_t id) const +{ + auto it = std::find_if(planes.constBegin(), planes.constEnd(), [id](const auto &p){return p->id == id;}); + return it == planes.constEnd() ? nullptr : (*it).get(); +} + +void MockGpu::flipPage(uint32_t crtcId) +{ + auto crtc = findCrtc(crtcId); + Q_ASSERT(crtc); + for (const auto &plane : std::as_const(planes)) { + if (plane->getProp(QStringLiteral("CRTC_ID")) == crtc->id) { + plane->currentFb = plane->nextFb; + } + } + // TODO page flip event? +} + +// + +MockObject::MockObject(MockGpu *gpu) + : id(gpu->idCounter++) + , gpu(gpu) +{ + gpu->objects << this; +} + +MockObject::~MockObject() +{ + gpu->objects.removeOne(this); +} + +uint64_t MockObject::getProp(const QString &propName) const +{ + for (const auto &prop : std::as_const(props)) { + if (prop.name == propName) { + return prop.value; + } + } + Q_UNREACHABLE(); +} + +void MockObject::setProp(const QString &propName, uint64_t value) +{ + for (auto &prop : props) { + if (prop.name == propName) { + prop.value = value; + return; + } + } + Q_UNREACHABLE(); +} + +uint32_t MockObject::getPropId(const QString &propName) const +{ + for (const auto &prop : std::as_const(props)) { + if (prop.name == propName) { + return prop.id; + } + } + Q_UNREACHABLE(); +} + +// + +MockProperty::MockProperty(MockObject *obj, QString name, uint64_t initialValue, uint32_t flags, QList enums) + : obj(obj) + , id(obj->gpu->idCounter++) + , flags(flags) + , name(name) + , value(initialValue) + , enums(enums) +{ + qDebug("Added property %s with id %u to object %u", qPrintable(name), id, obj->id); +} + +MockPropertyBlob::MockPropertyBlob(MockGpu *gpu, const void *d, size_t size) + : gpu(gpu) + , id(gpu->idCounter++) + , data(malloc(size)) + , size(size) +{ + memcpy(data, d, size); +} + +MockPropertyBlob::~MockPropertyBlob() +{ + free(data); +} + +// + +#define addProp(name, value, flags) props << MockProperty(this, QStringLiteral(name), value, flags) + +MockConnector::MockConnector(MockGpu *gpu, bool nonDesktop) + : MockObject(gpu) + , connection(DRM_MODE_CONNECTED) + , type(DRM_MODE_CONNECTOR_DisplayPort) + , encoder(std::make_shared(gpu, 0xFF)) +{ + gpu->encoders << encoder; + addProp("CRTC_ID", 0, DRM_MODE_PROP_ATOMIC); + + addProp("Subpixel", DRM_MODE_SUBPIXEL_UNKNOWN, DRM_MODE_PROP_IMMUTABLE); + addProp("non-desktop", nonDesktop, DRM_MODE_PROP_IMMUTABLE); + addProp("vrr_capable", 0, DRM_MODE_PROP_IMMUTABLE); + + addProp("DPMS", DRM_MODE_DPMS_OFF, 0); + addProp("EDID", 0, DRM_MODE_PROP_BLOB | DRM_MODE_PROP_IMMUTABLE); + + addMode(1920, 1080, 60.0); +} + +void MockConnector::addMode(uint32_t width, uint32_t height, float refreshRate, bool preferred) +{ + auto modeInfo = libxcvt_gen_mode_info(width, height, refreshRate, false, false); + + drmModeModeInfo mode{ + .clock = uint32_t(modeInfo->dot_clock), + .hdisplay = uint16_t(modeInfo->hdisplay), + .hsync_start = modeInfo->hsync_start, + .hsync_end = modeInfo->hsync_end, + .htotal = modeInfo->htotal, + .hskew = 0, + .vdisplay = uint16_t(modeInfo->vdisplay), + .vsync_start = modeInfo->vsync_start, + .vsync_end = modeInfo->vsync_end, + .vtotal = modeInfo->vtotal, + .vscan = 1, + .vrefresh = uint32_t(modeInfo->vrefresh), + .flags = modeInfo->mode_flags, + .type = DRM_MODE_TYPE_DRIVER, + }; + if (preferred) { + mode.type |= DRM_MODE_TYPE_PREFERRED; + } + sprintf(mode.name, "%dx%d@%d", width, height, mode.vrefresh); + + modes.push_back(mode); + free(modeInfo); +} + +void MockConnector::setVrrCapable(bool cap) +{ + auto &prop = *std::ranges::find_if(props, [](const auto &prop) { + return prop.name == "vrr_capable"; + }); + prop.value = cap ? 1 : 0; +} + +// + +MockCrtc::MockCrtc(MockGpu *gpu, const std::shared_ptr &legacyPlane, int pipeIndex, int gamma_size) + : MockObject(gpu) + , pipeIndex(pipeIndex) + , gamma_size(gamma_size) + , legacyPlane(legacyPlane) +{ + addProp("MODE_ID", 0, DRM_MODE_PROP_ATOMIC | DRM_MODE_PROP_BLOB); + addProp("ACTIVE", 0, DRM_MODE_PROP_ATOMIC); + addProp("GAMMA_LUT", 0, DRM_MODE_PROP_ATOMIC | DRM_MODE_PROP_BLOB); + addProp("GAMMA_LUT_SIZE", gamma_size, DRM_MODE_PROP_ATOMIC); +} + + +// + +MockPlane::MockPlane(MockGpu *gpu, PlaneType type, int crtcIndex) + : MockObject(gpu) + , possibleCrtcs(1 << crtcIndex) + , type(type) +{ + props << MockProperty(this, QStringLiteral("type"), static_cast(type), DRM_MODE_PROP_IMMUTABLE | DRM_MODE_PROP_ENUM, + {QByteArrayLiteral("Primary"), QByteArrayLiteral("Overlay"), QByteArrayLiteral("Cursor")}); + addProp("FB_ID", 0, DRM_MODE_PROP_ATOMIC); + addProp("CRTC_ID", 0, DRM_MODE_PROP_ATOMIC); + addProp("CRTC_X", 0, DRM_MODE_PROP_ATOMIC); + addProp("CRTC_Y", 0, DRM_MODE_PROP_ATOMIC); + addProp("CRTC_W", 0, DRM_MODE_PROP_ATOMIC); + addProp("CRTC_H", 0, DRM_MODE_PROP_ATOMIC); + addProp("SRC_X", 0, DRM_MODE_PROP_ATOMIC); + addProp("SRC_Y", 0, DRM_MODE_PROP_ATOMIC); + addProp("SRC_W", 0, DRM_MODE_PROP_ATOMIC); + addProp("SRC_H", 0, DRM_MODE_PROP_ATOMIC); +} + +// + +MockEncoder::MockEncoder(MockGpu* gpu, uint32_t possible_crtcs) + : MockObject(gpu) + , possible_crtcs(possible_crtcs) +{ +} + + +// + +MockFb::MockFb(MockGpu *gpu, uint32_t width, uint32_t height) + : id(gpu->idCounter++) + , width(width) + , height(height) + , gpu(gpu) +{ + gpu->fbs << this; +} + +MockFb::~MockFb() +{ + gpu->fbs.removeOne(this); +} + +// drm functions + +#define GPU(fd, error) \ + auto gpu = getGpu(fd); \ + if (!gpu) { \ + qWarning("invalid fd %d", fd); \ + errno = EINVAL; \ + return error; \ + } \ + std::scoped_lock lock(gpu->m_mutex); + +drmVersionPtr drmGetVersion(int fd) +{ + GPU(fd, nullptr); + drmVersionPtr ptr = new drmVersion; + ptr->name = new char[gpu->name.size() + 1]; + strcpy(ptr->name, gpu->name.data()); + return ptr; +} + +void drmFreeVersion(drmVersionPtr ptr) +{ + Q_ASSERT(ptr); + delete[] ptr->name; + delete ptr; +} + +int drmSetClientCap(int fd, uint64_t capability, uint64_t value) +{ + GPU(fd, -EINVAL); + if (capability == DRM_CLIENT_CAP_ATOMIC) { + if (!gpu->deviceCaps[MOCKDRM_DEVICE_CAP_ATOMIC]) { + return -(errno = ENOTSUP); + } + qDebug("Setting DRM_CLIENT_CAP_ATOMIC to %lu", value); + } + gpu->clientCaps.insert(capability, value); + return 0; +} + +int drmGetCap(int fd, uint64_t capability, uint64_t *value) +{ + GPU(fd, -EINVAL); + if (gpu->deviceCaps.contains(capability)) { + *value = gpu->deviceCaps[capability]; + return 0; + } + qDebug("Could not find capability %lu", capability); + return -(errno = EINVAL); +} + +int drmHandleEvent(int fd, drmEventContextPtr evctx) +{ + GPU(fd, -EINVAL); + return -(errno = ENOTSUP); +} + +int drmIoctl(int fd, unsigned long request, void *arg) +{ + if (request == DRM_IOCTL_PRIME_FD_TO_HANDLE) { + GPU(fd, -EINVAL); + auto args = static_cast(arg); + args->handle = 42; // just pass a dummy value so the request doesn't fail + return 0; + } else if (request == DRM_IOCTL_PRIME_HANDLE_TO_FD) { + return -(errno = ENOTSUP); + } else if (request == DRM_IOCTL_GEM_CLOSE) { + GPU(fd, -EINVAL); + return 0; + } else if (request == DRM_IOCTL_MODE_ATOMIC) { + const auto args = static_cast(arg); + auto req = drmModeAtomicAlloc(); + const uint32_t *const objects = reinterpret_cast(args->objs_ptr); + const uint32_t *const propsCounts = reinterpret_cast(args->count_props_ptr); + const uint32_t *const props = reinterpret_cast(args->props_ptr); + const uint64_t *const values = reinterpret_cast(args->prop_values_ptr); + uint32_t propIndex = 0; + for (uint32_t objIndex = 0; objIndex < args->count_objs; objIndex++) { + const uint32_t objectId = objects[objIndex]; + const uint32_t count = propsCounts[objIndex]; + for (uint32_t i = 0; i < count; i++) { + drmModeAtomicAddProperty(req, objectId, props[propIndex + i], values[propIndex + i]); + } + propIndex += count; + } + int ret = drmModeAtomicCommit(fd, req, args->flags, reinterpret_cast(args->user_data)); + drmModeAtomicFree(req); + return ret; + } else if (request == DRM_IOCTL_MODE_RMFB) { + drmModeRmFB(fd, *static_cast(arg)); + } + return -(errno = ENOTSUP); +} + +drmModeResPtr drmModeGetResources(int fd) +{ + GPU(fd, nullptr) + drmModeResPtr res = new drmModeRes; + + res->count_connectors = gpu->connectors.count(); + res->connectors = res->count_connectors ? new uint32_t[res->count_connectors] : nullptr; + int i = 0; + for (const auto &conn : std::as_const(gpu->connectors)) { + res->connectors[i++] = conn->id; + } + + res->count_encoders = gpu->encoders.count(); + res->encoders = res->count_encoders ? new uint32_t[res->count_encoders] : nullptr; + i = 0; + for (const auto &enc : std::as_const(gpu->encoders)) { + res->encoders[i++] = enc->id; + } + + res->count_crtcs = gpu->crtcs.count(); + res->crtcs = res->count_crtcs ? new uint32_t[res->count_crtcs] : nullptr; + i = 0; + for (const auto &crtc : std::as_const(gpu->crtcs)) { + res->crtcs[i++] = crtc->id; + } + + res->count_fbs = gpu->fbs.count(); + res->fbs = res->count_fbs ? new uint32_t[res->count_fbs] : nullptr; + i = 0; + for (const auto &fb : std::as_const(gpu->fbs)) { + res->fbs[i++] = fb->id; + } + + res->min_width = 0; + res->min_height = 0; + res->max_width = 2 << 14; + res->max_height = 2 << 14; + + gpu->resPtrs << res; + return res; +} + +int drmModeAddFB(int fd, uint32_t width, uint32_t height, uint8_t depth, + uint8_t bpp, uint32_t pitch, uint32_t bo_handle, + uint32_t *buf_id) +{ + GPU(fd, EINVAL) + auto fb = new MockFb(gpu, width, height); + *buf_id = fb->id; + return 0; +} + +int drmModeAddFB2(int fd, uint32_t width, uint32_t height, + uint32_t pixel_format, const uint32_t bo_handles[4], + const uint32_t pitches[4], const uint32_t offsets[4], + uint32_t *buf_id, uint32_t flags) +{ + GPU(fd, EINVAL) + auto fb = new MockFb(gpu, width, height); + *buf_id = fb->id; + return 0; +} + +int drmModeAddFB2WithModifiers(int fd, uint32_t width, uint32_t height, + uint32_t pixel_format, const uint32_t bo_handles[4], + const uint32_t pitches[4], const uint32_t offsets[4], + const uint64_t modifier[4], uint32_t *buf_id, + uint32_t flags) +{ + GPU(fd, EINVAL) + if (!gpu->deviceCaps.contains(DRM_CAP_ADDFB2_MODIFIERS)) { + return -(errno = ENOTSUP); + } + auto fb = new MockFb(gpu, width, height); + *buf_id = fb->id; + return 0; +} + +int drmModeRmFB(int fd, uint32_t bufferId) +{ + GPU(fd, EINVAL) + auto it = std::find_if(gpu->fbs.begin(), gpu->fbs.end(), [bufferId](const auto &fb){return fb->id == bufferId;}); + if (it == gpu->fbs.end()) { + qWarning("invalid bufferId %u passed to drmModeRmFB", bufferId); + return EINVAL; + } else { + auto fb = *it; + gpu->fbs.erase(it); + for (const auto &plane : std::as_const(gpu->planes)) { + if (plane->nextFb == fb) { + plane->nextFb = nullptr; + } + if (plane->currentFb == fb) { + qWarning("current fb %u of plane %u got removed. Deactivating plane", bufferId, plane->id); + plane->setProp(QStringLiteral("CRTC_ID"), 0); + plane->setProp(QStringLiteral("FB_ID"), 0); + plane->currentFb = nullptr; + + auto crtc = gpu->findCrtc(plane->getProp(QStringLiteral("CRTC_ID"))); + Q_ASSERT(crtc); + crtc->setProp(QStringLiteral("ACTIVE"), 0); + qWarning("deactvating crtc %u", crtc->id); + + for (const auto &conn : std::as_const(gpu->connectors)) { + if (conn->getProp(QStringLiteral("CRTC_ID")) == crtc->id) { + conn->setProp(QStringLiteral("CRTC_ID"), 0); + qWarning("deactvating connector %u", conn->id); + } + } + } + } + delete fb; + return 0; + } +} + +drmModeCrtcPtr drmModeGetCrtc(int fd, uint32_t crtcId) +{ + GPU(fd, nullptr); + if (auto crtc = gpu->findCrtc(crtcId)) { + drmModeCrtcPtr c = new drmModeCrtc; + c->crtc_id = crtcId; + c->buffer_id = crtc->currentFb ? crtc->currentFb->id : 0; + c->gamma_size = crtc->gamma_size; + c->mode_valid = crtc->modeValid; + c->mode = crtc->mode; + c->x = 0; + c->y = 0; + c->width = crtc->mode.hdisplay; + c->height = crtc->mode.vdisplay; + gpu->drmCrtcs << c; + return c; + } else { + qWarning("invalid crtcId %u passed to drmModeGetCrtc", crtcId); + errno = EINVAL; + return nullptr; + } +} + +int drmModeSetCrtc(int fd, uint32_t crtcId, uint32_t bufferId, + uint32_t x, uint32_t y, uint32_t *connectors, int count, + drmModeModeInfoPtr mode) +{ + GPU(fd, -EINVAL); + auto crtc = gpu->findCrtc(crtcId); + if (!crtc) { + qWarning("invalid crtcId %u passed to drmModeSetCrtc", crtcId); + return -(errno = EINVAL); + } + auto oldModeBlob = crtc->getProp(QStringLiteral("MODE_ID")); + uint32_t modeBlob = 0; + if (mode) { + drmModeCreatePropertyBlob(fd, mode, sizeof(drmModeModeInfo), &modeBlob); + } + + auto req = drmModeAtomicAlloc(); + req->legacyEmulation = true; + drmModeAtomicAddProperty(req, crtcId, crtc->getPropId(QStringLiteral("MODE_ID")), modeBlob); + drmModeAtomicAddProperty(req, crtcId, crtc->getPropId(QStringLiteral("ACTIVE")), modeBlob && count); + QList conns; + for (int i = 0; i < count; i++) { + conns << connectors[i]; + } + for (const auto &conn : std::as_const(gpu->connectors)) { + if (conns.contains(conn->id)) { + drmModeAtomicAddProperty(req, conn->id, conn->getPropId(QStringLiteral("CRTC_ID")), modeBlob ? crtc->id : 0); + conns.removeOne(conn->id); + } else if (conn->getProp(QStringLiteral("CRTC_ID")) == crtc->id) { + drmModeAtomicAddProperty(req, conn->id, conn->getPropId(QStringLiteral("CRTC_ID")), 0); + } + } + if (!conns.isEmpty()) { + for (const auto &c : std::as_const(conns)) { + qWarning("invalid connector %u passed to drmModeSetCrtc", c); + } + drmModeAtomicFree(req); + return -(errno = EINVAL); + } + drmModeAtomicAddProperty(req, crtc->legacyPlane->id, crtc->legacyPlane->getPropId(QStringLiteral("CRTC_ID")), modeBlob && count ? crtc->id : 0); + drmModeAtomicAddProperty(req, crtc->legacyPlane->id, crtc->legacyPlane->getPropId(QStringLiteral("CRTC_X")), x); + drmModeAtomicAddProperty(req, crtc->legacyPlane->id, crtc->legacyPlane->getPropId(QStringLiteral("CRTC_Y")), y); + drmModeAtomicAddProperty(req, crtc->legacyPlane->id, crtc->legacyPlane->getPropId(QStringLiteral("CRTC_W")), mode->hdisplay - x); + drmModeAtomicAddProperty(req, crtc->legacyPlane->id, crtc->legacyPlane->getPropId(QStringLiteral("CRTC_H")), mode->vdisplay - y); + drmModeAtomicAddProperty(req, crtc->legacyPlane->id, crtc->legacyPlane->getPropId(QStringLiteral("FB_ID")), bufferId); + int result = drmModeAtomicCommit(fd, req, DRM_MODE_ATOMIC_ALLOW_MODESET, nullptr); + drmModeAtomicFree(req); + if (result == 0) { + drmModeDestroyPropertyBlob(fd, oldModeBlob); + } + return result; +} + +int drmModeSetCursor(int fd, uint32_t crtcId, uint32_t bo_handle, uint32_t width, uint32_t height) +{ + GPU(fd, -EINVAL); + if (auto crtc = gpu->findCrtc(crtcId)) { + crtc->cursorRect.setSize(QSize(width, height)); + return 0; + } else { + qWarning("invalid crtcId %u passed to drmModeSetCursor", crtcId); + return -(errno = EINVAL); + } +} + +int drmModeSetCursor2(int fd, uint32_t crtcId, uint32_t bo_handle, uint32_t width, uint32_t height, int32_t hot_x, int32_t hot_y) +{ + GPU(fd, -EINVAL); + return -(errno = ENOTSUP); +} + +int drmModeMoveCursor(int fd, uint32_t crtcId, int x, int y) +{ + GPU(fd, -EINVAL); + if (auto crtc = gpu->findCrtc(crtcId)) { + crtc->cursorRect.moveTo(x, y); + return 0; + } else { + qWarning("invalid crtcId %u passed to drmModeMoveCursor", crtcId); + return -(errno = EINVAL); + } +} + +drmModeEncoderPtr drmModeGetEncoder(int fd, uint32_t encoder_id) +{ + GPU(fd, nullptr); + auto it = std::find_if(gpu->encoders.constBegin(), gpu->encoders.constEnd(), [encoder_id](const auto &e){return e->id == encoder_id;}); + if (it == gpu->encoders.constEnd()) { + qWarning("invalid encoder_id %u passed to drmModeGetEncoder", encoder_id); + errno = EINVAL; + return nullptr; + } else { + auto encoder = *it; + drmModeEncoderPtr enc = new drmModeEncoder; + enc->encoder_id = encoder_id; + enc->crtc_id = encoder->crtc ? encoder->crtc->id : 0; + enc->encoder_type = 0; + enc->possible_crtcs = encoder->possible_crtcs; + enc->possible_clones = encoder->possible_clones; + + gpu->drmEncoders << enc; + return enc; + } +} + +// Instance ID of (some) specific connector type, incremented +// for each new connector (of any type) being created. +// There are no particular guarantees on the _stability_ of +// connector type "instance IDs" issued by the kernel, +// so simply giving each (new) connector a fresh ID is +// acceptable. +static std::atomic autoIncrementedConnectorId{}; + +drmModeConnectorPtr drmModeGetConnector(int fd, uint32_t connectorId) +{ + GPU(fd, nullptr); + if (auto conn = gpu->findConnector(connectorId)) { + drmModeConnectorPtr c = new drmModeConnector{}; + c->connector_id = conn->id; + c->connection = conn->connection; + + c->connector_type = conn->type; + c->connector_type_id = autoIncrementedConnectorId++; + + c->encoder_id = conn->encoder ? conn->encoder->id : 0; + c->count_encoders = conn->encoder ? 1 : 0; + c->encoders = c->count_encoders ? new uint32_t[1] : nullptr; + if (c->encoders) { + c->encoders[0] = conn->encoder->id; + } + c->count_modes = conn->modes.count(); + c->modes = c->count_modes ? new drmModeModeInfo[c->count_modes] : nullptr; + for (int i = 0; i < c->count_modes; i++) { + c->modes[i] = conn->modes[i]; + } + c->mmHeight = 900; + c->mmWidth = 1600; + c->subpixel = DRM_MODE_SUBPIXEL_HORIZONTAL_RGB; + + // these are not used nor will they be + c->count_props = -1; + c->props = nullptr; + c->prop_values = nullptr; + + gpu->drmConnectors << c; + return c; + } else { + qWarning("invalid connectorId %u passed to drmModeGetConnector", connectorId); + errno = EINVAL; + return nullptr; + } +} + +drmModeConnectorPtr drmModeGetConnectorCurrent(int fd, uint32_t connector_id) +{ + return drmModeGetConnector(fd, connector_id); +} + +int drmModeCrtcSetGamma(int fd, uint32_t crtc_id, uint32_t size, uint16_t *red, uint16_t *green, uint16_t *blue) +{ + return -(errno = ENOTSUP); +} + +int drmModePageFlip(int fd, uint32_t crtc_id, uint32_t fb_id, uint32_t flags, void *user_data) +{ + GPU(fd, -EINVAL); + auto crtc = gpu->findCrtc(crtc_id); + if (!crtc) { + qWarning("invalid crtc_id %u passed to drmModePageFlip", crtc_id); + return -(errno = EINVAL); + } + auto req = drmModeAtomicAlloc(); + req->legacyEmulation = true; + drmModeAtomicAddProperty(req, crtc->legacyPlane->id, crtc->legacyPlane->getPropId(QStringLiteral("FB_ID")), fb_id); + int result = drmModeAtomicCommit(fd, req, flags, user_data); + drmModeAtomicFree(req); + return result; +} + + +drmModePlaneResPtr drmModeGetPlaneResources(int fd) +{ + GPU(fd, nullptr); + drmModePlaneResPtr res = new drmModePlaneRes; + res->count_planes = gpu->planes.count(); + res->planes = res->count_planes ? new uint32_t[res->count_planes] : nullptr; + for (uint i = 0; i < res->count_planes; i++) { + res->planes[i] = gpu->planes[i]->id; + } + gpu->drmPlaneRes << res; + return res; +} + +drmModePlanePtr drmModeGetPlane(int fd, uint32_t plane_id) +{ + GPU(fd, nullptr); + if (auto plane = gpu->findPlane(plane_id)) { + drmModePlanePtr p = new drmModePlane; + p->plane_id = plane_id; + p->crtc_id = plane->getProp(QStringLiteral("CRTC_ID")); + p->crtc_x = plane->getProp(QStringLiteral("CRTC_X")); + p->crtc_y = plane->getProp(QStringLiteral("CRTC_Y")); + p->fb_id = plane->getProp(QStringLiteral("FB_ID")); + p->x = plane->getProp(QStringLiteral("SRC_X")); + p->y = plane->getProp(QStringLiteral("SRC_Y")); + p->possible_crtcs = plane->possibleCrtcs; + + // unused atm: + p->count_formats = 0; + p->formats = nullptr; + p->gamma_size = 0; + + gpu->drmPlanes << p; + return p; + } else { + qWarning("invalid plane_id %u passed to drmModeGetPlane", plane_id); + errno = EINVAL; + return nullptr; + } +} + +drmModePropertyPtr drmModeGetProperty(int fd, uint32_t propertyId) +{ + GPU(fd, nullptr); + for (const auto &obj : std::as_const(gpu->objects)) { + for (auto &prop : std::as_const(obj->props)) { + if (prop.id == propertyId) { + drmModePropertyPtr p = new drmModePropertyRes; + p->prop_id = prop.id; + p->flags = prop.flags; + auto arr = prop.name.toLocal8Bit(); + strcpy(p->name, arr.constData()); + + p->count_blobs = prop.flags & DRM_MODE_PROP_BLOB ? 1 : 0; + if (p->count_blobs) { + p->blob_ids = new uint32_t[1]; + p->blob_ids[0] = prop.value; + } else { + p->blob_ids = nullptr; + } + + p->count_enums = prop.enums.count(); + p->enums = new drm_mode_property_enum[p->count_enums]; + for (int i = 0; i < p->count_enums; i++) { + strcpy(p->enums[i].name, prop.enums[i].constData()); + p->enums[i].value = i; + } + + p->count_values = 1; + p->values = new uint64_t[1]; + p->values[0] = prop.value; + + gpu->drmProps << p; + return p; + } + } + } + qWarning("invalid propertyId %u passed to drmModeGetProperty", propertyId); + errno = EINVAL; + return nullptr; +} + +void drmModeFreeProperty(drmModePropertyPtr ptr) +{ + if (!ptr) { + return; + } + for (const auto &gpu : std::as_const(s_gpus)) { + if (gpu->drmProps.removeOne(ptr)) { + delete[] ptr->values; + delete[] ptr->blob_ids; + delete[] ptr->enums; + delete ptr; + return; + } + } +} + + + +drmModePropertyBlobPtr drmModeGetPropertyBlob(int fd, uint32_t blob_id) +{ + GPU(fd, nullptr); + if (blob_id == 0) { + return nullptr; + } + auto it = std::find_if(gpu->propertyBlobs.begin(), gpu->propertyBlobs.end(), [blob_id](const auto &blob) { + return blob->id == blob_id; + }); + if (it == gpu->propertyBlobs.end()) { + qWarning("invalid blob_id %u passed to drmModeGetPropertyBlob", blob_id); + errno = EINVAL; + return nullptr; + } else { + auto blob = new drmModePropertyBlobRes; + blob->id = (*it)->id; + blob->length = (*it)->size; + blob->data = malloc(blob->length); + memcpy(blob->data, (*it)->data, blob->length); + return blob; + } +} + +void drmModeFreePropertyBlob(drmModePropertyBlobPtr ptr) +{ + if (!ptr) { + return; + } + for (const auto &gpu : std::as_const(s_gpus)) { + if (gpu->drmPropertyBlobs.removeOne(ptr)) { + free(ptr->data); + delete ptr; + return; + } + } +} + +int drmModeConnectorSetProperty(int fd, uint32_t connector_id, uint32_t property_id, uint64_t value) +{ + return drmModeObjectSetProperty(fd, connector_id, DRM_MODE_OBJECT_CONNECTOR, property_id, value); +} + +static uint32_t getType(MockObject *obj) +{ + if (dynamic_cast(obj)) { + return DRM_MODE_OBJECT_CONNECTOR; + } else if (dynamic_cast(obj)) { + return DRM_MODE_OBJECT_CRTC; + } else if (dynamic_cast(obj)) { + return DRM_MODE_OBJECT_PLANE; + } else { + return DRM_MODE_OBJECT_ANY; + } +} + +drmModeObjectPropertiesPtr drmModeObjectGetProperties(int fd, uint32_t object_id, uint32_t object_type) +{ + GPU(fd, nullptr); + auto it = std::find_if(gpu->objects.constBegin(), gpu->objects.constEnd(), [object_id](const auto &obj){return obj->id == object_id;}); + if (it == gpu->objects.constEnd()) { + qWarning("invalid object_id %u passed to drmModeObjectGetProperties", object_id); + errno = EINVAL; + return nullptr; + } else { + auto obj = *it; + if (auto type = getType(obj); type != object_type) { + qWarning("wrong object_type %u passed to drmModeObjectGetProperties for object %u with type %u", object_type, object_id, type); + errno = EINVAL; + return nullptr; + } + QList props; + bool deviceAtomic = gpu->clientCaps.contains(DRM_CLIENT_CAP_ATOMIC) && gpu->clientCaps[DRM_CLIENT_CAP_ATOMIC]; + for (const auto &prop : std::as_const(obj->props)) { + if (deviceAtomic || !(prop.flags & DRM_MODE_PROP_ATOMIC)) { + props << prop; + } + } + drmModeObjectPropertiesPtr p = new drmModeObjectProperties; + p->count_props = props.count(); + p->props = new uint32_t[p->count_props]; + p->prop_values = new uint64_t[p->count_props]; + int i = 0; + for (const auto &prop : std::as_const(props)) { + p->props[i] = prop.id; + p->prop_values[i] = prop.value; + i++; + } + gpu->drmObjectProperties << p; + return p; + } +} + +void drmModeFreeObjectProperties(drmModeObjectPropertiesPtr ptr) +{ + for (const auto &gpu : std::as_const(s_gpus)) { + if (gpu->drmObjectProperties.removeOne(ptr)) { + delete[] ptr->props; + delete[] ptr->prop_values; + delete ptr; + return; + } + } +} + +int drmModeObjectSetProperty(int fd, uint32_t object_id, uint32_t object_type, uint32_t property_id, uint64_t value) +{ + GPU(fd, -EINVAL); + auto it = std::find_if(gpu->objects.constBegin(), gpu->objects.constEnd(), [object_id](const auto &obj){return obj->id == object_id;}); + if (it == gpu->objects.constEnd()) { + qWarning("invalid object_id %u passed to drmModeObjectSetProperty", object_id); + return -(errno = EINVAL); + } else { + auto obj = *it; + if (auto type = getType(obj); type != object_type) { + qWarning("wrong object_type %u passed to drmModeObjectSetProperty for object %u with type %u", object_type, object_id, type); + return -(errno = EINVAL); + } + auto req = drmModeAtomicAlloc(); + req->legacyEmulation = true; + drmModeAtomicAddProperty(req, object_id, property_id, value); + int result = drmModeAtomicCommit(fd, req, 0, nullptr); + drmModeAtomicFree(req); + return result; + } +} + +static QList s_atomicReqs; + +drmModeAtomicReqPtr drmModeAtomicAlloc(void) +{ + auto req = new drmModeAtomicReq; + s_atomicReqs << req; + return req; +} + +void drmModeAtomicFree(drmModeAtomicReqPtr req) +{ + s_atomicReqs.removeOne(req); + delete req; +} + +int drmModeAtomicAddProperty(drmModeAtomicReqPtr req, uint32_t object_id, uint32_t property_id, uint64_t value) +{ + if (!req) { + return -(errno = EINVAL); + } + Prop p; + p.obj = object_id; + p.prop = property_id; + p.value = value; + req->props << p; + return req->props.count(); +} + +static bool checkIfEqual(const drmModeModeInfo &one, const drmModeModeInfo &two) +{ + return one.clock == two.clock + && one.hdisplay == two.hdisplay + && one.hsync_start == two.hsync_start + && one.hsync_end == two.hsync_end + && one.htotal == two.htotal + && one.hskew == two.hskew + && one.vdisplay == two.vdisplay + && one.vsync_start == two.vsync_start + && one.vsync_end == two.vsync_end + && one.vtotal == two.vtotal + && one.vscan == two.vscan + && one.vrefresh == two.vrefresh; +} + +int drmModeAtomicCommit(int fd, drmModeAtomicReqPtr req, uint32_t flags, void *user_data) +{ + GPU(fd, -EINVAL); + if (!req->legacyEmulation && (!gpu->clientCaps.contains(DRM_CLIENT_CAP_ATOMIC) || !gpu->clientCaps[DRM_CLIENT_CAP_ATOMIC])) { + qWarning("drmModeAtomicCommit requires the atomic capability"); + return -(errno = EINVAL); + } + + // verify flags + if ((flags & DRM_MODE_ATOMIC_NONBLOCK) && (flags & DRM_MODE_ATOMIC_ALLOW_MODESET)) { + qWarning() << "NONBLOCK and MODESET are not allowed together"; + return -(errno = EINVAL); + } else if ((flags & DRM_MODE_ATOMIC_TEST_ONLY) && (flags & DRM_MODE_PAGE_FLIP_EVENT)) { + qWarning() << "TEST_ONLY and PAGE_FLIP_EVENT are not allowed together"; + return -(errno = EINVAL); + } else if (flags & DRM_MODE_PAGE_FLIP_ASYNC) { + qWarning() << "PAGE_FLIP_ASYNC is currently not supported with AMS"; + return -(errno = EINVAL); + } + + QList connCopies; + for (const auto &conn : std::as_const(gpu->connectors)) { + connCopies << *conn; + } + QList crtcCopies; + for (const auto &crtc : std::as_const(gpu->crtcs)) { + crtcCopies << *crtc; + } + QList planeCopies; + for (const auto &plane : std::as_const(gpu->planes)) { + planeCopies << *plane; + } + + QList objects; + for (int i = 0; i < connCopies.count(); i++) { + objects << &connCopies[i]; + } + for (int i = 0; i < crtcCopies.count(); i++) { + objects << &crtcCopies[i]; + } + for (int i = 0; i < planeCopies.count(); i++) { + objects << &planeCopies[i]; + } + + // apply changes to the copies + for (int i = 0; i < req->props.count(); i++) { + auto p = req->props[i]; + auto it = std::find_if(objects.constBegin(), objects.constEnd(), [p](const auto &obj){return obj->id == p.obj;}); + if (it == objects.constEnd()) { + qWarning("Object %u in atomic request not found!", p.obj); + return -(errno = EINVAL); + } + auto &obj = *it; + if (obj->id == p.obj) { + auto prop = std::find_if(obj->props.begin(), obj->props.end(), [p](const auto &prop){return prop.id == p.prop;}); + if (prop == obj->props.end()) { + qWarning("Property %u in atomic request for object %u not found!", p.prop, p.obj); + return -(errno = EINVAL); + } + if (prop->value != p.value) { + if (!(flags & DRM_MODE_ATOMIC_ALLOW_MODESET) && (prop->name == QStringLiteral("CRTC_ID") || prop->name == QStringLiteral("ACTIVE"))) { + qWarning("Atomic request without DRM_MODE_ATOMIC_ALLOW_MODESET tries to do a modeset with object %u", obj->id); + return -(errno = EINVAL); + } + if (prop->flags & DRM_MODE_PROP_BLOB) { + auto blobExists = gpu->getBlob(p.value) != nullptr; + if (blobExists != (p.value > 0)) { + qWarning("Atomic request tries to set property %s on obj %u to invalid blob id %lu", qPrintable(prop->name), obj->id, p.value); + return -(errno = EINVAL); + } + } + prop->value = p.value; + } + } + } + + // check if the desired changes are allowed + struct Pipeline { + MockCrtc *crtc; + QList conns; + MockPlane *primaryPlane = nullptr; + }; + QList pipelines; + for (int i = 0; i < crtcCopies.count(); i++) { + if (crtcCopies[i].getProp(QStringLiteral("ACTIVE"))) { + auto blob = gpu->getBlob(crtcCopies[i].getProp(QStringLiteral("MODE_ID"))); + if (!blob) { + qWarning("Atomic request tries to enable CRTC %u without a mode", crtcCopies[i].id); + return -(errno = EINVAL); + } else if (blob->size != sizeof(drmModeModeInfo)) { + qWarning("Atomic request tries to enable CRTC %u with an invalid mode blob", crtcCopies[i].id); + return -(errno = EINVAL); + } + Pipeline pipeline; + pipeline.crtc = &crtcCopies[i]; + pipelines << pipeline; + } + } + for (int i = 0; i < connCopies.count(); i++) { + if (auto crtc = connCopies[i].getProp(QStringLiteral("CRTC_ID"))) { + bool found = false; + for (int p = 0; p < pipelines.count(); p++) { + if (pipelines[p].crtc->id == crtc) { + pipelines[p].conns << &connCopies[i]; + found = true; + break; + } + } + if (!found) { + qWarning("CRTC_ID of connector %u points to inactive or wrong crtc", connCopies[i].id); + return -(errno = EINVAL); + } + } + } + for (int i = 0; i < planeCopies.count(); i++) { + if (auto crtc = planeCopies[i].getProp(QStringLiteral("CRTC_ID"))) { + bool found = false; + for (int p = 0; p < pipelines.count(); p++) { + if (pipelines[p].crtc->id == crtc) { + if (pipelines[p].primaryPlane) { + qWarning("crtc %u has more than one primary planes assigned: %u and %u", pipelines[p].crtc->id, pipelines[p].primaryPlane->id, planeCopies[i].id); + return -(errno = EINVAL); + } else if (!(planeCopies[i].possibleCrtcs & (1 << pipelines[p].crtc->pipeIndex))) { + qWarning("crtc %u is not suitable for primary plane %u", pipelines[p].crtc->id, planeCopies[i].id); + return -(errno = EINVAL); + } else { + pipelines[p].primaryPlane = &planeCopies[i]; + found = true; + break; + } + } + } + if (!found) { + qWarning("CRTC_ID of plane %u points to inactive or wrong crtc", planeCopies[i].id); + return -(errno = EINVAL); + } + auto fbId = planeCopies[i].getProp(QStringLiteral("FB_ID")); + if (!fbId) { + qWarning("FB_ID of active plane %u is 0", planeCopies[i].id); + return -(errno = EINVAL); + } + auto it = std::find_if(gpu->fbs.constBegin(), gpu->fbs.constEnd(), [fbId](auto fb){return fb->id == fbId;}); + if (it == gpu->fbs.constEnd()) { + qWarning("FB_ID %lu of active plane %u is invalid", fbId, planeCopies[i].id); + return -(errno = EINVAL); + } + planeCopies[i].nextFb = *it; + } else { + planeCopies[i].nextFb = nullptr; + } + } + for (const auto &p : std::as_const(pipelines)) { + if (p.conns.isEmpty()) { + qWarning("Active crtc %u has no assigned connectors", p.crtc->id); + return -(errno = EINVAL); + } else if (!p.primaryPlane) { + qWarning("Active crtc %u has no assigned primary plane", p.crtc->id); + return -(errno = EINVAL); + } else { + drmModeModeInfo mode = *static_cast(gpu->getBlob(p.crtc->getProp(QStringLiteral("MODE_ID")))->data); + for (const auto &conn : p.conns) { + bool modeFound = std::find_if(conn->modes.constBegin(), conn->modes.constEnd(), [mode](const auto &m){ + return checkIfEqual(mode, m); + }) != conn->modes.constEnd(); + if (!modeFound) { + qWarning("mode on crtc %u is incompatible with connector %u", p.crtc->id, conn->id); + return -(errno = EINVAL); + } + } + } + } + + // if wanted, apply them + + if (!(flags & DRM_MODE_ATOMIC_TEST_ONLY)) { + for (auto &conn : std::as_const(gpu->connectors)) { + auto it = std::find_if(connCopies.constBegin(), connCopies.constEnd(), [conn](auto c){return c.id == conn->id;}); + if (it == connCopies.constEnd()) { + qCritical("implementation error: can't find connector %u", conn->id); + return -(errno = EINVAL); + } + *conn = *it; + } + for (auto &crtc : std::as_const(gpu->crtcs)) { + auto it = std::find_if(crtcCopies.constBegin(), crtcCopies.constEnd(), [crtc](auto c){return c.id == crtc->id;}); + if (it == crtcCopies.constEnd()) { + qCritical("implementation error: can't find crtc %u", crtc->id); + return -(errno = EINVAL); + } + *crtc = *it; + } + for (auto &plane : std::as_const(gpu->planes)) { + auto it = std::find_if(planeCopies.constBegin(), planeCopies.constEnd(), [plane](auto c){return c.id == plane->id;}); + if (it == planeCopies.constEnd()) { + qCritical("implementation error: can't find plane %u", plane->id); + return -(errno = EINVAL); + } + *plane = *it; + } + + if (flags & DRM_MODE_PAGE_FLIP_EVENT) { + // Unsupported + } + } + + return 0; +} + + +int drmModeCreatePropertyBlob(int fd, const void *data, size_t size, uint32_t *id) +{ + GPU(fd, -EINVAL); + if (!data || !size || !id) { + return -(errno = EINVAL); + } + auto blob = std::make_unique(gpu, data, size); + *id = blob->id; + gpu->propertyBlobs.push_back(std::move(blob)); + return 0; +} + +int drmModeDestroyPropertyBlob(int fd, uint32_t id) +{ + GPU(fd, -EINVAL); + auto it = std::remove_if(gpu->propertyBlobs.begin(), gpu->propertyBlobs.end(), [id](const auto &blob) { + return blob->id == id; + }); + if (it == gpu->propertyBlobs.end()) { + return -(errno = EINVAL); + } else { + gpu->propertyBlobs.erase(it, gpu->propertyBlobs.end()); + return 0; + } +} + +int drmModeCreateLease(int fd, const uint32_t *objects, int num_objects, int flags, uint32_t *lessee_id) +{ + return -(errno = ENOTSUP); +} + +drmModeLesseeListPtr drmModeListLessees(int fd) +{ + return nullptr; +} + +drmModeObjectListPtr drmModeGetLease(int fd) +{ + return nullptr; +} + +int drmModeRevokeLease(int fd, uint32_t lessee_id) +{ + return -(errno = ENOTSUP); +} + +void drmModeFreeResources(drmModeResPtr ptr) +{ + for (const auto &gpu : std::as_const(s_gpus)) { + if (gpu->resPtrs.removeOne(ptr)) { + delete[] ptr->connectors; + delete[] ptr->crtcs; + delete[] ptr->encoders; + delete[] ptr->fbs; + delete ptr; + } + } +} + +void drmModeFreePlaneResources(drmModePlaneResPtr ptr) +{ + for (const auto &gpu : std::as_const(s_gpus)) { + if (gpu->drmPlaneRes.removeOne(ptr)) { + delete[] ptr->planes; + delete ptr; + } + } +} + +void drmModeFreeCrtc(drmModeCrtcPtr ptr) +{ + for (const auto &gpu : std::as_const(s_gpus)) { + if (gpu->drmCrtcs.removeOne(ptr)) { + delete ptr; + return; + } + } + Q_UNREACHABLE(); +} + +void drmModeFreeConnector(drmModeConnectorPtr ptr) +{ + for (const auto &gpu : std::as_const(s_gpus)) { + if (gpu->drmConnectors.removeOne(ptr)) { + delete[] ptr->encoders; + delete[] ptr->props; + delete[] ptr->prop_values; + delete ptr; + return; + } + } + Q_UNREACHABLE(); +} + +void drmModeFreeEncoder(drmModeEncoderPtr ptr) +{ + for (const auto &gpu : std::as_const(s_gpus)) { + if (gpu->drmEncoders.removeOne(ptr)) { + delete ptr; + return; + } + } + Q_UNREACHABLE(); +} + +void drmModeFreePlane(drmModePlanePtr ptr) +{ + for (const auto &gpu : std::as_const(s_gpus)) { + if (gpu->drmPlanes.removeOne(ptr)) { + delete ptr; + return; + } + } + Q_UNREACHABLE(); +} diff --git a/local/recipes/kde/kwin/source/autotests/drm/mock_drm.h b/local/recipes/kde/kwin/source/autotests/drm/mock_drm.h new file mode 100644 index 0000000000..f795977fdc --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/drm/mock_drm.h @@ -0,0 +1,195 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +class MockGpu; +class MockFb; +class MockCrtc; +class MockEncoder; +class MockObject; +class MockPlane; + +class MockProperty { +public: + MockProperty(MockObject *obj, QString name, uint64_t initialValue, uint32_t flags, QList enums = {}); + ~MockProperty() = default; + + MockObject *obj; + uint32_t id; + uint32_t flags; + QString name; + uint64_t value; + QList enums; +}; + +class MockPropertyBlob { +public: + MockPropertyBlob(MockGpu *gpu, const void *data, size_t size); + ~MockPropertyBlob(); + + MockGpu *gpu; + uint32_t id; + void *data; + size_t size; +}; + +class MockObject { +public: + MockObject(MockGpu *gpu); + virtual ~MockObject(); + + uint64_t getProp(const QString &propName) const; + void setProp(const QString &propName, uint64_t value); + + uint32_t getPropId(const QString &propName) const; + + uint32_t id; + QList props; + MockGpu *gpu; +}; + +class MockConnector : public MockObject { +public: + MockConnector(MockGpu *gpu, bool nonDesktop = false); + MockConnector(const MockConnector &obj) = default; + ~MockConnector() = default; + + void addMode(uint32_t width, uint32_t height, float refreshRate, bool preferred = false); + void setVrrCapable(bool cap); + + drmModeConnection connection; + uint32_t type; + std::shared_ptr encoder; + QList modes; +}; + +class MockEncoder : public MockObject { +public: + MockEncoder(MockGpu *gpu, uint32_t possible_crtcs); + MockEncoder(const MockEncoder &obj) = default; + ~MockEncoder() = default; + + MockCrtc *crtc = nullptr; + uint32_t possible_crtcs; + uint32_t possible_clones = 0; +}; + +class MockCrtc : public MockObject { +public: + MockCrtc(MockGpu *gpu, const std::shared_ptr &legacyPlane, int pipeIndex, int gamma_size = 255); + MockCrtc(const MockCrtc &obj) = default; + ~MockCrtc() = default; + + int pipeIndex; + int gamma_size; + drmModeModeInfo mode; + bool modeValid = true; + MockFb *currentFb = nullptr; + MockFb *nextFb = nullptr; + QRect cursorRect; + std::shared_ptr legacyPlane; +}; + +enum class PlaneType { + Primary = 0, + Overlay, + Cursor +}; + +class MockPlane : public MockObject { +public: + MockPlane(MockGpu *gpu, PlaneType type, int crtcIndex); + MockPlane(const MockPlane &obj) = default; + ~MockPlane() = default; + + MockFb *currentFb = nullptr; + MockFb *nextFb = nullptr; + int possibleCrtcs; + PlaneType type; +}; + +class MockFb { +public: + MockFb(MockGpu *gpu, uint32_t width, uint32_t height); + ~MockFb(); + + uint32_t id; + uint32_t width, height; + MockGpu *gpu; +}; + +struct Prop { + uint32_t obj; + uint32_t prop; + uint64_t value; +}; + +struct _drmModeAtomicReq { + bool legacyEmulation = false; + QList props; +}; + +#define MOCKDRM_DEVICE_CAP_ATOMIC 0xFF + +class MockGpu { +public: + MockGpu(int fd, const QString &devNode, int numCrtcs, int gammaSize = 255); + ~MockGpu(); + + MockConnector *findConnector(uint32_t id) const; + MockCrtc *findCrtc(uint32_t id) const; + MockPlane *findPlane(uint32_t id) const; + MockPropertyBlob *getBlob(uint32_t id) const; + + void flipPage(uint32_t crtcId); + + int fd; + QString devNode; + QByteArray name = QByteArrayLiteral("mock"); + QMap clientCaps; + QMap deviceCaps; + + uint32_t idCounter = 1; + QList objects; + + QList> connectors; + QList drmConnectors; + + QList> encoders; + QList drmEncoders; + + QList> crtcs; + QList drmCrtcs; + + QList> planes; + QList drmPlanes; + + QList fbs; + std::vector> propertyBlobs; + + QList resPtrs; + QList drmProps; + QList drmPropertyBlobs; + QList drmObjectProperties; + QList drmPlaneRes; + std::mutex m_mutex; +}; + + + diff --git a/local/recipes/kde/kwin/source/autotests/effect/CMakeLists.txt b/local/recipes/kde/kwin/source/autotests/effect/CMakeLists.txt new file mode 100644 index 0000000000..6d5ccf01b6 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/CMakeLists.txt @@ -0,0 +1,20 @@ +include(ECMMarkAsTest) + +macro(KWINEFFECTS_UNIT_TESTS) + foreach(_testname ${ARGN}) + add_executable(${_testname} ${_testname}.cpp) + add_test(NAME kwineffects-${_testname} COMMAND ${_testname}) + target_link_libraries(${_testname} Qt::Test kwin) + ecm_mark_as_test(${_testname}) + endforeach() +endmacro() + +kwineffects_unit_tests( + windowquadlisttest + timelinetest +) + +add_executable(kwinglplatformtest kwinglplatformtest.cpp ../../src/opengl/glplatform.cpp ../../src/utils/version.cpp) +add_test(NAME kwineffects-kwinglplatformtest COMMAND kwinglplatformtest) +target_link_libraries(kwinglplatformtest Qt::Test Qt::Gui KF6::ConfigCore) +ecm_mark_as_test(kwinglplatformtest) diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/V3D-V3D_4_2-desktop-2.1 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/V3D-V3D_4_2-desktop-2.1 new file mode 100644 index 0000000000..3041e68b5f --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/V3D-V3D_4_2-desktop-2.1 @@ -0,0 +1,17 @@ +[Driver] +Vendor=Broadcom VideoCore 3D +Renderer=V3D 4.2 +Version=2.1 Mesa 19.1 + +[Settings] +LooseBinding=true +GLSL=false +TextureNPOT=false +Mesa=true +V3D=true +GLVersion=2,1 +MesaVersion=19,1 +DriverVersion=19,1 +Driver=21 +ChipClass=7000 +Compositor=4 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/VC4-V3D_2_1-desktop-2.1 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/VC4-V3D_2_1-desktop-2.1 new file mode 100644 index 0000000000..878204eb38 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/VC4-V3D_2_1-desktop-2.1 @@ -0,0 +1,17 @@ +[Driver] +Vendor=Broadcom VideoCore IV +Renderer=VC4 V3D 2.1 +Version=2.1 Mesa 19.1 + +[Settings] +LooseBinding=true +GLSL=false +TextureNPOT=false +Mesa=true +VC4=true +GLVersion=2,1 +MesaVersion=19,1 +DriverVersion=19,1 +Driver=20 +ChipClass=6000 +Compositor=4 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-catalyst-radeonhd-7700M-3.1.13399 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-catalyst-radeonhd-7700M-3.1.13399 new file mode 100644 index 0000000000..756766855c --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-catalyst-radeonhd-7700M-3.1.13399 @@ -0,0 +1,18 @@ +[Driver] +Vendor=ATI Technologies Inc. +Renderer=AMD Radeon HD 7700M Series +Version=3.1.13399 Compatibility Profile Context FireGL 15.201.1151 +ShadingLanguageVersion=4.40 + +[Settings] +LooseBinding=false +GLSL=true +TextureNPOT=true +Catalyst=true +Radeon=true +GLVersion=3,1,13399 +GLSLVersion=4,40 +DriverVersion=15,201,1151 +Driver=9 +ChipClass=999 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-bonaire-3.0 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-bonaire-3.0 new file mode 100644 index 0000000000..deb1c4d101 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-bonaire-3.0 @@ -0,0 +1,21 @@ +[Driver] +Vendor=X.Org +Renderer=Gallium 0.4 on AMD BONAIRE (DRM 2.43.0, LLVM 3.8.0) +Version=3.0 Mesa 11.2.2 +ShadingLanguageVersion=1.30 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Gallium=true +Radeon=true +GLVersion=3,0 +GLSLVersion=1,30 +MesaVersion=11,2,2 +GalliumVersion=0,4 +DriverVersion=11,2,2 +Driver=16 +ChipClass=10 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-cayman-gles-3.0 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-cayman-gles-3.0 new file mode 100644 index 0000000000..9d18d1a158 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-cayman-gles-3.0 @@ -0,0 +1,22 @@ +[Driver] +Vendor=X.Org +Renderer=Gallium 0.4 on AMD CAYMAN (DRM 2.43.0, LLVM 3.8.0) +Version=OpenGL ES 3.0 Mesa 11.2.2 +ShadingLanguageVersion=OpenGL ES GLSL ES 3.00 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Gallium=true +Radeon=true +GLVersion=3,0 +GLSLVersion=3,0 +GLES=true +MesaVersion=11,2,2 +GalliumVersion=0,4 +DriverVersion=11,2,2 +Driver=5 +ChipClass=8 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-hawaii-3.0 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-hawaii-3.0 new file mode 100644 index 0000000000..87e0812fcd --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-hawaii-3.0 @@ -0,0 +1,21 @@ +[Driver] +Vendor=X.Org +Renderer=Gallium 0.4 on AMD HAWAII (DRM 2.43.0, LLVM 3.7.1) +Version=3.0 Mesa 11.1.2 +ShadingLanguageVersion=1.30 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Gallium=true +Radeon=true +GLVersion=3,0 +GLSLVersion=1,30 +MesaVersion=11,1,2 +GalliumVersion=0,4 +DriverVersion=11,1,2 +Driver=16 +ChipClass=10 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-navi-4.5 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-navi-4.5 new file mode 100644 index 0000000000..69b289a4f5 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-navi-4.5 @@ -0,0 +1,21 @@ +[Driver] +Vendor=X.Org +Renderer=AMD NAVI10 (DRM 3.36.0, 5.5.1-arch1-1, LLVM 9.0.1) +Version=4.5 (Core Profile) Mesa 19.3.3 +ShadingLanguageVersion=4.50 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Gallium=true +Radeon=true +GLVersion=4,5 +GLSLVersion=4,50 +MesaVersion=19,3,3 +GalliumVersion=0,4 +DriverVersion=19,3,3 +Driver=16 +ChipClass=14 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-r9-290-4.5 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-r9-290-4.5 new file mode 100644 index 0000000000..2423dda389 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-r9-290-4.5 @@ -0,0 +1,21 @@ +[Driver] +Vendor=X.Org +Renderer=AMD Radeon R9 200 Series (HAWAII DRM 3.26.0 4.18.9-92.current LLVM 6.0.1) +Version=4.5 Mesa 18.1.6 +ShadingLanguageVersion=4.50 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Gallium=true +Radeon=true +GLVersion=4,5 +GLSLVersion=4,50 +MesaVersion=18,1,6 +GalliumVersion=0,4 +DriverVersion=18,1,6 +Driver=16 +ChipClass=10 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-480-series-4.5 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-480-series-4.5 new file mode 100644 index 0000000000..d8a2a3d340 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-480-series-4.5 @@ -0,0 +1,21 @@ +[Driver] +Vendor=X.Org +Renderer=AMD Radeon (TM) RX 480 Graphics (POLARIS10 / DRM 3.23.0 / 4.15.0-rc1-g516fb7f2e73d, LLVM 6.0.0) +Version=4.5 (Core Profile) Mesa 17.4.0-devel (git-b6b4b2c6d8) +ShadingLanguageVersion=4.50 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Gallium=true +Radeon=true +GLVersion=4,5 +GLSLVersion=4,50 +MesaVersion=17,4,0 +GalliumVersion=0,4 +DriverVersion=17,4,0 +Driver=16 +ChipClass=12 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-550-series-3.1 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-550-series-3.1 new file mode 100644 index 0000000000..a5bf537118 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-550-series-3.1 @@ -0,0 +1,21 @@ +[Driver] +Vendor=X.Org +Renderer=Radeon RX 550 Series (POLARIS12, DRM 3.25.0, 4.17.0-rc6-GTW1+, LLVM 6.0.0) +Version=3.1 Mesa 18.1.0 +ShadingLanguageVersion=1.40 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Gallium=true +Radeon=true +GLVersion=3,1 +GLSLVersion=1,40 +MesaVersion=18,1,0 +GalliumVersion=0,4 +DriverVersion=18,1,0 +Driver=16 +ChipClass=12 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-5700-xt-4.6 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-5700-xt-4.6 new file mode 100644 index 0000000000..4e360b2f9b --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-5700-xt-4.6 @@ -0,0 +1,21 @@ +[Driver] +Vendor=AMD +Renderer=AMD Radeon RX 5700 XT (NAVI10, DRM 3.40.0, 5.10.9-arch1-1, LLVM 11.0.1) +Version=4.6 (Compatibility Profile) Mesa 20.3.3 +ShadingLanguageVersion=4.60 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Gallium=true +Radeon=true +GLVersion=4,6 +GLSLVersion=4,60 +MesaVersion=20,3,3 +GalliumVersion=0,4 +DriverVersion=20,3,3 +Driver=16 +ChipClass=14 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-580-series-4.5 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-580-series-4.5 new file mode 100644 index 0000000000..3b4cd838fa --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-580-series-4.5 @@ -0,0 +1,21 @@ +[Driver] +Vendor=X.Org +Renderer=Radeon RX 580 Series (POLARIS10, DRM 3.27.0, 4.19.10-arch1-1-ARCH, LLVM 7.0.0) +Version=4.5 (Compatibility Profile) Mesa 18.3.1 +ShadingLanguageVersion=4.50 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Gallium=true +Radeon=true +GLVersion=4,5 +GLSLVersion=4,50 +MesaVersion=18,3,1 +GalliumVersion=0,4 +DriverVersion=18,3,1 +Driver=16 +ChipClass=12 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-vega-56-4.5 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-vega-56-4.5 new file mode 100644 index 0000000000..5606832043 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-vega-56-4.5 @@ -0,0 +1,21 @@ +[Driver] +Vendor=X.Org +Renderer=Radeon RX Vega (VEGA10, DRM 3.25.0, 4.17.0-trunk-amd64, LLVM 6.0.0) +Version=4.5 (Core Profile) Mesa 18.1.2 +ShadingLanguageVersion=4.50 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Gallium=true +Radeon=true +GLVersion=4,5 +GLSLVersion=4,50 +MesaVersion=18,1,2 +GalliumVersion=0,4 +DriverVersion=18,1,2 +Driver=16 +ChipClass=13 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-vega-64-4.5 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-vega-64-4.5 new file mode 100644 index 0000000000..0a9071af20 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-radeon-rx-vega-64-4.5 @@ -0,0 +1,21 @@ +[Driver] +Vendor=X.Org +Renderer=Radeon RX Vega (VEGA10 / DRM 3.23.0 / 4.16.16-300.fc28.x86_64, LLVM 6.0.0) +Version=4.5 (Core Profile) Mesa 18.0.5 +ShadingLanguageVersion=4.50 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Gallium=true +Radeon=true +GLVersion=4,5 +GLSLVersion=4,50 +MesaVersion=18,0,5 +GalliumVersion=0,4 +DriverVersion=18,0,5 +Driver=16 +ChipClass=13 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-redwood-3.0 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-redwood-3.0 new file mode 100644 index 0000000000..1ea71f5d85 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-redwood-3.0 @@ -0,0 +1,21 @@ +[Driver] +Vendor=X.Org +Renderer=Gallium 0.4 on AMD REDWOOD (DRM 2.43.0 / 4.6.4-1-ARCH, LLVM 3.8.0) +Version=3.0 Mesa 12.0.1 +ShadingLanguageVersion=1.30 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Gallium=true +Radeon=true +GLVersion=3,0 +GLSLVersion=1,30 +MesaVersion=12,0,1 +GalliumVersion=0,4 +DriverVersion=12,0,1 +Driver=5 +ChipClass=7 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-tonga-4.1 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-tonga-4.1 new file mode 100644 index 0000000000..d0e0a0c23f --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/amd-gallium-tonga-4.1 @@ -0,0 +1,21 @@ +[Driver] +Vendor=X.Org +Renderer=Gallium 0.4 on AMD TONGA (DRM 3.2.0 / 4.7.0-0-MANJARO, LLVM 3.8.0) +Version=4.1 (Core Profile) Mesa 12.0.1 +ShadingLanguageVersion=4.10 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Gallium=true +Radeon=true +GLVersion=4,1 +GLSLVersion=4,10 +MesaVersion=12,0,1 +GalliumVersion=0,4 +DriverVersion=12,0,1 +Driver=16 +ChipClass=11 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-broadwell-gt2-3.3 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-broadwell-gt2-3.3 new file mode 100644 index 0000000000..4488f4ab01 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-broadwell-gt2-3.3 @@ -0,0 +1,19 @@ +[Driver] +Vendor=Intel Open Source Technology Center +Renderer=Mesa DRI Intel(R) HD Graphics 5500 (Broadwell GT2) +Version=3.3 (Core Profile) Mesa 11.2.2 +ShadingLanguageVersion=3.30 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Intel=true +GLVersion=3,3 +GLSLVersion=3,30 +MesaVersion=11,2,2 +DriverVersion=11,2,2 +Driver=7 +ChipClass=2999 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-haswell-mobile-3.3 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-haswell-mobile-3.3 new file mode 100644 index 0000000000..3c214b78f0 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-haswell-mobile-3.3 @@ -0,0 +1,19 @@ +[Driver] +Vendor=Intel Open Source Technology Center +Renderer=Mesa DRI Intel(R) Haswell Mobile +Version=3.3 (Core Profile) Mesa 11.2.2 +ShadingLanguageVersion=3.30 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Intel=true +GLVersion=3,3 +GLSLVersion=3,30 +MesaVersion=11,2,2 +DriverVersion=11,2,2 +Driver=7 +ChipClass=2005 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-ivybridge-desktop-3.0 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-ivybridge-desktop-3.0 new file mode 100644 index 0000000000..6e859328c6 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-ivybridge-desktop-3.0 @@ -0,0 +1,20 @@ +[Driver] +Vendor=Intel Open Source Technology Center +Renderer=Mesa DRI Intel(R) Ivybridge Desktop +Version=3.0 Mesa 11.1.0 (git-525f3c2) +ShadingLanguageVersion=1.30 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Intel=true +GLVersion=3,0 +GLSLVersion=1,30 +MesaVersion=11,1,0 +DriverVersion=11,1,0 +Driver=7 +ChipClass=2004 +Compositor=1 + diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-ivybridge-desktop-3.3 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-ivybridge-desktop-3.3 new file mode 100644 index 0000000000..66fc3a75d2 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-ivybridge-desktop-3.3 @@ -0,0 +1,19 @@ +[Driver] +Vendor=Intel Open Source Technology Center +Renderer=Mesa DRI Intel(R) Ivybridge Desktop +Version=3.3 (Core Profile) Mesa 11.2.2 +ShadingLanguageVersion=3.30 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Intel=true +GLVersion=3,3 +GLSLVersion=3,30 +MesaVersion=11,2,2 +DriverVersion=11,2,2 +Driver=7 +ChipClass=2004 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-ivybridge-mobile-3.3 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-ivybridge-mobile-3.3 new file mode 100644 index 0000000000..8acf478dc8 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-ivybridge-mobile-3.3 @@ -0,0 +1,19 @@ +[Driver] +Vendor=Intel Open Source Technology Center +Renderer=Mesa DRI Intel(R) Ivybridge Mobile +Version=3.3 (Core Profile) Mesa 12.0.1 +ShadingLanguageVersion=3.30 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Intel=true +GLVersion=3,3 +GLSLVersion=3,30 +MesaVersion=12,0,1 +DriverVersion=12,0,1 +Driver=7 +ChipClass=2004 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-kabylake-gt2-4.6 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-kabylake-gt2-4.6 new file mode 100644 index 0000000000..2bdc4a4532 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-kabylake-gt2-4.6 @@ -0,0 +1,19 @@ +[Driver] +Vendor=Intel +Renderer=Mesa Intel(R) UHD Graphics 620 (KBL GT2) +Version=4.6 (Compatibility Profile) Mesa 20.3.2 +ShadingLanguageVersion=4.60 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Intel=true +GLVersion=4,6 +GLSLVersion=4,60 +MesaVersion=20,3,2 +DriverVersion=20,3,2 +Driver=7 +ChipClass=2012 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-sandybridge-mobile-3.3 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-sandybridge-mobile-3.3 new file mode 100644 index 0000000000..965d967286 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-sandybridge-mobile-3.3 @@ -0,0 +1,19 @@ +[Driver] +Vendor=Intel Open Source Technology Center +Renderer=Mesa DRI Intel(R) Sandybridge Mobile +Version=3.3 (Core Profile) Mesa 12.0.1 +ShadingLanguageVersion=3.30 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Intel=true +GLVersion=3,3 +GLSLVersion=3,30 +MesaVersion=12,0,1 +DriverVersion=12,0,1 +Driver=7 +ChipClass=2003 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-skylake-gt2-3.0 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-skylake-gt2-3.0 new file mode 100644 index 0000000000..83acc24a56 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/intel-skylake-gt2-3.0 @@ -0,0 +1,19 @@ +[Driver] +Vendor=Intel Open Source Technology Center +Renderer=Mesa DRI Intel(R) HD Graphics 520 (Skylake GT2) +Version=3.0 Mesa 11.2.0 +ShadingLanguageVersion=1.30 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Intel=true +GLVersion=3,0 +GLSLVersion=1,30 +MesaVersion=11,2,0 +DriverVersion=11,2,0 +Driver=7 +ChipClass=2999 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/lima-mali400-desktop-3.0 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/lima-mali400-desktop-3.0 new file mode 100644 index 0000000000..cf83810fd8 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/lima-mali400-desktop-3.0 @@ -0,0 +1,19 @@ +[Driver] +Vendor=Mali (Lima) +Renderer=Mali 400 (Lima) +Version=3.0 Mesa 19.1 +ShadingLanguageVersion=1.30 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Lima=true +GLVersion=3,0 +GLSLVersion=1,30 +MesaVersion=19,1 +DriverVersion=19,1 +Driver=19 +ChipClass=5000 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/llvmpipe-10.0 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/llvmpipe-10.0 new file mode 100644 index 0000000000..60047a8613 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/llvmpipe-10.0 @@ -0,0 +1,22 @@ +[Driver] +Vendor=Mesa/X.org +Renderer=llvmpipe (LLVM 10.0.1, 256 bits) +Version=3.1 Mesa 20.2.1 +ShadingLanguageVersion=1.40 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Gallium=true +SoftwareEmulation=true +GLVersion=3,1 +GLSLVersion=1,40 +MesaVersion=20,2,1 +GalliumVersion=0,4 +DriverVersion=20,2,1 +Driver=12 +ChipClass=99999 +Compositor=1 + diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/llvmpipe-3.0 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/llvmpipe-3.0 new file mode 100644 index 0000000000..aaa8d56b31 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/llvmpipe-3.0 @@ -0,0 +1,22 @@ +[Driver] +Vendor=VMware, Inc. +Renderer=Gallium 0.4 on llvmpipe (LLVM 3.8, 256 bits) +Version=3.0 Mesa 11.2.0 +ShadingLanguageVersion=1.30 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Gallium=true +SoftwareEmulation=true +GLVersion=3,0 +GLSLVersion=1,30 +MesaVersion=11,2,0 +GalliumVersion=0,4 +DriverVersion=11,2,0 +Driver=12 +ChipClass=99999 +Compositor=1 + diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/llvmpipe-5.0 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/llvmpipe-5.0 new file mode 100644 index 0000000000..fac5420a29 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/llvmpipe-5.0 @@ -0,0 +1,22 @@ +[Driver] +Vendor=VMware, Inc. +Renderer=llvmpipe (LLVM 5.0, 256 bits) +Version=3.0 Mesa 17.2.6 +ShadingLanguageVersion=1.30 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Gallium=true +SoftwareEmulation=true +GLVersion=3,0 +GLSLVersion=1,30 +MesaVersion=17,2,6 +GalliumVersion=0,4 +DriverVersion=17,2,6 +Driver=12 +ChipClass=99999 +Compositor=1 + diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-560-4.5 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-560-4.5 new file mode 100644 index 0000000000..6496d03088 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-560-4.5 @@ -0,0 +1,19 @@ +[Driver] +Vendor=NVIDIA Corporation +Renderer=GeForce GTX 560/PCIe/SSE2 +Version=4.5.0 NVIDIA 361.28 +ShadingLanguageVersion=4.50 NVIDIA + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Nvidia=true +PreferBufferSubData=true +GLVersion=4,5 +GLSLVersion=4,50 +DriverVersion=361,28 +Driver=8 +ChipClass=1005 +Compositor=1 + diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-660-3.1 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-660-3.1 new file mode 100644 index 0000000000..40f5f3bc14 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-660-3.1 @@ -0,0 +1,18 @@ +[Driver] +Vendor=NVIDIA Corporation +Renderer=GeForce GTX 660/PCIe/SSE2 +Version=3.1.0 NVIDIA 367.27 +ShadingLanguageVersion=1.40 NVIDIA via Cg compiler + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Nvidia=true +PreferBufferSubData=true +GLVersion=3,1 +GLSLVersion=1,40 +DriverVersion=367,27 +Driver=8 +ChipClass=1999 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-950-4.5 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-950-4.5 new file mode 100644 index 0000000000..6d5924f0a0 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-950-4.5 @@ -0,0 +1,18 @@ +[Driver] +Vendor=NVIDIA Corporation +Renderer=GeForce GTX 950/PCIe/SSE2 +Version=4.5.0 NVIDIA 364.19 +ShadingLanguageVersion=4.50 NVIDIA + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Nvidia=true +PreferBufferSubData=true +GLVersion=4,5 +GLSLVersion=4,50 +DriverVersion=364,19 +Driver=8 +ChipClass=1999 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-970-3.1 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-970-3.1 new file mode 100644 index 0000000000..833d07032a --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-970-3.1 @@ -0,0 +1,18 @@ +[Driver] +Vendor=NVIDIA Corporation +Renderer=GeForce GTX 970/PCIe/SSE2 +Version=3.1.0 NVIDIA 367.35 +ShadingLanguageVersion=1.40 NVIDIA via Cg compiler + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Nvidia=true +PreferBufferSubData=true +GLVersion=3,1 +GLSLVersion=1,40 +DriverVersion=367,35 +Driver=8 +ChipClass=1999 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-970M-3.1 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-970M-3.1 new file mode 100644 index 0000000000..def61db994 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-970M-3.1 @@ -0,0 +1,18 @@ +[Driver] +Vendor=NVIDIA Corporation +Renderer=GeForce GTX 970M/PCIe/SSE2 +Version=3.1.0 NVIDIA 364.12 +ShadingLanguageVersion=1.40 NVIDIA via Cg compiler + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Nvidia=true +PreferBufferSubData=true +GLVersion=3,1 +GLSLVersion=1,40 +DriverVersion=364,12 +Driver=8 +ChipClass=1999 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-980-3.1 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-980-3.1 new file mode 100644 index 0000000000..2b6ebc7b64 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/nvidia-geforce-gtx-980-3.1 @@ -0,0 +1,18 @@ +[Driver] +Vendor=NVIDIA Corporation +Renderer=GeForce GTX 980/PCIe/SSE2 +Version=3.1.0 NVIDIA 364.19 +ShadingLanguageVersion=1.40 NVIDIA via Cg compiler + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Nvidia=true +PreferBufferSubData=true +GLVersion=3,1 +GLSLVersion=1,40 +DriverVersion=364,19 +Driver=8 +ChipClass=1999 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/panfrost-malit860-desktop-3.0 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/panfrost-malit860-desktop-3.0 new file mode 100644 index 0000000000..69a120d275 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/panfrost-malit860-desktop-3.0 @@ -0,0 +1,19 @@ +[Driver] +Vendor=Panfrost +Renderer=Mali T860 (Panfrost) +Version=3.0 Mesa 19.1 +ShadingLanguageVersion=1.30 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Panfrost=true +GLVersion=3,0 +GLSLVersion=1,30 +MesaVersion=19,1 +DriverVersion=19,1 +Driver=18 +ChipClass=4001 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/qualcomm-adreno-330-libhybris-gles-3.0 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/qualcomm-adreno-330-libhybris-gles-3.0 new file mode 100644 index 0000000000..1394c00678 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/qualcomm-adreno-330-libhybris-gles-3.0 @@ -0,0 +1,16 @@ +[Driver] +Vendor=Qualcomm +Renderer=Adreno (TM) 330 +Version=OpenGL ES 2.0 (OpenGL ES 3.0 V@104.0 AU@ (GIT@Id3510ff6dc)) +ShadingLanguageVersion=OpenGL ES GLSL ES 3.00 + +[Settings] +GLSL=true +TextureNPOT=true +GLVersion=2,0 +GLSLVersion=3,0 +GLES=true +Adreno=true +Driver=15 +ChipClass=3002 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/virgl-3.1 b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/virgl-3.1 new file mode 100644 index 0000000000..3e665732b0 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/data/glplatform/virgl-3.1 @@ -0,0 +1,22 @@ +[Driver] +Vendor=Red Hat +Renderer=virgl +Version=3.1 Mesa 19.0.8 +ShadingLanguageVersion=1.40 + +[Settings] +LooseBinding=true +GLSL=true +TextureNPOT=true +Mesa=true +Gallium=true +Virgl=true +VirtualMachine=true +GLVersion=3,1 +GLSLVersion=1,40 +MesaVersion=19,0,8 +GalliumVersion=0,4 +DriverVersion=19,0,8 +Driver=17 +ChipClass=99999 +Compositor=1 diff --git a/local/recipes/kde/kwin/source/autotests/effect/kwinglplatformtest.cpp b/local/recipes/kde/kwin/source/autotests/effect/kwinglplatformtest.cpp new file mode 100644 index 0000000000..c2af05efdd --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/kwinglplatformtest.cpp @@ -0,0 +1,214 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "opengl/glplatform.h" +#include + +#include +#include + +Q_DECLARE_METATYPE(KWin::Driver) +Q_DECLARE_METATYPE(KWin::ChipClass) + +using namespace KWin; + +class GLPlatformTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testDriverToString_data(); + void testDriverToString(); + void testChipClassToString_data(); + void testChipClassToString(); + void testDetect_data(); + void testDetect(); +}; + +void GLPlatformTest::testDriverToString_data() +{ + QTest::addColumn("driver"); + QTest::addColumn("expected"); + + QTest::newRow("R100") << Driver_R100 << QStringLiteral("Radeon"); + QTest::newRow("R200") << Driver_R200 << QStringLiteral("R200"); + QTest::newRow("R300C") << Driver_R300C << QStringLiteral("R300C"); + QTest::newRow("R300G") << Driver_R300G << QStringLiteral("R300G"); + QTest::newRow("R600C") << Driver_R600C << QStringLiteral("R600C"); + QTest::newRow("R600G") << Driver_R600G << QStringLiteral("R600G"); + QTest::newRow("RadeonSI") << Driver_RadeonSI << QStringLiteral("RadeonSI"); + QTest::newRow("Nouveau") << Driver_Nouveau << QStringLiteral("Nouveau"); + QTest::newRow("Intel") << Driver_Intel << QStringLiteral("Intel"); + QTest::newRow("NVidia") << Driver_NVidia << QStringLiteral("NVIDIA"); + QTest::newRow("Catalyst") << Driver_Catalyst << QStringLiteral("Catalyst"); + QTest::newRow("Swrast") << Driver_Swrast << QStringLiteral("Software rasterizer"); + QTest::newRow("Softpipe") << Driver_Softpipe << QStringLiteral("softpipe"); + QTest::newRow("Llvmpipe") << Driver_Llvmpipe << QStringLiteral("LLVMpipe"); + QTest::newRow("VirtualBox") << Driver_VirtualBox << QStringLiteral("VirtualBox (Chromium)"); + QTest::newRow("VMware") << Driver_VMware << QStringLiteral("VMware (SVGA3D)"); + QTest::newRow("Qualcomm") << Driver_Qualcomm << QStringLiteral("Qualcomm"); + QTest::newRow("Virgl") << Driver_Virgl << QStringLiteral("Virgl (virtio-gpu, Qemu/KVM guest)"); + QTest::newRow("Panfrost") << Driver_Panfrost << QStringLiteral("Panfrost"); + QTest::newRow("Lima") << Driver_Lima << QStringLiteral("Mali (Lima)"); + QTest::newRow("VC4") << Driver_VC4 << QStringLiteral("VideoCore IV"); + QTest::newRow("V3D") << Driver_V3D << QStringLiteral("VideoCore 3D"); + QTest::newRow("Unknown") << Driver_Unknown << QStringLiteral("Unknown"); +} + +void GLPlatformTest::testDriverToString() +{ + QFETCH(Driver, driver); + QTEST(GLPlatform::driverToString(driver), "expected"); +} + +void GLPlatformTest::testChipClassToString_data() +{ + QTest::addColumn("chipClass"); + QTest::addColumn("expected"); + + QTest::newRow("R100") << R100 << QStringLiteral("R100"); + QTest::newRow("R200") << R200 << QStringLiteral("R200"); + QTest::newRow("R300") << R300 << QStringLiteral("R300"); + QTest::newRow("R400") << R400 << QStringLiteral("R400"); + QTest::newRow("R500") << R500 << QStringLiteral("R500"); + QTest::newRow("R600") << R600 << QStringLiteral("R600"); + QTest::newRow("R700") << R700 << QStringLiteral("R700"); + QTest::newRow("Evergreen") << Evergreen << QStringLiteral("EVERGREEN"); + QTest::newRow("NorthernIslands") << NorthernIslands << QStringLiteral("Northern Islands"); + QTest::newRow("SouthernIslands") << SouthernIslands << QStringLiteral("Southern Islands"); + QTest::newRow("SeaIslands") << SeaIslands << QStringLiteral("Sea Islands"); + QTest::newRow("VolcanicIslands") << VolcanicIslands << QStringLiteral("Volcanic Islands"); + QTest::newRow("Arctic Islands") << ArcticIslands << QStringLiteral("Arctic Islands"); + QTest::newRow("Vega") << Vega << QStringLiteral("Vega"); + QTest::newRow("UnknownRadeon") << UnknownRadeon << QStringLiteral("Unknown"); + QTest::newRow("NV10") << NV10 << QStringLiteral("NV10"); + QTest::newRow("NV20") << NV20 << QStringLiteral("NV20"); + QTest::newRow("NV30") << NV30 << QStringLiteral("NV30"); + QTest::newRow("NV40") << NV40 << QStringLiteral("NV40/G70"); + QTest::newRow("G80") << G80 << QStringLiteral("G80/G90"); + QTest::newRow("GF100") << GF100 << QStringLiteral("GF100"); + QTest::newRow("UnknownNVidia") << UnknownNVidia << QStringLiteral("Unknown"); + QTest::newRow("I8XX") << I8XX << QStringLiteral("i830/i835"); + QTest::newRow("I915") << I915 << QStringLiteral("i915/i945"); + QTest::newRow("I965") << I965 << QStringLiteral("i965"); + QTest::newRow("SandyBridge") << SandyBridge << QStringLiteral("SandyBridge"); + QTest::newRow("IvyBridge") << IvyBridge << QStringLiteral("IvyBridge"); + QTest::newRow("Haswell") << Haswell << QStringLiteral("Haswell"); + QTest::newRow("UnknownIntel") << UnknownIntel << QStringLiteral("Unknown"); + QTest::newRow("Adreno1XX") << Adreno1XX << QStringLiteral("Adreno 1xx series"); + QTest::newRow("Adreno2XX") << Adreno2XX << QStringLiteral("Adreno 2xx series"); + QTest::newRow("Adreno3XX") << Adreno3XX << QStringLiteral("Adreno 3xx series"); + QTest::newRow("Adreno4XX") << Adreno4XX << QStringLiteral("Adreno 4xx series"); + QTest::newRow("Adreno5XX") << Adreno5XX << QStringLiteral("Adreno 5xx series"); + QTest::newRow("UnknownAdreno") << UnknownAdreno << QStringLiteral("Unknown"); + QTest::newRow("MaliT7XX") << MaliT7XX << QStringLiteral("Mali T7xx series"); + QTest::newRow("MaliT8XX") << MaliT8XX << QStringLiteral("Mali T8xx series"); + QTest::newRow("MaliGXX") << MaliGXX << QStringLiteral("Mali Gxx series"); + QTest::newRow("UnknownPanfrost") << UnknownPanfrost << QStringLiteral("Unknown"); + QTest::newRow("Mali400") << Mali400 << QStringLiteral("Mali 400 series"); + QTest::newRow("Mali450") << Mali450 << QStringLiteral("Mali 450 series"); + QTest::newRow("Mali470") << Mali470 << QStringLiteral("Mali 470 series"); + QTest::newRow("UnknownLima") << UnknownLima << QStringLiteral("Unknown"); + QTest::newRow("VC4_2_1") << VC4_2_1 << QStringLiteral("VideoCore IV"); + QTest::newRow("UnknownVideoCore4") << UnknownVideoCore4 << QStringLiteral("Unknown"); + QTest::newRow("V3D_4_2") << V3D_4_2 << QStringLiteral("VideoCore 3D"); + QTest::newRow("UnknownVideoCore3D") << UnknownVideoCore3D << QStringLiteral("Unknown"); + QTest::newRow("UnknownChipClass") << UnknownChipClass << QStringLiteral("Unknown"); +} + +void GLPlatformTest::testChipClassToString() +{ + QFETCH(ChipClass, chipClass); + QTEST(GLPlatform::chipClassToString(chipClass), "expected"); +} + +void GLPlatformTest::testDetect_data() +{ + QTest::addColumn("configFile"); + + QDir dir(QFINDTESTDATA("data/glplatform")); + const QStringList entries = dir.entryList(QDir::NoDotAndDotDot | QDir::Files); + + for (const QString &file : entries) { + QTest::newRow(file.toUtf8().constData()) << dir.absoluteFilePath(file); + } +} + +static Version readVersion(const KConfigGroup &group, const char *entry) +{ + const QStringList parts = group.readEntry(entry, QString()).split(','); + if (parts.count() < 2) { + return Version(); + } + QList versionParts; + for (int i = 0; i < parts.count(); ++i) { + bool ok = false; + const auto value = parts.at(i).toLongLong(&ok); + if (ok) { + versionParts << value; + } else { + versionParts << 0; + } + } + while (versionParts.count() < 3) { + versionParts << 0; + } + return Version(versionParts.at(0), versionParts.at(1), versionParts.at(2)); +} + +void GLPlatformTest::testDetect() +{ + QFETCH(QString, configFile); + KConfig config(configFile); + const KConfigGroup driverGroup = config.group(QStringLiteral("Driver")); + + const auto version = driverGroup.readEntry("Version").toUtf8(); + const auto glslVersion = driverGroup.readEntry("ShadingLanguageVersion").toUtf8(); + const auto renderer = driverGroup.readEntry("Renderer").toUtf8(); + const auto vendor = driverGroup.readEntry("Vendor").toUtf8(); + GLPlatform gl(version, glslVersion, renderer, vendor); + + const KConfigGroup settingsGroup = config.group(QStringLiteral("Settings")); + + QCOMPARE(gl.isLooseBinding(), settingsGroup.readEntry("LooseBinding", false)); + + QCOMPARE(gl.glVersion(), readVersion(settingsGroup, "GLVersion")); + QCOMPARE(gl.glslVersion(), readVersion(settingsGroup, "GLSLVersion")); + QCOMPARE(gl.mesaVersion(), readVersion(settingsGroup, "MesaVersion")); + QEXPECT_FAIL("amd-catalyst-radeonhd-7700M-3.1.13399", "Detects GL version instead of driver version", Continue); + QCOMPARE(gl.driverVersion(), readVersion(settingsGroup, "DriverVersion")); + + QCOMPARE(gl.driver(), Driver(settingsGroup.readEntry("Driver", int(Driver_Unknown)))); + QCOMPARE(gl.chipClass(), ChipClass(settingsGroup.readEntry("ChipClass", int(UnknownChipClass)))); + + QCOMPARE(gl.isMesaDriver(), settingsGroup.readEntry("Mesa", false)); + QCOMPARE(gl.isRadeon(), settingsGroup.readEntry("Radeon", false)); + QCOMPARE(gl.isNvidia(), settingsGroup.readEntry("Nvidia", false)); + QCOMPARE(gl.isIntel(), settingsGroup.readEntry("Intel", false)); + QCOMPARE(gl.isVirtualBox(), settingsGroup.readEntry("VirtualBox", false)); + QCOMPARE(gl.isVMware(), settingsGroup.readEntry("VMware", false)); + QCOMPARE(gl.isAdreno(), settingsGroup.readEntry("Adreno", false)); + QCOMPARE(gl.isPanfrost(), settingsGroup.readEntry("Panfrost", false)); + QCOMPARE(gl.isLima(), settingsGroup.readEntry("Lima", false)); + QCOMPARE(gl.isVideoCore4(), settingsGroup.readEntry("VC4", false)); + QCOMPARE(gl.isVideoCore3D(), settingsGroup.readEntry("V3D", false)); + QCOMPARE(gl.isVirgl(), settingsGroup.readEntry("Virgl", false)); + + QCOMPARE(gl.isVirtualMachine(), settingsGroup.readEntry("VirtualMachine", false)); + + QCOMPARE(gl.glVersionString(), version); + QCOMPARE(gl.glRendererString(), renderer); + QCOMPARE(gl.glVendorString(), vendor); + QCOMPARE(gl.glShadingLanguageVersionString(), glslVersion); + + QCOMPARE(gl.isLooseBinding(), settingsGroup.readEntry("LooseBinding", false)); + QCOMPARE(gl.recommendedCompositor(), CompositingType(settingsGroup.readEntry("Compositor", int(NoCompositing)))); + QCOMPARE(gl.preferBufferSubData(), settingsGroup.readEntry("PreferBufferSubData", false)); +} + +QTEST_GUILESS_MAIN(GLPlatformTest) +#include "kwinglplatformtest.moc" diff --git a/local/recipes/kde/kwin/source/autotests/effect/timelinetest.cpp b/local/recipes/kde/kwin/source/autotests/effect/timelinetest.cpp new file mode 100644 index 0000000000..418ff19b89 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/timelinetest.cpp @@ -0,0 +1,415 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "effect/timeline.h" + +#include + +using namespace std::chrono_literals; + +// FIXME: Delete it in the future. +Q_DECLARE_METATYPE(std::chrono::milliseconds) + +class TimeLineTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void testUpdateForward(); + void testUpdateBackward(); + void testUpdateFinished(); + void testToggleDirection(); + void testReset(); + void testSetElapsed_data(); + void testSetElapsed(); + void testSetDuration(); + void testSetDurationRetargeting(); + void testSetDurationRetargetingSmallDuration(); + void testRunning(); + void testStrictRedirectSourceMode_data(); + void testStrictRedirectSourceMode(); + void testRelaxedRedirectSourceMode_data(); + void testRelaxedRedirectSourceMode(); + void testStrictRedirectTargetMode_data(); + void testStrictRedirectTargetMode(); + void testRelaxedRedirectTargetMode_data(); + void testRelaxedRedirectTargetMode(); +}; + +void TimeLineTest::testUpdateForward() +{ + KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Forward); + timeLine.setEasingCurve(QEasingCurve::Linear); + + // 0/1000 + timeLine.advance(0ms); + QCOMPARE(timeLine.value(), 0.0); + QVERIFY(!timeLine.done()); + + // 100/1000 + timeLine.advance(100ms); + QCOMPARE(timeLine.value(), 0.1); + QVERIFY(!timeLine.done()); + + // 400/1000 + timeLine.advance(400ms); + QCOMPARE(timeLine.value(), 0.4); + QVERIFY(!timeLine.done()); + + // 900/1000 + timeLine.advance(900ms); + QCOMPARE(timeLine.value(), 0.9); + QVERIFY(!timeLine.done()); + + // 1000/1000 + timeLine.advance(3000ms); + QCOMPARE(timeLine.value(), 1.0); + QVERIFY(timeLine.done()); +} + +void TimeLineTest::testUpdateBackward() +{ + KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Backward); + timeLine.setEasingCurve(QEasingCurve::Linear); + + // 0/1000 + timeLine.advance(0ms); + QCOMPARE(timeLine.value(), 1.0); + QVERIFY(!timeLine.done()); + + // 100/1000 + timeLine.advance(100ms); + QCOMPARE(timeLine.value(), 0.9); + QVERIFY(!timeLine.done()); + + // 400/1000 + timeLine.advance(400ms); + QCOMPARE(timeLine.value(), 0.6); + QVERIFY(!timeLine.done()); + + // 900/1000 + timeLine.advance(900ms); + QCOMPARE(timeLine.value(), 0.1); + QVERIFY(!timeLine.done()); + + // 1000/1000 + timeLine.advance(3000ms); + QCOMPARE(timeLine.value(), 0.0); + QVERIFY(timeLine.done()); +} + +void TimeLineTest::testUpdateFinished() +{ + KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Forward); + timeLine.advance(0ms); + timeLine.setEasingCurve(QEasingCurve::Linear); + + timeLine.advance(1000ms); + QCOMPARE(timeLine.value(), 1.0); + QVERIFY(timeLine.done()); + + timeLine.advance(1042ms); + QCOMPARE(timeLine.value(), 1.0); + QVERIFY(timeLine.done()); +} + +void TimeLineTest::testToggleDirection() +{ + KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Forward); + timeLine.setEasingCurve(QEasingCurve::Linear); + + timeLine.advance(0ms); + QCOMPARE(timeLine.value(), 0.0); + QVERIFY(!timeLine.done()); + + timeLine.advance(600ms); + QCOMPARE(timeLine.value(), 0.6); + QVERIFY(!timeLine.done()); + + timeLine.toggleDirection(); + QCOMPARE(timeLine.value(), 0.6); + QVERIFY(!timeLine.done()); + + timeLine.advance(800ms); + QCOMPARE(timeLine.value(), 0.4); + QVERIFY(!timeLine.done()); + + timeLine.advance(3000ms); + QCOMPARE(timeLine.value(), 0.0); + QVERIFY(timeLine.done()); +} + +void TimeLineTest::testReset() +{ + KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Forward); + timeLine.setEasingCurve(QEasingCurve::Linear); + timeLine.advance(0ms); + + timeLine.advance(1000ms); + QCOMPARE(timeLine.value(), 1.0); + QVERIFY(timeLine.done()); + + timeLine.reset(); + QCOMPARE(timeLine.value(), 0.0); + QVERIFY(!timeLine.done()); +} + +void TimeLineTest::testSetElapsed_data() +{ + QTest::addColumn("duration"); + QTest::addColumn("elapsed"); + QTest::addColumn("expectedElapsed"); + QTest::addColumn("expectedDone"); + QTest::addColumn("initiallyDone"); + + QTest::newRow("Less than duration, not finished") << 1000ms << 300ms << 300ms << false << false; + QTest::newRow("Less than duration, finished") << 1000ms << 300ms << 300ms << false << true; + QTest::newRow("Greater than duration, not finished") << 1000ms << 3000ms << 1000ms << true << false; + QTest::newRow("Greater than duration, finished") << 1000ms << 3000ms << 1000ms << true << true; + QTest::newRow("Equal to duration, not finished") << 1000ms << 1000ms << 1000ms << true << false; + QTest::newRow("Equal to duration, finished") << 1000ms << 1000ms << 1000ms << true << true; +} + +void TimeLineTest::testSetElapsed() +{ + QFETCH(std::chrono::milliseconds, duration); + QFETCH(std::chrono::milliseconds, elapsed); + QFETCH(std::chrono::milliseconds, expectedElapsed); + QFETCH(bool, expectedDone); + QFETCH(bool, initiallyDone); + + KWin::TimeLine timeLine(duration, KWin::TimeLine::Forward); + timeLine.setEasingCurve(QEasingCurve::Linear); + timeLine.advance(0ms); + + if (initiallyDone) { + timeLine.advance(duration); + QVERIFY(timeLine.done()); + } + + timeLine.setElapsed(elapsed); + QCOMPARE(timeLine.elapsed(), expectedElapsed); + QCOMPARE(timeLine.done(), expectedDone); +} + +void TimeLineTest::testSetDuration() +{ + KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Forward); + timeLine.setEasingCurve(QEasingCurve::Linear); + + QCOMPARE(timeLine.duration(), 1000ms); + + timeLine.setDuration(3000ms); + QCOMPARE(timeLine.duration(), 3000ms); +} + +void TimeLineTest::testSetDurationRetargeting() +{ + KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Forward); + timeLine.setEasingCurve(QEasingCurve::Linear); + timeLine.advance(0ms); + + timeLine.advance(500ms); + QCOMPARE(timeLine.value(), 0.5); + QVERIFY(!timeLine.done()); + + timeLine.setDuration(3000ms); + QCOMPARE(timeLine.value(), 0.5); + QVERIFY(!timeLine.done()); +} + +void TimeLineTest::testSetDurationRetargetingSmallDuration() +{ + KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Forward); + timeLine.setEasingCurve(QEasingCurve::Linear); + timeLine.advance(0ms); + + timeLine.advance(999ms); + QCOMPARE(timeLine.value(), 0.999); + QVERIFY(!timeLine.done()); + + timeLine.setDuration(3ms); + QCOMPARE(timeLine.value(), 1.0); + QVERIFY(timeLine.done()); +} + +void TimeLineTest::testRunning() +{ + KWin::TimeLine timeLine(1000ms, KWin::TimeLine::Forward); + timeLine.setEasingCurve(QEasingCurve::Linear); + timeLine.advance(0ms); + + QVERIFY(!timeLine.running()); + QVERIFY(!timeLine.done()); + + timeLine.advance(100ms); + QVERIFY(timeLine.running()); + QVERIFY(!timeLine.done()); + + timeLine.advance(1000ms); + QVERIFY(!timeLine.running()); + QVERIFY(timeLine.done()); +} + +void TimeLineTest::testStrictRedirectSourceMode_data() +{ + QTest::addColumn("initialDirection"); + QTest::addColumn("initialValue"); + QTest::addColumn("finalDirection"); + QTest::addColumn("finalValue"); + + QTest::newRow("forward -> backward") << KWin::TimeLine::Forward << 0.0 << KWin::TimeLine::Backward << 0.0; + QTest::newRow("backward -> forward") << KWin::TimeLine::Backward << 1.0 << KWin::TimeLine::Forward << 1.0; +} + +void TimeLineTest::testStrictRedirectSourceMode() +{ + QFETCH(KWin::TimeLine::Direction, initialDirection); + KWin::TimeLine timeLine(1000ms, initialDirection); + timeLine.setEasingCurve(QEasingCurve::Linear); + timeLine.setSourceRedirectMode(KWin::TimeLine::RedirectMode::Strict); + + QTEST(timeLine.direction(), "initialDirection"); + QTEST(timeLine.value(), "initialValue"); + QCOMPARE(timeLine.sourceRedirectMode(), KWin::TimeLine::RedirectMode::Strict); + QVERIFY(!timeLine.running()); + QVERIFY(!timeLine.done()); + + QFETCH(KWin::TimeLine::Direction, finalDirection); + timeLine.setDirection(finalDirection); + + QTEST(timeLine.direction(), "finalDirection"); + QTEST(timeLine.value(), "finalValue"); + QCOMPARE(timeLine.sourceRedirectMode(), KWin::TimeLine::RedirectMode::Strict); + QVERIFY(!timeLine.running()); + QVERIFY(timeLine.done()); +} + +void TimeLineTest::testRelaxedRedirectSourceMode_data() +{ + QTest::addColumn("initialDirection"); + QTest::addColumn("initialValue"); + QTest::addColumn("finalDirection"); + QTest::addColumn("finalValue"); + + QTest::newRow("forward -> backward") << KWin::TimeLine::Forward << 0.0 << KWin::TimeLine::Backward << 1.0; + QTest::newRow("backward -> forward") << KWin::TimeLine::Backward << 1.0 << KWin::TimeLine::Forward << 0.0; +} + +void TimeLineTest::testRelaxedRedirectSourceMode() +{ + QFETCH(KWin::TimeLine::Direction, initialDirection); + KWin::TimeLine timeLine(1000ms, initialDirection); + timeLine.setEasingCurve(QEasingCurve::Linear); + timeLine.setSourceRedirectMode(KWin::TimeLine::RedirectMode::Relaxed); + + QTEST(timeLine.direction(), "initialDirection"); + QTEST(timeLine.value(), "initialValue"); + QCOMPARE(timeLine.sourceRedirectMode(), KWin::TimeLine::RedirectMode::Relaxed); + QVERIFY(!timeLine.running()); + QVERIFY(!timeLine.done()); + + QFETCH(KWin::TimeLine::Direction, finalDirection); + timeLine.setDirection(finalDirection); + + QTEST(timeLine.direction(), "finalDirection"); + QTEST(timeLine.value(), "finalValue"); + QCOMPARE(timeLine.sourceRedirectMode(), KWin::TimeLine::RedirectMode::Relaxed); + QVERIFY(!timeLine.running()); + QVERIFY(!timeLine.done()); +} + +void TimeLineTest::testStrictRedirectTargetMode_data() +{ + QTest::addColumn("initialDirection"); + QTest::addColumn("initialValue"); + QTest::addColumn("finalDirection"); + QTest::addColumn("finalValue"); + + QTest::newRow("forward -> backward") << KWin::TimeLine::Forward << 0.0 << KWin::TimeLine::Backward << 1.0; + QTest::newRow("backward -> forward") << KWin::TimeLine::Backward << 1.0 << KWin::TimeLine::Forward << 0.0; +} + +void TimeLineTest::testStrictRedirectTargetMode() +{ + QFETCH(KWin::TimeLine::Direction, initialDirection); + KWin::TimeLine timeLine(1000ms, initialDirection); + timeLine.setEasingCurve(QEasingCurve::Linear); + timeLine.setTargetRedirectMode(KWin::TimeLine::RedirectMode::Strict); + timeLine.advance(0ms); + + QTEST(timeLine.direction(), "initialDirection"); + QTEST(timeLine.value(), "initialValue"); + QCOMPARE(timeLine.targetRedirectMode(), KWin::TimeLine::RedirectMode::Strict); + QVERIFY(!timeLine.running()); + QVERIFY(!timeLine.done()); + + timeLine.advance(1000ms); + QTEST(timeLine.value(), "finalValue"); + QVERIFY(!timeLine.running()); + QVERIFY(timeLine.done()); + + QFETCH(KWin::TimeLine::Direction, finalDirection); + timeLine.setDirection(finalDirection); + + QTEST(timeLine.direction(), "finalDirection"); + QTEST(timeLine.value(), "finalValue"); + QVERIFY(!timeLine.running()); + QVERIFY(timeLine.done()); +} + +void TimeLineTest::testRelaxedRedirectTargetMode_data() +{ + QTest::addColumn("initialDirection"); + QTest::addColumn("initialValue"); + QTest::addColumn("finalDirection"); + QTest::addColumn("finalValue"); + + QTest::newRow("forward -> backward") << KWin::TimeLine::Forward << 0.0 << KWin::TimeLine::Backward << 1.0; + QTest::newRow("backward -> forward") << KWin::TimeLine::Backward << 1.0 << KWin::TimeLine::Forward << 0.0; +} + +void TimeLineTest::testRelaxedRedirectTargetMode() +{ + QFETCH(KWin::TimeLine::Direction, initialDirection); + KWin::TimeLine timeLine(1000ms, initialDirection); + timeLine.setEasingCurve(QEasingCurve::Linear); + timeLine.setTargetRedirectMode(KWin::TimeLine::RedirectMode::Relaxed); + timeLine.advance(0ms); + + QTEST(timeLine.direction(), "initialDirection"); + QTEST(timeLine.value(), "initialValue"); + QCOMPARE(timeLine.targetRedirectMode(), KWin::TimeLine::RedirectMode::Relaxed); + QVERIFY(!timeLine.running()); + QVERIFY(!timeLine.done()); + + timeLine.advance(1000ms); + QTEST(timeLine.value(), "finalValue"); + QVERIFY(!timeLine.running()); + QVERIFY(timeLine.done()); + + QFETCH(KWin::TimeLine::Direction, finalDirection); + timeLine.setDirection(finalDirection); + timeLine.advance(1000ms); + + QTEST(timeLine.direction(), "finalDirection"); + QTEST(timeLine.value(), "finalValue"); + QVERIFY(!timeLine.running()); + QVERIFY(!timeLine.done()); + + timeLine.advance(2000ms); + QTEST(timeLine.direction(), "finalDirection"); + QTEST(timeLine.value(), "initialValue"); + QVERIFY(!timeLine.running()); + QVERIFY(timeLine.done()); +} + +QTEST_MAIN(TimeLineTest) + +#include "timelinetest.moc" diff --git a/local/recipes/kde/kwin/source/autotests/effect/windowquadlisttest.cpp b/local/recipes/kde/kwin/source/autotests/effect/windowquadlisttest.cpp new file mode 100644 index 0000000000..63d7bde4f4 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/effect/windowquadlisttest.cpp @@ -0,0 +1,215 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scene/itemgeometry.h" +#include + +Q_DECLARE_METATYPE(KWin::WindowQuadList) + +class WindowQuadListTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testMakeGrid_data(); + void testMakeGrid(); + void testMakeRegularGrid_data(); + void testMakeRegularGrid(); + +private: + KWin::WindowQuad makeQuad(const KWin::RectF &rect); +}; + +KWin::WindowQuad WindowQuadListTest::makeQuad(const KWin::RectF &r) +{ + KWin::WindowQuad quad; + quad[0] = KWin::WindowVertex(r.x(), r.y(), r.x(), r.y()); + quad[1] = KWin::WindowVertex(r.x() + r.width(), r.y(), r.x() + r.width(), r.y()); + quad[2] = KWin::WindowVertex(r.x() + r.width(), r.y() + r.height(), r.x() + r.width(), r.y() + r.height()); + quad[3] = KWin::WindowVertex(r.x(), r.y() + r.height(), r.x(), r.y() + r.height()); + return quad; +} + +void WindowQuadListTest::testMakeGrid_data() +{ + QTest::addColumn("orig"); + QTest::addColumn("quadSize"); + QTest::addColumn("expectedCount"); + QTest::addColumn("expected"); + + KWin::WindowQuadList orig; + KWin::WindowQuadList expected; + + QTest::newRow("empty") << orig << 10 << 0 << expected; + + orig.append(makeQuad(KWin::RectF(0, 0, 10, 10))); + expected.append(makeQuad(KWin::RectF(0, 0, 10, 10))); + QTest::newRow("quadSizeTooLarge") << orig << 10 << 1 << expected; + + expected.clear(); + expected.append(makeQuad(KWin::RectF(0, 0, 5, 5))); + expected.append(makeQuad(KWin::RectF(0, 5, 5, 5))); + expected.append(makeQuad(KWin::RectF(5, 0, 5, 5))); + expected.append(makeQuad(KWin::RectF(5, 5, 5, 5))); + QTest::newRow("regularGrid") << orig << 5 << 4 << expected; + + expected.clear(); + expected.append(makeQuad(KWin::RectF(0, 0, 9, 9))); + expected.append(makeQuad(KWin::RectF(0, 9, 9, 1))); + expected.append(makeQuad(KWin::RectF(9, 0, 1, 9))); + expected.append(makeQuad(KWin::RectF(9, 9, 1, 1))); + QTest::newRow("irregularGrid") << orig << 9 << 4 << expected; + + orig.append(makeQuad(KWin::RectF(0, 10, 4, 3))); + expected.clear(); + expected.append(makeQuad(KWin::RectF(0, 0, 4, 4))); + expected.append(makeQuad(KWin::RectF(0, 4, 4, 4))); + expected.append(makeQuad(KWin::RectF(0, 8, 4, 2))); + expected.append(makeQuad(KWin::RectF(0, 10, 4, 2))); + expected.append(makeQuad(KWin::RectF(0, 12, 4, 1))); + expected.append(makeQuad(KWin::RectF(4, 0, 4, 4))); + expected.append(makeQuad(KWin::RectF(4, 4, 4, 4))); + expected.append(makeQuad(KWin::RectF(4, 8, 4, 2))); + expected.append(makeQuad(KWin::RectF(8, 0, 2, 4))); + expected.append(makeQuad(KWin::RectF(8, 4, 2, 4))); + expected.append(makeQuad(KWin::RectF(8, 8, 2, 2))); + QTest::newRow("irregularGrid2") << orig << 4 << 11 << expected; +} + +void WindowQuadListTest::testMakeGrid() +{ + QFETCH(KWin::WindowQuadList, orig); + QFETCH(int, quadSize); + QFETCH(int, expectedCount); + KWin::WindowQuadList actual = orig.makeGrid(quadSize); + QCOMPARE(actual.count(), expectedCount); + + QFETCH(KWin::WindowQuadList, expected); + for (auto it = actual.constBegin(); it != actual.constEnd(); ++it) { + bool found = false; + const KWin::WindowQuad &actualQuad = (*it); + for (auto it2 = expected.constBegin(); it2 != expected.constEnd(); ++it2) { + const KWin::WindowQuad &expectedQuad = (*it2); + auto vertexTest = [actualQuad, expectedQuad](int index) { + const KWin::WindowVertex &actualVertex = actualQuad[index]; + const KWin::WindowVertex &expectedVertex = expectedQuad[index]; + if (actualVertex.x() != expectedVertex.x()) { + return false; + } + if (actualVertex.y() != expectedVertex.y()) { + return false; + } + if (!qFuzzyIsNull(actualVertex.u() - expectedVertex.u())) { + return false; + } + if (!qFuzzyIsNull(actualVertex.v() - expectedVertex.v())) { + return false; + } + return true; + }; + found = vertexTest(0) && vertexTest(1) && vertexTest(2) && vertexTest(3); + if (found) { + break; + } + } + QVERIFY2(found, qPrintable(QStringLiteral("%0, %1 / %2, %3").arg(QString::number(actualQuad.left()), QString::number(actualQuad.top()), QString::number(actualQuad.right()), QString::number(actualQuad.bottom())))); + } +} + +void WindowQuadListTest::testMakeRegularGrid_data() +{ + QTest::addColumn("orig"); + QTest::addColumn("xSubdivisions"); + QTest::addColumn("ySubdivisions"); + QTest::addColumn("expectedCount"); + QTest::addColumn("expected"); + + KWin::WindowQuadList orig; + KWin::WindowQuadList expected; + + QTest::newRow("empty") << orig << 1 << 1 << 0 << expected; + + orig.append(makeQuad(KWin::RectF(0, 0, 10, 10))); + expected.append(makeQuad(KWin::RectF(0, 0, 10, 10))); + QTest::newRow("noSplit") << orig << 1 << 1 << 1 << expected; + + expected.clear(); + expected.append(makeQuad(KWin::RectF(0, 0, 5, 10))); + expected.append(makeQuad(KWin::RectF(5, 0, 5, 10))); + QTest::newRow("xSplit") << orig << 2 << 1 << 2 << expected; + + expected.clear(); + expected.append(makeQuad(KWin::RectF(0, 0, 10, 5))); + expected.append(makeQuad(KWin::RectF(0, 5, 10, 5))); + QTest::newRow("ySplit") << orig << 1 << 2 << 2 << expected; + + expected.clear(); + expected.append(makeQuad(KWin::RectF(0, 0, 5, 5))); + expected.append(makeQuad(KWin::RectF(5, 0, 5, 5))); + expected.append(makeQuad(KWin::RectF(0, 5, 5, 5))); + expected.append(makeQuad(KWin::RectF(5, 5, 5, 5))); + QTest::newRow("xySplit") << orig << 2 << 2 << 4 << expected; + + orig.append(makeQuad(KWin::RectF(0, 10, 4, 2))); + expected.clear(); + expected.append(makeQuad(KWin::RectF(0, 0, 5, 3))); + expected.append(makeQuad(KWin::RectF(5, 0, 5, 3))); + expected.append(makeQuad(KWin::RectF(0, 3, 5, 3))); + expected.append(makeQuad(KWin::RectF(5, 3, 5, 3))); + expected.append(makeQuad(KWin::RectF(0, 6, 5, 3))); + expected.append(makeQuad(KWin::RectF(5, 6, 5, 3))); + expected.append(makeQuad(KWin::RectF(0, 9, 5, 1))); + expected.append(makeQuad(KWin::RectF(0, 10, 4, 2))); + expected.append(makeQuad(KWin::RectF(5, 9, 5, 1))); + QTest::newRow("multipleQuads") << orig << 2 << 4 << 9 << expected; +} + +void WindowQuadListTest::testMakeRegularGrid() +{ + QFETCH(KWin::WindowQuadList, orig); + QFETCH(int, xSubdivisions); + QFETCH(int, ySubdivisions); + QFETCH(int, expectedCount); + KWin::WindowQuadList actual = orig.makeRegularGrid(xSubdivisions, ySubdivisions); + QCOMPARE(actual.count(), expectedCount); + + QFETCH(KWin::WindowQuadList, expected); + for (auto it = actual.constBegin(); it != actual.constEnd(); ++it) { + bool found = false; + const KWin::WindowQuad &actualQuad = (*it); + for (auto it2 = expected.constBegin(); it2 != expected.constEnd(); ++it2) { + const KWin::WindowQuad &expectedQuad = (*it2); + auto vertexTest = [actualQuad, expectedQuad](int index) { + const KWin::WindowVertex &actualVertex = actualQuad[index]; + const KWin::WindowVertex &expectedVertex = expectedQuad[index]; + if (actualVertex.x() != expectedVertex.x()) { + return false; + } + if (actualVertex.y() != expectedVertex.y()) { + return false; + } + if (!qFuzzyIsNull(actualVertex.u() - expectedVertex.u())) { + return false; + } + if (!qFuzzyIsNull(actualVertex.v() - expectedVertex.v())) { + return false; + } + return true; + }; + found = vertexTest(0) && vertexTest(1) && vertexTest(2) && vertexTest(3); + if (found) { + break; + } + } + QVERIFY2(found, qPrintable(QStringLiteral("%0, %1 / %2, %3").arg(QString::number(actualQuad.left()), QString::number(actualQuad.top()), QString::number(actualQuad.right()), QString::number(actualQuad.bottom())))); + } +} + +QTEST_MAIN(WindowQuadListTest) + +#include "windowquadlisttest.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/CMakeLists.txt b/local/recipes/kde/kwin/source/autotests/integration/CMakeLists.txt new file mode 100644 index 0000000000..7f3e594d7b --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/CMakeLists.txt @@ -0,0 +1,216 @@ +add_subdirectory(helper) + +add_library(KWinIntegrationTestFramework STATIC) + +qt6_generate_wayland_protocol_client_sources(KWinIntegrationTestFramework + PRIVATE_CODE + NO_INCLUDE_CORE_ONLY + FILES + ${WaylandProtocols_DATADIR}/unstable/input-method/input-method-unstable-v1.xml + ${WaylandProtocols_DATADIR}/unstable/linux-dmabuf/linux-dmabuf-unstable-v1.xml +) +qt6_generate_wayland_protocol_client_sources(KWinIntegrationTestFramework + PRIVATE_CODE + FILES + ${Wayland_DATADIR}/wayland.xml + + ${CMAKE_SOURCE_DIR}/src/wayland/protocols/wlr-layer-shell-unstable-v1.xml + ${CMAKE_SOURCE_DIR}/src/wayland/protocols/xx-session-management-v1.xml + ${WaylandProtocols_DATADIR}/stable/presentation-time/presentation-time.xml + ${WaylandProtocols_DATADIR}/stable/tablet/tablet-v2.xml + ${WaylandProtocols_DATADIR}/stable/viewporter/viewporter.xml + ${WaylandProtocols_DATADIR}/stable/xdg-shell/xdg-shell.xml + ${WaylandProtocols_DATADIR}/staging/color-management/color-management-v1.xml + ${WaylandProtocols_DATADIR}/staging/color-representation/color-representation-v1.xml + ${WaylandProtocols_DATADIR}/staging/cursor-shape/cursor-shape-v1.xml + ${WaylandProtocols_DATADIR}/staging/fifo/fifo-v1.xml + ${WaylandProtocols_DATADIR}/staging/fractional-scale/fractional-scale-v1.xml + ${WaylandProtocols_DATADIR}/staging/security-context/security-context-v1.xml + ${WaylandProtocols_DATADIR}/staging/xdg-activation/xdg-activation-v1.xml + ${WaylandProtocols_DATADIR}/staging/xdg-dialog/xdg-dialog-v1.xml + ${WaylandProtocols_DATADIR}/staging/xdg-toplevel-drag/xdg-toplevel-drag-v1.xml + ${WaylandProtocols_DATADIR}/unstable/idle-inhibit/idle-inhibit-unstable-v1.xml + ${WaylandProtocols_DATADIR}/unstable/primary-selection/primary-selection-unstable-v1.xml + ${WaylandProtocols_DATADIR}/unstable/text-input/text-input-unstable-v3.xml + ${WaylandProtocols_DATADIR}/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml + + ${PLASMA_WAYLAND_PROTOCOLS_DIR}/fake-input.xml + ${PLASMA_WAYLAND_PROTOCOLS_DIR}/kde-output-device-v2.xml + ${PLASMA_WAYLAND_PROTOCOLS_DIR}/kde-output-management-v2.xml + ${PLASMA_WAYLAND_PROTOCOLS_DIR}/kde-screen-edge-v1.xml + ${PLASMA_WAYLAND_PROTOCOLS_DIR}/keystate.xml + ${PLASMA_WAYLAND_PROTOCOLS_DIR}/zkde-screencast-unstable-v1.xml +) +target_sources(KWinIntegrationTestFramework PRIVATE + generic_scene_opengl_test.cpp + kwin_wayland_test.cpp + test_helpers.cpp +) +target_link_libraries(KWinIntegrationTestFramework + PUBLIC + Qt::Test + Qt::Concurrent + Plasma::KWaylandClient + Wayland::Client + Libdrm::Libdrm + kwin + XKB::XKB + + PRIVATE + # Static plugins + KWinQpaPlugin + KF6WindowSystemKWinPlugin + KF6IdleTimeKWinPlugin +) +if(KWIN_BUILD_X11) + target_link_libraries(KWinIntegrationTestFramework PRIVATE KWinXwaylandServerModule) +endif() +if(TARGET KF6GlobalAccelKWinPlugin) + target_link_libraries(KWinIntegrationTestFramework PUBLIC KF6GlobalAccelKWinPlugin) +endif() + +if(TARGET PW::KScreenLocker) + target_link_libraries(KWinIntegrationTestFramework PUBLIC PW::KScreenLocker) +endif() + +function(integrationTest) + set(optionArgs BUILTIN_EFFECTS) + set(oneValueArgs NAME) + set(multiValueArgs SRCS LIBS OPTIONAL_LIBS) + cmake_parse_arguments(ARGS "${optionArgs}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + add_executable(${ARGS_NAME} ${ARGS_SRCS}) + target_link_libraries(${ARGS_NAME} KWinIntegrationTestFramework Qt::Test ${ARGS_LIBS}) + foreach(target ${ARGS_OPTIONAL_LIBS}) + if(TARGET ${target}) + target_link_libraries(${ARGS_NAME} ${target}) + endif() + endforeach() + if(${ARGS_BUILTIN_EFFECTS}) + kcoreaddons_target_static_plugins(${ARGS_NAME} NAMESPACE "kwin/effects/plugins") + endif() + add_test(NAME kwin-${ARGS_NAME} COMMAND dbus-run-session ${CMAKE_BINARY_DIR}/bin/${ARGS_NAME}) +endfunction() + +if(KWIN_BUILD_X11) + integrationTest(NAME testDontCrashGlxgears SRCS dont_crash_glxgears.cpp LIBS KF6::I18n KDecoration3::KDecoration) +endif() +if (KWIN_BUILD_SCREENLOCKER) + integrationTest(NAME testLockScreen SRCS lockscreen.cpp LIBS KF6::GlobalAccel) +endif() +integrationTest(NAME testBounceKeys SRCS bounce_keys_test.cpp) +integrationTest(NAME testButtonRebind SRCS buttonrebind_test.cpp) +if (KWIN_BUILD_GAMECONTROLLER) + integrationTest(NAME testGameController SRCS gamecontroller_test.cpp LIBS KF6::IdleTime PkgConfig::libevdev) +endif() +integrationTest(NAME testDecorationInput SRCS decoration_input_test.cpp LIBS KDecoration3::KDecoration KDecoration3::KDecoration3Private) +integrationTest(NAME testInternalWindow SRCS internal_window.cpp) +integrationTest(NAME testTouchInput SRCS touch_input_test.cpp) +integrationTest(NAME testInputStackingOrder SRCS input_stacking_order.cpp) +integrationTest(NAME testPointerInput SRCS pointer_input.cpp LIBS Libdrm::Libdrm OPTIONAL_LIBS XCB::ICCCM) +integrationTest(NAME testPlatformCursor SRCS platformcursor.cpp) +integrationTest(NAME testDontCrashCancelAnimation SRCS dont_crash_cancel_animation.cpp LIBS KDecoration3::KDecoration) +integrationTest(NAME testTransientPlacement SRCS transient_placement.cpp) +integrationTest(NAME testDebugConsole SRCS debug_console_test.cpp) +integrationTest(NAME testPlasmaSurface SRCS plasma_surface_test.cpp) +integrationTest(NAME testMaximized SRCS maximize_test.cpp LIBS KDecoration3::KDecoration KF6::Package) +integrationTest(NAME testXdgShellWindow SRCS xdgshellwindow_test.cpp LIBS KDecoration3::KDecoration) +integrationTest(NAME testSceneOpenGL SRCS scene_opengl_test.cpp ) +integrationTest(NAME testSceneOpenGLES SRCS scene_opengl_es_test.cpp ) +integrationTest(NAME testScreenChanges SRCS screen_changes_test.cpp) +if (KWIN_BUILD_TABBOX) + integrationTest(NAME testTabBox SRCS tabbox_test.cpp) +endif() +integrationTest(NAME testWindowSelection SRCS window_selection_test.cpp) +integrationTest(NAME testPointerConstraints SRCS pointer_constraints_test.cpp) +integrationTest(NAME testKeyboardLayout SRCS keyboard_layout_test.cpp LIBS KF6::GlobalAccel XKB::XKB) +integrationTest(NAME testKeymapCreationFailure SRCS keymap_creation_failure_test.cpp LIBS KF6::GlobalAccel) +integrationTest(NAME testShowingDesktop SRCS showing_desktop_test.cpp) +integrationTest(NAME testDontCrashUseractionsMenu SRCS dont_crash_useractions_menu.cpp LIBS KF6::I18n) +integrationTest(NAME testLayerShellV1Window SRCS layershellv1window_test.cpp) +integrationTest(NAME testVirtualDesktop SRCS virtual_desktop_test.cpp) +integrationTest(NAME testXdgShellWindowRules SRCS xdgshellwindow_rules_test.cpp) +integrationTest(NAME testIdleInhibition SRCS idle_inhibition_test.cpp) +integrationTest(NAME testDontCrashReinitializeCompositor SRCS dont_crash_reinitialize_compositor.cpp BUILTIN_EFFECTS) +integrationTest(NAME testNoGlobalShortcuts SRCS no_global_shortcuts_test.cpp LIBS KF6::GlobalAccel) +integrationTest(NAME testPlacement SRCS placement_test.cpp) +integrationTest(NAME testActivation SRCS activation_test.cpp OPTIONAL_LIBS XCB::ICCCM) +integrationTest(NAME testInputMethod SRCS inputmethod_test.cpp LIBS XKB::XKB) +integrationTest(NAME testScreens SRCS screens_test.cpp) +integrationTest(NAME testScreenEdges SRCS screenedges_test.cpp) +integrationTest(NAME testOutputChanges SRCS outputchanges_test.cpp OPTIONAL_LIBS XCB::ICCCM) +integrationTest(NAME testTiles SRCS tiles_test.cpp OPTIONAL_LIBS XCB::ICCCM) +integrationTest(NAME testFractionalScaling SRCS fractional_scaling_test.cpp) +integrationTest(NAME testMoveResize SRCS move_resize_window_test.cpp) +integrationTest(NAME testPlasmaWindow SRCS plasmawindow_test.cpp OPTIONAL_LIBS XCB::ICCCM) +integrationTest(NAME testQuickTiling SRCS quick_tiling_test.cpp LIBS KDecoration3::KDecoration OPTIONAL_LIBS XCB::ICCCM) +integrationTest(NAME testStackingOrder SRCS stacking_order_test.cpp OPTIONAL_LIBS XCB::ICCCM) +integrationTest(NAME testDbusInterface SRCS dbus_interface_test.cpp OPTIONAL_LIBS XCB::ICCCM) +integrationTest(NAME testFakeInput SRCS fakeinput_test.cpp) +integrationTest(NAME testSecurityContext SRCS security_context_test.cpp) +integrationTest(NAME testSlowKeys SRCS slow_keys_test.cpp) +integrationTest(NAME testStickyKeys SRCS sticky_keys_test.cpp) +integrationTest(NAME testWorkspace SRCS workspace_test.cpp) +integrationTest(NAME testMouseActions SRCS mouseactions_test.cpp LIBS) +if (HAVE_WAYLAND_PROTOCOLS_147) + integrationTest(NAME testColorManagement SRCS test_colormanagement.cpp) +endif() +integrationTest(NAME testKeyboardInput SRCS keyboard_input_test.cpp) +integrationTest(NAME testFifo SRCS test_fifo.cpp) +integrationTest(NAME testMouseKeys SRCS mouse_keys_test.cpp) +integrationTest(NAME testXdgSession SRCS xdgsession_test.cpp) +integrationTest(NAME testDnd SRCS dnd_test.cpp) +integrationTest(NAME testFractionalRepaint SRCS fractional_repaint_test.cpp) +integrationTest(NAME testDrm SRCS drm_test.cpp) +integrationTest(NAME testDrmLegacy SRCS drm_test.cpp) +target_compile_definitions(testDrmLegacy PRIVATE FORCE_DRM_LEGACY=1) +integrationTest(NAME testDrmNoModifiers SRCS drm_test.cpp) +target_compile_definitions(testDrmNoModifiers PRIVATE FORCE_NO_DRM_MODIFIERS=1) +integrationTest(NAME testSelection SRCS selection_test.cpp) +integrationTest(NAME testToplevelDrag SRCS topleveldrag_test.cpp) +integrationTest(NAME testA11yKeyboardMonitor SRCS a11ykeyboardmonitor_test.cpp) +integrationTest(NAME testSubsurface SRCS test_subsurface.cpp) + +if(KWIN_BUILD_X11) + integrationTest(NAME testDontCrashEmptyDeco SRCS dont_crash_empty_deco.cpp LIBS KDecoration3::KDecoration) + integrationTest(NAME testXwaylandDnd SRCS xwayland_dnd_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testXwaylandSelection SRCS xwayland_selection_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testXinerama SRCS xinerama_test.cpp) + integrationTest(NAME testX11KeyRead SRCS x11keyread.cpp LIBS XCB::XINPUT) + integrationTest(NAME testXwaylandInput SRCS xwayland_input_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testXwaylandServerCrash SRCS xwaylandserver_crash_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testXwaylandServerRestart SRCS xwaylandserver_restart_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testWindowRules SRCS window_rules_test.cpp LIBS XCB::ICCCM) + integrationTest(NAME testX11Window SRCS x11_window_test.cpp LIBS XCB::ICCCM XCB::PRESENT XCB::SHM) + integrationTest(NAME testDontCrashAuroraeDestroyDeco SRCS dont_crash_aurorae_destroy_deco.cpp LIBS KDecoration3::KDecoration LIBS XCB::ICCCM) +endif() + +qt_add_dbus_interfaces(DBUS_SRCS ${CMAKE_BINARY_DIR}/src/org.kde.kwin.VirtualKeyboard.xml) +integrationTest(NAME testVirtualKeyboardDBus SRCS test_virtualkeyboard_dbus.cpp ${DBUS_SRCS}) + +if (KWIN_BUILD_GLOBALSHORTCUTS) + integrationTest(NAME testGlobalShortcuts SRCS globalshortcuts_test.cpp LIBS XCB::ICCCM KF6::GlobalAccel KF6::I18n XKB::XKB) + integrationTest(NAME testKWinBindings SRCS kwinbindings_test.cpp LIBS KF6::I18n) +endif() +if (TARGET K::KPipeWire) + integrationTest(NAME testScreencasting SRCS screencasting_test.cpp LIBS K::KPipeWire) +endif() + +if (KWIN_BUILD_ACTIVITIES) + integrationTest(NAME testActivities SRCS activities_test.cpp LIBS XCB::ICCCM Plasma::Activities) +endif() + +if (KWIN_BUILD_EIS) + pkg_check_modules(PKG_libei REQUIRED IMPORTED_TARGET libei-1.0) + integrationTest(NAME testInputCapture SRCS input_capture_test.cpp LIBS PkgConfig::PKG_libei) + if (PKG_libei_VERSION VERSION_GREATER_EQUAL "1.4.0") + target_compile_definitions(testInputCapture PRIVATE -DHAVE_EI_EVENT_SYNC=1) + else() + target_compile_definitions(testInputCapture PRIVATE -DHAVE_EI_EVENT_SYNC=0) + endif() +endif() + + +add_subdirectory(scripting) +add_subdirectory(effects) +add_subdirectory(fakes) diff --git a/local/recipes/kde/kwin/source/autotests/integration/activation_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/activation_test.cpp new file mode 100644 index 0000000000..de138a69a3 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/activation_test.cpp @@ -0,0 +1,1186 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "input_event.h" +#include "input_event_spy.h" +#include "pointer_input.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" +#include "xdgactivationv1.h" + +#include +#include +#include + +#if KWIN_BUILD_X11 +#include "x11window.h" + +#include +#include +#endif + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_activation-0"); + +class ActivationTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testSwitchToWindowToLeft(); + void testSwitchToWindowToRight(); + void testSwitchToWindowAbove(); + void testSwitchToWindowBelow(); + void testSwitchToWindowMaximized(); + void testSwitchToWindowFullScreen(); + void testActiveFullscreen(); + void testXdgActivation(); + void testNoDemandAttentionWithoutActivationRequest(); + void testXdgActivationBeforeInitialCommit(); + void testXdgActivationBeforeMap(); + void testGlobalShortcutActivation(); + void testFocusMovesFromClosedDialogToParentWindow(); + void testFullAreaLayerSurfaceUnderlay_data(); + void testFullAreaLayerSurfaceUnderlay(); + void testFullAreaLayerSurfaceOverlay_data(); + void testFullAreaLayerSurfaceOverlay(); + void testPartialAreaLayerSurfaceOverlay_data(); + void testPartialAreaLayerSurfaceOverlay(); + +private: + void stackScreensHorizontally(); + void stackScreensVertically(); +}; + +void ActivationTest::initTestCase() +{ + qRegisterMetaType(); + + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); +} + +void ActivationTest::init() +{ + options->setFocusStealingPreventionLevel(FocusStealingPreventionLevel::Low); + + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::XdgActivation | Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::LayerShellV1)); + + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); +} + +void ActivationTest::cleanup() +{ + Test::destroyWaylandConnection(); + + stackScreensHorizontally(); +} + +void ActivationTest::testSwitchToWindowToLeft() +{ + // This test verifies that "Switch to Window to the Left" shortcut works. + + // Prepare the test environment. + stackScreensHorizontally(); + + // Create several windows on the left screen. + Test::XdgToplevelWindow window1; + QVERIFY(window1.show()); + QVERIFY(window1.m_window->isActive()); + + Test::XdgToplevelWindow window2; + QVERIFY(window2.show()); + QVERIFY(window2.m_window->isActive()); + + window1.m_window->move(QPoint(300, 200)); + window2.m_window->move(QPoint(500, 200)); + + // Create several windows on the right screen. + Test::XdgToplevelWindow window3; + QVERIFY(window3.show()); + QVERIFY(window3.m_window->isActive()); + + Test::XdgToplevelWindow window4; + QVERIFY(window4.show()); + QVERIFY(window4.m_window->isActive()); + + window3.m_window->move(QPoint(1380, 200)); + window4.m_window->move(QPoint(1580, 200)); + + // Switch to window to the left. + workspace()->switchWindow(Workspace::DirectionWest); + QVERIFY(window3.m_window->isActive()); + + // Switch to window to the left. + workspace()->switchWindow(Workspace::DirectionWest); + QVERIFY(window2.m_window->isActive()); + + // Switch to window to the left. + workspace()->switchWindow(Workspace::DirectionWest); + QVERIFY(window1.m_window->isActive()); + + // Switch to window to the left. + workspace()->switchWindow(Workspace::DirectionWest); + QVERIFY(window4.m_window->isActive()); +} + +void ActivationTest::testSwitchToWindowToRight() +{ + // This test verifies that "Switch to Window to the Right" shortcut works. + + // Prepare the test environment. + stackScreensHorizontally(); + + // Create several windows on the left screen. + Test::XdgToplevelWindow window1; + QVERIFY(window1.show()); + QVERIFY(window1.m_window->isActive()); + + Test::XdgToplevelWindow window2; + QVERIFY(window2.show()); + QVERIFY(window2.m_window->isActive()); + + window1.m_window->move(QPoint(300, 200)); + window2.m_window->move(QPoint(500, 200)); + + // Create several windows on the right screen. + Test::XdgToplevelWindow window3; + QVERIFY(window3.show()); + QVERIFY(window3.m_window->isActive()); + + Test::XdgToplevelWindow window4; + QVERIFY(window4.show()); + QVERIFY(window4.m_window->isActive()); + + window3.m_window->move(QPoint(1380, 200)); + window4.m_window->move(QPoint(1580, 200)); + + // Switch to window to the right. + workspace()->switchWindow(Workspace::DirectionEast); + QVERIFY(window1.m_window->isActive()); + + // Switch to window to the right. + workspace()->switchWindow(Workspace::DirectionEast); + QVERIFY(window2.m_window->isActive()); + + // Switch to window to the right. + workspace()->switchWindow(Workspace::DirectionEast); + QVERIFY(window3.m_window->isActive()); + + // Switch to window to the right. + workspace()->switchWindow(Workspace::DirectionEast); + QVERIFY(window4.m_window->isActive()); +} + +void ActivationTest::testSwitchToWindowAbove() +{ + // This test verifies that "Switch to Window Above" shortcut works. + + // Prepare the test environment. + stackScreensVertically(); + + // Create several windows on the top screen. + Test::XdgToplevelWindow window1; + QVERIFY(window1.show()); + QVERIFY(window1.m_window->isActive()); + + Test::XdgToplevelWindow window2; + QVERIFY(window2.show()); + QVERIFY(window2.m_window->isActive()); + + window1.m_window->move(QPoint(200, 300)); + window2.m_window->move(QPoint(200, 500)); + + // Create several windows on the bottom screen. + Test::XdgToplevelWindow window3; + QVERIFY(window3.show()); + QVERIFY(window3.m_window->isActive()); + + Test::XdgToplevelWindow window4; + QVERIFY(window4.show()); + QVERIFY(window4.m_window->isActive()); + + window3.m_window->move(QPoint(200, 1224)); + window4.m_window->move(QPoint(200, 1424)); + + // Switch to window above. + workspace()->switchWindow(Workspace::DirectionNorth); + QVERIFY(window3.m_window->isActive()); + + // Switch to window above. + workspace()->switchWindow(Workspace::DirectionNorth); + QVERIFY(window2.m_window->isActive()); + + // Switch to window above. + workspace()->switchWindow(Workspace::DirectionNorth); + QVERIFY(window1.m_window->isActive()); + + // Switch to window above. + workspace()->switchWindow(Workspace::DirectionNorth); + QVERIFY(window4.m_window->isActive()); +} + +void ActivationTest::testSwitchToWindowBelow() +{ + // This test verifies that "Switch to Window Bottom" shortcut works. + + // Prepare the test environment. + stackScreensVertically(); + + // Create several windows on the top screen. + Test::XdgToplevelWindow window1; + QVERIFY(window1.show()); + QVERIFY(window1.m_window->isActive()); + + Test::XdgToplevelWindow window2; + QVERIFY(window2.show()); + QVERIFY(window2.m_window->isActive()); + + window1.m_window->move(QPoint(200, 300)); + window2.m_window->move(QPoint(200, 500)); + + // Create several windows on the bottom screen. + Test::XdgToplevelWindow window3; + QVERIFY(window3.show()); + QVERIFY(window3.m_window->isActive()); + + Test::XdgToplevelWindow window4; + QVERIFY(window4.show()); + QVERIFY(window4.m_window->isActive()); + + window3.m_window->move(QPoint(200, 1224)); + window4.m_window->move(QPoint(200, 1424)); + + // Switch to window below. + workspace()->switchWindow(Workspace::DirectionSouth); + QVERIFY(window1.m_window->isActive()); + + // Switch to window below. + workspace()->switchWindow(Workspace::DirectionSouth); + QVERIFY(window2.m_window->isActive()); + + // Switch to window below. + workspace()->switchWindow(Workspace::DirectionSouth); + QVERIFY(window3.m_window->isActive()); + + // Switch to window below. + workspace()->switchWindow(Workspace::DirectionSouth); + QVERIFY(window4.m_window->isActive()); +} + +void ActivationTest::testSwitchToWindowMaximized() +{ + // This test verifies that we switch to the top-most maximized window, i.e. + // the one that user sees at the moment. See bug 411356. + + // Prepare the test environment. + stackScreensHorizontally(); + + // Create several maximized windows on the left screen. + Test::XdgToplevelWindow window1; + QVERIFY(window1.show()); + QVERIFY(window1.m_window->isActive()); + QVERIFY(window1.waitSurfaceConfigure()); + workspace()->slotWindowMaximize(); + QVERIFY(window1.handleConfigure(Qt::red)); + + Test::XdgToplevelWindow window2; + QVERIFY(window2.show()); + QVERIFY(window2.m_window->isActive()); + QVERIFY(window2.waitSurfaceConfigure()); + workspace()->slotWindowMaximize(); + QVERIFY(window2.handleConfigure(Qt::red)); + + const QList stackingOrder = workspace()->stackingOrder(); + QVERIFY(stackingOrder.indexOf(window1.m_window) < stackingOrder.indexOf(window2.m_window)); + QCOMPARE(window1.m_window->maximizeMode(), MaximizeFull); + QCOMPARE(window2.m_window->maximizeMode(), MaximizeFull); + + // Create several windows on the right screen. + Test::XdgToplevelWindow window3; + QVERIFY(window3.show()); + QVERIFY(window3.m_window->isActive()); + + Test::XdgToplevelWindow window4; + QVERIFY(window4.show()); + QVERIFY(window4.m_window->isActive()); + + window3.m_window->move(QPoint(1380, 200)); + window4.m_window->move(QPoint(1580, 200)); + + // Switch to window to the left. + workspace()->switchWindow(Workspace::DirectionWest); + QVERIFY(window3.m_window->isActive()); + + // Switch to window to the left. + workspace()->switchWindow(Workspace::DirectionWest); + QVERIFY(window2.m_window->isActive()); + + // Switch to window to the left. + workspace()->switchWindow(Workspace::DirectionWest); + QVERIFY(window4.m_window->isActive()); +} + +void ActivationTest::testSwitchToWindowFullScreen() +{ + // This test verifies that we switch to the top-most fullscreen window, i.e. + // the one that user sees at the moment. See bug 411356. + + // Prepare the test environment. + stackScreensVertically(); + + // Create several maximized windows on the top screen. + Test::XdgToplevelWindow window1; + QVERIFY(window1.show()); + QVERIFY(window1.m_window->isActive()); + QVERIFY(window1.waitSurfaceConfigure()); + workspace()->slotWindowFullScreen(); + QVERIFY(window1.handleConfigure(Qt::red)); + + Test::XdgToplevelWindow window2; + QVERIFY(window2.show()); + QVERIFY(window2.m_window->isActive()); + QVERIFY(window2.waitSurfaceConfigure()); + workspace()->slotWindowFullScreen(); + QVERIFY(window2.handleConfigure()); + + const QList stackingOrder = workspace()->stackingOrder(); + QVERIFY(stackingOrder.indexOf(window1.m_window) < stackingOrder.indexOf(window2.m_window)); + QVERIFY(window1.m_window->isFullScreen()); + QVERIFY(window2.m_window->isFullScreen()); + + // Create several windows on the bottom screen. + Test::XdgToplevelWindow window3; + QVERIFY(window3.show()); + QVERIFY(window3.m_window->isActive()); + + Test::XdgToplevelWindow window4; + QVERIFY(window4.show()); + QVERIFY(window4.m_window->isActive()); + + window3.m_window->move(QPoint(200, 1224)); + window4.m_window->move(QPoint(200, 1424)); + + // Switch to window above. + workspace()->switchWindow(Workspace::DirectionNorth); + QVERIFY(window3.m_window->isActive()); + + // Switch to window above. + workspace()->switchWindow(Workspace::DirectionNorth); + QVERIFY(window2.m_window->isActive()); + + // Switch to window above. + workspace()->switchWindow(Workspace::DirectionNorth); + QVERIFY(window4.m_window->isActive()); +} + +void ActivationTest::stackScreensHorizontally() +{ + // Process pending wl_output bind requests before destroying all outputs. + QTest::qWait(1); + + const QList screenGeometries{ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }; + Test::setOutputConfig(screenGeometries); +} + +void ActivationTest::stackScreensVertically() +{ + // Process pending wl_output bind requests before destroying all outputs. + QTest::qWait(1); + + const QList screenGeometries{ + Rect(0, 0, 1280, 1024), + Rect(0, 1024, 1280, 1024), + }; + Test::setOutputConfig(screenGeometries); +} + +#if KWIN_BUILD_X11 +static X11Window *createX11Window(xcb_connection_t *connection, const Rect &geometry, std::function setup = {}) +{ + xcb_window_t windowId = xcb_generate_id(connection); + xcb_create_window(connection, XCB_COPY_FROM_PARENT, windowId, rootWindow(), + geometry.x(), + geometry.y(), + geometry.width(), + geometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, geometry.x(), geometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, geometry.width(), geometry.height()); + xcb_icccm_set_wm_normal_hints(connection, windowId, &hints); + + if (setup) { + setup(windowId); + } + + xcb_map_window(connection, windowId); + xcb_flush(connection); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + if (!windowCreatedSpy.wait()) { + return nullptr; + } + return windowCreatedSpy.last().first().value(); +} +#endif + +void ActivationTest::testActiveFullscreen() +{ +#if KWIN_BUILD_X11 + // Tests that an active X11 fullscreen window gets removed from the active layer + // when activating a Wayland window, even if there's a pending activation request + // for the X11 window + Test::setOutputConfig({Rect(0, 0, 1280, 1024)}); + + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *x11Window = createX11Window(c.get(), Rect(0, 0, 100, 200)); + + // make it fullscreen + x11Window->setFullScreen(true); + QVERIFY(x11Window->isFullScreen()); + QCOMPARE(x11Window->layer(), Layer::ActiveLayer); + + // now, activate it again + workspace()->activateWindow(x11Window); + + // now, create and activate a Wayland window + Test::XdgToplevelWindow waylandWindow; + QVERIFY(waylandWindow.show()); + + // the Wayland window should become active + // and the X11 window should not be in the active layer anymore + QSignalSpy stackingOrder(workspace(), &Workspace::stackingOrderChanged); + workspace()->activateWindow(waylandWindow.m_window); + QCOMPARE(workspace()->activeWindow(), waylandWindow.m_window); + QCOMPARE(x11Window->layer(), Layer::NormalLayer); +#endif +} + +static std::vector> setupWindows(uint32_t &time) +{ + // re-create the same setup every time for reduced confusion + std::vector> ret; + for (int i = 0; i < 3; i++) { + auto window = std::make_unique(); + window->show(); + window->m_window->move(QPoint(150 * i, 0)); + workspace()->activateWindow(window->m_window); + + Test::pointerMotion(window->m_window->frameGeometry().center(), time++); + Test::pointerButtonPressed(1, time++); + Test::pointerButtonReleased(1, time++); + ret.push_back(std::move(window)); + } + return ret; +} + +void ActivationTest::testXdgActivation() +{ + Test::setOutputConfig({Rect(0, 0, 1280, 1024)}); + + uint32_t time = 0; + + auto windows = setupWindows(time); + + QSignalSpy activationSpy(workspace(), &Workspace::windowActivated); + + // activating a window without a valid token should fail + Test::xdgActivation()->activate(QString(), *windows[1]->m_surface); + QVERIFY(!activationSpy.wait(10)); + + // using the surface and a correct serial should make it work + auto token = Test::xdgActivation()->createToken(); + token->set_surface(*windows.back()->m_surface); + token->set_serial(windows.back()->m_window->lastUsageSerial(), *Test::waylandSeat()); + Test::xdgActivation()->activate(token->commitAndWait(), *windows[1]->m_surface); + QVERIFY(activationSpy.wait()); + QCOMPARE(workspace()->activeWindow(), windows[1]->m_window); + + // it should even work without the surface, if the serial is correct + windows = setupWindows(time); + token = Test::xdgActivation()->createToken(); + token->set_serial(windows.back()->m_window->lastUsageSerial(), *Test::waylandSeat()); + Test::xdgActivation()->activate(token->commitAndWait(), *windows[1]->m_surface); + QVERIFY(activationSpy.wait(10)); + QCOMPARE(workspace()->activeWindow(), windows[1]->m_window); + + // activation should still work if the window is closed after creating the token + windows = setupWindows(time); + token = Test::xdgActivation()->createToken(); + token->set_surface(*windows[2]->m_surface); + token->set_serial(windows[2]->m_window->lastUsageSerial(), *Test::waylandSeat()); + QString result = token->commitAndWait(); + + windows[2]->unmap(); + QVERIFY(activationSpy.wait(10)); + QCOMPARE(workspace()->activeWindow(), windows[1]->m_window); + + Test::xdgActivation()->activate(result, *windows[0]->m_surface); + QVERIFY(activationSpy.wait(10)); + QCOMPARE(workspace()->activeWindow(), windows[0]->m_window); + + // ...unless the user interacted with another window in between + windows = setupWindows(time); + token = Test::xdgActivation()->createToken(); + token->set_surface(*windows[2]->m_surface); + token->set_serial(windows[2]->m_window->lastUsageSerial(), *Test::waylandSeat()); + result = token->commitAndWait(); + + windows[2]->unmap(); + QVERIFY(activationSpy.wait(10)); + QCOMPARE(workspace()->activeWindow(), windows[1]->m_window); + + Test::pointerMotion(windows[1]->m_window->frameGeometry().center(), time++); + Test::pointerButtonPressed(1, time++); + Test::pointerButtonReleased(1, time++); + + Test::xdgActivation()->activate(result, *windows[0]->m_surface); + QVERIFY(!activationSpy.wait(10)); + QCOMPARE(workspace()->activeWindow(), windows[1]->m_window); + + // ensure that windows are only activated on show with a valid activation token + options->setFocusStealingPreventionLevel(FocusStealingPreventionLevel::Extreme); + + // creating a new window and immediately activating it should work + windows.clear(); + windows = setupWindows(time); + token = Test::xdgActivation()->createToken(); + token->set_surface(*windows[2]->m_surface); + token->set_serial(windows[2]->m_window->lastUsageSerial(), *Test::waylandSeat()); + result = token->commitAndWait(); + { + Test::XdgToplevelWindow window{[&result](KWayland::Client::Surface *surface, Test::XdgToplevel *toplevel) { + Test::xdgActivation()->activate(result, *surface); + }}; + QVERIFY(window.show()); + QCOMPARE(workspace()->activeWindow(), window.m_window); + } + + // activation should fail if the user clicks on another window in between + // creating the activation token and using it + windows = setupWindows(time); + token = Test::xdgActivation()->createToken(); + token->set_surface(*windows[2]->m_surface); + token->set_serial(windows[2]->m_window->lastUsageSerial(), *Test::waylandSeat()); + result = token->commitAndWait(); + + Test::pointerMotion(windows[1]->m_window->frameGeometry().center(), time++); + Test::pointerButtonPressed(1, time++); + Test::pointerButtonReleased(1, time++); + + { + Test::XdgToplevelWindow window{[&result](KWayland::Client::Surface *surface, Test::XdgToplevel *toplevel) { + Test::xdgActivation()->activate(result, *surface); + }}; + QVERIFY(window.show()); + QCOMPARE(workspace()->activeWindow(), windows[1]->m_window); + // if activation of a new window is not granted, it should be stacked behind the active window + QCOMPARE_LT(window.m_window->stackingOrder(), workspace()->activeWindow()->stackingOrder()); + } + + // same for pointer input on the currently focused window + windows = setupWindows(time); + token = Test::xdgActivation()->createToken(); + token->set_surface(*windows[2]->m_surface); + token->set_serial(windows[2]->m_window->lastUsageSerial(), *Test::waylandSeat()); + result = token->commitAndWait(); + + Test::pointerMotion(windows[2]->m_window->frameGeometry().center(), time++); + Test::pointerButtonPressed(1, time++); + Test::pointerButtonReleased(1, time++); + + { + Test::XdgToplevelWindow window{[&result](KWayland::Client::Surface *surface, Test::XdgToplevel *toplevel) { + Test::xdgActivation()->activate(result, *surface); + }}; + QVERIFY(window.show()); + QCOMPARE(workspace()->activeWindow(), windows[2]->m_window); + QCOMPARE_LT(window.m_window->stackingOrder(), workspace()->activeWindow()->stackingOrder()); + } + + // same for keyboard input on the currently focused window + windows = setupWindows(time); + token = Test::xdgActivation()->createToken(); + token->set_surface(*windows[2]->m_surface); + token->set_serial(windows[2]->m_window->lastUsageSerial(), *Test::waylandSeat()); + result = token->commitAndWait(); + + Test::keyboardKeyPressed(KEY_A, time++); + Test::keyboardKeyReleased(KEY_A, time++); + + { + Test::XdgToplevelWindow window{[&result](KWayland::Client::Surface *surface, Test::XdgToplevel *toplevel) { + Test::xdgActivation()->activate(result, *surface); + }}; + QVERIFY(window.show()); + QCOMPARE(workspace()->activeWindow(), windows[2]->m_window); + // if activation of a new window is not granted, it should be stacked behind the active window + QCOMPARE_LT(window.m_window->stackingOrder(), workspace()->activeWindow()->stackingOrder()); + } + + // but modifier keys must not interfere in activation + windows = setupWindows(time); + token = Test::xdgActivation()->createToken(); + token->set_surface(*windows[2]->m_surface); + token->set_serial(windows[2]->m_window->lastUsageSerial(), *Test::waylandSeat()); + result = token->commitAndWait(); + + Test::keyboardKeyPressed(KEY_LEFTSHIFT, time++); + Test::keyboardKeyReleased(KEY_LEFTSHIFT, time++); + + { + Test::XdgToplevelWindow window{[&result](KWayland::Client::Surface *surface, Test::XdgToplevel *toplevel) { + Test::xdgActivation()->activate(result, *surface); + }}; + QVERIFY(window.show()); + QCOMPARE(workspace()->activeWindow(), window.m_window); + } + + // a child window of the active one should always be automatically activated, + // even without an activation token + windows = setupWindows(time); + { + Test::XdgToplevelWindow window{[&windows](KWayland::Client::Surface *surface, Test::XdgToplevel *toplevel) { + toplevel->set_parent(windows[2]->m_toplevel->object()); + }}; + QVERIFY(window.show()); + QCOMPARE(workspace()->activeWindow(), window.m_window); + } + + // but a child window of a non-active one should not be automatically activated + windows = setupWindows(time); + { + Test::XdgToplevelWindow window{[&windows](KWayland::Client::Surface *surface, Test::XdgToplevel *toplevel) { + toplevel->set_parent(windows[1]->m_toplevel->object()); + }}; + QVERIFY(window.show()); + QCOMPARE_NE(workspace()->activeWindow(), window.m_window); + } + + // unless it has a valid activation token + windows = setupWindows(time); + token = Test::xdgActivation()->createToken(); + token->set_surface(*windows[2]->m_surface); + token->set_serial(windows[2]->m_window->lastUsageSerial(), *Test::waylandSeat()); + result = token->commitAndWait(); + { + Test::XdgToplevelWindow window{[&windows, &result](KWayland::Client::Surface *surface, Test::XdgToplevel *toplevel) { + toplevel->set_parent(windows[1]->m_toplevel->object()); + Test::xdgActivation()->activate(result, *surface); + }}; + QVERIFY(window.show()); + QCOMPARE(workspace()->activeWindow(), window.m_window); + } + + // focus stealing prevention level High is more lax and should activate windows + // even with an invalid token if the app id matches the last granted activation token + options->setFocusStealingPreventionLevel(FocusStealingPreventionLevel::High); + windows = setupWindows(time); + windows[1]->m_toplevel->set_app_id("test_app_id"); + token = Test::xdgActivation()->createToken(); + token->set_surface(*windows[2]->m_surface); + token->set_serial(windows[2]->m_window->lastUsageSerial(), *Test::waylandSeat()); + token->set_app_id("test_app_id"); + result = token->commitAndWait(); + Test::xdgActivation()->activate(QString(), *windows[1]->m_surface); + QVERIFY(activationSpy.wait()); + QCOMPARE(workspace()->activeWindow(), windows[1]->m_window); + + // new windows should also be activated if the app id matches, + // even if they don't actually request activation + windows = setupWindows(time); + token = Test::xdgActivation()->createToken(); + token->set_surface(*windows[2]->m_surface); + token->set_serial(windows[2]->m_window->lastUsageSerial(), *Test::waylandSeat()); + token->set_app_id("test_app_id_2"); + result = token->commitAndWait(); + { + Test::XdgToplevelWindow window{[&result](Test::XdgToplevel *toplevel) { + toplevel->set_app_id("test_app_id_2"); + }}; + QVERIFY(window.show()); + QCOMPARE(workspace()->activeWindow(), window.m_window); + } + + // with focus stealing prevention level Low, every new window should unconditionally be activated, + // even if it doesn't request an activation token at all + options->setFocusStealingPreventionLevel(FocusStealingPreventionLevel::Low); + windows = setupWindows(time); + { + Test::XdgToplevelWindow window; + QVERIFY(window.show()); + QCOMPARE(workspace()->activeWindow(), window.m_window); + } + + // with focus stealing prevention disabled, every activation token is considered "valid" + options->setFocusStealingPreventionLevel(FocusStealingPreventionLevel::None); + windows = setupWindows(time); + Test::xdgActivation()->activate(QString(), *windows[1]->m_surface); + QVERIFY(activationSpy.wait()); + QCOMPARE(workspace()->activeWindow(), windows[1]->m_window); +} + +void ActivationTest::testNoDemandAttentionWithoutActivationRequest() +{ + // This test verifies that the demand attention state will not be set if kwin doesn't want a + // window to be activated and the client hasn't made an explicit activation request. + + options->setFocusStealingPreventionLevel(FocusStealingPreventionLevel::High); + + // Show a window + auto gooseWindow = std::make_unique([](Test::XdgToplevel *toplevel) { + toplevel->set_app_id("org.kde.goose"); + }); + gooseWindow->show(); + QVERIFY(gooseWindow->m_window->isActive()); + QVERIFY(!gooseWindow->m_window->isDemandingAttention()); + + // Show another window. + auto wolfWindow = std::make_unique([](Test::XdgToplevel *toplevel) { + toplevel->set_app_id("org.kde.wolf"); + }); + wolfWindow->show(); + QVERIFY(!wolfWindow->m_window->isActive()); + QVERIFY(!wolfWindow->m_window->isDemandingAttention()); +} + +static QString generateActivationToken(const Test::XdgToplevelWindow &window, const QString &appId) +{ + std::unique_ptr token = Test::xdgActivation()->createToken(); + token->set_surface(*window.m_surface); + token->set_serial(window.m_window->lastUsageSerial(), *Test::waylandSeat()); + token->set_app_id(appId); + return token->commitAndWait(); +} + +void ActivationTest::testXdgActivationBeforeInitialCommit() +{ + // This test verifies that activation requests that are made before the initial xdg-toplevel + // commit are handled as expected, i.e. that the window will be eventually marked as active and + // that the Workspace state is not touched yet. + + options->setFocusStealingPreventionLevel(FocusStealingPreventionLevel::High); + + // Show a window + auto gooseWindow = std::make_unique([](Test::XdgToplevel *toplevel) { + toplevel->set_app_id("org.kde.goose"); + }); + gooseWindow->show(); + QVERIFY(gooseWindow->m_window->isActive()); + + // Click in the middle of the window so there is a proper usage serial. + uint32_t time = 0; + Test::pointerMotion(gooseWindow->m_window->frameGeometry().center(), time++); + Test::pointerButtonPressed(BTN_LEFT, time++); + Test::pointerButtonReleased(BTN_LEFT, time++); + + const QString invalidActivationToken = QStringLiteral("duckity duck"); + const QString activationToken = generateActivationToken(*gooseWindow, QStringLiteral("org.kde.wolf")); + + { + std::unique_ptr duckSurface(Test::createSurface()); + std::unique_ptr duckShellSurface(Test::createXdgToplevelSurface(duckSurface.get(), Test::CreationSetup::CreateOnly)); + QSignalSpy toplevelConfigureRequestedSpy(duckShellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(duckShellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + // Wait for the compositor side window to be created. + QSignalSpy windowCreatedSpy(waylandServer(), &WaylandServer::windowCreated); + QVERIFY(windowCreatedSpy.wait()); + Window *duckWindow = windowCreatedSpy.last().at(0).value(); + + // Attempt to activate the window, waylandSync() is needed to make sure that the compositor + // side has processed the activation request before we check the compositor state. + // + // The activation token should only be cached, no other activation related state should be set. + Test::xdgActivation()->activate(invalidActivationToken, *duckSurface); + QVERIFY(Test::waylandSync()); + QCOMPARE(duckWindow->activationToken(), invalidActivationToken); + QVERIFY(!duckWindow->isActive()); + QVERIFY(!duckWindow->isDemandingAttention()); + QVERIFY(!workspace()->stackingOrder().contains(duckWindow)); + + // Commit the initial state. + duckShellSurface->set_app_id(QStringLiteral("org.kde.duck")); + duckSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + // Map the window. + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(0, 0)); + duckShellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::renderAndWaitForShown(duckSurface.get(), QSize(800, 600), Qt::blue); + + // The window should not be activate because of the invalid activation token. + QVERIFY(!duckWindow->isActive()); + QVERIFY(duckWindow->isDemandingAttention()); + } + + { + std::unique_ptr wolfSurface(Test::createSurface()); + std::unique_ptr wolfShellSurface(Test::createXdgToplevelSurface(wolfSurface.get(), Test::CreationSetup::CreateOnly)); + QSignalSpy toplevelConfigureRequestedSpy(wolfShellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(wolfShellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + // Wait for the compositor side window to be created. + QSignalSpy windowCreatedSpy(waylandServer(), &WaylandServer::windowCreated); + QVERIFY(windowCreatedSpy.wait()); + Window *wolfWindow = windowCreatedSpy.last().at(0).value(); + + // Attempt to activate the window, waylandSync() is needed to make sure that the compositor + // side has processed the activation request before we check the compositor state. + // + // The activation token should only be cached, no other activation related state should be set. + Test::xdgActivation()->activate(activationToken, *wolfSurface); + QVERIFY(Test::waylandSync()); + QCOMPARE(wolfWindow->activationToken(), activationToken); + QVERIFY(!wolfWindow->isActive()); + QVERIFY(!wolfWindow->isDemandingAttention()); + QVERIFY(!workspace()->stackingOrder().contains(wolfWindow)); + + // Commit the initial state. + wolfShellSurface->set_app_id(QStringLiteral("org.kde.wolf")); + wolfSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + // Map the window. + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(0, 0)); + wolfShellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::renderAndWaitForShown(wolfSurface.get(), QSize(800, 600), Qt::blue); + + // The window should be activated as expected. + QVERIFY(wolfWindow->isActive()); + QVERIFY(!wolfWindow->isDemandingAttention()); + } +} + +void ActivationTest::testXdgActivationBeforeMap() +{ + // This test verifies that activation requests that are made before the xdg-toplevel surface + // is mapped are handled as expected, i.e. that the window will be eventually marked as active + // and that the Workspace state is not touched yet. + + options->setFocusStealingPreventionLevel(FocusStealingPreventionLevel::High); + + // Show a window + auto gooseWindow = std::make_unique([](Test::XdgToplevel *toplevel) { + toplevel->set_app_id("org.kde.goose"); + }); + gooseWindow->show(); + QVERIFY(gooseWindow->m_window->isActive()); + + // Click in the middle of the window so there is a proper usage serial. + uint32_t time = 0; + Test::pointerMotion(gooseWindow->m_window->frameGeometry().center(), time++); + Test::pointerButtonPressed(BTN_LEFT, time++); + Test::pointerButtonReleased(BTN_LEFT, time++); + + const QString invalidActivationToken = QStringLiteral("duckity duck"); + const QString activationToken = generateActivationToken(*gooseWindow, QStringLiteral("org.kde.wolf")); + + { + std::unique_ptr duckSurface(Test::createSurface()); + std::unique_ptr duckShellSurface(Test::createXdgToplevelSurface(duckSurface.get(), Test::CreationSetup::CreateOnly)); + QSignalSpy toplevelConfigureRequestedSpy(duckShellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(duckShellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + // Wait for the compositor side window to be created. + QSignalSpy windowCreatedSpy(waylandServer(), &WaylandServer::windowCreated); + QVERIFY(windowCreatedSpy.wait()); + Window *duckWindow = windowCreatedSpy.last().at(0).value(); + + // Commit the initial state. + duckShellSurface->set_app_id(QStringLiteral("org.kde.duck")); + duckSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + // Attempt to activate the window, waylandSync() is needed to make sure that the compositor + // side has processed the activation request before we check the compositor state. + // + // The activation token should only be cached, no other activation related state should be set. + Test::xdgActivation()->activate(invalidActivationToken, *duckSurface); + QVERIFY(Test::waylandSync()); + QCOMPARE(duckWindow->activationToken(), invalidActivationToken); + QVERIFY(!duckWindow->isActive()); + QVERIFY(!duckWindow->isDemandingAttention()); + QVERIFY(!workspace()->stackingOrder().contains(duckWindow)); + + // Map the window. + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(0, 0)); + duckShellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::renderAndWaitForShown(duckSurface.get(), QSize(800, 600), Qt::blue); + + // The window should not be activate because of the invalid activation token. + QVERIFY(!duckWindow->isActive()); + QVERIFY(duckWindow->isDemandingAttention()); + } + + { + std::unique_ptr wolfSurface(Test::createSurface()); + std::unique_ptr wolfShellSurface(Test::createXdgToplevelSurface(wolfSurface.get(), Test::CreationSetup::CreateOnly)); + QSignalSpy toplevelConfigureRequestedSpy(wolfShellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(wolfShellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + // Wait for the compositor side window to be created. + QSignalSpy windowCreatedSpy(waylandServer(), &WaylandServer::windowCreated); + QVERIFY(windowCreatedSpy.wait()); + Window *wolfWindow = windowCreatedSpy.last().at(0).value(); + + // Commit the initial state. + wolfShellSurface->set_app_id(QStringLiteral("org.kde.wolf")); + wolfSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + // Attempt to activate the window, waylandSync() is needed to make sure that the compositor + // side has processed the activation request before we check the compositor state. + // + // The activation token should only be cached, no other activation related state should be set. + Test::xdgActivation()->activate(activationToken, *wolfSurface); + QVERIFY(Test::waylandSync()); + QCOMPARE(wolfWindow->activationToken(), activationToken); + QVERIFY(!wolfWindow->isActive()); + QVERIFY(!wolfWindow->isDemandingAttention()); + QVERIFY(!workspace()->stackingOrder().contains(wolfWindow)); + + // Map the window. + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(0, 0)); + wolfShellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::renderAndWaitForShown(wolfSurface.get(), QSize(800, 600), Qt::blue); + + // The window should be activated as expected. + QVERIFY(wolfWindow->isActive()); + QVERIFY(!wolfWindow->isDemandingAttention()); + } +} + +class TokenSpy : public InputEventSpy +{ +public: + void keyboardKey(KeyboardKeyEvent *event) override + { + if (event->state == KeyboardKeyState::Pressed && event->key == Qt::Key::Key_A) { + latestToken = waylandServer()->xdgActivationIntegration()->requestPrivilegedToken( + nullptr, input()->lastInteractionSerial(), waylandServer()->seat(), "test"); + } + } + + QString latestToken; +}; + +void ActivationTest::testGlobalShortcutActivation() +{ + Test::setOutputConfig({Rect(0, 0, 1280, 1024)}); + options->setFocusStealingPreventionLevel(FocusStealingPreventionLevel::Extreme); + + // This spy needs to be used because normal keyboard shortcuts are signaled asynchronously, + // while the shortcut for launching an application creates the token immediately. + // The spy emulates the latter behavior. + TokenSpy tokenSpy; + input()->installInputEventSpy(&tokenSpy); + QSignalSpy activationSpy(workspace(), &Workspace::windowActivated); + + uint32_t time = 0; + + // just triggering the shortcut normally should have working activation + auto windows = setupWindows(time); + + Test::keyboardKeyPressed(KEY_LEFTSHIFT, time++); + Test::keyboardKeyPressed(KEY_A, time++); + Test::keyboardKeyReleased(KEY_A, time++); + Test::keyboardKeyReleased(KEY_LEFTSHIFT, time++); + + Test::xdgActivation()->activate(tokenSpy.latestToken, *windows[1]->m_surface); + QVERIFY(activationSpy.wait()); + QCOMPARE(workspace()->activeWindow(), windows[1]->m_window); + + // if we press a non-shift key after triggering the shortcut, + // activation should fail + windows = setupWindows(time); + + Test::keyboardKeyPressed(KEY_LEFTSHIFT, time++); + Test::keyboardKeyPressed(KEY_A, time++); + Test::keyboardKeyReleased(KEY_A, time++); + Test::keyboardKeyReleased(KEY_LEFTSHIFT, time++); + + Test::keyboardKeyPressed(KEY_B, time++); + Test::keyboardKeyReleased(KEY_B, time++); + + Test::xdgActivation()->activate(tokenSpy.latestToken, *windows[1]->m_surface); + QVERIFY(!activationSpy.wait(10)); + QCOMPARE(workspace()->activeWindow(), windows[2]->m_window); +} + +void ActivationTest::testFocusMovesFromClosedDialogToParentWindow() +{ + // This test verifies that input focus moves from a closed dialog to the parent window as expected. + + QSignalSpy windowActivatedSpy(workspace(), &Workspace::windowActivated); + + std::unique_ptr surface{Test::createSurface()}; + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QCOMPARE(windowActivatedSpy.count(), 1); + QCOMPARE(windowActivatedSpy.last().at(0).value(), window); + QCOMPARE(workspace()->activeWindow(), window); + + std::unique_ptr transientSurface{Test::createSurface()}; + std::unique_ptr transientShellSurface(Test::createXdgToplevelSurface(transientSurface.get())); + transientShellSurface->set_parent(shellSurface->object()); + auto transient = Test::renderAndWaitForShown(transientSurface.get(), QSize(100, 50), Qt::blue); + QVERIFY(transient); + QCOMPARE(windowActivatedSpy.count(), 2); + QCOMPARE(windowActivatedSpy.last().at(0).value(), transient); + QCOMPARE(workspace()->activeWindow(), transient); + + transientShellSurface.reset(); + transientSurface.reset(); + QVERIFY(windowActivatedSpy.wait()); + QCOMPARE(windowActivatedSpy.count(), 3); + QCOMPARE(windowActivatedSpy.last().at(0).value(), window); +} + +void ActivationTest::testFullAreaLayerSurfaceUnderlay_data() +{ + QTest::addColumn("layer"); + + QTest::addRow("background") << Test::LayerShellV1::layer_background; + QTest::addRow("bottom") << Test::LayerShellV1::layer_bottom; +} + +void ActivationTest::testFullAreaLayerSurfaceUnderlay() +{ + // This test verifies that an underlay layer shell surface that covers the whole screen will not + // be activated without a valid activation token by accident. + + options->setFocusStealingPreventionLevel(FocusStealingPreventionLevel::High); + + // Show a regular window. + Test::XdgToplevelWindow window; + QVERIFY(window.show()); + QCOMPARE(workspace()->activeWindow(), window.m_window); + + // Show a fullscreen layer shell surface. + QFETCH(Test::LayerShellV1::layer, layer); + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createLayerSurfaceV1(surface.get(), QStringLiteral("test"))); + shellSurface->set_layer(layer); + shellSurface->set_anchor(Test::LayerSurfaceV1::anchor_top | Test::LayerSurfaceV1::anchor_right | Test::LayerSurfaceV1::anchor_bottom | Test::LayerSurfaceV1::anchor_left); + shellSurface->set_size(0, 0); + shellSurface->set_keyboard_interactivity(1); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + QSignalSpy configureRequestedSpy(shellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + Test::renderAndWaitForShown(surface.get(), requestedSize, Qt::red); + QCOMPARE(workspace()->activeWindow(), window.m_window); +} + +void ActivationTest::testFullAreaLayerSurfaceOverlay_data() +{ + QTest::addColumn("layer"); + + QTest::addRow("top") << Test::LayerShellV1::layer_top; + QTest::addRow("overlay") << Test::LayerShellV1::layer_overlay; +} + +void ActivationTest::testFullAreaLayerSurfaceOverlay() +{ + // This test verifies that an overlay layer shell surface that covers the whole screen will be + // activated even without a valid activation token (because it will cover the active window). + + options->setFocusStealingPreventionLevel(FocusStealingPreventionLevel::High); + + // Show a regular window. + Test::XdgToplevelWindow window; + QVERIFY(window.show()); + QCOMPARE(workspace()->activeWindow(), window.m_window); + + // Show a fullscreen layer shell surface. + QFETCH(Test::LayerShellV1::layer, layer); + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createLayerSurfaceV1(surface.get(), QStringLiteral("test"))); + shellSurface->set_layer(layer); + shellSurface->set_anchor(Test::LayerSurfaceV1::anchor_top | Test::LayerSurfaceV1::anchor_right | Test::LayerSurfaceV1::anchor_bottom | Test::LayerSurfaceV1::anchor_left); + shellSurface->set_size(0, 0); + shellSurface->set_keyboard_interactivity(1); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + QSignalSpy configureRequestedSpy(shellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + Window *overlayWindow = Test::renderAndWaitForShown(surface.get(), requestedSize, Qt::red); + QCOMPARE(workspace()->activeWindow(), overlayWindow); +} + +void ActivationTest::testPartialAreaLayerSurfaceOverlay_data() +{ + QTest::addColumn("layer"); + + QTest::addRow("top") << Test::LayerShellV1::layer_top; + QTest::addRow("overlay") << Test::LayerShellV1::layer_overlay; +} + +void ActivationTest::testPartialAreaLayerSurfaceOverlay() +{ + // This test verifies that an overlay layer shell surface that partially covers the screen will + // not be activated without an activation token by accident. + + options->setFocusStealingPreventionLevel(FocusStealingPreventionLevel::High); + + // Show a regular window. + Test::XdgToplevelWindow window; + QVERIFY(window.show()); + QCOMPARE(workspace()->activeWindow(), window.m_window); + + // Show a fullscreen layer shell surface. + QFETCH(Test::LayerShellV1::layer, layer); + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createLayerSurfaceV1(surface.get(), QStringLiteral("test"))); + shellSurface->set_layer(layer); + shellSurface->set_size(1000, 1000); + shellSurface->set_keyboard_interactivity(1); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + QSignalSpy configureRequestedSpy(shellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + Test::renderAndWaitForShown(surface.get(), requestedSize, Qt::red); + QCOMPARE(workspace()->activeWindow(), window.m_window); +} +} + +WAYLANDTEST_MAIN(KWin::ActivationTest) +#include "activation_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/activities_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/activities_test.cpp new file mode 100644 index 0000000000..dafed65123 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/activities_test.cpp @@ -0,0 +1,134 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "activities.h" +#include "core/output.h" +#include "pointer_input.h" +#include "utils/xcbutils.h" +#include "wayland_server.h" +#include "workspace.h" +#include "x11window.h" + +#include +#include +#include + +#include +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_activities-0"); + +class ActivitiesTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + void init(); + void cleanup(); + void testSetOnActivitiesValidates(); + +private: +}; + +void ActivitiesTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->setUseKActivities(true); + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); +} + +void ActivitiesTest::cleanupTestCase() +{ + // terminate any still running kactivitymanagerd + QDBusConnection::sessionBus().asyncCall(QDBusMessage::createMethodCall( + QStringLiteral("org.kde.ActivityManager"), + QStringLiteral("/ActivityManager"), + QStringLiteral("org.qtproject.Qt.QCoreApplication"), + QStringLiteral("quit"))); +} + +void ActivitiesTest::init() +{ + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); +} + +void ActivitiesTest::cleanup() +{ +} + +void ActivitiesTest::testSetOnActivitiesValidates() +{ + // this test verifies that windows can't be placed on activities that don't exist + // create an xcb window + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + + xcb_window_t windowId = xcb_generate_id(c.get()); + const Rect windowGeometry(0, 0, 100, 200); + + auto cookie = xcb_create_window_checked(c.get(), 0, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, 0, 0, nullptr); + QVERIFY(!xcb_request_check(c.get(), cookie)); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + // we should get a window for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.first().first().value(); + QVERIFY(window); + QCOMPARE(window->window(), windowId); + QVERIFY(window->isDecorated()); + + // verify the test machine doesn't have the following activities used + QVERIFY(!Workspace::self()->activities()->all().contains(QStringLiteral("foo"))); + QVERIFY(!Workspace::self()->activities()->all().contains(QStringLiteral("bar"))); + + window->setOnActivities(QStringList{QStringLiteral("foo"), QStringLiteral("bar")}); + QVERIFY(!window->activities().contains(QLatin1String("foo"))); + QVERIFY(!window->activities().contains(QLatin1String("bar"))); + + // and destroy the window again + xcb_unmap_window(c.get(), windowId); + xcb_destroy_window(c.get(), windowId); + xcb_flush(c.get()); + c.reset(); + + QSignalSpy windowClosedSpy(window, &X11Window::closed); + QVERIFY(windowClosedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::ActivitiesTest) +#include "activities_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/bounce_keys_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/bounce_keys_test.cpp new file mode 100644 index 0000000000..54f8fa98ff --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/bounce_keys_test.cpp @@ -0,0 +1,185 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + SPDX-FileCopyrightText: 2023 Nicolas Fella + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "keyboard_input.h" +#include "pluginmanager.h" +#include "pointer_input.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include + +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_bounce_keys-0"); + +class BounceKeysTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testBounce(); + void testBounceKeyRepeat(); + +private: + std::unique_ptr m_connection; +}; + +void BounceKeysTest::initTestCase() +{ + KConfig kaccessConfig("kaccessrc"); + kaccessConfig.group(QStringLiteral("Keyboard")).writeEntry("BounceKeys", true); + kaccessConfig.group(QStringLiteral("Keyboard")).writeEntry("BounceKeysDelay", 200); + kaccessConfig.sync(); + + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + qputenv("XKB_DEFAULT_RULES", "evdev"); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void BounceKeysTest::init() +{ + m_connection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat); + QVERIFY(Test::waitForWaylandKeyboard(m_connection->seat)); +} + +void BounceKeysTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void BounceKeysTest::testBounce() +{ + const auto keyboard = m_connection->kwinSeat->getKeyboard(); + QVERIFY(keyboard); + QSignalSpy enteredSpy(keyboard.get(), &Test::WlKeyboard::enter); + QSignalSpy keySpy(keyboard.get(), &Test::WlKeyboard::key); + + Test::XdgToplevelWindow window{m_connection.get()}; + QVERIFY(window.show()); + QVERIFY(enteredSpy.wait()); + + quint32 timestamp = 0; + + // Press a key, verify that it goes through + Test::keyboardKeyPressed(KEY_A, timestamp); + QVERIFY(keySpy.wait()); + QCOMPARE(keySpy.first()[2], KEY_A); + QCOMPARE(keySpy.first().last().value(), Test::WlKeyboard::key_state::key_state_pressed); + keySpy.clear(); + + Test::keyboardKeyReleased(KEY_A, timestamp++); + QVERIFY(keySpy.wait()); + QCOMPARE(keySpy.first()[2], KEY_A); + QCOMPARE(keySpy.first().last().value(), Test::WlKeyboard::key_state::key_state_released); + keySpy.clear(); + + // Press it again within the bounce interval, verify that it does *not* go through + timestamp += 100; + Test::keyboardKeyPressed(KEY_A, timestamp); + QVERIFY(!keySpy.wait(100)); + keySpy.clear(); + + // Press it again after the bounce interval, verify that it does go through + timestamp += 1000; + Test::keyboardKeyPressed(KEY_A, timestamp); + QVERIFY(keySpy.wait()); + QCOMPARE(keySpy.first()[2], KEY_A); + QCOMPARE(keySpy.first().last().value(), Test::WlKeyboard::key_state::key_state_pressed); + keySpy.clear(); + + Test::keyboardKeyReleased(KEY_A, timestamp++); + QVERIFY(keySpy.wait()); + QCOMPARE(keySpy.first()[2], KEY_A); + QCOMPARE(keySpy.first().last().value(), Test::WlKeyboard::key_state::key_state_released); + keySpy.clear(); +} + +void BounceKeysTest::testBounceKeyRepeat() +{ + const auto keyboard = m_connection->kwinSeat->getKeyboard(); + QVERIFY(keyboard); + QSignalSpy enteredSpy(keyboard.get(), &Test::WlKeyboard::enter); + QSignalSpy keySpy(keyboard.get(), &Test::WlKeyboard::key); + + Test::XdgToplevelWindow window{m_connection.get()}; + QVERIFY(window.show()); + QVERIFY(enteredSpy.wait()); + + quint32 timestamp = 0; + + // Press and repeat a key within the bounce interval, make sure the repeat goes through + Test::keyboardKeyPressed(KEY_B, timestamp); + QVERIFY(keySpy.wait()); + QCOMPARE(keySpy.first()[2], KEY_B); + QCOMPARE(keySpy.first().last().value(), Test::WlKeyboard::key_state::key_state_pressed); + keySpy.clear(); + + // wait for the repeat + QVERIFY(keySpy.wait()); + QCOMPARE(keySpy.first()[2], KEY_B); + QCOMPARE(keySpy.first().last().value(), Test::WlKeyboard::key_state::key_state_repeated); + keySpy.clear(); + + Test::keyboardKeyReleased(KEY_B, timestamp++); + QVERIFY(keySpy.wait()); + QCOMPARE(keySpy.first()[2], KEY_B); + QCOMPARE(keySpy.first().last().value(), Test::WlKeyboard::key_state::key_state_released); + keySpy.clear(); + + // Press and repeat it again within the bounce interval, verify that the repeat does *not* go through + timestamp += 100; + Test::keyboardKeyPressed(KEY_B, timestamp++); + QVERIFY(!keySpy.wait(100)); + keySpy.clear(); + + // the repeat should get blocked + QVERIFY(!keySpy.wait(100)); + keySpy.clear(); + + // Press and repeat it again after the bounce interval, verify that it does go through + timestamp += 1000; + Test::keyboardKeyPressed(KEY_B, timestamp); + QVERIFY(keySpy.wait()); + QCOMPARE(keySpy.first()[2], KEY_B); + QCOMPARE(keySpy.first().last().value(), Test::WlKeyboard::key_state::key_state_pressed); + keySpy.clear(); + + // wait for the repeat again + QVERIFY(keySpy.wait()); + QCOMPARE(keySpy.first()[2], KEY_B); + QCOMPARE(keySpy.first().last().value(), Test::WlKeyboard::key_state::key_state_repeated); + keySpy.clear(); + + Test::keyboardKeyReleased(KEY_B, timestamp++); + QVERIFY(keySpy.wait()); + QCOMPARE(keySpy.first()[2], KEY_B); + QCOMPARE(keySpy.first().last().value(), Test::WlKeyboard::key_state::key_state_released); + keySpy.clear(); +} +} + +WAYLANDTEST_MAIN(KWin::BounceKeysTest) +#include "bounce_keys_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/buttonrebind_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/buttonrebind_test.cpp new file mode 100644 index 0000000000..755484b053 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/buttonrebind_test.cpp @@ -0,0 +1,490 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2024 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "pluginmanager.h" +#include "pointer_input.h" +#include "tablet_input.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_buttonrebind-0"); +static const QString s_pluginName = QStringLiteral("buttonsrebind"); + +class TestButtonRebind : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void init(); + void cleanup(); + void initTestCase(); + + void testKey_data(); + void testKey(); + + void testMouse_data(); + void testMouse(); + + void testMouseKeyboardMod_data(); + void testMouseKeyboardMod(); + + void testDisabled(); + + // NOTE: Mouse buttons are not tested because those are used in the other tests + void testBindingTabletPad(); + void testBindingTabletTool(); + void testBindingTabletPadDialScroll(); + void testBindingTabletPadDialKey(); + void testBindingTabletRingKey(); + + void testMouseTabletCursorSync(); + +private: + quint32 timestamp = 1; +}; + +void TestButtonRebind::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); + QVERIFY(Test::waitForWaylandPointer()); +} + +void TestButtonRebind::cleanup() +{ + Test::destroyWaylandConnection(); + QVERIFY(QFile::remove(QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("kcminputrc")))); +} + +void TestButtonRebind::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void TestButtonRebind::testKey_data() +{ + QTest::addColumn("boundKeys"); + QTest::addColumn>("expectedKeys"); + + QTest::newRow("single key") << QKeySequence(Qt::Key_A) << QList{KEY_A}; + QTest::newRow("single modifier") << QKeySequence(Qt::Key_Control) << QList{KEY_LEFTCTRL}; + QTest::newRow("single modifier plus key") << QKeySequence(Qt::ControlModifier | Qt::Key_N) << QList{KEY_LEFTCTRL, KEY_N}; + QTest::newRow("multiple modifiers plus key") << QKeySequence(Qt::ControlModifier | Qt::MetaModifier | Qt::Key_Y) << QList{KEY_LEFTCTRL, KEY_LEFTMETA, KEY_Y}; + QTest::newRow("delete") << QKeySequence(Qt::Key_Delete) << QList{KEY_DELETE}; + QTest::newRow("keypad delete") << QKeySequence(Qt::KeypadModifier | Qt::Key_Delete) << QList{KEY_KPDOT}; + QTest::newRow("keypad enter") << QKeySequence(Qt::KeypadModifier | Qt::Key_Enter) << QList{KEY_KPENTER}; + QTest::newRow("exclamation mark") << QKeySequence(Qt::Key_Exclam) << QList{KEY_LEFTSHIFT, KEY_1}; +} + +void TestButtonRebind::testKey() +{ + KConfigGroup buttonGroup = KSharedConfig::openConfig(QStringLiteral("kcminputrc"))->group(QStringLiteral("ButtonRebinds")).group(QStringLiteral("Mouse")); + QFETCH(QKeySequence, boundKeys); + buttonGroup.writeEntry("ExtraButton7", QStringList{"Key", boundKeys.toString(QKeySequence::PortableText)}, KConfig::Notify); + buttonGroup.sync(); + + kwinApp()->pluginManager()->unloadPlugin(s_pluginName); + kwinApp()->pluginManager()->loadPlugin(s_pluginName); + + std::unique_ptr surface = Test::createSurface(); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy enteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); + QSignalSpy keyChangedSpy(keyboard.get(), &KWayland::Client::Keyboard::keyChanged); + QVERIFY(enteredSpy.wait()); + + // 0x119 is Qt::ExtraButton7 + Test::pointerButtonPressed(0x119, timestamp++); + + QVERIFY(keyChangedSpy.wait()); + QFETCH(QList, expectedKeys); + QCOMPARE(keyChangedSpy.count(), expectedKeys.count()); + for (int i = 0; i < keyChangedSpy.count(); i++) { + QCOMPARE(keyChangedSpy.at(i).at(0).value(), expectedKeys.at(i)); + QCOMPARE(keyChangedSpy.at(i).at(1).value(), KWayland::Client::Keyboard::KeyState::Pressed); + } + Test::pointerButtonReleased(0x119, timestamp++); +} + +void TestButtonRebind::testMouse_data() +{ + QTest::addColumn("mouseButton"); + + QTest::newRow("left button") << BTN_LEFT; + QTest::newRow("middle button") << BTN_MIDDLE; + QTest::newRow("right button") << BTN_RIGHT; +} + +void TestButtonRebind::testMouse() +{ + KConfigGroup buttonGroup = KSharedConfig::openConfig(QStringLiteral("kcminputrc"))->group(QStringLiteral("ButtonRebinds")).group(QStringLiteral("Mouse")); + QFETCH(int, mouseButton); + buttonGroup.writeEntry("ExtraButton7", QStringList{"MouseButton", QString::number(mouseButton)}, KConfig::Notify); + buttonGroup.sync(); + + kwinApp()->pluginManager()->unloadPlugin(s_pluginName); + kwinApp()->pluginManager()->loadPlugin(s_pluginName); + + std::unique_ptr surface = Test::createSurface(); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + std::unique_ptr pointer(Test::waylandSeat()->createPointer()); + QSignalSpy enteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); + QSignalSpy buttonChangedSpy(pointer.get(), &KWayland::Client::Pointer::buttonStateChanged); + + const RectF startGeometry = window->frameGeometry(); + input()->pointer()->warp(startGeometry.center()); + + QVERIFY(enteredSpy.wait()); + + // 0x119 is Qt::ExtraButton7 + Test::pointerButtonPressed(0x119, timestamp++); + + QVERIFY(buttonChangedSpy.wait()); + + QCOMPARE(buttonChangedSpy.count(), 1); + Q_ASSERT(buttonChangedSpy.at(0).at(2).value() == mouseButton); + QCOMPARE(buttonChangedSpy.at(0).at(2).value(), mouseButton); + + Test::pointerButtonReleased(0x119, timestamp++); +} + +void TestButtonRebind::testMouseKeyboardMod_data() +{ + QTest::addColumn("modifiers"); + QTest::addColumn>("expectedKeys"); + + QTest::newRow("single ctrl") << Qt::KeyboardModifiers(Qt::ControlModifier) << QList{KEY_LEFTCTRL}; + QTest::newRow("single alt") << Qt::KeyboardModifiers(Qt::AltModifier) << QList{KEY_LEFTALT}; + QTest::newRow("single shift") << Qt::KeyboardModifiers(Qt::ShiftModifier) << QList{KEY_LEFTSHIFT}; + + // We have to test Meta with another key, because it will most likely trigger KWin to do some window operation. + QTest::newRow("meta + alt") << Qt::KeyboardModifiers(Qt::MetaModifier | Qt::AltModifier) << QList{KEY_LEFTALT, KEY_LEFTMETA}; + + QTest::newRow("ctrl + alt + shift + meta") << Qt::KeyboardModifiers(Qt::ControlModifier | Qt::AltModifier | Qt::ShiftModifier | Qt::MetaModifier) << QList{KEY_LEFTSHIFT, KEY_LEFTCTRL, KEY_LEFTALT, KEY_LEFTMETA}; +} + +void TestButtonRebind::testMouseKeyboardMod() +{ + QFETCH(Qt::KeyboardModifiers, modifiers); + + KConfigGroup buttonGroup = KSharedConfig::openConfig(QStringLiteral("kcminputrc"))->group(QStringLiteral("ButtonRebinds")).group(QStringLiteral("TabletTool")).group(QStringLiteral("Virtual Tablet Tool 1")); + buttonGroup.writeEntry(QString::number(BTN_STYLUS), QStringList{"MouseButton", QString::number(BTN_LEFT), QString::number(modifiers.toInt())}, KConfig::Notify); + buttonGroup.sync(); + + kwinApp()->pluginManager()->unloadPlugin(s_pluginName); + kwinApp()->pluginManager()->loadPlugin(s_pluginName); + + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy keyboardEnteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); + QSignalSpy keyboardKeyChangedSpy(keyboard.get(), &KWayland::Client::Keyboard::keyChanged); + + std::unique_ptr surface = Test::createSurface(); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(keyboardEnteredSpy.wait()); + + std::unique_ptr pointer(Test::waylandSeat()->createPointer()); + QSignalSpy pointerEnteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); + QSignalSpy pointerButtonChangedSpy(pointer.get(), &KWayland::Client::Pointer::buttonStateChanged); + + const RectF startGeometry = window->frameGeometry(); + + input()->pointer()->warp(startGeometry.center()); + QVERIFY(pointerEnteredSpy.wait()); + + // Send the tablet button event so it can be processed by the filter + Test::tabletToolButtonPressed(BTN_STYLUS, timestamp++); + + // The keyboard modifier is sent first + QVERIFY(keyboardKeyChangedSpy.wait()); + + QFETCH(QList, expectedKeys); + QCOMPARE(keyboardKeyChangedSpy.count(), expectedKeys.count()); + for (int i = 0; i < keyboardKeyChangedSpy.count(); i++) { + QCOMPARE(keyboardKeyChangedSpy.at(i).at(0).value(), expectedKeys.at(i)); + QCOMPARE(keyboardKeyChangedSpy.at(i).at(1).value(), KWayland::Client::Keyboard::KeyState::Pressed); + } + + // Then the mouse button is + QCOMPARE(pointerButtonChangedSpy.count(), 1); + QCOMPARE(pointerButtonChangedSpy.at(0).at(2).value(), BTN_LEFT); + + Test::tabletToolButtonReleased(BTN_STYLUS, timestamp++); +} + +void TestButtonRebind::testDisabled() +{ + KConfigGroup buttonGroup = KSharedConfig::openConfig(QStringLiteral("kcminputrc"))->group(QStringLiteral("ButtonRebinds")).group(QStringLiteral("Mouse")); + buttonGroup.writeEntry("ExtraButton7", QStringList{"Disabled"}, KConfig::Notify); + buttonGroup.sync(); + + kwinApp()->pluginManager()->unloadPlugin(s_pluginName); + kwinApp()->pluginManager()->loadPlugin(s_pluginName); + + std::unique_ptr surface = Test::createSurface(); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + std::unique_ptr pointer(Test::waylandSeat()->createPointer()); + QSignalSpy enteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); + QSignalSpy buttonChangedSpy(pointer.get(), &KWayland::Client::Pointer::buttonStateChanged); + + const RectF startGeometry = window->frameGeometry(); + input()->pointer()->warp(startGeometry.center()); + + QVERIFY(enteredSpy.wait()); + + // 0x119 is Qt::ExtraButton7 + Test::pointerButtonPressed(0x119, timestamp++); + + // Qt::ExtraButton7 should not have been emitted if this button is disabled + QVERIFY(!buttonChangedSpy.wait(std::chrono::milliseconds(100))); + QCOMPARE(buttonChangedSpy.count(), 0); + + Test::pointerButtonReleased(0x119, timestamp++); +} + +void TestButtonRebind::testBindingTabletPad() +{ + const QKeySequence sequence(Qt::Key_A); + + KConfigGroup buttonGroup = KSharedConfig::openConfig(QStringLiteral("kcminputrc"))->group(QStringLiteral("ButtonRebinds")).group(QStringLiteral("Tablet")).group(QStringLiteral("Virtual Tablet Pad 1")); + buttonGroup.writeEntry("1", QStringList{"Key", sequence.toString(QKeySequence::PortableText)}, KConfig::Notify); + buttonGroup.sync(); + + kwinApp()->pluginManager()->unloadPlugin(s_pluginName); + kwinApp()->pluginManager()->loadPlugin(s_pluginName); + + std::unique_ptr surface = Test::createSurface(); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy enteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); + QSignalSpy keyChangedSpy(keyboard.get(), &KWayland::Client::Keyboard::keyChanged); + QVERIFY(enteredSpy.wait()); + + Test::tabletPadButtonPressed(1, timestamp++); + + QVERIFY(keyChangedSpy.wait()); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.at(0).at(0), KEY_A); + + Test::tabletPadButtonReleased(1, timestamp++); +} + +void TestButtonRebind::testBindingTabletPadDialScroll() +{ + KConfigGroup buttonGroup = KSharedConfig::openConfig(QStringLiteral("kcminputrc"))->group(QStringLiteral("ButtonRebinds")).group(QStringLiteral("TabletDial")).group(QStringLiteral("Virtual Tablet Pad 1")); + buttonGroup.writeEntry("0", QStringList{"Scroll"}, KConfig::Notify); + buttonGroup.sync(); + + kwinApp()->pluginManager()->unloadPlugin(s_pluginName); + kwinApp()->pluginManager()->loadPlugin(s_pluginName); + + std::unique_ptr surface = Test::createSurface(); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + std::unique_ptr pointer(Test::waylandSeat()->createPointer()); + QSignalSpy enteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); + QSignalSpy axisChangedSpy(pointer.get(), &KWayland::Client::Pointer::axisChanged); + QVERIFY(enteredSpy.wait()); + + // twisting the dial "up" + Test::tabletPadDialEvent(120.0, 0, timestamp++); + + using KWayland::Client::Keyboard; + + QVERIFY(axisChangedSpy.wait()); + QCOMPARE(axisChangedSpy.count(), 1); + QCOMPARE(axisChangedSpy.at(0).at(2).value(), 15); + + // twisting the dial "down" + axisChangedSpy.clear(); + Test::tabletPadDialEvent(-120.0, 0, timestamp++); + + QVERIFY(axisChangedSpy.wait()); + QCOMPARE(axisChangedSpy.count(), 1); + QCOMPARE(axisChangedSpy.at(0).at(2).value(), -15); +} + +void TestButtonRebind::testBindingTabletPadDialKey() +{ + const QKeySequence upSequence(Qt::Key_BracketLeft); + const QKeySequence downSequence(Qt::Key_BracketRight); + + KConfigGroup buttonGroup = KSharedConfig::openConfig(QStringLiteral("kcminputrc"))->group(QStringLiteral("ButtonRebinds")).group(QStringLiteral("TabletDial")).group(QStringLiteral("Virtual Tablet Pad 1")); + buttonGroup.writeEntry("0", QStringList{"AxisKey", upSequence.toString(QKeySequence::PortableText), downSequence.toString(QKeySequence::PortableText), QStringLiteral("120")}, KConfig::Notify); + buttonGroup.sync(); + + kwinApp()->pluginManager()->unloadPlugin(s_pluginName); + kwinApp()->pluginManager()->loadPlugin(s_pluginName); + + std::unique_ptr surface = Test::createSurface(); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy enteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); + QSignalSpy keyChangedSpy(keyboard.get(), &KWayland::Client::Keyboard::keyChanged); + QVERIFY(enteredSpy.wait()); + + // twisting the dial "up" + Test::tabletPadDialEvent(120.0, 0, timestamp++); + + QVERIFY(keyChangedSpy.wait()); + QCOMPARE(keyChangedSpy.count(), 2); // two events are reported because it emulates a press then release + QCOMPARE(keyChangedSpy.at(0).at(0), KEY_LEFTBRACE); + + // twisting the dial "down" + keyChangedSpy.clear(); + Test::tabletPadDialEvent(-120.0, 0, timestamp++); + + QVERIFY(keyChangedSpy.wait()); + QCOMPARE(keyChangedSpy.count(), 2); + QCOMPARE(keyChangedSpy.at(0).at(0), KEY_RIGHTBRACE); +} + +void TestButtonRebind::testBindingTabletRingKey() +{ + const QKeySequence upSequence(Qt::Key_BracketLeft); + const QKeySequence downSequence(Qt::Key_BracketRight); + + KConfigGroup buttonGroup = KSharedConfig::openConfig(QStringLiteral("kcminputrc"))->group(QStringLiteral("ButtonRebinds")).group(QStringLiteral("TabletRing")).group(QStringLiteral("Virtual Tablet Pad 1")).group(QStringLiteral("0")); + buttonGroup.writeEntry("0", QStringList{"AxisKey", upSequence.toString(QKeySequence::PortableText), downSequence.toString(QKeySequence::PortableText), QStringLiteral("600")}, KConfig::Notify); + buttonGroup.sync(); + + kwinApp()->pluginManager()->unloadPlugin(s_pluginName); + kwinApp()->pluginManager()->loadPlugin(s_pluginName); + + std::unique_ptr surface = Test::createSurface(); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy enteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); + QSignalSpy keyChangedSpy(keyboard.get(), &KWayland::Client::Keyboard::keyChanged); + QVERIFY(enteredSpy.wait()); + + // touching the dial "clockwise" + for (int i = 359; i > 353; i--) { + Test::tabletPadRingEvent(i, 0, 0, 0, timestamp++); + } + Test::tabletPadRingEvent(-1, 0, 0, 0, timestamp++); // finger released + + QVERIFY(keyChangedSpy.wait()); + QCOMPARE(keyChangedSpy.count(), 2); // two events are reported because it emulates a press then release + QCOMPARE(keyChangedSpy.at(0).at(0), KEY_LEFTBRACE); + + // touching the dial "counter-clockwise" + keyChangedSpy.clear(); + for (int i = 1; i < 7; i++) { + Test::tabletPadRingEvent(i, 0, 0, 0, timestamp++); + } + Test::tabletPadRingEvent(-1, 0, 0, 0, timestamp++); // finger released + + QVERIFY(keyChangedSpy.wait()); + QCOMPARE(keyChangedSpy.count(), 2); + QCOMPARE(keyChangedSpy.at(0).at(0), KEY_RIGHTBRACE); +} + +void TestButtonRebind::testBindingTabletTool() +{ + const QKeySequence sequence(Qt::Key_A); + + KConfigGroup buttonGroup = KSharedConfig::openConfig(QStringLiteral("kcminputrc"))->group(QStringLiteral("ButtonRebinds")).group(QStringLiteral("TabletTool")).group(QStringLiteral("Virtual Tablet Tool 1")); + buttonGroup.writeEntry(QString::number(BTN_STYLUS), QStringList{"Key", sequence.toString(QKeySequence::PortableText)}, KConfig::Notify); + buttonGroup.sync(); + + kwinApp()->pluginManager()->unloadPlugin(s_pluginName); + kwinApp()->pluginManager()->loadPlugin(s_pluginName); + + std::unique_ptr surface = Test::createSurface(); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy enteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); + QSignalSpy keyChangedSpy(keyboard.get(), &KWayland::Client::Keyboard::keyChanged); + QVERIFY(enteredSpy.wait()); + + const RectF startGeometry = window->frameGeometry(); + Test::tabletToolProximityEvent(startGeometry.center(), 0, 0, 0, 0, false, 0, timestamp++); + + Test::tabletToolButtonPressed(BTN_STYLUS, timestamp++); + + QVERIFY(keyChangedSpy.wait()); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.at(0).at(0), KEY_A); + + Test::tabletToolButtonReleased(BTN_STYLUS, timestamp++); +} + +void TestButtonRebind::testMouseTabletCursorSync() +{ + KConfigGroup buttonGroup = KSharedConfig::openConfig(QStringLiteral("kcminputrc"))->group(QStringLiteral("ButtonRebinds")).group(QStringLiteral("TabletTool")).group(QStringLiteral("Virtual Tablet Tool 1")); + buttonGroup.writeEntry(QString::number(BTN_STYLUS), QStringList{"MouseButton", QString::number(BTN_LEFT)}, KConfig::Notify); + buttonGroup.sync(); + + kwinApp()->pluginManager()->unloadPlugin(s_pluginName); + kwinApp()->pluginManager()->loadPlugin(s_pluginName); + + std::unique_ptr surface = Test::createSurface(); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + std::unique_ptr pointer(Test::waylandSeat()->createPointer()); + QSignalSpy enteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); + QSignalSpy buttonChangedSpy(pointer.get(), &KWayland::Client::Pointer::buttonStateChanged); + + const RectF startGeometry = window->frameGeometry(); + + // Move the mouse cursor to (25, 25) + input()->pointer()->warp(startGeometry.topLeft() + QPointF{25.f, 25.5f}); + QVERIFY(enteredSpy.wait()); + + // Move the tablet cursor to (10,10) + Test::tabletToolProximityEvent(startGeometry.topLeft() + QPointF{10.f, 10.f}, 0, 0, 0, 0, false, 0, timestamp++); + + // Verify they are not starting in the same place + QVERIFY(input()->pointer()->pos() != input()->tablet()->position()); + + // Send the tablet button event so it can be processed by the filter + Test::tabletToolButtonPressed(BTN_STYLUS, timestamp++); + + QVERIFY(buttonChangedSpy.wait()); + QCOMPARE(buttonChangedSpy.count(), 1); + QCOMPARE(buttonChangedSpy.at(0).at(2).value(), BTN_LEFT); + + Test::tabletToolButtonReleased(BTN_STYLUS, timestamp++); + + // Verify that by using the mouse button binding, the mouse cursor was moved to the tablet cursor position + QVERIFY(input()->pointer()->pos() == input()->tablet()->position()); +} + +WAYLANDTEST_MAIN(TestButtonRebind) +#include "buttonrebind_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/data/anim-data-delete-effect/effect.js b/local/recipes/kde/kwin/source/autotests/integration/data/anim-data-delete-effect/effect.js new file mode 100644 index 0000000000..0a4170ba51 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/data/anim-data-delete-effect/effect.js @@ -0,0 +1,14 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +effects.windowAdded.connect(function(w) { + w.fadeAnimation = effect.animate(w, Effect.Opacity, 100, 1.0, 0.0); +}); +effect.animationEnded.connect(function(w) { + cancel(w.fadeAnimation); +}); diff --git a/local/recipes/kde/kwin/source/autotests/integration/data/example.desktop b/local/recipes/kde/kwin/source/autotests/integration/data/example.desktop new file mode 100644 index 0000000000..739f5d3087 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/data/example.desktop @@ -0,0 +1,3 @@ +[Desktop Entry] +Name=An example application +Icon=kwin diff --git a/local/recipes/kde/kwin/source/autotests/integration/data/rules/force-maximize b/local/recipes/kde/kwin/source/autotests/integration/data/rules/force-maximize new file mode 100644 index 0000000000..4e81b14b91 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/data/rules/force-maximize @@ -0,0 +1,14 @@ +[$Version] +update_info=kwinrules.upd:replace-placement-string-to-enum,kwinrules.upd:use-virtual-desktop-ids + +[General] +count=1 +rules=ad6fd81c-6dea-46e6-a652-dbffe0aeb7d7 + +[ad6fd81c-6dea-46e6-a652-dbffe0aeb7d7] +Description=force maximize everything +maximizehoriz=true +maximizehorizrule=2 +maximizevert=true +maximizevertrule=2 +types=1 diff --git a/local/recipes/kde/kwin/source/autotests/integration/data/rules/maximize-vert-apply-initial b/local/recipes/kde/kwin/source/autotests/integration/data/rules/maximize-vert-apply-initial new file mode 100644 index 0000000000..9d04c83839 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/data/rules/maximize-vert-apply-initial @@ -0,0 +1,18 @@ +[94b936eb-74e1-47a2-8a50-7d25fae2cfc7] +Description=Window settings for kpat +clientmachine=localhost +clientmachinematch=0 +maximizevert=true +maximizevertrule=3 +title=KPatience +titlematch=0 +types=1 +windowrole=mainwindow +windowrolematch=1 +wmclass=kpat +wmclasscomplete=false +wmclassmatch=1 + +[General] +count=1 +rules=94b936eb-74e1-47a2-8a50-7d25fae2cfc7 diff --git a/local/recipes/kde/kwin/source/autotests/integration/dbus_interface_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/dbus_interface_test.cpp new file mode 100644 index 0000000000..71f441dd3e --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/dbus_interface_test.cpp @@ -0,0 +1,371 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "config-kwin.h" + +#include "kwin_wayland_test.h" + +#include "rules.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include + +#include +#include +#include +#include +#include + +#if KWIN_BUILD_X11 +#include "atoms.h" +#include "x11window.h" + +#include +#include +#endif + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_dbus_interface-0"); + +const QString s_destination{QStringLiteral("org.kde.KWin")}; +const QString s_path{QStringLiteral("/KWin")}; +const QString s_interface{QStringLiteral("org.kde.KWin")}; + +class TestDbusInterface : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testGetWindowInfoInvalidUuid(); + void testGetWindowInfoXdgShellClient(); + void testGetWindowInfoX11Client(); +}; + +void TestDbusInterface::initTestCase() +{ + qRegisterMetaType(); + + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + VirtualDesktopManager::self()->setCount(4); +} + +void TestDbusInterface::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void TestDbusInterface::cleanup() +{ + Test::destroyWaylandConnection(); +} + +namespace +{ +QDBusPendingCall getWindowInfo(const QUuid &uuid) +{ + auto msg = QDBusMessage::createMethodCall(s_destination, s_path, s_interface, QStringLiteral("getWindowInfo")); + msg.setArguments({uuid.toString()}); + return QDBusConnection::sessionBus().asyncCall(msg); +} +} + +void TestDbusInterface::testGetWindowInfoInvalidUuid() +{ + QDBusPendingReply reply{getWindowInfo(QUuid::createUuid())}; + reply.waitForFinished(); + QVERIFY(reply.isValid()); + QVERIFY(!reply.isError()); + const auto windowData = reply.value(); + QVERIFY(windowData.empty()); +} + +void TestDbusInterface::testGetWindowInfoXdgShellClient() +{ + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + shellSurface->set_app_id(QStringLiteral("org.kde.foo")); + shellSurface->set_title(QStringLiteral("Test window")); + + // now let's render + Test::render(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(windowAddedSpy.isEmpty()); + QVERIFY(windowAddedSpy.wait()); + auto window = windowAddedSpy.first().first().value(); + QVERIFY(window); + + const QVariantMap expectedData = { + {QStringLiteral("type"), int(NET::Normal)}, + {QStringLiteral("x"), window->x()}, + {QStringLiteral("y"), window->y()}, + {QStringLiteral("width"), window->width()}, + {QStringLiteral("height"), window->height()}, + {QStringLiteral("desktops"), window->desktopIds()}, + {QStringLiteral("minimized"), false}, + {QStringLiteral("fullscreen"), false}, + {QStringLiteral("keepAbove"), false}, + {QStringLiteral("keepBelow"), false}, + {QStringLiteral("skipTaskbar"), false}, + {QStringLiteral("skipPager"), false}, + {QStringLiteral("skipSwitcher"), false}, + {QStringLiteral("maximizeHorizontal"), false}, + {QStringLiteral("maximizeVertical"), false}, + {QStringLiteral("noBorder"), false}, + {QStringLiteral("clientMachine"), QString()}, + {QStringLiteral("localhost"), true}, + {QStringLiteral("role"), QString()}, + {QStringLiteral("resourceName"), QStringLiteral("testDbusInterface")}, + {QStringLiteral("resourceClass"), QStringLiteral("org.kde.foo")}, + {QStringLiteral("desktopFile"), QStringLiteral("org.kde.foo")}, + {QStringLiteral("caption"), QStringLiteral("Test window")}, +#if KWIN_BUILD_ACTIVITIES + {QStringLiteral("activities"), QStringList()}, +#endif + {QStringLiteral("layer"), NormalLayer}, + }; + + // let's get the window info + QDBusPendingReply reply{getWindowInfo(window->internalId())}; + reply.waitForFinished(); + QVERIFY(reply.isValid()); + QVERIFY(!reply.isError()); + auto windowData = reply.value(); + windowData.remove(QStringLiteral("uuid")); + QCOMPARE(windowData, expectedData); + + auto verifyProperty = [window](const QString &name) { + QDBusPendingReply reply{getWindowInfo(window->internalId())}; + reply.waitForFinished(); + return reply.value().value(name).toBool(); + }; + + QVERIFY(!window->isMinimized()); + window->setMinimized(true); + QVERIFY(window->isMinimized()); + QCOMPARE(verifyProperty(QStringLiteral("minimized")), true); + + QVERIFY(!window->keepAbove()); + window->setKeepAbove(true); + QVERIFY(window->keepAbove()); + QCOMPARE(verifyProperty(QStringLiteral("keepAbove")), true); + + QVERIFY(!window->keepBelow()); + window->setKeepBelow(true); + QVERIFY(window->keepBelow()); + QCOMPARE(verifyProperty(QStringLiteral("keepBelow")), true); + + QVERIFY(!window->skipTaskbar()); + window->setSkipTaskbar(true); + QVERIFY(window->skipTaskbar()); + QCOMPARE(verifyProperty(QStringLiteral("skipTaskbar")), true); + + QVERIFY(!window->skipPager()); + window->setSkipPager(true); + QVERIFY(window->skipPager()); + QCOMPARE(verifyProperty(QStringLiteral("skipPager")), true); + + QVERIFY(!window->skipSwitcher()); + window->setSkipSwitcher(true); + QVERIFY(window->skipSwitcher()); + QCOMPARE(verifyProperty(QStringLiteral("skipSwitcher")), true); + + // not testing fullscreen, maximizeHorizontal, maximizeVertical and noBorder as those require window geometry changes + + const QList desktops = VirtualDesktopManager::self()->desktops(); + QCOMPARE(window->desktops(), QList{desktops[0]}); + workspace()->sendWindowToDesktops(window, {desktops[1]}, false); + QCOMPARE(window->desktops(), QList{desktops[1]}); + reply = getWindowInfo(window->internalId()); + reply.waitForFinished(); + QCOMPARE(reply.value().value(QStringLiteral("desktops")).toStringList(), window->desktopIds()); + + window->move(QPoint(10, 20)); + reply = getWindowInfo(window->internalId()); + reply.waitForFinished(); + QCOMPARE(reply.value().value(QStringLiteral("x")).toInt(), window->x()); + QCOMPARE(reply.value().value(QStringLiteral("y")).toInt(), window->y()); + // not testing width, height as that would require window geometry change + + // finally close window + const auto id = window->internalId(); + QSignalSpy windowClosedSpy(window, &Window::closed); + shellSurface.reset(); + surface.reset(); + QVERIFY(windowClosedSpy.wait()); + QCOMPARE(windowClosedSpy.count(), 1); + + reply = getWindowInfo(id); + reply.waitForFinished(); + QVERIFY(reply.value().empty()); +} + +void TestDbusInterface::testGetWindowInfoX11Client() +{ +#if KWIN_BUILD_X11 + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + const Rect windowGeometry(0, 0, 600, 400); + xcb_window_t windowId = xcb_generate_id(c.get()); + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + xcb_icccm_set_wm_class(c.get(), windowId, 7, "foo\0bar"); + NETWinInfo winInfo(c.get(), windowId, rootWindow(), NET::Properties(), NET::Properties2()); + winInfo.setName("Some caption"); + winInfo.setDesktopFileName("org.kde.foo"); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + // we should get a window for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.first().first().value(); + QVERIFY(window); + QCOMPARE(window->window(), windowId); + QCOMPARE(window->clientSize(), windowGeometry.size()); + + const QVariantMap expectedData = { + {QStringLiteral("type"), NET::Normal}, + {QStringLiteral("x"), window->x()}, + {QStringLiteral("y"), window->y()}, + {QStringLiteral("width"), window->width()}, + {QStringLiteral("height"), window->height()}, + {QStringLiteral("desktops"), window->desktopIds()}, + {QStringLiteral("minimized"), false}, + {QStringLiteral("fullscreen"), false}, + {QStringLiteral("keepAbove"), false}, + {QStringLiteral("keepBelow"), false}, + {QStringLiteral("skipTaskbar"), false}, + {QStringLiteral("skipPager"), false}, + {QStringLiteral("skipSwitcher"), false}, + {QStringLiteral("maximizeHorizontal"), false}, + {QStringLiteral("maximizeVertical"), false}, + {QStringLiteral("noBorder"), false}, + {QStringLiteral("role"), QString()}, + {QStringLiteral("resourceName"), QStringLiteral("foo")}, + {QStringLiteral("resourceClass"), QStringLiteral("bar")}, + {QStringLiteral("desktopFile"), QStringLiteral("org.kde.foo")}, + {QStringLiteral("caption"), QStringLiteral("Some caption")}, +#if KWIN_BUILD_ACTIVITIES + {QStringLiteral("activities"), QStringList()}, +#endif + {QStringLiteral("layer"), NormalLayer}, + }; + + // let's get the window info + QDBusPendingReply reply{getWindowInfo(window->internalId())}; + reply.waitForFinished(); + QVERIFY(reply.isValid()); + QVERIFY(!reply.isError()); + auto windowData = reply.value(); + // not testing clientmachine as that is system dependent due to that also not testing localhost + windowData.remove(QStringLiteral("clientMachine")); + windowData.remove(QStringLiteral("localhost")); + windowData.remove(QStringLiteral("uuid")); + QCOMPARE(windowData, expectedData); + + auto verifyProperty = [window](const QString &name) { + QDBusPendingReply reply{getWindowInfo(window->internalId())}; + reply.waitForFinished(); + return reply.value().value(name).toBool(); + }; + + QVERIFY(!window->isMinimized()); + window->setMinimized(true); + QVERIFY(window->isMinimized()); + QCOMPARE(verifyProperty(QStringLiteral("minimized")), true); + + QVERIFY(!window->keepAbove()); + window->setKeepAbove(true); + QVERIFY(window->keepAbove()); + QCOMPARE(verifyProperty(QStringLiteral("keepAbove")), true); + + QVERIFY(!window->keepBelow()); + window->setKeepBelow(true); + QVERIFY(window->keepBelow()); + QCOMPARE(verifyProperty(QStringLiteral("keepBelow")), true); + + QVERIFY(!window->skipTaskbar()); + window->setSkipTaskbar(true); + QVERIFY(window->skipTaskbar()); + QCOMPARE(verifyProperty(QStringLiteral("skipTaskbar")), true); + + QVERIFY(!window->skipPager()); + window->setSkipPager(true); + QVERIFY(window->skipPager()); + QCOMPARE(verifyProperty(QStringLiteral("skipPager")), true); + + QVERIFY(!window->skipSwitcher()); + window->setSkipSwitcher(true); + QVERIFY(window->skipSwitcher()); + QCOMPARE(verifyProperty(QStringLiteral("skipSwitcher")), true); + + QVERIFY(!window->noBorder()); + window->setNoBorder(true); + QVERIFY(window->noBorder()); + QCOMPARE(verifyProperty(QStringLiteral("noBorder")), true); + window->setNoBorder(false); + QVERIFY(!window->noBorder()); + + QVERIFY(!window->isFullScreen()); + window->setFullScreen(true); + QVERIFY(window->isFullScreen()); + QVERIFY(window->clientSize() != windowGeometry.size()); + QCOMPARE(verifyProperty(QStringLiteral("fullscreen")), true); + reply = getWindowInfo(window->internalId()); + reply.waitForFinished(); + QCOMPARE(reply.value().value(QStringLiteral("width")).toInt(), window->width()); + QCOMPARE(reply.value().value(QStringLiteral("height")).toInt(), window->height()); + window->setFullScreen(false); + QVERIFY(!window->isFullScreen()); + + // maximize + window->setMaximize(true, false); + QCOMPARE(verifyProperty(QStringLiteral("maximizeVertical")), true); + QCOMPARE(verifyProperty(QStringLiteral("maximizeHorizontal")), false); + window->setMaximize(false, true); + QCOMPARE(verifyProperty(QStringLiteral("maximizeVertical")), false); + QCOMPARE(verifyProperty(QStringLiteral("maximizeHorizontal")), true); + + const auto id = window->internalId(); + // destroy the window + xcb_destroy_window(c.get(), windowId); + xcb_flush(c.get()); + + QSignalSpy windowClosedSpy(window, &X11Window::closed); + QVERIFY(windowClosedSpy.wait()); + c.reset(); + + reply = getWindowInfo(id); + reply.waitForFinished(); + QVERIFY(reply.value().empty()); +#endif +} + +WAYLANDTEST_MAIN(TestDbusInterface) +#include "dbus_interface_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/debug_console_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/debug_console_test.cpp new file mode 100644 index 0000000000..6961248a83 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/debug_console_test.cpp @@ -0,0 +1,502 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "debug_console.h" +#include "internalwindow.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#if KWIN_BUILD_X11 +#include "utils/xcbutils.h" +#endif + +#include +#include +#include +#include + +#include +#include +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_debug_console-0"); + +class DebugConsoleTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void cleanup(); + void topLevelTest_data(); + void topLevelTest(); +#if KWIN_BUILD_X11 + void testX11Window(); + void testX11Unmanaged(); +#endif + void testWaylandClient(); + void testInternalWindow(); + void testClosingDebugConsole(); +}; + +void DebugConsoleTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); +} + +void DebugConsoleTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void DebugConsoleTest::topLevelTest_data() +{ + QTest::addColumn("row"); + QTest::addColumn("column"); + QTest::addColumn("expectedValid"); + + // this tests various combinations of row/column on the top level whether they are valid + // valid are rows 0-4 with column 0, everything else is invalid + QTest::newRow("0/0") << 0 << 0 << true; + QTest::newRow("0/1") << 0 << 1 << false; + QTest::newRow("0/3") << 0 << 3 << false; + QTest::newRow("1/0") << 1 << 0 << true; + QTest::newRow("1/1") << 1 << 1 << false; + QTest::newRow("1/3") << 1 << 3 << false; + QTest::newRow("2/0") << 2 << 0 << true; + QTest::newRow("3/0") << 3 << 0 << true; + QTest::newRow("4/0") << 4 << 0 << false; + QTest::newRow("100/0") << 4 << 0 << false; +} + +void DebugConsoleTest::topLevelTest() +{ + DebugConsoleModel model; + QCOMPARE(model.rowCount(QModelIndex()), 4); + QCOMPARE(model.columnCount(QModelIndex()), 2); + QFETCH(int, row); + QFETCH(int, column); + const QModelIndex index = model.index(row, column, QModelIndex()); + QTEST(index.isValid(), "expectedValid"); + if (index.isValid()) { + QVERIFY(!model.parent(index).isValid()); + QVERIFY(model.data(index, Qt::DisplayRole).isValid()); + QCOMPARE(model.data(index, Qt::DisplayRole).userType(), int(QMetaType::QString)); + for (int i = Qt::DecorationRole; i <= Qt::UserRole; i++) { + QVERIFY(!model.data(index, i).isValid()); + } + } +} + +#if KWIN_BUILD_X11 +void DebugConsoleTest::testX11Window() +{ + DebugConsoleModel model; + QModelIndex x11TopLevelIndex = model.index(0, 0, QModelIndex()); + QVERIFY(x11TopLevelIndex.isValid()); + // we don't have any windows yet + QCOMPARE(model.rowCount(x11TopLevelIndex), 0); + QVERIFY(!model.hasChildren(x11TopLevelIndex)); + // child index must be invalid + QVERIFY(!model.index(0, 0, x11TopLevelIndex).isValid()); + QVERIFY(!model.index(0, 1, x11TopLevelIndex).isValid()); + QVERIFY(!model.index(0, 2, x11TopLevelIndex).isValid()); + QVERIFY(!model.index(1, 0, x11TopLevelIndex).isValid()); + + // start glxgears, to get a window, which should be added to the model + QSignalSpy rowsInsertedSpy(&model, &QAbstractItemModel::rowsInserted); + + QProcess glxgears; + glxgears.setProgram(QStringLiteral("glxgears")); + glxgears.start(); + QVERIFY(glxgears.waitForStarted()); + + QVERIFY(rowsInsertedSpy.wait()); + QCOMPARE(rowsInsertedSpy.count(), 1); + QVERIFY(model.hasChildren(x11TopLevelIndex)); + QCOMPARE(model.rowCount(x11TopLevelIndex), 1); + QCOMPARE(rowsInsertedSpy.first().at(0).value(), x11TopLevelIndex); + QCOMPARE(rowsInsertedSpy.first().at(1).value(), 0); + QCOMPARE(rowsInsertedSpy.first().at(2).value(), 0); + + QModelIndex windowIndex = model.index(0, 0, x11TopLevelIndex); + QVERIFY(windowIndex.isValid()); + QCOMPARE(model.parent(windowIndex), x11TopLevelIndex); + QVERIFY(model.hasChildren(windowIndex)); + QVERIFY(model.rowCount(windowIndex) != 0); + QCOMPARE(model.columnCount(windowIndex), 2); + // other indexes are still invalid + QVERIFY(!model.index(0, 1, x11TopLevelIndex).isValid()); + QVERIFY(!model.index(0, 2, x11TopLevelIndex).isValid()); + QVERIFY(!model.index(1, 0, x11TopLevelIndex).isValid()); + + // the windowIndex has children and those are properties + for (int i = 0; i < model.rowCount(windowIndex); i++) { + const QModelIndex propNameIndex = model.index(i, 0, windowIndex); + QVERIFY(propNameIndex.isValid()); + QCOMPARE(model.parent(propNameIndex), windowIndex); + QVERIFY(!model.hasChildren(propNameIndex)); + QVERIFY(!model.index(0, 0, propNameIndex).isValid()); + QVERIFY(model.data(propNameIndex, Qt::DisplayRole).isValid()); + QCOMPARE(model.data(propNameIndex, Qt::DisplayRole).userType(), int(QMetaType::QString)); + + // and the value + const QModelIndex propValueIndex = model.index(i, 1, windowIndex); + QVERIFY(propValueIndex.isValid()); + QCOMPARE(model.parent(propValueIndex), windowIndex); + QVERIFY(!model.index(0, 0, propValueIndex).isValid()); + QVERIFY(!model.hasChildren(propValueIndex)); + // TODO: how to test whether the values actually work? + + // and on third column we should not get an index any more + QVERIFY(!model.index(i, 2, windowIndex).isValid()); + } + // row after count should be invalid + QVERIFY(!model.index(model.rowCount(windowIndex), 0, windowIndex).isValid()); + + // creating a second model should be initialized directly with the X11 child + DebugConsoleModel model2; + QVERIFY(model2.hasChildren(model2.index(0, 0, QModelIndex()))); + + // now close the window again, it should be removed from the model + QSignalSpy rowsRemovedSpy(&model, &QAbstractItemModel::rowsRemoved); + + glxgears.terminate(); + QVERIFY(glxgears.waitForFinished()); + + QVERIFY(rowsRemovedSpy.wait()); + QCOMPARE(rowsRemovedSpy.count(), 1); + QCOMPARE(rowsRemovedSpy.first().first().value(), x11TopLevelIndex); + QCOMPARE(rowsRemovedSpy.first().at(1).value(), 0); + QCOMPARE(rowsRemovedSpy.first().at(2).value(), 0); + + // the child should be gone again + QVERIFY(!model.hasChildren(x11TopLevelIndex)); + QVERIFY(!model2.hasChildren(model2.index(0, 0, QModelIndex()))); +} + +void DebugConsoleTest::testX11Unmanaged() +{ + DebugConsoleModel model; + QModelIndex unmanagedTopLevelIndex = model.index(1, 0, QModelIndex()); + QVERIFY(unmanagedTopLevelIndex.isValid()); + // we don't have any windows yet + QCOMPARE(model.rowCount(unmanagedTopLevelIndex), 0); + QVERIFY(!model.hasChildren(unmanagedTopLevelIndex)); + // child index must be invalid + QVERIFY(!model.index(0, 0, unmanagedTopLevelIndex).isValid()); + QVERIFY(!model.index(0, 1, unmanagedTopLevelIndex).isValid()); + QVERIFY(!model.index(0, 2, unmanagedTopLevelIndex).isValid()); + QVERIFY(!model.index(1, 0, unmanagedTopLevelIndex).isValid()); + + // we need to create an unmanaged window + QSignalSpy rowsInsertedSpy(&model, &QAbstractItemModel::rowsInserted); + + // let's create an override redirect window + const uint32_t values[] = {true}; + Xcb::Window window(Rect(0, 0, 10, 10), XCB_CW_OVERRIDE_REDIRECT, values); + window.map(); + + QVERIFY(rowsInsertedSpy.wait()); + QCOMPARE(rowsInsertedSpy.count(), 1); + QVERIFY(model.hasChildren(unmanagedTopLevelIndex)); + QCOMPARE(model.rowCount(unmanagedTopLevelIndex), 1); + QCOMPARE(rowsInsertedSpy.first().at(0).value(), unmanagedTopLevelIndex); + QCOMPARE(rowsInsertedSpy.first().at(1).value(), 0); + QCOMPARE(rowsInsertedSpy.first().at(2).value(), 0); + + QModelIndex windowIndex = model.index(0, 0, unmanagedTopLevelIndex); + QVERIFY(windowIndex.isValid()); + QCOMPARE(model.parent(windowIndex), unmanagedTopLevelIndex); + QVERIFY(model.hasChildren(windowIndex)); + QVERIFY(model.rowCount(windowIndex) != 0); + QCOMPARE(model.columnCount(windowIndex), 2); + // other indexes are still invalid + QVERIFY(!model.index(0, 1, unmanagedTopLevelIndex).isValid()); + QVERIFY(!model.index(0, 2, unmanagedTopLevelIndex).isValid()); + QVERIFY(!model.index(1, 0, unmanagedTopLevelIndex).isValid()); + + QCOMPARE(model.data(windowIndex, Qt::DisplayRole).toString(), QStringLiteral("0x%1").arg(static_cast(window), 0, 16)); + + // the windowIndex has children and those are properties + for (int i = 0; i < model.rowCount(windowIndex); i++) { + const QModelIndex propNameIndex = model.index(i, 0, windowIndex); + QVERIFY(propNameIndex.isValid()); + QCOMPARE(model.parent(propNameIndex), windowIndex); + QVERIFY(!model.hasChildren(propNameIndex)); + QVERIFY(!model.index(0, 0, propNameIndex).isValid()); + QVERIFY(model.data(propNameIndex, Qt::DisplayRole).isValid()); + QCOMPARE(model.data(propNameIndex, Qt::DisplayRole).userType(), int(QMetaType::QString)); + + // and the value + const QModelIndex propValueIndex = model.index(i, 1, windowIndex); + QVERIFY(propValueIndex.isValid()); + QCOMPARE(model.parent(propValueIndex), windowIndex); + QVERIFY(!model.index(0, 0, propValueIndex).isValid()); + QVERIFY(!model.hasChildren(propValueIndex)); + // TODO: how to test whether the values actually work? + + // and on third column we should not get an index any more + QVERIFY(!model.index(i, 2, windowIndex).isValid()); + } + // row after count should be invalid + QVERIFY(!model.index(model.rowCount(windowIndex), 0, windowIndex).isValid()); + + // creating a second model should be initialized directly with the X11 child + DebugConsoleModel model2; + QVERIFY(model2.hasChildren(model2.index(1, 0, QModelIndex()))); + + // now close the window again, it should be removed from the model + QSignalSpy rowsRemovedSpy(&model, &QAbstractItemModel::rowsRemoved); + + window.unmap(); + + QVERIFY(rowsRemovedSpy.wait()); + QCOMPARE(rowsRemovedSpy.count(), 1); + QCOMPARE(rowsRemovedSpy.first().first().value(), unmanagedTopLevelIndex); + QCOMPARE(rowsRemovedSpy.first().at(1).value(), 0); + QCOMPARE(rowsRemovedSpy.first().at(2).value(), 0); + + // the child should be gone again + QVERIFY(!model.hasChildren(unmanagedTopLevelIndex)); + QVERIFY(!model2.hasChildren(model2.index(1, 0, QModelIndex()))); +} +#endif + +void DebugConsoleTest::testWaylandClient() +{ + DebugConsoleModel model; + QModelIndex waylandTopLevelIndex = model.index(2, 0, QModelIndex()); + QVERIFY(waylandTopLevelIndex.isValid()); + + // we don't have any windows yet + QCOMPARE(model.rowCount(waylandTopLevelIndex), 0); + QVERIFY(!model.hasChildren(waylandTopLevelIndex)); + // child index must be invalid + QVERIFY(!model.index(0, 0, waylandTopLevelIndex).isValid()); + QVERIFY(!model.index(0, 1, waylandTopLevelIndex).isValid()); + QVERIFY(!model.index(0, 2, waylandTopLevelIndex).isValid()); + QVERIFY(!model.index(1, 0, waylandTopLevelIndex).isValid()); + + // we need to create a wayland window + QSignalSpy rowsInsertedSpy(&model, &QAbstractItemModel::rowsInserted); + + // create our connection + QVERIFY(Test::setupWaylandConnection()); + + // create the Surface and ShellSurface + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface->isValid()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Test::render(surface.get(), QSize(10, 10), Qt::red); + + // now we have the window, it should be added to our model + QVERIFY(rowsInsertedSpy.wait()); + QCOMPARE(rowsInsertedSpy.count(), 1); + + QVERIFY(model.hasChildren(waylandTopLevelIndex)); + QCOMPARE(model.rowCount(waylandTopLevelIndex), 1); + QCOMPARE(rowsInsertedSpy.first().at(0).value(), waylandTopLevelIndex); + QCOMPARE(rowsInsertedSpy.first().at(1).value(), 0); + QCOMPARE(rowsInsertedSpy.first().at(2).value(), 0); + + QModelIndex windowIndex = model.index(0, 0, waylandTopLevelIndex); + QVERIFY(windowIndex.isValid()); + QCOMPARE(model.parent(windowIndex), waylandTopLevelIndex); + QVERIFY(model.hasChildren(windowIndex)); + QVERIFY(model.rowCount(windowIndex) != 0); + QCOMPARE(model.columnCount(windowIndex), 2); + // other indexes are still invalid + QVERIFY(!model.index(0, 1, waylandTopLevelIndex).isValid()); + QVERIFY(!model.index(0, 2, waylandTopLevelIndex).isValid()); + QVERIFY(!model.index(1, 0, waylandTopLevelIndex).isValid()); + + // the windowIndex has children and those are properties + for (int i = 0; i < model.rowCount(windowIndex); i++) { + const QModelIndex propNameIndex = model.index(i, 0, windowIndex); + QVERIFY(propNameIndex.isValid()); + QCOMPARE(model.parent(propNameIndex), windowIndex); + QVERIFY(!model.hasChildren(propNameIndex)); + QVERIFY(!model.index(0, 0, propNameIndex).isValid()); + QVERIFY(model.data(propNameIndex, Qt::DisplayRole).isValid()); + QCOMPARE(model.data(propNameIndex, Qt::DisplayRole).userType(), int(QMetaType::QString)); + + // and the value + const QModelIndex propValueIndex = model.index(i, 1, windowIndex); + QVERIFY(propValueIndex.isValid()); + QCOMPARE(model.parent(propValueIndex), windowIndex); + QVERIFY(!model.index(0, 0, propValueIndex).isValid()); + QVERIFY(!model.hasChildren(propValueIndex)); + // TODO: how to test whether the values actually work? + + // and on third column we should not get an index any more + QVERIFY(!model.index(i, 2, windowIndex).isValid()); + } + // row after count should be invalid + QVERIFY(!model.index(model.rowCount(windowIndex), 0, windowIndex).isValid()); + + // creating a second model should be initialized directly with the X11 child + DebugConsoleModel model2; + QVERIFY(model2.hasChildren(model2.index(2, 0, QModelIndex()))); + + // now close the window again, it should be removed from the model + QSignalSpy rowsRemovedSpy(&model, &QAbstractItemModel::rowsRemoved); + + surface->attachBuffer(KWayland::Client::Buffer::Ptr()); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(rowsRemovedSpy.wait()); + + QCOMPARE(rowsRemovedSpy.count(), 1); + QCOMPARE(rowsRemovedSpy.first().first().value(), waylandTopLevelIndex); + QCOMPARE(rowsRemovedSpy.first().at(1).value(), 0); + QCOMPARE(rowsRemovedSpy.first().at(2).value(), 0); + + // the child should be gone again + QVERIFY(!model.hasChildren(waylandTopLevelIndex)); + QVERIFY(!model2.hasChildren(model2.index(2, 0, QModelIndex()))); +} + +class HelperWindow : public QRasterWindow +{ + Q_OBJECT +public: + HelperWindow() + : QRasterWindow(nullptr) + { + } + ~HelperWindow() override = default; + +Q_SIGNALS: + void entered(); + void left(); + void mouseMoved(const QPoint &global); + void mousePressed(); + void mouseReleased(); + void wheel(); + void keyPressed(); + void keyReleased(); + +protected: + void paintEvent(QPaintEvent *event) override + { + QPainter p(this); + p.fillRect(0, 0, width(), height(), Qt::red); + } +}; + +void DebugConsoleTest::testInternalWindow() +{ + DebugConsoleModel model; + QModelIndex internalTopLevelIndex = model.index(3, 0, QModelIndex()); + QVERIFY(internalTopLevelIndex.isValid()); + + // there might already be some internal windows, so we cannot reliable test whether there are children + // given that we just test whether adding a window works. + + QSignalSpy rowsInsertedSpy(&model, &QAbstractItemModel::rowsInserted); + + std::unique_ptr w(new HelperWindow); + w->setGeometry(0, 0, 100, 100); + w->show(); + + QTRY_COMPARE(rowsInsertedSpy.count(), 1); + QCOMPARE(rowsInsertedSpy.first().first().value(), internalTopLevelIndex); + + QModelIndex windowIndex = model.index(rowsInsertedSpy.first().last().toInt(), 0, internalTopLevelIndex); + QVERIFY(windowIndex.isValid()); + QCOMPARE(model.parent(windowIndex), internalTopLevelIndex); + QVERIFY(model.hasChildren(windowIndex)); + QVERIFY(model.rowCount(windowIndex) != 0); + QCOMPARE(model.columnCount(windowIndex), 2); + // other indexes are still invalid + QVERIFY(!model.index(rowsInsertedSpy.first().last().toInt(), 1, internalTopLevelIndex).isValid()); + QVERIFY(!model.index(rowsInsertedSpy.first().last().toInt(), 2, internalTopLevelIndex).isValid()); + QVERIFY(!model.index(rowsInsertedSpy.first().last().toInt() + 1, 0, internalTopLevelIndex).isValid()); + + // the wayland shell client top level should not have gained this window + QVERIFY(!model.hasChildren(model.index(2, 0, QModelIndex()))); + + // the windowIndex has children and those are properties + for (int i = 0; i < model.rowCount(windowIndex); i++) { + const QModelIndex propNameIndex = model.index(i, 0, windowIndex); + QVERIFY(propNameIndex.isValid()); + QCOMPARE(model.parent(propNameIndex), windowIndex); + QVERIFY(!model.hasChildren(propNameIndex)); + QVERIFY(!model.index(0, 0, propNameIndex).isValid()); + QVERIFY(model.data(propNameIndex, Qt::DisplayRole).isValid()); + QCOMPARE(model.data(propNameIndex, Qt::DisplayRole).userType(), int(QMetaType::QString)); + + // and the value + const QModelIndex propValueIndex = model.index(i, 1, windowIndex); + QVERIFY(propValueIndex.isValid()); + QCOMPARE(model.parent(propValueIndex), windowIndex); + QVERIFY(!model.index(0, 0, propValueIndex).isValid()); + QVERIFY(!model.hasChildren(propValueIndex)); + // TODO: how to test whether the values actually work? + + // and on third column we should not get an index any more + QVERIFY(!model.index(i, 2, windowIndex).isValid()); + } + // row after count should be invalid + QVERIFY(!model.index(model.rowCount(windowIndex), 0, windowIndex).isValid()); + + // now close the window again, it should be removed from the model + QSignalSpy rowsRemovedSpy(&model, &QAbstractItemModel::rowsRemoved); + + w->hide(); + w.reset(); + + QTRY_COMPARE(rowsRemovedSpy.count(), 1); + QCOMPARE(rowsRemovedSpy.first().first().value(), internalTopLevelIndex); +} + +void DebugConsoleTest::testClosingDebugConsole() +{ + // this test verifies that the DebugConsole gets destroyed when closing the window + // BUG: 369858 + + DebugConsole *console = new DebugConsole; + QSignalSpy destroyedSpy(console, &QObject::destroyed); + + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + console->show(); + QCOMPARE(console->windowHandle()->isVisible(), true); + QTRY_COMPARE(windowAddedSpy.count(), 1); + InternalWindow *window = windowAddedSpy.first().first().value(); + QVERIFY(window->isInternal()); + QCOMPARE(window->handle(), console->windowHandle()); + QVERIFY(window->isDecorated()); + QCOMPARE(window->isMinimizable(), false); + window->closeWindow(); + QVERIFY(destroyedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::DebugConsoleTest) +#include "debug_console_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/decoration_input_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/decoration_input_test.cpp new file mode 100644 index 0000000000..0b802a8244 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/decoration_input_test.cpp @@ -0,0 +1,774 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "cursor.h" +#include "internalwindow.h" +#include "pointer_input.h" +#include "touch_input.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include "decorations/decoratedwindow.h" +#include "decorations/decorationbridge.h" +#include "decorations/settings.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include + +Q_DECLARE_METATYPE(Qt::WindowFrameSection) + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_decoration_input-0"); + +class DecorationInputTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testAxis_data(); + void testAxis(); + void testDoubleClickOnAllDesktops(); + void testDoubleClickClose(); + void testDoubleTap(); + void testHover(); + void testPressToMove_data(); + void testPressToMove(); + void testTapToMove_data(); + void testTapToMove(); + void testResizeOutsideWindow_data(); + void testResizeOutsideWindow(); + void testModifierClickUnrestrictedMove_data(); + void testModifierClickUnrestrictedMove(); + void testModifierScrollOpacity_data(); + void testModifierScrollOpacity(); + void testTouchEvents(); + void testTooltipDoesntEatKeyEvents(); + +private: + std::tuple, std::unique_ptr, std::unique_ptr> showWindow(); +}; + +#define MOTION(target) Test::pointerMotion(target, timestamp++) + +#define PRESS Test::pointerButtonPressed(BTN_LEFT, timestamp++) + +#define RELEASE Test::pointerButtonReleased(BTN_LEFT, timestamp++) + +std::tuple, std::unique_ptr, std::unique_ptr> DecorationInputTest::showWindow() +{ +#define VERIFY(statement) \ + if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__)) \ + return {nullptr, nullptr, nullptr, nullptr}; +#define COMPARE(actual, expected) \ + if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__)) \ + return {nullptr, nullptr, nullptr, nullptr}; + + std::unique_ptr surface{Test::createSurface()}; + VERIFY(surface.get()); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly); + VERIFY(shellSurface.get()); + std::unique_ptr decoration = Test::createXdgToplevelDecorationV1(shellSurface.get()); + VERIFY(decoration.get()); + + QSignalSpy decorationConfigureRequestedSpy(decoration.get(), &Test::XdgToplevelDecorationV1::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + decoration->set_mode(Test::XdgToplevelDecorationV1::mode_server_side); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + VERIFY(surfaceConfigureRequestedSpy.wait()); + COMPARE(decorationConfigureRequestedSpy.last().at(0).value(), Test::XdgToplevelDecorationV1::mode_server_side); + + // let's render + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(500, 50), Qt::blue); + VERIFY(window); + COMPARE(workspace()->activeWindow(), window); + +#undef VERIFY +#undef COMPARE + + return {window, std::move(surface), std::move(shellSurface), std::move(decoration)}; +} + +void DecorationInputTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + // change some options + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group(QStringLiteral("MouseBindings")).writeEntry("CommandTitlebarWheel", QStringLiteral("above/below")); + config->group(QStringLiteral("Windows")).writeEntry("TitlebarDoubleClickCommand", QStringLiteral("OnAllDesktops")); + config->group(QStringLiteral("Desktops")).writeEntry("Number", 2); + config->sync(); + + kwinApp()->setConfig(config); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); +} + +void DecorationInputTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::XdgDecorationV1)); + QVERIFY(Test::waitForWaylandPointer()); + + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); +} + +void DecorationInputTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void DecorationInputTest::testAxis_data() +{ + QTest::addColumn("decoPoint"); + QTest::addColumn("expectedSection"); + + QTest::newRow("topLeft") << QPoint(0, 0) << Qt::TopLeftSection; + QTest::newRow("top") << QPoint(250, 0) << Qt::TopSection; + QTest::newRow("topRight") << QPoint(499, 0) << Qt::TopRightSection; +} + +void DecorationInputTest::testAxis() +{ + static constexpr double oneTick = 15; + + const auto [window, surface, shellSurface, decoration] = showWindow(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + QVERIFY(!window->noBorder()); + QCOMPARE(window->titlebarPosition(), Qt::TopEdge); + QVERIFY(!window->keepAbove()); + QVERIFY(!window->keepBelow()); + + quint32 timestamp = 1; + MOTION(QPoint(window->frameGeometry().center().x(), window->frameMargins().top() / 2.0)); + QVERIFY(input()->pointer()->decoration()); + QCOMPARE(input()->pointer()->decoration()->decoration()->sectionUnderMouse(), Qt::TitleBarArea); + + // TODO: mouse wheel direction looks wrong to me + // simulate wheel + Test::pointerAxisVertical(oneTick, timestamp++); + QVERIFY(window->keepBelow()); + QVERIFY(!window->keepAbove()); + Test::pointerAxisVertical(-oneTick, timestamp++); + QVERIFY(!window->keepBelow()); + QVERIFY(!window->keepAbove()); + Test::pointerAxisVertical(-oneTick, timestamp++); + QVERIFY(!window->keepBelow()); + QVERIFY(window->keepAbove()); + + // test top most deco pixel, BUG: 362860 + window->move(QPoint(0, 0)); + QFETCH(QPoint, decoPoint); + MOTION(decoPoint); + QVERIFY(input()->pointer()->decoration()); + QCOMPARE(input()->pointer()->decoration()->window(), window); + QTEST(input()->pointer()->decoration()->decoration()->sectionUnderMouse(), "expectedSection"); + Test::pointerAxisVertical(oneTick, timestamp++); + QVERIFY(!window->keepBelow()); + QVERIFY(!window->keepAbove()); +} + +void KWin::DecorationInputTest::testDoubleClickOnAllDesktops() +{ + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("TitlebarDoubleClickCommand", QStringLiteral("OnAllDesktops")); + group.sync(); + workspace()->slotReconfigure(); + + const auto [window, surface, shellSurface, decoration] = showWindow(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + QVERIFY(!window->noBorder()); + QVERIFY(!window->isOnAllDesktops()); + quint32 timestamp = 1; + MOTION(QPoint(window->frameGeometry().center().x(), window->frameMargins().top() / 2.0)); + + // double click + PRESS; + RELEASE; + PRESS; + RELEASE; + QVERIFY(window->isOnAllDesktops()); + // double click again + PRESS; + RELEASE; + QVERIFY(window->isOnAllDesktops()); + PRESS; + RELEASE; + QVERIFY(!window->isOnAllDesktops()); +} + +void DecorationInputTest::testDoubleClickClose() +{ + // this test verifies that no crash occurs when double click is configured to close action + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("TitlebarDoubleClickCommand", QStringLiteral("Close")); + group.sync(); + workspace()->slotReconfigure(); + + auto [window, surface, shellSurface, decoration] = showWindow(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + quint32 timestamp = 1; + MOTION(QPoint(window->frameGeometry().center().x(), window->frameMargins().top() / 2.0)); + + connect(shellSurface.get(), &Test::XdgToplevel::closeRequested, this, [&surface = surface]() { + surface.reset(); + }); + + // double click + QSignalSpy closedSpy(window, &Window::closed); + window->ref(); + PRESS; + RELEASE; + PRESS; + QVERIFY(closedSpy.wait()); + RELEASE; + + QVERIFY(window->isDeleted()); + window->unref(); +} + +void KWin::DecorationInputTest::testDoubleTap() +{ + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("TitlebarDoubleClickCommand", QStringLiteral("OnAllDesktops")); + group.sync(); + workspace()->slotReconfigure(); + + const auto [window, surface, shellSurface, decoration] = showWindow(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + QVERIFY(!window->noBorder()); + QVERIFY(!window->isOnAllDesktops()); + quint32 timestamp = 1; + const QPoint tapPoint(window->frameGeometry().center().x(), window->frameMargins().top() / 2.0); + + // double tap + Test::touchDown(0, tapPoint, timestamp++); + Test::touchUp(0, timestamp++); + Test::touchDown(0, tapPoint, timestamp++); + Test::touchUp(0, timestamp++); + QVERIFY(window->isOnAllDesktops()); + // double tap again + Test::touchDown(0, tapPoint, timestamp++); + Test::touchUp(0, timestamp++); + QVERIFY(window->isOnAllDesktops()); + Test::touchDown(0, tapPoint, timestamp++); + Test::touchUp(0, timestamp++); + QVERIFY(!window->isOnAllDesktops()); +} + +void DecorationInputTest::testHover() +{ + const auto [window, surface, shellSurface, decoration] = showWindow(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + QVERIFY(!window->noBorder()); + + // our left border is moved out of the visible area, so move the window to a better place + window->move(QPoint(20, 0)); + + quint32 timestamp = 1; + MOTION(QPoint(window->frameGeometry().center().x(), window->frameMargins().top() / 2.0)); + QCOMPARE(window->cursor(), CursorShape(Qt::ArrowCursor)); + + // There is a mismatch of the cursor key positions between windows + // with and without borders (with borders one can move inside a bit and still + // be on an edge, without not). We should make this consistent in KWin's core. + // + // TODO: Test input position with different border sizes. + // TODO: We should test with the fake decoration to have a fixed test environment. + const bool hasBorders = Workspace::self()->decorationBridge()->settings()->borderSize() != KDecoration3::BorderSize::None; + auto deviation = [hasBorders] { + return hasBorders ? -1 : 0; + }; + + MOTION(QPoint(window->frameGeometry().x(), 0)); + QCOMPARE(window->cursor(), CursorShape(KWin::ExtendedCursor::SizeNorthWest)); + MOTION(QPoint(window->frameGeometry().x() + window->frameGeometry().width() / 2, 0)); + QCOMPARE(window->cursor(), CursorShape(KWin::ExtendedCursor::SizeNorth)); + MOTION(QPoint(window->frameGeometry().x() + window->frameGeometry().width() - 1, 0)); + QCOMPARE(window->cursor(), CursorShape(KWin::ExtendedCursor::SizeNorthEast)); + MOTION(QPoint(window->frameGeometry().x() + window->frameGeometry().width() + deviation(), window->height() / 2)); + QCOMPARE(window->cursor(), CursorShape(KWin::ExtendedCursor::SizeEast)); + MOTION(QPoint(window->frameGeometry().x() + window->frameGeometry().width() + deviation(), window->height() - 1)); + QCOMPARE(window->cursor(), CursorShape(KWin::ExtendedCursor::SizeSouthEast)); + MOTION(QPoint(window->frameGeometry().x() + window->frameGeometry().width() / 2, window->height() + deviation())); + QCOMPARE(window->cursor(), CursorShape(KWin::ExtendedCursor::SizeSouth)); + MOTION(QPoint(window->frameGeometry().x(), window->height() + deviation())); + QCOMPARE(window->cursor(), CursorShape(KWin::ExtendedCursor::SizeSouthWest)); + MOTION(QPoint(window->frameGeometry().x() - 1, window->height() / 2)); + QCOMPARE(window->cursor(), CursorShape(KWin::ExtendedCursor::SizeWest)); + + MOTION(window->frameGeometry().center()); + QEXPECT_FAIL("", "Cursor not set back on leave", Continue); + QCOMPARE(window->cursor(), CursorShape(Qt::ArrowCursor)); +} + +void DecorationInputTest::testPressToMove_data() +{ + QTest::addColumn("offset"); + QTest::addColumn("offset2"); + QTest::addColumn("offset3"); + + QTest::newRow("To right") << QPoint(10, 0) << QPoint(20, 0) << QPoint(30, 0); + QTest::newRow("To left") << QPoint(-10, 0) << QPoint(-20, 0) << QPoint(-30, 0); + QTest::newRow("To bottom") << QPoint(0, 10) << QPoint(0, 20) << QPoint(0, 30); + QTest::newRow("To top") << QPoint(0, -10) << QPoint(0, -20) << QPoint(0, -30); +} + +void DecorationInputTest::testPressToMove() +{ + const auto [window, surface, shellSurface, decoration] = showWindow(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + QVERIFY(!window->noBorder()); + window->move(workspace()->activeOutput()->geometry().center() - QPoint(window->width() / 2, window->height() / 2)); + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + + quint32 timestamp = 1; + MOTION(QPoint(window->frameGeometry().center().x(), window->y() + window->frameMargins().top() / 2.0)); + QCOMPARE(window->cursor(), CursorShape(Qt::ArrowCursor)); + + PRESS; + QVERIFY(!window->isInteractiveMove()); + QFETCH(QPoint, offset); + MOTION(QPoint(window->frameGeometry().center().x(), window->y() + window->frameMargins().top() / 2.0) + offset); + const QPointF oldPos = window->pos(); + QVERIFY(window->isInteractiveMove()); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + + RELEASE; + QTRY_VERIFY(!window->isInteractiveMove()); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QEXPECT_FAIL("", "Just trigger move doesn't move the window", Continue); + QCOMPARE(window->pos(), oldPos + offset); + + // again + PRESS; + QVERIFY(!window->isInteractiveMove()); + QFETCH(QPoint, offset2); + MOTION(QPoint(window->frameGeometry().center().x(), window->y() + window->frameMargins().top() / 2.0) + offset2); + QVERIFY(window->isInteractiveMove()); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 2); + QFETCH(QPoint, offset3); + MOTION(QPoint(window->frameGeometry().center().x(), window->y() + window->frameMargins().top() / 2.0) + offset3); + + RELEASE; + QTRY_VERIFY(!window->isInteractiveMove()); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 2); + // TODO: the offset should also be included + QCOMPARE(window->pos(), oldPos + offset2 + offset3); +} + +void DecorationInputTest::testTapToMove_data() +{ + QTest::addColumn("offset"); + QTest::addColumn("offset2"); + QTest::addColumn("offset3"); + + QTest::newRow("To right") << QPoint(10, 0) << QPoint(20, 0) << QPoint(30, 0); + QTest::newRow("To left") << QPoint(-10, 0) << QPoint(-20, 0) << QPoint(-30, 0); + QTest::newRow("To bottom") << QPoint(0, 10) << QPoint(0, 20) << QPoint(0, 30); + QTest::newRow("To top") << QPoint(0, -10) << QPoint(0, -20) << QPoint(0, -30); +} + +void DecorationInputTest::testTapToMove() +{ + const auto [window, surface, shellSurface, decoration] = showWindow(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + QVERIFY(!window->noBorder()); + window->move(workspace()->activeOutput()->geometry().center() - QPoint(window->width() / 2, window->height() / 2)); + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + + quint32 timestamp = 1; + QPoint p = QPoint(window->frameGeometry().center().x(), window->y() + window->frameMargins().top() / 2.0); + + Test::touchDown(0, p, timestamp++); + QVERIFY(!window->isInteractiveMove()); + QFETCH(QPoint, offset); + QCOMPARE(input()->touch()->decorationPressId(), 0); + Test::touchMotion(0, p + offset, timestamp++); + const QPointF oldPos = window->pos(); + QVERIFY(window->isInteractiveMove()); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + + Test::touchUp(0, timestamp++); + QTRY_VERIFY(!window->isInteractiveMove()); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QEXPECT_FAIL("", "Just trigger move doesn't move the window", Continue); + QCOMPARE(window->pos(), oldPos + offset); + + // again + Test::touchDown(1, p + offset, timestamp++); + QCOMPARE(input()->touch()->decorationPressId(), 1); + QVERIFY(!window->isInteractiveMove()); + QFETCH(QPoint, offset2); + Test::touchMotion(1, QPoint(window->frameGeometry().center().x(), window->y() + window->frameMargins().top() / 2.0) + offset2, timestamp++); + QVERIFY(window->isInteractiveMove()); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 2); + QFETCH(QPoint, offset3); + Test::touchMotion(1, QPoint(window->frameGeometry().center().x(), window->y() + window->frameMargins().top() / 2.0) + offset3, timestamp++); + + Test::touchUp(1, timestamp++); + QTRY_VERIFY(!window->isInteractiveMove()); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 2); + // TODO: the offset should also be included + QCOMPARE(window->pos(), oldPos + offset2 + offset3); +} + +void DecorationInputTest::testResizeOutsideWindow_data() +{ + QTest::addColumn("edge"); + QTest::addColumn("expectedCursor"); + + QTest::newRow("left") << Qt::LeftEdge << Qt::SizeHorCursor; + QTest::newRow("right") << Qt::RightEdge << Qt::SizeHorCursor; + QTest::newRow("bottom") << Qt::BottomEdge << Qt::SizeVerCursor; +} + +void DecorationInputTest::testResizeOutsideWindow() +{ + // this test verifies that one can resize the window outside the decoration with NoSideBorder + + // first adjust config + kwinApp()->config()->group(QStringLiteral("org.kde.kdecoration2")).writeEntry("BorderSize", QStringLiteral("None")); + kwinApp()->config()->sync(); + workspace()->slotReconfigure(); + + // now create window + const auto [window, surface, shellSurface, decoration] = showWindow(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + QVERIFY(!window->noBorder()); + window->move(workspace()->activeOutput()->geometry().center() - QPoint(window->width() / 2, window->height() / 2)); + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + + // go to border + quint32 timestamp = 1; + QFETCH(Qt::Edge, edge); + switch (edge) { + case Qt::LeftEdge: + MOTION(QPoint(window->frameGeometry().x() - 1, window->frameGeometry().center().y())); + break; + case Qt::RightEdge: + MOTION(QPoint(window->frameGeometry().x() + window->frameGeometry().width() + 1, window->frameGeometry().center().y())); + break; + case Qt::BottomEdge: + MOTION(QPoint(window->frameGeometry().center().x(), window->frameGeometry().y() + window->frameGeometry().height() + 1)); + break; + default: + break; + } + QVERIFY(!window->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos())); + + // pressing should trigger resize + PRESS; + QVERIFY(!window->isInteractiveResize()); + QVERIFY(interactiveMoveResizeStartedSpy.wait()); + QVERIFY(window->isInteractiveResize()); + + RELEASE; + QVERIFY(!window->isInteractiveResize()); +} + +void DecorationInputTest::testModifierClickUnrestrictedMove_data() +{ + QTest::addColumn("modifierKey"); + QTest::addColumn("mouseButton"); + QTest::addColumn("modKey"); + QTest::addColumn("capsLock"); + + const QString alt = QStringLiteral("Alt"); + const QString meta = QStringLiteral("Meta"); + + QTest::newRow("Left Alt + Left Click") << KEY_LEFTALT << BTN_LEFT << alt << false; + QTest::newRow("Left Alt + Right Click") << KEY_LEFTALT << BTN_RIGHT << alt << false; + QTest::newRow("Left Alt + Middle Click") << KEY_LEFTALT << BTN_MIDDLE << alt << false; + QTest::newRow("Right Alt + Left Click") << KEY_RIGHTALT << BTN_LEFT << alt << false; + QTest::newRow("Right Alt + Right Click") << KEY_RIGHTALT << BTN_RIGHT << alt << false; + QTest::newRow("Right Alt + Middle Click") << KEY_RIGHTALT << BTN_MIDDLE << alt << false; + // now everything with meta + QTest::newRow("Left Meta + Left Click") << KEY_LEFTMETA << BTN_LEFT << meta << false; + QTest::newRow("Left Meta + Right Click") << KEY_LEFTMETA << BTN_RIGHT << meta << false; + QTest::newRow("Left Meta + Middle Click") << KEY_LEFTMETA << BTN_MIDDLE << meta << false; + QTest::newRow("Right Meta + Left Click") << KEY_RIGHTMETA << BTN_LEFT << meta << false; + QTest::newRow("Right Meta + Right Click") << KEY_RIGHTMETA << BTN_RIGHT << meta << false; + QTest::newRow("Right Meta + Middle Click") << KEY_RIGHTMETA << BTN_MIDDLE << meta << false; + + // and with capslock + QTest::newRow("Left Alt + Left Click/CapsLock") << KEY_LEFTALT << BTN_LEFT << alt << true; + QTest::newRow("Left Alt + Right Click/CapsLock") << KEY_LEFTALT << BTN_RIGHT << alt << true; + QTest::newRow("Left Alt + Middle Click/CapsLock") << KEY_LEFTALT << BTN_MIDDLE << alt << true; + QTest::newRow("Right Alt + Left Click/CapsLock") << KEY_RIGHTALT << BTN_LEFT << alt << true; + QTest::newRow("Right Alt + Right Click/CapsLock") << KEY_RIGHTALT << BTN_RIGHT << alt << true; + QTest::newRow("Right Alt + Middle Click/CapsLock") << KEY_RIGHTALT << BTN_MIDDLE << alt << true; + // now everything with meta + QTest::newRow("Left Meta + Left Click/CapsLock") << KEY_LEFTMETA << BTN_LEFT << meta << true; + QTest::newRow("Left Meta + Right Click/CapsLock") << KEY_LEFTMETA << BTN_RIGHT << meta << true; + QTest::newRow("Left Meta + Middle Click/CapsLock") << KEY_LEFTMETA << BTN_MIDDLE << meta << true; + QTest::newRow("Right Meta + Left Click/CapsLock") << KEY_RIGHTMETA << BTN_LEFT << meta << true; + QTest::newRow("Right Meta + Right Click/CapsLock") << KEY_RIGHTMETA << BTN_RIGHT << meta << true; + QTest::newRow("Right Meta + Middle Click/CapsLock") << KEY_RIGHTMETA << BTN_MIDDLE << meta << true; +} + +void DecorationInputTest::testModifierClickUnrestrictedMove() +{ + // this test ensures that Alt+mouse button press triggers unrestricted move + + // first modify the config for this run + QFETCH(QString, modKey); + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("MouseBindings")); + group.writeEntry("CommandAllKey", modKey); + group.writeEntry("CommandAll1", "Move"); + group.writeEntry("CommandAll2", "Move"); + group.writeEntry("CommandAll3", "Move"); + group.sync(); + workspace()->slotReconfigure(); + QCOMPARE(options->commandAllModifier(), modKey == QStringLiteral("Alt") ? Qt::AltModifier : Qt::MetaModifier); + QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove); + QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove); + QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove); + + // create a window + const auto [window, surface, shellSurface, decoration] = showWindow(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + QVERIFY(!window->noBorder()); + window->move(workspace()->activeOutput()->geometry().center() - QPoint(window->width() / 2, window->height() / 2)); + // move cursor on window + input()->pointer()->warp(QPoint(window->frameGeometry().center().x(), window->y() + window->frameMargins().top() / 2.0)); + + // simulate modifier+click + quint32 timestamp = 1; + QFETCH(bool, capsLock); + if (capsLock) { + Test::keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); + } + QFETCH(int, modifierKey); + QFETCH(int, mouseButton); + Test::keyboardKeyPressed(modifierKey, timestamp++); + QVERIFY(!window->isInteractiveMove()); + Test::pointerButtonPressed(mouseButton, timestamp++); + QVERIFY(window->isInteractiveMove()); + // release modifier should not change it + Test::keyboardKeyReleased(modifierKey, timestamp++); + QVERIFY(window->isInteractiveMove()); + // but releasing the key should end move/resize + Test::pointerButtonReleased(mouseButton, timestamp++); + QVERIFY(!window->isInteractiveMove()); + if (capsLock) { + Test::keyboardKeyReleased(KEY_CAPSLOCK, timestamp++); + } +} + +void DecorationInputTest::testModifierScrollOpacity_data() +{ + QTest::addColumn("modifierKey"); + QTest::addColumn("modKey"); + QTest::addColumn("capsLock"); + + const QString alt = QStringLiteral("Alt"); + const QString meta = QStringLiteral("Meta"); + + QTest::newRow("Left Alt") << KEY_LEFTALT << alt << false; + QTest::newRow("Right Alt") << KEY_RIGHTALT << alt << false; + QTest::newRow("Left Meta") << KEY_LEFTMETA << meta << false; + QTest::newRow("Right Meta") << KEY_RIGHTMETA << meta << false; + QTest::newRow("Left Alt/CapsLock") << KEY_LEFTALT << alt << true; + QTest::newRow("Right Alt/CapsLock") << KEY_RIGHTALT << alt << true; + QTest::newRow("Left Meta/CapsLock") << KEY_LEFTMETA << meta << true; + QTest::newRow("Right Meta/CapsLock") << KEY_RIGHTMETA << meta << true; +} + +void DecorationInputTest::testModifierScrollOpacity() +{ + // this test verifies that mod+wheel performs a window operation + + // first modify the config for this run + QFETCH(QString, modKey); + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("MouseBindings")); + group.writeEntry("CommandAllKey", modKey); + group.writeEntry("CommandAllWheel", "change opacity"); + group.sync(); + workspace()->slotReconfigure(); + + const auto [window, surface, shellSurface, decoration] = showWindow(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + QVERIFY(!window->noBorder()); + window->move(workspace()->activeOutput()->geometry().center() - QPoint(window->width() / 2, window->height() / 2)); + // move cursor on window + input()->pointer()->warp(QPoint(window->frameGeometry().center().x(), window->y() + window->frameMargins().top() / 2.0)); + // set the opacity to 0.5 + window->setOpacity(0.5); + QCOMPARE(window->opacity(), 0.5); + + // simulate modifier+wheel + quint32 timestamp = 1; + QFETCH(bool, capsLock); + if (capsLock) { + Test::keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); + } + QFETCH(int, modifierKey); + Test::keyboardKeyPressed(modifierKey, timestamp++); + Test::pointerAxisVertical(-15, timestamp++); + QCOMPARE(window->opacity(), 0.6); + Test::pointerAxisVertical(15, timestamp++); + QCOMPARE(window->opacity(), 0.5); + Test::keyboardKeyReleased(modifierKey, timestamp++); + if (capsLock) { + Test::keyboardKeyReleased(KEY_CAPSLOCK, timestamp++); + } +} + +class EventHelper : public QObject +{ + Q_OBJECT +public: + EventHelper() + : QObject() + { + } + ~EventHelper() override = default; + + bool eventFilter(QObject *watched, QEvent *event) override + { + if (event->type() == QEvent::HoverMove) { + Q_EMIT hoverMove(); + } else if (event->type() == QEvent::HoverLeave) { + Q_EMIT hoverLeave(); + } + return false; + } + +Q_SIGNALS: + void hoverMove(); + void hoverLeave(); +}; + +void DecorationInputTest::testTouchEvents() +{ + // this test verifies that the decoration gets a hover leave event on touch release + // see BUG 386231 + const auto [window, surface, shellSurface, decoration] = showWindow(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + QVERIFY(!window->noBorder()); + + EventHelper helper; + window->decoration()->installEventFilter(&helper); + QSignalSpy hoverMoveSpy(&helper, &EventHelper::hoverMove); + QSignalSpy hoverLeaveSpy(&helper, &EventHelper::hoverLeave); + + quint32 timestamp = 1; + const QPoint tapPoint(window->frameGeometry().center().x(), window->frameMargins().top() / 2.0); + + QVERIFY(!input()->touch()->decoration()); + Test::touchDown(0, tapPoint, timestamp++); + QVERIFY(input()->touch()->decoration()); + QCOMPARE(input()->touch()->decoration()->decoration(), window->decoration()); + QCOMPARE(hoverMoveSpy.count(), 1); + QCOMPARE(hoverLeaveSpy.count(), 0); + Test::touchUp(0, timestamp++); + QCOMPARE(hoverMoveSpy.count(), 1); + QCOMPARE(hoverLeaveSpy.count(), 1); + + QCOMPARE(window->isInteractiveMove(), false); + + // let's check that a hover motion is sent if the pointer is on deco, when touch release + input()->pointer()->warp(tapPoint); + QCOMPARE(hoverMoveSpy.count(), 2); + Test::touchDown(0, tapPoint, timestamp++); + QCOMPARE(hoverMoveSpy.count(), 3); + QCOMPARE(hoverLeaveSpy.count(), 1); + Test::touchUp(0, timestamp++); + QCOMPARE(hoverMoveSpy.count(), 3); + QCOMPARE(hoverLeaveSpy.count(), 2); +} + +void DecorationInputTest::testTooltipDoesntEatKeyEvents() +{ + // this test verifies that a tooltip on the decoration does not steal key events + // BUG: 393253 + + // first create a keyboard + auto keyboard = Test::waylandSeat()->createKeyboard(Test::waylandSeat()); + QVERIFY(keyboard); + QSignalSpy enteredSpy(keyboard, &KWayland::Client::Keyboard::entered); + + const auto [window, surface, shellSurface, decoration] = showWindow(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + QVERIFY(!window->noBorder()); + QVERIFY(enteredSpy.wait()); + + QSignalSpy keyEvent(keyboard, &KWayland::Client::Keyboard::keyChanged); + QVERIFY(keyEvent.isValid()); + + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + window->decoratedWindow()->requestShowToolTip(QStringLiteral("test")); + // now we should get an internal window + QVERIFY(windowAddedSpy.wait()); + InternalWindow *internal = windowAddedSpy.first().first().value(); + QVERIFY(internal->isInternal()); + QVERIFY(internal->handle()->flags().testFlag(Qt::ToolTip)); + + // now send a key + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_A, timestamp++); + QVERIFY(keyEvent.wait()); + Test::keyboardKeyReleased(KEY_A, timestamp++); + QVERIFY(keyEvent.wait()); + + window->decoratedWindow()->requestHideToolTip(); + Test::waitForWindowClosed(internal); +} + +} + +WAYLANDTEST_MAIN(KWin::DecorationInputTest) +#include "decoration_input_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/dont_crash_aurorae_destroy_deco.cpp b/local/recipes/kde/kwin/source/autotests/integration/dont_crash_aurorae_destroy_deco.cpp new file mode 100644 index 0000000000..eafd570f83 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/dont_crash_aurorae_destroy_deco.cpp @@ -0,0 +1,136 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "pointer_input.h" +#include "wayland_server.h" +#include "workspace.h" +#include "x11window.h" + +#include + +#include + +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_dont_crash_aurorae_destroy_deco-0"); + +class DontCrashAuroraeDestroyDecoTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void testBorderlessMaximizedWindows(); +}; + +void DontCrashAuroraeDestroyDecoTest::initTestCase() +{ + if (!Test::renderNodeAvailable()) { + QSKIP("no render node available"); + return; + } + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group(QStringLiteral("org.kde.kdecoration2")).writeEntry("library", "org.kde.kwin.aurorae"); + config->sync(); + kwinApp()->setConfig(config); + + // this test needs to enforce OpenGL compositing to get into the crashy condition + qputenv("KWIN_COMPOSE", QByteArrayLiteral("O2")); + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); +} + +void DontCrashAuroraeDestroyDecoTest::init() +{ + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); +} + +void DontCrashAuroraeDestroyDecoTest::testBorderlessMaximizedWindows() +{ + // this test verifies that Aurorae doesn't crash when clicking the maximize button + // with kwin config option BorderlessMaximizedWindows + // see BUG 362772 + + // first adjust the config + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("BorderlessMaximizedWindows", true); + group.sync(); + workspace()->slotReconfigure(); + QCOMPARE(options->borderlessMaximizedWindows(), true); + + // create an xcb window + Test::XcbConnectionPtr connection = Test::createX11Connection(); + auto c = connection.get(); + QVERIFY(!xcb_connection_has_error(c)); + + xcb_window_t windowId = xcb_generate_id(c); + xcb_create_window(c, XCB_COPY_FROM_PARENT, windowId, rootWindow(), 0, 0, 100, 200, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_map_window(c, windowId); + xcb_flush(c); + + // we should get a window for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.first().first().value(); + QVERIFY(window); + QCOMPARE(window->window(), windowId); + QVERIFY(window->isDecorated()); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->noBorder(), false); + // verify that the deco is Aurorae + QCOMPARE(qstrcmp(window->decoration()->metaObject()->className(), "Aurorae::Decoration"), 0); + // find the maximize button + QQuickItem *item = window->decoration()->property("item").value()->findChild("maximizeButton"); + QVERIFY(item); + const QPointF scenePoint = item->mapToScene(QPoint(0, 0)); + + // mark the window as ready for painting, otherwise it doesn't get input events + QMetaObject::invokeMethod(window, "setReadyForPainting"); + QVERIFY(window->readyForPainting()); + + // simulate click on maximize button + QSignalSpy maximizedStateChangedSpy(window, &Window::maximizedChanged); + quint32 timestamp = 1; + Test::pointerMotion(window->frameGeometry().topLeft() + scenePoint.toPoint(), timestamp++); + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + QVERIFY(maximizedStateChangedSpy.wait()); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->noBorder(), true); + + // and destroy the window again + xcb_unmap_window(c, windowId); + xcb_destroy_window(c, windowId); + xcb_flush(c); + + QSignalSpy windowClosedSpy(window, &X11Window::closed); + QVERIFY(windowClosedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::DontCrashAuroraeDestroyDecoTest) +#include "dont_crash_aurorae_destroy_deco.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/dont_crash_cancel_animation.cpp b/local/recipes/kde/kwin/source/autotests/integration/dont_crash_cancel_animation.cpp new file mode 100644 index 0000000000..b14b11e2d0 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/dont_crash_cancel_animation.cpp @@ -0,0 +1,111 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "compositor.h" +#include "effect/effecthandler.h" +#include "effect/effectloader.h" +#include "scripting/scriptedeffect.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" +#if KWIN_BUILD_X11 +#include "x11window.h" +#endif + +#include + +#include +#include +#include +#include + +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_dont_crash_cancel_animation-0"); + +class DontCrashCancelAnimationFromAnimationEndedTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testScript(); +}; + +void DontCrashCancelAnimationFromAnimationEndedTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + QVERIFY(Compositor::self()); + QVERIFY(effects); +} + +void DontCrashCancelAnimationFromAnimationEndedTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void DontCrashCancelAnimationFromAnimationEndedTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void DontCrashCancelAnimationFromAnimationEndedTest::testScript() +{ + // load a scripted effect which deletes animation data + ScriptedEffect *effect = ScriptedEffect::create(QStringLiteral("crashy"), QFINDTESTDATA("data/anim-data-delete-effect/effect.js"), 10, QString()); + QVERIFY(effect); + + const auto children = effects->children(); + for (auto it = children.begin(); it != children.end(); ++it) { + if (qstrcmp((*it)->metaObject()->className(), "KWin::EffectLoader") != 0) { + continue; + } + QVERIFY(QMetaObject::invokeMethod(*it, "effectLoaded", Q_ARG(KWin::Effect *, effect), Q_ARG(QString, QStringLiteral("crashy")))); + break; + } + QVERIFY(effects->isEffectLoaded(QStringLiteral("crashy"))); + + // create a window + std::unique_ptr surface{Test::createSurface()}; + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + // let's render + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QCOMPARE(workspace()->activeWindow(), window); + + // make sure we animate + QTest::qWait(200); + + // wait for the window to be passed to Deleted + QSignalSpy windowDeletedSpy(window, &Window::closed); + + surface.reset(); + + QVERIFY(windowDeletedSpy.wait()); + // make sure we animate + QTest::qWait(200); +} + +} + +WAYLANDTEST_MAIN(KWin::DontCrashCancelAnimationFromAnimationEndedTest) +#include "dont_crash_cancel_animation.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/dont_crash_empty_deco.cpp b/local/recipes/kde/kwin/source/autotests/integration/dont_crash_empty_deco.cpp new file mode 100644 index 0000000000..55cc0436dc --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/dont_crash_empty_deco.cpp @@ -0,0 +1,106 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "pointer_input.h" +#include "wayland_server.h" +#include "workspace.h" +#include "x11window.h" + +#include + +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_dont_crash_empty_decoration-0"); + +class DontCrashEmptyDecorationTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void testBug361551(); +}; + +void DontCrashEmptyDecorationTest::initTestCase() +{ + if (!Test::renderNodeAvailable()) { + QSKIP("no render node available"); + return; + } + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + // this test needs to enforce OpenGL compositing to get into the crashy condition + qputenv("KWIN_COMPOSE", QByteArrayLiteral("O2")); + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); +} + +void DontCrashEmptyDecorationTest::init() +{ + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); +} + +void DontCrashEmptyDecorationTest::testBug361551() +{ + // this test verifies that resizing an X11 window to an invalid size does not result in crash on unmap + // when the DecorationRenderer gets copied to the Deleted + // there a repaint is scheduled and the resulting texture is invalid if the window size is invalid + + // create an xcb window + Test::XcbConnectionPtr connection = Test::createX11Connection(); + auto c = connection.get(); + + QVERIFY(c); + QVERIFY(!xcb_connection_has_error(c)); + + xcb_window_t windowId = xcb_generate_id(c); + xcb_create_window(c, XCB_COPY_FROM_PARENT, windowId, rootWindow(), 0, 0, 10, 10, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_map_window(c, windowId); + xcb_flush(c); + + // we should get a window for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.first().first().value(); + QVERIFY(window); + QCOMPARE(window->window(), windowId); + QVERIFY(window->isDecorated()); + + // let's set a stupid geometry + window->moveResize({0, 0, 0, 0}); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 0, 0)); + + // and destroy the window again + xcb_unmap_window(c, windowId); + xcb_destroy_window(c, windowId); + xcb_flush(c); + + QSignalSpy windowClosedSpy(window, &X11Window::closed); + QVERIFY(windowClosedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::DontCrashEmptyDecorationTest) +#include "dont_crash_empty_deco.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/dont_crash_glxgears.cpp b/local/recipes/kde/kwin/source/autotests/integration/dont_crash_glxgears.cpp new file mode 100644 index 0000000000..ea0770ff6f --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/dont_crash_glxgears.cpp @@ -0,0 +1,93 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" +#include "x11window.h" + +#include + +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_dont_crash_glxgears-0"); + +class DontCrashGlxgearsTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testGlxgears(); +}; + +void DontCrashGlxgearsTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void DontCrashGlxgearsTest::testGlxgears() +{ + // closing a glxgears window through Aurorae themes used to crash KWin + // Let's make sure that doesn't happen anymore + + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + + QProcess glxgears; + glxgears.setProgram(QStringLiteral("glxgears")); + glxgears.start(); + QVERIFY(glxgears.waitForStarted()); + + QVERIFY(windowAddedSpy.wait()); + QCOMPARE(windowAddedSpy.count(), 1); + QCOMPARE(workspace()->windows().count(), 1); + Window *glxgearsWindow = workspace()->windows().first(); + QVERIFY(glxgearsWindow->isDecorated()); + QSignalSpy closedSpy(glxgearsWindow, &X11Window::closed); + KDecoration3::Decoration *decoration = glxgearsWindow->decoration(); + QVERIFY(decoration); + + // send a mouse event to the position of the close button + // TODO: position is dependent on the decoration in use. We should use a static target instead, a fake deco for autotests. + QPointF pos = decoration->rect().topRight() + QPointF(-decoration->borderTop() / 2, decoration->borderTop() / 2); + QHoverEvent event(QEvent::HoverMove, pos, pos); + QCoreApplication::instance()->sendEvent(decoration, &event); + // mouse press + QMouseEvent mousePressevent(QEvent::MouseButtonPress, pos, pos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + mousePressevent.setAccepted(false); + QCoreApplication::sendEvent(decoration, &mousePressevent); + QVERIFY(mousePressevent.isAccepted()); + // mouse Release + QMouseEvent mouseReleaseEvent(QEvent::MouseButtonRelease, pos, pos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + mouseReleaseEvent.setAccepted(false); + QCoreApplication::sendEvent(decoration, &mouseReleaseEvent); + QVERIFY(mouseReleaseEvent.isAccepted()); + + QVERIFY(closedSpy.wait()); + QCOMPARE(closedSpy.count(), 1); + xcb_flush(connection()); + + if (glxgears.state() == QProcess::Running) { + QVERIFY(glxgears.waitForFinished()); + } +} + +} + +WAYLANDTEST_MAIN(KWin::DontCrashGlxgearsTest) +#include "dont_crash_glxgears.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/dont_crash_reinitialize_compositor.cpp b/local/recipes/kde/kwin/source/autotests/integration/dont_crash_reinitialize_compositor.cpp new file mode 100644 index 0000000000..1c2248d896 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/dont_crash_reinitialize_compositor.cpp @@ -0,0 +1,139 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "compositor.h" +#include "core/output.h" +#include "effect/effecthandler.h" +#include "effect/effectloader.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_dont_crash_reinitialize_compositor-0"); + +class DontCrashReinitializeCompositorTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testReinitializeCompositor_data(); + void testReinitializeCompositor(); +}; + +void DontCrashReinitializeCompositorTest::initTestCase() +{ + if (!Test::renderNodeAvailable()) { + QSKIP("no render node available"); + return; + } + qputenv("XDG_DATA_DIRS", QCoreApplication::applicationDirPath().toUtf8()); + + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup plugins(config, QStringLiteral("Plugins")); + const auto builtinNames = EffectLoader().listOfKnownEffects(); + for (const QString &name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + config->sync(); + kwinApp()->setConfig(config); + + qputenv("KWIN_COMPOSE", QByteArrayLiteral("O2")); + qputenv("KWIN_EFFECTS_FORCE_ANIMATIONS", QByteArrayLiteral("1")); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); +} + +void DontCrashReinitializeCompositorTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void DontCrashReinitializeCompositorTest::cleanup() +{ + // Unload all effects. + effects->unloadAllEffects(); + QVERIFY(effects->loadedEffects().isEmpty()); + + Test::destroyWaylandConnection(); +} + +void DontCrashReinitializeCompositorTest::testReinitializeCompositor_data() +{ + QTest::addColumn("effectName"); + + QTest::newRow("Fade") << QStringLiteral("fade"); + QTest::newRow("Glide") << QStringLiteral("glide"); + QTest::newRow("Scale") << QStringLiteral("scale"); +} + +void DontCrashReinitializeCompositorTest::testReinitializeCompositor() +{ + // This test verifies that KWin doesn't crash when the compositor settings + // have been changed while a scripted effect animates the disappearing of + // a window. + + // Create the test window. + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // Make sure that only the test effect is loaded. + QFETCH(QString, effectName); + QVERIFY(effects->loadEffect(effectName)); + QCOMPARE(effects->loadedEffects().count(), 1); + QCOMPARE(effects->loadedEffects().first(), effectName); + Effect *effect = effects->findEffect(effectName); + QVERIFY(effect); + QVERIFY(!effect->isActive()); + + // Close the test window. + QSignalSpy windowClosedSpy(window, &Window::closed); + shellSurface.reset(); + surface.reset(); + QVERIFY(windowClosedSpy.wait()); + + // The test effect should start animating the test window. Is there a better + // way to verify that the test effect actually animates the test window? + QVERIFY(effect->isActive()); + + // Re-initialize the compositor, effects will be destroyed and created again. + Compositor::self()->reinitialize(); + + // By this time, KWin should still be alive. +} + +} // namespace KWin + +WAYLANDTEST_MAIN(KWin::DontCrashReinitializeCompositorTest) +#include "dont_crash_reinitialize_compositor.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/dont_crash_useractions_menu.cpp b/local/recipes/kde/kwin/source/autotests/integration/dont_crash_useractions_menu.cpp new file mode 100644 index 0000000000..582069df61 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/dont_crash_useractions_menu.cpp @@ -0,0 +1,100 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "pointer_input.h" +#include "useractions.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_dont_crash_useractions_menu-0"); + +class TestDontCrashUseractionsMenu : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testShowHideShowUseractionsMenu(); +}; + +void TestDontCrashUseractionsMenu::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + // force style to breeze as that's the one which triggered the crash + QVERIFY(kwinApp()->setStyle(QStringLiteral("breeze"))); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); +} + +void TestDontCrashUseractionsMenu::init() +{ + QVERIFY(Test::setupWaylandConnection()); + + workspace()->setActiveOutput(QPoint(640, 512)); + KWin::input()->pointer()->warp(QPoint(640, 512)); +} + +void TestDontCrashUseractionsMenu::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TestDontCrashUseractionsMenu::testShowHideShowUseractionsMenu() +{ + // this test creates the condition of BUG 382063 + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + auto window = Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + workspace()->showWindowMenu(Rect(), window); + auto userActionsMenu = workspace()->userActionsMenu(); + QTRY_VERIFY(userActionsMenu->isShown()); + QVERIFY(userActionsMenu->hasWindow()); + + Test::keyboardKeyPressed(KEY_ESC, 0); + Test::keyboardKeyReleased(KEY_ESC, 1); + QTRY_VERIFY(!userActionsMenu->isShown()); + QVERIFY(!userActionsMenu->hasWindow()); + + // and show again, this triggers BUG 382063 + workspace()->showWindowMenu(Rect(), window); + QTRY_VERIFY(userActionsMenu->isShown()); + QVERIFY(userActionsMenu->hasWindow()); +} + +WAYLANDTEST_MAIN(TestDontCrashUseractionsMenu) +#include "dont_crash_useractions_menu.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/CMakeLists.txt b/local/recipes/kde/kwin/source/autotests/integration/effects/CMakeLists.txt new file mode 100644 index 0000000000..37584967d4 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/CMakeLists.txt @@ -0,0 +1,11 @@ +integrationTest(NAME testSlidingPopups SRCS slidingpopups_test.cpp BUILTIN_EFFECTS) +integrationTest(NAME testScriptedEffects SRCS scripted_effects_test.cpp LIBS KF6::GlobalAccel BUILTIN_EFFECTS) +integrationTest(NAME testToplevelOpenCloseAnimation SRCS toplevel_open_close_animation_test.cpp BUILTIN_EFFECTS) +integrationTest(NAME testPopupOpenCloseAnimation SRCS popup_open_close_animation_test.cpp LIBS KF6::I18n KDecoration3::KDecoration3Private BUILTIN_EFFECTS) +integrationTest(NAME testDesktopSwitchingAnimation SRCS desktop_switching_animation_test.cpp BUILTIN_EFFECTS) +integrationTest(NAME testMinimizeAnimation SRCS minimize_animation_test.cpp BUILTIN_EFFECTS) +integrationTest(NAME testMaximizeAnimation SRCS maximize_animation_test.cpp BUILTIN_EFFECTS) + +if(KWIN_BUILD_X11) + integrationTest(NAME testTranslucency SRCS translucency_test.cpp LIBS XCB::ICCCM BUILTIN_EFFECTS) +endif() diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/desktop_switching_animation_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/effects/desktop_switching_animation_test.cpp new file mode 100644 index 0000000000..fa24c17b3c --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/desktop_switching_animation_test.cpp @@ -0,0 +1,139 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "effect/effecthandler.h" +#include "effect/effectloader.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_effects_desktop_switching_animation-0"); + +class DesktopSwitchingAnimationTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testSwitchDesktops_data(); + void testSwitchDesktops(); +}; + +void DesktopSwitchingAnimationTest::initTestCase() +{ + if (!Test::renderNodeAvailable()) { + QSKIP("no render node available"); + return; + } + qputenv("XDG_DATA_DIRS", QCoreApplication::applicationDirPath().toUtf8()); + + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup plugins(config, QStringLiteral("Plugins")); + const auto builtinNames = EffectLoader().listOfKnownEffects(); + for (const QString &name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + config->sync(); + kwinApp()->setConfig(config); + + qputenv("KWIN_COMPOSE", QByteArrayLiteral("O2")); + qputenv("KWIN_EFFECTS_FORCE_ANIMATIONS", QByteArrayLiteral("1")); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void DesktopSwitchingAnimationTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void DesktopSwitchingAnimationTest::cleanup() +{ + QVERIFY(effects); + effects->unloadAllEffects(); + QVERIFY(effects->loadedEffects().isEmpty()); + + VirtualDesktopManager::self()->setCount(1); + + Test::destroyWaylandConnection(); +} + +void DesktopSwitchingAnimationTest::testSwitchDesktops_data() +{ + QTest::addColumn("effectName"); + + QTest::newRow("Fade Desktop") << QStringLiteral("fadedesktop"); + QTest::newRow("Slide") << QStringLiteral("slide"); +} + +void DesktopSwitchingAnimationTest::testSwitchDesktops() +{ + // This test verifies that virtual desktop switching animation effects actually + // try to animate switching between desktops. + + // We need at least 2 virtual desktops for the test. + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(VirtualDesktopManager::self()->current(), 1u); + QCOMPARE(VirtualDesktopManager::self()->count(), 2u); + + // The Fade Desktop effect will do nothing if there are no windows to fade, + // so we have to create a dummy test window. + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QCOMPARE(window->desktops().count(), 1); + QCOMPARE(window->desktops().first(), VirtualDesktopManager::self()->desktops().first()); + + // Load effect that will be tested. + QFETCH(QString, effectName); + QVERIFY(effects); + QVERIFY(effects->loadEffect(effectName)); + QCOMPARE(effects->loadedEffects().count(), 1); + QCOMPARE(effects->loadedEffects().first(), effectName); + Effect *effect = effects->findEffect(effectName); + QVERIFY(effect); + QVERIFY(!effect->isActive()); + + // Switch to the second virtual desktop. + VirtualDesktopManager::self()->setCurrent(2u); + QCOMPARE(VirtualDesktopManager::self()->current(), 2u); + QVERIFY(effect->isActive()); + QCOMPARE(effects->activeFullScreenEffect(), effect); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + QTRY_COMPARE(effects->activeFullScreenEffect(), nullptr); + + // Destroy the test window. + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +WAYLANDTEST_MAIN(DesktopSwitchingAnimationTest) +#include "desktop_switching_animation_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/maximize_animation_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/effects/maximize_animation_test.cpp new file mode 100644 index 0000000000..ebeab4175b --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/maximize_animation_test.cpp @@ -0,0 +1,182 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "compositor.h" +#include "effect/effecthandler.h" +#include "effect/effectloader.h" +#include "scene/workspacescene.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_effects_maximize_animation-0"); + +class MaximizeAnimationTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testMaximizeRestore(); +}; + +void MaximizeAnimationTest::initTestCase() +{ + if (!Test::renderNodeAvailable()) { + QSKIP("no render node available"); + return; + } + qputenv("XDG_DATA_DIRS", QCoreApplication::applicationDirPath().toUtf8()); + + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup plugins(config, QStringLiteral("Plugins")); + const auto builtinNames = EffectLoader().listOfKnownEffects(); + for (const QString &name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + config->sync(); + kwinApp()->setConfig(config); + + qputenv("KWIN_COMPOSE", QByteArrayLiteral("O2")); + qputenv("KWIN_EFFECTS_FORCE_ANIMATIONS", QByteArrayLiteral("1")); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void MaximizeAnimationTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void MaximizeAnimationTest::cleanup() +{ + QVERIFY(effects); + effects->unloadAllEffects(); + QVERIFY(effects->loadedEffects().isEmpty()); + + Test::destroyWaylandConnection(); +} + +void MaximizeAnimationTest::testMaximizeRestore() +{ + // This test verifies that the maximize effect animates a window + // when it's maximized or restored. + + // Create the test window. + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + + // Wait for the initial configure event. + Test::XdgToplevel::States states; + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(0, 0)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Draw contents of the surface. + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(window->maximizeMode(), MaximizeMode::MaximizeRestore); + + // We should receive a configure event when the window becomes active. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Load effect that will be tested. + const QString effectName = QStringLiteral("maximize"); + QVERIFY(effects); + QVERIFY(effects->loadEffect(effectName)); + QCOMPARE(effects->loadedEffects().count(), 1); + QCOMPARE(effects->loadedEffects().first(), effectName); + Effect *effect = effects->findEffect(effectName); + QVERIFY(effect); + QVERIFY(!effect->isActive()); + + // Maximize the window. + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + QSignalSpy maximizeChangedSpy(window, &Window::maximizedChanged); + + workspace()->slotWindowMaximize(); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(1280, 1024)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Draw contents of the maximized window. + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(1280, 1024), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 1); + QCOMPARE(maximizeChangedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeMode::MaximizeFull); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Restore the window. + workspace()->slotWindowMaximize(); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 4); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(100, 50)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Draw contents of the restored window. + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 2); + QCOMPARE(maximizeChangedSpy.count(), 2); + QCOMPARE(window->maximizeMode(), MaximizeMode::MaximizeRestore); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Destroy the test window. + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +WAYLANDTEST_MAIN(MaximizeAnimationTest) +#include "maximize_animation_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/minimize_animation_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/effects/minimize_animation_test.cpp new file mode 100644 index 0000000000..5ae7b729e7 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/minimize_animation_test.cpp @@ -0,0 +1,165 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "effect/effecthandler.h" +#include "effect/effectloader.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_effects_minimize_animation-0"); + +class MinimizeAnimationTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testMinimizeUnminimize_data(); + void testMinimizeUnminimize(); +}; + +void MinimizeAnimationTest::initTestCase() +{ + if (!Test::renderNodeAvailable()) { + QSKIP("no render node available"); + return; + } + qputenv("XDG_DATA_DIRS", QCoreApplication::applicationDirPath().toUtf8()); + + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup plugins(config, QStringLiteral("Plugins")); + const auto builtinNames = EffectLoader().listOfKnownEffects(); + for (const QString &name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + config->sync(); + kwinApp()->setConfig(config); + + qputenv("KWIN_COMPOSE", QByteArrayLiteral("O2")); + qputenv("KWIN_EFFECTS_FORCE_ANIMATIONS", QByteArrayLiteral("1")); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void MinimizeAnimationTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::LayerShellV1 | Test::AdditionalWaylandInterface::WindowManagement)); +} + +void MinimizeAnimationTest::cleanup() +{ + QVERIFY(effects); + effects->unloadAllEffects(); + QVERIFY(effects->loadedEffects().isEmpty()); + + Test::destroyWaylandConnection(); +} + +void MinimizeAnimationTest::testMinimizeUnminimize_data() +{ + QTest::addColumn("effectName"); + + QTest::newRow("Magic Lamp") << QStringLiteral("magiclamp"); + QTest::newRow("Squash") << QStringLiteral("squash"); +} + +void MinimizeAnimationTest::testMinimizeUnminimize() +{ + // This test verifies that a minimize effect tries to animate a window + // when it's minimized or unminimized. + + // Create a panel at the top of the screen. + const Rect panelRect = Rect(0, 0, 1280, 36); + std::unique_ptr panelSurface{Test::createSurface()}; + std::unique_ptr panelShellSurface{Test::createLayerSurfaceV1(panelSurface.get(), QStringLiteral("dock"))}; + panelShellSurface->set_size(panelRect.width(), panelRect.height()); + panelShellSurface->set_exclusive_zone(panelRect.height()); + panelShellSurface->set_anchor(Test::LayerSurfaceV1::anchor_top); + panelSurface->commit(KWayland::Client::Surface::CommitFlag::None); + + QSignalSpy panelConfigureRequestedSpy(panelShellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(panelConfigureRequestedSpy.wait()); + Window *panel = Test::renderAndWaitForShown(panelSurface.get(), panelConfigureRequestedSpy.last().at(1).toSize(), Qt::blue); + QVERIFY(panel); + QVERIFY(panel->isDock()); + QCOMPARE(panel->frameGeometry(), panelRect); + + // Create the test window. + QSignalSpy plasmaWindowCreatedSpy(Test::waylandWindowManagement(), &KWayland::Client::PlasmaWindowManagement::windowCreated); + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::red); + QVERIFY(window); + QVERIFY(plasmaWindowCreatedSpy.wait()); + QCOMPARE(plasmaWindowCreatedSpy.count(), 1); + + // We have to set the minimized geometry because the squash effect needs it, + // otherwise it won't start animation. + auto plasmaWindow = plasmaWindowCreatedSpy.last().first().value(); + QVERIFY(plasmaWindow); + const RectF iconRect = RectF(0, 0, 42, 36); + plasmaWindow->setMinimizedGeometry(panelSurface.get(), iconRect.toRect()); + Test::flushWaylandConnection(); + QTRY_COMPARE(window->iconGeometry(), iconRect.translated(panel->frameGeometry().topLeft().toPoint())); + + // Load effect that will be tested. + QFETCH(QString, effectName); + QVERIFY(effects); + QVERIFY(effects->loadEffect(effectName)); + QCOMPARE(effects->loadedEffects().count(), 1); + QCOMPARE(effects->loadedEffects().first(), effectName); + Effect *effect = effects->findEffect(effectName); + QVERIFY(effect); + QVERIFY(!effect->isActive()); + + // Start the minimize animation. + window->setMinimized(true); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Start the unminimize animation. + window->setMinimized(false); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Destroy the panel. + panelSurface.reset(); + QVERIFY(Test::waitForWindowClosed(panel)); + + // Destroy the test window. + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +WAYLANDTEST_MAIN(MinimizeAnimationTest) +#include "minimize_animation_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/popup_open_close_animation_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/effects/popup_open_close_animation_test.cpp new file mode 100644 index 0000000000..e904f20a72 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/popup_open_close_animation_test.cpp @@ -0,0 +1,247 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "effect/effecthandler.h" +#include "effect/effectloader.h" +#include "internalwindow.h" +#include "useractions.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include "decorations/decoratedwindow.h" + +#include + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_effects_popup_open_close_animation-0"); + +class PopupOpenCloseAnimationTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testAnimatePopups(); + void testAnimateUserActionsPopup(); + void testAnimateDecorationTooltips(); +}; + +void PopupOpenCloseAnimationTest::initTestCase() +{ + qputenv("XDG_DATA_DIRS", QCoreApplication::applicationDirPath().toUtf8()); + + qRegisterMetaType(); + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup plugins(config, QStringLiteral("Plugins")); + const auto builtinNames = EffectLoader().listOfKnownEffects(); + for (const QString &name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + config->sync(); + kwinApp()->setConfig(config); + + qputenv("KWIN_EFFECTS_FORCE_ANIMATIONS", QByteArrayLiteral("1")); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void PopupOpenCloseAnimationTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::XdgDecorationV1)); +} + +void PopupOpenCloseAnimationTest::cleanup() +{ + QVERIFY(effects); + effects->unloadAllEffects(); + QVERIFY(effects->loadedEffects().isEmpty()); + + Test::destroyWaylandConnection(); +} + +void PopupOpenCloseAnimationTest::testAnimatePopups() +{ + // This test verifies that popup open/close animation effects try + // to animate popups(e.g. popup menus, tooltips, etc). + + // Create the main window. + std::unique_ptr mainWindowSurface(Test::createSurface()); + QVERIFY(mainWindowSurface != nullptr); + std::unique_ptr mainWindowShellSurface(Test::createXdgToplevelSurface(mainWindowSurface.get())); + QVERIFY(mainWindowShellSurface != nullptr); + Window *mainWindow = Test::renderAndWaitForShown(mainWindowSurface.get(), QSize(100, 50), Qt::blue); + QVERIFY(mainWindow); + + // Load effect that will be tested. + const QString effectName = QStringLiteral("fadingpopups"); + QVERIFY(effects->loadEffect(effectName)); + QCOMPARE(effects->loadedEffects().count(), 1); + QCOMPARE(effects->loadedEffects().first(), effectName); + Effect *effect = effects->findEffect(effectName); + QVERIFY(effect); + QVERIFY(!effect->isActive()); + + // Create a popup, it should be animated. + std::unique_ptr popupSurface(Test::createSurface()); + QVERIFY(popupSurface != nullptr); + std::unique_ptr positioner(Test::createXdgPositioner()); + positioner->set_size(20, 20); + positioner->set_anchor_rect(0, 0, 10, 10); + positioner->set_gravity(Test::XdgPositioner::gravity_bottom_right); + positioner->set_anchor(Test::XdgPositioner::anchor_bottom_left); + std::unique_ptr popupShellSurface(Test::createXdgPopupSurface(popupSurface.get(), mainWindowShellSurface->xdgSurface(), positioner.get())); + QVERIFY(popupShellSurface != nullptr); + Window *popup = Test::renderAndWaitForShown(popupSurface.get(), QSize(20, 20), Qt::red); + QVERIFY(popup); + QVERIFY(popup->isPopupWindow()); + QCOMPARE(popup->transientFor(), mainWindow); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Destroy the popup, it should not be animated. + QSignalSpy popupClosedSpy(popup, &Window::closed); + popupShellSurface.reset(); + popupSurface.reset(); + QVERIFY(popupClosedSpy.wait()); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Destroy the main window. + mainWindowSurface.reset(); + QVERIFY(Test::waitForWindowClosed(mainWindow)); +} + +void PopupOpenCloseAnimationTest::testAnimateUserActionsPopup() +{ + // This test verifies that popup open/close animation effects try + // to animate the user actions popup. + + // Create the test window. + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // Load effect that will be tested. + const QString effectName = QStringLiteral("fadingpopups"); + QVERIFY(effects->loadEffect(effectName)); + QCOMPARE(effects->loadedEffects().count(), 1); + QCOMPARE(effects->loadedEffects().first(), effectName); + Effect *effect = effects->findEffect(effectName); + QVERIFY(effect); + QVERIFY(!effect->isActive()); + + // Show the user actions popup. + workspace()->showWindowMenu(Rect(), window); + auto userActionsMenu = workspace()->userActionsMenu(); + QTRY_VERIFY(userActionsMenu->isShown()); + QVERIFY(userActionsMenu->hasWindow()); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Close the user actions popup. + Test::keyboardKeyPressed(KEY_ESC, 0); + Test::keyboardKeyReleased(KEY_ESC, 1); + QTRY_VERIFY(!userActionsMenu->isShown()); + QVERIFY(!userActionsMenu->hasWindow()); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Destroy the test window. + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void PopupOpenCloseAnimationTest::testAnimateDecorationTooltips() +{ + // This test verifies that popup open/close animation effects try + // to animate decoration tooltips. + + // Create the test window. + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + QVERIFY(shellSurface != nullptr); + std::unique_ptr deco(Test::createXdgToplevelDecorationV1(shellSurface.get())); + QVERIFY(deco != nullptr); + + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + deco->set_mode(Test::XdgToplevelDecorationV1::mode_server_side); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(window->isDecorated()); + + // Load effect that will be tested. + const QString effectName = QStringLiteral("fadingpopups"); + QVERIFY(effects->loadEffect(effectName)); + QCOMPARE(effects->loadedEffects().count(), 1); + QCOMPARE(effects->loadedEffects().first(), effectName); + Effect *effect = effects->findEffect(effectName); + QVERIFY(effect); + QVERIFY(!effect->isActive()); + + // Show a decoration tooltip. + QSignalSpy tooltipAddedSpy(workspace(), &Workspace::windowAdded); + window->decoratedWindow()->requestShowToolTip(QStringLiteral("KWin rocks!")); + QVERIFY(tooltipAddedSpy.wait()); + InternalWindow *tooltip = tooltipAddedSpy.first().first().value(); + QVERIFY(tooltip->isInternal()); + QVERIFY(tooltip->isPopupWindow()); + QVERIFY(tooltip->handle()->flags().testFlag(Qt::ToolTip)); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Hide the decoration tooltip. + QSignalSpy tooltipClosedSpy(tooltip, &InternalWindow::closed); + window->decoratedWindow()->requestHideToolTip(); + QVERIFY(tooltipClosedSpy.wait()); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Destroy the test window. + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +WAYLANDTEST_MAIN(PopupOpenCloseAnimationTest) +#include "popup_open_close_animation_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/scripted_effects_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/effects/scripted_effects_test.cpp new file mode 100644 index 0000000000..fb1dd2844a --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/scripted_effects_test.cpp @@ -0,0 +1,748 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "effect/anidata_p.h" +#include "effect/effecthandler.h" +#include "effect/effectloader.h" +#include "scripting/scriptedeffect.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace KWin; +using namespace std::chrono_literals; + +static const QString s_socketName = QStringLiteral("wayland_test_effects_scripts-0"); + +class ScriptedEffectsTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testEffectsHandler(); + void testEffectsContext(); + void testShortcuts(); + void testAnimations_data(); + void testAnimations(); + void testScreenEdge(); + void testScreenEdgeTouch(); + void testFullScreenEffect_data(); + void testFullScreenEffect(); + void testKeepAlive_data(); + void testKeepAlive(); + void testGrab(); + void testGrabAlreadyGrabbedWindow(); + void testGrabAlreadyGrabbedWindowForced(); + void testUngrab(); + void testRedirect_data(); + void testRedirect(); + void testComplete(); + +private: + ScriptedEffect *loadEffect(const QString &name); +}; + +class ScriptedEffectWithDebugSpy : public KWin::ScriptedEffect +{ + Q_OBJECT +public: + ScriptedEffectWithDebugSpy(); + bool load(const QString &name); + using AnimationEffect::AniMap; + using AnimationEffect::state; + Q_INVOKABLE void sendTestResponse(const QString &out); // proxies triggers out from the tests + QList actions(); // returns any QActions owned by the ScriptEngine +Q_SIGNALS: + void testOutput(const QString &data); +}; + +void ScriptedEffectWithDebugSpy::sendTestResponse(const QString &out) +{ + Q_EMIT testOutput(out); +} + +QList ScriptedEffectWithDebugSpy::actions() +{ + return findChildren(QString(), Qt::FindDirectChildrenOnly); +} + +ScriptedEffectWithDebugSpy::ScriptedEffectWithDebugSpy() + : ScriptedEffect() +{ +} + +bool ScriptedEffectWithDebugSpy::load(const QString &name) +{ + auto selfContext = engine()->newQObject(this); + QJSEngine::setObjectOwnership(this, QJSEngine::CppOwnership); + const QString path = QFINDTESTDATA("./scripts/" + name + ".js"); + engine()->globalObject().setProperty("sendTestResponse", selfContext.property("sendTestResponse")); + if (!init(name, path)) { + return false; + } + + // inject our newly created effect to be registered with the EffectsHandler::loaded_effects + // this is private API so some horrible code is used to find the internal effectloader + // and register ourselves + auto children = effects->children(); + for (auto it = children.begin(); it != children.end(); ++it) { + if (qstrcmp((*it)->metaObject()->className(), "KWin::EffectLoader") != 0) { + continue; + } + QMetaObject::invokeMethod(*it, "effectLoaded", Q_ARG(KWin::Effect *, this), Q_ARG(QString, name)); + break; + } + + return effects->isEffectLoaded(name); +} + +void ScriptedEffectsTest::initTestCase() +{ + if (!Test::renderNodeAvailable()) { + QSKIP("no render node available"); + return; + } + qRegisterMetaType(); + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + // disable all effects - we don't want to have it interact with the rendering + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup plugins(config, QStringLiteral("Plugins")); + const auto builtinNames = EffectLoader().listOfKnownEffects(); + for (QString name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + + config->sync(); + kwinApp()->setConfig(config); + + qputenv("KWIN_COMPOSE", QByteArrayLiteral("O2")); + qputenv("KWIN_EFFECTS_FORCE_ANIMATIONS", "1"); + kwinApp()->start(); + Test::setOutputConfig({Rect(0, 0, 1280, 1024)}); + + KWin::VirtualDesktopManager::self()->setCount(2); +} + +void ScriptedEffectsTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void ScriptedEffectsTest::cleanup() +{ + Test::destroyWaylandConnection(); + + effects->unloadAllEffects(); + QVERIFY(effects->loadedEffects().isEmpty()); + + KWin::VirtualDesktopManager::self()->setCurrent(1); +} + +void ScriptedEffectsTest::testEffectsHandler() +{ + // this triggers and tests some of the signals in EffectHandler, which is exposed to JS as context property "effects" + auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean + QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput); + auto waitFor = [&effectOutputSpy](const QString &expected) { + QVERIFY(effectOutputSpy.count() > 0 || effectOutputSpy.wait()); + QCOMPARE(effectOutputSpy.first().first(), expected); + effectOutputSpy.removeFirst(); + }; + QVERIFY(effect->load("effectsHandler")); + + // trigger windowAdded signal + + // create a window + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + shellSurface->set_title("WindowA"); + auto *c = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeWindow(), c); + + waitFor("windowAdded - WindowA"); + waitFor("stackingOrder - 1 WindowA"); + + // windowMinimsed + c->setMinimized(true); + waitFor("windowMinimized - WindowA"); + + c->setMinimized(false); + waitFor("windowUnminimized - WindowA"); + + surface.reset(); + waitFor("windowClosed - WindowA"); + + // desktop management + KWin::VirtualDesktopManager::self()->setCurrent(2); + waitFor("desktopChanged - 1 2"); +} + +void ScriptedEffectsTest::testEffectsContext() +{ + // this tests misc non-objects exposed to the script engine: animationTime, displaySize, use of external enums + + auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean + QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput); + QVERIFY(effect->load("effectContext")); + QCOMPARE(effectOutputSpy[0].first(), "1280x1024"); + QCOMPARE(effectOutputSpy[1].first(), "100"); + QCOMPARE(effectOutputSpy[2].first(), "2"); + QCOMPARE(effectOutputSpy[3].first(), "0"); +} + +void ScriptedEffectsTest::testShortcuts() +{ +#if !KWIN_BUILD_GLOBALSHORTCUTS + QSKIP("Can't test shortcuts without shortcuts"); + return; +#endif + + // this tests method registerShortcut + auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean + QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput); + QVERIFY(effect->load("shortcutsTest")); + QCOMPARE(effect->actions().count(), 1); + auto action = effect->actions()[0]; + QCOMPARE(action->objectName(), "testShortcut"); + QCOMPARE(action->text(), "Test Shortcut"); + QCOMPARE(KGlobalAccel::self()->shortcut(action).first(), QKeySequence("Meta+Shift+Y")); + action->trigger(); + QCOMPARE(effectOutputSpy[0].first(), "shortcutTriggered"); +} + +void ScriptedEffectsTest::testAnimations_data() +{ + QTest::addColumn("file"); + QTest::addColumn("animationCount"); + + QTest::newRow("single") << "animationTest" << 1; + QTest::newRow("multi") << "animationTestMulti" << 2; +} + +void ScriptedEffectsTest::testAnimations() +{ + // this tests animate/set/cancel + // methods take either an int or an array, as forced in the data above + // also splits animate vs effects.animate(..) + + QFETCH(QString, file); + QFETCH(int, animationCount); + + auto *effect = new ScriptedEffectWithDebugSpy; + QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput); + QVERIFY(effect->load(file)); + + // animated after window added connect + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + shellSurface->set_title("Window 1"); + auto *c = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeWindow(), c); + + { + const auto &state = effect->state(); + QCOMPARE(state.size(), 1); + QVERIFY(state.contains(c->effectWindow())); + const auto &animationsForWindow = state.at(c->effectWindow()).first; + QCOMPARE(animationsForWindow.size(), animationCount); + QCOMPARE(animationsForWindow[0].timeLine.duration(), 100ms); + QCOMPARE(animationsForWindow[0].to, FPx2(1.4)); + QCOMPARE(animationsForWindow[0].attribute, AnimationEffect::Scale); + QCOMPARE(animationsForWindow[0].timeLine.easingCurve().type(), QEasingCurve::OutCubic); + QCOMPARE(animationsForWindow[0].terminationFlags, + AnimationEffect::TerminateAtSource | AnimationEffect::TerminateAtTarget); + + if (animationCount == 2) { + QCOMPARE(animationsForWindow[1].timeLine.duration(), 100ms); + QCOMPARE(animationsForWindow[1].to, FPx2(0.0)); + QCOMPARE(animationsForWindow[1].attribute, AnimationEffect::Opacity); + QCOMPARE(animationsForWindow[1].terminationFlags, + AnimationEffect::TerminateAtSource | AnimationEffect::TerminateAtTarget); + } + } + QCOMPARE(effectOutputSpy[0].first(), "true"); + + // window state changes, scale should be retargetted + + c->setMinimized(true); + { + const auto &state = effect->state(); + QCOMPARE(state.size(), 1); + const auto &animationsForWindow = state.at(c->effectWindow()).first; + QCOMPARE(animationsForWindow.size(), animationCount); + QCOMPARE(animationsForWindow[0].timeLine.duration(), 200ms); + QCOMPARE(animationsForWindow[0].to, FPx2(1.5)); + QCOMPARE(animationsForWindow[0].attribute, AnimationEffect::Scale); + QCOMPARE(animationsForWindow[0].terminationFlags, + AnimationEffect::TerminateAtSource | AnimationEffect::TerminateAtTarget); + if (animationCount == 2) { + QCOMPARE(animationsForWindow[1].timeLine.duration(), 200ms); + QCOMPARE(animationsForWindow[1].to, FPx2(1.5)); + QCOMPARE(animationsForWindow[1].attribute, AnimationEffect::Opacity); + QCOMPARE(animationsForWindow[1].terminationFlags, + AnimationEffect::TerminateAtSource | AnimationEffect::TerminateAtTarget); + } + } + c->setMinimized(false); + { + const auto &state = effect->state(); + QCOMPARE(state.size(), 0); + } +} + +void ScriptedEffectsTest::testScreenEdge() +{ + // this test checks registerScreenEdge functions + auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean + QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput); + QVERIFY(effect->load("screenEdgeTest")); + effect->borderActivated(KWin::ElectricTopRight); + QCOMPARE(effectOutputSpy.count(), 1); +} + +void ScriptedEffectsTest::testScreenEdgeTouch() +{ + // this test checks registerTouchScreenEdge functions + auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean + QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput); + QVERIFY(effect->load("screenEdgeTouchTest")); + effect->actions()[0]->trigger(); + QCOMPARE(effectOutputSpy.count(), 1); +} + +void ScriptedEffectsTest::testFullScreenEffect_data() +{ + QTest::addColumn("file"); + + QTest::newRow("single") << "fullScreenEffectTest"; + QTest::newRow("multi") << "fullScreenEffectTestMulti"; + QTest::newRow("global") << "fullScreenEffectTestGlobal"; +} + +void ScriptedEffectsTest::testFullScreenEffect() +{ + QFETCH(QString, file); + + auto *effectMain = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean + QSignalSpy effectOutputSpy(effectMain, &ScriptedEffectWithDebugSpy::testOutput); + QSignalSpy fullScreenEffectActiveSpy(effects, &EffectsHandler::hasActiveFullScreenEffectChanged); + QSignalSpy isActiveFullScreenEffectSpy(effectMain, &ScriptedEffect::isActiveFullScreenEffectChanged); + + QVERIFY(effectMain->load(file)); + + // load any random effect from another test to confirm fullscreen effect state is correctly + // shown as being someone else + auto effectOther = new ScriptedEffectWithDebugSpy(); + QVERIFY(effectOther->load("screenEdgeTouchTest")); + QSignalSpy isActiveFullScreenEffectSpyOther(effectOther, &ScriptedEffect::isActiveFullScreenEffectChanged); + + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + shellSurface->set_title("Window 1"); + auto *c = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeWindow(), c); + + QCOMPARE(effects->hasActiveFullScreenEffect(), false); + QCOMPARE(effectMain->isActiveFullScreenEffect(), false); + + // trigger animation + KWin::VirtualDesktopManager::self()->setCurrent(2); + + QCOMPARE(effects->activeFullScreenEffect(), effectMain); + QCOMPARE(effects->hasActiveFullScreenEffect(), true); + QCOMPARE(fullScreenEffectActiveSpy.count(), 1); + + QCOMPARE(effectMain->isActiveFullScreenEffect(), true); + QCOMPARE(isActiveFullScreenEffectSpy.count(), 1); + + QCOMPARE(effectOther->isActiveFullScreenEffect(), false); + QCOMPARE(isActiveFullScreenEffectSpyOther.count(), 0); + + // after 500ms trigger another full screen animation + QTest::qWait(500); + KWin::VirtualDesktopManager::self()->setCurrent(1); + QCOMPARE(effects->activeFullScreenEffect(), effectMain); + + // after 1000ms (+a safety margin for time based tests) we should still be the active full screen effect + // despite first animation expiring + QTest::qWait(500 + 100); + QCOMPARE(effects->activeFullScreenEffect(), effectMain); + + // after 1500ms (+a safetey margin) we should have no full screen effect + QTest::qWait(500 + 100); + QCOMPARE(effects->activeFullScreenEffect(), nullptr); +} + +void ScriptedEffectsTest::testKeepAlive_data() +{ + QTest::addColumn("file"); + QTest::addColumn("keepAlive"); + + QTest::newRow("keep") << "keepAliveTest" << true; + QTest::newRow("don't keep") << "keepAliveTestDontKeep" << false; +} + +void ScriptedEffectsTest::testKeepAlive() +{ + // this test checks whether closed windows are kept alive + // when keepAlive property is set to true(false) + + QFETCH(QString, file); + QFETCH(bool, keepAlive); + + auto *effect = new ScriptedEffectWithDebugSpy; + QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput); + QVERIFY(effect->load(file)); + + // create a window + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + auto *c = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeWindow(), c); + + // no active animations at the beginning + QCOMPARE(effect->state().size(), 0); + + // trigger windowClosed signal + QSignalSpy deletedRemovedSpy(workspace(), &Workspace::deletedRemoved); + surface.reset(); + QVERIFY(effectOutputSpy.count() == 1 || effectOutputSpy.wait()); + + if (keepAlive) { + QCOMPARE(effect->state().size(), 1); + QCOMPARE(deletedRemovedSpy.count(), 0); + + QTest::qWait(500); + QCOMPARE(effect->state().size(), 1); + QCOMPARE(deletedRemovedSpy.count(), 0); + + QTest::qWait(500 + 100); // 100ms is extra safety margin + QCOMPARE(deletedRemovedSpy.count(), 1); + QCOMPARE(effect->state().size(), 0); + } else { + // the test effect doesn't keep the window alive, so it should be + // removed immediately + QVERIFY(deletedRemovedSpy.count() == 1 || deletedRemovedSpy.wait(100)); // 100ms is less than duration of the animation + QCOMPARE(effect->state().size(), 0); + } +} + +void ScriptedEffectsTest::testGrab() +{ + // this test verifies that scripted effects can grab windows that are + // not already grabbed + + // load the test effect + auto effect = new ScriptedEffectWithDebugSpy; + QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput); + QVERIFY(effect->load(QStringLiteral("grabTest"))); + + // create test window + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QCOMPARE(workspace()->activeWindow(), window); + + // the test effect should grab the test window successfully + QCOMPARE(effectOutputSpy.count(), 1); + QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok")); + QCOMPARE(window->effectWindow()->data(WindowAddedGrabRole).value(), effect); +} + +void ScriptedEffectsTest::testGrabAlreadyGrabbedWindow() +{ + // this test verifies that scripted effects cannot grab already grabbed + // windows (unless force is set to true of course) + + // load effect that will hold the window grab + auto owner = new ScriptedEffectWithDebugSpy; + QSignalSpy ownerOutputSpy(owner, &ScriptedEffectWithDebugSpy::testOutput); + QVERIFY(owner->load(QStringLiteral("grabAlreadyGrabbedWindowTest_owner"))); + + // load effect that will try to grab already grabbed window + auto grabber = new ScriptedEffectWithDebugSpy; + QSignalSpy grabberOutputSpy(grabber, &ScriptedEffectWithDebugSpy::testOutput); + QVERIFY(grabber->load(QStringLiteral("grabAlreadyGrabbedWindowTest_grabber"))); + + // create test window + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QCOMPARE(workspace()->activeWindow(), window); + + // effect that initially held the grab should still hold the grab + QCOMPARE(ownerOutputSpy.count(), 1); + QCOMPARE(ownerOutputSpy.first().first(), QStringLiteral("ok")); + QCOMPARE(window->effectWindow()->data(WindowAddedGrabRole).value(), owner); + + // effect that tried to grab already grabbed window should fail miserably + QCOMPARE(grabberOutputSpy.count(), 1); + QCOMPARE(grabberOutputSpy.first().first(), QStringLiteral("fail")); +} + +void ScriptedEffectsTest::testGrabAlreadyGrabbedWindowForced() +{ + // this test verifies that scripted effects can steal window grabs when + // they forcefully try to grab windows + + // load effect that initially will be holding the window grab + auto owner = new ScriptedEffectWithDebugSpy; + QSignalSpy ownerOutputSpy(owner, &ScriptedEffectWithDebugSpy::testOutput); + QVERIFY(owner->load(QStringLiteral("grabAlreadyGrabbedWindowForcedTest_owner"))); + + // load effect that will try to steal the window grab + auto thief = new ScriptedEffectWithDebugSpy; + QSignalSpy thiefOutputSpy(thief, &ScriptedEffectWithDebugSpy::testOutput); + QVERIFY(thief->load(QStringLiteral("grabAlreadyGrabbedWindowForcedTest_thief"))); + + // create test window + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QCOMPARE(workspace()->activeWindow(), window); + + // verify that the owner in fact held the grab + QCOMPARE(ownerOutputSpy.count(), 1); + QCOMPARE(ownerOutputSpy.first().first(), QStringLiteral("ok")); + + // effect that grabbed the test window forcefully should now hold the grab + QCOMPARE(thiefOutputSpy.count(), 1); + QCOMPARE(thiefOutputSpy.first().first(), QStringLiteral("ok")); + QCOMPARE(window->effectWindow()->data(WindowAddedGrabRole).value(), thief); +} + +void ScriptedEffectsTest::testUngrab() +{ + // this test verifies that scripted effects can ungrab windows that they + // are previously grabbed + + // load the test effect + auto effect = new ScriptedEffectWithDebugSpy; + QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput); + QVERIFY(effect->load(QStringLiteral("ungrabTest"))); + + // create test window + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QCOMPARE(workspace()->activeWindow(), window); + + // the test effect should grab the test window successfully + QCOMPARE(effectOutputSpy.count(), 1); + QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok")); + QCOMPARE(window->effectWindow()->data(WindowAddedGrabRole).value(), effect); + + // when the test effect sees that a window was minimized, it will try to ungrab it + effectOutputSpy.clear(); + window->setMinimized(true); + + QCOMPARE(effectOutputSpy.count(), 1); + QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok")); + QCOMPARE(window->effectWindow()->data(WindowAddedGrabRole).value(), nullptr); +} + +void ScriptedEffectsTest::testRedirect_data() +{ + QTest::addColumn("file"); + QTest::addColumn("shouldTerminate"); + QTest::newRow("animate/DontTerminateAtSource") << "redirectAnimateDontTerminateTest" << false; + QTest::newRow("animate/TerminateAtSource") << "redirectAnimateTerminateTest" << true; + QTest::newRow("set/DontTerminate") << "redirectSetDontTerminateTest" << false; + QTest::newRow("set/Terminate") << "redirectSetTerminateTest" << true; +} + +void ScriptedEffectsTest::testRedirect() +{ + // this test verifies that redirect() works + + // load the test effect + auto effect = new ScriptedEffectWithDebugSpy; + QFETCH(QString, file); + QVERIFY(effect->load(file)); + + // create test window + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QCOMPARE(workspace()->activeWindow(), window); + + auto around = [](std::chrono::milliseconds elapsed, + std::chrono::milliseconds pivot, + std::chrono::milliseconds margin) { + return std::abs(elapsed.count() - pivot.count()) < margin.count(); + }; + + // initially, the test animation is at the source position + + { + const auto &state = effect->state(); + QCOMPARE(state.size(), 1); + QVERIFY(state.contains(window->effectWindow())); + const auto &animations = state.at(window->effectWindow()).first; + QCOMPARE(animations.size(), 1); + QCOMPARE(animations[0].timeLine.direction(), TimeLine::Forward); + QVERIFY(around(animations[0].timeLine.elapsed(), 0ms, 50ms)); + } + + // minimize the test window after 250ms, when the test effect sees that + // a window was minimized, it will try to reverse animation for it + QTest::qWait(250); + + QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput); + + window->setMinimized(true); + + QCOMPARE(effectOutputSpy.count(), 1); + QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok")); + + { + const auto &state = effect->state(); + QCOMPARE(state.size(), 1); + QVERIFY(state.contains(window->effectWindow())); + const auto &animations = state.at(window->effectWindow()).first; + QCOMPARE(animations.size(), 1); + QCOMPARE(animations[0].timeLine.direction(), TimeLine::Backward); + QVERIFY(around(animations[0].timeLine.elapsed(), 1000ms - 250ms, 50ms)); + } + + // wait for the animation to reach the start position, 100ms is an extra + // safety margin + QTest::qWait(250 + 100); + + QFETCH(bool, shouldTerminate); + if (shouldTerminate) { + const auto &state = effect->state(); + QCOMPARE(state.size(), 0); + } else { + const auto &state = effect->state(); + QCOMPARE(state.size(), 1); + QVERIFY(state.contains(window->effectWindow())); + const auto &animations = state.at(window->effectWindow()).first; + QCOMPARE(animations.size(), 1); + QCOMPARE(animations[0].timeLine.direction(), TimeLine::Backward); + QCOMPARE(animations[0].timeLine.elapsed(), 1000ms); + QCOMPARE(animations[0].timeLine.value(), 0.0); + } +} + +void ScriptedEffectsTest::testComplete() +{ + // this test verifies that complete works + + // load the test effect + auto effect = new ScriptedEffectWithDebugSpy; + QVERIFY(effect->load(QStringLiteral("completeTest"))); + + // create test window + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QCOMPARE(workspace()->activeWindow(), window); + + auto around = [](std::chrono::milliseconds elapsed, + std::chrono::milliseconds pivot, + std::chrono::milliseconds margin) { + return std::abs(elapsed.count() - pivot.count()) < margin.count(); + }; + + // initially, the test animation should be at the start position + { + const auto &state = effect->state(); + QCOMPARE(state.size(), 1); + QVERIFY(state.contains(window->effectWindow())); + const auto &animations = state.at(window->effectWindow()).first; + QCOMPARE(animations.size(), 1); + QVERIFY(around(animations[0].timeLine.elapsed(), 0ms, 50ms)); + QVERIFY(!animations[0].timeLine.done()); + } + + // wait for 250ms + QTest::qWait(250); + + { + const auto &state = effect->state(); + QCOMPARE(state.size(), 1); + QVERIFY(state.contains(window->effectWindow())); + const auto &animations = state.at(window->effectWindow()).first; + QCOMPARE(animations.size(), 1); + QVERIFY(around(animations[0].timeLine.elapsed(), 250ms, 50ms)); + QVERIFY(!animations[0].timeLine.done()); + } + + // minimize the test window, when the test effect sees that a window was + // minimized, it will try to complete animation for it + QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput); + + window->setMinimized(true); + + QCOMPARE(effectOutputSpy.count(), 1); + QCOMPARE(effectOutputSpy.first().first(), QStringLiteral("ok")); + + { + const auto &state = effect->state(); + QCOMPARE(state.size(), 1); + QVERIFY(state.contains(window->effectWindow())); + const auto &animations = state.at(window->effectWindow()).first; + QCOMPARE(animations.size(), 1); + QCOMPARE(animations[0].timeLine.elapsed(), 1000ms); + QVERIFY(animations[0].timeLine.done()); + } +} + +WAYLANDTEST_MAIN(ScriptedEffectsTest) +#include "scripted_effects_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/animationTest.js b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/animationTest.js new file mode 100644 index 0000000000..83e4beb5b3 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/animationTest.js @@ -0,0 +1,12 @@ +effects.windowAdded.connect(function(w) { + w.anim1 = effect.animate(w, Effect.Scale, 100, 1.4, 0.2, 0, QEasingCurve.OutCubic); + sendTestResponse(typeof(w.anim1) == "number"); + + w.minimizedChanged.connect(() => { + if (w.minimized) { + retarget(w.anim1, 1.5, 200); + } else { + cancel(w.anim1); + } + }); +}); diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/animationTestMulti.js b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/animationTestMulti.js new file mode 100644 index 0000000000..6f1038bcbc --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/animationTestMulti.js @@ -0,0 +1,24 @@ +effects.windowAdded.connect(function(w) { + w.anim1 = animate({ + window: w, + duration: 100, + animations: [{ + type: Effect.Scale, + to: 1.4, + curve: QEasingCurve.OutCubic + }, { + type: Effect.Opacity, + curve: QEasingCurve.OutCubic, + to: 0.0 + }] + }); + sendTestResponse(typeof(w.anim1) == "object" && Array.isArray(w.anim1)); + + w.minimizedChanged.connect(() => { + if (w.minimized) { + retarget(w.anim1, 1.5, 200); + } else { + cancel(w.anim1); + } + }); +}); diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/completeTest.js b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/completeTest.js new file mode 100644 index 0000000000..055fa48cc8 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/completeTest.js @@ -0,0 +1,22 @@ +effects.windowAdded.connect(function (window) { + window.animation = set({ + window: window, + curve: QEasingCurve.Linear, + duration: animationTime(1000), + type: Effect.Opacity, + from: 0, + to: 1, + keepAlive: false + }); + + window.minimizedChanged.connect(() => { + if (!window.minimized) { + return; + } + if (complete(window.animation)) { + sendTestResponse('ok'); + } else { + sendTestResponse('fail'); + } + }); +}); diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/effectContext.js b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/effectContext.js new file mode 100644 index 0000000000..193afaba90 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/effectContext.js @@ -0,0 +1,6 @@ +sendTestResponse(displayWidth() + "x" + displayHeight()); +sendTestResponse(animationTime(100)); + +//test enums for Effect / QEasingCurve +sendTestResponse(Effect.Saturation) +sendTestResponse(QEasingCurve.Linear) diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/effectsHandler.js b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/effectsHandler.js new file mode 100644 index 0000000000..ee98578341 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/effectsHandler.js @@ -0,0 +1,18 @@ +effects.windowAdded.connect(function(window) { + sendTestResponse("windowAdded - " + window.caption); + sendTestResponse("stackingOrder - " + effects.stackingOrder.length + " " + effects.stackingOrder[0].caption); + + window.minimizedChanged.connect(() => { + if (window.minimized) { + sendTestResponse("windowMinimized - " + window.caption); + } else { + sendTestResponse("windowUnminimized - " + window.caption); + } + }); +}); +effects.windowClosed.connect(function(window) { + sendTestResponse("windowClosed - " + window.caption); +}); +effects.desktopChanged.connect(function(old, current) { + sendTestResponse("desktopChanged - " + old.x11DesktopNumber + " " + current.x11DesktopNumber); +}); diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/fullScreenEffectTest.js b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/fullScreenEffectTest.js new file mode 100644 index 0000000000..36e8317255 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/fullScreenEffectTest.js @@ -0,0 +1,8 @@ +effects.desktopChanged.connect(function(old, current) { + var stackingOrder = effects.stackingOrder; + for (var i=0; i { + if (!window.minimized) { + return; + } + if (redirect(window.animation, Effect.Backward, Effect.DontTerminate)) { + sendTestResponse('ok'); + } else { + sendTestResponse('fail'); + } + }); +}); diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/redirectAnimateTerminateTest.js b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/redirectAnimateTerminateTest.js new file mode 100644 index 0000000000..22422e8571 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/redirectAnimateTerminateTest.js @@ -0,0 +1,21 @@ +effects.windowAdded.connect(function (window) { + window.animation = animate({ + window: window, + curve: QEasingCurve.Linear, + duration: animationTime(1000), + type: Effect.Opacity, + from: 0.0, + to: 1.0 + }); + + window.minimizedChanged.connect(() => { + if (!window.minimized) { + return; + } + if (redirect(window.animation, Effect.Backward, Effect.TerminateAtSource)) { + sendTestResponse('ok'); + } else { + sendTestResponse('fail'); + } + }); +}); diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/redirectSetDontTerminateTest.js b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/redirectSetDontTerminateTest.js new file mode 100644 index 0000000000..8f24b51509 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/redirectSetDontTerminateTest.js @@ -0,0 +1,22 @@ +effects.windowAdded.connect(function (window) { + window.animation = set({ + window: window, + curve: QEasingCurve.Linear, + duration: animationTime(1000), + type: Effect.Opacity, + from: 0.0, + to: 1.0, + keepAlive: false + }); + + window.minimizedChanged.connect(() => { + if (!window.minimized) { + return; + } + if (redirect(window.animation, Effect.Backward, Effect.DontTerminate)) { + sendTestResponse('ok'); + } else { + sendTestResponse('fail'); + } + }); +}); diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/redirectSetTerminateTest.js b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/redirectSetTerminateTest.js new file mode 100644 index 0000000000..bf7bbbb23f --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/redirectSetTerminateTest.js @@ -0,0 +1,22 @@ +effects.windowAdded.connect(function (window) { + window.animation = set({ + window: window, + curve: QEasingCurve.Linear, + duration: animationTime(1000), + type: Effect.Opacity, + from: 0.0, + to: 1.0, + keepAlive: false + }); + + window.minimizedChanged.connect(() => { + if (!window.minimized) { + return; + } + if (redirect(window.animation, Effect.Backward, Effect.TerminateAtSource)) { + sendTestResponse('ok'); + } else { + sendTestResponse('fail'); + } + }); +}); diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/screenEdgeTest.js b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/screenEdgeTest.js new file mode 100644 index 0000000000..645137cb32 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/screenEdgeTest.js @@ -0,0 +1,3 @@ +registerScreenEdge(1, function() { + sendTestResponse("triggered"); +}); diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/screenEdgeTouchTest.js b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/screenEdgeTouchTest.js new file mode 100644 index 0000000000..6107f69bc0 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/screenEdgeTouchTest.js @@ -0,0 +1,3 @@ +registerTouchScreenEdge(1, function() { + sendTestResponse("triggered"); +}); diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/shortcutsTest.js b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/shortcutsTest.js new file mode 100644 index 0000000000..0e3fe7eaf1 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/shortcutsTest.js @@ -0,0 +1,3 @@ +registerShortcut("testShortcut", "Test Shortcut", "Meta+Shift+Y", function() { + sendTestResponse("shortcutTriggered"); +}); diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/ungrabTest.js b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/ungrabTest.js new file mode 100644 index 0000000000..ed96729079 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/scripts/ungrabTest.js @@ -0,0 +1,18 @@ +effects.windowAdded.connect(function (window) { + if (effect.grab(window, Effect.WindowAddedGrabRole)) { + sendTestResponse('ok'); + } else { + sendTestResponse('fail'); + } + + window.minimizedChanged.connect(() => { + if (!window.minimized) { + return; + } + if (effect.ungrab(window, Effect.WindowAddedGrabRole)) { + sendTestResponse('ok'); + } else { + sendTestResponse('fail'); + } + }); +}); diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/slidingpopups_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/effects/slidingpopups_test.cpp new file mode 100644 index 0000000000..abe098254c --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/slidingpopups_test.cpp @@ -0,0 +1,196 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "effect/effecthandler.h" +#include "effect/effectloader.h" +#include "kwin_wayland_test.h" +#include "wayland_server.h" +#include "workspace.h" + +#include + +#include +#include +#include +#include + +using namespace KWin; +static const QString s_socketName = QStringLiteral("wayland_test_effects_slidingpopups-0"); + +class SlidingPopupsTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testWithOtherEffectWayland_data(); + void testWithOtherEffectWayland(); +}; + +void SlidingPopupsTest::initTestCase() +{ + if (!Test::renderNodeAvailable()) { + QSKIP("no render node available"); + return; + } + qputenv("XDG_DATA_DIRS", QCoreApplication::applicationDirPath().toUtf8()); + qRegisterMetaType(); + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + // disable all effects - we don't want to have it interact with the rendering + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup plugins(config, QStringLiteral("Plugins")); + const auto builtinNames = EffectLoader().listOfKnownEffects(); + for (QString name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + KConfigGroup wobblyGroup = config->group(QStringLiteral("Effect-Wobbly")); + wobblyGroup.writeEntry(QStringLiteral("Settings"), QStringLiteral("Custom")); + wobblyGroup.writeEntry(QStringLiteral("OpenEffect"), true); + wobblyGroup.writeEntry(QStringLiteral("CloseEffect"), true); + + config->sync(); + kwinApp()->setConfig(config); + + qputenv("KWIN_COMPOSE", QByteArrayLiteral("O2")); + qputenv("KWIN_EFFECTS_FORCE_ANIMATIONS", "1"); + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void SlidingPopupsTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void SlidingPopupsTest::cleanup() +{ + Test::destroyWaylandConnection(); + while (!effects->loadedEffects().isEmpty()) { + const QString effect = effects->loadedEffects().first(); + effects->unloadEffect(effect); + QVERIFY(!effects->isEffectLoaded(effect)); + } +} + +void SlidingPopupsTest::testWithOtherEffectWayland_data() +{ + QTest::addColumn("effectsToLoad"); + + QTest::newRow("fade, slide") << QStringList{QStringLiteral("fade"), QStringLiteral("slidingpopups")}; + QTest::newRow("slide, fade") << QStringList{QStringLiteral("slidingpopups"), QStringLiteral("fade")}; + QTest::newRow("scale, slide") << QStringList{QStringLiteral("scale"), QStringLiteral("slidingpopups")}; + QTest::newRow("slide, scale") << QStringList{QStringLiteral("slidingpopups"), QStringLiteral("scale")}; + + if (effects->compositingType() & KWin::OpenGLCompositing) { + QTest::newRow("glide, slide") << QStringList{QStringLiteral("glide"), QStringLiteral("slidingpopups")}; + QTest::newRow("slide, glide") << QStringList{QStringLiteral("slidingpopups"), QStringLiteral("glide")}; + QTest::newRow("wobblywindows, slide") << QStringList{QStringLiteral("wobblywindows"), QStringLiteral("slidingpopups")}; + QTest::newRow("slide, wobblywindows") << QStringList{QStringLiteral("slidingpopups"), QStringLiteral("wobblywindows")}; + QTest::newRow("fallapart, slide") << QStringList{QStringLiteral("fallapart"), QStringLiteral("slidingpopups")}; + QTest::newRow("slide, fallapart") << QStringList{QStringLiteral("slidingpopups"), QStringLiteral("fallapart")}; + } +} + +void SlidingPopupsTest::testWithOtherEffectWayland() +{ + // this test verifies that slidingpopups effect grabs the window added role + // independently of the sequence how the effects are loaded. + // see BUG 336866 + // the test is like testWithOtherEffect, but simulates using a Wayland window + // find the effectsloader + auto effectloader = effects->findChild(); + QVERIFY(effectloader); + QSignalSpy effectLoadedSpy(effectloader, &AbstractEffectLoader::effectLoaded); + + Effect *slidingPoupus = nullptr; + Effect *otherEffect = nullptr; + QFETCH(QStringList, effectsToLoad); + for (const QString &effectName : effectsToLoad) { + QVERIFY(!effects->isEffectLoaded(effectName)); + QVERIFY(effects->loadEffect(effectName)); + QVERIFY(effects->isEffectLoaded(effectName)); + + QCOMPARE(effectLoadedSpy.count(), 1); + Effect *effect = effectLoadedSpy.first().first().value(); + if (effectName == QStringLiteral("slidingpopups")) { + slidingPoupus = effect; + } else { + otherEffect = effect; + } + effectLoadedSpy.clear(); + } + QVERIFY(slidingPoupus); + QVERIFY(otherEffect); + + QVERIFY(!slidingPoupus->isActive()); + QVERIFY(!otherEffect->isActive()); + QSignalSpy windowAddedSpy(effects, &EffectsHandler::windowAdded); + + // the test created the slide protocol, let's create a Registry and listen for it + std::unique_ptr registry(new KWayland::Client::Registry); + registry->create(Test::waylandConnection()); + + QSignalSpy interfacesAnnouncedSpy(registry.get(), &KWayland::Client::Registry::interfacesAnnounced); + registry->setup(); + QVERIFY(interfacesAnnouncedSpy.wait()); + auto slideInterface = registry->interface(KWayland::Client::Registry::Interface::Slide); + QVERIFY(slideInterface.name != 0); + std::unique_ptr slideManager(registry->createSlideManager(slideInterface.name, slideInterface.version)); + QVERIFY(slideManager); + + // create Wayland window + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface); + std::unique_ptr slide(slideManager->createSlide(surface.get())); + slide->setLocation(KWayland::Client::Slide::Location::Left); + slide->commit(); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface); + QCOMPARE(windowAddedSpy.count(), 0); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(10, 20), Qt::blue); + QVERIFY(window); + QVERIFY(window->isNormalWindow()); + + // sliding popups should be active + QCOMPARE(windowAddedSpy.count(), 1); + QTRY_VERIFY(slidingPoupus->isActive()); + QVERIFY(!otherEffect->isActive()); + + // wait till effect ends + QTRY_VERIFY(!slidingPoupus->isActive()); + QTRY_VERIFY(!otherEffect->isActive()); + + // and destroy the window again + shellSurface.reset(); + surface.reset(); + + QSignalSpy windowClosedSpy(window, &Window::closed); + + QSignalSpy windowDeletedSpy(effects, &EffectsHandler::windowDeleted); + QVERIFY(windowClosedSpy.wait()); + + // again we should have the sliding popups active + QVERIFY(slidingPoupus->isActive()); + QVERIFY(!otherEffect->isActive()); + + QVERIFY(windowDeletedSpy.wait()); + + QCOMPARE(windowDeletedSpy.count(), 1); + QVERIFY(!slidingPoupus->isActive()); + QVERIFY(!otherEffect->isActive()); +} + +WAYLANDTEST_MAIN(SlidingPopupsTest) +#include "slidingpopups_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/toplevel_open_close_animation_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/effects/toplevel_open_close_animation_test.cpp new file mode 100644 index 0000000000..86b53f5f1b --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/toplevel_open_close_animation_test.cpp @@ -0,0 +1,190 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "effect/effecthandler.h" +#include "effect/effectloader.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_effects_toplevel_open_close_animation-0"); + +class ToplevelOpenCloseAnimationTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testAnimateToplevels_data(); + void testAnimateToplevels(); + void testDontAnimatePopups_data(); + void testDontAnimatePopups(); +}; + +void ToplevelOpenCloseAnimationTest::initTestCase() +{ + if (!Test::renderNodeAvailable()) { + QSKIP("no render node available"); + return; + } + qputenv("XDG_DATA_DIRS", QCoreApplication::applicationDirPath().toUtf8()); + + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup plugins(config, QStringLiteral("Plugins")); + const auto builtinNames = EffectLoader().listOfKnownEffects(); + for (const QString &name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + config->sync(); + kwinApp()->setConfig(config); + + qputenv("KWIN_COMPOSE", QByteArrayLiteral("O2")); + qputenv("KWIN_EFFECTS_FORCE_ANIMATIONS", QByteArrayLiteral("1")); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void ToplevelOpenCloseAnimationTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void ToplevelOpenCloseAnimationTest::cleanup() +{ + effects->unloadAllEffects(); + QVERIFY(effects->loadedEffects().isEmpty()); + + Test::destroyWaylandConnection(); +} + +void ToplevelOpenCloseAnimationTest::testAnimateToplevels_data() +{ + QTest::addColumn("effectName"); + + QTest::newRow("Fade") << QStringLiteral("fade"); + QTest::newRow("Glide") << QStringLiteral("glide"); + QTest::newRow("Scale") << QStringLiteral("scale"); +} + +void ToplevelOpenCloseAnimationTest::testAnimateToplevels() +{ + // This test verifies that window open/close animation effects try to + // animate the appearing and the disappearing of toplevel windows. + + // Load effect that will be tested. + QFETCH(QString, effectName); + QVERIFY(effects->loadEffect(effectName)); + QCOMPARE(effects->loadedEffects().count(), 1); + QCOMPARE(effects->loadedEffects().first(), effectName); + Effect *effect = effects->findEffect(effectName); + QVERIFY(effect); + QVERIFY(!effect->isActive()); + + // Create the test window. + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); + + // Close the test window, the effect should start animating the disappearing + // of the window. + QSignalSpy windowClosedSpy(window, &Window::closed); + shellSurface.reset(); + surface.reset(); + QVERIFY(windowClosedSpy.wait()); + QVERIFY(effect->isActive()); + + // Eventually, the animation will be complete. + QTRY_VERIFY(!effect->isActive()); +} + +void ToplevelOpenCloseAnimationTest::testDontAnimatePopups_data() +{ + QTest::addColumn("effectName"); + + QTest::newRow("Fade") << QStringLiteral("fade"); + QTest::newRow("Glide") << QStringLiteral("glide"); + QTest::newRow("Scale") << QStringLiteral("scale"); +} + +void ToplevelOpenCloseAnimationTest::testDontAnimatePopups() +{ + // This test verifies that window open/close animation effects don't try + // to animate popups(e.g. popup menus, tooltips, etc). + + // Create the main window. + std::unique_ptr mainWindowSurface(Test::createSurface()); + QVERIFY(mainWindowSurface != nullptr); + std::unique_ptr mainWindowShellSurface(Test::createXdgToplevelSurface(mainWindowSurface.get())); + QVERIFY(mainWindowShellSurface != nullptr); + Window *mainWindow = Test::renderAndWaitForShown(mainWindowSurface.get(), QSize(100, 50), Qt::blue); + QVERIFY(mainWindow); + + // Load effect that will be tested. + QFETCH(QString, effectName); + QVERIFY(effects->loadEffect(effectName)); + QCOMPARE(effects->loadedEffects().count(), 1); + QCOMPARE(effects->loadedEffects().first(), effectName); + Effect *effect = effects->findEffect(effectName); + QVERIFY(effect); + QVERIFY(!effect->isActive()); + + // Create a popup, it should not be animated. + std::unique_ptr popupSurface(Test::createSurface()); + QVERIFY(popupSurface != nullptr); + std::unique_ptr positioner(Test::createXdgPositioner()); + QVERIFY(positioner); + positioner->set_size(20, 20); + positioner->set_anchor_rect(0, 0, 10, 10); + positioner->set_gravity(Test::XdgPositioner::gravity_bottom_right); + positioner->set_anchor(Test::XdgPositioner::anchor_bottom_left); + std::unique_ptr popupShellSurface(Test::createXdgPopupSurface(popupSurface.get(), mainWindowShellSurface->xdgSurface(), positioner.get())); + QVERIFY(popupShellSurface != nullptr); + Window *popup = Test::renderAndWaitForShown(popupSurface.get(), QSize(20, 20), Qt::red); + QVERIFY(popup); + QVERIFY(popup->isPopupWindow()); + QCOMPARE(popup->transientFor(), mainWindow); + QVERIFY(!effect->isActive()); + + // Destroy the popup, it should not be animated. + QSignalSpy popupClosedSpy(popup, &Window::closed); + popupShellSurface.reset(); + popupSurface.reset(); + QVERIFY(popupClosedSpy.wait()); + QVERIFY(!effect->isActive()); + + // Destroy the main window. + mainWindowSurface.reset(); + QVERIFY(Test::waitForWindowClosed(mainWindow)); +} + +WAYLANDTEST_MAIN(ToplevelOpenCloseAnimationTest) +#include "toplevel_open_close_animation_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/effects/translucency_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/effects/translucency_test.cpp new file mode 100644 index 0000000000..920ead6645 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/effects/translucency_test.cpp @@ -0,0 +1,219 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "compositor.h" +#include "effect/effecthandler.h" +#include "effect/effectloader.h" +#include "pointer_input.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "workspace.h" +#include "x11window.h" + +#include + +#include +#include + +using namespace KWin; +static const QString s_socketName = QStringLiteral("wayland_test_effects_translucency-0"); + +class TranslucencyTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testMoveAfterDesktopChange(); + void testDialogClose(); + +private: + Effect *m_translucencyEffect = nullptr; +}; + +void TranslucencyTest::initTestCase() +{ + qputenv("XDG_DATA_DIRS", QCoreApplication::applicationDirPath().toUtf8()); + qRegisterMetaType(); + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + // disable all effects - we don't want to have it interact with the rendering + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup plugins(config, QStringLiteral("Plugins")); + const auto builtinNames = EffectLoader().listOfKnownEffects(); + for (QString name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + config->group(QStringLiteral("Outline")).writeEntry(QStringLiteral("QmlPath"), QString("/does/not/exist.qml")); + config->group(QStringLiteral("Effect-translucency")).writeEntry(QStringLiteral("Dialogs"), 90); + + config->sync(); + kwinApp()->setConfig(config); + + qputenv("KWIN_EFFECTS_FORCE_ANIMATIONS", "1"); + kwinApp()->start(); + QVERIFY(Compositor::self()); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void TranslucencyTest::init() +{ + // find the effectsloader + auto effectloader = effects->findChild(); + QVERIFY(effectloader); + QSignalSpy effectLoadedSpy(effectloader, &AbstractEffectLoader::effectLoaded); + + QVERIFY(!effects->isEffectLoaded(QStringLiteral("translucency"))); + QVERIFY(effects->loadEffect(QStringLiteral("translucency"))); + QVERIFY(effects->isEffectLoaded(QStringLiteral("translucency"))); + + QCOMPARE(effectLoadedSpy.count(), 1); + m_translucencyEffect = effectLoadedSpy.first().first().value(); + QVERIFY(m_translucencyEffect); +} + +void TranslucencyTest::cleanup() +{ + if (effects->isEffectLoaded(QStringLiteral("translucency"))) { + effects->unloadEffect(QStringLiteral("translucency")); + } + QVERIFY(!effects->isEffectLoaded(QStringLiteral("translucency"))); + m_translucencyEffect = nullptr; +} + +void TranslucencyTest::testMoveAfterDesktopChange() +{ + // test tries to simulate the condition of bug 366081 + QVERIFY(!m_translucencyEffect->isActive()); + + QSignalSpy windowAddedSpy(effects, &EffectsHandler::windowAdded); + + // create an xcb window + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + const Rect windowGeometry(0, 0, 100, 200); + xcb_window_t windowId = xcb_generate_id(c.get()); + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + // we should get a window for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.first().first().value(); + QVERIFY(window); + QCOMPARE(window->window(), windowId); + QVERIFY(window->isDecorated()); + + QMetaObject::invokeMethod(window, "setReadyForPainting"); + QVERIFY(window->readyForPainting()); + + QCOMPARE(windowAddedSpy.count(), 1); + QVERIFY(!m_translucencyEffect->isActive()); + // let's send the window to desktop 2 + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + vds->setCount(2); + const QList desktops = vds->desktops(); + workspace()->sendWindowToDesktops(window, {desktops[1]}, false); + vds->setCurrent(desktops[1]); + QVERIFY(!m_translucencyEffect->isActive()); + KWin::input()->pointer()->warp(window->frameGeometry().center()); + workspace()->performWindowOperation(window, Options::MoveOp); + QVERIFY(m_translucencyEffect->isActive()); + QTest::qWait(200); + QVERIFY(m_translucencyEffect->isActive()); + // now end move resize + window->endInteractiveMoveResize(); + QVERIFY(m_translucencyEffect->isActive()); + QTest::qWait(500); + QTRY_VERIFY(!m_translucencyEffect->isActive()); + + // and destroy the window again + xcb_unmap_window(c.get(), windowId); + xcb_flush(c.get()); + + QSignalSpy windowClosedSpy(window, &X11Window::closed); + QVERIFY(windowClosedSpy.wait()); + xcb_destroy_window(c.get(), windowId); + c.reset(); +} + +void TranslucencyTest::testDialogClose() +{ + // this test simulates the condition of BUG 342716 + // with translucency settings for window type dialog the effect never ends when the window gets destroyed + QVERIFY(!m_translucencyEffect->isActive()); + QSignalSpy windowAddedSpy(effects, &EffectsHandler::windowAdded); + + // create an xcb window + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + const Rect windowGeometry(0, 0, 100, 200); + xcb_window_t windowId = xcb_generate_id(c.get()); + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + NETWinInfo winInfo(c.get(), windowId, rootWindow(), NET::Properties(), NET::Properties2()); + winInfo.setWindowType(NET::Dialog); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + // we should get a window for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.first().first().value(); + QVERIFY(window); + QCOMPARE(window->window(), windowId); + QVERIFY(window->isDecorated()); + QVERIFY(window->isDialog()); + + QCOMPARE(windowAddedSpy.count(), 1); + QTRY_VERIFY(m_translucencyEffect->isActive()); + // and destroy the window again + xcb_unmap_window(c.get(), windowId); + xcb_flush(c.get()); + + QSignalSpy windowClosedSpy(window, &X11Window::closed); + + QSignalSpy windowDeletedSpy(effects, &EffectsHandler::windowDeleted); + QVERIFY(windowClosedSpy.wait()); + if (windowDeletedSpy.isEmpty()) { + QVERIFY(windowDeletedSpy.wait()); + } + QCOMPARE(windowDeletedSpy.count(), 1); + QTRY_VERIFY(!m_translucencyEffect->isActive()); + xcb_destroy_window(c.get(), windowId); + c.reset(); +} + +WAYLANDTEST_MAIN(TranslucencyTest) +#include "translucency_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/fakeinput_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/fakeinput_test.cpp new file mode 100644 index 0000000000..8ce6b96d13 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/fakeinput_test.cpp @@ -0,0 +1,347 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Gräßlin + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "core/inputdevice.h" +#include "input.h" +#include "main.h" +#include "wayland_server.h" + +#include +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_fakeinput-0"); + +class FakeInputTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testPointerMotion(); + void testMotionAbsolute(); + void testPointerButton_data(); + void testPointerButton(); + void testPointerVerticalAxis(); + void testPointerHorizontalAxis(); + void testTouch(); + void testKeyboardKey_data(); + void testKeyboardKey(); + void testKeySym(); + +private: + InputDevice *m_inputDevice = nullptr; +}; + +void FakeInputTest::initTestCase() +{ + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void FakeInputTest::init() +{ + QSignalSpy deviceAddedSpy(input(), &InputRedirection::deviceAdded); + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::FakeInput | Test::AdditionalWaylandInterface::Seat)); + + QVERIFY(deviceAddedSpy.wait()); + m_inputDevice = deviceAddedSpy.last().at(0).value(); +} + +void FakeInputTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void FakeInputTest::testPointerMotion() +{ + Test::FakeInput *fakeInput = Test::waylandFakeInput(); + + // without an authentication we shouldn't get the signals + QSignalSpy pointerMotionSpy(m_inputDevice, &InputDevice::pointerMotion); + fakeInput->pointer_motion(wl_fixed_from_double(1), wl_fixed_from_double(2)); + QVERIFY(Test::waylandSync()); + QVERIFY(pointerMotionSpy.isEmpty()); + + // now let's authenticate the interface + fakeInput->authenticate(QStringLiteral("org.kde.foobar"), QStringLiteral("foobar")); + fakeInput->pointer_motion(wl_fixed_from_double(1), wl_fixed_from_double(2)); + QVERIFY(pointerMotionSpy.wait()); + QCOMPARE(pointerMotionSpy.last().first().toPointF(), QPointF(1, 2)); + + // just for the fun: once more + fakeInput->pointer_motion(wl_fixed_from_double(0), wl_fixed_from_double(0)); + QVERIFY(pointerMotionSpy.wait()); + QCOMPARE(pointerMotionSpy.last().first().toPointF(), QPointF(0, 0)); +} + +void FakeInputTest::testMotionAbsolute() +{ + Test::FakeInput *fakeInput = Test::waylandFakeInput(); + + // without an authentication we shouldn't get the signals + QSignalSpy pointerMotionAbsoluteSpy(m_inputDevice, &InputDevice::pointerMotionAbsolute); + fakeInput->pointer_motion_absolute(wl_fixed_from_double(1), wl_fixed_from_double(2)); + QVERIFY(Test::waylandSync()); + QVERIFY(pointerMotionAbsoluteSpy.isEmpty()); + + // now let's authenticate the interface + fakeInput->authenticate(QStringLiteral("org.kde.foobar"), QStringLiteral("foobar")); + fakeInput->pointer_motion_absolute(wl_fixed_from_double(1), wl_fixed_from_double(2)); + QVERIFY(pointerMotionAbsoluteSpy.wait()); + QCOMPARE(pointerMotionAbsoluteSpy.last().first().toPointF(), QPointF(1, 2)); +} + +void FakeInputTest::testPointerButton_data() +{ + QTest::addColumn("linuxButton"); + + QTest::newRow("left") << quint32(BTN_LEFT); + QTest::newRow("right") << quint32(BTN_RIGHT); + QTest::newRow("middle") << quint32(BTN_MIDDLE); + QTest::newRow("side") << quint32(BTN_SIDE); + QTest::newRow("extra") << quint32(BTN_EXTRA); + QTest::newRow("forward") << quint32(BTN_FORWARD); + QTest::newRow("back") << quint32(BTN_BACK); + QTest::newRow("task") << quint32(BTN_TASK); +} + +void FakeInputTest::testPointerButton() +{ + Test::FakeInput *fakeInput = Test::waylandFakeInput(); + + // without an authentication we shouldn't get the signals + QSignalSpy pointerButtonSpy(m_inputDevice, &InputDevice::pointerButtonChanged); + QFETCH(quint32, linuxButton); + fakeInput->button(linuxButton, WL_POINTER_BUTTON_STATE_PRESSED); + QVERIFY(Test::waylandSync()); + QVERIFY(pointerButtonSpy.isEmpty()); + + // now authenticate + fakeInput->authenticate(QStringLiteral("org.kde.foobar"), QStringLiteral("foobar")); + fakeInput->button(linuxButton, WL_POINTER_BUTTON_STATE_PRESSED); + QVERIFY(pointerButtonSpy.wait()); + QCOMPARE(pointerButtonSpy.last().at(0).value(), linuxButton); + QCOMPARE(pointerButtonSpy.last().at(1).value(), PointerButtonState::Pressed); + + // and release + fakeInput->button(linuxButton, WL_POINTER_BUTTON_STATE_RELEASED); + QVERIFY(pointerButtonSpy.wait()); + QCOMPARE(pointerButtonSpy.last().at(0).value(), linuxButton); + QCOMPARE(pointerButtonSpy.last().at(1).value(), PointerButtonState::Released); +} + +void FakeInputTest::testPointerVerticalAxis() +{ + Test::FakeInput *fakeInput = Test::waylandFakeInput(); + + // without an authentication we shouldn't get the signals + QSignalSpy pointerAxisSpy(m_inputDevice, &InputDevice::pointerAxisChanged); + fakeInput->axis(WL_POINTER_AXIS_VERTICAL_SCROLL, wl_fixed_from_double(15)); + QVERIFY(Test::waylandSync()); + QVERIFY(pointerAxisSpy.isEmpty()); + + // now authenticate + fakeInput->authenticate(QStringLiteral("org.kde.foobar"), QStringLiteral("foobar")); + fakeInput->axis(WL_POINTER_AXIS_VERTICAL_SCROLL, wl_fixed_from_double(15)); + QVERIFY(pointerAxisSpy.wait()); + QCOMPARE(pointerAxisSpy.last().at(0).value(), PointerAxis::Vertical); + QCOMPARE(pointerAxisSpy.last().at(1).value(), 15); +} + +void FakeInputTest::testPointerHorizontalAxis() +{ + Test::FakeInput *fakeInput = Test::waylandFakeInput(); + + // without an authentication we shouldn't get the signals + QSignalSpy pointerAxisSpy(m_inputDevice, &InputDevice::pointerAxisChanged); + fakeInput->axis(WL_POINTER_AXIS_HORIZONTAL_SCROLL, wl_fixed_from_double(15)); + QVERIFY(Test::waylandSync()); + QVERIFY(pointerAxisSpy.isEmpty()); + + // now authenticate + fakeInput->authenticate(QStringLiteral("org.kde.foobar"), QStringLiteral("foobar")); + fakeInput->axis(WL_POINTER_AXIS_HORIZONTAL_SCROLL, wl_fixed_from_double(15)); + QVERIFY(pointerAxisSpy.wait()); + QCOMPARE(pointerAxisSpy.last().at(0).value(), PointerAxis::Horizontal); + QCOMPARE(pointerAxisSpy.last().at(1).value(), 15); +} + +void FakeInputTest::testTouch() +{ + Test::FakeInput *fakeInput = Test::waylandFakeInput(); + + // without an authentication we shouldn't get the signals + QSignalSpy touchDownSpy(m_inputDevice, &InputDevice::touchDown); + QSignalSpy touchUpSpy(m_inputDevice, &InputDevice::touchUp); + QSignalSpy touchMotionSpy(m_inputDevice, &InputDevice::touchMotion); + QSignalSpy touchFrameSpy(m_inputDevice, &InputDevice::touchFrame); + QSignalSpy touchCanceledSpy(m_inputDevice, &InputDevice::touchCanceled); + fakeInput->touch_down(0, wl_fixed_from_double(1), wl_fixed_from_double(2)); + QVERIFY(Test::waylandSync()); + QVERIFY(touchDownSpy.isEmpty()); + + fakeInput->touch_motion(0, wl_fixed_from_double(3), wl_fixed_from_double(4)); + QVERIFY(Test::waylandSync()); + QVERIFY(touchMotionSpy.isEmpty()); + + fakeInput->touch_up(0); + QVERIFY(Test::waylandSync()); + QVERIFY(touchUpSpy.isEmpty()); + + fakeInput->touch_down(1, wl_fixed_from_double(5), wl_fixed_from_double(6)); + QVERIFY(Test::waylandSync()); + QVERIFY(touchDownSpy.isEmpty()); + + fakeInput->touch_frame(); + QVERIFY(Test::waylandSync()); + QVERIFY(touchFrameSpy.isEmpty()); + + fakeInput->touch_cancel(); + QVERIFY(Test::waylandSync()); + QVERIFY(touchCanceledSpy.isEmpty()); + + // now let's authenticate the interface + fakeInput->authenticate(QStringLiteral("org.kde.foobar"), QStringLiteral("foobar")); + fakeInput->touch_down(0, wl_fixed_from_double(1), wl_fixed_from_double(2)); + QVERIFY(touchDownSpy.wait()); + QCOMPARE(touchDownSpy.count(), 1); + QCOMPARE(touchDownSpy.last().at(0).value(), quint32(0)); + QCOMPARE(touchDownSpy.last().at(1).toPointF(), QPointF(1, 2)); + + // Same id should not trigger another touchDown. + fakeInput->touch_down(0, wl_fixed_from_double(5), wl_fixed_from_double(6)); + QVERIFY(Test::waylandSync()); + QCOMPARE(touchDownSpy.count(), 1); + + fakeInput->touch_motion(0, wl_fixed_from_double(3), wl_fixed_from_double(4)); + QVERIFY(touchMotionSpy.wait()); + QCOMPARE(touchMotionSpy.count(), 1); + QCOMPARE(touchMotionSpy.last().at(0).value(), quint32(0)); + QCOMPARE(touchMotionSpy.last().at(1).toPointF(), QPointF(3, 4)); + + // touchMotion with bogus id should not trigger signal. + fakeInput->touch_motion(1, wl_fixed_from_double(3), wl_fixed_from_double(4)); + QVERIFY(Test::waylandSync()); + QCOMPARE(touchMotionSpy.count(), 1); + + fakeInput->touch_up(0); + QVERIFY(touchUpSpy.wait()); + QCOMPARE(touchUpSpy.count(), 1); + QCOMPARE(touchUpSpy.last().at(0).value(), quint32(0)); + + // touchUp with bogus id should not trigger signal. + fakeInput->touch_up(1); + QVERIFY(Test::waylandSync()); + QCOMPARE(touchUpSpy.count(), 1); + + fakeInput->touch_down(1, wl_fixed_from_double(5), wl_fixed_from_double(6)); + QVERIFY(touchDownSpy.wait()); + QCOMPARE(touchDownSpy.count(), 2); + QCOMPARE(touchDownSpy.last().at(0).value(), quint32(1)); + QCOMPARE(touchDownSpy.last().at(1).toPointF(), QPointF(5, 6)); + + fakeInput->touch_frame(); + QVERIFY(touchFrameSpy.wait()); + QCOMPARE(touchFrameSpy.count(), 1); + + fakeInput->touch_cancel(); + QVERIFY(touchCanceledSpy.wait()); + QCOMPARE(touchCanceledSpy.count(), 1); +} + +void FakeInputTest::testKeyboardKey_data() +{ + QTest::addColumn("linuxKey"); + + QTest::newRow("A") << quint32(KEY_A); + QTest::newRow("S") << quint32(KEY_S); + QTest::newRow("D") << quint32(KEY_D); + QTest::newRow("F") << quint32(KEY_F); +} + +void FakeInputTest::testKeyboardKey() +{ + Test::FakeInput *fakeInput = Test::waylandFakeInput(); + + // without an authentication we shouldn't get the signals + QSignalSpy keyboardKeySpy(m_inputDevice, &InputDevice::keyChanged); + QFETCH(quint32, linuxKey); + fakeInput->keyboard_key(linuxKey, WL_KEYBOARD_KEY_STATE_PRESSED); + QVERIFY(Test::waylandSync()); + QVERIFY(keyboardKeySpy.isEmpty()); + + // now authenticate + fakeInput->authenticate(QStringLiteral("org.kde.foobar"), QStringLiteral("foobar")); + fakeInput->keyboard_key(linuxKey, WL_KEYBOARD_KEY_STATE_PRESSED); + QVERIFY(keyboardKeySpy.wait()); + QCOMPARE(keyboardKeySpy.last().at(0).value(), linuxKey); + QCOMPARE(keyboardKeySpy.last().at(1).value(), KeyboardKeyState::Pressed); + + // and release + fakeInput->keyboard_key(linuxKey, WL_KEYBOARD_KEY_STATE_RELEASED); + QVERIFY(keyboardKeySpy.wait()); + QCOMPARE(keyboardKeySpy.last().at(0).value(), linuxKey); + QCOMPARE(keyboardKeySpy.last().at(1).value(), KeyboardKeyState::Released); +} + +void FakeInputTest::testKeySym() +{ + // as keyboard input is complex, test the client gets the right keys as well as kwin + Test::FakeInput *fakeInput = Test::waylandFakeInput(); + fakeInput->authenticate(QStringLiteral("org.kde.foobar"), QStringLiteral("foobar")); + + std::unique_ptr surface = Test::createSurface(); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(window->frameGeometry().size(), QSize(1280, 1024)); + + auto keyboard = new Test::SimpleKeyboard(window); + + auto sendKey = [fakeInput](uint32_t keySym) { + fakeInput->keyboard_keysym(keySym, WL_KEYBOARD_KEY_STATE_PRESSED); + fakeInput->keyboard_keysym(keySym, WL_KEYBOARD_KEY_STATE_RELEASED); + }; + + sendKey(XKB_KEY_a); + sendKey(XKB_KEY_B); + sendKey(XKB_KEY_space); + sendKey(XKB_KEY_adiaeresis); // lowercase a with umlauts + sendKey(XKB_KEY_Adiaeresis); // uppercase a with umlauts + sendKey(XKB_KEY_space); + sendKey(0x01000000 | 0xC548); // korean symbol + sendKey(0x01000000 | 0x1F60A); // smily face + sendKey(XKB_KEY_f); + + QSignalSpy receivedTextChangedSpy(keyboard, &Test::SimpleKeyboard::receviedTextChanged); + bool matched = false; + for (int i = 0; i < 10; ++i) { + if (keyboard->receviedText() == "aB äÄ 안😊f") { + matched = true; + break; + } + receivedTextChangedSpy.wait(); + } + QVERIFY(matched); +} + +} // namespace KWin + +WAYLANDTEST_MAIN(KWin::FakeInputTest) +#include "fakeinput_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/fakes/CMakeLists.txt b/local/recipes/kde/kwin/source/autotests/integration/fakes/CMakeLists.txt new file mode 100644 index 0000000000..da3b74d25c --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/fakes/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(org.kde.kdecoration3) diff --git a/local/recipes/kde/kwin/source/autotests/integration/fakes/org.kde.kdecoration3/CMakeLists.txt b/local/recipes/kde/kwin/source/autotests/integration/fakes/org.kde.kdecoration3/CMakeLists.txt new file mode 100644 index 0000000000..744375cc1f --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/fakes/org.kde.kdecoration3/CMakeLists.txt @@ -0,0 +1,15 @@ +######################################################## +# FakeDecoWithShadows +######################################################## +add_library(fakedecoshadows MODULE fakedecoration_with_shadows.cpp) +set_target_properties(fakedecoshadows PROPERTIES + PREFIX "" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/fakes/org.kde.kdecoration3") +target_link_libraries(fakedecoshadows + PUBLIC + Qt::Core + Qt::Gui + PRIVATE + KDecoration3::KDecoration + KF6::CoreAddons) + diff --git a/local/recipes/kde/kwin/source/autotests/integration/fakes/org.kde.kdecoration3/fakedecoration_with_shadows.cpp b/local/recipes/kde/kwin/source/autotests/integration/fakes/org.kde.kdecoration3/fakedecoration_with_shadows.cpp new file mode 100644 index 0000000000..b3eee8888f --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/fakes/org.kde.kdecoration3/fakedecoration_with_shadows.cpp @@ -0,0 +1,64 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include +#include + +class FakeDecoWithShadows : public KDecoration3::Decoration +{ + Q_OBJECT + +public: + explicit FakeDecoWithShadows(QObject *parent = nullptr, const QVariantList &args = QVariantList()) + : Decoration(parent, args) + { + } + ~FakeDecoWithShadows() override + { + } + + void paint(QPainter *painter, const QRectF &repaintRegion) override + { + } + +public Q_SLOTS: + bool init() override + { + const int shadowSize = 128; + const int offsetTop = 64; + const int offsetLeft = 48; + const QRect shadowRect(0, 0, 4 * shadowSize + 1, 4 * shadowSize + 1); + + QImage shadowTexture(shadowRect.size(), QImage::Format_ARGB32_Premultiplied); + shadowTexture.fill(Qt::transparent); + + const QMargins padding( + shadowSize - offsetLeft, + shadowSize - offsetTop, + shadowSize + offsetLeft, + shadowSize + offsetTop); + + auto decoShadow = std::make_shared(); + decoShadow->setPadding(padding); + decoShadow->setInnerShadowRect(QRect(shadowRect.center(), QSize(1, 1))); + decoShadow->setShadow(shadowTexture); + + setShadow(decoShadow); + return true; + } +}; + +K_PLUGIN_FACTORY_WITH_JSON( + FakeDecoWithShadowsFactory, + "fakedecoration_with_shadows.json", + registerPlugin();) + +#include "fakedecoration_with_shadows.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/fakes/org.kde.kdecoration3/fakedecoration_with_shadows.json b/local/recipes/kde/kwin/source/autotests/integration/fakes/org.kde.kdecoration3/fakedecoration_with_shadows.json new file mode 100644 index 0000000000..028fc62f79 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/fakes/org.kde.kdecoration3/fakedecoration_with_shadows.json @@ -0,0 +1,11 @@ +{ + "KPlugin": { + "Description": "Window decoration to test shadow tile overlaps", + "EnabledByDefault": false, + "Id": "org.kde.test.fakedecowithshadows", + "Name": "Fake Decoration With Shadows" + }, + "org.kde.kdecoration2": { + "blur": false + } +} diff --git a/local/recipes/kde/kwin/source/autotests/integration/fractional_scaling_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/fractional_scaling_test.cpp new file mode 100644 index 0000000000..b3f91c4cfe --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/fractional_scaling_test.cpp @@ -0,0 +1,158 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "pointer_input.h" +#include "wayland/clientconnection.h" +#include "wayland/display.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include +#include + +#include + +// system +#include +#include +#include + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_fractionalScale-0"); + +class TestFractionalScale : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testToplevel(); + void testPopup(); +}; + +void TestFractionalScale::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->start(); + Test::setOutputConfig({ + Test::OutputInfo{ + .geometry = Rect(0, 0, 1280 / 1.25, 1024 / 1.25), + .scale = 1.25, + }, + Test::OutputInfo{ + .geometry = Rect(1280, 0, 1280 / 2, 1024 / 2), + .scale = 2.0, + }, + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1024, 819)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 640, 512)); + QCOMPARE(outputs[0]->scale(), 1.25); + QCOMPARE(outputs[1]->scale(), 2.0); +} + +void TestFractionalScale::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::FractionalScaleManagerV1)); + + workspace()->setActiveOutput(QPoint(640, 512)); + // put mouse in the middle of screen one + KWin::input()->pointer()->warp(QPoint(640, 512)); +} + +void TestFractionalScale::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TestFractionalScale::testToplevel() +{ + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr fractionalScale(Test::createFractionalScaleV1(surface.get())); + QSignalSpy fractionalScaleChanged(fractionalScale.get(), &Test::FractionalScaleV1::preferredScaleChanged); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + + // above call commits the surface and blocks for the configure event. We should have received the scale already + // We are sent the value in 120ths + QCOMPARE(fractionalScaleChanged.count(), 1); + QCOMPARE(fractionalScale->preferredScale(), std::round(1.25 * 120)); + + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QCOMPARE(fractionalScaleChanged.count(), 1); + QCOMPARE(fractionalScale->preferredScale(), std::round(1.25 * 120)); + + // move to screen 2 + window->move(QPoint(1280, 0)); + + QVERIFY(Test::waylandSync()); + QCOMPARE(fractionalScaleChanged.count(), 2); + QCOMPARE(fractionalScale->preferredScale(), std::round(2.0 * 120)); +} + +void TestFractionalScale::testPopup() +{ + std::unique_ptr toplevelSurface(Test::createSurface()); + std::unique_ptr toplevelFractionalScale(Test::createFractionalScaleV1(toplevelSurface.get())); + QSignalSpy toplevelFractionalScaleChanged(toplevelFractionalScale.get(), &Test::FractionalScaleV1::preferredScaleChanged); + std::unique_ptr toplevel(Test::createXdgToplevelSurface(toplevelSurface.get())); + + // above call commits the surface and blocks for the configure event. We should have received the scale already + // We are sent the value in 120ths + QCOMPARE(toplevelFractionalScaleChanged.count(), 1); + QCOMPARE(toplevelFractionalScale->preferredScale(), std::round(1.25 * 120)); + + auto toplevelWindow = Test::renderAndWaitForShown(toplevelSurface.get(), QSize(100, 50), Qt::blue); + QVERIFY(toplevelWindow); + QCOMPARE(toplevelFractionalScaleChanged.count(), 1); + QCOMPARE(toplevelFractionalScale->preferredScale(), std::round(1.25 * 120)); + + std::unique_ptr popupSurface(Test::createSurface()); + std::unique_ptr popupFractionalScale(Test::createFractionalScaleV1(popupSurface.get())); + QSignalSpy popupFractionalScaleChanged(popupFractionalScale.get(), &Test::FractionalScaleV1::preferredScaleChanged); + std::unique_ptr positioner(Test::createXdgPositioner()); + positioner->set_size(10, 10); + positioner->set_anchor_rect(10, 10, 10, 10); + std::unique_ptr popup(Test::createXdgPopupSurface(popupSurface.get(), toplevel->xdgSurface(), positioner.get())); + + // above call commits the surface and blocks for the configure event. We should have received the scale already + // We are sent the value in 120ths + QCOMPARE(popupFractionalScaleChanged.count(), 1); + QCOMPARE(popupFractionalScale->preferredScale(), std::round(1.25 * 120)); + + auto popupWindow = Test::renderAndWaitForShown(popupSurface.get(), QSize(10, 10), Qt::cyan); + QVERIFY(popupWindow); + QCOMPARE(popupFractionalScaleChanged.count(), 1); + QCOMPARE(popupFractionalScale->preferredScale(), std::round(1.25 * 120)); + + // move the parent to screen 2 + toplevelWindow->move(QPoint(1280, 0)); + + QVERIFY(Test::waylandSync()); + QCOMPARE(toplevelFractionalScaleChanged.count(), 2); + QCOMPARE(toplevelFractionalScale->preferredScale(), std::round(2.0 * 120)); + QCOMPARE(popupFractionalScaleChanged.count(), 2); + QCOMPARE(popupFractionalScale->preferredScale(), std::round(2.0 * 120)); +} + +WAYLANDTEST_MAIN(TestFractionalScale) +#include "fractional_scaling_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/generic_scene_opengl_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/generic_scene_opengl_test.cpp new file mode 100644 index 0000000000..0366b7ce0e --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/generic_scene_opengl_test.cpp @@ -0,0 +1,91 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "generic_scene_opengl_test.h" +#include "compositor.h" +#include "core/renderbackend.h" +#include "cursor.h" +#include "effect/effectloader.h" +#include "scene/workspacescene.h" +#include "wayland_server.h" +#include "window.h" + +#include +#include + +using namespace KWin; +static const QString s_socketName = QStringLiteral("wayland_test_kwin_scene_opengl-0"); + +GenericSceneOpenGLTest::GenericSceneOpenGLTest(const QByteArray &envVariable) + : QObject() + , m_envVariable(envVariable) +{ +} + +GenericSceneOpenGLTest::~GenericSceneOpenGLTest() +{ +} + +void GenericSceneOpenGLTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void GenericSceneOpenGLTest::initTestCase() +{ + if (!Test::renderNodeAvailable()) { + QSKIP("no render node available"); + return; + } + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + // disable all effects - we don't want to have it interact with the rendering + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup plugins(config, QStringLiteral("Plugins")); + const auto builtinNames = EffectLoader().listOfKnownEffects(); + for (QString name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + + config->sync(); + kwinApp()->setConfig(config); + + qputenv("XCURSOR_THEME", QByteArrayLiteral("breeze_cursors")); + qputenv("XCURSOR_SIZE", QByteArrayLiteral("24")); + qputenv("KWIN_COMPOSE", m_envVariable); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + QVERIFY(Compositor::self()); + + QCOMPARE(Compositor::self()->backend()->compositingType(), KWin::OpenGLCompositing); +} + +void GenericSceneOpenGLTest::testRestart() +{ + // simple restart of the OpenGL compositor without any windows being shown + QSignalSpy sceneCreatedSpy(KWin::Compositor::self(), &Compositor::sceneCreated); + KWin::Compositor::self()->reinitialize(); + if (sceneCreatedSpy.isEmpty()) { + QVERIFY(sceneCreatedSpy.wait()); + } + QCOMPARE(sceneCreatedSpy.count(), 1); + QCOMPARE(Compositor::self()->backend()->compositingType(), KWin::OpenGLCompositing); + + // trigger a repaint + KWin::Compositor::self()->scene()->addRepaintFull(); + // and wait 100 msec to ensure it's rendered + // TODO: introduce frameRendered signal in SceneOpenGL + QTest::qWait(100); +} + +#include "moc_generic_scene_opengl_test.cpp" diff --git a/local/recipes/kde/kwin/source/autotests/integration/generic_scene_opengl_test.h b/local/recipes/kde/kwin/source/autotests/integration/generic_scene_opengl_test.h new file mode 100644 index 0000000000..177f1eef9e --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/generic_scene_opengl_test.h @@ -0,0 +1,29 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "kwin_wayland_test.h" + +#include + +class GenericSceneOpenGLTest : public QObject +{ + Q_OBJECT +public: + ~GenericSceneOpenGLTest() override; + +protected: + GenericSceneOpenGLTest(const QByteArray &envVariable); +private Q_SLOTS: + void initTestCase(); + void cleanup(); + +private: + void testRestart(); + QByteArray m_envVariable; +}; diff --git a/local/recipes/kde/kwin/source/autotests/integration/globalshortcuts_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/globalshortcuts_test.cpp new file mode 100644 index 0000000000..a891d7695a --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/globalshortcuts_test.cpp @@ -0,0 +1,454 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "input.h" +#include "internalwindow.h" +#include "keyboard_input.h" +#include "pointer_input.h" +#include "useractions.h" +#include "wayland/keyboard.h" +#include "wayland/seat.h" +#include "wayland_server.h" +#include "workspace.h" +#include "x11window.h" +#include "xkb.h" + +#include + +#include + +#include + +#include +#include +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_globalshortcuts-0"); + +class GlobalShortcutsTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testNonLatinLayout_data(); + void testNonLatinLayout(); + void testConsumedShift(); + void testRepeatedTrigger(); + void testUserActionsMenu(); + void testMetaShiftW(); + void testComponseKey(); + void testKeypad(); + void testX11WindowShortcut(); + void testWaylandWindowShortcut(); + void testSetupWindowShortcut(); +}; + +void GlobalShortcutsTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + qputenv("KWIN_XKB_DEFAULT_KEYMAP", "1"); + qputenv("XKB_DEFAULT_RULES", "evdev"); + qputenv("XKB_DEFAULT_LAYOUT", "us,ru"); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void GlobalShortcutsTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); + workspace()->setActiveOutput(QPoint(640, 512)); + KWin::input()->pointer()->warp(QPoint(640, 512)); + + auto xkb = input()->keyboard()->xkb(); + xkb->switchToLayout(0); +} + +void GlobalShortcutsTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +Q_DECLARE_METATYPE(Qt::Modifier) + +void GlobalShortcutsTest::testNonLatinLayout_data() +{ + QTest::addColumn("modifierKey"); + QTest::addColumn("qtModifier"); + QTest::addColumn("key"); + QTest::addColumn("qtKey"); + + // KEY_W is "ц" in the RU layout and "w" in the US layout + // KEY_GRAVE is "ё" in the RU layout and "`" in the US layout + // TAB_KEY is the same both in the US and RU layout + + QTest::newRow("Left Ctrl + Tab") << KEY_LEFTCTRL << Qt::CTRL << KEY_TAB << Qt::Key_Tab; + QTest::newRow("Left Ctrl + W") << KEY_LEFTCTRL << Qt::CTRL << KEY_W << Qt::Key_W; + QTest::newRow("Left Ctrl + `") << KEY_LEFTCTRL << Qt::CTRL << KEY_GRAVE << Qt::Key_QuoteLeft; + + QTest::newRow("Left Alt + Tab") << KEY_LEFTALT << Qt::ALT << KEY_TAB << Qt::Key_Tab; + QTest::newRow("Left Alt + W") << KEY_LEFTALT << Qt::ALT << KEY_W << Qt::Key_W; + QTest::newRow("Left Alt + `") << KEY_LEFTALT << Qt::ALT << KEY_GRAVE << Qt::Key_QuoteLeft; + + QTest::newRow("Left Shift + Tab") << KEY_LEFTSHIFT << Qt::SHIFT << KEY_TAB << Qt::Key_Tab; + + QTest::newRow("Left Meta + Tab") << KEY_LEFTMETA << Qt::META << KEY_TAB << Qt::Key_Tab; + QTest::newRow("Left Meta + W") << KEY_LEFTMETA << Qt::META << KEY_W << Qt::Key_W; + QTest::newRow("Left Meta + `") << KEY_LEFTMETA << Qt::META << KEY_GRAVE << Qt::Key_QuoteLeft; +} + +void GlobalShortcutsTest::testNonLatinLayout() +{ + // Shortcuts on non-Latin layouts should still work, see BUG 375518 + auto xkb = input()->keyboard()->xkb(); + xkb->switchToLayout(1); + QCOMPARE(xkb->layoutName(), QStringLiteral("Russian")); + + QFETCH(int, modifierKey); + QFETCH(Qt::Modifier, qtModifier); + QFETCH(int, key); + QFETCH(Qt::Key, qtKey); + + const QKeySequence seq(qtModifier | qtKey); + + std::unique_ptr action(new QAction(nullptr)); + action->setProperty("componentName", QStringLiteral("kwin")); + action->setObjectName("globalshortcuts-test-non-latin-layout"); + + QSignalSpy triggeredSpy(action.get(), &QAction::triggered); + + KGlobalAccel::self()->stealShortcutSystemwide(seq); + KGlobalAccel::self()->setShortcut(action.get(), {seq}, KGlobalAccel::NoAutoloading); + + quint32 timestamp = 0; + Test::keyboardKeyPressed(modifierKey, timestamp++); + QCOMPARE(input()->keyboardModifiers(), qtModifier); + Test::keyboardKeyPressed(key, timestamp++); + + Test::keyboardKeyReleased(key, timestamp++); + Test::keyboardKeyReleased(modifierKey, timestamp++); + + QTRY_COMPARE_WITH_TIMEOUT(triggeredSpy.count(), 1, 100); +} + +void GlobalShortcutsTest::testConsumedShift() +{ + // this test verifies that a shortcut with a consumed shift modifier triggers + // create the action + std::unique_ptr action(new QAction(nullptr)); + action->setProperty("componentName", QStringLiteral("kwin")); + action->setObjectName(QStringLiteral("globalshortcuts-test-consumed-shift")); + QSignalSpy triggeredSpy(action.get(), &QAction::triggered); + KGlobalAccel::self()->setShortcut(action.get(), QList{Qt::Key_Percent}, KGlobalAccel::NoAutoloading); + + // press shift+5 + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::ShiftModifier); + Test::keyboardKeyPressed(KEY_5, timestamp++); + QTRY_COMPARE(triggeredSpy.count(), 1); + Test::keyboardKeyReleased(KEY_5, timestamp++); + + // release shift + Test::keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); +} + +void GlobalShortcutsTest::testRepeatedTrigger() +{ + // this test verifies that holding a key, triggers repeated global shortcut + // in addition pressing another key should stop triggering the shortcut + + std::unique_ptr action(new QAction(nullptr)); + action->setProperty("componentName", QStringLiteral("kwin")); + action->setObjectName(QStringLiteral("globalshortcuts-test-consumed-shift")); + QSignalSpy triggeredSpy(action.get(), &QAction::triggered); + KGlobalAccel::self()->setShortcut(action.get(), QList{Qt::Key_Percent}, KGlobalAccel::NoAutoloading); + + // we need to configure the key repeat first. It is only enabled on libinput + waylandServer()->seat()->keyboard()->setRepeatInfo(25, 300); + + // press shift+5 + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_WAKEUP, timestamp++); + Test::keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::ShiftModifier); + Test::keyboardKeyPressed(KEY_5, timestamp++); + QTRY_COMPARE(triggeredSpy.count(), 1); + // and should repeat + QVERIFY(triggeredSpy.wait()); + QVERIFY(triggeredSpy.wait()); + // now release the key + Test::keyboardKeyReleased(KEY_5, timestamp++); + QVERIFY(!triggeredSpy.wait(50)); + + Test::keyboardKeyReleased(KEY_WAKEUP, timestamp++); + QVERIFY(!triggeredSpy.wait(50)); + + // release shift + Test::keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); +} + +void GlobalShortcutsTest::testUserActionsMenu() +{ + // this test tries to trigger the window menu with Alt+F3 + // the problem here is that pressing F3 consumes modifiers as it's part of the + // Ctrl+alt+F3 keysym for vt switching. xkbcommon considers all modifiers as consumed + // which a transformation to any keysym would cause + // for more information see: + // https://bugs.freedesktop.org/show_bug.cgi?id=92818 + // https://github.com/xkbcommon/libxkbcommon/issues/17 + + // first create a window + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(window->isActive()); + + quint32 timestamp = 0; + QVERIFY(!workspace()->userActionsMenu()->isShown()); + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + Test::keyboardKeyPressed(KEY_F3, timestamp++); + Test::keyboardKeyReleased(KEY_F3, timestamp++); + QTRY_VERIFY(workspace()->userActionsMenu()->isShown()); + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); +} + +void GlobalShortcutsTest::testMetaShiftW() +{ + // BUG 370341 + std::unique_ptr action(new QAction(nullptr)); + action->setProperty("componentName", QStringLiteral("kwin")); + action->setObjectName(QStringLiteral("globalshortcuts-test-meta-shift-w")); + QSignalSpy triggeredSpy(action.get(), &QAction::triggered); + KGlobalAccel::self()->setShortcut(action.get(), QList{Qt::META | Qt::SHIFT | Qt::Key_W}, KGlobalAccel::NoAutoloading); + + // press meta+shift+w + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::MetaModifier); + Test::keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::ShiftModifier | Qt::MetaModifier); + Test::keyboardKeyPressed(KEY_W, timestamp++); + QTRY_COMPARE(triggeredSpy.count(), 1); + Test::keyboardKeyReleased(KEY_W, timestamp++); + + // release meta+shift + Test::keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); +} + +void GlobalShortcutsTest::testComponseKey() +{ + // BUG 390110 + std::unique_ptr action(new QAction(nullptr)); + action->setProperty("componentName", QStringLiteral("kwin")); + action->setObjectName(QStringLiteral("globalshortcuts-accent")); + QSignalSpy triggeredSpy(action.get(), &QAction::triggered); + KGlobalAccel::self()->setShortcut(action.get(), QList{Qt::NoModifier}, KGlobalAccel::NoAutoloading); + + // press & release ` + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_RESERVED, timestamp++); + Test::keyboardKeyReleased(KEY_RESERVED, timestamp++); + + QTRY_COMPARE(triggeredSpy.count(), 0); +} + +void GlobalShortcutsTest::testKeypad() +{ + auto zeroAction = std::make_unique(); + zeroAction->setProperty("componentName", QStringLiteral("kwin")); + zeroAction->setObjectName(QStringLiteral("globalshortcuts-test-keypad-0")); + QSignalSpy zeroActionTriggeredSpy(zeroAction.get(), &QAction::triggered); + KGlobalAccel::self()->setShortcut(zeroAction.get(), QList{Qt::MetaModifier | Qt::KeypadModifier | Qt::Key_0}, KGlobalAccel::NoAutoloading); + + auto insertAction = std::make_unique(); + insertAction->setProperty("componentName", QStringLiteral("kwin")); + insertAction->setObjectName(QStringLiteral("globalshortcuts-test-keypad-ins")); + QSignalSpy insertActionTriggeredSpy(insertAction.get(), &QAction::triggered); + KGlobalAccel::self()->setShortcut(insertAction.get(), QList{Qt::MetaModifier | Qt::KeypadModifier | Qt::Key_Insert}, KGlobalAccel::NoAutoloading); + + // Turn on numlock + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_NUMLOCK, timestamp++); + Test::keyboardKeyReleased(KEY_NUMLOCK, timestamp++); + + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + Test::keyboardKeyPressed(KEY_KP0, timestamp++); + Test::keyboardKeyReleased(KEY_KP0, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + QTRY_COMPARE(zeroActionTriggeredSpy.count(), 1); + QCOMPARE(insertActionTriggeredSpy.count(), 0); + + // Turn off numlock + Test::keyboardKeyPressed(KEY_NUMLOCK, timestamp++); + Test::keyboardKeyReleased(KEY_NUMLOCK, timestamp++); + + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + Test::keyboardKeyPressed(KEY_KP0, timestamp++); + Test::keyboardKeyReleased(KEY_KP0, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + QTRY_COMPARE(insertActionTriggeredSpy.count(), 1); + QCOMPARE(zeroActionTriggeredSpy.count(), 1); +} + +void GlobalShortcutsTest::testX11WindowShortcut() +{ + // create an X11 window + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + xcb_window_t windowId = xcb_generate_id(c.get()); + const Rect windowGeometry = Rect(0, 0, 10, 20); + const uint32_t values[] = { + XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW}; + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, XCB_CW_EVENT_MASK, values); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + NETWinInfo info(c.get(), windowId, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); + info.setWindowType(NET::Normal); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.last().first().value(); + QVERIFY(window); + + QCOMPARE(workspace()->activeWindow(), window); + QVERIFY(window->isActive()); + QCOMPARE(window->shortcut(), QKeySequence()); + const QKeySequence seq(Qt::META | Qt::SHIFT | Qt::Key_Y); + QVERIFY(workspace()->shortcutAvailable(seq)); + window->setShortcut(seq.toString()); + QCOMPARE(window->shortcut(), seq); + QVERIFY(!workspace()->shortcutAvailable(seq)); + QCOMPARE(window->caption(), QStringLiteral(" {Meta+Shift+Y}")); + + // it's delayed + QCoreApplication::processEvents(); + + workspace()->activateWindow(nullptr); + QVERIFY(!workspace()->activeWindow()); + QVERIFY(!window->isActive()); + + // now let's trigger the shortcut + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + Test::keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + Test::keyboardKeyPressed(KEY_Y, timestamp++); + QTRY_COMPARE(workspace()->activeWindow(), window); + Test::keyboardKeyReleased(KEY_Y, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + + // destroy window again + QSignalSpy windowClosedSpy(window, &X11Window::closed); + xcb_unmap_window(c.get(), windowId); + xcb_destroy_window(c.get(), windowId); + xcb_flush(c.get()); + QVERIFY(windowClosedSpy.wait()); +} + +void GlobalShortcutsTest::testWaylandWindowShortcut() +{ + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + QCOMPARE(workspace()->activeWindow(), window); + QVERIFY(window->isActive()); + QCOMPARE(window->shortcut(), QKeySequence()); + const QKeySequence seq(Qt::META | Qt::SHIFT | Qt::Key_Y); + QVERIFY(workspace()->shortcutAvailable(seq)); + window->setShortcut(seq.toString()); + QCOMPARE(window->shortcut(), seq); + QVERIFY(!workspace()->shortcutAvailable(seq)); + QCOMPARE(window->caption(), QStringLiteral(" {Meta+Shift+Y}")); + + workspace()->activateWindow(nullptr); + QVERIFY(!workspace()->activeWindow()); + QVERIFY(!window->isActive()); + + // now let's trigger the shortcut + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + Test::keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + Test::keyboardKeyPressed(KEY_Y, timestamp++); + QTRY_COMPARE(workspace()->activeWindow(), window); + Test::keyboardKeyReleased(KEY_Y, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); + QTRY_VERIFY_WITH_TIMEOUT(workspace()->shortcutAvailable(seq), 500); // we need the try since KGlobalAccelPrivate::unregister is async +} + +void GlobalShortcutsTest::testSetupWindowShortcut() +{ + // QTBUG-62102 + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + QCOMPARE(workspace()->activeWindow(), window); + QVERIFY(window->isActive()); + QCOMPARE(window->shortcut(), QKeySequence()); + + QSignalSpy shortcutDialogAddedSpy(workspace(), &Workspace::windowAdded); + workspace()->slotSetupWindowShortcut(); + QTRY_COMPARE(shortcutDialogAddedSpy.count(), 1); + auto dialog = shortcutDialogAddedSpy.first().first().value(); + QVERIFY(dialog); + QVERIFY(dialog->isInternal()); + auto sequenceEdit = workspace()->shortcutDialog()->findChild(); + QVERIFY(sequenceEdit); + QTRY_VERIFY(sequenceEdit->hasFocus()); + + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + Test::keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + Test::keyboardKeyPressed(KEY_Y, timestamp++); + Test::keyboardKeyReleased(KEY_Y, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + + // the sequence gets accepted after one second, so wait a bit longer + QTest::qWait(2000); + // now send in enter + Test::keyboardKeyPressed(KEY_ENTER, timestamp++); + Test::keyboardKeyReleased(KEY_ENTER, timestamp++); + QTRY_COMPARE(window->shortcut(), QKeySequence(Qt::META | Qt::SHIFT | Qt::Key_Y)); +} + +WAYLANDTEST_MAIN(GlobalShortcutsTest) +#include "globalshortcuts_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/helper/CMakeLists.txt b/local/recipes/kde/kwin/source/autotests/integration/helper/CMakeLists.txt new file mode 100644 index 0000000000..f73dd03a31 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/helper/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(kill kill.cpp) +target_link_libraries(kill Qt::Widgets) +ecm_mark_as_test(kill) diff --git a/local/recipes/kde/kwin/source/autotests/integration/helper/kill.cpp b/local/recipes/kde/kwin/source/autotests/integration/helper/kill.cpp new file mode 100644 index 0000000000..2f24cc6d1b --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/helper/kill.cpp @@ -0,0 +1,35 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + SPDX-FileCopyrightText: 2019 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include +#include +#include +#include +#include + +#include + +int main(int argc, char *argv[]) +{ + qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("wayland")); + QApplication app(argc, argv); + QWidget w; + w.setGeometry(QRect(0, 0, 100, 200)); + w.show(); + + auto freezeHandler = [](int) { + while (true) { + sleep(10000); + } + }; + + signal(SIGUSR1, freezeHandler); + + return app.exec(); +} diff --git a/local/recipes/kde/kwin/source/autotests/integration/idle_inhibition_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/idle_inhibition_test.cpp new file mode 100644 index 0000000000..d370c0bbb6 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/idle_inhibition_test.cpp @@ -0,0 +1,366 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "input.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_idle_inhbition_test-0"); + +class TestIdleInhibition : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testInhibit(); + void testDontInhibitWhenNotOnCurrentDesktop(); + void testDontInhibitWhenMinimized(); + void testDontInhibitWhenUnmapped(); + void testDontInhibitWhenLeftCurrentDesktop(); + void testSubsurface(); + void testSubsurfaceInitial(); +}; + +void TestIdleInhibition::initTestCase() +{ + qRegisterMetaType(); + + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void TestIdleInhibition::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::IdleInhibitV1)); +} + +void TestIdleInhibition::cleanup() +{ + Test::destroyWaylandConnection(); + + VirtualDesktopManager::self()->setCount(1); + QCOMPARE(VirtualDesktopManager::self()->count(), 1u); +} + +void TestIdleInhibition::testInhibit() +{ + // no idle inhibitors at the start + QCOMPARE(input()->idleInhibitors(), QList{}); + + // now create window + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + + // now create inhibition on window + std::unique_ptr inhibitor(Test::createIdleInhibitorV1(surface.get())); + QVERIFY(inhibitor); + + // render the window + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // this should inhibit our server object + QCOMPARE(input()->idleInhibitors(), QList{window}); + + // deleting the object should uninhibit again + inhibitor.reset(); + Test::flushWaylandConnection(); // don't use QTRY_COMPARE(), it doesn't spin event loop + QGuiApplication::processEvents(); + QCOMPARE(input()->idleInhibitors(), QList{}); + + // inhibit again and destroy window + std::unique_ptr inhibitor2(Test::createIdleInhibitorV1(surface.get())); + Test::flushWaylandConnection(); + QGuiApplication::processEvents(); + QCOMPARE(input()->idleInhibitors(), QList{window}); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); + QCOMPARE(input()->idleInhibitors(), QList{}); +} + +void TestIdleInhibition::testDontInhibitWhenNotOnCurrentDesktop() +{ + // This test verifies that the idle inhibitor object is not honored when + // the associated surface is not on the current virtual desktop. + + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(VirtualDesktopManager::self()->count(), 2u); + + // Create the test window. + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + + // Create the inhibitor object. + std::unique_ptr inhibitor(Test::createIdleInhibitorV1(surface.get())); + QVERIFY(inhibitor); + + // Render the window. + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // The test window should be only on the first virtual desktop. + QCOMPARE(window->desktops().count(), 1); + QCOMPARE(window->desktops().first(), VirtualDesktopManager::self()->desktops().first()); + + // This should inhibit our server object. + QCOMPARE(input()->idleInhibitors(), QList{window}); + + // Switch to the second virtual desktop. + VirtualDesktopManager::self()->setCurrent(2); + + // The surface is no longer visible, so the compositor don't have to honor the + // idle inhibitor object. + QCOMPARE(input()->idleInhibitors(), QList{}); + + // Switch back to the first virtual desktop. + VirtualDesktopManager::self()->setCurrent(1); + + // The test window became visible again, so the compositor has to honor the idle + // inhibitor object back again. + QCOMPARE(input()->idleInhibitors(), QList{window}); + + // Destroy the test window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); + QCOMPARE(input()->idleInhibitors(), QList{}); +} + +void TestIdleInhibition::testDontInhibitWhenMinimized() +{ + // This test verifies that the idle inhibitor object is not honored when the + // associated surface is minimized. + + // Create the test window. + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + + // Create the inhibitor object. + std::unique_ptr inhibitor(Test::createIdleInhibitorV1(surface.get())); + QVERIFY(inhibitor); + + // Render the window. + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // This should inhibit our server object. + QCOMPARE(input()->idleInhibitors(), QList{window}); + + // Minimize the window, the idle inhibitor object should not be honored. + window->setMinimized(true); + QCOMPARE(input()->idleInhibitors(), QList{}); + + // Unminimize the window, the idle inhibitor object should be honored back again. + window->setMinimized(false); + QCOMPARE(input()->idleInhibitors(), QList{window}); + + // Destroy the test window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); + QCOMPARE(input()->idleInhibitors(), QList{}); +} + +void TestIdleInhibition::testDontInhibitWhenUnmapped() +{ + // This test verifies that the idle inhibitor object is not honored by KWin + // when the associated window is unmapped. + + // Create the test window. + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + // Create the inhibitor object. + std::unique_ptr inhibitor(Test::createIdleInhibitorV1(surface.get())); + QVERIFY(inhibitor); + + // Map the window. + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + Test::render(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(windowAddedSpy.isEmpty()); + QVERIFY(windowAddedSpy.wait()); + QCOMPARE(windowAddedSpy.count(), 1); + Window *window = windowAddedSpy.last().first().value(); + QVERIFY(window); + QCOMPARE(window->readyForPainting(), true); + + // The compositor will respond with a configure event when the surface becomes active. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + // This should inhibit our server object. + QCOMPARE(input()->idleInhibitors(), QList{window}); + + // Unmap the window. + surface->attachBuffer(KWayland::Client::Buffer::Ptr()); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(Test::waitForWindowClosed(window)); + + // The surface is no longer visible, so the compositor doesn't have to honor the + // idle inhibitor object. + QCOMPARE(input()->idleInhibitors(), QList{}); + + // Tell the compositor that we want to map the surface. + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // The compositor will respond with a configure event. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + + // Map the window. + Test::render(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(windowAddedSpy.wait()); + QCOMPARE(windowAddedSpy.count(), 2); + window = windowAddedSpy.last().first().value(); + QVERIFY(window); + QCOMPARE(window->readyForPainting(), true); + + // The test window became visible again, so the compositor has to honor the idle + // inhibitor object back again. + QCOMPARE(input()->idleInhibitors(), QList{window}); + + // Destroy the test window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); + QCOMPARE(input()->idleInhibitors(), QList{}); +} + +void TestIdleInhibition::testDontInhibitWhenLeftCurrentDesktop() +{ + // This test verifies that the idle inhibitor object is not honored by KWin + // when the associated surface leaves the current virtual desktop. + + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(VirtualDesktopManager::self()->count(), 2u); + + // Create the test window. + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + + // Create the inhibitor object. + std::unique_ptr inhibitor(Test::createIdleInhibitorV1(surface.get())); + QVERIFY(inhibitor); + + // Render the window. + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // The test window should be only on the first virtual desktop. + QCOMPARE(window->desktops().count(), 1); + QCOMPARE(window->desktops().first(), VirtualDesktopManager::self()->desktops().first()); + + // This should inhibit our server object. + QCOMPARE(input()->idleInhibitors(), QList{window}); + + // Let the window enter the second virtual desktop. + window->enterDesktop(VirtualDesktopManager::self()->desktops().at(1)); + QCOMPARE(input()->idleInhibitors(), QList{window}); + + // If the window leaves the first virtual desktop, then the associated idle + // inhibitor object should not be honored. + window->leaveDesktop(VirtualDesktopManager::self()->desktops().at(0)); + QCOMPARE(input()->idleInhibitors(), QList{}); + + // If the window enters the first desktop, then the associated idle inhibitor + // object should be honored back again. + window->enterDesktop(VirtualDesktopManager::self()->desktops().at(0)); + QCOMPARE(input()->idleInhibitors(), QList{window}); + + // Destroy the test window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); + QCOMPARE(input()->idleInhibitors(), QList{}); +} + +void TestIdleInhibition::testSubsurface() +{ + QCOMPARE(input()->idleInhibitors(), QList{}); + + Test::XdgToplevelWindow window; + QVERIFY(window.show()); + + // create a subsurface + std::unique_ptr surface = Test::createSurface(); + const auto subsurface = Test::createSubSurface(surface.get(), window.m_surface.get()); + Test::render(surface.get(), QSize(10, 10), Qt::red); + QVERIFY(window.presentWait()); + + std::unique_ptr inhibitor = Test::createIdleInhibitorV1(surface.get()); + QVERIFY(window.presentWait()); + + // this should inhibit our server object + QCOMPARE(input()->idleInhibitors(), QList{window.m_window}); + + // deleting the object should uninhibit again + inhibitor.reset(); + QVERIFY(window.presentWait()); + QCOMPARE(input()->idleInhibitors(), QList{}); + + // inhibit again + inhibitor = Test::createIdleInhibitorV1(surface.get()); + QVERIFY(window.presentWait()); + QCOMPARE(input()->idleInhibitors(), QList{window.m_window}); + + // and destroy the window + QVERIFY(window.unmapAndWaitForClosed()); + QCOMPARE(input()->idleInhibitors(), QList{}); +} + +void TestIdleInhibition::testSubsurfaceInitial() +{ + // this verifies that inhibition is applied properly, + // even if the inhibition is added to the surface before + // it's attached to the parent surface + QCOMPARE(input()->idleInhibitors(), QList{}); + + Test::XdgToplevelWindow window; + QVERIFY(window.show()); + + // create a surface with an inhibition + std::unique_ptr surface = Test::createSurface(); + std::unique_ptr inhibitor = Test::createIdleInhibitorV1(surface.get()); + + // now attach it to the window + const auto subsurface = Test::createSubSurface(surface.get(), window.m_surface.get()); + Test::render(surface.get(), QSize(10, 10), Qt::red); + QVERIFY(window.presentWait()); + + QCOMPARE(input()->idleInhibitors(), QList{window.m_window}); + + // removing the subsurface should remove the inhibitor + surface.reset(); + QVERIFY(window.presentWait()); + QCOMPARE(input()->idleInhibitors(), QList{}); +} + +WAYLANDTEST_MAIN(TestIdleInhibition) +#include "idle_inhibition_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/input_capture_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/input_capture_test.cpp new file mode 100644 index 0000000000..df477e2040 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/input_capture_test.cpp @@ -0,0 +1,360 @@ +/* + SPDX-FileCopyrightText: 2024 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "input.h" +#include "kwin_wayland_test.h" +#include "main.h" +#include "pluginmanager.h" +#include "wayland_server.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_input_capture=0"); +static QString kwinInputCapturePath = QStringLiteral("/org/kde/KWin/EIS/InputCapture"); +static QString kwinInputCaptureManagerInterface = QStringLiteral("org.kde.KWin.EIS.InputCaptureManager"); +static QString kwinInputCaptureInterface = QStringLiteral("org.kde.KWin.EIS.InputCapture"); + +enum class PortalCapabilities { + Keyboard = 1, + Pointer = 2, + Touch = 4, + All = Keyboard | Pointer | Touch, +}; + +class InputCapture +{ +public: + InputCapture() + { + auto msg = QDBusMessage::createMethodCall(QDBusConnection::sessionBus().baseService(), kwinInputCapturePath, kwinInputCaptureManagerInterface, QStringLiteral("addInputCapture")); + msg << static_cast(PortalCapabilities::All); + QDBusReply captureReply = QDBusConnection::sessionBus().call(msg); + QVERIFY2(captureReply.isValid(), QTest::toString(captureReply.error())); + dbusPath = captureReply.value().path(); + + msg = QDBusMessage::createMethodCall(QDBusConnection::sessionBus().baseService(), dbusPath, kwinInputCaptureInterface, QStringLiteral("connectToEIS")); + QDBusReply eisReply = QDBusConnection::sessionBus().call(msg); + QVERIFY2(eisReply.isValid(), QTest::toString(eisReply.error())); + eifd = eisReply.value().takeFileDescriptor(); + + const QList> barriers = { + {{0, 0}, {0, 1023}}, + {{0, 0}, {1279, 0}}, + {{1279, 0}, {1279, 1023}}, + {{0, 1023}, {1279, 1023}}, + }; + msg = QDBusMessage::createMethodCall(QDBusConnection::sessionBus().baseService(), dbusPath, kwinInputCaptureInterface, QStringLiteral("enable")); + msg << QVariant::fromValue(barriers); + QDBusReply enableReply = QDBusConnection::sessionBus().call(msg); + QVERIFY2(enableReply.isValid(), QTest::toString(enableReply.error())); + } + + bool setupSuccessfully() const + { + return eifd != 1 && !dbusPath.isEmpty(); + } + + int eifd = -1; + QString dbusPath; +}; + +class TestInputCapture : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testInputCapture_data(); + void testInputCapture(); + void disconnectingEiRemovesCapture(); +}; + +void TestInputCapture::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); +} + +void TestInputCapture::cleanup() +{ + if (Test::waylandConnection()) { + Test::destroyWaylandConnection(); + } +} + +void TestInputCapture::initTestCase() +{ + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->start(); + Test::setOutputConfig({Rect(0, 0, 1280, 1024)}); + QVERIFY(kwinApp()->pluginManager()->loadedPlugins().contains("eis")); +} + +void TestInputCapture::testInputCapture_data() +{ + QTest::addColumn("mousePos"); + QTest::addColumn("delta"); + QTest::addRow("left") << QPoint(0, 512) << QPointF(-1, 0); + QTest::addRow("up") << QPoint(640, 0) << QPointF(0, -1); + QTest::addRow("right") << QPoint(1279, 512) << QPointF(1, 0); + QTest::addRow("down") << QPoint(640, 1023) << QPointF(0, 1); +} + +void TestInputCapture::testInputCapture() +{ + QFETCH(QPoint, mousePos); + QFETCH(QPointF, delta); + int timestamp = 0; + + QVERIFY(Test::waitForWaylandPointer()); + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + std::unique_ptr pointer(Test::waylandSeat()->createPointer()); + auto surface = Test::createSurface(); + auto shellSurface = Test::createXdgToplevelSurface(surface.get()); + + Test::renderAndWaitForShown(surface.get(), {1280, 1024}, Qt::cyan); + + InputCapture capture; + if (!capture.setupSuccessfully()) { + return; + } + + Test::pointerMotion(mousePos, ++timestamp); + Test::waylandSync(); + QCOMPARE(keyboard->enteredSurface(), surface.get()); + QCOMPARE(pointer->enteredSurface(), surface.get()); + + QSignalSpy motionSpy(pointer.get(), &KWayland::Client::Pointer::motion); + QSignalSpy buttonSpy(pointer.get(), &KWayland::Client::Pointer::buttonStateChanged); + QSignalSpy axisSpy(pointer.get(), &KWayland::Client::Pointer::axisChanged); + QSignalSpy keySpy(keyboard.get(), &KWayland::Client::Keyboard::keyChanged); + QVERIFY(motionSpy.isValid()); + QVERIFY(buttonSpy.isValid()); + QVERIFY(axisSpy.isValid()); + QVERIFY(keySpy.isValid()); + + auto ei = ei_new_receiver(nullptr); + ei_setup_backend_fd(ei, capture.eifd); + QSocketNotifier eiNotifier(ei_get_fd(ei), QSocketNotifier::Read); + QSignalSpy eiReadableSpy(&eiNotifier, &QSocketNotifier::activated); + + int numDevices = 0; + while (numDevices != 3 && eiReadableSpy.wait()) { + ei_dispatch(ei); + while (auto event = ei_get_event(ei)) { + switch (const auto type = ei_event_get_type(event)) { + case EI_EVENT_CONNECT: +#if HAVE_EI_EVENT_SYNC + case EI_EVENT_SYNC: +#endif + break; + case EI_EVENT_SEAT_ADDED: + ei_seat_bind_capabilities(ei_event_get_seat(event), EI_DEVICE_CAP_POINTER, EI_DEVICE_CAP_POINTER_ABSOLUTE, EI_DEVICE_CAP_KEYBOARD, EI_DEVICE_CAP_TOUCH, EI_DEVICE_CAP_SCROLL, EI_DEVICE_CAP_BUTTON, nullptr); + case EI_EVENT_DEVICE_ADDED: + break; + case EI_EVENT_DEVICE_RESUMED: + ++numDevices; + break; + default: + qFatal() << "unexpected event:" << type; + } + ei_event_unref(event); + } + } + + { + Test::pointerMotionRelative(delta, ++timestamp); + + Test::waylandSync(); + QVERIFY(motionSpy.empty()); + + ei_dispatch(ei); + + for (int i = 0; i < numDevices; ++i) { + auto event = ei_get_event(ei); + QCOMPARE(ei_event_get_type(event), EI_EVENT_DEVICE_START_EMULATING); + ei_event_unref(event); + } + + auto event = ei_get_event(ei); + QCOMPARE(ei_event_get_type(event), EI_EVENT_POINTER_MOTION); + QCOMPARE(ei_event_pointer_get_dx(event), delta.x()); + QCOMPARE(ei_event_pointer_get_dy(event), delta.y()); + ei_event_unref(event); + + event = ei_get_event(ei); + QCOMPARE(ei_event_get_type(event), EI_EVENT_FRAME); + ei_event_unref(event); + } + + { + Test::pointerButtonPressed(BTN_LEFT, ++timestamp); + Test::pointerButtonReleased(BTN_LEFT, ++timestamp); + + Test::waylandSync(); + QVERIFY(buttonSpy.empty()); + + ei_dispatch(ei); + + auto event = ei_get_event(ei); + QCOMPARE(ei_event_get_type(event), EI_EVENT_BUTTON_BUTTON); + QCOMPARE(ei_event_button_get_button(event), BTN_LEFT); + QCOMPARE(ei_event_button_get_is_press(event), true); + ei_event_unref(event); + + event = ei_get_event(ei); + QCOMPARE(ei_event_get_type(event), EI_EVENT_FRAME); + + ei_event_unref(event); + event = ei_get_event(ei); + QCOMPARE(ei_event_get_type(event), EI_EVENT_BUTTON_BUTTON); + QCOMPARE(ei_event_button_get_button(event), BTN_LEFT); + QCOMPARE(ei_event_button_get_is_press(event), false); + ei_event_unref(event); + + event = ei_get_event(ei); + QCOMPARE(ei_event_get_type(event), EI_EVENT_FRAME); + ei_event_unref(event); + event = ei_get_event(ei); + } + + { + Test::pointerAxisHorizontal(1, ++timestamp); + + Test::waylandSync(); + QVERIFY(axisSpy.empty()); + + ei_dispatch(ei); + + auto event = ei_get_event(ei); + QCOMPARE(ei_event_get_type(event), EI_EVENT_SCROLL_DELTA); + QCOMPARE(ei_event_scroll_get_dx(event), 1); + QCOMPARE(ei_event_scroll_get_dy(event), 0); + ei_event_unref(event); + + event = ei_get_event(ei); + QCOMPARE(ei_event_get_type(event), EI_EVENT_FRAME); + ei_event_unref(event); + } + + { + Test::keyboardKeyPressed(KEY_A, ++timestamp); + Test::keyboardKeyReleased(KEY_A, ++timestamp); + + Test::waylandSync(); + QVERIFY(buttonSpy.empty()); + + ei_dispatch(ei); + + auto event = ei_get_event(ei); + QCOMPARE(ei_event_get_type(event), EI_EVENT_KEYBOARD_KEY); + QCOMPARE(ei_event_keyboard_get_key(event), KEY_A); + QCOMPARE(ei_event_keyboard_get_key_is_press(event), true); + ei_event_unref(event); + + event = ei_get_event(ei); + QCOMPARE(ei_event_get_type(event), EI_EVENT_FRAME); + ei_event_unref(event); + + event = ei_get_event(ei); + QCOMPARE(ei_event_get_type(event), EI_EVENT_KEYBOARD_KEY); + QCOMPARE(ei_event_keyboard_get_key(event), KEY_A); + QCOMPARE(ei_event_keyboard_get_key_is_press(event), false); + ei_event_unref(event); + + event = ei_get_event(ei); + QCOMPARE(ei_event_get_type(event), EI_EVENT_FRAME); + ei_event_unref(event); + } + + auto msg = QDBusMessage::createMethodCall(QDBusConnection::sessionBus().baseService(), capture.dbusPath, kwinInputCaptureInterface, QStringLiteral("release")); + msg << QVariant::fromValue(QPointF(1, 1)) << true; + QDBusReply releaseReply = QDBusConnection::sessionBus().call(msg); + QVERIFY2(releaseReply.isValid(), QTest::toString(releaseReply.error())); + + QCOMPARE(input()->globalPointer(), QPoint(1, 1)); + + QVERIFY(eiReadableSpy.wait()); + eiReadableSpy.clear(); + ei_dispatch(ei); + // Throw away the disconnection events + while (auto event = ei_get_event(ei)) { + ei_event_unref(event); + } + + Test::pointerMotion({2, 2}, ++timestamp); + Test::pointerButtonPressed(BTN_LEFT, ++timestamp); + Test::pointerButtonReleased(BTN_LEFT, ++timestamp); + Test::pointerAxisHorizontal(1, ++timestamp); + Test::keyboardKeyPressed(KEY_A, ++timestamp); + Test::keyboardKeyReleased(KEY_A, ++timestamp); + QVERIFY(motionSpy.wait()); + QVERIFY(buttonSpy.count()); + QVERIFY(axisSpy.count()); + QVERIFY(keySpy.count()); + + if (!eiReadableSpy.empty()) { + ei_dispatch(ei); + QVERIFY(!ei_peek_event(ei)); + } + + msg = QDBusMessage::createMethodCall(QDBusConnection::sessionBus().baseService(), kwinInputCapturePath, kwinInputCaptureManagerInterface, QStringLiteral("removeInputCapture")); + msg << QDBusObjectPath(capture.dbusPath); + QDBusReply removeReply = QDBusConnection::sessionBus().call(msg); + QVERIFY2(removeReply.isValid(), QTest::toString(removeReply.error())); + + ei_unref(ei); +} + +void TestInputCapture::disconnectingEiRemovesCapture() +{ + InputCapture capture; + if (!capture.setupSuccessfully()) { + return; + } + + auto ei = ei_new_receiver(nullptr); + ei_setup_backend_fd(ei, capture.eifd); + QSocketNotifier eiNotifier(ei_get_fd(ei), QSocketNotifier::Read); + QSignalSpy eiReadableSpy(&eiNotifier, &QSocketNotifier::activated); + + bool connected = false; + while (!connected && eiReadableSpy.wait()) { + ei_dispatch(ei); + + if (auto event = ei_get_event(ei)) { + if (ei_event_get_type(event) == EI_EVENT_CONNECT) { + connected = true; + } + ei_event_unref(event); + } + } + + ei_unref(ei); + + QTRY_COMPARE(QDBusConnection::sessionBus().objectRegisteredAt(capture.dbusPath), nullptr); +} + +WAYLANDTEST_MAIN(TestInputCapture) + +#include "input_capture_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/input_stacking_order.cpp b/local/recipes/kde/kwin/source/autotests/integration/input_stacking_order.cpp new file mode 100644 index 0000000000..f4dd07ee1c --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/input_stacking_order.cpp @@ -0,0 +1,157 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "pointer_input.h" +#include "wayland/seat.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_input_stacking_order-0"); + +class InputStackingOrderTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testPointerFocusUpdatesOnStackingOrderChange(); + +private: + void render(KWayland::Client::Surface *surface); +}; + +void InputStackingOrderTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); +} + +void InputStackingOrderTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); + QVERIFY(Test::waitForWaylandPointer()); + + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); +} + +void InputStackingOrderTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void InputStackingOrderTest::render(KWayland::Client::Surface *surface) +{ + Test::render(surface, QSize(100, 50), Qt::blue); + Test::flushWaylandConnection(); +} + +void InputStackingOrderTest::testPointerFocusUpdatesOnStackingOrderChange() +{ + // this test creates two windows which overlap + // the pointer is in the overlapping area which means the top most window has focus + // as soon as the top most window gets lowered the window should lose focus and the + // other window should gain focus without a mouse event in between + + // create pointer and signal spy for enter and leave signals + auto pointer = Test::waylandSeat()->createPointer(Test::waylandSeat()); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy enteredSpy(pointer, &KWayland::Client::Pointer::entered); + QSignalSpy leftSpy(pointer, &KWayland::Client::Pointer::left); + + // now create the two windows and make them overlap + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + std::unique_ptr surface1 = Test::createSurface(); + QVERIFY(surface1); + std::unique_ptr shellSurface1 = Test::createXdgToplevelSurface(surface1.get()); + QVERIFY(shellSurface1); + render(surface1.get()); + QVERIFY(windowAddedSpy.wait()); + Window *window1 = workspace()->activeWindow(); + QVERIFY(window1); + + std::unique_ptr surface2 = Test::createSurface(); + QVERIFY(surface2); + std::unique_ptr shellSurface2 = Test::createXdgToplevelSurface(surface2.get()); + QVERIFY(shellSurface2); + render(surface2.get()); + QVERIFY(windowAddedSpy.wait()); + + Window *window2 = workspace()->activeWindow(); + QVERIFY(window2); + QVERIFY(window1 != window2); + + // now make windows overlap + window2->move(window1->pos()); + QCOMPARE(window1->frameGeometry(), window2->frameGeometry()); + + // enter + Test::pointerMotion(QPointF(25, 25), 1); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 1); + // window 2 should have focus + QCOMPARE(pointer->enteredSurface(), surface2.get()); + // also on the server + QCOMPARE(waylandServer()->seat()->focusedPointerSurface(), window2->surface()); + + // raise window 1 above window 2 + QVERIFY(leftSpy.isEmpty()); + workspace()->raiseWindow(window1); + // should send leave to window2 + QVERIFY(leftSpy.wait()); + QCOMPARE(leftSpy.count(), 1); + // and an enter to window1 + QCOMPARE(enteredSpy.count(), 2); + QCOMPARE(pointer->enteredSurface(), surface1.get()); + QCOMPARE(waylandServer()->seat()->focusedPointerSurface(), window1->surface()); + + // let's destroy window1, that should pass focus to window2 again + QSignalSpy windowClosedSpy(window1, &Window::closed); + surface1.reset(); + QVERIFY(windowClosedSpy.wait()); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 3); + QCOMPARE(pointer->enteredSurface(), surface2.get()); + QCOMPARE(waylandServer()->seat()->focusedPointerSurface(), window2->surface()); +} + +} + +WAYLANDTEST_MAIN(KWin::InputStackingOrderTest) +#include "input_stacking_order.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/inputmethod_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/inputmethod_test.cpp new file mode 100644 index 0000000000..db96fb632d --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/inputmethod_test.cpp @@ -0,0 +1,1017 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "inputmethod.h" +#include "inputpanelv1window.h" +#include "keyboard_input.h" +#include "main.h" +#include "pointer_input.h" +#include "qwayland-input-method-unstable-v1.h" +#include "qwayland-text-input-unstable-v3.h" +#include "virtualkeyboard_dbus.h" +#include "wayland/clientconnection.h" +#include "wayland/display.h" +#include "wayland/seat.h" +#include "wayland/surface.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" +#include "xkb.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace KWin; +using KWin::VirtualKeyboardDBus; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_inputmethod-0"); + +class InputMethodTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testOpenClose(); + void testEnableDisableV3(); + void testEnableActive(); + void testHidePanel(); + void testUnmapPanel(); + void testReactivateFocus(); + void testSwitchFocusedSurfaces(); + void testV2V3SameClient(); + void testV3Styling(); + void testDisableShowInputPanel(); + void testModifierForwarding(); + void testFakeEventFallback(); + void testOverlayPositioning_data(); + void testOverlayPositioning(); + void testV3AutoCommit(); + void testSendRepeatInfo(); + void testSendRepeatInfoV10(); + +private: + void touchNow() + { + static int time = 0; + Test::touchDown(0, {100, 100}, ++time); + Test::touchUp(0, ++time); + } +}; + +void InputMethodTest::initTestCase() +{ + QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.kwin.testvirtualkeyboard")); + + qRegisterMetaType(); + qRegisterMetaType(); + + QVERIFY(waylandServer()->init(s_socketName)); + + static_cast(kwinApp())->setInputMethodServerToStart("internal"); + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); +} + +void InputMethodTest::init() +{ + workspace()->setActiveOutput(QPoint(640, 512)); + KWin::input()->pointer()->warp(QPoint(640, 512)); + + touchNow(); + + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::TextInputManagerV2 | Test::AdditionalWaylandInterface::InputMethodV1 | Test::AdditionalWaylandInterface::TextInputManagerV3)); + + kwinApp()->inputMethod()->setEnabled(true); +} + +void InputMethodTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void InputMethodTest::testOpenClose() +{ + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + QSignalSpy windowRemovedSpy(workspace(), &Workspace::windowRemoved); + + // Create an xdg_toplevel surface and wait for the compositor to catch up. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(window->frameGeometry().size(), QSize(1280, 1024)); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + std::unique_ptr textInput(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat())); + QVERIFY(textInput != nullptr); + + // Show the keyboard + touchNow(); + textInput->enable(surface.get()); + textInput->showInputPanel(); + QSignalSpy paneladded(kwinApp()->inputMethod(), &KWin::InputMethod::panelChanged); + QVERIFY(windowAddedSpy.wait()); + QCOMPARE(paneladded.count(), 1); + + Window *keyboardClient = windowAddedSpy.last().first().value(); + QVERIFY(keyboardClient); + QVERIFY(keyboardClient->isInputMethod()); + + // Do the actual resize + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().value(), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + + QCOMPARE(window->frameGeometry().height(), 1024 - keyboardClient->frameGeometry().height()); + + // Hide the keyboard + textInput->hideInputPanel(); + + QVERIFY(surfaceConfigureRequestedSpy.wait()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().value(), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + + QCOMPARE(window->frameGeometry().height(), 1024); + + // show the keyboard again + touchNow(); + textInput->enable(surface.get()); + textInput->showInputPanel(); + + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QVERIFY(keyboardClient->isShown()); + + // Destroy the test window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void InputMethodTest::testEnableDisableV3() +{ + // Create an xdg_toplevel surface and wait for the compositor to catch up. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(window->frameGeometry().size(), QSize(1280, 1024)); + + auto textInputV3 = std::make_unique(); + textInputV3->init(Test::waylandTextInputManagerV3()->get_text_input(*(Test::waylandSeat()))); + + // Show the keyboard + touchNow(); + textInputV3->enable(); + + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + QSignalSpy inputMethodActiveSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged); + // just enabling the text-input should not show it but rather on commit + QVERIFY(!kwinApp()->inputMethod()->isActive()); + textInputV3->commit(); + QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait()); + QVERIFY(kwinApp()->inputMethod()->isActive()); + + QVERIFY(windowAddedSpy.wait()); + Window *keyboardClient = windowAddedSpy.last().first().value(); + QVERIFY(keyboardClient); + QVERIFY(keyboardClient->isInputMethod()); + QVERIFY(keyboardClient->isShown()); + + // Text input v3 doesn't have hideInputPanel, just simiulate the hide from dbus call + kwinApp()->inputMethod()->hide(); + QVERIFY(!keyboardClient->isShown()); + + QSignalSpy hiddenChangedSpy(keyboardClient, &Window::hiddenChanged); + // Force enable the text input object. This is what's done by Gtk. + textInputV3->enable(); + textInputV3->commit(); + + hiddenChangedSpy.wait(); + QVERIFY(keyboardClient->isShown()); + + // disable text input and ensure that it is not hiding input panel without commit + inputMethodActiveSpy.clear(); + QVERIFY(kwinApp()->inputMethod()->isActive()); + textInputV3->disable(); + textInputV3->commit(); + QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait()); + QVERIFY(!kwinApp()->inputMethod()->isActive()); +} + +void InputMethodTest::testEnableActive() +{ + // This test verifies that enabling text-input twice won't change the active input method status. + QVERIFY(!kwinApp()->inputMethod()->isActive()); + + // Create an xdg_toplevel surface and wait for the compositor to catch up. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red); + QVERIFY(window); + QVERIFY(window->isActive()); + + // Show the keyboard + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + std::unique_ptr textInput(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat())); + textInput->enable(surface.get()); + QSignalSpy paneladded(kwinApp()->inputMethod(), &KWin::InputMethod::panelChanged); + QVERIFY(paneladded.wait()); + textInput->showInputPanel(); + QVERIFY(windowAddedSpy.wait()); + QVERIFY(kwinApp()->inputMethod()->isActive()); + + // Ask the keyboard to be shown again. + QSignalSpy activateSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged); + textInput->enable(surface.get()); + textInput->showInputPanel(); + activateSpy.wait(200); + QVERIFY(activateSpy.isEmpty()); + QVERIFY(kwinApp()->inputMethod()->isActive()); + + // Destroy the test window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void InputMethodTest::testHidePanel() +{ + QVERIFY(!kwinApp()->inputMethod()->isActive()); + + touchNow(); + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + QSignalSpy windowRemovedSpy(workspace(), &Workspace::windowRemoved); + + QSignalSpy activateSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged); + std::unique_ptr textInput(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat())); + + // Create an xdg_toplevel surface and wait for the compositor to catch up. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red); + waylandServer()->seat()->setFocusedTextInputSurface(window->surface()); + + textInput->enable(surface.get()); + QSignalSpy paneladded(kwinApp()->inputMethod(), &KWin::InputMethod::panelChanged); + QVERIFY(paneladded.wait()); + textInput->showInputPanel(); + QVERIFY(windowAddedSpy.wait()); + + QCOMPARE(workspace()->activeWindow(), window); + + QCOMPARE(windowAddedSpy.count(), 2); + QVERIFY(activateSpy.count() || activateSpy.wait()); + QVERIFY(kwinApp()->inputMethod()->isActive()); + + auto keyboardWindow = kwinApp()->inputMethod()->panel(); + auto ipsurface = Test::inputPanelSurface(); + QVERIFY(keyboardWindow); + windowRemovedSpy.clear(); + delete ipsurface; + QVERIFY(kwinApp()->inputMethod()->isVisible()); + QVERIFY(windowRemovedSpy.count() || windowRemovedSpy.wait()); + QVERIFY(!kwinApp()->inputMethod()->isVisible()); + + // Destroy the test window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void InputMethodTest::testUnmapPanel() +{ + QVERIFY(!kwinApp()->inputMethod()->isActive()); + + touchNow(); + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + + QSignalSpy activateSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged); + std::unique_ptr textInput(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat())); + + // Create an xdg_toplevel surface and wait for the compositor to catch up. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red); + waylandServer()->seat()->setFocusedTextInputSurface(window->surface()); + + textInput->enable(surface.get()); + QSignalSpy paneladded(kwinApp()->inputMethod(), &KWin::InputMethod::panelChanged); + QVERIFY(paneladded.wait()); + textInput->showInputPanel(); + QVERIFY(windowAddedSpy.wait()); + + QCOMPARE(workspace()->activeWindow(), window); + + QCOMPARE(windowAddedSpy.count(), 2); + QVERIFY(activateSpy.count() || activateSpy.wait()); + QVERIFY(kwinApp()->inputMethod()->isActive()); + + auto keyboardWindow = kwinApp()->inputMethod()->panel(); + auto ipsurface = Test::inputPanelSurface(); + QVERIFY(keyboardWindow); + QVERIFY(kwinApp()->inputMethod()->isVisible()); + + QSignalSpy panelHiddenSpy(kwinApp()->inputMethod(), &InputMethod::visibleChanged); + ipsurface->attachBuffer((wl_buffer *)nullptr); + ipsurface->commit(); + QVERIFY(panelHiddenSpy.count() || panelHiddenSpy.wait()); + QVERIFY(!kwinApp()->inputMethod()->isVisible()); + + // Destroy the test window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void InputMethodTest::testReactivateFocus() +{ + touchNow(); + QVERIFY(!kwinApp()->inputMethod()->isActive()); + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(window->frameGeometry().size(), QSize(1280, 1024)); + + // Show the keyboard + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + std::unique_ptr textInput(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat())); + textInput->enable(surface.get()); + QSignalSpy paneladded(kwinApp()->inputMethod(), &KWin::InputMethod::panelChanged); + QVERIFY(paneladded.wait()); + textInput->showInputPanel(); + QVERIFY(windowAddedSpy.wait()); + QVERIFY(kwinApp()->inputMethod()->isActive()); + + QSignalSpy activeSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged); + + // Hide keyboard like keyboardToggle button on navigation panel + kwinApp()->inputMethod()->setActive(false); + activeSpy.wait(200); + QVERIFY(!kwinApp()->inputMethod()->isActive()); + + // Reactivate + textInput->enable(surface.get()); + textInput->showInputPanel(); + activeSpy.wait(200); + QVERIFY(kwinApp()->inputMethod()->isActive()); + + // Destroy the test window + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void InputMethodTest::testSwitchFocusedSurfaces() +{ + touchNow(); + QVERIFY(!kwinApp()->inputMethod()->isActive()); + + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + QSignalSpy windowRemovedSpy(workspace(), &Workspace::windowRemoved); + + QSignalSpy activateSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged); + std::unique_ptr textInput(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat())); + + QList windows; + std::vector> surfaces; + std::vector> toplevels; + // We create 3 surfaces + for (int i = 0; i < 3; ++i) { + std::unique_ptr surface = Test::createSurface(); + auto shellSurface = Test::createXdgToplevelSurface(surface.get()); + windows += Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red); + QCOMPARE(workspace()->activeWindow(), windows.constLast()); + surfaces.push_back(std::move(surface)); + toplevels.push_back(std::move(shellSurface)); + } + QCOMPARE(windowAddedSpy.count(), 3); + waylandServer()->seat()->setFocusedTextInputSurface(windows.constFirst()->surface()); + + QVERIFY(!kwinApp()->inputMethod()->isActive()); + textInput->enable(surfaces.back().get()); + QVERIFY(!kwinApp()->inputMethod()->isActive()); + waylandServer()->seat()->setFocusedTextInputSurface(windows.first()->surface()); + QVERIFY(!kwinApp()->inputMethod()->isActive()); + activateSpy.clear(); + waylandServer()->seat()->setFocusedTextInputSurface(windows.last()->surface()); + QVERIFY(activateSpy.count() || activateSpy.wait()); + QVERIFY(kwinApp()->inputMethod()->isActive()); + + activateSpy.clear(); + waylandServer()->seat()->setFocusedTextInputSurface(windows.first()->surface()); + QVERIFY(activateSpy.count() || activateSpy.wait()); + QVERIFY(!kwinApp()->inputMethod()->isActive()); +} + +void InputMethodTest::testV2V3SameClient() +{ + touchNow(); + QVERIFY(!kwinApp()->inputMethod()->isActive()); + + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + QSignalSpy windowRemovedSpy(workspace(), &Workspace::windowRemoved); + + QSignalSpy activateSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged); + std::unique_ptr textInput(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat())); + + auto textInputV3 = std::make_unique(); + textInputV3->init(Test::waylandTextInputManagerV3()->get_text_input(*(Test::waylandSeat()))); + + std::unique_ptr surface = Test::createSurface(); + std::unique_ptr toplevel(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red); + QCOMPARE(workspace()->activeWindow(), window); + QCOMPARE(windowAddedSpy.count(), 1); + waylandServer()->seat()->setFocusedTextInputSurface(window->surface()); + QVERIFY(!kwinApp()->inputMethod()->isActive()); + + // Enable and disable v2 + textInput->enable(surface.get()); + QVERIFY(activateSpy.count() || activateSpy.wait()); + QVERIFY(kwinApp()->inputMethod()->isActive()); + + activateSpy.clear(); + textInput->disable(surface.get()); + QVERIFY(activateSpy.count() || activateSpy.wait()); + QVERIFY(!kwinApp()->inputMethod()->isActive()); + + // Enable and disable v3 + activateSpy.clear(); + textInputV3->enable(); + textInputV3->commit(); + QVERIFY(activateSpy.count() || activateSpy.wait()); + QVERIFY(kwinApp()->inputMethod()->isActive()); + + activateSpy.clear(); + textInputV3->disable(); + textInputV3->commit(); + activateSpy.clear(); + QVERIFY(activateSpy.count() || activateSpy.wait()); + QVERIFY(!kwinApp()->inputMethod()->isActive()); + + // Enable v2 and v3 + activateSpy.clear(); + textInputV3->enable(); + textInputV3->commit(); + textInput->enable(surface.get()); + QVERIFY(activateSpy.count() || activateSpy.wait()); + QVERIFY(kwinApp()->inputMethod()->isActive()); + + // Disable v3, should still be active since v2 is active. + activateSpy.clear(); + textInputV3->disable(); + textInputV3->commit(); + activateSpy.wait(200); + QVERIFY(kwinApp()->inputMethod()->isActive()); + + // Disable v2 + activateSpy.clear(); + textInput->disable(surface.get()); + QVERIFY(activateSpy.count() || activateSpy.wait()); + QVERIFY(!kwinApp()->inputMethod()->isActive()); + + toplevel.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void InputMethodTest::testV3Styling() +{ + // Create an xdg_toplevel surface and wait for the compositor to catch up. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(window->frameGeometry().size(), QSize(1280, 1024)); + + auto textInputV3 = std::make_unique(); + textInputV3->init(Test::waylandTextInputManagerV3()->get_text_input(*(Test::waylandSeat()))); + textInputV3->enable(); + + QSignalSpy inputMethodActiveSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged); + QSignalSpy inputMethodActivateSpy(Test::inputMethod(), &Test::MockInputMethod::activate); + // just enabling the text-input should not show it but rather on commit + QVERIFY(!kwinApp()->inputMethod()->isActive()); + textInputV3->commit(); + QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait()); + QVERIFY(kwinApp()->inputMethod()->isActive()); + QVERIFY(inputMethodActivateSpy.wait()); + auto context = Test::inputMethod()->context(); + QSignalSpy textInputPreeditSpy(textInputV3.get(), &Test::TextInputV3::preeditString); + zwp_input_method_context_v1_preedit_cursor(context, 0); + zwp_input_method_context_v1_preedit_styling(context, 0, 3, 7); + zwp_input_method_context_v1_preedit_string(context, 0, "ABCD", "ABCD"); + QVERIFY(textInputPreeditSpy.wait()); + QCOMPARE(textInputPreeditSpy.last().at(0), QString("ABCD")); + QCOMPARE(textInputPreeditSpy.last().at(1), 0); + QCOMPARE(textInputPreeditSpy.last().at(2), 0); + + zwp_input_method_context_v1_preedit_cursor(context, 1); + zwp_input_method_context_v1_preedit_styling(context, 0, 3, 7); + zwp_input_method_context_v1_preedit_string(context, 0, "ABCDE", "ABCDE"); + QVERIFY(textInputPreeditSpy.wait()); + QCOMPARE(textInputPreeditSpy.last().at(0), QString("ABCDE")); + QCOMPARE(textInputPreeditSpy.last().at(1), 1); + QCOMPARE(textInputPreeditSpy.last().at(2), 1); + + zwp_input_method_context_v1_preedit_cursor(context, 2); + // Use selection for [2, 2+2) + zwp_input_method_context_v1_preedit_styling(context, 2, 2, 6); + // Use high light for [3, 3+3) + zwp_input_method_context_v1_preedit_styling(context, 3, 3, 4); + zwp_input_method_context_v1_preedit_string(context, 0, "ABCDEF", "ABCDEF"); + QVERIFY(textInputPreeditSpy.wait()); + QCOMPARE(textInputPreeditSpy.last().at(0), QString("ABCDEF")); + // Merged range should be [2, 6) + QCOMPARE(textInputPreeditSpy.last().at(1), 2); + QCOMPARE(textInputPreeditSpy.last().at(2), 6); + + zwp_input_method_context_v1_preedit_cursor(context, 2); + // Use selection for [0, 0+2) + zwp_input_method_context_v1_preedit_styling(context, 0, 2, 6); + // Use high light for [3, 3+3) + zwp_input_method_context_v1_preedit_styling(context, 3, 3, 4); + zwp_input_method_context_v1_preedit_string(context, 0, "ABCDEF", "ABCDEF"); + QVERIFY(textInputPreeditSpy.wait()); + QCOMPARE(textInputPreeditSpy.last().at(0), QString("ABCDEF")); + // Merged range should be none, because of the disjunction highlight. + QCOMPARE(textInputPreeditSpy.last().at(1), 2); + QCOMPARE(textInputPreeditSpy.last().at(2), 2); + + zwp_input_method_context_v1_preedit_cursor(context, 1); + // Use selection for [0, 0+2) + zwp_input_method_context_v1_preedit_styling(context, 0, 2, 6); + // Use high light for [2, 2+3) + zwp_input_method_context_v1_preedit_styling(context, 2, 3, 4); + zwp_input_method_context_v1_preedit_string(context, 0, "ABCDEF", "ABCDEF"); + QVERIFY(textInputPreeditSpy.wait()); + QCOMPARE(textInputPreeditSpy.last().at(0), QString("ABCDEF")); + // Merged range should be none, starting offset does not match. + QCOMPARE(textInputPreeditSpy.last().at(1), 1); + QCOMPARE(textInputPreeditSpy.last().at(2), 1); + + // Use different order of styling and cursor + // Use high light for [3, 3+3) + zwp_input_method_context_v1_preedit_styling(context, 3, 3, 4); + zwp_input_method_context_v1_preedit_cursor(context, 1); + // Use selection for [1, 1+2) + zwp_input_method_context_v1_preedit_styling(context, 1, 2, 6); + zwp_input_method_context_v1_preedit_string(context, 0, "ABCDEF", "ABCDEF"); + QVERIFY(textInputPreeditSpy.wait()); + QCOMPARE(textInputPreeditSpy.last().at(0), QString("ABCDEF")); + // Merged range should be [1,6). + QCOMPARE(textInputPreeditSpy.last().at(1), 1); + QCOMPARE(textInputPreeditSpy.last().at(2), 6); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); + QVERIFY(!kwinApp()->inputMethod()->isActive()); +} + +void InputMethodTest::testDisableShowInputPanel() +{ + // Create an xdg_toplevel surface and wait for the compositor to catch up. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(window->frameGeometry().size(), QSize(1280, 1024)); + + std::unique_ptr textInputV2(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat())); + + QSignalSpy inputMethodActiveSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged); + // just enabling the text-input should not show it but rather on commit + QVERIFY(!kwinApp()->inputMethod()->isActive()); + textInputV2->enable(surface.get()); + QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait()); + QVERIFY(kwinApp()->inputMethod()->isActive()); + + // disable text input and ensure that it is not hiding input panel without commit + inputMethodActiveSpy.clear(); + QVERIFY(kwinApp()->inputMethod()->isActive()); + textInputV2->disable(surface.get()); + QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait()); + QVERIFY(!kwinApp()->inputMethod()->isActive()); + + QSignalSpy requestShowInputPanelSpy(waylandServer()->seat()->textInputV2(), &TextInputV2Interface::requestShowInputPanel); + textInputV2->showInputPanel(); + QVERIFY(requestShowInputPanelSpy.count() || requestShowInputPanelSpy.wait()); + QVERIFY(!kwinApp()->inputMethod()->isActive()); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void InputMethodTest::testModifierForwarding() +{ + // Create an xdg_toplevel surface and wait for the compositor to catch up. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(window->frameGeometry().size(), QSize(1280, 1024)); + + auto textInputV3 = std::make_unique(); + textInputV3->init(Test::waylandTextInputManagerV3()->get_text_input(*(Test::waylandSeat()))); + textInputV3->enable(); + + QSignalSpy inputMethodActiveSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged); + QSignalSpy inputMethodActivateSpy(Test::inputMethod(), &Test::MockInputMethod::activate); + // just enabling the text-input should not show it but rather on commit + QVERIFY(!kwinApp()->inputMethod()->isActive()); + textInputV3->commit(); + QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait()); + QVERIFY(kwinApp()->inputMethod()->isActive()); + QVERIFY(inputMethodActivateSpy.wait()); + auto context = Test::inputMethod()->context(); + std::unique_ptr keyboardGrab(new KWayland::Client::Keyboard); + keyboardGrab->setup(zwp_input_method_context_v1_grab_keyboard(context)); + QSignalSpy modifierSpy(keyboardGrab.get(), &KWayland::Client::Keyboard::modifiersChanged); + // Wait for initial modifiers update + QVERIFY(modifierSpy.wait()); + + quint32 timestamp = 1; + + QSignalSpy keySpy(keyboardGrab.get(), &KWayland::Client::Keyboard::keyChanged); + bool keyChanged = false; + bool modifiersChanged = false; + // We want to verify the order of two signals, so SignalSpy is not very useful here. + auto keyChangedConnection = connect(keyboardGrab.get(), &KWayland::Client::Keyboard::keyChanged, [&keyChanged, &modifiersChanged]() { + QVERIFY(!modifiersChanged); + keyChanged = true; + }); + auto modifiersChangedConnection = connect(keyboardGrab.get(), &KWayland::Client::Keyboard::modifiersChanged, [&keyChanged, &modifiersChanged]() { + QVERIFY(keyChanged); + modifiersChanged = true; + }); + Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + QVERIFY(keySpy.count() == 1 || keySpy.wait()); + QVERIFY(modifierSpy.count() == 2 || modifierSpy.wait()); + disconnect(keyChangedConnection); + disconnect(modifiersChangedConnection); + + Test::keyboardKeyPressed(KEY_A, timestamp++); + QVERIFY(keySpy.count() == 2 || keySpy.wait()); + QVERIFY(modifierSpy.count() == 2 || modifierSpy.wait()); + + // verify the order of key and modifiers again. Key first, then modifiers. + keyChanged = false; + modifiersChanged = false; + keyChangedConnection = connect(keyboardGrab.get(), &KWayland::Client::Keyboard::keyChanged, [&keyChanged, &modifiersChanged]() { + QVERIFY(!modifiersChanged); + keyChanged = true; + }); + modifiersChangedConnection = connect(keyboardGrab.get(), &KWayland::Client::Keyboard::modifiersChanged, [&keyChanged, &modifiersChanged]() { + QVERIFY(keyChanged); + modifiersChanged = true; + }); + Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(keySpy.count() == 3 || keySpy.wait()); + QVERIFY(modifierSpy.count() == 3 || modifierSpy.wait()); + disconnect(keyChangedConnection); + disconnect(modifiersChangedConnection); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); + QVERIFY(!kwinApp()->inputMethod()->isActive()); + + Test::keyboardKeyReleased(KEY_A, timestamp++); +} + +void InputMethodTest::testFakeEventFallback() +{ + // Create an xdg_toplevel surface and wait for the compositor to catch up. + std::unique_ptr surface = Test::createSurface(); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(window->frameGeometry().size(), QSize(1280, 1024)); + + // Since we don't have a way to communicate with the client, manually activate + // the input method. + QSignalSpy inputMethodActiveSpy(Test::inputMethod(), &Test::MockInputMethod::activate); + kwinApp()->inputMethod()->setActive(true); + QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait()); + + auto keyboard = new Test::SimpleKeyboard(window); + auto context = Test::inputMethod()->context(); + QVERIFY(context); + + zwp_input_method_context_v1_commit_string(context, 0, "aB äÄ 안 😊"); + + QSignalSpy receivedTextChangedSpy(keyboard, &Test::SimpleKeyboard::receviedTextChanged); + bool matched = false; + for (int i = 0; i < 100; ++i) { + if (keyboard->receviedText() == "aB äÄ 안 😊") { + matched = true; + break; + } + receivedTextChangedSpy.wait(); + } + QVERIFY(matched); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); + kwinApp()->inputMethod()->setActive(false); + QVERIFY(!kwinApp()->inputMethod()->isActive()); +} + +void InputMethodTest::testOverlayPositioning_data() +{ + QTest::addColumn("cursorRectangle"); + QTest::addColumn("result"); + + QTest::newRow("regular") << Rect(10, 20, 30, 40) << RectF(60, 160, 200, 50); + QTest::newRow("offscreen-left") << Rect(-200, 40, 30, 40) << RectF(0, 180, 200, 50); + QTest::newRow("offscreen-right") << Rect(1200, 40, 30, 40) << RectF(1080, 180, 200, 50); + QTest::newRow("offscreen-top") << Rect(1200, -400, 30, 40) << RectF(1080, 0, 200, 50); + // Check it is flipped near the bottom of screen (anchor point 844 + 100 + 40 = 1024 - 40) + QTest::newRow("offscreen-bottom-flip") << Rect(1200, 844, 30, 40) << RectF(1080, 894, 200, 50); + // Top is (screen height 1024 - window height 50) = 984 + QTest::newRow("offscreen-bottom-slide") << Rect(1200, 1200, 30, 40) << RectF(1080, 974, 200, 50); +} + +void InputMethodTest::testOverlayPositioning() +{ + QFETCH(Rect, cursorRectangle); + QFETCH(RectF, result); + Test::inputMethod()->setMode(Test::MockInputMethod::Mode::Overlay); + QVERIFY(!kwinApp()->inputMethod()->isActive()); + + touchNow(); + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + QSignalSpy windowRemovedSpy(workspace(), &Workspace::windowRemoved); + + QSignalSpy activateSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged); + std::unique_ptr textInput(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat())); + + // Create an xdg_toplevel surface and wait for the compositor to catch up. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + // Make the window smaller than the screen and move it. + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1080, 824), Qt::red); + window->move(QPointF(50, 100)); + waylandServer()->seat()->setFocusedTextInputSurface(window->surface()); + + textInput->setCursorRectangle(cursorRectangle); + textInput->enable(surface.get()); + // Overlay is shown upon activate + QVERIFY(windowAddedSpy.wait()); + + QCOMPARE(workspace()->activeWindow(), window); + + QCOMPARE(windowAddedSpy.count(), 2); + QVERIFY(activateSpy.count() || activateSpy.wait()); + QVERIFY(kwinApp()->inputMethod()->isActive()); + + auto keyboardWindow = kwinApp()->inputMethod()->panel(); + QVERIFY(keyboardWindow); + // Check the overlay window is placed with cursor rectangle + window position. + QCOMPARE(keyboardWindow->frameGeometry(), result); + + // Destroy the test window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); + + Test::inputMethod()->setMode(Test::MockInputMethod::Mode::TopLevel); +} + +void InputMethodTest::testV3AutoCommit() +{ + Test::inputMethod()->setMode(Test::MockInputMethod::Mode::Overlay); + + // Create an xdg_toplevel surface and wait for the compositor to catch up. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red); + QVERIFY(window); + QVERIFY(window->isActive()); + + auto textInputV3 = std::make_unique(); + textInputV3->init(Test::waylandTextInputManagerV3()->get_text_input(*(Test::waylandSeat()))); + textInputV3->enable(); + + QSignalSpy textInputPreeditSpy(textInputV3.get(), &Test::TextInputV3::preeditString); + QSignalSpy textInputCommitTextSpy(textInputV3.get(), &Test::TextInputV3::commitString); + + QSignalSpy inputMethodActiveSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged); + QSignalSpy inputMethodActivateSpy(Test::inputMethod(), &Test::MockInputMethod::activate); + // just enabling the text-input should not show it but rather on commit + QVERIFY(!kwinApp()->inputMethod()->isActive()); + textInputV3->commit(); + QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait()); + QVERIFY(kwinApp()->inputMethod()->isActive()); + QVERIFY(inputMethodActivateSpy.wait()); + auto context = Test::inputMethod()->context(); + + zwp_input_method_context_v1_preedit_string(context, 1, "preedit1", "commit1"); + QVERIFY(textInputPreeditSpy.wait()); + QVERIFY(textInputCommitTextSpy.count() == 0); + + // ****************** + // Non-grabbing key press + int timestamp = 0; + Test::keyboardKeyPressed(KEY_A, timestamp++); + Test::keyboardKeyReleased(KEY_A, timestamp++); + QVERIFY(textInputCommitTextSpy.wait()); + QCOMPARE(textInputCommitTextSpy.last()[0].toString(), "commit1"); + QCOMPARE(textInputPreeditSpy.last()[0].toString(), QString()); + + // ************** + // Mouse clicks + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + textInputV3->disable(); + textInputV3->enable(); + const QList windows = workspace()->windows(); + auto it = std::find_if(windows.begin(), windows.end(), [](Window *w) { + return w->isInputMethod(); + }); + QVERIFY(it != windows.end()); + auto textInputWindow = *it; + + textInputV3->commit(); + zwp_input_method_context_v1_preedit_string(context, 1, "preedit2", "commit2"); + QVERIFY(textInputPreeditSpy.wait()); + QCOMPARE(textInputPreeditSpy.last()[0].toString(), QString("preedit2")); + + // mouse clicks on a VK does not submit + Test::pointerMotion(textInputWindow->frameGeometry().center(), timestamp++); + Test::pointerButtonPressed(1, timestamp++); + Test::pointerButtonReleased(1, timestamp++); + QVERIFY(!textInputCommitTextSpy.wait(20)); + + // mouse clicks on our main window submits the string + Test::pointerMotion(window->frameGeometry().center(), timestamp++); + Test::pointerButtonPressed(1, timestamp++); + Test::pointerButtonReleased(1, timestamp++); + + QVERIFY(textInputCommitTextSpy.wait()); + QCOMPARE(textInputCommitTextSpy.last()[0].toString(), "commit2"); + QCOMPARE(textInputPreeditSpy.last()[0].toString(), QString()); + + // ***************** + // Change focus + textInputV3->commit(); + zwp_input_method_context_v1_preedit_string(context, 1, "preedit3", "commit3"); + QVERIFY(textInputPreeditSpy.wait()); + QCOMPARE(textInputPreeditSpy.last()[0].toString(), QString("preedit3")); + + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + Window *window2 = Test::renderAndWaitForShown(surface2.get(), QSize(1280, 1024), Qt::blue); + QVERIFY(window2->isActive()); + + // these variables refer to the old window + QVERIFY(textInputCommitTextSpy.wait()); + QCOMPARE(textInputCommitTextSpy.last()[0].toString(), "commit3"); + QCOMPARE(textInputPreeditSpy.last()[0].toString(), QString()); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowClosed(window2)); +} + +void InputMethodTest::testSendRepeatInfo() +{ + // Create a client that's using old seat version prior 10. + auto legacyClient = Test::Connection::setup(Test::AdditionalWaylandInterface::TextInputManagerV3); + auto *registry = legacyClient->registry; + + auto seatInterface = registry->interface(KWayland::Client::Registry::Interface::Seat); + QVERIFY(seatInterface.version >= 5); + + // Use an old version supported by kwayland right now. + legacyClient->seat = registry->createSeat(seatInterface.name, 5); + QVERIFY(Test::waitForWaylandKeyboard(legacyClient->seat)); + auto legacyKeyboard = std::unique_ptr(legacyClient->seat->createKeyboard()); + auto *wlKeyboard = static_cast(*legacyKeyboard); + QVERIFY(wl_proxy_get_version(reinterpret_cast(wlKeyboard)) == 5); + + QSignalSpy keyrepeatSpy(legacyKeyboard.get(), &KWayland::Client::Keyboard::keyRepeatChanged); + // Wait for initial modifiers update + QVERIFY(keyrepeatSpy.wait()); + QVERIFY(legacyKeyboard->isKeyRepeatEnabled()); + auto surface = Test::createSurface(legacyClient->compositor); + auto shellSurface = Test::createXdgToplevelSurface(legacyClient->xdgShell, surface.get()); + Test::renderAndWaitForShown(legacyClient->shm, surface.get(), QSize(100, 100), Qt::cyan); + QSignalSpy firstEnteredSpy(legacyKeyboard.get(), &KWayland::Client::Keyboard::entered); + QVERIFY(firstEnteredSpy.wait()); + + auto textInputV3 = std::make_unique(); + textInputV3->init(legacyClient->textInputManagerV3->get_text_input(*legacyClient->seat)); + textInputV3->enable(); + + QSignalSpy inputMethodActiveSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged); + QSignalSpy inputMethodActivateSpy(Test::inputMethod(), &Test::MockInputMethod::activate); + // just enabling the text-input should not show it but rather on commit + QVERIFY(!kwinApp()->inputMethod()->isActive()); + textInputV3->commit(); + QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait()); + QVERIFY(kwinApp()->inputMethod()->isActive()); + QVERIFY(inputMethodActivateSpy.wait()); + auto *context = Test::inputMethod()->context(); + std::unique_ptr keyboardGrab(new KWayland::Client::Keyboard); + keyboardGrab->setup(zwp_input_method_context_v1_grab_keyboard(context)); + + QSignalSpy keygrabRepeatSpy(keyboardGrab.get(), &KWayland::Client::Keyboard::keyRepeatChanged); + // Wait for initial modifiers update + QVERIFY(keygrabRepeatSpy.wait()); + QVERIFY(keyboardGrab->isKeyRepeatEnabled()); +} + +void InputMethodTest::testSendRepeatInfoV10() +{ + // Create a client that's using latest seat version supported by kwin. + auto client = Test::Connection::setup(Test::AdditionalWaylandInterface::TextInputManagerV3); + auto *registry = client->registry; + + auto seatInterface = registry->interface(KWayland::Client::Registry::Interface::Seat); + QVERIFY(seatInterface.version >= 10); + + // KWayland::Client's interface version is too low to do this test, have to use native API. + auto *wlSeat = reinterpret_cast(wl_registry_bind(registry->registry(), seatInterface.name, &wl_seat_interface, seatInterface.version)); + client->seat = new KWayland::Client::Seat; + client->seat->setup(wlSeat); + QVERIFY(Test::waitForWaylandKeyboard(client->seat)); + auto keyboard = std::unique_ptr(client->seat->createKeyboard()); + auto *wlKeyboard = static_cast(*keyboard); + QVERIFY(wl_proxy_get_version(reinterpret_cast(wlKeyboard)) == seatInterface.version); + QSignalSpy keyrepeatSpy(keyboard.get(), &KWayland::Client::Keyboard::keyRepeatChanged); + // Wait for initial modifiers update + QVERIFY(keyrepeatSpy.wait()); + QVERIFY(!keyboard->isKeyRepeatEnabled()); + auto surface = Test::createSurface(client->compositor); + auto shellSurface = Test::createXdgToplevelSurface(client->xdgShell, surface.get()); + Test::renderAndWaitForShown(client->shm, surface.get(), QSize(100, 100), Qt::cyan); + QSignalSpy firstEnteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); + QVERIFY(firstEnteredSpy.wait()); + + auto textInputV3 = std::make_unique(); + textInputV3->init(client->textInputManagerV3->get_text_input(*client->seat)); + textInputV3->enable(); + + QSignalSpy inputMethodActiveSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged); + QSignalSpy inputMethodActivateSpy(Test::inputMethod(), &Test::MockInputMethod::activate); + // just enabling the text-input should not show it but rather on commit + QVERIFY(!kwinApp()->inputMethod()->isActive()); + textInputV3->commit(); + QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait()); + QVERIFY(kwinApp()->inputMethod()->isActive()); + QVERIFY(inputMethodActivateSpy.wait()); + auto *context = Test::inputMethod()->context(); + std::unique_ptr keyboardGrab(new KWayland::Client::Keyboard); + keyboardGrab->setup(zwp_input_method_context_v1_grab_keyboard(context)); + + QSignalSpy keygrabRepeatSpy(keyboardGrab.get(), &KWayland::Client::Keyboard::keyRepeatChanged); + // Wait for initial modifiers update + QVERIFY(keygrabRepeatSpy.wait()); + + QVERIFY(!keyboardGrab->isKeyRepeatEnabled()); +} + +WAYLANDTEST_MAIN(InputMethodTest) + +#include "inputmethod_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/internal_window.cpp b/local/recipes/kde/kwin/source/autotests/integration/internal_window.cpp new file mode 100644 index 0000000000..2946e056c5 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/internal_window.cpp @@ -0,0 +1,675 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "effect/effecthandler.h" +#include "internalwindow.h" +#include "pointer_input.h" +#include "wayland/surface.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_internal_window-0"); + +class InternalWindowTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testGeometry(); + void testEnterLeave(); + void testPointerPressRelease(); + void testPointerAxis(); + void testTouch(); + void testOpacity(); + void testMove(); + void testSkipCloseAnimation_data(); + void testSkipCloseAnimation(); + void testModifierClickUnrestrictedMove(); + void testModifierScroll(); + void testPopup(); + void testScale(); + void testEffectWindow(); + void testReentrantMoveResize(); + void testDismissPopup(); +}; + +class HelperWindow : public QRasterWindow +{ + Q_OBJECT +public: + HelperWindow(); + ~HelperWindow() override; + + QPoint latestGlobalMousePos() const + { + return m_latestGlobalMousePos; + } + Qt::MouseButtons pressedButtons() const + { + return m_pressedButtons; + } + +Q_SIGNALS: + void entered(); + void left(); + void mouseMoved(const QPoint &global); + void mousePressed(); + void mouseReleased(); + void wheel(); + void keyPressed(); + void keyReleased(); + void touchDown(int id, const QPointF &pos); + void touchUp(int id); + void touchMotion(int id, const QPointF &pos); + +protected: + void paintEvent(QPaintEvent *event) override; + bool event(QEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void wheelEvent(QWheelEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void keyReleaseEvent(QKeyEvent *event) override; + void touchEvent(QTouchEvent *event) override; + +private: + QPoint m_latestGlobalMousePos; + Qt::MouseButtons m_pressedButtons = Qt::MouseButtons(); +}; + +HelperWindow::HelperWindow() + : QRasterWindow(nullptr) +{ + setFlags(Qt::FramelessWindowHint); +} + +HelperWindow::~HelperWindow() = default; + +void HelperWindow::paintEvent(QPaintEvent *event) +{ + QPainter p(this); + p.fillRect(0, 0, width(), height(), Qt::red); +} + +bool HelperWindow::event(QEvent *event) +{ + if (event->type() == QEvent::Enter) { + Q_EMIT entered(); + } + if (event->type() == QEvent::Leave) { + Q_EMIT left(); + } + return QRasterWindow::event(event); +} + +void HelperWindow::mouseMoveEvent(QMouseEvent *event) +{ + m_latestGlobalMousePos = event->globalPosition().toPoint(); + Q_EMIT mouseMoved(m_latestGlobalMousePos); +} + +void HelperWindow::mousePressEvent(QMouseEvent *event) +{ + m_latestGlobalMousePos = event->globalPosition().toPoint(); + m_pressedButtons = event->buttons(); + Q_EMIT mousePressed(); +} + +void HelperWindow::mouseReleaseEvent(QMouseEvent *event) +{ + m_latestGlobalMousePos = event->globalPosition().toPoint(); + m_pressedButtons = event->buttons(); + Q_EMIT mouseReleased(); +} + +void HelperWindow::wheelEvent(QWheelEvent *event) +{ + Q_EMIT wheel(); +} + +void HelperWindow::keyPressEvent(QKeyEvent *event) +{ + Q_EMIT keyPressed(); +} + +void HelperWindow::keyReleaseEvent(QKeyEvent *event) +{ + Q_EMIT keyReleased(); +} + +void HelperWindow::touchEvent(QTouchEvent *event) +{ + for (int i = 0; i < event->pointCount(); ++i) { + QEventPoint &point = event->point(i); + + switch (point.state()) { + case QEventPoint::Unknown: + case QEventPoint::Stationary: + break; + case QEventPoint::Pressed: + Q_EMIT touchDown(point.id(), point.position()); + break; + case QEventPoint::Updated: + Q_EMIT touchMotion(point.id(), point.position()); + break; + case QEventPoint::Released: + Q_EMIT touchUp(point.id()); + break; + } + } +} + +void InternalWindowTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); +} + +void InternalWindowTest::init() +{ + input()->pointer()->warp(QPoint(512, 512)); + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); + QVERIFY(Test::waitForWaylandKeyboard()); +} + +void InternalWindowTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void InternalWindowTest::testGeometry() +{ + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.show(); + QTRY_COMPARE(windowAddedSpy.count(), 1); + auto internalWindow = windowAddedSpy.first().first().value(); + + // client initiated move + QSignalSpy frameGeometryChangedSpy(internalWindow, &Window::frameGeometryChanged); + win.setPosition(QPoint(20, 30)); + QCOMPARE(frameGeometryChangedSpy.count(), 1); + QCOMPARE(internalWindow->clientGeometry(), RectF(20, 30, 100, 100)); + QCOMPARE(win.geometry(), QRect(20, 30, 100, 100)); + + win.setPosition(QPoint(20, 30)); + QCOMPARE(frameGeometryChangedSpy.count(), 1); + + // client initiated resize + win.resize(150, 150); + QCOMPARE(frameGeometryChangedSpy.count(), 2); + QCOMPARE(internalWindow->clientGeometry(), RectF(20, 30, 150, 150)); + QCOMPARE(win.geometry(), QRect(20, 30, 150, 150)); + + win.resize(150, 150); + QCOMPARE(frameGeometryChangedSpy.count(), 2); + + // client initiated move+resize + win.setGeometry(QRect(50, 50, 200, 200)); + QCOMPARE(frameGeometryChangedSpy.count(), 3); + QCOMPARE(internalWindow->clientGeometry(), RectF(50, 50, 200, 200)); + QCOMPARE(win.geometry(), QRect(50, 50, 200, 200)); + + win.setGeometry(QRect(50, 50, 200, 200)); + QCOMPARE(frameGeometryChangedSpy.count(), 3); + + // server initiated move + internalWindow->move(internalWindow->clientPosToFramePos(QPointF(5, 5))); + QCOMPARE(frameGeometryChangedSpy.count(), 4); + QCOMPARE(internalWindow->clientGeometry(), RectF(5, 5, 200, 200)); + QCOMPARE(win.geometry(), QRect(5, 5, 200, 200)); + + internalWindow->move(internalWindow->clientPosToFramePos(QPointF(5, 5))); + QCOMPARE(frameGeometryChangedSpy.count(), 4); + + // server initiated resize + internalWindow->resize(internalWindow->clientSizeToFrameSize(QSizeF(100, 100))); + QCOMPARE(frameGeometryChangedSpy.count(), 5); + QCOMPARE(internalWindow->clientGeometry(), RectF(5, 5, 100, 100)); + QCOMPARE(win.geometry(), QRect(5, 5, 100, 100)); + + internalWindow->resize(internalWindow->clientSizeToFrameSize(QSizeF(100, 100))); + QCOMPARE(frameGeometryChangedSpy.count(), 5); + + // server initiated move+resize + internalWindow->moveResize(internalWindow->clientRectToFrameRect(RectF(100, 100, 300, 300))); + QCOMPARE(frameGeometryChangedSpy.count(), 6); + QCOMPARE(internalWindow->clientGeometry(), RectF(100, 100, 300, 300)); + QCOMPARE(win.geometry(), QRect(100, 100, 300, 300)); + + internalWindow->moveResize(internalWindow->clientRectToFrameRect(RectF(100, 100, 300, 300))); + QCOMPARE(frameGeometryChangedSpy.count(), 6); +} + +void InternalWindowTest::testEnterLeave() +{ + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + HelperWindow win; + QVERIFY(!workspace()->findInternal(nullptr)); + QVERIFY(!workspace()->findInternal(&win)); + win.setGeometry(0, 0, 100, 100); + win.show(); + + QTRY_COMPARE(windowAddedSpy.count(), 1); + QVERIFY(!workspace()->activeWindow()); + InternalWindow *window = windowAddedSpy.first().first().value(); + QVERIFY(window); + QVERIFY(window->isInternal()); + QVERIFY(!window->isDecorated()); + QCOMPARE(workspace()->findInternal(&win), window); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 100, 100)); + QVERIFY(window->isShown()); + QVERIFY(workspace()->stackingOrder().contains(window)); + + QSignalSpy enterSpy(&win, &HelperWindow::entered); + QSignalSpy leaveSpy(&win, &HelperWindow::left); + QSignalSpy moveSpy(&win, &HelperWindow::mouseMoved); + + quint32 timestamp = 1; + Test::pointerMotion(QPoint(50, 50), timestamp++); + QTRY_COMPARE(moveSpy.count(), 1); + + Test::pointerMotion(QPoint(60, 50), timestamp++); + QTRY_COMPARE(moveSpy.count(), 2); + QCOMPARE(moveSpy[1].first().toPoint(), QPoint(60, 50)); + + Test::pointerMotion(QPoint(101, 50), timestamp++); + QTRY_COMPARE(leaveSpy.count(), 1); + + // set a mask on the window + win.setMask(QRegion(10, 20, 30, 40)); + // outside the mask we should not get an enter + Test::pointerMotion(QPoint(5, 5), timestamp++); + QVERIFY(!enterSpy.wait(100)); + QCOMPARE(enterSpy.count(), 1); + // inside the mask we should still get an enter + Test::pointerMotion(QPoint(25, 27), timestamp++); + QTRY_COMPARE(enterSpy.count(), 2); +} + +void InternalWindowTest::testPointerPressRelease() +{ + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.show(); + QSignalSpy pressSpy(&win, &HelperWindow::mousePressed); + QSignalSpy releaseSpy(&win, &HelperWindow::mouseReleased); + + QTRY_COMPARE(windowAddedSpy.count(), 1); + + quint32 timestamp = 1; + Test::pointerMotion(QPoint(50, 50), timestamp++); + + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + QTRY_COMPARE(pressSpy.count(), 1); + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + QTRY_COMPARE(releaseSpy.count(), 1); +} + +void InternalWindowTest::testPointerAxis() +{ + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.show(); + QSignalSpy wheelSpy(&win, &HelperWindow::wheel); + QTRY_COMPARE(windowAddedSpy.count(), 1); + + quint32 timestamp = 1; + Test::pointerMotion(QPoint(50, 50), timestamp++); + + Test::pointerAxisVertical(15.0, timestamp++); + QTRY_COMPARE(wheelSpy.count(), 1); + Test::pointerAxisHorizontal(15.0, timestamp++); + QTRY_COMPARE(wheelSpy.count(), 2); +} + +void InternalWindowTest::testTouch() +{ + // touch events for internal windows are emulated through mouse events + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.show(); + QTRY_COMPARE(windowAddedSpy.count(), 1); + + QSignalSpy touchDownSpy(&win, &HelperWindow::touchDown); + QSignalSpy touchUpSpy(&win, &HelperWindow::touchUp); + QSignalSpy touchMotionSpy(&win, &HelperWindow::touchMotion); + + quint32 timestamp = 1; + Test::touchDown(1, QPointF(50, 50), timestamp++); + QCOMPARE(touchDownSpy.count(), 1); + QCOMPARE(touchDownSpy.last().at(0).toInt(), 1); + QCOMPARE(touchDownSpy.last().at(1).toPointF(), QPointF(50, 50)); + + Test::touchMotion(1, QPointF(90, 80), timestamp++); + QCOMPARE(touchMotionSpy.count(), 1); + QCOMPARE(touchMotionSpy.last().at(0).toInt(), 1); + QCOMPARE(touchMotionSpy.last().at(1).toPointF(), QPointF(90, 80)); + + Test::touchUp(1, timestamp++); + QCOMPARE(touchUpSpy.count(), 1); + QCOMPARE(touchUpSpy.last().at(0).toInt(), 1); +} + +void InternalWindowTest::testOpacity() +{ + // this test verifies that opacity is properly synced from QWindow to InternalClient + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + HelperWindow win; + win.setOpacity(0.5); + win.setGeometry(0, 0, 100, 100); + win.show(); + QTRY_COMPARE(windowAddedSpy.count(), 1); + auto internalWindow = windowAddedSpy.first().first().value(); + QVERIFY(internalWindow); + QVERIFY(internalWindow->isInternal()); + QCOMPARE(internalWindow->opacity(), 0.5); + + QSignalSpy opacityChangedSpy(internalWindow, &InternalWindow::opacityChanged); + win.setOpacity(0.75); + QCOMPARE(opacityChangedSpy.count(), 1); + QCOMPARE(internalWindow->opacity(), 0.75); +} + +void InternalWindowTest::testMove() +{ + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + HelperWindow win; + win.setOpacity(0.5); + win.setGeometry(0, 0, 100, 100); + win.show(); + QTRY_COMPARE(windowAddedSpy.count(), 1); + auto internalWindow = windowAddedSpy.first().first().value(); + QVERIFY(internalWindow); + QCOMPARE(internalWindow->frameGeometry(), RectF(0, 0, 100, 100)); + + // normal move should be synced + internalWindow->move(QPoint(5, 10)); + QCOMPARE(internalWindow->frameGeometry(), RectF(5, 10, 100, 100)); + QTRY_COMPARE(win.geometry(), QRect(5, 10, 100, 100)); + // another move should also be synced + internalWindow->move(QPoint(10, 20)); + QCOMPARE(internalWindow->frameGeometry(), RectF(10, 20, 100, 100)); + QTRY_COMPARE(win.geometry(), QRect(10, 20, 100, 100)); +} + +void InternalWindowTest::testSkipCloseAnimation_data() +{ + QTest::addColumn("initial"); + + QTest::newRow("set") << true; + QTest::newRow("not set") << false; +} + +void InternalWindowTest::testSkipCloseAnimation() +{ + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + HelperWindow win; + win.setOpacity(0.5); + win.setGeometry(0, 0, 100, 100); + QFETCH(bool, initial); + win.setProperty("KWIN_SKIP_CLOSE_ANIMATION", initial); + win.show(); + QTRY_COMPARE(windowAddedSpy.count(), 1); + auto internalWindow = windowAddedSpy.first().first().value(); + QVERIFY(internalWindow); + QCOMPARE(internalWindow->skipsCloseAnimation(), initial); + QSignalSpy skipCloseChangedSpy(internalWindow, &Window::skipCloseAnimationChanged); + win.setProperty("KWIN_SKIP_CLOSE_ANIMATION", !initial); + QCOMPARE(skipCloseChangedSpy.count(), 1); + QCOMPARE(internalWindow->skipsCloseAnimation(), !initial); + win.setProperty("KWIN_SKIP_CLOSE_ANIMATION", initial); + QCOMPARE(skipCloseChangedSpy.count(), 2); + QCOMPARE(internalWindow->skipsCloseAnimation(), initial); +} + +void InternalWindowTest::testModifierClickUnrestrictedMove() +{ + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.setFlags(win.flags() & ~Qt::FramelessWindowHint); + win.show(); + QTRY_COMPARE(windowAddedSpy.count(), 1); + auto internalWindow = windowAddedSpy.first().first().value(); + QVERIFY(internalWindow); + QVERIFY(internalWindow->isDecorated()); + + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("MouseBindings")); + group.writeEntry("CommandAllKey", "Meta"); + group.writeEntry("CommandAll1", "Move"); + group.writeEntry("CommandAll2", "Move"); + group.writeEntry("CommandAll3", "Move"); + group.sync(); + workspace()->slotReconfigure(); + QCOMPARE(options->commandAllModifier(), Qt::MetaModifier); + QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove); + QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove); + QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove); + + // move cursor on window + input()->pointer()->warp(internalWindow->frameGeometry().center()); + + // simulate modifier+click + quint32 timestamp = 1; + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + QVERIFY(!internalWindow->isInteractiveMove()); + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + QVERIFY(internalWindow->isInteractiveMove()); + // release modifier should not change it + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + QVERIFY(internalWindow->isInteractiveMove()); + // but releasing the key should end move/resize + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + QVERIFY(!internalWindow->isInteractiveMove()); +} + +void InternalWindowTest::testModifierScroll() +{ + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.setFlags(win.flags() & ~Qt::FramelessWindowHint); + win.show(); + QTRY_COMPARE(windowAddedSpy.count(), 1); + auto internalWindow = windowAddedSpy.first().first().value(); + QVERIFY(internalWindow); + QVERIFY(internalWindow->isDecorated()); + + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("MouseBindings")); + group.writeEntry("CommandAllKey", "Meta"); + group.writeEntry("CommandAllWheel", "change opacity"); + group.sync(); + workspace()->slotReconfigure(); + + // move cursor on window + input()->pointer()->warp(internalWindow->frameGeometry().center()); + + // set the opacity to 0.5 + internalWindow->setOpacity(0.5); + QCOMPARE(internalWindow->opacity(), 0.5); + quint32 timestamp = 1; + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + Test::pointerAxisVertical(-15, timestamp++); + QCOMPARE(internalWindow->opacity(), 0.6); + Test::pointerAxisVertical(15, timestamp++); + QCOMPARE(internalWindow->opacity(), 0.5); + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); +} + +void InternalWindowTest::testPopup() +{ + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.setFlags(win.flags() | Qt::Popup); + win.show(); + QTRY_COMPARE(windowAddedSpy.count(), 1); + auto internalWindow = windowAddedSpy.first().first().value(); + QVERIFY(internalWindow); + QCOMPARE(internalWindow->isPopupWindow(), true); + + QSignalSpy pressSpy(&win, &HelperWindow::keyPressed); + QSignalSpy releaseSpy(&win, &HelperWindow::keyReleased); + + quint32 timestamp = 1; + Test::keyboardKeyPressed(KEY_A, timestamp++); + QTRY_COMPARE(pressSpy.count(), 1); + QCOMPARE(releaseSpy.count(), 0); + Test::keyboardKeyReleased(KEY_A, timestamp++); + QTRY_COMPARE(releaseSpy.count(), 1); + QCOMPARE(pressSpy.count(), 1); +} + +void InternalWindowTest::testScale() +{ + Test::setOutputConfig({ + Test::OutputInfo{ + .geometry = Rect(0, 0, 1280, 1024), + .scale = 2.0, + }, + Test::OutputInfo{ + .geometry = Rect(1280, 0, 1280, 1024), + .scale = 2.0, + }, + }); + + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.setFlags(win.flags() | Qt::Popup); + win.show(); + QCOMPARE(win.devicePixelRatio(), 2.0); + QTRY_COMPARE(windowAddedSpy.count(), 1); + auto internalWindow = windowAddedSpy.first().first().value(); + QCOMPARE(internalWindow->bufferScale(), 2); +} + +void InternalWindowTest::testEffectWindow() +{ + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.show(); + QTRY_COMPARE(windowAddedSpy.count(), 1); + auto internalWindow = windowAddedSpy.first().first().value(); + QVERIFY(internalWindow); + QVERIFY(internalWindow->effectWindow()); + QCOMPARE(internalWindow->effectWindow()->internalWindow(), &win); + + QCOMPARE(effects->findWindow(&win), internalWindow->effectWindow()); + QCOMPARE(effects->findWindow(&win)->internalWindow(), &win); +} + +void InternalWindowTest::testReentrantMoveResize() +{ + // This test verifies that calling moveResize() from a slot connected directly + // to the frameGeometryChanged() signal won't cause an infinite recursion. + + // Create an internal window. + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.show(); + QTRY_COMPARE(windowAddedSpy.count(), 1); + auto window = windowAddedSpy.first().first().value(); + QVERIFY(window); + QCOMPARE(window->pos(), QPoint(0, 0)); + + // Let's pretend that there is a script that really wants the window to be at (100, 100). + connect(window, &Window::frameGeometryChanged, this, [window]() { + window->moveResize(RectF(QPointF(100, 100), window->size())); + }); + + // Trigger the lambda above. + window->move(QPoint(40, 50)); + + // Eventually, the window will end up at (100, 100). + QCOMPARE(window->pos(), QPoint(100, 100)); +} + +void InternalWindowTest::testDismissPopup() +{ + // This test verifies that a popup window created by the compositor will be dismissed + // when user clicks another window. + + // Create a toplevel window. + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + HelperWindow clientToplevel; + clientToplevel.setGeometry(0, 0, 100, 100); + clientToplevel.show(); + QTRY_COMPARE(windowAddedSpy.count(), 1); + auto serverToplevel = windowAddedSpy.last().first().value(); + QVERIFY(serverToplevel); + + // Create a popup window. + QRasterWindow clientPopup; + clientPopup.setFlag(Qt::Popup); + clientPopup.setTransientParent(&clientToplevel); + clientPopup.setGeometry(0, 0, 50, 50); + clientPopup.show(); + QTRY_COMPARE(windowAddedSpy.count(), 2); + auto serverPopup = windowAddedSpy.last().first().value(); + QVERIFY(serverPopup); + + // Create the other window to click + HelperWindow otherClientToplevel; + otherClientToplevel.setGeometry(100, 100, 100, 100); + otherClientToplevel.show(); + QTRY_COMPARE(windowAddedSpy.count(), 3); + auto serverOtherToplevel = windowAddedSpy.last().first().value(); + QVERIFY(serverOtherToplevel); + + // Click somewhere outside the popup window. + QSignalSpy popupClosedSpy(serverPopup, &InternalWindow::closed); + quint32 timestamp = 0; + Test::pointerMotion(serverOtherToplevel->frameGeometry().center(), timestamp++); + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + QTRY_COMPARE(popupClosedSpy.count(), 1); +} + +} + +WAYLANDTEST_MAIN(KWin::InternalWindowTest) +#include "internal_window.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/keyboard_input_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/keyboard_input_test.cpp new file mode 100644 index 0000000000..54a7996e17 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/keyboard_input_test.cpp @@ -0,0 +1,222 @@ +/* + SPDX-FileCopyrightText: 2025 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "config-kwin.h" + +#include "wayland_server.h" +#include "workspace.h" + +#if KWIN_BUILD_GLOBALSHORTCUTS +#include +#endif + +#include +#include + +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_kbd_input-0"); + +class KeyboardInputTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void implicitGrab(); + void implicitGrabByClosedWindow(); + void globalShortcut(); + void testServerSideKeyRepeat(); + +private: + std::unique_ptr m_firstConnection; + std::unique_ptr m_firstKeyboard; + std::unique_ptr m_firstSurface; + std::unique_ptr m_firstShellSurface; + QPointer m_firstWindow; + + std::unique_ptr m_secondConnection; + std::unique_ptr m_secondKeyboard; + std::unique_ptr m_secondSurface; + std::unique_ptr m_secondShellSurface; + QPointer m_secondWindow; +}; + +void KeyboardInputTest::initTestCase() +{ + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void KeyboardInputTest::init() +{ + m_firstConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat); + QVERIFY(Test::waitForWaylandKeyboard(m_firstConnection->seat)); + m_firstKeyboard = std::unique_ptr(m_firstConnection->seat->createKeyboard()); + m_firstSurface = Test::createSurface(m_firstConnection->compositor); + m_firstShellSurface = Test::createXdgToplevelSurface(m_firstConnection->xdgShell, m_firstSurface.get()); + m_firstWindow = Test::renderAndWaitForShown(m_firstConnection->shm, m_firstSurface.get(), QSize(100, 100), Qt::cyan); + QSignalSpy firstEnteredSpy(m_firstKeyboard.get(), &KWayland::Client::Keyboard::entered); + QVERIFY(firstEnteredSpy.wait()); + + m_secondConnection = Test::Connection::setup(Test::AdditionalWaylandInterface::Seat); + QVERIFY(Test::waitForWaylandKeyboard(m_secondConnection->seat)); + m_secondKeyboard = std::unique_ptr(m_secondConnection->seat->createKeyboard()); + m_secondSurface = Test::createSurface(m_secondConnection->compositor); + m_secondShellSurface = Test::createXdgToplevelSurface(m_secondConnection->xdgShell, m_secondSurface.get()); + m_secondWindow = Test::renderAndWaitForShown(m_secondConnection->shm, m_secondSurface.get(), QSize(100, 100), Qt::cyan); + QSignalSpy secondEnteredSpy(m_secondKeyboard.get(), &KWayland::Client::Keyboard::entered); + QVERIFY(secondEnteredSpy.wait()); +} + +void KeyboardInputTest::cleanup() +{ + m_firstShellSurface.reset(); + m_firstSurface.reset(); + m_firstKeyboard.reset(); + m_firstConnection.reset(); + + m_secondShellSurface.reset(); + m_secondSurface.reset(); + m_secondKeyboard.reset(); + m_secondConnection.reset(); +} + +void KeyboardInputTest::implicitGrab() +{ + QSignalSpy firstEnteredSpy(m_firstKeyboard.get(), &KWayland::Client::Keyboard::entered); + QSignalSpy firstKeyChangedSpy(m_firstKeyboard.get(), &KWayland::Client::Keyboard::keyChanged); + QSignalSpy secondEnteredSpy(m_secondKeyboard.get(), &KWayland::Client::Keyboard::entered); + QSignalSpy secondKeyChangedSpy(m_secondKeyboard.get(), &KWayland::Client::Keyboard::keyChanged); + + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_Q, timestamp++); + QVERIFY(secondKeyChangedSpy.wait()); + QCOMPARE(secondKeyChangedSpy.last().at(0).value(), KEY_Q); + QCOMPARE(secondKeyChangedSpy.last().at(1).value(), KWayland::Client::Keyboard::KeyState::Pressed); + + workspace()->activateWindow(m_firstWindow); + QVERIFY(firstEnteredSpy.wait()); // TODO: perhaps we should not receive the enter event until the q key is released + QCOMPARE(m_firstKeyboard->enteredKeys(), (QList{KEY_Q})); + + Test::keyboardKeyReleased(KEY_Q, timestamp++); + QVERIFY(firstKeyChangedSpy.wait()); + QCOMPARE(firstKeyChangedSpy.last().at(0).value(), KEY_Q); + QCOMPARE(firstKeyChangedSpy.last().at(1).value(), KWayland::Client::Keyboard::KeyState::Released); +} + +void KeyboardInputTest::implicitGrabByClosedWindow() +{ + // This test verifies that an implicit grab is preserved even after the window is closed. Note: + // currently it is not the case, but it should be. + + QSignalSpy firstEnteredSpy(m_firstKeyboard.get(), &KWayland::Client::Keyboard::entered); + QSignalSpy firstKeyChangedSpy(m_firstKeyboard.get(), &KWayland::Client::Keyboard::keyChanged); + QSignalSpy secondEnteredSpy(m_secondKeyboard.get(), &KWayland::Client::Keyboard::entered); + QSignalSpy secondKeyChangedSpy(m_secondKeyboard.get(), &KWayland::Client::Keyboard::keyChanged); + + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_Q, timestamp++); + QVERIFY(secondKeyChangedSpy.wait()); + QCOMPARE(secondKeyChangedSpy.last().at(0).value(), KEY_Q); + QCOMPARE(secondKeyChangedSpy.last().at(1).value(), KWayland::Client::Keyboard::KeyState::Pressed); + + m_secondShellSurface.reset(); + m_secondSurface.reset(); + QVERIFY(firstEnteredSpy.wait()); // TODO: perhaps we should not receive the enter event until the q key is released + QCOMPARE(m_firstKeyboard->enteredKeys(), (QList{KEY_Q})); + + Test::keyboardKeyReleased(KEY_Q, timestamp++); + QVERIFY(firstKeyChangedSpy.wait()); + QCOMPARE(firstKeyChangedSpy.last().at(0).value(), KEY_Q); + QCOMPARE(firstKeyChangedSpy.last().at(1).value(), KWayland::Client::Keyboard::KeyState::Released); +} + +void KeyboardInputTest::globalShortcut() +{ + // This test verifies that keys are not leaked to the clients when pressing a global shortcut. + +#if KWIN_BUILD_GLOBALSHORTCUTS + QSignalSpy firstEnteredSpy(m_firstKeyboard.get(), &KWayland::Client::Keyboard::entered); + QSignalSpy firstKeyChangedSpy(m_firstKeyboard.get(), &KWayland::Client::Keyboard::keyChanged); + QSignalSpy secondEnteredSpy(m_secondKeyboard.get(), &KWayland::Client::Keyboard::entered); + QSignalSpy secondKeyChangedSpy(m_secondKeyboard.get(), &KWayland::Client::Keyboard::keyChanged); + + auto action = std::make_unique(); + action->setObjectName(QStringLiteral("test")); + action->setProperty("componentName", QStringLiteral("test")); + KGlobalAccel::self()->setShortcut(action.get(), QList{Qt::META | Qt::Key_Space}, KGlobalAccel::NoAutoloading); + QSignalSpy actionTriggeredSpy(action.get(), &QAction::triggered); + + // the client should not see the space key being pressed or released + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + QVERIFY(secondKeyChangedSpy.wait()); + Test::keyboardKeyPressed(KEY_SPACE, timestamp++); + QVERIFY(!secondKeyChangedSpy.wait(10)); + QCOMPARE(actionTriggeredSpy.count(), 1); + Test::keyboardKeyReleased(KEY_SPACE, timestamp++); + QVERIFY(!secondKeyChangedSpy.wait(10)); + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + QVERIFY(secondKeyChangedSpy.wait()); + + // the space key should not be leaked even if the focused surface changes between pressing and releasing the space key + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + QVERIFY(secondKeyChangedSpy.wait()); + Test::keyboardKeyPressed(KEY_SPACE, timestamp++); + QVERIFY(!secondKeyChangedSpy.wait(10)); + QCOMPARE(actionTriggeredSpy.count(), 2); + m_secondShellSurface.reset(); + m_secondSurface.reset(); + QVERIFY(firstEnteredSpy.wait()); + QCOMPARE(m_firstKeyboard->enteredKeys(), (QList{KEY_LEFTMETA})); + Test::keyboardKeyReleased(KEY_SPACE, timestamp++); + QVERIFY(!firstKeyChangedSpy.wait(10)); + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + QVERIFY(firstKeyChangedSpy.wait()); +#endif +} + +void KeyboardInputTest::testServerSideKeyRepeat() +{ + const auto keyboard = m_firstConnection->kwinSeat->getKeyboard(); + QSignalSpy enter(keyboard.get(), &Test::WlKeyboard::enter); + QSignalSpy key(keyboard.get(), &Test::WlKeyboard::key); + + Test::XdgToplevelWindow window{m_firstConnection.get()}; + QVERIFY(window.show()); + QVERIFY(enter.wait()); + + uint32_t timestamp = 0; + Test::keyboardKeyPressed(KEY_SPACE, timestamp++); + QVERIFY(key.wait()); + QCOMPARE(key.last().last().value(), Test::WlKeyboard::key_state::key_state_pressed); + + // afer some time, we should get a key repeat + QVERIFY(key.wait()); + QCOMPARE(key.last().last().value(), Test::WlKeyboard::key_state::key_state_repeated); + + Test::keyboardKeyReleased(KEY_SPACE, timestamp++); + QVERIFY(key.wait()); + QCOMPARE(key.last().last().value(), Test::WlKeyboard::key_state::key_state_released); +} + +} // namespace KWin + +WAYLANDTEST_MAIN(KWin::KeyboardInputTest) +#include "keyboard_input_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/keyboard_layout_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/keyboard_layout_test.cpp new file mode 100644 index 0000000000..42e76a04a9 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/keyboard_layout_test.cpp @@ -0,0 +1,531 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "keyboard_input.h" +#include "keyboard_layout.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" +#include "xkb.h" + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_keyboard_laout-0"); + +class KeyboardLayoutTest : public QObject +{ + Q_OBJECT +public: + KeyboardLayoutTest() + : layoutsReconfiguredSpy(this, &KeyboardLayoutTest::layoutListChanged) + , layoutChangedSpy(this, &KeyboardLayoutTest::layoutChanged) + { + + QVERIFY(QDBusConnection::sessionBus().connect(QStringLiteral("org.kde.keyboard"), QStringLiteral("/Layouts"), QStringLiteral("org.kde.KeyboardLayouts"), QStringLiteral("layoutListChanged"), this, SIGNAL(layoutListChanged()))); + QVERIFY(QDBusConnection::sessionBus().connect(QStringLiteral("org.kde.keyboard"), QStringLiteral("/Layouts"), QStringLiteral("org.kde.KeyboardLayouts"), QStringLiteral("layoutChanged"), this, SIGNAL(layoutChanged(uint)))); + } + +Q_SIGNALS: + void layoutChanged(uint index); + void layoutListChanged(); + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testReconfigure(); + void testChangeLayoutThroughDBus(); + void testPerLayoutShortcut(); + void testVirtualDesktopPolicy(); + void testWindowPolicy(); + void testApplicationPolicy(); + void testNumLock(); + +private: + void reconfigureLayouts(); + void resetLayouts(); + auto changeLayout(uint index); + void callSession(const QString &method); + QSignalSpy layoutsReconfiguredSpy; + QSignalSpy layoutChangedSpy; + KConfigGroup layoutGroup; +}; + +void KeyboardLayoutTest::reconfigureLayouts() +{ + QVERIFY(layoutsReconfiguredSpy.wait(1000)); + QCOMPARE(layoutsReconfiguredSpy.count(), 1); + layoutsReconfiguredSpy.clear(); +} + +void KeyboardLayoutTest::resetLayouts() +{ + /* Switch Policy to destroy layouts from memory. + * On return to original Policy they should reload from disk. + */ + callSession(QStringLiteral("aboutToSaveSession")); + + const QString policy = layoutGroup.readEntry("SwitchMode", "Global"); + + if (policy == QLatin1String("Global")) { + layoutGroup.writeEntry("SwitchMode", "Desktop", KConfig::Notify); + } else { + layoutGroup.deleteEntry("SwitchMode", KConfig::Notify); + } + layoutGroup.sync(); + reconfigureLayouts(); + + layoutGroup.writeEntry("SwitchMode", policy, KConfig::Notify); + layoutGroup.sync(); + reconfigureLayouts(); + + callSession(QStringLiteral("loadSession")); +} + +auto KeyboardLayoutTest::changeLayout(uint index) +{ + QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.keyboard"), + QStringLiteral("/Layouts"), + QStringLiteral("org.kde.KeyboardLayouts"), + QStringLiteral("setLayout")); + msg << index; + return QDBusConnection::sessionBus().asyncCall(msg); +} + +void KeyboardLayoutTest::callSession(const QString &method) +{ + QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Session"), + QStringLiteral("org.kde.KWin.Session"), + method); + msg << QLatin1String(); // session name + QVERIFY(QDBusConnection::sessionBus().call(msg).type() != QDBusMessage::ErrorMessage); +} + +void KeyboardLayoutTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->setKxkbConfig(KSharedConfig::openConfig(QStringLiteral("kxkbrc"), KConfig::NoGlobals)); + + layoutGroup = kwinApp()->kxkbConfig()->group(QStringLiteral("Layout")); + layoutGroup.deleteGroup(); + layoutGroup.sync(); + + kwinApp()->start(); + + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + + // don't get DBus signal on one-layout configuration + // QVERIFY(layoutsReconfiguredSpy.wait()); + // QCOMPARE(layoutsReconfiguredSpy.count(), 1); + // layoutsReconfiguredSpy.clear(); +} + +void KeyboardLayoutTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void KeyboardLayoutTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void KeyboardLayoutTest::testReconfigure() +{ + // verifies that we can change the keymap + + // default should be a keymap with only us layout + auto xkb = input()->keyboard()->xkb(); + QCOMPARE(xkb->numberOfLayouts(), 1u); + QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + QCOMPARE(xkb->numberOfLayouts(), 1); + QCOMPARE(xkb->layoutName(0), QStringLiteral("English (US)")); + + // create a new keymap + KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group(QStringLiteral("Layout")); + layoutGroup.writeEntry("LayoutList", QStringLiteral("de,us"), KConfig::Notify); + layoutGroup.sync(); + reconfigureLayouts(); + + // now we should have two layouts + QCOMPARE(xkb->numberOfLayouts(), 2u); + // default layout is German + QCOMPARE(xkb->layoutName(), QStringLiteral("German")); + QCOMPARE(xkb->numberOfLayouts(), 2); + QCOMPARE(xkb->layoutName(0), QStringLiteral("German")); + QCOMPARE(xkb->layoutName(1), QStringLiteral("English (US)")); +} + +void KeyboardLayoutTest::testChangeLayoutThroughDBus() +{ + // this test verifies that the layout can be changed through DBus + // first configure layouts + enum Layout { + de, + us, + de_neo, + bad, + }; + layoutGroup.writeEntry("LayoutList", QStringLiteral("de,us,de(neo)"), KConfig::Notify); + layoutGroup.sync(); + reconfigureLayouts(); + // now we should have three layouts + auto xkb = input()->keyboard()->xkb(); + QCOMPARE(xkb->numberOfLayouts(), 3u); + // default layout is German + xkb->switchToLayout(0); + QCOMPARE(xkb->layoutName(), QStringLiteral("German")); + + // place garbage to layout entry + layoutGroup.writeEntry("LayoutDefaultFoo", "garbage", KConfig::Notify); + // make sure the garbage is wiped out on saving + resetLayouts(); + QVERIFY(!layoutGroup.hasKey("LayoutDefaultFoo")); + + // now change through DBus to English + auto reply = changeLayout(Layout::us); + reply.waitForFinished(); + QVERIFY(!reply.isError()); + QCOMPARE(reply.reply().arguments().first().toBool(), true); + QVERIFY(layoutChangedSpy.wait()); + QCOMPARE(layoutChangedSpy.count(), 1); + layoutChangedSpy.clear(); + + // layout should persist after reset + resetLayouts(); + QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + QVERIFY(layoutChangedSpy.wait()); + QCOMPARE(layoutChangedSpy.count(), 1); + layoutChangedSpy.clear(); + + // switch to a layout which does not exist + reply = changeLayout(Layout::bad); + QVERIFY(!reply.isError()); + QCOMPARE(reply.reply().arguments().first().toBool(), false); + QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + QVERIFY(!layoutChangedSpy.wait(1000)); + + // switch to another layout should work + reply = changeLayout(Layout::de); + QVERIFY(!reply.isError()); + QCOMPARE(reply.reply().arguments().first().toBool(), true); + QCOMPARE(xkb->layoutName(), QStringLiteral("German")); + QVERIFY(layoutChangedSpy.wait(1000)); + QCOMPARE(layoutChangedSpy.count(), 1); + + // switching to same layout should also work + reply = changeLayout(Layout::de); + QVERIFY(!reply.isError()); + QCOMPARE(reply.reply().arguments().first().toBool(), true); + QCOMPARE(xkb->layoutName(), QStringLiteral("German")); + QVERIFY(!layoutChangedSpy.wait(1000)); +} + +void KeyboardLayoutTest::testPerLayoutShortcut() +{ +#if !KWIN_BUILD_GLOBALSHORTCUTS + QSKIP("Can't test shortcuts without shortcuts"); + return; +#endif + + // this test verifies that per-layout global shortcuts are working correctly. + // first configure layouts + layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)"), KConfig::Notify); + layoutGroup.sync(); + + // and create the global shortcuts + const QString componentName = QStringLiteral("KDE Keyboard Layout Switcher"); + QAction *a = new QAction(this); + a->setObjectName(QStringLiteral("Switch keyboard layout to English (US)")); + a->setProperty("componentName", componentName); + KGlobalAccel::self()->setShortcut(a, QList{Qt::CTRL | Qt::ALT | Qt::Key_1}, KGlobalAccel::NoAutoloading); + delete a; + a = new QAction(this); + a->setObjectName(QStringLiteral("Switch keyboard layout to German")); + a->setProperty("componentName", componentName); + KGlobalAccel::self()->setShortcut(a, QList{Qt::CTRL | Qt::ALT | Qt::Key_2}, KGlobalAccel::NoAutoloading); + delete a; + + // now we should have three layouts + auto xkb = input()->keyboard()->xkb(); + reconfigureLayouts(); + QCOMPARE(xkb->numberOfLayouts(), 3u); + // default layout is English + xkb->switchToLayout(0); + QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + + // now switch to English through the global shortcut + quint32 timestamp = 1; + Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + Test::keyboardKeyPressed(KEY_2, timestamp++); + QVERIFY(layoutChangedSpy.wait()); + // now layout should be German + QCOMPARE(xkb->layoutName(), QStringLiteral("German")); + // release keys again + Test::keyboardKeyReleased(KEY_2, timestamp++); + // switch back to English + Test::keyboardKeyPressed(KEY_1, timestamp++); + QVERIFY(layoutChangedSpy.wait()); + QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + // release keys again + Test::keyboardKeyReleased(KEY_1, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); +} + +void KeyboardLayoutTest::testVirtualDesktopPolicy() +{ + layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)"), KConfig::Notify); + layoutGroup.writeEntry("SwitchMode", QStringLiteral("Desktop"), KConfig::Notify); + layoutGroup.sync(); + + reconfigureLayouts(); + auto xkb = input()->keyboard()->xkb(); + QCOMPARE(xkb->numberOfLayouts(), 3u); + QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + + VirtualDesktopManager::self()->setCount(4); + QCOMPARE(VirtualDesktopManager::self()->count(), 4u); + auto desktops = VirtualDesktopManager::self()->desktops(); + QCOMPARE(desktops.count(), 4); + + // give desktops different layouts + uint desktop, layout; + for (desktop = 0; desktop < VirtualDesktopManager::self()->count(); ++desktop) { + // switch to another virtual desktop + VirtualDesktopManager::self()->setCurrent(desktops.at(desktop)); + QCOMPARE(desktops.at(desktop), VirtualDesktopManager::self()->currentDesktop()); + // should be reset to English + QCOMPARE(xkb->currentLayout(), 0); + // change first desktop to German + layout = (desktop + 1) % xkb->numberOfLayouts(); + changeLayout(layout).waitForFinished(); + QCOMPARE(xkb->currentLayout(), layout); + } + + // imitate app restart to test layouts saving feature + resetLayouts(); + + // check layout set on desktop switching as intended + for (--desktop;;) { + QCOMPARE(desktops.at(desktop), VirtualDesktopManager::self()->currentDesktop()); + layout = (desktop + 1) % xkb->numberOfLayouts(); + QCOMPARE(xkb->currentLayout(), layout); + if (--desktop >= VirtualDesktopManager::self()->count()) { // overflow + break; + } + VirtualDesktopManager::self()->setCurrent(desktops.at(desktop)); + } + + // remove virtual desktops + desktop = 0; + const KWin::VirtualDesktop *deletedDesktop = desktops.last(); + VirtualDesktopManager::self()->setCount(1); + QCOMPARE(xkb->currentLayout(), layout = (desktop + 1) % xkb->numberOfLayouts()); + QCOMPARE(xkb->layoutName(), QStringLiteral("German")); + + // add another desktop + VirtualDesktopManager::self()->setCount(2); + // switching to it should result in going to default + desktops = VirtualDesktopManager::self()->desktops(); + QCOMPARE(desktops.count(), 2); + QCOMPARE(desktops.first(), VirtualDesktopManager::self()->currentDesktop()); + VirtualDesktopManager::self()->setCurrent(desktops.last()); + QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + + // check there are no more layouts left in config than the last actual non-default layouts number + QSignalSpy deletedDesktopSpy(deletedDesktop, &VirtualDesktop::aboutToBeDestroyed); + QVERIFY(deletedDesktopSpy.wait()); + resetLayouts(); + QCOMPARE(layoutGroup.keyList().filter(QStringLiteral("LayoutDefault")).count(), 1); +} + +void KeyboardLayoutTest::testWindowPolicy() +{ + enum Layout { + us, + de, + de_neo, + bad, + }; + layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)"), KConfig::Notify); + layoutGroup.writeEntry("SwitchMode", QStringLiteral("Window"), KConfig::Notify); + layoutGroup.sync(); + reconfigureLayouts(); + auto xkb = input()->keyboard()->xkb(); + QCOMPARE(xkb->numberOfLayouts(), 3u); + QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + + // create a window + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto c1 = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue); + QVERIFY(c1); + + // now switch layout + auto reply = changeLayout(Layout::de); + reply.waitForFinished(); + QCOMPARE(xkb->layoutName(), QStringLiteral("German")); + + // create a second window + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + auto c2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 100), Qt::red); + QVERIFY(c2); + // this should have switched back to English + QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + // now change to another layout + reply = changeLayout(Layout::de_neo); + reply.waitForFinished(); + QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + + // activate other window + workspace()->activateWindow(c1); + QCOMPARE(xkb->layoutName(), QStringLiteral("German")); + workspace()->activateWindow(c2); + QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); +} + +void KeyboardLayoutTest::testApplicationPolicy() +{ + enum Layout { + us, + de, + de_neo, + bad, + }; + layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)"), KConfig::Notify); + layoutGroup.writeEntry("SwitchMode", QStringLiteral("WinClass"), KConfig::Notify); + layoutGroup.sync(); + reconfigureLayouts(); + auto xkb = input()->keyboard()->xkb(); + QCOMPARE(xkb->numberOfLayouts(), 3u); + QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + + // create a window + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + shellSurface->set_app_id(QStringLiteral("org.kde.foo")); + auto c1 = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue); + QVERIFY(c1); + + // create a second window + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + shellSurface2->set_app_id(QStringLiteral("org.kde.foo")); + auto c2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 100), Qt::red); + QVERIFY(c2); + // now switch layout + layoutChangedSpy.clear(); + changeLayout(Layout::de_neo); + QVERIFY(layoutChangedSpy.wait()); + QCOMPARE(layoutChangedSpy.count(), 1); + layoutChangedSpy.clear(); + QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + + resetLayouts(); + // to trigger layout application for current client + workspace()->activateWindow(c1); + workspace()->activateWindow(c2); + QVERIFY(layoutChangedSpy.wait()); + QCOMPARE(layoutChangedSpy.count(), 1); + QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + + // activate other window + workspace()->activateWindow(c1); + // it is the same application and should not switch the layout + QVERIFY(!layoutChangedSpy.wait(1000)); + QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + workspace()->activateWindow(c2); + QVERIFY(!layoutChangedSpy.wait(1000)); + QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + + shellSurface2.reset(); + surface2.reset(); + QVERIFY(Test::waitForWindowClosed(c2)); + QVERIFY(!layoutChangedSpy.wait(1000)); + QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + + resetLayouts(); + QCOMPARE(layoutGroup.keyList().filter(QStringLiteral("LayoutDefault")).count(), 1); +} + +void KeyboardLayoutTest::testNumLock() +{ + qputenv("KWIN_FORCE_NUM_LOCK_EVALUATION", "1"); + layoutGroup.writeEntry("LayoutList", QStringLiteral("us"), KConfig::Notify); + layoutGroup.sync(); + reconfigureLayouts(); + + auto xkb = input()->keyboard()->xkb(); + QCOMPARE(xkb->numberOfLayouts(), 1u); + QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + + // by default not set + QVERIFY(!xkb->leds().testFlag(LED::NumLock)); + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_NUMLOCK, timestamp++); + Test::keyboardKeyReleased(KEY_NUMLOCK, timestamp++); + // now it should be on + QVERIFY(xkb->leds().testFlag(LED::NumLock)); + // and back to off + Test::keyboardKeyPressed(KEY_NUMLOCK, timestamp++); + Test::keyboardKeyReleased(KEY_NUMLOCK, timestamp++); + QVERIFY(!xkb->leds().testFlag(LED::NumLock)); + + // let's reconfigure to enable through config + auto group = kwinApp()->inputConfig()->group(QStringLiteral("Keyboard")); + group.writeEntry("NumLock", 0, KConfig::Notify); + group.sync(); + xkb->reconfigure(); + // now it should be on + QVERIFY(xkb->leds().testFlag(LED::NumLock)); + // pressing should result in it being off + Test::keyboardKeyPressed(KEY_NUMLOCK, timestamp++); + Test::keyboardKeyReleased(KEY_NUMLOCK, timestamp++); + QVERIFY(!xkb->leds().testFlag(LED::NumLock)); + + // pressing again should enable it + Test::keyboardKeyPressed(KEY_NUMLOCK, timestamp++); + Test::keyboardKeyReleased(KEY_NUMLOCK, timestamp++); + QVERIFY(xkb->leds().testFlag(LED::NumLock)); + + // now reconfigure to disable on load + group.writeEntry("NumLock", 1, KConfig::Notify); + group.sync(); + xkb->reconfigure(); + QVERIFY(!xkb->leds().testFlag(LED::NumLock)); +} + +WAYLANDTEST_MAIN(KeyboardLayoutTest) +#include "keyboard_layout_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/keymap_creation_failure_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/keymap_creation_failure_test.cpp new file mode 100644 index 0000000000..b7a8b62be2 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/keymap_creation_failure_test.cpp @@ -0,0 +1,88 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "keyboard_input.h" +#include "keyboard_layout.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_keymap_creation_failure-0"); + +class KeymapCreationFailureTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testPointerButton(); +}; + +void KeymapCreationFailureTest::initTestCase() +{ + // situation for for BUG 381210 + // this will fail to create keymap + qputenv("XKB_DEFAULT_RULES", "no"); + qputenv("XKB_DEFAULT_MODEL", "no"); + qputenv("XKB_DEFAULT_LAYOUT", "no"); + qputenv("XKB_DEFAULT_VARIANT", "no"); + qputenv("XKB_DEFAULT_OPTIONS", "no"); + + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + kwinApp()->setKxkbConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group(QStringLiteral("Layout")); + layoutGroup.writeEntry("LayoutList", QStringLiteral("no")); + layoutGroup.writeEntry("Model", "no"); + layoutGroup.writeEntry("Options", "no"); + layoutGroup.sync(); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void KeymapCreationFailureTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void KeymapCreationFailureTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void KeymapCreationFailureTest::testPointerButton() +{ + // test case for BUG 381210 + // pressing a pointer button results in crash + + // now create the crashing condition + // which is sending in a pointer event + Test::pointerButtonPressed(BTN_LEFT, 0); + Test::pointerButtonReleased(BTN_LEFT, 1); +} + +WAYLANDTEST_MAIN(KeymapCreationFailureTest) +#include "keymap_creation_failure_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/kwin_wayland_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/kwin_wayland_test.cpp new file mode 100644 index 0000000000..bff13b760f --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/kwin_wayland_test.cpp @@ -0,0 +1,417 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "backends/virtual/virtual_backend.h" +#include "compositor.h" +#include "core/outputconfiguration.h" +#include "core/session.h" +#include "effect/effecthandler.h" +#include "input.h" +#include "inputmethod.h" +#include "placement.h" +#include "pluginmanager.h" +#include "wayland_server.h" +#include "workspace.h" +#include "backends/drm/drm_backend.h" + +#include + +#if KWIN_BUILD_X11 +#include "utils/xcbutils.h" +#include "xwayland/xwayland.h" +#include "xwayland/xwaylandlauncher.h" +#endif + +#include + +#include +#include +#include +#include +#include + +// system +#include +#include +#include +#include +#include + +Q_IMPORT_PLUGIN(KWinIntegrationPlugin) +#if KWIN_BUILD_GLOBALSHORTCUTS +Q_IMPORT_PLUGIN(KGlobalAccelImpl) +#endif +Q_IMPORT_PLUGIN(KWindowSystemKWinPlugin) +Q_IMPORT_PLUGIN(KWinIdleTimePoller) + +namespace KWin +{ + +WaylandTestApplication::WaylandTestApplication(int &argc, char **argv, bool runOnKMS) + : Application(argc, argv) +{ + QStandardPaths::setTestModeEnabled(true); + + const QStringList configs{ + QStringLiteral("kaccessrc"), + QStringLiteral("kglobalshortcutsrc"), + QStringLiteral("kcminputrc"), + QStringLiteral("kxkbrc"), + QStringLiteral("kwinoutputconfig.json"), + }; + for (const QString &config : configs) { + if (const QString &fileName = QStandardPaths::locate(QStandardPaths::ConfigLocation, config); !fileName.isEmpty()) { + QFile::remove(fileName); + } + } + + QIcon::setThemeName(QStringLiteral("breeze")); +#if KWIN_BUILD_ACTIVITIES + setUseKActivities(false); +#endif + if (!runOnKMS) { + qputenv("KWIN_COMPOSE", QByteArrayLiteral("Q")); + } + qputenv("XDG_CURRENT_DESKTOP", QByteArrayLiteral("KDE")); + qunsetenv("XKB_DEFAULT_RULES"); + qunsetenv("XKB_DEFAULT_MODEL"); + qunsetenv("XKB_DEFAULT_LAYOUT"); + qunsetenv("XKB_DEFAULT_VARIANT"); + qunsetenv("XKB_DEFAULT_OPTIONS"); + + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup windowsGroup = config->group(QStringLiteral("Windows")); + windowsGroup.writeEntry("Placement", Placement::policyToString(PlacementSmart)); + windowsGroup.sync(); + KConfigGroup edgeBarrierGroup = config->group(QStringLiteral("EdgeBarrier")); + edgeBarrierGroup.writeEntry("EdgeBarrier", 0); + edgeBarrierGroup.writeEntry("CornerBarrier", false); + edgeBarrierGroup.sync(); + setConfig(config); + + const auto ownPath = libraryPaths().last(); + removeLibraryPath(ownPath); + addLibraryPath(ownPath); + + if (runOnKMS) { + // in order to allow running the test manually on a tty, + // we need the real session. This doesn't work in CI though, + // so manually check for that and use the noop session instead + if (qEnvironmentVariable("CI") == "true") { + setSession(Session::create(Session::Type::Noop)); + } else { + setSession(Session::create()); + } + setOutputBackend(std::make_unique(session())); + } else { + setSession(Session::create(Session::Type::Noop)); + setOutputBackend(std::make_unique()); + } + m_waylandServer.reset(WaylandServer::create()); + setProcessStartupEnvironment(QProcessEnvironment::systemEnvironment()); +} + +WaylandTestApplication::~WaylandTestApplication() +{ + setTerminating(); + // need to unload all effects prior to destroying X connection as they might do X calls + // also before destroy Workspace, as effects might call into Workspace + if (effects) { + effects->unloadAllEffects(); + } + destroyPlugins(); +#if KWIN_BUILD_X11 + m_xwayland.reset(); +#endif + destroyVirtualInputDevices(); + destroyWorkspace(); + destroyInputMethod(); + destroyCompositor(); + destroyInput(); + m_waylandServer.reset(); +} + +void WaylandTestApplication::createVirtualInputDevices() +{ + m_virtualKeyboard = std::make_unique(); + m_virtualKeyboard->setName(QStringLiteral("Virtual Keyboard 1")); + m_virtualKeyboard->setKeyboard(true); + + m_virtualPointer = std::make_unique(); + m_virtualPointer->setName(QStringLiteral("Virtual Pointer 1")); + m_virtualPointer->setPointer(true); + + m_virtualTouch = std::make_unique(); + m_virtualTouch->setName(QStringLiteral("Virtual Touch 1")); + m_virtualTouch->setTouch(true); + + m_virtualTabletPad = std::make_unique(); + m_virtualTabletPad->setName(QStringLiteral("Virtual Tablet Pad 1")); + m_virtualTabletPad->setTabletPad(true); + m_virtualTabletPad->setGroup(0xdeadbeef); + + m_virtualTablet = std::make_unique(); + m_virtualTablet->setName(QStringLiteral("Virtual Tablet Tool 1")); + m_virtualTablet->setTabletTool(true); + m_virtualTablet->setGroup(0xdeadbeef); + + m_virtualTabletTool = std::make_unique(); + m_virtualTabletTool->setSerialId(42); + m_virtualTabletTool->setUniqueId(42); + m_virtualTabletTool->setType(InputDeviceTabletTool::Pen); + m_virtualTabletTool->setCapabilities({}); + + input()->addInputDevice(m_virtualPointer.get()); + input()->addInputDevice(m_virtualTouch.get()); + input()->addInputDevice(m_virtualKeyboard.get()); + input()->addInputDevice(m_virtualTabletPad.get()); + input()->addInputDevice(m_virtualTablet.get()); +} + +void WaylandTestApplication::destroyVirtualInputDevices() +{ + if (m_virtualPointer) { + input()->removeInputDevice(m_virtualPointer.get()); + } + if (m_virtualTouch) { + input()->removeInputDevice(m_virtualTouch.get()); + } + if (m_virtualKeyboard) { + input()->removeInputDevice(m_virtualKeyboard.get()); + } + if (m_virtualTabletPad) { + input()->removeInputDevice(m_virtualTabletPad.get()); + } + if (m_virtualTabletTool) { + input()->removeInputDevice(m_virtualTablet.get()); + } +} + +void WaylandTestApplication::performStartup() +{ + if (!m_inputMethodServerToStart.isEmpty()) { + createInputMethod(); + if (m_inputMethodServerToStart != QStringLiteral("internal")) { + inputMethod()->setInputMethodCommand(m_inputMethodServerToStart); + inputMethod()->setEnabled(true); + } + } + + // first load options - done internally by a different thread + createOptions(); + if (!outputBackend()->initialize()) { + std::exit(1); + } + + // try creating the Wayland Backend + createInput(); + createVirtualInputDevices(); + createTabletModeManager(); + + auto compositor = Compositor::create(); + compositor->createRenderer(); + createWorkspace(); + createPlugins(); + + compositor->start(); + + waylandServer()->initWorkspace(); + + if (!waylandServer()->start()) { + qFatal("Failed to initialize the Wayland server, exiting now"); + } + +#if KWIN_BUILD_X11 + m_xwayland = std::make_unique(this); + m_xwayland->init(); +#endif +} + +Test::VirtualInputDevice *WaylandTestApplication::virtualPointer() const +{ + return m_virtualPointer.get(); +} + +Test::VirtualInputDevice *WaylandTestApplication::virtualKeyboard() const +{ + return m_virtualKeyboard.get(); +} + +Test::VirtualInputDevice *WaylandTestApplication::virtualTouch() const +{ + return m_virtualTouch.get(); +} + +Test::VirtualInputDevice *WaylandTestApplication::virtualTabletPad() const +{ + return m_virtualTabletPad.get(); +} + +Test::VirtualInputDevice *WaylandTestApplication::virtualTablet() const +{ + return m_virtualTablet.get(); +} + +Test::VirtualInputDeviceTabletTool *WaylandTestApplication::virtualTabletTool() const +{ + return m_virtualTabletTool.get(); +} + +#if KWIN_BUILD_X11 +XwaylandInterface *WaylandTestApplication::xwayland() const +{ + return m_xwayland.get(); +} +#endif + +Test::FractionalScaleManagerV1::~FractionalScaleManagerV1() +{ + destroy(); +} + +Test::FractionalScaleV1::~FractionalScaleV1() +{ + destroy(); +} + +int Test::FractionalScaleV1::preferredScale() +{ + return m_preferredScale; +} + +void Test::FractionalScaleV1::wp_fractional_scale_v1_preferred_scale(uint32_t scale) +{ + if (m_preferredScale == scale) { + return; + } + m_preferredScale = scale; + Q_EMIT preferredScaleChanged(); +} + +void Test::setOutputConfig(const QList &geometries) +{ + setOutputConfig(geometries | std::views::transform([](const Rect &geometry) { + return OutputInfo{ + .geometry = geometry, + }; + }) | std::ranges::to()); +} + +void Test::setOutputConfig(const QList &infos) +{ + Q_ASSERT(qobject_cast(kwinApp()->outputBackend())); + QList converted; + std::transform(infos.begin(), infos.end(), std::back_inserter(converted), [](const auto &info) { + return VirtualBackend::OutputInfo{ + .geometry = info.geometry, + .scale = info.scale, + .internal = info.internal, + .physicalSizeInMM = info.physicalSizeInMM, + .modes = info.modes, + .panelOrientation = info.panelOrientation, + .edid = info.edid, + .edidIdentifierOverride = info.edidIdentifierOverride, + .connectorName = info.connectorName, + .mstPath = info.mstPath, + }; + }); + static_cast(kwinApp()->outputBackend())->setVirtualOutputs(converted); + + const auto outputs = kwinApp()->outputBackend()->outputs(); + OutputConfiguration config; + for (int i = 0; i < outputs.size(); i++) { + const auto &info = infos[i]; + *config.changeSet(outputs[i]) = OutputChangeSet{ + .desiredModeSize = info.geometry.size() * info.scale, + .enabled = true, + .pos = info.geometry.topLeft(), + .scale = info.scale, + .scaleSetting = info.scale, + }; + } + workspace()->applyOutputConfiguration(config); +} + +Test::SimpleKeyboard::SimpleKeyboard(QObject *parent) + : QObject(parent) + , m_keyboard(Test::waylandSeat()->createKeyboard(parent)) +{ + static const int EVDEV_OFFSET = 8; + + connect(m_keyboard, &KWayland::Client::Keyboard::keymapChanged, this, [this](int fd, uint32_t size) { + char *map_shm = static_cast( + mmap(nullptr, size, PROT_READ, MAP_SHARED, fd, 0)); + close(fd); + + Q_ASSERT(map_shm != MAP_FAILED); + + m_keymap = XkbKeymapPtr( + xkb_keymap_new_from_string( + m_ctx.get(), + map_shm, + XKB_KEYMAP_FORMAT_TEXT_V1, + XKB_KEYMAP_COMPILE_NO_FLAGS), + &xkb_keymap_unref); + + munmap(map_shm, size); + Q_ASSERT(m_keymap); + + m_state = XkbStatePtr(xkb_state_new(m_keymap.get()), &xkb_state_unref); + Q_ASSERT(m_state); + }); + + connect(m_keyboard, &KWayland::Client::Keyboard::modifiersChanged, this, [this](quint32 depressed, quint32 latched, quint32 locked, quint32 group) { + if (!m_state) { + return; + } + xkb_state_update_mask( + m_state.get(), + depressed, + latched, + locked, + 0, 0, + group); + }); + + connect(m_keyboard, &KWayland::Client::Keyboard::keyChanged, this, [this](quint32 key, KWayland::Client::Keyboard::KeyState state, quint32 time) { + if (!m_state) { + return; + } + + xkb_keycode_t kc = key + EVDEV_OFFSET; + + if (state == KWayland::Client::Keyboard::KeyState::Pressed) { + const xkb_keysym_t *syms; + int nsyms = xkb_state_key_get_syms(m_state.get(), kc, &syms); + for (int i = 0; i < nsyms; i++) { + char buf[64]; + int len = xkb_keysym_to_utf8(syms[i], buf, sizeof(buf)); + if (len > 1) { + m_receviedText.append(QString::fromUtf8(buf, len - 1)); // xkb_keysym_to_utf8 contains terminating byte + Q_EMIT receviedTextChanged(); + } + Q_EMIT keySymRecevied(syms[i]); + } + } + }); +} + +KWayland::Client::Keyboard *Test::SimpleKeyboard::keyboard() +{ + return m_keyboard; +} + +QString Test::SimpleKeyboard::receviedText() +{ + return m_receviedText; +} +} + +#include "moc_kwin_wayland_test.cpp" diff --git a/local/recipes/kde/kwin/source/autotests/integration/kwin_wayland_test.h b/local/recipes/kde/kwin/source/autotests/integration/kwin_wayland_test.h new file mode 100644 index 0000000000..707a7ff8c9 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/kwin_wayland_test.h @@ -0,0 +1,1513 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef KWIN_WAYLAND_TEST_H +#define KWIN_WAYLAND_TEST_H + +#include "core/inputdevice.h" +#include "main.h" +#include "window.h" + +// KWayland +#include +#include + +// Qt +#include +#include +#include + +#include + +#include + +#include "qwayland-color-management-v1.h" +#include "qwayland-color-representation-v1.h" +#include "qwayland-cursor-shape-v1.h" +#include "qwayland-fake-input.h" +#include "qwayland-fifo-v1.h" +#include "qwayland-fractional-scale-v1.h" +#include "qwayland-idle-inhibit-unstable-v1.h" +#include "qwayland-input-method-unstable-v1.h" +#include "qwayland-kde-output-device-v2.h" +#include "qwayland-kde-output-management-v2.h" +#include "qwayland-kde-screen-edge-v1.h" +#include "qwayland-keystate.h" +#include "qwayland-presentation-time.h" +#include "qwayland-primary-selection-unstable-v1.h" +#include "qwayland-security-context-v1.h" +#include "qwayland-tablet-v2.h" +#include "qwayland-text-input-unstable-v3.h" +#include "qwayland-wayland.h" +#include "qwayland-wlr-layer-shell-unstable-v1.h" +#include "qwayland-xdg-activation-v1.h" +#include "qwayland-xdg-decoration-unstable-v1.h" +#include "qwayland-xdg-dialog-v1.h" +#include "qwayland-xdg-shell.h" +#include "qwayland-xdg-toplevel-drag-v1.h" +#include "qwayland-xx-session-management-v1.h" +#include "qwayland-zkde-screencast-unstable-v1.h" + +namespace KWayland +{ +namespace Client +{ +class AppMenuManager; +class ConnectionThread; +class Compositor; +class DataSource; +class EventQueue; +class LogicalOutput; +class PlasmaShell; +class PlasmaWindowManagement; +class Pointer; +class PointerConstraints; +class Registry; +class Seat; +class ShadowManager; +class ShmPool; +class SubCompositor; +class SubSurface; +class Surface; +class TextInputManager; +class DataDeviceManager; +} +} + +namespace QtWayland +{ +class zwp_input_panel_surface_v1; +class zwp_text_input_v3; +class zwp_text_input_manager_v3; +} + +class ScreencastingV1; + +namespace KWin +{ + +namespace WaylandClient +{ +class LinuxDmabufV1; +class Viewporter; +} + +class WaylandServer; + +#if KWIN_BUILD_X11 +namespace Xwl +{ +class Xwayland; +} +#endif + +namespace Test +{ +class VirtualInputDevice; +class VirtualInputDeviceTabletTool; +} + +class WaylandTestApplication : public Application +{ + Q_OBJECT +public: + WaylandTestApplication(int &argc, char **argv, bool runOnKMS); + ~WaylandTestApplication() override; + + void setInputMethodServerToStart(const QString &inputMethodServer) + { + m_inputMethodServerToStart = inputMethodServer; + } + + Test::VirtualInputDevice *virtualPointer() const; + Test::VirtualInputDevice *virtualKeyboard() const; + Test::VirtualInputDevice *virtualTouch() const; + Test::VirtualInputDevice *virtualTabletPad() const; + Test::VirtualInputDevice *virtualTablet() const; + Test::VirtualInputDeviceTabletTool *virtualTabletTool() const; + +#if KWIN_BUILD_X11 + XwaylandInterface *xwayland() const override; +#endif + +protected: + void performStartup() override; + +private: + void finalizeStartup(); + + void createVirtualInputDevices(); + void destroyVirtualInputDevices(); + + std::unique_ptr m_waylandServer; +#if KWIN_BUILD_X11 + std::unique_ptr m_xwayland; +#endif + QString m_inputMethodServerToStart; + + std::unique_ptr m_virtualPointer; + std::unique_ptr m_virtualKeyboard; + std::unique_ptr m_virtualTouch; + std::unique_ptr m_virtualTabletPad; + std::unique_ptr m_virtualTablet; + std::unique_ptr m_virtualTabletTool; +}; + +namespace Test +{ + +class ScreencastingV1; +class MockInputMethod; +class WpTabletV2; +class WpTabletPadV2; +class WpTabletSeatV2; +class WpTabletToolV2; + +class TextInputManagerV3 : public QtWayland::zwp_text_input_manager_v3 +{ +public: + ~TextInputManagerV3() override + { + destroy(); + } +}; + +class TextInputV3 : public QObject, public QtWayland::zwp_text_input_v3 +{ + Q_OBJECT +public: + ~TextInputV3() override + { + destroy(); + } + +Q_SIGNALS: + void preeditString(const QString &text, int cursor_begin, int cursor_end); + void commitString(const QString &text); + +protected: + void zwp_text_input_v3_preedit_string(const QString &text, int32_t cursor_begin, int32_t cursor_end) override + { + Q_EMIT preeditString(text, cursor_begin, cursor_end); + } + void zwp_text_input_v3_commit_string(const QString &text) override + { + Q_EMIT commitString(text); + } +}; + +class LayerShellV1 : public QtWayland::zwlr_layer_shell_v1 +{ +public: + ~LayerShellV1() override; +}; + +class LayerSurfaceV1 : public QObject, public QtWayland::zwlr_layer_surface_v1 +{ + Q_OBJECT + +public: + ~LayerSurfaceV1() override; + +protected: + void zwlr_layer_surface_v1_configure(uint32_t serial, uint32_t width, uint32_t height) override; + void zwlr_layer_surface_v1_closed() override; + +Q_SIGNALS: + void closeRequested(); + void configureRequested(quint32 serial, const QSize &size); +}; + +/** + * The XdgShell class represents the @c xdg_wm_base global. + */ +class XdgShell : public QtWayland::xdg_wm_base +{ +public: + ~XdgShell() override; + void xdg_wm_base_ping(uint32_t serial) override + { + pong(serial); + } +}; + +/** + * The XdgSurface class represents an xdg_surface object. + */ +class XdgSurface : public QObject, public QtWayland::xdg_surface +{ + Q_OBJECT + +public: + explicit XdgSurface(XdgShell *shell, KWayland::Client::Surface *surface, QObject *parent = nullptr); + ~XdgSurface() override; + + KWayland::Client::Surface *surface() const; + +Q_SIGNALS: + void configureRequested(quint32 serial); + +protected: + void xdg_surface_configure(uint32_t serial) override; + +private: + KWayland::Client::Surface *m_surface; +}; + +/** + * The XdgToplevel class represents an xdg_toplevel surface. Note that the XdgToplevel surface + * takes the ownership of the underlying XdgSurface object. + */ +class XdgToplevel : public QObject, public QtWayland::xdg_toplevel +{ + Q_OBJECT + +public: + enum class State { + Maximized = 1 << 0, + Fullscreen = 1 << 1, + Resizing = 1 << 2, + Activated = 1 << 3 + }; + Q_DECLARE_FLAGS(States, State) + + explicit XdgToplevel(XdgSurface *surface, QObject *parent = nullptr); + ~XdgToplevel() override; + + XdgSurface *xdgSurface() const; + +Q_SIGNALS: + void configureRequested(const QSize &size, KWin::Test::XdgToplevel::States states); + void closeRequested(); + +protected: + void xdg_toplevel_configure(int32_t width, int32_t height, wl_array *states) override; + void xdg_toplevel_close() override; + +private: + std::unique_ptr m_xdgSurface; +}; + +/** + * The XdgPositioner class represents an xdg_positioner object. + */ +class XdgPositioner : public QtWayland::xdg_positioner +{ +public: + explicit XdgPositioner(XdgShell *shell); + ~XdgPositioner() override; +}; + +/** + * The XdgPopup class represents an xdg_popup surface. Note that the XdgPopup surface takes + * the ownership of the underlying XdgSurface object. + */ +class XdgPopup : public QObject, public QtWayland::xdg_popup +{ + Q_OBJECT + +public: + XdgPopup(XdgSurface *surface, XdgSurface *parentSurface, XdgPositioner *positioner, QObject *parent = nullptr); + ~XdgPopup() override; + + XdgSurface *xdgSurface() const; + +Q_SIGNALS: + void configureRequested(const Rect &rect); + void doneReceived(); + void repositioned(quint32 token); + +protected: + void xdg_popup_configure(int32_t x, int32_t y, int32_t width, int32_t height) override; + void xdg_popup_popup_done() override; + void xdg_popup_repositioned(uint32_t token) override; + +private: + std::unique_ptr m_xdgSurface; +}; + +class XdgDecorationManagerV1 : public QtWayland::zxdg_decoration_manager_v1 +{ +public: + ~XdgDecorationManagerV1() override; +}; + +class XdgToplevelDecorationV1 : public QObject, public QtWayland::zxdg_toplevel_decoration_v1 +{ + Q_OBJECT + +public: + XdgToplevelDecorationV1(XdgDecorationManagerV1 *manager, XdgToplevel *toplevel, QObject *parent = nullptr); + ~XdgToplevelDecorationV1() override; + +Q_SIGNALS: + void configureRequested(QtWayland::zxdg_toplevel_decoration_v1::mode mode); + +protected: + void zxdg_toplevel_decoration_v1_configure(uint32_t mode) override; +}; + +class IdleInhibitManagerV1 : public QtWayland::zwp_idle_inhibit_manager_v1 +{ +public: + ~IdleInhibitManagerV1() override; +}; + +class IdleInhibitorV1 : public QtWayland::zwp_idle_inhibitor_v1 +{ +public: + IdleInhibitorV1(IdleInhibitManagerV1 *manager, KWayland::Client::Surface *surface); + ~IdleInhibitorV1() override; +}; + +class WaylandOutputConfigurationV2 : public QObject, public QtWayland::kde_output_configuration_v2 +{ + Q_OBJECT +public: + WaylandOutputConfigurationV2(struct ::kde_output_configuration_v2 *object); + +Q_SIGNALS: + void applied(); + void failed(); + +protected: + void kde_output_configuration_v2_applied() override; + void kde_output_configuration_v2_failed() override; +}; + +class WaylandOutputManagementV2 : public QObject, public QtWayland::kde_output_management_v2 +{ + Q_OBJECT +public: + WaylandOutputManagementV2(struct ::wl_registry *registry, int id, int version); + + WaylandOutputConfigurationV2 *createConfiguration(); +}; + +class WaylandOutputDeviceV2Mode : public QObject, public QtWayland::kde_output_device_mode_v2 +{ + Q_OBJECT + +public: + WaylandOutputDeviceV2Mode(struct ::kde_output_device_mode_v2 *object); + ~WaylandOutputDeviceV2Mode() override; + + int refreshRate() const; + QSize size() const; + bool preferred() const; + + bool operator==(const WaylandOutputDeviceV2Mode &other) const; + + static WaylandOutputDeviceV2Mode *get(struct ::kde_output_device_mode_v2 *object); + +Q_SIGNALS: + void removed(); + +protected: + void kde_output_device_mode_v2_size(int32_t width, int32_t height) override; + void kde_output_device_mode_v2_refresh(int32_t refresh) override; + void kde_output_device_mode_v2_preferred() override; + void kde_output_device_mode_v2_removed() override; + +private: + int m_refreshRate = 60000; + QSize m_size; + bool m_preferred = false; +}; + +class WaylandOutputDeviceV2 : public QObject, public QtWayland::kde_output_device_v2 +{ + Q_OBJECT + +public: + WaylandOutputDeviceV2(int id); + ~WaylandOutputDeviceV2() override; + + QByteArray edid() const; + bool enabled() const; + int id() const; + QString name() const; + QString model() const; + QString manufacturer() const; + qreal scale() const; + QPoint globalPosition() const; + QSize pixelSize() const; + int refreshRate() const; + uint32_t vrrPolicy() const; + uint32_t overscan() const; + uint32_t capabilities() const; + uint32_t rgbRange() const; + + QString modeId() const; + +Q_SIGNALS: + void enabledChanged(); + void done(); + +protected: + void kde_output_device_v2_geometry(int32_t x, + int32_t y, + int32_t physical_width, + int32_t physical_height, + int32_t subpixel, + const QString &make, + const QString &model, + int32_t transform) override; + void kde_output_device_v2_current_mode(struct ::kde_output_device_mode_v2 *mode) override; + void kde_output_device_v2_mode(struct ::kde_output_device_mode_v2 *mode) override; + void kde_output_device_v2_done() override; + void kde_output_device_v2_scale(wl_fixed_t factor) override; + void kde_output_device_v2_edid(const QString &raw) override; + void kde_output_device_v2_enabled(int32_t enabled) override; + void kde_output_device_v2_uuid(const QString &uuid) override; + void kde_output_device_v2_serial_number(const QString &serialNumber) override; + void kde_output_device_v2_eisa_id(const QString &eisaId) override; + void kde_output_device_v2_capabilities(uint32_t flags) override; + void kde_output_device_v2_overscan(uint32_t overscan) override; + void kde_output_device_v2_vrr_policy(uint32_t vrr_policy) override; + void kde_output_device_v2_rgb_range(uint32_t rgb_range) override; + +private: + QString modeName(const WaylandOutputDeviceV2Mode *m) const; + WaylandOutputDeviceV2Mode *deviceModeFromId(const int modeId) const; + + WaylandOutputDeviceV2Mode *m_mode; + QList m_modes; + + int m_id; + QPoint m_pos; + QSize m_physicalSize; + int32_t m_subpixel; + QString m_manufacturer; + QString m_model; + int32_t m_transform; + qreal m_factor; + QByteArray m_edid; + int32_t m_enabled; + QString m_uuid; + QString m_serialNumber; + QString m_eisaId; + uint32_t m_flags; + uint32_t m_overscan; + uint32_t m_vrr_policy; + uint32_t m_rgbRange; +}; + +class MockInputMethod : public QObject, QtWayland::zwp_input_method_v1 +{ + Q_OBJECT +public: + enum class Mode { + TopLevel, + Overlay, + }; + + MockInputMethod(struct wl_registry *registry, int id, int version); + ~MockInputMethod(); + + KWayland::Client::Surface *inputPanelSurface() const + { + return m_inputSurface.get(); + } + auto *context() const + { + return m_context; + } + + void setMode(Mode mode); + +Q_SIGNALS: + void activate(); + +protected: + void zwp_input_method_v1_activate(struct ::zwp_input_method_context_v1 *context) override; + void zwp_input_method_v1_deactivate(struct ::zwp_input_method_context_v1 *context) override; + +private: + std::unique_ptr m_inputSurface; + std::unique_ptr m_inputMethodSurface; + struct ::zwp_input_method_context_v1 *m_context = nullptr; + Mode m_mode = Mode::TopLevel; +}; + +class FractionalScaleManagerV1 : public QObject, public QtWayland::wp_fractional_scale_manager_v1 +{ + Q_OBJECT +public: + ~FractionalScaleManagerV1() override; +}; + +class FractionalScaleV1 : public QObject, public QtWayland::wp_fractional_scale_v1 +{ + Q_OBJECT +public: + ~FractionalScaleV1() override; + int preferredScale(); + +protected: + void wp_fractional_scale_v1_preferred_scale(uint32_t scale) override; + +Q_SIGNALS: + void preferredScaleChanged(); + +private: + uint m_preferredScale = 120; +}; + +class ScreenEdgeManagerV1 : public QObject, public QtWayland::kde_screen_edge_manager_v1 +{ + Q_OBJECT +public: + ~ScreenEdgeManagerV1() override; +}; + +class AutoHideScreenEdgeV1 : public QObject, public QtWayland::kde_auto_hide_screen_edge_v1 +{ + Q_OBJECT +public: + AutoHideScreenEdgeV1(ScreenEdgeManagerV1 *manager, KWayland::Client::Surface *surface, uint32_t border); + ~AutoHideScreenEdgeV1() override; +}; + +class CursorShapeManagerV1 : public QObject, public QtWayland::wp_cursor_shape_manager_v1 +{ + Q_OBJECT +public: + ~CursorShapeManagerV1() override; +}; + +class CursorShapeDeviceV1 : public QObject, public QtWayland::wp_cursor_shape_device_v1 +{ + Q_OBJECT +public: + CursorShapeDeviceV1(CursorShapeManagerV1 *manager, KWayland::Client::Pointer *pointer); + ~CursorShapeDeviceV1() override; +}; + +class FakeInput : public QtWayland::org_kde_kwin_fake_input +{ +public: + ~FakeInput() override; +}; + +class SecurityContextManagerV1 : public QtWayland::wp_security_context_manager_v1 +{ +public: + ~SecurityContextManagerV1() override; +}; + +class XdgWmDialogV1 : public QtWayland::xdg_wm_dialog_v1 +{ +public: + ~XdgWmDialogV1() override; +}; + +class XdgDialogV1 : public QtWayland::xdg_dialog_v1 +{ +public: + XdgDialogV1(XdgWmDialogV1 *wm, XdgToplevel *toplevel); + ~XdgDialogV1() override; +}; + +class WpTabletManagerV2 : public QtWayland::zwp_tablet_manager_v2 +{ +public: + WpTabletManagerV2(::wl_registry *registry, uint32_t id, int version); + ~WpTabletManagerV2() override; + + std::unique_ptr createSeat(KWayland::Client::Seat *seat); +}; + +class WpTabletSeatV2 : public QObject, public QtWayland::zwp_tablet_seat_v2 +{ + Q_OBJECT + +public: + explicit WpTabletSeatV2(::zwp_tablet_seat_v2 *seat); + ~WpTabletSeatV2() override; + +Q_SIGNALS: + void toolAdded(WpTabletToolV2 *tool); + +protected: + void zwp_tablet_seat_v2_tablet_added(::zwp_tablet_v2 *id) override; + void zwp_tablet_seat_v2_tool_added(::zwp_tablet_tool_v2 *id) override; + void zwp_tablet_seat_v2_pad_added(::zwp_tablet_pad_v2 *id) override; + +private: + std::vector> m_tablets; + std::vector> m_tools; + std::vector> m_pads; +}; + +class WpTabletV2 : public QtWayland::zwp_tablet_v2 +{ +public: + explicit WpTabletV2(::zwp_tablet_v2 *id); + ~WpTabletV2() override; +}; + +class WpTabletPadV2 : public QtWayland::zwp_tablet_pad_v2 +{ +public: + explicit WpTabletPadV2(::zwp_tablet_pad_v2 *id); + ~WpTabletPadV2() override; +}; + +class WpTabletToolV2 : public QObject, public QtWayland::zwp_tablet_tool_v2 +{ + Q_OBJECT + +public: + explicit WpTabletToolV2(::zwp_tablet_tool_v2 *id); + ~WpTabletToolV2() override; + + bool ready() const; + +Q_SIGNALS: + void done(); + void down(uint32_t serial); + void up(); + void motion(const QPointF &position); + +protected: + void zwp_tablet_tool_v2_done() override; + void zwp_tablet_tool_v2_down(uint32_t serial) override; + void zwp_tablet_tool_v2_up() override; + void zwp_tablet_tool_v2_motion(wl_fixed_t x, wl_fixed_t y) override; + +private: + bool m_ready = false; +}; + +class WpPrimarySelectionOfferV1 : public QObject, public QtWayland::zwp_primary_selection_offer_v1 +{ + Q_OBJECT + +public: + explicit WpPrimarySelectionOfferV1(::zwp_primary_selection_offer_v1 *id); + ~WpPrimarySelectionOfferV1() override; + + QList mimeTypes() const; + +protected: + void zwp_primary_selection_offer_v1_offer(const QString &mime_type) override; + +private: + QList m_mimeTypes; +}; + +class WpPrimarySelectionSourceV1 : public QObject, public QtWayland::zwp_primary_selection_source_v1 +{ + Q_OBJECT + +public: + explicit WpPrimarySelectionSourceV1(::zwp_primary_selection_source_v1 *id); + ~WpPrimarySelectionSourceV1() override; + +Q_SIGNALS: + void sendDataRequested(const QString &mimeType, int32_t fd); + void cancelled(); + +protected: + void zwp_primary_selection_source_v1_send(const QString &mime_type, int32_t fd) override; + void zwp_primary_selection_source_v1_cancelled() override; +}; + +class WpPrimarySelectionDeviceV1 : public QObject, public QtWayland::zwp_primary_selection_device_v1 +{ + Q_OBJECT + +public: + explicit WpPrimarySelectionDeviceV1(::zwp_primary_selection_device_v1 *id); + ~WpPrimarySelectionDeviceV1() override; + + WpPrimarySelectionOfferV1 *offer() const; + std::unique_ptr takeOffer(); + +Q_SIGNALS: + void selectionOffered(WpPrimarySelectionOfferV1 *offer); + void selectionCleared(); + +protected: + void zwp_primary_selection_device_v1_data_offer(::zwp_primary_selection_offer_v1 *offer) override; + void zwp_primary_selection_device_v1_selection(::zwp_primary_selection_offer_v1 *id) override; + +private: + std::unique_ptr m_offer; +}; + +class WpPrimarySelectionDeviceManagerV1 : public QtWayland::zwp_primary_selection_device_manager_v1 +{ +public: + WpPrimarySelectionDeviceManagerV1(::wl_registry *registry, uint32_t id, int version); + ~WpPrimarySelectionDeviceManagerV1() override; + + std::unique_ptr getDevice(KWayland::Client::Seat *seat); + std::unique_ptr createSource(); +}; + +class XdgToplevelDragV1 : public QtWayland::xdg_toplevel_drag_v1 +{ +public: + explicit XdgToplevelDragV1(::xdg_toplevel_drag_v1 *id); + ~XdgToplevelDragV1() override; +}; + +class XdgToplevelDragManagerV1 : public QtWayland::xdg_toplevel_drag_manager_v1 +{ +public: + XdgToplevelDragManagerV1(::wl_registry *registry, uint32_t id, int version); + ~XdgToplevelDragManagerV1() override; + + std::unique_ptr createDrag(KWayland::Client::DataSource *source); +}; + +enum class AdditionalWaylandInterface : uint64_t { + Seat = 1 << 0, + DataDeviceManager = 1 << 1, + PlasmaShell = 1 << 2, + WindowManagement = 1 << 3, + PointerConstraints = 1 << 4, + IdleInhibitV1 = 1 << 5, + AppMenu = 1 << 6, + ShadowManager = 1 << 7, + XdgDecorationV1 = 1 << 8, + OutputManagementV2 = 1 << 9, + TextInputManagerV2 = 1 << 10, + InputMethodV1 = 1 << 11, + LayerShellV1 = 1 << 12, + TextInputManagerV3 = 1 << 13, + OutputDeviceV2 = 1 << 14, + FractionalScaleManagerV1 = 1 << 15, + ScreencastingV1 = 1 << 16, + ScreenEdgeV1 = 1 << 17, + CursorShapeV1 = 1 << 18, + FakeInput = 1 << 19, + SecurityContextManagerV1 = 1 << 20, + XdgDialogV1 = 1 << 21, + ColorManagement = 1 << 22, + FifoV1 = 1 << 23, + PresentationTime = 1 << 24, + XdgActivation = 1 << 25, + XdgSessionV1 = 1 << 26, + WpTabletV2 = 1 << 27, + KeyState = 1 << 28, + WpPrimarySelectionV1 = 1 << 29, + XdgToplevelDragV1 = 1 << 30, + LinuxDmabuf = 1ull << 31, + ColorRepresentation = 1ull << 32, + Viewporter = 1ull << 33, +}; +Q_DECLARE_FLAGS(AdditionalWaylandInterfaces, AdditionalWaylandInterface) + +class VirtualInputDeviceTabletTool : public InputDeviceTabletTool +{ + Q_OBJECT + +public: + explicit VirtualInputDeviceTabletTool(QObject *parent = nullptr); + + void setSerialId(quint64 serialId); + void setUniqueId(quint64 uniqueId); + void setType(Type type); + void setCapabilities(const QList &capabilities); + + quint64 serialId() const override; + quint64 uniqueId() const override; + Type type() const override; + QList capabilities() const override; + +private: + quint64 m_serialId = 0; + quint64 m_uniqueId = 0; + Type m_type = Type::Pen; + QList m_capabilities; +}; + +class VirtualInputDevice : public InputDevice +{ + Q_OBJECT + +public: + explicit VirtualInputDevice(QObject *parent = nullptr); + + void setPointer(bool set); + void setKeyboard(bool set); + void setTouch(bool set); + void setLidSwitch(bool set); + void setTabletPad(bool set); + void setTabletTool(bool set); + void setName(const QString &name); + void setGroup(uintptr_t group); + + QString name() const override; + void *group() const override; + + bool isEnabled() const override; + void setEnabled(bool enabled) override; + + bool isKeyboard() const override; + bool isPointer() const override; + bool isTouchpad() const override; + bool isTouch() const override; + bool isTabletTool() const override; + bool isTabletPad() const override; + bool isTabletModeSwitch() const override; + bool isLidSwitch() const override; + +private: + QString m_name; + void *m_group = nullptr; + bool m_pointer = false; + bool m_keyboard = false; + bool m_touch = false; + bool m_lidSwitch = false; + bool m_tabletPad = false; + bool m_tabletTool = false; +}; + +class ColorManagerV1 : public QtWayland::wp_color_manager_v1 +{ +public: + explicit ColorManagerV1(::wl_registry *registry, uint32_t id, int version); + ~ColorManagerV1() override; +}; + +class FifoManagerV1 : public QtWayland::wp_fifo_manager_v1 +{ +public: + explicit FifoManagerV1(::wl_registry *registry, uint32_t id, int version); + ~FifoManagerV1() override; +}; + +class PresentationTime : public QtWayland::wp_presentation +{ +public: + explicit PresentationTime(::wl_registry *registry, uint32_t id, int version); + ~PresentationTime() override; +}; + +class WpPresentationFeedback : public QObject, public QtWayland::wp_presentation_feedback +{ + Q_OBJECT +public: + explicit WpPresentationFeedback(struct ::wp_presentation_feedback *obj); + ~WpPresentationFeedback() override; + +Q_SIGNALS: + void presented(std::chrono::nanoseconds timestamp, std::chrono::nanoseconds refreshDuration); + void discarded(); + +private: + void wp_presentation_feedback_presented(uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec, uint32_t refresh, uint32_t seq_hi, uint32_t seq_lo, uint32_t flags) override; + void wp_presentation_feedback_discarded() override; +}; + +class XdgActivationToken : public QObject, public QtWayland::xdg_activation_token_v1 +{ + Q_OBJECT +public: + explicit XdgActivationToken(::xdg_activation_token_v1 *object); + ~XdgActivationToken() override; + + QString commitAndWait(); + +Q_SIGNALS: + void tokenReceived(); + +private: + void xdg_activation_token_v1_done(const QString &token) override; + + QString m_token; +}; + +class XdgActivation : public QtWayland::xdg_activation_v1 +{ +public: + explicit XdgActivation(::wl_registry *registry, uint32_t id, int version); + ~XdgActivation() override; + + std::unique_ptr createToken(); +}; + +class XdgToplevelSessionV1 : public QObject, public QtWayland::xx_toplevel_session_v1 +{ + Q_OBJECT + +public: + explicit XdgToplevelSessionV1(::xx_toplevel_session_v1 *session); + ~XdgToplevelSessionV1() override; + +Q_SIGNALS: + void restored(); + +protected: + void xx_toplevel_session_v1_restored(struct ::xdg_toplevel *surface) override; +}; + +class XdgSessionV1 : public QObject, public QtWayland::xx_session_v1 +{ + Q_OBJECT + +public: + explicit XdgSessionV1(::xx_session_v1 *session); + ~XdgSessionV1() override; + + std::unique_ptr add(XdgToplevel *toplevel, const QString &toplevelId); + std::unique_ptr restore(XdgToplevel *toplevel, const QString &toplevelId); + +Q_SIGNALS: + void created(const QString &id); + void restored(); + void replaced(); + +protected: + void xx_session_v1_created(const QString &id) override; + void xx_session_v1_restored() override; + void xx_session_v1_replaced() override; +}; + +class XdgSessionManagerV1 : public QtWayland::xx_session_manager_v1 +{ +public: + XdgSessionManagerV1(::wl_registry *registry, uint32_t id, int version); + ~XdgSessionManagerV1() override; +}; + +class KeyStateV1 : public QObject, public QtWayland::org_kde_kwin_keystate +{ + Q_OBJECT +public: + explicit KeyStateV1(::wl_registry *registry, uint32_t id, int version); + ~KeyStateV1() override; + + QHash keyToState; + +Q_SIGNALS: + void stateChanged(); + +private: + void org_kde_kwin_keystate_stateChanged(uint32_t key, uint32_t state) override; +}; + +class ColorRepresentationV1 : public QtWayland::wp_color_representation_manager_v1 +{ +public: + explicit ColorRepresentationV1(::wl_registry *registry, uint32_t id, int version); + ~ColorRepresentationV1() override; +}; + +class ColorRepresentationSurfaceV1 : public QtWayland::wp_color_representation_surface_v1 +{ +public: + explicit ColorRepresentationSurfaceV1(::wp_color_representation_surface_v1 *object); + ~ColorRepresentationSurfaceV1() override; +}; + +class WlKeyboard; + +class WlSeat : public QtWayland::wl_seat +{ +public: + explicit WlSeat(::wl_registry *registry, uint32_t id, int version); + ~WlSeat() override; + + std::unique_ptr getKeyboard(); +}; + +class WlKeyboard : public QObject, public QtWayland::wl_keyboard +{ + Q_OBJECT +public: + explicit WlKeyboard(::wl_keyboard *object); + ~WlKeyboard() override; + +Q_SIGNALS: + void enter(uint32_t serial, ::wl_surface *surface); + void leave(uint32_t serial, ::wl_surface *surface); + void key(uint32_t serial, uint32_t time, uint32_t key, key_state state); + +private: + void keyboard_keymap(uint32_t format, int32_t fd, uint32_t size) override; + void keyboard_enter(uint32_t serial, ::wl_surface *surface, wl_array *keys) override; + void keyboard_leave(uint32_t serial, ::wl_surface *surface) override; + void keyboard_key(uint32_t serial, uint32_t time, uint32_t key, uint32_t state) override; +}; + +struct Connection +{ + static std::unique_ptr setup(AdditionalWaylandInterfaces interfaces = AdditionalWaylandInterfaces()); + ~Connection(); + + bool sync(); + + KWayland::Client::ConnectionThread *connection = nullptr; + KWayland::Client::EventQueue *queue = nullptr; + KWayland::Client::Compositor *compositor = nullptr; + KWayland::Client::SubCompositor *subCompositor = nullptr; + KWayland::Client::ShadowManager *shadowManager = nullptr; + KWayland::Client::DataDeviceManager *dataDeviceManager = nullptr; + XdgShell *xdgShell = nullptr; + KWayland::Client::ShmPool *shm = nullptr; + KWayland::Client::Seat *seat = nullptr; + KWayland::Client::PlasmaShell *plasmaShell = nullptr; + KWayland::Client::PlasmaWindowManagement *windowManagement = nullptr; + KWayland::Client::PointerConstraints *pointerConstraints = nullptr; + KWayland::Client::Registry *registry = nullptr; + WaylandOutputManagementV2 *outputManagementV2 = nullptr; + QThread *thread = nullptr; + QList outputs; + QList outputDevicesV2; + IdleInhibitManagerV1 *idleInhibitManagerV1 = nullptr; + KWayland::Client::AppMenuManager *appMenu = nullptr; + XdgDecorationManagerV1 *xdgDecorationManagerV1 = nullptr; + KWayland::Client::TextInputManager *textInputManager = nullptr; + QtWayland::zwp_input_panel_v1 *inputPanelV1 = nullptr; + MockInputMethod *inputMethodV1 = nullptr; + QtWayland::zwp_input_method_context_v1 *inputMethodContextV1 = nullptr; + LayerShellV1 *layerShellV1 = nullptr; + TextInputManagerV3 *textInputManagerV3 = nullptr; + FractionalScaleManagerV1 *fractionalScaleManagerV1 = nullptr; + ScreencastingV1 *screencastingV1 = nullptr; + ScreenEdgeManagerV1 *screenEdgeManagerV1 = nullptr; + CursorShapeManagerV1 *cursorShapeManagerV1 = nullptr; + FakeInput *fakeInput = nullptr; + SecurityContextManagerV1 *securityContextManagerV1 = nullptr; + XdgWmDialogV1 *xdgWmDialogV1; + std::unique_ptr colorManager; + std::unique_ptr fifoManager; + std::unique_ptr presentationTime; + std::unique_ptr xdgActivation; + std::unique_ptr sessionManager; + std::unique_ptr tabletManager; + std::unique_ptr keyState; + std::unique_ptr primarySelectionManager; + std::unique_ptr toplevelDragManager; + std::unique_ptr linuxDmabuf; + std::unique_ptr colorRepresentation; + std::unique_ptr viewporter; + // TODO port everything away from KWayland::Client::Seat + std::unique_ptr kwinSeat; +}; + +void keyboardKeyPressed(quint32 key, quint32 time); +void keyboardKeyReleased(quint32 key, quint32 time); +void pointerAxisHorizontal(qreal delta, + quint32 time, + qint32 discreteDelta = 0, + PointerAxisSource source = PointerAxisSource::Unknown); +void pointerAxisVertical(qreal delta, + quint32 time, + qint32 discreteDelta = 0, + PointerAxisSource source = PointerAxisSource::Unknown); +void pointerButtonPressed(quint32 button, quint32 time); +void pointerButtonReleased(quint32 button, quint32 time); +void pointerMotion(const QPointF &position, quint32 time); +void pointerMotionRelative(const QPointF &delta, quint32 time); +void touchCancel(); +void touchDown(qint32 id, const QPointF &pos, quint32 time); +void touchMotion(qint32 id, const QPointF &pos, quint32 time); +void touchUp(qint32 id, quint32 time); +void tabletPadButtonPressed(quint32 button, quint32 time); +void tabletPadButtonReleased(quint32 button, quint32 time); +void tabletPadDialEvent(double delta, int number, quint32 time); +void tabletPadRingEvent(qreal position, int number, quint32 group, quint32 mode, quint32 time); +void tabletToolButtonPressed(quint32 button, quint32 time); +void tabletToolButtonReleased(quint32 button, quint32 time); +void tabletToolProximityEvent(const QPointF &pos, qreal xTilt, qreal yTilt, qreal rotation, qreal distance, bool tipNear, qreal sliderPosition, quint32 time); +void tabletToolAxisEvent(const QPointF &pos, qreal pressure, qreal xTilt, qreal yTilt, qreal rotation, qreal distance, bool tipDown, qreal sliderPosition, quint32 time); +void tabletToolTipEvent(const QPointF &pos, qreal pressure, qreal xTilt, qreal yTilt, qreal rotation, qreal distance, bool tipDown, qreal sliderPosition, quint32 time); + +/** + * Creates a Wayland Connection in a dedicated thread and creates various + * client side objects which can be used to create windows. + * @returns @c true if created successfully, @c false if there was an error + * @see destroyWaylandConnection + */ +bool setupWaylandConnection(AdditionalWaylandInterfaces flags = AdditionalWaylandInterfaces()); + +/** + * Destroys the Wayland Connection created with @link{setupWaylandConnection}. + * This can be called from cleanup in order to ensure that no Wayland Connection + * leaks into the next test method. + * @see setupWaylandConnection + */ +void destroyWaylandConnection(); + +KWayland::Client::ConnectionThread *waylandConnection(); +KWayland::Client::Compositor *waylandCompositor(); +KWayland::Client::SubCompositor *waylandSubCompositor(); +KWayland::Client::ShadowManager *waylandShadowManager(); +KWayland::Client::ShmPool *waylandShmPool(); +KWayland::Client::Seat *waylandSeat(); +KWayland::Client::DataDeviceManager *waylandDataDeviceManager(); +KWayland::Client::PlasmaShell *waylandPlasmaShell(); +KWayland::Client::PlasmaWindowManagement *waylandWindowManagement(); +KWayland::Client::PointerConstraints *waylandPointerConstraints(); +KWayland::Client::AppMenuManager *waylandAppMenuManager(); +WaylandOutputManagementV2 *waylandOutputManagementV2(); +KWayland::Client::TextInputManager *waylandTextInputManager(); +QList waylandOutputs(); +KWayland::Client::Output *waylandOutput(const QString &name); +ScreencastingV1 *screencasting(); +QList waylandOutputDevicesV2(); +FakeInput *waylandFakeInput(); +SecurityContextManagerV1 *waylandSecurityContextManagerV1(); +ColorManagerV1 *colorManager(); +FifoManagerV1 *fifoManager(); +PresentationTime *presentationTime(); +XdgActivation *xdgActivation(); +WpTabletManagerV2 *tabletManager(); +KeyStateV1 *keyState(); +WpPrimarySelectionDeviceManagerV1 *primarySelectionManager(); +XdgToplevelDragManagerV1 *toplevelDragManager(); +WaylandClient::LinuxDmabufV1 *linuxDmabuf(); +ColorRepresentationV1 *colorRepresentation(); +WaylandClient::Viewporter *viewporter(); + +bool waitForWaylandSurface(Window *window); + +bool waitForWaylandPointer(); +bool waitForWaylandPointer(KWayland::Client::Seat *seat); +bool waitForWaylandTouch(); +bool waitForWaylandTouch(KWayland::Client::Seat *seat); +bool waitForWaylandKeyboard(); +bool waitForWaylandKeyboard(KWayland::Client::Seat *seat); +bool waitForWaylandTabletTool(Test::WpTabletToolV2 *tool); + +void flushWaylandConnection(); + +/** + * Ensures that all client requests are processed by the compositor and all events + * sent by the compositor are seen by the client. + */ +bool waylandSync(); + +std::unique_ptr createSurface(); +std::unique_ptr createSurface(KWayland::Client::Compositor *compositor); +std::unique_ptr createSubSurface(KWayland::Client::Surface *surface, + KWayland::Client::Surface *parentSurface); + +std::unique_ptr createLayerSurfaceV1(KWayland::Client::Surface *surface, + const QString &scope, + KWayland::Client::Output *output = nullptr, + LayerShellV1::layer layer = LayerShellV1::layer_top); + +TextInputManagerV3 *waylandTextInputManagerV3(); + +enum class CreationSetup { + CreateOnly, + CreateAndConfigure, /// commit and wait for the configure event, making this surface ready to commit buffers +}; + +std::unique_ptr createInputPanelSurfaceV1(KWayland::Client::Surface *surface, + KWayland::Client::Output *output, + MockInputMethod::Mode mode); + +std::unique_ptr createFractionalScaleV1(KWayland::Client::Surface *surface); + +std::unique_ptr createXdgToplevelSurface(KWayland::Client::Surface *surface); +std::unique_ptr createXdgToplevelSurface(XdgShell *shell, KWayland::Client::Surface *surface); +std::unique_ptr createXdgToplevelSurface(KWayland::Client::Surface *surface, CreationSetup configureMode); +std::unique_ptr createXdgToplevelSurface(XdgShell *shell, KWayland::Client::Surface *surface, CreationSetup configureMode); +std::unique_ptr createXdgToplevelSurface(KWayland::Client::Surface *surface, std::function setup); +std::unique_ptr createXdgToplevelSurface(XdgShell *shell, KWayland::Client::Surface *surface, std::function setup); + +std::unique_ptr createXdgPositioner(); + +std::unique_ptr createXdgPopupSurface(KWayland::Client::Surface *surface, XdgSurface *parentSurface, + XdgPositioner *positioner, + CreationSetup configureMode = CreationSetup::CreateAndConfigure); + +std::unique_ptr createXdgToplevelDecorationV1(XdgToplevel *toplevel); +std::unique_ptr createIdleInhibitorV1(KWayland::Client::Surface *surface); +std::unique_ptr createAutoHideScreenEdgeV1(KWayland::Client::Surface *surface, uint32_t border); +std::unique_ptr createCursorShapeDeviceV1(KWayland::Client::Pointer *pointer); +std::unique_ptr createXdgDialogV1(XdgToplevel *toplevel); +std::unique_ptr createXdgSessionV1(XdgSessionManagerV1::reason reason, const QString &sessionId = QString()); +std::unique_ptr createXdgSessionV1(XdgSessionManagerV1 *manager, XdgSessionManagerV1::reason reason, const QString &sessionId = QString()); + +/** + * Creates a shared memory buffer of @p size in @p color and attaches it to the @p surface. + * The @p surface gets damaged and committed, thus it's rendered. + */ +void render(KWayland::Client::Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format = QImage::Format_ARGB32_Premultiplied); +void render(KWayland::Client::ShmPool *shm, KWayland::Client::Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format = QImage::Format_ARGB32_Premultiplied); + +/** + * Creates a shared memory buffer using the supplied image @p img and attaches it to the @p surface + */ +void render(KWayland::Client::Surface *surface, const QImage &img); +void render(KWayland::Client::ShmPool *shm, KWayland::Client::Surface *surface, const QImage &img); + +/** + * Waits till a new Window is shown and returns the created Window. + * If no Window gets shown during @p timeout @c null is returned. + */ +Window *waitForWaylandWindowShown(int timeout = 5000); + +/** + * Combination of @link{render} and @link{waitForWaylandWindowShown}. + */ +Window *renderAndWaitForShown(KWayland::Client::Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format = QImage::Format_ARGB32, int timeout = 5000); +Window *renderAndWaitForShown(KWayland::Client::ShmPool *shm, KWayland::Client::Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format = QImage::Format_ARGB32, int timeout = 5000); + +Window *renderAndWaitForShown(KWayland::Client::Surface *surface, const QImage &img, int timeout = 5000); +Window *renderAndWaitForShown(KWayland::Client::ShmPool *shm, KWayland::Client::Surface *surface, const QImage &img, int timeout = 5000); + +/** + * Waits for the @p window to be destroyed. + */ +bool waitForWindowClosed(Window *window); + +/** + * Locks the screen and waits till the screen is locked. + * @returns @c true if the screen could be locked, @c false otherwise + */ +bool lockScreen(); + +/** + * Unlocks the screen and waits till the screen is unlocked. + * @returns @c true if the screen could be unlocked, @c false otherwise + */ +bool unlockScreen(); + +/** + * Returns @c true if the system has at least one render node; otherwise returns @c false. + * + * This can be used to test whether the system is capable of allocating and sharing prime buffers, etc. + */ +bool renderNodeAvailable(); + +/** + * Returns @c true if the system has at least one primary node; otherwise returns @c false. + */ +bool primaryNodeAvailable(); + +/** + * Creates an X11 connection + * Internally a nested event loop is spawned whilst we connect to avoid a deadlock + * with X on demand + */ + +#if KWIN_BUILD_X11 +struct XcbConnectionDeleter +{ + void operator()(xcb_connection_t *pointer); +}; +typedef std::unique_ptr XcbConnectionPtr; +XcbConnectionPtr createX11Connection(); + +enum { + MWM_HINTS_FUNCTIONS = (1L << 0), + + MWM_FUNC_ALL = (1L << 0), + MWM_FUNC_RESIZE = (1L << 1), + MWM_FUNC_MOVE = (1L << 2), + MWM_FUNC_MINIMIZE = (1L << 3), + MWM_FUNC_MAXIMIZE = (1L << 4), + MWM_FUNC_CLOSE = (1L << 5), + + MWM_HINTS_DECORATIONS = (1L << 1), + + MWM_DECOR_ALL = (1L << 0), + MWM_DECOR_BORDER = (1L << 1), + MWM_DECOR_RESIZEH = (1L << 2), + MWM_DECOR_TITLE = (1L << 3), + MWM_DECOR_MENU = (1L << 4), + MWM_DECOR_MINIMIZE = (1L << 5), + MWM_DECOR_MAXIMIZE = (1L << 6), +}; + +struct MotifHints +{ + uint32_t flags = 0; + uint32_t functions = 0; + uint32_t decorations = 0; + int32_t input_mode = 0; + uint32_t status = 0; +}; + +void applyMotifHints(xcb_connection_t *connection, xcb_window_t window, const MotifHints &hints); +#endif + +MockInputMethod *inputMethod(); +KWayland::Client::Surface *inputPanelSurface(); + +class ScreencastingStreamV1 : public QObject, public QtWayland::zkde_screencast_stream_unstable_v1 +{ + Q_OBJECT + friend class ScreencastingV1; + +public: + ScreencastingStreamV1(QObject *parent) + : QObject(parent) + { + } + + ~ScreencastingStreamV1() override + { + if (isInitialized()) { + close(); + } + } + + quint32 nodeId() const + { + Q_ASSERT(m_nodeId.has_value()); + return *m_nodeId; + } + + void zkde_screencast_stream_unstable_v1_created(uint32_t node) override + { + m_nodeId = node; + Q_EMIT created(node); + } + + void zkde_screencast_stream_unstable_v1_closed() override + { + Q_EMIT closed(); + } + + void zkde_screencast_stream_unstable_v1_failed(const QString &error) override + { + Q_EMIT failed(error); + } + +Q_SIGNALS: + void created(quint32 nodeid); + void failed(const QString &error); + void closed(); + +private: + std::optional m_nodeId; +}; + +class ScreencastingV1 : public QObject, public QtWayland::zkde_screencast_unstable_v1 +{ + Q_OBJECT +public: + explicit ScreencastingV1(QObject *parent = nullptr) + : QObject(parent) + { + } + + ScreencastingStreamV1 *createOutputStream(wl_output *output, pointer mode) + { + auto stream = new ScreencastingStreamV1(this); + stream->init(stream_output(output, mode)); + return stream; + } + + ScreencastingStreamV1 *createWindowStream(const QString &uuid, pointer mode) + { + auto stream = new ScreencastingStreamV1(this); + stream->init(stream_window(uuid, mode)); + return stream; + } +}; + +using XkbContextPtr = std::unique_ptr; +using XkbKeymapPtr = std::unique_ptr; +using XkbStatePtr = std::unique_ptr; + +class SimpleKeyboard : public QObject +{ + Q_OBJECT +public: + explicit SimpleKeyboard(QObject *parent = nullptr); + KWayland::Client::Keyboard *keyboard(); + QString receviedText(); +Q_SIGNALS: + void receviedTextChanged(); + void keySymRecevied(xkb_keysym_t keysym); + +private: + KWayland::Client::Keyboard *m_keyboard; + QString m_receviedText; + XkbContextPtr m_ctx = XkbContextPtr(xkb_context_new(XKB_CONTEXT_NO_FLAGS), &xkb_context_unref); + XkbKeymapPtr m_keymap{nullptr, &xkb_keymap_unref}; + XkbStatePtr m_state{nullptr, &xkb_state_unref}; +}; + +struct OutputInfo +{ + Rect geometry; + double scale = 1; + bool internal = false; + QSize physicalSizeInMM; + QList> modes; + OutputTransform panelOrientation = OutputTransform::Kind::Normal; + QByteArray edid; + std::optional edidIdentifierOverride; + std::optional connectorName; + std::optional mstPath; +}; +void setOutputConfig(const QList &geometries); +void setOutputConfig(const QList &infos); + +class XdgToplevelWindow +{ +public: + explicit XdgToplevelWindow(const std::function &setup); + explicit XdgToplevelWindow(const std::function &setup = {}); + explicit XdgToplevelWindow(Connection *connection, const std::function &setup); + explicit XdgToplevelWindow(Connection *connection, const std::function &setup = {}); + XdgToplevelWindow(const XdgToplevelWindow ©) = delete; + ~XdgToplevelWindow(); + + bool show(const QSize &size = QSize(100, 100), const QColor &color = Qt::blue); + bool show(const QImage &image); + void unmap(); + bool unmapAndWaitForClosed(); + + /** + * Commits and waits for the commit to be presented. + * NOTE that this requires the presentation time protocol! + */ + bool presentWait(); + bool waitSurfaceConfigure(); + std::optional handleConfigure(const QColor &color = Qt::blue); + + Connection *m_connection = nullptr; + std::unique_ptr m_surface; + std::unique_ptr m_toplevel; + Window *m_window = nullptr; +}; +} + +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::Test::AdditionalWaylandInterfaces) +Q_DECLARE_METATYPE(KWin::Test::XdgToplevel::States) +Q_DECLARE_METATYPE(QtWayland::zxdg_toplevel_decoration_v1::mode) + +#define WAYLANDTEST_MAIN_OPT(TestObject, useDrm) \ + int main(int argc, char *argv[]) \ + { \ + setenv("QT_QPA_PLATFORM", "wayland-org.kde.kwin.qpa", true); \ + setenv("QT_QPA_PLATFORM_PLUGIN_PATH", QFileInfo(QString::fromLocal8Bit(argv[0])).absolutePath().toLocal8Bit().constData(), true); \ + setenv("KWIN_FORCE_OWN_QPA", "1", true); \ + qunsetenv("KDE_FULL_SESSION"); \ + qunsetenv("KDE_SESSION_VERSION"); \ + qunsetenv("XDG_SESSION_DESKTOP"); \ + qunsetenv("XDG_CURRENT_DESKTOP"); \ + KWin::WaylandTestApplication app(argc, argv, useDrm); \ + qunsetenv("QT_QPA_PLATFORM"); \ + qunsetenv("QT_QPA_PLATFORM_PLUGIN_PATH"); \ + qunsetenv("KWIN_FORCE_OWN_QPA"); \ + app.setAttribute(Qt::AA_Use96Dpi, true); \ + TestObject tc; \ + return QTest::qExec(&tc, argc, argv); \ + } + +#define WAYLANDTEST_MAIN(TestObject) WAYLANDTEST_MAIN_OPT(TestObject, false) +#define WAYLAND_DRM_TEST_MAIN(TestObject) WAYLANDTEST_MAIN_OPT(TestObject, true) + +#endif diff --git a/local/recipes/kde/kwin/source/autotests/integration/kwinbindings_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/kwinbindings_test.cpp new file mode 100644 index 0000000000..c364217cf7 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/kwinbindings_test.cpp @@ -0,0 +1,257 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "input.h" +#include "pointer_input.h" +#include "scripting/scripting.h" +#include "useractions.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include + +#include +#include +#include +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_kwinbindings-0"); + +class KWinBindingsTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testSwitchWindow(); + void testSwitchWindowScript(); + void testWindowToDesktop_data(); + void testWindowToDesktop(); +}; + +void KWinBindingsTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void KWinBindingsTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); + workspace()->setActiveOutput(QPoint(640, 512)); + KWin::input()->pointer()->warp(QPoint(640, 512)); +} + +void KWinBindingsTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void KWinBindingsTest::testSwitchWindow() +{ + // first create windows + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + auto c1 = Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::blue); + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + auto c2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 50), Qt::blue); + std::unique_ptr surface3(Test::createSurface()); + std::unique_ptr shellSurface3(Test::createXdgToplevelSurface(surface3.get())); + auto c3 = Test::renderAndWaitForShown(surface3.get(), QSize(100, 50), Qt::blue); + std::unique_ptr surface4(Test::createSurface()); + std::unique_ptr shellSurface4(Test::createXdgToplevelSurface(surface4.get())); + auto c4 = Test::renderAndWaitForShown(surface4.get(), QSize(100, 50), Qt::blue); + + QVERIFY(c4->isActive()); + QVERIFY(c4 != c3); + QVERIFY(c3 != c2); + QVERIFY(c2 != c1); + + // let's position all windows + c1->move(QPoint(0, 0)); + c2->move(QPoint(200, 0)); + c3->move(QPoint(200, 200)); + c4->move(QPoint(0, 200)); + + // now let's trigger the shortcuts + + // invoke global shortcut through dbus + auto invokeShortcut = [](const QString &shortcut) { + auto msg = QDBusMessage::createMethodCall( + QStringLiteral("org.kde.kglobalaccel"), + QStringLiteral("/component/kwin"), + QStringLiteral("org.kde.kglobalaccel.Component"), + QStringLiteral("invokeShortcut")); + msg.setArguments(QList{shortcut}); + QDBusConnection::sessionBus().asyncCall(msg); + }; + invokeShortcut(QStringLiteral("Switch Window Up")); + QTRY_COMPARE(workspace()->activeWindow(), c1); + invokeShortcut(QStringLiteral("Switch Window Right")); + QTRY_COMPARE(workspace()->activeWindow(), c2); + invokeShortcut(QStringLiteral("Switch Window Down")); + QTRY_COMPARE(workspace()->activeWindow(), c3); + invokeShortcut(QStringLiteral("Switch Window Left")); + QTRY_COMPARE(workspace()->activeWindow(), c4); + // test opposite direction + invokeShortcut(QStringLiteral("Switch Window Left")); + QTRY_COMPARE(workspace()->activeWindow(), c3); + invokeShortcut(QStringLiteral("Switch Window Down")); + QTRY_COMPARE(workspace()->activeWindow(), c2); + invokeShortcut(QStringLiteral("Switch Window Right")); + QTRY_COMPARE(workspace()->activeWindow(), c1); + invokeShortcut(QStringLiteral("Switch Window Up")); + QTRY_COMPARE(workspace()->activeWindow(), c4); +} + +void KWinBindingsTest::testSwitchWindowScript() +{ + QVERIFY(Scripting::self()); + + // first create windows + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + auto c1 = Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::blue); + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + auto c2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 50), Qt::blue); + std::unique_ptr surface3(Test::createSurface()); + std::unique_ptr shellSurface3(Test::createXdgToplevelSurface(surface3.get())); + auto c3 = Test::renderAndWaitForShown(surface3.get(), QSize(100, 50), Qt::blue); + std::unique_ptr surface4(Test::createSurface()); + std::unique_ptr shellSurface4(Test::createXdgToplevelSurface(surface4.get())); + auto c4 = Test::renderAndWaitForShown(surface4.get(), QSize(100, 50), Qt::blue); + + QVERIFY(c4->isActive()); + QVERIFY(c4 != c3); + QVERIFY(c3 != c2); + QVERIFY(c2 != c1); + + // let's position all windows + c1->move(QPoint(0, 0)); + c2->move(QPoint(200, 0)); + c3->move(QPoint(200, 200)); + c4->move(QPoint(0, 200)); + + auto runScript = [](const QString &slot) { + QTemporaryFile tmpFile; + QVERIFY(tmpFile.open()); + QTextStream out(&tmpFile); + out << "workspace." << slot << "()"; + out.flush(); + + const int id = Scripting::self()->loadScript(tmpFile.fileName()); + QVERIFY(id != -1); + QVERIFY(Scripting::self()->isScriptLoaded(tmpFile.fileName())); + auto s = Scripting::self()->findScript(tmpFile.fileName()); + QVERIFY(s); + QSignalSpy runningChangedSpy(s, &AbstractScript::runningChanged); + s->run(); + QTRY_COMPARE(runningChangedSpy.count(), 1); + }; + + runScript(QStringLiteral("slotSwitchWindowUp")); + QTRY_COMPARE(workspace()->activeWindow(), c1); + runScript(QStringLiteral("slotSwitchWindowRight")); + QTRY_COMPARE(workspace()->activeWindow(), c2); + runScript(QStringLiteral("slotSwitchWindowDown")); + QTRY_COMPARE(workspace()->activeWindow(), c3); + runScript(QStringLiteral("slotSwitchWindowLeft")); + QTRY_COMPARE(workspace()->activeWindow(), c4); +} + +void KWinBindingsTest::testWindowToDesktop_data() +{ + QTest::addColumn("desktop"); + + QTest::newRow("2") << 2; + QTest::newRow("3") << 3; + QTest::newRow("4") << 4; + QTest::newRow("5") << 5; + QTest::newRow("6") << 6; + QTest::newRow("7") << 7; + QTest::newRow("8") << 8; + QTest::newRow("9") << 9; + QTest::newRow("10") << 10; + QTest::newRow("11") << 11; + QTest::newRow("12") << 12; + QTest::newRow("13") << 13; + QTest::newRow("14") << 14; + QTest::newRow("15") << 15; + QTest::newRow("16") << 16; + QTest::newRow("17") << 17; + QTest::newRow("18") << 18; + QTest::newRow("19") << 19; + QTest::newRow("20") << 20; +} + +void KWinBindingsTest::testWindowToDesktop() +{ + // first go to desktop one + VirtualDesktopManager::self()->setCurrent(VirtualDesktopManager::self()->desktops().first()); + + // now create a window + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QSignalSpy desktopsChangedSpy(window, &Window::desktopsChanged); + QCOMPARE(workspace()->activeWindow(), window); + + QFETCH(int, desktop); + VirtualDesktopManager::self()->setCount(desktop); + const auto desktops = VirtualDesktopManager::self()->desktops(); + + // now trigger the shortcut + auto invokeShortcut = [](int desktop) { + auto msg = QDBusMessage::createMethodCall( + QStringLiteral("org.kde.kglobalaccel"), + QStringLiteral("/component/kwin"), + QStringLiteral("org.kde.kglobalaccel.Component"), + QStringLiteral("invokeShortcut")); + msg.setArguments(QList{QStringLiteral("Window to Desktop %1").arg(desktop)}); + QDBusConnection::sessionBus().asyncCall(msg); + }; + invokeShortcut(desktop); + QVERIFY(desktopsChangedSpy.wait()); + QCOMPARE(window->desktops(), QList{desktops.at(desktop - 1)}); + + // window to Desktop does not change the current desktop, change it manually + VirtualDesktopManager::self()->setCurrent(desktops.at(desktop - 1)); + + // back to desktop 1 + invokeShortcut(1); + QVERIFY(desktopsChangedSpy.wait()); + QCOMPARE(window->desktops(), QList{desktops.at(0)}); + + VirtualDesktopManager::self()->setCurrent(desktops.at(0)); + + // invoke with one desktop too many + invokeShortcut(desktop + 1); + // that should fail + QVERIFY(!desktopsChangedSpy.wait(100)); +} + +WAYLANDTEST_MAIN(KWinBindingsTest) +#include "kwinbindings_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/layershellv1window_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/layershellv1window_test.cpp new file mode 100644 index 0000000000..3a63af6a21 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/layershellv1window_test.cpp @@ -0,0 +1,867 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "core/outputconfiguration.h" +#include "main.h" +#include "pointer_input.h" +#include "screenedge.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include + +Q_DECLARE_METATYPE(QMargins) +Q_DECLARE_METATYPE(KWin::Layer) + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_layershellv1window-0"); + +class LayerShellV1WindowTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testOutput_data(); + void testOutput(); + void testAnchor_data(); + void testAnchor(); + void testMargins_data(); + void testMargins(); + void testLayer_data(); + void testLayer(); + void testChangeLayer(); + void testPlacementArea_data(); + void testPlacementArea(); + void testPlacementAreaAfterOutputLayoutChange(); + void testFill_data(); + void testFill(); + void testStack(); + void testKeyboardInteractivityNone(); + void testKeyboardInteractivityOnDemand(); + void testActivate_data(); + void testActivate(); + void testUnmap(); + void testScreenEdge_data(); + void testScreenEdge(); + void testUnconfiguredBuffer(); +}; + +void LayerShellV1WindowTest::initTestCase() +{ + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); +} + +void LayerShellV1WindowTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::LayerShellV1 | Test::AdditionalWaylandInterface::ScreenEdgeV1)); + + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); +} + +void LayerShellV1WindowTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void LayerShellV1WindowTest::testOutput_data() +{ + QTest::addColumn("screenId"); + + QTest::addRow("first output") << 0; + QTest::addRow("second output") << 1; +} + +void LayerShellV1WindowTest::testOutput() +{ + // Fetch the wl_output object. + QFETCH(int, screenId); + KWayland::Client::Output *output = Test::waylandOutputs().value(screenId); + QVERIFY(output); + + // Create a layer shell surface. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createLayerSurfaceV1(surface.get(), QStringLiteral("test"), output)); + + // Set the initial state of the layer surface. + shellSurface->set_size(280, 124); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + Window *window = Test::renderAndWaitForShown(surface.get(), requestedSize, Qt::red); + QVERIFY(window); + + // Verify that the window is on the requested screen. + QVERIFY(output->geometry().contains(window->frameGeometry().toRect())); + + // Destroy the window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void LayerShellV1WindowTest::testAnchor_data() +{ + QTest::addColumn("anchor"); + QTest::addColumn("expectedGeometry"); + + QTest::addRow("left") << int(Test::LayerSurfaceV1::anchor_left) + << RectF(0, 450, 280, 124); + + QTest::addRow("top left") << (Test::LayerSurfaceV1::anchor_top | Test::LayerSurfaceV1::anchor_left) + << RectF(0, 0, 280, 124); + + QTest::addRow("top") << int(Test::LayerSurfaceV1::anchor_top) + << RectF(500, 0, 280, 124); + + QTest::addRow("top right") << (Test::LayerSurfaceV1::anchor_top | Test::LayerSurfaceV1::anchor_right) + << RectF(1000, 0, 280, 124); + + QTest::addRow("right") << int(Test::LayerSurfaceV1::anchor_right) + << RectF(1000, 450, 280, 124); + + QTest::addRow("bottom right") << (Test::LayerSurfaceV1::anchor_bottom | Test::LayerSurfaceV1::anchor_right) + << RectF(1000, 900, 280, 124); + + QTest::addRow("bottom") << int(Test::LayerSurfaceV1::anchor_bottom) + << RectF(500, 900, 280, 124); + + QTest::addRow("bottom left") << (Test::LayerSurfaceV1::anchor_bottom | Test::LayerSurfaceV1::anchor_left) + << RectF(0, 900, 280, 124); +} + +void LayerShellV1WindowTest::testAnchor() +{ + // Create a layer shell surface. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createLayerSurfaceV1(surface.get(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + QFETCH(int, anchor); + shellSurface->set_anchor(anchor); + shellSurface->set_size(280, 124); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + QCOMPARE(requestedSize, QSize(280, 124)); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(280, 124), Qt::red); + QVERIFY(window); + + // Verify that the window is placed at expected location. + QTEST(window->frameGeometry(), "expectedGeometry"); + + // Destroy the window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void LayerShellV1WindowTest::testMargins_data() +{ + QTest::addColumn("anchor"); + QTest::addColumn("margins"); + QTest::addColumn("expectedGeometry"); + + QTest::addRow("left") << int(Test::LayerSurfaceV1::anchor_left) + << QMargins(100, 0, 0, 0) + << RectF(100, 450, 280, 124); + + QTest::addRow("top left") << (Test::LayerSurfaceV1::anchor_top | Test::LayerSurfaceV1::anchor_left) + << QMargins(100, 200, 0, 0) + << RectF(100, 200, 280, 124); + + QTest::addRow("top") << int(Test::LayerSurfaceV1::anchor_top) + << QMargins(0, 200, 0, 0) + << RectF(500, 200, 280, 124); + + QTest::addRow("top right") << (Test::LayerSurfaceV1::anchor_top | Test::LayerSurfaceV1::anchor_right) + << QMargins(0, 200, 300, 0) + << RectF(700, 200, 280, 124); + + QTest::addRow("right") << int(Test::LayerSurfaceV1::anchor_right) + << QMargins(0, 0, 300, 0) + << RectF(700, 450, 280, 124); + + QTest::addRow("bottom right") << (Test::LayerSurfaceV1::anchor_bottom | Test::LayerSurfaceV1::anchor_right) + << QMargins(0, 0, 300, 400) + << RectF(700, 500, 280, 124); + + QTest::addRow("bottom") << int(Test::LayerSurfaceV1::anchor_bottom) + << QMargins(0, 0, 0, 400) + << RectF(500, 500, 280, 124); + + QTest::addRow("bottom left") << (Test::LayerSurfaceV1::anchor_bottom | Test::LayerSurfaceV1::anchor_left) + << QMargins(100, 0, 0, 400) + << RectF(100, 500, 280, 124); +} + +void LayerShellV1WindowTest::testMargins() +{ + // Create a layer shell surface. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createLayerSurfaceV1(surface.get(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + QFETCH(QMargins, margins); + QFETCH(int, anchor); + shellSurface->set_anchor(anchor); + shellSurface->set_margin(margins.top(), margins.right(), margins.bottom(), margins.left()); + shellSurface->set_size(280, 124); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + Window *window = Test::renderAndWaitForShown(surface.get(), requestedSize, Qt::red); + QVERIFY(window); + + // Verify that the window is placed at expected location. + QTEST(window->frameGeometry(), "expectedGeometry"); + + // Destroy the window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void LayerShellV1WindowTest::testLayer_data() +{ + QTest::addColumn("protocolLayer"); + QTest::addColumn("compositorLayer"); + + QTest::addRow("overlay") << int(Test::LayerShellV1::layer_overlay) << OverlayLayer; + QTest::addRow("top") << int(Test::LayerShellV1::layer_top) << AboveLayer; + QTest::addRow("bottom") << int(Test::LayerShellV1::layer_bottom) << BelowLayer; + QTest::addRow("background") << int(Test::LayerShellV1::layer_background) << DesktopLayer; +} + +void LayerShellV1WindowTest::testLayer() +{ + // Create a layer shell surface. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createLayerSurfaceV1(surface.get(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + QFETCH(int, protocolLayer); + shellSurface->set_layer(protocolLayer); + shellSurface->set_size(280, 124); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + Window *window = Test::renderAndWaitForShown(surface.get(), requestedSize, Qt::red); + QVERIFY(window); + + // Verify that the window is placed at expected location. + QTEST(window->layer(), "compositorLayer"); + + // Destroy the window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void LayerShellV1WindowTest::testChangeLayer() +{ + // This test verifies that set_layer requests are handled properly after the surface has + // been mapped on the screen. + + // Create layer shell surfaces. + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createLayerSurfaceV1(surface1.get(), QStringLiteral("test"))); + shellSurface1->set_layer(Test::LayerShellV1::layer_bottom); + shellSurface1->set_size(200, 100); + surface1->commit(KWayland::Client::Surface::CommitFlag::None); + + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createLayerSurfaceV1(surface2.get(), QStringLiteral("test"))); + shellSurface2->set_layer(Test::LayerShellV1::layer_bottom); + shellSurface2->set_size(200, 100); + surface2->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surfaces. + QSignalSpy configureRequestedSpy1(shellSurface1.get(), &Test::LayerSurfaceV1::configureRequested); + QSignalSpy configureRequestedSpy2(shellSurface2.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy2.wait()); + const QSize requestedSize1 = configureRequestedSpy1.last().at(1).toSize(); + const QSize requestedSize2 = configureRequestedSpy2.last().at(1).toSize(); + + // Map the layer surfaces. + shellSurface1->ack_configure(configureRequestedSpy1.last().at(0).toUInt()); + Window *window1 = Test::renderAndWaitForShown(surface1.get(), requestedSize1, Qt::red); + QVERIFY(window1); + shellSurface2->ack_configure(configureRequestedSpy2.last().at(0).toUInt()); + Window *window2 = Test::renderAndWaitForShown(surface2.get(), requestedSize2, Qt::red); + QVERIFY(window2); + + // The first layer shell window is stacked below the second one. + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window2})); + + // Move the first layer shell window to the top layer. + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + shellSurface1->set_layer(Test::LayerShellV1::layer_top); + surface1->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(stackingOrderChangedSpy.wait()); + + // The first layer shell window should be on top now. + QCOMPARE(workspace()->stackingOrder(), (QList{window2, window1})); + + // Destroy the window. + shellSurface1.reset(); + QVERIFY(Test::waitForWindowClosed(window1)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowClosed(window2)); +} + +void LayerShellV1WindowTest::testPlacementArea_data() +{ + QTest::addColumn("anchor"); + QTest::addColumn("margins"); + QTest::addColumn("exclusiveZone"); + QTest::addColumn("exclusiveEdge"); + QTest::addColumn("placementArea"); + + QTest::addRow("left") << int(Test::LayerSurfaceV1::anchor_left) << QMargins(0, 0, 0, 0) << 300 << 0 << RectF(300, 0, 980, 1024); + QTest::addRow("top") << int(Test::LayerSurfaceV1::anchor_top) << QMargins(0, 0, 0, 0) << 300 << 0 << RectF(0, 300, 1280, 724); + QTest::addRow("right") << int(Test::LayerSurfaceV1::anchor_right) << QMargins(0, 0, 0, 0) << 300 << 0 << RectF(0, 0, 980, 1024); + QTest::addRow("bottom") << int(Test::LayerSurfaceV1::anchor_bottom) << QMargins(0, 0, 0, 0) << 300 << 0 << RectF(0, 0, 1280, 724); + + QTest::addRow("top | left") << int(Test::LayerSurfaceV1::anchor_top | Test::LayerSurfaceV1::anchor_left) << QMargins(0, 0, 0, 0) << 300 << 0 << RectF(0, 0, 1280, 1024); + QTest::addRow("top | right") << int(Test::LayerSurfaceV1::anchor_top | Test::LayerSurfaceV1::anchor_right) << QMargins(0, 0, 0, 0) << 300 << 0 << RectF(0, 0, 1280, 1024); + QTest::addRow("bottom | left") << int(Test::LayerSurfaceV1::anchor_bottom | Test::LayerSurfaceV1::anchor_left) << QMargins(0, 0, 0, 0) << 300 << 0 << RectF(0, 0, 1280, 1024); + QTest::addRow("bottom | right") << int(Test::LayerSurfaceV1::anchor_bottom | Test::LayerSurfaceV1::anchor_right) << QMargins(0, 0, 0, 0) << 300 << 0 << RectF(0, 0, 1280, 1024); + + QTest::addRow("left, negative margin") << int(Test::LayerSurfaceV1::anchor_left) << QMargins(-5, 0, 0, 0) << 300 << 0 << RectF(295, 0, 985, 1024); + QTest::addRow("top, negative margin") << int(Test::LayerSurfaceV1::anchor_top) << QMargins(0, -5, 0, 0) << 300 << 0 << RectF(0, 295, 1280, 729); + QTest::addRow("right, negative margin") << int(Test::LayerSurfaceV1::anchor_right) << QMargins(0, 0, -5, 0) << 300 << 0 << RectF(0, 0, 985, 1024); + QTest::addRow("bottom, negative margin") << int(Test::LayerSurfaceV1::anchor_bottom) << QMargins(0, 0, 0, -5) << 300 << 0 << RectF(0, 0, 1280, 729); + + QTest::addRow("left, positive margin") << int(Test::LayerSurfaceV1::anchor_left) << QMargins(5, 0, 0, 0) << 300 << 0 << RectF(305, 0, 975, 1024); + QTest::addRow("top, positive margin") << int(Test::LayerSurfaceV1::anchor_top) << QMargins(0, 5, 0, 0) << 300 << 0 << RectF(0, 305, 1280, 719); + QTest::addRow("right, positive margin") << int(Test::LayerSurfaceV1::anchor_right) << QMargins(0, 0, 5, 0) << 300 << 0 << RectF(0, 0, 975, 1024); + QTest::addRow("bottom, positive margin") << int(Test::LayerSurfaceV1::anchor_bottom) << QMargins(0, 0, 0, 5) << 300 << 0 << RectF(0, 0, 1280, 719); + + QTest::addRow("left + left exclusive edge") << int(Test::LayerSurfaceV1::anchor_left) << QMargins(0, 0, 0, 0) << 300 << int(Test::LayerSurfaceV1::anchor_left) << RectF(300, 0, 980, 1024); + QTest::addRow("top + top exclusive edge") << int(Test::LayerSurfaceV1::anchor_top) << QMargins(0, 0, 0, 0) << 300 << int(Test::LayerSurfaceV1::anchor_top) << RectF(0, 300, 1280, 724); + QTest::addRow("right + right exclusive edge") << int(Test::LayerSurfaceV1::anchor_right) << QMargins(0, 0, 0, 0) << 300 << int(Test::LayerSurfaceV1::anchor_right) << RectF(0, 0, 980, 1024); + QTest::addRow("bottom + bottom exclusive edge") << int(Test::LayerSurfaceV1::anchor_bottom) << QMargins(0, 0, 0, 0) << 300 << int(Test::LayerSurfaceV1::anchor_bottom) << RectF(0, 0, 1280, 724); + + QTest::addRow("top | left + top exclusive edge") << int(Test::LayerSurfaceV1::anchor_top | Test::LayerSurfaceV1::anchor_left) << QMargins(0, 0, 0, 0) << 300 << int(Test::LayerSurfaceV1::anchor_top) << RectF(0, 300, 1280, 724); + QTest::addRow("top | left + left exclusive edge") << int(Test::LayerSurfaceV1::anchor_top | Test::LayerSurfaceV1::anchor_left) << QMargins(0, 0, 0, 0) << 300 << int(Test::LayerSurfaceV1::anchor_left) << RectF(300, 0, 980, 1024); + QTest::addRow("top | right + top exclusive edge") << int(Test::LayerSurfaceV1::anchor_top | Test::LayerSurfaceV1::anchor_right) << QMargins(0, 0, 0, 0) << 300 << int(Test::LayerSurfaceV1::anchor_top) << RectF(0, 300, 1280, 724); + QTest::addRow("top | right + right exclusive edge") << int(Test::LayerSurfaceV1::anchor_top | Test::LayerSurfaceV1::anchor_right) << QMargins(0, 0, 0, 0) << 300 << int(Test::LayerSurfaceV1::anchor_right) << RectF(0, 0, 980, 1024); + QTest::addRow("bottom | left + bottom exclusive edge") << int(Test::LayerSurfaceV1::anchor_bottom | Test::LayerSurfaceV1::anchor_left) << QMargins(0, 0, 0, 0) << 300 << int(Test::LayerSurfaceV1::anchor_bottom) << RectF(0, 0, 1280, 724); + QTest::addRow("bottom | left + left exclusive edge") << int(Test::LayerSurfaceV1::anchor_bottom | Test::LayerSurfaceV1::anchor_left) << QMargins(0, 0, 0, 0) << 300 << int(Test::LayerSurfaceV1::anchor_left) << RectF(300, 0, 980, 1024); + QTest::addRow("bottom | right + bottom exclusive edge") << int(Test::LayerSurfaceV1::anchor_bottom | Test::LayerSurfaceV1::anchor_right) << QMargins(0, 0, 0, 0) << 300 << int(Test::LayerSurfaceV1::anchor_bottom) << RectF(0, 0, 1280, 724); + QTest::addRow("bottom | right + right exclusive edge") << int(Test::LayerSurfaceV1::anchor_bottom | Test::LayerSurfaceV1::anchor_right) << QMargins(0, 0, 0, 0) << 300 << int(Test::LayerSurfaceV1::anchor_right) << RectF(0, 0, 980, 1024); +} + +void LayerShellV1WindowTest::testPlacementArea() +{ + // Create a layer shell surface. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createLayerSurfaceV1(surface.get(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + QFETCH(int, anchor); + QFETCH(QMargins, margins); + QFETCH(int, exclusiveZone); + QFETCH(int, exclusiveEdge); + shellSurface->set_anchor(anchor); + shellSurface->set_margin(margins.top(), margins.right(), margins.bottom(), margins.left()); + shellSurface->set_exclusive_zone(exclusiveZone); + shellSurface->set_exclusive_edge(exclusiveEdge); + shellSurface->set_size(280, 124); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + Window *window = Test::renderAndWaitForShown(surface.get(), requestedSize, Qt::red); + QVERIFY(window); + + // Verify that the work area has been adjusted. + QTEST(workspace()->clientArea(PlacementArea, window), "placementArea"); + + // Destroy the window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void LayerShellV1WindowTest::testPlacementAreaAfterOutputLayoutChange() +{ + // This test verifies that layer shell windows correctly react to output layout changes. + + // The output where the layer surface should be placed. + LogicalOutput *logicalOutput = workspace()->activeOutput(); + BackendOutput *backendOutput = logicalOutput->backendOutput(); + + // Create a layer surface with an exclusive zone. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createLayerSurfaceV1(surface.get(), QStringLiteral("dock"), Test::waylandOutput(backendOutput->name()))); + shellSurface->set_layer(Test::LayerShellV1::layer_top); + shellSurface->set_anchor(Test::LayerSurfaceV1::anchor_bottom); + shellSurface->set_size(100, 50); + shellSurface->set_exclusive_edge(Test::LayerSurfaceV1::anchor_bottom); + shellSurface->set_exclusive_zone(50); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the layer surface. + QSignalSpy configureRequestedSpy(shellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.wait()); + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + Window *window = Test::renderAndWaitForShown(surface.get(), configureRequestedSpy.last().at(1).toSize(), Qt::red); + QVERIFY(window); + QCOMPARE(workspace()->clientArea(PlacementArea, window), logicalOutput->geometry().adjusted(0, 0, 0, -50)); + + // Move the output 100px down. + OutputConfiguration config1; + { + auto changeSet = config1.changeSet(backendOutput); + changeSet->pos = logicalOutput->geometry().topLeft() + QPoint(0, 100); + } + workspace()->applyOutputConfiguration(config1); + QCOMPARE(workspace()->clientArea(PlacementArea, window), logicalOutput->geometry().adjusted(0, 0, 0, -50)); + + // Move the output back to its original position. + OutputConfiguration config2; + { + auto changeSet = config2.changeSet(backendOutput); + changeSet->pos = logicalOutput->geometry().topLeft() - QPoint(0, 100); + } + workspace()->applyOutputConfiguration(config2); + QCOMPARE(workspace()->clientArea(PlacementArea, window), logicalOutput->geometry().adjusted(0, 0, 0, -50)); + + // Destroy the window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void LayerShellV1WindowTest::testFill_data() +{ + QTest::addColumn("anchor"); + QTest::addColumn("desiredSize"); + QTest::addColumn("expectedGeometry"); + + QTest::addRow("horizontal") << (Test::LayerSurfaceV1::anchor_left | Test::LayerSurfaceV1::anchor_right) + << QSize(0, 124) + << RectF(0, 450, 1280, 124); + + QTest::addRow("vertical") << (Test::LayerSurfaceV1::anchor_top | Test::LayerSurfaceV1::anchor_bottom) + << QSize(280, 0) + << RectF(500, 0, 280, 1024); + + QTest::addRow("all") << (Test::LayerSurfaceV1::anchor_left | Test::LayerSurfaceV1::anchor_top | Test::LayerSurfaceV1::anchor_right | Test::LayerSurfaceV1::anchor_bottom) + << QSize(0, 0) + << RectF(0, 0, 1280, 1024); +} + +void LayerShellV1WindowTest::testFill() +{ + // Create a layer shell surface. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createLayerSurfaceV1(surface.get(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + QFETCH(int, anchor); + QFETCH(QSize, desiredSize); + shellSurface->set_anchor(anchor); + shellSurface->set_size(desiredSize.width(), desiredSize.height()); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + Window *window = Test::renderAndWaitForShown(surface.get(), requestedSize, Qt::red); + QVERIFY(window); + + // Verify that the window is placed at expected location. + QTEST(window->frameGeometry(), "expectedGeometry"); + + // Destroy the window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void LayerShellV1WindowTest::testStack() +{ + // Create a layer shell surface. + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createLayerSurfaceV1(surface1.get(), QStringLiteral("test"))); + + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createLayerSurfaceV1(surface2.get(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + shellSurface1->set_anchor(Test::LayerSurfaceV1::anchor_left); + shellSurface1->set_size(80, 124); + shellSurface1->set_exclusive_zone(80); + surface1->commit(KWayland::Client::Surface::CommitFlag::None); + + shellSurface2->set_anchor(Test::LayerSurfaceV1::anchor_left); + shellSurface2->set_size(200, 124); + shellSurface2->set_exclusive_zone(200); + surface2->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surfaces. + QSignalSpy configureRequestedSpy1(shellSurface1.get(), &Test::LayerSurfaceV1::configureRequested); + QSignalSpy configureRequestedSpy2(shellSurface2.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy2.wait()); + const QSize requestedSize1 = configureRequestedSpy1.last().at(1).toSize(); + const QSize requestedSize2 = configureRequestedSpy2.last().at(1).toSize(); + + // Map the layer surface. + shellSurface1->ack_configure(configureRequestedSpy1.last().at(0).toUInt()); + Window *window1 = Test::renderAndWaitForShown(surface1.get(), requestedSize1, Qt::red); + QVERIFY(window1); + + shellSurface2->ack_configure(configureRequestedSpy2.last().at(0).toUInt()); + Window *window2 = Test::renderAndWaitForShown(surface2.get(), requestedSize2, Qt::red); + QVERIFY(window2); + + // Check that the second layer surface is placed next to the first. + QCOMPARE(window1->frameGeometry(), RectF(0, 450, 80, 124)); + QCOMPARE(window2->frameGeometry(), RectF(80, 450, 200, 124)); + + // Check that the work area has been adjusted accordingly. + QCOMPARE(workspace()->clientArea(PlacementArea, window1), RectF(280, 0, 1000, 1024)); + QCOMPARE(workspace()->clientArea(PlacementArea, window2), RectF(280, 0, 1000, 1024)); + + // Destroy the window. + shellSurface1.reset(); + QVERIFY(Test::waitForWindowClosed(window1)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowClosed(window2)); +} + +void LayerShellV1WindowTest::testKeyboardInteractivityNone() +{ + // Create a layer shell surface. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createLayerSurfaceV1(surface.get(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + shellSurface->set_keyboard_interactivity(0); + shellSurface->set_size(100, 50); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + Window *window = Test::renderAndWaitForShown(surface.get(), requestedSize, Qt::red); + QVERIFY(window); + QVERIFY(!window->isActive()); + + // Try to activate the surface. + workspace()->activateWindow(window); + QVERIFY(!window->isActive()); + + // Destroy the window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void LayerShellV1WindowTest::testKeyboardInteractivityOnDemand() +{ + // Create a layer shell surface. + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createLayerSurfaceV1(surface1.get(), QStringLiteral("test"))); + shellSurface1->set_keyboard_interactivity(1); + shellSurface1->set_size(280, 124); + surface1->commit(KWayland::Client::Surface::CommitFlag::None); + + QSignalSpy configureRequestedSpy1(shellSurface1.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy1.wait()); + const QSize requestedSize1 = configureRequestedSpy1.last().at(1).toSize(); + shellSurface1->ack_configure(configureRequestedSpy1.last().at(0).toUInt()); + Window *window1 = Test::renderAndWaitForShown(surface1.get(), requestedSize1, Qt::red); + QVERIFY(window1); + QVERIFY(window1->isActive()); + + // Create the second layer shell surface. + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createLayerSurfaceV1(surface2.get(), QStringLiteral("test"))); + shellSurface2->set_keyboard_interactivity(1); + shellSurface2->set_size(280, 124); + surface2->commit(KWayland::Client::Surface::CommitFlag::None); + + QSignalSpy configureRequestedSpy2(shellSurface2.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy2.wait()); + const QSize requestedSize2 = configureRequestedSpy2.last().at(1).toSize(); + shellSurface2->ack_configure(configureRequestedSpy2.last().at(0).toUInt()); + Window *window2 = Test::renderAndWaitForShown(surface2.get(), requestedSize2, Qt::red); + QVERIFY(window2); + QVERIFY(window2->isActive()); + QVERIFY(!window1->isActive()); + + // Activate the first surface. + workspace()->activateWindow(window1); + QVERIFY(window1->isActive()); + QVERIFY(!window2->isActive()); + + // Destroy the window. + shellSurface1.reset(); + QVERIFY(Test::waitForWindowClosed(window1)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowClosed(window2)); +} + +void LayerShellV1WindowTest::testActivate_data() +{ + QTest::addColumn("layer"); + QTest::addColumn("active"); + + QTest::addRow("overlay") << int(Test::LayerShellV1::layer_overlay) << true; + QTest::addRow("top") << int(Test::LayerShellV1::layer_top) << true; + QTest::addRow("bottom") << int(Test::LayerShellV1::layer_bottom) << false; + QTest::addRow("background") << int(Test::LayerShellV1::layer_background) << false; +} + +void LayerShellV1WindowTest::testActivate() +{ + // Create a layer shell surface. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createLayerSurfaceV1(surface.get(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + QFETCH(int, layer); + shellSurface->set_layer(layer); + shellSurface->set_size(280, 124); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + Window *window = Test::renderAndWaitForShown(surface.get(), requestedSize, Qt::red); + QVERIFY(window); + QVERIFY(!window->isActive()); + + // Try to activate the layer surface. + shellSurface->set_keyboard_interactivity(1); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + QSignalSpy activeChangedSpy(window, &Window::activeChanged); + QTEST(activeChangedSpy.wait(1000), "active"); + + // Destroy the window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void LayerShellV1WindowTest::testUnmap() +{ + // Create a layer shell surface. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createLayerSurfaceV1(surface.get(), QStringLiteral("test"))); + + // Set the initial state of the layer surface. + shellSurface->set_size(280, 124); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.wait()); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(280, 124), Qt::red); + QVERIFY(window); + + // Unmap the layer surface. + surface->attachBuffer(KWayland::Client::Buffer::Ptr()); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(Test::waitForWindowClosed(window)); + + // Notify the compositor that we want to map the layer surface. + shellSurface->set_size(280, 124); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the configure event. + QVERIFY(configureRequestedSpy.wait()); + + // Map the layer surface back. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + window = Test::renderAndWaitForShown(surface.get(), QSize(280, 124), Qt::red); + QVERIFY(window); + + // Destroy the window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void LayerShellV1WindowTest::testScreenEdge_data() +{ + QTest::addColumn("margins"); + + QTest::addRow("normal") << QMargins(0, 0, 0, 0); + QTest::addRow("with margin") << QMargins(0, 0, 0, 10); +} + +void LayerShellV1WindowTest::testScreenEdge() +{ + auto config = kwinApp()->config(); + config->group(QStringLiteral("Windows")).writeEntry("ElectricBorderDelay", 75); + config->sync(); + workspace()->slotReconfigure(); + + // Create a layer shell surface. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createLayerSurfaceV1(surface.get(), QStringLiteral("test"))); + std::unique_ptr screenEdge(Test::createAutoHideScreenEdgeV1(surface.get(), Test::ScreenEdgeManagerV1::border_bottom)); + + // Set the initial state of the layer surface. + QFETCH(QMargins, margins); + shellSurface->set_layer(Test::LayerShellV1::layer_top); + shellSurface->set_anchor(Test::LayerSurfaceV1::anchor_bottom); + shellSurface->set_size(100, 50); + shellSurface->set_margin(margins.top(), margins.right(), margins.bottom(), margins.left()); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + Window *window = Test::renderAndWaitForShown(surface.get(), requestedSize, Qt::red); + QVERIFY(window); + QVERIFY(!window->isActive()); + + QSignalSpy hiddenChangedSpy(window, &Window::hiddenChanged); + quint32 timestamp = 0; + + // The layer surface will be hidden and shown when the screen edge is activated or deactivated. + { + screenEdge->activate(); + QVERIFY(hiddenChangedSpy.wait()); + QVERIFY(!window->isShown()); + + screenEdge->deactivate(); + QVERIFY(hiddenChangedSpy.wait()); + QVERIFY(window->isShown()); + } + + // The layer surface will be shown when the screen edge is triggered. + { + screenEdge->activate(); + QVERIFY(hiddenChangedSpy.wait()); + QVERIFY(!window->isShown()); + + Test::pointerMotion(QPointF(640, 1023), timestamp); + timestamp += 160; + Test::pointerMotion(QPointF(640, 1023), timestamp); + timestamp += 160; + Test::pointerMotion(QPointF(640, 512), timestamp); + QVERIFY(hiddenChangedSpy.wait()); + QVERIFY(window->isShown()); + } + + // The approaching state will be reset if the window is shown manually. + { + QSignalSpy approachingSpy(workspace()->screenEdges(), &ScreenEdges::approaching); + screenEdge->activate(); + QVERIFY(hiddenChangedSpy.wait()); + QVERIFY(!window->isShown()); + + Test::pointerMotion(QPointF(640, 1020), timestamp++); + QVERIFY(approachingSpy.last().at(1).toReal() == 0.0); + Test::pointerMotion(QPointF(640, 1021), timestamp++); + QVERIFY(approachingSpy.last().at(1).toReal() != 0.0); + + screenEdge->deactivate(); + QVERIFY(hiddenChangedSpy.wait()); + QVERIFY(window->isShown()); + QVERIFY(approachingSpy.last().at(1).toReal() == 0.0); + + Test::pointerMotion(QPointF(640, 512), timestamp++); + } + + // The layer surface will be shown when the screen edge is destroyed. + { + screenEdge->activate(); + QVERIFY(hiddenChangedSpy.wait()); + QVERIFY(!window->isShown()); + + screenEdge.reset(); + QVERIFY(hiddenChangedSpy.wait()); + QVERIFY(window->isShown()); + } +} + +void LayerShellV1WindowTest::testUnconfiguredBuffer() +{ + // This test verifies that a protocol error is posted when a client attaches a buffer to + // the initial commit. + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createLayerSurfaceV1(surface.get(), QStringLiteral("test"))); + shellSurface->set_anchor(Test::LayerSurfaceV1::anchor_bottom); + shellSurface->set_size(100, 50); + Test::render(surface.get(), QSize(100, 50), Qt::blue); + + QSignalSpy connectionErrorSpy(Test::waylandConnection(), &KWayland::Client::ConnectionThread::errorOccurred); + QVERIFY(connectionErrorSpy.wait()); +} + +} // namespace KWin + +WAYLANDTEST_MAIN(KWin::LayerShellV1WindowTest) +#include "layershellv1window_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/lockscreen.cpp b/local/recipes/kde/kwin/source/autotests/integration/lockscreen.cpp new file mode 100644 index 0000000000..d5d85feb39 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/lockscreen.cpp @@ -0,0 +1,818 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "compositor.h" +#include "core/output.h" +#include "core/renderbackend.h" +#include "effect/effecthandler.h" +#include "pointer_input.h" +#include "screenedge.h" +#include "wayland/keyboard.h" +#include "wayland/seat.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// screenlocker +#include + +#include + +#include +#include + +#include + +Q_DECLARE_METATYPE(Qt::Orientation) + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_lock_screen-0"); + +class LockScreenTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testStackingOrder(); + void testPointer(); + void testPointerButton(); + void testPointerAxis(); + void testKeyboard(); + void testScreenEdge(); + void testEffects(); + void testEffectsKeyboard(); + void testEffectsKeyboardAutorepeat(); + void testMoveWindow(); + void testPointerShortcut(); + void testAxisShortcut_data(); + void testAxisShortcut(); + void testKeyboardLockShortcut(); + void testKeyboardShortcutsDisabledWhenLocked(); + void testTouch(); + +private: + struct WindowHandle + { + Window *window; + std::unique_ptr surface; + std::unique_ptr shellSurface; + }; + void unlock(); + WindowHandle showWindow(); + KWayland::Client::ConnectionThread *m_connection = nullptr; + KWayland::Client::Compositor *m_compositor = nullptr; + KWayland::Client::Seat *m_seat = nullptr; + KWayland::Client::ShmPool *m_shm = nullptr; +}; + +class HelperEffect : public Effect +{ + Q_OBJECT +public: + HelperEffect() + { + } + ~HelperEffect() override + { + } + + void windowInputMouseEvent(QEvent *) override + { + Q_EMIT inputEvent(); + } + void grabbedKeyboardEvent(QKeyEvent *e) override + { + Q_EMIT keyEvent(e->text()); + } + +Q_SIGNALS: + void inputEvent(); + void keyEvent(const QString &); +}; + +#define LOCK \ + do { \ + QVERIFY(!waylandServer()->isScreenLocked()); \ + QSignalSpy lockStateChangedSpy(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged); \ + ScreenLocker::KSldApp::self()->lock(ScreenLocker::EstablishLock::Immediate); \ + QTRY_COMPARE(ScreenLocker::KSldApp::self()->lockState(), ScreenLocker::KSldApp::Locked); \ + QVERIFY(waylandServer()->isScreenLocked()); \ + } while (false) + +#define UNLOCK \ + do { \ + QSignalSpy lockStateChangedSpy(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged); \ + unlock(); \ + if (lockStateChangedSpy.count() != 1) { \ + QVERIFY(lockStateChangedSpy.wait()); \ + } \ + QCOMPARE(lockStateChangedSpy.count(), 1); \ + QVERIFY(!waylandServer()->isScreenLocked()); \ + } while (false) + +#define MOTION(target) Test::pointerMotion(target, timestamp++) + +#define PRESS Test::pointerButtonPressed(BTN_LEFT, timestamp++) + +#define RELEASE Test::pointerButtonReleased(BTN_LEFT, timestamp++) + +#define KEYPRESS(key) Test::keyboardKeyPressed(key, timestamp++) + +#define KEYRELEASE(key) Test::keyboardKeyReleased(key, timestamp++) + +void LockScreenTest::unlock() +{ + using namespace ScreenLocker; + const auto children = KSldApp::self()->children(); + for (auto it = children.begin(); it != children.end(); ++it) { + if (qstrcmp((*it)->metaObject()->className(), "LogindIntegration") != 0) { + continue; + } + QMetaObject::invokeMethod(*it, "requestUnlock"); + break; + } +} + +LockScreenTest::WindowHandle LockScreenTest::showWindow() +{ +#define VERIFY(statement) \ + if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__)) \ + return {nullptr, nullptr}; +#define COMPARE(actual, expected) \ + if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__)) \ + return {nullptr, nullptr}; + + std::unique_ptr surface = Test::createSurface(); + VERIFY(surface.get()); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + VERIFY(shellSurface.get()); + // let's render + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + VERIFY(window); + COMPARE(workspace()->activeWindow(), window); + +#undef VERIFY +#undef COMPARE + + return {window, std::move(surface), std::move(shellSurface)}; +} + +void LockScreenTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType("ElectricBorder"); + + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); +} + +void LockScreenTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); + QVERIFY(Test::waitForWaylandPointer()); + m_connection = Test::waylandConnection(); + m_compositor = Test::waylandCompositor(); + m_shm = Test::waylandShmPool(); + m_seat = Test::waylandSeat(); + + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); + options->setSeparateScreenFocus(false); +} + +void LockScreenTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void LockScreenTest::testStackingOrder() +{ + // This test verifies that the lockscreen greeter is placed above other windows. + + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + + LOCK; + QVERIFY(windowAddedSpy.wait()); + + Window *window = windowAddedSpy.first().first().value(); + QVERIFY(window); + QVERIFY(window->isLockScreen()); + QCOMPARE(window->layer(), AboveLayer); + + UNLOCK; +} + +void LockScreenTest::testPointer() +{ + std::unique_ptr pointer(m_seat->createPointer()); + QVERIFY(pointer != nullptr); + QSignalSpy enteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); + QSignalSpy leftSpy(pointer.get(), &KWayland::Client::Pointer::left); + + auto [window, surface, shellSurface] = showWindow(); + QVERIFY(window); + + // first move cursor into the center of the window + quint32 timestamp = 1; + MOTION(window->frameGeometry().center()); + QVERIFY(enteredSpy.wait()); + + LOCK; + + QVERIFY(leftSpy.wait()); + QCOMPARE(leftSpy.count(), 1); + + // simulate moving out in and out again + MOTION(window->frameGeometry().center()); + MOTION(window->frameGeometry().bottomRight() + QPoint(100, 100)); + MOTION(window->frameGeometry().bottomRight() + QPoint(100, 100)); + QVERIFY(!leftSpy.wait(10)); + QCOMPARE(leftSpy.count(), 1); + QCOMPARE(enteredSpy.count(), 1); + + // go back on the window + MOTION(window->frameGeometry().center()); + // and unlock + UNLOCK; + + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 2); + // move on the window + MOTION(window->frameGeometry().center() + QPoint(100, 100)); + QVERIFY(leftSpy.wait()); + MOTION(window->frameGeometry().center()); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 3); +} + +void LockScreenTest::testPointerButton() +{ + std::unique_ptr pointer(m_seat->createPointer()); + QVERIFY(pointer != nullptr); + QSignalSpy enteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); + QSignalSpy buttonChangedSpy(pointer.get(), &KWayland::Client::Pointer::buttonStateChanged); + + auto [window, surface, shellSurface] = showWindow(); + QVERIFY(window); + + // first move cursor into the center of the window + quint32 timestamp = 1; + MOTION(window->frameGeometry().center()); + QVERIFY(enteredSpy.wait()); + // and simulate a click + PRESS; + QVERIFY(buttonChangedSpy.wait()); + RELEASE; + QVERIFY(buttonChangedSpy.wait()); + + LOCK; + + // and simulate a click + PRESS; + QVERIFY(!buttonChangedSpy.wait(10)); + RELEASE; + QVERIFY(!buttonChangedSpy.wait(10)); + + UNLOCK; + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 2); + + // and click again + PRESS; + QVERIFY(buttonChangedSpy.wait()); + RELEASE; + QVERIFY(buttonChangedSpy.wait()); +} + +void LockScreenTest::testPointerAxis() +{ + std::unique_ptr pointer(m_seat->createPointer()); + QVERIFY(pointer != nullptr); + QSignalSpy axisChangedSpy(pointer.get(), &KWayland::Client::Pointer::axisChanged); + QSignalSpy enteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); + + auto [window, surface, shellSurface] = showWindow(); + QVERIFY(window); + + // first move cursor into the center of the window + quint32 timestamp = 1; + MOTION(window->frameGeometry().center()); + QVERIFY(enteredSpy.wait()); + // and simulate axis + Test::pointerAxisHorizontal(5.0, timestamp++); + QVERIFY(axisChangedSpy.wait()); + + LOCK; + + // and simulate axis + Test::pointerAxisHorizontal(5.0, timestamp++); + QVERIFY(!axisChangedSpy.wait(10)); + Test::pointerAxisVertical(15.0, timestamp++); + QVERIFY(!axisChangedSpy.wait(10)); + + // and unlock + UNLOCK; + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 2); + + // and move axis again + Test::pointerAxisHorizontal(15.0, timestamp++); + QVERIFY(axisChangedSpy.wait()); + Test::pointerAxisVertical(15.0, timestamp++); + QVERIFY(axisChangedSpy.wait()); +} + +void LockScreenTest::testKeyboard() +{ + std::unique_ptr keyboard(m_seat->createKeyboard()); + QVERIFY(keyboard != nullptr); + QSignalSpy enteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); + QSignalSpy leftSpy(keyboard.get(), &KWayland::Client::Keyboard::left); + QSignalSpy keyChangedSpy(keyboard.get(), &KWayland::Client::Keyboard::keyChanged); + + auto [window, surface, shellSurface] = showWindow(); + QVERIFY(window); + QVERIFY(enteredSpy.wait()); + QTRY_COMPARE(enteredSpy.count(), 1); + + quint32 timestamp = 1; + KEYPRESS(KEY_A); + QVERIFY(keyChangedSpy.wait()); + QCOMPARE(keyChangedSpy.count(), 1); + QCOMPARE(keyChangedSpy.at(0).at(0).value(), quint32(KEY_A)); + QCOMPARE(keyChangedSpy.at(0).at(1).value(), KWayland::Client::Keyboard::KeyState::Pressed); + QCOMPARE(keyChangedSpy.at(0).at(2).value(), quint32(1)); + KEYRELEASE(KEY_A); + QVERIFY(keyChangedSpy.wait()); + QCOMPARE(keyChangedSpy.count(), 2); + QCOMPARE(keyChangedSpy.at(1).at(0).value(), quint32(KEY_A)); + QCOMPARE(keyChangedSpy.at(1).at(1).value(), KWayland::Client::Keyboard::KeyState::Released); + QCOMPARE(keyChangedSpy.at(1).at(2).value(), quint32(2)); + + LOCK; + QVERIFY(leftSpy.wait()); + KEYPRESS(KEY_B); + KEYRELEASE(KEY_B); + QCOMPARE(leftSpy.count(), 1); + QCOMPARE(keyChangedSpy.count(), 2); + + UNLOCK; + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 2); + KEYPRESS(KEY_C); + QVERIFY(keyChangedSpy.wait()); + QCOMPARE(keyChangedSpy.count(), 3); + KEYRELEASE(KEY_C); + QVERIFY(keyChangedSpy.wait()); + QCOMPARE(keyChangedSpy.count(), 4); + QCOMPARE(enteredSpy.count(), 2); + QCOMPARE(keyChangedSpy.at(2).at(0).value(), quint32(KEY_C)); + QCOMPARE(keyChangedSpy.at(3).at(0).value(), quint32(KEY_C)); + QCOMPARE(keyChangedSpy.at(2).at(2).value(), quint32(5)); + QCOMPARE(keyChangedSpy.at(3).at(2).value(), quint32(6)); + QCOMPARE(keyChangedSpy.at(2).at(1).value(), KWayland::Client::Keyboard::KeyState::Pressed); + QCOMPARE(keyChangedSpy.at(3).at(1).value(), KWayland::Client::Keyboard::KeyState::Released); +} + +class TestObject : public QObject +{ + Q_OBJECT + +public Q_SLOTS: + bool callback(ElectricBorder border) + { + return true; + } +}; + +void LockScreenTest::testScreenEdge() +{ + QSignalSpy screenEdgeSpy(workspace()->screenEdges(), &ScreenEdges::approaching); + QCOMPARE(screenEdgeSpy.count(), 0); + + TestObject callback; + workspace()->screenEdges()->reserve(ElectricTopLeft, &callback, "callback"); + + quint32 timestamp = 1; + MOTION(QPoint(5, 5)); + QCOMPARE(screenEdgeSpy.count(), 1); + + LOCK; + + MOTION(QPoint(4, 4)); + QCOMPARE(screenEdgeSpy.count(), 1); + + // and unlock + UNLOCK; + + MOTION(QPoint(5, 5)); + QCOMPARE(screenEdgeSpy.count(), 2); +} + +void LockScreenTest::testEffects() +{ + std::unique_ptr effect(new HelperEffect); + QSignalSpy inputSpy(effect.get(), &HelperEffect::inputEvent); + effects->startMouseInterception(effect.get(), Qt::ArrowCursor); + + quint32 timestamp = 1; + QCOMPARE(inputSpy.count(), 0); + MOTION(QPoint(5, 5)); + QCOMPARE(inputSpy.count(), 1); + // simlate click + PRESS; + QCOMPARE(inputSpy.count(), 2); + RELEASE; + QCOMPARE(inputSpy.count(), 3); + + LOCK; + + MOTION(QPoint(6, 6)); + QCOMPARE(inputSpy.count(), 3); + // simlate click + PRESS; + QCOMPARE(inputSpy.count(), 3); + RELEASE; + QCOMPARE(inputSpy.count(), 3); + + UNLOCK; + + MOTION(QPoint(5, 5)); + QCOMPARE(inputSpy.count(), 4); + // simlate click + PRESS; + QCOMPARE(inputSpy.count(), 5); + RELEASE; + QCOMPARE(inputSpy.count(), 6); + + effects->stopMouseInterception(effect.get()); +} + +void LockScreenTest::testEffectsKeyboard() +{ + std::unique_ptr effect(new HelperEffect); + QSignalSpy inputSpy(effect.get(), &HelperEffect::keyEvent); + effects->grabKeyboard(effect.get()); + + quint32 timestamp = 1; + KEYPRESS(KEY_A); + QCOMPARE(inputSpy.count(), 1); + QCOMPARE(inputSpy.first().first().toString(), QStringLiteral("a")); + KEYRELEASE(KEY_A); + QCOMPARE(inputSpy.count(), 2); + QCOMPARE(inputSpy.first().first().toString(), QStringLiteral("a")); + QCOMPARE(inputSpy.at(1).first().toString(), QStringLiteral("a")); + + LOCK; + KEYPRESS(KEY_B); + QCOMPARE(inputSpy.count(), 2); + KEYRELEASE(KEY_B); + QCOMPARE(inputSpy.count(), 2); + + UNLOCK; + KEYPRESS(KEY_C); + QCOMPARE(inputSpy.count(), 3); + QCOMPARE(inputSpy.first().first().toString(), QStringLiteral("a")); + QCOMPARE(inputSpy.at(1).first().toString(), QStringLiteral("a")); + QCOMPARE(inputSpy.at(2).first().toString(), QStringLiteral("c")); + KEYRELEASE(KEY_C); + QCOMPARE(inputSpy.count(), 4); + QCOMPARE(inputSpy.first().first().toString(), QStringLiteral("a")); + QCOMPARE(inputSpy.at(1).first().toString(), QStringLiteral("a")); + QCOMPARE(inputSpy.at(2).first().toString(), QStringLiteral("c")); + QCOMPARE(inputSpy.at(3).first().toString(), QStringLiteral("c")); + + effects->ungrabKeyboard(); +} + +void LockScreenTest::testEffectsKeyboardAutorepeat() +{ + // this test is just like testEffectsKeyboard, but tests auto repeat key events + // while the key is pressed the Effect should get auto repeated events + // but the lock screen should filter them out + std::unique_ptr effect(new HelperEffect); + QSignalSpy inputSpy(effect.get(), &HelperEffect::keyEvent); + effects->grabKeyboard(effect.get()); + + // we need to configure the key repeat first. It is only enabled on libinput + waylandServer()->seat()->keyboard()->setRepeatInfo(25, 300); + + quint32 timestamp = 1; + KEYPRESS(KEY_A); + QCOMPARE(inputSpy.count(), 1); + QCOMPARE(inputSpy.first().first().toString(), QStringLiteral("a")); + QVERIFY(inputSpy.wait()); + QVERIFY(inputSpy.count() > 1); + // and still more events + QVERIFY(inputSpy.wait()); + QCOMPARE(inputSpy.at(1).first().toString(), QStringLiteral("a")); + + // now release + inputSpy.clear(); + KEYRELEASE(KEY_A); + QCOMPARE(inputSpy.count(), 1); + + // while locked key repeat should not pass any events to the Effect + LOCK; + KEYPRESS(KEY_B); + QVERIFY(!inputSpy.wait(10)); + KEYRELEASE(KEY_B); + QVERIFY(!inputSpy.wait(10)); + + UNLOCK; + // don't test again, that's covered by testEffectsKeyboard + + effects->ungrabKeyboard(); +} + +void LockScreenTest::testMoveWindow() +{ + auto [window, surface, shellSurface] = showWindow(); + QVERIFY(window); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &Window::interactiveMoveResizeStepped); + quint32 timestamp = 1; + + workspace()->slotWindowMove(); + QCOMPARE(workspace()->moveResizeWindow(), window); + QVERIFY(window->isInteractiveMove()); + Test::keyboardKeyPressed(KEY_RIGHT, timestamp++); + Test::keyboardKeyReleased(KEY_RIGHT, timestamp++); + QEXPECT_FAIL("", "First event is ignored", Continue); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + + // TODO adjust once the expected fail is fixed + Test::keyboardKeyPressed(KEY_RIGHT, timestamp++); + Test::keyboardKeyReleased(KEY_RIGHT, timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + + // interactive move resize session should end when the screen is locked + LOCK; + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!window->isInteractiveMove()); + Test::keyboardKeyPressed(KEY_RIGHT, timestamp++); + Test::keyboardKeyReleased(KEY_RIGHT, timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + + UNLOCK; + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!window->isInteractiveMove()); + Test::keyboardKeyPressed(KEY_RIGHT, timestamp++); + Test::keyboardKeyReleased(KEY_RIGHT, timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + Test::keyboardKeyPressed(KEY_ESC, timestamp++); + Test::keyboardKeyReleased(KEY_ESC, timestamp++); + QVERIFY(!window->isInteractiveMove()); +} + +void LockScreenTest::testPointerShortcut() +{ +#if !KWIN_BUILD_GLOBALSHORTCUTS + QSKIP("Can't test shortcuts without shortcuts"); + return; +#endif + + std::unique_ptr action(new QAction(nullptr)); + QSignalSpy actionSpy(action.get(), &QAction::triggered); + input()->registerPointerShortcut(Qt::MetaModifier, Qt::LeftButton, action.get()); + + // try to trigger the shortcut + quint32 timestamp = 1; +#define PERFORM(expectedCount) \ + do { \ + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); \ + PRESS; \ + QCoreApplication::instance()->processEvents(); \ + QCOMPARE(actionSpy.count(), expectedCount); \ + RELEASE; \ + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); \ + QCoreApplication::instance()->processEvents(); \ + QCOMPARE(actionSpy.count(), expectedCount); \ + } while (false) + + PERFORM(1); + + // now the same thing with a locked screen + LOCK; + PERFORM(1); + + // and as unlocked + UNLOCK; + PERFORM(2); +#undef PERFORM +} + +void LockScreenTest::testAxisShortcut_data() +{ +#if !KWIN_BUILD_GLOBALSHORTCUTS + QSKIP("Can't test shortcuts without shortcuts"); + return; +#endif + + QTest::addColumn("direction"); + QTest::addColumn("sign"); + + QTest::newRow("up") << Qt::Vertical << -1; + QTest::newRow("down") << Qt::Vertical << 1; + QTest::newRow("left") << Qt::Horizontal << -1; + QTest::newRow("right") << Qt::Horizontal << 1; +} + +void LockScreenTest::testAxisShortcut() +{ + std::unique_ptr action(new QAction(nullptr)); + QSignalSpy actionSpy(action.get(), &QAction::triggered); + QFETCH(Qt::Orientation, direction); + QFETCH(int, sign); + PointerAxisDirection axisDirection = PointerAxisUp; + if (direction == Qt::Vertical) { + axisDirection = sign < 0 ? PointerAxisUp : PointerAxisDown; + } else { + axisDirection = sign < 0 ? PointerAxisLeft : PointerAxisRight; + } + input()->registerAxisShortcut(Qt::MetaModifier, axisDirection, action.get()); + + // try to trigger the shortcut + quint32 timestamp = 1; +#define PERFORM(expectedCount) \ + do { \ + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); \ + if (direction == Qt::Vertical) \ + Test::pointerAxisVertical(sign * 15.0, timestamp++); \ + else \ + Test::pointerAxisHorizontal(sign * 15.0, timestamp++); \ + QCoreApplication::instance()->processEvents(); \ + QCOMPARE(actionSpy.count(), expectedCount); \ + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); \ + QCoreApplication::instance()->processEvents(); \ + QCOMPARE(actionSpy.count(), expectedCount); \ + } while (false) + + PERFORM(1); + + // now the same thing with a locked screen + LOCK; + PERFORM(1); + + // and as unlocked + UNLOCK; + PERFORM(2); +#undef PERFORM +} + +/** + * This test verifies that keyboard shortcuts are disabled when the screen is locked + */ +void LockScreenTest::testKeyboardShortcutsDisabledWhenLocked() +{ +#if !KWIN_BUILD_GLOBALSHORTCUTS + QSKIP("Can't test shortcuts without shortcuts"); + return; +#endif + + std::unique_ptr action(new QAction(nullptr)); + QSignalSpy actionSpy(action.get(), &QAction::triggered); + action->setProperty("componentName", QStringLiteral("kwin")); + action->setObjectName("LockScreenTest::testKeyboardShortcut"); + KGlobalAccel::self()->setDefaultShortcut(action.get(), QList{Qt::CTRL | Qt::META | Qt::ALT | Qt::Key_Space}); + KGlobalAccel::self()->setShortcut(action.get(), QList{Qt::CTRL | Qt::META | Qt::ALT | Qt::Key_Space}, + KGlobalAccel::NoAutoloading); + + // try to trigger the shortcut + quint32 timestamp = 1; + KEYPRESS(KEY_LEFTCTRL); + KEYPRESS(KEY_LEFTMETA); + KEYPRESS(KEY_LEFTALT); + KEYPRESS(KEY_SPACE); + QVERIFY(actionSpy.wait()); + QCOMPARE(actionSpy.count(), 1); + KEYRELEASE(KEY_SPACE); + QVERIFY(!actionSpy.wait(10)); + QCOMPARE(actionSpy.count(), 1); + + LOCK; + KEYPRESS(KEY_SPACE); + QVERIFY(!actionSpy.wait(10)); + QCOMPARE(actionSpy.count(), 1); + KEYRELEASE(KEY_SPACE); + QVERIFY(!actionSpy.wait(10)); + QCOMPARE(actionSpy.count(), 1); + + UNLOCK; + KEYPRESS(KEY_SPACE); + QVERIFY(actionSpy.wait()); + QCOMPARE(actionSpy.count(), 2); + KEYRELEASE(KEY_SPACE); + QVERIFY(!actionSpy.wait(10)); + QCOMPARE(actionSpy.count(), 2); + KEYRELEASE(KEY_LEFTCTRL); + KEYRELEASE(KEY_LEFTMETA); + KEYRELEASE(KEY_LEFTALT); +} + +/** + * This test verifies that the global keyboard shortcut to lock the screen works + */ +void LockScreenTest::testKeyboardLockShortcut() +{ +#if !KWIN_BUILD_GLOBALSHORTCUTS + QSKIP("Can't test shortcuts without shortcuts"); + return; +#endif + + QList shortcuts = KGlobalAccel::self()->globalShortcut("ksmserver", "Lock Session"); + // Verify the shortcut is Meta + L, the default + QCOMPARE(shortcuts.first().toString(), QString("Meta+L")); + + // Verify the screen is not locked + QVERIFY(!waylandServer()->isScreenLocked()); + + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + + // Trigger the shortcut + quint32 timestamp = 1; + KEYPRESS(KEY_LEFTMETA); + KEYPRESS(KEY_L); + KEYRELEASE(KEY_L); + KEYRELEASE(KEY_LEFTMETA); + + // Verify the screen gets locked + QVERIFY(windowAddedSpy.wait()); + Window *window = windowAddedSpy.first().first().value(); + QVERIFY(window); + QVERIFY(window->isLockScreen()); + QTRY_COMPARE(ScreenLocker::KSldApp::self()->lockState(), ScreenLocker::KSldApp::Locked); + QVERIFY(waylandServer()->isScreenLocked()); + + UNLOCK; +} + +void LockScreenTest::testTouch() +{ + auto touch = m_seat->createTouch(m_seat); + QVERIFY(touch); + QVERIFY(touch->isValid()); + auto [window, surface, shellSurface] = showWindow(); + QVERIFY(window); + QSignalSpy sequenceStartedSpy(touch, &KWayland::Client::Touch::sequenceStarted); + QSignalSpy cancelSpy(touch, &KWayland::Client::Touch::sequenceCanceled); + QSignalSpy pointRemovedSpy(touch, &KWayland::Client::Touch::pointRemoved); + + quint32 timestamp = 1; + Test::touchDown(1, QPointF(25, 25), timestamp++); + QVERIFY(sequenceStartedSpy.wait()); + QCOMPARE(sequenceStartedSpy.count(), 1); + + LOCK; + QVERIFY(cancelSpy.wait()); + + Test::touchUp(1, timestamp++); + QVERIFY(!pointRemovedSpy.wait(10)); + Test::touchDown(1, QPointF(25, 25), timestamp++); + Test::touchMotion(1, QPointF(26, 26), timestamp++); + Test::touchUp(1, timestamp++); + + UNLOCK; + Test::touchDown(1, QPointF(25, 25), timestamp++); + QVERIFY(sequenceStartedSpy.wait()); + QCOMPARE(sequenceStartedSpy.count(), 2); + Test::touchUp(1, timestamp++); + QVERIFY(pointRemovedSpy.wait()); + QCOMPARE(pointRemovedSpy.count(), 1); +} + +} + +WAYLANDTEST_MAIN(KWin::LockScreenTest) +#include "lockscreen.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/maximize_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/maximize_test.cpp new file mode 100644 index 0000000000..d8ff110cc5 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/maximize_test.cpp @@ -0,0 +1,315 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "decorations/decorationbridge.h" +#include "decorations/settings.h" +#include "pointer_input.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include + +#include +#include +#include + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_maximized-0"); + +class TestMaximized : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testMaximizedPassedToDeco(); + void testInitiallyMaximizedBorderless(); + void testBorderlessMaximizedWindow(); + void testMaximizedGainFocusAndBeActivated(); +}; + +void TestMaximized::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); +} + +void TestMaximized::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::XdgDecorationV1)); + + workspace()->setActiveOutput(QPoint(640, 512)); + KWin::input()->pointer()->warp(QPoint(640, 512)); +} + +void TestMaximized::cleanup() +{ + Test::destroyWaylandConnection(); + + // adjust config + auto group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("BorderlessMaximizedWindows", false); + group.sync(); + Workspace::self()->slotReconfigure(); + QCOMPARE(options->borderlessMaximizedWindows(), false); +} + +void TestMaximized::testMaximizedPassedToDeco() +{ + // this test verifies that when a XdgShellClient gets maximized the Decoration receives the signal + + // Create the test window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + std::unique_ptr xdgDecoration(Test::createXdgToplevelDecorationV1(shellSurface.get())); + + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(window->isDecorated()); + + auto decoration = window->decoration(); + QVERIFY(decoration); + QCOMPARE(window->maximizeMode(), MaximizeMode::MaximizeRestore); + + // Wait for configure event that signals the window is active now. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + + // now maximize + QSignalSpy bordersChangedSpy(decoration, &KDecoration3::Decoration::bordersChanged); + QSignalSpy maximizedChangedSpy(decoration->window(), &KDecoration3::DecoratedWindow::maximizedChanged); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + + workspace()->slotWindowMaximize(); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024 - decoration->borderTop())); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + + // If no borders, there is only the initial geometry shape change, but none through border resizing. + QCOMPARE(frameGeometryChangedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(maximizedChangedSpy.count(), 1); + QCOMPARE(maximizedChangedSpy.last().first().toBool(), true); + + // now unmaximize again + workspace()->slotWindowMaximize(); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 4); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(100, 50)); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(100, 50), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 2); + QCOMPARE(window->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(maximizedChangedSpy.count(), 2); + QCOMPARE(maximizedChangedSpy.last().first().toBool(), false); + + // Destroy the test window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void TestMaximized::testInitiallyMaximizedBorderless() +{ + // This test verifies that a window created as maximized, will be maximized and without Border with BorderlessMaximizedWindows + + // adjust config + auto group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("BorderlessMaximizedWindows", true); + group.sync(); + Workspace::self()->slotReconfigure(); + QCOMPARE(options->borderlessMaximizedWindows(), true); + + // Create the test window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + std::unique_ptr decoration(Test::createXdgToplevelDecorationV1(shellSurface.get())); + + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + shellSurface->set_maximized(); + QSignalSpy decorationConfigureRequestedSpy(decoration.get(), &Test::XdgToplevelDecorationV1::configureRequested); + decoration->set_mode(Test::XdgToplevelDecorationV1::mode_server_side); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the initial configure event. + Test::XdgToplevel::States states; + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::blue); + QVERIFY(window); + QVERIFY(!window->isDecorated()); + QVERIFY(window->isActive()); + QVERIFY(window->isMaximizable()); + QCOMPARE(window->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); + QCOMPARE(decorationConfigureRequestedSpy.last().at(0).value(), + Test::XdgToplevelDecorationV1::mode_server_side); + + // Destroy the window. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} +void TestMaximized::testBorderlessMaximizedWindow() +{ + // This test verifies that a maximized window looses it's server-side + // decoration when the borderless maximized option is on. + + // Enable the borderless maximized windows option. + auto group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("BorderlessMaximizedWindows", true); + group.sync(); + Workspace::self()->slotReconfigure(); + QCOMPARE(options->borderlessMaximizedWindows(), true); + + // Create the test window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + std::unique_ptr decoration(Test::createXdgToplevelDecorationV1(shellSurface.get())); + + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QSignalSpy decorationConfigureRequestedSpy(decoration.get(), &Test::XdgToplevelDecorationV1::configureRequested); + decoration->set_mode(Test::XdgToplevelDecorationV1::mode_server_side); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the initial configure event. + Test::XdgToplevel::States states; + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(0, 0)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Map the window. + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(window->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(window->isDecorated(), true); + + // We should receive a configure event when the window becomes active. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Maximize the window. + const RectF maximizeRestoreGeometry = window->frameGeometry(); + workspace()->slotWindowMaximize(); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(1280, 1024), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); + QCOMPARE(window->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(window->isDecorated(), false); + + // Restore the window. + workspace()->slotWindowMaximize(); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 4); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(100, 50)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(100, 50), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), maximizeRestoreGeometry); + QCOMPARE(window->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(window->isDecorated(), true); + + // Destroy the window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void TestMaximized::testMaximizedGainFocusAndBeActivated() +{ + // This test verifies that a window will be raised and gain focus when it's maximized + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr xdgShellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr xdgShellSurface2(Test::createXdgToplevelSurface(surface2.get())); + auto window2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 50), Qt::blue); + + QVERIFY(!window->isActive()); + QVERIFY(window2->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{window, window2})); + + workspace()->performWindowOperation(window, Options::MaximizeOp); + + QVERIFY(window->isActive()); + QVERIFY(!window2->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{window2, window})); + + xdgShellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); + xdgShellSurface2.reset(); + QVERIFY(Test::waitForWindowClosed(window2)); +} + +WAYLANDTEST_MAIN(TestMaximized) +#include "maximize_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/mouseactions_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/mouseactions_test.cpp new file mode 100644 index 0000000000..8107c0e310 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/mouseactions_test.cpp @@ -0,0 +1,268 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2024 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "pointer_input.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_mouseactions-0"); + +class MouseActionsTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testMouseActivate(); + void testMouseActivateInteractiveMoveResize(); + void testMouseActivateAndRaise(); + void testMouseActivateRaiseOnReleaseAndPassClick(); + void testMouseActivateRaiseOnReleaseAndPassClickInteractiveMoveResize(); +}; + +void MouseActionsTest::initTestCase() +{ + qRegisterMetaType(); + + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + }); +} + +void MouseActionsTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); + + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); +} + +void MouseActionsTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void MouseActionsTest::testMouseActivate() +{ + // This test verifies that a window will not be raised with the MouseActivate command. + + options->setCommandWindow1(Options::MouseActivate); + + // Create two windows, window1 is covered by window2. + Test::XdgToplevelWindow window1; + QVERIFY(window1.show(QSize(100, 100))); + QVERIFY(window1.m_window->isActive()); + window1.m_window->move(QPoint(0, 0)); + + Test::XdgToplevelWindow window2; + QVERIFY(window2.show(QSize(100, 100))); + QVERIFY(window2.m_window->isActive()); + window2.m_window->move(QPoint(50, 50)); + + QCOMPARE(workspace()->stackingOrder(), (QList{window1.m_window, window2.m_window})); + + // Click left mouse button over the first window. + uint32_t time = 0; + Test::pointerMotion(QPoint(25, 25), time++); + Test::pointerButtonPressed(BTN_LEFT, time++); + QVERIFY(window1.m_window->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{window1.m_window, window2.m_window})); + + // Release the button. + Test::pointerButtonReleased(BTN_LEFT, time++); + QCOMPARE(workspace()->stackingOrder(), (QList{window1.m_window, window2.m_window})); +} + +void MouseActionsTest::testMouseActivateInteractiveMoveResize() +{ + // This test verifies that a window will not be accidentally raised after starting an interactive + // move resize operation while the MouseActivate command is assigned to LMB. + + options->setCommandWindow1(Options::MouseActivate); + + // Create two windows, window1 is covered by window2. + Test::XdgToplevelWindow window1; + QVERIFY(window1.show(QSize(100, 100))); + QVERIFY(window1.m_window->isActive()); + window1.m_window->move(QPoint(0, 0)); + + Test::XdgToplevelWindow window2; + QVERIFY(window2.show(QSize(100, 100))); + QVERIFY(window2.m_window->isActive()); + window2.m_window->move(QPoint(50, 50)); + + QCOMPARE(workspace()->stackingOrder(), (QList{window1.m_window, window2.m_window})); + + // Click left mouse button over the first window. + uint32_t time = 0; + Test::pointerMotion(QPoint(25, 25), time++); + Test::pointerButtonPressed(BTN_LEFT, time++); + QCOMPARE(workspace()->stackingOrder(), (QList{window1.m_window, window2.m_window})); + + // Start an interactive move operation, window1 should not be raised. + workspace()->slotWindowMove(); + QCOMPARE(workspace()->stackingOrder(), (QList{window1.m_window, window2.m_window})); + + // Finish the interactive move operation. + Test::pointerButtonReleased(BTN_LEFT, time++); + QCOMPARE(workspace()->stackingOrder(), (QList{window1.m_window, window2.m_window})); +} + +void MouseActionsTest::testMouseActivateAndRaise() +{ + // This test verifies that the MouseActivateAndRaise command works as expected. That is, the window + // gets immediately activated and raised on a button press. + + options->setCommandWindow1(Options::MouseActivateAndRaise); + + // Create two windows, window1 is covered by window2. + Test::XdgToplevelWindow window1; + QVERIFY(window1.show(QSize(100, 100))); + QVERIFY(window1.m_window->isActive()); + window1.m_window->move(QPoint(0, 0)); + + Test::XdgToplevelWindow window2; + QVERIFY(window2.show(QSize(100, 100))); + QVERIFY(window2.m_window->isActive()); + window2.m_window->move(QPoint(50, 50)); + + QCOMPARE(workspace()->stackingOrder(), (QList{window1.m_window, window2.m_window})); + + // Click left mouse button over the first window. + uint32_t time = 0; + Test::pointerMotion(QPoint(25, 25), time++); + Test::pointerButtonPressed(BTN_LEFT, time++); + QVERIFY(window1.m_window->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{window2.m_window, window1.m_window})); + + // Release the button. + Test::pointerButtonReleased(BTN_LEFT, time++); + QCOMPARE(workspace()->stackingOrder(), (QList{window2.m_window, window1.m_window})); +} + +void MouseActionsTest::testMouseActivateRaiseOnReleaseAndPassClick() +{ + // This test verifies that MouseActivateRaiseOnReleaseAndPassClick works as expected. In other + // words, the window gets activated imediately on a button press, but raised on the button release. + + options->setCommandWindow1(Options::MouseActivateRaiseOnReleaseAndPassClick); + + // Create two windows on the left screen. + auto surface1 = Test::createSurface(); + auto shellSurface1 = Test::createXdgToplevelSurface(surface1.get()); + Window *window1 = Test::renderAndWaitForShown(surface1.get(), QSize(100, 100), Qt::blue); + QVERIFY(window1); + QVERIFY(window1->isActive()); + + auto surface2 = Test::createSurface(); + auto shellSurface2 = Test::createXdgToplevelSurface(surface2.get()); + Window *window2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 100), Qt::blue); + QVERIFY(window2); + QVERIFY(window2->isActive()); + + window1->move(QPoint(200, 200)); + window2->move(QPoint(300, 300)); + + workspace()->activateWindow(window2); + workspace()->raiseWindow(window2); + QVERIFY(window2->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window2})); + + uint32_t time = 0; + + Test::pointerMotion(QPoint(250, 250), time++); + + // the window should be activated on press, and raised on release + Test::pointerButtonPressed(BTN_LEFT, time++); + QVERIFY(window1->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window2})); + Test::pointerButtonReleased(BTN_LEFT, time++); + QVERIFY(window1->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{window2, window1})); + + workspace()->activateWindow(window2); + workspace()->raiseWindow(window2); + + // the window should not be raised if the cursor's moved to a different window in between + Test::pointerMotion(QPoint(250, 250), time++); + Test::pointerButtonPressed(BTN_LEFT, time++); + QVERIFY(window1->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window2})); + Test::pointerMotion(QPoint(350, 350), time++); + Test::pointerButtonReleased(BTN_LEFT, time++); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window2})); + + workspace()->activateWindow(window2); + workspace()->raiseWindow(window2); + + // also check if the windows overlap: only the topmost one should be raised on button release + window1->move(QPoint(250, 250)); + Test::pointerMotion(QPoint(275, 275), time++); + Test::pointerButtonPressed(BTN_LEFT, time++); + QVERIFY(window1->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window2})); + Test::pointerMotion(QPoint(325, 325), time++); + Test::pointerButtonReleased(BTN_LEFT, time++); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window2})); +} + +void MouseActionsTest::testMouseActivateRaiseOnReleaseAndPassClickInteractiveMoveResize() +{ + // This test verifies that a window will be immediately raised if an interactive move resize + // operation is started while the left mouse button is being held. The window should be raised + // when the move resize operation starts not when the left mouse button is released. + + options->setCommandWindow1(Options::MouseActivateRaiseOnReleaseAndPassClick); + + // Create two windows, window1 is covered by window2. + Test::XdgToplevelWindow window1; + QVERIFY(window1.show(QSize(100, 100))); + QVERIFY(window1.m_window->isActive()); + window1.m_window->move(QPoint(0, 0)); + + Test::XdgToplevelWindow window2; + QVERIFY(window2.show(QSize(100, 100))); + QVERIFY(window2.m_window->isActive()); + window2.m_window->move(QPoint(50, 50)); + + QCOMPARE(workspace()->stackingOrder(), (QList{window1.m_window, window2.m_window})); + + // Click left mouse button over the first window. + uint32_t time = 0; + Test::pointerMotion(QPoint(25, 25), time++); + Test::pointerButtonPressed(BTN_LEFT, time++); + QCOMPARE(workspace()->stackingOrder(), (QList{window1.m_window, window2.m_window})); + + // Start an interactive move operation, window1 should be raised now. + workspace()->slotWindowMove(); + QCOMPARE(workspace()->stackingOrder(), (QList{window2.m_window, window1.m_window})); + + // Finish the interactive move operation. + Test::pointerButtonReleased(BTN_LEFT, time++); + QCOMPARE(workspace()->stackingOrder(), (QList{window2.m_window, window1.m_window})); +} +} + +WAYLANDTEST_MAIN(KWin::MouseActionsTest) +#include "mouseactions_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/move_resize_window_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/move_resize_window_test.cpp new file mode 100644 index 0000000000..090f9a80a0 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/move_resize_window_test.cpp @@ -0,0 +1,1261 @@ + +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "cursor.h" +#include "placement.h" +#include "pointer_input.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include +#include +#include + +#include + +Q_DECLARE_METATYPE(KWin::QuickTileMode) +Q_DECLARE_METATYPE(KWin::MaximizeMode) + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_quick_tiling-0"); + +class MoveResizeWindowTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testMove(); + void testResize(); + void testPackTo_data(); + void testPackTo(); + void testPackAgainstClient_data(); + void testPackAgainstClient(); + void testGrowShrink_data(); + void testGrowShrink(); + void testPointerMoveEnd_data(); + void testPointerMoveEnd(); + void testClientSideMove(); + void testResizeForVirtualKeyboard_data(); + void testResizeForVirtualKeyboard(); + void testResizeForVirtualKeyboardWithMaximize(); + void testResizeForVirtualKeyboardWithFullScreen(); + void testDestroyMoveClient(); + void testDestroyResizeClient(); + void testCancelInteractiveMoveResize_data(); + void testCancelInteractiveMoveResize(); + void testRestrictedMove_data(); + void testRestrictedMove(); + void testRestrictedMoveMultiMonitor_data(); + void testRestrictedMoveMultiMonitor(); + void testRestrictedResizeUp(); + void testRestrictedResizeRight(); + +private: + std::tuple, std::unique_ptr> showWindow(); + std::pair, std::unique_ptr> addPanel(const Rect &geometry, int anchor); + KWayland::Client::ConnectionThread *m_connection = nullptr; + KWayland::Client::Compositor *m_compositor = nullptr; +}; + +void MoveResizeWindowTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType("MaximizeMode"); + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->start(); + Test::setOutputConfig({Rect(0, 0, 1280, 1024)}); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 1); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); +} + +void MoveResizeWindowTest::init() +{ + Test::setOutputConfig({Rect(0, 0, 1280, 1024)}); + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::LayerShellV1 | Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::XdgDecorationV1)); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 1); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QVERIFY(Test::waitForWaylandPointer()); + m_connection = Test::waylandConnection(); + m_compositor = Test::waylandCompositor(); + + workspace()->setActiveOutput(QPoint(640, 512)); +} + +void MoveResizeWindowTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void MoveResizeWindowTest::testMove() +{ + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + // let's render + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + QVERIFY(window); + QCOMPARE(workspace()->activeWindow(), window); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 100, 50)); + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy moveResizedChangedSpy(window, &Window::moveResizedChanged); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + + // begin move + QVERIFY(workspace()->moveResizeWindow() == nullptr); + QCOMPARE(window->isInteractiveMove(), false); + workspace()->slotWindowMove(); + QCOMPARE(workspace()->moveResizeWindow(), window); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QCOMPARE(moveResizedChangedSpy.count(), 1); + QCOMPARE(window->isInteractiveMove(), true); + QCOMPARE(window->geometryRestore(), RectF()); + + // send some key events, not going through input redirection + const QPointF cursorPos = Cursors::self()->mouse()->pos(); + window->keyPressEvent(Qt::Key_Right); + window->updateInteractiveMoveResize(Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QEXPECT_FAIL("", "First event is ignored", Continue); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + interactiveMoveResizeSteppedSpy.clear(); + + window->keyPressEvent(Qt::Key_Right); + window->updateInteractiveMoveResize(Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(16, 0)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + + window->keyPressEvent(Qt::Key_Down | Qt::ALT); + window->updateInteractiveMoveResize(Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 2); + QCOMPARE(window->frameGeometry(), RectF(16, 32, 100, 50)); + QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(16, 32)); + + // let's end + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 0); + window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QCOMPARE(moveResizedChangedSpy.count(), 2); + QCOMPARE(window->frameGeometry(), RectF(16, 32, 100, 50)); + QCOMPARE(window->isInteractiveMove(), false); + QVERIFY(workspace()->moveResizeWindow() == nullptr); + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void MoveResizeWindowTest::testResize() +{ + // a test case which manually resizes a window + + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + QVERIFY(shellSurface != nullptr); + + // Wait for the initial configure event. + Test::XdgToplevel::States states; + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 1); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Resizing)); + + // Let's render. + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + // We have to receive a configure event when the client becomes active. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 2); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Resizing)); + + QVERIFY(window); + QCOMPARE(workspace()->activeWindow(), window); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 100, 50)); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy moveResizedChangedSpy(window, &Window::moveResizedChanged); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + + // begin resize + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QCOMPARE(window->isInteractiveMove(), false); + QCOMPARE(window->isInteractiveResize(), false); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeWindow(), window); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QCOMPARE(moveResizedChangedSpy.count(), 1); + QCOMPARE(window->isInteractiveResize(), true); + QCOMPARE(window->geometryRestore(), RectF()); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 3); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing)); + + // Trigger a change. + const QPointF cursorPos = Cursors::self()->mouse()->pos(); + window->keyPressEvent(Qt::Key_Right); + window->updateInteractiveMoveResize(Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + + // The client should receive a configure event with the new size. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 4); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 4); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing)); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(108, 50)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + + // Now render new size. + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(108, 50), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 108, 50)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + + // Go down. + window->keyPressEvent(Qt::Key_Down); + window->updateInteractiveMoveResize(Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 8)); + + // The client should receive another configure event. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 5); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 5); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing)); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(108, 58)); + + // Now render new size. + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(108, 58), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 108, 58)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 2); + + // Let's finalize the resize operation. + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 0); + window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QCOMPARE(moveResizedChangedSpy.count(), 2); + QCOMPARE(window->isInteractiveResize(), false); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 6); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 6); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Resizing)); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void MoveResizeWindowTest::testPackTo_data() +{ + QTest::addColumn("methodCall"); + QTest::addColumn("expectedGeometry"); + + QTest::newRow("left") << QStringLiteral("slotWindowMoveLeft") << RectF(0, 487, 100, 50); + QTest::newRow("up") << QStringLiteral("slotWindowMoveUp") << RectF(590, 0, 100, 50); + QTest::newRow("right") << QStringLiteral("slotWindowMoveRight") << RectF(1180, 487, 100, 50); + QTest::newRow("down") << QStringLiteral("slotWindowMoveDown") << RectF(590, 974, 100, 50); +} + +void MoveResizeWindowTest::testPackTo() +{ + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + // let's render + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + QVERIFY(window); + QCOMPARE(workspace()->activeWindow(), window); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 100, 50)); + + // let's place it centered + window->place(*workspace()->placement()->placeCentered(window, RectF(0, 0, 1280, 1024))); + QCOMPARE(window->frameGeometry(), RectF(590, 487, 100, 50)); + + QFETCH(QString, methodCall); + QMetaObject::invokeMethod(workspace(), methodCall.toLocal8Bit().constData()); + QTEST(window->frameGeometry(), "expectedGeometry"); + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void MoveResizeWindowTest::testPackAgainstClient_data() +{ + QTest::addColumn("methodCall"); + QTest::addColumn("expectedGeometry"); + + QTest::newRow("left") << QStringLiteral("slotWindowMoveLeft") << RectF(10, 487, 100, 50); + QTest::newRow("up") << QStringLiteral("slotWindowMoveUp") << RectF(590, 10, 100, 50); + QTest::newRow("right") << QStringLiteral("slotWindowMoveRight") << RectF(1170, 487, 100, 50); + QTest::newRow("down") << QStringLiteral("slotWindowMoveDown") << RectF(590, 964, 100, 50); +} + +void MoveResizeWindowTest::testPackAgainstClient() +{ + std::unique_ptr surface1(Test::createSurface()); + QVERIFY(surface1 != nullptr); + std::unique_ptr surface2(Test::createSurface()); + QVERIFY(surface2 != nullptr); + std::unique_ptr surface3(Test::createSurface()); + QVERIFY(surface3 != nullptr); + std::unique_ptr surface4(Test::createSurface()); + QVERIFY(surface4 != nullptr); + + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + QVERIFY(shellSurface1 != nullptr); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + QVERIFY(shellSurface2 != nullptr); + std::unique_ptr shellSurface3(Test::createXdgToplevelSurface(surface3.get())); + QVERIFY(shellSurface3 != nullptr); + std::unique_ptr shellSurface4(Test::createXdgToplevelSurface(surface4.get())); + QVERIFY(shellSurface4 != nullptr); + auto renderWindow = [](KWayland::Client::Surface *surface, const QString &methodCall, const RectF &expectedGeometry) { + // let's render + auto window = Test::renderAndWaitForShown(surface, QSize(10, 10), Qt::blue); + + QVERIFY(window); + QCOMPARE(workspace()->activeWindow(), window); + QCOMPARE(window->frameGeometry().size(), QSize(10, 10)); + // let's place it centered + window->place(*workspace()->placement()->placeCentered(window, RectF(0, 0, 1280, 1024))); + QCOMPARE(window->frameGeometry(), RectF(635, 507, 10, 10)); + QMetaObject::invokeMethod(workspace(), methodCall.toLocal8Bit().constData()); + QCOMPARE(window->frameGeometry(), expectedGeometry); + }; + renderWindow(surface1.get(), QStringLiteral("slotWindowMoveLeft"), RectF(0, 507, 10, 10)); + renderWindow(surface2.get(), QStringLiteral("slotWindowMoveUp"), RectF(635, 0, 10, 10)); + renderWindow(surface3.get(), QStringLiteral("slotWindowMoveRight"), RectF(1270, 507, 10, 10)); + renderWindow(surface4.get(), QStringLiteral("slotWindowMoveDown"), RectF(635, 1014, 10, 10)); + + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + QVERIFY(window); + QCOMPARE(workspace()->activeWindow(), window); + // let's place it centered + window->place(*workspace()->placement()->placeCentered(window, RectF(0, 0, 1280, 1024))); + QCOMPARE(window->frameGeometry(), RectF(590, 487, 100, 50)); + + QFETCH(QString, methodCall); + QMetaObject::invokeMethod(workspace(), methodCall.toLocal8Bit().constData()); + QTEST(window->frameGeometry(), "expectedGeometry"); +} + +void MoveResizeWindowTest::testGrowShrink_data() +{ + QTest::addColumn("methodCall"); + QTest::addColumn("expectedGeometry"); + + QTest::newRow("grow vertical") << QStringLiteral("slotWindowExpandVertical") << RectF(590, 487, 100, 537); + QTest::newRow("grow horizontal") << QStringLiteral("slotWindowExpandHorizontal") << RectF(590, 487, 690, 50); + QTest::newRow("shrink vertical") << QStringLiteral("slotWindowShrinkVertical") << RectF(590, 487, 100, 23); + QTest::newRow("shrink horizontal") << QStringLiteral("slotWindowShrinkHorizontal") << RectF(590, 487, 40, 50); +} + +void MoveResizeWindowTest::testGrowShrink() +{ + // block geometry helper + std::unique_ptr surface1(Test::createSurface()); + QVERIFY(surface1 != nullptr); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + QVERIFY(shellSurface1 != nullptr); + Test::render(surface1.get(), QSize(650, 514), Qt::blue); + QVERIFY(Test::waitForWaylandWindowShown()); + workspace()->slotWindowMoveRight(); + workspace()->slotWindowMoveDown(); + + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + // let's render + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(toplevelConfigureRequestedSpy.wait()); + + QVERIFY(window); + QCOMPARE(workspace()->activeWindow(), window); + + // let's place it centered + window->place(*workspace()->placement()->placeCentered(window, RectF(0, 0, 1280, 1024))); + QCOMPARE(window->frameGeometry(), RectF(590, 487, 100, 50)); + + QFETCH(QString, methodCall); + QMetaObject::invokeMethod(workspace(), methodCall.toLocal8Bit().constData()); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().toSize(), Qt::red); + + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + m_connection->flush(); + QVERIFY(frameGeometryChangedSpy.wait()); + QTEST(window->frameGeometry(), "expectedGeometry"); +} + +void MoveResizeWindowTest::testPointerMoveEnd_data() +{ + QTest::addColumn("additionalButton"); + + QTest::newRow("BTN_RIGHT") << BTN_RIGHT; + QTest::newRow("BTN_MIDDLE") << BTN_MIDDLE; + QTest::newRow("BTN_SIDE") << BTN_SIDE; + QTest::newRow("BTN_EXTRA") << BTN_EXTRA; + QTest::newRow("BTN_FORWARD") << BTN_FORWARD; + QTest::newRow("BTN_BACK") << BTN_BACK; + QTest::newRow("BTN_TASK") << BTN_TASK; + for (int i = BTN_TASK + 1; i < BTN_JOYSTICK; i++) { + QTest::newRow(QByteArray::number(i, 16).constData()) << i; + } +} + +void MoveResizeWindowTest::testPointerMoveEnd() +{ + // this test verifies that moving a window through pointer only ends if all buttons are released + + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + // let's render + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + QVERIFY(window); + QCOMPARE(window, workspace()->activeWindow()); + QVERIFY(!window->isInteractiveMove()); + + // let's trigger the left button + quint32 timestamp = 1; + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + QVERIFY(!window->isInteractiveMove()); + workspace()->slotWindowMove(); + QVERIFY(window->isInteractiveMove()); + + // let's press another button + QFETCH(int, additionalButton); + Test::pointerButtonPressed(additionalButton, timestamp++); + QVERIFY(window->isInteractiveMove()); + + // release the left button, should still have the window moving + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + QVERIFY(window->isInteractiveMove()); + + // but releasing the other button should now end moving + Test::pointerButtonReleased(additionalButton, timestamp++); + QVERIFY(!window->isInteractiveMove()); + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} +void MoveResizeWindowTest::testClientSideMove() +{ + input()->pointer()->warp(QPointF(640, 512)); + std::unique_ptr pointer(Test::waylandSeat()->createPointer()); + QSignalSpy pointerEnteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); + QSignalSpy pointerLeftSpy(pointer.get(), &KWayland::Client::Pointer::left); + QSignalSpy buttonSpy(pointer.get(), &KWayland::Client::Pointer::buttonStateChanged); + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // move pointer into center of geometry + const RectF startGeometry = window->frameGeometry(); + input()->pointer()->warp(startGeometry.center()); + QVERIFY(pointerEnteredSpy.wait()); + QCOMPARE(pointerEnteredSpy.first().last().toPoint(), QPoint(50, 25)); + // simulate press + quint32 timestamp = 1; + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + QVERIFY(buttonSpy.wait()); + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + shellSurface->move(*Test::waylandSeat(), buttonSpy.first().first().value()); + QVERIFY(interactiveMoveResizeStartedSpy.wait()); + QCOMPARE(window->isInteractiveMove(), true); + QVERIFY(pointerLeftSpy.wait()); + + // move a bit + QSignalSpy clientMoveStepSpy(window, &Window::interactiveMoveResizeStepped); + const QPointF startPoint = startGeometry.center(); + const int dragDistance = QApplication::startDragDistance(); + // Why? + Test::pointerMotion(startPoint + QPoint(dragDistance, dragDistance) + QPoint(6, 6), timestamp++); + QCOMPARE(clientMoveStepSpy.count(), 1); + + // and release again + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + QVERIFY(pointerEnteredSpy.wait()); + QCOMPARE(window->isInteractiveMove(), false); + QCOMPARE(window->frameGeometry(), startGeometry.translated(QPoint(dragDistance, dragDistance) + QPoint(6, 6))); + QCOMPARE(pointerEnteredSpy.last().last().toPoint(), QPoint(50, 25)); +} + +void MoveResizeWindowTest::testResizeForVirtualKeyboard_data() +{ + QTest::addColumn("windowRect"); + QTest::addColumn("keyboardRect"); + QTest::addColumn("resizedWindowRect"); + + QTest::newRow("standard") << RectF(100, 300, 500, 800) << RectF(0, 100, 1280, 500) << RectF(100, 0, 500, 100); + QTest::newRow("same size") << RectF(100, 300, 500, 500) << RectF(0, 600, 1280, 400) << RectF(100, 100, 500, 500); + QTest::newRow("smaller width") << RectF(100, 300, 500, 800) << RectF(300, 100, 100, 500) << RectF(100, 0, 500, 100); + QTest::newRow("no height change") << RectF(100, 300, 500, 500) << RectF(0, 900, 1280, 124) << RectF(100, 300, 500, 500); + QTest::newRow("no width change") << RectF(100, 300, 500, 500) << RectF(0, 400, 100, 500) << RectF(100, 300, 500, 500); +} + +void MoveResizeWindowTest::testResizeForVirtualKeyboard() +{ + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + + QFETCH(RectF, windowRect); + QFETCH(RectF, keyboardRect); + QFETCH(RectF, resizedWindowRect); + + // There are three things that may happen when the virtual keyboard geometry + // is set: We move the window to the top and resize it, we move the window + // but don't change its size (if the window is already small enough) or we + // do not change anything because the virtual keyboard does not overlap the + // window. We should verify that, for the first, we get both a position and + // a size change, for the second we only get a position change and for the + // last we get no changes. + bool sizeChange = windowRect.size() != resizedWindowRect.size(); + bool positionChange = windowRect.topLeft() != resizedWindowRect.topLeft(); + + // let's render + auto window = Test::renderAndWaitForShown(surface.get(), windowRect.size().toSize(), Qt::blue); + QVERIFY(window); + + // The client should receive a configure event upon becoming active. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + surfaceConfigureRequestedSpy.clear(); + + window->move(windowRect.topLeft()); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + + QCOMPARE(window->frameGeometry(), windowRect); + window->setVirtualKeyboardGeometry(keyboardRect); + + if (sizeChange) { + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last()[0].toInt()); + } else { + QVERIFY(surfaceConfigureRequestedSpy.count() == 0); + } + // render at the new size + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().toSize(), Qt::blue); + + if (positionChange || sizeChange) { + QVERIFY(frameGeometryChangedSpy.count() > 0 || frameGeometryChangedSpy.wait()); + frameGeometryChangedSpy.clear(); + } else { + QVERIFY(frameGeometryChangedSpy.count() == 0); + } + + QCOMPARE(window->frameGeometry(), resizedWindowRect); + window->setVirtualKeyboardGeometry(RectF()); + + if (sizeChange) { + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last()[0].toInt()); + } + // render at the new size + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().toSize(), Qt::blue); + + if (positionChange || sizeChange) { + QVERIFY(frameGeometryChangedSpy.count() > 0 || frameGeometryChangedSpy.wait()); + } else { + QVERIFY(frameGeometryChangedSpy.count() == 0); + } + + QCOMPARE(window->frameGeometry(), windowRect); +} + +void MoveResizeWindowTest::testResizeForVirtualKeyboardWithMaximize() +{ + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + + // let's render + auto window = Test::renderAndWaitForShown(surface.get(), QSize(500, 800), Qt::blue); + QVERIFY(window); + + // The client should receive a configure event upon becoming active. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + window->move(QPoint(100, 300)); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + + QCOMPARE(window->frameGeometry(), RectF(100, 300, 500, 800)); + window->setVirtualKeyboardGeometry(RectF(0, 100, 1280, 500)); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last()[0].toInt()); + // render at the new size + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().toSize(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), RectF(100, 0, 500, 100)); + + window->setMaximize(true, true); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last()[0].toInt()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().toSize(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); + + window->setVirtualKeyboardGeometry(RectF()); + QVERIFY(!surfaceConfigureRequestedSpy.wait(10)); + + // render at the size of the configureRequested.. it won't have changed + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().toSize(), Qt::blue); + QVERIFY(!frameGeometryChangedSpy.wait(10)); + + // Size will NOT be restored + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); +} + +void MoveResizeWindowTest::testResizeForVirtualKeyboardWithFullScreen() +{ + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + + // let's render + auto window = Test::renderAndWaitForShown(surface.get(), QSize(500, 800), Qt::blue); + QVERIFY(window); + + // The client should receive a configure event upon becoming active. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + window->move(QPoint(100, 300)); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + + QCOMPARE(window->frameGeometry(), RectF(100, 300, 500, 800)); + window->setVirtualKeyboardGeometry(RectF(0, 100, 1280, 500)); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last()[0].toInt()); + // render at the new size + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().toSize(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), RectF(100, 0, 500, 100)); + + window->setFullScreen(true); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last()[0].toInt()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().toSize(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); + + window->setVirtualKeyboardGeometry(RectF()); + QVERIFY(!surfaceConfigureRequestedSpy.wait(10)); + + // render at the size of the configureRequested.. it won't have changed + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().toSize(), Qt::blue); + QVERIFY(!frameGeometryChangedSpy.wait(10)); + // Size will NOT be restored + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); +} + +void MoveResizeWindowTest::testDestroyMoveClient() +{ + // This test verifies that active move operation gets finished when + // the associated client is destroyed. + + // Create the test client. + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // Start moving the client. + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QCOMPARE(window->isInteractiveMove(), false); + QCOMPARE(window->isInteractiveResize(), false); + workspace()->slotWindowMove(); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeWindow(), window); + QCOMPARE(window->isInteractiveMove(), true); + QCOMPARE(window->isInteractiveResize(), false); + + // Let's pretend that the client crashed. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); +} + +void MoveResizeWindowTest::testDestroyResizeClient() +{ + // This test verifies that active resize operation gets finished when + // the associated client is destroyed. + + // Create the test client. + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // Start resizing the client. + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QCOMPARE(window->isInteractiveMove(), false); + QCOMPARE(window->isInteractiveResize(), false); + workspace()->slotWindowResize(); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeWindow(), window); + QCOMPARE(window->isInteractiveMove(), false); + QCOMPARE(window->isInteractiveResize(), true); + + // Let's pretend that the client crashed. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); +} + +void MoveResizeWindowTest::testCancelInteractiveMoveResize_data() +{ + QTest::addColumn("quickTileMode"); + QTest::addColumn("maximizeMode"); + + QTest::newRow("quicktile_bottom") << QuickTileMode(QuickTileFlag::Bottom) << MaximizeMode::MaximizeRestore; + QTest::newRow("quicktile_top") << QuickTileMode(QuickTileFlag::Top) << MaximizeMode::MaximizeRestore; + QTest::newRow("quicktile_left") << QuickTileMode(QuickTileFlag::Left) << MaximizeMode::MaximizeRestore; + QTest::newRow("quicktile_right") << QuickTileMode(QuickTileFlag::Right) << MaximizeMode::MaximizeRestore; + QTest::newRow("maximize_vertical") << QuickTileMode(QuickTileFlag::None) << MaximizeMode::MaximizeVertical; + QTest::newRow("maximize_horizontal") << QuickTileMode(QuickTileFlag::None) << MaximizeMode::MaximizeHorizontal; + QTest::newRow("maximize_full") << QuickTileMode(QuickTileFlag::None) << MaximizeMode::MaximizeFull; +} + +void MoveResizeWindowTest::testCancelInteractiveMoveResize() +{ + // This test verifies that after moveresize is cancelled, all relevant window states are restored + // to what they were before moveresize began + + // Create the test client. + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QSignalSpy frameGeomtryChangedSpy(window, &Window::frameGeometryChanged); + QSignalSpy quickTileChangedSpy(window, &Window::quickTileModeChanged); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + // tile / maximize window + QFETCH(QuickTileMode, quickTileMode); + QFETCH(MaximizeMode, maximizeMode); + if (maximizeMode) { + window->setMaximize(maximizeMode & MaximizeMode::MaximizeVertical, maximizeMode & MaximizeMode::MaximizeHorizontal); + } else { + window->setQuickTileModeAtCurrentPosition(quickTileMode); + } + QCOMPARE(window->requestedQuickTileMode(), quickTileMode); + QCOMPARE(window->requestedMaximizeMode(), maximizeMode); + + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().toSize(), Qt::blue); + QVERIFY(frameGeomtryChangedSpy.wait()); + QCOMPARE(window->quickTileMode(), quickTileMode); + const RectF geometry = window->moveResizeGeometry(); + const RectF geometryRestore = window->geometryRestore(); + + // Start resizing the client. + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QCOMPARE(window->isInteractiveMove(), false); + QCOMPARE(window->isInteractiveResize(), false); + workspace()->slotWindowResize(); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeWindow(), window); + QCOMPARE(window->isInteractiveMove(), false); + QCOMPARE(window->isInteractiveResize(), true); + + Test::pointerMotionRelative(QPoint(-10, -10), 1); + + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().toSize(), Qt::blue); + QVERIFY(frameGeomtryChangedSpy.wait()); + QCOMPARE(window->quickTileMode(), QuickTileMode()); + QCOMPARE(window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + + // cancel moveresize, all state from before should be restored + window->keyPressEvent(Qt::Key::Key_Escape); + + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().toSize(), Qt::blue); + QVERIFY(frameGeomtryChangedSpy.wait()); + QCOMPARE(window->moveResizeGeometry(), geometry); + QCOMPARE(window->quickTileMode(), quickTileMode); + QCOMPARE(window->requestedMaximizeMode(), maximizeMode); + QCOMPARE(window->geometryRestore(), geometryRestore); +} + +std::tuple, std::unique_ptr> MoveResizeWindowTest::showWindow() +{ +#define VERIFY(statement) \ + if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__)) \ + return {nullptr, nullptr, nullptr}; +#define COMPARE(actual, expected) \ + if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__)) \ + return {nullptr, nullptr, nullptr}; + + std::unique_ptr surface{Test::createSurface()}; + VERIFY(surface.get()); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly); + VERIFY(shellSurface.get()); + std::unique_ptr decoration = Test::createXdgToplevelDecorationV1(shellSurface.get()); + VERIFY(decoration.get()); + + QSignalSpy decorationConfigureRequestedSpy(decoration.get(), &Test::XdgToplevelDecorationV1::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + decoration->set_mode(Test::XdgToplevelDecorationV1::mode_server_side); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + VERIFY(surfaceConfigureRequestedSpy.wait()); + COMPARE(decorationConfigureRequestedSpy.last().at(0).value(), Test::XdgToplevelDecorationV1::mode_server_side); + + // let's render + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue); + VERIFY(window); + COMPARE(workspace()->activeWindow(), window); + +#undef VERIFY +#undef COMPARE + + return {window, std::move(surface), std::move(shellSurface)}; +} + +std::pair, std::unique_ptr> MoveResizeWindowTest::addPanel(const Rect &geometry, int anchor) +{ +#define VERIFY(statement) \ + if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__)) \ + return {nullptr, nullptr}; +#define COMPARE(actual, expected) \ + if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__)) \ + return {nullptr, nullptr}; + // Create a layer shell surface. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createLayerSurfaceV1(surface.get(), QStringLiteral("dock"))); + + // Set the initial state of the layer surface. + shellSurface->set_anchor(anchor); + shellSurface->set_size(geometry.width(), geometry.height()); + shellSurface->set_exclusive_zone(std::min(geometry.width(), geometry.height())); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to position the surface. + QSignalSpy configureRequestedSpy(shellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + VERIFY(configureRequestedSpy.wait()); + const QSize requestedSize = configureRequestedSpy.last().at(1).toSize(); + COMPARE(requestedSize, geometry.size()); + + // Map the layer surface. + shellSurface->ack_configure(configureRequestedSpy.last().at(0).toUInt()); + Window *panel = Test::renderAndWaitForShown(surface.get(), requestedSize, Qt::red); + VERIFY(panel); + VERIFY(panel->isDock()); + panel->move(geometry.topLeft()); + // Verify that the panel is placed at expected location. + COMPARE(panel->frameGeometry(), geometry); + +#undef VERIFY +#undef COMPARE + + return {std::move(surface), std::move(shellSurface)}; +} + +#define MOTION(target) Test::pointerMotion(target, timestamp++) + +#define PRESS Test::pointerButtonPressed(BTN_LEFT, timestamp++) + +#define RELEASE Test::pointerButtonReleased(BTN_LEFT, timestamp++) + +void MoveResizeWindowTest::testRestrictedMove_data() +{ + QTest::addColumn("hasStruts"); + QTest::addColumn("pointerMotion"); + QTest::addColumn("expectedTopLeft"); + QTest::addColumn("subtractTitlebar"); + QTest::addColumn("subtractLRBorders"); + + // window is initially at (500, 500, 100 + left & right borders, 100 + titleThickness) + QTest::newRow("push down") << false << QPoint(0, -501) << QPoint(500, 0) << false << false; + QTest::newRow("push up") << false << QPoint(0, 530) << QPoint(500, 1024) << true << false; + QTest::newRow("push left") << false << QPoint(700, 0) << QPoint(1180, 500) << false << false; + QTest::newRow("push right") << false << QPoint(-520, 0) << QPoint(0, 500) << false << true; + + // strut denoted by "*", border by "|" and "-" + // *----**********----* + // * * + // * * + // *----**********----* + QTest::newRow("push down with struts") << true << QPoint(0, -410) << QPoint(500, 100) << false << false; + QTest::newRow("push up with struts") << true << QPoint(200, 770) << QPoint(700, 924) << true << false; + QTest::newRow("push left with struts") << true << QPoint(600, 400) << QPoint(1080, 900) << false << false; + QTest::newRow("push right with struts") << true << QPoint(-410, 0) << QPoint(100, 500) << false << true; +} + +void MoveResizeWindowTest::testRestrictedMove() +{ + QFETCH(bool, hasStruts); + std::vector> surfaces; + std::vector> shellSurfaces; + const auto add = [this, &surfaces, &shellSurfaces](const Rect &geometry, int anchor) { + auto [surface, shellSurface] = addPanel(geometry, anchor); + QVERIFY(surface); + QVERIFY(shellSurface); + surfaces.push_back(std::move(surface)); + shellSurfaces.push_back(std::move(shellSurface)); + }; + if (hasStruts) { + add(Rect(320, 0, 640, 100), Test::LayerSurfaceV1::anchor_top); + add(Rect(320, 924, 640, 100), Test::LayerSurfaceV1::anchor_bottom); + add(Rect(0, 0, 100, 1024), Test::LayerSurfaceV1::anchor_left); + add(Rect(1180, 0, 100, 1024), Test::LayerSurfaceV1::anchor_right); + } + + auto [window, surface, shellSurface] = showWindow(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + QVERIFY(!window->noBorder()); + QCOMPARE(window->titlebarPosition(), Qt::TopEdge); + QVERIFY(surface); + + QCOMPARE(workspace()->activeWindow(), window); + RectF decorationLeft, decorationRight, decorationTop, decorationBottom; + window->layoutDecorationRects(decorationLeft, decorationTop, decorationRight, decorationBottom); + QVERIFY(!decorationTop.isEmpty()); + const auto titleThickness = decorationTop.height(); + + // move to center + window->move(QPoint(500, 500)); + QCOMPARE(window->frameGeometry().topLeft(), QPoint(500, 500)); + + // move to center of titlebar + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + quint32 timestamp = 1; + MOTION(QPoint(window->frameGeometry().center().x(), window->frameGeometry().y() + window->frameMargins().top() - 2)); + PRESS; + QVERIFY(!window->isInteractiveMove()); + QFETCH(QPoint, pointerMotion); + MOTION(Cursors::self()->mouse()->pos() + pointerMotion); + QVERIFY(window->isInteractiveMove()); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + RELEASE; + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QFETCH(QPoint, expectedTopLeft); + QFETCH(bool, subtractTitlebar); + if (subtractTitlebar) { + expectedTopLeft.setY(expectedTopLeft.y() - titleThickness); + } + QFETCH(bool, subtractLRBorders); + if (subtractLRBorders) { + expectedTopLeft.setX(expectedTopLeft.x() - window->frameMargins().left() - window->frameMargins().right()); + } + QCOMPARE(window->frameGeometry().topLeft(), expectedTopLeft); + // let's end + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void MoveResizeWindowTest::testRestrictedMoveMultiMonitor_data() +{ + QTest::addColumn("initialPoint"); + QTest::addColumn("pointerMotion"); + QTest::addColumn("expectedTopLeft"); + QTest::addColumn("subtractTitlebar"); + QTest::addColumn("subtractLRBorders"); + + // Outputs: (each char represents 200x1000 pixels) + // + // |---| + // | | + // |---|| | + // |---|| | + // | | + // |---| + + QTest::newRow("push up") << QPoint(500, 1500) << QPoint(0, 510) << QPoint(500, 2000) << true << false; + QTest::newRow("push right") << QPoint(1500, 2500) << QPoint(-520, 0) << QPoint(1000, 2500) << false << true; + QTest::newRow("push down") << QPoint(500, 1500) << QPoint(0, -510) << QPoint(500, 1000) << false << false; +} + +void MoveResizeWindowTest::testRestrictedMoveMultiMonitor() +{ + // Outputs: (each char represents 200x1000 pixels) + // + // |---| + // | | + // |---|| | + // |---|| | + // | | + // |---| + + Test::setOutputConfig({Rect(0, 1000, 1000, 1000), Rect(1000, 0, 1000, 3000)}); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 1000, 1000, 1000)); + QCOMPARE(outputs[1]->geometry(), Rect(1000, 0, 1000, 3000)); + + auto [window, surface, shellSurface] = showWindow(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + QVERIFY(!window->noBorder()); + QCOMPARE(window->titlebarPosition(), Qt::TopEdge); + QVERIFY(surface); + + QCOMPARE(workspace()->activeWindow(), window); + RectF decorationLeft, decorationRight, decorationTop, decorationBottom; + window->layoutDecorationRects(decorationLeft, decorationTop, decorationRight, decorationBottom); + QVERIFY(!decorationTop.isEmpty()); + const auto titleThickness = decorationTop.height(); + + // move to center + QFETCH(QPoint, initialPoint); + window->move(initialPoint); + QCOMPARE(window->frameGeometry().topLeft(), initialPoint); + + // move to center of titlebar + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + quint32 timestamp = 1; + MOTION(QPoint(window->frameGeometry().center().x(), window->frameGeometry().y() + window->frameMargins().top() - 2)); + PRESS; + QVERIFY(!window->isInteractiveMove()); + QFETCH(QPoint, pointerMotion); + MOTION(Cursors::self()->mouse()->pos() + pointerMotion); + QVERIFY(window->isInteractiveMove()); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + RELEASE; + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QFETCH(QPoint, expectedTopLeft); + QFETCH(bool, subtractTitlebar); + if (subtractTitlebar) { + expectedTopLeft.setY(expectedTopLeft.y() - titleThickness); + } + QFETCH(bool, subtractLRBorders); + if (subtractLRBorders) { + expectedTopLeft.setX(expectedTopLeft.x() - window->frameMargins().left() - window->frameMargins().right()); + } + QCOMPARE(window->frameGeometry().topLeft(), expectedTopLeft); + // let's end + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void MoveResizeWindowTest::testRestrictedResizeUp() +{ + std::vector> surfaces; + std::vector> shellSurfaces; + const auto add = [this, &surfaces, &shellSurfaces](const Rect &geometry, int anchor) { + auto [surface, shellSurface] = addPanel(geometry, anchor); + QVERIFY(surface); + QVERIFY(shellSurface); + surfaces.push_back(std::move(surface)); + shellSurfaces.push_back(std::move(shellSurface)); + }; + add(Rect(320, 0, 640, 100), Test::LayerSurfaceV1::anchor_top); + add(Rect(320, 924, 640, 100), Test::LayerSurfaceV1::anchor_bottom); + add(Rect(0, 0, 100, 1024), Test::LayerSurfaceV1::anchor_left); + add(Rect(1180, 0, 100, 1024), Test::LayerSurfaceV1::anchor_right); + + auto [window, surface, shellSurface] = showWindow(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + QVERIFY(!window->noBorder()); + QCOMPARE(window->titlebarPosition(), Qt::TopEdge); + QVERIFY(surface); + + QCOMPARE(workspace()->activeWindow(), window); + RectF decorationLeft, decorationRight, decorationTop, decorationBottom; + window->layoutDecorationRects(decorationLeft, decorationTop, decorationRight, decorationBottom); + QVERIFY(!decorationTop.isEmpty()); + + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + + quint32 timestamp = 1; + + // strut denoted by "*", border by "|" and "-" + // *----**********----* + // * * + // * * + // *----**********----* + + // cannot resize up past strut + window->move(QPoint(320, 200)); + MOTION(QPoint(window->frameGeometry().center().x(), window->frameGeometry().top())); + PRESS; + QVERIFY(!window->isInteractiveResize()); + QVERIFY(interactiveMoveResizeStartedSpy.wait()); + QVERIFY(window->isInteractiveResize()); + MOTION(QPoint(window->frameGeometry().center().x(), window->frameGeometry().top() - 150)); + RELEASE; + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 2); + QSize toplevelSize = toplevelConfigureRequestedSpy.last().at(0).toSize(); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelSize, Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry().topLeft(), QPoint(320, 100)); + + // let's end + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void MoveResizeWindowTest::testRestrictedResizeRight() +{ + std::vector> surfaces; + std::vector> shellSurfaces; + const auto add = [this, &surfaces, &shellSurfaces](const Rect &geometry, int anchor) { + auto [surface, shellSurface] = addPanel(geometry, anchor); + QVERIFY(surface); + QVERIFY(shellSurface); + surfaces.push_back(std::move(surface)); + shellSurfaces.push_back(std::move(shellSurface)); + }; + add(Rect(320, 0, 640, 100), Test::LayerSurfaceV1::anchor_top); + add(Rect(320, 924, 640, 100), Test::LayerSurfaceV1::anchor_bottom); + add(Rect(0, 0, 100, 1024), Test::LayerSurfaceV1::anchor_left); + add(Rect(1180, 0, 100, 1024), Test::LayerSurfaceV1::anchor_right); + + auto [window, surface, shellSurface] = showWindow(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + QVERIFY(!window->noBorder()); + QCOMPARE(window->titlebarPosition(), Qt::TopEdge); + QVERIFY(surface); + + QCOMPARE(workspace()->activeWindow(), window); + RectF decorationLeft, decorationRight, decorationTop, decorationBottom; + window->layoutDecorationRects(decorationLeft, decorationTop, decorationRight, decorationBottom); + QVERIFY(!decorationTop.isEmpty()); + + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + + quint32 timestamp = 1; + + // strut denoted by "*", border by "|" and "-" + // *----**********----* + // * * + // * * + // *----**********----* + + // strut will push window to the right + window->move(QPoint(900, 100)); + MOTION(QPoint(window->frameGeometry().right() - 1, window->frameGeometry().top())); + PRESS; + QVERIFY(!window->isInteractiveResize()); + QVERIFY(interactiveMoveResizeStartedSpy.wait()); + QVERIFY(window->isInteractiveResize()); + MOTION(QPoint(window->frameGeometry().right() + 40, window->frameGeometry().top() - 50)); + RELEASE; + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 2); + auto toplevelSize = toplevelConfigureRequestedSpy.last().at(0).toSize(); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelSize, Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry().topRight(), QPoint(1060, 50)); + + // let's end + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} +} + +WAYLANDTEST_MAIN(KWin::MoveResizeWindowTest) +#include "move_resize_window_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/no_global_shortcuts_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/no_global_shortcuts_test.cpp new file mode 100644 index 0000000000..889171206a --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/no_global_shortcuts_test.cpp @@ -0,0 +1,203 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "input.h" +#include "keyboard_input.h" +#include "pointer_input.h" +#include "screenedge.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include + +#include +#include + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_no_global_shortcuts-0"); + +Q_DECLARE_METATYPE(KWin::ElectricBorder) + +/** + * This test verifies the NoGlobalShortcuts initialization flag + */ +class NoGlobalShortcutsTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testKGlobalAccel(); + void testPointerShortcut(); + void testAxisShortcut_data(); + void testAxisShortcut(); + void testScreenEdge(); +}; + +class Target : public QObject +{ + Q_OBJECT + +public: + Target(); + ~Target() override; + +public Q_SLOTS: + Q_SCRIPTABLE void shortcut(); + +Q_SIGNALS: + void shortcutTriggered(); +}; + +Target::Target() + : QObject() +{ +} + +Target::~Target() +{ +} + +void Target::shortcut() +{ + Q_EMIT shortcutTriggered(); +} + +void NoGlobalShortcutsTest::initTestCase() +{ + qRegisterMetaType("ElectricBorder"); + kwinApp()->setSupportsGlobalShortcuts(false); + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + qputenv("KWIN_XKB_DEFAULT_KEYMAP", "1"); + qputenv("XKB_DEFAULT_RULES", "evdev"); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void NoGlobalShortcutsTest::init() +{ + workspace()->setActiveOutput(QPoint(640, 512)); + KWin::input()->pointer()->warp(QPoint(640, 512)); +} + +void NoGlobalShortcutsTest::cleanup() +{ +} + +void NoGlobalShortcutsTest::testKGlobalAccel() +{ + std::unique_ptr action(new QAction(nullptr)); + action->setProperty("componentName", QStringLiteral("kwin")); + action->setObjectName(QStringLiteral("globalshortcuts-test-meta-shift-w")); + QSignalSpy triggeredSpy(action.get(), &QAction::triggered); + KGlobalAccel::self()->setShortcut(action.get(), QList{Qt::META | Qt::SHIFT | Qt::Key_W}, KGlobalAccel::NoAutoloading); + + // press meta+shift+w + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::MetaModifier); + Test::keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::ShiftModifier | Qt::MetaModifier); + Test::keyboardKeyPressed(KEY_W, timestamp++); + Test::keyboardKeyReleased(KEY_W, timestamp++); + + // release meta+shift + Test::keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + + QVERIFY(!triggeredSpy.wait(100)); + QCOMPARE(triggeredSpy.count(), 0); +} + +void NoGlobalShortcutsTest::testPointerShortcut() +{ + // based on LockScreenTest::testPointerShortcut + std::unique_ptr action(new QAction(nullptr)); + QSignalSpy actionSpy(action.get(), &QAction::triggered); + input()->registerPointerShortcut(Qt::MetaModifier, Qt::LeftButton, action.get()); + + // try to trigger the shortcut + quint32 timestamp = 1; + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + QCoreApplication::instance()->processEvents(); + QCOMPARE(actionSpy.count(), 0); + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + QCoreApplication::instance()->processEvents(); + QCOMPARE(actionSpy.count(), 0); +} + +void NoGlobalShortcutsTest::testAxisShortcut_data() +{ + QTest::addColumn("direction"); + QTest::addColumn("sign"); + + QTest::newRow("up") << Qt::Vertical << -1; + QTest::newRow("down") << Qt::Vertical << 1; + QTest::newRow("left") << Qt::Horizontal << -1; + QTest::newRow("right") << Qt::Horizontal << 1; +} + +void NoGlobalShortcutsTest::testAxisShortcut() +{ + // based on LockScreenTest::testAxisShortcut + std::unique_ptr action(new QAction(nullptr)); + QSignalSpy actionSpy(action.get(), &QAction::triggered); + QFETCH(Qt::Orientation, direction); + QFETCH(int, sign); + PointerAxisDirection axisDirection = PointerAxisUp; + if (direction == Qt::Vertical) { + axisDirection = sign < 0 ? PointerAxisUp : PointerAxisDown; + } else { + axisDirection = sign < 0 ? PointerAxisLeft : PointerAxisRight; + } + input()->registerAxisShortcut(Qt::MetaModifier, axisDirection, action.get()); + + // try to trigger the shortcut + quint32 timestamp = 1; + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + if (direction == Qt::Vertical) { + Test::pointerAxisVertical(sign * 15.0, timestamp++); + } else { + Test::pointerAxisHorizontal(sign * 15.0, timestamp++); + } + QCoreApplication::instance()->processEvents(); + QCOMPARE(actionSpy.count(), 0); + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + QCoreApplication::instance()->processEvents(); + QCOMPARE(actionSpy.count(), 0); +} + +void NoGlobalShortcutsTest::testScreenEdge() +{ + // based on LockScreenTest::testScreenEdge + QSignalSpy screenEdgeSpy(workspace()->screenEdges(), &ScreenEdges::approaching); + QCOMPARE(screenEdgeSpy.count(), 0); + + quint32 timestamp = 1; + Test::pointerMotion({5, 5}, timestamp++); + QCOMPARE(screenEdgeSpy.count(), 0); +} + +WAYLANDTEST_MAIN(NoGlobalShortcutsTest) +#include "no_global_shortcuts_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/outputchanges_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/outputchanges_test.cpp new file mode 100644 index 0000000000..1ff669b3fa --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/outputchanges_test.cpp @@ -0,0 +1,2441 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "backends/virtual/virtual_backend.h" +#include "core/output.h" +#include "core/outputbackend.h" +#include "core/outputconfiguration.h" +#include "outputconfigurationstore.h" +#include "pointer_input.h" +#include "tiles/tilemanager.h" +#include "utils/orientationsensor.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include + +#if KWIN_BUILD_X11 +#include "x11window.h" + +#include +#include +#endif + +#include + +using namespace std::chrono_literals; + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_output_changes-0"); + +enum class DeviceType { + Desktop, + Laptop, + Phone, +}; + +class LidSwitch : public InputDevice +{ + Q_OBJECT + +public: + explicit LidSwitch(QObject *parent = nullptr) + : InputDevice(parent) + { + } + + QString name() const override + { + return QStringLiteral("lid switch"); + } + + bool isEnabled() const override + { + return true; + } + void setEnabled(bool enabled) override + { + } + + bool isKeyboard() const override + { + return false; + } + bool isPointer() const override + { + return false; + } + bool isTouchpad() const override + { + return false; + } + bool isTouch() const override + { + return false; + } + bool isTabletTool() const override + { + return false; + } + bool isTabletPad() const override + { + return false; + } + bool isTabletModeSwitch() const override + { + return false; + } + bool isLidSwitch() const override + { + return true; + } +}; + +class OutputChangesTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testWindowSticksToOutputAfterOutputIsDisabled(); + void testWindowSticksToOutputAfterAnotherOutputIsDisabled(); + void testWindowSticksToOutputAfterOutputIsMoved_data(); + void testWindowSticksToOutputAfterOutputIsMoved(); + void testWindowSticksToOutputAfterOutputsAreSwappedLeftToRight(); + void testWindowSticksToOutputAfterOutputsAreSwappedRightToLeft(); + + void testWindowRestoredAfterEnablingOutput(); + void testMaximizedWindowRestoredAfterEnablingOutput(); + void testFullScreenWindowRestoredAfterEnablingOutput(); + void testQuickTiledWindowRestoredAfterEnablingOutput(); + void testQuickTileUntileWindowRestoredAfterEnablingOutput(); + void testCustomTiledWindowRestoredAfterEnablingOutput_data(); + void testCustomTiledWindowRestoredAfterEnablingOutput(); + void testWindowRestoredAfterChangingScale(); + void testMaximizeStateRestoredAfterEnablingOutput_data(); + void testMaximizeStateRestoredAfterEnablingOutput(); + void testInvalidGeometryRestoreAfterEnablingOutput(); + void testMaximizedWindowDoesntDisappear_data(); + void testMaximizedWindowDoesntDisappear(); + void testXwaylandScaleChange(); + + void testWindowNotRestoredAfterMovingWindowAndEnablingOutput(); + void testLaptopLidClosed(); + void testGenerateConfigs_data(); + void testGenerateConfigs(); + void testGeneratePartialConfigs(); + void testAutorotate_data(); + void testAutorotate(); + void testSettingRestoration_data(); + void testSettingRestoration(); + void testSettingRestoration_initialParsingFailure(); + void testSettingRestoration_replacedMode(); + void testCursorRestoration(); + + void testEvacuateTiledWindowFromRemovedOutput_data(); + void testEvacuateTiledWindowFromRemovedOutput(); + + void testMirroring_data(); + void testMirroring(); + + void testAutoBrightness(); + void testTemporaryDpmsHotplug(); +}; + +void OutputChangesTest::initTestCase() +{ + qRegisterMetaType(); + + QVERIFY(waylandServer()->init(s_socketName)); + + // delete the previous output config, to avoid previous runs messing with this one + // TODO reset it per test function instead? + QFile(QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("kwinoutputconfig.json"))).remove(); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); +} + +void OutputChangesTest::init() +{ + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + QVERIFY(Test::setupWaylandConnection()); + + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); +} + +void OutputChangesTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void OutputChangesTest::testWindowSticksToOutputAfterOutputIsDisabled() +{ + auto outputs = kwinApp()->outputBackend()->outputs(); + + // Create a window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // Move the window to some predefined position so the test is more robust. + window->move(QPoint(42, 67)); + QCOMPARE(window->frameGeometry(), RectF(42, 67, 100, 50)); + + // Disable the output where the window is on. + OutputConfiguration config; + { + auto changeSet = config.changeSet(outputs[0]); + changeSet->enabled = false; + } + workspace()->applyOutputConfiguration(config); + + // The window will be sent to the second output, which is at (1280, 0). + QCOMPARE(window->frameGeometry(), RectF(1280 + 42, 0 + 67, 100, 50)); +} + +void OutputChangesTest::testWindowSticksToOutputAfterAnotherOutputIsDisabled() +{ + auto outputs = kwinApp()->outputBackend()->outputs(); + + // Create a window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // Move the window to the second output. + window->move(QPoint(1280 + 42, 67)); + QCOMPARE(window->frameGeometry(), RectF(1280 + 42, 67, 100, 50)); + + // Disable the first output. + OutputConfiguration config; + { + auto changeSet = config.changeSet(outputs[0]); + changeSet->enabled = false; + } + { + auto changeSet = config.changeSet(outputs[1]); + changeSet->pos = QPoint(0, 0); + changeSet->enabled = true; + } + workspace()->applyOutputConfiguration(config); + + QCOMPARE(workspace()->outputs().front()->geometry(), Rect(0, 0, 1280, 1024)); + + // The position of the window relative to its output should remain the same. + QCOMPARE(window->frameGeometry(), RectF(42, 67, 100, 50)); +} + +void OutputChangesTest::testWindowSticksToOutputAfterOutputIsMoved_data() +{ + QTest::addColumn("tileMode"); + + QTest::addRow("Not tiled") << QuickTileFlag::None; + QTest::addRow("Quick Left") << QuickTileFlag::Left; + QTest::addRow("Quick Right") << QuickTileFlag::Right; + QTest::addRow("Quick Top") << QuickTileFlag::Top; + QTest::addRow("Quick Bottom") << QuickTileFlag::Bottom; + QTest::addRow("Custom") << QuickTileFlag::Custom; +} + +void OutputChangesTest::testWindowSticksToOutputAfterOutputIsMoved() +{ + auto outputs = kwinApp()->outputBackend()->outputs(); + + Test::XdgToplevelWindow window; + QVERIFY(window.show(QSize(100, 50))); + QFETCH(QuickTileFlag, tileMode); + + { + OutputConfiguration config; + config.changeSet(outputs[0])->pos = QPoint(20, 20); + workspace()->applyOutputConfiguration(config); + } + + // Move the window to some predefined position so the test is more robust. + window.m_window->move(QPoint(42, 67)); + if (tileMode != QuickTileFlag::None) { + // this ensures any configure events up to this point were received + QVERIFY(Test::waylandSync()); + // now tile it + window.m_window->setQuickTileModeAtCurrentPosition(tileMode); + QVERIFY(window.handleConfigure()); + } else { + QCOMPARE(window.m_window->frameGeometry(), RectF(42, 67, 100, 50)); + } + const RectF oldGeometry = window.m_window->frameGeometry(); + + // move the first output + { + OutputConfiguration config; + config.changeSet(outputs[0])->pos = QPoint(0, 40); + workspace()->applyOutputConfiguration(config); + } + + // The position of the window relative to its output should remain the same. + QCOMPARE(window.m_window->frameGeometry(), oldGeometry.translated(-20, 20)); +} + +void OutputChangesTest::testWindowSticksToOutputAfterOutputsAreSwappedLeftToRight() +{ + // This test verifies that a window placed on the left monitor sticks + // to that monitor even after the monitors are swapped horizontally. + + const auto outputs = kwinApp()->outputBackend()->outputs(); + + // Create a window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // Move the window to the left output. + window->move(QPointF(0, 0)); + QCOMPARE(window->output()->backendOutput(), outputs[0]); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 100, 50)); + + // Swap outputs. + OutputConfiguration config; + { + auto changeSet1 = config.changeSet(outputs[0]); + changeSet1->pos = QPoint(1280, 0); + auto changeSet2 = config.changeSet(outputs[1]); + changeSet2->pos = QPoint(0, 0); + } + workspace()->applyOutputConfiguration(config); + + // The window should be still on its original output. + QCOMPARE(window->output()->backendOutput(), outputs[0]); + QCOMPARE(window->frameGeometry(), RectF(1280, 0, 100, 50)); +} + +void OutputChangesTest::testWindowSticksToOutputAfterOutputsAreSwappedRightToLeft() +{ + // This test verifies that a window placed on the right monitor sticks + // to that monitor even after the monitors are swapped horizontally. + + const auto outputs = kwinApp()->outputBackend()->outputs(); + + // Create a window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // Move the window to the right output. + window->move(QPointF(1280, 0)); + QCOMPARE(window->output()->backendOutput(), outputs[1]); + QCOMPARE(window->frameGeometry(), RectF(1280, 0, 100, 50)); + + // Swap outputs. + OutputConfiguration config; + { + auto changeSet1 = config.changeSet(outputs[0]); + changeSet1->pos = QPoint(1280, 0); + auto changeSet2 = config.changeSet(outputs[1]); + changeSet2->pos = QPoint(0, 0); + } + workspace()->applyOutputConfiguration(config); + + // The window should be still on its original output. + QCOMPARE(window->output()->backendOutput(), outputs[1]); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 100, 50)); +} + +void OutputChangesTest::testWindowRestoredAfterEnablingOutput() +{ + // This test verifies that a window will be moved back to its original output when it's hotplugged. + + const auto outputs = kwinApp()->outputBackend()->outputs(); + + // Create a window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // Move the window to the right output. + window->move(QPointF(1280 + 50, 100)); + QCOMPARE(window->output()->backendOutput(), outputs[1]); + QCOMPARE(window->frameGeometry(), RectF(1280 + 50, 100, 100, 50)); + + // Disable the right output. + OutputConfiguration config1; + { + auto changeSet = config1.changeSet(outputs[1]); + changeSet->enabled = false; + } + workspace()->applyOutputConfiguration(config1); + + // The window will be moved to the left monitor. + QCOMPARE(window->output()->backendOutput(), outputs[0]); + QCOMPARE(window->frameGeometry(), RectF(50, 100, 100, 50)); + + // Enable the right monitor. + OutputConfiguration config2; + { + auto changeSet = config2.changeSet(outputs[1]); + changeSet->enabled = true; + } + workspace()->applyOutputConfiguration(config2); + + // The window will be moved back to the right monitor. + QCOMPARE(window->output()->backendOutput(), outputs[1]); + QCOMPARE(window->frameGeometry(), RectF(1280 + 50, 100, 100, 50)); +} + +void OutputChangesTest::testWindowNotRestoredAfterMovingWindowAndEnablingOutput() +{ + // This test verifies that a window won't be moved to its original output when it's + // hotplugged because the window was moved manually by the user. + + const auto outputs = kwinApp()->outputBackend()->outputs(); + + // Create a window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // Move the window to the right output. + window->move(QPointF(1280 + 50, 100)); + QCOMPARE(window->output()->backendOutput(), outputs[1]); + QCOMPARE(window->frameGeometry(), RectF(1280 + 50, 100, 100, 50)); + + // Disable the right output. + OutputConfiguration config1; + { + auto changeSet = config1.changeSet(outputs[1]); + changeSet->enabled = false; + } + workspace()->applyOutputConfiguration(config1); + + // The window will be moved to the left monitor. + QCOMPARE(window->output()->backendOutput(), outputs[0]); + QCOMPARE(window->frameGeometry(), RectF(50, 100, 100, 50)); + + // Pretend that the user moved the window. + workspace()->slotWindowMove(); + QVERIFY(window->isInteractiveMove()); + window->keyPressEvent(Qt::Key_Right); + window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(window->frameGeometry(), RectF(58, 100, 100, 50)); + + // Enable the right monitor. + OutputConfiguration config2; + { + auto changeSet = config2.changeSet(outputs[1]); + changeSet->enabled = true; + } + workspace()->applyOutputConfiguration(config2); + + // The window is still on the left monitor because user manually moved it. + QCOMPARE(window->output()->backendOutput(), outputs[0]); + QCOMPARE(window->frameGeometry(), RectF(58, 100, 100, 50)); +} + +void OutputChangesTest::testMaximizedWindowRestoredAfterEnablingOutput() +{ + // This test verifies that a maximized window will be moved to its original + // output when it's re-enabled. + + const auto outputs = kwinApp()->outputBackend()->outputs(); + + // Create a window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // kwin will send a configure event with the activated state. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + // Move the window to the right monitor and make it maximized. + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + window->move(QPointF(1280 + 50, 100)); + window->maximize(MaximizeFull); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(1280, 1024)); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(1280, 1024), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), RectF(1280, 0, 1280, 1024)); + QCOMPARE(window->moveResizeGeometry(), RectF(1280, 0, 1280, 1024)); + QCOMPARE(window->output()->backendOutput(), outputs[1]); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + QCOMPARE(window->geometryRestore(), RectF(1280 + 50, 100, 100, 50)); + + // Disable the right output. + OutputConfiguration config1; + { + auto changeSet = config1.changeSet(outputs[1]); + changeSet->enabled = false; + } + workspace()->applyOutputConfiguration(config1); + + // The window will be moved to the left monitor, the geometry restore will be updated too. + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); + QCOMPARE(window->moveResizeGeometry(), RectF(0, 0, 1280, 1024)); + QCOMPARE(window->output()->backendOutput(), outputs[0]); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + QCOMPARE(window->geometryRestore(), RectF(50, 100, 100, 50)); + + // Enable the right monitor. + OutputConfiguration config2; + { + auto changeSet = config2.changeSet(outputs[1]); + changeSet->enabled = true; + } + workspace()->applyOutputConfiguration(config2); + + // The window will be moved back to the right monitor, the geometry restore will be updated too. + QCOMPARE(window->frameGeometry(), RectF(1280, 0, 1280, 1024)); + QCOMPARE(window->moveResizeGeometry(), RectF(1280, 0, 1280, 1024)); + QCOMPARE(window->output()->backendOutput(), outputs[1]); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + QCOMPARE(window->geometryRestore(), RectF(1280 + 50, 100, 100, 50)); +} + +void OutputChangesTest::testFullScreenWindowRestoredAfterEnablingOutput() +{ + // This test verifies that a fullscreen window will be moved to its original + // output when it's re-enabled. + + const auto outputs = kwinApp()->outputBackend()->outputs(); + + // Create a window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // kwin will send a configure event with the activated state. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + // Move the window to the right monitor and make it fullscreen. + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + window->move(QPointF(1280 + 50, 100)); + window->setFullScreen(true); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(1280, 1024)); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(1280, 1024), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), RectF(1280, 0, 1280, 1024)); + QCOMPARE(window->moveResizeGeometry(), RectF(1280, 0, 1280, 1024)); + QCOMPARE(window->output()->backendOutput(), outputs[1]); + QCOMPARE(window->isFullScreen(), true); + QCOMPARE(window->isRequestedFullScreen(), true); + QCOMPARE(window->fullscreenGeometryRestore(), RectF(1280 + 50, 100, 100, 50)); + + // Disable the right output. + OutputConfiguration config1; + { + auto changeSet = config1.changeSet(outputs[1]); + changeSet->enabled = false; + } + workspace()->applyOutputConfiguration(config1); + + // The window will be moved to the left monitor, the geometry restore will be updated too. + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); + QCOMPARE(window->moveResizeGeometry(), RectF(0, 0, 1280, 1024)); + QCOMPARE(window->output()->backendOutput(), outputs[0]); + QCOMPARE(window->isFullScreen(), true); + QCOMPARE(window->isRequestedFullScreen(), true); + QCOMPARE(window->fullscreenGeometryRestore(), RectF(50, 100, 100, 50)); + + // Enable the right monitor. + OutputConfiguration config2; + { + auto changeSet = config2.changeSet(outputs[1]); + changeSet->enabled = true; + } + workspace()->applyOutputConfiguration(config2); + + // The window will be moved back to the right monitor, the geometry restore will be updated too. + QCOMPARE(window->frameGeometry(), RectF(1280, 0, 1280, 1024)); + QCOMPARE(window->moveResizeGeometry(), RectF(1280, 0, 1280, 1024)); + QCOMPARE(window->output()->backendOutput(), outputs[1]); + QCOMPARE(window->isFullScreen(), true); + QCOMPARE(window->isRequestedFullScreen(), true); + QCOMPARE(window->fullscreenGeometryRestore(), RectF(1280 + 50, 100, 100, 50)); +} + +void OutputChangesTest::testQuickTiledWindowRestoredAfterEnablingOutput() +{ + // This test verifies that a quick tiled window will be moved to + // its original output and tile when the output is re-enabled + + const auto outputs = kwinApp()->outputBackend()->outputs(); + + // Create a window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // kwin will send a configure event with the activated state. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QSignalSpy tileChangedSpy(window, &Window::tileChanged); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + // Move the window to the right monitor and tile it to the right. + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + window->move(QPointF(1280 + 50, 100)); + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::Right); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(1280 / 2, 1024)); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(1280 / 2, 1024), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + const RectF rightQuickTileGeomScreen2 = RectF(1280 + 1280 / 2, 0, 1280 / 2, 1024); + const RectF rightQuickTileGeomScreen1 = RectF(1280 / 2, 0, 1280 / 2, 1024); + QCOMPARE(window->frameGeometry(), rightQuickTileGeomScreen2); + QCOMPARE(window->moveResizeGeometry(), rightQuickTileGeomScreen2); + QCOMPARE(window->output()->backendOutput(), outputs[1]); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->geometryRestore(), RectF(1280 + 50, 100, 100, 50)); + + // Disable the right output. + OutputConfiguration config1; + { + auto changeSet = config1.changeSet(outputs[1]); + changeSet->enabled = false; + } + + workspace()->applyOutputConfiguration(config1); + + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(1280 / 2, 1024)); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(1280 / 2, 1024), Qt::blue); + + // The window will be moved to the left monitor + QCOMPARE(window->output()->backendOutput(), outputs[0]); + QVERIFY(tileChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), rightQuickTileGeomScreen1); + QCOMPARE(window->moveResizeGeometry(), rightQuickTileGeomScreen1); + + // Enable the right monitor again + OutputConfiguration config2; + { + auto changeSet = config2.changeSet(outputs[1]); + changeSet->enabled = true; + } + workspace()->applyOutputConfiguration(config2); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(1280 / 2, 1024), Qt::blue); + QVERIFY(tileChangedSpy.wait()); + + // The window will be moved back to the right monitor, and put in the correct tile + QCOMPARE(window->frameGeometry(), rightQuickTileGeomScreen2); + QCOMPARE(window->moveResizeGeometry(), rightQuickTileGeomScreen2); + QCOMPARE(window->output()->backendOutput(), outputs[1]); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->geometryRestore(), RectF(1280 + 50, 100, 100, 50)); +} + +void OutputChangesTest::testQuickTileUntileWindowRestoredAfterEnablingOutput() +{ + // This test verifies that a quick tiled window will be moved to + // its original output and tile when the output is re-enabled + // even if the window changed quick tile state + const auto outputs = kwinApp()->outputBackend()->outputs(); + + // start with only one output + { + OutputConfiguration config; + auto changeSet = config.changeSet(outputs[1]); + changeSet->enabled = false; + workspace()->applyOutputConfiguration(config); + } + + // Create a window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // kwin will send a configure event with the current state. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(100, 50)); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(100, 50), Qt::blue); + + const RectF originalGeometry = window->frameGeometry(); + + // add a second output + { + OutputConfiguration config; + auto changeSet = config.changeSet(outputs[1]); + changeSet->enabled = true; + workspace()->applyOutputConfiguration(config); + } + + // Move the window to the second output and tile it to the right. + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + window->move(QPointF(1280 + 50, 100)); + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::Right); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(1280 / 2, 1024)); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(1280 / 2, 1024), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + const RectF rightQuickTileGeom = RectF(1280 + 1280 / 2, 0, 1280 / 2, 1024); + QCOMPARE(window->frameGeometry(), rightQuickTileGeom); + QCOMPARE(window->moveResizeGeometry(), rightQuickTileGeom); + QCOMPARE(window->output()->backendOutput(), outputs[1]); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->geometryRestore(), RectF(1280 + 50, 100, 100, 50)); + + // remove the second output again + { + OutputConfiguration config; + auto changeSet = config.changeSet(outputs[1]); + changeSet->enabled = false; + workspace()->applyOutputConfiguration(config); + } + + // the window should now be untiled, and put back in its original position + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(100, 50)); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), originalGeometry); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + + // add the second output again + { + OutputConfiguration config; + auto changeSet = config.changeSet(outputs[1]); + changeSet->enabled = true; + workspace()->applyOutputConfiguration(config); + } + + // the window should now be put back into the tile on the second output + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(1280 / 2, 1024)); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(1280 / 2, 1024), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), rightQuickTileGeom); + QCOMPARE(window->moveResizeGeometry(), rightQuickTileGeom); + QCOMPARE(window->output()->backendOutput(), outputs[1]); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->geometryRestore(), RectF(1280 + 50, 100, 100, 50)); +} + +void OutputChangesTest::testCustomTiledWindowRestoredAfterEnablingOutput_data() +{ + const auto outputs = workspace()->outputs(); + const size_t tileCount = workspace()->rootTile(outputs[1])->childTiles().size(); + + QTest::addColumn("tileIndex"); + for (size_t i = 0; i < tileCount; i++) { + QTest::addRow("tile %lu", i) << i; + } +} + +void OutputChangesTest::testCustomTiledWindowRestoredAfterEnablingOutput() +{ + // This test verifies that a custom tiled window will be moved to + // its original output and tile when the output is re-enabled + + const auto outputs = kwinApp()->outputBackend()->outputs(); + + // start with only one output + { + OutputConfiguration config; + auto changeSet = config.changeSet(outputs[1]); + changeSet->enabled = false; + workspace()->applyOutputConfiguration(config); + } + + // Create a window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + const auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // kwin will send a configure event with the activated state. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + const RectF originalGeometry = window->moveResizeGeometry(); + + // Enable the right output + { + OutputConfiguration config; + auto changeSet = config.changeSet(outputs[1]); + changeSet->enabled = true; + workspace()->applyOutputConfiguration(config); + } + + QFETCH(size_t, tileIndex); + const RectF customTileGeom = workspace()->rootTile(workspace()->findOutput(outputs[1]))->childTiles()[tileIndex]->windowGeometry(); + + // Move the window to the right monitor and put it in the middle tile. + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + window->move(customTileGeom.topLeft() + QPointF(50, 50)); + const auto geomBeforeTiling = window->moveResizeGeometry(); + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::Custom); + + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), customTileGeom.size().toSize()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), customTileGeom.size().toSize(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + + QCOMPARE(window->frameGeometry(), customTileGeom); + QCOMPARE(window->moveResizeGeometry(), customTileGeom); + QCOMPARE(window->output()->backendOutput(), outputs[1]); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Custom); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Custom); + QCOMPARE(window->geometryRestore(), geomBeforeTiling); + + // Disable the right output. + { + OutputConfiguration config; + auto changeSet = config.changeSet(outputs[1]); + changeSet->enabled = false; + workspace()->applyOutputConfiguration(config); + } + + // The window will be moved to the left monitor, and the original geometry restored + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), originalGeometry.size().toSize()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), originalGeometry.size().toSize(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + + QCOMPARE(window->frameGeometry(), originalGeometry); + QCOMPARE(window->moveResizeGeometry(), originalGeometry); + QCOMPARE(window->output()->backendOutput(), outputs[0]); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + + // Enable the right monitor again + { + OutputConfiguration config; + auto changeSet = config.changeSet(outputs[1]); + changeSet->enabled = true; + workspace()->applyOutputConfiguration(config); + } + + // The window will be moved back to the right monitor, and put in the correct tile + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), customTileGeom.size().toSize()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), customTileGeom.size().toSize(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + + QCOMPARE(window->frameGeometry(), customTileGeom); + QCOMPARE(window->moveResizeGeometry(), customTileGeom); + QCOMPARE(window->output()->backendOutput(), outputs[1]); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Custom); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Custom); + QCOMPARE(window->geometryRestore(), geomBeforeTiling); +} + +void OutputChangesTest::testWindowRestoredAfterChangingScale() +{ + // This test verifies that a window will be moved to its original position after changing the scale of an output + + const auto output = kwinApp()->outputBackend()->outputs().front(); + const auto logicalOutput = workspace()->findOutput(output); + + // Create a window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // Move the window to the bottom right + const QPointF originalPosition(logicalOutput->geometry().width() - window->width(), logicalOutput->geometry().height() - window->height()); + window->move(originalPosition); + QCOMPARE(window->pos(), originalPosition); + QCOMPARE(window->output()->backendOutput(), output); + + // change the scale of the output + OutputConfiguration config1; + { + auto changeSet = config1.changeSet(output); + changeSet->scaleSetting = 2; + } + workspace()->applyOutputConfiguration(config1); + + // The window will be moved to still be in the monitor + QCOMPARE(window->pos(), QPointF(logicalOutput->geometry().width() - window->width(), logicalOutput->geometry().height() - window->height())); + QCOMPARE(window->output()->backendOutput(), output); + + // Change scale back + OutputConfiguration config2; + { + auto changeSet = config2.changeSet(output); + changeSet->scaleSetting = 1; + } + workspace()->applyOutputConfiguration(config2); + + // The window will be moved back to where it was before + QCOMPARE(window->pos(), originalPosition); + QCOMPARE(window->output()->backendOutput(), output); +} + +void OutputChangesTest::testMaximizeStateRestoredAfterEnablingOutput_data() +{ + QTest::addColumn("maximizeMode"); + QTest::addRow("Vertical Maximization") << MaximizeMode::MaximizeVertical; + QTest::addRow("Horizontal Maximization") << MaximizeMode::MaximizeHorizontal; + QTest::addRow("Full Maximization") << MaximizeMode::MaximizeFull; +} + +void OutputChangesTest::testMaximizeStateRestoredAfterEnablingOutput() +{ + // This test verifies that the window state will get restored after disabling and enabling an output, + // even if its maximize state changed in the process + + QFETCH(MaximizeMode, maximizeMode); + + const auto outputs = kwinApp()->outputBackend()->outputs(); + + // Disable the right output + { + OutputConfiguration config; + auto changeSet = config.changeSet(outputs[1]); + changeSet->enabled = false; + workspace()->applyOutputConfiguration(config); + } + + // Create a window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // kwin will send a configure event with the activated state. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + const RectF originalGeometry = window->moveResizeGeometry(); + + // Enable the right output + { + OutputConfiguration config; + auto changeSet = config.changeSet(outputs[1]); + changeSet->enabled = true; + workspace()->applyOutputConfiguration(config); + } + + // Move the window to the right monitor and make it maximized. + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + window->move(QPointF(1280 + 50, 100)); + window->maximize(maximizeMode); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + const auto maximizedGeometry = window->moveResizeGeometry(); + QCOMPARE(window->frameGeometry(), maximizedGeometry); + QCOMPARE(window->output()->backendOutput(), outputs[1]); + QCOMPARE(window->maximizeMode(), maximizeMode); + QCOMPARE(window->requestedMaximizeMode(), maximizeMode); + QCOMPARE(window->geometryRestore(), RectF(1280 + 50, 100, 100, 50)); + + // Disable the right output + { + OutputConfiguration config; + auto changeSet = config.changeSet(outputs[1]); + changeSet->enabled = false; + workspace()->applyOutputConfiguration(config); + } + + // The window will be moved to its prior position on the left monitor and unmaximized + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), originalGeometry.size().toSize()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), originalGeometry.size().toSize(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), originalGeometry); + QCOMPARE(window->moveResizeGeometry(), originalGeometry); + QCOMPARE(window->output()->backendOutput(), outputs[0]); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + + // Enable the right output again + { + OutputConfiguration config; + auto changeSet = config.changeSet(outputs[1]); + changeSet->enabled = true; + workspace()->applyOutputConfiguration(config); + } + + // The window will be moved back to the right monitor, maximized and the geometry restore will be updated + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), maximizedGeometry.size()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), maximizedGeometry); + QCOMPARE(window->moveResizeGeometry(), maximizedGeometry); + QCOMPARE(window->output()->backendOutput(), outputs[1]); + QCOMPARE(window->maximizeMode(), maximizeMode); + QCOMPARE(window->requestedMaximizeMode(), maximizeMode); + QCOMPARE(window->geometryRestore(), RectF(1280 + 50, 100, 100, 50)); +} + +void OutputChangesTest::testInvalidGeometryRestoreAfterEnablingOutput() +{ + // This test verifies that the geometry restore gets restore correctly, even if it's invalid + + const auto outputs = kwinApp()->outputBackend()->outputs(); + + // Disable the right output + { + OutputConfiguration config; + auto changeSet = config.changeSet(outputs[1]); + changeSet->enabled = false; + workspace()->applyOutputConfiguration(config); + } + + // Create a window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + shellSurface->set_maximized(); + { + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + shellSurface->xdgSurface()->surface()->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().first().toUInt()); + } + auto window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::blue); + QVERIFY(window); + QCOMPARE(window->maximizeMode(), MaximizeFull); + + const RectF originalGeometry = window->moveResizeGeometry(); + const RectF originalGeometryRestore = window->geometryRestore(); + + // Enable the right output + { + OutputConfiguration config; + auto changeSet = config.changeSet(outputs[1]); + changeSet->enabled = true; + workspace()->applyOutputConfiguration(config); + } + + // Move the window to the right monitor + window->sendToOutput(workspace()->findOutput(outputs[1])); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QCOMPARE(window->frameGeometry(), RectF(1280, 0, 1280, 1024)); + QCOMPARE(window->moveResizeGeometry(), RectF(1280, 0, 1280, 1024)); + QCOMPARE(window->output()->backendOutput(), outputs[1]); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + QVERIFY(workspace()->findOutput(outputs[1])->geometry().contains(window->geometryRestore().topLeft().toPoint())); + QCOMPARE(window->geometryRestore().size(), QSizeF(0, 0)); + + const RectF rightGeometryRestore = window->geometryRestore(); + + // Disable the right output + { + OutputConfiguration config; + auto changeSet = config.changeSet(outputs[1]); + changeSet->enabled = false; + workspace()->applyOutputConfiguration(config); + } + + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + + // The window will be moved to its prior position on the left monitor, and still maximized + QCOMPARE(window->frameGeometry(), originalGeometry); + QCOMPARE(window->moveResizeGeometry(), originalGeometry); + QCOMPARE(window->output()->backendOutput(), outputs[0]); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + QVERIFY(workspace()->findOutput(outputs[0])->geometry().contains(window->geometryRestore().topLeft().toPoint())); + QCOMPARE(window->geometryRestore(), originalGeometryRestore); + + // Enable the right output again + { + OutputConfiguration config; + auto changeSet = config.changeSet(outputs[1]); + changeSet->enabled = true; + workspace()->applyOutputConfiguration(config); + } + + // The window will be moved back to the right monitor, maximized and the geometry restore will be updated + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), workspace()->findOutput(outputs[1])->geometry().size()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), workspace()->findOutput(outputs[1])->geometry().size(), Qt::blue); + QCOMPARE(window->frameGeometry(), RectF(1280, 0, 1280, 1024)); + QCOMPARE(window->moveResizeGeometry(), RectF(1280, 0, 1280, 1024)); + QCOMPARE(window->output()->backendOutput(), outputs[1]); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + QCOMPARE(window->geometryRestore(), rightGeometryRestore); +} + +void OutputChangesTest::testMaximizedWindowDoesntDisappear_data() +{ + QTest::addColumn("maximizeMode"); + QTest::addRow("Vertical Maximization") << MaximizeMode::MaximizeVertical; + QTest::addRow("Horizontal Maximization") << MaximizeMode::MaximizeHorizontal; + QTest::addRow("Full Maximization") << MaximizeMode::MaximizeFull; +} + +void OutputChangesTest::testMaximizedWindowDoesntDisappear() +{ + // This test verifies that (vertically, horizontally) maximized windows don't get placed out of the screen + // when the output they're on gets disabled or removed + + Test::setOutputConfig({ + Test::OutputInfo{ + .geometry = Rect(5120 / 3, 1440, 2256 / 1.3, 1504 / 1.3), + .scale = 1.3, + .internal = true, + }, + Test::OutputInfo{ + .geometry = Rect(0, 0, 5120, 1440), + .scale = 1, + .internal = false, + }, + }); + const auto outputs = kwinApp()->outputBackend()->outputs(); + QFETCH(MaximizeMode, maximizeMode); + + workspace()->setActiveOutput(workspace()->findOutput(outputs[1])); + + // Create a window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(500, 300), Qt::blue); + QVERIFY(window); + + // kwin will send a configure event with the activated state. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + window->move(workspace()->findOutput(outputs[1])->geometry().topLeft() + QPoint(3500, 500)); + const RectF originalGeometry = window->frameGeometry(); + QVERIFY(workspace()->findOutput(outputs[1])->geometryF().contains(originalGeometry)); + + // vertically maximize the window + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + window->maximize(maximizeMode); + + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + + QCOMPARE(window->output()->backendOutput(), outputs[1]); + const auto maximizedGeometry = window->moveResizeGeometry(); + QCOMPARE(window->frameGeometry(), maximizedGeometry); + QCOMPARE(window->maximizeMode(), maximizeMode); + QCOMPARE(window->requestedMaximizeMode(), maximizeMode); + QCOMPARE(window->geometryRestore(), originalGeometry); + + // Disable the top output + { + OutputConfiguration config; + auto changeSet0 = config.changeSet(outputs[0]); + changeSet0->pos = QPoint(0, 0); + auto changeSet = config.changeSet(outputs[1]); + changeSet->enabled = false; + workspace()->applyOutputConfiguration(config); + } + + // The window should be moved to the left output + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + + QCOMPARE(window->output()->backendOutput(), outputs[0]); + QVERIFY(workspace()->findOutput(outputs[0])->geometryF().contains(window->frameGeometry())); + QVERIFY(workspace()->findOutput(outputs[0])->geometryF().contains(window->moveResizeGeometry())); + QCOMPARE(window->maximizeMode(), maximizeMode); + QCOMPARE(window->requestedMaximizeMode(), maximizeMode); +} + +void OutputChangesTest::testLaptopLidClosed() +{ + Test::setOutputConfig({ + Test::OutputInfo{ + .geometry = Rect(0, 0, 1280, 1024), + .internal = true, + }, + Test::OutputInfo{ + .geometry = Rect(1280, 0, 1280, 1024), + .internal = false, + }, + }); + const auto outputs = kwinApp()->outputBackend()->outputs(); + const auto internal = outputs.front(); + QVERIFY(internal->isInternal()); + const auto external = outputs.back(); + QVERIFY(!external->isInternal()); + + auto lidSwitch = std::make_unique(); + lidSwitch->setLidSwitch(true); + lidSwitch->setName("virtual lid switch"); + input()->addInputDevice(lidSwitch.get()); + + auto timestamp = 1ms; + Q_EMIT lidSwitch->switchToggle(SwitchState::Off, timestamp++, lidSwitch.get()); + QVERIFY(internal->isEnabled()); + QVERIFY(external->isEnabled()); + + Q_EMIT lidSwitch->switchToggle(SwitchState::On, timestamp++, lidSwitch.get()); + QVERIFY(!internal->isEnabled()); + QVERIFY(external->isEnabled()); + + Q_EMIT lidSwitch->switchToggle(SwitchState::Off, timestamp++, lidSwitch.get()); + QVERIFY(internal->isEnabled()); + QVERIFY(external->isEnabled()); + + input()->removeInputDevice(lidSwitch.get()); +} + +#if KWIN_BUILD_X11 +static X11Window *createX11Window(xcb_connection_t *connection, const Rect &geometry, std::function setup = {}) +{ + xcb_window_t windowId = xcb_generate_id(connection); + xcb_create_window(connection, XCB_COPY_FROM_PARENT, windowId, rootWindow(), + geometry.x(), + geometry.y(), + geometry.width(), + geometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, geometry.x(), geometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, geometry.width(), geometry.height()); + xcb_icccm_set_wm_normal_hints(connection, windowId, &hints); + + if (setup) { + setup(windowId); + } + + xcb_map_window(connection, windowId); + xcb_flush(connection); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + if (!windowCreatedSpy.wait()) { + return nullptr; + } + return windowCreatedSpy.last().first().value(); +} +#endif + +void OutputChangesTest::testXwaylandScaleChange() +{ +#if KWIN_BUILD_X11 + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = kwinApp()->outputBackend()->outputs(); + + { + OutputConfiguration config; + config.changeSet(outputs[0])->scaleSetting = 2; + config.changeSet(outputs[1])->scaleSetting = 1; + workspace()->applyOutputConfiguration(config); + } + QCOMPARE(kwinApp()->xwaylandScale(), 2); + + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createX11Window(c.get(), Rect(0, 0, 100, 200)); + const RectF originalGeometry = window->frameGeometry(); + + // disable the left output -> window gets moved to the right output + { + OutputConfiguration config; + config.changeSet(outputs[0])->enabled = false; + workspace()->applyOutputConfiguration(config); + } + + // the window should still have logical size of 100, 200 + QCOMPARE(kwinApp()->xwaylandScale(), 1); + QCOMPARE(window->frameGeometry().size(), originalGeometry.size()); + + // enable the left output again + { + OutputConfiguration config; + config.changeSet(outputs[0])->enabled = true; + workspace()->applyOutputConfiguration(config); + } + + // the window should be back in its original geometry + QCOMPARE(kwinApp()->xwaylandScale(), 2); + QCOMPARE(window->frameGeometry(), originalGeometry); +#endif +} + +using ModeInfo = std::tuple; + +static QByteArray readEdid(const QString &path) +{ + QFile file(path); + (void)file.open(QIODeviceBase::OpenModeFlag::ReadOnly); + return file.readAll(); +}; + +void OutputChangesTest::testGenerateConfigs_data() +{ + QTest::addColumn("deviceType"); + QTest::addColumn("outputInfo"); + QTest::addColumn>("defaultMode"); + QTest::addColumn("defaultScale"); + QTest::addColumn("defaultDDCValue"); + + QTest::addRow("1080p 27\"") + << DeviceType::Desktop + << Test::OutputInfo{ + .geometry = Rect(0, 0, 1920, 1080), + .internal = false, + .physicalSizeInMM = QSize(598, 336), + .modes = {ModeInfo(QSize(1920, 1080), 60000, OutputMode::Flag::Preferred)}, + } + << ModeInfo(QSize(1920, 1080), 60000ul, OutputMode::Flag::Preferred) << 1.0 << true; + + QTest::addRow("2160p 27\"") + << DeviceType::Desktop + << Test::OutputInfo{ + .geometry = Rect(0, 0, 3840, 2160), + .internal = false, + .physicalSizeInMM = QSize(598, 336), + .modes = {ModeInfo(QSize(3840, 2160), 60000, OutputMode::Flag::Preferred)}, + } + << ModeInfo(QSize(3840, 2160), 60000ul, OutputMode::Flag::Preferred) << 1.70 << true; + + QTest::addRow("2160p invalid size") + << DeviceType::Desktop + << Test::OutputInfo{ + .geometry = Rect(0, 0, 3840, 2160), + .internal = false, + .physicalSizeInMM = QSize(), + .modes = {ModeInfo(QSize(3840, 2160), 60000, OutputMode::Flag::Preferred)}, + } + << ModeInfo(QSize(3840, 2160), 60000ul, OutputMode::Flag::Preferred) << 1.0 << true; + + QTest::addRow("2160p impossibly tiny size") + << DeviceType::Desktop + << Test::OutputInfo{ + .geometry = Rect(0, 0, 3840, 2160), + .internal = false, + .physicalSizeInMM = QSize(1, 1), + .modes = {ModeInfo(QSize(3840, 2160), 60000, OutputMode::Flag::Preferred)}, + } + << ModeInfo(QSize(3840, 2160), 60000ul, OutputMode::Flag::Preferred) << 1.0 << true; + + QTest::addRow("1080p 27\" with non-preferred high refresh option") + << DeviceType::Desktop + << Test::OutputInfo{ + .geometry = Rect(0, 0, 1920, 1080), + .internal = false, + .physicalSizeInMM = QSize(598, 336), + .modes = {ModeInfo(QSize(1920, 1080), 60000, OutputMode::Flag::Preferred), ModeInfo(QSize(1920, 1080), 120000, OutputMode::Flags{})}, + } + << ModeInfo(QSize(1920, 1080), 120000ul, OutputMode::Flags{}) << 1.0 << true; + + QTest::addRow("2160p 27\" with 30Hz preferred mode") + << DeviceType::Desktop + << Test::OutputInfo{ + .geometry = Rect(0, 0, 3840, 2160), + .internal = false, + .physicalSizeInMM = QSize(598, 336), + .modes = {ModeInfo(QSize(3840, 2160), 30000, OutputMode::Flag::Preferred), ModeInfo(QSize(2560, 1440), 60000, OutputMode::Flags{})}, + } + << ModeInfo(QSize(2560, 1440), 60000ul, OutputMode::Flags{}) << 1.0 << true; + + QTest::addRow("2160p 27\" with 30Hz preferred and a generated 60Hz mode") + << DeviceType::Desktop + << Test::OutputInfo{ + .geometry = Rect(0, 0, 3840, 2160), + .internal = false, + .physicalSizeInMM = QSize(598, 336), + .modes = {ModeInfo(QSize(3840, 2160), 30000, OutputMode::Flag::Preferred), ModeInfo(QSize(2560, 1440), 60000, OutputMode::Flag::Generated)}, + } + << ModeInfo(QSize(3840, 2160), 30000ul, OutputMode::Flag::Preferred) << 1.70 << true; + + QTest::addRow("1440p 32:9 49\" with two preferred modes") + << DeviceType::Desktop + << Test::OutputInfo{ + .geometry = Rect(0, 0, 5120, 1440), + .internal = false, + .physicalSizeInMM = QSize(1190, 340), + .modes = {ModeInfo(QSize(3840, 1080), 120000, OutputMode::Flag::Preferred), ModeInfo(QSize(5120, 1440), 120000, OutputMode::Flag::Preferred)}, + } + << ModeInfo(QSize(5120, 1440), 120000ul, OutputMode::Flag::Preferred) << 1.0 << true; + + QTest::addRow("2160p 32:9 57\" with non-native preferred mode") + << DeviceType::Desktop + << Test::OutputInfo{ + .geometry = Rect(0, 0, 7680, 2160), + .internal = false, + .physicalSizeInMM = QSize(1400, 400), + .modes = {ModeInfo(QSize(3840, 1080), 60000, OutputMode::Flag::Preferred), ModeInfo(QSize(7680, 2160), 120000, OutputMode::Flags{})}, + } + << ModeInfo(QSize(7680, 2160), 120000ul, OutputMode::Flags{}) << 1.45 << true; + + QTest::addRow("Framework 1920p 13.5\"") + << DeviceType::Laptop + << Test::OutputInfo{ + .geometry = Rect(0, 0, 2880, 1920), + .internal = true, + .physicalSizeInMM = QSize(285, 190), + .modes = {ModeInfo(QSize(2880, 1920), 120000, OutputMode::Flag::Preferred)}, + } + << ModeInfo(QSize(2880, 1920), 120000, OutputMode::Flag::Preferred) << 2.05 << true; + + QTest::addRow("DELL XPS 13 1080p 13\"") + << DeviceType::Laptop + << Test::OutputInfo{ + .geometry = Rect(0, 0, 1920, 1080), + .internal = true, + .physicalSizeInMM = QSize(293, 162), + .modes = {ModeInfo(QSize(1920, 1080), 60000, OutputMode::Flag::Preferred)}, + } + << ModeInfo(QSize(1920, 1080), 60000, OutputMode::Flag::Preferred) << 1.35 << true; + + QTest::addRow("DELL XPS 13 2160p 13\"") + << DeviceType::Laptop + << Test::OutputInfo{ + .geometry = Rect(0, 0, 3840, 2160), + .internal = true, + .physicalSizeInMM = QSize(294, 165), + .modes = {ModeInfo(QSize(3840, 2160), 60000, OutputMode::Flag::Preferred)}, + } + << ModeInfo(QSize(3840, 2160), 60000, OutputMode::Flag::Preferred) << 2.65 << true; + + QTest::addRow("ThinkPad T14 2400p 14\"") + << DeviceType::Laptop + << Test::OutputInfo{ + .geometry = Rect(0, 0, 3840, 2400), + .internal = true, + .physicalSizeInMM = QSize(301, 188), + .modes = {ModeInfo(QSize(3840, 2400), 60000, OutputMode::Flag::Preferred)}, + } + << ModeInfo(QSize(3840, 2400), 60000, OutputMode::Flag::Preferred) << 2.60 << true; + + QTest::addRow("SteamDeck OLED") + << DeviceType::Laptop + << Test::OutputInfo{ + .geometry = Rect(0, 0, 800, 1280), + .internal = true, + .physicalSizeInMM = QSize(100, 160), + .modes = {ModeInfo(QSize(800, 1280), 90000, OutputMode::Flag::Preferred)}, + .panelOrientation = OutputTransform::Kind::Rotate90, + } + << ModeInfo(QSize(800, 1280), 90000ul, OutputMode::Flag::Preferred) << 1.0 << true; + + QTest::addRow("Pixel 3a") + << DeviceType::Phone + << Test::OutputInfo{ + .geometry = Rect(0, 0, 1080, 2220), + .internal = true, + .physicalSizeInMM = QSize(62, 128), + .modes = {ModeInfo(QSize(1080, 2220), 60000, OutputMode::Flags{}), ModeInfo(QSize(1080, 2220), 120000, OutputMode::Flag::Preferred)}, + } + << ModeInfo(QSize(1080, 2220), 120000ul, OutputMode::Flag::Preferred) << 2.95 << true; + + QTest::addRow("OnePlus 6") + << DeviceType::Phone + << Test::OutputInfo{ + .geometry = Rect(0, 0, 1080, 2280), + .internal = true, + .physicalSizeInMM = QSize(68, 145), + .modes = {ModeInfo(QSize(1080, 2280), 60000, OutputMode::Flag::Preferred)}, + } + << ModeInfo(QSize(1080, 2280), 60000ul, OutputMode::Flag::Preferred) << 2.65 << true; + + QTest::addRow("Samsung Odyssey G5") + << DeviceType::Desktop + << Test::OutputInfo{ + .geometry = Rect(), + .internal = false, + .physicalSizeInMM = QSize(698, 393), + .modes = {ModeInfo(QSize(2560, 1440), 164831, OutputMode::Flag::Preferred)}, + .edid = readEdid(QFINDTESTDATA("data/Odyssey G5.bin")), + } + << ModeInfo(QSize(2560, 1440), 164831, OutputMode::Flag::Preferred) << 1.0 << false; + + QTest::addRow("LG C4 77\"") + << DeviceType::Desktop + << Test::OutputInfo{ + .geometry = Rect(), + .internal = false, + .physicalSizeInMM = QSize(1600, 900), + .modes = {ModeInfo(QSize(3840, 2160), 120000, OutputMode::Flag::Preferred)}, + } + << ModeInfo(QSize(3840, 2160), 120000, OutputMode::Flag::Preferred) << 2.0 << true; + + QTest::addRow("Acer 24 CB242Ybmiprx") + << DeviceType::Desktop + << Test::OutputInfo{ + .geometry = QRect(), + .internal = false, + .physicalSizeInMM = QSize(527, 296), + .modes = { + ModeInfo(QSize(1920, 1080), 60000, OutputMode::Flag::Preferred), + ModeInfo(QSize(1920, 1080), 75000, OutputMode::Flags{}), + ModeInfo(QSize(1920, 1080), 60000, OutputMode::Flags{}), + ModeInfo(QSize(1920, 1080), 50000, OutputMode::Flags{}), + ModeInfo(QSize(1680, 1050), 60000, OutputMode::Flags{}), + ModeInfo(QSize(1280, 1024), 75000, OutputMode::Flags{}), + ModeInfo(QSize(1280, 1024), 60000, OutputMode::Flags{}), + ModeInfo(QSize(1440, 900), 50000, OutputMode::Flags{}), + ModeInfo(QSize(1280, 960), 60000, OutputMode::Flags{}), + ModeInfo(QSize(1920, 540), 60000, OutputMode::Flags{}), + ModeInfo(QSize(1280, 800), 60000, OutputMode::Flags{}), + ModeInfo(QSize(1152, 864), 75000, OutputMode::Flags{}), + ModeInfo(QSize(1280, 720), 60000, OutputMode::Flags{}), + }, + } + << ModeInfo(QSize(1920, 1080), 75000, OutputMode::Flags{}) << 1.0 << true; +} + +void OutputChangesTest::testGenerateConfigs() +{ + // Whether there is a lid switch input device is not a totally reliable way to determine if it's + // a laptop, but on the other hand, we don't have any other better hints. + QFETCH(DeviceType, deviceType); + LidSwitch lidSwitch; + if (deviceType == DeviceType::Laptop) { + input()->addInputDevice(&lidSwitch); + } + const auto lidSwitchGuard = qScopeGuard([&]() { + if (deviceType == DeviceType::Laptop) { + input()->removeInputDevice(&lidSwitch); + } + }); + + QFETCH(Test::OutputInfo, outputInfo); + Test::setOutputConfig({outputInfo}); + + // delete the previous config to avoid loading the config from workspace + QFile(QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("kwinoutputconfig.json"))).remove(); + + const auto outputs = kwinApp()->outputBackend()->outputs(); + OutputConfigurationStore configs; + auto cfg = configs.queryConfig(outputs, false, AccelerometerOrientation::Undefined, false); + QVERIFY(cfg.has_value()); + const auto [config, type] = *cfg; + const auto outputConfig = config.constChangeSet(outputs.front()); + + QFETCH(ModeInfo, defaultMode); + const auto &[modeSize, modeRefresh, modeFlags] = defaultMode; + + const auto mode = outputConfig->mode->lock(); + QVERIFY(mode); + QCOMPARE(mode->size(), modeSize); + QCOMPARE(mode->refreshRate(), modeRefresh); + QCOMPARE(mode->flags(), modeFlags); + + QFETCH(double, defaultScale); + QVERIFY(outputConfig->scale); + QCOMPARE(*outputConfig->scale, defaultScale); + + QFETCH(bool, defaultDDCValue); + QCOMPARE(*outputConfig->allowDdcCi, defaultDDCValue); +} + +void OutputChangesTest::testGeneratePartialConfigs() +{ + // This test verifies that adding an output to an existing configuration + // keeps some properties of that configuration (position, priority) + // instead of generating a completely new one + + // TODO change test API so it's possible to add outputs without also configuring them + const auto outputBackend = qobject_cast(kwinApp()->outputBackend()); + outputBackend->setVirtualOutputs({ + VirtualBackend::OutputInfo{ + .geometry = Rect(0, 0, 1920, 1080), + .edid = readEdid(QFINDTESTDATA("data/Odyssey G5.bin")), + .edidIdentifierOverride = QByteArrayLiteral("GeneratePartialConfigs-1"), + }, + VirtualBackend::OutputInfo{ + .geometry = Rect(0, 0, 1920, 1080), + .edid = readEdid(QFINDTESTDATA("data/Odyssey G5.bin")), + .edidIdentifierOverride = QByteArrayLiteral("GeneratePartialConfigs-2"), + }, + }); + auto outputs = kwinApp()->outputBackend()->outputs(); + + // workspace should have the outputs configured to be next to each other, + // with default priority in the order of the outputs + QCOMPARE(outputs[0]->position(), QPoint(0, 0)); + QCOMPARE(outputs[0]->priority(), 0); + QCOMPARE(outputs[1]->position(), QPoint(1920, 0)); + QCOMPARE(outputs[1]->priority(), 1); + + { + // change the priority values and positions + OutputConfiguration config; + *config.changeSet(outputs[0]) = OutputChangeSet{ + .pos = QPoint(0, 1080), + .priority = 1, + }; + *config.changeSet(outputs[1]) = OutputChangeSet{ + .pos = QPoint(500, 0), + .priority = 0, + }; + QCOMPARE(workspace()->applyOutputConfiguration(config), OutputConfigurationError::None); + } + + // now add another output + outputBackend->setVirtualOutputs({ + VirtualBackend::OutputInfo{ + .geometry = Rect(0, 0, 1920, 1080), + .edid = readEdid(QFINDTESTDATA("data/Odyssey G5.bin")), + .edidIdentifierOverride = QByteArrayLiteral("GeneratePartialConfigs-1"), + }, + VirtualBackend::OutputInfo{ + .geometry = Rect(0, 0, 1920, 1080), + .edid = readEdid(QFINDTESTDATA("data/Odyssey G5.bin")), + .edidIdentifierOverride = QByteArrayLiteral("GeneratePartialConfigs-2"), + }, + VirtualBackend::OutputInfo{ + .geometry = Rect(0, 0, 1920, 1080), + }, + }); + outputs = kwinApp()->outputBackend()->outputs(); + + // position and priority should still be what we applied before + QCOMPARE(outputs[0]->position(), QPoint(0, 1080)); + QCOMPARE(outputs[0]->priority(), 1); + QCOMPARE(outputs[1]->position(), QPoint(500, 0)); + QCOMPARE(outputs[1]->priority(), 0); + // the new output should also have sane default values + QCOMPARE(outputs[2]->position(), QPoint(2420, 0)); + QCOMPARE(outputs[2]->priority(), 2); +} + +void OutputChangesTest::testAutorotate_data() +{ + QTest::addColumn("panelOrientation"); + QTest::addColumn("orientation"); + QTest::addColumn("expectedRotation"); + + QTest::addRow("panel orientation normal, no rotation") << OutputTransform::Kind::Normal << AccelerometerOrientation::TopUp << OutputTransform::Kind::Normal; + QTest::addRow("panel orientation normal, rotated 90° right") << OutputTransform::Kind::Normal << AccelerometerOrientation::LeftUp << OutputTransform::Kind::Rotate90; + QTest::addRow("panel orientation normal, rotated 180°") << OutputTransform::Kind::Normal << AccelerometerOrientation::TopDown << OutputTransform::Kind::Rotate180; + QTest::addRow("panel orientation normal, rotated 90° left") << OutputTransform::Kind::Normal << AccelerometerOrientation::RightUp << OutputTransform::Kind::Rotate270; + + QTest::addRow("panel orientation left up, no rotation") << OutputTransform::Kind::Rotate90 << AccelerometerOrientation::TopUp << OutputTransform::Kind::Normal; + QTest::addRow("panel orientation left up, rotated 90° right") << OutputTransform::Kind::Rotate90 << AccelerometerOrientation::LeftUp << OutputTransform::Kind::Rotate90; + QTest::addRow("panel orientation left up, rotated 180°") << OutputTransform::Kind::Rotate90 << AccelerometerOrientation::TopDown << OutputTransform::Kind::Rotate180; + QTest::addRow("panel orientation left up, rotated 90° left") << OutputTransform::Kind::Rotate90 << AccelerometerOrientation::RightUp << OutputTransform::Kind::Rotate270; + + QTest::addRow("panel orientation upside down, no rotation") << OutputTransform::Kind::Rotate180 << AccelerometerOrientation::TopUp << OutputTransform::Kind::Normal; + QTest::addRow("panel orientation upside down, rotated 90° right") << OutputTransform::Kind::Rotate180 << AccelerometerOrientation::LeftUp << OutputTransform::Kind::Rotate90; + QTest::addRow("panel orientation upside down, rotated 180°") << OutputTransform::Kind::Rotate180 << AccelerometerOrientation::TopDown << OutputTransform::Kind::Rotate180; + QTest::addRow("panel orientation upside down, rotated 90° left") << OutputTransform::Kind::Rotate180 << AccelerometerOrientation::RightUp << OutputTransform::Kind::Rotate270; + + QTest::addRow("panel orientation right up, no rotation") << OutputTransform::Kind::Rotate270 << AccelerometerOrientation::TopUp << OutputTransform::Kind::Normal; + QTest::addRow("panel orientation right up, rotated 90° right") << OutputTransform::Kind::Rotate270 << AccelerometerOrientation::LeftUp << OutputTransform::Kind::Rotate90; + QTest::addRow("panel orientation right up, rotated 180°") << OutputTransform::Kind::Rotate270 << AccelerometerOrientation::TopDown << OutputTransform::Kind::Rotate180; + QTest::addRow("panel orientation right up, rotated 90° left") << OutputTransform::Kind::Rotate270 << AccelerometerOrientation::RightUp << OutputTransform::Kind::Rotate270; +} + +void OutputChangesTest::testAutorotate() +{ + // delete the previous config to avoid clashes between test runs + QFile(QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("kwinoutputconfig.json"))).remove(); + + QFETCH(OutputTransform::Kind, panelOrientation); + Test::setOutputConfig({Test::OutputInfo{ + .geometry = Rect(0, 0, 1280, 1024), + .internal = true, + .physicalSizeInMM = QSize(598, 336), + .modes = {ModeInfo(QSize(1280, 1024), 60000, OutputMode::Flag::Preferred)}, + .panelOrientation = panelOrientation, + }}); + + QFETCH(AccelerometerOrientation, orientation); + + const auto outputs = kwinApp()->outputBackend()->outputs(); + OutputConfigurationStore configs; + auto cfg = configs.queryConfig(outputs, false, orientation, true); + QVERIFY(cfg.has_value()); + const auto [config, type] = *cfg; + const auto outputConfig = config.constChangeSet(outputs.front()); + + QCOMPARE(outputConfig->autoRotationPolicy, BackendOutput::AutoRotationPolicy::InTabletMode); + + QFETCH(OutputTransform::Kind, expectedRotation); + QVERIFY(outputConfig->transform.has_value()); + QCOMPARE(outputConfig->transform->kind(), expectedRotation); +} + +struct IdentificationData +{ + std::optional connectorName; + QByteArray edid; + std::optional mstPath; +}; + +void OutputChangesTest::testSettingRestoration_data() +{ + QTest::addColumn>("outputData"); + + QTest::addRow("Same EDID ID, different hash") << QList{ + IdentificationData{ + .connectorName = std::nullopt, + .edid = readEdid(QFINDTESTDATA("data/same serial number/edid.bin")), + .mstPath = std::nullopt, + }, + IdentificationData{ + .connectorName = std::nullopt, + .edid = readEdid(QFINDTESTDATA("data/same serial number/edid2.bin")), + .mstPath = std::nullopt, + }, + }; + + QTest::addRow("Same EDID") << QList{ + IdentificationData{ + .connectorName = QStringLiteral("connector1"), + .edid = readEdid(QFINDTESTDATA("data/same serial number/edid.bin")), + .mstPath = std::nullopt, + }, + IdentificationData{ + .connectorName = QStringLiteral("connector2"), + .edid = readEdid(QFINDTESTDATA("data/same serial number/edid.bin")), + .mstPath = std::nullopt, + }, + }; + + QTest::addRow("No EDID") << QList{ + IdentificationData{ + .connectorName = QStringLiteral("connector1"), + .edid = QByteArray{}, + .mstPath = std::nullopt, + }, + IdentificationData{ + .connectorName = QStringLiteral("connector2"), + .edid = QByteArray{}, + .mstPath = std::nullopt, + }, + }; + + QTest::addRow("One has EDID, the other doesn't") << QList{ + IdentificationData{ + .connectorName = QStringLiteral("connector1"), + .edid = readEdid(QFINDTESTDATA("data/same serial number/edid.bin")), + .mstPath = std::nullopt, + }, + IdentificationData{ + .connectorName = QStringLiteral("connector2"), + .edid = QByteArray{}, + .mstPath = std::nullopt, + }, + }; + + QTest::addRow("Same EDID, no connector names, different MST paths") << QList{ + IdentificationData{ + .connectorName = std::nullopt, + .edid = readEdid(QFINDTESTDATA("data/same serial number/edid.bin")), + .mstPath = QByteArrayLiteral("MST-1-1"), + }, + IdentificationData{ + .connectorName = std::nullopt, + .edid = readEdid(QFINDTESTDATA("data/same serial number/edid.bin")), + .mstPath = QByteArrayLiteral("MST-1-2"), + }, + }; + + QTest::addRow("Same EDID ID, different hash, no connector names, different MST paths") << QList{ + IdentificationData{ + .connectorName = std::nullopt, + .edid = readEdid(QFINDTESTDATA("data/same serial number/edid.bin")), + .mstPath = QByteArrayLiteral("MST-1-1"), + }, + IdentificationData{ + .connectorName = std::nullopt, + .edid = readEdid(QFINDTESTDATA("data/same serial number/edid2.bin")), + .mstPath = QByteArrayLiteral("MST-1-2"), + }, + }; + + QTest::addRow("No EDID, no connector names, different MST paths") << QList{ + IdentificationData{ + .connectorName = std::nullopt, + .edid = QByteArray{}, + .mstPath = QByteArrayLiteral("MST-1-1"), + }, + IdentificationData{ + .connectorName = std::nullopt, + .edid = QByteArray{}, + .mstPath = QByteArrayLiteral("MST-1-2"), + }, + }; + + QTest::addRow("One EDID, the other not, no connector names, different MST paths") << QList{ + IdentificationData{ + .connectorName = std::nullopt, + .edid = readEdid(QFINDTESTDATA("data/same serial number/edid.bin")), + .mstPath = QByteArrayLiteral("MST-1-1"), + }, + IdentificationData{ + .connectorName = std::nullopt, + .edid = QByteArray{}, + .mstPath = QByteArrayLiteral("MST-1-2"), + }, + }; + + QTest::addRow("Only EDID hash, no connector names, no MST path") << QList{ + IdentificationData{ + .connectorName = std::nullopt, + .edid = QByteArrayLiteral("bbbbbbbbbbbbbbbb"), + .mstPath = std::nullopt, + }, + IdentificationData{ + .connectorName = std::nullopt, + .edid = QByteArrayLiteral("aaaaaaaaaaaaaaaa"), + .mstPath = std::nullopt, + }, + }; + + QTest::addRow("One EDID ID, other only EDID hash") << QList{ + IdentificationData{ + .connectorName = std::nullopt, + .edid = readEdid(QFINDTESTDATA("data/same serial number/edid.bin")), + .mstPath = std::nullopt, + }, + IdentificationData{ + .connectorName = std::nullopt, + .edid = QByteArrayLiteral("aaaaaaaaaaaaaaaa"), + .mstPath = std::nullopt, + }, + }; + + QTest::addRow("three outputs, two with the same EDID, with overlapping MST paths") << QList{ + IdentificationData{ + .connectorName = std::nullopt, + .edid = readEdid(QFINDTESTDATA("data/same serial number/edid.bin")), + .mstPath = QByteArrayLiteral("MST-1-1"), + }, + IdentificationData{ + .connectorName = std::nullopt, + .edid = readEdid(QFINDTESTDATA("data/same serial number/edid.bin")), + .mstPath = QByteArrayLiteral("MST-1-2"), + }, + IdentificationData{ + .connectorName = std::nullopt, + .edid = readEdid(QFINDTESTDATA("data/same serial number/edid2.bin")), + .mstPath = QByteArrayLiteral("MST-1-1"), + }, + }; +} + +void OutputChangesTest::testSettingRestoration() +{ + // this test verifies that we restore configs correctly, + // even if there's no unique EDID ID to match them with + + QFETCH(QList, outputData); + + const auto outputBackend = qobject_cast(kwinApp()->outputBackend()); + outputBackend->setVirtualOutputs(outputData | std::views::transform([](const IdentificationData &data) { + return VirtualBackend::OutputInfo{ + .geometry = Rect(0, 0, 1280, 1024), + .internal = false, + .physicalSizeInMM = QSize(598, 336), + .modes = {ModeInfo(QSize(1280, 1024), 60000, OutputMode::Flag::Preferred)}, + .edid = data.edid, + .edidIdentifierOverride = std::nullopt, + .connectorName = data.connectorName, + .mstPath = data.mstPath, + }; + }) | std::ranges::to()); + + OutputConfigurationStore *configurationStore = workspace()->outputConfigureStore(); + configurationStore->clear(); + + auto outputs = kwinApp()->outputBackend()->outputs(); + + QList> outputPositions; + { + auto cfg = configurationStore->queryConfig(outputs, false, AccelerometerOrientation::Undefined, false); + QVERIFY(cfg.has_value()); + auto [config, type] = *cfg; + workspace()->applyOutputConfiguration(config); + for (const auto output : outputs) { + outputPositions.push_back(config.constChangeSet(output)->pos); + } + } + + // the positions must be independent of the order of outputs in the list + std::ranges::reverse(outputs); + { + auto cfg = configurationStore->queryConfig(outputs, false, AccelerometerOrientation::Undefined, false); + QVERIFY(cfg.has_value()); + const auto [config, type] = *cfg; + auto revertedPositions = outputPositions | std::views::reverse; + for (int i = 0; i < outputs.size(); i++) { + QCOMPARE(revertedPositions[i], config.constChangeSet(outputs[i])->pos); + } + } + + // this must work if one of the outputs is removed in between as well + outputBackend->setVirtualOutputs({ + VirtualBackend::OutputInfo{ + .geometry = Rect(0, 0, 1280, 1024), + .internal = false, + .physicalSizeInMM = QSize(598, 336), + .modes = {ModeInfo(QSize(1280, 1024), 60000, OutputMode::Flag::Preferred)}, + .edid = outputData.back().edid, + .edidIdentifierOverride = std::nullopt, + .connectorName = outputData.back().connectorName, + .mstPath = outputData.back().mstPath, + }, + }); + + // and add it again, with the inverted order + outputBackend->setVirtualOutputs(outputData | std::views::reverse | std::views::transform([](const IdentificationData &data) { + return VirtualBackend::OutputInfo{ + .geometry = Rect(0, 0, 1280, 1024), + .internal = false, + .physicalSizeInMM = QSize(598, 336), + .modes = {ModeInfo(QSize(1280, 1024), 60000, OutputMode::Flag::Preferred)}, + .edid = data.edid, + .edidIdentifierOverride = std::nullopt, + .connectorName = data.connectorName, + .mstPath = data.mstPath, + }; + }) | std::ranges::to()); + outputs = kwinApp()->outputBackend()->outputs(); + + { + auto cfg = configurationStore->queryConfig(outputs, false, AccelerometerOrientation::Undefined, false); + QVERIFY(cfg.has_value()); + const auto [config, type] = *cfg; + auto revertedPositions = outputPositions | std::views::reverse; + for (int i = 0; i < outputs.size(); i++) { + QCOMPARE(revertedPositions[i], config.constChangeSet(outputs[i])->pos); + } + } +} + +void OutputChangesTest::testSettingRestoration_initialParsingFailure() +{ + // this test checks that when libdisplay-info fails to parse an EDID + // and gets fixed later, we still pick the same settings as before + + const auto outputBackend = qobject_cast(kwinApp()->outputBackend()); + + QFile file(QFINDTESTDATA("data/same serial number/edid.bin")); + file.open(QIODeviceBase::OpenModeFlag::ReadOnly); + const auto edid = file.readAll(); + + // first, libdisplay-info failed to parse the EDID and we don't have an EDID ID + // note that this uses two displays with the same EDID, + // to additionally test the case when EDID ID isn't unique when this happens + outputBackend->setVirtualOutputs({ + VirtualBackend::OutputInfo{ + .geometry = Rect(0, 0, 1280, 1024), + .internal = false, + .physicalSizeInMM = QSize(598, 336), + .modes = { + ModeInfo(QSize(1280, 1024), 60000, OutputMode::Flag::Preferred), + ModeInfo(QSize(640, 480), 60000, OutputMode::Flags{}), + }, + .edid = edid, + .edidIdentifierOverride = QByteArray(), + .connectorName = std::nullopt, + .mstPath = QByteArrayLiteral("MST-1-1"), + }, + VirtualBackend::OutputInfo{ + .geometry = Rect(0, 0, 1280, 1024), + .internal = false, + .physicalSizeInMM = QSize(598, 336), + .modes = { + ModeInfo(QSize(1280, 1024), 60000, OutputMode::Flag::Preferred), + ModeInfo(QSize(640, 480), 60000, OutputMode::Flags{}), + }, + .edid = edid, + .edidIdentifierOverride = QByteArray(), + .connectorName = std::nullopt, + .mstPath = QByteArrayLiteral("MST-1-2"), + }, + }); + + OutputConfigurationStore *configurationStore = workspace()->outputConfigureStore(); + configurationStore->clear(); + + auto outputs = kwinApp()->outputBackend()->outputs(); + + { + // query the generated config, like KWin normally would + auto cfg = configurationStore->queryConfig(outputs, false, AccelerometerOrientation::Undefined, false); + QVERIFY(cfg.has_value()); + auto [config, type] = *cfg; + workspace()->applyOutputConfiguration(config); + QCOMPARE(config.constChangeSet(outputs[0])->desiredModeSize.value(), QSize(1280, 1024)); + } + { + // change the mode, so that we know if a new config entry was generated + OutputConfiguration config; + const auto changeSet = config.changeSet(outputs[0]); + changeSet->mode = outputs[0]->modes()[1]; + changeSet->desiredModeSize = QSize(640, 480); + changeSet->desiredModeRefreshRate = 60000; + changeSet->desiredModeFlags = 0; + workspace()->applyOutputConfiguration(config); + } + { + // verify that querying the config also shows the changed mode + // things could already go wrong here + auto cfg = configurationStore->queryConfig(outputs, false, AccelerometerOrientation::Undefined, false); + QVERIFY(cfg.has_value()); + auto [config, type] = *cfg; + QCOMPARE(type, OutputConfigurationStore::ConfigType::Preexisting); + workspace()->applyOutputConfiguration(config); + QCOMPARE(config.constChangeSet(outputs[0])->desiredModeSize.value(), QSize(640, 480)); + } + + // now libdisplay-info was updated, and we have an EDID ID for the same hash + outputBackend->setVirtualOutputs({ + VirtualBackend::OutputInfo{ + .geometry = Rect(0, 0, 1280, 1024), + .internal = false, + .physicalSizeInMM = QSize(598, 336), + .modes = { + ModeInfo(QSize(1280, 1024), 60000, OutputMode::Flag::Preferred), + ModeInfo(QSize(640, 480), 60000, OutputMode::Flags{}), + }, + .edid = edid, + .edidIdentifierOverride = std::nullopt, + .connectorName = std::nullopt, + .mstPath = QByteArrayLiteral("MST-1-1"), + }, + VirtualBackend::OutputInfo{ + .geometry = Rect(0, 0, 1280, 1024), + .internal = false, + .physicalSizeInMM = QSize(598, 336), + .modes = { + ModeInfo(QSize(1280, 1024), 60000, OutputMode::Flag::Preferred), + ModeInfo(QSize(640, 480), 60000, OutputMode::Flags{}), + }, + .edid = edid, + .edidIdentifierOverride = std::nullopt, + .connectorName = std::nullopt, + .mstPath = QByteArrayLiteral("MST-1-2"), + }, + }); + outputs = kwinApp()->outputBackend()->outputs(); + + { + auto cfg = configurationStore->queryConfig(outputs, false, AccelerometerOrientation::Undefined, false); + QVERIFY(cfg.has_value()); + const auto [config, type] = *cfg; + QCOMPARE(config.constChangeSet(outputs[0])->desiredModeSize.value(), QSize(640, 480)); + } +} + +void OutputChangesTest::testSettingRestoration_replacedMode() +{ + const auto outputBackend = qobject_cast(kwinApp()->outputBackend()); + outputBackend->setVirtualOutputs({ + VirtualBackend::OutputInfo{ + .geometry = Rect(0, 0, 1280, 1024), + .internal = false, + .physicalSizeInMM = QSize(598, 336), + .modes = { + ModeInfo(QSize(1280, 1024), 120'000, OutputMode::Flag::Preferred), + ModeInfo(QSize(1280, 1024), 60'000, OutputMode::Flags{}), + ModeInfo(QSize(1280, 1024), 60'000, OutputMode::Flags{}), + }, + .edid = QByteArray(), + .edidIdentifierOverride = QByteArrayLiteral("ID"), + .connectorName = std::nullopt, + .mstPath = std::nullopt, + }, + }); + + OutputConfigurationStore *configurationStore = workspace()->outputConfigureStore(); + configurationStore->clear(); + + const auto outputs = kwinApp()->outputBackend()->outputs(); + const auto output = outputs.front(); + + { + // first, select the second mode + OutputConfiguration config; + const auto changeSet = config.changeSet(output); + changeSet->mode = output->modes()[1]; + changeSet->desiredModeSize = QSize(1280, 1024); + changeSet->desiredModeRefreshRate = 60000; + changeSet->desiredModeFlags = 0; + workspace()->applyOutputConfiguration(config); + } + + // now, mark the mode as "removed". Its replacement is already in the mode list + outputs[0]->modes()[1]->setRemoved(); + + const auto opt = configurationStore->queryConfig(outputs, false, AccelerometerOrientation::Undefined, false); + QVERIFY(opt.has_value()); + auto [config, type] = *opt; + workspace()->applyOutputConfiguration(config); + + // the preferred mode size and refresh rate should be the same, + // and the third mode should be selected + QCOMPARE(output->desiredModeSize(), QSize(1280, 1024)); + QCOMPARE(output->desiredModeRefreshRate(), 60000); + QCOMPARE(output->currentMode(), output->modes()[2]); +} + +void OutputChangesTest::testCursorRestoration() +{ + // This test verifies that the cursor gets put back to its original position + // when output changes happen, even with edge barriers enabled + options->setEdgeBarrier(100); + + Test::setOutputConfig({ + Test::OutputInfo{ + .geometry = Rect(0, 0, 2880, 1920), + .scale = 1.6, + .connectorName = QStringLiteral("eDP-1"), + }, + }); + + BackendOutput *tmp = kwinApp()->outputBackend()->createVirtualOutput("DP-1", "", QSize(5120, 1440), 1.0); + + auto outputs = kwinApp()->outputBackend()->outputs(); + + OutputConfiguration config; + config.changeSet(outputs[0])->pos = QPoint(1691, 1440); + config.changeSet(outputs[1])->pos = QPoint(0, 0); + QCOMPARE(workspace()->applyOutputConfiguration(config), OutputConfigurationError::None); + + input()->pointer()->warp(outputs[0]->position() + QPoint(1500, 1000)); + + // if an unrelated output is removed, the cursor should stay where it was + // relative to the output it's on + kwinApp()->outputBackend()->removeVirtualOutput(tmp); + outputs = kwinApp()->outputBackend()->outputs(); + QCOMPARE(input()->pointer()->pos(), outputs[0]->position() + QPoint(1500, 1000)); + + // same when it's added back + tmp = kwinApp()->outputBackend()->createVirtualOutput("DP-1", "", QSize(5120, 1440), 1.0); + outputs = kwinApp()->outputBackend()->outputs(); + QCOMPARE(input()->pointer()->pos(), outputs[0]->position() + QPoint(1500, 1000)); +} + +void OutputChangesTest::testEvacuateTiledWindowFromRemovedOutput_data() +{ + QTest::addColumn("tileMode"); + + QTest::addRow("Not tiled") << QuickTileFlag::None; + QTest::addRow("Quick Left") << QuickTileFlag::Left; + QTest::addRow("Quick Right") << QuickTileFlag::Right; + QTest::addRow("Quick Top") << QuickTileFlag::Top; + QTest::addRow("Quick Bottom") << QuickTileFlag::Bottom; + QTest::addRow("Custom") << QuickTileFlag::Custom; +} + +void OutputChangesTest::testEvacuateTiledWindowFromRemovedOutput() +{ + Test::setOutputConfig({ + Test::OutputInfo{ + .geometry = Rect(0, 0, 5120, 1440), + .internal = false, + }, + Test::OutputInfo{ + .geometry = Rect(1705, 1440, 1800, 1200), + .scale = 1.6, + .internal = true, + }, + }); + + const auto outputs = kwinApp()->outputBackend()->outputs(); + const auto external = outputs[0]; + const auto internal = outputs[1]; + QVERIFY(!external->isInternal()); + + // create a window on the external output + workspace()->setActiveOutput(workspace()->findOutput(external)); + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(500, 300), Qt::blue); + QVERIFY(window); + + // kwin will send a configure event with the active state. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QSignalSpy tileChangedSpy(window, &Window::tileChanged); + QSignalSpy frameCallback(surface.get(), &KWayland::Client::Surface::frameRendered); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + QVERIFY(workspace()->findOutput(external)->geometryF().contains(window->frameGeometry())); + + surface->setupFrameCallback(); + + // possibly tile it + QFETCH(QuickTileFlag, tileMode); + if (tileMode != QuickTileFlag::None) { + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + + window->setQuickTileModeAtCurrentPosition(tileMode); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::blue); + + QVERIFY(frameGeometryChangedSpy.wait()); + QVERIFY(workspace()->findOutput(external)->geometryF().contains(window->frameGeometry())); + } + + const RectF originalGeometry = window->frameGeometry(); + + // now remove the external output + { + OutputConfiguration config; + config.changeSet(external)->enabled = false; + workspace()->applyOutputConfiguration(config); + } + + if (tileMode != QuickTileFlag::None) { + // react to the configure event + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + + // before committing, wait for the frame callback + // like some real-world clients do (like Firefox) + QVERIFY(frameCallback.count() || frameCallback.wait(100)); + + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + } + + // the window should be moved to be completely in the internal output + QVERIFY(workspace()->findOutput(internal)->geometryF().contains(window->frameGeometry())); + + // when re-adding the output, the window should be back at its original spot + { + OutputConfiguration config; + config.changeSet(external)->enabled = true; + workspace()->applyOutputConfiguration(config); + } + + if (tileMode != QuickTileFlag::None) { + // react to the configure event + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + } + + QCOMPARE(window->frameGeometry(), originalGeometry); +} + +void OutputChangesTest::testMirroring_data() +{ + QTest::addColumn("resolution"); + QTest::addColumn("deviceOffset"); + + QTest::addRow("1280x1200") << QSize(1280, 1200) << QPoint(0, 0); + QTest::addRow("2000x1200") << QSize(2000, 1200) << QPoint(360, 0); + QTest::addRow("800x1280") << QSize(800, 1280) << QPoint(0, 265); +} + +void OutputChangesTest::testMirroring() +{ + QFETCH(QSize, resolution); + QFETCH(QPoint, deviceOffset); + + Test::setOutputConfig({ + Test::OutputInfo{ + .geometry = Rect(0, 0, 1280, 1200), + .scale = 1.0, + .internal = true, + }, + Test::OutputInfo{ + .geometry = Rect(QPoint(1280, 0), resolution), + .scale = 1.0, + .internal = false, + }, + }); + + BackendOutput *internal = kwinApp()->outputBackend()->outputs().front(); + BackendOutput *external = kwinApp()->outputBackend()->outputs().back(); + QVERIFY(internal->isInternal()); + QVERIFY(!external->isInternal()); + + QCOMPARE(workspace()->outputs().size(), 2); + + { + OutputConfiguration cfg; + cfg.changeSet(external)->replicationSource = internal->uuid(); + QCOMPARE(workspace()->applyOutputConfiguration(cfg), OutputConfigurationError::None); + } + + QCOMPARE(workspace()->outputs().size(), 1); + QCOMPARE(workspace()->outputs()[0]->backendOutput(), internal); + QCOMPARE(workspace()->outputs()[0]->modeSize(), QSize(1280, 1200)); + QCOMPARE(internal->deviceOffset(), QPoint()); + QCOMPARE(external->deviceOffset(), deviceOffset); + + LidSwitch lidSwitch; + input()->addInputDevice(&lidSwitch); + + auto timestamp = 1ms; + Q_EMIT lidSwitch.switchToggle(SwitchState::On, timestamp++, &lidSwitch); + QVERIFY(!internal->isEnabled()); + QVERIFY(external->isEnabled()); + QCOMPARE(external->deviceOffset(), QPoint()); + + Q_EMIT lidSwitch.switchToggle(SwitchState::Off, timestamp++, &lidSwitch); + QVERIFY(internal->isEnabled()); + QVERIFY(external->isEnabled()); + QCOMPARE(external->deviceOffset(), deviceOffset); + + input()->removeInputDevice(&lidSwitch); +} + +#define COMPARE_RANGE(expression, value, uncertainty) \ + QCOMPARE_GE(expression, value - uncertainty); \ + QCOMPARE_LE(expression, value + uncertainty); + +void OutputChangesTest::testAutoBrightness() +{ + constexpr double eta = 0.001; + // we don't need to be exact in all cases + constexpr double laxEta = 0.02; + + AutoBrightnessCurve curve; + curve.adjust(1.00, 100); + COMPARE_RANGE(curve.sample(100), 1.00, eta); + curve.adjust(0.75, 50); + COMPARE_RANGE(curve.sample(50), 0.75, eta); + curve.adjust(0.40, 10); + COMPARE_RANGE(curve.sample(10), 0.40, eta); + curve.adjust(0.20, 1); + COMPARE_RANGE(curve.sample(1), 0.20, eta); + + COMPARE_RANGE(curve.sample(100), 1.00, eta); + COMPARE_RANGE(curve.sample(50), 0.75, eta); + COMPARE_RANGE(curve.sample(10), 0.40, eta); + COMPARE_RANGE(curve.sample(1), 0.20, eta); + + // reduce brightness at higher lux values + curve.adjust(0.40, 0); + curve.adjust(0.20, 10); + COMPARE_RANGE(curve.sample(10), 0.20, eta); + + // reduce brightness at zero lux + curve.adjust(0.40, 0); + curve.adjust(0.35, 0); + curve.adjust(0.20, 0); + COMPARE_RANGE(curve.sample(0), 0.20, eta); + + // increase brightness at zero lux + curve.adjust(0.20, 0); + curve.adjust(0.25, 0); + curve.adjust(0.30, 0); + curve.adjust(0.35, 0); + COMPARE_RANGE(curve.sample(0), 0.35, eta); + + // higher luminance values should be unaffected by the changes at low brightness + COMPARE_RANGE(curve.sample(100), 1.00, eta); + COMPARE_RANGE(curve.sample(50), 0.75, eta); + + // reducing brightness at high luminance should work + curve.adjust(0.8, 150); + COMPARE_RANGE(curve.sample(150), 0.8, eta); + // afterwards, slightly increased luminance should *not* make brightness jump to 100% + COMPARE_RANGE(curve.sample(151), 0.8, laxEta); + + // same as above, but in the middle of the curve + curve.adjust(0.4, 100); + COMPARE_RANGE(curve.sample(100), 0.4, eta); + COMPARE_RANGE(curve.sample(101), 0.4, laxEta); +} + +void OutputChangesTest::testTemporaryDpmsHotplug() +{ + workspace()->outputConfigureStore()->clear(); + + Test::setOutputConfig({ + Test::OutputInfo{ + .geometry = Rect(0, 0, 1280, 1200), + .connectorName = QStringLiteral("TEST"), + }, + }); + + // turn off the outputs + workspace()->requestDpmsState(Workspace::DpmsState::Off); + QCOMPARE(workspace()->dpmsState(), Workspace::DpmsState::AboutToTurnOff); + QSignalSpy dpmsChanged(workspace(), &Workspace::dpmsStateChanged); + QVERIFY(dpmsChanged.wait()); + QCOMPARE(workspace()->dpmsState(), Workspace::DpmsState::Off); + + // remove the output + Test::setOutputConfig(QList{}); + + // displays should still be off + QCOMPARE(workspace()->dpmsState(), Workspace::DpmsState::Off); + + // re-adding the output immediately should keep it off + Test::setOutputConfig({ + Test::OutputInfo{ + .geometry = Rect(0, 0, 1280, 1200), + .connectorName = QStringLiteral("TEST"), + }, + }); + QCOMPARE(workspace()->dpmsState(), Workspace::DpmsState::Off); +} + +} // namespace KWin + +WAYLANDTEST_MAIN(KWin::OutputChangesTest) +#include "outputchanges_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/placement_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/placement_test.cpp new file mode 100644 index 0000000000..2f9042bdcc --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/placement_test.cpp @@ -0,0 +1,569 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 David Edmundson + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + SPDX-FileCopyrightText: 2023 Natalie Clarius + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "placement.h" +#include "pointer_input.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_placement-0"); + +class TestPlacement : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void init(); + void cleanup(); + void initTestCase(); + + void testPlaceSmart(); + void testPlaceMaximized(); + void testPlaceMaximizedLeavesFullscreen(); + void testPlaceCentered(); + void testPlaceUnderMouse(); + void testPlaceZeroCornered(); + void testPlaceRandom(); + void testFullscreen(); + void testCascadeIfCovering(); + void testCascadeIfCoveringIgnoreNonCovering(); + void testCascadeIfCoveringIgnoreOutOfArea(); + void testCascadeIfCoveringIgnoreAlreadyCovered(); + void testTitlebarOnScreen_data(); + void testTitlebarOnScreen(); + +private: + void setPlacementPolicy(PlacementPolicy policy); + struct WindowHandle + { + Window *window; + std::unique_ptr surface; + std::unique_ptr shellSurface; + }; + struct PlaceWindowResult + { + QSizeF initiallyConfiguredSize; + Test::XdgToplevel::States initiallyConfiguredStates; + RectF finalGeometry; + }; + /* + * Create a window and return relevant results for testing + * defaultSize is the buffer size to use if the compositor returns an empty size in the first configure + * event. + */ + std::tuple createAndPlaceWindow(const QSize &defaultSize); +}; + +void TestPlacement::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::LayerShellV1)); + + workspace()->setActiveOutput(QPoint(640, 512)); + KWin::input()->pointer()->warp(QPoint(640, 512)); +} + +void TestPlacement::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TestPlacement::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); +} + +void TestPlacement::setPlacementPolicy(PlacementPolicy policy) +{ + auto group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("Placement", Placement::policyToString(policy)); + group.sync(); + Workspace::self()->slotReconfigure(); +} + +std::tuple TestPlacement::createAndPlaceWindow(const QSize &defaultSize) +{ + PlaceWindowResult rc; + + // create a new window + std::unique_ptr surface = Test::createSurface(); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly); + + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + surfaceConfigureRequestedSpy.wait(); + + rc.initiallyConfiguredSize = toplevelConfigureRequestedSpy[0][0].toSize(); + rc.initiallyConfiguredStates = toplevelConfigureRequestedSpy[0][1].value(); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy[0][0].toUInt()); + + QSizeF size = rc.initiallyConfiguredSize; + + if (size.isEmpty()) { + size = defaultSize; + } + + auto window = Test::renderAndWaitForShown(surface.get(), size.toSize(), Qt::red); + + rc.finalGeometry = window->frameGeometry(); + return {rc, WindowHandle{ + .window = window, + .surface = std::move(surface), + .shellSurface = std::move(shellSurface), + }}; +} + +void TestPlacement::testPlaceSmart() +{ + const auto outputs = workspace()->outputs(); + const QList desiredGeometries{ + Rect(0, 0, 600, 500), + Rect(600, 0, 600, 500), + Rect(0, 500, 600, 500), + Rect(600, 500, 600, 500), + Rect(680, 524, 600, 500), + Rect(680, 0, 600, 500), + Rect(0, 524, 600, 500), + Rect(0, 0, 600, 500), + }; + + setPlacementPolicy(PlacementSmart); + + std::vector handles; + + for (const Rect &desiredGeometry : desiredGeometries) { + auto [windowPlacement, handle] = createAndPlaceWindow(QSize(600, 500)); + handles.push_back(std::move(handle)); + + // smart placement shouldn't define a size on windows + QCOMPARE(windowPlacement.initiallyConfiguredSize, QSize(0, 0)); + QCOMPARE(windowPlacement.finalGeometry.size(), QSize(600, 500)); + + QVERIFY(outputs[0]->geometry().contains(windowPlacement.finalGeometry.toRect())); + + QCOMPARE(windowPlacement.finalGeometry.toRect(), desiredGeometry); + } +} + +void TestPlacement::testPlaceMaximized() +{ + setPlacementPolicy(PlacementMaximizing); + + // add a top panel + std::unique_ptr panelSurface{Test::createSurface()}; + std::unique_ptr panelShellSurface{Test::createLayerSurfaceV1(panelSurface.get(), QStringLiteral("dock"))}; + panelShellSurface->set_size(1280, 20); + panelShellSurface->set_anchor(Test::LayerSurfaceV1::anchor_top); + panelShellSurface->set_exclusive_zone(20); + panelSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QSignalSpy panelConfigureRequestedSpy(panelShellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(panelConfigureRequestedSpy.wait()); + Test::renderAndWaitForShown(panelSurface.get(), panelConfigureRequestedSpy.last().at(1).toSize(), Qt::blue); + + std::vector handles; + + // all windows should be initially maximized with an initial configure size sent + for (int i = 0; i < 4; i++) { + auto [windowPlacement, handle] = createAndPlaceWindow(QSize(600, 500)); + QVERIFY(windowPlacement.initiallyConfiguredStates & Test::XdgToplevel::State::Maximized); + QCOMPARE(windowPlacement.initiallyConfiguredSize, QSize(1280, 1024 - 20)); + QCOMPARE(windowPlacement.finalGeometry, RectF(0, 20, 1280, 1024 - 20)); // under the panel + handles.push_back(std::move(handle)); + } +} + +void TestPlacement::testPlaceMaximizedLeavesFullscreen() +{ + setPlacementPolicy(PlacementMaximizing); + + // add a top panel + std::unique_ptr panelSurface{Test::createSurface()}; + std::unique_ptr panelShellSurface{Test::createLayerSurfaceV1(panelSurface.get(), QStringLiteral("dock"))}; + panelShellSurface->set_size(1280, 20); + panelShellSurface->set_anchor(Test::LayerSurfaceV1::anchor_top); + panelShellSurface->set_exclusive_zone(20); + panelSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QSignalSpy panelConfigureRequestedSpy(panelShellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(panelConfigureRequestedSpy.wait()); + Test::renderAndWaitForShown(panelSurface.get(), panelConfigureRequestedSpy.last().at(1).toSize(), Qt::blue); + + std::vector handles; + + // all windows should be initially fullscreen with an initial configure size sent, despite the policy + for (int i = 0; i < 4; i++) { + std::unique_ptr surface = Test::createSurface(); + auto shellSurface = Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly); + shellSurface->set_fullscreen(nullptr); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + auto initiallyConfiguredSize = toplevelConfigureRequestedSpy[0][0].toSize(); + auto initiallyConfiguredStates = toplevelConfigureRequestedSpy[0][1].value(); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy[0][0].toUInt()); + + auto window = Test::renderAndWaitForShown(surface.get(), initiallyConfiguredSize, Qt::red); + + QVERIFY(initiallyConfiguredStates & Test::XdgToplevel::State::Fullscreen); + QCOMPARE(initiallyConfiguredSize, QSize(1280, 1024)); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); + + handles.emplace_back(WindowHandle{ + .window = window, + .surface = std::move(surface), + .shellSurface = std::move(shellSurface), + }); + } +} + +void TestPlacement::testPlaceCentered() +{ + // This test verifies that Centered placement policy works. + + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("Placement", Placement::policyToString(PlacementCentered)); + group.sync(); + workspace()->slotReconfigure(); + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::red); + QVERIFY(window); + QCOMPARE(window->frameGeometry(), RectF(590, 487, 100, 50)); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void TestPlacement::testPlaceUnderMouse() +{ + // This test verifies that Under Mouse placement policy works. + + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("Placement", Placement::policyToString(PlacementUnderMouse)); + group.sync(); + workspace()->slotReconfigure(); + + KWin::input()->pointer()->warp(QPoint(200, 300)); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), QPoint(200, 300)); + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::red); + QVERIFY(window); + QCOMPARE(window->frameGeometry(), RectF(150, 275, 100, 50)); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void TestPlacement::testPlaceZeroCornered() +{ + // This test verifies that the Zero-Cornered placement policy works. + + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("Placement", Placement::policyToString(PlacementZeroCornered)); + group.sync(); + workspace()->slotReconfigure(); + + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + Window *window1 = Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::red); + QVERIFY(window1); + QCOMPARE(window1->pos(), QPoint(0, 0)); + QCOMPARE(window1->size(), QSize(100, 50)); + + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + Window *window2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 50), Qt::blue); + QVERIFY(window2); + QCOMPARE(window2->pos(), window1->pos() + workspace()->cascadeOffset(workspace()->clientArea(PlacementArea, window2))); + QCOMPARE(window2->size(), QSize(100, 50)); + + std::unique_ptr surface3(Test::createSurface()); + std::unique_ptr shellSurface3(Test::createXdgToplevelSurface(surface3.get())); + Window *window3 = Test::renderAndWaitForShown(surface3.get(), QSize(100, 50), Qt::green); + QVERIFY(window3); + QCOMPARE(window3->pos(), window2->pos() + workspace()->cascadeOffset(workspace()->clientArea(PlacementArea, window3))); + QCOMPARE(window3->size(), QSize(100, 50)); + + shellSurface3.reset(); + QVERIFY(Test::waitForWindowClosed(window3)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowClosed(window2)); + shellSurface1.reset(); + QVERIFY(Test::waitForWindowClosed(window1)); +} + +void TestPlacement::testPlaceRandom() +{ + // This test verifies that Random placement policy works. + + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("Placement", Placement::policyToString(PlacementRandom)); + group.sync(); + workspace()->slotReconfigure(); + + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + Window *window1 = Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::red); + QVERIFY(window1); + QCOMPARE(window1->size(), QSize(100, 50)); + + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + Window *window2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 50), Qt::blue); + QVERIFY(window2); + QVERIFY(window2->pos() != window1->pos()); + QCOMPARE(window2->size(), QSize(100, 50)); + + std::unique_ptr surface3(Test::createSurface()); + std::unique_ptr shellSurface3(Test::createXdgToplevelSurface(surface3.get())); + Window *window3 = Test::renderAndWaitForShown(surface3.get(), QSize(100, 50), Qt::green); + QVERIFY(window3); + QVERIFY(window3->pos() != window1->pos()); + QVERIFY(window3->pos() != window2->pos()); + QCOMPARE(window3->size(), QSize(100, 50)); + + shellSurface3.reset(); + QVERIFY(Test::waitForWindowClosed(window3)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowClosed(window2)); + shellSurface1.reset(); + QVERIFY(Test::waitForWindowClosed(window1)); +} + +void TestPlacement::testFullscreen() +{ + const QList outputs = workspace()->outputs(); + + setPlacementPolicy(PlacementSmart); + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::red); + QVERIFY(window); + window->sendToOutput(outputs[0]); + + // Wait for the configure event with the activated state. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + window->setFullScreen(true); + + QSignalSpy geometryChangedSpy(window, &Window::frameGeometryChanged); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::red); + QVERIFY(geometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), RectF(outputs[0]->geometry())); + + // this doesn't require a round trip, so should be immediate + window->sendToOutput(outputs[1]); + QCOMPARE(window->frameGeometry(), RectF(outputs[1]->geometry())); + QCOMPARE(geometryChangedSpy.count(), 2); +} + +void TestPlacement::testCascadeIfCovering() +{ + // This test verifies that the cascade-if-covering adjustment works for the Centered placement + // policy. + + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("Placement", Placement::policyToString(PlacementCentered)); + group.sync(); + workspace()->slotReconfigure(); + + // window should be in center + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + Window *window1 = Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::red); + QVERIFY(window1); + QCOMPARE(window1->pos(), QPoint(590, 487)); + QCOMPARE(window1->size(), QSize(100, 50)); + + // window should be cascaded to avoid overlapping window 1 + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + Window *window2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 50), Qt::blue); + QVERIFY(window2); + QCOMPARE(window2->pos(), window1->pos() + workspace()->cascadeOffset(workspace()->clientArea(PlacementArea, window2))); + QCOMPARE(window2->size(), QSize(100, 50)); + + // window should be cascaded to avoid overlapping window 1 and 2 + std::unique_ptr surface3(Test::createSurface()); + std::unique_ptr shellSurface3(Test::createXdgToplevelSurface(surface3.get())); + Window *window3 = Test::renderAndWaitForShown(surface3.get(), QSize(100, 50), Qt::green); + QVERIFY(window3); + QCOMPARE(window3->pos(), window2->pos() + workspace()->cascadeOffset(workspace()->clientArea(PlacementArea, window3))); + QCOMPARE(window3->size(), QSize(100, 50)); + + shellSurface3.reset(); + QVERIFY(Test::waitForWindowClosed(window3)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowClosed(window2)); + shellSurface1.reset(); + QVERIFY(Test::waitForWindowClosed(window1)); +} + +void TestPlacement::testCascadeIfCoveringIgnoreNonCovering() +{ + // This test verifies that the cascade-if-covering adjustment doesn't take effect when the + // other window wouldn't be fully covered. + + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("Placement", Placement::policyToString(PlacementCentered)); + group.sync(); + workspace()->slotReconfigure(); + + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + Window *window1 = Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::red); + QVERIFY(window1); + + // window should not be cascaded since it wouldn't fully overlap + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + Window *window2 = Test::renderAndWaitForShown(surface2.get(), QSize(50, 50), Qt::blue); + QVERIFY(window2); + QCOMPARE(window2->pos(), QPoint(615, 487)); + QCOMPARE(window2->size(), QSize(50, 50)); + + shellSurface2.reset(); + QVERIFY(Test::waitForWindowClosed(window2)); + shellSurface1.reset(); + QVERIFY(Test::waitForWindowClosed(window1)); +} + +void TestPlacement::testCascadeIfCoveringIgnoreOutOfArea() +{ + // This test verifies that the cascade-if-covering adjustment doesn't take effect when there is + // not enough space on the placement area to cascade. + + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("Placement", Placement::policyToString(PlacementCentered)); + group.sync(); + workspace()->slotReconfigure(); + + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + Window *window1 = Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::red); + QVERIFY(window1); + + // window should not be cascaded since it would be out of bounds of work area + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + Window *window2 = Test::renderAndWaitForShown(surface2.get(), QSize(1280, 1024), Qt::blue); + QVERIFY(window2); + QCOMPARE(window2->pos(), QPoint(0, 0)); + QCOMPARE(window2->size(), QSize(1280, 1024)); + + shellSurface2.reset(); + QVERIFY(Test::waitForWindowClosed(window2)); + shellSurface1.reset(); + QVERIFY(Test::waitForWindowClosed(window1)); +} + +void TestPlacement::testCascadeIfCoveringIgnoreAlreadyCovered() +{ + // This test verifies that the cascade-if-covering adjustment doesn't take effect when the + // other window is already fully covered by other windows anyway. + + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("Placement", Placement::policyToString(PlacementCentered)); + group.sync(); + workspace()->slotReconfigure(); + + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + Window *window1 = Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::red); + QVERIFY(window1); + + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + Window *window2 = Test::renderAndWaitForShown(surface2.get(), QSize(1280, 1024), Qt::blue); + QVERIFY(window2); + + // window should not be cascaded since the small window is already fully covered by the + // large window anyway + std::unique_ptr surface3(Test::createSurface()); + std::unique_ptr shellSurface3(Test::createXdgToplevelSurface(surface3.get())); + Window *window3 = Test::renderAndWaitForShown(surface3.get(), QSize(100, 50), Qt::green); + QVERIFY(window3); + QCOMPARE(window3->pos(), QPoint(590, 487)); + QCOMPARE(window3->size(), QSize(100, 50)); + + shellSurface3.reset(); + QVERIFY(Test::waitForWindowClosed(window3)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowClosed(window2)); + shellSurface1.reset(); + QVERIFY(Test::waitForWindowClosed(window1)); +} + +void TestPlacement::testTitlebarOnScreen_data() +{ + QTest::addColumn("placementMode"); + QTest::addRow("PlacementRandom") << PlacementPolicy::PlacementRandom; + QTest::addRow("PlacementSmart") << PlacementPolicy::PlacementSmart; + QTest::addRow("PlacementCentered") << PlacementPolicy::PlacementCentered; + QTest::addRow("PlacementZeroCornered") << PlacementPolicy::PlacementZeroCornered; + QTest::addRow("PlacementUnderMouse") << PlacementPolicy::PlacementUnderMouse; + QTest::addRow("PlacementMaximizing") << PlacementPolicy::PlacementMaximizing; +} + +void TestPlacement::testTitlebarOnScreen() +{ + // this test verifies that windows that are bigger than the screen + // still get placed with their title bar on the screen + + QFETCH(PlacementPolicy, placementMode); + setPlacementPolicy(PlacementPolicy(placementMode)); + + KWin::input()->pointer()->warp(QPoint(200, 0)); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), QPoint(200, 0)); + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, workspace()->outputs().front()->geometry().height() + 100), Qt::red); + QVERIFY(window); + QCOMPARE(window->frameGeometry().y(), 0); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +WAYLANDTEST_MAIN(TestPlacement) +#include "placement_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/plasma_surface_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/plasma_surface_test.cpp new file mode 100644 index 0000000000..21aa19e4fe --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/plasma_surface_test.cpp @@ -0,0 +1,299 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "pointer_input.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace KWin; + +Q_DECLARE_METATYPE(KWin::Layer) + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_plasma_surface-0"); + +class PlasmaSurfaceTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testRoleOnAllDesktops_data(); + void testRoleOnAllDesktops(); + void testAcceptsFocus_data(); + void testAcceptsFocus(); + void testOSDPlacement(); + void testOSDPlacementManualPosition(); + void testPanelActivate_data(); + void testPanelActivate(); + void testMovable_data(); + void testMovable(); + +private: + KWayland::Client::Compositor *m_compositor = nullptr; + KWayland::Client::PlasmaShell *m_plasmaShell = nullptr; +}; + +void PlasmaSurfaceTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->start(); + Test::setOutputConfig({Rect(0, 0, 1280, 1024)}); +} + +void PlasmaSurfaceTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::PlasmaShell)); + m_compositor = Test::waylandCompositor(); + m_plasmaShell = Test::waylandPlasmaShell(); + + KWin::input()->pointer()->warp(QPointF(640, 512)); +} + +void PlasmaSurfaceTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void PlasmaSurfaceTest::testRoleOnAllDesktops_data() +{ + QTest::addColumn("role"); + QTest::addColumn("expectedOnAllDesktops"); + + QTest::newRow("Desktop") << KWayland::Client::PlasmaShellSurface::Role::Desktop << true; + QTest::newRow("Panel") << KWayland::Client::PlasmaShellSurface::Role::Panel << true; + QTest::newRow("OSD") << KWayland::Client::PlasmaShellSurface::Role::OnScreenDisplay << true; + QTest::newRow("Normal") << KWayland::Client::PlasmaShellSurface::Role::Normal << false; + QTest::newRow("Notification") << KWayland::Client::PlasmaShellSurface::Role::Notification << true; + QTest::newRow("ToolTip") << KWayland::Client::PlasmaShellSurface::Role::ToolTip << true; + QTest::newRow("CriticalNotification") << KWayland::Client::PlasmaShellSurface::Role::CriticalNotification << true; + QTest::newRow("AppletPopup") << KWayland::Client::PlasmaShellSurface::Role::AppletPopup << true; +} + +void PlasmaSurfaceTest::testRoleOnAllDesktops() +{ + // this test verifies that a XdgShellClient is set on all desktops when the role changes + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + std::unique_ptr plasmaSurface(m_plasmaShell->createSurface(surface.get())); + QVERIFY(plasmaSurface != nullptr); + + // now render to map the window + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QCOMPARE(workspace()->activeWindow(), window); + + // currently the role is not yet set, so the window should not be on all desktops + QCOMPARE(window->isOnAllDesktops(), false); + + // now let's try to change that + QSignalSpy onAllDesktopsSpy(window, &Window::desktopsChanged); + QFETCH(KWayland::Client::PlasmaShellSurface::Role, role); + plasmaSurface->setRole(role); + QFETCH(bool, expectedOnAllDesktops); + QCOMPARE(onAllDesktopsSpy.wait(), expectedOnAllDesktops); + QCOMPARE(window->isOnAllDesktops(), expectedOnAllDesktops); + + // let's create a second window where we init a little bit different + // first creating the PlasmaSurface then the Shell Surface + std::unique_ptr surface2(Test::createSurface()); + QVERIFY(surface2 != nullptr); + std::unique_ptr plasmaSurface2(m_plasmaShell->createSurface(surface2.get())); + QVERIFY(plasmaSurface2 != nullptr); + plasmaSurface2->setRole(role); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + QVERIFY(shellSurface2 != nullptr); + auto c2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 50), Qt::blue); + QVERIFY(c2); + QVERIFY(window != c2); + + QCOMPARE(c2->isOnAllDesktops(), expectedOnAllDesktops); +} + +void PlasmaSurfaceTest::testAcceptsFocus_data() +{ + QTest::addColumn("role"); + QTest::addColumn("wantsInput"); + QTest::addColumn("active"); + + QTest::newRow("Desktop") << KWayland::Client::PlasmaShellSurface::Role::Desktop << true << true; + QTest::newRow("Panel") << KWayland::Client::PlasmaShellSurface::Role::Panel << false << false; + QTest::newRow("OSD") << KWayland::Client::PlasmaShellSurface::Role::OnScreenDisplay << false << false; + QTest::newRow("Normal") << KWayland::Client::PlasmaShellSurface::Role::Normal << true << true; + QTest::newRow("Notification") << KWayland::Client::PlasmaShellSurface::Role::Notification << false << false; + QTest::newRow("ToolTip") << KWayland::Client::PlasmaShellSurface::Role::ToolTip << false << false; + QTest::newRow("CriticalNotification") << KWayland::Client::PlasmaShellSurface::Role::CriticalNotification << false << false; + QTest::newRow("AppletPopup") << KWayland::Client::PlasmaShellSurface::Role::AppletPopup << true << true; +} + +void PlasmaSurfaceTest::testAcceptsFocus() +{ + // this test verifies that some surface roles don't get focus + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + std::unique_ptr plasmaSurface(m_plasmaShell->createSurface(surface.get())); + QVERIFY(plasmaSurface != nullptr); + QFETCH(KWayland::Client::PlasmaShellSurface::Role, role); + plasmaSurface->setRole(role); + + // now render to map the window + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + QVERIFY(window); + QTEST(window->wantsInput(), "wantsInput"); + QTEST(window->isActive(), "active"); +} + +void PlasmaSurfaceTest::testOSDPlacement() +{ + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + std::unique_ptr plasmaSurface(m_plasmaShell->createSurface(surface.get())); + QVERIFY(plasmaSurface != nullptr); + plasmaSurface->setRole(KWayland::Client::PlasmaShellSurface::Role::OnScreenDisplay); + + // now render and map the window + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + QVERIFY(window); + QCOMPARE(window->windowType(), WindowType::OnScreenDisplay); + QVERIFY(window->isOnScreenDisplay()); + QCOMPARE(window->frameGeometry(), RectF(1280 / 2 - 100 / 2, 2 * 1024 / 3 - 50 / 2, 100, 50)); + + // change the screen size + const QList geometries{Rect(0, 0, 1280, 1024), Rect(1280, 0, 1280, 1024)}; + Test::setOutputConfig(geometries); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), geometries[0]); + QCOMPARE(outputs[1]->geometry(), geometries[1]); + + QCOMPARE(window->frameGeometry(), RectF(1280 / 2 - 100 / 2, 2 * 1024 / 3 - 50 / 2, 100, 50)); + + // change size of window + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + Test::render(surface.get(), QSize(200, 100), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), RectF(1280 / 2 - 200 / 2, 2 * 1024 / 3 - 100 / 2, 200, 100)); +} + +void PlasmaSurfaceTest::testOSDPlacementManualPosition() +{ + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr plasmaSurface(m_plasmaShell->createSurface(surface.get())); + QVERIFY(plasmaSurface != nullptr); + plasmaSurface->setRole(KWayland::Client::PlasmaShellSurface::Role::OnScreenDisplay); + + plasmaSurface->setPosition(QPoint(50, 70)); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + + // now render and map the window + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + QVERIFY(window); + QVERIFY(!window->isPlaceable()); + QCOMPARE(window->windowType(), WindowType::OnScreenDisplay); + QVERIFY(window->isOnScreenDisplay()); + QCOMPARE(window->frameGeometry(), RectF(50, 70, 100, 50)); +} + +void PlasmaSurfaceTest::testPanelActivate_data() +{ + QTest::addColumn("wantsFocus"); + QTest::addColumn("active"); + + QTest::newRow("no focus") << false << false; + QTest::newRow("focus") << true << true; +} + +void PlasmaSurfaceTest::testPanelActivate() +{ + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + std::unique_ptr plasmaSurface(m_plasmaShell->createSurface(surface.get())); + QVERIFY(plasmaSurface != nullptr); + plasmaSurface->setRole(KWayland::Client::PlasmaShellSurface::Role::Panel); + QFETCH(bool, wantsFocus); + plasmaSurface->setPanelTakesFocus(wantsFocus); + + auto panel = Test::renderAndWaitForShown(surface.get(), QSize(100, 200), Qt::blue); + + QVERIFY(panel); + QCOMPARE(panel->windowType(), WindowType::Dock); + QVERIFY(panel->isDock()); + QFETCH(bool, active); + QCOMPARE(panel->wantsInput(), active); + QCOMPARE(panel->isActive(), active); +} + +void PlasmaSurfaceTest::testMovable_data() +{ + QTest::addColumn("role"); + QTest::addColumn("movable"); + QTest::addColumn("movableAcrossScreens"); + QTest::addColumn("resizable"); + + QTest::newRow("normal") << KWayland::Client::PlasmaShellSurface::Role::Normal << true << true << true; + QTest::newRow("desktop") << KWayland::Client::PlasmaShellSurface::Role::Desktop << false << false << false; + QTest::newRow("panel") << KWayland::Client::PlasmaShellSurface::Role::Panel << false << false << false; + QTest::newRow("osd") << KWayland::Client::PlasmaShellSurface::Role::OnScreenDisplay << false << false << false; +} + +void PlasmaSurfaceTest::testMovable() +{ + // this test verifies that certain window types from PlasmaShellSurface are not moveable or resizable + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + // and a PlasmaShellSurface + std::unique_ptr plasmaSurface(Test::waylandPlasmaShell()->createSurface(surface.get())); + QVERIFY(plasmaSurface != nullptr); + QFETCH(KWayland::Client::PlasmaShellSurface::Role, role); + plasmaSurface->setRole(role); + // let's render + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + QVERIFY(window); + QTEST(window->isMovable(), "movable"); + QTEST(window->isMovableAcrossScreens(), "movableAcrossScreens"); + QTEST(window->isResizable(), "resizable"); + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +WAYLANDTEST_MAIN(PlasmaSurfaceTest) +#include "plasma_surface_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/plasmawindow_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/plasmawindow_test.cpp new file mode 100644 index 0000000000..c70d4f1d2c --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/plasmawindow_test.cpp @@ -0,0 +1,285 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "pointer_input.h" +#include "wayland/seat.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include +// screenlocker +#if KWIN_BUILD_SCREENLOCKER +#include +#endif + +#include +#include + +#if KWIN_BUILD_X11 +#include "x11window.h" + +#include +#include +#endif + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_plasma-window-0"); + +class PlasmaWindowTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testCreateDestroyX11PlasmaWindow(); + void testInternalWindowNoPlasmaWindow(); + void testPopupWindowNoPlasmaWindow(); + void testLockScreenNoPlasmaWindow(); + void testDestroyedButNotUnmapped(); + +private: + KWayland::Client::PlasmaWindowManagement *m_windowManagement = nullptr; + KWayland::Client::Compositor *m_compositor = nullptr; +}; + +void PlasmaWindowTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); + setenv("QMLSCENE_DEVICE", "softwarecontext", true); +} + +void PlasmaWindowTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::WindowManagement)); + m_windowManagement = Test::waylandWindowManagement(); + m_compositor = Test::waylandCompositor(); + + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); +} + +void PlasmaWindowTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void PlasmaWindowTest::testCreateDestroyX11PlasmaWindow() +{ +#if KWIN_BUILD_X11 + // this test verifies that a PlasmaWindow gets unmapped on Client side when an X11 window is destroyed + QSignalSpy plasmaWindowCreatedSpy(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated); + + // create an xcb window + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + const Rect windowGeometry(0, 0, 100, 200); + xcb_window_t windowId = xcb_generate_id(c.get()); + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + // we should get a window for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.first().first().value(); + QVERIFY(window); + QCOMPARE(window->window(), windowId); + QVERIFY(window->isDecorated()); + QVERIFY(window->isActive()); + // verify that it gets the keyboard focus + if (!window->surface()) { + // we don't have a surface yet, so focused keyboard surface if set is not ours + QVERIFY(!waylandServer()->seat()->focusedKeyboardSurface()); + QVERIFY(Test::waitForWaylandSurface(window)); + } + QCOMPARE(waylandServer()->seat()->focusedKeyboardSurface(), window->surface()); + + // now that should also give it to us on client side + QVERIFY(plasmaWindowCreatedSpy.wait()); + QCOMPARE(plasmaWindowCreatedSpy.count(), 1); + QCOMPARE(m_windowManagement->windows().count(), 1); + auto pw = m_windowManagement->windows().first(); + QCOMPARE(pw->geometry(), QRect(window->frameGeometry().toRect())); + QSignalSpy geometryChangedSpy(pw, &KWayland::Client::PlasmaWindow::geometryChanged); + + QSignalSpy unmappedSpy(m_windowManagement->windows().first(), &KWayland::Client::PlasmaWindow::unmapped); + QSignalSpy destroyedSpy(m_windowManagement->windows().first(), &QObject::destroyed); + + // and destroy the window again + xcb_unmap_window(c.get(), windowId); + xcb_flush(c.get()); + + QSignalSpy windowClosedSpy(window, &X11Window::closed); + QVERIFY(windowClosedSpy.wait()); + xcb_destroy_window(c.get(), windowId); + c.reset(); + + QVERIFY(unmappedSpy.wait()); + QCOMPARE(unmappedSpy.count(), 1); + + QVERIFY(destroyedSpy.wait()); +#endif +} + +class HelperWindow : public QRasterWindow +{ + Q_OBJECT +public: + HelperWindow(); + ~HelperWindow() override; + +protected: + void paintEvent(QPaintEvent *event) override; +}; + +HelperWindow::HelperWindow() + : QRasterWindow(nullptr) +{ +} + +HelperWindow::~HelperWindow() = default; + +void HelperWindow::paintEvent(QPaintEvent *event) +{ + QPainter p(this); + p.fillRect(0, 0, width(), height(), Qt::red); +} + +void PlasmaWindowTest::testInternalWindowNoPlasmaWindow() +{ + // this test verifies that an internal window is not added as a PlasmaWindow + QSignalSpy plasmaWindowCreatedSpy(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.show(); + + QVERIFY(!plasmaWindowCreatedSpy.wait(100)); +} + +void PlasmaWindowTest::testPopupWindowNoPlasmaWindow() +{ + // this test verifies that a popup window is not added as a PlasmaWindow + QSignalSpy plasmaWindowCreatedSpy(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated); + + // first create the parent window + std::unique_ptr parentSurface(Test::createSurface()); + std::unique_ptr parentShellSurface(Test::createXdgToplevelSurface(parentSurface.get())); + Window *parentClient = Test::renderAndWaitForShown(parentSurface.get(), QSize(100, 50), Qt::blue); + QVERIFY(parentClient); + QVERIFY(plasmaWindowCreatedSpy.wait()); + QCOMPARE(plasmaWindowCreatedSpy.count(), 1); + + // now let's create a popup window for it + std::unique_ptr positioner(Test::createXdgPositioner()); + positioner->set_size(10, 10); + positioner->set_anchor_rect(0, 0, 10, 10); + positioner->set_anchor(Test::XdgPositioner::anchor_bottom_right); + positioner->set_gravity(Test::XdgPositioner::gravity_bottom_right); + std::unique_ptr popupSurface(Test::createSurface()); + std::unique_ptr popupShellSurface(Test::createXdgPopupSurface(popupSurface.get(), parentShellSurface->xdgSurface(), positioner.get())); + Window *popupWindow = Test::renderAndWaitForShown(popupSurface.get(), QSize(10, 10), Qt::blue); + QVERIFY(popupWindow); + QVERIFY(!plasmaWindowCreatedSpy.wait(100)); + QCOMPARE(plasmaWindowCreatedSpy.count(), 1); + + // let's destroy the windows + popupShellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(popupWindow)); + parentShellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(parentClient)); +} + +void PlasmaWindowTest::testLockScreenNoPlasmaWindow() +{ +#if KWIN_BUILD_SCREENLOCKER + // this test verifies that lock screen windows are not exposed to PlasmaWindow + QSignalSpy plasmaWindowCreatedSpy(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated); + + // this time we use a QSignalSpy on XdgShellClient as it'a a little bit more complex setup + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + // lock + ScreenLocker::KSldApp::self()->lock(ScreenLocker::EstablishLock::Immediate); + QVERIFY(windowAddedSpy.wait()); + QVERIFY(windowAddedSpy.first().first().value()->isLockScreen()); + // should not be sent to the window + QVERIFY(plasmaWindowCreatedSpy.isEmpty()); + QVERIFY(!plasmaWindowCreatedSpy.wait(100)); + + // fake unlock + QSignalSpy lockStateChangedSpy(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged); + const auto children = ScreenLocker::KSldApp::self()->children(); + for (auto it = children.begin(); it != children.end(); ++it) { + if (qstrcmp((*it)->metaObject()->className(), "LogindIntegration") != 0) { + continue; + } + QMetaObject::invokeMethod(*it, "requestUnlock"); + break; + } + QVERIFY(lockStateChangedSpy.wait()); + QVERIFY(!waylandServer()->isScreenLocked()); +#else + QSKIP("KWin was built without lockscreen support"); +#endif +} + +void PlasmaWindowTest::testDestroyedButNotUnmapped() +{ + // this test verifies that also when a ShellSurface gets destroyed without a prior unmap + // the PlasmaWindow gets destroyed on Client side + QSignalSpy plasmaWindowCreatedSpy(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated); + + // first create the parent window + std::unique_ptr parentSurface(Test::createSurface()); + std::unique_ptr parentShellSurface(Test::createXdgToplevelSurface(parentSurface.get())); + // map that window + Test::render(parentSurface.get(), QSize(100, 50), Qt::blue); + // this should create a plasma window + QVERIFY(plasmaWindowCreatedSpy.wait()); + QCOMPARE(plasmaWindowCreatedSpy.count(), 1); + auto window = plasmaWindowCreatedSpy.first().first().value(); + QVERIFY(window); + QSignalSpy destroyedSpy(window, &QObject::destroyed); + + // now destroy without an unmap + parentShellSurface.reset(); + parentSurface.reset(); + QVERIFY(destroyedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::PlasmaWindowTest) +#include "plasmawindow_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/platformcursor.cpp b/local/recipes/kde/kwin/source/autotests/integration/platformcursor.cpp new file mode 100644 index 0000000000..29a5f9e20e --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/platformcursor.cpp @@ -0,0 +1,65 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "cursor.h" +#include "wayland_server.h" + +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_platform_cursor-0"); + +class PlatformCursorTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testPos(); +}; + +void PlatformCursorTest::initTestCase() +{ + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + + // QCursor requires QScreen but our QPA will create QScreen later on a timer timeout. + QSignalSpy screenAddedSpy(qGuiApp, &QGuiApplication::screenAdded); + QVERIFY(screenAddedSpy.wait()); +} + +void PlatformCursorTest::testPos() +{ + // this test verifies that the PlatformCursor of the QPA plugin forwards ::pos and ::setPos correctly + // that is QCursor should work just like KWin::Cursor + + // cursor should be centered on screen + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(640, 512)); + + // let's set the pos through QCursor API + QCursor::setPos(QPoint(10, 10)); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(10, 10)); + QCOMPARE(QCursor::pos(), QPoint(10, 10)); + + // and let's set the pos through Cursor API + QCursor::setPos(QPoint(20, 20)); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(20, 20)); + QCOMPARE(QCursor::pos(), QPoint(20, 20)); +} + +} + +WAYLANDTEST_MAIN(KWin::PlatformCursorTest) +#include "platformcursor.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/pointer_constraints_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/pointer_constraints_test.cpp new file mode 100644 index 0000000000..eb50d7d1a3 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/pointer_constraints_test.cpp @@ -0,0 +1,364 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "cursor.h" +#include "keyboard_input.h" +#include "pointer_input.h" +#include "wayland/seat.h" +#include "wayland/surface.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +using namespace KWin; + +typedef std::function PointerFunc; +Q_DECLARE_METATYPE(PointerFunc) + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_pointer_constraints-0"); + +class TestPointerConstraints : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testConfinedPointer_data(); + void testConfinedPointer(); + void testLockedPointer(); + void testCloseWindowWithLockedPointer(); +}; + +void TestPointerConstraints::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + // set custom config which disables the OnScreenNotification + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup group = config->group(QStringLiteral("OnScreenNotification")); + group.writeEntry(QStringLiteral("QmlPath"), QString("/does/not/exist.qml")); + group.sync(); + + kwinApp()->setConfig(config); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); +} + +void TestPointerConstraints::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::PointerConstraints)); + QVERIFY(Test::waitForWaylandPointer()); + + workspace()->setActiveOutput(QPoint(640, 512)); + KWin::input()->pointer()->warp(QPoint(640, 512)); +} + +void TestPointerConstraints::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TestPointerConstraints::testConfinedPointer_data() +{ + QTest::addColumn("positionFunction"); + QTest::addColumn("xOffset"); + QTest::addColumn("yOffset"); + PointerFunc bottomLeft = [](const RectF &rect) { + return QPointF(rect.left(), rect.bottom() - 1); + }; + PointerFunc bottomRight = [](const RectF &rect) { + return QPointF(rect.right() - 1, rect.bottom() - 1); + }; + PointerFunc topRight = [](const RectF &rect) { + return QPointF(rect.right() - 1, rect.top()); + }; + PointerFunc topLeft = [](const RectF &rect) { + return QPointF(rect.left(), rect.top()); + }; + + QTest::newRow("XdgWmBase - bottomLeft") << bottomLeft << -1 << 1; + QTest::newRow("XdgWmBase - bottomRight") << bottomRight << 1 << 1; + QTest::newRow("XdgWmBase - topLeft") << topLeft << -1 << -1; + QTest::newRow("XdgWmBase - topRight") << topRight << 1 << -1; +} + +void TestPointerConstraints::testConfinedPointer() +{ + // this test sets up a Surface with a confined pointer + // simple interaction test to verify that the pointer gets confined + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + std::unique_ptr pointer(Test::waylandSeat()->createPointer()); + std::unique_ptr confinedPointer(Test::waylandPointerConstraints()->confinePointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::OneShot)); + QSignalSpy confinedSpy(confinedPointer.get(), &KWayland::Client::ConfinedPointer::confined); + QSignalSpy unconfinedSpy(confinedPointer.get(), &KWayland::Client::ConfinedPointer::unconfined); + + // now map the window + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue); + QVERIFY(window); + if (window->pos() == QPoint(0, 0)) { + window->move(QPoint(1, 1)); + } + QVERIFY(!window->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos())); + + // now let's confine + QCOMPARE(input()->pointer()->isConstrained(), false); + KWin::input()->pointer()->warp(window->frameGeometry().center()); + QCOMPARE(input()->pointer()->isConstrained(), true); + QVERIFY(confinedSpy.wait()); + + // picking a position outside the window geometry should not move pointer + QSignalSpy pointerPositionChangedSpy(input(), &InputRedirection::globalPointerChanged); + KWin::input()->pointer()->warp(QPoint(512, 512)); + QVERIFY(pointerPositionChangedSpy.isEmpty()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center()); + + // TODO: test relative motion + QFETCH(PointerFunc, positionFunction); + const QPointF position = positionFunction(window->frameGeometry()); + KWin::input()->pointer()->warp(position); + QCOMPARE(pointerPositionChangedSpy.count(), 1); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), position); + // moving one to right should not be possible + QFETCH(int, xOffset); + KWin::input()->pointer()->warp(position + QPoint(xOffset, 0)); + QCOMPARE(pointerPositionChangedSpy.count(), 1); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), position); + // moving one to bottom should not be possible + QFETCH(int, yOffset); + KWin::input()->pointer()->warp(position + QPoint(0, yOffset)); + QCOMPARE(pointerPositionChangedSpy.count(), 1); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), position); + + // modifier + click should be ignored + // first ensure the settings are ok + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("MouseBindings")); + group.writeEntry("CommandAllKey", QStringLiteral("Meta")); + group.writeEntry("CommandAll1", "Move"); + group.writeEntry("CommandAll2", "Move"); + group.writeEntry("CommandAll3", "Move"); + group.writeEntry("CommandAllWheel", "change opacity"); + group.sync(); + workspace()->slotReconfigure(); + QCOMPARE(options->commandAllModifier(), Qt::MetaModifier); + QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove); + QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove); + QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove); + + quint32 timestamp = 1; + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + QVERIFY(!window->isInteractiveMove()); + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + + // set the opacity to 0.5 + window->setOpacity(0.5); + QCOMPARE(window->opacity(), 0.5); + + // pointer is confined so shortcut should not work + Test::pointerAxisVertical(-15, timestamp++); + QCOMPARE(window->opacity(), 0.5); + Test::pointerAxisVertical(15, timestamp++); + QCOMPARE(window->opacity(), 0.5); + + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + + // deactivate the window, this should unconfine + workspace()->activateWindow(nullptr); + QVERIFY(unconfinedSpy.wait()); + QCOMPARE(input()->pointer()->isConstrained(), false); + + // reconfine pointer (this time with persistent life time) + confinedPointer.reset(Test::waylandPointerConstraints()->confinePointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::Persistent)); + QSignalSpy confinedSpy2(confinedPointer.get(), &KWayland::Client::ConfinedPointer::confined); + QSignalSpy unconfinedSpy2(confinedPointer.get(), &KWayland::Client::ConfinedPointer::unconfined); + + // activate it again, this confines again + workspace()->activateWindow(static_cast(input()->pointer()->focus())); + QVERIFY(confinedSpy2.wait()); + QCOMPARE(input()->pointer()->isConstrained(), true); + + // deactivate the window one more time with the persistent life time constraint, this should unconfine + workspace()->activateWindow(nullptr); + QVERIFY(unconfinedSpy2.wait()); + QCOMPARE(input()->pointer()->isConstrained(), false); + // activate it again, this confines again + workspace()->activateWindow(static_cast(input()->pointer()->focus())); + QVERIFY(confinedSpy2.wait()); + QCOMPARE(input()->pointer()->isConstrained(), true); + + // create a second window and move it above our constrained window + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + auto c2 = Test::renderAndWaitForShown(surface2.get(), QSize(1280, 1024), Qt::blue); + QVERIFY(c2); + QVERIFY(unconfinedSpy2.wait()); + // and unmapping the second window should confine again + shellSurface2.reset(); + surface2.reset(); + QVERIFY(confinedSpy2.wait()); + + // let's set a region which results in unconfined + auto r = Test::waylandCompositor()->createRegion(QRegion(2, 2, 3, 3)); + confinedPointer->setRegion(r.get()); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(unconfinedSpy2.wait()); + QCOMPARE(input()->pointer()->isConstrained(), false); + // and set a full region again, that should confine + confinedPointer->setRegion(nullptr); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(confinedSpy2.wait()); + QCOMPARE(input()->pointer()->isConstrained(), true); + + // delete pointer confine + confinedPointer.reset(nullptr); + Test::flushWaylandConnection(); + + QSignalSpy constraintsChangedSpy(input()->pointer()->focus()->surface(), &SurfaceInterface::pointerConstraintsChanged); + QVERIFY(constraintsChangedSpy.wait()); + + // should be unconfined + QCOMPARE(input()->pointer()->isConstrained(), false); + + // confine again + confinedPointer.reset(Test::waylandPointerConstraints()->confinePointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::Persistent)); + QSignalSpy confinedSpy3(confinedPointer.get(), &KWayland::Client::ConfinedPointer::confined); + QVERIFY(confinedSpy3.wait()); + QCOMPARE(input()->pointer()->isConstrained(), true); + + // and now unmap + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); + QCOMPARE(input()->pointer()->isConstrained(), false); +} + +void TestPointerConstraints::testLockedPointer() +{ + // this test sets up a Surface with a locked pointer + // simple interaction test to verify that the pointer gets locked + // the various ways to unlock are not tested as that's already verified by testConfinedPointer + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + std::unique_ptr pointer(Test::waylandSeat()->createPointer()); + std::unique_ptr lockedPointer(Test::waylandPointerConstraints()->lockPointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::OneShot)); + QSignalSpy lockedSpy(lockedPointer.get(), &KWayland::Client::LockedPointer::locked); + QSignalSpy unlockedSpy(lockedPointer.get(), &KWayland::Client::LockedPointer::unlocked); + + // now map the window + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue); + QVERIFY(window); + QVERIFY(!window->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos())); + + // now let's lock + QCOMPARE(input()->pointer()->isConstrained(), false); + KWin::input()->pointer()->warp(window->frameGeometry().center()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center()); + QCOMPARE(input()->pointer()->isConstrained(), true); + QVERIFY(lockedSpy.wait()); + + // try to move the pointer + // TODO: add relative pointer + KWin::input()->pointer()->warp(window->frameGeometry().center() + QPoint(1, 1)); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center()); + + // deactivate the window, this should unlock + workspace()->activateWindow(nullptr); + QCOMPARE(input()->pointer()->isConstrained(), false); + QVERIFY(unlockedSpy.wait()); + + // moving cursor should be allowed again + KWin::input()->pointer()->warp(window->frameGeometry().center() + QPoint(1, 1)); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center() + QPoint(1, 1)); + + lockedPointer.reset(Test::waylandPointerConstraints()->lockPointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::Persistent)); + QSignalSpy lockedSpy2(lockedPointer.get(), &KWayland::Client::LockedPointer::locked); + + // activate the window again, this should lock again + workspace()->activateWindow(static_cast(input()->pointer()->focus())); + QVERIFY(lockedSpy2.wait()); + QCOMPARE(input()->pointer()->isConstrained(), true); + + // try to move the pointer + QCOMPARE(input()->pointer()->isConstrained(), true); + KWin::input()->pointer()->warp(window->frameGeometry().center()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center() + QPoint(1, 1)); + + // delete pointer lock + lockedPointer.reset(nullptr); + Test::flushWaylandConnection(); + + QSignalSpy constraintsChangedSpy(input()->pointer()->focus()->surface(), &SurfaceInterface::pointerConstraintsChanged); + QVERIFY(constraintsChangedSpy.wait()); + + // moving cursor should be allowed again + QCOMPARE(input()->pointer()->isConstrained(), false); + KWin::input()->pointer()->warp(window->frameGeometry().center()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center()); +} + +void TestPointerConstraints::testCloseWindowWithLockedPointer() +{ + // test case which verifies that the pointer gets unlocked when the window for it gets closed + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + std::unique_ptr pointer(Test::waylandSeat()->createPointer()); + std::unique_ptr lockedPointer(Test::waylandPointerConstraints()->lockPointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::OneShot)); + QSignalSpy lockedSpy(lockedPointer.get(), &KWayland::Client::LockedPointer::locked); + QSignalSpy unlockedSpy(lockedPointer.get(), &KWayland::Client::LockedPointer::unlocked); + + // now map the window + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue); + QVERIFY(window); + QVERIFY(!window->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos())); + + // now let's lock + QCOMPARE(input()->pointer()->isConstrained(), false); + KWin::input()->pointer()->warp(window->frameGeometry().center()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center()); + QCOMPARE(input()->pointer()->isConstrained(), true); + QVERIFY(lockedSpy.wait()); + + // close the window + shellSurface.reset(); + surface.reset(); + // this should result in unlocked + QVERIFY(unlockedSpy.wait()); + QCOMPARE(input()->pointer()->isConstrained(), false); +} + +WAYLANDTEST_MAIN(TestPointerConstraints) +#include "pointer_constraints_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/pointer_input.cpp b/local/recipes/kde/kwin/source/autotests/integration/pointer_input.cpp new file mode 100644 index 0000000000..93352a00f0 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/pointer_input.cpp @@ -0,0 +1,1944 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "core/outputconfiguration.h" +#include "cursor.h" +#include "cursorsource.h" +#include "effect/effecthandler.h" +#include "options.h" +#include "pointer_input.h" +#include "utils/cursortheme.h" +#include "virtualdesktops.h" +#include "wayland/seat.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#if KWIN_BUILD_X11 +#include "x11window.h" + +#include +#endif + +namespace KWin +{ + +static PlatformCursorImage loadReferenceThemeCursor(const QByteArray &name) +{ + const Cursor *pointerCursor = Cursors::self()->mouse(); + + const CursorTheme theme(pointerCursor->themeName(), pointerCursor->themeSize(), kwinApp()->devicePixelRatio()); + if (theme.isEmpty()) { + return PlatformCursorImage(); + } + + ShapeCursorSource source; + source.setShape(name); + source.setTheme(theme); + + return PlatformCursorImage(source.image(), source.hotspot()); +} + +static PlatformCursorImage loadReferenceThemeCursor(const CursorShape &shape) +{ + return loadReferenceThemeCursor(shape.name()); +} + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_pointer_input-0"); + +class PointerInputTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testWarpingUpdatesFocus(); + void testWarpingGeneratesPointerMotion(); + void testWarpingBetweenWindows(); + void testUpdateFocusAfterScreenChange(); + void testUpdateFocusOnDecorationDestroy(); + void testModifierClickUnrestrictedMove_data(); + void testModifierClickUnrestrictedMove(); + void testModifierClickUnrestrictedFullscreenMove(); + void testModifierClickUnrestrictedMoveGlobalShortcutsDisabled(); + void testModifierScrollOpacity_data(); + void testModifierScrollOpacity(); + void testModifierScrollOpacityGlobalShortcutsDisabled(); + void testScrollAction(); + void testFocusFollowsMouse(); + void testMouseActionInactiveWindow_data(); + void testMouseActionInactiveWindow(); + void testMouseActionActiveWindow_data(); + void testMouseActionActiveWindow(); + void testCursorImage(); + void testCursorShapeV1(); + void testEffectOverrideCursorImage(); + void testPopup(); + void testDecoCancelsPopup(); + void testWindowUnderCursorWhileButtonPressed(); + void testConfineToScreenGeometry_data(); + void testConfineToScreenGeometry(); + void testEdgeBarrier_data(); + void testEdgeBarrier(); + void testResizeCursor_data(); + void testResizeCursor(); + void testMoveCursor(); + void testHideShowCursor(); + void testDefaultInputRegion(); + void testEmptyInputRegion(); + void testUnfocusedModifiers(); + +private: + void render(KWayland::Client::Surface *surface, const QSize &size = QSize(100, 50)); + KWayland::Client::Compositor *m_compositor = nullptr; + KWayland::Client::Seat *m_seat = nullptr; +}; + +void PointerInputTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + qputenv("XCURSOR_THEME", QByteArrayLiteral("breeze_cursors")); + qputenv("XCURSOR_SIZE", QByteArrayLiteral("24")); + qputenv("XKB_DEFAULT_RULES", "evdev"); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); +} + +void PointerInputTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::XdgDecorationV1 | Test::AdditionalWaylandInterface::CursorShapeV1)); + QVERIFY(Test::waitForWaylandPointer()); + m_compositor = Test::waylandCompositor(); + m_seat = Test::waylandSeat(); + + auto group = kwinApp()->config()->group(QStringLiteral("EdgeBarrier")); + group.writeEntry("EdgeBarrier", 0); + group.writeEntry("CornerBarrier", false); + group.sync(); + Workspace::self()->slotReconfigure(); + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); +} + +void PointerInputTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void PointerInputTest::render(KWayland::Client::Surface *surface, const QSize &size) +{ + Test::render(surface, size, Qt::blue); + Test::flushWaylandConnection(); +} + +void PointerInputTest::testWarpingUpdatesFocus() +{ + // this test verifies that warping the pointer creates pointer enter and leave events + + // create pointer and signal spy for enter and leave signals + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy enteredSpy(pointer, &KWayland::Client::Pointer::entered); + QSignalSpy leftSpy(pointer, &KWayland::Client::Pointer::left); + + // create a window + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + render(surface.get()); + QVERIFY(windowAddedSpy.wait()); + Window *window = workspace()->activeWindow(); + QVERIFY(window); + + // currently there should not be a focused pointer surface + QVERIFY(!waylandServer()->seat()->focusedPointerSurface()); + QVERIFY(!pointer->enteredSurface()); + + // enter + input()->pointer()->warp(QPoint(25, 25)); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 1); + QCOMPARE(enteredSpy.first().at(1).toPointF(), QPointF(25, 25)); + // window should have focus + QCOMPARE(pointer->enteredSurface(), surface.get()); + // also on the server + QCOMPARE(waylandServer()->seat()->focusedPointerSurface(), window->surface()); + + // and out again + input()->pointer()->warp(QPoint(250, 250)); + QVERIFY(leftSpy.wait()); + QCOMPARE(leftSpy.count(), 1); + // there should not be a focused pointer surface anymore + QVERIFY(!waylandServer()->seat()->focusedPointerSurface()); + QVERIFY(!pointer->enteredSurface()); +} + +void PointerInputTest::testWarpingGeneratesPointerMotion() +{ + // this test verifies that warping the pointer creates pointer motion events + + // create pointer and signal spy for enter and motion + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy enteredSpy(pointer, &KWayland::Client::Pointer::entered); + QSignalSpy movedSpy(pointer, &KWayland::Client::Pointer::motion); + + // create a window + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + render(surface.get()); + QVERIFY(windowAddedSpy.wait()); + Window *window = workspace()->activeWindow(); + QVERIFY(window); + + // enter + Test::pointerMotion(QPointF(25, 25), 1); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.first().at(1).toPointF(), QPointF(25, 25)); + + // now warp + input()->pointer()->warp(QPoint(26, 26)); + QVERIFY(movedSpy.wait()); + QCOMPARE(movedSpy.count(), 1); + QCOMPARE(movedSpy.last().first().toPointF(), QPointF(26, 26)); +} + +void PointerInputTest::testWarpingBetweenWindows() +{ + // This test verifies that the compositor will send correct events when the pointer + // leaves one window and enters another window. + + std::unique_ptr pointer(m_seat->createPointer(m_seat)); + QSignalSpy enteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); + QSignalSpy leftSpy(pointer.get(), &KWayland::Client::Pointer::left); + QSignalSpy motionSpy(pointer.get(), &KWayland::Client::Pointer::motion); + + // create windows + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + auto window1 = Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::cyan); + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + auto window2 = Test::renderAndWaitForShown(surface2.get(), QSize(200, 100), Qt::red); + + // place windows side by side + window1->move(QPoint(0, 0)); + window2->move(QPoint(100, 0)); + + quint32 timestamp = 0; + + // put the pointer at the center of the first window + Test::pointerMotion(window1->frameGeometry().center(), timestamp++); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 1); + QCOMPARE(enteredSpy.last().at(1).toPointF(), QPointF(50, 25)); + QCOMPARE(leftSpy.count(), 0); + QCOMPARE(motionSpy.count(), 0); + QCOMPARE(pointer->enteredSurface(), surface1.get()); + + // put the pointer at the center of the second window + Test::pointerMotion(window2->frameGeometry().center(), timestamp++); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 2); + QCOMPARE(enteredSpy.last().at(1).toPointF(), QPointF(100, 50)); + QCOMPARE(leftSpy.count(), 1); + QCOMPARE(motionSpy.count(), 0); + QCOMPARE(pointer->enteredSurface(), surface2.get()); +} + +void PointerInputTest::testUpdateFocusAfterScreenChange() +{ + // this test verifies that a pointer enter event is generated when the cursor changes to another + // screen due to removal of screen + + // create pointer and signal spy for enter and motion + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy enteredSpy(pointer, &KWayland::Client::Pointer::entered); + QSignalSpy leftSpy(pointer, &KWayland::Client::Pointer::left); + + // create a window + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + render(surface.get(), QSize(1280, 1024)); + QVERIFY(windowAddedSpy.wait()); + Window *window = workspace()->activeWindow(); + QVERIFY(window); + QVERIFY(window->frameGeometry().contains(Cursors::self()->mouse()->pos())); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 1); + + // move the cursor to the second screen + input()->pointer()->warp(QPointF(1500, 300)); + QVERIFY(!window->frameGeometry().contains(Cursors::self()->mouse()->pos())); + QVERIFY(leftSpy.wait()); + + // now let's remove the screen containing the cursor + Test::setOutputConfig({Rect(0, 0, 1280, 1024)}); + QCOMPARE(workspace()->outputs().count(), 1); + + // this should have warped the cursor + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(640, 512)); + QVERIFY(window->frameGeometry().contains(Cursors::self()->mouse()->pos())); + + // and we should get an enter event + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 2); +} + +void PointerInputTest::testUpdateFocusOnDecorationDestroy() +{ + // This test verifies that a maximized window gets it's pointer focus + // if decoration was focused and then destroyed on maximize with BorderlessMaximizedWindows option. + + // create pointer for focus tracking + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy buttonStateChangedSpy(pointer, &KWayland::Client::Pointer::buttonStateChanged); + + // Enable the borderless maximized windows option. + auto group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("BorderlessMaximizedWindows", true); + group.sync(); + Workspace::self()->slotReconfigure(); + QCOMPARE(options->borderlessMaximizedWindows(), true); + + // Create the test window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + std::unique_ptr decoration(Test::createXdgToplevelDecorationV1(shellSurface.get())); + + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QSignalSpy decorationConfigureRequestedSpy(decoration.get(), &Test::XdgToplevelDecorationV1::configureRequested); + decoration->set_mode(Test::XdgToplevelDecorationV1::mode_server_side); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the initial configure event. + Test::XdgToplevel::States states; + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(0, 0)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Map the window. + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(window->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(window->isDecorated(), true); + + // We should receive a configure event when the window becomes active. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Simulate decoration hover + quint32 timestamp = 0; + Test::pointerMotion(window->frameGeometry().topLeft(), timestamp++); + QVERIFY(input()->pointer()->decoration()); + + // Maximize when on decoration + workspace()->slotWindowMaximize(); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(1280, 1024), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + QCOMPARE(window->isDecorated(), false); + + // Window should have focus, BUG 411884 + QVERIFY(!input()->pointer()->decoration()); + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + QVERIFY(buttonStateChangedSpy.wait()); + QCOMPARE(pointer->enteredSurface(), surface.get()); + + // Destroy the window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void PointerInputTest::testModifierClickUnrestrictedMove_data() +{ + QTest::addColumn("modifierKey"); + QTest::addColumn("mouseButton"); + QTest::addColumn("modKey"); + QTest::addColumn("capsLock"); + + const QString alt = QStringLiteral("Alt"); + const QString meta = QStringLiteral("Meta"); + + QTest::newRow("Left Alt + Left Click") << KEY_LEFTALT << BTN_LEFT << alt << false; + QTest::newRow("Left Alt + Right Click") << KEY_LEFTALT << BTN_RIGHT << alt << false; + QTest::newRow("Left Alt + Middle Click") << KEY_LEFTALT << BTN_MIDDLE << alt << false; + QTest::newRow("Right Alt + Left Click") << KEY_RIGHTALT << BTN_LEFT << alt << false; + QTest::newRow("Right Alt + Right Click") << KEY_RIGHTALT << BTN_RIGHT << alt << false; + QTest::newRow("Right Alt + Middle Click") << KEY_RIGHTALT << BTN_MIDDLE << alt << false; + // now everything with meta + QTest::newRow("Left Meta + Left Click") << KEY_LEFTMETA << BTN_LEFT << meta << false; + QTest::newRow("Left Meta + Right Click") << KEY_LEFTMETA << BTN_RIGHT << meta << false; + QTest::newRow("Left Meta + Middle Click") << KEY_LEFTMETA << BTN_MIDDLE << meta << false; + QTest::newRow("Right Meta + Left Click") << KEY_RIGHTMETA << BTN_LEFT << meta << false; + QTest::newRow("Right Meta + Right Click") << KEY_RIGHTMETA << BTN_RIGHT << meta << false; + QTest::newRow("Right Meta + Middle Click") << KEY_RIGHTMETA << BTN_MIDDLE << meta << false; + + // and with capslock + QTest::newRow("Left Alt + Left Click/CapsLock") << KEY_LEFTALT << BTN_LEFT << alt << true; + QTest::newRow("Left Alt + Right Click/CapsLock") << KEY_LEFTALT << BTN_RIGHT << alt << true; + QTest::newRow("Left Alt + Middle Click/CapsLock") << KEY_LEFTALT << BTN_MIDDLE << alt << true; + QTest::newRow("Right Alt + Left Click/CapsLock") << KEY_RIGHTALT << BTN_LEFT << alt << true; + QTest::newRow("Right Alt + Right Click/CapsLock") << KEY_RIGHTALT << BTN_RIGHT << alt << true; + QTest::newRow("Right Alt + Middle Click/CapsLock") << KEY_RIGHTALT << BTN_MIDDLE << alt << true; + // now everything with meta + QTest::newRow("Left Meta + Left Click/CapsLock") << KEY_LEFTMETA << BTN_LEFT << meta << true; + QTest::newRow("Left Meta + Right Click/CapsLock") << KEY_LEFTMETA << BTN_RIGHT << meta << true; + QTest::newRow("Left Meta + Middle Click/CapsLock") << KEY_LEFTMETA << BTN_MIDDLE << meta << true; + QTest::newRow("Right Meta + Left Click/CapsLock") << KEY_RIGHTMETA << BTN_LEFT << meta << true; + QTest::newRow("Right Meta + Right Click/CapsLock") << KEY_RIGHTMETA << BTN_RIGHT << meta << true; + QTest::newRow("Right Meta + Middle Click/CapsLock") << KEY_RIGHTMETA << BTN_MIDDLE << meta << true; +} + +void PointerInputTest::testModifierClickUnrestrictedMove() +{ + // this test ensures that Alt+mouse button press triggers unrestricted move + + // create pointer and signal spy for button events + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy buttonSpy(pointer, &KWayland::Client::Pointer::buttonStateChanged); + + // first modify the config for this run + QFETCH(QString, modKey); + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("MouseBindings")); + group.writeEntry("CommandAllKey", modKey); + group.writeEntry("CommandAll1", "Move"); + group.writeEntry("CommandAll2", "Move"); + group.writeEntry("CommandAll3", "Move"); + group.sync(); + workspace()->slotReconfigure(); + QCOMPARE(options->commandAllModifier(), modKey == QStringLiteral("Alt") ? Qt::AltModifier : Qt::MetaModifier); + QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove); + QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove); + QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove); + + // create a window + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + render(surface.get()); + QVERIFY(windowAddedSpy.wait()); + Window *window = workspace()->activeWindow(); + QVERIFY(window); + + // move cursor on window + input()->pointer()->warp(window->frameGeometry().center()); + + // simulate modifier+click + quint32 timestamp = 1; + QFETCH(bool, capsLock); + if (capsLock) { + Test::keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); + } + QFETCH(int, modifierKey); + QFETCH(int, mouseButton); + Test::keyboardKeyPressed(modifierKey, timestamp++); + QVERIFY(!window->isInteractiveMove()); + Test::pointerButtonPressed(mouseButton, timestamp++); + QVERIFY(window->isInteractiveMove()); + // release modifier should not change it + Test::keyboardKeyReleased(modifierKey, timestamp++); + QVERIFY(window->isInteractiveMove()); + // but releasing the key should end move/resize + Test::pointerButtonReleased(mouseButton, timestamp++); + QVERIFY(!window->isInteractiveMove()); + if (capsLock) { + Test::keyboardKeyReleased(KEY_CAPSLOCK, timestamp++); + } + + // all of that should not have triggered button events on the surface + QCOMPARE(buttonSpy.count(), 0); + // also waiting shouldn't give us the event + QVERIFY(Test::waylandSync()); + QCOMPARE(buttonSpy.count(), 0); +} + +void PointerInputTest::testModifierClickUnrestrictedFullscreenMove() +{ + // this test ensures that Meta+mouse button press triggers unrestricted move for fullscreen windows + if (workspace()->outputs().size() < 2) { + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + } + + // first modify the config for this run + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("MouseBindings")); + group.writeEntry("CommandAllKey", "Meta"); + group.writeEntry("CommandAll1", "Move"); + group.writeEntry("CommandAll2", "Move"); + group.writeEntry("CommandAll3", "Move"); + group.sync(); + workspace()->slotReconfigure(); + QCOMPARE(options->commandAllModifier(), Qt::MetaModifier); + QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove); + QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove); + QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove); + + // create a window + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + shellSurface->set_fullscreen(nullptr); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Window *window = Test::renderAndWaitForShown(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::blue); + QVERIFY(window); + QVERIFY(window->isFullScreen()); + + // move cursor on window + input()->pointer()->warp(window->frameGeometry().center()); + + // simulate modifier+click + quint32 timestamp = 1; + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + QVERIFY(!window->isInteractiveMove()); + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + QVERIFY(window->isInteractiveMove()); + // release modifier should not change it + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + QVERIFY(window->isInteractiveMove()); + // but releasing the key should end move/resize + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + QVERIFY(!window->isInteractiveMove()); +} + +void PointerInputTest::testModifierClickUnrestrictedMoveGlobalShortcutsDisabled() +{ + // this test ensures that Alt+mouse button press triggers unrestricted move + + // create pointer and signal spy for button events + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy buttonSpy(pointer, &KWayland::Client::Pointer::buttonStateChanged); + + // first modify the config for this run + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("MouseBindings")); + group.writeEntry("CommandAllKey", "Meta"); + group.writeEntry("CommandAll1", "Move"); + group.writeEntry("CommandAll2", "Move"); + group.writeEntry("CommandAll3", "Move"); + group.sync(); + workspace()->slotReconfigure(); + QCOMPARE(options->commandAllModifier(), Qt::MetaModifier); + QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove); + QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove); + QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove); + + // create a window + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + render(surface.get()); + QVERIFY(windowAddedSpy.wait()); + Window *window = workspace()->activeWindow(); + QVERIFY(window); + + // disable global shortcuts + QVERIFY(!workspace()->globalShortcutsDisabled()); + workspace()->disableGlobalShortcutsForClient(true); + QVERIFY(workspace()->globalShortcutsDisabled()); + + // move cursor on window + input()->pointer()->warp(window->frameGeometry().center()); + + // simulate modifier+click + quint32 timestamp = 1; + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + QVERIFY(!window->isInteractiveMove()); + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + QVERIFY(!window->isInteractiveMove()); + // release modifier should not change it + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + QVERIFY(!window->isInteractiveMove()); + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + + workspace()->disableGlobalShortcutsForClient(false); +} + +void PointerInputTest::testModifierScrollOpacity_data() +{ + QTest::addColumn("modifierKey"); + QTest::addColumn("modKey"); + QTest::addColumn("capsLock"); + + const QString alt = QStringLiteral("Alt"); + const QString meta = QStringLiteral("Meta"); + + QTest::newRow("Left Alt") << KEY_LEFTALT << alt << false; + QTest::newRow("Right Alt") << KEY_RIGHTALT << alt << false; + QTest::newRow("Left Meta") << KEY_LEFTMETA << meta << false; + QTest::newRow("Right Meta") << KEY_RIGHTMETA << meta << false; + QTest::newRow("Left Alt/CapsLock") << KEY_LEFTALT << alt << true; + QTest::newRow("Right Alt/CapsLock") << KEY_RIGHTALT << alt << true; + QTest::newRow("Left Meta/CapsLock") << KEY_LEFTMETA << meta << true; + QTest::newRow("Right Meta/CapsLock") << KEY_RIGHTMETA << meta << true; +} + +void PointerInputTest::testModifierScrollOpacity() +{ + // this test verifies that mod+wheel performs a window operation and does not + // pass the wheel to the window + + // create pointer and signal spy for button events + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy axisSpy(pointer, &KWayland::Client::Pointer::axisChanged); + + // first modify the config for this run + QFETCH(QString, modKey); + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("MouseBindings")); + group.writeEntry("CommandAllKey", modKey); + group.writeEntry("CommandAllWheel", "change opacity"); + group.sync(); + workspace()->slotReconfigure(); + + // create a window + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + render(surface.get()); + QVERIFY(windowAddedSpy.wait()); + Window *window = workspace()->activeWindow(); + QVERIFY(window); + // set the opacity to 0.5 + window->setOpacity(0.5); + QCOMPARE(window->opacity(), 0.5); + + // move cursor on window + input()->pointer()->warp(window->frameGeometry().center()); + + // simulate modifier+wheel + quint32 timestamp = 1; + QFETCH(bool, capsLock); + if (capsLock) { + Test::keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); + } + QFETCH(int, modifierKey); + Test::keyboardKeyPressed(modifierKey, timestamp++); + Test::pointerAxisVertical(-15, timestamp++); + QCOMPARE(window->opacity(), 0.6); + Test::pointerAxisVertical(15, timestamp++); + QCOMPARE(window->opacity(), 0.5); + Test::keyboardKeyReleased(modifierKey, timestamp++); + if (capsLock) { + Test::keyboardKeyReleased(KEY_CAPSLOCK, timestamp++); + } + + // axis should have been filtered out + QCOMPARE(axisSpy.count(), 0); + QVERIFY(Test::waylandSync()); + QCOMPARE(axisSpy.count(), 0); +} + +void PointerInputTest::testModifierScrollOpacityGlobalShortcutsDisabled() +{ + // this test verifies that mod+wheel performs a window operation and does not + // pass the wheel to the window + + // create pointer and signal spy for button events + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy axisSpy(pointer, &KWayland::Client::Pointer::axisChanged); + + // first modify the config for this run + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("MouseBindings")); + group.writeEntry("CommandAllKey", "Meta"); + group.writeEntry("CommandAllWheel", "change opacity"); + group.sync(); + workspace()->slotReconfigure(); + + // create a window + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + render(surface.get()); + QVERIFY(windowAddedSpy.wait()); + Window *window = workspace()->activeWindow(); + QVERIFY(window); + // set the opacity to 0.5 + window->setOpacity(0.5); + QCOMPARE(window->opacity(), 0.5); + + // move cursor on window + input()->pointer()->warp(window->frameGeometry().center()); + + // disable global shortcuts + QVERIFY(!workspace()->globalShortcutsDisabled()); + workspace()->disableGlobalShortcutsForClient(true); + QVERIFY(workspace()->globalShortcutsDisabled()); + + // simulate modifier+wheel + quint32 timestamp = 1; + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + Test::pointerAxisVertical(-15, timestamp++); + QCOMPARE(window->opacity(), 0.5); + Test::pointerAxisVertical(15, timestamp++); + QCOMPARE(window->opacity(), 0.5); + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + + workspace()->disableGlobalShortcutsForClient(false); +} + +void PointerInputTest::testScrollAction() +{ + // this test verifies that scroll on inactive window performs a mouse action + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy axisSpy(pointer, &KWayland::Client::Pointer::axisChanged); + + // first modify the config for this run + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("MouseBindings")); + group.writeEntry("CommandWindowWheel", "activate and scroll"); + group.sync(); + workspace()->slotReconfigure(); + // create two windows + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + std::unique_ptr surface1 = Test::createSurface(); + QVERIFY(surface1); + std::unique_ptr shellSurface1 = Test::createXdgToplevelSurface(surface1.get()); + QVERIFY(shellSurface1); + render(surface1.get()); + QVERIFY(windowAddedSpy.wait()); + Window *window1 = workspace()->activeWindow(); + QVERIFY(window1); + std::unique_ptr surface2 = Test::createSurface(); + QVERIFY(surface2); + std::unique_ptr shellSurface2 = Test::createXdgToplevelSurface(surface2.get()); + QVERIFY(shellSurface2); + render(surface2.get()); + QVERIFY(windowAddedSpy.wait()); + Window *window2 = workspace()->activeWindow(); + QVERIFY(window2); + QVERIFY(window1 != window2); + + // move cursor to the inactive window + input()->pointer()->warp(window1->frameGeometry().center()); + + quint32 timestamp = 1; + QVERIFY(!window1->isActive()); + // the action should be triggered only once enough delta is accumulated + Test::pointerAxisVertical(5, timestamp++); + QVERIFY(!window1->isActive()); + Test::pointerAxisVertical(5, timestamp++); + QVERIFY(!window1->isActive()); + Test::pointerAxisVertical(5, timestamp++); + QVERIFY(window1->isActive()); + + // but also the wheel event should be passed to the window + QVERIFY(axisSpy.wait()); +} + +void PointerInputTest::testFocusFollowsMouse() +{ + // need to create a pointer, otherwise it doesn't accept focus + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + // move cursor out of the way of first window to be created + input()->pointer()->warp(QPointF(900, 900)); + + // first modify the config for this run + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("AutoRaise", true); + group.writeEntry("AutoRaiseInterval", 20); + group.writeEntry("DelayFocusInterval", 200); + group.writeEntry("FocusPolicy", "FocusFollowsMouse"); + group.sync(); + workspace()->slotReconfigure(); + // verify the settings + QCOMPARE(options->focusPolicy(), Options::FocusFollowsMouse); + QVERIFY(options->isAutoRaise()); + QCOMPARE(options->autoRaiseInterval(), 20); + QCOMPARE(options->delayFocusInterval(), 200); + + // create two windows + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + std::unique_ptr surface1 = Test::createSurface(); + QVERIFY(surface1); + std::unique_ptr shellSurface1 = Test::createXdgToplevelSurface(surface1.get()); + QVERIFY(shellSurface1); + render(surface1.get(), QSize(800, 800)); + QVERIFY(windowAddedSpy.wait()); + Window *window1 = workspace()->activeWindow(); + QVERIFY(window1); + std::unique_ptr surface2 = Test::createSurface(); + QVERIFY(surface2); + std::unique_ptr shellSurface2 = Test::createXdgToplevelSurface(surface2.get()); + QVERIFY(shellSurface2); + render(surface2.get(), QSize(800, 800)); + QVERIFY(windowAddedSpy.wait()); + Window *window2 = workspace()->activeWindow(); + QVERIFY(window2); + QVERIFY(window1 != window2); + QCOMPARE(workspace()->topWindowOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window2); + // geometry of the two windows should be overlapping + QVERIFY(window1->frameGeometry().intersects(window2->frameGeometry())); + + // signal spies for active window changed and stacking order changed + QSignalSpy activeWindowChangedSpy(workspace(), &Workspace::windowActivated); + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + + QVERIFY(!window1->isActive()); + QVERIFY(window2->isActive()); + + // move on top of first window + QVERIFY(window1->frameGeometry().contains(QPointF(10, 10))); + QVERIFY(!window2->frameGeometry().contains(QPointF(10, 10))); + input()->pointer()->warp(QPointF(10, 10)); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(stackingOrderChangedSpy.count(), 1); + QCOMPARE(workspace()->topWindowOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window1); + QTRY_VERIFY(window1->isActive()); + + // move on second window, but move away before active window change delay hits + input()->pointer()->warp(QPointF(810, 810)); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(stackingOrderChangedSpy.count(), 2); + QCOMPARE(workspace()->topWindowOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window2); + input()->pointer()->warp(QPointF(10, 10)); + QVERIFY(!activeWindowChangedSpy.wait(250)); + QVERIFY(window1->isActive()); + QCOMPARE(workspace()->topWindowOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window1); + // as we moved back on window 1 that should been raised in the mean time + QCOMPARE(stackingOrderChangedSpy.count(), 3); + + // quickly move on window 2 and back on window 1 should not raise window 2 + input()->pointer()->warp(QPointF(810, 810)); + input()->pointer()->warp(QPointF(10, 10)); + QVERIFY(!stackingOrderChangedSpy.wait(250)); +} + +void PointerInputTest::testMouseActionInactiveWindow_data() +{ + QTest::addColumn("button"); + + QTest::newRow("Left") << quint32(BTN_LEFT); + QTest::newRow("Middle") << quint32(BTN_MIDDLE); + QTest::newRow("Right") << quint32(BTN_RIGHT); +} + +void PointerInputTest::testMouseActionInactiveWindow() +{ + // this test performs the mouse button window action on an inactive window + // it should activate the window and raise it + + // first modify the config for this run - disable FocusFollowsMouse + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("FocusPolicy", "ClickToFocus"); + group.sync(); + group = kwinApp()->config()->group(QStringLiteral("MouseBindings")); + group.writeEntry("CommandWindow1", "Activate, raise and pass click"); + group.writeEntry("CommandWindow2", "Activate, raise and pass click"); + group.writeEntry("CommandWindow3", "Activate, raise and pass click"); + group.sync(); + workspace()->slotReconfigure(); + + // create two windows + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + std::unique_ptr surface1 = Test::createSurface(); + QVERIFY(surface1); + std::unique_ptr shellSurface1 = Test::createXdgToplevelSurface(surface1.get()); + QVERIFY(shellSurface1); + render(surface1.get(), QSize(800, 800)); + QVERIFY(windowAddedSpy.wait()); + Window *window1 = workspace()->activeWindow(); + QVERIFY(window1); + std::unique_ptr surface2 = Test::createSurface(); + QVERIFY(surface2); + std::unique_ptr shellSurface2 = Test::createXdgToplevelSurface(surface2.get()); + QVERIFY(shellSurface2); + render(surface2.get(), QSize(800, 800)); + QVERIFY(windowAddedSpy.wait()); + Window *window2 = workspace()->activeWindow(); + QVERIFY(window2); + QVERIFY(window1 != window2); + QCOMPARE(workspace()->topWindowOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window2); + // geometry of the two windows should be overlapping + QVERIFY(window1->frameGeometry().intersects(window2->frameGeometry())); + + // signal spies for active window changed and stacking order changed + QSignalSpy activeWindowChangedSpy(workspace(), &Workspace::windowActivated); + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + + QVERIFY(!window1->isActive()); + QVERIFY(window2->isActive()); + + // move on top of first window + QVERIFY(window1->frameGeometry().contains(QPointF(10, 10))); + QVERIFY(!window2->frameGeometry().contains(QPointF(10, 10))); + input()->pointer()->warp(QPointF(10, 10)); + // no focus follows mouse + QVERIFY(stackingOrderChangedSpy.isEmpty()); + QVERIFY(activeWindowChangedSpy.isEmpty()); + QVERIFY(window2->isActive()); + // and click + quint32 timestamp = 1; + QFETCH(quint32, button); + Test::pointerButtonPressed(button, timestamp++); + // should raise window1 and activate it + QCOMPARE(stackingOrderChangedSpy.count(), 1); + QVERIFY(!activeWindowChangedSpy.isEmpty()); + QCOMPARE(workspace()->topWindowOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window1); + QVERIFY(window1->isActive()); + QVERIFY(!window2->isActive()); + + // release again + Test::pointerButtonReleased(button, timestamp++); +} + +void PointerInputTest::testMouseActionActiveWindow_data() +{ + QTest::addColumn("clickRaise"); + QTest::addColumn("button"); + + for (quint32 i = BTN_LEFT; i < BTN_JOYSTICK; i++) { + QByteArray number = QByteArray::number(i, 16); + QTest::newRow(QByteArrayLiteral("click raise/").append(number).constData()) << true << i; + QTest::newRow(QByteArrayLiteral("no click raise/").append(number).constData()) << false << i; + } +} + +void PointerInputTest::testMouseActionActiveWindow() +{ + // this test verifies the mouse action performed on an active window + // for all buttons it should trigger a window raise depending on the + // click raise option + + // create a button spy - all clicks should be passed through + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy buttonSpy(pointer, &KWayland::Client::Pointer::buttonStateChanged); + + // adjust config for this run + QFETCH(bool, clickRaise); + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("ClickRaise", clickRaise); + group.sync(); + workspace()->slotReconfigure(); + QCOMPARE(options->isClickRaise(), clickRaise); + + // create two windows + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + std::unique_ptr surface1 = Test::createSurface(); + QVERIFY(surface1); + std::unique_ptr shellSurface1 = Test::createXdgToplevelSurface(surface1.get()); + QVERIFY(shellSurface1); + render(surface1.get(), QSize(800, 800)); + QVERIFY(windowAddedSpy.wait()); + Window *window1 = workspace()->activeWindow(); + QVERIFY(window1); + QSignalSpy window1DestroyedSpy(window1, &QObject::destroyed); + std::unique_ptr surface2 = Test::createSurface(); + QVERIFY(surface2); + std::unique_ptr shellSurface2 = Test::createXdgToplevelSurface(surface2.get()); + QVERIFY(shellSurface2); + render(surface2.get(), QSize(800, 800)); + QVERIFY(windowAddedSpy.wait()); + Window *window2 = workspace()->activeWindow(); + QVERIFY(window2); + QVERIFY(window1 != window2); + QSignalSpy window2DestroyedSpy(window2, &QObject::destroyed); + QCOMPARE(workspace()->topWindowOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window2); + // geometry of the two windows should be overlapping + QVERIFY(window1->frameGeometry().intersects(window2->frameGeometry())); + // lower the currently active window + workspace()->lowerWindow(window2); + QCOMPARE(workspace()->topWindowOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window1); + + // signal spy for stacking order spy + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + + // move on top of second window + QVERIFY(!window1->frameGeometry().contains(QPointF(900, 900))); + QVERIFY(window2->frameGeometry().contains(QPointF(900, 900))); + input()->pointer()->warp(QPointF(900, 900)); + + // and click + quint32 timestamp = 1; + QFETCH(quint32, button); + Test::pointerButtonPressed(button, timestamp++); + QVERIFY(buttonSpy.wait()); + if (clickRaise) { + QCOMPARE(stackingOrderChangedSpy.count(), 1); + QCOMPARE(workspace()->topWindowOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window2); + } else { + QCOMPARE(stackingOrderChangedSpy.count(), 0); + QCOMPARE(workspace()->topWindowOnDesktop(VirtualDesktopManager::self()->currentDesktop()), window1); + } + + // release again + Test::pointerButtonReleased(button, timestamp++); + + surface1.reset(); + QVERIFY(window1DestroyedSpy.wait()); + surface2.reset(); + QVERIFY(window2DestroyedSpy.wait()); +} + +void PointerInputTest::testCursorImage() +{ + // this test verifies that the pointer image gets updated correctly from the client provided data + + // we need a pointer to get the enter event + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy enteredSpy(pointer, &KWayland::Client::Pointer::entered); + + // move cursor somewhere the new window won't open + auto cursor = Cursors::self()->mouse(); + input()->pointer()->warp(QPointF(800, 800)); + auto p = input()->pointer(); + // at the moment it should be the fallback cursor + const QImage fallbackCursor = kwinApp()->cursorImage().image(); + QVERIFY(!fallbackCursor.isNull()); + + // create a window + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + render(surface.get()); + QVERIFY(windowAddedSpy.wait()); + Window *window = workspace()->activeWindow(); + QVERIFY(window); + + // move cursor to center of window, this should first set a null pointer, so we still show old cursor + input()->pointer()->warp(window->frameGeometry().center()); + QCOMPARE(p->focus(), window); + QCOMPARE(kwinApp()->cursorImage().image(), fallbackCursor); + QVERIFY(enteredSpy.wait()); + + // create a cursor on the pointer + auto cursorSurface = Test::createSurface(); + QVERIFY(cursorSurface); + QSignalSpy cursorRenderedSpy(cursorSurface.get(), &KWayland::Client::Surface::frameRendered); + QImage red = QImage(QSize(10, 10), QImage::Format_ARGB32_Premultiplied); + red.fill(Qt::red); + cursorSurface->attachBuffer(Test::waylandShmPool()->createBuffer(red)); + cursorSurface->damage(QRect(0, 0, 10, 10)); + cursorSurface->commit(); + pointer->setCursor(cursorSurface.get(), QPoint(5, 5)); + QVERIFY(cursorRenderedSpy.wait()); + QCOMPARE(kwinApp()->cursorImage().image(), red); + QCOMPARE(cursor->hotspot(), QPoint(5, 5)); + // change hotspot + pointer->setCursor(cursorSurface.get(), QPoint(6, 6)); + Test::flushWaylandConnection(); + QTRY_COMPARE(cursor->hotspot(), QPoint(6, 6)); + QCOMPARE(kwinApp()->cursorImage().image(), red); + + // change the buffer + QImage blue = QImage(QSize(10, 10), QImage::Format_ARGB32_Premultiplied); + blue.fill(Qt::blue); + auto b = Test::waylandShmPool()->createBuffer(blue); + cursorSurface->attachBuffer(b); + cursorSurface->damage(QRect(0, 0, 10, 10)); + cursorSurface->commit(); + QVERIFY(cursorRenderedSpy.wait()); + QTRY_COMPARE(kwinApp()->cursorImage().image(), blue); + QCOMPARE(cursor->hotspot(), QPoint(6, 6)); + + // hide the cursor + pointer->setCursor(nullptr); + Test::flushWaylandConnection(); + QTRY_VERIFY(kwinApp()->cursorImage().image().isNull()); + + // move cursor somewhere else, should reset to fallback cursor + input()->pointer()->warp(window->frameGeometry().bottomLeft() + QPoint(20, 20)); + QVERIFY(!p->focus()); + QVERIFY(!kwinApp()->cursorImage().image().isNull()); + QCOMPARE(kwinApp()->cursorImage().image(), fallbackCursor); +} + +static QByteArray currentCursorShape() +{ + if (auto source = qobject_cast(Cursors::self()->currentCursor()->source())) { + return source->shape(); + } + return QByteArray(); +} + +void PointerInputTest::testCursorShapeV1() +{ + // this test verifies the integration of the cursor-shape-v1 protocol + + // get the pointer + std::unique_ptr pointer(m_seat->createPointer()); + std::unique_ptr cursorShapeDevice(Test::createCursorShapeDeviceV1(pointer.get())); + + // move cursor somewhere the new window won't open + input()->pointer()->warp(QPointF(800, 800)); + QCOMPARE(currentCursorShape(), QByteArrayLiteral("default")); + + // create a window + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::cyan); + QVERIFY(window); + + // move the pointer to the center of the window + QSignalSpy enteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); + input()->pointer()->warp(window->frameGeometry().center()); + QVERIFY(enteredSpy.wait()); + + // set a custom cursor shape + QSignalSpy cursorChanged(Cursors::self(), &Cursors::currentCursorChanged); + cursorShapeDevice->set_shape(enteredSpy.last().at(0).value(), Test::CursorShapeDeviceV1::shape_text); + QVERIFY(cursorChanged.wait()); + QCOMPARE(currentCursorShape(), QByteArray("text")); + + // cursor shape won't be changed if the window has no pointer focus + input()->pointer()->warp(QPointF(800, 800)); + QCOMPARE(currentCursorShape(), QByteArrayLiteral("default")); + cursorShapeDevice->set_shape(enteredSpy.last().at(0).value(), Test::CursorShapeDeviceV1::shape_grab); + QVERIFY(Test::waylandSync()); + QCOMPARE(currentCursorShape(), QByteArrayLiteral("default")); +} + +class HelperEffect : public Effect +{ + Q_OBJECT +public: + HelperEffect() + { + } + ~HelperEffect() override + { + } +}; + +void PointerInputTest::testEffectOverrideCursorImage() +{ + // this test verifies the effect cursor override handling + + // we need a pointer to get the enter event and set a cursor + std::unique_ptr pointer(m_seat->createPointer()); + std::unique_ptr cursorShapeDevice(Test::createCursorShapeDeviceV1(pointer.get())); + QSignalSpy enteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); + QSignalSpy leftSpy(pointer.get(), &KWayland::Client::Pointer::left); + QSignalSpy cursorChanged(Cursors::self(), &Cursors::currentCursorChanged); + + // move cursor somewhere the new window won't open + input()->pointer()->warp(QPointF(800, 800)); + + // now let's create a window + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + render(surface.get()); + QVERIFY(windowAddedSpy.wait()); + Window *window = workspace()->activeWindow(); + QVERIFY(window); + + // and move cursor to the window + QVERIFY(!window->frameGeometry().contains(QPoint(800, 800))); + input()->pointer()->warp(window->frameGeometry().center()); + QVERIFY(enteredSpy.wait()); + cursorShapeDevice->set_shape(enteredSpy.last().at(0).value(), Test::CursorShapeDeviceV1::shape_wait); + QVERIFY(cursorChanged.wait()); + QCOMPARE(currentCursorShape(), QByteArray("wait")); + + // now create an effect and set an override cursor + std::unique_ptr effect(new HelperEffect); + effects->startMouseInterception(effect.get(), Qt::SizeAllCursor); + QCOMPARE(currentCursorShape(), QByteArrayLiteral("all-scroll")); + + // let's change to arrow cursor, this should be our fallback + effects->defineCursor(Qt::ArrowCursor); + QCOMPARE(currentCursorShape(), QByteArrayLiteral("default")); + + // back to size all + effects->defineCursor(Qt::SizeAllCursor); + QCOMPARE(currentCursorShape(), QByteArrayLiteral("all-scroll")); + + // move cursor outside the window area + input()->pointer()->warp(QPointF(800, 800)); + QCOMPARE(currentCursorShape(), QByteArrayLiteral("all-scroll")); + + // move cursor to area of window + input()->pointer()->warp(window->frameGeometry().center()); + // this should not result in an enter event + QVERIFY(Test::waylandSync()); + QCOMPARE(enteredSpy.count(), 1); + + // after ending the interception we should get an enter event + effects->stopMouseInterception(effect.get()); + QVERIFY(enteredSpy.wait()); + cursorShapeDevice->set_shape(enteredSpy.last().at(0).value(), Test::CursorShapeDeviceV1::shape_crosshair); + QVERIFY(cursorChanged.wait()); + QCOMPARE(currentCursorShape(), QByteArrayLiteral("crosshair")); +} + +void PointerInputTest::testPopup() +{ + // this test validates the basic popup behavior + // a button press outside the window should dismiss the popup + + // first create a parent surface + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy enteredSpy(pointer, &KWayland::Client::Pointer::entered); + QSignalSpy leftSpy(pointer, &KWayland::Client::Pointer::left); + QSignalSpy buttonStateChangedSpy(pointer, &KWayland::Client::Pointer::buttonStateChanged); + QSignalSpy motionSpy(pointer, &KWayland::Client::Pointer::motion); + + input()->pointer()->warp(QPointF(800, 800)); + + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + render(surface.get()); + QVERIFY(windowAddedSpy.wait()); + Window *window = workspace()->activeWindow(); + QVERIFY(window); + QCOMPARE(window->hasPopupGrab(), false); + // move pointer into window + QVERIFY(!window->frameGeometry().contains(QPoint(800, 800))); + input()->pointer()->warp(window->frameGeometry().center()); + QVERIFY(enteredSpy.wait()); + // click inside window to create serial + quint32 timestamp = 0; + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + QVERIFY(buttonStateChangedSpy.wait()); + + // now create the popup surface + std::unique_ptr positioner(Test::createXdgPositioner()); + positioner->set_size(100, 50); + positioner->set_anchor_rect(0, 0, 80, 20); + positioner->set_anchor(Test::XdgPositioner::anchor_bottom_right); + positioner->set_gravity(Test::XdgPositioner::gravity_bottom_right); + std::unique_ptr popupSurface = Test::createSurface(); + QVERIFY(popupSurface); + std::unique_ptr popupShellSurface = Test::createXdgPopupSurface(popupSurface.get(), shellSurface->xdgSurface(), positioner.get()); + QVERIFY(popupShellSurface); + QSignalSpy doneReceivedSpy(popupShellSurface.get(), &Test::XdgPopup::doneReceived); + popupShellSurface->grab(*Test::waylandSeat(), 0); // FIXME: Serial. + render(popupSurface.get(), QSize(100, 50)); + QVERIFY(windowAddedSpy.wait()); + auto popupWindow = windowAddedSpy.last().first().value(); + QVERIFY(popupWindow); + QVERIFY(popupWindow != window); + QCOMPARE(window, workspace()->activeWindow()); + QCOMPARE(popupWindow->transientFor(), window); + QCOMPARE(popupWindow->pos(), window->pos() + QPoint(80, 20)); + QCOMPARE(popupWindow->hasPopupGrab(), true); + + // let's move the pointer into the center of the window + input()->pointer()->warp(popupWindow->frameGeometry().center()); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 2); + QCOMPARE(leftSpy.count(), 1); + QCOMPARE(pointer->enteredSurface(), popupSurface.get()); + + // let's move the pointer outside of the popup window + // this should not really change anything, it gets a leave event + input()->pointer()->warp(popupWindow->frameGeometry().bottomRight() + QPoint(2, 2)); + QVERIFY(leftSpy.wait()); + QCOMPARE(leftSpy.count(), 2); + QVERIFY(doneReceivedSpy.isEmpty()); + // now click, should trigger popupDone + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + QVERIFY(doneReceivedSpy.wait()); + Test::pointerButtonReleased(BTN_LEFT, timestamp++); +} + +void PointerInputTest::testDecoCancelsPopup() +{ + // this test verifies that clicking the window decoration of parent window + // cancels the popup + + // first create a parent surface + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy enteredSpy(pointer, &KWayland::Client::Pointer::entered); + QSignalSpy leftSpy(pointer, &KWayland::Client::Pointer::left); + QSignalSpy buttonStateChangedSpy(pointer, &KWayland::Client::Pointer::buttonStateChanged); + QSignalSpy motionSpy(pointer, &KWayland::Client::Pointer::motion); + + input()->pointer()->warp(QPointF(800, 800)); + + // create a decorated window + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + std::unique_ptr decoration(Test::createXdgToplevelDecorationV1(shellSurface.get())); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + decoration->set_mode(Test::XdgToplevelDecorationV1::mode_server_side); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QCOMPARE(window->hasPopupGrab(), false); + QVERIFY(window->isDecorated()); + + // move pointer into window + QVERIFY(!window->frameGeometry().contains(QPoint(800, 800))); + input()->pointer()->warp(window->frameGeometry().center()); + QVERIFY(enteredSpy.wait()); + // click inside window to create serial + quint32 timestamp = 0; + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + QVERIFY(buttonStateChangedSpy.wait()); + + // now create the popup surface + std::unique_ptr positioner(Test::createXdgPositioner()); + positioner->set_size(100, 50); + positioner->set_anchor_rect(0, 0, 80, 20); + positioner->set_anchor(Test::XdgPositioner::anchor_bottom_right); + positioner->set_gravity(Test::XdgPositioner::gravity_bottom_right); + std::unique_ptr popupSurface = Test::createSurface(); + QVERIFY(popupSurface); + std::unique_ptr popupShellSurface = Test::createXdgPopupSurface(popupSurface.get(), shellSurface->xdgSurface(), positioner.get()); + QVERIFY(popupShellSurface); + QSignalSpy doneReceivedSpy(popupShellSurface.get(), &Test::XdgPopup::doneReceived); + popupShellSurface->grab(*Test::waylandSeat(), 0); // FIXME: Serial. + auto popupWindow = Test::renderAndWaitForShown(popupSurface.get(), QSize(100, 50), Qt::red); + QVERIFY(popupWindow); + QVERIFY(popupWindow != window); + QCOMPARE(window, workspace()->activeWindow()); + QCOMPARE(popupWindow->transientFor(), window); + QCOMPARE(popupWindow->pos(), window->mapFromLocal(QPoint(80, 20))); + QCOMPARE(popupWindow->hasPopupGrab(), true); + + // let's move the pointer into the center of the deco + input()->pointer()->warp(QPointF(window->frameGeometry().center().x(), window->y() + (window->height() - window->clientSize().height()) / 2)); + + Test::pointerButtonPressed(BTN_RIGHT, timestamp++); + QVERIFY(doneReceivedSpy.wait()); + Test::pointerButtonReleased(BTN_RIGHT, timestamp++); +} + +void PointerInputTest::testWindowUnderCursorWhileButtonPressed() +{ + // this test verifies that opening a window underneath the mouse cursor does not + // trigger a leave event if a button is pressed + // see BUG: 372876 + + // first create a parent surface + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy enteredSpy(pointer, &KWayland::Client::Pointer::entered); + QSignalSpy leftSpy(pointer, &KWayland::Client::Pointer::left); + + input()->pointer()->warp(QPointF(800, 800)); + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(shellSurface); + render(surface.get()); + QVERIFY(windowAddedSpy.wait()); + Window *window = workspace()->activeWindow(); + QVERIFY(window); + + // move cursor over window + QVERIFY(!window->frameGeometry().contains(QPoint(800, 800))); + input()->pointer()->warp(window->frameGeometry().center()); + QVERIFY(enteredSpy.wait()); + // click inside window + quint32 timestamp = 0; + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + + // now create a second window as transient + std::unique_ptr positioner(Test::createXdgPositioner()); + positioner->set_size(99, 49); + positioner->set_anchor_rect(0, 0, 1, 1); + positioner->set_anchor(Test::XdgPositioner::anchor_bottom_right); + positioner->set_gravity(Test::XdgPositioner::gravity_bottom_right); + std::unique_ptr popupSurface = Test::createSurface(); + QVERIFY(popupSurface); + std::unique_ptr popupShellSurface = Test::createXdgPopupSurface(popupSurface.get(), shellSurface->xdgSurface(), positioner.get()); + QVERIFY(popupShellSurface); + render(popupSurface.get(), QSize(99, 49)); + QVERIFY(windowAddedSpy.wait()); + auto popupWindow = windowAddedSpy.last().first().value(); + QVERIFY(popupWindow); + QVERIFY(popupWindow != window); + QVERIFY(window->frameGeometry().contains(Cursors::self()->mouse()->pos())); + QVERIFY(popupWindow->frameGeometry().contains(Cursors::self()->mouse()->pos())); + QVERIFY(Test::waylandSync()); + QCOMPARE(leftSpy.count(), 0); + + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + // now that the button is no longer pressed we should get the leave event + QVERIFY(leftSpy.wait()); + QCOMPARE(leftSpy.count(), 1); + QCOMPARE(enteredSpy.count(), 2); +} + +void PointerInputTest::testConfineToScreenGeometry_data() +{ + QTest::addColumn("startPos"); + QTest::addColumn("targetPos"); + QTest::addColumn("expectedPos"); + + // screen layout: + // + // +----------+----------+---------+ + // | left | top | right | + // +----------+----------+---------+ + // | bottom | + // +----------+ + // + + QTest::newRow("move top-left - left screen") << QPoint(640, 512) << QPoint(-100, -100) << QPoint(0, 0); + QTest::newRow("move top - left screen") << QPoint(640, 512) << QPoint(640, -100) << QPoint(640, 0); + QTest::newRow("move top-right - left screen") << QPoint(640, 512) << QPoint(1380, -100) << QPoint(1380, 0); + QTest::newRow("move right - left screen") << QPoint(640, 512) << QPoint(1380, 512) << QPoint(1380, 512); + QTest::newRow("move bottom-right - left screen") << QPoint(640, 512) << QPoint(1380, 1124) << QPoint(1380, 1124); + QTest::newRow("move bottom - left screen") << QPoint(640, 512) << QPoint(640, 1124) << QPoint(640, 1023); + QTest::newRow("move bottom-left - left screen") << QPoint(640, 512) << QPoint(-100, 1124) << QPoint(0, 1023); + QTest::newRow("move left - left screen") << QPoint(640, 512) << QPoint(-100, 512) << QPoint(0, 512); + + QTest::newRow("move top-left - top screen") << QPoint(1920, 512) << QPoint(1180, -100) << QPoint(1180, 0); + QTest::newRow("move top - top screen") << QPoint(1920, 512) << QPoint(1920, -100) << QPoint(1920, 0); + QTest::newRow("move top-right - top screen") << QPoint(1920, 512) << QPoint(2660, -100) << QPoint(2660, 0); + QTest::newRow("move right - top screen") << QPoint(1920, 512) << QPoint(2660, 512) << QPoint(2660, 512); + QTest::newRow("move bottom-right - top screen") << QPoint(1920, 512) << QPoint(2660, 1124) << QPoint(2660, 1023); + QTest::newRow("move bottom - top screen") << QPoint(1920, 512) << QPoint(1920, 1124) << QPoint(1920, 1124); + QTest::newRow("move bottom-left - top screen") << QPoint(1920, 512) << QPoint(1180, 1124) << QPoint(1280, 1124); + QTest::newRow("move left - top screen") << QPoint(1920, 512) << QPoint(1180, 512) << QPoint(1180, 512); + + QTest::newRow("move top-left - right screen") << QPoint(3200, 512) << QPoint(2460, -100) << QPoint(2460, 0); + QTest::newRow("move top - right screen") << QPoint(3200, 512) << QPoint(3200, -100) << QPoint(3200, 0); + QTest::newRow("move top-right - right screen") << QPoint(3200, 512) << QPoint(3940, -100) << QPoint(3839, 0); + QTest::newRow("move right - right screen") << QPoint(3200, 512) << QPoint(3940, 512) << QPoint(3839, 512); + QTest::newRow("move bottom-right - right screen") << QPoint(3200, 512) << QPoint(3940, 1124) << QPoint(3839, 1023); + QTest::newRow("move bottom - right screen") << QPoint(3200, 512) << QPoint(3200, 1124) << QPoint(3200, 1023); + QTest::newRow("move bottom-left - right screen") << QPoint(3200, 512) << QPoint(2460, 1124) << QPoint(2460, 1124); + QTest::newRow("move left - right screen") << QPoint(3200, 512) << QPoint(2460, 512) << QPoint(2460, 512); + + QTest::newRow("move top-left - bottom screen") << QPoint(1920, 1536) << QPoint(1180, 924) << QPoint(1180, 924); + QTest::newRow("move top - bottom screen") << QPoint(1920, 1536) << QPoint(1920, 924) << QPoint(1920, 924); + QTest::newRow("move top-right - bottom screen") << QPoint(1920, 1536) << QPoint(2660, 924) << QPoint(2660, 924); + QTest::newRow("move right - bottom screen") << QPoint(1920, 1536) << QPoint(2660, 1536) << QPoint(2559, 1536); + QTest::newRow("move bottom-right - bottom screen") << QPoint(1920, 1536) << QPoint(2660, 2148) << QPoint(2559, 2047); + QTest::newRow("move bottom - bottom screen") << QPoint(1920, 1536) << QPoint(1920, 2148) << QPoint(1920, 2047); + QTest::newRow("move bottom-left - bottom screen") << QPoint(1920, 1536) << QPoint(1180, 2148) << QPoint(1280, 2047); + QTest::newRow("move left - bottom screen") << QPoint(1920, 1536) << QPoint(1180, 1536) << QPoint(1280, 1536); +} + +void PointerInputTest::testConfineToScreenGeometry() +{ + // this test verifies that pointer belongs to at least one screen + // after moving it to off-screen area + + // setup screen layout + const QList geometries{ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + Rect(2560, 0, 1280, 1024), + Rect(1280, 1024, 1280, 1024)}; + Test::setOutputConfig(geometries); + + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), geometries.count()); + QCOMPARE(outputs[0]->geometry(), geometries.at(0)); + QCOMPARE(outputs[1]->geometry(), geometries.at(1)); + QCOMPARE(outputs[2]->geometry(), geometries.at(2)); + QCOMPARE(outputs[3]->geometry(), geometries.at(3)); + + // move pointer to initial position + QFETCH(QPoint, startPos); + input()->pointer()->warp(startPos); + QCOMPARE(Cursors::self()->mouse()->pos(), startPos); + + // perform movement + QFETCH(QPoint, targetPos); + Test::pointerMotion(targetPos, 1); + + QFETCH(QPoint, expectedPos); + QCOMPARE(Cursors::self()->mouse()->pos(), expectedPos); +} + +void PointerInputTest::testEdgeBarrier_data() +{ + QTest::addColumn("startPos"); + QTest::addColumn>("movements"); + QTest::addColumn("targetOutputId"); + QTest::addColumn("cornerBarrier"); + + // screen layout: + // + // +----------+----------+---------+ + // | left | top | right | + // +----------+----------+---------+ + // | bottom | + // +----------+ + // + QTest::newRow("move right - barred") << QPoint(1270, 512) << QList{QPoint(20, 0)} << 0 << false; + QTest::newRow("move left - barred") << QPoint(1290, 512) << QList{QPoint(-20, 0)} << 1 << false; + QTest::newRow("move down - barred") << QPoint(1920, 1014) << QList{QPoint(0, 20)} << 1 << false; + QTest::newRow("move up - barred") << QPoint(1920, 1034) << QList{QPoint(0, -20)} << 3 << false; + QTest::newRow("move top-right - barred") << QPoint(2550, 1034) << QList{QPoint(20, -20)} << 3 << false; + QTest::newRow("move top-left - barred") << QPoint(1290, 1034) << QList{QPoint(-20, -20)} << 3 << false; + QTest::newRow("move bottom-right - barred") << QPoint(1270, 1014) << QList{QPoint(20, 20)} << 0 << false; + QTest::newRow("move bottom-left - barred") << QPoint(2570, 1014) << QList{QPoint(-20, 20)} << 2 << false; + + QTest::newRow("move right - not barred") << QPoint(1270, 512) << QList{QPoint(100, 0)} << 1 << false; + QTest::newRow("move left - not barred") << QPoint(1290, 512) << QList{QPoint(-100, 0)} << 0 << false; + QTest::newRow("move down - not barred") << QPoint(1920, 1014) << QList{QPoint(0, 100)} << 3 << false; + QTest::newRow("move up - not barred") << QPoint(1920, 1034) << QList{QPoint(0, -100)} << 1 << false; + QTest::newRow("move top-right - not barred") << QPoint(2550, 1034) << QList{QPoint(100, -100)} << 2 << false; + QTest::newRow("move top-left - not barred") << QPoint(1290, 1034) << QList{QPoint(-100, -100)} << 0 << false; + QTest::newRow("move bottom-right - not barred") << QPoint(1270, 1014) << QList{QPoint(100, 100)} << 3 << false; + QTest::newRow("move bottom-left - not barred") << QPoint(2570, 1014) << QList{QPoint(-100, 100)} << 3 << false; + + QTest::newRow("move cumulative") << QPoint(1279, 512) << QList{QPoint(24, 0), QPoint(24, 0)} << 1 << false; + QTest::newRow("move then idle") << QPoint(1279, 512) << QList{QPoint(24, 0), QPoint(0, 0), QPoint(0, 0), QPoint(3, 0)} << 0 << false; + + QTest::newRow("move top-right - corner barrier") << QPoint(2550, 1034) << QList{QPoint(100, -100)} << 3 << true; + QTest::newRow("move top-left - corner barrier") << QPoint(1290, 1034) << QList{QPoint(-100, -100)} << 3 << true; + QTest::newRow("move bottom-right - corner barrier") << QPoint(1270, 1014) << QList{QPoint(100, 100)} << 0 << true; + QTest::newRow("move bottom-left - corner barrier") << QPoint(2570, 1014) << QList{QPoint(-100, 100)} << 2 << true; +} + +void PointerInputTest::testEdgeBarrier() +{ + // setup screen layout + const QList geometries{ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + Rect(2560, 0, 1280, 1024), + Rect(1280, 1024, 1280, 1024)}; + Test::setOutputConfig(geometries); + + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), geometries.count()); + QCOMPARE(outputs[0]->geometry(), geometries.at(0)); + QCOMPARE(outputs[1]->geometry(), geometries.at(1)); + QCOMPARE(outputs[2]->geometry(), geometries.at(2)); + QCOMPARE(outputs[3]->geometry(), geometries.at(3)); + + QFETCH(QPoint, startPos); + input()->pointer()->warp(startPos); + quint32 timestamp = waylandServer()->seat()->timestamp().count() + 5000; + Test::pointerMotionRelative(QPoint(0, 0), timestamp); + timestamp += 1000; + QCOMPARE(Cursors::self()->mouse()->pos(), startPos); + + auto group = kwinApp()->config()->group(QStringLiteral("EdgeBarrier")); + group.writeEntry("EdgeBarrier", 25); + QFETCH(bool, cornerBarrier); + group.writeEntry("CornerBarrier", cornerBarrier); + group.sync(); + workspace()->slotReconfigure(); + + QFETCH(QList, movements); + for (const auto &movement : movements) { + Test::pointerMotionRelative(movement, timestamp); + timestamp += 1000; + } + QFETCH(int, targetOutputId); + QCOMPARE(workspace()->outputAt(Cursors::self()->mouse()->pos()), workspace()->outputs().at(targetOutputId)); +} + +void PointerInputTest::testResizeCursor_data() +{ + QTest::addColumn("edges"); + QTest::addColumn("cursorShape"); + + QTest::newRow("top-left") << Qt::Edges(Qt::TopEdge | Qt::LeftEdge) << CursorShape(ExtendedCursor::SizeNorthWest); + QTest::newRow("top") << Qt::Edges(Qt::TopEdge) << CursorShape(ExtendedCursor::SizeNorth); + QTest::newRow("top-right") << Qt::Edges(Qt::TopEdge | Qt::RightEdge) << CursorShape(ExtendedCursor::SizeNorthEast); + QTest::newRow("right") << Qt::Edges(Qt::RightEdge) << CursorShape(ExtendedCursor::SizeEast); + QTest::newRow("bottom-right") << Qt::Edges(Qt::BottomEdge | Qt::RightEdge) << CursorShape(ExtendedCursor::SizeSouthEast); + QTest::newRow("bottom") << Qt::Edges(Qt::BottomEdge) << CursorShape(ExtendedCursor::SizeSouth); + QTest::newRow("bottom-left") << Qt::Edges(Qt::BottomEdge | Qt::LeftEdge) << CursorShape(ExtendedCursor::SizeSouthWest); + QTest::newRow("left") << Qt::Edges(Qt::LeftEdge) << CursorShape(ExtendedCursor::SizeWest); +} + +void PointerInputTest::testResizeCursor() +{ + // this test verifies that the cursor has correct shape during resize operation + + // first modify the config for this run + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("MouseBindings")); + group.writeEntry("CommandAllKey", "Meta"); + group.writeEntry("CommandAll3", "Resize"); + group.sync(); + workspace()->slotReconfigure(); + QCOMPARE(options->commandAllModifier(), Qt::MetaModifier); + QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedResize); + + // load the fallback cursor (arrow cursor) + const PlatformCursorImage arrowCursor = loadReferenceThemeCursor(Qt::ArrowCursor); + QVERIFY(!arrowCursor.isNull()); + QCOMPARE(kwinApp()->cursorImage().image(), arrowCursor.image()); + QCOMPARE(kwinApp()->cursorImage().hotSpot(), arrowCursor.hotSpot()); + + // we need a pointer to get the enter event + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy enteredSpy(pointer, &KWayland::Client::Pointer::entered); + + // create a test window + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // move the cursor to the test position + QPoint cursorPos; + QFETCH(Qt::Edges, edges); + + if (edges & Qt::LeftEdge) { + cursorPos.setX(window->frameGeometry().left()); + } else if (edges & Qt::RightEdge) { + cursorPos.setX(window->frameGeometry().right() - 1); + } else { + cursorPos.setX(window->frameGeometry().center().x()); + } + + if (edges & Qt::TopEdge) { + cursorPos.setY(window->frameGeometry().top()); + } else if (edges & Qt::BottomEdge) { + cursorPos.setY(window->frameGeometry().bottom() - 1); + } else { + cursorPos.setY(window->frameGeometry().center().y()); + } + + input()->pointer()->warp(cursorPos); + + // wait for the enter event and set the cursor + QVERIFY(enteredSpy.wait()); + std::unique_ptr cursorSurface(Test::createSurface()); + QVERIFY(cursorSurface); + QSignalSpy cursorRenderedSpy(cursorSurface.get(), &KWayland::Client::Surface::frameRendered); + cursorSurface->attachBuffer(Test::waylandShmPool()->createBuffer(arrowCursor.image())); + cursorSurface->damage(arrowCursor.image().rect()); + cursorSurface->commit(); + pointer->setCursor(cursorSurface.get(), arrowCursor.hotSpot().toPoint()); + QVERIFY(cursorRenderedSpy.wait()); + + // start resizing the window + int timestamp = 1; + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + Test::pointerButtonPressed(BTN_RIGHT, timestamp++); + QVERIFY(window->isInteractiveResize()); + + QFETCH(KWin::CursorShape, cursorShape); + const PlatformCursorImage resizeCursor = loadReferenceThemeCursor(cursorShape); + QVERIFY(!resizeCursor.isNull()); + QCOMPARE(kwinApp()->cursorImage().image(), resizeCursor.image()); + QCOMPARE(kwinApp()->cursorImage().hotSpot(), resizeCursor.hotSpot()); + + // finish resizing the window + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + Test::pointerButtonReleased(BTN_RIGHT, timestamp++); + QVERIFY(!window->isInteractiveResize()); + + QCOMPARE(kwinApp()->cursorImage().image(), arrowCursor.image()); + QCOMPARE(kwinApp()->cursorImage().hotSpot(), arrowCursor.hotSpot()); +} + +void PointerInputTest::testMoveCursor() +{ + // this test verifies that the cursor has correct shape during move operation + + // first modify the config for this run + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("MouseBindings")); + group.writeEntry("CommandAllKey", "Meta"); + group.writeEntry("CommandAll1", "Move"); + group.sync(); + workspace()->slotReconfigure(); + QCOMPARE(options->commandAllModifier(), Qt::MetaModifier); + QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove); + + // load the fallback cursor (arrow cursor) + const PlatformCursorImage arrowCursor = loadReferenceThemeCursor(Qt::ArrowCursor); + QVERIFY(!arrowCursor.isNull()); + QCOMPARE(kwinApp()->cursorImage().image(), arrowCursor.image()); + QCOMPARE(kwinApp()->cursorImage().hotSpot(), arrowCursor.hotSpot()); + + // we need a pointer to get the enter event + auto pointer = m_seat->createPointer(m_seat); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy enteredSpy(pointer, &KWayland::Client::Pointer::entered); + + // create a test window + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // move cursor to the test position + input()->pointer()->warp(window->frameGeometry().center()); + + // wait for the enter event and set the cursor + QVERIFY(enteredSpy.wait()); + std::unique_ptr cursorSurface = Test::createSurface(); + QVERIFY(cursorSurface); + QSignalSpy cursorRenderedSpy(cursorSurface.get(), &KWayland::Client::Surface::frameRendered); + cursorSurface->attachBuffer(Test::waylandShmPool()->createBuffer(arrowCursor.image())); + cursorSurface->damage(arrowCursor.image().rect()); + cursorSurface->commit(); + pointer->setCursor(cursorSurface.get(), arrowCursor.hotSpot().toPoint()); + QVERIFY(cursorRenderedSpy.wait()); + + // start moving the window + int timestamp = 1; + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + QVERIFY(window->isInteractiveMove()); + + const PlatformCursorImage moveCursor = loadReferenceThemeCursor(Qt::ClosedHandCursor); + QVERIFY(!moveCursor.isNull()); + QCOMPARE(kwinApp()->cursorImage().image(), moveCursor.image()); + QCOMPARE(kwinApp()->cursorImage().hotSpot(), moveCursor.hotSpot()); + + // finish moving the window + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + QVERIFY(!window->isInteractiveMove()); + + QCOMPARE(kwinApp()->cursorImage().image(), arrowCursor.image()); + QCOMPARE(kwinApp()->cursorImage().hotSpot(), arrowCursor.hotSpot()); +} + +void PointerInputTest::testHideShowCursor() +{ + QCOMPARE(Cursors::self()->isCursorHidden(), false); + Cursors::self()->hideCursor(); + QCOMPARE(Cursors::self()->isCursorHidden(), true); + Cursors::self()->showCursor(); + QCOMPARE(Cursors::self()->isCursorHidden(), false); + + Cursors::self()->hideCursor(); + QCOMPARE(Cursors::self()->isCursorHidden(), true); + Cursors::self()->hideCursor(); + Cursors::self()->hideCursor(); + Cursors::self()->hideCursor(); + QCOMPARE(Cursors::self()->isCursorHidden(), true); + + Cursors::self()->showCursor(); + QCOMPARE(Cursors::self()->isCursorHidden(), true); + Cursors::self()->showCursor(); + QCOMPARE(Cursors::self()->isCursorHidden(), true); + Cursors::self()->showCursor(); + QCOMPARE(Cursors::self()->isCursorHidden(), true); + Cursors::self()->showCursor(); + QCOMPARE(Cursors::self()->isCursorHidden(), false); +} + +void PointerInputTest::testDefaultInputRegion() +{ + // This test verifies that a surface that hasn't specified the input region can be focused. + + // Create a test window. + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // Move the point to the center of the surface. + input()->pointer()->warp(window->frameGeometry().center()); + QCOMPARE(waylandServer()->seat()->focusedPointerSurface(), window->surface()); + + // Destroy the test window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void PointerInputTest::testEmptyInputRegion() +{ + // This test verifies that a surface that has specified an empty input region can't be focused. + + // Create a test window. + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr inputRegion(m_compositor->createRegion(QRegion())); + surface->setInputRegion(inputRegion.get()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // Move the point to the center of the surface. + input()->pointer()->warp(window->frameGeometry().center()); + QVERIFY(!waylandServer()->seat()->focusedPointerSurface()); + + // Destroy the test window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void PointerInputTest::testUnfocusedModifiers() +{ +#if KWIN_BUILD_X11 + // This test verifies that a window under the cursor gets modifier events, + // even if it isn't focused + + QVERIFY(Test::waylandSeat()->hasKeyboard()); + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + + // create a Wayland window + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Window *waylandWindow = Test::renderAndWaitForShown(surface.get(), QSize(10, 10), Qt::blue); + QVERIFY(waylandWindow); + waylandWindow->move(QPoint(0, 0)); + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + const Rect windowGeometry(0, 0, 10, 10); + xcb_window_t windowId = xcb_generate_id(c.get()); + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints = {}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *x11window = windowCreatedSpy.last().first().value(); + QVERIFY(waylandWindow); + x11window->move(QPoint(10, 10)); + + workspace()->activateWindow(x11window, true); + + // Move the pointer over the now unfocused Wayland window + input()->pointer()->warp(waylandWindow->frameGeometry().center()); + QCOMPARE(waylandServer()->seat()->focusedPointerSurface(), waylandWindow->surface()); + + QSignalSpy spy(keyboard.get(), &KWayland::Client::Keyboard::modifiersChanged); + Test::keyboardKeyPressed(KEY_LEFTCTRL, 1); + QVERIFY(spy.wait()); + QCOMPARE(spy.last().at(0).toInt(), XCB_MOD_MASK_CONTROL); + + Test::keyboardKeyReleased(KEY_LEFTCTRL, 2); + + // Destroy the x11 window. + QSignalSpy windowClosedSpy(waylandWindow, &X11Window::closed); + xcb_unmap_window(c.get(), windowId); + xcb_destroy_window(c.get(), windowId); + xcb_flush(c.get()); + c.reset(); + + // Destroy the Wayland window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(waylandWindow)); +#endif +} +} + +WAYLANDTEST_MAIN(KWin::PointerInputTest) +#include "pointer_input.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/quick_tiling_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/quick_tiling_test.cpp new file mode 100644 index 0000000000..1026bd3755 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/quick_tiling_test.cpp @@ -0,0 +1,2335 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "cursor.h" +#include "decorations/decorationbridge.h" +#include "decorations/settings.h" +#include "pointer_input.h" +#include "scripting/scripting.h" +#include "tiles/tilemanager.h" +#include "utils/common.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#if KWIN_BUILD_X11 +#include "x11window.h" + +#include +#include +#endif + +Q_DECLARE_METATYPE(KWin::QuickTileMode) +Q_DECLARE_METATYPE(KWin::MaximizeMode) + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_quick_tiling-0"); + +#if KWIN_BUILD_X11 +static X11Window *createWindow(xcb_connection_t *connection, const Rect &geometry) +{ + xcb_window_t windowId = xcb_generate_id(connection); + xcb_create_window(connection, XCB_COPY_FROM_PARENT, windowId, rootWindow(), + geometry.x(), + geometry.y(), + geometry.width(), + geometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + + xcb_size_hints_t hints; + memset(&hints, 0, sizeof(hints)); + xcb_icccm_size_hints_set_position(&hints, 1, geometry.x(), geometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, geometry.width(), geometry.height()); + xcb_icccm_set_wm_normal_hints(connection, windowId, &hints); + + xcb_map_window(connection, windowId); + xcb_flush(connection); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + if (!windowCreatedSpy.wait()) { + return nullptr; + } + return windowCreatedSpy.last().first().value(); +} +#endif + +class QuickTilingTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testQuickTiling_data(); + void testQuickTiling(); + void testQuickTilingKeyboardMove_data(); + void testQuickTilingKeyboardMove(); + void testQuickTilingPointerMove_data(); + void testQuickTilingPointerMove(); + void testQuickTilingTouchMove_data(); + void testQuickTilingTouchMove(); + void testX11QuickTiling_data(); + void testX11QuickTiling(); + void testX11QuickTilingAfterVertMaximize_data(); + void testX11QuickTilingAfterVertMaximize(); + void testShortcut_data(); + void testShortcut(); + void testMultiScreen(); + void testMultiScreenX11(); + void testQuickTileAndMaximize(); + void testQuickTileAndMaximizeX11(); + void testQuickTileAndFullScreen(); + void testQuickTileAndFullScreenX11(); + void testPerDesktop(); + void testPerDesktopX11(); + void testMoveBetweenQuickTileAndCustomTileSameDesktop(); + void testMoveBetweenQuickTileAndCustomTileSameDesktopX11(); + void testMoveBetweenQuickTileAndCustomTileCrossDesktops(); + void testMoveBetweenQuickTileAndCustomTileCrossDesktopsX11(); + void testEvacuateFromRemovedDesktop(); + void testEvacuateFromRemovedDesktopX11(); + void testCloseTiledWindow(); + void testCloseTiledWindowX11(); + void testScript_data(); + void testScript(); + void testDontCrashWithMaximizeWindowRule(); +}; + +void QuickTilingTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType("MaximizeMode"); + QVERIFY(waylandServer()->init(s_socketName)); + + // set custom config which disables the Outline + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup group = config->group(QStringLiteral("Outline")); + group.writeEntry(QStringLiteral("QmlPath"), QString("/does/not/exist.qml")); + group.sync(); + + kwinApp()->setConfig(config); + + qputenv("XKB_DEFAULT_RULES", "evdev"); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); +} + +void QuickTilingTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::XdgDecorationV1)); + + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); + VirtualDesktopManager::self()->setCount(2); + VirtualDesktopManager::self()->setCurrent(1); +} + +void QuickTilingTest::cleanup() +{ + Test::destroyWaylandConnection(); + + // discard window rules + workspace()->rulebook()->load(); +} + +void QuickTilingTest::testQuickTiling_data() +{ + QTest::addColumn("mode"); + QTest::addColumn("expectedGeometry"); + QTest::addColumn("secondScreen"); + QTest::addColumn("expectedModeAfterToggle"); + +#define FLAG(name) QuickTileMode(QuickTileFlag::name) + + QTest::newRow("left") << FLAG(Left) << RectF(0, 0, 640, 1024) << RectF(1280, 0, 640, 1024) << FLAG(Right); + QTest::newRow("top") << FLAG(Top) << RectF(0, 0, 1280, 512) << RectF(1280, 0, 1280, 512) << FLAG(Top); + QTest::newRow("right") << FLAG(Right) << RectF(640, 0, 640, 1024) << RectF(1920, 0, 640, 1024) << FLAG(Right); + QTest::newRow("bottom") << FLAG(Bottom) << RectF(0, 512, 1280, 512) << RectF(1280, 512, 1280, 512) << FLAG(Bottom); + + QTest::newRow("top left") << (FLAG(Left) | FLAG(Top)) << RectF(0, 0, 640, 512) << RectF(1280, 0, 640, 512) << (FLAG(Right) | FLAG(Top)); + QTest::newRow("top right") << (FLAG(Right) | FLAG(Top)) << RectF(640, 0, 640, 512) << RectF(1920, 0, 640, 512) << (FLAG(Right) | FLAG(Top)); + QTest::newRow("bottom left") << (FLAG(Left) | FLAG(Bottom)) << RectF(0, 512, 640, 512) << RectF(1280, 512, 640, 512) << (FLAG(Right) | FLAG(Bottom)); + QTest::newRow("bottom right") << (FLAG(Right) | FLAG(Bottom)) << RectF(640, 512, 640, 512) << RectF(1920, 512, 640, 512) << (FLAG(Right) | FLAG(Bottom)); +#undef FLAG +} + +void QuickTilingTest::testQuickTiling() +{ + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + + // Map the window. + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QCOMPARE(workspace()->activeWindow(), window); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 100, 50)); + QCOMPARE(window->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + + // We have to receive a configure event when the window becomes active. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + QSignalSpy quickTileChangedSpy(window, &Window::quickTileModeChanged); + QSignalSpy tileChangedSpy(window, &Window::tileChanged); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + + QFETCH(QuickTileMode, mode); + QFETCH(QuickTileMode, expectedModeAfterToggle); + QFETCH(RectF, expectedGeometry); + const QuickTileMode oldQuickTileMode = window->quickTileMode(); + window->handleQuickTileShortcut(mode); + + // at this point the geometry did not yet change + QCOMPARE(window->frameGeometry(), RectF(0, 0, 100, 50)); + // Manually maximized window is always without a tile + QCOMPARE(window->requestedQuickTileMode(), mode); + // Actual quickTileMOde didn't change yet + QCOMPARE(window->quickTileMode(), oldQuickTileMode); + + // but we got requested a new geometry + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), expectedGeometry.size()); + + // attach a new image + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), expectedGeometry.size().toSize(), Qt::red); + + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 1); + QCOMPARE(window->frameGeometry(), expectedGeometry); + QCOMPARE(quickTileChangedSpy.count(), 1); + QCOMPARE(window->quickTileMode(), mode); + + // send window to other screen + QList outputs = workspace()->outputs(); + QCOMPARE(window->output(), outputs[0]); + + window->sendToOutput(outputs[1]); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), expectedGeometry.size()); + // attach a new image + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), expectedGeometry.size().toSize(), Qt::red); + QVERIFY(tileChangedSpy.wait()); + QCOMPARE(window->output(), outputs[1]); + // quick tile should not be changed + QCOMPARE(window->quickTileMode(), mode); + QTEST(window->frameGeometry(), "secondScreen"); + Tile *tile = workspace()->tileManager(outputs[1])->quickTile(mode); + QCOMPARE(window->tile(), tile); + + // now try to toggle again + window->handleQuickTileShortcut(mode); + QCOMPARE(window->requestedQuickTileMode(), expectedModeAfterToggle); + if (expectedModeAfterToggle != mode) { + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 4); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::red); + QVERIFY(quickTileChangedSpy.wait()); + QCOMPARE(quickTileChangedSpy.count(), 2); + } + QCOMPARE(window->quickTileMode(), expectedModeAfterToggle); +} + +void QuickTilingTest::testQuickTilingKeyboardMove_data() +{ + QTest::addColumn("targetPos"); + QTest::addColumn("expectedMode"); + + QTest::newRow("topRight") << QPoint(2559, 24) << QuickTileMode(QuickTileFlag::Top | QuickTileFlag::Right); + QTest::newRow("right") << QPoint(2559, 512) << QuickTileMode(QuickTileFlag::Right); + QTest::newRow("bottomRight") << QPoint(2559, 1023) << QuickTileMode(QuickTileFlag::Bottom | QuickTileFlag::Right); + QTest::newRow("bottomLeft") << QPoint(0, 1023) << QuickTileMode(QuickTileFlag::Bottom | QuickTileFlag::Left); + QTest::newRow("Left") << QPoint(0, 512) << QuickTileMode(QuickTileFlag::Left); + QTest::newRow("topLeft") << QPoint(0, 24) << QuickTileMode(QuickTileFlag::Top | QuickTileFlag::Left); +} + +void QuickTilingTest::testQuickTilingKeyboardMove() +{ + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + // let's render + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + QVERIFY(window); + QCOMPARE(workspace()->activeWindow(), window); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 100, 50)); + QCOMPARE(window->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + + // We have to receive a configure event when the window becomes active. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + QSignalSpy quickTileChangedSpy(window, &Window::quickTileModeChanged); + + workspace()->performWindowOperation(window, Options::UnrestrictedMoveOp); + QCOMPARE(window, workspace()->moveResizeWindow()); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(50, 25)); + + QFETCH(QPoint, targetPos); + quint32 timestamp = 1; + Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + while (Cursors::self()->mouse()->pos().x() > targetPos.x()) { + Test::keyboardKeyPressed(KEY_LEFT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFT, timestamp++); + } + while (Cursors::self()->mouse()->pos().x() < targetPos.x()) { + Test::keyboardKeyPressed(KEY_RIGHT, timestamp++); + Test::keyboardKeyReleased(KEY_RIGHT, timestamp++); + } + while (Cursors::self()->mouse()->pos().y() < targetPos.y()) { + Test::keyboardKeyPressed(KEY_DOWN, timestamp++); + Test::keyboardKeyReleased(KEY_DOWN, timestamp++); + } + while (Cursors::self()->mouse()->pos().y() > targetPos.y()) { + Test::keyboardKeyPressed(KEY_UP, timestamp++); + Test::keyboardKeyReleased(KEY_UP, timestamp++); + } + Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + Test::keyboardKeyPressed(KEY_ENTER, timestamp++); + Test::keyboardKeyReleased(KEY_ENTER, timestamp++); + QCOMPARE(Cursors::self()->mouse()->pos(), targetPos); + QVERIFY(!workspace()->moveResizeWindow()); + + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::red); + QVERIFY(quickTileChangedSpy.wait()); + QCOMPARE(quickTileChangedSpy.count(), 1); + QTEST(window->quickTileMode(), "expectedMode"); +} + +void QuickTilingTest::testQuickTilingPointerMove_data() +{ + QTest::addColumn("pointerPos"); + QTest::addColumn("tileSize"); + QTest::addColumn("expectedMode"); + + QTest::newRow("topRight") << QPoint(2559, 24) << QSize(640, 512) << QuickTileMode(QuickTileFlag::Top | QuickTileFlag::Right); + QTest::newRow("right") << QPoint(2559, 512) << QSize(640, 1024) << QuickTileMode(QuickTileFlag::Right); + QTest::newRow("bottomRight") << QPoint(2559, 1023) << QSize(640, 512) << QuickTileMode(QuickTileFlag::Bottom | QuickTileFlag::Right); + QTest::newRow("bottomLeft") << QPoint(0, 1023) << QSize(640, 512) << QuickTileMode(QuickTileFlag::Bottom | QuickTileFlag::Left); + QTest::newRow("Left") << QPoint(0, 512) << QSize(640, 1024) << QuickTileMode(QuickTileFlag::Left); + QTest::newRow("topLeft") << QPoint(0, 24) << QSize(640, 512) << QuickTileMode(QuickTileFlag::Top | QuickTileFlag::Left); +} + +void QuickTilingTest::testQuickTilingPointerMove() +{ + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + + // let's render + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QCOMPARE(workspace()->activeWindow(), window); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 100, 50)); + QCOMPARE(window->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + + // we have to receive a configure event when the window becomes active + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + // verify that basic quick tile mode works as expected, i.e. the window is going to be + // tiled if the user drags it to a screen edge or a corner + QSignalSpy quickTileChangedSpy(window, &Window::quickTileModeChanged); + workspace()->performWindowOperation(window, Options::UnrestrictedMoveOp); + QCOMPARE(window, workspace()->moveResizeWindow()); + QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(50, 25)); + + QFETCH(QPoint, pointerPos); + QFETCH(QSize, tileSize); + quint32 timestamp = 1; + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + Test::pointerMotion(pointerPos, timestamp++); + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + QTEST(window->requestedQuickTileMode(), "expectedMode"); + const QPoint tileOutputPositon = workspace()->outputAt(pointerPos)->geometry().topLeft(); + QCOMPARE(window->geometryRestore(), RectF(tileOutputPositon, QSize(100, 50))); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), tileSize); + + // attach a new image + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), tileSize, Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry().size(), tileSize); + QCOMPARE(quickTileChangedSpy.count(), 1); + QTEST(window->quickTileMode(), "expectedMode"); + + // verify that geometry restore is correct after user untiles the window, but changes + // their mind and tiles the window again while still holding left button + workspace()->performWindowOperation(window, Options::UnrestrictedMoveOp); + QCOMPARE(window, workspace()->moveResizeWindow()); + + Test::pointerButtonPressed(BTN_LEFT, timestamp++); // untile the window + Test::pointerMotion(QPoint(1280, 1024) / 2, timestamp++); + QCOMPARE(window->requestedQuickTileMode(), QuickTileMode(QuickTileFlag::None)); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(100, 50)); + + // attach a new image + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(100, 50), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry().size(), QSize(100, 50)); + QCOMPARE(quickTileChangedSpy.count(), 2); + QCOMPARE(window->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + + Test::pointerMotion(pointerPos, timestamp++); // tile the window again + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + QTEST(window->requestedQuickTileMode(), "expectedMode"); + QCOMPARE(window->geometryRestore(), RectF(tileOutputPositon, QSize(100, 50))); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 4); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), tileSize); + + // attach a new image + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(100, 50), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry().size(), QSize(100, 50)); + QCOMPARE(quickTileChangedSpy.count(), 3); + QTEST(window->quickTileMode(), "expectedMode"); +} + +void QuickTilingTest::testQuickTilingTouchMove_data() +{ + QTest::addColumn("targetPos"); + QTest::addColumn("expectedMode"); + + QTest::newRow("topRight") << QPoint(2559, 24) << QuickTileMode(QuickTileFlag::Top | QuickTileFlag::Right); + QTest::newRow("right") << QPoint(2559, 512) << QuickTileMode(QuickTileFlag::Right); + QTest::newRow("bottomRight") << QPoint(2559, 1023) << QuickTileMode(QuickTileFlag::Bottom | QuickTileFlag::Right); + QTest::newRow("bottomLeft") << QPoint(0, 1023) << QuickTileMode(QuickTileFlag::Bottom | QuickTileFlag::Left); + QTest::newRow("Left") << QPoint(0, 512) << QuickTileMode(QuickTileFlag::Left); + QTest::newRow("topLeft") << QPoint(0, 24) << QuickTileMode(QuickTileFlag::Top | QuickTileFlag::Left); +} + +void QuickTilingTest::testQuickTilingTouchMove() +{ + // test verifies that touch on decoration also allows quick tiling + // see BUG: 390113 + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + std::unique_ptr deco(Test::createXdgToplevelDecorationV1(shellSurface.get())); + + QSignalSpy decorationConfigureRequestedSpy(deco.get(), &Test::XdgToplevelDecorationV1::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + + // wait for the initial configure event + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + // let's render + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(1000, 50), Qt::blue); + + QVERIFY(window); + QVERIFY(window->isDecorated()); + const auto decoration = window->decoration(); + QCOMPARE(workspace()->activeWindow(), window); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1000 + decoration->borderLeft() + decoration->borderRight(), 50 + decoration->borderTop() + decoration->borderBottom())); + QCOMPARE(window->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + + // we have to receive a configure event when the window becomes active + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QTRY_COMPARE(surfaceConfigureRequestedSpy.count(), 2); + + QSignalSpy quickTileChangedSpy(window, &Window::quickTileModeChanged); + + // Note that interactive move will be started with a delay. + quint32 timestamp = 1; + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + Test::touchDown(0, QPointF(window->frameGeometry().center().x(), window->frameGeometry().y() + decoration->borderTop() / 2), timestamp++); + QVERIFY(interactiveMoveResizeStartedSpy.wait()); + QCOMPARE(window, workspace()->moveResizeWindow()); + + QFETCH(QPoint, targetPos); + Test::touchMotion(0, targetPos, timestamp++); + Test::touchUp(0, timestamp++); + QVERIFY(!workspace()->moveResizeWindow()); + + // When there are no borders, there is no change to them when quick-tiling. + // TODO: we should test both cases with fixed fake decoration for autotests. + const bool hasBorders = Workspace::self()->decorationBridge()->settings()->borderSize() != KDecoration3::BorderSize::None; + + QTEST(window->requestedQuickTileMode(), "expectedMode"); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QTRY_COMPARE(surfaceConfigureRequestedSpy.count(), hasBorders ? 4 : 3); + QCOMPARE(false, toplevelConfigureRequestedSpy.last().first().toSize().isEmpty()); + + // attach a new image + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::red); + QVERIFY(quickTileChangedSpy.wait()); + QCOMPARE(quickTileChangedSpy.count(), 1); + QTEST(window->quickTileMode(), "expectedMode"); +} + +void QuickTilingTest::testX11QuickTiling_data() +{ + QTest::addColumn("mode"); + QTest::addColumn("expectedGeometry"); + QTest::addColumn("screenId"); + QTest::addColumn("modeAfterToggle"); + +#define FLAG(name) QuickTileMode(QuickTileFlag::name) + + QTest::newRow("left") << FLAG(Left) << RectF(0, 0, 640, 1024) << 0 << FLAG(Left); + QTest::newRow("top") << FLAG(Top) << RectF(0, 0, 1280, 512) << 0 << FLAG(Top); + QTest::newRow("right") << FLAG(Right) << RectF(640, 0, 640, 1024) << 1 << FLAG(Left); + QTest::newRow("bottom") << FLAG(Bottom) << RectF(0, 512, 1280, 512) << 0 << FLAG(Bottom); + + QTest::newRow("top left") << (FLAG(Left) | FLAG(Top)) << RectF(0, 0, 640, 512) << 0 << (FLAG(Left) | FLAG(Top)); + QTest::newRow("top right") << (FLAG(Right) | FLAG(Top)) << RectF(640, 0, 640, 512) << 1 << (FLAG(Left) | FLAG(Top)); + QTest::newRow("bottom left") << (FLAG(Left) | FLAG(Bottom)) << RectF(0, 512, 640, 512) << 0 << (FLAG(Left) | FLAG(Bottom)); + QTest::newRow("bottom right") << (FLAG(Right) | FLAG(Bottom)) << RectF(640, 512, 640, 512) << 1 << (FLAG(Left) | FLAG(Bottom)); + +#undef FLAG +} +void QuickTilingTest::testX11QuickTiling() +{ +#if KWIN_BUILD_X11 + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + const Rect windowGeometry(0, 0, 100, 200); + xcb_window_t windowId = xcb_generate_id(c.get()); + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + // we should get a window for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.first().first().value(); + QVERIFY(window); + QCOMPARE(window->window(), windowId); + + // now quick tile + QSignalSpy quickTileChangedSpy(window, &Window::quickTileModeChanged); + const RectF origGeo = window->frameGeometry(); + QFETCH(QuickTileMode, mode); + window->handleQuickTileShortcut(mode); + QCOMPARE(quickTileChangedSpy.count(), 1); + QCOMPARE(window->quickTileMode(), mode); + QTEST(window->frameGeometry(), "expectedGeometry"); + QCOMPARE(window->geometryRestore(), origGeo); + + // quick tile to same edge again should also act like send to screen + // if screen is on the same edge + const auto outputs = workspace()->outputs(); + QCOMPARE(window->output(), outputs[0]); + window->handleQuickTileShortcut(mode); + QFETCH(int, screenId); + QCOMPARE(window->output(), outputs[screenId]); + QTEST(window->quickTileMode(), "modeAfterToggle"); + QCOMPARE(window->geometryRestore(), origGeo); + + // and destroy the window again + xcb_unmap_window(c.get(), windowId); + xcb_destroy_window(c.get(), windowId); + xcb_flush(c.get()); + c.reset(); + + QSignalSpy windowClosedSpy(window, &X11Window::closed); + QVERIFY(windowClosedSpy.wait()); +#endif +} + +void QuickTilingTest::testX11QuickTilingAfterVertMaximize_data() +{ + QTest::addColumn("mode"); + QTest::addColumn("expectedGeometry"); + +#define FLAG(name) QuickTileMode(QuickTileFlag::name) + + QTest::newRow("left") << FLAG(Left) << RectF(0, 0, 640, 1024); + QTest::newRow("top") << FLAG(Top) << RectF(0, 0, 1280, 512); + QTest::newRow("right") << FLAG(Right) << RectF(640, 0, 640, 1024); + QTest::newRow("bottom") << FLAG(Bottom) << RectF(0, 512, 1280, 512); + + QTest::newRow("top left") << (FLAG(Left) | FLAG(Top)) << RectF(0, 0, 640, 512); + QTest::newRow("top right") << (FLAG(Right) | FLAG(Top)) << RectF(640, 0, 640, 512); + QTest::newRow("bottom left") << (FLAG(Left) | FLAG(Bottom)) << RectF(0, 512, 640, 512); + QTest::newRow("bottom right") << (FLAG(Right) | FLAG(Bottom)) << RectF(640, 512, 640, 512); + +#undef FLAG +} + +void QuickTilingTest::testX11QuickTilingAfterVertMaximize() +{ +#if KWIN_BUILD_X11 + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + const Rect windowGeometry(0, 0, 100, 200); + xcb_window_t windowId = xcb_generate_id(c.get()); + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + // we should get a window for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.first().first().value(); + QVERIFY(window); + QCOMPARE(window->window(), windowId); + + const RectF origGeo = window->frameGeometry(); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + // vertically maximize the window + window->maximize(window->maximizeMode() ^ MaximizeVertical); + QCOMPARE(window->frameGeometry().width(), origGeo.width()); + QCOMPARE(window->height(), window->output()->geometry().height()); + QCOMPARE(window->geometryRestore(), origGeo); + + // now quick tile + QSignalSpy quickTileChangedSpy(window, &Window::quickTileModeChanged); + QFETCH(QuickTileMode, mode); + window->setQuickTileModeAtCurrentPosition(mode); + QCOMPARE(window->quickTileMode(), mode); + QCOMPARE(quickTileChangedSpy.count(), 1); + QTEST(window->frameGeometry(), "expectedGeometry"); + + // and destroy the window again + xcb_unmap_window(c.get(), windowId); + xcb_destroy_window(c.get(), windowId); + xcb_flush(c.get()); + c.reset(); + + QSignalSpy windowClosedSpy(window, &X11Window::closed); + QVERIFY(windowClosedSpy.wait()); +#endif +} + +void QuickTilingTest::testShortcut_data() +{ + const auto N = QuickTileMode(QuickTileFlag::None); + const auto L = QuickTileMode(QuickTileFlag::Left); + const auto R = QuickTileMode(QuickTileFlag::Right); + const auto T = QuickTileMode(QuickTileFlag::Top); + const auto B = QuickTileMode(QuickTileFlag::Bottom); + const auto TL = T | L; + const auto TR = T | R; + const auto BL = B | L; + const auto BR = B | R; + + // The transition table for quick tile mode: + // _ mode1 mode2 mode3... + // oldMode1 newMode1 newMode2 newMode3... + // oldMode2 newMode1 newMode2 newMode3... + const QuickTileMode quickTileTransition[][9] = { + {N, L, R, T, B, TL, TR, BL, BR}, // transition from N + {L, L, R, TL, BL, TL, TR, BL, BR}, // transition from L + {R, L, R, TR, BR, TL, TR, BL, BR}, + {T, TL, TR, T, B, TL, TR, BL, BR}, + {B, BL, BR, T, B, TL, TR, BL, BR}, + {TL, TL, T, TL, L, TL, TR, BL, BR}, + {TR, T, TR, TR, R, TL, TR, BL, BR}, + {BL, BL, B, L, BL, TL, TR, BL, BR}, + {BR, B, BR, R, BR, TL, TR, BL, BR}, + }; + + const QHash geometries = { + {N, RectF()}, + {L, RectF(0, 0, 640, 1024)}, + {R, RectF(640, 0, 640, 1024)}, + {T, RectF(0, 0, 1280, 512)}, + {B, RectF(0, 512, 1280, 512)}, + {TL, RectF(0, 0, 640, 512)}, + {TR, RectF(640, 0, 640, 512)}, + {BL, RectF(0, 512, 640, 512)}, + {BR, RectF(640, 512, 640, 512)}, + }; + + const QHash shortcuts = { + {L, QStringLiteral("Window Quick Tile Left")}, + {R, QStringLiteral("Window Quick Tile Right")}, + {T, QStringLiteral("Window Quick Tile Top")}, + {B, QStringLiteral("Window Quick Tile Bottom")}, + {TL, QStringLiteral("Window Quick Tile Top Left")}, + {TR, QStringLiteral("Window Quick Tile Top Right")}, + {BL, QStringLiteral("Window Quick Tile Bottom Left")}, + {BR, QStringLiteral("Window Quick Tile Bottom Right")}, + }; + + const QHash names = { + {N, QStringLiteral("None")}, + {L, QStringLiteral("Left")}, + {R, QStringLiteral("Right")}, + {T, QStringLiteral("Top")}, + {B, QStringLiteral("Bottom")}, + {TL, QStringLiteral("TopLeft")}, + {TR, QStringLiteral("TopRight")}, + {BL, QStringLiteral("BottomLeft")}, + {BR, QStringLiteral("BottomRight")}, + }; + + QTest::addColumn("oldMode"); + QTest::addColumn("shortcut"); + QTest::addColumn("expectedMode"); + QTest::addColumn("expectedGeometry"); + + for (size_t row = 0; row < sizeof(quickTileTransition) / sizeof(quickTileTransition[0]); ++row) { + for (size_t col = 1; col < sizeof(quickTileTransition[0]) / sizeof(quickTileTransition[0][0]); ++col) { + const auto oldMode = quickTileTransition[row][0]; + auto newMode = quickTileTransition[row][col]; + const auto action = quickTileTransition[0][col]; + const auto shortcut = shortcuts[action]; + auto geometry = geometries[newMode]; + + // We have another screen to the right, so when pressing right on the right edge, it goes to left + // edge of the next screen. + if (oldMode == newMode) { + if (action & QuickTileFlag::Right) { + newMode.setFlag(QuickTileFlag::Right, false); + newMode.setFlag(QuickTileFlag::Left, true); + geometry.moveLeft(1280); + } + } + + QTest::newRow(QStringLiteral("%1 -> %2 = %3").arg(names[oldMode]).arg(shortcut).arg(names[newMode]).toLatin1().constData()) + << oldMode << shortcut << newMode << geometry; + } + } +} + +void QuickTilingTest::testShortcut() +{ +#if !KWIN_BUILD_GLOBALSHORTCUTS + QSKIP("Can't test shortcuts without shortcuts"); + return; +#endif + + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + + // Map the window. + const auto initialGeometry = Rect(0, 0, 100, 50); + auto window = Test::renderAndWaitForShown(surface.get(), initialGeometry.size(), Qt::blue); + QVERIFY(window); + QCOMPARE(workspace()->activeWindow(), window); + QCOMPARE(window->frameGeometry(), initialGeometry); + QCOMPARE(window->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + + // We have to receive a configure event when the window becomes active. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + QFETCH(QuickTileMode, oldMode); + QFETCH(QString, shortcut); + QFETCH(QuickTileMode, expectedMode); + QFETCH(RectF, expectedGeometry); + + if (expectedMode == QuickTileMode(QuickTileFlag::None)) { + expectedGeometry = initialGeometry; + } + + QSignalSpy quickTileChangedSpy(window, &Window::quickTileModeChanged); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + + int numberOfQuickTileActions = 1; + + if (oldMode != QuickTileMode(QuickTileFlag::None)) { + window->handleQuickTileShortcut(oldMode); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::red); + QVERIFY(quickTileChangedSpy.wait()); + ++numberOfQuickTileActions; + } + + // invoke global shortcut through dbus + auto msg = QDBusMessage::createMethodCall( + QStringLiteral("org.kde.kglobalaccel"), + QStringLiteral("/component/kwin"), + QStringLiteral("org.kde.kglobalaccel.Component"), + QStringLiteral("invokeShortcut")); + msg.setArguments(QList{shortcut}); + QDBusConnection::sessionBus().asyncCall(msg); + + if (oldMode == expectedMode) { + QVERIFY(!surfaceConfigureRequestedSpy.wait(10)); + } else { + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::red); + QVERIFY(quickTileChangedSpy.wait()); + + QCOMPARE(surfaceConfigureRequestedSpy.count(), numberOfQuickTileActions + 1); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), expectedGeometry.size()); + QCOMPARE(frameGeometryChangedSpy.count(), numberOfQuickTileActions); + QTRY_COMPARE(quickTileChangedSpy.count(), numberOfQuickTileActions); + } + + // geometry already changed + QCOMPARE(window->frameGeometry(), expectedGeometry); + // quick tile mode already changed + QCOMPARE(window->quickTileMode(), expectedMode); + + QEXPECT_FAIL("maximize", "Geometry changed called twice for maximize", Continue); + QCOMPARE(window->frameGeometry(), expectedGeometry); +} + +void QuickTilingTest::testMultiScreen() +{ + // This test verifies that a window can be moved between screens by continuously pressing Meta+arrow. + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue); + + // We have to receive a configure event when the window becomes active. + QSignalSpy tileChangedSpy(window, &Window::tileChanged); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + TileManager *firstTileManager = workspace()->tileManager(workspace()->outputs().at(0)); + TileManager *secondTileManager = workspace()->tileManager(workspace()->outputs().at(1)); + + const struct + { + QuickTileMode shortcut; + QuickTileMode previous; + Tile *previousTile; + QuickTileMode next; + Tile *nextTile; + RectF geometry; + } steps[] = { + // Not tiled -> tiled on the left half of the first screen + { + .shortcut = QuickTileFlag::Left, + .previous = QuickTileFlag::None, + .previousTile = nullptr, + .next = QuickTileFlag::Left, + .nextTile = firstTileManager->quickTile(QuickTileFlag::Left), + .geometry = RectF(0, 0, 640, 1024), + }, + // Tiled on the left half of the first screen -> tiled on the right half of the first screen + { + .shortcut = QuickTileFlag::Right, + .previous = QuickTileFlag::Left, + .previousTile = firstTileManager->quickTile(QuickTileFlag::Left), + .next = QuickTileFlag::Right, + .nextTile = firstTileManager->quickTile(QuickTileFlag::Right), + .geometry = RectF(640, 0, 640, 1024), + }, + // Tiled on the right half of the first screen -> tiled on the left half of the second screen + { + .shortcut = QuickTileFlag::Right, + .previous = QuickTileFlag::Right, + .previousTile = firstTileManager->quickTile(QuickTileFlag::Right), + .next = QuickTileFlag::Left, + .nextTile = secondTileManager->quickTile(QuickTileFlag::Left), + .geometry = RectF(1280, 0, 640, 1024), + }, + // Tiled on the left half of the second screen -> tiled on the right half of the second screen + { + .shortcut = QuickTileFlag::Right, + .previous = QuickTileFlag::Left, + .previousTile = secondTileManager->quickTile(QuickTileFlag::Left), + .next = QuickTileFlag::Right, + .nextTile = secondTileManager->quickTile(QuickTileFlag::Right), + .geometry = RectF(1920, 0, 640, 1024), + }, + // Tiled on the right half of the second screen -> tiled on the left half of the second screen + { + .shortcut = QuickTileFlag::Left, + .previous = QuickTileFlag::Right, + .previousTile = secondTileManager->quickTile(QuickTileFlag::Right), + .next = QuickTileFlag::Left, + .nextTile = secondTileManager->quickTile(QuickTileFlag::Left), + .geometry = RectF(1280, 0, 640, 1024), + }, + // Tiled on the left half of the second screen -> tiled on the right half of the first screen + { + .shortcut = QuickTileFlag::Left, + .previous = QuickTileFlag::Left, + .previousTile = secondTileManager->quickTile(QuickTileFlag::Left), + .next = QuickTileFlag::Right, + .nextTile = firstTileManager->quickTile(QuickTileFlag::Right), + .geometry = RectF(640, 0, 640, 1024), + }, + // Tiled on the right half of the first screen -> tiled on the left half of the first screen + { + .shortcut = QuickTileFlag::Left, + .previous = QuickTileFlag::Right, + .previousTile = firstTileManager->quickTile(QuickTileFlag::Right), + .next = QuickTileFlag::Left, + .nextTile = firstTileManager->quickTile(QuickTileFlag::Left), + .geometry = RectF(0, 0, 640, 1024), + }, + }; + + for (const auto &step : steps) { + window->handleQuickTileShortcut(step.shortcut); + + QCOMPARE(window->quickTileMode(), step.previous); + QCOMPARE(window->requestedQuickTileMode(), step.next); + + QCOMPARE(window->tile(), step.previousTile); + QVERIFY(!step.previousTile || !step.previousTile->windows().contains(window)); + QCOMPARE(window->requestedTile(), step.nextTile); + QVERIFY(step.nextTile->windows().contains(window)); + + QCOMPARE(window->moveResizeGeometry(), step.geometry); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::blue); + QVERIFY(tileChangedSpy.wait()); + QCOMPARE(window->quickTileMode(), step.next); + QCOMPARE(window->requestedQuickTileMode(), step.next); + QCOMPARE(window->tile(), step.nextTile); + QCOMPARE(window->requestedTile(), step.nextTile); + QCOMPARE(window->frameGeometry(), step.geometry); + QCOMPARE(window->moveResizeGeometry(), step.geometry); + } +} + +void QuickTilingTest::testMultiScreenX11() +{ +#if KWIN_BUILD_X11 + // This test verifies that an X11 window can be moved between screens by continuously pressing Meta+arrow. + + Test::XcbConnectionPtr connection = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(connection.get())); + X11Window *window = createWindow(connection.get(), Rect(0, 0, 100, 200)); + + TileManager *firstTileManager = workspace()->tileManager(workspace()->outputs().at(0)); + TileManager *secondTileManager = workspace()->tileManager(workspace()->outputs().at(1)); + + const struct + { + QuickTileMode shortcut; + QuickTileMode previous; + Tile *previousTile; + QuickTileMode next; + Tile *nextTile; + RectF geometry; + } steps[] = { + // Not tiled -> tiled on the left half of the first screen + { + .shortcut = QuickTileFlag::Left, + .previous = QuickTileFlag::None, + .previousTile = nullptr, + .next = QuickTileFlag::Left, + .nextTile = firstTileManager->quickTile(QuickTileFlag::Left), + .geometry = RectF(0, 0, 640, 1024), + }, + // Tiled on the left half of the first screen -> tiled on the right half of the first screen + { + .shortcut = QuickTileFlag::Right, + .previous = QuickTileFlag::Left, + .previousTile = firstTileManager->quickTile(QuickTileFlag::Left), + .next = QuickTileFlag::Right, + .nextTile = firstTileManager->quickTile(QuickTileFlag::Right), + .geometry = RectF(640, 0, 640, 1024), + }, + // Tiled on the right half of the first screen -> tiled on the left half of the second screen + { + .shortcut = QuickTileFlag::Right, + .previous = QuickTileFlag::Right, + .previousTile = firstTileManager->quickTile(QuickTileFlag::Right), + .next = QuickTileFlag::Left, + .nextTile = secondTileManager->quickTile(QuickTileFlag::Left), + .geometry = RectF(1280, 0, 640, 1024), + }, + // Tiled on the left half of the second screen -> tiled on the right half of the second screen + { + .shortcut = QuickTileFlag::Right, + .previous = QuickTileFlag::Left, + .previousTile = secondTileManager->quickTile(QuickTileFlag::Left), + .next = QuickTileFlag::Right, + .nextTile = secondTileManager->quickTile(QuickTileFlag::Right), + .geometry = RectF(1920, 0, 640, 1024), + }, + // Tiled on the right half of the second screen -> tiled on the left half of the second screen + { + .shortcut = QuickTileFlag::Left, + .previous = QuickTileFlag::Right, + .previousTile = secondTileManager->quickTile(QuickTileFlag::Right), + .next = QuickTileFlag::Left, + .nextTile = secondTileManager->quickTile(QuickTileFlag::Left), + .geometry = RectF(1280, 0, 640, 1024), + }, + // Tiled on the left half of the second screen -> tiled on the right half of the first screen + { + .shortcut = QuickTileFlag::Left, + .previous = QuickTileFlag::Left, + .previousTile = secondTileManager->quickTile(QuickTileFlag::Left), + .next = QuickTileFlag::Right, + .nextTile = firstTileManager->quickTile(QuickTileFlag::Right), + .geometry = RectF(640, 0, 640, 1024), + }, + // Tiled on the right half of the first screen -> tiled on the left half of the first screen + { + .shortcut = QuickTileFlag::Left, + .previous = QuickTileFlag::Right, + .previousTile = firstTileManager->quickTile(QuickTileFlag::Right), + .next = QuickTileFlag::Left, + .nextTile = firstTileManager->quickTile(QuickTileFlag::Left), + .geometry = RectF(0, 0, 640, 1024), + }, + }; + + for (const auto &step : steps) { + QCOMPARE(window->quickTileMode(), step.previous); + QCOMPARE(window->requestedQuickTileMode(), step.previous); + QCOMPARE(window->tile(), step.previousTile); + QCOMPARE(window->requestedTile(), step.previousTile); + + window->handleQuickTileShortcut(step.shortcut); + + QVERIFY(!step.previousTile || !step.previousTile->windows().contains(window)); + QVERIFY(step.nextTile->windows().contains(window)); + + QCOMPARE(window->moveResizeGeometry(), step.geometry); + QCOMPARE(window->quickTileMode(), step.next); + QCOMPARE(window->requestedQuickTileMode(), step.next); + QCOMPARE(window->tile(), step.nextTile); + QCOMPARE(window->requestedTile(), step.nextTile); + QCOMPARE(window->frameGeometry(), step.geometry); + QCOMPARE(window->moveResizeGeometry(), step.geometry); + } +#endif +} + +void QuickTilingTest::testQuickTileAndMaximize() +{ + // This test verifies that quick tile and maximize mode are mutually exclusive. + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue); + + // We have to receive a configure event when the window becomes active. + QSignalSpy tileChangedSpy(window, &Window::tileChanged); + QSignalSpy maximizedChanged(window, &Window::maximizedChanged); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + QuickTileMode previousQuickTileMode = QuickTileFlag::None; + MaximizeMode previousMaximizeMode = MaximizeRestore; + + auto quickTile = [&]() { + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::Right); + QCOMPARE(window->geometryRestore(), RectF(0, 0, 100, 100)); + QCOMPARE(window->quickTileMode(), previousQuickTileMode); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->maximizeMode(), previousMaximizeMode); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::blue); + QVERIFY(tileChangedSpy.wait()); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), RectF(640, 0, 640, 1024)); + QCOMPARE(window->moveResizeGeometry(), RectF(640, 0, 640, 1024)); + + previousMaximizeMode = window->maximizeMode(); + previousQuickTileMode = window->quickTileMode(); + }; + + auto maximize = [&]() { + window->maximize(MaximizeFull); + QCOMPARE(window->geometryRestore(), RectF(0, 0, 100, 100)); + QCOMPARE(window->quickTileMode(), previousQuickTileMode); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + QCOMPARE(window->maximizeMode(), previousMaximizeMode); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::blue); + QVERIFY(maximizedChanged.wait()); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); + QCOMPARE(window->moveResizeGeometry(), RectF(0, 0, 1280, 1024)); + + previousMaximizeMode = window->maximizeMode(); + previousQuickTileMode = window->quickTileMode(); + }; + + auto restore = [&]() { + window->maximize(MaximizeRestore); + QCOMPARE(window->geometryRestore(), RectF(0, 0, 100, 100)); + QCOMPARE(window->quickTileMode(), previousQuickTileMode); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + QCOMPARE(window->maximizeMode(), previousMaximizeMode); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::blue); + QVERIFY(maximizedChanged.wait()); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 100, 100)); + QCOMPARE(window->moveResizeGeometry(), RectF(0, 0, 100, 100)); + + previousMaximizeMode = window->maximizeMode(); + previousQuickTileMode = window->quickTileMode(); + }; + + quickTile(); + maximize(); + restore(); + + quickTile(); + maximize(); + restore(); + + quickTile(); + maximize(); + quickTile(); + maximize(); +} + +void QuickTilingTest::testQuickTileAndMaximizeX11() +{ +#if KWIN_BUILD_X11 + // This test verifies that quick tile and maximize mode are mutually exclusive. + + Test::XcbConnectionPtr connection = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(connection.get())); + X11Window *window = createWindow(connection.get(), Rect(0, 0, 100, 200)); + + QuickTileMode previousQuickTileMode = QuickTileFlag::None; + MaximizeMode previousMaximizeMode = MaximizeRestore; + RectF originalGeometry = window->frameGeometry(); + + auto quickTile = [&]() { + QCOMPARE(window->geometryRestore(), originalGeometry); + QCOMPARE(window->quickTileMode(), previousQuickTileMode); + QCOMPARE(window->requestedQuickTileMode(), previousQuickTileMode); + QCOMPARE(window->maximizeMode(), previousMaximizeMode); + QCOMPARE(window->requestedMaximizeMode(), previousMaximizeMode); + + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::Right); + + QCOMPARE(window->quickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), RectF(640, 0, 640, 1024)); + QCOMPARE(window->moveResizeGeometry(), RectF(640, 0, 640, 1024)); + + previousMaximizeMode = window->maximizeMode(); + previousQuickTileMode = window->quickTileMode(); + }; + + auto maximize = [&]() { + QCOMPARE(window->geometryRestore(), originalGeometry); + QCOMPARE(window->quickTileMode(), previousQuickTileMode); + QCOMPARE(window->requestedQuickTileMode(), previousQuickTileMode); + QCOMPARE(window->maximizeMode(), previousMaximizeMode); + QCOMPARE(window->requestedMaximizeMode(), previousMaximizeMode); + + window->maximize(MaximizeFull); + + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); + QCOMPARE(window->moveResizeGeometry(), RectF(0, 0, 1280, 1024)); + + previousMaximizeMode = window->maximizeMode(); + previousQuickTileMode = window->quickTileMode(); + }; + + auto restore = [&]() { + QCOMPARE(window->geometryRestore(), originalGeometry); + QCOMPARE(window->quickTileMode(), previousQuickTileMode); + QCOMPARE(window->requestedQuickTileMode(), previousQuickTileMode); + QCOMPARE(window->maximizeMode(), previousMaximizeMode); + QCOMPARE(window->requestedMaximizeMode(), previousMaximizeMode); + + window->maximize(MaximizeRestore); + + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), originalGeometry); + QCOMPARE(window->moveResizeGeometry(), originalGeometry); + + previousMaximizeMode = window->maximizeMode(); + previousQuickTileMode = window->quickTileMode(); + }; + + quickTile(); + maximize(); + restore(); + + quickTile(); + maximize(); + restore(); + + quickTile(); + maximize(); + quickTile(); + maximize(); +#endif +} + +void QuickTilingTest::testQuickTileAndFullScreen() +{ + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue); + + // We have to receive a configure event when the window becomes active. + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + auto ackConfigure = [&]() { + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + }; + + // tile the window in the left half of the screen on the first virtual desktop + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::Left); + QCOMPARE(window->geometryRestore(), RectF(0, 0, 100, 100)); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + ackConfigure(); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 640, 1024)); + + // make the window fullscreen + window->setFullScreen(true); + QCOMPARE(window->fullscreenGeometryRestore(), RectF(0, 0, 640, 1024)); + QCOMPARE(window->isFullScreen(), false); + QCOMPARE(window->isRequestedFullScreen(), true); + QCOMPARE(window->geometryRestore(), RectF(0, 0, 100, 100)); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + ackConfigure(); + QCOMPARE(window->isFullScreen(), true); + QCOMPARE(window->isRequestedFullScreen(), true); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); + + // leave fullscreen mode + window->setFullScreen(false); + QCOMPARE(window->isFullScreen(), true); + QCOMPARE(window->isRequestedFullScreen(), false); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + ackConfigure(); + QCOMPARE(window->isFullScreen(), false); + QCOMPARE(window->isRequestedFullScreen(), false); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 640, 1024)); + + // untile the window + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::None); + QCOMPARE(window->isFullScreen(), false); + QCOMPARE(window->isRequestedFullScreen(), false); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + ackConfigure(); + QCOMPARE(window->isFullScreen(), false); + QCOMPARE(window->isRequestedFullScreen(), false); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 100, 100)); + + // make the window fullscreen + window->setFullScreen(true); + QCOMPARE(window->fullscreenGeometryRestore(), RectF(0, 0, 100, 100)); + QCOMPARE(window->isFullScreen(), false); + QCOMPARE(window->isRequestedFullScreen(), true); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + ackConfigure(); + QCOMPARE(window->isFullScreen(), true); + QCOMPARE(window->isRequestedFullScreen(), true); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); + + // attempt to tile the window + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::Left); + QCOMPARE(window->isFullScreen(), true); + QCOMPARE(window->isRequestedFullScreen(), true); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); +} + +void QuickTilingTest::testQuickTileAndFullScreenX11() +{ +#if KWIN_BUILD_X11 + Test::XcbConnectionPtr connection = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(connection.get())); + X11Window *window = createWindow(connection.get(), Rect(0, 0, 100, 200)); + + const RectF originalGeometry = window->frameGeometry(); + + // tile the window in the left half of the screen on the first virtual desktop + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::Left); + QCOMPARE(window->geometryRestore(), originalGeometry); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 640, 1024)); + + // make the window fullscreen + window->setFullScreen(true); + QCOMPARE(window->fullscreenGeometryRestore(), RectF(0, 0, 640, 1024)); + QCOMPARE(window->isFullScreen(), true); + QCOMPARE(window->isRequestedFullScreen(), true); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); + + // leave fullscreen mode + window->setFullScreen(false); + QCOMPARE(window->isFullScreen(), false); + QCOMPARE(window->isRequestedFullScreen(), false); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 640, 1024)); + + // untile the window + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::None); + QCOMPARE(window->isFullScreen(), false); + QCOMPARE(window->isRequestedFullScreen(), false); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + QCOMPARE(window->frameGeometry(), originalGeometry); + + // make the window fullscreen + window->setFullScreen(true); + QCOMPARE(window->fullscreenGeometryRestore(), originalGeometry); + QCOMPARE(window->isFullScreen(), true); + QCOMPARE(window->isRequestedFullScreen(), true); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); + + // attempt to tile the window + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::Left); + QCOMPARE(window->isFullScreen(), true); + QCOMPARE(window->isRequestedFullScreen(), true); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); +#endif +} + +void QuickTilingTest::testPerDesktop() +{ + // This test verifies that a window can be tiled differently depending on the virtual desktop. + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue); + + // We have to receive a configure event when the window becomes active. + QSignalSpy tileChangedSpy(window, &Window::tileChanged); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + auto ackConfigure = [&]() { + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::blue); + QVERIFY(tileChangedSpy.wait()); + }; + + // tile the window in the left half of the screen on the first virtual desktop + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::Left); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + ackConfigure(); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 640, 1024)); + + // switch to the second virtual desktop, the window will still remain tiled, although invisible + VirtualDesktopManager::self()->setCurrent(2); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + VirtualDesktopManager::self()->setCurrent(1); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + + // nothing will happen if the window is untiled on the second virtual desktop + VirtualDesktopManager::self()->setCurrent(2); + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::None); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + + // tile the window in the right half of the screen on the second virtual desktop + window->setOnAllDesktops(true); + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::Right); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Right); + ackConfigure(); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->frameGeometry(), RectF(640, 0, 640, 1024)); + + // when we return back to the first virtual desktop, the window will be tiled in the left half of the screen + VirtualDesktopManager::self()->setCurrent(1); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + ackConfigure(); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 640, 1024)); + + // and if we go back to the second virtual desktop, the window will be tiled in the right half of the screen + VirtualDesktopManager::self()->setCurrent(2); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Right); + ackConfigure(); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->frameGeometry(), RectF(640, 0, 640, 1024)); + + // untile the window on the second virtual desktop + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::None); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + ackConfigure(); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 100, 100)); + + // go back to the first virtual desktop, the window will be tiled + VirtualDesktopManager::self()->setCurrent(1); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + ackConfigure(); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 640, 1024)); + + // go to the second virtual desktop, the window will be untiled + VirtualDesktopManager::self()->setCurrent(2); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + ackConfigure(); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 100, 100)); +} + +void QuickTilingTest::testPerDesktopX11() +{ +#if KWIN_BUILD_X11 + // This test verifies that an X11 window can be tiled differently depending on the virtual desktop. + + Test::XcbConnectionPtr connection = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(connection.get())); + X11Window *window = createWindow(connection.get(), Rect(0, 0, 100, 200)); + + QSignalSpy tileChangedSpy(window, &Window::tileChanged); + const RectF originalGeometry = window->frameGeometry(); + + // tile the window in the left half of the screen on the first virtual desktop + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::Left); + QCOMPARE(tileChangedSpy.count(), 1); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 640, 1024)); + + // switch to the second virtual desktop, the window will still remain tiled, although invisible + VirtualDesktopManager::self()->setCurrent(2); + QCOMPARE(tileChangedSpy.count(), 1); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + VirtualDesktopManager::self()->setCurrent(1); + QCOMPARE(tileChangedSpy.count(), 1); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + + // nothing will happen if the window is untiled on the second virtual desktop + VirtualDesktopManager::self()->setCurrent(2); + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::None); + QCOMPARE(tileChangedSpy.count(), 1); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + + // tile the window in the right half of the screen on the second virtual desktop + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + window->setOnAllDesktops(true); + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::Right); + QCOMPARE(tileChangedSpy.count(), 2); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->frameGeometry(), RectF(640, 0, 640, 1024)); + + // when we return back to the first virtual desktop, the window will be tiled in the left half of the screen + VirtualDesktopManager::self()->setCurrent(1); + QCOMPARE(tileChangedSpy.count(), 3); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 640, 1024)); + + // and if we go back to the second virtual desktop, the window will be tiled in the right half of the screen + VirtualDesktopManager::self()->setCurrent(2); + QCOMPARE(tileChangedSpy.count(), 4); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->frameGeometry(), RectF(640, 0, 640, 1024)); + + // untile the window on the second virtual desktop + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::None); + QCOMPARE(tileChangedSpy.count(), 5); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + QCOMPARE(window->frameGeometry(), originalGeometry); + + // go back to the first virtual desktop, the window will be tiled + VirtualDesktopManager::self()->setCurrent(1); + QCOMPARE(tileChangedSpy.count(), 6); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Left); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 640, 1024)); + + // go to the second virtual desktop, the window will be untiled + VirtualDesktopManager::self()->setCurrent(2); + QCOMPARE(tileChangedSpy.count(), 7); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + QCOMPARE(window->frameGeometry(), originalGeometry); +#endif +} + +void QuickTilingTest::testMoveBetweenQuickTileAndCustomTileSameDesktop() +{ + // This test checks that a window can be moved between quick tiles and custom tiles on the same virtual desktop. + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue); + + // We have to receive a configure event when the window becomes active. + QSignalSpy tileChangedSpy(window, &Window::tileChanged); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + auto ackConfigure = [&]() { + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::blue); + QVERIFY(tileChangedSpy.wait()); + }; + + const RectF originalGeometry = window->frameGeometry(); + const auto outputs = workspace()->outputs(); + for (LogicalOutput *first : outputs) { + for (LogicalOutput *second : outputs) { + const QPointF customPoint = first->geometry().center(); + const QPointF quickPoint = second->geometry().center(); + Tile *customTile = workspace()->rootTile(first)->pick(customPoint); + Tile *quickTile = workspace()->tileManager(second)->quickTile(QuickTileFlag::Left); + + window->setQuickTileMode(QuickTileFlag::Left, quickPoint); + QCOMPARE(window->tile(), nullptr); + QVERIFY(!customTile->windows().contains(window)); + QCOMPARE(window->requestedTile(), quickTile); + QVERIFY(quickTile->windows().contains(window)); + ackConfigure(); + QCOMPARE(window->tile(), quickTile); + QCOMPARE(window->requestedTile(), quickTile); + QCOMPARE(window->frameGeometry(), quickTile->windowGeometry()); + + window->setQuickTileMode(QuickTileFlag::Custom, customPoint); + QCOMPARE(window->tile(), quickTile); + QVERIFY(!quickTile->windows().contains(window)); + QCOMPARE(window->requestedTile(), customTile); + QVERIFY(customTile->windows().contains(window)); + ackConfigure(); + QCOMPARE(window->tile(), customTile); + QCOMPARE(window->requestedTile(), customTile); + QCOMPARE(window->frameGeometry(), customTile->windowGeometry()); + + window->setQuickTileMode(QuickTileFlag::Left, quickPoint); + QCOMPARE(window->tile(), customTile); + QVERIFY(!customTile->windows().contains(window)); + QCOMPARE(window->requestedTile(), quickTile); + QVERIFY(quickTile->windows().contains(window)); + ackConfigure(); + QCOMPARE(window->tile(), quickTile); + QCOMPARE(window->requestedTile(), quickTile); + QCOMPARE(window->frameGeometry(), quickTile->windowGeometry()); + + window->setQuickTileMode(QuickTileFlag::Custom, customPoint); + QCOMPARE(window->tile(), quickTile); + QVERIFY(!quickTile->windows().contains(window)); + QCOMPARE(window->requestedTile(), customTile); + QVERIFY(customTile->windows().contains(window)); + ackConfigure(); + QCOMPARE(window->tile(), customTile); + QCOMPARE(window->requestedTile(), customTile); + QCOMPARE(window->frameGeometry(), customTile->windowGeometry()); + + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::None); + QCOMPARE(window->tile(), customTile); + QVERIFY(!customTile->windows().contains(window)); + QCOMPARE(window->requestedTile(), nullptr); + QVERIFY(!quickTile->windows().contains(window)); + ackConfigure(); + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->requestedTile(), nullptr); + QCOMPARE(window->frameGeometry(), originalGeometry); + } + } +} + +void QuickTilingTest::testMoveBetweenQuickTileAndCustomTileSameDesktopX11() +{ +#if KWIN_BUILD_X11 + // This test checks that an X11 window can be moved between quick tiles and custom tiles on the same virtual desktop. + + Test::XcbConnectionPtr connection = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(connection.get())); + X11Window *window = createWindow(connection.get(), Rect(0, 0, 100, 200)); + + QSignalSpy tileChangedSpy(window, &Window::tileChanged); + const RectF originalGeometry = window->frameGeometry(); + + const auto outputs = workspace()->outputs(); + for (LogicalOutput *first : outputs) { + for (LogicalOutput *second : outputs) { + const QPointF customPoint = first->geometry().center(); + const QPointF quickPoint = second->geometry().center(); + Tile *customTile = workspace()->rootTile(first)->pick(customPoint); + Tile *quickTile = workspace()->tileManager(second)->quickTile(QuickTileFlag::Left); + + { + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->requestedTile(), nullptr); + QVERIFY(!customTile->windows().contains(window)); + QVERIFY(!quickTile->windows().contains(window)); + + window->setQuickTileMode(QuickTileFlag::Left, quickPoint); + + QCOMPARE(window->tile(), quickTile); + QCOMPARE(window->requestedTile(), quickTile); + QVERIFY(!customTile->windows().contains(window)); + QVERIFY(quickTile->windows().contains(window)); + QCOMPARE(window->frameGeometry(), quickTile->windowGeometry()); + } + + { + QCOMPARE(window->tile(), quickTile); + QCOMPARE(window->requestedTile(), quickTile); + QVERIFY(!customTile->windows().contains(window)); + QVERIFY(quickTile->windows().contains(window)); + + window->setQuickTileMode(QuickTileFlag::Custom, customPoint); + + QCOMPARE(window->tile(), customTile); + QCOMPARE(window->requestedTile(), customTile); + QVERIFY(customTile->windows().contains(window)); + QVERIFY(!quickTile->windows().contains(window)); + QCOMPARE(window->frameGeometry(), customTile->windowGeometry()); + } + + { + QCOMPARE(window->tile(), customTile); + QCOMPARE(window->requestedTile(), customTile); + QVERIFY(customTile->windows().contains(window)); + QVERIFY(!quickTile->windows().contains(window)); + + window->setQuickTileMode(QuickTileFlag::Left, quickPoint); + + QCOMPARE(window->tile(), quickTile); + QCOMPARE(window->requestedTile(), quickTile); + QVERIFY(!customTile->windows().contains(window)); + QVERIFY(quickTile->windows().contains(window)); + QCOMPARE(window->frameGeometry(), quickTile->windowGeometry()); + } + + { + QCOMPARE(window->tile(), quickTile); + QCOMPARE(window->requestedTile(), quickTile); + QVERIFY(!customTile->windows().contains(window)); + QVERIFY(quickTile->windows().contains(window)); + + window->setQuickTileMode(QuickTileFlag::Custom, customPoint); + + QCOMPARE(window->tile(), customTile); + QCOMPARE(window->requestedTile(), customTile); + QVERIFY(customTile->windows().contains(window)); + QVERIFY(!quickTile->windows().contains(window)); + QCOMPARE(window->frameGeometry(), customTile->windowGeometry()); + } + + { + QCOMPARE(window->tile(), customTile); + QCOMPARE(window->requestedTile(), customTile); + QVERIFY(customTile->windows().contains(window)); + QVERIFY(!quickTile->windows().contains(window)); + + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::None); + + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->requestedTile(), nullptr); + QVERIFY(!customTile->windows().contains(window)); + QVERIFY(!quickTile->windows().contains(window)); + QCOMPARE(window->frameGeometry(), originalGeometry); + } + } + } +#endif +} + +void QuickTilingTest::testMoveBetweenQuickTileAndCustomTileCrossDesktops() +{ + auto vds = VirtualDesktopManager::self(); + const auto desktops = vds->desktops(); + const auto outputs = workspace()->outputs(); + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue); + window->setOnAllDesktops(true); + + // We have to receive a configure event when the window becomes active. + QSignalSpy tileChangedSpy(window, &Window::tileChanged); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + auto ackConfigure = [&]() { + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::blue); + QVERIFY(tileChangedSpy.wait()); + }; + + auto applyTileLayout = [](CustomTile *tile, qreal left, qreal right) { + const auto previousKiddos = tile->childTiles(); + for (Tile *kiddo : previousKiddos) { + tile->destroyChild(kiddo); + } + + tile->split(Tile::LayoutDirection::Horizontal); + tile->childTiles().at(0)->setRelativeGeometry(RectF(0, 0, left, 1.0)); + + QCOMPARE(tile->childTiles().at(0)->relativeGeometry(), RectF(0, 0, left, 1)); + QCOMPARE(tile->childTiles().at(1)->relativeGeometry(), RectF(left, 0, right, 1)); + }; + applyTileLayout(workspace()->rootTile(outputs.at(0), desktops.at(0)), 0.4, 0.6); + applyTileLayout(workspace()->rootTile(outputs.at(0), desktops.at(1)), 0.35, 0.65); + applyTileLayout(workspace()->rootTile(outputs.at(1), desktops.at(0)), 0.3, 0.7); + applyTileLayout(workspace()->rootTile(outputs.at(1), desktops.at(1)), 0.25, 0.75); + + const RectF originalGeometry = window->frameGeometry(); + for (VirtualDesktop *customTileDesktop : desktops) { + for (VirtualDesktop *quickTileDesktop : desktops) { + if (customTileDesktop == quickTileDesktop) { + continue; + } + + for (LogicalOutput *customTileOutput : outputs) { + for (LogicalOutput *quickTileOutput : outputs) { + Tile *quickTile = workspace()->tileManager(quickTileOutput)->quickRootTile(quickTileDesktop)->tileForMode(QuickTileFlag::Left); + Tile *customTile = workspace()->rootTile(customTileOutput, customTileDesktop)->childTile(1); + + // put the window in a custom tile on the first virtual desktop + vds->setCurrent(customTileDesktop); + customTile->manage(window); + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->requestedTile(), customTile); + QCOMPARE(window->frameGeometry(), originalGeometry); + ackConfigure(); + QCOMPARE(window->tile(), customTile); + QCOMPARE(window->requestedTile(), customTile); + QCOMPARE(window->frameGeometry(), customTile->windowGeometry()); + + // switch to the second virtual desktop, the window will be untiled + vds->setCurrent(quickTileDesktop); + QCOMPARE(window->tile(), customTile); + QCOMPARE(window->requestedTile(), nullptr); + QCOMPARE(window->frameGeometry(), customTile->windowGeometry()); + ackConfigure(); + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->requestedTile(), nullptr); + QCOMPARE(window->frameGeometry(), originalGeometry); + + // put the window in a quick tile on the second virtual desktop + quickTile->manage(window); + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->requestedTile(), quickTile); + QCOMPARE(window->frameGeometry(), originalGeometry); + ackConfigure(); + QCOMPARE(window->tile(), quickTile); + QCOMPARE(window->requestedTile(), quickTile); + QCOMPARE(window->frameGeometry(), quickTile->windowGeometry()); + + // switch to the first virtual desktop + vds->setCurrent(customTileDesktop); + QCOMPARE(window->tile(), quickTile); + QCOMPARE(window->requestedTile(), customTile); + QCOMPARE(window->frameGeometry(), quickTile->windowGeometry()); + ackConfigure(); + QCOMPARE(window->tile(), customTile); + QCOMPARE(window->requestedTile(), customTile); + QCOMPARE(window->frameGeometry(), customTile->windowGeometry()); + + // switch to the second virtual desktop + vds->setCurrent(quickTileDesktop); + QCOMPARE(window->tile(), customTile); + QCOMPARE(window->requestedTile(), quickTile); + QCOMPARE(window->frameGeometry(), customTile->windowGeometry()); + ackConfigure(); + QCOMPARE(window->tile(), quickTile); + QCOMPARE(window->requestedTile(), quickTile); + QCOMPARE(window->frameGeometry(), quickTile->windowGeometry()); + + // remove the window from the quick tile on the second virtual desktop + quickTile->unmanage(window); + QCOMPARE(window->tile(), quickTile); + QCOMPARE(window->requestedTile(), nullptr); + QCOMPARE(window->frameGeometry(), quickTile->windowGeometry()); + ackConfigure(); + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->requestedTile(), nullptr); + QCOMPARE(window->frameGeometry(), originalGeometry); + + // switch to the first virtual desktop + vds->setCurrent(customTileDesktop); + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->requestedTile(), customTile); + QCOMPARE(window->frameGeometry(), originalGeometry); + ackConfigure(); + QCOMPARE(window->tile(), customTile); + QCOMPARE(window->requestedTile(), customTile); + QCOMPARE(window->frameGeometry(), customTile->windowGeometry()); + + // remove the window from the custom tile on the first virtual desktop + customTile->unmanage(window); + QCOMPARE(window->tile(), customTile); + QCOMPARE(window->requestedTile(), nullptr); + QCOMPARE(window->frameGeometry(), customTile->windowGeometry()); + ackConfigure(); + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->requestedTile(), nullptr); + QCOMPARE(window->frameGeometry(), originalGeometry); + } + } + } + } +} + +void QuickTilingTest::testMoveBetweenQuickTileAndCustomTileCrossDesktopsX11() +{ +#if KWIN_BUILD_X11 + auto vds = VirtualDesktopManager::self(); + const auto desktops = vds->desktops(); + const auto outputs = workspace()->outputs(); + + Test::XcbConnectionPtr connection = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(connection.get())); + X11Window *window = createWindow(connection.get(), Rect(0, 0, 100, 200)); + window->setOnAllDesktops(true); + + auto applyTileLayout = [](CustomTile *tile, qreal left, qreal right) { + const auto previousKiddos = tile->childTiles(); + for (Tile *kiddo : previousKiddos) { + tile->destroyChild(kiddo); + } + + tile->split(Tile::LayoutDirection::Horizontal); + tile->childTiles().at(0)->setRelativeGeometry(RectF(0, 0, left, 1.0)); + + QCOMPARE(tile->childTiles().at(0)->relativeGeometry(), RectF(0, 0, left, 1)); + QCOMPARE(tile->childTiles().at(1)->relativeGeometry(), RectF(left, 0, right, 1)); + }; + applyTileLayout(workspace()->rootTile(outputs.at(0), desktops.at(0)), 0.4, 0.6); + applyTileLayout(workspace()->rootTile(outputs.at(0), desktops.at(1)), 0.35, 0.65); + applyTileLayout(workspace()->rootTile(outputs.at(1), desktops.at(0)), 0.3, 0.7); + applyTileLayout(workspace()->rootTile(outputs.at(1), desktops.at(1)), 0.25, 0.75); + + const RectF originalGeometry = window->frameGeometry(); + for (VirtualDesktop *customTileDesktop : desktops) { + for (VirtualDesktop *quickTileDesktop : desktops) { + if (customTileDesktop == quickTileDesktop) { + continue; + } + + for (LogicalOutput *customTileOutput : outputs) { + for (LogicalOutput *quickTileOutput : outputs) { + Tile *quickTile = workspace()->tileManager(quickTileOutput)->quickRootTile(quickTileDesktop)->tileForMode(QuickTileFlag::Left); + Tile *customTile = workspace()->rootTile(customTileOutput, customTileDesktop)->childTile(1); + + // put the window in a custom tile on the first virtual desktop + { + vds->setCurrent(customTileDesktop); + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->requestedTile(), nullptr); + QCOMPARE(window->frameGeometry(), originalGeometry); + + customTile->manage(window); + + QCOMPARE(window->tile(), customTile); + QCOMPARE(window->requestedTile(), customTile); + QCOMPARE(window->frameGeometry(), customTile->windowGeometry()); + } + + // switch to the second virtual desktop, the window will be untiled + { + QCOMPARE(window->tile(), customTile); + QCOMPARE(window->requestedTile(), customTile); + QCOMPARE(window->frameGeometry(), customTile->windowGeometry()); + + vds->setCurrent(quickTileDesktop); + + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->requestedTile(), nullptr); + QCOMPARE(window->frameGeometry(), originalGeometry); + } + + // put the window in a quick tile on the second virtual desktop + { + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->requestedTile(), nullptr); + QCOMPARE(window->frameGeometry(), originalGeometry); + + quickTile->manage(window); + + QCOMPARE(window->tile(), quickTile); + QCOMPARE(window->requestedTile(), quickTile); + QCOMPARE(window->frameGeometry(), quickTile->windowGeometry()); + } + + // switch to the first virtual desktop + { + QCOMPARE(window->tile(), quickTile); + QCOMPARE(window->requestedTile(), quickTile); + QCOMPARE(window->frameGeometry(), quickTile->windowGeometry()); + + vds->setCurrent(customTileDesktop); + + QCOMPARE(window->tile(), customTile); + QCOMPARE(window->requestedTile(), customTile); + QCOMPARE(window->frameGeometry(), customTile->windowGeometry()); + } + + // switch to the second virtual desktop + { + QCOMPARE(window->tile(), customTile); + QCOMPARE(window->requestedTile(), customTile); + QCOMPARE(window->frameGeometry(), customTile->windowGeometry()); + + vds->setCurrent(quickTileDesktop); + + QCOMPARE(window->tile(), quickTile); + QCOMPARE(window->requestedTile(), quickTile); + QCOMPARE(window->frameGeometry(), quickTile->windowGeometry()); + } + + // remove the window from the quick tile on the second virtual desktop + { + QCOMPARE(window->tile(), quickTile); + QCOMPARE(window->requestedTile(), quickTile); + QCOMPARE(window->frameGeometry(), quickTile->windowGeometry()); + + quickTile->unmanage(window); + + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->requestedTile(), nullptr); + QCOMPARE(window->frameGeometry(), originalGeometry); + } + + // switch to the first virtual desktop + { + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->requestedTile(), nullptr); + QCOMPARE(window->frameGeometry(), originalGeometry); + + vds->setCurrent(customTileDesktop); + + QCOMPARE(window->tile(), customTile); + QCOMPARE(window->requestedTile(), customTile); + QCOMPARE(window->frameGeometry(), customTile->windowGeometry()); + } + + // remove the window from the custom tile on the first virtual desktop + { + QCOMPARE(window->tile(), customTile); + QCOMPARE(window->requestedTile(), customTile); + QCOMPARE(window->frameGeometry(), customTile->windowGeometry()); + + customTile->unmanage(window); + + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->requestedTile(), nullptr); + QCOMPARE(window->frameGeometry(), originalGeometry); + } + } + } + } + } +#endif +} + +void QuickTilingTest::testEvacuateFromRemovedDesktop() +{ + // This test verifies that a window is properly evacuated from a removed virtual desktop. + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue); + + // We have to receive a configure event when the window becomes active. + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + auto ackConfigure = [&]() { + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + }; + + const RectF originalGeometry = window->frameGeometry(); + + // tile the window in the right half of the screen + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::Right); + QCOMPARE(window->geometryRestore(), originalGeometry); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Right); + ackConfigure(); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->frameGeometry(), RectF(640, 0, 640, 1024)); + + // remove the current virtual desktop + VirtualDesktopManager::self()->removeVirtualDesktop(VirtualDesktopManager::self()->currentDesktop()); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); // technically, it should be "Right" but the tile object is gone + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + ackConfigure(); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + QCOMPARE(window->frameGeometry(), originalGeometry); +} + +void QuickTilingTest::testEvacuateFromRemovedDesktopX11() +{ +#if KWIN_BUILD_X11 + // This test verifies that an X11 window is properly evacuated from a removed virtual desktop. + + Test::XcbConnectionPtr connection = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(connection.get())); + X11Window *window = createWindow(connection.get(), Rect(0, 0, 100, 200)); + + const RectF originalGeometry = window->frameGeometry(); + + // tile the window in the right half of the screen + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::Right); + QCOMPARE(window->quickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::Right); + QCOMPARE(window->frameGeometry(), RectF(640, 0, 640, 1024)); + + // remove the current virtual desktop + VirtualDesktopManager::self()->removeVirtualDesktop(VirtualDesktopManager::self()->currentDesktop()); + QCOMPARE(window->quickTileMode(), QuickTileFlag::None); + QCOMPARE(window->requestedQuickTileMode(), QuickTileFlag::None); + QCOMPARE(window->frameGeometry(), originalGeometry); +#endif +} + +void QuickTilingTest::testCloseTiledWindow() +{ + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue); + + // We have to receive a configure event when the window becomes active. + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + auto ackConfigure = [&]() { + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + }; + + Tile *tile = workspace()->tileManager(workspace()->activeOutput())->quickTile(QuickTileFlag::Right); + + const RectF originalGeometry = window->frameGeometry(); + tile->manage(window); + QCOMPARE(window->geometryRestore(), originalGeometry); + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->requestedTile(), tile); + ackConfigure(); + QCOMPARE(window->tile(), tile); + QCOMPARE(window->requestedTile(), tile); + QCOMPARE(window->frameGeometry(), tile->windowGeometry()); + + window->ref(); + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); + QVERIFY(!tile->windows().contains(window)); + QCOMPARE(window->tile(), tile); + QCOMPARE(window->requestedTile(), tile); + QCOMPARE(window->frameGeometry(), tile->windowGeometry()); + window->unref(); +} + +void QuickTilingTest::testCloseTiledWindowX11() +{ +#if KWIN_BUILD_X11 + Test::XcbConnectionPtr connection = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(connection.get())); + X11Window *window = createWindow(connection.get(), Rect(0, 0, 100, 200)); + + Tile *tile = workspace()->tileManager(workspace()->activeOutput())->quickTile(QuickTileFlag::Right); + + tile->manage(window); + QCOMPARE(window->tile(), tile); + QCOMPARE(window->requestedTile(), tile); + QCOMPARE(window->frameGeometry(), tile->windowGeometry()); + + window->ref(); + connection.reset(); + QVERIFY(Test::waitForWindowClosed(window)); + QVERIFY(!tile->windows().contains(window)); + QCOMPARE(window->tile(), tile); + QCOMPARE(window->requestedTile(), tile); + QCOMPARE(window->frameGeometry(), tile->windowGeometry()); + window->unref(); +#endif +} + +void QuickTilingTest::testScript_data() +{ + QTest::addColumn("action"); + QTest::addColumn("expectedMode"); + QTest::addColumn("expectedGeometry"); + +#define FLAG(name) QuickTileMode(QuickTileFlag::name) + QTest::newRow("top") << QStringLiteral("Top") << FLAG(Top) << RectF(0, 0, 1280, 512); + QTest::newRow("bottom") << QStringLiteral("Bottom") << FLAG(Bottom) << RectF(0, 512, 1280, 512); + QTest::newRow("top right") << QStringLiteral("TopRight") << (FLAG(Top) | FLAG(Right)) << RectF(640, 0, 640, 512); + QTest::newRow("top left") << QStringLiteral("TopLeft") << (FLAG(Top) | FLAG(Left)) << RectF(0, 0, 640, 512); + QTest::newRow("bottom right") << QStringLiteral("BottomRight") << (FLAG(Bottom) | FLAG(Right)) << RectF(640, 512, 640, 512); + QTest::newRow("bottom left") << QStringLiteral("BottomLeft") << (FLAG(Bottom) | FLAG(Left)) << RectF(0, 512, 640, 512); + QTest::newRow("left") << QStringLiteral("Left") << FLAG(Left) << RectF(0, 0, 640, 1024); + QTest::newRow("right") << QStringLiteral("Right") << FLAG(Right) << RectF(640, 0, 640, 1024); +#undef FLAG +} + +void QuickTilingTest::testScript() +{ + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + + // Map the window. + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QCOMPARE(workspace()->activeWindow(), window); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 100, 50)); + QCOMPARE(window->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + + // We have to receive a configure event upon the window becoming active. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + QSignalSpy quickTileChangedSpy(window, &Window::quickTileModeChanged); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + + QVERIFY(Scripting::self()); + QTemporaryFile tmpFile; + QVERIFY(tmpFile.open()); + QTextStream out(&tmpFile); + + QFETCH(QString, action); + out << "workspace.slotWindowQuickTile" << action << "()"; + out.flush(); + + QFETCH(QuickTileMode, expectedMode); + QFETCH(RectF, expectedGeometry); + + const int id = Scripting::self()->loadScript(tmpFile.fileName()); + QVERIFY(id != -1); + QVERIFY(Scripting::self()->isScriptLoaded(tmpFile.fileName())); + auto s = Scripting::self()->findScript(tmpFile.fileName()); + QVERIFY(s); + QSignalSpy runningChangedSpy(s, &AbstractScript::runningChanged); + s->run(); + + QVERIFY(runningChangedSpy.wait()); + QCOMPARE(runningChangedSpy.count(), 1); + QCOMPARE(runningChangedSpy.first().first().toBool(), true); + + // at this point the geometry did not yet change + QCOMPARE(window->frameGeometry(), RectF(0, 0, 100, 50)); + // but requested quick tile mode already changed + QCOMPARE(window->requestedQuickTileMode(), expectedMode); + + // but we got requested a new geometry + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), expectedGeometry.size()); + + // attach a new image + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), expectedGeometry.size().toSize(), Qt::red); + + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(quickTileChangedSpy.count(), 1); + QCOMPARE(window->quickTileMode(), expectedMode); + QEXPECT_FAIL("maximize", "Geometry changed called twice for maximize", Continue); + QCOMPARE(frameGeometryChangedSpy.count(), 1); + QCOMPARE(window->frameGeometry(), expectedGeometry); +} + +void QuickTilingTest::testDontCrashWithMaximizeWindowRule() +{ + // this test verifies that a force-maximize window rule doesn't cause + // setQuickTileMode to loop forever + + workspace()->rulebook()->setConfig(KSharedConfig::openConfig(QFINDTESTDATA("./data/rules/force-maximize"), KConfig::SimpleConfig)); + workspace()->slotReconfigure(); + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 800), Qt::blue); + QVERIFY(window); + + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(workspace()->activeWindow(), window); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 800)); + QCOMPARE(window->requestedQuickTileMode(), QuickTileMode(QuickTileFlag::None)); + QCOMPARE(window->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + + window->setQuickTileModeAtCurrentPosition(QuickTileFlag::Right); + QCOMPARE(window->requestedQuickTileMode(), QuickTileMode(QuickTileFlag::None)); + QCOMPARE(window->requestedMaximizeMode(), MaximizeMode::MaximizeFull); +} +} + +WAYLANDTEST_MAIN(KWin::QuickTilingTest) +#include "quick_tiling_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/scene_opengl_es_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/scene_opengl_es_test.cpp new file mode 100644 index 0000000000..90c115d643 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/scene_opengl_es_test.cpp @@ -0,0 +1,22 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "generic_scene_opengl_test.h" + +class SceneOpenGLESTest : public GenericSceneOpenGLTest +{ + Q_OBJECT +public: + SceneOpenGLESTest() + : GenericSceneOpenGLTest(QByteArrayLiteral("O2ES")) + { + } +}; + +WAYLANDTEST_MAIN(SceneOpenGLESTest) +#include "scene_opengl_es_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/scene_opengl_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/scene_opengl_test.cpp new file mode 100644 index 0000000000..bbc15d89cf --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/scene_opengl_test.cpp @@ -0,0 +1,22 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "generic_scene_opengl_test.h" + +class SceneOpenGLTest : public GenericSceneOpenGLTest +{ + Q_OBJECT +public: + SceneOpenGLTest() + : GenericSceneOpenGLTest(QByteArrayLiteral("O2")) + { + } +}; + +WAYLANDTEST_MAIN(SceneOpenGLTest) +#include "scene_opengl_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/screen_changes_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/screen_changes_test.cpp new file mode 100644 index 0000000000..29989207fb --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/screen_changes_test.cpp @@ -0,0 +1,169 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "core/outputbackend.h" +#include "pointer_input.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_screen_changes-0"); + +class ScreenChangesTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testScreenAddRemove(); +}; + +void ScreenChangesTest::initTestCase() +{ + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->start(); + Test::setOutputConfig({Rect(0, 0, 1280, 1024)}); + setenv("QT_QPA_PLATFORM", "wayland", true); +} + +void ScreenChangesTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); + + workspace()->setActiveOutput(QPoint(640, 512)); + KWin::input()->pointer()->warp(QPoint(640, 512)); +} + +void ScreenChangesTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void ScreenChangesTest::testScreenAddRemove() +{ + // this test verifies that when a new screen is added it gets synced to Wayland + + // first create a registry to get signals about Outputs announced/removed + KWayland::Client::Registry registry; + QSignalSpy allAnnounced(®istry, &KWayland::Client::Registry::interfacesAnnounced); + QSignalSpy outputAnnouncedSpy(®istry, &KWayland::Client::Registry::outputAnnounced); + QSignalSpy outputRemovedSpy(®istry, &KWayland::Client::Registry::outputRemoved); + registry.create(Test::waylandConnection()); + QVERIFY(registry.isValid()); + registry.setup(); + QVERIFY(allAnnounced.wait()); + const auto xdgOMData = registry.interface(KWayland::Client::Registry::Interface::XdgOutputUnstableV1); + auto xdgOutputManager = registry.createXdgOutputManager(xdgOMData.name, xdgOMData.version); + + // should be one output + QCOMPARE(workspace()->outputs().count(), 1); + QCOMPARE(outputAnnouncedSpy.count(), 1); + const quint32 firstOutputId = outputAnnouncedSpy.first().first().value(); + QVERIFY(firstOutputId != 0u); + outputAnnouncedSpy.clear(); + + // let's announce a new output + const QList geometries{Rect(0, 0, 1280, 1024), Rect(1280, 0, 1280, 1024)}; + Test::setOutputConfig(geometries); + auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), geometries[0]); + QCOMPARE(outputs[1]->geometry(), geometries[1]); + + // this should result in it getting announced, two new outputs are added... + QVERIFY(outputAnnouncedSpy.wait()); + if (outputAnnouncedSpy.count() < 2) { + QVERIFY(outputAnnouncedSpy.wait()); + } + QCOMPARE(outputAnnouncedSpy.count(), 2); + // ... and afterward the previous output gets removed + if (outputRemovedSpy.isEmpty()) { + QVERIFY(outputRemovedSpy.wait()); + } + QCOMPARE(outputRemovedSpy.count(), 1); + QCOMPARE(outputRemovedSpy.first().first().value(), firstOutputId); + + // let's wait a little bit to ensure we don't get more events + QTest::qWait(100); + QCOMPARE(outputAnnouncedSpy.count(), 2); + QCOMPARE(outputRemovedSpy.count(), 1); + + // let's create the output objects to ensure they are correct + std::unique_ptr o1(registry.createOutput(outputAnnouncedSpy.first().first().value(), outputAnnouncedSpy.first().last().value())); + QVERIFY(o1->isValid()); + QSignalSpy o1ChangedSpy(o1.get(), &KWayland::Client::Output::changed); + QVERIFY(o1ChangedSpy.wait()); + KWin::LogicalOutput *serverOutput1 = workspace()->findOutput(o1->name()); // use wl_output.name to find the compositor side output + QCOMPARE(o1->globalPosition(), serverOutput1->geometry().topLeft()); + QCOMPARE(o1->pixelSize(), serverOutput1->modeSize()); + std::unique_ptr o2(registry.createOutput(outputAnnouncedSpy.last().first().value(), outputAnnouncedSpy.last().last().value())); + QVERIFY(o2->isValid()); + QSignalSpy o2ChangedSpy(o2.get(), &KWayland::Client::Output::changed); + QVERIFY(o2ChangedSpy.wait()); + KWin::LogicalOutput *serverOutput2 = workspace()->findOutput(o2->name()); // use wl_output.name to find the compositor side output + QCOMPARE(o2->globalPosition(), serverOutput2->geometry().topLeft()); + QCOMPARE(o2->pixelSize(), serverOutput2->modeSize()); + + // and check XDGOutput is synced + std::unique_ptr xdgO1(xdgOutputManager->getXdgOutput(o1.get())); + QSignalSpy xdgO1ChangedSpy(xdgO1.get(), &KWayland::Client::XdgOutput::changed); + QVERIFY(xdgO1ChangedSpy.wait()); + QCOMPARE(xdgO1->logicalPosition(), serverOutput1->geometry().topLeft()); + QCOMPARE(xdgO1->logicalSize(), serverOutput1->geometry().size()); + std::unique_ptr xdgO2(xdgOutputManager->getXdgOutput(o2.get())); + QSignalSpy xdgO2ChangedSpy(xdgO2.get(), &KWayland::Client::XdgOutput::changed); + QVERIFY(xdgO2ChangedSpy.wait()); + QCOMPARE(xdgO2->logicalPosition(), serverOutput2->geometry().topLeft()); + QCOMPARE(xdgO2->logicalSize(), serverOutput2->geometry().size()); + + QVERIFY(xdgO1->name().startsWith("Virtual-")); + QVERIFY(xdgO1->name() != xdgO2->name()); + QVERIFY(!xdgO1->description().isEmpty()); + + // now let's try to remove one output again + outputAnnouncedSpy.clear(); + outputRemovedSpy.clear(); + + QSignalSpy o1RemovedSpy(o1.get(), &KWayland::Client::Output::removed); + QSignalSpy o2RemovedSpy(o2.get(), &KWayland::Client::Output::removed); + + const QList geometries2{Rect(0, 0, 1280, 1024)}; + Test::setOutputConfig(geometries2); + outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 1); + QCOMPARE(outputs[0]->geometry(), geometries2.at(0)); + + QVERIFY(outputAnnouncedSpy.wait()); + QCOMPARE(outputAnnouncedSpy.count(), 1); + if (o1RemovedSpy.isEmpty()) { + QVERIFY(o1RemovedSpy.wait()); + } + if (o2RemovedSpy.isEmpty()) { + QVERIFY(o2RemovedSpy.wait()); + } + // now wait a bit to ensure we don't get more events + QTest::qWait(100); + QCOMPARE(outputAnnouncedSpy.count(), 1); + QCOMPARE(o1RemovedSpy.count(), 1); + QCOMPARE(o2RemovedSpy.count(), 1); + QCOMPARE(outputRemovedSpy.count(), 2); +} + +WAYLANDTEST_MAIN(ScreenChangesTest) +#include "screen_changes_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/screencasting_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/screencasting_test.cpp new file mode 100644 index 0000000000..0252d71af1 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/screencasting_test.cpp @@ -0,0 +1,271 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "compositor.h" +#include "core/output.h" +#include "generic_scene_opengl_test.h" +#include "opengl/glplatform.h" +#include "pointer_input.h" +#include "scene/workspacescene.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include + +#define QCOMPAREIMG(actual, expected, id) \ + { \ + if ((actual) != (expected)) { \ + const auto actualFile = QStringLiteral("appium_artifact_actual_%1.png").arg(id); \ + const auto expectedFile = QStringLiteral("appium_artifact_expected_%1.png").arg(id); \ + (actual).save(actualFile); \ + (expected).save(expectedFile); \ + qDebug() << "Generated failed file" << actualFile << expectedFile; \ + } \ + QCOMPARE(actual, expected); \ + } + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_buffer_size_change-0"); + +class ScreencastingTest : public GenericSceneOpenGLTest +{ + Q_OBJECT +public: + ScreencastingTest() + : GenericSceneOpenGLTest(QByteArrayLiteral("O2")) + { + auto wrap = [this](const QString &process, const QStringList &arguments = {}) { + // Make sure PipeWire is running. If it's already running it will just exit + QProcess *p = new QProcess(this); + p->setProcessChannelMode(QProcess::MergedChannels); + p->setArguments(arguments); + connect(this, &QObject::destroyed, p, [p] { + p->terminate(); + p->waitForFinished(); + p->kill(); + }); + connect(p, &QProcess::errorOccurred, p, [p](auto status) { + qDebug() << "error" << status << p->program(); + }); + connect(p, &QProcess::finished, p, [p](int code, auto status) { + if (code != 0) { + qDebug() << p->readAll(); + } + qDebug() << "finished" << code << status << p->program(); + }); + p->setProgram(process); + p->start(); + }; + + // If I run this outside the CI, it breaks the system's pipewire + if (qgetenv("KDECI_BUILD") == "TRUE") { + wrap("pipewire"); + wrap("dbus-launch", {"wireplumber"}); + } + } +private Q_SLOTS: + void init(); + void testWindowCasting(); + void testWindowWithPopup(); + void testWindowWithPopupDynamic(); + void testOutputCasting(); + +private: + std::optional oneFrameAndClose(Test::ScreencastingStreamV1 *stream); +}; + +void ScreencastingTest::init() +{ + if (qgetenv("KDECI_BUILD") == "TRUE") { + QSKIP("CI has pipewire 1.2 that has known process callback issues"); // TODO: Remove it later when CI ships pipewire 1.2 with the fix + } + + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::ScreencastingV1 | Test::AdditionalWaylandInterface::PresentationTime)); + QVERIFY(KWin::Test::screencasting()); + Cursors::self()->hideCursor(); +} + +std::optional ScreencastingTest::oneFrameAndClose(Test::ScreencastingStreamV1 *stream) +{ + Q_ASSERT(stream); + PipeWireSourceStream pwStream; + qDebug() << "start" << stream; + connect(stream, &Test::ScreencastingStreamV1::failed, qGuiApp, [](const QString &error) { + qDebug() << "stream failed with error" << error; + Q_ASSERT(false); + }); + connect(stream, &Test::ScreencastingStreamV1::closed, qGuiApp, [&pwStream] { + pwStream.setActive(false); + }); + connect(stream, &Test::ScreencastingStreamV1::created, qGuiApp, [&pwStream](quint32 nodeId) { + pwStream.createStream(nodeId, 0); + }); + + std::optional img; + connect(&pwStream, &PipeWireSourceStream::frameReceived, qGuiApp, [&img](const PipeWireFrame &frame) { + if (frame.dataFrame) { + img = frame.dataFrame->toImage(); + } + }); + + QSignalSpy spy(&pwStream, &PipeWireSourceStream::frameReceived); + if (!spy.wait()) { + qDebug() << "Did not receive any frames"; + } + pwStream.stopStreaming(); + return img; +} + +void ScreencastingTest::testWindowCasting() +{ + QImage sourceImage(QSize(30, 10), QImage::Format_RGBA8888_Premultiplied); + sourceImage.fill(Qt::red); + Test::XdgToplevelWindow window; + QVERIFY(window.show(sourceImage)); + + auto stream = KWin::Test::screencasting()->createWindowStream(window.m_window->internalId().toString(), QtWayland::zkde_screencast_unstable_v1::pointer_hidden); + + std::optional img = oneFrameAndClose(stream); + QVERIFY(img); + img->convertTo(sourceImage.format()); + QCOMPAREIMG(*img, sourceImage, QLatin1String("window_cast")); +} + +void ScreencastingTest::testWindowWithPopup() +{ + QImage windowImage(QSize(30, 10), QImage::Format_RGBA8888_Premultiplied); + windowImage.fill(Qt::red); + Test::XdgToplevelWindow window; + QVERIFY(window.show(windowImage)); + + QSize popupSize(5, 5); + Rect anchoRect(0, 0, 1, 1); + std::unique_ptr popupSurface(Test::createSurface()); + std::unique_ptr positioner(Test::createXdgPositioner()); + positioner->set_size(popupSize.width(), popupSize.height()); + positioner->set_anchor_rect(anchoRect.x(), anchoRect.y(), anchoRect.width(), anchoRect.height()); + positioner->set_gravity(Test::XdgPositioner::gravity_bottom_right); + std::unique_ptr popup(Test::createXdgPopupSurface(popupSurface.get(), window.m_toplevel->xdgSurface(), positioner.get())); + + QImage popupImage(popupSize, QImage::Format_RGBA8888_Premultiplied); + popupImage.fill(Qt::blue); + auto popupWindow = Test::renderAndWaitForShown(popupSurface.get(), popupImage); + QVERIFY(popupWindow); + + std::unique_ptr stream(KWin::Test::screencasting()->createWindowStream(window.m_window->internalId().toString(), Test::ScreencastingV1::pointer_hidden)); + + QImage expectedImage = windowImage; + QPainter(&expectedImage).drawImage(anchoRect.bottomRight(), popupImage); + + std::optional img = oneFrameAndClose(stream.get()); + QVERIFY(img); + img->convertTo(expectedImage.format()); + QCOMPAREIMG(*img, expectedImage, QLatin1String("window_popup")); +} + +void ScreencastingTest::testWindowWithPopupDynamic() +{ + QImage windowImage(QSize(30, 10), QImage::Format_RGBA8888_Premultiplied); + windowImage.fill(Qt::red); + Test::XdgToplevelWindow window; + QVERIFY(window.show(windowImage)); + + std::unique_ptr stream(KWin::Test::screencasting()->createWindowStream(window.m_window->internalId().toString(), Test::ScreencastingV1::pointer_hidden)); + + PipeWireSourceStream pwStream; + connect(stream.get(), &Test::ScreencastingStreamV1::failed, qGuiApp, [](const QString &error) { + QFAIL("Creating stream failed: " + error.toUtf8()); + }); + connect(stream.get(), &Test::ScreencastingStreamV1::closed, qGuiApp, [&pwStream] { + pwStream.setActive(false); + }); + QSignalSpy createdSpy(stream.get(), &Test::ScreencastingStreamV1::created); + QVERIFY(createdSpy.wait()); + pwStream.createStream(createdSpy.first().first().toUInt(), 0); + + auto fetchNextImageFrame = [&pwStream](QImage::Format format) { + QSignalSpy frameSpy(&pwStream, &PipeWireSourceStream::frameReceived); + if (!frameSpy.wait()) { + Q_ASSERT_X(false, "", "no frame received"); + return QImage(); + } + auto frame = frameSpy.first().first().value(); + return frame.dataFrame->toImage().convertedTo(format); + }; + + QImage expectedImage = windowImage; + auto image = fetchNextImageFrame(expectedImage.format()); + QCOMPAREIMG(image, expectedImage, "dynamic_initial"); + + QSize popupSize(5, 5); + Rect anchoRect(0, 0, 1, 1); + std::unique_ptr popupSurface(Test::createSurface()); + std::unique_ptr positioner(Test::createXdgPositioner()); + positioner->set_size(popupSize.width(), popupSize.height()); + positioner->set_anchor_rect(anchoRect.x(), anchoRect.y(), anchoRect.width(), anchoRect.height()); + positioner->set_gravity(Test::XdgPositioner::gravity_bottom_right); + std::unique_ptr popup(Test::createXdgPopupSurface(popupSurface.get(), window.m_toplevel->xdgSurface(), positioner.get())); + + QImage popupImage(popupSize, QImage::Format_RGBA8888_Premultiplied); + popupImage.fill(Qt::blue); + auto popupWindow = Test::renderAndWaitForShown(popupSurface.get(), popupImage); + QVERIFY(popupWindow); + + expectedImage = windowImage; + QPainter(&expectedImage).drawImage(anchoRect.bottomRight(), popupImage); + image = fetchNextImageFrame(expectedImage.format()); + QCOMPAREIMG(image, expectedImage, "dynamic_popup"); + + popup.reset(); + + expectedImage = windowImage; + image = fetchNextImageFrame(expectedImage.format()); + QCOMPAREIMG(image, expectedImage, "dynamic_popup_closed"); +} + +void ScreencastingTest::testOutputCasting() +{ + auto theOutput = KWin::Test::waylandOutputs().constFirst(); + + Test::XdgToplevelWindow window{[theOutput](Test::XdgToplevel *toplevel) { + toplevel->set_fullscreen(theOutput->output()); + }}; + + QImage sourceImage(theOutput->pixelSize(), QImage::Format_RGBA8888_Premultiplied); + sourceImage.fill(Qt::green); + { + QPainter p(&sourceImage); + p.drawRect(100, 100, 100, 100); + } + + QVERIFY(window.show(sourceImage)); + QVERIFY(window.m_window->isFullScreen()); + QCOMPARE(window.m_window->frameGeometry(), RectF(window.m_window->output()->geometry())); + + QVERIFY(window.presentWait()); + + auto stream = KWin::Test::screencasting()->createOutputStream(theOutput->output(), QtWayland::zkde_screencast_unstable_v1::pointer_hidden); + + std::optional img = oneFrameAndClose(stream); + QVERIFY(img); + img->convertTo(sourceImage.format()); + QCOMPAREIMG(*img, sourceImage, QLatin1String("output_cast")); +} + +} + +WAYLANDTEST_MAIN(KWin::ScreencastingTest) +#include "screencasting_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/screenedges_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/screenedges_test.cpp new file mode 100644 index 0000000000..d8c158c3e9 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/screenedges_test.cpp @@ -0,0 +1,350 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "cursor.h" +#include "effect/effectloader.h" +#include "main.h" +#include "pointer_input.h" +#include "screenedge.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include + +#include +#include +#include + +Q_DECLARE_METATYPE(KWin::ElectricBorder) + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_screen-edges-0"); + +class TestObject : public QObject +{ + Q_OBJECT + +public Q_SLOTS: + bool callback(ElectricBorder border) + { + Q_EMIT gotCallback(border); + return true; + } + +Q_SIGNALS: + void gotCallback(KWin::ElectricBorder); +}; + +class ScreenEdgesTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testTouchCallback_data(); + void testTouchCallback(); + void testPushBack_data(); + void testPushBack(); + void testObjectEdge_data(); + void testObjectEdge(); + void testMultipleEntry_data(); + void testMultipleEntry(); +}; + +void ScreenEdgesTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType("ElectricBorder"); + + QVERIFY(waylandServer()->init(s_socketName)); + + // Disable effects, in particular present windows, which reserves a screen edge. + auto config = kwinApp()->config(); + KConfigGroup plugins(config, QStringLiteral("Plugins")); + const auto builtinNames = EffectLoader().listOfKnownEffects(); + for (const QString &name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + + config->sync(); + kwinApp()->setConfig(config); + + kwinApp()->start(); + Test::setOutputConfig({Rect(0, 0, 1280, 1024)}); +} + +void ScreenEdgesTest::init() +{ + workspace()->screenEdges()->recreateEdges(); + Workspace::self()->setActiveOutput(QPoint(640, 512)); + KWin::input()->pointer()->warp(QPoint(640, 512)); + + QVERIFY(Test::setupWaylandConnection()); +} + +void ScreenEdgesTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void ScreenEdgesTest::testTouchCallback_data() +{ + QTest::addColumn("border"); + QTest::addColumn("startPos"); + QTest::addColumn("delta"); + + QTest::newRow("left") << ElectricLeft << QPointF(0, 50) << QPointF(256, 20); + QTest::newRow("top") << ElectricTop << QPointF(50, 0) << QPointF(20, 250); + QTest::newRow("right") << ElectricRight << QPointF(1279, 50) << QPointF(-256, 0); + QTest::newRow("bottom") << ElectricBottom << QPointF(50, 1023) << QPointF(0, -205); +} + +void ScreenEdgesTest::testTouchCallback() +{ + // This test verifies that touch screen edges trigger associated callbacks. + + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + auto group = config->group(QStringLiteral("TouchEdges")); + group.writeEntry("Top", "none"); + group.writeEntry("Left", "none"); + group.writeEntry("Bottom", "none"); + group.writeEntry("Right", "none"); + config->sync(); + + auto s = workspace()->screenEdges(); + s->setConfig(config); + s->reconfigure(); + + // none of our actions should be reserved + const auto &edges = s->edges(); + QCOMPARE(edges.size(), 8); + for (auto &edge : edges) { + QCOMPARE(edge->isReserved(), false); + QCOMPARE(edge->activatesForPointer(), false); + QCOMPARE(edge->activatesForTouchGesture(), false); + } + + // let's reserve an action + QAction action; + QSignalSpy actionTriggeredSpy(&action, &QAction::triggered); + + // reserve on edge + QFETCH(KWin::ElectricBorder, border); + s->reserveTouch(border, &action); + for (auto &edge : edges) { + QCOMPARE(edge->isReserved(), edge->border() == border); + QCOMPARE(edge->activatesForPointer(), false); + QCOMPARE(edge->activatesForTouchGesture(), edge->border() == border); + } + + quint32 timestamp = 0; + + // press the finger + QFETCH(QPointF, startPos); + Test::touchDown(1, startPos, timestamp++); + QVERIFY(actionTriggeredSpy.isEmpty()); + + // move the finger + QFETCH(QPointF, delta); + Test::touchMotion(1, startPos + delta, timestamp++); + QVERIFY(actionTriggeredSpy.isEmpty()); + + // release the finger + Test::touchUp(1, timestamp++); + QVERIFY(actionTriggeredSpy.wait()); + QCOMPARE(actionTriggeredSpy.count(), 1); + + // unreserve again + s->unreserveTouch(border, &action); + for (auto &edge : edges) { + QCOMPARE(edge->isReserved(), false); + QCOMPARE(edge->activatesForPointer(), false); + QCOMPARE(edge->activatesForTouchGesture(), false); + } + + // reserve another action + std::unique_ptr action2(new QAction); + s->reserveTouch(border, action2.get()); + for (auto &edge : edges) { + QCOMPARE(edge->isReserved(), edge->border() == border); + QCOMPARE(edge->activatesForPointer(), false); + QCOMPARE(edge->activatesForTouchGesture(), edge->border() == border); + } + + // and unreserve by destroying + action2.reset(); + for (auto &edge : edges) { + QCOMPARE(edge->isReserved(), false); + QCOMPARE(edge->activatesForPointer(), false); + QCOMPARE(edge->activatesForTouchGesture(), false); + } +} + +void ScreenEdgesTest::testPushBack_data() +{ + QTest::addColumn("border"); + QTest::addColumn("pushback"); + QTest::addColumn("trigger"); + QTest::addColumn("expected"); + + QTest::newRow("top-left-3") << ElectricTopLeft << 3 << QPointF(0, 0) << QPointF(3, 3); + QTest::newRow("top-5") << ElectricTop << 5 << QPointF(50, 0) << QPointF(50, 5); + QTest::newRow("top-right-2") << ElectricTopRight << 2 << QPointF(1279, 0) << QPointF(1277, 2); + QTest::newRow("right-10") << ElectricRight << 10 << QPointF(1279, 50) << QPointF(1269, 50); + QTest::newRow("bottom-right-5") << ElectricBottomRight << 5 << QPointF(1279, 1023) << QPointF(1274, 1018); + QTest::newRow("bottom-10") << ElectricBottom << 10 << QPointF(50, 1023) << QPointF(50, 1013); + QTest::newRow("bottom-left-3") << ElectricBottomLeft << 3 << QPointF(0, 1023) << QPointF(3, 1020); + QTest::newRow("left-10") << ElectricLeft << 10 << QPointF(0, 50) << QPointF(10, 50); + QTest::newRow("invalid") << ElectricLeft << 10 << QPointF(50, 0) << QPointF(50, 0); +} + +void ScreenEdgesTest::testPushBack() +{ + // This test verifies that the pointer will be pushed back if it approached a screen edge. + + QFETCH(int, pushback); + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group(QStringLiteral("Windows")).writeEntry("ElectricBorderPushbackPixels", pushback); + config->sync(); + + auto s = workspace()->screenEdges(); + s->setConfig(config); + s->reconfigure(); + + TestObject callback; + QSignalSpy spy(&callback, &TestObject::gotCallback); + + QFETCH(ElectricBorder, border); + s->reserve(border, &callback, "callback"); + + QFETCH(QPointF, trigger); + Test::pointerMotion(trigger, 0); + QVERIFY(spy.isEmpty()); + QTEST(Cursors::self()->mouse()->pos(), "expected"); +} + +void ScreenEdgesTest::testObjectEdge_data() +{ + QTest::addColumn("border"); + QTest::addColumn("triggerPoint"); + QTest::addColumn("delta"); + + QTest::newRow("top") << ElectricTop << QPointF(640, 0) << QPointF(0, 50); + QTest::newRow("right") << ElectricRight << QPointF(1279, 512) << QPointF(-50, 0); + QTest::newRow("bottom") << ElectricBottom << QPointF(640, 1023) << QPointF(0, -50); + QTest::newRow("left") << ElectricLeft << QPointF(0, 512) << QPointF(50, 0); +} + +void ScreenEdgesTest::testObjectEdge() +{ + // This test verifies that a screen edge reserved by a script or any QObject is activated. + + TestObject callback; + QSignalSpy spy(&callback, &TestObject::gotCallback); + + // Reserve a screen edge border. + QFETCH(ElectricBorder, border); + workspace()->screenEdges()->reserve(border, &callback, "callback"); + + QFETCH(QPointF, triggerPoint); + QFETCH(QPointF, delta); + + // doesn't trigger as the edge was not triggered yet + qint64 timestamp = 0; + Test::pointerMotion(triggerPoint + delta, timestamp); + QVERIFY(spy.isEmpty()); + + // test doesn't trigger due to too much offset + timestamp += 160; + Test::pointerMotion(triggerPoint, timestamp); + QVERIFY(spy.isEmpty()); + + // doesn't activate as we are waiting too short + timestamp += 50; + Test::pointerMotion(triggerPoint, timestamp); + QVERIFY(spy.isEmpty()); + + // and this one triggers + timestamp += 110; + Test::pointerMotion(triggerPoint, timestamp); + QVERIFY(!spy.isEmpty()); + + // now let's try to trigger again + timestamp += 351; + Test::pointerMotion(triggerPoint, timestamp); + QCOMPARE(spy.count(), 1); + + // it's still under the reactivation + timestamp += 50; + Test::pointerMotion(triggerPoint, timestamp); + QCOMPARE(spy.count(), 1); + + // now it should trigger again + timestamp += 250; + Test::pointerMotion(triggerPoint, timestamp); + QCOMPARE(spy.count(), 2); +} + +void ScreenEdgesTest::testMultipleEntry_data() +{ + QTest::addColumn("border"); + QTest::addColumn("triggerPoint"); + QTest::addColumn("delta"); + + QTest::newRow("top") << ElectricTop << QPointF(640, 0) << QPointF(0, 50); + QTest::newRow("right") << ElectricRight << QPointF(1279, 512) << QPointF(-50, 0); + QTest::newRow("bottom") << ElectricBottom << QPointF(640, 1023) << QPointF(0, -50); + QTest::newRow("left") << ElectricLeft << QPointF(0, 512) << QPointF(50, 0); +} + +void ScreenEdgesTest::testMultipleEntry() +{ + TestObject callback; + QSignalSpy spy(&callback, &TestObject::gotCallback); + + // Reserve a screen edge border. + QFETCH(ElectricBorder, border); + workspace()->screenEdges()->reserve(border, &callback, "callback"); + + QFETCH(QPointF, triggerPoint); + QFETCH(QPointF, delta); + + qint64 timestamp = 0; + + while (timestamp < 300) { + // doesn't activate from repeated entries of short duration + Test::pointerMotion(triggerPoint, timestamp); + QVERIFY(spy.isEmpty()); + timestamp += 50; + Test::pointerMotion(triggerPoint + delta, timestamp); + QVERIFY(spy.isEmpty()); + timestamp += 50; + } + + // and this one triggers + Test::pointerMotion(triggerPoint, timestamp); + timestamp += 110; + Test::pointerMotion(triggerPoint, timestamp); + QVERIFY(!spy.isEmpty()); + QCOMPARE(spy.count(), 1); +} + +} // namespace KWin + +WAYLANDTEST_MAIN(KWin::ScreenEdgesTest) +#include "screenedges_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/screens_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/screens_test.cpp new file mode 100644 index 0000000000..33f589b8c5 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/screens_test.cpp @@ -0,0 +1,156 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "pointer_input.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_screens-0"); + +class ScreensTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testActiveOutputFollowsMouse_data(); + void testActiveOutputFollowsMouse(); + void testCurrentPoint_data(); + void testCurrentPoint(); +}; + +void ScreensTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); +} + +void ScreensTest::init() +{ + workspace()->setActiveOutput(QPoint(640, 512)); + KWin::input()->pointer()->warp(QPoint(640, 512)); + + QVERIFY(Test::setupWaylandConnection()); +} + +static void purge(KConfig *config) +{ + const QStringList groups = config->groupList(); + for (const QString &group : groups) { + config->deleteGroup(group); + } +} + +void ScreensTest::cleanup() +{ + // Destroy the wayland connection of the test window. + Test::destroyWaylandConnection(); + + // Wipe the screens config clean. + auto config = kwinApp()->config(); + purge(config.data()); + config->sync(); + workspace()->slotReconfigure(); + + // Reset the screen layout of the test environment. + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void ScreensTest::testActiveOutputFollowsMouse_data() +{ + QTest::addColumn>("geometries"); + QTest::addColumn("cursorPos"); + QTest::addColumn("expectedId"); + + QTest::newRow("cloned") << QList{{Rect{0, 0, 200, 100}, Rect{0, 0, 200, 100}}} << QPoint(50, 50) << 0; + QTest::newRow("adjacent-0") << QList{{Rect{0, 0, 200, 100}, Rect{200, 100, 400, 300}}} << QPoint(199, 99) << 0; + QTest::newRow("adjacent-1") << QList{{Rect{0, 0, 200, 100}, Rect{200, 100, 400, 300}}} << QPoint(200, 100) << 1; + QTest::newRow("gap") << QList{{Rect{0, 0, 10, 20}, Rect{20, 40, 10, 20}}} << QPoint(15, 30) << 1; +} + +void ScreensTest::testActiveOutputFollowsMouse() +{ + auto edgeBarrierGroup = kwinApp()->config()->group(QStringLiteral("EdgeBarrier")); + edgeBarrierGroup.writeEntry("EdgeBarrier", 0); + edgeBarrierGroup.writeEntry("CornerBarrier", false); + edgeBarrierGroup.sync(); + workspace()->slotReconfigure(); + + QFETCH(QList, geometries); + Test::setOutputConfig(geometries); + + QFETCH(QPoint, cursorPos); + KWin::input()->pointer()->warp(cursorPos); + + QFETCH(int, expectedId); + LogicalOutput *expected = workspace()->outputs().at(expectedId); + QCOMPARE(workspace()->activeOutput(), expected); +} + +void ScreensTest::testCurrentPoint_data() +{ + QTest::addColumn>("geometries"); + QTest::addColumn("cursorPos"); + QTest::addColumn("expectedId"); + + QTest::newRow("cloned") << QList{{Rect{0, 0, 200, 100}, Rect{0, 0, 200, 100}}} << QPoint(50, 50) << 0; + QTest::newRow("adjacent-0") << QList{{Rect{0, 0, 200, 100}, Rect{200, 100, 400, 300}}} << QPoint(199, 99) << 0; + QTest::newRow("adjacent-1") << QList{{Rect{0, 0, 200, 100}, Rect{200, 100, 400, 300}}} << QPoint(200, 100) << 1; + QTest::newRow("gap") << QList{{Rect{0, 0, 10, 20}, Rect{20, 40, 10, 20}}} << QPoint(15, 30) << 1; +} + +void ScreensTest::testCurrentPoint() +{ + QFETCH(QList, geometries); + Test::setOutputConfig(geometries); + + // Disable "active screen follows mouse" + auto group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("ActiveMouseScreen", false); + group.sync(); + workspace()->slotReconfigure(); + + QFETCH(QPoint, cursorPos); + workspace()->setActiveOutput(cursorPos); + + QFETCH(int, expectedId); + LogicalOutput *expected = workspace()->outputs().at(expectedId); + QCOMPARE(workspace()->activeOutput(), expected); +} + +} // namespace KWin + +WAYLANDTEST_MAIN(KWin::ScreensTest) +#include "screens_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/scripting/CMakeLists.txt b/local/recipes/kde/kwin/source/autotests/integration/scripting/CMakeLists.txt new file mode 100644 index 0000000000..0080803a1f --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/scripting/CMakeLists.txt @@ -0,0 +1,2 @@ +integrationTest(NAME testScriptingScreenEdge SRCS screenedge_test.cpp) +integrationTest(NAME testMinimizeAllScript SRCS minimizeall_test.cpp LIBS KF6::Package) diff --git a/local/recipes/kde/kwin/source/autotests/integration/scripting/minimizeall_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/scripting/minimizeall_test.cpp new file mode 100644 index 0000000000..25d04cbb38 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/scripting/minimizeall_test.cpp @@ -0,0 +1,155 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "scripting/scripting.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include + +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_minimizeall-0"); +static const QString s_scriptName = QStringLiteral("minimizeall"); + +class MinimizeAllScriptTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testMinimizeUnminimize(); +}; + +void MinimizeAllScriptTest::initTestCase() +{ + qputenv("XDG_DATA_DIRS", QCoreApplication::applicationDirPath().toUtf8()); + + qRegisterMetaType(); + + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); +} + +static QString locateMainScript(const QString &pluginName) +{ + const QList offers = KPackage::PackageLoader::self()->findPackages( + QStringLiteral("KWin/Script"), + QStringLiteral("kwin-wayland/scripts"), + [&](const KPluginMetaData &metaData) { + return metaData.pluginId() == pluginName; + }); + if (offers.isEmpty()) { + return QString(); + } + const QFileInfo metaDataFileInfo(offers.first().fileName()); + return metaDataFileInfo.path() + QLatin1String("/contents/code/main.js"); +} + +void MinimizeAllScriptTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); + + Scripting::self()->loadScript(locateMainScript(s_scriptName), s_scriptName); + QTRY_VERIFY(Scripting::self()->isScriptLoaded(s_scriptName)); + + AbstractScript *script = Scripting::self()->findScript(s_scriptName); + QVERIFY(script); + QSignalSpy runningChangedSpy(script, &AbstractScript::runningChanged); + script->run(); + QTRY_COMPARE(runningChangedSpy.count(), 1); +} + +void MinimizeAllScriptTest::cleanup() +{ + Test::destroyWaylandConnection(); + + Scripting::self()->unloadScript(s_scriptName); + QTRY_VERIFY(!Scripting::self()->isScriptLoaded(s_scriptName)); +} + +void MinimizeAllScriptTest::testMinimizeUnminimize() +{ +#if !KWIN_BUILD_GLOBALSHORTCUTS + QSKIP("Can't test shortcuts without shortcuts"); + return; +#endif + + // This test verifies that all windows are minimized when Meta+Shift+D + // is pressed, and unminimized when the shortcut is pressed once again. + + // Create a couple of test windows. + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + Window *window1 = Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::blue); + QVERIFY(window1); + QVERIFY(window1->isActive()); + QVERIFY(window1->isMinimizable()); + + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + Window *window2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 50), Qt::red); + QVERIFY(window2); + QVERIFY(window2->isActive()); + QVERIFY(window2->isMinimizable()); + + // Minimize the windows. + quint32 timestamp = 1; + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + Test::keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + Test::keyboardKeyPressed(KEY_D, timestamp++); + Test::keyboardKeyReleased(KEY_D, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + + QTRY_VERIFY(window1->isMinimized()); + QTRY_VERIFY(window2->isMinimized()); + + // Unminimize the windows. + Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); + Test::keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + Test::keyboardKeyPressed(KEY_D, timestamp++); + Test::keyboardKeyReleased(KEY_D, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); + + QTRY_VERIFY(!window1->isMinimized()); + QTRY_VERIFY(!window2->isMinimized()); + + // Destroy test windows. + shellSurface2.reset(); + QVERIFY(Test::waitForWindowClosed(window2)); + shellSurface1.reset(); + QVERIFY(Test::waitForWindowClosed(window1)); +} + +} + +WAYLANDTEST_MAIN(KWin::MinimizeAllScriptTest) +#include "minimizeall_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/scripting/screenedge_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/scripting/screenedge_test.cpp new file mode 100644 index 0000000000..6c2a51da35 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/scripting/screenedge_test.cpp @@ -0,0 +1,278 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "effect/effectloader.h" +#include "pointer_input.h" +#include "scripting/scripting.h" +#include "wayland_server.h" +#include "workspace.h" + +#define private public +#include "screenedge.h" +#undef private + +#include + +Q_DECLARE_METATYPE(KWin::ElectricBorder) + +using namespace std::chrono_literals; +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_scripting_screenedge-0"); + +class ScreenEdgeTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testEdge_data(); + void testEdge(); + void testTouchEdge_data(); + void testTouchEdge(); + void testEdgeUnregister(); + void testDeclarativeTouchEdge(); + +private: + void triggerConfigReload(); +}; + +void ScreenEdgeTest::initTestCase() +{ + QVERIFY(waylandServer()->init(s_socketName)); + + // empty config to have defaults + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + + // disable all effects to prevent them grabbing edges + KConfigGroup plugins(config, QStringLiteral("Plugins")); + const auto builtinNames = EffectLoader().listOfKnownEffects(); + for (QString name : builtinNames) { + plugins.writeEntry(name + QStringLiteral("Enabled"), false); + } + + // disable electric border pushback + config->group(QStringLiteral("Windows")).writeEntry("ElectricBorderPushbackPixels", 0); + config->group(QStringLiteral("TabBox")).writeEntry("TouchBorderActivate", int(ElectricNone)); + + config->sync(); + kwinApp()->setConfig(config); + + kwinApp()->start(); + Test::setOutputConfig({Rect(0, 0, 1280, 1024)}); + QVERIFY(Scripting::self()); + + workspace()->screenEdges()->setTimeThreshold(0ms); + workspace()->screenEdges()->setReActivationThreshold(0ms); +} + +void ScreenEdgeTest::init() +{ + KWin::input()->pointer()->warp(QPointF(640, 512)); + if (workspace()->showingDesktop()) { + workspace()->slotToggleShowDesktop(); + } + QVERIFY(!workspace()->showingDesktop()); +} + +void ScreenEdgeTest::cleanup() +{ + // try to unload the script + const QStringList scripts = {QFINDTESTDATA("./scripts/screenedge.js"), QFINDTESTDATA("./scripts/screenedgeunregister.js"), QFINDTESTDATA("./scripts/touchScreenedge.js")}; + for (const QString &script : scripts) { + if (!script.isEmpty()) { + if (Scripting::self()->isScriptLoaded(script)) { + QVERIFY(Scripting::self()->unloadScript(script)); + QTRY_VERIFY(!Scripting::self()->isScriptLoaded(script)); + } + } + } +} + +void ScreenEdgeTest::testEdge_data() +{ + QTest::addColumn("edge"); + QTest::addColumn("triggerPos"); + + QTest::newRow("Top") << KWin::ElectricTop << QPoint(512, 0); + QTest::newRow("TopRight") << KWin::ElectricTopRight << QPoint(1279, 0); + QTest::newRow("Right") << KWin::ElectricRight << QPoint(1279, 512); + QTest::newRow("BottomRight") << KWin::ElectricBottomRight << QPoint(1279, 1023); + QTest::newRow("Bottom") << KWin::ElectricBottom << QPoint(512, 1023); + QTest::newRow("BottomLeft") << KWin::ElectricBottomLeft << QPoint(0, 1023); + QTest::newRow("Left") << KWin::ElectricLeft << QPoint(0, 512); + QTest::newRow("TopLeft") << KWin::ElectricTopLeft << QPoint(0, 0); + + // repeat a row to show previously unloading and re-registering works + QTest::newRow("Top") << KWin::ElectricTop << QPoint(512, 0); +} + +void ScreenEdgeTest::testEdge() +{ + const QString scriptToLoad = QFINDTESTDATA("./scripts/screenedge.js"); + QVERIFY(!scriptToLoad.isEmpty()); + + // mock the config + auto config = kwinApp()->config(); + QFETCH(KWin::ElectricBorder, edge); + config->group(QLatin1String("Script-") + scriptToLoad).writeEntry("Edge", int(edge)); + config->sync(); + + QVERIFY(!Scripting::self()->isScriptLoaded(scriptToLoad)); + const int id = Scripting::self()->loadScript(scriptToLoad); + QVERIFY(id != -1); + QVERIFY(Scripting::self()->isScriptLoaded(scriptToLoad)); + auto s = Scripting::self()->findScript(scriptToLoad); + QVERIFY(s); + QSignalSpy runningChangedSpy(s, &AbstractScript::runningChanged); + s->run(); + QVERIFY(runningChangedSpy.wait()); + QCOMPARE(runningChangedSpy.count(), 1); + QCOMPARE(runningChangedSpy.first().first().toBool(), true); + // triggering the edge will result in show desktop being triggered + QSignalSpy showDesktopSpy(workspace(), &Workspace::showingDesktopChanged); + + // trigger the edge + QFETCH(QPoint, triggerPos); + KWin::input()->pointer()->warp(triggerPos); + QCOMPARE(showDesktopSpy.count(), 1); + QVERIFY(workspace()->showingDesktop()); +} + +void ScreenEdgeTest::testTouchEdge_data() +{ + QTest::addColumn("edge"); + QTest::addColumn("triggerPos"); + QTest::addColumn("motionPos"); + + QTest::newRow("Top") << KWin::ElectricTop << QPoint(50, 0) << QPoint(50, 500); + QTest::newRow("Right") << KWin::ElectricRight << QPoint(1279, 50) << QPoint(500, 50); + QTest::newRow("Bottom") << KWin::ElectricBottom << QPoint(512, 1023) << QPoint(512, 500); + QTest::newRow("Left") << KWin::ElectricLeft << QPoint(0, 50) << QPoint(500, 50); + + // repeat a row to show previously unloading and re-registering works + QTest::newRow("Top") << KWin::ElectricTop << QPoint(512, 0) << QPoint(512, 500); +} + +void ScreenEdgeTest::testTouchEdge() +{ + const QString scriptToLoad = QFINDTESTDATA("./scripts/touchScreenedge.js"); + QVERIFY(!scriptToLoad.isEmpty()); + + // mock the config + auto config = kwinApp()->config(); + QFETCH(KWin::ElectricBorder, edge); + config->group(QLatin1String("Script-") + scriptToLoad).writeEntry("Edge", int(edge)); + config->sync(); + + QVERIFY(!Scripting::self()->isScriptLoaded(scriptToLoad)); + const int id = Scripting::self()->loadScript(scriptToLoad); + QVERIFY(id != -1); + QVERIFY(Scripting::self()->isScriptLoaded(scriptToLoad)); + auto s = Scripting::self()->findScript(scriptToLoad); + QVERIFY(s); + QSignalSpy runningChangedSpy(s, &AbstractScript::runningChanged); + s->run(); + QVERIFY(runningChangedSpy.wait()); + QCOMPARE(runningChangedSpy.count(), 1); + QCOMPARE(runningChangedSpy.first().first().toBool(), true); + // triggering the edge will result in show desktop being triggered + QSignalSpy showDesktopSpy(workspace(), &Workspace::showingDesktopChanged); + + // trigger the edge + QFETCH(QPoint, triggerPos); + quint32 timestamp = 0; + Test::touchDown(0, triggerPos, timestamp++); + QFETCH(QPoint, motionPos); + Test::touchMotion(0, motionPos, timestamp++); + Test::touchUp(0, timestamp++); + QVERIFY(showDesktopSpy.wait()); + QCOMPARE(showDesktopSpy.count(), 1); + QVERIFY(workspace()->showingDesktop()); +} + +void ScreenEdgeTest::triggerConfigReload() +{ + workspace()->slotReconfigure(); +} + +void ScreenEdgeTest::testEdgeUnregister() +{ + const QString scriptToLoad = QFINDTESTDATA("./scripts/screenedgeunregister.js"); + QVERIFY(!scriptToLoad.isEmpty()); + + Scripting::self()->loadScript(scriptToLoad); + auto s = Scripting::self()->findScript(scriptToLoad); + auto configGroup = s->config(); + configGroup.writeEntry("Edge", int(KWin::ElectricLeft)); + configGroup.sync(); + const QPoint triggerPos = QPoint(0, 512); + + QSignalSpy runningChangedSpy(s, &AbstractScript::runningChanged); + s->run(); + QVERIFY(runningChangedSpy.wait()); + + QSignalSpy showDesktopSpy(workspace(), &Workspace::showingDesktopChanged); + + // trigger the edge + KWin::input()->pointer()->warp(triggerPos); + QCOMPARE(showDesktopSpy.count(), 1); + + // reset + KWin::input()->pointer()->warp(QPointF(500, 500)); + workspace()->slotToggleShowDesktop(); + showDesktopSpy.clear(); + + // trigger again, to show that retriggering works + KWin::input()->pointer()->warp(triggerPos); + QCOMPARE(showDesktopSpy.count(), 1); + + // reset + KWin::input()->pointer()->warp(QPointF(500, 500)); + workspace()->slotToggleShowDesktop(); + showDesktopSpy.clear(); + + // make the script unregister the edge + configGroup.writeEntry("mode", "unregister"); + triggerConfigReload(); + KWin::input()->pointer()->warp(triggerPos); + QCOMPARE(showDesktopSpy.count(), 0); // not triggered + + // force the script to unregister a non-registered edge to prove it doesn't explode + triggerConfigReload(); +} + +void ScreenEdgeTest::testDeclarativeTouchEdge() +{ + const QString scriptToLoad = QFINDTESTDATA("./scripts/screenedgetouch.qml"); + QVERIFY(!scriptToLoad.isEmpty()); + QVERIFY(Scripting::self()->loadDeclarativeScript(scriptToLoad) != -1); + QVERIFY(Scripting::self()->isScriptLoaded(scriptToLoad)); + + auto s = Scripting::self()->findScript(scriptToLoad); + QSignalSpy runningChangedSpy(s, &AbstractScript::runningChanged); + s->run(); + QTRY_COMPARE(runningChangedSpy.count(), 1); + + QSignalSpy showDesktopSpy(workspace(), &Workspace::showingDesktopChanged); + + // Trigger the edge through touch + quint32 timestamp = 0; + Test::touchDown(0, QPointF(0, 50), timestamp++); + Test::touchMotion(0, QPointF(500, 50), timestamp++); + Test::touchUp(0, timestamp++); + + QVERIFY(showDesktopSpy.wait()); +} + +WAYLANDTEST_MAIN(ScreenEdgeTest) +#include "screenedge_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/scripting/scripts/screenedge.js b/local/recipes/kde/kwin/source/autotests/integration/scripting/scripts/screenedge.js new file mode 100644 index 0000000000..49c995adcd --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/scripting/scripts/screenedge.js @@ -0,0 +1 @@ +registerScreenEdge(readConfig("Edge", 1), workspace.slotToggleShowDesktop); diff --git a/local/recipes/kde/kwin/source/autotests/integration/scripting/scripts/screenedgetouch.qml b/local/recipes/kde/kwin/source/autotests/integration/scripting/scripts/screenedgetouch.qml new file mode 100644 index 0000000000..c9e696624a --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/scripting/scripts/screenedgetouch.qml @@ -0,0 +1,10 @@ +import QtQuick +import org.kde.kwin + +ScreenEdgeHandler { + edge: ScreenEdgeHandler.LeftEdge + mode: ScreenEdgeHandler.Touch + onActivated: { + Workspace.slotToggleShowDesktop(); + } +} diff --git a/local/recipes/kde/kwin/source/autotests/integration/scripting/scripts/screenedgeunregister.js b/local/recipes/kde/kwin/source/autotests/integration/scripting/scripts/screenedgeunregister.js new file mode 100644 index 0000000000..240ad18754 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/scripting/scripts/screenedgeunregister.js @@ -0,0 +1,12 @@ +function init() { + const edge = readConfig("Edge", 1); + if (readConfig("mode", "") == "unregister") { + unregisterScreenEdge(edge); + } else { + registerScreenEdge(edge, workspace.slotToggleShowDesktop); + } +} +options.configChanged.connect(init); + +init(); + diff --git a/local/recipes/kde/kwin/source/autotests/integration/scripting/scripts/touchScreenedge.js b/local/recipes/kde/kwin/source/autotests/integration/scripting/scripts/touchScreenedge.js new file mode 100644 index 0000000000..0d762a9e27 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/scripting/scripts/touchScreenedge.js @@ -0,0 +1 @@ +registerTouchScreenEdge(readConfig("Edge", 1), workspace.slotToggleShowDesktop); diff --git a/local/recipes/kde/kwin/source/autotests/integration/security_context_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/security_context_test.cpp new file mode 100644 index 0000000000..3f040bb0a8 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/security_context_test.cpp @@ -0,0 +1,205 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "wayland/clientconnection.h" +#include "wayland/display.h" +#include "wayland_server.h" + +#include + +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/registry.h" + +#include +#include +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_security_context-0"); + +class SecurityContextTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testSecurityContext(); + void testClosedCloseFdOnStartup(); +}; + +void SecurityContextTest::initTestCase() +{ + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->start(); +} + +void SecurityContextTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::SecurityContextManagerV1)); +} + +void SecurityContextTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void SecurityContextTest::testSecurityContext() +{ + // This tests a mock flatpak server creating a Security Context + // connecting a client to the newly created server + // and making sure everything is torn down after the closeFd is closed + auto securityContextManager = Test::waylandSecurityContextManagerV1(); + QVERIFY(securityContextManager); + + int listenFd = socket(AF_UNIX, SOCK_STREAM, 0); + QVERIFY(listenFd != 0); + + QTemporaryDir tempDir; + + sockaddr_un sockaddr; + sockaddr.sun_family = AF_UNIX; + snprintf(sockaddr.sun_path, sizeof(sockaddr.sun_path), "%s", tempDir.filePath("socket").toUtf8().constData()); + qDebug() << "listening socket:" << sockaddr.sun_path; + QVERIFY(bind(listenFd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) == 0); + QVERIFY(listen(listenFd, 0) == 0); + + int syncFds[2]; + QVERIFY(pipe(syncFds) >= 0); + int closeFdForClientToKeep = syncFds[0]; + int closeFdToGiveToKwin = syncFds[1]; + + auto securityContext = new QtWayland::wp_security_context_v1(securityContextManager->create_listener(listenFd, closeFdToGiveToKwin)); + close(closeFdToGiveToKwin); + close(listenFd); + securityContext->set_instance_id("kde.unitest.instance_id"); + securityContext->set_app_id("kde.unittest.app_id"); + securityContext->set_sandbox_engine("test_sandbox_engine"); + securityContext->commit(); + securityContext->destroy(); + delete securityContext; + + qputenv("WAYLAND_DISPLAY", tempDir.filePath("socket").toUtf8()); + QSignalSpy clientConnectedspy(waylandServer()->display(), &Display::clientConnected); + + // connect a client using the newly created listening socket + KWayland::Client::ConnectionThread restrictedClientConnection; + QSignalSpy connectedSpy(&restrictedClientConnection, &KWayland::Client::ConnectionThread::connected); + QThread restictedClientThread; + auto restictedClientThreadQuitter = qScopeGuard([&restictedClientThread]() { + restictedClientThread.quit(); + restictedClientThread.wait(); + }); + restrictedClientConnection.moveToThread(&restictedClientThread); + restictedClientThread.start(); + restrictedClientConnection.initConnection(); + QVERIFY(connectedSpy.wait()); + + // verify that our new restricted client is seen by kwin with the right security context + QVERIFY(clientConnectedspy.count()); + QCOMPARE(clientConnectedspy.first().first().value()->securityContextAppId(), "kde.unittest.app_id"); + + // verify that the globals for the restricted client does not contain the security context + KWayland::Client::Registry registry; + registry.create(&restrictedClientConnection); + QSignalSpy interfaceAnnounced(®istry, &KWayland::Client::Registry::interfaceAnnounced); + QSignalSpy allAnnouncedSpy(®istry, &KWayland::Client::Registry::interfacesAnnounced); + registry.setup(); + QVERIFY(allAnnouncedSpy.wait()); + for (auto interfaceSignal : interfaceAnnounced) { + QVERIFY(interfaceSignal.first().toString() != "wp_security_context_manager_v1"); + } + + // close the mock flatpak closeFDs + close(closeFdForClientToKeep); + + // security context properties should have not changed after close-fd is closed + QVERIFY(Test::waylandSync()); + QCOMPARE(clientConnectedspy.first().first().value()->securityContextAppId(), "kde.unittest.app_id"); + + // new clients can't connect anymore + KWayland::Client::ConnectionThread restrictedClientConnection2; + QSignalSpy connectedSpy2(&restrictedClientConnection2, &KWayland::Client::ConnectionThread::connected); + QSignalSpy failedSpy2(&restrictedClientConnection2, &KWayland::Client::ConnectionThread::failed); + QThread restictedClientThread2; + auto restictedClientThreadQuitter2 = qScopeGuard([&restictedClientThread2]() { + restictedClientThread2.quit(); + restictedClientThread2.wait(); + }); + restrictedClientConnection2.moveToThread(&restictedClientThread2); + restictedClientThread2.start(); + restrictedClientConnection2.initConnection(); + QVERIFY(failedSpy2.wait()); + QVERIFY(connectedSpy2.isEmpty()); +} + +void SecurityContextTest::testClosedCloseFdOnStartup() +{ + // This tests what would happen if the closeFd is already closed when kwin processes the security context + auto securityContextManager = Test::waylandSecurityContextManagerV1(); + QVERIFY(securityContextManager); + + int listenFd = socket(AF_UNIX, SOCK_STREAM, 0); + QVERIFY(listenFd != 0); + + QTemporaryDir tempDir; + + sockaddr_un sockaddr; + sockaddr.sun_family = AF_UNIX; + snprintf(sockaddr.sun_path, sizeof(sockaddr.sun_path), "%s", tempDir.filePath("socket").toUtf8().constData()); + qDebug() << "listening socket:" << sockaddr.sun_path; + QVERIFY(bind(listenFd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) == 0); + QVERIFY(listen(listenFd, 0) == 0); + + int syncFds[2]; + QVERIFY(pipe(syncFds) >= 0); + int closeFdForClientToKeep = syncFds[0]; + int closeFdToGiveToKwin = syncFds[1]; + + close(closeFdForClientToKeep); // closes the connection + + auto securityContext = new QtWayland::wp_security_context_v1(securityContextManager->create_listener(listenFd, closeFdToGiveToKwin)); + close(closeFdToGiveToKwin); + close(listenFd); + securityContext->set_instance_id("kde.unitest.instance_id"); + securityContext->set_app_id("kde.unittest.app_id"); + securityContext->set_sandbox_engine("test_sandbox_engine"); + securityContext->commit(); + securityContext->destroy(); + delete securityContext; + + QVERIFY(Test::waylandSync()); + + qputenv("WAYLAND_DISPLAY", tempDir.filePath("socket").toUtf8()); + QSignalSpy clientConnectedspy(waylandServer()->display(), &Display::clientConnected); + + // new clients can't connect anymore + KWayland::Client::ConnectionThread restrictedClientConnection; + QSignalSpy connectedSpy(&restrictedClientConnection, &KWayland::Client::ConnectionThread::connected); + QSignalSpy failedSpy(&restrictedClientConnection, &KWayland::Client::ConnectionThread::failed); + QThread restictedClientThread; + auto restictedClientThreadQuitter = qScopeGuard([&restictedClientThread]() { + restictedClientThread.quit(); + restictedClientThread.wait(); + }); + restrictedClientConnection.moveToThread(&restictedClientThread); + restictedClientThread.start(); + restrictedClientConnection.initConnection(); + QVERIFY(failedSpy.wait()); + QVERIFY(connectedSpy.isEmpty()); + QVERIFY(clientConnectedspy.isEmpty()); +} +} + +WAYLANDTEST_MAIN(KWin::SecurityContextTest) +#include "security_context_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/showing_desktop_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/showing_desktop_test.cpp new file mode 100644 index 0000000000..c408e0e5f4 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/showing_desktop_test.cpp @@ -0,0 +1,236 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_showing_desktop-0"); + +class ShowingDesktopTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testRestoreFocus(); + void testRestoreFocusWithDesktopWindow(); + void testQuitAfterActivatingHiddenWindow(); + void testDontQuitAfterActivatingDock(); + void testQuitAfterAddingWindow(); + void testDontQuitAfterAddingDock(); +}; + +void ShowingDesktopTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void ShowingDesktopTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::LayerShellV1)); +} + +void ShowingDesktopTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void ShowingDesktopTest::testRestoreFocus() +{ + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + auto window1 = Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::blue); + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + auto window2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 50), Qt::blue); + QVERIFY(window1 != window2); + + QCOMPARE(workspace()->activeWindow(), window2); + workspace()->slotToggleShowDesktop(); + QVERIFY(workspace()->showingDesktop()); + workspace()->slotToggleShowDesktop(); + QVERIFY(!workspace()->showingDesktop()); + + QVERIFY(workspace()->activeWindow()); + QCOMPARE(workspace()->activeWindow(), window2); +} + +void ShowingDesktopTest::testRestoreFocusWithDesktopWindow() +{ + // first create a desktop window + + std::unique_ptr desktopSurface(Test::createSurface()); + std::unique_ptr desktopShellSurface(Test::createLayerSurfaceV1(desktopSurface.get(), QStringLiteral("desktop"))); + desktopShellSurface->set_keyboard_interactivity(1); + desktopShellSurface->set_layer(Test::LayerShellV1::layer_background); + desktopShellSurface->set_size(0, 0); + desktopShellSurface->set_exclusive_zone(-1); + desktopShellSurface->set_anchor(Test::LayerSurfaceV1::anchor_bottom + | Test::LayerSurfaceV1::anchor_top + | Test::LayerSurfaceV1::anchor_left + | Test::LayerSurfaceV1::anchor_right); + desktopSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QSignalSpy desktopConfigureRequestedSpy(desktopShellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(desktopConfigureRequestedSpy.wait()); + auto desktop = Test::renderAndWaitForShown(desktopSurface.get(), desktopConfigureRequestedSpy.last().at(1).toSize(), Qt::blue); + QVERIFY(desktop); + QVERIFY(desktop->isDesktop()); + + // now create some windows + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + auto window1 = Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::blue); + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + auto window2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 50), Qt::blue); + QVERIFY(window1 != window2); + + QCOMPARE(workspace()->activeWindow(), window2); + workspace()->slotToggleShowDesktop(); + QVERIFY(workspace()->showingDesktop()); + QCOMPARE(workspace()->activeWindow(), desktop); + workspace()->slotToggleShowDesktop(); + QVERIFY(!workspace()->showingDesktop()); + + QVERIFY(workspace()->activeWindow()); + QCOMPARE(workspace()->activeWindow(), window2); +} + +void ShowingDesktopTest::testQuitAfterActivatingHiddenWindow() +{ + // This test verifies that the show desktop mode is deactivated after activating a hidden window. + + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + auto window1 = Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::blue); + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + auto window2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 50), Qt::blue); + QCOMPARE(workspace()->activeWindow(), window2); + + workspace()->slotToggleShowDesktop(); + QVERIFY(workspace()->showingDesktop()); + + workspace()->activateWindow(window1); + QVERIFY(!workspace()->showingDesktop()); +} + +void ShowingDesktopTest::testDontQuitAfterActivatingDock() +{ + // This test verifies that activating windows belonging to desktop doesn't break showing desktop mode. + + std::unique_ptr desktopSurface(Test::createSurface()); + std::unique_ptr desktopShellSurface(Test::createLayerSurfaceV1(desktopSurface.get(), QStringLiteral("desktop"))); + desktopShellSurface->set_keyboard_interactivity(1); + desktopShellSurface->set_layer(Test::LayerShellV1::layer_background); + desktopShellSurface->set_size(0, 0); + desktopShellSurface->set_exclusive_zone(-1); + desktopShellSurface->set_anchor(Test::LayerSurfaceV1::anchor_bottom + | Test::LayerSurfaceV1::anchor_top + | Test::LayerSurfaceV1::anchor_left + | Test::LayerSurfaceV1::anchor_right); + desktopSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QSignalSpy desktopConfigureRequestedSpy(desktopShellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(desktopConfigureRequestedSpy.wait()); + auto desktop = Test::renderAndWaitForShown(desktopSurface.get(), desktopConfigureRequestedSpy.last().at(1).toSize(), Qt::blue); + + std::unique_ptr dockSurface{Test::createSurface()}; + std::unique_ptr dockShellSurface{Test::createLayerSurfaceV1(dockSurface.get(), QStringLiteral("dock"))}; + dockShellSurface->set_size(1280, 50); + dockShellSurface->set_anchor(Test::LayerSurfaceV1::anchor_bottom); + dockShellSurface->set_exclusive_zone(50); + dockShellSurface->set_keyboard_interactivity(1); + dockSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QSignalSpy dockConfigureRequestedSpy(dockShellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(dockConfigureRequestedSpy.wait()); + auto dock = Test::renderAndWaitForShown(dockSurface.get(), dockConfigureRequestedSpy.last().at(1).toSize(), Qt::blue); + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window->isActive()); + + workspace()->slotToggleShowDesktop(); + QVERIFY(workspace()->showingDesktop()); + QVERIFY(desktop->isActive()); + + workspace()->activateWindow(dock); + QVERIFY(workspace()->showingDesktop()); + QVERIFY(dock->isActive()); + + workspace()->activateWindow(desktop); + QVERIFY(workspace()->showingDesktop()); + QVERIFY(desktop->isActive()); + + workspace()->slotToggleShowDesktop(); + QVERIFY(!workspace()->showingDesktop()); +} + +void ShowingDesktopTest::testQuitAfterAddingWindow() +{ + // This test verifies that the show desktop mode is deactivated after mapping a new window. + + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::blue); + + workspace()->slotToggleShowDesktop(); + QVERIFY(workspace()->showingDesktop()); + + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + Test::renderAndWaitForShown(surface2.get(), QSize(100, 50), Qt::blue); + + QVERIFY(!workspace()->showingDesktop()); +} + +void ShowingDesktopTest::testDontQuitAfterAddingDock() +{ + // This test verifies that the show desktop mode is not broken after adding a dock. + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window->isActive()); + + workspace()->slotToggleShowDesktop(); + QVERIFY(workspace()->showingDesktop()); + + std::unique_ptr dockSurface{Test::createSurface()}; + std::unique_ptr dockShellSurface{Test::createLayerSurfaceV1(dockSurface.get(), QStringLiteral("dock"))}; + dockShellSurface->set_size(1280, 50); + dockShellSurface->set_anchor(Test::LayerSurfaceV1::anchor_bottom); + dockShellSurface->set_exclusive_zone(50); + dockShellSurface->set_keyboard_interactivity(1); + dockSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QSignalSpy dockConfigureRequestedSpy(dockShellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(dockConfigureRequestedSpy.wait()); + auto dock = Test::renderAndWaitForShown(dockSurface.get(), dockConfigureRequestedSpy.last().at(1).toSize(), Qt::blue); + QVERIFY(dock->isActive()); + + QVERIFY(workspace()->showingDesktop()); + workspace()->slotToggleShowDesktop(); +} + +WAYLANDTEST_MAIN(ShowingDesktopTest) +#include "showing_desktop_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/stacking_order_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/stacking_order_test.cpp new file mode 100644 index 0000000000..c89eeadaaf --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/stacking_order_test.cpp @@ -0,0 +1,1138 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "main.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include + +#if KWIN_BUILD_X11 +#include "atoms.h" +#include "x11window.h" + +#include +#include +#endif + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_stacking_order-0"); + +class StackingOrderTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testTransientIsAboveParent(); + void testRaiseTransient(); + void testLowerTransient(); + void testDeletedTransient(); + + void testGroupTransientIsAboveWindowGroup(); + void testRaiseGroupTransient(); + void testDeletedGroupTransient(); + void testDontKeepAboveNonModalDialogGroupTransients(); + + void testKeepAbove(); + void testKeepBelow(); + + void testPreserveRelativeWindowStacking(); + + void testToggleRaiseLowerInSingleLayer(); + void testToggleRaiseLowerInMultipleLayers(); +}; + +void StackingOrderTest::initTestCase() +{ + qRegisterMetaType(); + + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void StackingOrderTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void StackingOrderTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void StackingOrderTest::testTransientIsAboveParent() +{ + // This test verifies that transients are always above their parents. + + // Create the parent. + std::unique_ptr parentSurface = Test::createSurface(); + QVERIFY(parentSurface); + std::unique_ptr parentShellSurface(Test::createXdgToplevelSurface(parentSurface.get())); + QVERIFY(parentShellSurface); + Window *parent = Test::renderAndWaitForShown(parentSurface.get(), QSize(256, 256), Qt::blue); + QVERIFY(parent); + QVERIFY(parent->isActive()); + QVERIFY(!parent->isTransient()); + + // Initially, the stacking order should contain only the parent window. + QCOMPARE(workspace()->stackingOrder(), (QList{parent})); + + // Create the transient. + std::unique_ptr transientSurface = Test::createSurface(); + QVERIFY(transientSurface); + std::unique_ptr transientShellSurface(Test::createXdgToplevelSurface(transientSurface.get())); + QVERIFY(transientShellSurface); + transientShellSurface->set_parent(parentShellSurface->object()); + Window *transient = Test::renderAndWaitForShown(transientSurface.get(), QSize(128, 128), Qt::red); + QVERIFY(transient); + QVERIFY(transient->isActive()); + QVERIFY(transient->isTransient()); + + // The transient should be above the parent. + QCOMPARE(workspace()->stackingOrder(), (QList{parent, transient})); + + // The transient still stays above the parent if we activate the latter. + workspace()->activateWindow(parent); + QTRY_VERIFY(parent->isActive()); + QTRY_VERIFY(!transient->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{parent, transient})); +} + +void StackingOrderTest::testRaiseTransient() +{ + // This test verifies that both the parent and the transient will be + // raised if either one of them is activated. + + // Create the parent. + std::unique_ptr parentSurface = Test::createSurface(); + QVERIFY(parentSurface); + std::unique_ptr parentShellSurface(Test::createXdgToplevelSurface(parentSurface.get())); + QVERIFY(parentShellSurface); + Window *parent = Test::renderAndWaitForShown(parentSurface.get(), QSize(256, 256), Qt::blue); + QVERIFY(parent); + QVERIFY(parent->isActive()); + QVERIFY(!parent->isTransient()); + + // Initially, the stacking order should contain only the parent window. + QCOMPARE(workspace()->stackingOrder(), (QList{parent})); + + // Create the transient. + std::unique_ptr transientSurface = Test::createSurface(); + QVERIFY(transientSurface); + std::unique_ptr transientShellSurface(Test::createXdgToplevelSurface(transientSurface.get())); + QVERIFY(transientShellSurface); + transientShellSurface->set_parent(parentShellSurface->object()); + Window *transient = Test::renderAndWaitForShown(transientSurface.get(), QSize(128, 128), Qt::red); + QVERIFY(transient); + QTRY_VERIFY(transient->isActive()); + QVERIFY(transient->isTransient()); + + // The transient should be above the parent. + QCOMPARE(workspace()->stackingOrder(), (QList{parent, transient})); + + // Create a window that doesn't have any relationship to the parent or the transient. + std::unique_ptr anotherSurface = Test::createSurface(); + QVERIFY(anotherSurface); + std::unique_ptr anotherShellSurface(Test::createXdgToplevelSurface(anotherSurface.get())); + QVERIFY(anotherShellSurface); + Window *anotherWindow = Test::renderAndWaitForShown(anotherSurface.get(), QSize(128, 128), Qt::green); + QVERIFY(anotherWindow); + QVERIFY(anotherWindow->isActive()); + QVERIFY(!anotherWindow->isTransient()); + + // The newly created surface has to be above both the parent and the transient. + QCOMPARE(workspace()->stackingOrder(), (QList{parent, transient, anotherWindow})); + + // If we activate the parent, the transient should be raised too. + workspace()->activateWindow(parent); + QTRY_VERIFY(parent->isActive()); + QTRY_VERIFY(!transient->isActive()); + QTRY_VERIFY(!anotherWindow->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{anotherWindow, parent, transient})); + + // Go back to the initial setup. + workspace()->activateWindow(anotherWindow); + QTRY_VERIFY(!parent->isActive()); + QTRY_VERIFY(!transient->isActive()); + QTRY_VERIFY(anotherWindow->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{parent, transient, anotherWindow})); + + // If we activate the transient, the parent should be raised too. + workspace()->activateWindow(transient); + QTRY_VERIFY(!parent->isActive()); + QTRY_VERIFY(transient->isActive()); + QTRY_VERIFY(!anotherWindow->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{anotherWindow, parent, transient})); +} + +void StackingOrderTest::testLowerTransient() +{ + // This test verifies that when a window that has transients or is a transient itself is lowered, + // then the whole window hierarchy that this window is a part of is lowered too, + // as well as that a relative stacking of windows within this hierarchy is left intact. + + const int windowsQuantity = 5; + + std::unique_ptr surfaces[windowsQuantity]; + std::unique_ptr shellSurfaces[windowsQuantity]; + Window *windows[windowsQuantity]; + + for (int i = 0; i < windowsQuantity; i++) { + surfaces[i] = Test::createSurface(); + QVERIFY(surfaces[i]); + shellSurfaces[i] = std::unique_ptr(Test::createXdgToplevelSurface(surfaces[i].get())); + QVERIFY(shellSurfaces[i]); + } + + // link 5 windows into the following hierarchy: + // * 0 - unrelated to 1..4 + // * 1 - parent of 2, 3 + // * 3 - parent of 4 + // + // +---+ + // | 4 | + // +-+-+ + // | + // +-+-+ +---+ +---+ + // | 2 | | 3 | | 0 | + // +---+ +---+ +---+ + // \ / + // +---+ + // | 1 | + // +---+ + + shellSurfaces[2]->set_parent(shellSurfaces[1]->object()); + shellSurfaces[3]->set_parent(shellSurfaces[1]->object()); + shellSurfaces[4]->set_parent(shellSurfaces[3]->object()); + + for (int i = 0; i < windowsQuantity; i++) { + windows[i] = Test::renderAndWaitForShown(surfaces[i].get(), QSize(128, 128), Qt::green); + QVERIFY(windows[i]); + } + + // base windows order for following tests + QList baseWindowsOrder{windows[0], windows[1], windows[2], windows[3], windows[4]}; + // expected result for the following tests - window hierarchy 1..4 went below window 0, + // relative order within hierarchy is preserved + QList expectedWindowsOrder{windows[1], windows[2], windows[3], windows[4], windows[0]}; + + // verify initial windows order + QCOMPARE(workspace()->stackingOrder(), baseWindowsOrder); + + // for every window from 1..4 hierarchy + for (int i = 1; i < 5; i++) { + // lower that window + workspace()->lowerWindow(windows[i]); + // verify result + QCOMPARE(workspace()->stackingOrder(), expectedWindowsOrder); + // lower unrelated window 0 and verify that we restored base windows order + workspace()->lowerWindow(windows[0]); + QCOMPARE(workspace()->stackingOrder(), baseWindowsOrder); + } + + // restack transients so that 2 goes on top + workspace()->raiseWindow(windows[2]); + // update base order of windows for the rest of tests + baseWindowsOrder = QList{windows[0], windows[1], windows[3], windows[4], windows[2]}; + // update expected order of windows for the rest of tests - window hierarchy 1..4 goes below window 0, + // relative order within hierarchy is preserved + expectedWindowsOrder = QList{windows[1], windows[3], windows[4], windows[2], windows[0]}; + + // verify initial windows order + QCOMPARE(workspace()->stackingOrder(), baseWindowsOrder); + + // for every window from 1..4 hierarchy + for (int i = 1; i <= 4; i++) { + // lower that window + workspace()->lowerWindow(windows[i]); + // verify result + QCOMPARE(workspace()->stackingOrder(), expectedWindowsOrder); + // lower unrelated window 0 and verify that we restored base windows order + workspace()->lowerWindow(windows[0]); + QCOMPARE(workspace()->stackingOrder(), baseWindowsOrder); + } +} + +struct WindowUnrefDeleter +{ + void operator()(Window *d) + { + if (d != nullptr) { + d->unref(); + } + } +}; + +void StackingOrderTest::testDeletedTransient() +{ + // This test verifies that deleted transients are kept above their + // old parents. + + // Create the parent. + std::unique_ptr parentSurface = Test::createSurface(); + QVERIFY(parentSurface); + std::unique_ptr parentShellSurface(Test::createXdgToplevelSurface(parentSurface.get())); + QVERIFY(parentShellSurface); + Window *parent = Test::renderAndWaitForShown(parentSurface.get(), QSize(256, 256), Qt::blue); + QVERIFY(parent); + QVERIFY(parent->isActive()); + QVERIFY(!parent->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{parent})); + + // Create the first transient. + std::unique_ptr transient1Surface = Test::createSurface(); + QVERIFY(transient1Surface); + std::unique_ptr transient1ShellSurface(Test::createXdgToplevelSurface(transient1Surface.get())); + QVERIFY(transient1ShellSurface); + transient1ShellSurface->set_parent(parentShellSurface->object()); + Window *transient1 = Test::renderAndWaitForShown(transient1Surface.get(), QSize(128, 128), Qt::red); + QVERIFY(transient1); + QTRY_VERIFY(transient1->isActive()); + QVERIFY(transient1->isTransient()); + QCOMPARE(transient1->transientFor(), parent); + + QCOMPARE(workspace()->stackingOrder(), (QList{parent, transient1})); + + // Create the second transient. + std::unique_ptr transient2Surface = Test::createSurface(); + QVERIFY(transient2Surface); + std::unique_ptr transient2ShellSurface(Test::createXdgToplevelSurface(transient2Surface.get())); + QVERIFY(transient2ShellSurface); + transient2ShellSurface->set_parent(transient1ShellSurface->object()); + Window *transient2 = Test::renderAndWaitForShown(transient2Surface.get(), QSize(128, 128), Qt::red); + QVERIFY(transient2); + QTRY_VERIFY(transient2->isActive()); + QVERIFY(transient2->isTransient()); + QCOMPARE(transient2->transientFor(), transient1); + + QCOMPARE(workspace()->stackingOrder(), (QList{parent, transient1, transient2})); + + // Activate the parent, both transients have to be above it. + workspace()->activateWindow(parent); + QTRY_VERIFY(parent->isActive()); + QTRY_VERIFY(!transient1->isActive()); + QTRY_VERIFY(!transient2->isActive()); + + // Close the top-most transient. + connect(transient2, &Window::closed, transient2, &Window::ref); + auto cleanup = qScopeGuard([transient2]() { + transient2->unref(); + }); + + QSignalSpy windowClosedSpy(transient2, &Window::closed); + transient2ShellSurface.reset(); + transient2Surface.reset(); + QVERIFY(windowClosedSpy.wait()); + + // The deleted transient still has to be above its old parent (transient1). + QTRY_VERIFY(parent->isActive()); + QTRY_VERIFY(!transient1->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{parent, transient1, transient2})); +} + +#if KWIN_BUILD_X11 +static xcb_window_t createGroupWindow(xcb_connection_t *conn, + const Rect &geometry, + xcb_window_t leaderWid = XCB_WINDOW_NONE) +{ + xcb_window_t wid = xcb_generate_id(conn); + xcb_create_window( + conn, // c + XCB_COPY_FROM_PARENT, // depth + wid, // wid + rootWindow(), // parent + geometry.x(), // x + geometry.y(), // y + geometry.width(), // width + geometry.height(), // height + 0, // border_width + XCB_WINDOW_CLASS_INPUT_OUTPUT, // _class + XCB_COPY_FROM_PARENT, // visual + 0, // value_mask + nullptr // value_list + ); + + xcb_size_hints_t sizeHints = {}; + xcb_icccm_size_hints_set_position(&sizeHints, 1, geometry.x(), geometry.y()); + xcb_icccm_size_hints_set_size(&sizeHints, 1, geometry.width(), geometry.height()); + xcb_icccm_set_wm_normal_hints(conn, wid, &sizeHints); + + if (leaderWid == XCB_WINDOW_NONE) { + leaderWid = wid; + } + + xcb_change_property( + conn, // c + XCB_PROP_MODE_REPLACE, // mode + wid, // window + atoms->wm_client_leader, // property + XCB_ATOM_WINDOW, // type + 32, // format + 1, // data_len + &leaderWid // data + ); + + return wid; +} +#endif + +void StackingOrderTest::testGroupTransientIsAboveWindowGroup() +{ +#if KWIN_BUILD_X11 + // This test verifies that group transients are always above other + // window group members. + + const Rect geometry = Rect(0, 0, 128, 128); + + Test::XcbConnectionPtr conn = Test::createX11Connection(); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + + // Create the group leader. + xcb_window_t leaderWid = createGroupWindow(conn.get(), geometry); + xcb_map_window(conn.get(), leaderWid); + xcb_flush(conn.get()); + + QVERIFY(windowCreatedSpy.wait()); + X11Window *leader = windowCreatedSpy.first().first().value(); + QVERIFY(leader); + QVERIFY(leader->isActive()); + QCOMPARE(leader->window(), leaderWid); + QVERIFY(!leader->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{leader})); + + // Create another group member. + windowCreatedSpy.clear(); + xcb_window_t member1Wid = createGroupWindow(conn.get(), geometry, leaderWid); + xcb_map_window(conn.get(), member1Wid); + xcb_flush(conn.get()); + + QVERIFY(windowCreatedSpy.wait()); + X11Window *member1 = windowCreatedSpy.first().first().value(); + QVERIFY(member1); + QVERIFY(member1->isActive()); + QCOMPARE(member1->window(), member1Wid); + QCOMPARE(member1->group(), leader->group()); + QVERIFY(!member1->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1})); + + // Create yet another group member. + windowCreatedSpy.clear(); + xcb_window_t member2Wid = createGroupWindow(conn.get(), geometry, leaderWid); + xcb_map_window(conn.get(), member2Wid); + xcb_flush(conn.get()); + + QVERIFY(windowCreatedSpy.wait()); + X11Window *member2 = windowCreatedSpy.first().first().value(); + QVERIFY(member2); + QVERIFY(member2->isActive()); + QCOMPARE(member2->window(), member2Wid); + QCOMPARE(member2->group(), leader->group()); + QVERIFY(!member2->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1, member2})); + + // Create a group transient. + windowCreatedSpy.clear(); + xcb_window_t transientWid = createGroupWindow(conn.get(), geometry, leaderWid); + xcb_icccm_set_wm_transient_for(conn.get(), transientWid, rootWindow()); + + // Currently, we have some weird bug workaround: if a group transient + // is a non-modal dialog, then it won't be kept above its window group. + // We need to explicitly specify window type, otherwise the window type + // will be deduced to _NET_WM_WINDOW_TYPE_DIALOG because we set transient + // for before (the EWMH spec says to do that). + xcb_atom_t net_wm_window_type = Xcb::Atom( + QByteArrayLiteral("_NET_WM_WINDOW_TYPE"), false, conn.get()); + xcb_atom_t net_wm_window_type_normal = Xcb::Atom( + QByteArrayLiteral("_NET_WM_WINDOW_TYPE_NORMAL"), false, conn.get()); + xcb_change_property( + conn.get(), // c + XCB_PROP_MODE_REPLACE, // mode + transientWid, // window + net_wm_window_type, // property + XCB_ATOM_ATOM, // type + 32, // format + 1, // data_len + &net_wm_window_type_normal // data + ); + + xcb_map_window(conn.get(), transientWid); + xcb_flush(conn.get()); + + QVERIFY(windowCreatedSpy.wait()); + X11Window *transient = windowCreatedSpy.first().first().value(); + QVERIFY(transient); + QVERIFY(transient->isActive()); + QCOMPARE(transient->window(), transientWid); + QCOMPARE(transient->group(), leader->group()); + QVERIFY(transient->isTransient()); + QVERIFY(transient->groupTransient()); + QVERIFY(!transient->isDialog()); // See above why + + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1, member2, transient})); + + // If we activate any member of the window group, the transient will be above it. + workspace()->activateWindow(leader); + QTRY_VERIFY(leader->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{member1, member2, leader, transient})); + + workspace()->activateWindow(member1); + QTRY_VERIFY(member1->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{member2, leader, member1, transient})); + + workspace()->activateWindow(member2); + QTRY_VERIFY(member2->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1, member2, transient})); + + workspace()->activateWindow(transient); + QTRY_VERIFY(transient->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1, member2, transient})); +#endif +} + +void StackingOrderTest::testRaiseGroupTransient() +{ +#if KWIN_BUILD_X11 + const Rect geometry = Rect(0, 0, 128, 128); + + Test::XcbConnectionPtr conn = Test::createX11Connection(); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + + // Create the group leader. + xcb_window_t leaderWid = createGroupWindow(conn.get(), geometry); + xcb_map_window(conn.get(), leaderWid); + xcb_flush(conn.get()); + + QVERIFY(windowCreatedSpy.wait()); + X11Window *leader = windowCreatedSpy.first().first().value(); + QVERIFY(leader); + QVERIFY(leader->isActive()); + QCOMPARE(leader->window(), leaderWid); + QVERIFY(!leader->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{leader})); + + // Create another group member. + windowCreatedSpy.clear(); + xcb_window_t member1Wid = createGroupWindow(conn.get(), geometry, leaderWid); + xcb_map_window(conn.get(), member1Wid); + xcb_flush(conn.get()); + + QVERIFY(windowCreatedSpy.wait()); + X11Window *member1 = windowCreatedSpy.first().first().value(); + QVERIFY(member1); + QVERIFY(member1->isActive()); + QCOMPARE(member1->window(), member1Wid); + QCOMPARE(member1->group(), leader->group()); + QVERIFY(!member1->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1})); + + // Create yet another group member. + windowCreatedSpy.clear(); + xcb_window_t member2Wid = createGroupWindow(conn.get(), geometry, leaderWid); + xcb_map_window(conn.get(), member2Wid); + xcb_flush(conn.get()); + + QVERIFY(windowCreatedSpy.wait()); + X11Window *member2 = windowCreatedSpy.first().first().value(); + QVERIFY(member2); + QVERIFY(member2->isActive()); + QCOMPARE(member2->window(), member2Wid); + QCOMPARE(member2->group(), leader->group()); + QVERIFY(!member2->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1, member2})); + + // Create a group transient. + windowCreatedSpy.clear(); + xcb_window_t transientWid = createGroupWindow(conn.get(), geometry, leaderWid); + xcb_icccm_set_wm_transient_for(conn.get(), transientWid, rootWindow()); + + // Currently, we have some weird bug workaround: if a group transient + // is a non-modal dialog, then it won't be kept above its window group. + // We need to explicitly specify window type, otherwise the window type + // will be deduced to _NET_WM_WINDOW_TYPE_DIALOG because we set transient + // for before (the EWMH spec says to do that). + xcb_atom_t net_wm_window_type = Xcb::Atom( + QByteArrayLiteral("_NET_WM_WINDOW_TYPE"), false, conn.get()); + xcb_atom_t net_wm_window_type_normal = Xcb::Atom( + QByteArrayLiteral("_NET_WM_WINDOW_TYPE_NORMAL"), false, conn.get()); + xcb_change_property( + conn.get(), // c + XCB_PROP_MODE_REPLACE, // mode + transientWid, // window + net_wm_window_type, // property + XCB_ATOM_ATOM, // type + 32, // format + 1, // data_len + &net_wm_window_type_normal // data + ); + + xcb_map_window(conn.get(), transientWid); + xcb_flush(conn.get()); + + QVERIFY(windowCreatedSpy.wait()); + X11Window *transient = windowCreatedSpy.first().first().value(); + QVERIFY(transient); + QVERIFY(transient->isActive()); + QCOMPARE(transient->window(), transientWid); + QCOMPARE(transient->group(), leader->group()); + QVERIFY(transient->isTransient()); + QVERIFY(transient->groupTransient()); + QVERIFY(!transient->isDialog()); // See above why + + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1, member2, transient})); + + // Create a Wayland window that is not a member of the window group. + std::unique_ptr anotherSurface = Test::createSurface(); + QVERIFY(anotherSurface); + std::unique_ptr anotherShellSurface(Test::createXdgToplevelSurface(anotherSurface.get())); + QVERIFY(anotherShellSurface); + Window *anotherWindow = Test::renderAndWaitForShown(anotherSurface.get(), QSize(128, 128), Qt::green); + QVERIFY(anotherWindow); + QVERIFY(anotherWindow->isActive()); + QVERIFY(!anotherWindow->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1, member2, transient, anotherWindow})); + + // If we activate the leader, then only it and the transient have to be raised. + workspace()->activateWindow(leader); + QTRY_VERIFY(leader->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{member1, member2, anotherWindow, leader, transient})); + + // If another member of the window group is activated, then the transient will + // be above that member and the leader. + workspace()->activateWindow(member2); + QTRY_VERIFY(member2->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{member1, anotherWindow, leader, member2, transient})); + + // FIXME: If we activate the transient, only it will be raised. + workspace()->activateWindow(anotherWindow); + QTRY_VERIFY(anotherWindow->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{member1, leader, member2, transient, anotherWindow})); + + workspace()->activateWindow(transient); + QTRY_VERIFY(transient->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{member1, leader, member2, anotherWindow, transient})); +#endif +} + +void StackingOrderTest::testDeletedGroupTransient() +{ +#if KWIN_BUILD_X11 + // This test verifies that deleted group transients are kept above their + // old window groups. + + const Rect geometry = Rect(0, 0, 128, 128); + + Test::XcbConnectionPtr conn = Test::createX11Connection(); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + + // Create the group leader. + xcb_window_t leaderWid = createGroupWindow(conn.get(), geometry); + xcb_map_window(conn.get(), leaderWid); + xcb_flush(conn.get()); + + QVERIFY(windowCreatedSpy.wait()); + X11Window *leader = windowCreatedSpy.first().first().value(); + QVERIFY(leader); + QVERIFY(leader->isActive()); + QCOMPARE(leader->window(), leaderWid); + QVERIFY(!leader->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{leader})); + + // Create another group member. + windowCreatedSpy.clear(); + xcb_window_t member1Wid = createGroupWindow(conn.get(), geometry, leaderWid); + xcb_map_window(conn.get(), member1Wid); + xcb_flush(conn.get()); + + QVERIFY(windowCreatedSpy.wait()); + X11Window *member1 = windowCreatedSpy.first().first().value(); + QVERIFY(member1); + QVERIFY(member1->isActive()); + QCOMPARE(member1->window(), member1Wid); + QCOMPARE(member1->group(), leader->group()); + QVERIFY(!member1->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1})); + + // Create yet another group member. + windowCreatedSpy.clear(); + xcb_window_t member2Wid = createGroupWindow(conn.get(), geometry, leaderWid); + xcb_map_window(conn.get(), member2Wid); + xcb_flush(conn.get()); + + QVERIFY(windowCreatedSpy.wait()); + X11Window *member2 = windowCreatedSpy.first().first().value(); + QVERIFY(member2); + QVERIFY(member2->isActive()); + QCOMPARE(member2->window(), member2Wid); + QCOMPARE(member2->group(), leader->group()); + QVERIFY(!member2->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1, member2})); + + // Create a group transient. + windowCreatedSpy.clear(); + xcb_window_t transientWid = createGroupWindow(conn.get(), geometry, leaderWid); + xcb_icccm_set_wm_transient_for(conn.get(), transientWid, rootWindow()); + + // Currently, we have some weird bug workaround: if a group transient + // is a non-modal dialog, then it won't be kept above its window group. + // We need to explicitly specify window type, otherwise the window type + // will be deduced to _NET_WM_WINDOW_TYPE_DIALOG because we set transient + // for before (the EWMH spec says to do that). + xcb_atom_t net_wm_window_type = Xcb::Atom( + QByteArrayLiteral("_NET_WM_WINDOW_TYPE"), false, conn.get()); + xcb_atom_t net_wm_window_type_normal = Xcb::Atom( + QByteArrayLiteral("_NET_WM_WINDOW_TYPE_NORMAL"), false, conn.get()); + xcb_change_property( + conn.get(), // c + XCB_PROP_MODE_REPLACE, // mode + transientWid, // window + net_wm_window_type, // property + XCB_ATOM_ATOM, // type + 32, // format + 1, // data_len + &net_wm_window_type_normal // data + ); + + xcb_map_window(conn.get(), transientWid); + xcb_flush(conn.get()); + + QVERIFY(windowCreatedSpy.wait()); + X11Window *transient = windowCreatedSpy.first().first().value(); + QVERIFY(transient); + QVERIFY(transient->isActive()); + QCOMPARE(transient->window(), transientWid); + QCOMPARE(transient->group(), leader->group()); + QVERIFY(transient->isTransient()); + QVERIFY(transient->groupTransient()); + QVERIFY(!transient->isDialog()); // See above why + + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1, member2, transient})); + + // Unmap the transient. + connect(transient, &Window::closed, transient, &Window::ref); + auto cleanup = qScopeGuard([transient]() { + transient->unref(); + }); + + QSignalSpy windowClosedSpy(transient, &X11Window::closed); + xcb_unmap_window(conn.get(), transientWid); + xcb_flush(conn.get()); + QVERIFY(windowClosedSpy.wait()); + + // The transient has to be above each member of the window group. + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1, member2, transient})); +#endif +} + +void StackingOrderTest::testDontKeepAboveNonModalDialogGroupTransients() +{ +#if KWIN_BUILD_X11 + // Bug 76026 + + const Rect geometry = Rect(0, 0, 128, 128); + + Test::XcbConnectionPtr conn = Test::createX11Connection(); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + + // Create the group leader. + xcb_window_t leaderWid = createGroupWindow(conn.get(), geometry); + xcb_map_window(conn.get(), leaderWid); + xcb_flush(conn.get()); + + QVERIFY(windowCreatedSpy.wait()); + X11Window *leader = windowCreatedSpy.first().first().value(); + QVERIFY(leader); + QVERIFY(leader->isActive()); + QCOMPARE(leader->window(), leaderWid); + QVERIFY(!leader->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{leader})); + + // Create another group member. + windowCreatedSpy.clear(); + xcb_window_t member1Wid = createGroupWindow(conn.get(), geometry, leaderWid); + xcb_map_window(conn.get(), member1Wid); + xcb_flush(conn.get()); + + QVERIFY(windowCreatedSpy.wait()); + X11Window *member1 = windowCreatedSpy.first().first().value(); + QVERIFY(member1); + QVERIFY(member1->isActive()); + QCOMPARE(member1->window(), member1Wid); + QCOMPARE(member1->group(), leader->group()); + QVERIFY(!member1->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1})); + + // Create yet another group member. + windowCreatedSpy.clear(); + xcb_window_t member2Wid = createGroupWindow(conn.get(), geometry, leaderWid); + xcb_map_window(conn.get(), member2Wid); + xcb_flush(conn.get()); + + QVERIFY(windowCreatedSpy.wait()); + X11Window *member2 = windowCreatedSpy.first().first().value(); + QVERIFY(member2); + QVERIFY(member2->isActive()); + QCOMPARE(member2->window(), member2Wid); + QCOMPARE(member2->group(), leader->group()); + QVERIFY(!member2->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1, member2})); + + // Create a group transient. + windowCreatedSpy.clear(); + xcb_window_t transientWid = createGroupWindow(conn.get(), geometry, leaderWid); + xcb_icccm_set_wm_transient_for(conn.get(), transientWid, rootWindow()); + xcb_map_window(conn.get(), transientWid); + xcb_flush(conn.get()); + + QVERIFY(windowCreatedSpy.wait()); + X11Window *transient = windowCreatedSpy.first().first().value(); + QVERIFY(transient); + QVERIFY(transient->isActive()); + QCOMPARE(transient->window(), transientWid); + QCOMPARE(transient->group(), leader->group()); + QVERIFY(transient->isTransient()); + QVERIFY(transient->groupTransient()); + QVERIFY(transient->isDialog()); + QVERIFY(!transient->isModal()); + + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1, member2, transient})); + + workspace()->activateWindow(leader); + QTRY_VERIFY(leader->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{member1, member2, transient, leader})); + + workspace()->activateWindow(member1); + QTRY_VERIFY(member1->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{member2, transient, leader, member1})); + + workspace()->activateWindow(member2); + QTRY_VERIFY(member2->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{transient, leader, member1, member2})); + + workspace()->activateWindow(transient); + QTRY_VERIFY(transient->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{leader, member1, member2, transient})); +#endif +} + +void StackingOrderTest::testKeepAbove() +{ + // This test verifies that "keep-above" windows are kept above other windows. + + // Create the first window. + std::unique_ptr surface1 = Test::createSurface(); + QVERIFY(surface1); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + QVERIFY(shellSurface1); + Window *window1 = Test::renderAndWaitForShown(surface1.get(), QSize(128, 128), Qt::green); + QVERIFY(window1); + QVERIFY(window1->isActive()); + QVERIFY(!window1->keepAbove()); + + QCOMPARE(workspace()->stackingOrder(), (QList{window1})); + + // Create the second window. + std::unique_ptr surface2 = Test::createSurface(); + QVERIFY(surface2); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + QVERIFY(shellSurface2); + Window *window2 = Test::renderAndWaitForShown(surface2.get(), QSize(128, 128), Qt::green); + QVERIFY(window2); + QVERIFY(window2->isActive()); + QVERIFY(!window2->keepAbove()); + + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window2})); + + // Go to the initial test position. + workspace()->activateWindow(window1); + QTRY_VERIFY(window1->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{window2, window1})); + + // Set the "keep-above" flag on the window2, it should go above other windows. + { + StackingUpdatesBlocker blocker(workspace()); + window2->setKeepAbove(true); + } + + QVERIFY(window2->keepAbove()); + QVERIFY(!window2->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window2})); +} + +void StackingOrderTest::testKeepBelow() +{ + // This test verifies that "keep-below" windows are kept below other windows. + + // Create the first window. + std::unique_ptr surface1 = Test::createSurface(); + QVERIFY(surface1); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + QVERIFY(shellSurface1); + Window *window1 = Test::renderAndWaitForShown(surface1.get(), QSize(128, 128), Qt::green); + QVERIFY(window1); + QVERIFY(window1->isActive()); + QVERIFY(!window1->keepBelow()); + + QCOMPARE(workspace()->stackingOrder(), (QList{window1})); + + // Create the second window. + std::unique_ptr surface2 = Test::createSurface(); + QVERIFY(surface2); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + QVERIFY(shellSurface2); + Window *window2 = Test::renderAndWaitForShown(surface2.get(), QSize(128, 128), Qt::green); + QVERIFY(window2); + QVERIFY(window2->isActive()); + QVERIFY(!window2->keepBelow()); + + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window2})); + + // Set the "keep-below" flag on the window2, it should go below other windows. + { + StackingUpdatesBlocker blocker(workspace()); + window2->setKeepBelow(true); + } + + QVERIFY(window2->isActive()); + QVERIFY(window2->keepBelow()); + QCOMPARE(workspace()->stackingOrder(), (QList{window2, window1})); +} + +void StackingOrderTest::testPreserveRelativeWindowStacking() +{ + // This test verifies that raising a window doesn't affect the order of transient windows that are constrained + // to be above it, see BUG: 477262 + + const int windowsQuantity = 5; + + std::unique_ptr surfaces[windowsQuantity]; + std::unique_ptr shellSurfaces[windowsQuantity]; + Window *windows[windowsQuantity]; + + // Create 5 windows. + for (int i = 0; i < windowsQuantity; i++) { + surfaces[i] = Test::createSurface(); + QVERIFY(surfaces[i]); + shellSurfaces[i] = Test::createXdgToplevelSurface(surfaces[i].get()); + QVERIFY(shellSurfaces[i]); + } + + // link them into the following hierarchy: + // * 0 - parent to all + // * 1, 2, 3 - children of 0 + // * 4 - child of 3 + shellSurfaces[1]->set_parent(shellSurfaces[0]->object()); + shellSurfaces[2]->set_parent(shellSurfaces[0]->object()); + shellSurfaces[3]->set_parent(shellSurfaces[0]->object()); + shellSurfaces[4]->set_parent(shellSurfaces[3]->object()); + + for (int i = 0; i < windowsQuantity; i++) { + windows[i] = Test::renderAndWaitForShown(surfaces[i].get(), QSize(128, 128), Qt::green); + QVERIFY(windows[i]); + } + + // verify initial windows order + QCOMPARE(workspace()->stackingOrder(), (QList{windows[0], windows[1], windows[2], windows[3], windows[4]})); + + // activate parent + workspace()->activateWindow(windows[0]); + // verify that order hasn't changed + QCOMPARE(workspace()->stackingOrder(), (QList{windows[0], windows[1], windows[2], windows[3], windows[4]})); + + // change stacking order + workspace()->activateWindow(windows[2]); + workspace()->activateWindow(windows[1]); + // verify order + QCOMPARE(workspace()->stackingOrder(), (QList{windows[0], windows[3], windows[4], windows[2], windows[1]})); + + // activate parent + workspace()->activateWindow(windows[0]); + // verify that order hasn't changed + QCOMPARE(workspace()->stackingOrder(), (QList{windows[0], windows[3], windows[4], windows[2], windows[1]})); + + // activate child 3 + workspace()->activateWindow(windows[3]); + // verify that both child 3 and 4 have been raised + QCOMPARE(workspace()->stackingOrder(), (QList{windows[0], windows[2], windows[1], windows[3], windows[4]})); + + // activate parent + workspace()->activateWindow(windows[0]); + // verify that order hasn't changed + QCOMPARE(workspace()->stackingOrder(), (QList{windows[0], windows[2], windows[1], windows[3], windows[4]})); + + // yet another check - add KeepAbove attribute to parent window (see BUG: 477262) + windows[0]->setKeepAbove(true); + // verify that order hasn't changed + QCOMPARE(workspace()->stackingOrder(), (QList{windows[0], windows[2], windows[1], windows[3], windows[4]})); + // verify that child windows can still be restacked freely + workspace()->activateWindow(windows[1]); + workspace()->activateWindow(windows[2]); + QCOMPARE(workspace()->stackingOrder(), (QList{windows[0], windows[3], windows[4], windows[1], windows[2]})); +} + +void StackingOrderTest::testToggleRaiseLowerInSingleLayer() +{ + // This test verifies that Toggle Raise Lower causes proper action (lowering or raising) to top-most, + // bottom-most and middle-stacked windows, as well as this action doesn't transfer focus + + const int windowCount = 3; + + std::unique_ptr surfaces[windowCount]; + std::unique_ptr shellSurfaces[windowCount]; + Window *windows[windowCount]; + + // Create 3 windows. + for (int i = 0; i < windowCount; i++) { + surfaces[i] = Test::createSurface(); + QVERIFY(surfaces[i]); + shellSurfaces[i] = Test::createXdgToplevelSurface(surfaces[i].get()); + QVERIFY(shellSurfaces[i]); + windows[i] = Test::renderAndWaitForShown(surfaces[i].get(), QSize(128, 128), Qt::green); + QVERIFY(windows[i]); + } + QCOMPARE(workspace()->stackingOrder(), (QList{windows[0], windows[1], windows[2]})); + + // Activate window 0, verify effects. + workspace()->activateWindow(windows[0]); + QVERIFY(windows[0]->isActive()); + QCOMPARE(workspace()->stackingOrder(), (QList{windows[1], windows[2], windows[0]})); + + // TR&L (Toggle Raise Lower) top-most window 0. + workspace()->raiseOrLowerWindow(windows[0]); + + // Verify that window 0 has been lowered and still has focus. + QCOMPARE(workspace()->stackingOrder(), (QList{windows[0], windows[1], windows[2]})); + QVERIFY(windows[0]->isActive()); + + // TR&L window 0, which is now bottom-most. + workspace()->raiseOrLowerWindow(windows[0]); + + // verify that window 0 has been raised and still has focus. + QCOMPARE(workspace()->stackingOrder(), (QList{windows[1], windows[2], windows[0]})); + QVERIFY(windows[0]->isActive()); + + // TR&L window 2, which is in the middle. + workspace()->raiseOrLowerWindow(windows[2]); + // verify that it got raised and focus stays with window 0. + QCOMPARE(workspace()->stackingOrder(), (QList{windows[1], windows[0], windows[2]})); + QVERIFY(!windows[2]->isActive()); + QVERIFY(windows[0]->isActive()); + + // TR&L window 2, which is top-most now, but doesn't have focus. + workspace()->raiseOrLowerWindow(windows[2]); + // verify that it got lower and focus stays with window 0. + QCOMPARE(workspace()->stackingOrder(), (QList{windows[2], windows[1], windows[0]})); + QVERIFY(!windows[2]->isActive()); + QVERIFY(windows[0]->isActive()); +} + +void StackingOrderTest::testToggleRaiseLowerInMultipleLayers() +{ + // This test verifies that Toggle Raise & Lower works independently within each window layer + + const int windowCount = 9; + + std::unique_ptr surfaces[windowCount]; + std::unique_ptr shellSurfaces[windowCount]; + Window *windows[windowCount]; + + // Create 9 windows, place them in 3 layers, move to the same position, so they overlap each other. + for (int i = 0; i < windowCount; i++) { + surfaces[i] = Test::createSurface(); + QVERIFY(surfaces[i]); + shellSurfaces[i] = Test::createXdgToplevelSurface(surfaces[i].get()); + QVERIFY(shellSurfaces[i]); + windows[i] = Test::renderAndWaitForShown(surfaces[i].get(), QSize(128, 128), Qt::green); + QVERIFY(windows[i]); + windows[i]->move(QPoint(0, 0)); + } + windows[0]->setKeepBelow(true); + windows[1]->setKeepBelow(true); + windows[2]->setKeepBelow(true); + windows[6]->setKeepAbove(true); + windows[7]->setKeepAbove(true); + windows[8]->setKeepAbove(true); + + // Verify initial stacking order + QCOMPARE(workspace()->stackingOrder(), (QList{windows[0], windows[1], windows[2], + windows[3], windows[4], windows[5], + windows[6], windows[7], windows[8]})); + + // TR&L window 0 (lowest in BelowLayer), verify that it got raised within its layer + workspace()->raiseOrLowerWindow(windows[0]); + QCOMPARE(workspace()->stackingOrder(), (QList{windows[1], windows[2], windows[0], + windows[3], windows[4], windows[5], + windows[6], windows[7], windows[8]})); + + // TR&L window 0, verify that it got lowered within its layer + workspace()->raiseOrLowerWindow(windows[0]); + QCOMPARE(workspace()->stackingOrder(), (QList{windows[0], windows[1], windows[2], + windows[3], windows[4], windows[5], + windows[6], windows[7], windows[8]})); + + // TR&L window 4 (middle in NormalLayer), verify that it got raised within its layer + workspace()->raiseOrLowerWindow(windows[4]); + QCOMPARE(workspace()->stackingOrder(), (QList{windows[0], windows[1], windows[2], + windows[3], windows[5], windows[4], + windows[6], windows[7], windows[8]})); + + // TR&L window 4, verify that it got lowered within NormalLayer + workspace()->raiseOrLowerWindow(windows[4]); + QCOMPARE(workspace()->stackingOrder(), (QList{windows[0], windows[1], windows[2], + windows[4], windows[3], windows[5], + windows[6], windows[7], windows[8]})); + + // TR&L window 8 (topmost in AboveLayer), verify that it got lowered within its layer + workspace()->raiseOrLowerWindow(windows[8]); + QCOMPARE(workspace()->stackingOrder(), (QList{windows[0], windows[1], windows[2], + windows[4], windows[3], windows[5], + windows[8], windows[6], windows[7]})); + + // TR&L window 8, verify that it got raised within its layer + workspace()->raiseOrLowerWindow(windows[8]); + QCOMPARE(workspace()->stackingOrder(), (QList{windows[0], windows[1], windows[2], + windows[4], windows[3], windows[5], + windows[6], windows[7], windows[8]})); +} + +WAYLANDTEST_MAIN(StackingOrderTest) +#include "stacking_order_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/sticky_keys_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/sticky_keys_test.cpp new file mode 100644 index 0000000000..9ebf594d74 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/sticky_keys_test.cpp @@ -0,0 +1,366 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + SPDX-FileCopyrightText: 2023 Nicolas Fella + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "keyboard_input.h" +#include "pluginmanager.h" +#include "pointer_input.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include + +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_sticky_keys-0"); + +class StickyKeysTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testStick(); + void testStick_data(); + void testLock(); + void testLock_data(); + void testMouse(); + void testMouse_data(); + void testDisableTwoKeys(); +}; + +void StickyKeysTest::initTestCase() +{ + KConfig kaccessConfig("kaccessrc"); + kaccessConfig.group(QStringLiteral("Keyboard")).writeEntry("StickyKeys", true); + kaccessConfig.group(QStringLiteral("Keyboard")).writeEntry("StickyKeysAutoOff", true); + kaccessConfig.sync(); + + // Use a keyboard layout where right alt triggers Mod5/AltGr + KConfig kxkbrc("kxkbrc"); + kxkbrc.group(QStringLiteral("Layout")).writeEntry("LayoutList", "us"); + kxkbrc.group(QStringLiteral("Layout")).writeEntry("VariantList", "altgr-intl"); + kxkbrc.sync(); + + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + qputenv("XKB_DEFAULT_RULES", "evdev"); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void StickyKeysTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::KeyState)); + QVERIFY(Test::waitForWaylandKeyboard()); +} + +void StickyKeysTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void StickyKeysTest::testStick_data() +{ + QTest::addColumn("modifierKey"); + QTest::addColumn("expectedMods"); + + QTest::addRow("Shift") << KEY_LEFTSHIFT << 1; + QTest::addRow("Ctrl") << KEY_LEFTCTRL << 4; + QTest::addRow("Alt") << KEY_LEFTALT << 8; + QTest::addRow("AltGr") << KEY_RIGHTALT << 128; +} + +void StickyKeysTest::testStick() +{ + QFETCH(int, modifierKey); + QFETCH(int, expectedMods); + + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Window *waylandWindow = Test::renderAndWaitForShown(surface.get(), QSize(10, 10), Qt::blue); + QVERIFY(waylandWindow); + + QSignalSpy modifierSpy(keyboard.get(), &KWayland::Client::Keyboard::modifiersChanged); + QVERIFY(modifierSpy.wait()); + modifierSpy.clear(); + + quint32 timestamp = 0; + + // press mod to latch it + Test::keyboardKeyPressed(modifierKey, ++timestamp); + QVERIFY(modifierSpy.wait()); + // arguments are: quint32 depressed, quint32 latched, quint32 locked, quint32 group + QCOMPARE(modifierSpy.first()[0], expectedMods); // verify that mod is depressed + QCOMPARE(modifierSpy.first()[1], expectedMods); // verify that mod is latched + + modifierSpy.clear(); + // release mod, the modifier should still be latched + Test::keyboardKeyReleased(modifierKey, ++timestamp); + QVERIFY(modifierSpy.wait()); + QCOMPARE(modifierSpy.first()[0], 0); // verify that mod is not depressed + QCOMPARE(modifierSpy.first()[1], expectedMods); // verify that mod is still latched + + // press and release a letter, this unlatches the modifier + modifierSpy.clear(); + Test::keyboardKeyPressed(KEY_A, ++timestamp); + QVERIFY(modifierSpy.wait()); + QCOMPARE(modifierSpy.first()[0], 0); // verify that mod is not depressed + QCOMPARE(modifierSpy.first()[1], 0); // verify that mod is not latched any more + + Test::keyboardKeyReleased(KEY_A, ++timestamp); +} + +void StickyKeysTest::testLock_data() +{ + QTest::addColumn("modifierKey"); + QTest::addColumn("expectedMods"); + QTest::addColumn("keyStateKey"); + + QTest::addRow("Shift") << KEY_LEFTSHIFT << 1 << Test::KeyStateV1::key_shift; + QTest::addRow("Ctrl") << KEY_LEFTCTRL << 4 << Test::KeyStateV1::key_control; + QTest::addRow("Alt") << KEY_LEFTALT << 8 << Test::KeyStateV1::key_alt; + QTest::addRow("AltGr") << KEY_RIGHTALT << 128 << Test::KeyStateV1::key_altgr; +} + +static bool waitSignals(QSignalSpy &one, QSignalSpy &two) +{ + int count2 = two.count(); + if (!one.wait()) { + return false; + } + return two.count() > count2 || two.wait(); +} + +void StickyKeysTest::testLock() +{ + QFETCH(int, modifierKey); + QFETCH(int, expectedMods); + QFETCH(Test::KeyStateV1::key, keyStateKey); + + KConfig kaccessConfig("kaccessrc"); + kaccessConfig.group(QStringLiteral("Keyboard")).writeEntry("StickyKeysLatch", true); + kaccessConfig.sync(); + + // reload the plugin to pick up the new config + kwinApp()->pluginManager()->unloadPlugin("StickyKeysPlugin"); + kwinApp()->pluginManager()->loadPlugin("StickyKeysPlugin"); + + QVERIFY(Test::waylandSeat()->hasKeyboard()); + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Window *waylandWindow = Test::renderAndWaitForShown(surface.get(), QSize(10, 10), Qt::blue); + QVERIFY(waylandWindow); + waylandWindow->move(QPoint(0, 0)); + + QSignalSpy modifierSpy(keyboard.get(), &KWayland::Client::Keyboard::modifiersChanged); + QVERIFY(modifierSpy.wait()); + modifierSpy.clear(); + + QSignalSpy keyStateSpy(Test::keyState(), &Test::KeyStateV1::stateChanged); + + quint32 timestamp = 0; + + // press mod to latch it + Test::keyboardKeyPressed(modifierKey, ++timestamp); + QVERIFY(waitSignals(modifierSpy, keyStateSpy)); + // arguments are: quint32 depressed, quint32 latched, quint32 locked, quint32 group + QCOMPARE(modifierSpy.first()[0], expectedMods); // verify that mod is depressed + QCOMPARE(modifierSpy.first()[1], expectedMods); // verify that mod is latched + QCOMPARE(Test::keyState()->keyToState[keyStateKey], Test::KeyStateV1::state_latched); + + modifierSpy.clear(); + // release mod, the modifier should still be latched + Test::keyboardKeyReleased(modifierKey, ++timestamp); + QVERIFY(waitSignals(modifierSpy, keyStateSpy)); + QCOMPARE(modifierSpy.first()[0], 0); // verify that mod is not depressed + QCOMPARE(modifierSpy.first()[1], expectedMods); // verify that mod is still latched + QCOMPARE(Test::keyState()->keyToState[keyStateKey], Test::KeyStateV1::state_latched); + + // press mod again to lock it + modifierSpy.clear(); + Test::keyboardKeyPressed(modifierKey, ++timestamp); + QVERIFY(waitSignals(modifierSpy, keyStateSpy)); + QCOMPARE(modifierSpy.first()[0], expectedMods); // verify that mod is depressed + QCOMPARE(modifierSpy.first()[1], 0); // verify that mod is unlatched + QCOMPARE(modifierSpy.first()[2], expectedMods); // verify that mod is locked + QCOMPARE(Test::keyState()->keyToState[keyStateKey], Test::KeyStateV1::state_locked); + + // release mod, modifier should still be locked + modifierSpy.clear(); + Test::keyboardKeyReleased(modifierKey, ++timestamp); + QVERIFY(modifierSpy.wait()); + QCOMPARE(modifierSpy.first()[0], 0); + QCOMPARE(modifierSpy.first()[1], 0); + QEXPECT_FAIL("", "FIXME!", Continue); + QCOMPARE(modifierSpy.first()[2], expectedMods); + QEXPECT_FAIL("", "FIXME!", Continue); + QCOMPARE(Test::keyState()->keyToState[keyStateKey], Test::KeyStateV1::state_locked); + + // press and release a letter, this does not unlock the modifier + modifierSpy.clear(); + Test::keyboardKeyPressed(KEY_A, ++timestamp); + QVERIFY(!modifierSpy.wait(10)); + + Test::keyboardKeyReleased(KEY_A, ++timestamp); + QVERIFY(!modifierSpy.wait(10)); + + // press mod again to unlock it + Test::keyboardKeyPressed(modifierKey, ++timestamp); + QVERIFY(waitSignals(modifierSpy, keyStateSpy)); + QCOMPARE(modifierSpy.first()[0], expectedMods); // verify that mod is depressed + QCOMPARE(modifierSpy.first()[1], 0); // verify that mod is unlatched + QCOMPARE(modifierSpy.first()[2], 0); // verify that mod is not locked + QEXPECT_FAIL("", "FIXME!", Continue); + QCOMPARE(Test::keyState()->keyToState[keyStateKey], Test::KeyStateV1::state_unlocked); + + Test::keyboardKeyReleased(modifierKey, ++timestamp); +} + +void StickyKeysTest::testDisableTwoKeys() +{ + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Window *waylandWindow = Test::renderAndWaitForShown(surface.get(), QSize(10, 10), Qt::blue); + QVERIFY(waylandWindow); + + QSignalSpy modifierSpy(keyboard.get(), &KWayland::Client::Keyboard::modifiersChanged); + QVERIFY(modifierSpy.wait()); + modifierSpy.clear(); + + quint32 timestamp = 0; + + // press mod to latch it + Test::keyboardKeyPressed(KEY_LEFTSHIFT, ++timestamp); + QVERIFY(modifierSpy.wait()); + // arguments are: quint32 depressed, quint32 latched, quint32 locked, quint32 group + QCOMPARE(modifierSpy.first()[0], 1); // verify that mod is depressed + QCOMPARE(modifierSpy.first()[1], 1); // verify that mod is latched + modifierSpy.clear(); + + // press key while modifier is pressed, this disables sticky keys + Test::keyboardKeyPressed(KEY_A, ++timestamp); + QVERIFY(modifierSpy.wait()); + QCOMPARE(modifierSpy.first()[0], 1); // verify that mod is depressed + QCOMPARE(modifierSpy.first()[1], 0); // verify that mod is not latched any more + modifierSpy.clear(); + + Test::keyboardKeyReleased(KEY_A, ++timestamp); + + // release mod, the modifier should not be depressed or latched + Test::keyboardKeyReleased(KEY_LEFTSHIFT, ++timestamp); + QVERIFY(modifierSpy.wait()); + QCOMPARE(modifierSpy.first()[0], 0); // verify that mod is not depressed + QCOMPARE(modifierSpy.first()[1], 0); // verify that mod is not latched + modifierSpy.clear(); + + // verify that sticky keys are not enabled any more + + // press mod, should be depressed but not latched + Test::keyboardKeyPressed(KEY_LEFTCTRL, ++timestamp); + QVERIFY(modifierSpy.wait()); + QCOMPARE(modifierSpy.first()[0], 4); // verify that mod is depressed + QCOMPARE(modifierSpy.first()[1], 0); // verify that mod is not latched + modifierSpy.clear(); + + // release mod, should not be depressed any more + Test::keyboardKeyReleased(KEY_LEFTCTRL, ++timestamp); + QVERIFY(modifierSpy.wait()); + QCOMPARE(modifierSpy.first()[0], 0); // verify that mod is not depressed + QCOMPARE(modifierSpy.first()[1], 0); // verify that mod is not latched + modifierSpy.clear(); + + Test::keyboardKeyPressed(KEY_A, ++timestamp); + QVERIFY(!modifierSpy.wait(10)); + Test::keyboardKeyReleased(KEY_A, ++timestamp); + QVERIFY(!modifierSpy.wait(10)); +} + +void StickyKeysTest::testMouse_data() +{ + QTest::addColumn("modifierKey"); + QTest::addColumn("expectedMods"); + + QTest::addRow("Shift") << KEY_LEFTSHIFT << 1; + QTest::addRow("Ctrl") << KEY_LEFTCTRL << 4; + QTest::addRow("Alt") << KEY_LEFTALT << 8; + QTest::addRow("AltGr") << KEY_RIGHTALT << 128; +} + +void StickyKeysTest::testMouse() +{ + QFETCH(int, modifierKey); + QFETCH(int, expectedMods); + + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Window *waylandWindow = Test::renderAndWaitForShown(surface.get(), QSize(10, 10), Qt::blue); + QVERIFY(waylandWindow); + + QSignalSpy modifierSpy(keyboard.get(), &KWayland::Client::Keyboard::modifiersChanged); + QVERIFY(modifierSpy.wait()); + modifierSpy.clear(); + + quint32 timestamp = 0; + + // press mod to latch it + Test::keyboardKeyPressed(modifierKey, ++timestamp); + QVERIFY(modifierSpy.wait()); + // arguments are: quint32 depressed, quint32 latched, quint32 locked, quint32 group + QCOMPARE(modifierSpy.first()[0], expectedMods); // verify that mod is depressed + QCOMPARE(modifierSpy.first()[1], expectedMods); // verify that mod is latched + + modifierSpy.clear(); + // release mod, the modifier should still be latched + Test::keyboardKeyReleased(modifierKey, ++timestamp); + QVERIFY(modifierSpy.wait()); + QCOMPARE(modifierSpy.first()[0], 0); // verify that mod is not depressed + QCOMPARE(modifierSpy.first()[1], expectedMods); // verify that mod is still latched + + // press and release a mouse button, this unlatches the modifier + modifierSpy.clear(); + Test::pointerButtonPressed(BTN_LEFT, ++timestamp); + QVERIFY(!modifierSpy.wait(10)); + Test::pointerButtonReleased(BTN_LEFT, ++timestamp); + QVERIFY(modifierSpy.wait()); + QCOMPARE(modifierSpy.first()[0], 0); // verify that mod is not depressed + QCOMPARE(modifierSpy.first()[1], 0); // verify that mod is not latched any more +} +} + +WAYLANDTEST_MAIN(KWin::StickyKeysTest) +#include "sticky_keys_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/tabbox_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/tabbox_test.cpp new file mode 100644 index 0000000000..89c8d02b80 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/tabbox_test.cpp @@ -0,0 +1,417 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/outputbackend.h" +#include "input.h" +#include "pointer_input.h" +#include "tabbox/tabbox.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include +#include + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_tabbox-0"); + +class TabBoxTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testMoveForward(); + void testMoveBackward(); + void testCapsLock(); + void testKeyboardFocus(); + void testActiveClientOutsideModel(); +}; + +void TabBoxTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + KSharedConfigPtr c = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + c->group(QStringLiteral("TabBox")).writeEntry("ShowTabBox", false); + c->sync(); + kwinApp()->setConfig(c); + qputenv("KWIN_XKB_DEFAULT_KEYMAP", "1"); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +void TabBoxTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); + workspace()->setActiveOutput(QPoint(640, 512)); + KWin::input()->pointer()->warp(QPoint(640, 512)); +} + +void TabBoxTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TabBoxTest::testCapsLock() +{ +#if !KWIN_BUILD_GLOBALSHORTCUTS + QSKIP("Can't test shortcuts without shortcuts"); + return; +#endif + + // this test verifies that Alt+tab works correctly also when Capslock is on + // bug 368590 + + // first create three windows + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + auto c1 = Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::blue); + QVERIFY(c1); + QVERIFY(c1->isActive()); + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + auto c2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 50), Qt::red); + QVERIFY(c2); + QVERIFY(c2->isActive()); + std::unique_ptr surface3(Test::createSurface()); + std::unique_ptr shellSurface3(Test::createXdgToplevelSurface(surface3.get())); + auto c3 = Test::renderAndWaitForShown(surface3.get(), QSize(100, 50), Qt::red); + QVERIFY(c3); + QVERIFY(c3->isActive()); + + // Setup tabbox signal spies + QSignalSpy tabboxAddedSpy(workspace()->tabbox(), &TabBox::TabBox::tabBoxAdded); + QSignalSpy tabboxClosedSpy(workspace()->tabbox(), &TabBox::TabBox::tabBoxClosed); + + // enable capslock + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); + Test::keyboardKeyReleased(KEY_CAPSLOCK, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::NoModifier); + + // press alt+tab + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::AltModifier); + Test::keyboardKeyPressed(KEY_TAB, timestamp++); + Test::keyboardKeyReleased(KEY_TAB, timestamp++); + + QVERIFY(tabboxAddedSpy.wait()); + QVERIFY(workspace()->tabbox()->isGrabbed()); + + // release alt + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + QCOMPARE(tabboxClosedSpy.count(), 1); + QCOMPARE(workspace()->tabbox()->isGrabbed(), false); + + // release caps lock + Test::keyboardKeyPressed(KEY_CAPSLOCK, timestamp++); + Test::keyboardKeyReleased(KEY_CAPSLOCK, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::NoModifier); + QCOMPARE(tabboxClosedSpy.count(), 1); + QCOMPARE(workspace()->tabbox()->isGrabbed(), false); + QCOMPARE(workspace()->activeWindow(), c2); + + surface3.reset(); + QVERIFY(Test::waitForWindowClosed(c3)); + surface2.reset(); + QVERIFY(Test::waitForWindowClosed(c2)); + surface1.reset(); + QVERIFY(Test::waitForWindowClosed(c1)); +} + +void TabBoxTest::testMoveForward() +{ +#if !KWIN_BUILD_GLOBALSHORTCUTS + QSKIP("Can't test shortcuts without shortcuts"); + return; +#endif + + // this test verifies that Alt+tab works correctly moving forward + + // first create three windows + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + auto c1 = Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::blue); + QVERIFY(c1); + QVERIFY(c1->isActive()); + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + auto c2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 50), Qt::red); + QVERIFY(c2); + QVERIFY(c2->isActive()); + std::unique_ptr surface3(Test::createSurface()); + std::unique_ptr shellSurface3(Test::createXdgToplevelSurface(surface3.get())); + auto c3 = Test::renderAndWaitForShown(surface3.get(), QSize(100, 50), Qt::red); + QVERIFY(c3); + QVERIFY(c3->isActive()); + + // Setup tabbox signal spies + QSignalSpy tabboxAddedSpy(workspace()->tabbox(), &TabBox::TabBox::tabBoxAdded); + QSignalSpy tabboxClosedSpy(workspace()->tabbox(), &TabBox::TabBox::tabBoxClosed); + + // press alt+tab + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::AltModifier); + Test::keyboardKeyPressed(KEY_TAB, timestamp++); + Test::keyboardKeyReleased(KEY_TAB, timestamp++); + + QVERIFY(tabboxAddedSpy.wait()); + QVERIFY(workspace()->tabbox()->isGrabbed()); + + // release alt + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + QCOMPARE(tabboxClosedSpy.count(), 1); + QCOMPARE(workspace()->tabbox()->isGrabbed(), false); + QCOMPARE(workspace()->activeWindow(), c2); + + surface3.reset(); + QVERIFY(Test::waitForWindowClosed(c3)); + surface2.reset(); + QVERIFY(Test::waitForWindowClosed(c2)); + surface1.reset(); + QVERIFY(Test::waitForWindowClosed(c1)); +} + +void TabBoxTest::testMoveBackward() +{ +#if !KWIN_BUILD_GLOBALSHORTCUTS + QSKIP("Can't test shortcuts without shortcuts"); + return; +#endif + + // this test verifies that Alt+Shift+tab works correctly moving backward + + // first create three windows + std::unique_ptr surface1(Test::createSurface()); + std::unique_ptr shellSurface1(Test::createXdgToplevelSurface(surface1.get())); + auto c1 = Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::blue); + QVERIFY(c1); + QVERIFY(c1->isActive()); + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + auto c2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 50), Qt::red); + QVERIFY(c2); + QVERIFY(c2->isActive()); + std::unique_ptr surface3(Test::createSurface()); + std::unique_ptr shellSurface3(Test::createXdgToplevelSurface(surface3.get())); + auto c3 = Test::renderAndWaitForShown(surface3.get(), QSize(100, 50), Qt::red); + QVERIFY(c3); + QVERIFY(c3->isActive()); + + // Setup tabbox signal spies + QSignalSpy tabboxAddedSpy(workspace()->tabbox(), &TabBox::TabBox::tabBoxAdded); + QSignalSpy tabboxClosedSpy(workspace()->tabbox(), &TabBox::TabBox::tabBoxClosed); + + // press alt+shift+tab + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::AltModifier); + Test::keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::AltModifier | Qt::ShiftModifier); + Test::keyboardKeyPressed(KEY_TAB, timestamp++); + Test::keyboardKeyReleased(KEY_TAB, timestamp++); + + QVERIFY(tabboxAddedSpy.wait()); + QVERIFY(workspace()->tabbox()->isGrabbed()); + + // release alt + Test::keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); + QCOMPARE(tabboxClosedSpy.count(), 0); + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + QCOMPARE(tabboxClosedSpy.count(), 1); + QCOMPARE(workspace()->tabbox()->isGrabbed(), false); + QCOMPARE(workspace()->activeWindow(), c1); + + surface3.reset(); + QVERIFY(Test::waitForWindowClosed(c3)); + surface2.reset(); + QVERIFY(Test::waitForWindowClosed(c2)); + surface1.reset(); + QVERIFY(Test::waitForWindowClosed(c1)); +} + +void TabBoxTest::testKeyboardFocus() +{ + // This test verifies that the keyboard focus will be withdrawn from the currently activated + // window when the task switcher is active and restored once the task switcher is dismissed. + + QVERIFY(Test::waitForWaylandKeyboard()); + + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy enteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); + QSignalSpy leftSpy(keyboard.get(), &KWayland::Client::Keyboard::left); + + // add a window + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + // the keyboard focus will be moved to the surface after it's mapped + QVERIFY(enteredSpy.wait()); + + QSignalSpy tabboxAddedSpy(workspace()->tabbox(), &TabBox::TabBox::tabBoxAdded); + QSignalSpy tabboxClosedSpy(workspace()->tabbox(), &TabBox::TabBox::tabBoxClosed); + + // press alt+tab + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + Test::keyboardKeyPressed(KEY_TAB, timestamp++); + Test::keyboardKeyReleased(KEY_TAB, timestamp++); + QVERIFY(tabboxAddedSpy.wait()); + + // the surface should have no keyboard focus anymore because tabbox grabs input + QCOMPARE(leftSpy.count(), 1); + + // release alt + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + QCOMPARE(tabboxClosedSpy.count(), 1); + + // the surface should regain keyboard focus after the tabbox is dismissed + QVERIFY(enteredSpy.wait()); +} + +void TabBoxTest::testActiveClientOutsideModel() +{ +#if !KWIN_BUILD_GLOBALSHORTCUTS + QSKIP("Can't test shortcuts without shortcuts"); + return; +#endif + + // This test verifies behaviour when the active client is outside the + // client list model: + // + // 1) reset() should correctly set the index to 0 if the active window is + // not part of the client list. + // 2) the selection should not be advanced initially if the active window + // is not part of the client list. + + const auto outputs = workspace()->outputs(); + + // Initially, set up MultiScreenMode such that alt+tab will only switch + // within windows on the same screen. + KConfigGroup group = kwinApp()->config()->group(QStringLiteral("TabBox")); + group.writeEntry("MultiScreenMode", "1"); + group.sync(); + workspace()->slotReconfigure(); + + // Create a window on the left output + std::unique_ptr leftSurface1(Test::createSurface()); + std::unique_ptr leftShellSurface1(Test::createXdgToplevelSurface(leftSurface1.get())); + auto l1 = Test::renderAndWaitForShown(leftSurface1.get(), QSize(100, 50), Qt::blue); + l1->move(QPointF(50, 100)); + QVERIFY(l1); + QVERIFY(l1->isActive()); + QCOMPARE(l1->output(), outputs[0]); + + // Create three windows on the right output + std::unique_ptr rightSurface1(Test::createSurface()); + std::unique_ptr rightShellSurface1(Test::createXdgToplevelSurface(rightSurface1.get())); + auto r1 = Test::renderAndWaitForShown(rightSurface1.get(), QSize(100, 50), Qt::blue); + r1->move(QPointF(1280 + 50, 100)); + QVERIFY(r1); + QVERIFY(r1->isActive()); + QCOMPARE(r1->output(), outputs[1]); + std::unique_ptr rightSurface2(Test::createSurface()); + std::unique_ptr rightShellSurface2(Test::createXdgToplevelSurface(rightSurface2.get())); + auto r2 = Test::renderAndWaitForShown(rightSurface2.get(), QSize(100, 50), Qt::red); + r2->move(QPointF(1280 + 50, 100)); + QVERIFY(r2); + QVERIFY(r2->isActive()); + QCOMPARE(r2->output(), outputs[1]); + std::unique_ptr rightSurface3(Test::createSurface()); + std::unique_ptr rightShellSurface3(Test::createXdgToplevelSurface(rightSurface3.get())); + auto r3 = Test::renderAndWaitForShown(rightSurface3.get(), QSize(100, 50), Qt::red); + r3->move(QPointF(1280 + 50, 100)); + QVERIFY(r3); + QVERIFY(r3->isActive()); + QCOMPARE(r3->output(), outputs[1]); + + // Focus r3 such that we're on the right output + input()->pointer()->warp(r3->frameGeometry().center()); + QCOMPARE(workspace()->activeOutput(), outputs[1]); + + // Setup tabbox signal spies + QSignalSpy tabboxAddedSpy(workspace()->tabbox(), &TabBox::TabBox::tabBoxAdded); + QSignalSpy tabboxClosedSpy(workspace()->tabbox(), &TabBox::TabBox::tabBoxClosed); + + // Press Alt+Tab, this will only show clients on the same output + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::AltModifier); + Test::keyboardKeyPressed(KEY_TAB, timestamp++); + Test::keyboardKeyReleased(KEY_TAB, timestamp++); + + QVERIFY(tabboxAddedSpy.wait()); + QVERIFY(workspace()->tabbox()->isGrabbed()); + + // Release Alt+Tab. This will have moved our index to 1 and focused r2 (the + // previously created window) + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + QCOMPARE(tabboxClosedSpy.count(), 1); + QCOMPARE(workspace()->tabbox()->isGrabbed(), false); + QCOMPARE(workspace()->activeWindow(), r2); + + // Now reconfigure MultiScreenMode such that alt+tab will only switch + // between windows on the other screen + group.writeEntry("MultiScreenMode", 2); + group.sync(); + workspace()->slotReconfigure(); + + // Activate and focus l1 to switch to the left output + workspace()->activateWindow(l1); + QCOMPARE(workspace()->activeWindow(), l1); + input()->pointer()->warp(l1->frameGeometry().center()); + QCOMPARE(workspace()->activeOutput(), outputs[0]); + + // Press Alt+Tab, this will show only clients on the other output. Our old + // index from the last invocation of tabbox should be reset to 0 since the + // active window (l1) cannot be located in the current client list + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + QCOMPARE(input()->keyboardModifiers(), Qt::AltModifier); + Test::keyboardKeyPressed(KEY_TAB, timestamp++); + Test::keyboardKeyReleased(KEY_TAB, timestamp++); + + QVERIFY(tabboxAddedSpy.wait()); + QVERIFY(workspace()->tabbox()->isGrabbed()); + + // Release Alt. With a correctly reset index we should start from the + // beginning, skip advancing one window and focus r2 - the last window in + // focus on the other output + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + QCOMPARE(tabboxClosedSpy.count(), 2); + QCOMPARE(workspace()->tabbox()->isGrabbed(), false); + QCOMPARE(workspace()->activeWindow(), r2); + + rightSurface3.reset(); + QVERIFY(Test::waitForWindowClosed(r3)); + rightSurface2.reset(); + QVERIFY(Test::waitForWindowClosed(r2)); + rightSurface1.reset(); + QVERIFY(Test::waitForWindowClosed(r1)); + leftSurface1.reset(); + QVERIFY(Test::waitForWindowClosed(l1)); +} + +WAYLANDTEST_MAIN(TabBoxTest) +#include "tabbox_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/test_colormanagement.cpp b/local/recipes/kde/kwin/source/autotests/integration/test_colormanagement.cpp new file mode 100644 index 0000000000..3e7f3ff68d --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/test_colormanagement.cpp @@ -0,0 +1,571 @@ +/* + SPDX-FileCopyrightText: 2024 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "core/colorpipeline.h" +#include "core/drmdevice.h" +#include "core/graphicsbuffer.h" +#include "core/output.h" +#include "core/outputbackend.h" +#include "core/outputconfiguration.h" +#include "outputconfigurationstore.h" +#include "pointer_input.h" +#include "tiles/tilemanager.h" +#include "wayland-client/linuxdmabuf.h" +#include "wayland/surface.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "qwayland-color-management-v1.h" +#include "wayland-linux-dmabuf-unstable-v1-client-protocol.h" + +using namespace std::chrono_literals; + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_color_management-0"); + +class ImageDescription : public QObject, public QtWayland::wp_image_description_v1 +{ + Q_OBJECT +public: + explicit ImageDescription(::wp_image_description_v1 *descr) + : QtWayland::wp_image_description_v1(descr) + { + } + + ~ImageDescription() override + { + wp_image_description_v1_destroy(object()); + } + + void wp_image_description_v1_ready2(uint32_t identity_hi, uint32_t identity_lo) override + { + Q_EMIT ready(); + } + + void wp_image_description_v1_failed(uint32_t cause, const QString &msg) override + { + Q_EMIT failed(); + } + +Q_SIGNALS: + void ready(); + void failed(); +}; + +class ColorManagementSurface : public QObject, public QtWayland::wp_color_management_surface_v1 +{ + Q_OBJECT +public: + explicit ColorManagementSurface(::wp_color_management_surface_v1 *obj) + : QtWayland::wp_color_management_surface_v1(obj) + { + } + + ~ColorManagementSurface() override + { + wp_color_management_surface_v1_destroy(object()); + } +}; + +class ColorManagementTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testSetImageDescription_data(); + void testSetImageDescription(); + void testUnsupportedPrimaries(); + void testNoPrimaries(); + void testNoTf(); + void testRenderIntentOnly(); + void testInertError(); +}; + +void ColorManagementTest::initTestCase() +{ + QSKIP("v1 is forced because v2 breaks Firefox"); + qputenv("KWIN_COMPOSE", QByteArrayLiteral("O2")); + + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); +} + +void ColorManagementTest::init() +{ + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::ColorManagement + | Test::AdditionalWaylandInterface::ColorRepresentation)); + + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); +} + +void ColorManagementTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void ColorManagementTest::testSetImageDescription_data() +{ + QTest::addColumn>("input"); + QTest::addColumn("renderingIntent"); + QTest::addColumn("protocolError"); + QTest::addColumn("shouldSucceed"); + QTest::addColumn>>("expectedResult"); + + // sRGB is not tested, because it's the default (and thus no change signal will be emitted) + QTest::addRow("rec.2020 PQ") + << std::make_shared(ColorDescription{ + Colorimetry::BT2020, + TransferFunction(TransferFunction::PerceptualQuantizer), + 203, + 0, + 400, + 400, + }) + << RenderingIntent::Perceptual + << false << true + << std::optional>(); + QTest::addRow("scRGB") + << std::make_shared(ColorDescription{ + Colorimetry::BT709, + TransferFunction(TransferFunction::linear, 0, 80), + 80, + 0, + 80, + 80, + }) + << RenderingIntent::Perceptual + << false << true + << std::optional>(); + QTest::addRow("custom") + << std::make_shared(ColorDescription{ + Colorimetry::BT2020, + TransferFunction(TransferFunction::gamma22, 0.05, 400), + 203, + 0, + 400, + 400, + }) + << RenderingIntent::Perceptual + << false << true + << std::optional>(); + QTest::addRow("invalid tf") + << std::make_shared(ColorDescription{ + Colorimetry::BT2020, + TransferFunction(TransferFunction::gamma22, 204, 205), + 203, + 0, + 400, + 400, + }) + << RenderingIntent::Perceptual + << true << false + << std::optional>(); + QTest::addRow("invalid HDR metadata") + << std::make_shared(ColorDescription{ + Colorimetry::BT2020, + TransferFunction(TransferFunction::PerceptualQuantizer), + 203, + 500, + 400, + 400, + }) + << RenderingIntent::Perceptual + << true << false + << std::optional>(); + QTest::addRow("rec.2020 PQ with out of bounds white point") + << std::make_shared(ColorDescription{ + Colorimetry::BT2020.withWhitepoint(xyY{0.9, 0.9, 1}), + TransferFunction(TransferFunction::PerceptualQuantizer), + 203, + 0, + 400, + 400, + }) + << RenderingIntent::Perceptual + << false << false + << std::optional>(); + QTest::addRow("nonsense primaries") + << std::make_shared(ColorDescription{ + Colorimetry(xy{0, 0}, xy{0, 0}, xy{0, 0}, xy{0, 0}), + TransferFunction(TransferFunction::PerceptualQuantizer), + 203, + 0, + 400, + 400, + }) + << RenderingIntent::Perceptual + << false << false + << std::optional>(); + QTest::addRow("custom PQ luminances are ignored") + << std::make_shared(ColorDescription{ + Colorimetry::BT2020, + TransferFunction(TransferFunction::PerceptualQuantizer, 10, 100), + 203, + 0, + 400, + 400, + }) + << RenderingIntent::Perceptual + << false << true + << std::optional(std::make_shared(ColorDescription{ + Colorimetry::BT2020, + TransferFunction(TransferFunction::PerceptualQuantizer, 10, 10'010), + 203, + 0, + 400, + 400, + })); + + QTest::addRow("rec.2020 PQ relative colorimetric") + << std::make_shared(ColorDescription{ + Colorimetry::BT2020, + TransferFunction(TransferFunction::PerceptualQuantizer), + 203, + 0, + 400, + 400, + }) + << RenderingIntent::RelativeColorimetric + << false << true + << std::optional>(); + QTest::addRow("rec.2020 PQ relative colorimetric bpc") + << std::make_shared(ColorDescription{ + Colorimetry::BT2020, + TransferFunction(TransferFunction::PerceptualQuantizer), + 203, + 0, + 400, + 400, + }) + << RenderingIntent::RelativeColorimetricWithBPC + << false << true + << std::optional>(); + QTest::addRow("rec.2020 PQ absolute colorimetric") + << std::make_shared(ColorDescription{ + Colorimetry::BT2020, + TransferFunction(TransferFunction::PerceptualQuantizer), + 203, + 0, + 400, + 400, + }) + << RenderingIntent::AbsoluteColorimetricNoAdaptation + << false << true + << std::optional>(); + QTest::addRow("rec.709 + BT1886") + << std::make_shared(ColorDescription{ + Colorimetry::BT709, + TransferFunction(TransferFunction::BT1886), + 100, + 0.1, + 100, + 100, + }) + << RenderingIntent::Perceptual + << false << true + << std::optional>(); + + QTest::addRow("rec601 limited range YUV") + << std::make_shared(ColorDescription{ + Colorimetry::BT709, + TransferFunction(TransferFunction::BT1886), + YUVMatrixCoefficients::BT601, + EncodingRange::Limited, + }) + << RenderingIntent::Perceptual + << false << true + << std::optional>(); + QTest::addRow("rec709 full range YUV") + << std::make_shared(ColorDescription{ + Colorimetry::BT709, + TransferFunction(TransferFunction::BT1886), + YUVMatrixCoefficients::BT709, + EncodingRange::Full, + }) + << RenderingIntent::Perceptual + << false << true + << std::optional>(); + QTest::addRow("rec2020 limited range YUV") + << std::make_shared(ColorDescription{ + Colorimetry::BT709, + TransferFunction(TransferFunction::PerceptualQuantizer), + YUVMatrixCoefficients::BT2020, + EncodingRange::Limited, + }) + << RenderingIntent::Perceptual + << false << true + << std::optional>(); +} + +static ImageDescription createImageDescription(ColorManagementSurface *surface, const ColorDescription &color) +{ + QtWayland::wp_image_description_creator_params_v1 creator(Test::colorManager()->create_parametric_creator()); + + creator.set_primaries(std::round(1'000'000.0 * color.containerColorimetry().red().toxyY().x), + std::round(1'000'000.0 * color.containerColorimetry().red().toxyY().y), + std::round(1'000'000.0 * color.containerColorimetry().green().toxyY().x), + std::round(1'000'000.0 * color.containerColorimetry().green().toxyY().y), + std::round(1'000'000.0 * color.containerColorimetry().blue().toxyY().x), + std::round(1'000'000.0 * color.containerColorimetry().blue().toxyY().y), + std::round(1'000'000.0 * color.containerColorimetry().white().toxyY().x), + std::round(1'000'000.0 * color.containerColorimetry().white().toxyY().y)); + switch (color.transferFunction().type) { + case TransferFunction::sRGB: + creator.set_tf_named(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_COMPOUND_POWER_2_4); + break; + case TransferFunction::gamma22: + creator.set_tf_named(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA22); + break; + case TransferFunction::linear: + creator.set_tf_named(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR); + break; + case TransferFunction::PerceptualQuantizer: + creator.set_tf_named(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ); + break; + case TransferFunction::BT1886: + creator.set_tf_named(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_BT1886); + break; + } + creator.set_luminances(std::round(color.transferFunction().minLuminance * 10'000), std::round(color.transferFunction().maxLuminance), std::round(color.referenceLuminance())); + creator.set_max_fall(std::round(color.maxAverageLuminance().value_or(0))); + creator.set_max_cll(std::round(color.maxHdrLuminance().value_or(0))); + creator.set_mastering_luminance(std::round(color.minLuminance() * 10'000), std::round(color.maxHdrLuminance().value_or(0))); + + return ImageDescription(wp_image_description_creator_params_v1_create(creator.object())); +} + +static const std::unordered_map s_coefficientsMap = { + {YUVMatrixCoefficients::Identity, WP_COLOR_REPRESENTATION_SURFACE_V1_COEFFICIENTS_IDENTITY}, + {YUVMatrixCoefficients::BT601, WP_COLOR_REPRESENTATION_SURFACE_V1_COEFFICIENTS_BT601}, + {YUVMatrixCoefficients::BT709, WP_COLOR_REPRESENTATION_SURFACE_V1_COEFFICIENTS_BT709}, + {YUVMatrixCoefficients::BT2020, WP_COLOR_REPRESENTATION_SURFACE_V1_COEFFICIENTS_BT2020}, +}; +static const std::unordered_map s_rangeMap = { + {EncodingRange::Limited, WP_COLOR_REPRESENTATION_SURFACE_V1_RANGE_LIMITED}, + {EncodingRange::Full, WP_COLOR_REPRESENTATION_SURFACE_V1_RANGE_FULL}, +}; + +static std::unique_ptr createRepresentation(KWayland::Client::Surface *surf, + const std::shared_ptr &color) +{ + auto ret = std::make_unique(Test::colorRepresentation()->get_surface(*surf)); + ret->set_coefficients_and_range(s_coefficientsMap.at(color->yuvCoefficients()), s_rangeMap.at(color->range())); + return ret; +} + +#if HAVE_MEMFD +static wl_buffer *createYuvBuffer(const QSize &size) +{ + FileDescriptor fd{memfd_create("kwayland-shared", MFD_CLOEXEC | MFD_ALLOW_SEALING)}; + if (!fd.isValid()) { + return nullptr; + } + const uint32_t bytesPerPixel = 4; + if (ftruncate(fd.get(), size.width() * size.height() * bytesPerPixel) < 0) { + return nullptr; + } + fcntl(fd.get(), F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_SEAL); + wl_shm_pool *pool = wl_shm_create_pool(Test::waylandShmPool()->shm(), fd.get(), size.width() * size.height() * bytesPerPixel); + wl_buffer *buffer = wl_shm_pool_create_buffer(pool, 0, size.width(), size.height(), size.width() * bytesPerPixel, DRM_FORMAT_XYUV8888); + wl_shm_pool_destroy(pool); + return buffer; +} +#endif + +void ColorManagementTest::testSetImageDescription() +{ + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + auto cmSurf = std::make_unique(Test::colorManager()->get_surface(*surface)); + + QFETCH(std::shared_ptr, input); + QFETCH(RenderingIntent, renderingIntent); + + ImageDescription imageDescr = createImageDescription(cmSurf.get(), *input); + auto representation = createRepresentation(surface.get(), input); + + // YUV color descriptions require YUV buffers + wl_buffer *buffer = nullptr; + if (input->yuvCoefficients() != YUVMatrixCoefficients::Identity) { +#if HAVE_MEMFD + buffer = createYuvBuffer(QSize(100, 50)); + QVERIFY(buffer); + surface->attachBuffer(buffer); +#else + Q_SKIP("YUV tests without memfd aren't implemented"); +#endif + } + + QFETCH(bool, protocolError); + if (protocolError) { + QSignalSpy error(Test::waylandConnection(), &KWayland::Client::ConnectionThread::errorOccurred); + QVERIFY(error.wait(50ms)); + return; + } + + uint32_t waylandRenderIntent; + switch (renderingIntent) { + case RenderingIntent::Perceptual: + waylandRenderIntent = WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL; + break; + case RenderingIntent::RelativeColorimetric: + waylandRenderIntent = WP_COLOR_MANAGER_V1_RENDER_INTENT_RELATIVE; + break; + case RenderingIntent::RelativeColorimetricWithBPC: + waylandRenderIntent = WP_COLOR_MANAGER_V1_RENDER_INTENT_RELATIVE_BPC; + break; + case RenderingIntent::AbsoluteColorimetricNoAdaptation: + waylandRenderIntent = WP_COLOR_MANAGER_V1_RENDER_INTENT_ABSOLUTE_NO_ADAPTATION; + break; + default: + Q_UNREACHABLE(); + } + + QFETCH(bool, shouldSucceed); + QFETCH(std::optional>, expectedResult); + if (shouldSucceed) { + QSignalSpy ready(&imageDescr, &ImageDescription::ready); + QVERIFY(ready.wait(50ms)); + + cmSurf->set_image_description(imageDescr.object(), waylandRenderIntent); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + QSignalSpy colorChange(window->surface(), &SurfaceInterface::colorDescriptionChanged); + QVERIFY(colorChange.wait()); + + QCOMPARE(*window->surface()->colorDescription(), *expectedResult.value_or(input)); + QCOMPARE(window->surface()->renderingIntent(), renderingIntent); + } else { + QSignalSpy fail(&imageDescr, &ImageDescription::failed); + QVERIFY(fail.wait(50ms)); + + // trying to use a failed image description should cause an error + QSignalSpy error(Test::waylandConnection(), &KWayland::Client::ConnectionThread::errorOccurred); + cmSurf->set_image_description(imageDescr.object(), waylandRenderIntent); + QVERIFY(error.wait(50ms)); + } + if (buffer) { + wl_buffer_destroy(buffer); + } +} + +void ColorManagementTest::testUnsupportedPrimaries() +{ + QtWayland::wp_image_description_creator_params_v1 creator = QtWayland::wp_image_description_creator_params_v1(Test::colorManager()->create_parametric_creator()); + creator.set_primaries_named(-1); + wp_image_description_creator_params_v1_create(creator.object()); + QSignalSpy error(Test::waylandConnection(), &KWayland::Client::ConnectionThread::errorOccurred); + QVERIFY(error.wait(50ms)); +} + +void ColorManagementTest::testNoPrimaries() +{ + QtWayland::wp_image_description_creator_params_v1 creator = QtWayland::wp_image_description_creator_params_v1(Test::colorManager()->create_parametric_creator()); + creator.set_tf_named(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR); + wp_image_description_creator_params_v1_create(creator.object()); + QSignalSpy error(Test::waylandConnection(), &KWayland::Client::ConnectionThread::errorOccurred); + QVERIFY(error.wait(50ms)); +} + +void ColorManagementTest::testNoTf() +{ + QtWayland::wp_image_description_creator_params_v1 creator = QtWayland::wp_image_description_creator_params_v1(Test::colorManager()->create_parametric_creator()); + creator.set_primaries_named(WP_COLOR_MANAGER_V1_PRIMARIES_CIE1931_XYZ); + wp_image_description_creator_params_v1_create(creator.object()); + QSignalSpy error(Test::waylandConnection(), &KWayland::Client::ConnectionThread::errorOccurred); + QVERIFY(error.wait(50ms)); +} + +void ColorManagementTest::testRenderIntentOnly() +{ + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + const auto color = std::make_shared(ColorDescription{ + Colorimetry::BT2020, + TransferFunction(TransferFunction::PerceptualQuantizer), + }); + + auto cmSurf = std::make_unique(Test::colorManager()->get_surface(*surface)); + ImageDescription imageDescr = createImageDescription(cmSurf.get(), *color); + + QSignalSpy ready(&imageDescr, &ImageDescription::ready); + QVERIFY(ready.wait(50ms)); + + cmSurf->set_image_description(imageDescr.object(), WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + QSignalSpy colorChange(window->surface(), &SurfaceInterface::colorDescriptionChanged); + QVERIFY(colorChange.wait()); + + QCOMPARE(*window->surface()->colorDescription(), *color); + QCOMPARE(window->surface()->renderingIntent(), RenderingIntent::Perceptual); + + // only changing the rendering intent should also trigger the colorDescriptionChanged signal to be emitted + cmSurf->set_image_description(imageDescr.object(), WP_COLOR_MANAGER_V1_RENDER_INTENT_RELATIVE); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + QVERIFY(colorChange.wait()); + QCOMPARE(window->surface()->colorDescription(), color); + QCOMPARE(window->surface()->renderingIntent(), RenderingIntent::RelativeColorimetric); +} + +void ColorManagementTest::testInertError() +{ + std::unique_ptr surface(Test::createSurface()); + + auto cmSurf = std::make_unique(Test::colorManager()->get_surface(*surface)); + ImageDescription imageDescr = createImageDescription(cmSurf.get(), *ColorDescription::sRGB); + + QSignalSpy ready(&imageDescr, &ImageDescription::ready); + QVERIFY(ready.wait(50ms)); + + surface.reset(); + cmSurf->set_image_description(imageDescr.object(), WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL); + + QSignalSpy error(Test::waylandConnection(), &KWayland::Client::ConnectionThread::errorOccurred); + QVERIFY(error.wait(50ms)); +} + +} // namespace KWin + +WAYLANDTEST_MAIN(KWin::ColorManagementTest) +#include "test_colormanagement.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/test_helpers.cpp b/local/recipes/kde/kwin/source/autotests/integration/test_helpers.cpp new file mode 100644 index 0000000000..15830dcbc4 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/test_helpers.cpp @@ -0,0 +1,2608 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include + +#include + +#include "kwin_wayland_test.h" + +#if KWIN_BUILD_X11 +#include "atoms.h" +#endif +#include "input_event.h" +#include "inputmethod.h" +#include "wayland-client/linuxdmabuf.h" +#include "wayland-client/viewporter.h" +#include "wayland-linux-dmabuf-unstable-v1-client-protocol.h" +#include "wayland-viewporter-client-protocol.h" +#include "wayland-zkde-screencast-unstable-v1-client-protocol.h" +#include "wayland/display.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// screenlocker +#if KWIN_BUILD_SCREENLOCKER +#include +#endif + +#include +#include +#include +#include + +// system +#include +#include +#include +#include + +namespace KWin +{ +namespace Test +{ + +static std::unique_ptr s_waylandConnection; + +LayerShellV1::~LayerShellV1() +{ + destroy(); +} + +LayerSurfaceV1::~LayerSurfaceV1() +{ + destroy(); +} + +void LayerSurfaceV1::zwlr_layer_surface_v1_configure(uint32_t serial, uint32_t width, uint32_t height) +{ + Q_EMIT configureRequested(serial, QSize(width, height)); +} + +void LayerSurfaceV1::zwlr_layer_surface_v1_closed() +{ + Q_EMIT closeRequested(); +} + +XdgShell::~XdgShell() +{ + destroy(); +} + +XdgSurface::XdgSurface(XdgShell *shell, KWayland::Client::Surface *surface, QObject *parent) + : QObject(parent) + , QtWayland::xdg_surface(shell->get_xdg_surface(*surface)) + , m_surface(surface) +{ +} + +XdgSurface::~XdgSurface() +{ + destroy(); +} + +KWayland::Client::Surface *XdgSurface::surface() const +{ + return m_surface; +} + +void XdgSurface::xdg_surface_configure(uint32_t serial) +{ + Q_EMIT configureRequested(serial); +} + +XdgToplevel::XdgToplevel(XdgSurface *surface, QObject *parent) + : QObject(parent) + , QtWayland::xdg_toplevel(surface->get_toplevel()) + , m_xdgSurface(surface) +{ +} + +XdgToplevel::~XdgToplevel() +{ + destroy(); +} + +XdgSurface *XdgToplevel::xdgSurface() const +{ + return m_xdgSurface.get(); +} + +void XdgToplevel::xdg_toplevel_configure(int32_t width, int32_t height, wl_array *states) +{ + States requestedStates; + + const uint32_t *stateData = static_cast(states->data); + const size_t stateCount = states->size / sizeof(uint32_t); + + for (size_t i = 0; i < stateCount; ++i) { + switch (stateData[i]) { + case QtWayland::xdg_toplevel::state_maximized: + requestedStates |= State::Maximized; + break; + case QtWayland::xdg_toplevel::state_fullscreen: + requestedStates |= State::Fullscreen; + break; + case QtWayland::xdg_toplevel::state_resizing: + requestedStates |= State::Resizing; + break; + case QtWayland::xdg_toplevel::state_activated: + requestedStates |= State::Activated; + break; + } + } + + Q_EMIT configureRequested(QSize(width, height), requestedStates); +} + +void XdgToplevel::xdg_toplevel_close() +{ + Q_EMIT closeRequested(); +} + +XdgPositioner::XdgPositioner(XdgShell *shell) + : QtWayland::xdg_positioner(shell->create_positioner()) +{ +} + +XdgPositioner::~XdgPositioner() +{ + destroy(); +} + +XdgPopup::XdgPopup(XdgSurface *surface, XdgSurface *parentSurface, XdgPositioner *positioner, QObject *parent) + : QObject(parent) + , QtWayland::xdg_popup(surface->get_popup(parentSurface->object(), positioner->object())) + , m_xdgSurface(surface) +{ +} + +XdgPopup::~XdgPopup() +{ + destroy(); +} + +XdgSurface *XdgPopup::xdgSurface() const +{ + return m_xdgSurface.get(); +} + +void XdgPopup::xdg_popup_configure(int32_t x, int32_t y, int32_t width, int32_t height) +{ + Q_EMIT configureRequested(Rect(x, y, width, height)); +} + +void XdgPopup::xdg_popup_popup_done() +{ + Q_EMIT doneReceived(); +} + +void XdgPopup::xdg_popup_repositioned(uint32_t token) +{ + Q_EMIT repositioned(token); +} + +XdgDecorationManagerV1::~XdgDecorationManagerV1() +{ + destroy(); +} + +XdgToplevelDecorationV1::XdgToplevelDecorationV1(XdgDecorationManagerV1 *manager, + XdgToplevel *toplevel, QObject *parent) + : QObject(parent) + , QtWayland::zxdg_toplevel_decoration_v1(manager->get_toplevel_decoration(toplevel->object())) +{ +} + +XdgToplevelDecorationV1::~XdgToplevelDecorationV1() +{ + destroy(); +} + +void XdgToplevelDecorationV1::zxdg_toplevel_decoration_v1_configure(uint32_t m) +{ + Q_EMIT configureRequested(mode(m)); +} + +IdleInhibitManagerV1::~IdleInhibitManagerV1() +{ + destroy(); +} + +IdleInhibitorV1::IdleInhibitorV1(IdleInhibitManagerV1 *manager, KWayland::Client::Surface *surface) + : QtWayland::zwp_idle_inhibitor_v1(manager->create_inhibitor(*surface)) +{ +} + +IdleInhibitorV1::~IdleInhibitorV1() +{ + destroy(); +} + +ScreenEdgeManagerV1::~ScreenEdgeManagerV1() +{ + destroy(); +} + +AutoHideScreenEdgeV1::AutoHideScreenEdgeV1(ScreenEdgeManagerV1 *manager, KWayland::Client::Surface *surface, uint32_t border) + : QtWayland::kde_auto_hide_screen_edge_v1(manager->get_auto_hide_screen_edge(border, *surface)) +{ +} + +AutoHideScreenEdgeV1::~AutoHideScreenEdgeV1() +{ + destroy(); +} + +CursorShapeManagerV1::~CursorShapeManagerV1() +{ + destroy(); +} + +CursorShapeDeviceV1::CursorShapeDeviceV1(CursorShapeManagerV1 *manager, KWayland::Client::Pointer *pointer) + : QtWayland::wp_cursor_shape_device_v1(manager->get_pointer(*pointer)) +{ +} + +CursorShapeDeviceV1::~CursorShapeDeviceV1() +{ + destroy(); +} + +FakeInput::~FakeInput() +{ + destroy(); +} + +SecurityContextManagerV1::~SecurityContextManagerV1() +{ + destroy(); +} + +XdgWmDialogV1::~XdgWmDialogV1() +{ + destroy(); +} + +XdgDialogV1::XdgDialogV1(XdgWmDialogV1 *wm, XdgToplevel *toplevel) + : QtWayland::xdg_dialog_v1(wm->get_xdg_dialog(toplevel->object())) +{ +} + +XdgDialogV1::~XdgDialogV1() +{ + destroy(); +} + +MockInputMethod *inputMethod() +{ + return s_waylandConnection->inputMethodV1; +} + +KWayland::Client::Surface *inputPanelSurface() +{ + return s_waylandConnection->inputMethodV1->inputPanelSurface(); +} + +MockInputMethod::MockInputMethod(struct wl_registry *registry, int id, int version) + : QtWayland::zwp_input_method_v1(registry, id, version) +{ +} +MockInputMethod::~MockInputMethod() +{ +} + +void MockInputMethod::zwp_input_method_v1_activate(struct ::zwp_input_method_context_v1 *context) +{ + if (!m_inputSurface) { + m_inputSurface = Test::createSurface(); + m_inputMethodSurface = Test::createInputPanelSurfaceV1(m_inputSurface.get(), s_waylandConnection->outputs.first(), m_mode); + } + m_context = context; + + switch (m_mode) { + case Mode::TopLevel: + Test::render(m_inputSurface.get(), QSize(1280, 400), Qt::blue); + break; + case Mode::Overlay: + Test::render(m_inputSurface.get(), QSize(200, 50), Qt::blue); + break; + } + + Q_EMIT activate(); +} + +void MockInputMethod::setMode(MockInputMethod::Mode mode) +{ + m_mode = mode; +} + +void MockInputMethod::zwp_input_method_v1_deactivate(struct ::zwp_input_method_context_v1 *context) +{ + QCOMPARE(context, m_context); + zwp_input_method_context_v1_destroy(context); + m_context = nullptr; + + if (m_inputSurface) { + m_inputSurface->release(); + m_inputSurface->destroy(); + m_inputSurface.reset(); + m_inputMethodSurface.reset(); + } +} + +bool setupWaylandConnection(AdditionalWaylandInterfaces flags) +{ + if (s_waylandConnection) { + return false; + } + s_waylandConnection = Connection::setup(flags); + return bool(s_waylandConnection); +} + +void destroyWaylandConnection() +{ + s_waylandConnection.reset(); +} + +std::unique_ptr Connection::setup(AdditionalWaylandInterfaces flags) +{ + int sx[2]; + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sx) < 0) { + return nullptr; + } + KWin::waylandServer()->display()->createClient(sx[0]); + // setup connection + auto connection = std::make_unique(); + connection->connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(connection->connection, &KWayland::Client::ConnectionThread::connected); + if (!connectedSpy.isValid()) { + return nullptr; + } + connection->connection->setSocketFd(sx[1]); + + connection->thread = new QThread(kwinApp()); + connection->connection->moveToThread(connection->thread); + connection->thread->start(); + + connection->connection->initConnection(); + if (!connectedSpy.wait()) { + return nullptr; + } + + connection->queue = new KWayland::Client::EventQueue; + connection->queue->setup(connection->connection); + if (!connection->queue->isValid()) { + return nullptr; + } + + KWayland::Client::Registry *registry = new KWayland::Client::Registry; + connection->registry = registry; + registry->setEventQueue(connection->queue); + + QObject::connect(registry, &KWayland::Client::Registry::outputAnnounced, [c = connection.get()](quint32 name, quint32 version) { + KWayland::Client::Output *output = c->registry->createOutput(name, version, c->registry); + c->outputs << output; + QObject::connect(output, &KWayland::Client::Output::removed, [=]() { + output->deleteLater(); + c->outputs.removeOne(output); + }); + QObject::connect(output, &KWayland::Client::Output::destroyed, [=]() { + c->outputs.removeOne(output); + }); + }); + + QObject::connect(registry, &KWayland::Client::Registry::interfaceAnnounced, [c = connection.get(), flags](const QByteArray &interface, quint32 name, quint32 version) { + if (flags & AdditionalWaylandInterface::InputMethodV1) { + if (interface == QByteArrayLiteral("zwp_input_method_v1")) { + c->inputMethodV1 = new MockInputMethod(*c->registry, name, version); + } else if (interface == QByteArrayLiteral("zwp_input_panel_v1")) { + c->inputPanelV1 = new QtWayland::zwp_input_panel_v1(*c->registry, name, version); + } + } + if (flags & AdditionalWaylandInterface::LayerShellV1) { + if (interface == QByteArrayLiteral("zwlr_layer_shell_v1")) { + c->layerShellV1 = new LayerShellV1(); + c->layerShellV1->init(*c->registry, name, version); + } + } + if (flags & AdditionalWaylandInterface::TextInputManagerV3) { + // do something + if (interface == QByteArrayLiteral("zwp_text_input_manager_v3")) { + c->textInputManagerV3 = new TextInputManagerV3(); + c->textInputManagerV3->init(*c->registry, name, version); + } + } + if (interface == QByteArrayLiteral("xdg_wm_base")) { + c->xdgShell = new XdgShell(); + c->xdgShell->init(*c->registry, name, version); + } + if (flags & AdditionalWaylandInterface::XdgDecorationV1) { + if (interface == zxdg_decoration_manager_v1_interface.name) { + c->xdgDecorationManagerV1 = new XdgDecorationManagerV1(); + c->xdgDecorationManagerV1->init(*c->registry, name, version); + return; + } + } + if (flags & AdditionalWaylandInterface::IdleInhibitV1) { + if (interface == zwp_idle_inhibit_manager_v1_interface.name) { + c->idleInhibitManagerV1 = new IdleInhibitManagerV1(); + c->idleInhibitManagerV1->init(*c->registry, name, version); + return; + } + } + if (flags & AdditionalWaylandInterface::OutputDeviceV2) { + if (interface == kde_output_device_v2_interface.name) { + WaylandOutputDeviceV2 *device = new WaylandOutputDeviceV2(name); + device->init(*c->registry, name, version); + + c->outputDevicesV2 << device; + + QObject::connect(device, &WaylandOutputDeviceV2::destroyed, [=]() { + c->outputDevicesV2.removeOne(device); + device->deleteLater(); + }); + + QObject::connect(c->registry, &KWayland::Client::Registry::interfaceRemoved, device, [c, name, device](const quint32 &interfaceName) { + if (name == interfaceName) { + c->outputDevicesV2.removeOne(device); + device->deleteLater(); + } + }); + + return; + } + } + if (flags & AdditionalWaylandInterface::OutputManagementV2) { + if (interface == kde_output_management_v2_interface.name) { + c->outputManagementV2 = new WaylandOutputManagementV2(*c->registry, name, version); + return; + } + } + if (flags & AdditionalWaylandInterface::FractionalScaleManagerV1) { + if (interface == wp_fractional_scale_manager_v1_interface.name) { + c->fractionalScaleManagerV1 = new FractionalScaleManagerV1(); + c->fractionalScaleManagerV1->init(*c->registry, name, version); + return; + } + } + if (flags & AdditionalWaylandInterface::ScreencastingV1) { + if (interface == zkde_screencast_unstable_v1_interface.name) { + c->screencastingV1 = new ScreencastingV1(); + c->screencastingV1->init(*c->registry, name, version); + return; + } + } + if (flags & AdditionalWaylandInterface::ScreenEdgeV1) { + if (interface == kde_screen_edge_manager_v1_interface.name) { + c->screenEdgeManagerV1 = new ScreenEdgeManagerV1(); + c->screenEdgeManagerV1->init(*c->registry, name, version); + return; + } + } + if (flags & AdditionalWaylandInterface::CursorShapeV1) { + if (interface == wp_cursor_shape_manager_v1_interface.name) { + c->cursorShapeManagerV1 = new CursorShapeManagerV1(); + c->cursorShapeManagerV1->init(*c->registry, name, version); + } + } + if (flags & AdditionalWaylandInterface::FakeInput) { + if (interface == org_kde_kwin_fake_input_interface.name) { + c->fakeInput = new FakeInput(); + c->fakeInput->init(*c->registry, name, version); + } + } + if (flags & AdditionalWaylandInterface::SecurityContextManagerV1) { + if (interface == wp_security_context_manager_v1_interface.name) { + c->securityContextManagerV1 = new SecurityContextManagerV1(); + c->securityContextManagerV1->init(*c->registry, name, version); + } + } + if (flags & AdditionalWaylandInterface::XdgDialogV1) { + if (interface == xdg_wm_dialog_v1_interface.name) { + c->xdgWmDialogV1 = new XdgWmDialogV1(); + c->xdgWmDialogV1->init(*c->registry, name, version); + } + } + if (flags & AdditionalWaylandInterface::ColorManagement) { + if (interface == wp_color_manager_v1_interface.name) { + c->colorManager = std::make_unique(*c->registry, name, version); + } + } + if (flags & AdditionalWaylandInterface::FifoV1) { + if (interface == wp_fifo_manager_v1_interface.name) { + c->fifoManager = std::make_unique(*c->registry, name, version); + } + } + if (interface == wp_presentation_interface.name) { + c->presentationTime = std::make_unique(*c->registry, name, version); + } + if (flags & AdditionalWaylandInterface::XdgActivation) { + if (interface == xdg_activation_v1_interface.name) { + c->xdgActivation = std::make_unique(*c->registry, name, version); + } + } + if (flags & AdditionalWaylandInterface::XdgSessionV1) { + if (interface == xx_session_manager_v1_interface.name) { + c->sessionManager = std::make_unique(*c->registry, name, version); + } + } + if (flags & AdditionalWaylandInterface::WpTabletV2) { + if (interface == zwp_tablet_manager_v2_interface.name) { + c->tabletManager = std::make_unique(*c->registry, name, version); + } + } + if (flags & AdditionalWaylandInterface::KeyState && interface == org_kde_kwin_keystate_interface.name) { + c->keyState = std::make_unique(*c->registry, name, version); + } + if (flags & AdditionalWaylandInterface::WpPrimarySelectionV1) { + if (interface == zwp_primary_selection_device_manager_v1_interface.name) { + c->primarySelectionManager = std::make_unique(*c->registry, name, version); + } + } + if (flags & AdditionalWaylandInterface::XdgToplevelDragV1) { + if (interface == xdg_toplevel_drag_manager_v1_interface.name) { + c->toplevelDragManager = std::make_unique(*c->registry, name, version); + } + } + if (flags & AdditionalWaylandInterface::LinuxDmabuf && interface == zwp_linux_dmabuf_v1_interface.name) { + c->linuxDmabuf = std::make_unique(*c->registry, name, version); + } + if (flags & AdditionalWaylandInterface::ColorRepresentation && interface == wp_color_representation_manager_v1_interface.name) { + c->colorRepresentation = std::make_unique(*c->registry, name, version); + } + if (flags & AdditionalWaylandInterface::Viewporter && interface == wp_viewporter_interface.name) { + c->viewporter = std::make_unique(*c->registry, name, 1u); + } + if (flags.testFlag(AdditionalWaylandInterface::Seat) && interface == wl_seat_interface.name) { + c->kwinSeat = std::make_unique(*c->registry, name, version); + } + }); + + QSignalSpy allAnnounced(registry, &KWayland::Client::Registry::interfacesAnnounced); + if (!allAnnounced.isValid()) { + return nullptr; + } + registry->create(connection->connection); + if (!registry->isValid()) { + return nullptr; + } + registry->setup(); + if (!allAnnounced.wait()) { + return nullptr; + } + + connection->compositor = registry->createCompositor(registry->interface(KWayland::Client::Registry::Interface::Compositor).name, registry->interface(KWayland::Client::Registry::Interface::Compositor).version); + if (!connection->compositor->isValid()) { + return nullptr; + } + connection->subCompositor = registry->createSubCompositor(registry->interface(KWayland::Client::Registry::Interface::SubCompositor).name, registry->interface(KWayland::Client::Registry::Interface::SubCompositor).version); + if (!connection->subCompositor->isValid()) { + return nullptr; + } + connection->shm = registry->createShmPool(registry->interface(KWayland::Client::Registry::Interface::Shm).name, registry->interface(KWayland::Client::Registry::Interface::Shm).version); + if (!connection->shm->isValid()) { + return nullptr; + } + if (flags.testFlag(AdditionalWaylandInterface::Seat)) { + connection->seat = registry->createSeat(registry->interface(KWayland::Client::Registry::Interface::Seat).name, registry->interface(KWayland::Client::Registry::Interface::Seat).version); + if (!connection->seat->isValid()) { + return nullptr; + } + } + if (flags.testFlag(AdditionalWaylandInterface::DataDeviceManager)) { + connection->dataDeviceManager = registry->createDataDeviceManager(registry->interface(KWayland::Client::Registry::Interface::DataDeviceManager).name, registry->interface(KWayland::Client::Registry::Interface::DataDeviceManager).version); + if (!connection->dataDeviceManager->isValid()) { + return nullptr; + } + } + if (flags.testFlag(AdditionalWaylandInterface::ShadowManager)) { + connection->shadowManager = registry->createShadowManager(registry->interface(KWayland::Client::Registry::Interface::Shadow).name, + registry->interface(KWayland::Client::Registry::Interface::Shadow).version); + if (!connection->shadowManager->isValid()) { + return nullptr; + } + } + if (flags.testFlag(AdditionalWaylandInterface::PlasmaShell)) { + connection->plasmaShell = registry->createPlasmaShell(registry->interface(KWayland::Client::Registry::Interface::PlasmaShell).name, + registry->interface(KWayland::Client::Registry::Interface::PlasmaShell).version); + if (!connection->plasmaShell->isValid()) { + return nullptr; + } + } + if (flags.testFlag(AdditionalWaylandInterface::WindowManagement)) { + connection->windowManagement = registry->createPlasmaWindowManagement(registry->interface(KWayland::Client::Registry::Interface::PlasmaWindowManagement).name, + registry->interface(KWayland::Client::Registry::Interface::PlasmaWindowManagement).version); + if (!connection->windowManagement->isValid()) { + return nullptr; + } + } + if (flags.testFlag(AdditionalWaylandInterface::PointerConstraints)) { + connection->pointerConstraints = registry->createPointerConstraints(registry->interface(KWayland::Client::Registry::Interface::PointerConstraintsUnstableV1).name, + registry->interface(KWayland::Client::Registry::Interface::PointerConstraintsUnstableV1).version); + if (!connection->pointerConstraints->isValid()) { + return nullptr; + } + } + if (flags.testFlag(AdditionalWaylandInterface::AppMenu)) { + connection->appMenu = registry->createAppMenuManager(registry->interface(KWayland::Client::Registry::Interface::AppMenu).name, registry->interface(KWayland::Client::Registry::Interface::AppMenu).version); + if (!connection->appMenu->isValid()) { + return nullptr; + } + } + if (flags.testFlag(AdditionalWaylandInterface::TextInputManagerV2)) { + connection->textInputManager = registry->createTextInputManager(registry->interface(KWayland::Client::Registry::Interface::TextInputManagerUnstableV2).name, registry->interface(KWayland::Client::Registry::Interface::TextInputManagerUnstableV2).version); + if (!connection->textInputManager->isValid()) { + return nullptr; + } + } + + return connection; +} + +Connection::~Connection() +{ + delete compositor; + compositor = nullptr; + delete subCompositor; + subCompositor = nullptr; + delete windowManagement; + windowManagement = nullptr; + delete plasmaShell; + plasmaShell = nullptr; + delete seat; + seat = nullptr; + delete dataDeviceManager; + dataDeviceManager = nullptr; + delete pointerConstraints; + pointerConstraints = nullptr; + delete xdgShell; + xdgShell = nullptr; + delete shadowManager; + shadowManager = nullptr; + delete idleInhibitManagerV1; + idleInhibitManagerV1 = nullptr; + delete shm; + shm = nullptr; + delete registry; + registry = nullptr; + delete appMenu; + appMenu = nullptr; + delete xdgDecorationManagerV1; + xdgDecorationManagerV1 = nullptr; + delete textInputManager; + textInputManager = nullptr; + delete inputPanelV1; + inputPanelV1 = nullptr; + delete layerShellV1; + layerShellV1 = nullptr; + delete outputManagementV2; + outputManagementV2 = nullptr; + delete fractionalScaleManagerV1; + fractionalScaleManagerV1 = nullptr; + delete screencastingV1; + screencastingV1 = nullptr; + delete screenEdgeManagerV1; + screenEdgeManagerV1 = nullptr; + delete cursorShapeManagerV1; + cursorShapeManagerV1 = nullptr; + delete fakeInput; + fakeInput = nullptr; + delete securityContextManagerV1; + securityContextManagerV1 = nullptr; + delete xdgWmDialogV1; + xdgWmDialogV1 = nullptr; + colorManager.reset(); + fifoManager.reset(); + presentationTime.reset(); + xdgActivation.reset(); + sessionManager.reset(); + tabletManager.reset(); + keyState.reset(); + primarySelectionManager.reset(); + toplevelDragManager.reset(); + linuxDmabuf.reset(); + colorRepresentation.reset(); + viewporter.reset(); + kwinSeat.reset(); + + delete queue; // Must be destroyed last + queue = nullptr; + + if (thread) { + connection->deleteLater(); + thread->quit(); + thread->wait(); + delete thread; + thread = nullptr; + connection = nullptr; + } + outputs.clear(); + outputDevicesV2.clear(); +} + +class WaylandSyncPoint : public QObject +{ + Q_OBJECT + +public: + explicit WaylandSyncPoint(KWayland::Client::ConnectionThread *connection, KWayland::Client::EventQueue *eventQueue) + { + static const wl_callback_listener listener = { + .done = [](void *data, wl_callback *callback, uint32_t callback_data) { + auto syncPoint = static_cast(data); + Q_EMIT syncPoint->done(); + }, + }; + + m_callback = wl_display_sync(connection->display()); + eventQueue->addProxy(m_callback); + wl_callback_add_listener(m_callback, &listener, this); + } + + ~WaylandSyncPoint() override + { + wl_callback_destroy(m_callback); + } + +Q_SIGNALS: + void done(); + +private: + wl_callback *m_callback; +}; + +bool Connection::sync() +{ + WaylandSyncPoint syncPoint(connection, queue); + QSignalSpy doneSpy(&syncPoint, &WaylandSyncPoint::done); + return doneSpy.wait(); +} + +KWayland::Client::ConnectionThread *waylandConnection() +{ + return s_waylandConnection->connection; +} + +KWayland::Client::Compositor *waylandCompositor() +{ + return s_waylandConnection->compositor; +} + +KWayland::Client::SubCompositor *waylandSubCompositor() +{ + return s_waylandConnection->subCompositor; +} + +KWayland::Client::ShadowManager *waylandShadowManager() +{ + return s_waylandConnection->shadowManager; +} + +KWayland::Client::ShmPool *waylandShmPool() +{ + return s_waylandConnection->shm; +} + +KWayland::Client::Seat *waylandSeat() +{ + return s_waylandConnection->seat; +} + +KWayland::Client::DataDeviceManager *waylandDataDeviceManager() +{ + return s_waylandConnection->dataDeviceManager; +} + +KWayland::Client::PlasmaShell *waylandPlasmaShell() +{ + return s_waylandConnection->plasmaShell; +} + +KWayland::Client::PlasmaWindowManagement *waylandWindowManagement() +{ + return s_waylandConnection->windowManagement; +} + +KWayland::Client::PointerConstraints *waylandPointerConstraints() +{ + return s_waylandConnection->pointerConstraints; +} + +KWayland::Client::AppMenuManager *waylandAppMenuManager() +{ + return s_waylandConnection->appMenu; +} + +KWin::Test::WaylandOutputManagementV2 *waylandOutputManagementV2() +{ + return s_waylandConnection->outputManagementV2; +} + +KWayland::Client::TextInputManager *waylandTextInputManager() +{ + return s_waylandConnection->textInputManager; +} + +TextInputManagerV3 *waylandTextInputManagerV3() +{ + return s_waylandConnection->textInputManagerV3; +} + +QList waylandOutputs() +{ + return s_waylandConnection->outputs; +} + +KWayland::Client::Output *waylandOutput(const QString &name) +{ + for (KWayland::Client::Output *output : std::as_const(s_waylandConnection->outputs)) { + if (output->name() == name) { + return output; + } + } + return nullptr; +} + +ScreencastingV1 *screencasting() +{ + return s_waylandConnection->screencastingV1; +} + +QList waylandOutputDevicesV2() +{ + return s_waylandConnection->outputDevicesV2; +} + +FakeInput *waylandFakeInput() +{ + return s_waylandConnection->fakeInput; +} + +SecurityContextManagerV1 *waylandSecurityContextManagerV1() +{ + return s_waylandConnection->securityContextManagerV1; +} + +ColorManagerV1 *colorManager() +{ + return s_waylandConnection->colorManager.get(); +} + +FifoManagerV1 *fifoManager() +{ + return s_waylandConnection->fifoManager.get(); +} + +PresentationTime *presentationTime() +{ + return s_waylandConnection->presentationTime.get(); +} + +XdgActivation *xdgActivation() +{ + return s_waylandConnection->xdgActivation.get(); +} + +WpTabletManagerV2 *tabletManager() +{ + return s_waylandConnection->tabletManager.get(); +} + +KeyStateV1 *keyState() +{ + return s_waylandConnection->keyState.get(); +} + +WpPrimarySelectionDeviceManagerV1 *primarySelectionManager() +{ + return s_waylandConnection->primarySelectionManager.get(); +} + +XdgToplevelDragManagerV1 *toplevelDragManager() +{ + return s_waylandConnection->toplevelDragManager.get(); +} + +WaylandClient::LinuxDmabufV1 *linuxDmabuf() +{ + return s_waylandConnection->linuxDmabuf.get(); +} + +ColorRepresentationV1 *colorRepresentation() +{ + return s_waylandConnection->colorRepresentation.get(); +} + +WaylandClient::Viewporter *viewporter() +{ + return s_waylandConnection->viewporter.get(); +} + +bool waitForWaylandSurface(Window *window) +{ + if (window->surface()) { + return true; + } + QSignalSpy surfaceChangedSpy(window, &Window::surfaceChanged); + return surfaceChangedSpy.wait(); +} + +bool waitForWaylandPointer() +{ + if (!s_waylandConnection->seat) { + return false; + } + return waitForWaylandPointer(s_waylandConnection->seat); +} + +bool waitForWaylandPointer(KWayland::Client::Seat *seat) +{ + QSignalSpy hasPointerSpy(seat, &KWayland::Client::Seat::hasPointerChanged); + return hasPointerSpy.wait(); +} + +bool waitForWaylandTouch() +{ + if (!s_waylandConnection->seat) { + return false; + } + return waitForWaylandTouch(s_waylandConnection->seat); +} + +bool waitForWaylandTouch(KWayland::Client::Seat *seat) +{ + QSignalSpy hasTouchSpy(seat, &KWayland::Client::Seat::hasTouchChanged); + return hasTouchSpy.wait(); +} + +bool waitForWaylandKeyboard() +{ + if (!s_waylandConnection->seat) { + return false; + } + return waitForWaylandKeyboard(s_waylandConnection->seat); +} + +bool waitForWaylandKeyboard(KWayland::Client::Seat *seat) +{ + QSignalSpy hasKeyboardSpy(seat, &KWayland::Client::Seat::hasKeyboardChanged); + return hasKeyboardSpy.wait(); +} + +bool waitForWaylandTabletTool(Test::WpTabletToolV2 *tool) +{ + if (tool->ready()) { + return true; + } + QSignalSpy doneSpy(tool, &WpTabletToolV2::done); + return doneSpy.wait(); +} + +void render(KWayland::Client::Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format) +{ + render(s_waylandConnection->shm, surface, size, color, format); +} + +void render(KWayland::Client::ShmPool *shm, KWayland::Client::Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format) +{ + QImage img(size, format); + img.fill(color); + render(surface, img); +} + +void render(KWayland::Client::Surface *surface, const QImage &img) +{ + render(s_waylandConnection->shm, surface, img); +} + +void render(KWayland::Client::ShmPool *shm, KWayland::Client::Surface *surface, const QImage &img) +{ + surface->attachBuffer(shm->createBuffer(img)); + surface->damage(QRect(QPoint(0, 0), img.size())); + surface->commit(KWayland::Client::Surface::CommitFlag::None); +} + +Window *waitForWaylandWindowShown(int timeout) +{ + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + if (!windowAddedSpy.isValid()) { + return nullptr; + } + if (!windowAddedSpy.wait(timeout)) { + return nullptr; + } + return windowAddedSpy.first().first().value(); +} + +Window *renderAndWaitForShown(KWayland::Client::Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format, int timeout) +{ + return renderAndWaitForShown(s_waylandConnection->shm, surface, size, color, format, timeout); +} + +Window *renderAndWaitForShown(KWayland::Client::ShmPool *shm, KWayland::Client::Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format, int timeout) +{ + QImage img(size, format); + img.fill(color); + return renderAndWaitForShown(shm, surface, img, timeout); +} + +Window *renderAndWaitForShown(KWayland::Client::Surface *surface, const QImage &img, int timeout) +{ + return renderAndWaitForShown(s_waylandConnection->shm, surface, img, timeout); +} + +Window *renderAndWaitForShown(KWayland::Client::ShmPool *shm, KWayland::Client::Surface *surface, const QImage &img, int timeout) +{ + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + if (!windowAddedSpy.isValid()) { + return nullptr; + } + render(shm, surface, img); + flushWaylandConnection(); + if (!windowAddedSpy.wait(timeout)) { + return nullptr; + } + return windowAddedSpy.first().first().value(); +} + +void flushWaylandConnection() +{ + if (s_waylandConnection) { + s_waylandConnection->connection->flush(); + } +} + +bool waylandSync() +{ + return s_waylandConnection->sync(); +} + +std::unique_ptr createSurface() +{ + if (!s_waylandConnection->compositor) { + return nullptr; + } + return createSurface(s_waylandConnection->compositor); +} + +std::unique_ptr createSurface(KWayland::Client::Compositor *compositor) +{ + std::unique_ptr s{compositor->createSurface()}; + return s->isValid() ? std::move(s) : nullptr; +} + +std::unique_ptr createSubSurface(KWayland::Client::Surface *surface, KWayland::Client::Surface *parentSurface) +{ + if (!s_waylandConnection->subCompositor) { + return nullptr; + } + std::unique_ptr s(s_waylandConnection->subCompositor->createSubSurface(surface, parentSurface)); + if (!s->isValid()) { + return nullptr; + } + return s; +} + +std::unique_ptr createLayerSurfaceV1(KWayland::Client::Surface *surface, const QString &scope, KWayland::Client::Output *output, LayerShellV1::layer layer) +{ + LayerShellV1 *shell = s_waylandConnection->layerShellV1; + if (!shell) { + qWarning() << "Could not create a layer surface because the layer shell global is not bound"; + return nullptr; + } + + struct ::wl_output *nativeOutput = nullptr; + if (output) { + nativeOutput = *output; + } + + auto shellSurface = std::make_unique(); + shellSurface->init(shell->get_layer_surface(*surface, nativeOutput, layer, scope)); + + return shellSurface; +} + +std::unique_ptr createInputPanelSurfaceV1(KWayland::Client::Surface *surface, KWayland::Client::Output *output, MockInputMethod::Mode mode) +{ + if (!s_waylandConnection->inputPanelV1) { + qWarning() << "Unable to create the input panel surface. The interface input_panel global is not bound"; + return nullptr; + } + auto s = std::make_unique(s_waylandConnection->inputPanelV1->get_input_panel_surface(*surface)); + if (!s->isInitialized()) { + return nullptr; + } + + switch (mode) { + case MockInputMethod::Mode::TopLevel: + s->set_toplevel(output->output(), QtWayland::zwp_input_panel_surface_v1::position_center_bottom); + break; + case MockInputMethod::Mode::Overlay: + s->set_overlay_panel(); + break; + } + + return s; +} + +std::unique_ptr createFractionalScaleV1(KWayland::Client::Surface *surface) +{ + if (!s_waylandConnection->fractionalScaleManagerV1) { + qWarning() << "Unable to create fractional scale surface. The global is not bound"; + return nullptr; + } + auto scale = std::make_unique(); + scale->init(s_waylandConnection->fractionalScaleManagerV1->get_fractional_scale(*surface)); + + return scale; +} + +static void waitForConfigured(XdgSurface *shellSurface) +{ + QSignalSpy surfaceConfigureRequestedSpy(shellSurface, &XdgSurface::configureRequested); + + shellSurface->surface()->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + shellSurface->ack_configure(surfaceConfigureRequestedSpy.last().first().toUInt()); +} + +std::unique_ptr createXdgToplevelSurface(KWayland::Client::Surface *surface) +{ + return createXdgToplevelSurface(surface, CreationSetup::CreateAndConfigure); +} + +std::unique_ptr createXdgToplevelSurface(XdgShell *shell, KWayland::Client::Surface *surface) +{ + return createXdgToplevelSurface(shell, surface, CreationSetup::CreateAndConfigure); +} + +std::unique_ptr createXdgToplevelSurface(KWayland::Client::Surface *surface, CreationSetup configureMode) +{ + XdgShell *shell = s_waylandConnection->xdgShell; + + if (!shell) { + qWarning() << "Could not create an xdg_toplevel surface because xdg_wm_base global is not bound"; + return nullptr; + } + + return createXdgToplevelSurface(shell, surface, configureMode); +} + +std::unique_ptr createXdgToplevelSurface(XdgShell *shell, KWayland::Client::Surface *surface, CreationSetup configureMode) +{ + XdgSurface *xdgSurface = new XdgSurface(shell, surface); + std::unique_ptr xdgToplevel = std::make_unique(xdgSurface); + + if (configureMode == CreationSetup::CreateAndConfigure) { + waitForConfigured(xdgSurface); + } + + return xdgToplevel; +} + +std::unique_ptr createXdgToplevelSurface(KWayland::Client::Surface *surface, std::function setup) +{ + XdgShell *shell = s_waylandConnection->xdgShell; + + if (!shell) { + qWarning() << "Could not create an xdg_toplevel surface because xdg_wm_base global is not bound"; + return nullptr; + } + + return createXdgToplevelSurface(shell, surface, setup); +} + +std::unique_ptr createXdgToplevelSurface(XdgShell *shell, KWayland::Client::Surface *surface, std::function setup) +{ + XdgSurface *xdgSurface = new XdgSurface(shell, surface); + std::unique_ptr xdgToplevel = std::make_unique(xdgSurface); + + if (setup) { + setup(xdgToplevel.get()); + } + waitForConfigured(xdgSurface); + + return xdgToplevel; +} + +std::unique_ptr createXdgPositioner() +{ + XdgShell *shell = s_waylandConnection->xdgShell; + + if (!shell) { + qWarning() << "Could not create an xdg_positioner object because xdg_wm_base global is not bound"; + return nullptr; + } + + return std::make_unique(shell); +} + +std::unique_ptr createXdgPopupSurface(KWayland::Client::Surface *surface, XdgSurface *parentSurface, XdgPositioner *positioner, CreationSetup configureMode) +{ + XdgShell *shell = s_waylandConnection->xdgShell; + + if (!shell) { + qWarning() << "Could not create an xdg_popup surface because xdg_wm_base global is not bound"; + return nullptr; + } + + XdgSurface *xdgSurface = new XdgSurface(shell, surface); + std::unique_ptr xdgPopup = std::make_unique(xdgSurface, parentSurface, positioner); + + if (configureMode == CreationSetup::CreateAndConfigure) { + waitForConfigured(xdgSurface); + } + + return xdgPopup; +} + +std::unique_ptr createXdgToplevelDecorationV1(XdgToplevel *toplevel) +{ + XdgDecorationManagerV1 *manager = s_waylandConnection->xdgDecorationManagerV1; + + if (!manager) { + qWarning() << "Could not create an xdg_toplevel_decoration_v1 because xdg_decoration_manager_v1 global is not bound"; + return nullptr; + } + + return std::make_unique(manager, toplevel); +} + +std::unique_ptr createIdleInhibitorV1(KWayland::Client::Surface *surface) +{ + IdleInhibitManagerV1 *manager = s_waylandConnection->idleInhibitManagerV1; + if (!manager) { + qWarning() << "Could not create an idle_inhibitor_v1 because idle_inhibit_manager_v1 global is not bound"; + return nullptr; + } + + return std::make_unique(manager, surface); +} + +std::unique_ptr createAutoHideScreenEdgeV1(KWayland::Client::Surface *surface, uint32_t border) +{ + ScreenEdgeManagerV1 *manager = s_waylandConnection->screenEdgeManagerV1; + if (!manager) { + qWarning() << "Could not create an kde_auto_hide_screen_edge_v1 because kde_screen_edge_manager_v1 global is not bound"; + return nullptr; + } + + return std::make_unique(manager, surface, border); +} + +std::unique_ptr createCursorShapeDeviceV1(KWayland::Client::Pointer *pointer) +{ + CursorShapeManagerV1 *manager = s_waylandConnection->cursorShapeManagerV1; + if (!manager) { + qWarning() << "Could not create a wp_cursor_shape_device_v1 because wp_cursor_shape_manager_v1 global is not bound"; + return nullptr; + } + + return std::make_unique(manager, pointer); +} + +std::unique_ptr createXdgDialogV1(XdgToplevel *toplevel) +{ + XdgWmDialogV1 *wm = s_waylandConnection->xdgWmDialogV1; + if (!wm) { + qWarning() << "Could not create a xdg_dialog_v1 because xdg_wm_dialog_v1 global is not bound"; + return nullptr; + } + return std::make_unique(wm, toplevel); +} + +std::unique_ptr createXdgSessionV1(XdgSessionManagerV1::reason reason, const QString &sessionId) +{ + XdgSessionManagerV1 *manager = s_waylandConnection->sessionManager.get(); + if (!manager) { + qWarning() << "Could not create a xx_session_v1 because xx_session_manager_v1 global is not bound"; + return nullptr; + } + return createXdgSessionV1(manager, reason, sessionId); +} + +std::unique_ptr createXdgSessionV1(XdgSessionManagerV1 *manager, XdgSessionManagerV1::reason reason, const QString &sessionId) +{ + return std::make_unique(manager->get_session(reason, sessionId)); +} + +bool waitForWindowClosed(Window *window) +{ + QSignalSpy closedSpy(window, &Window::closed); + if (!closedSpy.isValid()) { + return false; + } + return closedSpy.wait(); +} + +#if KWIN_BUILD_SCREENLOCKER +bool lockScreen() +{ + if (waylandServer()->isScreenLocked()) { + return false; + } + QSignalSpy lockStateChangedSpy(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged); + if (!lockStateChangedSpy.isValid()) { + return false; + } + ScreenLocker::KSldApp::self()->lock(ScreenLocker::EstablishLock::Immediate); + if (lockStateChangedSpy.count() != 1) { + return false; + } + if (!waylandServer()->isScreenLocked()) { + return false; + } + if (ScreenLocker::KSldApp::self()->lockState() != ScreenLocker::KSldApp::Locked) { + QSignalSpy lockedSpy(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::locked); + if (!lockedSpy.isValid()) { + return false; + } + if (!lockedSpy.wait()) { + return false; + } + } + return true; +} + +bool unlockScreen() +{ + QSignalSpy lockStateChangedSpy(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged); + if (!lockStateChangedSpy.isValid()) { + return false; + } + using namespace ScreenLocker; + const auto children = KSldApp::self()->children(); + for (auto it = children.begin(); it != children.end(); ++it) { + if (qstrcmp((*it)->metaObject()->className(), "LogindIntegration") != 0) { + continue; + } + QMetaObject::invokeMethod(*it, "requestUnlock"); + break; + } + if (waylandServer()->isScreenLocked()) { + lockStateChangedSpy.wait(); + } + if (waylandServer()->isScreenLocked()) { + return true; + } + if (ScreenLocker::KSldApp::self()->lockState() == ScreenLocker::KSldApp::Locked) { + QSignalSpy lockedSpy(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::unlocked); + if (!lockedSpy.isValid()) { + return false; + } + if (!lockedSpy.wait()) { + return false; + } + } + return true; +} +#endif // KWIN_BUILD_LOCKSCREEN + +static bool haveDrmNode(int type) +{ +#if !HAVE_LIBDRM_FAUX +#if defined(Q_OS_LINUX) + // Workaround for libdrm being unaware of faux bus. + if (qEnvironmentVariableIsSet("CI")) { + return true; + } +#endif +#endif + + const int deviceCount = drmGetDevices2(0, nullptr, 0); + if (deviceCount <= 0) { + return false; + } + + QList devices(deviceCount); + if (drmGetDevices2(0, devices.data(), devices.size()) < 0) { + return false; + } + auto deviceCleanup = qScopeGuard([&devices]() { + drmFreeDevices(devices.data(), devices.size()); + }); + + return std::any_of(devices.constBegin(), devices.constEnd(), [type](drmDevice *device) { + return device->available_nodes & (1 << type); + }); +} + +bool renderNodeAvailable() +{ + return haveDrmNode(DRM_NODE_RENDER); +} + +bool primaryNodeAvailable() +{ + return haveDrmNode(DRM_NODE_PRIMARY); +} + +#if KWIN_BUILD_X11 +void XcbConnectionDeleter::operator()(xcb_connection_t *pointer) +{ + xcb_disconnect(pointer); +}; + +Test::XcbConnectionPtr createX11Connection() +{ + QFutureWatcher watcher; + QEventLoop e; + e.connect(&watcher, &QFutureWatcher::finished, &e, &QEventLoop::quit); + QFuture future = QtConcurrent::run([]() { + return xcb_connect(nullptr, nullptr); + }); + watcher.setFuture(future); + e.exec(); + return Test::XcbConnectionPtr(future.result()); +} + +void applyMotifHints(xcb_connection_t *connection, xcb_window_t window, const MotifHints &hints) +{ + if (hints.flags) { + xcb_change_property(connection, XCB_PROP_MODE_REPLACE, window, atoms->motif_wm_hints, atoms->motif_wm_hints, 32, 5, &hints); + } else { + xcb_delete_property(connection, window, atoms->motif_wm_hints); + } +} +#endif + +WaylandOutputManagementV2::WaylandOutputManagementV2(struct ::wl_registry *registry, int id, int version) + : QObject() + , QtWayland::kde_output_management_v2() +{ + init(registry, id, version); +} + +WaylandOutputConfigurationV2 *WaylandOutputManagementV2::createConfiguration() +{ + return new WaylandOutputConfigurationV2(create_configuration()); +} + +WaylandOutputConfigurationV2::WaylandOutputConfigurationV2(struct ::kde_output_configuration_v2 *object) + : QObject() + , QtWayland::kde_output_configuration_v2() +{ + init(object); +} + +void WaylandOutputConfigurationV2::kde_output_configuration_v2_applied() +{ + Q_EMIT applied(); +} +void WaylandOutputConfigurationV2::kde_output_configuration_v2_failed() +{ + Q_EMIT failed(); +} + +WaylandOutputDeviceV2Mode::WaylandOutputDeviceV2Mode(struct ::kde_output_device_mode_v2 *object) + : QtWayland::kde_output_device_mode_v2(object) +{ +} + +WaylandOutputDeviceV2Mode::~WaylandOutputDeviceV2Mode() +{ + kde_output_device_mode_v2_destroy(object()); +} + +void WaylandOutputDeviceV2Mode::kde_output_device_mode_v2_size(int32_t width, int32_t height) +{ + m_size = QSize(width, height); +} + +void WaylandOutputDeviceV2Mode::kde_output_device_mode_v2_refresh(int32_t refresh) +{ + m_refreshRate = refresh; +} + +void WaylandOutputDeviceV2Mode::kde_output_device_mode_v2_preferred() +{ + m_preferred = true; +} + +void WaylandOutputDeviceV2Mode::kde_output_device_mode_v2_removed() +{ + Q_EMIT removed(); +} + +int WaylandOutputDeviceV2Mode::refreshRate() const +{ + return m_refreshRate; +} + +QSize WaylandOutputDeviceV2Mode::size() const +{ + return m_size; +} + +bool WaylandOutputDeviceV2Mode::preferred() const +{ + return m_preferred; +} + +bool WaylandOutputDeviceV2Mode::operator==(const WaylandOutputDeviceV2Mode &other) const +{ + return m_size == other.m_size && m_refreshRate == other.m_refreshRate && m_preferred == other.m_preferred; +} + +WaylandOutputDeviceV2Mode *WaylandOutputDeviceV2Mode::get(struct ::kde_output_device_mode_v2 *object) +{ + auto mode = QtWayland::kde_output_device_mode_v2::fromObject(object); + return static_cast(mode); +} + +WaylandOutputDeviceV2::WaylandOutputDeviceV2(int id) + : QObject() + , kde_output_device_v2() + , m_id(id) +{ +} + +WaylandOutputDeviceV2::~WaylandOutputDeviceV2() +{ + qDeleteAll(m_modes); + + kde_output_device_v2_destroy(object()); +} + +void WaylandOutputDeviceV2::kde_output_device_v2_geometry(int32_t x, + int32_t y, + int32_t physical_width, + int32_t physical_height, + int32_t subpixel, + const QString &make, + const QString &model, + int32_t transform) +{ + m_pos = QPoint(x, y); + m_physicalSize = QSize(physical_width, physical_height); + m_subpixel = subpixel; + m_manufacturer = make; + m_model = model; + m_transform = transform; +} + +void WaylandOutputDeviceV2::kde_output_device_v2_current_mode(struct ::kde_output_device_mode_v2 *mode) +{ + auto m = WaylandOutputDeviceV2Mode::get(mode); + + if (*m == *m_mode) { + // unchanged + return; + } + m_mode = m; +} + +void WaylandOutputDeviceV2::kde_output_device_v2_mode(struct ::kde_output_device_mode_v2 *mode) +{ + WaylandOutputDeviceV2Mode *m = new WaylandOutputDeviceV2Mode(mode); + // last mode sent is the current one + m_mode = m; + m_modes.append(m); + + connect(m, &WaylandOutputDeviceV2Mode::removed, this, [this, m]() { + m_modes.removeOne(m); + if (m_mode == m) { + if (!m_modes.isEmpty()) { + m_mode = m_modes.first(); + } else { + // was last mode + qFatal("KWaylandBackend: no output modes available anymore, this seems like a compositor bug"); + } + } + + delete m; + }); +} + +QString WaylandOutputDeviceV2::modeId() const +{ + return QString::number(m_modes.indexOf(m_mode)); +} + +WaylandOutputDeviceV2Mode *WaylandOutputDeviceV2::deviceModeFromId(const int modeId) const +{ + return m_modes.at(modeId); +} + +QString WaylandOutputDeviceV2::modeName(const WaylandOutputDeviceV2Mode *m) const +{ + return QString::number(m->size().width()) + QLatin1Char('x') + QString::number(m->size().height()) + QLatin1Char('@') + + QString::number(qRound(m->refreshRate() / 1000.0)); +} + +QString WaylandOutputDeviceV2::name() const +{ + return QStringLiteral("%1 %2").arg(m_manufacturer, m_model); +} + +QDebug operator<<(QDebug dbg, const WaylandOutputDeviceV2 *output) +{ + dbg << "WaylandOutput(Id:" << output->id() << ", Name:" << QString(output->manufacturer() + QLatin1Char(' ') + output->model()) << ")"; + return dbg; +} + +void WaylandOutputDeviceV2::kde_output_device_v2_done() +{ + Q_EMIT done(); +} + +void WaylandOutputDeviceV2::kde_output_device_v2_scale(wl_fixed_t factor) +{ + m_factor = wl_fixed_to_double(factor); +} + +void WaylandOutputDeviceV2::kde_output_device_v2_edid(const QString &edid) +{ + m_edid = QByteArray::fromBase64(edid.toUtf8()); +} + +void WaylandOutputDeviceV2::kde_output_device_v2_enabled(int32_t enabled) +{ + if (m_enabled != enabled) { + m_enabled = enabled; + Q_EMIT enabledChanged(); + } +} + +void WaylandOutputDeviceV2::kde_output_device_v2_uuid(const QString &uuid) +{ + m_uuid = uuid; +} + +void WaylandOutputDeviceV2::kde_output_device_v2_serial_number(const QString &serialNumber) +{ + m_serialNumber = serialNumber; +} + +void WaylandOutputDeviceV2::kde_output_device_v2_eisa_id(const QString &eisaId) +{ + m_eisaId = eisaId; +} + +void WaylandOutputDeviceV2::kde_output_device_v2_capabilities(uint32_t flags) +{ + m_flags = flags; +} + +void WaylandOutputDeviceV2::kde_output_device_v2_overscan(uint32_t overscan) +{ + m_overscan = overscan; +} + +void WaylandOutputDeviceV2::kde_output_device_v2_vrr_policy(uint32_t vrr_policy) +{ + m_vrr_policy = vrr_policy; +} + +void WaylandOutputDeviceV2::kde_output_device_v2_rgb_range(uint32_t rgb_range) +{ + m_rgbRange = rgb_range; +} + +QByteArray WaylandOutputDeviceV2::edid() const +{ + return m_edid; +} + +bool WaylandOutputDeviceV2::enabled() const +{ + return m_enabled; +} + +int WaylandOutputDeviceV2::id() const +{ + return m_id; +} + +qreal WaylandOutputDeviceV2::scale() const +{ + return m_factor; +} + +QString WaylandOutputDeviceV2::manufacturer() const +{ + return m_manufacturer; +} + +QString WaylandOutputDeviceV2::model() const +{ + return m_model; +} + +QPoint WaylandOutputDeviceV2::globalPosition() const +{ + return m_pos; +} + +QSize WaylandOutputDeviceV2::pixelSize() const +{ + return m_mode->size(); +} + +int WaylandOutputDeviceV2::refreshRate() const +{ + return m_mode->refreshRate(); +} + +uint32_t WaylandOutputDeviceV2::vrrPolicy() const +{ + return m_vrr_policy; +} + +uint32_t WaylandOutputDeviceV2::overscan() const +{ + return m_overscan; +} + +uint32_t WaylandOutputDeviceV2::capabilities() const +{ + return m_flags; +} + +uint32_t WaylandOutputDeviceV2::rgbRange() const +{ + return m_rgbRange; +} + +VirtualInputDeviceTabletTool::VirtualInputDeviceTabletTool(QObject *parent) + : InputDeviceTabletTool(parent) +{ +} + +void VirtualInputDeviceTabletTool::setSerialId(quint64 serialId) +{ + m_serialId = serialId; +} + +void VirtualInputDeviceTabletTool::setUniqueId(quint64 uniqueId) +{ + m_uniqueId = uniqueId; +} + +void VirtualInputDeviceTabletTool::setType(Type type) +{ + m_type = type; +} + +void VirtualInputDeviceTabletTool::setCapabilities(const QList &capabilities) +{ + m_capabilities = capabilities; +} + +quint64 VirtualInputDeviceTabletTool::serialId() const +{ + return m_serialId; +} + +quint64 VirtualInputDeviceTabletTool::uniqueId() const +{ + return m_uniqueId; +} + +VirtualInputDeviceTabletTool::Type VirtualInputDeviceTabletTool::type() const +{ + return m_type; +} + +QList VirtualInputDeviceTabletTool::capabilities() const +{ + return m_capabilities; +} + +VirtualInputDevice::VirtualInputDevice(QObject *parent) + : InputDevice(parent) +{ +} + +void VirtualInputDevice::setPointer(bool set) +{ + m_pointer = set; +} + +void VirtualInputDevice::setKeyboard(bool set) +{ + m_keyboard = set; +} + +void VirtualInputDevice::setTouch(bool set) +{ + m_touch = set; +} + +void VirtualInputDevice::setLidSwitch(bool set) +{ + m_lidSwitch = set; +} + +void VirtualInputDevice::setTabletPad(bool set) +{ + m_tabletPad = set; +} + +void VirtualInputDevice::setTabletTool(bool set) +{ + m_tabletTool = set; +} + +void VirtualInputDevice::setName(const QString &name) +{ + m_name = name; +} + +void VirtualInputDevice::setGroup(uintptr_t group) +{ + m_group = reinterpret_cast(group); +} + +QString VirtualInputDevice::name() const +{ + return m_name; +} + +void *VirtualInputDevice::group() const +{ + return m_group; +} + +bool VirtualInputDevice::isEnabled() const +{ + return true; +} + +void VirtualInputDevice::setEnabled(bool enabled) +{ +} + +bool VirtualInputDevice::isKeyboard() const +{ + return m_keyboard; +} + +bool VirtualInputDevice::isPointer() const +{ + return m_pointer; +} + +bool VirtualInputDevice::isTouchpad() const +{ + return false; +} + +bool VirtualInputDevice::isTouch() const +{ + return m_touch; +} + +bool VirtualInputDevice::isTabletTool() const +{ + return m_tabletTool; +} + +bool VirtualInputDevice::isTabletPad() const +{ + return m_tabletPad; +} + +bool VirtualInputDevice::isTabletModeSwitch() const +{ + return false; +} + +bool VirtualInputDevice::isLidSwitch() const +{ + return m_lidSwitch; +} + +ColorManagerV1::ColorManagerV1(::wl_registry *registry, uint32_t id, int version) + : QtWayland::wp_color_manager_v1(registry, id, version) +{ +} + +ColorManagerV1::~ColorManagerV1() +{ + wp_color_manager_v1_destroy(object()); +} + +ColorRepresentationV1::ColorRepresentationV1(::wl_registry *registry, uint32_t id, int version) + : QtWayland::wp_color_representation_manager_v1(registry, id, version) +{ +} + +ColorRepresentationV1::~ColorRepresentationV1() +{ + destroy(); +} + +ColorRepresentationSurfaceV1::ColorRepresentationSurfaceV1(::wp_color_representation_surface_v1 *object) + : QtWayland::wp_color_representation_surface_v1(object) +{ +} + +ColorRepresentationSurfaceV1::~ColorRepresentationSurfaceV1() +{ + destroy(); +} + +FifoManagerV1::FifoManagerV1(::wl_registry *registry, uint32_t id, int version) + : QtWayland::wp_fifo_manager_v1(registry, id, version) +{ +} + +FifoManagerV1::~FifoManagerV1() +{ + wp_fifo_manager_v1_destroy(object()); +} + +PresentationTime::PresentationTime(::wl_registry *registry, uint32_t id, int version) + : QtWayland::wp_presentation(registry, id, version) +{ +} + +PresentationTime::~PresentationTime() +{ + wp_presentation_destroy(object()); +} + +WpPresentationFeedback::WpPresentationFeedback(struct ::wp_presentation_feedback *obj) + : QtWayland::wp_presentation_feedback(obj) +{ +} + +WpPresentationFeedback::~WpPresentationFeedback() +{ + wp_presentation_feedback_destroy(object()); +} + +void WpPresentationFeedback::wp_presentation_feedback_presented(uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec, uint32_t refresh, uint32_t seq_hi, uint32_t seq_lo, uint32_t flags) +{ + const std::chrono::nanoseconds timestamp = std::chrono::seconds((uint64_t(tv_sec_hi) << 32) | tv_sec_lo) + std::chrono::nanoseconds(tv_nsec); + Q_EMIT presented(timestamp, std::chrono::nanoseconds(refresh)); +} + +void WpPresentationFeedback::wp_presentation_feedback_discarded() +{ + Q_EMIT discarded(); +} + +XdgActivationToken::XdgActivationToken(::xdg_activation_token_v1 *object) + : QtWayland::xdg_activation_token_v1(object) +{ +} + +XdgActivationToken::~XdgActivationToken() +{ + destroy(); +} + +QString XdgActivationToken::commitAndWait() +{ + QSignalSpy received(this, &XdgActivationToken::tokenReceived); + commit(); + received.wait(); + return m_token; +} + +void XdgActivationToken::xdg_activation_token_v1_done(const QString &token) +{ + m_token = token; + Q_EMIT tokenReceived(); +} + +XdgActivation::XdgActivation(::wl_registry *registry, uint32_t id, int version) + : QtWayland::xdg_activation_v1(registry, id, version) +{ +} + +XdgActivation::~XdgActivation() +{ + destroy(); +} + +std::unique_ptr XdgActivation::createToken() +{ + return std::make_unique(get_activation_token()); +} + +XdgToplevelSessionV1::XdgToplevelSessionV1(::xx_toplevel_session_v1 *session) + : QtWayland::xx_toplevel_session_v1(session) +{ +} + +XdgToplevelSessionV1::~XdgToplevelSessionV1() +{ + destroy(); +} + +void XdgToplevelSessionV1::xx_toplevel_session_v1_restored(struct ::xdg_toplevel *surface) +{ + Q_EMIT restored(); +} + +XdgSessionV1::XdgSessionV1(::xx_session_v1 *session) + : QtWayland::xx_session_v1(session) +{ +} + +XdgSessionV1::~XdgSessionV1() +{ + destroy(); +} + +std::unique_ptr XdgSessionV1::add(XdgToplevel *toplevel, const QString &toplevelId) +{ + return std::make_unique(add_toplevel(toplevel->object(), toplevelId)); +} + +std::unique_ptr XdgSessionV1::restore(XdgToplevel *toplevel, const QString &toplevelId) +{ + return std::make_unique(restore_toplevel(toplevel->object(), toplevelId)); +} + +void XdgSessionV1::xx_session_v1_created(const QString &id) +{ + Q_EMIT created(id); +} + +void XdgSessionV1::xx_session_v1_restored() +{ + Q_EMIT restored(); +} + +void XdgSessionV1::xx_session_v1_replaced() +{ + Q_EMIT replaced(); +} + +XdgSessionManagerV1::XdgSessionManagerV1(::wl_registry *registry, uint32_t id, int version) + : QtWayland::xx_session_manager_v1(registry, id, version) +{ +} + +XdgSessionManagerV1::~XdgSessionManagerV1() +{ + destroy(); +} + +KeyStateV1::KeyStateV1(::wl_registry *registry, uint32_t id, int version) + : QtWayland::org_kde_kwin_keystate(registry, id, version) +{ + fetchStates(); +} + +KeyStateV1::~KeyStateV1() +{ + destroy(); +} + +void KeyStateV1::org_kde_kwin_keystate_stateChanged(uint32_t key, uint32_t state) +{ + keyToState[key] = state; + Q_EMIT stateChanged(); +} + +WpTabletManagerV2::WpTabletManagerV2(::wl_registry *registry, uint32_t id, int version) + : QtWayland::zwp_tablet_manager_v2(registry, id, version) +{ +} + +WpTabletManagerV2::~WpTabletManagerV2() +{ + destroy(); +} + +std::unique_ptr WpTabletManagerV2::createSeat(KWayland::Client::Seat *seat) +{ + return std::make_unique(get_tablet_seat(*seat)); +} + +WpTabletSeatV2::WpTabletSeatV2(::zwp_tablet_seat_v2 *seat) + : QtWayland::zwp_tablet_seat_v2(seat) +{ +} + +WpTabletSeatV2::~WpTabletSeatV2() +{ + destroy(); +} + +void WpTabletSeatV2::zwp_tablet_seat_v2_tablet_added(::zwp_tablet_v2 *id) +{ + m_tablets.emplace_back(std::make_unique(id)); +} + +void WpTabletSeatV2::zwp_tablet_seat_v2_tool_added(::zwp_tablet_tool_v2 *id) +{ + auto &tool = m_tools.emplace_back(std::make_unique(id)); + Q_EMIT toolAdded(tool.get()); +} + +void WpTabletSeatV2::zwp_tablet_seat_v2_pad_added(::zwp_tablet_pad_v2 *id) +{ + m_pads.emplace_back(std::make_unique(id)); +} + +WpTabletV2::WpTabletV2(::zwp_tablet_v2 *id) + : QtWayland::zwp_tablet_v2(id) +{ +} + +WpTabletV2::~WpTabletV2() +{ + destroy(); +} + +WpTabletToolV2::WpTabletToolV2(::zwp_tablet_tool_v2 *id) + : QtWayland::zwp_tablet_tool_v2(id) +{ +} + +WpTabletToolV2::~WpTabletToolV2() +{ + destroy(); +} + +bool WpTabletToolV2::ready() const +{ + return m_ready; +} + +void WpTabletToolV2::zwp_tablet_tool_v2_done() +{ + m_ready = true; + Q_EMIT done(); +} + +void WpTabletToolV2::zwp_tablet_tool_v2_down(uint32_t serial) +{ + Q_EMIT down(serial); +} + +void WpTabletToolV2::zwp_tablet_tool_v2_up() +{ + Q_EMIT up(); +} + +void WpTabletToolV2::zwp_tablet_tool_v2_motion(wl_fixed_t x, wl_fixed_t y) +{ + Q_EMIT motion(QPointF(wl_fixed_to_double(x), wl_fixed_to_double(y))); +} + +WpTabletPadV2::WpTabletPadV2(::zwp_tablet_pad_v2 *id) + : QtWayland::zwp_tablet_pad_v2(id) +{ +} + +WpTabletPadV2::~WpTabletPadV2() +{ + destroy(); +} + +WpPrimarySelectionOfferV1::WpPrimarySelectionOfferV1(::zwp_primary_selection_offer_v1 *id) + : QtWayland::zwp_primary_selection_offer_v1(id) +{ +} + +WpPrimarySelectionOfferV1::~WpPrimarySelectionOfferV1() +{ + destroy(); +} + +QList WpPrimarySelectionOfferV1::mimeTypes() const +{ + return m_mimeTypes; +} + +void WpPrimarySelectionOfferV1::zwp_primary_selection_offer_v1_offer(const QString &mime_type) +{ + m_mimeTypes.append(QMimeDatabase().mimeTypeForName(mime_type)); +} + +WpPrimarySelectionSourceV1::WpPrimarySelectionSourceV1(::zwp_primary_selection_source_v1 *id) + : QtWayland::zwp_primary_selection_source_v1(id) +{ +} + +WpPrimarySelectionSourceV1::~WpPrimarySelectionSourceV1() +{ + destroy(); +} + +void WpPrimarySelectionSourceV1::zwp_primary_selection_source_v1_send(const QString &mime_type, int32_t fd) +{ + Q_EMIT sendDataRequested(mime_type, fd); +} + +void WpPrimarySelectionSourceV1::zwp_primary_selection_source_v1_cancelled() +{ + Q_EMIT cancelled(); +} + +WpPrimarySelectionDeviceV1::WpPrimarySelectionDeviceV1(::zwp_primary_selection_device_v1 *id) + : QtWayland::zwp_primary_selection_device_v1(id) +{ +} + +WpPrimarySelectionDeviceV1::~WpPrimarySelectionDeviceV1() +{ + destroy(); +} + +WpPrimarySelectionOfferV1 *WpPrimarySelectionDeviceV1::offer() const +{ + return m_offer.get(); +} + +std::unique_ptr WpPrimarySelectionDeviceV1::takeOffer() +{ + return std::move(m_offer); +} + +void WpPrimarySelectionDeviceV1::zwp_primary_selection_device_v1_data_offer(::zwp_primary_selection_offer_v1 *offer) +{ + m_offer = std::make_unique(offer); +} + +void WpPrimarySelectionDeviceV1::zwp_primary_selection_device_v1_selection(::zwp_primary_selection_offer_v1 *id) +{ + if (id) { + Q_EMIT selectionOffered(m_offer.get()); + } else { + m_offer.reset(); + Q_EMIT selectionCleared(); + } +} + +WpPrimarySelectionDeviceManagerV1::WpPrimarySelectionDeviceManagerV1(::wl_registry *registry, uint32_t id, int version) + : QtWayland::zwp_primary_selection_device_manager_v1(registry, id, version) +{ +} + +WpPrimarySelectionDeviceManagerV1::~WpPrimarySelectionDeviceManagerV1() +{ + destroy(); +} + +std::unique_ptr WpPrimarySelectionDeviceManagerV1::getDevice(KWayland::Client::Seat *seat) +{ + return std::make_unique(get_device(*seat)); +} + +std::unique_ptr WpPrimarySelectionDeviceManagerV1::createSource() +{ + return std::make_unique(create_source()); +} + +XdgToplevelDragManagerV1::XdgToplevelDragManagerV1(::wl_registry *registry, uint32_t id, int version) + : QtWayland::xdg_toplevel_drag_manager_v1(registry, id, version) +{ +} + +std::unique_ptr XdgToplevelDragManagerV1::createDrag(KWayland::Client::DataSource *source) +{ + return std::make_unique(get_xdg_toplevel_drag(*source)); +} + +XdgToplevelDragManagerV1::~XdgToplevelDragManagerV1() +{ + destroy(); +} + +XdgToplevelDragV1::XdgToplevelDragV1(::xdg_toplevel_drag_v1 *id) + : QtWayland::xdg_toplevel_drag_v1(id) +{ +} + +XdgToplevelDragV1::~XdgToplevelDragV1() +{ + destroy(); +} + +WlSeat::WlSeat(::wl_registry *registry, uint32_t id, int version) + : QtWayland::wl_seat(registry, id, version) +{ +} + +WlSeat::~WlSeat() +{ + release(); +} + +std::unique_ptr WlSeat::getKeyboard() +{ + return std::make_unique(get_keyboard()); +} + +WlKeyboard::WlKeyboard(::wl_keyboard *object) + : QtWayland::wl_keyboard(object) +{ +} + +WlKeyboard::~WlKeyboard() +{ + release(); +} + +void WlKeyboard::keyboard_keymap(uint32_t format, int32_t fd, uint32_t size) +{ + ::close(fd); +} + +void WlKeyboard::keyboard_enter(uint32_t serial, ::wl_surface *surface, wl_array *keys) +{ + Q_EMIT enter(serial, surface); +} + +void WlKeyboard::keyboard_leave(uint32_t serial, ::wl_surface *surface) +{ + Q_EMIT leave(serial, surface); +} + +void WlKeyboard::keyboard_key(uint32_t serial, uint32_t time, uint32_t keyValue, uint32_t state) +{ + Q_EMIT key(serial, time, keyValue, key_state(state)); +} + +void keyboardKeyPressed(quint32 key, quint32 time) +{ + auto virtualKeyboard = static_cast(kwinApp())->virtualKeyboard(); + Q_EMIT virtualKeyboard->keyChanged(key, KeyboardKeyState::Pressed, std::chrono::milliseconds(time), virtualKeyboard); +} + +void keyboardKeyReleased(quint32 key, quint32 time) +{ + auto virtualKeyboard = static_cast(kwinApp())->virtualKeyboard(); + Q_EMIT virtualKeyboard->keyChanged(key, KeyboardKeyState::Released, std::chrono::milliseconds(time), virtualKeyboard); +} + +void pointerAxisHorizontal(qreal delta, quint32 time, qint32 discreteDelta, PointerAxisSource source) +{ + auto virtualPointer = static_cast(kwinApp())->virtualPointer(); + Q_EMIT virtualPointer->pointerAxisChanged(PointerAxis::Horizontal, delta, discreteDelta, source, false, std::chrono::milliseconds(time), virtualPointer); + Q_EMIT virtualPointer->pointerFrame(virtualPointer); +} + +void pointerAxisVertical(qreal delta, quint32 time, qint32 discreteDelta, PointerAxisSource source) +{ + auto virtualPointer = static_cast(kwinApp())->virtualPointer(); + Q_EMIT virtualPointer->pointerAxisChanged(PointerAxis::Vertical, delta, discreteDelta, source, false, std::chrono::milliseconds(time), virtualPointer); + Q_EMIT virtualPointer->pointerFrame(virtualPointer); +} + +void pointerButtonPressed(quint32 button, quint32 time) +{ + auto virtualPointer = static_cast(kwinApp())->virtualPointer(); + Q_EMIT virtualPointer->pointerButtonChanged(button, PointerButtonState::Pressed, std::chrono::milliseconds(time), virtualPointer); + Q_EMIT virtualPointer->pointerFrame(virtualPointer); +} + +void pointerButtonReleased(quint32 button, quint32 time) +{ + auto virtualPointer = static_cast(kwinApp())->virtualPointer(); + Q_EMIT virtualPointer->pointerButtonChanged(button, PointerButtonState::Released, std::chrono::milliseconds(time), virtualPointer); + Q_EMIT virtualPointer->pointerFrame(virtualPointer); +} + +void pointerMotion(const QPointF &position, quint32 time) +{ + auto virtualPointer = static_cast(kwinApp())->virtualPointer(); + Q_EMIT virtualPointer->pointerMotionAbsolute(position, std::chrono::milliseconds(time), virtualPointer); + Q_EMIT virtualPointer->pointerFrame(virtualPointer); +} + +void pointerMotionRelative(const QPointF &delta, quint32 time) +{ + auto virtualPointer = static_cast(kwinApp())->virtualPointer(); + Q_EMIT virtualPointer->pointerMotion(delta, delta, std::chrono::milliseconds(time), virtualPointer); + Q_EMIT virtualPointer->pointerFrame(virtualPointer); +} + +void touchCancel() +{ + auto virtualTouch = static_cast(kwinApp())->virtualTouch(); + Q_EMIT virtualTouch->touchCanceled(virtualTouch); +} + +void touchDown(qint32 id, const QPointF &pos, quint32 time) +{ + auto virtualTouch = static_cast(kwinApp())->virtualTouch(); + Q_EMIT virtualTouch->touchDown(id, pos, std::chrono::milliseconds(time), virtualTouch); +} + +void touchMotion(qint32 id, const QPointF &pos, quint32 time) +{ + auto virtualTouch = static_cast(kwinApp())->virtualTouch(); + Q_EMIT virtualTouch->touchMotion(id, pos, std::chrono::milliseconds(time), virtualTouch); +} + +void touchUp(qint32 id, quint32 time) +{ + auto virtualTouch = static_cast(kwinApp())->virtualTouch(); + Q_EMIT virtualTouch->touchUp(id, std::chrono::milliseconds(time), virtualTouch); +} + +void tabletPadButtonPressed(quint32 button, quint32 time) +{ + auto virtualTabletPad = static_cast(kwinApp())->virtualTabletPad(); + Q_EMIT virtualTabletPad->tabletPadButtonEvent(button, true, 0, 0, false, std::chrono::milliseconds(time), virtualTabletPad); +} + +void tabletPadButtonReleased(quint32 button, quint32 time) +{ + auto virtualTabletPad = static_cast(kwinApp())->virtualTabletPad(); + Q_EMIT virtualTabletPad->tabletPadButtonEvent(button, false, 0, 0, false, std::chrono::milliseconds(time), virtualTabletPad); +} + +void tabletPadDialEvent(double delta, int number, quint32 time) +{ + auto virtualTabletPad = static_cast(kwinApp())->virtualTabletPad(); + Q_EMIT virtualTabletPad->tabletPadDialEvent(number, delta, 0, std::chrono::milliseconds(time), virtualTabletPad); +} + +void tabletPadRingEvent(qreal position, int number, quint32 group, quint32 mode, quint32 time) +{ + auto virtualTabletPad = static_cast(kwinApp())->virtualTabletPad(); + Q_EMIT virtualTabletPad->tabletPadRingEvent(number, position, true, group, mode, std::chrono::milliseconds(time), virtualTabletPad); +} + +void tabletToolButtonPressed(quint32 button, quint32 time) +{ + auto tablet = static_cast(kwinApp())->virtualTablet(); + auto tool = static_cast(kwinApp())->virtualTabletTool(); + Q_EMIT tablet->tabletToolButtonEvent(button, true, tool, std::chrono::milliseconds(time), tablet); +} + +void tabletToolButtonReleased(quint32 button, quint32 time) +{ + auto tablet = static_cast(kwinApp())->virtualTablet(); + auto tool = static_cast(kwinApp())->virtualTabletTool(); + Q_EMIT tablet->tabletToolButtonEvent(button, false, tool, std::chrono::milliseconds(time), tablet); +} + +void tabletToolProximityEvent(const QPointF &pos, qreal xTilt, qreal yTilt, qreal rotation, qreal distance, bool tipNear, qreal sliderPosition, quint32 time) +{ + auto tablet = static_cast(kwinApp())->virtualTablet(); + auto tool = static_cast(kwinApp())->virtualTabletTool(); + Q_EMIT tablet->tabletToolProximityEvent(pos, xTilt, yTilt, rotation, distance, tipNear, sliderPosition, tool, std::chrono::milliseconds(time), tablet); +} + +void tabletToolAxisEvent(const QPointF &pos, qreal pressure, qreal xTilt, qreal yTilt, qreal rotation, qreal distance, bool tipDown, qreal sliderPosition, quint32 time) +{ + auto tablet = static_cast(kwinApp())->virtualTablet(); + auto tool = static_cast(kwinApp())->virtualTabletTool(); + Q_EMIT tablet->tabletToolAxisEvent(pos, pressure, xTilt, yTilt, rotation, distance, tipDown, sliderPosition, tool, std::chrono::milliseconds(time), tablet); +} + +void tabletToolTipEvent(const QPointF &pos, qreal pressure, qreal xTilt, qreal yTilt, qreal rotation, qreal distance, bool tipDown, qreal sliderPosition, quint32 time) +{ + auto tablet = static_cast(kwinApp())->virtualTablet(); + auto tool = static_cast(kwinApp())->virtualTabletTool(); + Q_EMIT tablet->tabletToolTipEvent(pos, pressure, xTilt, yTilt, rotation, distance, tipDown, sliderPosition, tool, std::chrono::milliseconds(time), tablet); +} + +XdgToplevelWindow::XdgToplevelWindow(const std::function &setup) + : XdgToplevelWindow(s_waylandConnection.get(), setup) +{ +} + +XdgToplevelWindow::XdgToplevelWindow(const std::function &setup) + : XdgToplevelWindow(s_waylandConnection.get(), setup) +{ +} + +XdgToplevelWindow::XdgToplevelWindow(Connection *connection, const std::function &setup) + : m_connection(connection) + , m_surface(createSurface(connection->compositor)) + , m_toplevel(createXdgToplevelSurface(connection->xdgShell, m_surface.get(), setup)) +{ +} + +XdgToplevelWindow::XdgToplevelWindow(Connection *connection, const std::function &setup) + : m_connection(connection) + , m_surface(createSurface(connection->compositor)) + , m_toplevel(createXdgToplevelSurface(connection->xdgShell, m_surface.get(), [this, &setup](XdgToplevel *toplevel) { + setup(m_surface.get(), toplevel); + })) +{ +} + +XdgToplevelWindow::~XdgToplevelWindow() +{ + if (m_window) { + m_toplevel.reset(); + m_surface.reset(); + waitForWindowClosed(m_window); + } +} + +bool XdgToplevelWindow::show(const QSize &size, const QColor &color) +{ + m_window = renderAndWaitForShown(m_connection->shm, m_surface.get(), size, color); + return m_window != nullptr; +} + +bool XdgToplevelWindow::show(const QImage &image) +{ + m_window = renderAndWaitForShown(m_connection->shm, m_surface.get(), image); + return m_window != nullptr; +} + +void XdgToplevelWindow::unmap() +{ + m_surface->attachBuffer((wl_buffer *)nullptr); + m_surface->commit(KWayland::Client::Surface::CommitFlag::None); + // unmapping destroys the KWin::Window + m_window = nullptr; +} + +bool XdgToplevelWindow::unmapAndWaitForClosed() +{ + Window *window = m_window; + unmap(); + return waitForWindowClosed(window); +} + +bool XdgToplevelWindow::presentWait() +{ + const auto feedback = std::make_unique(m_connection->presentationTime->feedback(*m_surface)); + m_surface->commit(KWayland::Client::Surface::CommitFlag::None); + QSignalSpy spy(feedback.get(), &Test::WpPresentationFeedback::presented); + return spy.wait(); +} + +bool XdgToplevelWindow::waitSurfaceConfigure() +{ + QSignalSpy surfaceConfigure(m_toplevel->xdgSurface(), &Test::XdgSurface::configureRequested); + return surfaceConfigure.wait(); +} + +std::optional XdgToplevelWindow::handleConfigure(const QColor &color) +{ + QSignalSpy toplevelConfigure(m_toplevel.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigure(m_toplevel->xdgSurface(), &Test::XdgSurface::configureRequested); + if (!toplevelConfigure.wait()) { + return std::nullopt; + } + m_toplevel->xdgSurface()->ack_configure(surfaceConfigure.last().at(0).value()); + const QSize ret = toplevelConfigure.last().at(0).toSize(); + if (ret == m_surface->size()) { + m_surface->commit(KWayland::Client::Surface::CommitFlag::None); + return ret; + } + Test::render(m_connection->shm, m_surface.get(), toplevelConfigure.last().at(0).toSize(), color); + QSignalSpy frameGeometryChanged(m_window, &KWin::Window::frameGeometryChanged); + if (!frameGeometryChanged.wait()) { + return std::nullopt; + } + return ret; +} +} +} + +#include "test_helpers.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/test_virtualkeyboard_dbus.cpp b/local/recipes/kde/kwin/source/autotests/integration/test_virtualkeyboard_dbus.cpp new file mode 100644 index 0000000000..4ea33bc5a6 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/test_virtualkeyboard_dbus.cpp @@ -0,0 +1,124 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "main.h" +#include "virtualkeyboard_dbus.h" +#include "wayland_server.h" + +#include +#include +#include +#include +#include + +#include + +using KWin::VirtualKeyboardDBus; +using namespace KWin; +using namespace KWin::Test; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_virtualkeyboarddbus-0"); + +class VirtualKeyboardDBusTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testEnabled(); + void testRequestEnabled_data(); + void testRequestEnabled(); + void init(); + void cleanup(); +}; + +void VirtualKeyboardDBusTest::initTestCase() +{ + QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.kwin.testvirtualkeyboard")); + QVERIFY(waylandServer()->init(s_socketName)); + + static_cast(kwinApp())->setInputMethodServerToStart("internal"); + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + + QVERIFY(setupWaylandConnection(AdditionalWaylandInterface::Seat | AdditionalWaylandInterface::InputMethodV1 | AdditionalWaylandInterface::TextInputManagerV2 | AdditionalWaylandInterface::TextInputManagerV3)); +} + +void VirtualKeyboardDBusTest::init() +{ + kwinApp()->inputMethod()->setEnabled(false); +} + +void VirtualKeyboardDBusTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void VirtualKeyboardDBusTest::testEnabled() +{ + VirtualKeyboardDBus dbus(KWin::kwinApp()->inputMethod()); + OrgKdeKwinVirtualKeyboardInterface iface(QStringLiteral("org.kde.kwin.testvirtualkeyboard"), QStringLiteral("/VirtualKeyboard"), QDBusConnection::sessionBus()); + QSignalSpy helperChangedSpy(&iface, &OrgKdeKwinVirtualKeyboardInterface::enabledChanged); + + QCOMPARE(dbus.isEnabled(), false); + QCOMPARE(dbus.property("enabled").toBool(), false); + QSignalSpy enabledChangedSpy(&dbus, &VirtualKeyboardDBus::enabledChanged); + + QVERIFY(iface.isValid()); + QCOMPARE(iface.enabled(), false); + + dbus.setEnabled(true); + QCOMPARE(enabledChangedSpy.count(), 1); + QVERIFY(helperChangedSpy.wait()); + QCOMPARE(helperChangedSpy.count(), 1); + QCOMPARE(dbus.isEnabled(), true); + QCOMPARE(dbus.property("enabled").toBool(), true); + QCOMPARE(iface.enabled(), true); + + // setting again to enabled should not change anything + dbus.setEnabled(true); + QCOMPARE(enabledChangedSpy.count(), 1); + + // back to false + dbus.setEnabled(false); + QCOMPARE(enabledChangedSpy.count(), 2); + QVERIFY(helperChangedSpy.wait()); + QCOMPARE(helperChangedSpy.count(), 2); + QCOMPARE(dbus.isEnabled(), false); + QCOMPARE(dbus.property("enabled").toBool(), false); + QCOMPARE(iface.enabled(), false); +} + +void VirtualKeyboardDBusTest::testRequestEnabled_data() +{ + QTest::addColumn("method"); + QTest::addColumn("expectedResult"); + + QTest::newRow("enable") << QStringLiteral("setEnabled") << true; + QTest::newRow("disable") << QStringLiteral("setEnabled") << false; +} + +void VirtualKeyboardDBusTest::testRequestEnabled() +{ + QFETCH(QString, method); + QFETCH(bool, expectedResult); + + VirtualKeyboardDBus dbus(KWin::kwinApp()->inputMethod()); + OrgKdeKwinVirtualKeyboardInterface iface(QStringLiteral("org.kde.kwin.testvirtualkeyboard"), QStringLiteral("/VirtualKeyboard"), QDBusConnection::sessionBus()); + + iface.setEnabled(expectedResult); + QTRY_COMPARE(iface.enabled(), expectedResult); +} + +WAYLANDTEST_MAIN(VirtualKeyboardDBusTest) +#include "test_virtualkeyboard_dbus.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/tiles_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/tiles_test.cpp new file mode 100644 index 0000000000..5fa441ffdc --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/tiles_test.cpp @@ -0,0 +1,895 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "pointer_input.h" +#include "tiles/tilemanager.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#if KWIN_BUILD_X11 +#include "x11window.h" +#include +#include +#endif + +#include + +Q_DECLARE_METATYPE(KWin::QuickTileMode) +Q_DECLARE_METATYPE(KWin::MaximizeMode) + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_transient_placement-0"); + +#if KWIN_BUILD_X11 +static X11Window *createWindow(xcb_connection_t *connection, const Rect &geometry, std::function setup = {}) +{ + xcb_window_t windowId = xcb_generate_id(connection); + xcb_create_window(connection, XCB_COPY_FROM_PARENT, windowId, rootWindow(), + geometry.x(), + geometry.y(), + geometry.width(), + geometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + + xcb_size_hints_t hints; + memset(&hints, 0, sizeof(hints)); + xcb_icccm_size_hints_set_position(&hints, 1, geometry.x(), geometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, geometry.width(), geometry.height()); + xcb_icccm_set_wm_normal_hints(connection, windowId, &hints); + + if (setup) { + setup(windowId); + } + + xcb_map_window(connection, windowId); + xcb_flush(connection); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + if (!windowCreatedSpy.wait()) { + return nullptr; + } + return windowCreatedSpy.last().first().value(); +} +#endif + +class TilesTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testWindowInteraction(); + void testAssignedTileDeletion(); + void resizeTileFromWindow(); + void shortcuts(); + void testPerDesktopTiles(); + void sendToOutput(); + void sendToOutputX11(); + void tileAndMaximize(); + void evacuateFromRemovedDesktop(); + +private: + void createSimpleLayout(); + void createComplexLayout(); + + LogicalOutput *m_output; + TileManager *m_tileManager; + CustomTile *m_rootTile; +}; + +void TilesTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); +} + +void TilesTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); + + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); + m_output = workspace()->activeOutput(); + m_tileManager = workspace()->tileManager(m_output); + m_rootTile = workspace()->rootTile(m_output); + QAbstractItemModelTester(m_tileManager->model(), QAbstractItemModelTester::FailureReportingMode::QtTest); + + VirtualDesktopManager::self()->setCount(3); + VirtualDesktopManager::self()->setCurrent(1); + createSimpleLayout(); +} + +void TilesTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TilesTest::createSimpleLayout() +{ + std::vector leftTileWidths = {0.5, 0.45, 0.4, 0.35, 0.3, 0.25}; + int i = 0; + for (VirtualDesktop *desk : VirtualDesktopManager::self()->desktops()) { + for (LogicalOutput *out : workspace()->outputs()) { + qreal leftTileWidth = leftTileWidths[i++]; + CustomTile *rootTile = workspace()->rootTile(out, desk); + while (rootTile->childCount() > 0) { + static_cast(rootTile->childTile(0))->remove(); + } + + QCOMPARE(rootTile->childCount(), 0); + rootTile->split(CustomTile::LayoutDirection::Horizontal); + QCOMPARE(rootTile->childCount(), 2); + + auto leftTile = qobject_cast(rootTile->childTiles().first()); + auto rightTile = qobject_cast(rootTile->childTiles().last()); + QVERIFY(leftTile); + QVERIFY(rightTile); + + leftTile->setRelativeGeometry(RectF(0, 0, leftTileWidth, 1)); + + QCOMPARE(leftTile->relativeGeometry(), RectF(0, 0, leftTileWidth, 1)); + QCOMPARE(rightTile->relativeGeometry(), RectF(leftTileWidth, 0, 1 - leftTileWidth, 1)); + } + } +} + +void TilesTest::createComplexLayout() +{ + while (m_rootTile->childCount() > 0) { + static_cast(m_rootTile->childTile(0))->remove(); + } + + QCOMPARE(m_rootTile->childCount(), 0); + m_rootTile->split(CustomTile::LayoutDirection::Horizontal); + QCOMPARE(m_rootTile->childCount(), 2); + + auto leftTile = qobject_cast(m_rootTile->childTiles().first()); + auto rightTile = qobject_cast(m_rootTile->childTiles().last()); + QVERIFY(leftTile); + QVERIFY(rightTile); + + QCOMPARE(leftTile->relativeGeometry(), RectF(0, 0, 0.5, 1)); + QCOMPARE(rightTile->relativeGeometry(), RectF(0.5, 0, 0.5, 1)); + + // Splitting with the same layout direction creates a sibling, not 2 children + rightTile->split(CustomTile::LayoutDirection::Horizontal); + auto newRightTile = qobject_cast(m_rootTile->childTiles().last()); + + QCOMPARE(m_rootTile->childCount(), 3); + QCOMPARE(m_rootTile->relativeGeometry(), RectF(0, 0, 1, 1)); + QCOMPARE(leftTile->relativeGeometry(), RectF(0, 0, 0.5, 1)); + QCOMPARE(rightTile->relativeGeometry(), RectF(0.5, 0, 0.25, 1)); + QCOMPARE(newRightTile->relativeGeometry(), RectF(0.75, 0, 0.25, 1)); + + QCOMPARE(m_rootTile->windowGeometry(), RectF(4, 4, 1272, 1016)); + QCOMPARE(leftTile->windowGeometry(), RectF(4, 4, 634, 1016)); + QCOMPARE(rightTile->windowGeometry(), RectF(642, 4, 316, 1016)); + QCOMPARE(newRightTile->windowGeometry(), RectF(962, 4, 314, 1016)); + + // Splitting with a different layout direction creates 2 children in the tile + QVERIFY(!rightTile->isLayout()); + QCOMPARE(rightTile->childCount(), 0); + rightTile->split(CustomTile::LayoutDirection::Vertical); + QVERIFY(rightTile->isLayout()); + QCOMPARE(rightTile->childCount(), 2); + auto verticalTopTile = qobject_cast(rightTile->childTiles().first()); + auto verticalBottomTile = qobject_cast(rightTile->childTiles().last()); + + // geometry of rightTile should be the same + QCOMPARE(m_rootTile->childCount(), 3); + QCOMPARE(rightTile->relativeGeometry(), RectF(0.5, 0, 0.25, 1)); + QCOMPARE(rightTile->windowGeometry(), RectF(642, 4, 316, 1016)); + + QCOMPARE(verticalTopTile->relativeGeometry(), RectF(0.5, 0, 0.25, 0.5)); + QCOMPARE(verticalBottomTile->relativeGeometry(), RectF(0.5, 0.5, 0.25, 0.5)); + QCOMPARE(verticalTopTile->windowGeometry(), RectF(642, 4, 316, 506)); + QCOMPARE(verticalBottomTile->windowGeometry(), RectF(642, 514, 316, 506)); + + // TODO: add tests for the tile flags +} + +void TilesTest::testWindowInteraction() +{ + createComplexLayout(); + // Test that resizing a tile resizes the contained window and resizes the neighboring tiles as well + std::unique_ptr surface(Test::createSurface()); + + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + + auto rootWindow = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::cyan); + QVERIFY(rootWindow); + QSignalSpy frameGeometryChangedSpy(rootWindow, &Window::frameGeometryChanged); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 1); + + auto leftTile = qobject_cast(m_rootTile->childTiles().first()); + QVERIFY(leftTile); + + leftTile->manage(rootWindow); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 2); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().value(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(rootWindow->frameGeometry(), RectF(leftTile->windowGeometry().toRect())); + + QCOMPARE(toplevelConfigureRequestedSpy.last().first().value(), leftTile->windowGeometry().toRect().size()); + + // Resize owning tile + leftTile->setRelativeGeometry({0, 0, 0.4, 1}); + + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 3); + + QCOMPARE(toplevelConfigureRequestedSpy.last().first().value(), leftTile->windowGeometry().toRect().size()); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + + QCOMPARE(toplevelConfigureRequestedSpy.last().first().value(), leftTile->windowGeometry().toRect().size()); + + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().value(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(rootWindow->frameGeometry(), RectF(leftTile->windowGeometry().toRect())); + + auto middleTile = qobject_cast(m_rootTile->childTiles()[1]); + QVERIFY(middleTile); + auto rightTile = qobject_cast(m_rootTile->childTiles()[2]); + QVERIFY(rightTile); + auto verticalTopTile = qobject_cast(middleTile->childTiles().first()); + QVERIFY(verticalTopTile); + auto verticalBottomTile = qobject_cast(middleTile->childTiles().last()); + QVERIFY(verticalBottomTile); + + QCOMPARE(leftTile->relativeGeometry(), RectF(0, 0, 0.4, 1)); + QCOMPARE(middleTile->relativeGeometry(), RectF(0.4, 0, 0.35, 1)); + QCOMPARE(rightTile->relativeGeometry(), RectF(0.75, 0, 0.25, 1)); + QCOMPARE(verticalTopTile->relativeGeometry(), RectF(0.4, 0, 0.35, 0.5)); + QCOMPARE(verticalBottomTile->relativeGeometry(), RectF(0.4, 0.5, 0.35, 0.5)); +} + +void TilesTest::testAssignedTileDeletion() +{ + createComplexLayout(); + + auto leftTile = qobject_cast(m_rootTile->childTiles().first()); + QVERIFY(leftTile); + leftTile->setRelativeGeometry({0, 0, 0.4, 1}); + + std::unique_ptr rootSurface(Test::createSurface()); + + std::unique_ptr root(Test::createXdgToplevelSurface(rootSurface.get())); + + QSignalSpy surfaceConfigureRequestedSpy(root->xdgSurface(), &Test::XdgSurface::configureRequested); + QSignalSpy toplevelConfigureRequestedSpy(root.get(), &Test::XdgToplevel::configureRequested); + + auto rootWindow = Test::renderAndWaitForShown(rootSurface.get(), QSize(100, 100), Qt::cyan); + QVERIFY(rootWindow); + QSignalSpy frameGeometryChangedSpy(rootWindow, &Window::frameGeometryChanged); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 1); + root->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + + auto middleTile = qobject_cast(m_rootTile->childTiles()[1]); + QVERIFY(middleTile); + auto middleBottomTile = qobject_cast(m_rootTile->childTiles()[1]->childTiles()[1]); + QVERIFY(middleBottomTile); + + middleBottomTile->manage(rootWindow); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 2); + + root->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + + Test::render(rootSurface.get(), toplevelConfigureRequestedSpy.last().first().value(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(rootWindow->frameGeometry(), RectF(middleBottomTile->windowGeometry().toRect())); + + QCOMPARE(toplevelConfigureRequestedSpy.last().first().value(), middleBottomTile->windowGeometry().toRect().size()); + + QCOMPARE(middleBottomTile->windowGeometry().toRect(), Rect(514, 514, 444, 506)); + + middleBottomTile->remove(); + + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 3); + + root->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + + // The window has been reassigned to middleTile after deletion of the children + QCOMPARE(toplevelConfigureRequestedSpy.last().first().value(), middleTile->windowGeometry().toRect().size()); + + Test::render(rootSurface.get(), toplevelConfigureRequestedSpy.last().first().value(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(rootWindow->frameGeometry(), RectF(middleTile->windowGeometry().toRect())); + + // Both children have been deleted as the system avoids tiles with ha single child + QCOMPARE(middleTile->isLayout(), false); + QCOMPARE(middleTile->childCount(), 0); + QCOMPARE(rootWindow->tile(), middleTile); +} + +void TilesTest::resizeTileFromWindow() +{ + createComplexLayout(); + + auto middleBottomTile = qobject_cast(m_rootTile->childTiles()[1]->childTiles()[1]); + QVERIFY(middleBottomTile); + middleBottomTile->remove(); + + std::unique_ptr rootSurface(Test::createSurface()); + + std::unique_ptr root(Test::createXdgToplevelSurface(rootSurface.get())); + + QSignalSpy surfaceConfigureRequestedSpy(root->xdgSurface(), &Test::XdgSurface::configureRequested); + QSignalSpy toplevelConfigureRequestedSpy(root.get(), &Test::XdgToplevel::configureRequested); + + Test::XdgToplevel::States states; + auto window = Test::renderAndWaitForShown(rootSurface.get(), QSize(100, 100), Qt::cyan); + QVERIFY(window); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 1); + root->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + + auto leftTile = qobject_cast(m_rootTile->childTiles().first()); + QVERIFY(leftTile); + leftTile->setRelativeGeometry({0, 0, 0.4, 1}); + QCOMPARE(leftTile->windowGeometry(), RectF(4, 4, 506, 1016)); + + auto middleTile = qobject_cast(m_rootTile->childTiles()[1]); + QVERIFY(middleTile); + QCOMPARE(middleTile->windowGeometry(), RectF(514, 4, 444, 1016)); + + leftTile->split(CustomTile::LayoutDirection::Vertical); + auto topLeftTile = qobject_cast(leftTile->childTiles().first()); + QVERIFY(topLeftTile); + QCOMPARE(topLeftTile->windowGeometry(), RectF(4, 4, 506, 506)); + QSignalSpy tileGeometryChangedSpy(topLeftTile, &Tile::windowGeometryChanged); + auto bottomLeftTile = qobject_cast(leftTile->childTiles().last()); + QVERIFY(bottomLeftTile); + QCOMPARE(bottomLeftTile->windowGeometry(), RectF(4, 514, 506, 506)); + + topLeftTile->manage(window); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 2); + + root->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + + QCOMPARE(toplevelConfigureRequestedSpy.last().first().value(), topLeftTile->windowGeometry().toRect().size()); + Test::render(rootSurface.get(), toplevelConfigureRequestedSpy.last().first().value(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), RectF(4, 4, 506, 506)); + + QCOMPARE(workspace()->activeWindow(), window); + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QVERIFY(interactiveMoveResizeStartedSpy.isValid()); + QSignalSpy moveResizedChangedSpy(window, &Window::moveResizedChanged); + QVERIFY(moveResizedChangedSpy.isValid()); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + QVERIFY(interactiveMoveResizeFinishedSpy.isValid()); + + // begin resize + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QCOMPARE(window->isInteractiveMove(), false); + QCOMPARE(window->isInteractiveResize(), false); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeWindow(), window); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QCOMPARE(moveResizedChangedSpy.count(), 1); + QCOMPARE(window->isInteractiveResize(), true); + QCOMPARE(window->geometryRestore(), RectF(0, 0, 100, 100)); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 3); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing)); + // Trigger a change. + QPoint cursorPos = window->frameGeometry().bottomRight().toPoint(); + input()->pointer()->warp(cursorPos + QPoint(8, 0)); + window->updateInteractiveMoveResize(Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + + // The client should receive a configure event with the new size. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 4); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 4); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing)); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(516, 508)); + + // Now render new size. + root->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(rootSurface.get(), toplevelConfigureRequestedSpy.last().first().value(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), RectF(4, 4, 516, 508)); + + QTRY_COMPARE(tileGeometryChangedSpy.count(), 2); + QCOMPARE(window->tile(), topLeftTile); + QCOMPARE(topLeftTile->windowGeometry(), Rect(4, 4, 516, 508)); + QCOMPARE(bottomLeftTile->windowGeometry(), Rect(4, 516, 516, 504)); + QCOMPARE(leftTile->windowGeometry(), Rect(4, 4, 516, 1016)); + QCOMPARE(middleTile->windowGeometry(), Rect(524, 4, 434, 1016)); + + // Resize vertically + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeWindow(), window); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 2); + QCOMPARE(moveResizedChangedSpy.count(), 3); + QCOMPARE(window->isInteractiveResize(), true); + QCOMPARE(window->geometryRestore(), RectF(0, 0, 100, 100)); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 5); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 5); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing)); + + // Trigger a change. + cursorPos = window->frameGeometry().bottomRight().toPoint(); + input()->pointer()->warp(cursorPos + QPoint(0, 8)); + window->updateInteractiveMoveResize(Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(0, 8)); + + // The client should receive a configure event with the new size. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 6); + QCOMPARE(toplevelConfigureRequestedSpy.count(), 6); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing)); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(518, 518)); + + // Now render new size. + root->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(rootSurface.get(), toplevelConfigureRequestedSpy.last().first().value(), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), RectF(4, 4, 518, 518)); + + QTRY_COMPARE(tileGeometryChangedSpy.count(), 5); + QCOMPARE(window->tile(), topLeftTile); + QCOMPARE(topLeftTile->windowGeometry(), Rect(4, 4, 518, 518)); + QCOMPARE(bottomLeftTile->windowGeometry(), Rect(4, 526, 518, 494)); + QCOMPARE(leftTile->windowGeometry(), Rect(4, 4, 518, 1016)); + QCOMPARE(middleTile->windowGeometry(), Rect(526, 4, 432, 1016)); +} + +void TilesTest::shortcuts() +{ + createComplexLayout(); + + // Our tile layout + // | | | | + // | |-| | + // | | | | + auto leftTile = qobject_cast(m_rootTile->childTiles()[0]); + auto centerTile = qobject_cast(m_rootTile->childTiles()[1]); + auto rightTile = qobject_cast(m_rootTile->childTiles()[2]); + auto topCenterTile = qobject_cast(centerTile->childTiles()[0]); + auto bottomCenterTile = qobject_cast(centerTile->childTiles()[1]); + + // Create a window, don't tile yet + std::unique_ptr rootSurface(Test::createSurface()); + std::unique_ptr root(Test::createXdgToplevelSurface(rootSurface.get())); + + auto window = Test::renderAndWaitForShown(rootSurface.get(), QSize(100, 100), Qt::cyan); + QVERIFY(window); + + // Trigger the shortcut, window should be tiled now + // |w| | | + // |w|-| | + // |w| | | + window->handleCustomQuickTileShortcut(QuickTileFlag::Left); + QCOMPARE(window->requestedTile(), leftTile); + QVERIFY(leftTile->windows().contains(window)); + + // Make the window move around the grid + // | |w| | + // | |-| | + // | | | | + window->handleCustomQuickTileShortcut(QuickTileFlag::Right); + QCOMPARE(window->requestedTile(), topCenterTile); + QVERIFY(!leftTile->windows().contains(window)); + QVERIFY(topCenterTile->windows().contains(window)); + + // | | |w| + // | |-|w| + // | | |w| + window->handleCustomQuickTileShortcut(QuickTileFlag::Right); + QCOMPARE(window->requestedTile(), rightTile); + QVERIFY(!topCenterTile->windows().contains(window)); + QVERIFY(rightTile->windows().contains(window)); + + // Right doesn't do anything now + // | | |w| + // | |-|w| + // | | |w| + window->handleCustomQuickTileShortcut(QuickTileFlag::Right); + QCOMPARE(window->requestedTile(), rightTile); + QVERIFY(!topCenterTile->windows().contains(window)); + QVERIFY(rightTile->windows().contains(window)); + + // | |w| | + // | |-| | + // | | | | + window->handleCustomQuickTileShortcut(QuickTileFlag::Left); + QCOMPARE(window->requestedTile(), topCenterTile); + QVERIFY(!rightTile->windows().contains(window)); + QVERIFY(topCenterTile->windows().contains(window)); + + // | | | | + // | |-| | + // | |w| | + window->handleCustomQuickTileShortcut(QuickTileFlag::Bottom); + QCOMPARE(window->requestedTile(), bottomCenterTile); + QVERIFY(!topCenterTile->windows().contains(window)); + QVERIFY(bottomCenterTile->windows().contains(window)); + + // |w| | | + // |w|-| | + // |w| | | + window->handleCustomQuickTileShortcut(QuickTileFlag::Left); + QCOMPARE(window->requestedTile(), leftTile); + QVERIFY(!bottomCenterTile->windows().contains(window)); + QVERIFY(leftTile->windows().contains(window)); +} + +void TilesTest::testPerDesktopTiles() +{ + auto rootTileD1 = m_tileManager->rootTile(VirtualDesktopManager::self()->desktops()[0]); + auto rightTileD1 = rootTileD1->childTiles()[1]; + + auto rootTileD2 = m_tileManager->rootTile(VirtualDesktopManager::self()->desktops()[1]); + auto leftTileD2 = rootTileD2->childTiles()[0]; + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr root(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::cyan); + window->setOnAllDesktops(true); + + QSignalSpy tileChangedSpy(window, &Window::tileChanged); + QSignalSpy surfaceConfigureRequestedSpy(root->xdgSurface(), &Test::XdgSurface::configureRequested); + QSignalSpy toplevelConfigureRequestedSpy(root.get(), &Test::XdgToplevel::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + auto ackConfigure = [&]() { + QVERIFY(surfaceConfigureRequestedSpy.wait()); + root->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().value(), Qt::blue); + }; + + // Add the window to a tile in desktop 1 + { + rightTileD1->manage(window); + ackConfigure(); + QVERIFY(tileChangedSpy.wait()); + QCOMPARE(window->tile(), rightTileD1); + QCOMPARE(window->frameGeometry(), RectF(rightTileD1->windowGeometry())); + } + + // Set current Desktop 2 + { + VirtualDesktopManager::self()->setCurrent(2); + ackConfigure(); + QVERIFY(tileChangedSpy.wait()); + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 100, 100)); + } + + // Set a new tile for Desktop 2 + { + leftTileD2->manage(window); + ackConfigure(); + QVERIFY(tileChangedSpy.wait()); + QCOMPARE(window->tile(), leftTileD2); + QCOMPARE(window->frameGeometry(), RectF(leftTileD2->windowGeometry())); + } + + // Go back to desktop 1, we go back to rightTileD1 + { + VirtualDesktopManager::self()->setCurrent(1); + ackConfigure(); + QVERIFY(tileChangedSpy.wait()); + QCOMPARE(window->tile(), rightTileD1); + QCOMPARE(window->frameGeometry(), RectF(rightTileD1->windowGeometry())); + } + + // Switch to desktop 2 + { + VirtualDesktopManager::self()->setCurrent(2); + ackConfigure(); + QVERIFY(tileChangedSpy.wait()); + QCOMPARE(window->tile(), leftTileD2); + QCOMPARE(window->frameGeometry(), RectF(leftTileD2->windowGeometry())); + } +} + +void TilesTest::sendToOutput() +{ + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr root(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::cyan); + window->setOnAllDesktops(true); + + QSignalSpy tileChangedSpy(window, &Window::tileChanged); + QSignalSpy surfaceConfigureRequestedSpy(root->xdgSurface(), &Test::XdgSurface::configureRequested); + QSignalSpy toplevelConfigureRequestedSpy(root.get(), &Test::XdgToplevel::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + auto ackConfigure = [&]() { + QVERIFY(surfaceConfigureRequestedSpy.wait()); + root->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().value(), Qt::blue); + }; + + const auto desktops = VirtualDesktopManager::self()->desktops(); + const auto outputs = workspace()->outputs(); + Tile *firstTile = workspace()->rootTile(outputs[0], desktops[0])->childTiles()[0]; + Tile *secondTile = workspace()->rootTile(outputs[0], desktops[1])->childTiles()[1]; + + // Tile window on desktop 1 + { + VirtualDesktopManager::self()->setCurrent(desktops[0]); + firstTile->manage(window); + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->requestedTile(), firstTile); + ackConfigure(); + QVERIFY(tileChangedSpy.wait()); + QCOMPARE(window->tile(), firstTile); + QCOMPARE(window->requestedTile(), firstTile); + QCOMPARE(window->frameGeometry(), RectF(firstTile->windowGeometry())); + } + + // Tile window on desktop 2 + { + VirtualDesktopManager::self()->setCurrent(desktops[1]); + secondTile->manage(window); + ackConfigure(); + QVERIFY(tileChangedSpy.wait()); + QCOMPARE(window->tile(), secondTile); + QCOMPARE(window->requestedTile(), secondTile); + QCOMPARE(window->frameGeometry(), RectF(secondTile->windowGeometry())); + } + + // Send window to the second output + { + window->sendToOutput(outputs[1]); + QVERIFY(!firstTile->windows().contains(window)); + QVERIFY(!secondTile->windows().contains(window)); + QCOMPARE(window->tile(), secondTile); + QCOMPARE(window->requestedTile(), nullptr); + ackConfigure(); + QVERIFY(tileChangedSpy.wait()); + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->requestedTile(), nullptr); + } +} + +void TilesTest::sendToOutputX11() +{ +#if KWIN_BUILD_X11 + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200)); + window->setOnAllDesktops(true); + + const auto desktops = VirtualDesktopManager::self()->desktops(); + const auto outputs = workspace()->outputs(); + Tile *firstTile = workspace()->rootTile(outputs[0], desktops[0])->childTiles()[0]; + Tile *secondTile = workspace()->rootTile(outputs[0], desktops[1])->childTiles()[1]; + + // Tile window on desktop 1 + { + VirtualDesktopManager::self()->setCurrent(desktops[0]); + firstTile->manage(window); + QCOMPARE(window->tile(), firstTile); + QCOMPARE(window->requestedTile(), firstTile); + QCOMPARE(window->frameGeometry(), RectF(firstTile->windowGeometry())); + } + + // Tile window on desktop 2 + { + VirtualDesktopManager::self()->setCurrent(desktops[1]); + secondTile->manage(window); + QCOMPARE(window->tile(), secondTile); + QCOMPARE(window->requestedTile(), secondTile); + QCOMPARE(window->frameGeometry(), RectF(secondTile->windowGeometry())); + } + + // Send window to the second output + { + window->sendToOutput(outputs[1]); + QVERIFY(!firstTile->windows().contains(window)); + QVERIFY(!secondTile->windows().contains(window)); + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->requestedTile(), nullptr); + } +#endif +} + +void TilesTest::tileAndMaximize() +{ + // This tests the interaction between the tiled and maximized states + auto rootTileD2 = m_tileManager->rootTile(VirtualDesktopManager::self()->desktops()[1]); + auto rightTileD2 = qobject_cast(rootTileD2->childTiles()[1]); + + auto leftQuickTileD1 = m_tileManager->quickTile(QuickTileFlag::Left); + + const QList outputs = workspace()->outputs(); + TileManager *out2TileMan = workspace()->tileManager(outputs[1]); + auto rootTileD3O2 = out2TileMan->rootTile(VirtualDesktopManager::self()->desktops()[2]); + auto leftTileD3O2 = qobject_cast(rootTileD3O2->childTiles()[0]); + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr root(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::cyan); + window->setOnAllDesktops(true); + + QSignalSpy tileChangedSpy(window, &Window::tileChanged); + QSignalSpy surfaceConfigureRequestedSpy(root->xdgSurface(), &Test::XdgSurface::configureRequested); + QSignalSpy toplevelConfigureRequestedSpy(root.get(), &Test::XdgToplevel::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + auto ackConfigure = [&]() { + QVERIFY(surfaceConfigureRequestedSpy.wait()); + root->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().value(), Qt::blue); + }; + + // Add the window to a quick tile in desktop 1 and a custom tile on desktop 2 + { + leftQuickTileD1->manage(window); + rightTileD2->manage(window); + QCOMPARE(window->requestedTile(), leftQuickTileD1); + ackConfigure(); + QVERIFY(tileChangedSpy.wait()); + QCOMPARE(window->tile(), leftQuickTileD1); + QCOMPARE(window->frameGeometry(), RectF(leftQuickTileD1->windowGeometry())); + } + + // Set current Desktop 2 + { + VirtualDesktopManager::self()->setCurrent(2); + // Tile becomes rightTileD2 + QCOMPARE(window->requestedTile(), rightTileD2); + ackConfigure(); + QVERIFY(tileChangedSpy.wait()); + QCOMPARE(window->tile(), rightTileD2); + QCOMPARE(window->frameGeometry(), RectF(rightTileD2->windowGeometry())); + } + + // Add the window also on a tile of another output + leftTileD3O2->manage(window); + + // Maximize the window, it should lose its tile + { + window->maximize(MaximizeFull); + // No requestedTile anymore + QCOMPARE(window->requestedTile(), nullptr); + ackConfigure(); + QVERIFY(tileChangedSpy.wait()); + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); + + // Both tiles have an empty window list now + QVERIFY(leftQuickTileD1->windows().isEmpty()); + QVERIFY(rightTileD2->windows().isEmpty()); + // Also the tile on the other output llost the window + QVERIFY(leftTileD3O2->windows().isEmpty()); + } + + // Set a tile again, it should unmaximize + { + rightTileD2->manage(window); + QCOMPARE(window->requestedTile(), rightTileD2); + ackConfigure(); + QVERIFY(tileChangedSpy.wait()); + QCOMPARE(window->tile(), rightTileD2); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), RectF(rightTileD2->windowGeometry())); + } +} + +void TilesTest::evacuateFromRemovedDesktop() +{ + const auto desktops = VirtualDesktopManager::self()->desktops(); + auto rightTileD2 = m_tileManager->rootTile(desktops[1])->childTiles()[1]; + auto leftTileD3 = m_tileManager->rootTile(desktops[2])->childTiles()[0]; + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr root(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::cyan); + window->setOnAllDesktops(true); + + QSignalSpy surfaceConfigureRequestedSpy(root->xdgSurface(), &Test::XdgSurface::configureRequested); + QSignalSpy toplevelConfigureRequestedSpy(root.get(), &Test::XdgToplevel::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + auto ackConfigure = [&]() { + QVERIFY(surfaceConfigureRequestedSpy.wait()); + root->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().value(), Qt::blue); + QVERIFY(Test::waylandSync()); + }; + + // Set current Desktop 2 + // Add the window to a tile in desktop 2 and desktop 3 + { + VirtualDesktopManager::self()->setCurrent(desktops[1]); + rightTileD2->manage(window); + leftTileD3->manage(window); + QCOMPARE(window->requestedTile(), rightTileD2); + ackConfigure(); + QCOMPARE(window->tile(), rightTileD2); + QCOMPARE(window->frameGeometry(), RectF(rightTileD2->windowGeometry())); + } + + // Set current Desktop 3 + { + VirtualDesktopManager::self()->setCurrent(desktops[2]); + // Tile becomes leftTileD3 + QCOMPARE(window->requestedTile(), leftTileD3); + ackConfigure(); + QCOMPARE(window->tile(), leftTileD3); + QCOMPARE(window->frameGeometry(), RectF(leftTileD3->windowGeometry())); + } + + // Remove the current desktop 3, the window will be tiled again to rightTileD2 + { + VirtualDesktopManager::self()->removeVirtualDesktop(desktops[2]); + QCOMPARE(window->requestedTile(), rightTileD2); + ackConfigure(); + QCOMPARE(window->tile(), rightTileD2); + QCOMPARE(window->frameGeometry(), RectF(rightTileD2->windowGeometry())); + } + + // Remove the current desktop 2, the window is now untiles + { + VirtualDesktopManager::self()->removeVirtualDesktop(desktops[1]); + QCOMPARE(window->requestedTile(), nullptr); + ackConfigure(); + QCOMPARE(window->tile(), nullptr); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 100, 100)); + } +} +} + +WAYLANDTEST_MAIN(KWin::TilesTest) +#include "tiles_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/touch_input_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/touch_input_test.cpp new file mode 100644 index 0000000000..51a4b22afb --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/touch_input_test.cpp @@ -0,0 +1,446 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "pointer_input.h" +#include "touch_input.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include +#include +#include + +#include +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_touch_input-0"); + +class TouchInputTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testTouchHidesCursor(); + void testMultipleTouchPoints_data(); + void testMultipleTouchPoints(); + void testCancel(); + void testTouchMouseAction(); + void testTouchPointCount(); + void testUpdateFocusOnDecorationDestroy(); + void testGestureDetection(); + +private: + struct WindowHandle + { + Window *window; + std::unique_ptr surface; + std::unique_ptr shellSurface; + std::unique_ptr decoration; + }; + WindowHandle showWindow(bool decorated = false); + KWayland::Client::Touch *m_touch = nullptr; +}; + +void TouchInputTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); +} + +void TouchInputTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::XdgDecorationV1)); + QVERIFY(Test::waitForWaylandTouch()); + m_touch = Test::waylandSeat()->createTouch(Test::waylandSeat()); + QVERIFY(m_touch); + QVERIFY(m_touch->isValid()); + + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); +} + +void TouchInputTest::cleanup() +{ + delete m_touch; + m_touch = nullptr; + Test::destroyWaylandConnection(); +} + +TouchInputTest::WindowHandle TouchInputTest::showWindow(bool decorated) +{ +#define VERIFY(statement) \ + if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__)) \ + return {}; +#define COMPARE(actual, expected) \ + if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__)) \ + return {}; + + std::unique_ptr surface = Test::createSurface(); + VERIFY(surface.get()); + std::unique_ptr shellSurface = Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly); + VERIFY(shellSurface.get()); + std::unique_ptr decoration; + if (decorated) { + decoration = Test::createXdgToplevelDecorationV1(shellSurface.get()); + decoration->set_mode(Test::XdgToplevelDecorationV1::mode_server_side); + } + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + VERIFY(surfaceConfigureRequestedSpy.wait()); + // let's render + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + VERIFY(window); + COMPARE(workspace()->activeWindow(), window); + +#undef VERIFY +#undef COMPARE + + return {window, std::move(surface), std::move(shellSurface), std::move(decoration)}; +} + +void TouchInputTest::testTouchHidesCursor() +{ + QCOMPARE(Cursors::self()->isCursorHidden(), false); + quint32 timestamp = 1; + Test::touchDown(1, QPointF(125, 125), timestamp++); + QCOMPARE(Cursors::self()->isCursorHidden(), true); + Test::touchDown(2, QPointF(130, 125), timestamp++); + Test::touchUp(2, timestamp++); + Test::touchUp(1, timestamp++); + + // now a mouse event should show the cursor again + Test::pointerMotion(QPointF(0, 0), timestamp++); + QCOMPARE(Cursors::self()->isCursorHidden(), false); + + // touch should hide again + Test::touchDown(1, QPointF(125, 125), timestamp++); + Test::touchUp(1, timestamp++); + QCOMPARE(Cursors::self()->isCursorHidden(), true); + + // wheel should also show + Test::pointerAxisVertical(15.0, timestamp++); + QCOMPARE(Cursors::self()->isCursorHidden(), false); +} + +void TouchInputTest::testMultipleTouchPoints_data() +{ + QTest::addColumn("decorated"); + + QTest::newRow("undecorated") << false; + QTest::newRow("decorated") << true; +} + +void TouchInputTest::testMultipleTouchPoints() +{ + QFETCH(bool, decorated); + auto [window, surface, shellSurface, decoration] = showWindow(decorated); + QCOMPARE(window->isDecorated(), decorated); + window->move(QPoint(100, 100)); + QVERIFY(window); + QSignalSpy sequenceStartedSpy(m_touch, &KWayland::Client::Touch::sequenceStarted); + QSignalSpy pointAddedSpy(m_touch, &KWayland::Client::Touch::pointAdded); + QSignalSpy pointMovedSpy(m_touch, &KWayland::Client::Touch::pointMoved); + QSignalSpy pointRemovedSpy(m_touch, &KWayland::Client::Touch::pointRemoved); + QSignalSpy endedSpy(m_touch, &KWayland::Client::Touch::sequenceEnded); + + quint32 timestamp = 1; + Test::touchDown(1, window->mapFromLocal(QPointF(25, 25)), timestamp++); + QVERIFY(sequenceStartedSpy.wait()); + QCOMPARE(sequenceStartedSpy.count(), 1); + QCOMPARE(m_touch->sequence().count(), 1); + QCOMPARE(m_touch->sequence().first()->isDown(), true); + QCOMPARE(m_touch->sequence().first()->position(), QPointF(25, 25)); + QCOMPARE(pointAddedSpy.count(), 0); + QCOMPARE(pointMovedSpy.count(), 0); + + auto [window2, surface2, shellSurface2, decoration2] = showWindow(decorated); + QCOMPARE(window2->isDecorated(), decorated); + QVERIFY(window2); + window2->moveResize(Rect(0, 0, 100, 50)); + + // a point outside the window, where window2 is + Test::touchDown(2, window->mapFromLocal(QPointF(-100, -100)), timestamp++); + QVERIFY(pointAddedSpy.wait()); + QCOMPARE(pointAddedSpy.count(), 1); + QCOMPARE(m_touch->sequence().count(), 2); + QCOMPARE(m_touch->sequence().at(1)->isDown(), true); + QCOMPARE(m_touch->sequence().at(1)->surface(), surface2.get()); + QCOMPARE(m_touch->sequence().at(1)->position(), QPointF(0, 0)); + QCOMPARE(pointMovedSpy.count(), 0); + + // let's move that one. Since it started in window2 it stays with window2 + Test::touchMotion(2, window->mapFromLocal(QPointF(0, 0)), timestamp++); + QVERIFY(pointMovedSpy.wait()); + QCOMPARE(pointMovedSpy.count(), 1); + QCOMPARE(m_touch->sequence().count(), 2); + QCOMPARE(m_touch->sequence().at(1)->isDown(), true); + QCOMPARE(m_touch->sequence().at(1)->surface(), surface2.get()); + QCOMPARE(m_touch->sequence().at(1)->position(), QPointF(100, 100)); + + Test::touchUp(1, timestamp++); + QVERIFY(pointRemovedSpy.wait()); + QCOMPARE(pointRemovedSpy.count(), 1); + QCOMPARE(m_touch->sequence().count(), 2); + QCOMPARE(m_touch->sequence().first()->isDown(), false); + QCOMPARE(endedSpy.count(), 0); + + Test::touchUp(2, timestamp++); + QVERIFY(pointRemovedSpy.wait()); + QCOMPARE(pointRemovedSpy.count(), 2); + QCOMPARE(m_touch->sequence().count(), 2); + QCOMPARE(m_touch->sequence().first()->isDown(), false); + QCOMPARE(m_touch->sequence().at(1)->isDown(), false); + QCOMPARE(endedSpy.count(), 1); +} + +void TouchInputTest::testCancel() +{ + auto [window, surface, shellSurface, decoration] = showWindow(); + window->move(QPoint(100, 100)); + QVERIFY(window); + QSignalSpy sequenceStartedSpy(m_touch, &KWayland::Client::Touch::sequenceStarted); + QSignalSpy cancelSpy(m_touch, &KWayland::Client::Touch::sequenceCanceled); + QSignalSpy pointRemovedSpy(m_touch, &KWayland::Client::Touch::pointRemoved); + + quint32 timestamp = 1; + Test::touchDown(1, QPointF(125, 125), timestamp++); + QVERIFY(sequenceStartedSpy.wait()); + QCOMPARE(sequenceStartedSpy.count(), 1); + + // cancel + Test::touchCancel(); + QVERIFY(cancelSpy.wait()); + QCOMPARE(cancelSpy.count(), 1); +} + +void TouchInputTest::testTouchMouseAction() +{ + // this test verifies that a touch down on an inactive window will activate it + + // create two windows + auto [c1, surface, shellSurface, decoration] = showWindow(); + QVERIFY(c1); + auto [c2, surface2, shellSurface2, decoration2] = showWindow(); + QVERIFY(c2); + + QVERIFY(!c1->isActive()); + QVERIFY(c2->isActive()); + + // also create a sequence started spy as the touch event should be passed through + QSignalSpy sequenceStartedSpy(m_touch, &KWayland::Client::Touch::sequenceStarted); + + quint32 timestamp = 1; + Test::touchDown(1, c1->frameGeometry().center(), timestamp++); + QVERIFY(c1->isActive()); + + QVERIFY(sequenceStartedSpy.wait()); + QCOMPARE(sequenceStartedSpy.count(), 1); + + // cleanup + input()->touch()->cancel(); +} + +void TouchInputTest::testTouchPointCount() +{ + QCOMPARE(input()->touch()->touchPointCount(), 0); + quint32 timestamp = 1; + Test::touchDown(0, QPointF(125, 125), timestamp++); + Test::touchDown(1, QPointF(125, 125), timestamp++); + Test::touchDown(2, QPointF(125, 125), timestamp++); + QCOMPARE(input()->touch()->touchPointCount(), 3); + + Test::touchUp(1, timestamp++); + QCOMPARE(input()->touch()->touchPointCount(), 2); + + input()->touch()->cancel(); + QCOMPARE(input()->touch()->touchPointCount(), 0); +} + +void TouchInputTest::testUpdateFocusOnDecorationDestroy() +{ + // This test verifies that a maximized window gets it's touch focus + // if decoration was focused and then destroyed on maximize with BorderlessMaximizedWindows option. + + QSignalSpy sequenceEndedSpy(m_touch, &KWayland::Client::Touch::sequenceEnded); + + // Enable the borderless maximized windows option. + auto group = kwinApp()->config()->group(QStringLiteral("Windows")); + group.writeEntry("BorderlessMaximizedWindows", true); + group.sync(); + Workspace::self()->slotReconfigure(); + QCOMPARE(options->borderlessMaximizedWindows(), true); + + // Create the test window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + std::unique_ptr decoration(Test::createXdgToplevelDecorationV1(shellSurface.get())); + + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QSignalSpy decorationConfigureRequestedSpy(decoration.get(), &Test::XdgToplevelDecorationV1::configureRequested); + decoration->set_mode(Test::XdgToplevelDecorationV1::mode_server_side); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the initial configure event. + Test::XdgToplevel::States states; + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(0, 0)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Map the window. + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(window->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(window->isDecorated(), true); + + // We should receive a configure event when the window becomes active. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Simulate decoration hover + quint32 timestamp = 0; + Test::touchDown(1, window->frameGeometry().topLeft(), timestamp++); + QVERIFY(input()->touch()->decoration()); + + // Maximize when on decoration + workspace()->slotWindowMaximize(); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(1280, 1024), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + QCOMPARE(window->isDecorated(), false); + + // Window should have focus + QVERIFY(!input()->touch()->decoration()); + Test::touchUp(1, timestamp++); + QVERIFY(!sequenceEndedSpy.wait(100)); + Test::touchDown(2, window->frameGeometry().center(), timestamp++); + Test::touchUp(2, timestamp++); + QVERIFY(sequenceEndedSpy.wait()); + + // Destroy the window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void TouchInputTest::testGestureDetection() +{ +#if !KWIN_BUILD_GLOBALSHORTCUTS + QSKIP("Can't test shortcuts without shortcuts"); + return; +#endif + + bool callbackTriggered = false; + const auto callback = [&callbackTriggered](float progress) { + callbackTriggered = true; + qWarning() << "progress callback!" << progress; + }; + QAction action; + input()->forceRegisterTouchscreenSwipeShortcut(SwipeDirection::Right, 3, &action, callback); + + // verify that gestures are detected + + quint32 timestamp = 1; + Test::touchDown(0, QPointF(500, 125), timestamp++); + Test::touchDown(1, QPointF(500, 125), timestamp++); + Test::touchDown(2, QPointF(500, 125), timestamp++); + + Test::touchMotion(0, QPointF(100, 125), timestamp++); + QVERIFY(callbackTriggered); + + // verify that gestures are canceled properly + QSignalSpy gestureCancelled(&action, &QAction::triggered); + Test::touchUp(0, timestamp++); + QVERIFY(gestureCancelled.wait()); + + Test::touchUp(1, timestamp++); + Test::touchUp(2, timestamp++); + + callbackTriggered = false; + + // verify that touch points too far apart don't trigger a gesture + Test::touchDown(0, QPointF(125, 125), timestamp++); + Test::touchDown(1, QPointF(10000, 125), timestamp++); + Test::touchDown(2, QPointF(125, 125), timestamp++); + QVERIFY(!callbackTriggered); + + Test::touchUp(0, timestamp++); + Test::touchUp(1, timestamp++); + Test::touchUp(2, timestamp++); + + // verify that touch points triggered too slow don't trigger a gesture + Test::touchDown(0, QPointF(125, 125), timestamp++); + timestamp += 1000; + Test::touchDown(1, QPointF(125, 125), timestamp++); + Test::touchDown(2, QPointF(125, 125), timestamp++); + QVERIFY(!callbackTriggered); + + Test::touchUp(0, timestamp++); + Test::touchUp(1, timestamp++); + Test::touchUp(2, timestamp++); + + // verify that after a gesture has been canceled but never initiated, gestures still work + Test::touchDown(0, QPointF(500, 125), timestamp++); + Test::touchDown(1, QPointF(500, 125), timestamp++); + Test::touchDown(2, QPointF(500, 125), timestamp++); + + Test::touchMotion(0, QPointF(100, 125), timestamp++); + Test::touchMotion(1, QPointF(100, 125), timestamp++); + Test::touchMotion(2, QPointF(100, 125), timestamp++); + QVERIFY(callbackTriggered); + + Test::touchUp(0, timestamp++); + Test::touchUp(1, timestamp++); + Test::touchUp(2, timestamp++); +} +} + +WAYLANDTEST_MAIN(KWin::TouchInputTest) +#include "touch_input_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/transient_placement.cpp b/local/recipes/kde/kwin/source/autotests/integration/transient_placement.cpp new file mode 100644 index 0000000000..5e9df87ed6 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/transient_placement.cpp @@ -0,0 +1,551 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "pointer_input.h" +#include "wayland/seat.h" +#include "wayland/surface.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +struct PopupLayout +{ + KWin::Rect anchorRect; + QSize size; + quint32 anchor = 0; + quint32 gravity = 0; + quint32 constraint = 0; +}; +Q_DECLARE_METATYPE(PopupLayout) + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_transient_placement-0"); + +class TransientPlacementTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testXdgPopup_data(); + void testXdgPopup(); + void testXdgPopupWithPanel(); +}; + +void TransientPlacementTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); +} + +void TransientPlacementTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::LayerShellV1)); + + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); +} + +void TransientPlacementTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TransientPlacementTest::testXdgPopup_data() +{ + QTest::addColumn("parentSize"); + QTest::addColumn("parentPosition"); + QTest::addColumn("layout"); + QTest::addColumn("expectedGeometry"); + + // parent window is 500,500, starting at 300,300, anchorRect is therefore between 350->750 in both dirs + + // ---------------------------------------------------------------- + // window in the middle, plenty of room either side: Changing anchor + + const PopupLayout layoutNone{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_top_left, + .gravity = Test::XdgPositioner::gravity_top_left, + }; + QTest::newRow("no constraint adjustments") << QSize(500, 500) << QPoint(0, 0) << layoutNone << Rect(50 - 200, 50 - 200, 200, 200); + + const PopupLayout layoutAnchorCenter{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_none, + .gravity = Test::XdgPositioner::gravity_bottom_right, + }; + QTest::newRow("anchorCenter") << QSize(500, 500) << QPoint(300, 300) << layoutAnchorCenter << Rect(550, 550, 200, 200); + + const PopupLayout layoutAnchorTopLeft{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_top_left, + .gravity = Test::XdgPositioner::gravity_bottom_right, + }; + QTest::newRow("anchorTopLeft") << QSize(500, 500) << QPoint(300, 300) << layoutAnchorTopLeft << Rect(350, 350, 200, 200); + + const PopupLayout layoutAnchorTop{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_top, + .gravity = Test::XdgPositioner::gravity_bottom_right, + }; + QTest::newRow("anchorTop") << QSize(500, 500) << QPoint(300, 300) << layoutAnchorTop << Rect(550, 350, 200, 200); + + const PopupLayout layoutAnchorTopRight{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_top_right, + .gravity = Test::XdgPositioner::gravity_bottom_right, + }; + QTest::newRow("anchorTopRight") << QSize(500, 500) << QPoint(300, 300) << layoutAnchorTopRight << Rect(750, 350, 200, 200); + + const PopupLayout layoutAnchorRight{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_right, + .gravity = Test::XdgPositioner::gravity_bottom_right, + }; + QTest::newRow("anchorRight") << QSize(500, 500) << QPoint(300, 300) << layoutAnchorRight << Rect(750, 550, 200, 200); + + const PopupLayout layoutAnchorBottomRight{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_bottom_right, + .gravity = Test::XdgPositioner::gravity_bottom_right, + }; + QTest::newRow("anchorBottomRight") << QSize(500, 500) << QPoint(300, 300) << layoutAnchorBottomRight << Rect(750, 750, 200, 200); + + const PopupLayout layoutAnchorBottom{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_bottom, + .gravity = Test::XdgPositioner::gravity_bottom_right, + }; + QTest::newRow("anchorBottom") << QSize(500, 500) << QPoint(300, 300) << layoutAnchorBottom << Rect(550, 750, 200, 200); + + const PopupLayout layoutAnchorBottomLeft{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_bottom_left, + .gravity = Test::XdgPositioner::gravity_bottom_right, + }; + QTest::newRow("anchorBottomLeft") << QSize(500, 500) << QPoint(300, 300) << layoutAnchorBottomLeft << Rect(350, 750, 200, 200); + + const PopupLayout layoutAnchorLeft{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_left, + .gravity = Test::XdgPositioner::gravity_bottom_right, + }; + QTest::newRow("anchorLeft") << QSize(500, 500) << QPoint(300, 300) << layoutAnchorLeft << Rect(350, 550, 200, 200); + + // ---------------------------------------------------------------- + // window in the middle, plenty of room either side: Changing gravity around the bottom right anchor + + const PopupLayout layoutGravityCenter{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_bottom_right, + .gravity = Test::XdgPositioner::gravity_none, + }; + QTest::newRow("gravityCenter") << QSize(500, 500) << QPoint(300, 300) << layoutGravityCenter << Rect(650, 650, 200, 200); + + const PopupLayout layoutGravityTopLeft{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_bottom_right, + .gravity = Test::XdgPositioner::gravity_top_left, + }; + QTest::newRow("gravityTopLeft") << QSize(500, 500) << QPoint(300, 300) << layoutGravityTopLeft << Rect(550, 550, 200, 200); + + const PopupLayout layoutGravityTop{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_bottom_right, + .gravity = Test::XdgPositioner::gravity_top, + }; + QTest::newRow("gravityTop") << QSize(500, 500) << QPoint(300, 300) << layoutGravityTop << Rect(650, 550, 200, 200); + + const PopupLayout layoutGravityTopRight{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_bottom_right, + .gravity = Test::XdgPositioner::gravity_top_right, + }; + QTest::newRow("gravityTopRight") << QSize(500, 500) << QPoint(300, 300) << layoutGravityTopRight << Rect(750, 550, 200, 200); + + const PopupLayout layoutGravityRight{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_bottom_right, + .gravity = Test::XdgPositioner::gravity_right, + }; + QTest::newRow("gravityRight") << QSize(500, 500) << QPoint(300, 300) << layoutGravityRight << Rect(750, 650, 200, 200); + + const PopupLayout layoutGravityBottomRight{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_bottom_right, + .gravity = Test::XdgPositioner::gravity_bottom_right, + }; + QTest::newRow("gravityBottomRight") << QSize(500, 500) << QPoint(300, 300) << layoutGravityBottomRight << Rect(750, 750, 200, 200); + + const PopupLayout layoutGravityBottom{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_bottom_right, + .gravity = Test::XdgPositioner::gravity_bottom, + }; + QTest::newRow("gravityBottom") << QSize(500, 500) << QPoint(300, 300) << layoutGravityBottom << Rect(650, 750, 200, 200); + + const PopupLayout layoutGravityBottomLeft{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_bottom_right, + .gravity = Test::XdgPositioner::gravity_bottom_left, + }; + QTest::newRow("gravityBottomLeft") << QSize(500, 500) << QPoint(300, 300) << layoutGravityBottomLeft << Rect(550, 750, 200, 200); + + const PopupLayout layoutGravityLeft{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_bottom_right, + .gravity = Test::XdgPositioner::gravity_left, + }; + QTest::newRow("gravityLeft") << QSize(500, 500) << QPoint(300, 300) << layoutGravityLeft << Rect(550, 650, 200, 200); + + // ---------------------------------------------------------------- + // constrain and slide + // popup is still 200,200. window moved near edge of screen, popup always comes out towards the screen edge + + const PopupLayout layoutSlideTop{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_top, + .gravity = Test::XdgPositioner::gravity_top, + .constraint = Test::XdgPositioner::constraint_adjustment_slide_x | Test::XdgPositioner::constraint_adjustment_slide_y, + }; + QTest::newRow("constraintSlideTop") << QSize(500, 500) << QPoint(80, 80) << layoutSlideTop << Rect(80 + 250 - 100, 0, 200, 200); + + const PopupLayout layoutSlideLeft{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_left, + .gravity = Test::XdgPositioner::gravity_left, + .constraint = Test::XdgPositioner::constraint_adjustment_slide_x | Test::XdgPositioner::constraint_adjustment_slide_y, + }; + QTest::newRow("constraintSlideLeft") << QSize(500, 500) << QPoint(80, 80) << layoutSlideLeft << Rect(0, 80 + 250 - 100, 200, 200); + + const PopupLayout layoutSlideRight{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_right, + .gravity = Test::XdgPositioner::gravity_right, + .constraint = Test::XdgPositioner::constraint_adjustment_slide_x | Test::XdgPositioner::constraint_adjustment_slide_y, + }; + QTest::newRow("constraintSlideRight") << QSize(500, 500) << QPoint(700, 80) << layoutSlideRight << Rect(1280 - 200, 80 + 250 - 100, 200, 200); + + const PopupLayout layoutSlideBottom{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_bottom, + .gravity = Test::XdgPositioner::gravity_bottom, + .constraint = Test::XdgPositioner::constraint_adjustment_slide_x | Test::XdgPositioner::constraint_adjustment_slide_y, + }; + QTest::newRow("constraintSlideBottom") << QSize(500, 500) << QPoint(80, 500) << layoutSlideBottom << Rect(80 + 250 - 100, 1024 - 200, 200, 200); + + const PopupLayout layoutSlideBottomRight{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_bottom_right, + .gravity = Test::XdgPositioner::gravity_bottom_right, + .constraint = Test::XdgPositioner::constraint_adjustment_slide_x | Test::XdgPositioner::constraint_adjustment_slide_y, + }; + QTest::newRow("constraintSlideBottomRight") << QSize(500, 500) << QPoint(700, 1000) << layoutSlideBottomRight << Rect(1280 - 200, 1024 - 200, 200, 200); + + // ---------------------------------------------------------------- + // constrain and flip + + const PopupLayout layoutFlipTop{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_top, + .gravity = Test::XdgPositioner::gravity_top, + .constraint = Test::XdgPositioner::constraint_adjustment_flip_x | Test::XdgPositioner::constraint_adjustment_flip_y, + }; + QTest::newRow("constraintFlipTop") << QSize(500, 500) << QPoint(80, 80) << layoutFlipTop << Rect(230, 80 + 500 - 50, 200, 200); + + const PopupLayout layoutFlipLeft{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_left, + .gravity = Test::XdgPositioner::gravity_left, + .constraint = Test::XdgPositioner::constraint_adjustment_flip_x | Test::XdgPositioner::constraint_adjustment_flip_y, + }; + QTest::newRow("constraintFlipLeft") << QSize(500, 500) << QPoint(80, 80) << layoutFlipLeft << Rect(80 + 500 - 50, 230, 200, 200); + + const PopupLayout layoutFlipRight{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_right, + .gravity = Test::XdgPositioner::gravity_right, + .constraint = Test::XdgPositioner::constraint_adjustment_flip_x | Test::XdgPositioner::constraint_adjustment_flip_y, + }; + QTest::newRow("constraintFlipRight") << QSize(500, 500) << QPoint(700, 80) << layoutFlipRight << Rect(700 + 50 - 200, 230, 200, 200); + + const PopupLayout layoutFlipBottom{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_bottom, + .gravity = Test::XdgPositioner::gravity_bottom, + .constraint = Test::XdgPositioner::constraint_adjustment_flip_x | Test::XdgPositioner::constraint_adjustment_flip_y, + }; + QTest::newRow("constraintFlipBottom") << QSize(500, 500) << QPoint(80, 500) << layoutFlipBottom << Rect(230, 500 + 50 - 200, 200, 200); + + const PopupLayout layoutFlipBottomRight{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_bottom_right, + .gravity = Test::XdgPositioner::gravity_bottom_right, + .constraint = Test::XdgPositioner::constraint_adjustment_flip_x | Test::XdgPositioner::constraint_adjustment_flip_y, + }; + QTest::newRow("constraintFlipBottomRight") << QSize(500, 500) << QPoint(700, 500) << layoutFlipBottomRight << Rect(700 + 50 - 200, 500 + 50 - 200, 200, 200); + + const PopupLayout layoutFlipRightNoAnchor{ + .anchorRect = Rect(50, 50, 400, 400), + // as popup is positioned in the middle of the parent we need a massive popup to be able to overflow + .size = QSize(400, 400), + .anchor = Test::XdgPositioner::anchor_top, + .gravity = Test::XdgPositioner::gravity_right, + .constraint = Test::XdgPositioner::constraint_adjustment_flip_x | Test::XdgPositioner::constraint_adjustment_flip_y, + }; + QTest::newRow("constraintFlipRightNoAnchor") << QSize(500, 500) << QPoint(700, 80) << layoutFlipRightNoAnchor << Rect(700 + 250 - 400, 330, 400, 400); + + const PopupLayout layoutFlipRightNoGravity{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(300, 200), + .anchor = Test::XdgPositioner::anchor_right, + .gravity = Test::XdgPositioner::gravity_top, + .constraint = Test::XdgPositioner::constraint_adjustment_flip_x | Test::XdgPositioner::constraint_adjustment_flip_y, + }; + QTest::newRow("constraintFlipRightNoGravity") << QSize(500, 500) << QPoint(700, 80) << layoutFlipRightNoGravity << Rect(700 + 50 - 150, 130, 300, 200); + + // ---------------------------------------------------------------- + // resize + + const PopupLayout layoutResizeTop{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_top, + .gravity = Test::XdgPositioner::gravity_top, + .constraint = Test::XdgPositioner::constraint_adjustment_resize_x | Test::XdgPositioner::constraint_adjustment_resize_y, + }; + QTest::newRow("resizeTop") << QSize(500, 500) << QPoint(80, 80) << layoutResizeTop << Rect(80 + 250 - 100, 0, 200, 130); + + const PopupLayout layoutResizeLeft{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_left, + .gravity = Test::XdgPositioner::gravity_left, + .constraint = Test::XdgPositioner::constraint_adjustment_resize_x | Test::XdgPositioner::constraint_adjustment_resize_y, + }; + QTest::newRow("resizeLeft") << QSize(500, 500) << QPoint(80, 80) << layoutResizeLeft << Rect(0, 80 + 250 - 100, 130, 200); + + const PopupLayout layoutResizeRight{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_right, + .gravity = Test::XdgPositioner::gravity_right, + .constraint = Test::XdgPositioner::constraint_adjustment_resize_x | Test::XdgPositioner::constraint_adjustment_resize_y, + }; + QTest::newRow("resizeRight") << QSize(500, 500) << QPoint(700, 80) << layoutResizeRight << Rect(700 + 50 + 400, 80 + 250 - 100, 130, 200); + + const PopupLayout layoutResizeBottom{ + .anchorRect = Rect(50, 50, 400, 400), + .size = QSize(200, 200), + .anchor = Test::XdgPositioner::anchor_bottom, + .gravity = Test::XdgPositioner::gravity_bottom, + .constraint = Test::XdgPositioner::constraint_adjustment_resize_x | Test::XdgPositioner::constraint_adjustment_resize_y, + }; + QTest::newRow("resizeBottom") << QSize(500, 500) << QPoint(80, 500) << layoutResizeBottom << Rect(80 + 250 - 100, 500 + 50 + 400, 200, 74); +} + +void TransientPlacementTest::testXdgPopup() +{ + // this test verifies that the position of a transient window is taken from the passed position + // there are no further constraints like window too large to fit screen, cascading transients, etc + // some test cases also verify that the transient fits on the screen + QFETCH(QSize, parentSize); + QFETCH(QPoint, parentPosition); + QFETCH(Rect, expectedGeometry); + const Rect expectedRelativeGeometry = expectedGeometry.translated(-parentPosition); + + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr parentShellSurface = Test::createXdgToplevelSurface(surface.get()); + QVERIFY(parentShellSurface); + auto parent = Test::renderAndWaitForShown(surface.get(), parentSize, Qt::blue); + QVERIFY(parent); + + QVERIFY(!parent->isDecorated()); + parent->move(parentPosition); + QCOMPARE(parent->frameGeometry(), RectF(parentPosition, parentSize)); + + // create popup + QFETCH(PopupLayout, layout); + + std::unique_ptr transientSurface = Test::createSurface(); + QVERIFY(transientSurface); + + std::unique_ptr positioner(Test::createXdgPositioner()); + positioner->set_anchor_rect(layout.anchorRect.x(), layout.anchorRect.y(), layout.anchorRect.width(), layout.anchorRect.height()); + positioner->set_size(layout.size.width(), layout.size.height()); + positioner->set_anchor(layout.anchor); + positioner->set_gravity(layout.gravity); + positioner->set_constraint_adjustment(layout.constraint); + std::unique_ptr popup(Test::createXdgPopupSurface(transientSurface.get(), parentShellSurface->xdgSurface(), positioner.get(), Test::CreationSetup::CreateOnly)); + QSignalSpy popupConfigureRequestedSpy(popup.get(), &Test::XdgPopup::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(popup->xdgSurface(), &Test::XdgSurface::configureRequested); + transientSurface->commit(KWayland::Client::Surface::CommitFlag::None); + + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + QCOMPARE(popupConfigureRequestedSpy.last()[0].value(), expectedRelativeGeometry); + popup->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last()[0].toUInt()); + + auto transient = Test::renderAndWaitForShown(transientSurface.get(), expectedRelativeGeometry.size(), Qt::red); + QVERIFY(transient); + + QVERIFY(!transient->isDecorated()); + QCOMPARE(transient->frameGeometry(), expectedGeometry); + + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); // check that we did not get reconfigured +} + +void TransientPlacementTest::testXdgPopupWithPanel() +{ + const LogicalOutput *output = workspace()->activeOutput(); + + std::unique_ptr dockSurface{Test::createSurface()}; + std::unique_ptr dockShellSurface{Test::createLayerSurfaceV1(dockSurface.get(), QStringLiteral("dock"))}; + dockShellSurface->set_size(1280, 50); + dockShellSurface->set_anchor(Test::LayerSurfaceV1::anchor_bottom); + dockShellSurface->set_exclusive_zone(50); + dockSurface->commit(KWayland::Client::Surface::CommitFlag::None); + + // now render and map the window + QSignalSpy dockConfigureRequestedSpy(dockShellSurface.get(), &Test::LayerSurfaceV1::configureRequested); + QVERIFY(dockConfigureRequestedSpy.wait()); + auto dock = Test::renderAndWaitForShown(dockSurface.get(), dockConfigureRequestedSpy.last().at(1).toSize(), Qt::blue); + QVERIFY(dock); + QCOMPARE(dock->windowType(), WindowType::Dock); + QVERIFY(dock->isDock()); + QCOMPARE(dock->frameGeometry(), RectF(0, output->geometry().height() - 50, 1280, 50)); + QCOMPARE(dock->hasStrut(), true); + QCOMPARE(workspace()->clientArea(PlacementArea, dock), RectF(0, 0, 1280, 1024 - 50)); + QCOMPARE(workspace()->clientArea(FullScreenArea, dock), RectF(0, 0, 1280, 1024)); + + // create parent + std::unique_ptr parentSurface(Test::createSurface()); + QVERIFY(parentSurface); + auto parentShellSurface = Test::createXdgToplevelSurface(parentSurface.get()); + QVERIFY(parentShellSurface); + auto parent = Test::renderAndWaitForShown(parentSurface.get(), {800, 600}, Qt::blue); + QVERIFY(parent); + + QVERIFY(!parent->isDecorated()); + parent->move(QPointF(0, output->geometry().height() - 600)); + parent->moveResize(parent->keepInArea(parent->moveResizeGeometry(), workspace()->clientArea(PlacementArea, parent))); + QCOMPARE(parent->frameGeometry(), RectF(0, output->geometry().height() - 600 - 50, 800, 600)); + + std::unique_ptr transientSurface(Test::createSurface()); + QVERIFY(transientSurface); + + std::unique_ptr positioner(Test::createXdgPositioner()); + positioner->set_size(200, 200); + positioner->set_anchor_rect(50, 500, 200, 200); + positioner->set_constraint_adjustment(Test::XdgPositioner::constraint_adjustment_slide_x | Test::XdgPositioner::constraint_adjustment_slide_y); + + std::unique_ptr transientShellSurface(Test::createXdgPopupSurface(transientSurface.get(), parentShellSurface->xdgSurface(), positioner.get())); + auto transient = Test::renderAndWaitForShown(transientSurface.get(), QSize(200, 200), Qt::red); + QVERIFY(transient); + + QVERIFY(!transient->isDecorated()); + QCOMPARE(transient->frameGeometry(), RectF(50, output->geometry().height() - 200 - 50, 200, 200)); + + transientShellSurface.reset(); + transientSurface.reset(); + QVERIFY(Test::waitForWindowClosed(transient)); + + // now parent to fullscreen - on fullscreen the panel is ignored + QSignalSpy toplevelConfigureRequestedSpy(parentShellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(parentShellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + parent->setFullScreen(true); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + parentShellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + QSignalSpy frameGeometryChangedSpy{parent, &Window::frameGeometryChanged}; + Test::render(parentSurface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(parent->frameGeometry(), RectF(output->geometry())); + QVERIFY(parent->isFullScreen()); + + // another transient, with same hints as before from bottom of window + transientSurface = Test::createSurface(); + QVERIFY(transientSurface); + + const Rect anchorRect2(50, output->geometry().height() - 100, 200, 200); + std::unique_ptr positioner2(Test::createXdgPositioner()); + positioner2->set_size(200, 200); + positioner2->set_anchor_rect(anchorRect2.x(), anchorRect2.y(), anchorRect2.width(), anchorRect2.height()); + positioner2->set_constraint_adjustment(Test::XdgPositioner::constraint_adjustment_slide_x | Test::XdgPositioner::constraint_adjustment_slide_y); + transientShellSurface = Test::createXdgPopupSurface(transientSurface.get(), parentShellSurface->xdgSurface(), positioner2.get()); + transient = Test::renderAndWaitForShown(transientSurface.get(), QSize(200, 200), Qt::red); + QVERIFY(transient); + + QVERIFY(!transient->isDecorated()); + QCOMPARE(transient->frameGeometry(), RectF(50, output->geometry().height() - 200, 200, 200)); +} + +} + +WAYLANDTEST_MAIN(KWin::TransientPlacementTest) +#include "transient_placement.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/virtual_desktop_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/virtual_desktop_test.cpp new file mode 100644 index 0000000000..f44c0c090c --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/virtual_desktop_test.cpp @@ -0,0 +1,266 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "main.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#if KWIN_BUILD_X11 +#include "utils/xcbutils.h" +#endif + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_virtualdesktop-0"); + +class VirtualDesktopTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); +#if KWIN_BUILD_X11 + void testNetCurrentDesktop(); +#endif + void testLastDesktopRemoved(); + void testWindowOnMultipleDesktops(); + void testRemoveDesktopWithWindow(); +}; + +void VirtualDesktopTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + qputenv("KWIN_XKB_DEFAULT_KEYMAP", "1"); + qputenv("XKB_DEFAULT_RULES", "evdev"); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + +#if KWIN_BUILD_X11 + if (kwinApp()->x11Connection()) { + // verify the current desktop x11 property on startup, see BUG: 391034 + Xcb::Atom currentDesktopAtom("_NET_CURRENT_DESKTOP"); + QVERIFY(currentDesktopAtom.isValid()); + Xcb::Property currentDesktop(0, kwinApp()->x11RootWindow(), currentDesktopAtom, XCB_ATOM_CARDINAL, 0, 1); + QCOMPARE(currentDesktop.value(), 0); + } +#endif +} + +void VirtualDesktopTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); + workspace()->setActiveOutput(QPoint(640, 512)); + VirtualDesktopManager::self()->setCount(1); +} + +void VirtualDesktopTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +#if KWIN_BUILD_X11 +void VirtualDesktopTest::testNetCurrentDesktop() +{ + if (!kwinApp()->x11Connection()) { + QSKIP("Skipped on Wayland only"); + } + QCOMPARE(VirtualDesktopManager::self()->count(), 1u); + VirtualDesktopManager::self()->setCount(4); + QCOMPARE(VirtualDesktopManager::self()->count(), 4u); + + Xcb::Atom currentDesktopAtom("_NET_CURRENT_DESKTOP"); + QVERIFY(currentDesktopAtom.isValid()); + Xcb::Property currentDesktop(0, kwinApp()->x11RootWindow(), currentDesktopAtom, XCB_ATOM_CARDINAL, 0, 1); + QCOMPARE(currentDesktop.value(), 0); + + // go to desktop 2 + VirtualDesktopManager::self()->setCurrent(2); + currentDesktop = Xcb::Property(0, kwinApp()->x11RootWindow(), currentDesktopAtom, XCB_ATOM_CARDINAL, 0, 1); + QCOMPARE(currentDesktop.value(), 1); + + // go to desktop 3 + VirtualDesktopManager::self()->setCurrent(3); + currentDesktop = Xcb::Property(0, kwinApp()->x11RootWindow(), currentDesktopAtom, XCB_ATOM_CARDINAL, 0, 1); + QCOMPARE(currentDesktop.value(), 2); + + // go to desktop 4 + VirtualDesktopManager::self()->setCurrent(4); + currentDesktop = Xcb::Property(0, kwinApp()->x11RootWindow(), currentDesktopAtom, XCB_ATOM_CARDINAL, 0, 1); + QCOMPARE(currentDesktop.value(), 3); + + // and back to first + VirtualDesktopManager::self()->setCurrent(1); + currentDesktop = Xcb::Property(0, kwinApp()->x11RootWindow(), currentDesktopAtom, XCB_ATOM_CARDINAL, 0, 1); + QCOMPARE(currentDesktop.value(), 0); +} +#endif + +void VirtualDesktopTest::testLastDesktopRemoved() +{ + // first create a new desktop + QCOMPARE(VirtualDesktopManager::self()->count(), 1u); + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(VirtualDesktopManager::self()->count(), 2u); + + // switch to last desktop + VirtualDesktopManager::self()->setCurrent(VirtualDesktopManager::self()->desktops().last()); + QCOMPARE(VirtualDesktopManager::self()->current(), 2u); + + // now create a window on this desktop + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + QVERIFY(window); + QCOMPARE(window->desktops().count(), 1u); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), window->desktops().first()); + + // and remove last desktop + VirtualDesktopManager::self()->setCount(1); + QCOMPARE(VirtualDesktopManager::self()->count(), 1u); + // now the window should be moved as well + QCOMPARE(window->desktops().count(), 1u); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), window->desktops().first()); +} + +void VirtualDesktopTest::testWindowOnMultipleDesktops() +{ + // first create two new desktops + QCOMPARE(VirtualDesktopManager::self()->count(), 1u); + VirtualDesktopManager::self()->setCount(3); + QCOMPARE(VirtualDesktopManager::self()->count(), 3u); + + // switch to last desktop + const auto desktops = VirtualDesktopManager::self()->desktops(); + VirtualDesktopManager::self()->setCurrent(desktops.at(2)); + + // now create a window on this desktop + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QCOMPARE(window->desktops(), (QList{desktops.at(2)})); + + // Set the window on desktop 2 as well + window->enterDesktop(VirtualDesktopManager::self()->desktopForX11Id(2)); + QCOMPARE(window->desktops().count(), 2u); + QCOMPARE(window->desktops()[0], desktops.at(2)); + QCOMPARE(window->desktops()[1], desktops.at(1)); + + // leave desktop 3 + window->leaveDesktop(desktops.at(2)); + QCOMPARE(window->desktops(), (QList{desktops.at(1)})); + // leave desktop 2 + window->leaveDesktop(desktops.at(1)); + QCOMPARE(window->desktops(), QList{}); + // we should be on all desktops now + QVERIFY(window->isOnAllDesktops()); + // put on desktop 1 + window->enterDesktop(desktops.at(0)); + QVERIFY(window->isOnDesktop(desktops.at(0))); + QVERIFY(!window->isOnDesktop(desktops.at(1))); + QVERIFY(!window->isOnDesktop(desktops.at(2))); + QCOMPARE(window->desktops().count(), 1u); + // put on desktop 2 + window->enterDesktop(desktops.at(1)); + QVERIFY(window->isOnDesktop(desktops.at(0))); + QVERIFY(window->isOnDesktop(desktops.at(1))); + QVERIFY(!window->isOnDesktop(desktops.at(2))); + QCOMPARE(window->desktops().count(), 2u); + // put on desktop 3 + window->enterDesktop(desktops.at(2)); + QVERIFY(window->isOnDesktop(desktops.at(0))); + QVERIFY(window->isOnDesktop(desktops.at(1))); + QVERIFY(window->isOnDesktop(desktops.at(2))); + QCOMPARE(window->desktops().count(), 3u); + + // entering twice dooes nothing + window->enterDesktop(desktops.at(2)); + QCOMPARE(window->desktops().count(), 3u); + + // adding to "all desktops" results in just that one desktop + window->setOnAllDesktops(true); + QCOMPARE(window->desktops().count(), 0u); + window->enterDesktop(desktops.at(2)); + QVERIFY(window->isOnDesktop(desktops.at(2))); + QCOMPARE(window->desktops().count(), 1u); + + // leaving a desktop on "all desktops" puts on everything else + window->setOnAllDesktops(true); + QCOMPARE(window->desktops().count(), 0u); + window->leaveDesktop(desktops.at(2)); + QVERIFY(window->isOnDesktop(desktops.at(0))); + QVERIFY(window->isOnDesktop(desktops.at(1))); + QCOMPARE(window->desktops().count(), 2u); +} + +void VirtualDesktopTest::testRemoveDesktopWithWindow() +{ + // first create two new desktops + QCOMPARE(VirtualDesktopManager::self()->count(), 1u); + VirtualDesktopManager::self()->setCount(3); + QCOMPARE(VirtualDesktopManager::self()->count(), 3u); + + // switch to last desktop + VirtualDesktopManager::self()->setCurrent(VirtualDesktopManager::self()->desktops().last()); + QCOMPARE(VirtualDesktopManager::self()->current(), 3u); + + // now create a window on this desktop + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + QVERIFY(window); + + QCOMPARE(window->desktops().count(), 1u); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), window->desktops().first()); + + // Set the window on desktop 2 as well + window->enterDesktop(VirtualDesktopManager::self()->desktops()[1]); + QCOMPARE(window->desktops().count(), 2u); + QCOMPARE(VirtualDesktopManager::self()->desktops()[2], window->desktops()[0]); + QCOMPARE(VirtualDesktopManager::self()->desktops()[1], window->desktops()[1]); + + // remove desktop 3 + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(window->desktops().count(), 1u); + // window is only on desktop 2 + QCOMPARE(VirtualDesktopManager::self()->desktops()[1], window->desktops()[0]); + + // Again 3 desktops + VirtualDesktopManager::self()->setCount(3); + // move window to be only on desktop 3 + window->enterDesktop(VirtualDesktopManager::self()->desktops()[2]); + window->leaveDesktop(VirtualDesktopManager::self()->desktops()[1]); + QCOMPARE(window->desktops().count(), 1u); + // window is only on desktop 3 + QCOMPARE(VirtualDesktopManager::self()->desktops()[2], window->desktops()[0]); + + // remove desktop 3 + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(window->desktops().count(), 1u); + // window is only on desktop 2 + QCOMPARE(VirtualDesktopManager::self()->desktops()[1], window->desktops()[0]); +} + +WAYLANDTEST_MAIN(VirtualDesktopTest) +#include "virtual_desktop_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/window_rules_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/window_rules_test.cpp new file mode 100644 index 0000000000..deb8d97a05 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/window_rules_test.cpp @@ -0,0 +1,204 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "atoms.h" +#include "core/output.h" +#include "pointer_input.h" +#include "rules.h" +#include "wayland_server.h" +#include "workspace.h" +#include "x11window.h" + +#include +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_window_rules-0"); + +class WindowRuleTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testApplyInitialMaximizeVert(); + void testWindowClassChange(); +}; + +void WindowRuleTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); +} + +void WindowRuleTest::init() +{ + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); + QVERIFY(waylandServer()->windows().isEmpty()); +} + +void WindowRuleTest::cleanup() +{ + // discards old rules + workspace()->rulebook()->load(); +} + +struct XcbConnectionDeleter +{ + void operator()(xcb_connection_t *pointer) + { + xcb_disconnect(pointer); + } +}; + +void WindowRuleTest::testApplyInitialMaximizeVert() +{ + // this test creates the situation of BUG 367554: creates a window and initial apply maximize vertical + // the window is matched by class and role + // load the rule + workspace()->rulebook()->setConfig(KSharedConfig::openConfig(QFINDTESTDATA("./data/rules/maximize-vert-apply-initial"), KConfig::SimpleConfig)); + workspace()->slotReconfigure(); + + // create the test window + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + + xcb_window_t windowId = xcb_generate_id(c.get()); + const Rect windowGeometry = Rect(0, 0, 10, 20); + const uint32_t values[] = { + XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW}; + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, XCB_CW_EVENT_MASK, values); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + xcb_icccm_set_wm_class(c.get(), windowId, 9, "kpat\0kpat"); + + const QByteArray role = QByteArrayLiteral("mainwindow"); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_window_role, XCB_ATOM_STRING, 8, role.length(), role.constData()); + + NETWinInfo info(c.get(), windowId, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); + info.setWindowType(NET::Normal); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.last().first().value(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + QVERIFY(!window->hasStrut()); + QVERIFY(!window->readyForPainting()); + QMetaObject::invokeMethod(window, "setReadyForPainting"); + QVERIFY(window->readyForPainting()); + QVERIFY(Test::waitForWaylandSurface(window)); + QCOMPARE(window->maximizeMode(), MaximizeVertical); + + // destroy window again + QSignalSpy windowClosedSpy(window, &X11Window::closed); + xcb_unmap_window(c.get(), windowId); + xcb_destroy_window(c.get(), windowId); + xcb_flush(c.get()); + QVERIFY(windowClosedSpy.wait()); +} + +void WindowRuleTest::testWindowClassChange() +{ + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + const QString ruleGroupName = QStringLiteral("above-test-rule"); + config->group(QStringLiteral("General")).writeEntry("rules", QStringList({ruleGroupName})); + + auto group = config->group(ruleGroupName); + group.writeEntry("above", true); + group.writeEntry("aboverule", 2); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", 1); + group.sync(); + + workspace()->rulebook()->setConfig(config); + workspace()->slotReconfigure(); + + // create the test window + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + + xcb_window_t windowId = xcb_generate_id(c.get()); + const Rect windowGeometry = Rect(0, 0, 10, 20); + const uint32_t values[] = { + XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW}; + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, XCB_CW_EVENT_MASK, values); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + xcb_icccm_set_wm_class(c.get(), windowId, 23, "org.kde.bar\0org.kde.bar"); + + NETWinInfo info(c.get(), windowId, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); + info.setWindowType(NET::Normal); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.last().first().value(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + QVERIFY(!window->hasStrut()); + QVERIFY(!window->readyForPainting()); + QMetaObject::invokeMethod(window, "setReadyForPainting"); + QVERIFY(window->readyForPainting()); + QVERIFY(Test::waitForWaylandSurface(window)); + QCOMPARE(window->keepAbove(), false); + + // now change class + QSignalSpy windowClassChangedSpy{window, &X11Window::windowClassChanged}; + xcb_icccm_set_wm_class(c.get(), windowId, 23, "org.kde.foo\0org.kde.foo"); + xcb_flush(c.get()); + QVERIFY(windowClassChangedSpy.wait()); + QCOMPARE(window->keepAbove(), true); + + // destroy window + QSignalSpy windowClosedSpy(window, &X11Window::closed); + xcb_unmap_window(c.get(), windowId); + xcb_destroy_window(c.get(), windowId); + xcb_flush(c.get()); + QVERIFY(windowClosedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::WindowRuleTest) +#include "window_rules_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/window_selection_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/window_selection_test.cpp new file mode 100644 index 0000000000..cade4a855a --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/window_selection_test.cpp @@ -0,0 +1,517 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "keyboard_input.h" +#include "pointer_input.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_window_selection-0"); + +class TestWindowSelection : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testSelectOnWindowPointer(); + void testSelectOnWindowKeyboard_data(); + void testSelectOnWindowKeyboard(); + void testSelectOnWindowTouch(); + void testCancelOnWindowPointer(); + void testCancelOnWindowKeyboard(); + + void testSelectPointPointer(); + void testSelectPointTouch(); +}; + +void TestWindowSelection::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + qputenv("XKB_DEFAULT_RULES", "evdev"); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); +} + +void TestWindowSelection::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); + QVERIFY(Test::waitForWaylandPointer()); + + workspace()->setActiveOutput(QPoint(640, 512)); + KWin::input()->pointer()->warp(QPoint(640, 512)); +} + +void TestWindowSelection::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TestWindowSelection::testSelectOnWindowPointer() +{ + // this test verifies window selection through pointer works + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + std::unique_ptr pointer(Test::waylandSeat()->createPointer()); + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy pointerEnteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); + QSignalSpy pointerLeftSpy(pointer.get(), &KWayland::Client::Pointer::left); + QSignalSpy keyboardEnteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); + QSignalSpy keyboardLeftSpy(keyboard.get(), &KWayland::Client::Keyboard::left); + + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(keyboardEnteredSpy.wait()); + KWin::input()->pointer()->warp(window->frameGeometry().center()); + QCOMPARE(input()->pointer()->focus(), window); + QVERIFY(pointerEnteredSpy.wait()); + + Window *selectedWindow = nullptr; + auto callback = [&selectedWindow](Window *t) { + selectedWindow = t; + }; + + // start the interaction + QCOMPARE(input()->isSelectingWindow(), false); + kwinApp()->startInteractiveWindowSelection(callback); + QCOMPARE(input()->isSelectingWindow(), true); + QVERIFY(!selectedWindow); + QCOMPARE(keyboardLeftSpy.count(), 0); + QVERIFY(pointerLeftSpy.wait()); + if (keyboardLeftSpy.isEmpty()) { + QVERIFY(keyboardLeftSpy.wait()); + } + QCOMPARE(pointerLeftSpy.count(), 1); + QCOMPARE(keyboardLeftSpy.count(), 1); + + // simulate left button press + quint32 timestamp = 0; + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + // should not have ended the mode + QCOMPARE(input()->isSelectingWindow(), true); + QVERIFY(!selectedWindow); + QVERIFY(!input()->pointer()->focus()); + + // updating the pointer should not change anything + input()->pointer()->update(); + QVERIFY(!input()->pointer()->focus()); + // updating keyboard should also not change + input()->keyboard()->update(); + + // perform a right button click + Test::pointerButtonPressed(BTN_RIGHT, timestamp++); + Test::pointerButtonReleased(BTN_RIGHT, timestamp++); + // should not have ended the mode + QCOMPARE(input()->isSelectingWindow(), true); + QVERIFY(!selectedWindow); + // now release + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QCOMPARE(selectedWindow, window); + QCOMPARE(input()->pointer()->focus(), window); + // should give back keyboard and pointer + QVERIFY(pointerEnteredSpy.wait()); + if (keyboardEnteredSpy.count() != 2) { + QVERIFY(keyboardEnteredSpy.wait()); + } + QCOMPARE(pointerLeftSpy.count(), 1); + QCOMPARE(keyboardLeftSpy.count(), 1); + QCOMPARE(pointerEnteredSpy.count(), 2); + QCOMPARE(keyboardEnteredSpy.count(), 2); +} + +void TestWindowSelection::testSelectOnWindowKeyboard_data() +{ + QTest::addColumn("key"); + + QTest::newRow("enter") << KEY_ENTER; + QTest::newRow("keypad enter") << KEY_KPENTER; + QTest::newRow("space") << KEY_SPACE; +} + +void TestWindowSelection::testSelectOnWindowKeyboard() +{ + // this test verifies window selection through keyboard key + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + std::unique_ptr pointer(Test::waylandSeat()->createPointer()); + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy pointerEnteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); + QSignalSpy pointerLeftSpy(pointer.get(), &KWayland::Client::Pointer::left); + QSignalSpy keyboardEnteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); + QSignalSpy keyboardLeftSpy(keyboard.get(), &KWayland::Client::Keyboard::left); + + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(keyboardEnteredSpy.wait()); + QVERIFY(!window->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos())); + + Window *selectedWindow = nullptr; + auto callback = [&selectedWindow](Window *t) { + selectedWindow = t; + }; + + // start the interaction + QCOMPARE(input()->isSelectingWindow(), false); + kwinApp()->startInteractiveWindowSelection(callback); + QCOMPARE(input()->isSelectingWindow(), true); + QVERIFY(!selectedWindow); + QCOMPARE(keyboardLeftSpy.count(), 0); + QVERIFY(keyboardLeftSpy.wait()); + QCOMPARE(pointerLeftSpy.count(), 0); + QCOMPARE(keyboardLeftSpy.count(), 1); + + // simulate key press + quint32 timestamp = 0; + // move cursor through keys + auto keyPress = [×tamp](qint32 key) { + Test::keyboardKeyPressed(key, timestamp++); + Test::keyboardKeyReleased(key, timestamp++); + }; + while (KWin::Cursors::self()->mouse()->pos().x() >= window->frameGeometry().x() + window->frameGeometry().width()) { + keyPress(KEY_LEFT); + } + while (KWin::Cursors::self()->mouse()->pos().x() <= window->frameGeometry().x()) { + keyPress(KEY_RIGHT); + } + while (KWin::Cursors::self()->mouse()->pos().y() <= window->frameGeometry().y()) { + keyPress(KEY_DOWN); + } + while (KWin::Cursors::self()->mouse()->pos().y() >= window->frameGeometry().y() + window->frameGeometry().height()) { + keyPress(KEY_UP); + } + QFETCH(qint32, key); + Test::keyboardKeyPressed(key, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QCOMPARE(selectedWindow, window); + QCOMPARE(input()->pointer()->focus(), window); + // should give back keyboard and pointer + QVERIFY(pointerEnteredSpy.wait()); + if (keyboardEnteredSpy.count() != 2) { + QVERIFY(keyboardEnteredSpy.wait()); + } + QCOMPARE(pointerLeftSpy.count(), 0); + QCOMPARE(keyboardLeftSpy.count(), 1); + QCOMPARE(pointerEnteredSpy.count(), 1); + QCOMPARE(keyboardEnteredSpy.count(), 2); + Test::keyboardKeyReleased(key, timestamp++); +} + +void TestWindowSelection::testSelectOnWindowTouch() +{ + // this test verifies window selection through touch + std::unique_ptr touch(Test::waylandSeat()->createTouch()); + QSignalSpy touchStartedSpy(touch.get(), &KWayland::Client::Touch::sequenceStarted); + QSignalSpy touchCanceledSpy(touch.get(), &KWayland::Client::Touch::sequenceCanceled); + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + Window *selectedWindow = nullptr; + auto callback = [&selectedWindow](Window *t) { + selectedWindow = t; + }; + + // start the interaction + QCOMPARE(input()->isSelectingWindow(), false); + kwinApp()->startInteractiveWindowSelection(callback); + QCOMPARE(input()->isSelectingWindow(), true); + QVERIFY(!selectedWindow); + + // simulate touch down + quint32 timestamp = 0; + Test::touchDown(0, window->frameGeometry().center(), timestamp++); + QVERIFY(!selectedWindow); + Test::touchUp(0, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QCOMPARE(selectedWindow, window); + + // with movement + selectedWindow = nullptr; + kwinApp()->startInteractiveWindowSelection(callback); + Test::touchDown(0, window->frameGeometry().bottomRight() + QPoint(20, 20), timestamp++); + QVERIFY(!selectedWindow); + Test::touchMotion(0, window->frameGeometry().bottomRight() - QPoint(1, 1), timestamp++); + QVERIFY(!selectedWindow); + Test::touchUp(0, timestamp++); + QCOMPARE(selectedWindow, window); + QCOMPARE(input()->isSelectingWindow(), false); + + // it cancels active touch sequence on the window + Test::touchDown(0, window->frameGeometry().center(), timestamp++); + QVERIFY(touchStartedSpy.wait()); + selectedWindow = nullptr; + kwinApp()->startInteractiveWindowSelection(callback); + QCOMPARE(input()->isSelectingWindow(), true); + QVERIFY(touchCanceledSpy.wait()); + QVERIFY(!selectedWindow); + // this touch up does not yet select the window, it was started prior to the selection + Test::touchUp(0, timestamp++); + QVERIFY(!selectedWindow); + Test::touchDown(0, window->frameGeometry().center(), timestamp++); + Test::touchUp(0, timestamp++); + QCOMPARE(selectedWindow, window); + QCOMPARE(input()->isSelectingWindow(), false); + + QCOMPARE(touchStartedSpy.count(), 1); + QCOMPARE(touchCanceledSpy.count(), 1); +} + +void TestWindowSelection::testCancelOnWindowPointer() +{ + // this test verifies that window selection cancels through right button click + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + std::unique_ptr pointer(Test::waylandSeat()->createPointer()); + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy pointerEnteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); + QSignalSpy pointerLeftSpy(pointer.get(), &KWayland::Client::Pointer::left); + QSignalSpy keyboardEnteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); + QSignalSpy keyboardLeftSpy(keyboard.get(), &KWayland::Client::Keyboard::left); + + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(keyboardEnteredSpy.wait()); + KWin::input()->pointer()->warp(window->frameGeometry().center()); + QCOMPARE(input()->pointer()->focus(), window); + QVERIFY(pointerEnteredSpy.wait()); + + Window *selectedWindow = nullptr; + auto callback = [&selectedWindow](Window *t) { + selectedWindow = t; + }; + + // start the interaction + QCOMPARE(input()->isSelectingWindow(), false); + kwinApp()->startInteractiveWindowSelection(callback); + QCOMPARE(input()->isSelectingWindow(), true); + QVERIFY(!selectedWindow); + QCOMPARE(keyboardLeftSpy.count(), 0); + QVERIFY(pointerLeftSpy.wait()); + if (keyboardLeftSpy.isEmpty()) { + QVERIFY(keyboardLeftSpy.wait()); + } + QCOMPARE(pointerLeftSpy.count(), 1); + QCOMPARE(keyboardLeftSpy.count(), 1); + + // simulate left button press + quint32 timestamp = 0; + Test::pointerButtonPressed(BTN_RIGHT, timestamp++); + Test::pointerButtonReleased(BTN_RIGHT, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QVERIFY(!selectedWindow); + QCOMPARE(input()->pointer()->focus(), window); + // should give back keyboard and pointer + QVERIFY(pointerEnteredSpy.wait()); + if (keyboardEnteredSpy.count() != 2) { + QVERIFY(keyboardEnteredSpy.wait()); + } + QCOMPARE(pointerLeftSpy.count(), 1); + QCOMPARE(keyboardLeftSpy.count(), 1); + QCOMPARE(pointerEnteredSpy.count(), 2); + QCOMPARE(keyboardEnteredSpy.count(), 2); +} + +void TestWindowSelection::testCancelOnWindowKeyboard() +{ + // this test verifies that cancel window selection through escape key works + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + std::unique_ptr pointer(Test::waylandSeat()->createPointer()); + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy pointerEnteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); + QSignalSpy pointerLeftSpy(pointer.get(), &KWayland::Client::Pointer::left); + QSignalSpy keyboardEnteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); + QSignalSpy keyboardLeftSpy(keyboard.get(), &KWayland::Client::Keyboard::left); + + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(keyboardEnteredSpy.wait()); + KWin::input()->pointer()->warp(window->frameGeometry().center()); + QCOMPARE(input()->pointer()->focus(), window); + QVERIFY(pointerEnteredSpy.wait()); + + Window *selectedWindow = nullptr; + auto callback = [&selectedWindow](Window *t) { + selectedWindow = t; + }; + + // start the interaction + QCOMPARE(input()->isSelectingWindow(), false); + kwinApp()->startInteractiveWindowSelection(callback); + QCOMPARE(input()->isSelectingWindow(), true); + QVERIFY(!selectedWindow); + QCOMPARE(keyboardLeftSpy.count(), 0); + QVERIFY(pointerLeftSpy.wait()); + if (keyboardLeftSpy.isEmpty()) { + QVERIFY(keyboardLeftSpy.wait()); + } + QCOMPARE(pointerLeftSpy.count(), 1); + QCOMPARE(keyboardLeftSpy.count(), 1); + + // simulate left button press + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_ESC, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QVERIFY(!selectedWindow); + QCOMPARE(input()->pointer()->focus(), window); + // should give back keyboard and pointer + QVERIFY(pointerEnteredSpy.wait()); + if (keyboardEnteredSpy.count() != 2) { + QVERIFY(keyboardEnteredSpy.wait()); + } + QCOMPARE(pointerLeftSpy.count(), 1); + QCOMPARE(keyboardLeftSpy.count(), 1); + QCOMPARE(pointerEnteredSpy.count(), 2); + QCOMPARE(keyboardEnteredSpy.count(), 2); + Test::keyboardKeyReleased(KEY_ESC, timestamp++); +} + +void TestWindowSelection::testSelectPointPointer() +{ + // this test verifies point selection through pointer works + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + std::unique_ptr pointer(Test::waylandSeat()->createPointer()); + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy pointerEnteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); + QSignalSpy pointerLeftSpy(pointer.get(), &KWayland::Client::Pointer::left); + QSignalSpy keyboardEnteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered); + QSignalSpy keyboardLeftSpy(keyboard.get(), &KWayland::Client::Keyboard::left); + + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(keyboardEnteredSpy.wait()); + KWin::input()->pointer()->warp(window->frameGeometry().center()); + QCOMPARE(input()->pointer()->focus(), window); + QVERIFY(pointerEnteredSpy.wait()); + + // start the interaction + QCOMPARE(input()->isSelectingWindow(), false); + QPointF point; + kwinApp()->startInteractivePositionSelection([&point](const QPointF &p) { + point = p; + }); + QCOMPARE(input()->isSelectingWindow(), true); + QCOMPARE(point, QPoint()); + QCOMPARE(keyboardLeftSpy.count(), 0); + QVERIFY(pointerLeftSpy.wait()); + if (keyboardLeftSpy.isEmpty()) { + QVERIFY(keyboardLeftSpy.wait()); + } + QCOMPARE(pointerLeftSpy.count(), 1); + QCOMPARE(keyboardLeftSpy.count(), 1); + + // trying again should not be allowed + QPointF point2; + kwinApp()->startInteractivePositionSelection([&point2](const QPointF &p) { + point2 = p; + }); + QCOMPARE(point2, QPoint(-1, -1)); + + // simulate left button press + quint32 timestamp = 0; + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + // should not have ended the mode + QCOMPARE(input()->isSelectingWindow(), true); + QCOMPARE(point, QPoint()); + QVERIFY(!input()->pointer()->focus()); + + // updating the pointer should not change anything + input()->pointer()->update(); + QVERIFY(!input()->pointer()->focus()); + // updating keyboard should also not change + input()->keyboard()->update(); + + // perform a right button click + Test::pointerButtonPressed(BTN_RIGHT, timestamp++); + Test::pointerButtonReleased(BTN_RIGHT, timestamp++); + // should not have ended the mode + QCOMPARE(input()->isSelectingWindow(), true); + QCOMPARE(point, QPoint()); + // now release + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QCOMPARE(point, input()->globalPointer().toPoint()); + QCOMPARE(input()->pointer()->focus(), window); + // should give back keyboard and pointer + QVERIFY(pointerEnteredSpy.wait()); + if (keyboardEnteredSpy.count() != 2) { + QVERIFY(keyboardEnteredSpy.wait()); + } + QCOMPARE(pointerLeftSpy.count(), 1); + QCOMPARE(keyboardLeftSpy.count(), 1); + QCOMPARE(pointerEnteredSpy.count(), 2); + QCOMPARE(keyboardEnteredSpy.count(), 2); +} + +void TestWindowSelection::testSelectPointTouch() +{ + // this test verifies point selection through touch works + // start the interaction + QCOMPARE(input()->isSelectingWindow(), false); + QPointF point; + kwinApp()->startInteractivePositionSelection([&point](const QPointF &p) { + point = p; + }); + QCOMPARE(input()->isSelectingWindow(), true); + QCOMPARE(point, QPoint()); + + // let's create multiple touch points + quint32 timestamp = 0; + Test::touchDown(0, QPointF(0, 1), timestamp++); + QCOMPARE(input()->isSelectingWindow(), true); + Test::touchDown(1, QPointF(10, 20), timestamp++); + QCOMPARE(input()->isSelectingWindow(), true); + Test::touchDown(2, QPointF(30, 40), timestamp++); + QCOMPARE(input()->isSelectingWindow(), true); + + // let's move our points + Test::touchMotion(0, QPointF(5, 10), timestamp++); + Test::touchMotion(2, QPointF(20, 25), timestamp++); + Test::touchMotion(1, QPointF(25, 35), timestamp++); + QCOMPARE(input()->isSelectingWindow(), true); + Test::touchUp(0, timestamp++); + QCOMPARE(input()->isSelectingWindow(), true); + Test::touchUp(2, timestamp++); + QCOMPARE(input()->isSelectingWindow(), true); + Test::touchUp(1, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QCOMPARE(point, QPoint(25, 35)); +} + +WAYLANDTEST_MAIN(TestWindowSelection) +#include "window_selection_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/workspace_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/workspace_test.cpp new file mode 100644 index 0000000000..a86ee727ab --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/workspace_test.cpp @@ -0,0 +1,399 @@ +/* + SPDX-FileCopyrightText: 2024 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "core/outputbackend.h" +#include "core/outputconfiguration.h" +#include "pointer_input.h" +#include "wayland_server.h" +#include "workspace.h" + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_workspace-0"); + +class WorkspaceTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void evacuateMappedWindowFromRemovedOutput(); + void evacuateUnmappedWindowFromRemovedOutput(); + void activeOutputFollowsPointer(); + void activeOutputFollowsTouch(); + void activeOutputFollowsTablet(); + void activeOutputFollowsActiveWindow(); + void activeOutputDoesntFollowInactiveWindow(); + void disableActiveOutput(); + void activeOutputAfterActivateNextWindowOnOutputAdded(); + void activeOutputAfterActivateNextWindowOnOutputRemoved_data(); + void activeOutputAfterActivateNextWindowOnOutputRemoved(); +}; + +void WorkspaceTest::initTestCase() +{ + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->start(); +} + +void WorkspaceTest::init() +{ + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + + QVERIFY(Test::setupWaylandConnection()); + + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); +} + +void WorkspaceTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void WorkspaceTest::evacuateMappedWindowFromRemovedOutput() +{ + // This test verifies that a window will be evacuated to another output if the output it is + // currently on has been either removed or disabled. + + const auto firstOutput = workspace()->outputs()[0]; + const auto secondOutput = workspace()->outputs()[1]; + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QCOMPARE(window->output(), firstOutput); + QCOMPARE(window->moveResizeOutput(), firstOutput); + + QSignalSpy outputChangedSpy(window, &Window::outputChanged); + { + OutputConfiguration config; + config.changeSet(firstOutput->backendOutput())->enabled = false; + workspace()->applyOutputConfiguration(config); + } + QCOMPARE(outputChangedSpy.count(), 1); + QCOMPARE(window->output(), secondOutput); + QCOMPARE(window->moveResizeOutput(), secondOutput); +} + +void WorkspaceTest::evacuateUnmappedWindowFromRemovedOutput() +{ + // This test verifies that a window, which is not fully managed by the Workspace yet, will be + // evacuated to another output if the output it is currently on has been withdrawn. + + const auto firstOutput = workspace()->outputs()[0]; + const auto secondOutput = workspace()->outputs()[1]; + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(Test::waylandSync()); + QCOMPARE(waylandServer()->windows().count(), 1); + Window *window = waylandServer()->windows().constFirst(); + QVERIFY(!window->readyForPainting()); + QCOMPARE(window->output(), firstOutput); + QCOMPARE(window->moveResizeOutput(), firstOutput); + + QSignalSpy outputChangedSpy(window, &Window::outputChanged); + { + OutputConfiguration config; + config.changeSet(firstOutput->backendOutput())->enabled = false; + workspace()->applyOutputConfiguration(config); + } + QCOMPARE(outputChangedSpy.count(), 1); + QCOMPARE(window->output(), secondOutput); + QCOMPARE(window->moveResizeOutput(), secondOutput); +} + +void WorkspaceTest::activeOutputFollowsPointer() +{ + // This test verifies that the active output follows pointer input. + + const auto firstOutput = workspace()->outputs()[0]; + const auto secondOutput = workspace()->outputs()[1]; + + quint32 timestamp = 0; + Test::pointerMotion(firstOutput->geometry().center(), timestamp++); + QCOMPARE(workspace()->activeOutput(), firstOutput); + + Test::pointerMotion(secondOutput->geometry().center(), timestamp++); + QCOMPARE(workspace()->activeOutput(), secondOutput); + + Test::pointerMotion(firstOutput->geometry().center(), timestamp++); + QCOMPARE(workspace()->activeOutput(), firstOutput); +} + +void WorkspaceTest::activeOutputFollowsTouch() +{ + // This test verifies that the active output follows touch input. + + const auto firstOutput = workspace()->outputs()[0]; + const auto secondOutput = workspace()->outputs()[1]; + + quint32 timestamp = 0; + + { + Test::touchDown(0, firstOutput->geometry().center(), timestamp++); + QCOMPARE(workspace()->activeOutput(), firstOutput); + + Test::touchMotion(0, firstOutput->geometry().center() + QPointF(10, 10), timestamp++); + QCOMPARE(workspace()->activeOutput(), firstOutput); + + Test::touchUp(0, timestamp++); + QCOMPARE(workspace()->activeOutput(), firstOutput); + } + + { + Test::touchDown(0, secondOutput->geometry().center(), timestamp++); + QCOMPARE(workspace()->activeOutput(), secondOutput); + + Test::touchMotion(0, secondOutput->geometry().center() + QPointF(10, 10), timestamp++); + QCOMPARE(workspace()->activeOutput(), secondOutput); + + Test::touchUp(0, timestamp++); + QCOMPARE(workspace()->activeOutput(), secondOutput); + } +} + +void WorkspaceTest::activeOutputFollowsTablet() +{ + // This test verifies that the active output follows tablet input. + + const auto firstOutput = workspace()->outputs()[0]; + const auto secondOutput = workspace()->outputs()[1]; + + quint32 timestamp = 0; + Test::tabletToolProximityEvent(firstOutput->geometry().center(), 0, 0, 0, 0, true, 0, timestamp++); + QCOMPARE(workspace()->activeOutput(), firstOutput); + + Test::tabletToolTipEvent(firstOutput->geometry().center(), 1, 0, 0, 0, 0, true, 0, timestamp++); + QCOMPARE(workspace()->activeOutput(), firstOutput); + + Test::tabletToolAxisEvent(firstOutput->geometry().center(), 1, 0, 0, 0, 0, true, 0, timestamp++); + QCOMPARE(workspace()->activeOutput(), firstOutput); + + Test::tabletToolAxisEvent(secondOutput->geometry().center(), 1, 0, 0, 0, 0, true, 0, timestamp++); + QCOMPARE(workspace()->activeOutput(), secondOutput); + + Test::tabletToolAxisEvent(firstOutput->geometry().center(), 1, 0, 0, 0, 0, true, 0, timestamp++); + QCOMPARE(workspace()->activeOutput(), firstOutput); + + Test::tabletToolTipEvent(firstOutput->geometry().center(), 0, 0, 0, 0, 0, false, 0, timestamp++); + QCOMPARE(workspace()->activeOutput(), firstOutput); + + Test::tabletToolProximityEvent(firstOutput->geometry().center(), 0, 0, 0, 0, false, 0, timestamp++); + QCOMPARE(workspace()->activeOutput(), firstOutput); +} + +void WorkspaceTest::activeOutputFollowsActiveWindow() +{ + // This test verifies that the active output follows the active window. + + const auto firstOutput = workspace()->outputs()[0]; + const auto secondOutput = workspace()->outputs()[1]; + + std::unique_ptr firstSurface(Test::createSurface()); + std::unique_ptr firstShellSurface(Test::createXdgToplevelSurface(firstSurface.get())); + auto firstWindow = Test::renderAndWaitForShown(firstSurface.get(), QSize(100, 50), Qt::blue); + firstWindow->sendToOutput(firstOutput); + QCOMPARE(workspace()->activeOutput(), firstOutput); + + std::unique_ptr secondSurface(Test::createSurface()); + std::unique_ptr secondShellSurface(Test::createXdgToplevelSurface(secondSurface.get())); + auto secondWindow = Test::renderAndWaitForShown(secondSurface.get(), QSize(100, 50), Qt::red); + secondWindow->sendToOutput(secondOutput); + QCOMPARE(workspace()->activeOutput(), secondOutput); + + workspace()->activateWindow(firstWindow); + QCOMPARE(workspace()->activeOutput(), firstOutput); + + workspace()->activateWindow(secondWindow); + QCOMPARE(workspace()->activeOutput(), secondOutput); + + workspace()->activateWindow(firstWindow); + QCOMPARE(workspace()->activeOutput(), firstOutput); + + quint32 timestamp = 0; + Test::pointerMotion(secondOutput->geometry().center(), timestamp++); + QCOMPARE(workspace()->activeOutput(), secondOutput); +} + +void WorkspaceTest::activeOutputDoesntFollowInactiveWindow() +{ + // This test verifies that the active output doesn't follow inactive windows. + + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + Rect(2560, 0, 1280, 1024), + }); + + const auto firstOutput = workspace()->outputs()[0]; + const auto secondOutput = workspace()->outputs()[1]; + const auto thirdOutput = workspace()->outputs()[2]; + + std::unique_ptr firstSurface(Test::createSurface()); + std::unique_ptr firstShellSurface(Test::createXdgToplevelSurface(firstSurface.get())); + auto firstWindow = Test::renderAndWaitForShown(firstSurface.get(), QSize(100, 50), Qt::blue); + firstWindow->sendToOutput(firstOutput); + QCOMPARE(workspace()->activeOutput(), firstOutput); + + std::unique_ptr secondSurface(Test::createSurface()); + std::unique_ptr secondShellSurface(Test::createXdgToplevelSurface(secondSurface.get())); + auto secondWindow = Test::renderAndWaitForShown(secondSurface.get(), QSize(100, 50), Qt::red); + secondWindow->sendToOutput(secondOutput); + QCOMPARE(workspace()->activeOutput(), secondOutput); + + firstWindow->sendToOutput(thirdOutput); + QCOMPARE(workspace()->activeOutput(), secondOutput); + + firstWindow->sendToOutput(firstOutput); + QCOMPARE(workspace()->activeOutput(), secondOutput); +} + +void WorkspaceTest::disableActiveOutput() +{ + // This test verifies that the active output property will be reset when the corresponding output is disabled. + + const auto firstOutput = workspace()->outputs()[0]; + const auto secondOutput = workspace()->outputs()[1]; + QCOMPARE(workspace()->activeOutput(), firstOutput); + + OutputConfiguration config; + { + auto changeSet = config.changeSet(workspace()->activeOutput()->backendOutput()); + changeSet->enabled = false; + } + workspace()->applyOutputConfiguration(config); + QCOMPARE(workspace()->activeOutput(), secondOutput); +} + +void WorkspaceTest::activeOutputAfterActivateNextWindowOnOutputAdded() +{ + // This test verifies that the workspace doesn't end up with corrupted state when the Workspace::outputAdded() signal is emitted. + // activateNextWindow() is interesting because it changes the active output. + + const auto firstOutput = kwinApp()->outputBackend()->outputs()[0]; + const auto secondOutput = kwinApp()->outputBackend()->outputs()[1]; + + { + OutputConfiguration config; + { + auto changeSet = config.changeSet(firstOutput); + changeSet->enabled = false; + } + { + auto changeSet = config.changeSet(secondOutput); + changeSet->enabled = false; + } + workspace()->applyOutputConfiguration(config); + } + + std::unique_ptr firstSurface(Test::createSurface()); + std::unique_ptr firstShellSurface(Test::createXdgToplevelSurface(firstSurface.get())); + auto firstWindow = Test::renderAndWaitForShown(firstSurface.get(), QSize(100, 50), Qt::blue); + + std::unique_ptr secondSurface(Test::createSurface()); + std::unique_ptr secondShellSurface(Test::createXdgToplevelSurface(secondSurface.get())); + auto secondWindow = Test::renderAndWaitForShown(secondSurface.get(), QSize(100, 50), Qt::red); + QCOMPARE(workspace()->activeWindow(), secondWindow); + + connect(workspace(), &Workspace::outputAdded, secondWindow, [secondWindow]() { + workspace()->activateNextWindow(secondWindow); + }); + + { + OutputConfiguration config; + { + auto changeSet = config.changeSet(firstOutput); + changeSet->enabled = true; + } + workspace()->applyOutputConfiguration(config); + } + + QCOMPARE(workspace()->activeWindow(), firstWindow); + QCOMPARE(workspace()->activeOutput()->backendOutput(), firstOutput); +} + +void WorkspaceTest::activeOutputAfterActivateNextWindowOnOutputRemoved_data() +{ + QTest::addColumn("separateScreenFocus"); + + QTest::addRow("split screen focus") << true; + QTest::addRow("unified screen focus") << false; +} + +void WorkspaceTest::activeOutputAfterActivateNextWindowOnOutputRemoved() +{ + // This test verifies that the workspace doesn't end up with corrupted state when the Workspace::outputAdded() signal is emitted. + // activateNextWindow() is interesting because it changes the active output. + + QFETCH(bool, separateScreenFocus); + options->setSeparateScreenFocus(separateScreenFocus); + + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + Rect(2560, 0, 1280, 1024), + }); + + const auto firstOutput = workspace()->outputs()[0]; + const auto secondOutput = workspace()->outputs()[1]; + const auto thirdOutput = workspace()->outputs()[2]; + + std::unique_ptr firstSurface(Test::createSurface()); + std::unique_ptr firstShellSurface(Test::createXdgToplevelSurface(firstSurface.get())); + auto firstWindow = Test::renderAndWaitForShown(firstSurface.get(), QSize(100, 50), Qt::blue); + firstWindow->sendToOutput(firstOutput); + + std::unique_ptr secondSurface(Test::createSurface()); + std::unique_ptr secondShellSurface(Test::createXdgToplevelSurface(secondSurface.get())); + auto secondWindow = Test::renderAndWaitForShown(secondSurface.get(), QSize(100, 50), Qt::red); + secondWindow->sendToOutput(secondOutput); + + workspace()->activateWindow(firstWindow); + QCOMPARE(workspace()->activeWindow(), firstWindow); + + connect(workspace(), &Workspace::outputRemoved, firstWindow, [firstOutput, firstWindow](LogicalOutput *output) { + if (output == firstOutput) { + workspace()->activateNextWindow(firstWindow); + } + }); + + { + OutputConfiguration config; + { + auto changeSet = config.changeSet(firstOutput->backendOutput()); + changeSet->enabled = false; + } + { + auto changeSet = config.changeSet(secondOutput->backendOutput()); + changeSet->enabled = false; + } + { + auto changeSet = config.changeSet(thirdOutput->backendOutput()); + changeSet->pos = QPoint(0, 0); + } + workspace()->applyOutputConfiguration(config); + } + + QCOMPARE(workspace()->activeWindow(), separateScreenFocus ? nullptr : secondWindow); + QCOMPARE(workspace()->activeOutput(), thirdOutput); +} + +WAYLANDTEST_MAIN(WorkspaceTest) +#include "workspace_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/x11_window_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/x11_window_test.cpp new file mode 100644 index 0000000000..d3f3ba5df1 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/x11_window_test.cpp @@ -0,0 +1,3842 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "atoms.h" +#include "compositor.h" +#include "core/graphicsbuffer.h" +#include "core/shmgraphicsbufferallocator.h" +#include "cursor.h" +#include "pointer_input.h" +#include "virtualdesktops.h" +#include "wayland/surface.h" +#include "wayland_server.h" +#include "workspace.h" +#include "x11eventfilter.h" +#include "x11window.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace KWin; +using namespace std::chrono_literals; + +static const QString s_socketName = QStringLiteral("wayland_test_x11_window-0"); + +class X11WindowTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase_data(); + void initTestCase(); + void init(); + void cleanup(); + + void testMaximizedFull(); + void testInitiallyMaximizedFull(); + void testRequestMaximizedFull(); + void testMaximizedVertical(); + void testInitiallyMaximizedVertical(); + void testRequestMaximizedVertical(); + void testMaximizedHorizontal(); + void testInitiallyMaximizedHorizontal(); + void testRequestMaximizedHorizontal(); + void testInteractiveMoveUnmaximizeFull(); + void testInteractiveMoveUnmaximizeInitiallyFull(); + void testInteractiveMoveUnmaximizeHorizontal(); + void testInteractiveMoveUnmaximizeInitiallyHorizontal(); + void testInteractiveMoveUnmaximizeVertical(); + void testInteractiveMoveUnmaximizeInitiallyVertical(); + void testFullScreen(); + void testInitiallyFullScreen(); + void testRequestFullScreen(); + void testFullscreenLayerWithActiveWaylandWindow(); + void testFullscreenWindowGroups(); + void testKeepBelow(); + void testInitiallyKeepBelow(); + void testKeepAbove(); + void testInitiallyKeepAbove(); + void testMinimized(); + void testInitiallyMinimized(); + void testRequestMinimized(); + void testSkipSwitcher(); + void testInitiallySkipSwitcher(); + void testRequestSkipSwitcher(); + void testSkipPager(); + void testInitiallySkipPager(); + void testRequestSkipPager(); + void testSkipTaskbar(); + void testInitiallySkipTaskbar(); + void testRequestSkipTaskbar(); + void testOpacity(); + void testNetWmKeyboardMove(); + void testNetWmKeyboardMoveCancel(); + void testNetWmKeyboardResize(); + void testNetWmKeyboardResizeCancel(); + void testNetWmButtonMove(); + void testNetWmButtonMoveNotPressed(); + void testNetWmButtonMoveCancel(); + void testNetWmButtonSize_data(); + void testNetWmButtonSize(); + void testNetWmButtonSizeNotPressed(); + void testNetWmButtonSizeCancel(); + void testMinimumSize(); + void testMaximumSize(); + void testTrimCaption_data(); + void testTrimCaption(); + void testFocusInWithWaylandLastActiveWindow(); + void testCaptionChanges(); + void testCaptionWmName(); + void testActivateFocusedWindow(); + void testReentrantMoveResize(); + void testTransient(); + void testGroupTransient(); + void testCloseTransient(); + void testCloseInactiveTransient(); + void testCloseGroupTransient(); + void testCloseInactiveGroupTransient(); + void testModal(); + void testGroupModal(); + void testCloseModal(); + void testCloseInactiveModal(); + void testCloseGroupModal(); + void testCloseInactiveGroupModal(); + void testStackAboveFromApplication(); + void testStackAboveFromTool(); + void testStackAboveSibling(); + void testStackBelowFromApplication(); + void testStackBelowFromTool(); + void testStackBelowSibling(); + void testStackTopIfFromApplication(); + void testStackTopIfFromTool(); + void testStackBottomIfFromApplication(); + void testStackBottomIfFromTool(); + void testStackOppositeFromApplication(); + void testStackOppositeFromTool(); + void testStackOppositeNoSibling(); + void testOverrideRedirectReparent(); + void testOverrideRedirectStackingAbove(); + void testOverrideRedirectStackingBelow(); + void testRandrEmulation(); + void testRestoreFocusToDestroyedWindow(); +}; + +void X11WindowTest::initTestCase_data() +{ + QTest::addColumn("scale"); + QTest::newRow("normal") << 1.0; + QTest::newRow("scaled2x") << 2.0; +} + +void X11WindowTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + QVERIFY(KWin::Compositor::self()); +} + +void X11WindowTest::init() +{ + VirtualDesktopManager::self()->setCount(2); + + QFETCH_GLOBAL(qreal, scale); + kwinApp()->setXwaylandScale(scale); + + QVERIFY(Test::setupWaylandConnection()); +} + +void X11WindowTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +static X11Window *createWindow(xcb_connection_t *connection, const Rect &geometry, std::function setup = {}) +{ + xcb_window_t windowId = xcb_generate_id(connection); + xcb_create_window(connection, XCB_COPY_FROM_PARENT, windowId, rootWindow(), + geometry.x(), + geometry.y(), + geometry.width(), + geometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, geometry.x(), geometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, geometry.width(), geometry.height()); + xcb_icccm_set_wm_normal_hints(connection, windowId, &hints); + + if (setup) { + setup(windowId); + } + + xcb_map_window(connection, windowId); + xcb_flush(connection); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + if (!windowCreatedSpy.wait()) { + return nullptr; + } + return windowCreatedSpy.last().first().value(); +} + +void X11WindowTest::testMaximizedFull() +{ + // This test verifies that toggling maximized mode works as expected and state changes are propagated to the client. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200)); + + // Make the window maximized. + const RectF originalGeometry = window->frameGeometry(); + const RectF workArea = workspace()->clientArea(MaximizeArea, window); + QSignalSpy maximizedChangedSpy(window, &Window::maximizedChanged); + window->maximize(MaximizeFull); + QCOMPARE(maximizedChangedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + QCOMPARE(window->frameGeometry(), workArea); + QCOMPARE(window->geometryRestore(), originalGeometry); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY((winInfo.state() & NET::Max) == NET::Max); + } + + // Restore the window. + window->maximize(MaximizeRestore); + QCOMPARE(maximizedChangedSpy.count(), 2); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), originalGeometry); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY(!(winInfo.state() & NET::Max)); + } +} + +void X11WindowTest::testInitiallyMaximizedFull() +{ + // This test verifies that a window can be shown already in the maximized state. + + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + NETWinInfo info(c.get(), windowId, kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::Max, NET::Max); + }); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); +} + +void X11WindowTest::testRequestMaximizedFull() +{ + // This test verifies that the client can toggle the maximized state. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200)); + + // Set maximized state. + { + NETWinInfo info(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::Max, NET::Max); + xcb_flush(c.get()); + } + QSignalSpy maximizedChangedSpy(window, &Window::maximizedChanged); + QVERIFY(maximizedChangedSpy.wait()); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + + // Unset maximized state. + { + NETWinInfo info(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::State(), NET::Max); + xcb_flush(c.get()); + } + QVERIFY(maximizedChangedSpy.wait()); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); +} + +void X11WindowTest::testMaximizedVertical() +{ + // This test verifies that toggling maximized vertically mode works as expected and state changes are propagated to the client. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200)); + + // Make the window maximized. + const RectF originalGeometry = window->frameGeometry(); + const RectF workArea = workspace()->clientArea(MaximizeArea, window); + QSignalSpy maximizedChangedSpy(window, &Window::maximizedChanged); + window->maximize(MaximizeVertical); + QCOMPARE(maximizedChangedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeVertical); + QCOMPARE(window->requestedMaximizeMode(), MaximizeVertical); + QCOMPARE(window->frameGeometry(), RectF(originalGeometry.x(), workArea.y(), originalGeometry.width(), workArea.height())); + QCOMPARE(window->geometryRestore(), originalGeometry); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY((winInfo.state() & NET::Max) == NET::MaxVert); + } + + // Restore the window. + window->maximize(MaximizeRestore); + QCOMPARE(maximizedChangedSpy.count(), 2); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), originalGeometry); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY(!(winInfo.state() & NET::Max)); + } +} + +void X11WindowTest::testInitiallyMaximizedVertical() +{ + // This test verifies that a window can be shown already in the maximized vertically state. + + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + NETWinInfo info(c.get(), windowId, kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::MaxVert, NET::MaxVert); + }); + QCOMPARE(window->maximizeMode(), MaximizeVertical); + QCOMPARE(window->requestedMaximizeMode(), MaximizeVertical); +} + +void X11WindowTest::testRequestMaximizedVertical() +{ + // This test verifies that the client can toggle the maximized vertically state. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200)); + + // Set maximized state. + { + NETWinInfo info(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::MaxVert, NET::MaxVert); + xcb_flush(c.get()); + } + QSignalSpy maximizedChangedSpy(window, &Window::maximizedChanged); + QVERIFY(maximizedChangedSpy.wait()); + QCOMPARE(window->maximizeMode(), MaximizeVertical); + QCOMPARE(window->requestedMaximizeMode(), MaximizeVertical); + + // Unset maximized state. + { + NETWinInfo info(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::State(), NET::MaxVert); + xcb_flush(c.get()); + } + QVERIFY(maximizedChangedSpy.wait()); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); +} + +void X11WindowTest::testMaximizedHorizontal() +{ + // This test verifies that toggling maximized horizontally mode works as expected and state changes are propagated to the client. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200)); + + // Make the window maximized. + const RectF originalGeometry = window->frameGeometry(); + const RectF workArea = workspace()->clientArea(MaximizeArea, window); + QSignalSpy maximizedChangedSpy(window, &Window::maximizedChanged); + window->maximize(MaximizeHorizontal); + QCOMPARE(maximizedChangedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeHorizontal); + QCOMPARE(window->requestedMaximizeMode(), MaximizeHorizontal); + QCOMPARE(window->frameGeometry(), RectF(workArea.x(), originalGeometry.y(), workArea.width(), originalGeometry.height())); + QCOMPARE(window->geometryRestore(), originalGeometry); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY((winInfo.state() & NET::Max) == NET::MaxHoriz); + } + + // Restore the window. + window->maximize(MaximizeRestore); + QCOMPARE(maximizedChangedSpy.count(), 2); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), originalGeometry); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY(!(winInfo.state() & NET::Max)); + } +} + +void X11WindowTest::testInitiallyMaximizedHorizontal() +{ + // This test verifies that a window can be shown already in the maximized horizontally state. + + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + NETWinInfo info(c.get(), windowId, kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::MaxHoriz, NET::MaxHoriz); + }); + QCOMPARE(window->maximizeMode(), MaximizeHorizontal); + QCOMPARE(window->requestedMaximizeMode(), MaximizeHorizontal); +} + +void X11WindowTest::testRequestMaximizedHorizontal() +{ + // This test verifies that the client can toggle the maximized horizontally state. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200)); + + // Set maximized state. + { + NETWinInfo info(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::MaxHoriz, NET::MaxHoriz); + xcb_flush(c.get()); + } + QSignalSpy maximizedChangedSpy(window, &Window::maximizedChanged); + QVERIFY(maximizedChangedSpy.wait()); + QCOMPARE(window->maximizeMode(), MaximizeHorizontal); + QCOMPARE(window->requestedMaximizeMode(), MaximizeHorizontal); + + // Unset maximized state. + { + NETWinInfo info(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::State(), NET::MaxHoriz); + xcb_flush(c.get()); + } + QVERIFY(maximizedChangedSpy.wait()); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); +} + +void X11WindowTest::testInteractiveMoveUnmaximizeFull() +{ + // This test verifies that a maximized x11 window is going to be properly unmaximized when it's dragged. + + // Create the window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(100, 100, 100, 200)); + + // Make the window maximized. + const RectF originalGeometry = window->frameGeometry(); + window->maximize(MaximizeFull); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + + // Start interactive move. + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + const qreal xOffset = 0.2; + const qreal yOffset = 0.5; + quint32 timestamp = 0; + Test::pointerMotion(QPointF(window->x() + window->width() * xOffset, window->y() + window->height() * yOffset), timestamp++); + window->performMousePressCommand(Options::MouseMove, input()->pointer()->pos()); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + + // Move the window to unmaximize it. + Test::pointerMotionRelative(QPointF(0, 100), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), Xcb::fromXNative(Xcb::toXNative(RectF(input()->pointer()->pos() - QPointF(originalGeometry.width() * xOffset, originalGeometry.height() * yOffset), originalGeometry.size())))); + + // Move the window again. + const RectF normalGeometry = window->frameGeometry(); + Test::pointerMotionRelative(QPointF(0, 10), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), normalGeometry.translated(0, 10)); + + // Finish interactive move. + window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); +} + +void X11WindowTest::testInteractiveMoveUnmaximizeInitiallyFull() +{ + // This test verifies that an initially maximized x11 window will be properly unmaximized when it's dragged. + + // Create the window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(100, 100, 100, 200), [&c](xcb_window_t windowId) { + NETWinInfo info(c.get(), windowId, kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::Max, NET::Max); + }); + + // Start interactive move. + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + const qreal xOffset = 0.2; + const qreal yOffset = 0.5; + quint32 timestamp = 0; + Test::pointerMotion(QPointF(window->x() + window->width() * xOffset, window->y() + window->height() * yOffset), timestamp++); + window->performMousePressCommand(Options::MouseMove, input()->pointer()->pos()); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + + // Move the window to unmaximize it. + const QSizeF restoredSize = window->geometryRestore().size(); + Test::pointerMotionRelative(QPointF(0, 100), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), Xcb::fromXNative(Xcb::toXNative(RectF(input()->pointer()->pos() - QPointF(restoredSize.width() * xOffset, restoredSize.height() * yOffset), restoredSize)))); + + // Move the window again. + const RectF normalGeometry = window->frameGeometry(); + Test::pointerMotionRelative(QPointF(0, 10), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), normalGeometry.translated(0, 10)); + + // Finish interactive move. + window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); +} + +void X11WindowTest::testInteractiveMoveUnmaximizeHorizontal() +{ + // This test verifies that a maximized horizontally x11 window is going to be properly unmaximized when it's dragged. + + // Create the window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(100, 100, 100, 200)); + + // Make the window maximized. + const RectF originalGeometry = window->frameGeometry(); + window->maximize(MaximizeHorizontal); + QCOMPARE(window->maximizeMode(), MaximizeHorizontal); + QCOMPARE(window->requestedMaximizeMode(), MaximizeHorizontal); + + // Start interactive move. + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + const qreal xOffset = 0.2; + const qreal yOffset = 0.5; + quint32 timestamp = 0; + Test::pointerMotion(QPointF(window->x() + window->width() * xOffset, window->y() + window->height() * yOffset), timestamp++); + window->performMousePressCommand(Options::MouseMove, input()->pointer()->pos()); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeHorizontal); + QCOMPARE(window->requestedMaximizeMode(), MaximizeHorizontal); + + // Move the window to unmaximize it. + Test::pointerMotionRelative(QPointF(100, 0), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), Xcb::fromXNative(Xcb::toXNative(RectF(input()->pointer()->pos() - QPointF(originalGeometry.width() * xOffset, originalGeometry.height() * yOffset), originalGeometry.size())))); + + // Move the window again. + const RectF normalGeometry = window->frameGeometry(); + Test::pointerMotionRelative(QPointF(10, 0), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), normalGeometry.translated(10, 0)); + + // Finish interactive move. + window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); +} + +void X11WindowTest::testInteractiveMoveUnmaximizeInitiallyHorizontal() +{ + // This test verifies that an initially maximized horizontally x11 window will be properly unmaximized when it's dragged. + + // Create the window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(100, 100, 100, 200), [&c](xcb_window_t windowId) { + NETWinInfo info(c.get(), windowId, kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::MaxHoriz, NET::MaxHoriz); + }); + + // Start interactive move. + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + const qreal xOffset = 0.2; + const qreal yOffset = 0.5; + quint32 timestamp = 0; + Test::pointerMotion(QPointF(window->x() + window->width() * xOffset, window->y() + window->height() * yOffset), timestamp++); + window->performMousePressCommand(Options::MouseMove, input()->pointer()->pos()); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeHorizontal); + QCOMPARE(window->requestedMaximizeMode(), MaximizeHorizontal); + + // Move the window to unmaximize it. + const QSizeF restoredSize = window->geometryRestore().size(); + Test::pointerMotionRelative(QPointF(100, 0), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), Xcb::fromXNative(Xcb::toXNative(RectF(input()->pointer()->pos() - QPointF(restoredSize.width() * xOffset, restoredSize.height() * yOffset), restoredSize)))); + + // Move the window again. + const RectF normalGeometry = window->frameGeometry(); + Test::pointerMotionRelative(QPointF(10, 0), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), normalGeometry.translated(10, 0)); + + // Finish interactive move. + window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); +} + +void X11WindowTest::testInteractiveMoveUnmaximizeVertical() +{ + // This test verifies that a maximized vertically x11 window is going to be properly unmaximized when it's dragged. + + // Create the window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(100, 100, 100, 200)); + + // Make the window maximized. + const RectF originalGeometry = window->frameGeometry(); + window->maximize(MaximizeVertical); + QCOMPARE(window->maximizeMode(), MaximizeVertical); + QCOMPARE(window->requestedMaximizeMode(), MaximizeVertical); + + // Start interactive move. + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + const qreal xOffset = 0.2; + const qreal yOffset = 0.5; + quint32 timestamp = 0; + Test::pointerMotion(QPointF(window->x() + window->width() * xOffset, window->y() + window->height() * yOffset), timestamp++); + window->performMousePressCommand(Options::MouseMove, input()->pointer()->pos()); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeVertical); + QCOMPARE(window->requestedMaximizeMode(), MaximizeVertical); + + // Move the window to unmaximize it. + Test::pointerMotionRelative(QPointF(0, 100), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), Xcb::fromXNative(Xcb::toXNative(RectF(input()->pointer()->pos() - QPointF(originalGeometry.width() * xOffset, originalGeometry.height() * yOffset), originalGeometry.size())))); + + // Move the window again. + const RectF normalGeometry = window->frameGeometry(); + Test::pointerMotionRelative(QPointF(0, 10), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), normalGeometry.translated(0, 10)); + + // Finish interactive move. + window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); +} + +void X11WindowTest::testInteractiveMoveUnmaximizeInitiallyVertical() +{ + // This test verifies that an initially maximized vertically x11 window will be properly unmaximized when it's dragged. + + // Create the window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(100, 100, 100, 200), [&c](xcb_window_t windowId) { + NETWinInfo info(c.get(), windowId, kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::MaxVert, NET::MaxVert); + }); + + // Start interactive move. + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + const qreal xOffset = 0.2; + const qreal yOffset = 0.5; + quint32 timestamp = 0; + Test::pointerMotion(QPointF(window->x() + window->width() * xOffset, window->y() + window->height() * yOffset), timestamp++); + window->performMousePressCommand(Options::MouseMove, input()->pointer()->pos()); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeVertical); + QCOMPARE(window->requestedMaximizeMode(), MaximizeVertical); + + // Move the window to unmaximize it. + const QSizeF restoredSize = window->geometryRestore().size(); + Test::pointerMotionRelative(QPointF(0, 100), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), Xcb::fromXNative(Xcb::toXNative(RectF(input()->pointer()->pos() - QPointF(restoredSize.width() * xOffset, restoredSize.height() * yOffset), restoredSize)))); + + // Move the window again. + const RectF normalGeometry = window->frameGeometry(); + Test::pointerMotionRelative(QPointF(0, 10), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), normalGeometry.translated(0, 10)); + + // Finish interactive move. + window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); +} + +void X11WindowTest::testFullScreen() +{ + // This test verifies that the fullscreen mode can be toggled and state changes are propagated to the client. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200)); + + // Make the window maximized. + const RectF originalGeometry = window->frameGeometry(); + const RectF screenArea = workspace()->clientArea(ScreenArea, window); + QSignalSpy fullScreenChangedSpy(window, &Window::fullScreenChanged); + window->setFullScreen(true); + QCOMPARE(fullScreenChangedSpy.count(), 1); + QCOMPARE(window->isFullScreen(), true); + QCOMPARE(window->isRequestedFullScreen(), true); + QCOMPARE(window->frameGeometry(), screenArea); + QCOMPARE(window->fullscreenGeometryRestore(), originalGeometry); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY(winInfo.state() & NET::FullScreen); + } + + // Restore the window. + window->setFullScreen(false); + QCOMPARE(fullScreenChangedSpy.count(), 2); + QCOMPARE(window->isFullScreen(), false); + QCOMPARE(window->isRequestedFullScreen(), false); + QCOMPARE(window->frameGeometry(), originalGeometry); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY(!(winInfo.state() & NET::FullScreen)); + } +} + +void X11WindowTest::testInitiallyFullScreen() +{ + // This test verifies that a window can be shown already in the fullscreen state. + + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + NETWinInfo info(c.get(), windowId, kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::FullScreen, NET::FullScreen); + }); + QCOMPARE(window->isFullScreen(), true); + QCOMPARE(window->isRequestedFullScreen(), true); +} + +void X11WindowTest::testRequestFullScreen() +{ + // This test verifies that the client can toggle the fullscreen state. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200)); + + // Set fullscreen state. + { + NETWinInfo info(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::FullScreen, NET::FullScreen); + xcb_flush(c.get()); + } + QSignalSpy fullScreenChangedSpy(window, &Window::fullScreenChanged); + QVERIFY(fullScreenChangedSpy.wait()); + QCOMPARE(window->isFullScreen(), true); + QCOMPARE(window->isRequestedFullScreen(), true); + + // Unset fullscreen state. + { + NETWinInfo info(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::State(), NET::FullScreen); + xcb_flush(c.get()); + } + QVERIFY(fullScreenChangedSpy.wait()); + QCOMPARE(window->isFullScreen(), false); + QCOMPARE(window->isRequestedFullScreen(), false); +} + +void X11WindowTest::testKeepBelow() +{ + // This test verifies that keep below state can be toggled and its changes are propagated to the client. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200)); + + // Set keep below. + QSignalSpy keepBelowChangedSpy(window, &Window::keepBelowChanged); + window->setKeepBelow(true); + QCOMPARE(keepBelowChangedSpy.count(), 1); + QVERIFY(window->keepBelow()); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY(winInfo.state() & NET::KeepBelow); + } + + // Unset keep below. + window->setKeepBelow(false); + QCOMPARE(keepBelowChangedSpy.count(), 2); + QVERIFY(!window->keepBelow()); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY(!(winInfo.state() & NET::KeepBelow)); + } +} + +void X11WindowTest::testInitiallyKeepBelow() +{ + // This test verifies that a window can be shown already with the keep below state set. + + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + NETWinInfo info(c.get(), windowId, kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::KeepBelow, NET::KeepBelow); + }); + QVERIFY(window->keepBelow()); +} + +void X11WindowTest::testKeepAbove() +{ + // This test verifies that keep above state can be toggled and its changes are propagated to the client. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200)); + + // Set keep above. + QSignalSpy keepAboveChangedSpy(window, &Window::keepAboveChanged); + window->setKeepAbove(true); + QCOMPARE(keepAboveChangedSpy.count(), 1); + QVERIFY(window->keepAbove()); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY(winInfo.state() & NET::KeepAbove); + } + + // Unset keep above. + window->setKeepAbove(false); + QCOMPARE(keepAboveChangedSpy.count(), 2); + QVERIFY(!window->keepAbove()); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY(!(winInfo.state() & NET::KeepAbove)); + } +} + +void X11WindowTest::testInitiallyKeepAbove() +{ + // This test verifies that a window can be shown already with the keep above state set. + + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + NETWinInfo info(c.get(), windowId, kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::KeepAbove, NET::KeepAbove); + }); + QVERIFY(window->keepAbove()); +} + +void X11WindowTest::testMinimized() +{ + // This test verifies that a window can be minimized/unminimized and its changes are propagated to the client. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200)); + + // Minimize. + QSignalSpy minimizedChangedSpy(window, &Window::minimizedChanged); + window->setMinimized(true); + QCOMPARE(minimizedChangedSpy.count(), 1); + QVERIFY(window->isMinimized()); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY(winInfo.state() & NET::Hidden); + } + + // Unminimize. + window->setMinimized(false); + QCOMPARE(minimizedChangedSpy.count(), 2); + QVERIFY(!window->isMinimized()); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY(!(winInfo.state() & NET::Hidden)); + } +} + +void X11WindowTest::testInitiallyMinimized() +{ + // This test verifies that a window can be shown already in the minimized state. + + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + xcb_icccm_wm_hints_t hints{}; + xcb_icccm_wm_hints_set_iconic(&hints); + xcb_icccm_set_wm_hints(c.get(), windowId, &hints); + }); + QVERIFY(window->isMinimized()); + + { + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY(winInfo.state() & NET::Hidden); + } +} + +void X11WindowTest::testRequestMinimized() +{ + // This test verifies that the client can set the minimized state. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200)); + + // Set minimized state. + { + xcb_client_message_event_t event; + event.response_type = XCB_CLIENT_MESSAGE; + event.format = 32; + event.sequence = 0; + event.window = window->window(); + event.type = atoms->wm_change_state; + event.data.data32[0] = XCB_ICCCM_WM_STATE_ICONIC; + event.data.data32[1] = 0; + event.data.data32[2] = 0; + event.data.data32[3] = 0; + event.data.data32[4] = 0; + + xcb_send_event(c.get(), 0, kwinApp()->x11RootWindow(), + XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT, + reinterpret_cast(&event)); + xcb_flush(c.get()); + } + QSignalSpy minimizedChangedSpy(window, &Window::minimizedChanged); + QVERIFY(minimizedChangedSpy.wait()); + QVERIFY(window->isMinimized()); +} + +void X11WindowTest::testSkipSwitcher() +{ + // This test verifies that skip switcher changes are propagated to the client. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200)); + + // Set skip switcher. + QSignalSpy skipSwitcherChangedSpy(window, &Window::skipSwitcherChanged); + window->setSkipSwitcher(true); + QCOMPARE(skipSwitcherChangedSpy.count(), 1); + QVERIFY(window->skipSwitcher()); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY(winInfo.state() & NET::SkipSwitcher); + } + + // Unset skip switcher. + window->setSkipSwitcher(false); + QCOMPARE(skipSwitcherChangedSpy.count(), 2); + QVERIFY(!window->skipSwitcher()); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY(!(winInfo.state() & NET::SkipSwitcher)); + } +} + +void X11WindowTest::testInitiallySkipSwitcher() +{ + // This test verifies that a window can be shown already with the skip switcher state set. + + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + NETWinInfo info(c.get(), windowId, kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::SkipSwitcher, NET::SkipSwitcher); + }); + QVERIFY(window->skipSwitcher()); +} + +void X11WindowTest::testRequestSkipSwitcher() +{ + // This test verifies that the client can change the skip switcher state. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200)); + + // Set the skip switcher state. + { + NETWinInfo info(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::SkipSwitcher, NET::SkipSwitcher); + xcb_flush(c.get()); + } + QSignalSpy skipSwitcherChangedSpy(window, &Window::skipSwitcherChanged); + QVERIFY(skipSwitcherChangedSpy.wait()); + QVERIFY(window->skipSwitcher()); + + // Unset the skip switcher state. + { + NETWinInfo info(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::State(), NET::SkipSwitcher); + xcb_flush(c.get()); + } + QVERIFY(skipSwitcherChangedSpy.wait()); + QVERIFY(!window->skipSwitcher()); +} + +void X11WindowTest::testSkipPager() +{ + // This test verifies that skip pager changes are propagated to the client. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200)); + + // Set skip pager. + QSignalSpy skipPagerChangedSpy(window, &Window::skipPagerChanged); + window->setSkipPager(true); + QCOMPARE(skipPagerChangedSpy.count(), 1); + QVERIFY(window->skipPager()); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY(winInfo.state() & NET::SkipPager); + } + + // Unset skip pager. + window->setSkipPager(false); + QCOMPARE(skipPagerChangedSpy.count(), 2); + QVERIFY(!window->skipPager()); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY(!(winInfo.state() & NET::SkipPager)); + } +} + +void X11WindowTest::testInitiallySkipPager() +{ + // This test verifies that a window can be shown already with the skip pager state set. + + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + NETWinInfo info(c.get(), windowId, kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::SkipPager, NET::SkipPager); + }); + QVERIFY(window->skipPager()); +} + +void X11WindowTest::testRequestSkipPager() +{ + // This test verifies that the client can change the skip pager state. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200)); + + // Set the skip pager state. + { + NETWinInfo info(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::SkipPager, NET::SkipPager); + xcb_flush(c.get()); + } + QSignalSpy skipPagerChangedSpy(window, &Window::skipPagerChanged); + QVERIFY(skipPagerChangedSpy.wait()); + QVERIFY(window->skipPager()); + + // Unset the skip pager state. + { + NETWinInfo info(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::State(), NET::SkipPager); + xcb_flush(c.get()); + } + QVERIFY(skipPagerChangedSpy.wait()); + QVERIFY(!window->skipPager()); +} + +void X11WindowTest::testSkipTaskbar() +{ + // This test verifies that skip taskbar changes are propagated to the client. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200)); + + // Set skip taskbar. + QSignalSpy skipTaskbarChangedSpy(window, &Window::skipTaskbarChanged); + window->setSkipTaskbar(true); + QCOMPARE(skipTaskbarChangedSpy.count(), 1); + QVERIFY(window->skipTaskbar()); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY(winInfo.state() & NET::SkipTaskbar); + } + + // Unset skip taskbar. + window->setSkipTaskbar(false); + QCOMPARE(skipTaskbarChangedSpy.count(), 2); + QVERIFY(!window->skipTaskbar()); + + { + Xcb::sync(); + NETWinInfo winInfo(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + QVERIFY(!(winInfo.state() & NET::SkipTaskbar)); + } +} + +void X11WindowTest::testInitiallySkipTaskbar() +{ + // This test verifies that a window can be shown already with the skip taskbar state set. + + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + NETWinInfo info(c.get(), windowId, kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::SkipTaskbar, NET::SkipTaskbar); + }); + QVERIFY(window->skipTaskbar()); +} + +void X11WindowTest::testRequestSkipTaskbar() +{ + // This test verifies that the client can change the skip taskbar state. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200)); + + // Set the skip taskbar state. + { + NETWinInfo info(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::SkipTaskbar, NET::SkipTaskbar); + xcb_flush(c.get()); + } + QSignalSpy skipTaskbarChangedSpy(window, &Window::skipTaskbarChanged); + QVERIFY(skipTaskbarChangedSpy.wait()); + QVERIFY(window->skipTaskbar()); + + // Unset the skip taskbar state. + { + NETWinInfo info(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::State(), NET::SkipTaskbar); + xcb_flush(c.get()); + } + QVERIFY(skipTaskbarChangedSpy.wait()); + QVERIFY(!window->skipTaskbar()); +} + +/** + * Some high precision bits can be lost when transmitting the opacity due to the _NET_WM_OPACITY + * property being a uint32_t number. This function computes the actual opacity that the wm will get. + */ +static qreal effectiveOpacity(qreal opacity) +{ + return static_cast(static_cast(opacity * 0xffffffff)) / 0xffffffff; +} + +void X11WindowTest::testOpacity() +{ + // This test verifies that _NET_WM_WINDOW_OPACITY is properly sync'ed with Window::opacity(). + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + NETWinInfo info(c.get(), windowId, kwinApp()->x11RootWindow(), NET::Properties(), NET::WM2Opacity); + info.setOpacityF(0.5); + }); + QCOMPARE(window->opacity(), effectiveOpacity(0.5)); + + // Change the opacity. + { + NETWinInfo info(c.get(), window->window(), kwinApp()->x11RootWindow(), NET::Properties(), NET::WM2Opacity); + info.setOpacityF(0.8); + xcb_flush(c.get()); + } + QSignalSpy opacityChangedSpy(window, &Window::opacityChanged); + QVERIFY(opacityChangedSpy.wait()); + QCOMPARE(window->opacity(), effectiveOpacity(0.8)); +} + +void X11WindowTest::testNetWmKeyboardMove() +{ + // This test verifies that a client can initiate a keyboard interactive move operation. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(100, 100, 100, 200)); + + // Request interactive move. + { + NETRootInfo root(c.get(), NET::Properties()); + root.moveResizeRequest(window->window(), Xcb::toXNative(window->x() + window->width() / 2), Xcb::toXNative(window->y() + window->height() / 2), NET::KeyboardMove, XCB_BUTTON_INDEX_1); + xcb_flush(c.get()); + } + QSignalSpy interactiveMoveResizeStartedSpy(window, &X11Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &X11Window::interactiveMoveResizeFinished); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &X11Window::interactiveMoveResizeStepped); + QVERIFY(interactiveMoveResizeStartedSpy.wait()); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QVERIFY(window->isInteractiveMove()); + + // Move the window to the right. + const RectF originalGeometry = window->frameGeometry(); + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_RIGHT, timestamp++); + Test::keyboardKeyReleased(KEY_RIGHT, timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->frameGeometry(), originalGeometry.translated(8, 0)); + + // Finish the interactive move. + Test::keyboardKeyPressed(KEY_ENTER, timestamp++); + Test::keyboardKeyReleased(KEY_ENTER, timestamp++); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QCOMPARE(window->frameGeometry(), originalGeometry.translated(8, 0)); +} + +void X11WindowTest::testNetWmKeyboardMoveCancel() +{ + // This test verifies that a client can initiate a keyboard interactive move operation and then cancel it. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(100, 100, 100, 200)); + + // Request interactive move. + { + NETRootInfo root(c.get(), NET::Properties()); + root.moveResizeRequest(window->window(), Xcb::toXNative(window->x() + window->width() / 2), Xcb::toXNative(window->y() + window->height() / 2), NET::KeyboardMove, XCB_BUTTON_INDEX_ANY); + xcb_flush(c.get()); + } + QSignalSpy interactiveMoveResizeStartedSpy(window, &X11Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &X11Window::interactiveMoveResizeFinished); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &X11Window::interactiveMoveResizeStepped); + QVERIFY(interactiveMoveResizeStartedSpy.wait()); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QVERIFY(window->isInteractiveMove()); + + // Move the window to the right. + const RectF originalGeometry = window->frameGeometry(); + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_RIGHT, timestamp++); + Test::keyboardKeyReleased(KEY_RIGHT, timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->frameGeometry(), originalGeometry.translated(8, 0)); + + // Cancel the interactive move. + { + NETRootInfo root(c.get(), NET::Properties()); + root.moveResizeRequest(window->window(), Xcb::toXNative(window->x() + window->width() / 2), Xcb::toXNative(window->y() + window->height() / 2), NET::MoveResizeCancel, XCB_BUTTON_INDEX_ANY); + xcb_flush(c.get()); + } + QVERIFY(interactiveMoveResizeFinishedSpy.wait()); + QCOMPARE(window->frameGeometry(), originalGeometry); +} + +void X11WindowTest::testNetWmKeyboardResize() +{ + // This test verifies that a client can initiate a keyboard interactive resize operation. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(100, 100, 100, 200)); + + // Request interactive resize. + { + NETRootInfo root(c.get(), NET::Properties()); + root.moveResizeRequest(window->window(), Xcb::toXNative(window->x() + window->width()), Xcb::toXNative(window->y() + window->height()), NET::KeyboardSize, XCB_BUTTON_INDEX_1); + xcb_flush(c.get()); + } + QSignalSpy interactiveMoveResizeStartedSpy(window, &X11Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &X11Window::interactiveMoveResizeFinished); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &X11Window::interactiveMoveResizeStepped); + QVERIFY(interactiveMoveResizeStartedSpy.wait()); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QVERIFY(window->isInteractiveResize()); + + // Move the window to the right, the frame geometry will be updated some time later. + const RectF originalGeometry = window->frameGeometry(); + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_RIGHT, timestamp++); + Test::keyboardKeyReleased(KEY_RIGHT, timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->frameGeometry(), originalGeometry.adjusted(0, 0, 8, 0)); + + // Finish the interactive move. + Test::keyboardKeyPressed(KEY_ENTER, timestamp++); + Test::keyboardKeyReleased(KEY_ENTER, timestamp++); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QCOMPARE(window->frameGeometry(), originalGeometry.adjusted(0, 0, 8, 0)); +} + +void X11WindowTest::testNetWmKeyboardResizeCancel() +{ + // This test verifies that a client can initiate a keyboard interactive resize operation and then cancel it. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(100, 100, 100, 200)); + + // Request interactive resize. + { + NETRootInfo root(c.get(), NET::Properties()); + root.moveResizeRequest(window->window(), Xcb::toXNative(window->x() + window->width()), Xcb::toXNative(window->y() + window->height()), NET::KeyboardSize, XCB_BUTTON_INDEX_1); + xcb_flush(c.get()); + } + QSignalSpy interactiveMoveResizeStartedSpy(window, &X11Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &X11Window::interactiveMoveResizeFinished); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &X11Window::interactiveMoveResizeStepped); + QVERIFY(interactiveMoveResizeStartedSpy.wait()); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QVERIFY(window->isInteractiveResize()); + + // Move the window to the right, the frame geometry will be updated some time later. + const RectF originalGeometry = window->frameGeometry(); + quint32 timestamp = 0; + Test::keyboardKeyPressed(KEY_RIGHT, timestamp++); + Test::keyboardKeyReleased(KEY_RIGHT, timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->frameGeometry(), originalGeometry.adjusted(0, 0, 8, 0)); + + // Cancel the interactive move. + { + NETRootInfo root(c.get(), NET::Properties()); + root.moveResizeRequest(window->window(), Xcb::toXNative(window->x() + window->width()), Xcb::toXNative(window->y() + window->height()), NET::MoveResizeCancel, XCB_BUTTON_INDEX_ANY); + xcb_flush(c.get()); + } + QVERIFY(interactiveMoveResizeFinishedSpy.wait()); + QCOMPARE(window->frameGeometry(), originalGeometry); +} + +void X11WindowTest::testNetWmButtonMove() +{ + // This test verifies that a client can initiate an interactive move operation controlled by the pointer. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(100, 100, 100, 200)); + + // Request interactive move. + const RectF originalGeometry = window->frameGeometry(); + quint32 timestamp = 0; + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + { + NETRootInfo root(c.get(), NET::Properties()); + root.moveResizeRequest(window->window(), Xcb::toXNative(window->x() + window->width() / 2), Xcb::toXNative(window->y() + window->height() / 2), NET::Move, XCB_BUTTON_INDEX_1); + xcb_flush(c.get()); + } + QSignalSpy interactiveMoveResizeStartedSpy(window, &X11Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &X11Window::interactiveMoveResizeFinished); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &X11Window::interactiveMoveResizeStepped); + QVERIFY(interactiveMoveResizeStartedSpy.wait()); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QVERIFY(window->isInteractiveMove()); + + // Move the window to the right. + Test::pointerMotionRelative(QPointF(8, 0), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->frameGeometry(), originalGeometry.translated(8, 0)); + + // Finish the interactive move. + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QCOMPARE(window->frameGeometry(), originalGeometry.translated(8, 0)); +} + +void X11WindowTest::testNetWmButtonMoveNotPressed() +{ + // This test verifies that an interactive move operation won't be started if the specified button is not pressed. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(100, 100, 100, 200)); + + // Request interactive move. + { + NETRootInfo root(c.get(), NET::Properties()); + root.moveResizeRequest(window->window(), Xcb::toXNative(window->x() + window->width() / 2), Xcb::toXNative(window->y() + window->height() / 2), NET::Move, XCB_BUTTON_INDEX_1); + xcb_flush(c.get()); + } + QSignalSpy interactiveMoveResizeStartedSpy(window, &X11Window::interactiveMoveResizeStarted); + QVERIFY(!interactiveMoveResizeStartedSpy.wait(10)); +} + +void X11WindowTest::testNetWmButtonMoveCancel() +{ + // This test verifies that a client can initiate an interactive move operation controlled by the pointer and then cancel it. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(100, 100, 100, 200)); + + // Request interactive move. + const RectF originalGeometry = window->frameGeometry(); + quint32 timestamp = 0; + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + auto releaseButton = qScopeGuard([×tamp]() { + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + }); + { + NETRootInfo root(c.get(), NET::Properties()); + root.moveResizeRequest(window->window(), Xcb::toXNative(window->x() + window->width() / 2), Xcb::toXNative(window->y() + window->height() / 2), NET::Move, XCB_BUTTON_INDEX_1); + xcb_flush(c.get()); + } + QSignalSpy interactiveMoveResizeStartedSpy(window, &X11Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &X11Window::interactiveMoveResizeFinished); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &X11Window::interactiveMoveResizeStepped); + QVERIFY(interactiveMoveResizeStartedSpy.wait()); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QVERIFY(window->isInteractiveMove()); + + // Move the window to the right. + Test::pointerMotionRelative(QPointF(8, 0), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->frameGeometry(), originalGeometry.translated(8, 0)); + + // Cancel the interactive move. + { + NETRootInfo root(c.get(), NET::Properties()); + root.moveResizeRequest(window->window(), Xcb::toXNative(window->x() + window->width() / 2), Xcb::toXNative(window->y() + window->height() / 2), NET::MoveResizeCancel, XCB_BUTTON_INDEX_ANY); + xcb_flush(c.get()); + } + QVERIFY(interactiveMoveResizeFinishedSpy.wait()); + QCOMPARE(window->frameGeometry(), originalGeometry); +} + +void X11WindowTest::testNetWmButtonSize_data() +{ + QTest::addColumn("gravity"); + QTest::addColumn("direction"); + + QTest::addRow("top-left") << Gravity::TopLeft << NET::Direction::TopLeft; + QTest::addRow("top") << Gravity::Top << NET::Direction::Top; + QTest::addRow("top-right") << Gravity::TopRight << NET::Direction::TopRight; + QTest::addRow("right") << Gravity::Right << NET::Direction::Right; + QTest::addRow("bottom-right") << Gravity::BottomRight << NET::Direction::BottomRight; + QTest::addRow("bottom") << Gravity::Bottom << NET::Direction::Bottom; + QTest::addRow("bottom-left") << Gravity::BottomLeft << NET::Direction::BottomLeft; + QTest::addRow("left") << Gravity::Left << NET::Direction::Left; +} + +static QPointF directionToVector(NET::Direction direction, const QSizeF &size) +{ + switch (direction) { + case NET::Direction::TopLeft: + return QPointF(-size.width(), -size.height()); + case NET::Direction::Top: + return QPointF(0, -size.height()); + case NET::Direction::TopRight: + return QPointF(size.width(), -size.height()); + case NET::Direction::Right: + return QPointF(size.width(), 0); + case NET::Direction::BottomRight: + return QPointF(size.width(), size.height()); + case NET::Direction::Bottom: + return QPointF(0, size.height()); + case NET::Direction::BottomLeft: + return QPointF(-size.width(), size.height()); + case NET::Direction::Left: + return QPointF(-size.width(), 0); + default: + Q_UNREACHABLE(); + } +} + +static RectF expandRect(const RectF &rect, NET::Direction direction, const QSizeF &amount) +{ + switch (direction) { + case NET::Direction::TopLeft: + return rect.adjusted(-amount.width(), -amount.height(), 0, 0); + case NET::Direction::Top: + return rect.adjusted(0, -amount.height(), 0, 0); + case NET::Direction::TopRight: + return rect.adjusted(0, -amount.height(), amount.width(), 0); + case NET::Direction::Right: + return rect.adjusted(0, 0, amount.width(), 0); + case NET::Direction::BottomRight: + return rect.adjusted(0, 0, amount.width(), amount.height()); + case NET::Direction::Bottom: + return rect.adjusted(0, 0, 0, amount.height()); + case NET::Direction::BottomLeft: + return rect.adjusted(-amount.width(), 0, 0, amount.height()); + case NET::Direction::Left: + return rect.adjusted(-amount.width(), 0, 0, 0); + default: + Q_UNREACHABLE(); + } +} + +void X11WindowTest::testNetWmButtonSize() +{ + // This test verifies that a client can initiate an interactive move operation controlled by the pointer. + + QFETCH(Gravity::Kind, gravity); + QFETCH(NET::Direction, direction); + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(100, 100, 300, 400)); + + // Request interactive move. + const RectF originalGeometry = window->frameGeometry(); + const QPointF initialPointer = window->frameGeometry().center() + directionToVector(direction, originalGeometry.size() * 0.5); + quint32 timestamp = 0; + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + Test::pointerMotion(initialPointer, timestamp++); + { + NETRootInfo root(c.get(), NET::Properties()); + root.moveResizeRequest(window->window(), Xcb::toXNative(initialPointer.x()), Xcb::toXNative(initialPointer.y()), direction, XCB_BUTTON_INDEX_1); + xcb_flush(c.get()); + } + QSignalSpy interactiveMoveResizeStartedSpy(window, &X11Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &X11Window::interactiveMoveResizeFinished); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &X11Window::interactiveMoveResizeStepped); + QVERIFY(interactiveMoveResizeStartedSpy.wait()); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QVERIFY(window->isInteractiveResize()); + QCOMPARE(window->interactiveMoveResizeGravity(), gravity); + + // Resize the window a tiny bit. + Test::pointerMotionRelative(directionToVector(direction, QSizeF(8, 8)), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->frameGeometry(), expandRect(originalGeometry, direction, QSizeF(8, 8))); + + // Finish the interactive move. + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QCOMPARE(window->frameGeometry(), expandRect(originalGeometry, direction, QSizeF(8, 8))); +} + +void X11WindowTest::testNetWmButtonSizeNotPressed() +{ + // This test verifies that an interactive siize operation won't be started if the specified button is not pressed. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(100, 100, 300, 400)); + + // Request interactive move. + { + NETRootInfo root(c.get(), NET::Properties()); + root.moveResizeRequest(window->window(), Xcb::toXNative(window->x()), Xcb::toXNative(window->y()), NET::TopLeft, XCB_BUTTON_INDEX_1); + xcb_flush(c.get()); + } + QSignalSpy interactiveMoveResizeStartedSpy(window, &X11Window::interactiveMoveResizeStarted); + QVERIFY(!interactiveMoveResizeStartedSpy.wait(10)); +} + +void X11WindowTest::testNetWmButtonSizeCancel() +{ + // This test verifies that a client can start an interactive resize and then cancel it. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *window = createWindow(c.get(), Rect(100, 100, 300, 400)); + + // Request interactive resize. + const RectF originalGeometry = window->frameGeometry(); + quint32 timestamp = 0; + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + auto releaseButton = qScopeGuard([×tamp]() { + Test::pointerButtonReleased(BTN_LEFT, timestamp++); + }); + Test::pointerMotion(originalGeometry.topLeft(), timestamp++); + { + NETRootInfo root(c.get(), NET::Properties()); + root.moveResizeRequest(window->window(), Xcb::toXNative(window->x()), Xcb::toXNative(window->y()), NET::TopLeft, XCB_BUTTON_INDEX_1); + xcb_flush(c.get()); + } + QSignalSpy interactiveMoveResizeStartedSpy(window, &X11Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &X11Window::interactiveMoveResizeFinished); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &X11Window::interactiveMoveResizeStepped); + QVERIFY(interactiveMoveResizeStartedSpy.wait()); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QVERIFY(window->isInteractiveResize()); + + // Resize the window a tiny bit. + Test::pointerMotionRelative(QPointF(-8, -8), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->frameGeometry(), originalGeometry.adjusted(-8, -8, 0, 0)); + + // Cancel the interactive resize. + { + NETRootInfo root(c.get(), NET::Properties()); + root.moveResizeRequest(window->window(), Xcb::toXNative(window->x()), Xcb::toXNative(window->y()), NET::MoveResizeCancel, XCB_BUTTON_INDEX_1); + xcb_flush(c.get()); + } + QVERIFY(interactiveMoveResizeFinishedSpy.wait()); + QCOMPARE(window->frameGeometry(), originalGeometry); +} + +void X11WindowTest::testMinimumSize() +{ + // This test verifies that the minimum size constraint is correctly applied. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + const Rect windowGeometry(0, 0, 100, 200); + xcb_window_t windowId = xcb_generate_id(c.get()); + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_size_hints_set_min_size(&hints, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.last().first().value(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + + // Begin resize. + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!window->isInteractiveResize()); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeWindow(), window); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QVERIFY(window->isInteractiveResize()); + + const QPointF cursorPos = KWin::Cursors::self()->mouse()->pos(); + + const qreal scale = kwinApp()->xwaylandScale(); + window->keyPressEvent(Qt::Key_Left); + window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(-8, 0)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QVERIFY(!frameGeometryChangedSpy.wait(10)); + QCOMPARE(window->clientSize().width(), 100 / scale); + + window->keyPressEvent(Qt::Key_Right); + window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QVERIFY(!frameGeometryChangedSpy.wait(10)); + QCOMPARE(window->clientSize().width(), 100 / scale); + + window->keyPressEvent(Qt::Key_Right); + window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QVERIFY(!frameGeometryChangedSpy.wait(10)); + // whilst X11 window size goes through scale, the increment is a logical value kwin side + QCOMPARE(window->clientSize().width(), 100 / scale + 8); + + window->keyPressEvent(Qt::Key_Up); + window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, -8)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QVERIFY(!frameGeometryChangedSpy.wait(10)); + QCOMPARE(window->clientSize().height(), 200 / scale); + + window->keyPressEvent(Qt::Key_Down); + window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QVERIFY(!frameGeometryChangedSpy.wait(10)); + QCOMPARE(window->clientSize().height(), 200 / scale); + + window->keyPressEvent(Qt::Key_Down); + window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 8)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 2); + QVERIFY(!frameGeometryChangedSpy.wait(10)); + QCOMPARE(window->clientSize().height(), 200 / scale + 8); + + // Finish the resize operation. + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 0); + window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!window->isInteractiveResize()); + + // Destroy the window. + QSignalSpy windowClosedSpy(window, &X11Window::closed); + xcb_unmap_window(c.get(), windowId); + xcb_destroy_window(c.get(), windowId); + xcb_flush(c.get()); + QVERIFY(windowClosedSpy.wait()); + c.reset(); +} + +void X11WindowTest::testMaximumSize() +{ + // This test verifies that the maximum size constraint is correctly applied. + + // Create an xcb window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + const Rect windowGeometry(0, 0, 100, 200); + xcb_window_t windowId = xcb_generate_id(c.get()); + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_size_hints_set_max_size(&hints, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.last().first().value(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + + // Begin resize. + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!window->isInteractiveResize()); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeWindow(), window); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QVERIFY(window->isInteractiveResize()); + + const QPointF cursorPos = KWin::Cursors::self()->mouse()->pos(); + + const qreal scale = kwinApp()->xwaylandScale(); + window->keyPressEvent(Qt::Key_Right); + window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QVERIFY(!frameGeometryChangedSpy.wait(10)); + QCOMPARE(window->clientSize().width(), 100 / scale); + + window->keyPressEvent(Qt::Key_Left); + window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos); + QVERIFY(!interactiveMoveResizeSteppedSpy.wait(10)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QCOMPARE(window->clientSize().width(), 100 / scale); + + window->keyPressEvent(Qt::Key_Left); + window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(-8, 0)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QVERIFY(!frameGeometryChangedSpy.wait(10)); + QCOMPARE(window->clientSize().width(), 100 / scale - 8); + + window->keyPressEvent(Qt::Key_Down); + window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(-8, 8)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QVERIFY(!frameGeometryChangedSpy.wait(10)); + QCOMPARE(window->clientSize().height(), 200 / scale); + + window->keyPressEvent(Qt::Key_Up); + window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(-8, 0)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QVERIFY(!frameGeometryChangedSpy.wait(10)); + QCOMPARE(window->clientSize().height(), 200 / scale); + + window->keyPressEvent(Qt::Key_Up); + window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(-8, -8)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 2); + QVERIFY(!frameGeometryChangedSpy.wait(10)); + QCOMPARE(window->clientSize().height(), 200 / scale - 8); + + // Finish the resize operation. + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 0); + window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!window->isInteractiveResize()); + + // Destroy the window. + QSignalSpy windowClosedSpy(window, &X11Window::closed); + xcb_unmap_window(c.get(), windowId); + xcb_destroy_window(c.get(), windowId); + xcb_flush(c.get()); + QVERIFY(windowClosedSpy.wait()); + c.reset(); +} + +void X11WindowTest::testTrimCaption_data() +{ + QTest::addColumn("originalTitle"); + QTest::addColumn("expectedTitle"); + + QTest::newRow("simplified") + << QByteArrayLiteral("Was tun, wenn Schüler Autismus haben?\342\200\250\342\200\250\342\200\250 – Marlies Hübner - Mozilla Firefox") + << QByteArrayLiteral("Was tun, wenn Schüler Autismus haben? – Marlies Hübner - Mozilla Firefox"); + + QTest::newRow("with emojis") + << QByteArrayLiteral("\bTesting non\302\255printable:\177, emoij:\360\237\230\203, non-characters:\357\277\276") + << QByteArrayLiteral("Testing nonprintable:, emoij:\360\237\230\203, non-characters:"); +} + +void X11WindowTest::testTrimCaption() +{ + // this test verifies that caption is properly trimmed + + // create an xcb window + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + const Rect windowGeometry(0, 0, 100, 200); + xcb_window_t windowId = xcb_generate_id(c.get()); + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + NETWinInfo winInfo(c.get(), windowId, rootWindow(), NET::Properties(), NET::Properties2()); + QFETCH(QByteArray, originalTitle); + winInfo.setName(originalTitle); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + // we should get a window for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.first().first().value(); + QVERIFY(window); + QCOMPARE(window->window(), windowId); + QFETCH(QByteArray, expectedTitle); + QCOMPARE(window->caption(), QString::fromUtf8(expectedTitle)); + + // and destroy the window again + xcb_unmap_window(c.get(), windowId); + xcb_flush(c.get()); + + QSignalSpy windowClosedSpy(window, &X11Window::closed); + QVERIFY(windowClosedSpy.wait()); + xcb_destroy_window(c.get(), windowId); + c.reset(); +} + +void X11WindowTest::testFullscreenLayerWithActiveWaylandWindow() +{ + // this test verifies that an X11 fullscreen window does not stay in the active layer + // when a Wayland window is active, see BUG: 375759 + + // first create an X11 window + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + const Rect windowGeometry(0, 0, 100, 200); + xcb_window_t windowId = xcb_generate_id(c.get()); + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + // we should get a window for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.first().first().value(); + QVERIFY(window); + QCOMPARE(window->window(), windowId); + QVERIFY(!window->isFullScreen()); + QVERIFY(window->isActive()); + QCOMPARE(window->layer(), NormalLayer); + + workspace()->slotWindowFullScreen(); + QVERIFY(window->isFullScreen()); + QCOMPARE(window->layer(), ActiveLayer); + QCOMPARE(workspace()->stackingOrder().last(), window); + + // now let's open a Wayland window + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto waylandWindow = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(waylandWindow); + QVERIFY(waylandWindow->isActive()); + QCOMPARE(waylandWindow->layer(), NormalLayer); + QCOMPARE(workspace()->stackingOrder().last(), waylandWindow); + QCOMPARE(workspace()->stackingOrder().last(), waylandWindow); + QCOMPARE(window->layer(), NormalLayer); + + // now activate fullscreen again + workspace()->activateWindow(window); + QTRY_VERIFY(window->isActive()); + QCOMPARE(window->layer(), ActiveLayer); + QCOMPARE(workspace()->stackingOrder().last(), window); + QCOMPARE(workspace()->stackingOrder().last(), window); + + // activate wayland window again + workspace()->activateWindow(waylandWindow); + QTRY_VERIFY(waylandWindow->isActive()); + QCOMPARE(workspace()->stackingOrder().last(), waylandWindow); + QCOMPARE(workspace()->stackingOrder().last(), waylandWindow); + + // back to x window + workspace()->activateWindow(window); + QTRY_VERIFY(window->isActive()); + // remove fullscreen + QVERIFY(window->isFullScreen()); + workspace()->slotWindowFullScreen(); + QVERIFY(!window->isFullScreen()); + // and fullscreen again + workspace()->slotWindowFullScreen(); + QVERIFY(window->isFullScreen()); + QCOMPARE(workspace()->stackingOrder().last(), window); + QCOMPARE(workspace()->stackingOrder().last(), window); + + // activate wayland window again + workspace()->activateWindow(waylandWindow); + QTRY_VERIFY(waylandWindow->isActive()); + QCOMPARE(workspace()->stackingOrder().last(), waylandWindow); + QCOMPARE(workspace()->stackingOrder().last(), waylandWindow); + + // back to X11 window + workspace()->activateWindow(window); + QTRY_VERIFY(window->isActive()); + // remove fullscreen + QVERIFY(window->isFullScreen()); + workspace()->slotWindowFullScreen(); + QVERIFY(!window->isFullScreen()); + + // and fullscreen through X API + { + Xcb::sync(); // sync so NETWinInfo fetches the correct current state + + NETWinInfo info(c.get(), windowId, kwinApp()->x11RootWindow(), NET::Properties(), NET::Properties2()); + info.setState(NET::FullScreen, NET::FullScreen); + NETRootInfo rootInfo(c.get(), NET::Properties()); + rootInfo.setActiveWindow(windowId, NET::FromApplication, XCB_CURRENT_TIME, XCB_WINDOW_NONE); + xcb_flush(c.get()); + QTRY_VERIFY(window->isFullScreen()); + QCOMPARE(workspace()->stackingOrder().last(), window); + QCOMPARE(workspace()->stackingOrder().last(), window); + } + + // activate wayland window again + workspace()->activateWindow(waylandWindow); + QTRY_VERIFY(waylandWindow->isActive()); + QCOMPARE(workspace()->stackingOrder().last(), waylandWindow); + QCOMPARE(workspace()->stackingOrder().last(), waylandWindow); + QCOMPARE(window->layer(), NormalLayer); + + // close the window + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowClosed(waylandWindow)); + QTRY_VERIFY(window->isActive()); + QCOMPARE(window->layer(), ActiveLayer); + + // and destroy the window again + xcb_unmap_window(c.get(), windowId); + xcb_flush(c.get()); +} + +void X11WindowTest::testFocusInWithWaylandLastActiveWindow() +{ + // this test verifies that Workspace::allowWindowActivation does not crash if last client was a Wayland client + + // create an X11 window + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + const Rect windowGeometry(0, 0, 100, 200); + xcb_window_t windowId = xcb_generate_id(c.get()); + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + // we should get a window for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.first().first().value(); + QVERIFY(window); + QCOMPARE(window->window(), windowId); + QVERIFY(window->isActive()); + + // create Wayland window + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto waylandWindow = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(waylandWindow); + QVERIFY(waylandWindow->isActive()); + // activate no window + workspace()->activateWindow(nullptr); + QVERIFY(!waylandWindow->isActive()); + QVERIFY(!workspace()->activeWindow()); + // and close Wayland window again + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowClosed(waylandWindow)); + + // and try to activate the x11 window through X11 api + const auto cookie = xcb_set_input_focus_checked(c.get(), XCB_INPUT_FOCUS_NONE, windowId, XCB_CURRENT_TIME); + auto error = xcb_request_check(c.get(), cookie); + QVERIFY(!error); + // this accesses m_lastActiveWindow on trying to activate + QTRY_VERIFY(window->isActive()); + + // and destroy the window again + xcb_unmap_window(c.get(), windowId); + xcb_flush(c.get()); +} + +void X11WindowTest::testCaptionChanges() +{ + // verifies that caption is updated correctly when the X11 window updates it + // BUG: 383444 + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + const Rect windowGeometry(0, 0, 100, 200); + xcb_window_t windowId = xcb_generate_id(c.get()); + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + NETWinInfo info(c.get(), windowId, kwinApp()->x11RootWindow(), NET::Properties(), NET::Properties2()); + info.setName("foo"); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + // we should get a window for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.first().first().value(); + QVERIFY(window); + QCOMPARE(window->window(), windowId); + QCOMPARE(window->caption(), QStringLiteral("foo")); + + QSignalSpy captionChangedSpy(window, &X11Window::captionChanged); + info.setName("bar"); + xcb_flush(c.get()); + QVERIFY(captionChangedSpy.wait()); + QCOMPARE(window->caption(), QStringLiteral("bar")); + + // and destroy the window again + QSignalSpy windowClosedSpy(window, &X11Window::closed); + xcb_unmap_window(c.get(), windowId); + xcb_flush(c.get()); + QVERIFY(windowClosedSpy.wait()); + xcb_destroy_window(c.get(), windowId); + c.reset(); +} + +void X11WindowTest::testCaptionWmName() +{ + // this test verifies that a caption set through WM_NAME is read correctly + + // open glxgears as that one only uses WM_NAME + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + + QProcess glxgears; + glxgears.setProgram(QStringLiteral("glxgears")); + glxgears.start(); + QVERIFY(glxgears.waitForStarted()); + + QVERIFY(windowAddedSpy.wait()); + QCOMPARE(windowAddedSpy.count(), 1); + QCOMPARE(workspace()->windows().count(), 1); + Window *glxgearsWindow = workspace()->windows().first(); + QCOMPARE(glxgearsWindow->caption(), QStringLiteral("glxgears")); + + glxgears.terminate(); + QVERIFY(glxgears.waitForFinished()); +} + +void X11WindowTest::testFullscreenWindowGroups() +{ + // this test creates an X11 window and puts it to full screen + // then a second window is created which is in the same window group + // BUG: 388310 + + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + const Rect windowGeometry(0, 0, 100, 200); + xcb_window_t windowId = xcb_generate_id(c.get()); + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.first().first().value(); + QVERIFY(window); + QCOMPARE(window->window(), windowId); + QCOMPARE(window->isActive(), true); + + QCOMPARE(window->isFullScreen(), false); + QCOMPARE(window->layer(), NormalLayer); + workspace()->slotWindowFullScreen(); + QCOMPARE(window->isFullScreen(), true); + QCOMPARE(window->layer(), ActiveLayer); + + // now let's create a second window + windowCreatedSpy.clear(); + xcb_window_t w2 = xcb_generate_id(c.get()); + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, w2, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints2{}; + xcb_icccm_size_hints_set_position(&hints2, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints2, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), w2, &hints2); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, w2, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + xcb_map_window(c.get(), w2); + xcb_flush(c.get()); + + QVERIFY(windowCreatedSpy.wait()); + X11Window *window2 = windowCreatedSpy.first().first().value(); + QVERIFY(window2); + QVERIFY(window != window2); + QCOMPARE(window2->window(), w2); + QCOMPARE(window2->isActive(), true); + QCOMPARE(window2->group(), window->group()); + // first window should be moved back to normal layer + QCOMPARE(window->isActive(), false); + QCOMPARE(window->isFullScreen(), true); + QCOMPARE(window->layer(), NormalLayer); + + // activating the fullscreen window again, should move it to active layer + workspace()->activateWindow(window); + QTRY_COMPARE(window->layer(), ActiveLayer); +} + +void X11WindowTest::testActivateFocusedWindow() +{ + // The window manager may call XSetInputFocus() on a window that already has focus, in which + // case no FocusIn event will be generated and the window won't be marked as active. This test + // verifies that we handle that subtle case properly. + + Test::XcbConnectionPtr connection = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(connection.get())); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + + const Rect windowGeometry(0, 0, 100, 200); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + + // Create the first test window. + const xcb_window_t windowId1 = xcb_generate_id(connection.get()); + xcb_create_window(connection.get(), XCB_COPY_FROM_PARENT, windowId1, rootWindow(), + windowGeometry.x(), windowGeometry.y(), + windowGeometry.width(), windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_icccm_set_wm_normal_hints(connection.get(), windowId1, &hints); + xcb_change_property(connection.get(), XCB_PROP_MODE_REPLACE, windowId1, + atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId1); + xcb_map_window(connection.get(), windowId1); + xcb_flush(connection.get()); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window1 = windowCreatedSpy.first().first().value(); + QVERIFY(window1); + QCOMPARE(window1->window(), windowId1); + QCOMPARE(window1->isActive(), true); + + // Create the second test window. + const xcb_window_t windowId2 = xcb_generate_id(connection.get()); + xcb_create_window(connection.get(), XCB_COPY_FROM_PARENT, windowId2, rootWindow(), + windowGeometry.x(), windowGeometry.y(), + windowGeometry.width(), windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_icccm_set_wm_normal_hints(connection.get(), windowId2, &hints); + xcb_change_property(connection.get(), XCB_PROP_MODE_REPLACE, windowId2, + atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId2); + xcb_map_window(connection.get(), windowId2); + xcb_flush(connection.get()); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window2 = windowCreatedSpy.last().first().value(); + QVERIFY(window2); + QCOMPARE(window2->window(), windowId2); + QCOMPARE(window2->isActive(), true); + + // When the second test window is destroyed, the window manager will attempt to activate the + // next window in the focus chain, which is the first window. + xcb_set_input_focus(connection.get(), XCB_INPUT_FOCUS_POINTER_ROOT, windowId1, XCB_CURRENT_TIME); + xcb_destroy_window(connection.get(), windowId2); + xcb_flush(connection.get()); + QVERIFY(Test::waitForWindowClosed(window2)); + QVERIFY(window1->isActive()); + + // Destroy the first test window. + xcb_destroy_window(connection.get(), windowId1); + xcb_flush(connection.get()); + QVERIFY(Test::waitForWindowClosed(window1)); +} + +void X11WindowTest::testReentrantMoveResize() +{ + // This test verifies that calling moveResize() from a slot connected directly + // to the frameGeometryChanged() signal won't cause an infinite recursion. + + // Create a test window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + const Rect windowGeometry(0, 0, 100, 200); + xcb_window_t windowId = xcb_generate_id(c.get()); + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.first().first().value(); + QVERIFY(window); + QCOMPARE(window->pos(), QPoint(0, 0)); + + // Let's pretend that there is a script that really wants the window to be at (100, 100). + connect(window, &Window::frameGeometryChanged, this, [window]() { + window->moveResize(RectF(QPointF(100, 100), window->size())); + }); + + // Trigger the lambda above. + window->move(QPoint(40, 50)); + + // Eventually, the window will end up at (100, 100). + QCOMPARE(window->pos(), QPoint(100, 100)); + + // Destroy the test window. + xcb_destroy_window(c.get(), windowId); + xcb_flush(c.get()); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void X11WindowTest::testTransient() +{ + // Create a parent and a child windows. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *parent = createWindow(c.get(), Rect(0, 0, 100, 200)); + X11Window *child = createWindow(c.get(), Rect(0, 0, 100, 200), [&c, &parent](xcb_window_t windowId) { + xcb_icccm_set_wm_transient_for(c.get(), windowId, parent->window()); + }); + QVERIFY(child->isTransient()); + QCOMPARE(child->transientFor(), parent); + QVERIFY(parent->hasTransient(child, true)); +} + +void X11WindowTest::testGroupTransient() +{ + // Create the leader, a follower and a dialog window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *leader = createWindow(c.get(), Rect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + }); + X11Window *follower = createWindow(c.get(), Rect(0, 0, 100, 200), [&c, &leader](xcb_window_t windowId) { + const xcb_window_t leaderId = leader->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + }); + X11Window *dialog = createWindow(c.get(), Rect(0, 0, 100, 200), [&c, &leader](xcb_window_t windowId) { + const xcb_window_t leaderId = leader->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + xcb_icccm_set_wm_transient_for(c.get(), windowId, kwinApp()->x11RootWindow()); + }); + QVERIFY(dialog->isTransient()); + QCOMPARE(dialog->transientFor(), nullptr); + QVERIFY(leader->hasTransient(dialog, true)); + QVERIFY(follower->hasTransient(dialog, true)); + + // The group transient should not act as transient for unrelated windows. + Test::XcbConnectionPtr c1 = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *unrelated = createWindow(c1.get(), Rect(0, 0, 100, 200), [&c1](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c1.get(), windowId, 7, "foo\0foo"); + }); + QVERIFY(!unrelated->hasTransient(dialog, true)); +} + +void X11WindowTest::testCloseTransient() +{ + // This test verifies that the parent window will be activated when a transient is closed. + + // Create a parent and a child windows. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *parent = createWindow(c.get(), Rect(0, 0, 100, 200)); + X11Window *child = createWindow(c.get(), Rect(0, 0, 100, 200), [&c, &parent](xcb_window_t windowId) { + xcb_icccm_set_wm_transient_for(c.get(), windowId, parent->window()); + }); + QCOMPARE(child->transientFor(), parent); + QCOMPARE(workspace()->activeWindow(), child); + + // Close the child. + QSignalSpy childClosedSpy(child, &Window::closed); + xcb_unmap_window(c.get(), child->window()); + xcb_destroy_window(c.get(), child->window()); + xcb_flush(c.get()); + QVERIFY(childClosedSpy.wait()); + QCOMPARE(workspace()->activeWindow(), parent); +} + +void X11WindowTest::testCloseInactiveTransient() +{ + // This test verifies that the parent window will not be activated when an inactive transient is closed. + + // Create a parent and a child windows. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *parent = createWindow(c.get(), Rect(0, 0, 100, 200)); + X11Window *child = createWindow(c.get(), Rect(0, 0, 100, 200), [&c, &parent](xcb_window_t windowId) { + xcb_icccm_set_wm_transient_for(c.get(), windowId, parent->window()); + }); + QCOMPARE(child->transientFor(), parent); + QCOMPARE(workspace()->activeWindow(), child); + + // Show another window. + Test::XcbConnectionPtr c1 = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *unrelated = createWindow(c1.get(), Rect(0, 0, 100, 200), [&c1](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c1.get(), windowId, 7, "foo\0foo"); + }); + QCOMPARE(workspace()->activeWindow(), unrelated); + + // Close the child. + QSignalSpy childClosedSpy(child, &Window::closed); + xcb_unmap_window(c.get(), child->window()); + xcb_destroy_window(c.get(), child->window()); + xcb_flush(c.get()); + QVERIFY(childClosedSpy.wait()); + QCOMPARE(workspace()->activeWindow(), unrelated); +} + +void X11WindowTest::testCloseGroupTransient() +{ + // This test verifies that when an active group transient is closed, the focus will be passed to one of its main windows. + + // Create the leader, a follower and a dialog window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *leader = createWindow(c.get(), Rect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + }); + X11Window *follower = createWindow(c.get(), Rect(0, 0, 100, 200), [&c, &leader](xcb_window_t windowId) { + const xcb_window_t leaderId = leader->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + }); + X11Window *dialog = createWindow(c.get(), Rect(0, 0, 100, 200), [&c, &leader](xcb_window_t windowId) { + const xcb_window_t leaderId = leader->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + xcb_icccm_set_wm_transient_for(c.get(), windowId, kwinApp()->x11RootWindow()); + }); + QVERIFY(dialog->isTransient()); + QCOMPARE(dialog->transientFor(), nullptr); + QVERIFY(leader->hasTransient(dialog, true)); + QVERIFY(follower->hasTransient(dialog, true)); + QCOMPARE(workspace()->activeWindow(), dialog); + + // Close the dialog. + QSignalSpy dialogClosedSpy(dialog, &Window::closed); + xcb_unmap_window(c.get(), dialog->window()); + xcb_destroy_window(c.get(), dialog->window()); + xcb_flush(c.get()); + QVERIFY(dialogClosedSpy.wait()); + QVERIFY(workspace()->activeWindow() == leader || workspace()->activeWindow() == follower); +} + +void X11WindowTest::testCloseInactiveGroupTransient() +{ + // This test verifies that when an inactive group transient is closed, the focus will not be passed to one of its main windows. + + // Create the leader, a follower and a dialog window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *leader = createWindow(c.get(), Rect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + }); + X11Window *follower = createWindow(c.get(), Rect(0, 0, 100, 200), [&c, &leader](xcb_window_t windowId) { + const xcb_window_t leaderId = leader->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + }); + X11Window *dialog = createWindow(c.get(), Rect(0, 0, 100, 200), [&c, &leader](xcb_window_t windowId) { + const xcb_window_t leaderId = leader->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + xcb_icccm_set_wm_transient_for(c.get(), windowId, kwinApp()->x11RootWindow()); + }); + QVERIFY(dialog->isTransient()); + QCOMPARE(dialog->transientFor(), nullptr); + QVERIFY(leader->hasTransient(dialog, true)); + QVERIFY(follower->hasTransient(dialog, true)); + QCOMPARE(workspace()->activeWindow(), dialog); + + // Show another window. + Test::XcbConnectionPtr c1 = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *unrelated = createWindow(c1.get(), Rect(0, 0, 100, 200), [&c1](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c1.get(), windowId, 7, "foo\0foo"); + }); + QCOMPARE(workspace()->activeWindow(), unrelated); + + // Close the dialog. + QSignalSpy dialogClosedSpy(dialog, &Window::closed); + xcb_unmap_window(c.get(), dialog->window()); + xcb_destroy_window(c.get(), dialog->window()); + xcb_flush(c.get()); + QVERIFY(dialogClosedSpy.wait()); + QCOMPARE(workspace()->activeWindow(), unrelated); +} + +void X11WindowTest::testModal() +{ + // Create a parent and a child windows. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *parent = createWindow(c.get(), Rect(0, 0, 100, 200)); + X11Window *child = createWindow(c.get(), Rect(0, 0, 100, 200), [&c, &parent](xcb_window_t windowId) { + xcb_icccm_set_wm_transient_for(c.get(), windowId, parent->window()); + }); + QVERIFY(!child->isModal()); + QCOMPARE(child->transientFor(), parent); + + // Set modal state. + { + NETWinInfo info(c.get(), child->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::Modal, NET::Modal); + xcb_flush(c.get()); + } + QSignalSpy modalChangedSpy(child, &Window::modalChanged); + QVERIFY(modalChangedSpy.wait()); + QVERIFY(child->isModal()); + + // Unset modal state. + { + NETWinInfo info(c.get(), child->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::State(), NET::Modal); + xcb_flush(c.get()); + } + QVERIFY(modalChangedSpy.wait()); + QVERIFY(!child->isModal()); + + // Set modal state and try to activate the parent window, it should not succeed. + { + NETWinInfo info(c.get(), child->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::Modal, NET::Modal); + xcb_flush(c.get()); + } + QVERIFY(modalChangedSpy.wait()); + QVERIFY(child->isModal()); + workspace()->activateWindow(parent); + QCOMPARE(workspace()->activeWindow(), child); + + // It should be okay to activate an unrelated window. + Test::XcbConnectionPtr c1 = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *unrelated = createWindow(c1.get(), Rect(0, 0, 100, 200), [&c1](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c1.get(), windowId, 7, "foo\0foo"); + }); + QCOMPARE(workspace()->activeWindow(), unrelated); +} + +void X11WindowTest::testGroupModal() +{ + // This test verifies that a dialog can be modal to the window group. + + // Create the leader, a follower and a dialog window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *leader = createWindow(c.get(), Rect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + }); + X11Window *follower = createWindow(c.get(), Rect(0, 0, 100, 200), [&c, &leader](xcb_window_t windowId) { + const xcb_window_t leaderId = leader->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + }); + X11Window *dialog = createWindow(c.get(), Rect(0, 0, 100, 200), [&c, &leader](xcb_window_t windowId) { + const xcb_window_t leaderId = leader->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + xcb_icccm_set_wm_transient_for(c.get(), windowId, kwinApp()->x11RootWindow()); + }); + QVERIFY(dialog->isTransient()); + QVERIFY(leader->hasTransient(dialog, true)); + QVERIFY(follower->hasTransient(dialog, true)); + + // Set modal state. + { + NETWinInfo info(c.get(), dialog->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::Modal, NET::Modal); + xcb_flush(c.get()); + } + QSignalSpy modalChangedSpy(dialog, &Window::modalChanged); + QVERIFY(modalChangedSpy.wait()); + QVERIFY(dialog->isModal()); + + // Unset modal state. + { + NETWinInfo info(c.get(), dialog->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::State(), NET::Modal); + xcb_flush(c.get()); + } + QVERIFY(modalChangedSpy.wait()); + QVERIFY(!dialog->isModal()); + + // Set modal state and try to activate other windows in the group, it should not succeed. + { + NETWinInfo info(c.get(), dialog->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::Modal, NET::Modal); + xcb_flush(c.get()); + } + QVERIFY(modalChangedSpy.wait()); + QVERIFY(dialog->isModal()); + workspace()->activateWindow(leader); + QCOMPARE(workspace()->activeWindow(), dialog); + workspace()->activateWindow(follower); + QCOMPARE(workspace()->activeWindow(), dialog); + + // It should be okay to activate an unrelated window. + Test::XcbConnectionPtr c1 = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *unrelated = createWindow(c1.get(), Rect(0, 0, 100, 200), [&c1](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c1.get(), windowId, 7, "foo\0foo"); + }); + QCOMPARE(workspace()->activeWindow(), unrelated); +} + +void X11WindowTest::testCloseModal() +{ + // This test verifies that the parent window will be activated when an active modal dialog is closed. + + // Create a parent and a child windows. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *parent = createWindow(c.get(), Rect(0, 0, 100, 200)); + X11Window *child = createWindow(c.get(), Rect(0, 0, 100, 200), [&c, &parent](xcb_window_t windowId) { + xcb_icccm_set_wm_transient_for(c.get(), windowId, parent->window()); + }); + QVERIFY(!child->isModal()); + QCOMPARE(child->transientFor(), parent); + + // Set modal state. + { + NETWinInfo info(c.get(), child->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::Modal, NET::Modal); + xcb_flush(c.get()); + } + QSignalSpy modalChangedSpy(child, &Window::modalChanged); + QVERIFY(modalChangedSpy.wait()); + QVERIFY(child->isModal()); + QCOMPARE(workspace()->activeWindow(), child); + + // Close the child. + QSignalSpy childClosedSpy(child, &Window::closed); + xcb_unmap_window(c.get(), child->window()); + xcb_destroy_window(c.get(), child->window()); + xcb_flush(c.get()); + QVERIFY(childClosedSpy.wait()); + QCOMPARE(workspace()->activeWindow(), parent); +} + +void X11WindowTest::testCloseInactiveModal() +{ + // This test verifies that the parent window will not be activated when an inactive modal dialog is closed. + + // Create a parent and a child windows. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *parent = createWindow(c.get(), Rect(0, 0, 100, 200)); + X11Window *child = createWindow(c.get(), Rect(0, 0, 100, 200), [&c, &parent](xcb_window_t windowId) { + xcb_icccm_set_wm_transient_for(c.get(), windowId, parent->window()); + }); + QVERIFY(!child->isModal()); + QCOMPARE(child->transientFor(), parent); + + // Set modal state. + { + NETWinInfo info(c.get(), child->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::Modal, NET::Modal); + xcb_flush(c.get()); + } + QSignalSpy modalChangedSpy(child, &Window::modalChanged); + QVERIFY(modalChangedSpy.wait()); + QVERIFY(child->isModal()); + QCOMPARE(workspace()->activeWindow(), child); + + // Show another window. + Test::XcbConnectionPtr c1 = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *unrelated = createWindow(c1.get(), Rect(0, 0, 100, 200), [&c1](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c1.get(), windowId, 7, "foo\0foo"); + }); + QCOMPARE(workspace()->activeWindow(), unrelated); + + // Close the child. + QSignalSpy childClosedSpy(child, &Window::closed); + xcb_unmap_window(c.get(), child->window()); + xcb_destroy_window(c.get(), child->window()); + xcb_flush(c.get()); + QVERIFY(childClosedSpy.wait()); + QCOMPARE(workspace()->activeWindow(), unrelated); +} + +void X11WindowTest::testCloseGroupModal() +{ + // This test verifies that when an active modal group dialog is closed, the focus will be passed to one of its main windows. + + // Create the leader, a follower and a dialog window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *leader = createWindow(c.get(), Rect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + }); + X11Window *follower = createWindow(c.get(), Rect(0, 0, 100, 200), [&c, &leader](xcb_window_t windowId) { + const xcb_window_t leaderId = leader->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + }); + X11Window *dialog = createWindow(c.get(), Rect(0, 0, 100, 200), [&c, &leader](xcb_window_t windowId) { + const xcb_window_t leaderId = leader->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + xcb_icccm_set_wm_transient_for(c.get(), windowId, kwinApp()->x11RootWindow()); + }); + QVERIFY(dialog->isTransient()); + QVERIFY(leader->hasTransient(dialog, true)); + QVERIFY(follower->hasTransient(dialog, true)); + + // Set modal state. + { + NETWinInfo info(c.get(), dialog->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::Modal, NET::Modal); + xcb_flush(c.get()); + } + QSignalSpy modalChangedSpy(dialog, &Window::modalChanged); + QVERIFY(modalChangedSpy.wait()); + QVERIFY(dialog->isModal()); + QCOMPARE(workspace()->activeWindow(), dialog); + + // Close the dialog. + QSignalSpy dialogClosedSpy(dialog, &Window::closed); + xcb_unmap_window(c.get(), dialog->window()); + xcb_destroy_window(c.get(), dialog->window()); + xcb_flush(c.get()); + QVERIFY(dialogClosedSpy.wait()); + QVERIFY(workspace()->activeWindow() == leader || workspace()->activeWindow() == follower); +} + +void X11WindowTest::testCloseInactiveGroupModal() +{ + // This test verifies that when an inactive modal group dialog is closed, the focus will not be passed to one of its main windows. + + // Create the leader, a follower and a dialog window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *leader = createWindow(c.get(), Rect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + }); + X11Window *follower = createWindow(c.get(), Rect(0, 0, 100, 200), [&c, &leader](xcb_window_t windowId) { + const xcb_window_t leaderId = leader->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + }); + X11Window *dialog = createWindow(c.get(), Rect(0, 0, 100, 200), [&c, &leader](xcb_window_t windowId) { + const xcb_window_t leaderId = leader->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + xcb_icccm_set_wm_transient_for(c.get(), windowId, kwinApp()->x11RootWindow()); + }); + QVERIFY(dialog->isTransient()); + QVERIFY(leader->hasTransient(dialog, true)); + QVERIFY(follower->hasTransient(dialog, true)); + + // Set modal state. + { + NETWinInfo info(c.get(), dialog->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::Modal, NET::Modal); + xcb_flush(c.get()); + } + QSignalSpy modalChangedSpy(dialog, &Window::modalChanged); + QVERIFY(modalChangedSpy.wait()); + QVERIFY(dialog->isModal()); + QCOMPARE(workspace()->activeWindow(), dialog); + + // Show another window. + Test::XcbConnectionPtr c1 = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *unrelated = createWindow(c1.get(), Rect(0, 0, 100, 200), [&c1](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c1.get(), windowId, 7, "foo\0foo"); + }); + QCOMPARE(workspace()->activeWindow(), unrelated); + + // Close the dialog. + QSignalSpy dialogClosedSpy(dialog, &Window::closed); + xcb_unmap_window(c.get(), dialog->window()); + xcb_destroy_window(c.get(), dialog->window()); + xcb_flush(c.get()); + QVERIFY(dialogClosedSpy.wait()); + QCOMPARE(workspace()->activeWindow(), unrelated); +} + +void X11WindowTest::testStackAboveFromApplication() +{ + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + NETRootInfo root(c.get(), NET::Properties()); + + X11Window *window1 = createWindow(c.get(), Rect(0, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + xcb_icccm_set_wm_class(c.get(), windowId, 7, "foo\0foo"); + }); + X11Window *window2 = createWindow(c.get(), Rect(0, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_window_t leaderId = window1->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + xcb_icccm_set_wm_class(c.get(), windowId, 7, "foo\0foo"); + }); + X11Window *window3 = createWindow(c.get(), Rect(0, 0, 100, 100), [&c](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "bar\0bar"); + }); + X11Window *window4 = createWindow(c.get(), Rect(0, 0, 100, 100), [&c](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "baz\0baz"); + }); + + // window1 and window2 belong to the same application, window1 will be raised only above window2. + root.restackRequest(window1->window(), NET::FromApplication, XCB_WINDOW_NONE, XCB_STACK_MODE_ABOVE, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window2, window1, window3, window4})); + + // There are no other windows that belong to the same application as window3, so the stack won't change. + root.restackRequest(window3->window(), NET::FromApplication, XCB_WINDOW_NONE, XCB_STACK_MODE_ABOVE, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(!stackingOrderChangedSpy.wait(10)); + QCOMPARE(workspace()->stackingOrder(), (QList{window2, window1, window3, window4})); +} + +void X11WindowTest::testStackAboveFromTool() +{ + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + NETRootInfo root(c.get(), NET::Properties()); + + X11Window *window1 = createWindow(c.get(), Rect(0, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + xcb_icccm_set_wm_class(c.get(), windowId, 7, "foo\0foo"); + }); + X11Window *window2 = createWindow(c.get(), Rect(0, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_window_t leaderId = window1->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + xcb_icccm_set_wm_class(c.get(), windowId, 7, "foo\0foo"); + }); + X11Window *window3 = createWindow(c.get(), Rect(0, 0, 100, 100), [&c](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "bar\0bar"); + }); + X11Window *window4 = createWindow(c.get(), Rect(0, 0, 100, 100), [&c](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "baz\0baz"); + }); + + // window1 and window2 belong to the same application, but window1 will be raised globally because of the from_tool flag. + root.restackRequest(window1->window(), NET::FromTool, XCB_WINDOW_NONE, XCB_STACK_MODE_ABOVE, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window2, window3, window4, window1})); + + // same with window3, whether there are other windows that belong to the same application doesn't matter. + root.restackRequest(window3->window(), NET::FromTool, XCB_WINDOW_NONE, XCB_STACK_MODE_ABOVE, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window2, window4, window1, window3})); +} + +void X11WindowTest::testStackAboveSibling() +{ + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + NETRootInfo root(c.get(), NET::Properties()); + + X11Window *window1 = createWindow(c.get(), Rect(0, 0, 100, 100), [&c](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "foo\0foo"); + }); + X11Window *window2 = createWindow(c.get(), Rect(0, 0, 100, 100), [&c](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "bar\0bar"); + }); + X11Window *window3 = createWindow(c.get(), Rect(0, 0, 100, 100), [&c](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "baz\0baz"); + }); + + // Restack window1 above window3. + root.restackRequest(window1->window(), NET::FromApplication, window3->window(), XCB_STACK_MODE_ABOVE, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window2, window3, window1})); + + // Repeat again. + root.restackRequest(window1->window(), NET::FromApplication, window3->window(), XCB_STACK_MODE_ABOVE, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(!stackingOrderChangedSpy.wait(10)); + QCOMPARE(workspace()->stackingOrder(), (QList{window2, window3, window1})); + + // Restack window1 above window2. Note that since window1 is already window2, it's okay if the WM noops. + root.restackRequest(window1->window(), NET::FromApplication, window2->window(), XCB_STACK_MODE_ABOVE, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window2, window1, window3})); +} + +void X11WindowTest::testStackBelowFromApplication() +{ + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + NETRootInfo root(c.get(), NET::Properties()); + + X11Window *window1 = createWindow(c.get(), Rect(0, 0, 100, 100), [&c](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "foo\0foo"); + }); + X11Window *window2 = createWindow(c.get(), Rect(0, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + xcb_icccm_set_wm_class(c.get(), windowId, 7, "bar\0bar"); + }); + X11Window *window3 = createWindow(c.get(), Rect(0, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_window_t leaderId = window2->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + xcb_icccm_set_wm_class(c.get(), windowId, 7, "bar\0bar"); + }); + X11Window *window4 = createWindow(c.get(), Rect(0, 0, 100, 100), [&c](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "baz\0baz"); + }); + + root.restackRequest(window3->window(), NET::FromApplication, XCB_WINDOW_NONE, XCB_STACK_MODE_BELOW, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window3, window1, window2, window4})); + + root.restackRequest(window4->window(), NET::FromApplication, XCB_WINDOW_NONE, XCB_STACK_MODE_BELOW, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window4, window3, window1, window2})); +} + +void X11WindowTest::testStackBelowFromTool() +{ + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + NETRootInfo root(c.get(), NET::Properties()); + + X11Window *window1 = createWindow(c.get(), Rect(0, 0, 100, 100), [&c](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "foo\0foo"); + }); + X11Window *window2 = createWindow(c.get(), Rect(0, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + xcb_icccm_set_wm_class(c.get(), windowId, 7, "bar\0bar"); + }); + X11Window *window3 = createWindow(c.get(), Rect(0, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_window_t leaderId = window2->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + xcb_icccm_set_wm_class(c.get(), windowId, 7, "bar\0bar"); + }); + X11Window *window4 = createWindow(c.get(), Rect(0, 0, 100, 100), [&c](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "baz\0baz"); + }); + + // window2 and window3 belong to the same application, but window3 will be lowered globally because of the from_tool flag. + root.restackRequest(window3->window(), NET::FromApplication, XCB_WINDOW_NONE, XCB_STACK_MODE_BELOW, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window3, window1, window2, window4})); + + // same with window4. + root.restackRequest(window4->window(), NET::FromApplication, XCB_WINDOW_NONE, XCB_STACK_MODE_BELOW, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window4, window3, window1, window2})); +} + +void X11WindowTest::testStackBelowSibling() +{ + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + NETRootInfo root(c.get(), NET::Properties()); + + X11Window *window1 = createWindow(c.get(), Rect(0, 0, 100, 100), [&c](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "foo\0foo"); + }); + X11Window *window2 = createWindow(c.get(), Rect(0, 0, 100, 100), [&c](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "bar\0bar"); + }); + X11Window *window3 = createWindow(c.get(), Rect(0, 0, 100, 100), [&c](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "baz\0baz"); + }); + + // Restack window3 below window2. + root.restackRequest(window3->window(), NET::FromApplication, window2->window(), XCB_STACK_MODE_BELOW, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window3, window2})); + + // Repeat again. + root.restackRequest(window3->window(), NET::FromApplication, window2->window(), XCB_STACK_MODE_BELOW, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(!stackingOrderChangedSpy.wait(10)); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window3, window2})); + + // Restack window1 below window2. Note that since window1 is already window2, it's okay if the WM noops. + root.restackRequest(window1->window(), NET::FromApplication, window2->window(), XCB_STACK_MODE_BELOW, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window3, window1, window2})); +} + +void X11WindowTest::testStackTopIfFromApplication() +{ + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + NETRootInfo root(c.get(), NET::Properties()); + + X11Window *window1 = createWindow(c.get(), Rect(100, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + xcb_icccm_set_wm_class(c.get(), windowId, 7, "foo\0foo"); + Test::applyMotifHints(c.get(), windowId, Test::MotifHints{ + .flags = Test::MWM_HINTS_DECORATIONS, + .decorations = 0, + }); + }); + X11Window *window2 = createWindow(c.get(), Rect(200, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_window_t leaderId = window1->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + xcb_icccm_set_wm_class(c.get(), windowId, 7, "foo\0foo"); + Test::applyMotifHints(c.get(), windowId, Test::MotifHints{ + .flags = Test::MWM_HINTS_DECORATIONS, + .decorations = 0, + }); + }); + X11Window *window3 = createWindow(c.get(), Rect(300, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "bar\0bar"); + Test::applyMotifHints(c.get(), windowId, Test::MotifHints{ + .flags = Test::MWM_HINTS_DECORATIONS, + .decorations = 0, + }); + }); + + // Restack window1 above window2, no change will occur because there's no overlap. + root.restackRequest(window1->window(), NET::FromApplication, window2->window(), XCB_STACK_MODE_TOP_IF, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + QVERIFY(!stackingOrderChangedSpy.wait(10)); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window2, window3})); + + // Create an overlap between window1 and window2, now, TopIf should work. + root.moveResizeWindowRequest(window1->window(), (1 << 8) | (1 << 9), 150, 0, 0, 0); + root.restackRequest(window1->window(), NET::FromApplication, window2->window(), XCB_STACK_MODE_TOP_IF, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window2, window1, window3})); + + // Repeat again + root.restackRequest(window1->window(), NET::FromApplication, window2->window(), XCB_STACK_MODE_TOP_IF, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(!stackingOrderChangedSpy.wait(10)); + QCOMPARE(workspace()->stackingOrder(), (QList{window2, window1, window3})); +} + +void X11WindowTest::testStackTopIfFromTool() +{ + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + NETRootInfo root(c.get(), NET::Properties()); + + X11Window *window1 = createWindow(c.get(), Rect(100, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "foo\0foo"); + Test::applyMotifHints(c.get(), windowId, Test::MotifHints{ + .flags = Test::MWM_HINTS_DECORATIONS, + .decorations = 0, + }); + }); + X11Window *window2 = createWindow(c.get(), Rect(200, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "bar\0bar"); + Test::applyMotifHints(c.get(), windowId, Test::MotifHints{ + .flags = Test::MWM_HINTS_DECORATIONS, + .decorations = 0, + }); + }); + X11Window *window3 = createWindow(c.get(), Rect(300, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "baz\0baz"); + Test::applyMotifHints(c.get(), windowId, Test::MotifHints{ + .flags = Test::MWM_HINTS_DECORATIONS, + .decorations = 0, + }); + }); + + // Restack window1 above window2, no change will occur because there's no overlap. + root.restackRequest(window1->window(), NET::FromTool, window2->window(), XCB_STACK_MODE_TOP_IF, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + QVERIFY(!stackingOrderChangedSpy.wait(10)); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window2, window3})); + + // Create an overlap between window1 and window2, now, TopIf should work. + root.moveResizeWindowRequest(window1->window(), (1 << 8) | (1 << 9), 150, 0, 0, 0); + root.restackRequest(window1->window(), NET::FromTool, window2->window(), XCB_STACK_MODE_TOP_IF, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window2, window3, window1})); + + // Repeat again + root.restackRequest(window1->window(), NET::FromTool, window2->window(), XCB_STACK_MODE_TOP_IF, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(!stackingOrderChangedSpy.wait(10)); + QCOMPARE(workspace()->stackingOrder(), (QList{window2, window3, window1})); +} + +void X11WindowTest::testStackBottomIfFromApplication() +{ + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + NETRootInfo root(c.get(), NET::Properties()); + + X11Window *window1 = createWindow(c.get(), Rect(100, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "foo\0foo"); + Test::applyMotifHints(c.get(), windowId, Test::MotifHints{ + .flags = Test::MWM_HINTS_DECORATIONS, + .decorations = 0, + }); + }); + X11Window *window2 = createWindow(c.get(), Rect(200, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + xcb_icccm_set_wm_class(c.get(), windowId, 7, "bar\0bar"); + Test::applyMotifHints(c.get(), windowId, Test::MotifHints{ + .flags = Test::MWM_HINTS_DECORATIONS, + .decorations = 0, + }); + }); + X11Window *window3 = createWindow(c.get(), Rect(300, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_window_t leaderId = window2->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + xcb_icccm_set_wm_class(c.get(), windowId, 7, "bar\0bar"); + Test::applyMotifHints(c.get(), windowId, Test::MotifHints{ + .flags = Test::MWM_HINTS_DECORATIONS, + .decorations = 0, + }); + }); + + // Restack window3 below window2, no change will occur because there's no overlap. + root.restackRequest(window3->window(), NET::FromApplication, window2->window(), XCB_STACK_MODE_BOTTOM_IF, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + QVERIFY(!stackingOrderChangedSpy.wait(10)); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window2, window3})); + + // Create an overlap between window2 and window3, now, BottomIf should work. + root.moveResizeWindowRequest(window2->window(), (1 << 8) | (1 << 9), 250, 0, 0, 0); + root.restackRequest(window3->window(), NET::FromApplication, window2->window(), XCB_STACK_MODE_BOTTOM_IF, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window3, window1, window2})); + + // Repeat again + root.restackRequest(window3->window(), NET::FromApplication, window2->window(), XCB_STACK_MODE_BOTTOM_IF, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(!stackingOrderChangedSpy.wait(10)); + QCOMPARE(workspace()->stackingOrder(), (QList{window3, window1, window2})); +} + +void X11WindowTest::testStackBottomIfFromTool() +{ + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + NETRootInfo root(c.get(), NET::Properties()); + + X11Window *window1 = createWindow(c.get(), Rect(100, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "foo\0foo"); + Test::applyMotifHints(c.get(), windowId, Test::MotifHints{ + .flags = Test::MWM_HINTS_DECORATIONS, + .decorations = 0, + }); + }); + X11Window *window2 = createWindow(c.get(), Rect(200, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + xcb_icccm_set_wm_class(c.get(), windowId, 7, "bar\0bar"); + Test::applyMotifHints(c.get(), windowId, Test::MotifHints{ + .flags = Test::MWM_HINTS_DECORATIONS, + .decorations = 0, + }); + }); + X11Window *window3 = createWindow(c.get(), Rect(300, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_window_t leaderId = window2->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + xcb_icccm_set_wm_class(c.get(), windowId, 7, "bar\0bar"); + Test::applyMotifHints(c.get(), windowId, Test::MotifHints{ + .flags = Test::MWM_HINTS_DECORATIONS, + .decorations = 0, + }); + }); + + // Restack window3 below window2, no change will occur because there's no overlap. + root.restackRequest(window3->window(), NET::FromTool, window2->window(), XCB_STACK_MODE_BOTTOM_IF, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + QVERIFY(!stackingOrderChangedSpy.wait(10)); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window2, window3})); + + // Create an overlap between window2 and window3, now, BottomIf should work. + root.moveResizeWindowRequest(window2->window(), (1 << 8) | (1 << 9), 250, 0, 0, 0); + root.restackRequest(window3->window(), NET::FromTool, window2->window(), XCB_STACK_MODE_BOTTOM_IF, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window3, window1, window2})); + + // Repeat again + root.restackRequest(window3->window(), NET::FromTool, window2->window(), XCB_STACK_MODE_BOTTOM_IF, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(!stackingOrderChangedSpy.wait(10)); + QCOMPARE(workspace()->stackingOrder(), (QList{window3, window1, window2})); +} + +void X11WindowTest::testStackOppositeFromApplication() +{ + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + NETRootInfo root(c.get(), NET::Properties()); + + X11Window *window1 = createWindow(c.get(), Rect(100, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + xcb_icccm_set_wm_class(c.get(), windowId, 7, "foo\0foo"); + Test::applyMotifHints(c.get(), windowId, Test::MotifHints{ + .flags = Test::MWM_HINTS_DECORATIONS, + .decorations = 0, + }); + }); + X11Window *window2 = createWindow(c.get(), Rect(200, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_window_t leaderId = window1->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + xcb_icccm_set_wm_class(c.get(), windowId, 7, "foo\0foo"); + Test::applyMotifHints(c.get(), windowId, Test::MotifHints{ + .flags = Test::MWM_HINTS_DECORATIONS, + .decorations = 0, + }); + }); + X11Window *window3 = createWindow(c.get(), Rect(300, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "bar\0bar"); + Test::applyMotifHints(c.get(), windowId, Test::MotifHints{ + .flags = Test::MWM_HINTS_DECORATIONS, + .decorations = 0, + }); + }); + + // window2 is above window1, so it will be lowered + root.restackRequest(window2->window(), NET::FromApplication, window1->window(), XCB_STACK_MODE_OPPOSITE, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window2, window1, window3})); + + // Repeat again + root.restackRequest(window2->window(), NET::FromApplication, window1->window(), XCB_STACK_MODE_OPPOSITE, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window2, window3})); + + // Other window + root.restackRequest(window2->window(), NET::FromApplication, window3->window(), XCB_STACK_MODE_OPPOSITE, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window3, window2})); + + // Repeat again + root.restackRequest(window2->window(), NET::FromApplication, window3->window(), XCB_STACK_MODE_OPPOSITE, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window2, window3})); +} + +void X11WindowTest::testStackOppositeFromTool() +{ + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + NETRootInfo root(c.get(), NET::Properties()); + + X11Window *window1 = createWindow(c.get(), Rect(100, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + xcb_icccm_set_wm_class(c.get(), windowId, 7, "foo\0foo"); + }); + X11Window *window2 = createWindow(c.get(), Rect(200, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_window_t leaderId = window1->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + xcb_icccm_set_wm_class(c.get(), windowId, 7, "foo\0foo"); + }); + X11Window *window3 = createWindow(c.get(), Rect(300, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "bar\0bar"); + }); + + // window2 is above window1, so it will be lowered + root.restackRequest(window2->window(), NET::FromTool, window1->window(), XCB_STACK_MODE_OPPOSITE, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window2, window1, window3})); + + // Repeat again + root.restackRequest(window2->window(), NET::FromTool, window1->window(), XCB_STACK_MODE_OPPOSITE, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window2, window3})); + + // Other window + root.restackRequest(window2->window(), NET::FromTool, window3->window(), XCB_STACK_MODE_OPPOSITE, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window3, window2})); + + // Repeat again + root.restackRequest(window2->window(), NET::FromTool, window3->window(), XCB_STACK_MODE_OPPOSITE, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window2, window3})); +} + +void X11WindowTest::testStackOppositeNoSibling() +{ + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + NETRootInfo root(c.get(), NET::Properties()); + + X11Window *window1 = createWindow(c.get(), Rect(100, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "foo\0foo"); + }); + X11Window *window2 = createWindow(c.get(), Rect(200, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "bar\0bar"); + }); + X11Window *window3 = createWindow(c.get(), Rect(300, 0, 100, 100), [&](xcb_window_t windowId) { + xcb_icccm_set_wm_class(c.get(), windowId, 7, "baz\0baz"); + }); + + root.restackRequest(window2->window(), NET::FromTool, XCB_WINDOW_NONE, XCB_STACK_MODE_OPPOSITE, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window3, window2})); + + root.restackRequest(window2->window(), NET::FromTool, XCB_WINDOW_NONE, XCB_STACK_MODE_OPPOSITE, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window2, window1, window3})); + + root.restackRequest(window2->window(), NET::FromTool, XCB_WINDOW_NONE, XCB_STACK_MODE_OPPOSITE, XCB_CURRENT_TIME); + xcb_flush(c.get()); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(workspace()->stackingOrder(), (QList{window1, window3, window2})); +} + +void X11WindowTest::testOverrideRedirectReparent() +{ + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + + X11Window *parent = createWindow(c.get(), Rect(0, 0, 200, 200)); + X11Window *child = createWindow(c.get(), Rect(0, 0, 100, 100), [&](xcb_window_t windowId) { + quint32 value = 1; + xcb_change_window_attributes(c.get(), windowId, XCB_CW_OVERRIDE_REDIRECT, &value); + }); + + xcb_reparent_window(c.get(), child->window(), parent->window(), 0, 0); + xcb_flush(c.get()); + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(!windowAddedSpy.wait(10)); +} + +void X11WindowTest::testOverrideRedirectStackingAbove() +{ + Test::XcbConnectionPtr c = Test::createX11Connection(); + xcb_window_t windowAId = 0; + X11Window *windowA = createWindow(c.get(), Rect(0, 0, 100, 100), [&](xcb_window_t windowId) { + windowAId = windowId; + quint32 value = 1; + xcb_change_window_attributes(c.get(), windowId, XCB_CW_OVERRIDE_REDIRECT, &value); + }); + X11Window *windowB = createWindow(c.get(), Rect(0, 0, 100, 100), [&](xcb_window_t windowId) { + quint32 value = 1; + xcb_change_window_attributes(c.get(), windowId, XCB_CW_OVERRIDE_REDIRECT, &value); + + // restack before showing + uint32_t values[] = {windowAId, XCB_STACK_MODE_ABOVE}; + xcb_configure_window(c.get(), windowId, + XCB_CONFIG_WINDOW_SIBLING | XCB_CONFIG_WINDOW_STACK_MODE, + values); + }); + QVERIFY(workspace()->windows().count() == 2); + QVERIFY(workspace()->stackingOrder().indexOf(windowA) == 0); + QVERIFY(workspace()->stackingOrder().indexOf(windowB) == 1); +} + +void X11WindowTest::testOverrideRedirectStackingBelow() +{ + Test::XcbConnectionPtr c = Test::createX11Connection(); + xcb_window_t windowAId = 0; + X11Window *windowA = createWindow(c.get(), Rect(0, 0, 100, 100), [&](xcb_window_t windowId) { + windowAId = windowId; + quint32 value = 1; + xcb_change_window_attributes(c.get(), windowId, XCB_CW_OVERRIDE_REDIRECT, &value); + }); + X11Window *windowB = createWindow(c.get(), Rect(0, 0, 100, 100), [&](xcb_window_t windowId) { + quint32 value = 1; + xcb_change_window_attributes(c.get(), windowId, XCB_CW_OVERRIDE_REDIRECT, &value); + + // restack before showing + uint32_t values[] = {windowAId, XCB_STACK_MODE_BELOW}; + xcb_configure_window(c.get(), windowId, + XCB_CONFIG_WINDOW_SIBLING | XCB_CONFIG_WINDOW_STACK_MODE, + values); + }); + QVERIFY(workspace()->windows().count() == 2); + QVERIFY(workspace()->stackingOrder().indexOf(windowA) == 1); + QVERIFY(workspace()->stackingOrder().indexOf(windowB) == 0); +} + +class X11Display; + +class X11Object +{ +public: + explicit X11Object(X11Display *display); + virtual ~X11Object(); + + virtual void handle(xcb_generic_event_t *event) = 0; + + X11Display *m_display; +}; + +class X11Display : public QObject +{ + Q_OBJECT + +public: + static std::unique_ptr create() + { + auto connection = Test::createX11Connection(); + if (!connection) { + return nullptr; + } + return std::unique_ptr(new X11Display(std::move(connection))); + } + + xcb_connection_t *connection() const + { + return m_connection.get(); + } + + void addObject(X11Object *object) + { + m_objects.append(object); + } + + void removeObject(X11Object *object) + { + m_objects.removeOne(object); + } + +private: + X11Display(Test::XcbConnectionPtr connection) + : m_connection(std::move(connection)) + , m_notifier(new QSocketNotifier(xcb_get_file_descriptor(m_connection.get()), QSocketNotifier::Read)) + { + xcb_screen_t *screen = xcb_setup_roots_iterator(xcb_get_setup(m_connection.get())).data; + + connect(m_notifier.get(), &QSocketNotifier::activated, this, &X11Display::dispatchEvents); + connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::aboutToBlock, this, &X11Display::dispatchEvents); + connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::awake, this, &X11Display::dispatchEvents); + } + + void dispatchEvents() + { + xcb_generic_event_t *event; + while ((event = xcb_poll_for_event(m_connection.get()))) { + const auto objects = m_objects; // The object list can change while handling events. + for (X11Object *object : objects) { + object->handle(event); + } + std::free(event); + } + + xcb_flush(m_connection.get()); + } + + Test::XcbConnectionPtr m_connection; + std::unique_ptr m_notifier; + QList m_objects; +}; + +X11Object::X11Object(X11Display *display) + : m_display(display) +{ + m_display->addObject(this); +} + +X11Object::~X11Object() +{ + m_display->removeObject(this); +} + +class X11TestWindow : public QObject, public X11Object +{ + Q_OBJECT +public: + X11TestWindow(X11Display *display, const QSize &size) + : X11Object(display) + , m_window(createWindow(m_display->connection(), Rect(QPoint(), size))) + , m_size(size) + { + // request configure events + uint32_t value = XCB_EVENT_MASK_STRUCTURE_NOTIFY; + xcb_change_window_attributes_checked(m_display->connection(), m_window->window(), XCB_CW_EVENT_MASK, &value); + } + + void handle(xcb_generic_event_t *event) override + { + const uint8_t eventType = event->response_type & ~0x80; + if (eventType != XCB_CONFIGURE_NOTIFY) { + return; + } + auto configureEvent = reinterpret_cast(event); + present(QSize(configureEvent->width, configureEvent->height)); + Q_EMIT handledConfigure(); + } + + bool waitForAndProcessConfigure(const QSize &expectedSize) + { + QSignalSpy configure(this, &X11TestWindow::handledConfigure); + const auto t = std::chrono::steady_clock::now(); + while (std::chrono::steady_clock::now() < t + 5s) { + if (!configure.wait()) { + return false; + } + if (m_size == expectedSize) { + return true; + } + } + return false; + } + + bool present(const QSize &size) + { + m_size = size; + ShmGraphicsBufferAllocator allocator; + GraphicsBufferRef buffer = allocator.allocate(GraphicsBufferOptions{ + .size = size, + .format = DRM_FORMAT_XRGB8888, + .modifiers = QList{DRM_FORMAT_MOD_LINEAR}, + .software = true, + }); + if (!buffer) { + return false; + } + // xcb_shm_attach_fd() takes the ownership of the passed shm file descriptor. + FileDescriptor poolFileDescriptor = buffer->shmAttributes()->fd.duplicate(); + if (!poolFileDescriptor.isValid()) { + return false; + } + + xcb_shm_seg_t segment = xcb_generate_id(m_display->connection()); + xcb_shm_attach_fd(m_display->connection(), segment, poolFileDescriptor.take(), 0); + + xcb_pixmap_t pixmap = xcb_generate_id(m_display->connection()); + xcb_shm_create_pixmap(m_display->connection(), pixmap, m_window->window(), size.width(), size.height(), 24, segment, 0); + xcb_shm_detach(m_display->connection(), segment); + + xcb_xfixes_region_t valid = 0; + xcb_xfixes_region_t update = 0; + uint32_t serial = 0; + uint32_t options = 0; + uint64_t targetMsc = 0; + + xcb_present_pixmap(m_display->connection(), + m_window->window(), + pixmap, + serial, + valid, + update, + 0, + 0, + XCB_NONE, + XCB_NONE, + XCB_NONE, + options, + targetMsc, + 0, + 0, + 0, + nullptr); + return true; + } + +Q_SIGNALS: + void handledConfigure(); + +public: + X11Window *const m_window; + QSize m_size; +}; + +void X11WindowTest::testRandrEmulation() +{ + // This test verifies that randr mode emulation for fullscreen X11 clients works + const auto x11Display = X11Display::create(); + QVERIFY(x11Display); + + xcb_randr_select_input(x11Display->connection(), rootWindow(), XCB_RANDR_NOTIFY_MASK_OUTPUT_CHANGE); + + X11TestWindow window(x11Display.get(), QSize(100, 100)); + + // present something, so that Xwayland creates a surface for the window + QSignalSpy surface(window.m_window, &X11Window::surfaceChanged); + QVERIFY(window.present(QSize(100, 100))); + QVERIFY(window.m_window->surface() || surface.wait()); + QVERIFY(window.m_window->surface()); + + window.m_window->move(QPointF(0, 0)); + + // change the resolution through randr, to any non-current mode + // NOTE that randr emulation is per connection, this doesn't affect other X11 clients! + auto screenResources = xcb_randr_get_screen_resources_reply(x11Display->connection(), + xcb_randr_get_screen_resources(x11Display->connection(), kwinApp()->x11RootWindow()), + nullptr); + auto x11Outputs = xcb_randr_get_screen_resources_outputs(screenResources); + auto allModes = xcb_randr_get_screen_resources_modes(screenResources); + + // find the correct output + xcb_randr_get_output_info_reply_t *outputInfo = nullptr; + xcb_randr_get_crtc_info_reply_t *crtcInfo = nullptr; + int outputNum = 0; + for (; outputNum < screenResources->num_outputs; outputNum++) { + outputInfo = xcb_randr_get_output_info_reply(x11Display->connection(), + xcb_randr_get_output_info(x11Display->connection(), x11Outputs[outputNum], screenResources->config_timestamp), + nullptr); + QVERIFY(outputInfo); + crtcInfo = xcb_randr_get_crtc_info_reply(x11Display->connection(), + xcb_randr_get_crtc_info(x11Display->connection(), outputInfo->crtc, screenResources->config_timestamp), + nullptr); + QVERIFY(crtcInfo); + if (crtcInfo->x == 0 && crtcInfo->y == 0) { + break; + } + } + + auto outputModes = xcb_randr_get_output_info_modes(outputInfo); + const auto originalModeSize = QSize(crtcInfo->width, crtcInfo->height); + xcb_randr_mode_t emulatedMode = 0; + QSize emulatedSize; + for (int i = 0; i < screenResources->num_modes; i++) { + const bool isOutputMode = std::ranges::any_of(std::span(outputModes, outputInfo->num_modes), [id = allModes[i].id](xcb_randr_mode_t mode) { + return mode == id; + }); + if (!isOutputMode) { + continue; + } + QSize size(allModes[i].width, allModes[i].height); + if (size != originalModeSize) { + emulatedMode = allModes[i].id; + emulatedSize = size; + auto cookie = xcb_randr_set_crtc_config(x11Display->connection(), + outputInfo->crtc, + screenResources->config_timestamp, + screenResources->config_timestamp, + crtcInfo->x, + crtcInfo->y, + allModes[i].id, + XCB_RANDR_ROTATION_ROTATE_0, + 1, + &x11Outputs[outputNum]); + xcb_generic_error_t *err = nullptr; + auto reply = xcb_randr_set_crtc_config_reply(x11Display->connection(), cookie, &err); + QVERIFY(reply); + QCOMPARE(reply->status, XCB_RANDR_SET_CONFIG_SUCCESS); + break; + } + } + QVERIFY(emulatedMode != 0); + + // Now make the window fullscreen + { + NETWinInfo info(x11Display->connection(), window.m_window->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::FullScreen, NET::FullScreen); + xcb_flush(x11Display->connection()); + } + + // wait for the window to be reconfigured, and react to it + QSignalSpy commit(window.m_window->surface(), &SurfaceInterface::committed); + QVERIFY(window.waitForAndProcessConfigure(emulatedSize)); + QCOMPARE(window.m_window->isFullScreen(), true); + QCOMPARE(window.m_window->moveResizeGeometry().size(), workspace()->outputs().front()->geometryF().size()); + + // Xwayland should set the viewport to scale it up to fullscreen. + // Note that Xwayland could be doing other commits in between, + // so we can't just wait for the first commit + auto t = std::chrono::steady_clock::now(); + while (std::chrono::steady_clock::now() < t + 5s) { + if (window.m_window->surface()->bufferSourceBox().size() == emulatedSize + && window.m_window->surface()->size() == workspace()->outputs().front()->geometryF().size()) { + break; + } + QVERIFY(commit.wait()); + } + + // if the window is moved to the other screen, which does not have an emulated mode, + // the window should get resized back to the normal screen size + const auto outputs = workspace()->outputs(); + const auto it = std::ranges::find_if(outputs, [&window](LogicalOutput *output) { + return output != window.m_window->moveResizeOutput(); + }); + QCOMPARE_NE(it, outputs.end()); + LogicalOutput *otherOutput = *it; + window.m_window->sendToOutput(otherOutput); + + // wait for the window to be reconfigured, and react to it + QVERIFY(window.waitForAndProcessConfigure(originalModeSize)); + QCOMPARE(window.m_window->isFullScreen(), true); + QCOMPARE(window.m_window->moveResizeGeometry(), otherOutput->geometryF()); + + // Xwayland should reset the viewport to the new fullscreen size. + // Note that Xwayland could be doing other commits in between, + // so we can't just wait for the first commit + t = std::chrono::steady_clock::now(); + while (std::chrono::steady_clock::now() < t + 5s) { + if (window.m_window->surface()->bufferSourceBox().size() == originalModeSize + && window.m_window->surface()->size() == otherOutput->geometryF().size()) { + break; + } + QVERIFY(commit.wait()); + } +} + +/** + * A tiny helper to destroy a window when another window gets a FocusIn event. + */ +class DestroyWindowOnFocusInTask : public X11EventFilter +{ +public: + explicit DestroyWindowOnFocusInTask(xcb_window_t triggerId, xcb_window_t windowId) + : X11EventFilter(XCB_FOCUS_IN) + , m_triggerId(triggerId) + , m_windowId(windowId) + { + } + + bool event(xcb_generic_event_t *event) override + { + if (XCB_EVENT_RESPONSE_TYPE(event) == XCB_FOCUS_IN) { + const auto focusInEvent = reinterpret_cast(event); + if (focusInEvent->event == m_triggerId) { + xcb_destroy_window(kwinApp()->x11Connection(), m_windowId); + xcb_flush(kwinApp()->x11Connection()); + } + } + return false; + } +private: + xcb_window_t m_triggerId; + xcb_window_t m_windowId; +}; + +void X11WindowTest::testRestoreFocusToDestroyedWindow() +{ + // This test verifies that the Workspace::activeWindow() won't get stuck with the wrong value after + // attempting to restore the focus to a destroyed window. This case is interesting because of the + // following event sequence: a client focuses itself, the wm rejects that and attempts to restore + // focus to a destroyed window (this will fail, X server focus will not change), the wm finally + // receives the corresponding DestroyNotify event, and activates the client that attempted to focus + // itself, this will not generate a FocusIn event. If the wm waits for the FocusIn event to update + // its internal state, e.g. currently active window, it may end up with the wrong state in case it + // doesn't consider that extreme corner case. + + // Note that the window class is set so the focus stealing prevention policies consider the + // two windows belonging to different applications. + Test::XcbConnectionPtr firstConnection = Test::createX11Connection(); + X11Window *firstWindow = createWindow(firstConnection.get(), Rect(0, 0, 100, 100), [&firstConnection](xcb_window_t windowId) { + xcb_icccm_set_wm_class(firstConnection.get(), windowId, 7, "foo\0foo"); + }); + QCOMPARE(workspace()->activeWindow(), firstWindow); + + Test::XcbConnectionPtr secondConnection = Test::createX11Connection(); + X11Window *secondWindow = createWindow(secondConnection.get(), Rect(100, 0, 100, 100), [&secondConnection](xcb_window_t windowId) { + xcb_icccm_set_wm_class(secondConnection.get(), windowId, 7, "bar\0bar"); + }); + QCOMPARE(workspace()->activeWindow(), secondWindow); + + // Try to "steal" input focus. + xcb_set_input_focus(firstConnection.get(), XCB_INPUT_FOCUS_POINTER_ROOT, firstWindow->window(), XCB_TIME_CURRENT_TIME); + xcb_flush(firstConnection.get()); + + // The main purpose of this event filter is to destroy the active window to create a situation + // where the active window is already destroyed when the window manager processes the FocusIn event + // from the first window. + // + // The second window cannot be destroyed using the secondConnection because it looks like the window + // manager side will receive the DestroyNotify event first no matter which order the XSetInputFocus() + // and XDestroyWindow() requests are called. Performing roundtrips to the X server doesn't help either. + // So we install an event filter to destroy the active window right before the wm processes the + // FocusIn event. + auto activeWindowDestroyer = std::make_unique(firstWindow->window(), secondWindow->window()); + + // Let the window manager side catch up with the X11 events. + QSignalSpy windowActivatedSpy(workspace(), &Workspace::windowActivated); + QVERIFY(windowActivatedSpy.wait()); + QCOMPARE(workspace()->activeWindow(), firstWindow); +} + +WAYLANDTEST_MAIN(X11WindowTest) +#include "x11_window_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/x11keyread.cpp b/local/recipes/kde/kwin/source/autotests/integration/x11keyread.cpp new file mode 100644 index 0000000000..5148487932 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/x11keyread.cpp @@ -0,0 +1,377 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2024 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "input.h" +#include "options.h" +#include "pointer_input.h" +#include "utils/xcbutils.h" +#include "wayland/display.h" +#include "wayland/keyboard.h" +#include "wayland/seat.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include + +#include +#include + +#include +#define explicit xcb_explicit +#include +#include +#include +#include +#undef explicit + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_x11-key-read-0"); + +enum class State { + Press, + Release +} state; +typedef QPair KeyAction; +Q_DECLARE_METATYPE(KeyAction); + +/* + * This tests the "Legacy App Support" feature of allowing X11 apps to get notified of some key press events + */ +class X11KeyReadTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase_data(); + void initTestCase(); + void init(); + void cleanup(); + + void testSimpleLetter(); + void onlyModifier(); + void letterWithModifier(); + void testWaylandWindowHasFocus(); + void tabBox(); + +private: + QList recievedX11EventsForInput(const QList &keyEventsIn); +}; + +void X11KeyReadTest::initTestCase_data() +{ + QTest::addColumn("operatingMode"); + QTest::newRow("all") << XwaylandEavesdropsMode::All; + QTest::newRow("allWithModifier") << XwaylandEavesdropsMode::AllKeysWithModifier; + QTest::newRow("nonCharacter") << XwaylandEavesdropsMode::NonCharacterKeys; + QTest::newRow("none") << XwaylandEavesdropsMode::None; +} + +void X11KeyReadTest::initTestCase() +{ + QVERIFY(waylandServer()->init(s_socketName)); + + qputenv("KWIN_XKB_DEFAULT_KEYMAP", "1"); + qputenv("XKB_DEFAULT_RULES", "evdev"); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + }); + + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); +} + +void X11KeyReadTest::init() +{ + QFETCH_GLOBAL(XwaylandEavesdropsMode, operatingMode); + options->setXwaylandEavesdrops(operatingMode); + workspace()->setActiveOutput(QPoint(640, 512)); + KWin::input()->pointer()->warp(QPoint(640, 512)); + + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); + QVERIFY(Test::waitForWaylandKeyboard()); +} + +void X11KeyReadTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void X11KeyReadTest::testSimpleLetter() +{ + // press a + QList keyEvents = { + {State::Press, KEY_A}, + {State::Release, KEY_A}, + }; + auto received = recievedX11EventsForInput(keyEvents); + + QList expected; + QFETCH_GLOBAL(XwaylandEavesdropsMode, operatingMode); + switch (operatingMode) { + case XwaylandEavesdropsMode::None: + case XwaylandEavesdropsMode::NonCharacterKeys: + case XwaylandEavesdropsMode::AllKeysWithModifier: + expected = {}; + break; + case XwaylandEavesdropsMode::All: + expected = keyEvents; + break; + } + + QCOMPARE(received, expected); +} + +void X11KeyReadTest::onlyModifier() +{ + QList keyEvents = { + {State::Press, KEY_LEFTALT}, + {State::Release, KEY_LEFTALT}, + }; + auto received = recievedX11EventsForInput(keyEvents); + + QList expected; + QFETCH_GLOBAL(XwaylandEavesdropsMode, operatingMode); + switch (operatingMode) { + case XwaylandEavesdropsMode::None: + expected = {}; + break; + case XwaylandEavesdropsMode::NonCharacterKeys: + case XwaylandEavesdropsMode::AllKeysWithModifier: + case XwaylandEavesdropsMode::All: + expected = keyEvents; + break; + } + + QCOMPARE(received, expected); +} + +void X11KeyReadTest::letterWithModifier() +{ + QList keyEvents = { + {State::Press, KEY_LEFTALT}, + {State::Press, KEY_F}, + {State::Release, KEY_F}, + {State::Release, KEY_LEFTALT}, + }; + auto received = recievedX11EventsForInput(keyEvents); + + QList expected; + QFETCH_GLOBAL(XwaylandEavesdropsMode, operatingMode); + switch (operatingMode) { + case XwaylandEavesdropsMode::None: + expected = {}; + break; + case XwaylandEavesdropsMode::NonCharacterKeys: + expected = { + {State::Press, KEY_LEFTALT}, + {State::Release, KEY_LEFTALT}, + }; + break; + case XwaylandEavesdropsMode::AllKeysWithModifier: + case XwaylandEavesdropsMode::All: + expected = keyEvents; + break; + } + QCOMPARE(received, expected); +} + +void X11KeyReadTest::testWaylandWindowHasFocus() +{ + // A wayland window should be unaffected + int timestamp = 0; + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy modifierSpy(keyboard.get(), &KWayland::Client::Keyboard::modifiersChanged); + QSignalSpy keyChangedSpy(keyboard.get(), &KWayland::Client::Keyboard::keyChanged); + + Window *waylandWindow = Test::renderAndWaitForShown(surface.get(), QSize(10, 10), Qt::blue); + QVERIFY(waylandWindow->isActive()); + modifierSpy.wait(); // initial modifiers + modifierSpy.clear(); + + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + QVERIFY(keyChangedSpy.wait()); + QCOMPARE(keyChangedSpy.last()[0], KEY_LEFTALT); + QCOMPARE(keyChangedSpy.last()[1].value(), KWayland::Client::Keyboard::KeyState::Pressed); + + QCOMPARE(modifierSpy.count(), 1); + QCOMPARE(modifierSpy.last()[0], 8); + QCOMPARE(modifierSpy.last()[1], 0); + QCOMPARE(modifierSpy.last()[2], 0); + QCOMPARE(modifierSpy.last()[3], 0); + + Test::keyboardKeyPressed(KEY_G, timestamp++); + QVERIFY(keyChangedSpy.wait()); + QCOMPARE(keyChangedSpy.last()[0], KEY_G); + QCOMPARE(keyChangedSpy.last()[1].value(), KWayland::Client::Keyboard::KeyState::Pressed); + + Test::keyboardKeyReleased(KEY_G, timestamp++); + QVERIFY(keyChangedSpy.wait()); + QCOMPARE(keyChangedSpy.last()[0], KEY_G); + QCOMPARE(keyChangedSpy.last()[1].value(), KWayland::Client::Keyboard::KeyState::Released); + + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + QVERIFY(keyChangedSpy.wait()); + QCOMPARE(keyChangedSpy.last()[0], KEY_LEFTALT); + QCOMPARE(keyChangedSpy.last()[1].value(), KWayland::Client::Keyboard::KeyState::Released); + + QCOMPARE(modifierSpy.count(), 2); + QCOMPARE(modifierSpy.last()[0], 0); + QCOMPARE(modifierSpy.last()[1], 0); + QCOMPARE(modifierSpy.last()[2], 0); + QCOMPARE(modifierSpy.last()[3], 0); +} + +void X11KeyReadTest::tabBox() +{ + // Note this test will fail if you forget to use dbus-run-session! +#if KWIN_BUILD_TABBOX + QList keyEvents = { + {State::Press, KEY_LEFTALT}, + {State::Press, KEY_TAB}, + {State::Release, KEY_TAB}, + {State::Press, KEY_TAB}, + {State::Release, KEY_TAB}, + {State::Release, KEY_LEFTALT}, + }; + auto received = recievedX11EventsForInput(keyEvents); + + QList expected; + QFETCH_GLOBAL(XwaylandEavesdropsMode, operatingMode); + switch (operatingMode) { + case XwaylandEavesdropsMode::None: + expected = {}; + break; + // even though tab is a regular key whilst holding alt, the tab switcher should be grabbing + case XwaylandEavesdropsMode::AllKeysWithModifier: + case XwaylandEavesdropsMode::NonCharacterKeys: + case XwaylandEavesdropsMode::All: + expected = { + {State::Press, KEY_LEFTALT}, + {State::Release, KEY_LEFTALT}, + }; + break; + } + + QCOMPARE(received, expected); +#endif +} + +class X11EventRecorder : public QObject +{ + Q_OBJECT +public: + X11EventRecorder(xcb_connection_t *c); + QList keyEvents() const + { + return m_keyEvents; + } +Q_SIGNALS: + void fenceReceived(); + +private: + void processXcbEvents(); + QList m_keyEvents; + xcb_connection_t *m_connection; + QSocketNotifier *m_notifier; +}; + +X11EventRecorder::X11EventRecorder(xcb_connection_t *c) + : QObject() + , m_connection(c) + , m_notifier(new QSocketNotifier(xcb_get_file_descriptor(m_connection), QSocketNotifier::Read, this)) +{ + struct + { + xcb_input_event_mask_t head; + xcb_input_xi_event_mask_t mask; + } mask; + mask.head.deviceid = XCB_INPUT_DEVICE_ALL; + mask.head.mask_len = sizeof(mask.mask) / sizeof(uint32_t); + mask.mask = static_cast( + XCB_INPUT_XI_EVENT_MASK_KEY_PRESS + | XCB_INPUT_XI_EVENT_MASK_KEY_RELEASE + | XCB_INPUT_RAW_KEY_PRESS + | XCB_INPUT_RAW_KEY_RELEASE); + + xcb_input_xi_select_events(c, kwinApp()->x11RootWindow(), 1, &mask.head); + // Block until the X server has processed this event, not + // just until it received it (which xcb_flush would do). + // Otherwise we might miss key events + Xcb::sync(c); + + connect(m_notifier, &QSocketNotifier::activated, this, &X11EventRecorder::processXcbEvents); + connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::aboutToBlock, this, &X11EventRecorder::processXcbEvents); + connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::awake, this, &X11EventRecorder::processXcbEvents); +} + +void X11EventRecorder::processXcbEvents() +{ + xcb_generic_event_t *event; + while ((event = xcb_poll_for_event(m_connection))) { + u_int8_t responseType = event->response_type & ~0x80; + if (responseType == XCB_GE_GENERIC) { + auto *geEvent = reinterpret_cast(event); + if (geEvent->event_type == XCB_INPUT_KEY_PRESS || geEvent->event_type == XCB_INPUT_KEY_RELEASE) { + + auto keyEvent = reinterpret_cast(geEvent); + int nativeKeyCode = keyEvent->detail - 0x08; + + if (nativeKeyCode == 0) { + if (geEvent->event_type == XCB_INPUT_KEY_RELEASE) { + Q_EMIT fenceReceived(); + } + } else { + KeyAction action({geEvent->event_type == XCB_INPUT_KEY_PRESS ? State::Press : State::Release, nativeKeyCode}); + m_keyEvents << action; + } + } + } + std::free(event); + } + + xcb_flush(m_connection); +} + +QList X11KeyReadTest::recievedX11EventsForInput(const QList &keysIn) +{ + quint32 timestamp = 1; + Test::XcbConnectionPtr c = Test::createX11Connection(); + + X11EventRecorder eventReader(c.get()); + + QSignalSpy fenceEventSpy(&eventReader, &X11EventRecorder::fenceReceived); + + for (const KeyAction &action : keysIn) { + if (action.first == State::Press) { + Test::keyboardKeyPressed(action.second, timestamp++); + } else { + Test::keyboardKeyReleased(action.second, timestamp++); + } + } + // special case, explicitly send key 0, to use as a fence + ClientConnection *xwaylandClient = waylandServer()->xWaylandConnection(); + waylandServer()->seat()->keyboard()->sendKey(0, KeyboardKeyState::Pressed, xwaylandClient, waylandServer()->display()->nextSerial()); + waylandServer()->seat()->keyboard()->sendKey(0, KeyboardKeyState::Released, xwaylandClient, waylandServer()->display()->nextSerial()); + + bool fenceComplete = fenceEventSpy.wait(); + Q_ASSERT(fenceComplete); + + return eventReader.keyEvents(); +} + +WAYLANDTEST_MAIN(X11KeyReadTest) +#include "x11keyread.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/xdgshellwindow_rules_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/xdgshellwindow_rules_test.cpp new file mode 100644 index 0000000000..28f827e119 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/xdgshellwindow_rules_test.cpp @@ -0,0 +1,3085 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + SPDX-FileCopyrightText: 2022 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "core/outputbackend.h" +#include "core/outputconfiguration.h" +#include "cursor.h" +#include "rules.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_xdgshellwindow_rules-0"); + +class TestXdgShellWindowRules : public QObject +{ + Q_OBJECT + + enum ClientFlag { + None = 0, + ClientShouldBeInactive = 1 << 0, // Window should be inactive. Used on Minimize tests + ServerSideDecoration = 1 << 1, // Create window with server side decoration. Used on noBorder tests + ReturnAfterSurfaceConfiguration = 1 << 2, // Do not create the window now, but return after surface configuration. + }; + Q_DECLARE_FLAGS(ClientFlags, ClientFlag) + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testPositionDontAffect(); + void testPositionApply(); + void testPositionRemember(); + void testPositionForce(); + void testPositionApplyNow(); + void testPositionForceTemporarily(); + + void testSizeDontAffect(); + void testSizeApply(); + void testSizeRemember(); + void testSizeForce(); + void testSizeApplyNow(); + void testSizeForceTemporarily(); + + void testMaximizeDontAffect(); + void testMaximizeApply(); + void testMaximizeRemember(); + void testMaximizeForce(); + void testMaximizeApplyNow(); + void testMaximizeForceTemporarily(); + + void testDesktopsDontAffect(); + void testDesktopsApply(); + void testDesktopsRemember(); + void testDesktopsForce(); + void testDesktopsApplyNow(); + void testDesktopsForceTemporarily(); + + void testMinimizeDontAffect(); + void testMinimizeApply(); + void testMinimizeRemember(); + void testMinimizeForce(); + void testMinimizeApplyNow(); + void testMinimizeForceTemporarily(); + + void testSkipTaskbarDontAffect(); + void testSkipTaskbarApply(); + void testSkipTaskbarRemember(); + void testSkipTaskbarForce(); + void testSkipTaskbarApplyNow(); + void testSkipTaskbarForceTemporarily(); + + void testSkipPagerDontAffect(); + void testSkipPagerApply(); + void testSkipPagerRemember(); + void testSkipPagerForce(); + void testSkipPagerApplyNow(); + void testSkipPagerForceTemporarily(); + + void testSkipSwitcherDontAffect(); + void testSkipSwitcherApply(); + void testSkipSwitcherRemember(); + void testSkipSwitcherForce(); + void testSkipSwitcherApplyNow(); + void testSkipSwitcherForceTemporarily(); + + void testKeepAboveDontAffect(); + void testKeepAboveApply(); + void testKeepAboveRemember(); + void testKeepAboveForce(); + void testKeepAboveApplyNow(); + void testKeepAboveForceTemporarily(); + + void testKeepBelowDontAffect(); + void testKeepBelowApply(); + void testKeepBelowRemember(); + void testKeepBelowForce(); + void testKeepBelowApplyNow(); + void testKeepBelowForceTemporarily(); + + void testShortcutDontAffect(); + void testShortcutApply(); + void testShortcutRemember(); + void testShortcutForce(); + void testShortcutApplyNow(); + void testShortcutForceTemporarily(); + + void testDesktopFileDontAffect(); + void testDesktopFileApply(); + void testDesktopFileRemember(); + void testDesktopFileForce(); + void testDesktopFileApplyNow(); + void testDesktopFileForceTemporarily(); + + void testActiveOpacityDontAffect(); + void testActiveOpacityForce(); + void testActiveOpacityForceTemporarily(); + + void testInactiveOpacityDontAffect(); + void testInactiveOpacityForce(); + void testInactiveOpacityForceTemporarily(); + + void testNoBorderDontAffect(); + void testNoBorderApply(); + void testNoBorderRemember(); + void testNoBorderForce(); + void testNoBorderApplyNow(); + void testNoBorderForceTemporarily(); + + void testScreenDontAffect(); + void testScreenApply(); + void testScreenRemember(); + void testScreenForce(); + void testScreenApplyNow(); + void testScreenForceTemporarily(); + + void testLayerDontAffect(); + void testLayerForce(); + void testLayerForceTemporarily(); + + void testCloseableDontAffect(); + void testCloseableForce(); + void testCloseableForceTemporarily(); + + void testMatchAfterNameChange(); + void testNotEnabled(); + +private: + void createTestWindow(ClientFlags flags = None); + void mapClientToSurface(QSize clientSize, ClientFlags flags = None); + void destroyTestWindow(); + + template + void setWindowRule(const QString &property, const T &value, int policy); + +private: + KSharedConfig::Ptr m_config; + + Window *m_window; + std::unique_ptr m_surface; + std::unique_ptr m_shellSurface; + std::unique_ptr m_decoration; + + std::unique_ptr m_toplevelConfigureRequestedSpy; + std::unique_ptr m_surfaceConfigureRequestedSpy; +}; + +void TestXdgShellWindowRules::initTestCase() +{ + qRegisterMetaType(); + + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); + + m_config = KSharedConfig::openConfig(QStringLiteral("kwinrulesrc"), KConfig::SimpleConfig); + workspace()->rulebook()->setConfig(m_config); +} + +void TestXdgShellWindowRules::init() +{ + VirtualDesktopManager::self()->setCurrent(VirtualDesktopManager::self()->desktops().first()); + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::XdgDecorationV1)); + + workspace()->setActiveOutput(QPoint(640, 512)); +} + +void TestXdgShellWindowRules::cleanup() +{ + if (m_shellSurface) { + destroyTestWindow(); + } + + Test::destroyWaylandConnection(); + + // Wipe the window rule config clean. + for (const QString &group : m_config->groupList()) { + m_config->deleteGroup(group); + } + workspace()->slotReconfigure(); + + // Restore virtual desktops to the initial state. + VirtualDesktopManager::self()->setCount(1); + QCOMPARE(VirtualDesktopManager::self()->count(), 1u); +} + +void TestXdgShellWindowRules::createTestWindow(ClientFlags flags) +{ + // Apply flags for special windows and rules + const bool createClient = !(flags & ReturnAfterSurfaceConfiguration); + const auto decorationMode = (flags & ServerSideDecoration) ? Test::XdgToplevelDecorationV1::mode_server_side + : Test::XdgToplevelDecorationV1::mode_client_side; + // Create an xdg surface. + m_surface = Test::createSurface(); + m_shellSurface = Test::createXdgToplevelSurface(m_surface.get(), Test::CreationSetup::CreateOnly); + m_decoration = Test::createXdgToplevelDecorationV1(m_shellSurface.get()); + + // Add signal watchers + m_toplevelConfigureRequestedSpy = std::make_unique(m_shellSurface.get(), &Test::XdgToplevel::configureRequested); + m_surfaceConfigureRequestedSpy = std::make_unique(m_shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + m_shellSurface->set_app_id(QStringLiteral("org.kde.foo")); + m_decoration->set_mode(decorationMode); + + // Wait for the initial configure event + m_surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + + if (createClient) { + mapClientToSurface(QSize(100, 50), flags); + } +} + +void TestXdgShellWindowRules::mapClientToSurface(QSize clientSize, ClientFlags flags) +{ + const bool clientShouldBeActive = !(flags & ClientShouldBeInactive); + + QVERIFY(m_surface != nullptr); + QVERIFY(m_shellSurface != nullptr); + QVERIFY(m_surfaceConfigureRequestedSpy != nullptr); + + // Draw content of the surface. + m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value()); + + // Create the window + m_window = Test::renderAndWaitForShown(m_surface.get(), clientSize, Qt::blue); + QVERIFY(m_window); + QCOMPARE(m_window->isActive(), clientShouldBeActive); +} + +void TestXdgShellWindowRules::destroyTestWindow() +{ + m_surfaceConfigureRequestedSpy.reset(); + m_toplevelConfigureRequestedSpy.reset(); + m_decoration.reset(); + m_shellSurface.reset(); + m_surface.reset(); + QVERIFY(Test::waitForWindowClosed(m_window)); +} + +template +void TestXdgShellWindowRules::setWindowRule(const QString &property, const T &value, int policy) +{ + // Initialize RuleBook with the test rule. + const QString ruleGroupName = QStringLiteral("test-rule"); + m_config->group(QStringLiteral("General")).writeEntry("rules", QStringList{ruleGroupName}); + KConfigGroup group = m_config->group(ruleGroupName); + + group.writeEntry(property, value); + group.writeEntry(QStringLiteral("%1rule").arg(property), policy); + + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + + workspace()->slotReconfigure(); +} + +void TestXdgShellWindowRules::testPositionDontAffect() +{ + setWindowRule("position", QPoint(42, 42), int(Rules::DontAffect)); + + createTestWindow(); + + // The position of the window should not be affected by the rule. The default + // placement policy will put the window in the top-left corner of the screen. + QVERIFY(m_window->isMovable()); + QVERIFY(m_window->isMovableAcrossScreens()); + QCOMPARE(m_window->pos(), QPoint(0, 0)); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testPositionApply() +{ + setWindowRule("position", QPoint(42, 42), int(Rules::Apply)); + + createTestWindow(); + + // The window should be moved to the position specified by the rule. + QVERIFY(m_window->isMovable()); + QVERIFY(m_window->isMovableAcrossScreens()); + QCOMPARE(m_window->pos(), QPoint(42, 42)); + + // One should still be able to move the window around. + QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeSteppedSpy(m_window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(m_window, &Window::interactiveMoveResizeFinished); + + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + workspace()->slotWindowMove(); + QCOMPARE(workspace()->moveResizeWindow(), m_window); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QVERIFY(m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + + const QPointF cursorPos = KWin::Cursors::self()->mouse()->pos(); + m_window->keyPressEvent(Qt::Key_Right); + m_window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(m_window->pos(), QPoint(50, 42)); + + m_window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + QCOMPARE(m_window->pos(), QPoint(50, 42)); + + // The rule should be applied again if the window appears after it's been closed. + destroyTestWindow(); + createTestWindow(); + + QVERIFY(m_window->isMovable()); + QVERIFY(m_window->isMovableAcrossScreens()); + QCOMPARE(m_window->pos(), QPoint(42, 42)); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testPositionRemember() +{ + setWindowRule("position", QPoint(42, 42), int(Rules::Remember)); + createTestWindow(); + + // The window should be moved to the position specified by the rule. + QVERIFY(m_window->isMovable()); + QVERIFY(m_window->isMovableAcrossScreens()); + QCOMPARE(m_window->pos(), QPoint(42, 42)); + + // One should still be able to move the window around. + QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeSteppedSpy(m_window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(m_window, &Window::interactiveMoveResizeFinished); + + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + workspace()->slotWindowMove(); + QCOMPARE(workspace()->moveResizeWindow(), m_window); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QVERIFY(m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + + const QPointF cursorPos = KWin::Cursors::self()->mouse()->pos(); + m_window->keyPressEvent(Qt::Key_Right); + m_window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(m_window->pos(), QPoint(50, 42)); + + m_window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + QCOMPARE(m_window->pos(), QPoint(50, 42)); + + // The window should be placed at the last know position if we reopen it. + destroyTestWindow(); + createTestWindow(); + + QVERIFY(m_window->isMovable()); + QVERIFY(m_window->isMovableAcrossScreens()); + QCOMPARE(m_window->pos(), QPoint(50, 42)); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testPositionForce() +{ + setWindowRule("position", QPoint(42, 42), int(Rules::Force)); + + createTestWindow(); + + // The window should be moved to the position specified by the rule. + QVERIFY(!m_window->isMovable()); + QVERIFY(!m_window->isMovableAcrossScreens()); + QCOMPARE(m_window->pos(), QPoint(42, 42)); + + // User should not be able to move the window. + QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + workspace()->slotWindowMove(); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 0); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + + // The position should still be forced if we reopen the window. + destroyTestWindow(); + createTestWindow(); + + QVERIFY(!m_window->isMovable()); + QVERIFY(!m_window->isMovableAcrossScreens()); + QCOMPARE(m_window->pos(), QPoint(42, 42)); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testPositionApplyNow() +{ + createTestWindow(); + + // The position of the window isn't set by any rule, thus the default placement + // policy will try to put the window in the top-left corner of the screen. + QVERIFY(m_window->isMovable()); + QVERIFY(m_window->isMovableAcrossScreens()); + QCOMPARE(m_window->pos(), QPoint(0, 0)); + + QSignalSpy frameGeometryChangedSpy(m_window, &Window::frameGeometryChanged); + + setWindowRule("position", QPoint(42, 42), int(Rules::ApplyNow)); + + // The window should be moved to the position specified by the rule. + QCOMPARE(frameGeometryChangedSpy.count(), 1); + QCOMPARE(m_window->pos(), QPoint(42, 42)); + + // We still have to be able to move the window around. + QVERIFY(m_window->isMovable()); + QVERIFY(m_window->isMovableAcrossScreens()); + QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeSteppedSpy(m_window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(m_window, &Window::interactiveMoveResizeFinished); + + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + workspace()->slotWindowMove(); + QCOMPARE(workspace()->moveResizeWindow(), m_window); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QVERIFY(m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + + const QPointF cursorPos = KWin::Cursors::self()->mouse()->pos(); + m_window->keyPressEvent(Qt::Key_Right); + m_window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(m_window->pos(), QPoint(50, 42)); + + m_window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + QCOMPARE(m_window->pos(), QPoint(50, 42)); + + // The rule should not be applied again. + m_window->evaluateWindowRules(); + QCOMPARE(m_window->pos(), QPoint(50, 42)); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testPositionForceTemporarily() +{ + setWindowRule("position", QPoint(42, 42), int(Rules::ForceTemporarily)); + + createTestWindow(); + + // The window should be moved to the position specified by the rule. + QVERIFY(!m_window->isMovable()); + QVERIFY(!m_window->isMovableAcrossScreens()); + QCOMPARE(m_window->pos(), QPoint(42, 42)); + + // User should not be able to move the window. + QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + workspace()->slotWindowMove(); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 0); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + + // The rule should be discarded if we close the window. + destroyTestWindow(); + createTestWindow(); + + QVERIFY(m_window->isMovable()); + QVERIFY(m_window->isMovableAcrossScreens()); + QCOMPARE(m_window->pos(), QPoint(0, 0)); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSizeDontAffect() +{ + setWindowRule("size", QSize(480, 640), int(Rules::DontAffect)); + + createTestWindow(ReturnAfterSurfaceConfiguration); + + // The window size shouldn't be enforced by the rule. + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(0, 0)); + + // Map the window. + mapClientToSurface(QSize(100, 50)); + QVERIFY(m_window->isResizable()); + QCOMPARE(m_window->size(), QSize(100, 50)); + + // We should receive a configure event when the window becomes active. + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSizeApply() +{ + setWindowRule("size", QSize(480, 640), int(Rules::Apply)); + + createTestWindow(ReturnAfterSurfaceConfiguration); + + // The initial configure event should contain size hint set by the rule. + Test::XdgToplevel::States states; + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(480, 640)); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Resizing)); + + // Map the window. + mapClientToSurface(QSize(480, 640)); + QVERIFY(m_window->isResizable()); + QCOMPARE(m_window->size(), QSize(480, 640)); + + // We should receive a configure event when the window becomes active. + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Resizing)); + + // One still should be able to resize the window. + QSignalSpy frameGeometryChangedSpy(m_window, &Window::frameGeometryChanged); + QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeSteppedSpy(m_window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(m_window, &Window::interactiveMoveResizeFinished); + + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeWindow(), m_window); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(m_window->isInteractiveResize()); + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 3); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 3); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing)); + m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value()); + + const QPointF cursorPos = KWin::Cursors::self()->mouse()->pos(); + m_window->keyPressEvent(Qt::Key_Right); + m_window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 4); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 4); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing)); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(488, 640)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value()); + Test::render(m_surface.get(), QSize(488, 640), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(m_window->size(), QSize(488, 640)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + + m_window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 5); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 5); + + // The rule should be applied again if the window appears after it's been closed. + destroyTestWindow(); + createTestWindow(ReturnAfterSurfaceConfiguration); + + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(480, 640)); + + mapClientToSurface(QSize(480, 640)); + QVERIFY(m_window->isResizable()); + QCOMPARE(m_window->size(), QSize(480, 640)); + + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSizeRemember() +{ + setWindowRule("size", QSize(480, 640), int(Rules::Remember)); + + createTestWindow(ReturnAfterSurfaceConfiguration); + + // The initial configure event should contain size hint set by the rule. + Test::XdgToplevel::States states; + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(480, 640)); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Resizing)); + + // Map the window. + mapClientToSurface(QSize(480, 640)); + QVERIFY(m_window->isResizable()); + QCOMPARE(m_window->size(), QSize(480, 640)); + + // We should receive a configure event when the window becomes active. + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Resizing)); + + // One should still be able to resize the window. + QSignalSpy frameGeometryChangedSpy(m_window, &Window::frameGeometryChanged); + QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeSteppedSpy(m_window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(m_window, &Window::interactiveMoveResizeFinished); + + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeWindow(), m_window); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(m_window->isInteractiveResize()); + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 3); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 3); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing)); + m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value()); + + const QPointF cursorPos = KWin::Cursors::self()->mouse()->pos(); + m_window->keyPressEvent(Qt::Key_Right); + m_window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 4); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 4); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing)); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(488, 640)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value()); + Test::render(m_surface.get(), QSize(488, 640), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(m_window->size(), QSize(488, 640)); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + + m_window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 5); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 5); + + // If the window appears again, it should have the last known size. + destroyTestWindow(); + createTestWindow(ReturnAfterSurfaceConfiguration); + + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(488, 640)); + + mapClientToSurface(QSize(488, 640)); + QVERIFY(m_window->isResizable()); + QCOMPARE(m_window->size(), QSize(488, 640)); + + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSizeForce() +{ + setWindowRule("size", QSize(480, 640), int(Rules::Force)); + + createTestWindow(ReturnAfterSurfaceConfiguration); + + // The initial configure event should contain size hint set by the rule. + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(480, 640)); + + // Map the window. + mapClientToSurface(QSize(480, 640)); + QVERIFY(!m_window->isResizable()); + QCOMPARE(m_window->size(), QSize(480, 640)); + + // We should receive a configure event when the window becomes active. + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + + // Any attempt to resize the window should not succeed. + QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 0); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + QVERIFY(!m_surfaceConfigureRequestedSpy->wait(100)); + + // If the window appears again, the size should still be forced. + destroyTestWindow(); + createTestWindow(ReturnAfterSurfaceConfiguration); + + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(480, 640)); + + mapClientToSurface(QSize(480, 640)); + QVERIFY(!m_window->isResizable()); + QCOMPARE(m_window->size(), QSize(480, 640)); + + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSizeApplyNow() +{ + createTestWindow(ReturnAfterSurfaceConfiguration); + + // The expected surface dimensions should be set by the rule. + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(0, 0)); + + // Map the window. + mapClientToSurface(QSize(100, 50)); + QVERIFY(m_window->isResizable()); + QCOMPARE(m_window->size(), QSize(100, 50)); + + // We should receive a configure event when the window becomes active. + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + + setWindowRule("size", QSize(480, 640), int(Rules::ApplyNow)); + + // The compositor should send a configure event with a new size. + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 3); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 3); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(480, 640)); + + // Draw the surface with the new size. + QSignalSpy frameGeometryChangedSpy(m_window, &Window::frameGeometryChanged); + m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value()); + Test::render(m_surface.get(), QSize(480, 640), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(m_window->size(), QSize(480, 640)); + QVERIFY(!m_surfaceConfigureRequestedSpy->wait(100)); + + // The rule should not be applied again. + m_window->evaluateWindowRules(); + QVERIFY(!m_surfaceConfigureRequestedSpy->wait(100)); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSizeForceTemporarily() +{ + setWindowRule("size", QSize(480, 640), int(Rules::ForceTemporarily)); + + createTestWindow(ReturnAfterSurfaceConfiguration); + + // The initial configure event should contain size hint set by the rule. + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(480, 640)); + + // Map the window. + mapClientToSurface(QSize(480, 640)); + QVERIFY(!m_window->isResizable()); + QCOMPARE(m_window->size(), QSize(480, 640)); + + // We should receive a configure event when the window becomes active. + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + + // Any attempt to resize the window should not succeed. + QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 0); + QVERIFY(!m_window->isInteractiveMove()); + QVERIFY(!m_window->isInteractiveResize()); + QVERIFY(!m_surfaceConfigureRequestedSpy->wait(100)); + + // The rule should be discarded when the window is closed. + destroyTestWindow(); + createTestWindow(ReturnAfterSurfaceConfiguration); + + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(0, 0)); + + mapClientToSurface(QSize(100, 50)); + QVERIFY(m_window->isResizable()); + QCOMPARE(m_window->size(), QSize(100, 50)); + + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testMaximizeDontAffect() +{ + setWindowRule("maximizehoriz", true, int(Rules::DontAffect)); + setWindowRule("maximizevert", true, int(Rules::DontAffect)); + + createTestWindow(ReturnAfterSurfaceConfiguration); + + // Wait for the initial configure event. + Test::XdgToplevel::States states; + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Map the window. + mapClientToSurface(QSize(100, 50)); + + QVERIFY(m_window->isMaximizable()); + QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(m_window->size(), QSize(100, 50)); + + // We should receive a configure event when the window becomes active. + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testMaximizeApply() +{ + setWindowRule("maximizehoriz", true, int(Rules::Apply)); + setWindowRule("maximizevert", true, int(Rules::Apply)); + + createTestWindow(ReturnAfterSurfaceConfiguration); + + // Wait for the initial configure event. + Test::XdgToplevel::States states; + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Map the window. + mapClientToSurface(QSize(1280, 1024)); + + QVERIFY(m_window->isMaximizable()); + QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(m_window->size(), QSize(1280, 1024)); + + // We should receive a configure event when the window becomes active. + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + // One should still be able to change the maximized state of the window. + workspace()->slotWindowMaximize(); + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 3); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 3); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + QSignalSpy frameGeometryChangedSpy(m_window, &Window::frameGeometryChanged); + m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value()); + Test::render(m_surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(m_window->size(), QSize(100, 50)); + QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + + // If we create the window again, it should be initially maximized. + destroyTestWindow(); + createTestWindow(ReturnAfterSurfaceConfiguration); + + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + mapClientToSurface(QSize(1280, 1024)); + QVERIFY(m_window->isMaximizable()); + QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(m_window->size(), QSize(1280, 1024)); + + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testMaximizeRemember() +{ + setWindowRule("maximizehoriz", true, int(Rules::Remember)); + setWindowRule("maximizevert", true, int(Rules::Remember)); + + createTestWindow(ReturnAfterSurfaceConfiguration); + + // Wait for the initial configure event. + Test::XdgToplevel::States states; + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Map the window. + mapClientToSurface(QSize(1280, 1024)); + + QVERIFY(m_window->isMaximizable()); + QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(m_window->size(), QSize(1280, 1024)); + + // We should receive a configure event when the window becomes active. + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + // One should still be able to change the maximized state of the window. + workspace()->slotWindowMaximize(); + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 3); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 3); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + QSignalSpy frameGeometryChangedSpy(m_window, &Window::frameGeometryChanged); + m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value()); + Test::render(m_surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(m_window->size(), QSize(100, 50)); + QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + + // If we create the window again, it should not be maximized (because last time it wasn't). + destroyTestWindow(); + createTestWindow(ReturnAfterSurfaceConfiguration); + + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + mapClientToSurface(QSize(100, 50)); + + QVERIFY(m_window->isMaximizable()); + QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(m_window->size(), QSize(100, 50)); + + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testMaximizeForce() +{ + setWindowRule("maximizehoriz", true, int(Rules::Force)); + setWindowRule("maximizevert", true, int(Rules::Force)); + + createTestWindow(ReturnAfterSurfaceConfiguration); + + // Wait for the initial configure event. + Test::XdgToplevel::States states; + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Map the window. + mapClientToSurface(QSize(1280, 1024)); + + QVERIFY(!m_window->isMaximizable()); + QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(m_window->size(), QSize(1280, 1024)); + + // We should receive a configure event when the window becomes active. + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Any attempt to change the maximized state should not succeed. + const RectF oldGeometry = m_window->frameGeometry(); + workspace()->slotWindowMaximize(); + QVERIFY(!m_surfaceConfigureRequestedSpy->wait(100)); + QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(m_window->frameGeometry(), oldGeometry); + + // If we create the window again, the maximized state should still be forced. + destroyTestWindow(); + createTestWindow(ReturnAfterSurfaceConfiguration); + + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + mapClientToSurface(QSize(1280, 1024)); + + QVERIFY(!m_window->isMaximizable()); + QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(m_window->size(), QSize(1280, 1024)); + + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testMaximizeApplyNow() +{ + createTestWindow(ReturnAfterSurfaceConfiguration); + + // Wait for the initial configure event. + Test::XdgToplevel::States states; + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Map the window. + mapClientToSurface(QSize(100, 50)); + + QVERIFY(m_window->isMaximizable()); + QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(m_window->size(), QSize(100, 50)); + + // We should receive a configure event when the window becomes active. + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + setWindowRule("maximizehoriz", true, int(Rules::ApplyNow)); + setWindowRule("maximizevert", true, int(Rules::ApplyNow)); + + // We should receive a configure event with a new surface size. + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 3); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 3); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Draw contents of the maximized client. + QSignalSpy frameGeometryChangedSpy(m_window, &Window::frameGeometryChanged); + m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value()); + Test::render(m_surface.get(), QSize(1280, 1024), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(m_window->size(), QSize(1280, 1024)); + QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + + // The window still has to be maximizeable. + QVERIFY(m_window->isMaximizable()); + + // Restore the window. + workspace()->slotWindowMaximize(); + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 4); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 4); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(100, 50)); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value()); + Test::render(m_surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(m_window->size(), QSize(100, 50)); + QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + + // The rule should be discarded after it's been applied. + const RectF oldGeometry = m_window->frameGeometry(); + m_window->evaluateWindowRules(); + QVERIFY(!m_surfaceConfigureRequestedSpy->wait(100)); + QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(m_window->frameGeometry(), oldGeometry); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testMaximizeForceTemporarily() +{ + setWindowRule("maximizehoriz", true, int(Rules::ForceTemporarily)); + setWindowRule("maximizevert", true, int(Rules::ForceTemporarily)); + + createTestWindow(ReturnAfterSurfaceConfiguration); + + // Wait for the initial configure event. + Test::XdgToplevel::States states; + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Map the window. + mapClientToSurface(QSize(1280, 1024)); + + QVERIFY(!m_window->isMaximizable()); + QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(m_window->size(), QSize(1280, 1024)); + + // We should receive a configure event when the window becomes active. + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Any attempt to change the maximized state should not succeed. + const RectF oldGeometry = m_window->frameGeometry(); + workspace()->slotWindowMaximize(); + QVERIFY(!m_surfaceConfigureRequestedSpy->wait(100)); + QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(m_window->frameGeometry(), oldGeometry); + + // The rule should be discarded if we close the window. + destroyTestWindow(); + createTestWindow(ReturnAfterSurfaceConfiguration); + + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1); + QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + mapClientToSurface(QSize(100, 50)); + + QVERIFY(m_window->isMaximizable()); + QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(m_window->size(), QSize(100, 50)); + + QVERIFY(m_surfaceConfigureRequestedSpy->wait()); + QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2); + QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2); + states = m_toplevelConfigureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testDesktopsDontAffect() +{ + // We need at least two virtual desktop for this test. + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(VirtualDesktopManager::self()->count(), 2u); + VirtualDesktop *vd1 = VirtualDesktopManager::self()->desktops().at(0); + VirtualDesktop *vd2 = VirtualDesktopManager::self()->desktops().at(1); + + VirtualDesktopManager::self()->setCurrent(vd1); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1); + + setWindowRule("desktops", QStringList{vd2->id()}, int(Rules::DontAffect)); + + createTestWindow(); + + // The window should appear on the current virtual desktop. + QCOMPARE(m_window->desktops(), {vd1}); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testDesktopsApply() +{ + // We need at least two virtual desktop for this test. + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(VirtualDesktopManager::self()->count(), 2u); + VirtualDesktop *vd1 = VirtualDesktopManager::self()->desktops().at(0); + VirtualDesktop *vd2 = VirtualDesktopManager::self()->desktops().at(1); + + VirtualDesktopManager::self()->setCurrent(vd1); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1); + + setWindowRule("desktops", QStringList{vd2->id()}, int(Rules::Apply)); + + createTestWindow(); + + // The window should appear on the second virtual desktop. + QCOMPARE(m_window->desktops(), {vd2}); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2); + + // We still should be able to move the window between desktops. + m_window->setDesktops({vd1}); + QCOMPARE(m_window->desktops(), {vd1}); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2); + + // If we re-open the window, it should appear on the second virtual desktop again. + destroyTestWindow(); + VirtualDesktopManager::self()->setCurrent(vd1); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1); + createTestWindow(); + + QCOMPARE(m_window->desktops(), {vd2}); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testDesktopsRemember() +{ + // We need at least two virtual desktop for this test. + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(VirtualDesktopManager::self()->count(), 2u); + VirtualDesktop *vd1 = VirtualDesktopManager::self()->desktops().at(0); + VirtualDesktop *vd2 = VirtualDesktopManager::self()->desktops().at(1); + + VirtualDesktopManager::self()->setCurrent(vd1); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1); + + setWindowRule("desktops", QStringList{vd2->id()}, int(Rules::Remember)); + + createTestWindow(); + + QCOMPARE(m_window->desktops(), {vd2}); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2); + + // Move the window to the first virtual desktop. + m_window->setDesktops({vd1}); + QCOMPARE(m_window->desktops(), {vd1}); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2); + + // If we create the window again, it should appear on the first virtual desktop. + destroyTestWindow(); + createTestWindow(); + + QCOMPARE(m_window->desktops(), {vd1}); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testDesktopsForce() +{ + // We need at least two virtual desktop for this test. + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(VirtualDesktopManager::self()->count(), 2u); + VirtualDesktop *vd1 = VirtualDesktopManager::self()->desktops().at(0); + VirtualDesktop *vd2 = VirtualDesktopManager::self()->desktops().at(1); + + VirtualDesktopManager::self()->setCurrent(vd1); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1); + + setWindowRule("desktops", QStringList{vd2->id()}, int(Rules::Force)); + + createTestWindow(); + + // The window should appear on the second virtual desktop. + QCOMPARE(m_window->desktops(), {vd2}); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2); + + // Any attempt to move the window to another virtual desktop should fail. + m_window->setDesktops({vd1}); + QCOMPARE(m_window->desktops(), {vd2}); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2); + + // If we re-open the window, it should appear on the second virtual desktop again. + destroyTestWindow(); + VirtualDesktopManager::self()->setCurrent(vd1); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1); + createTestWindow(); + + QCOMPARE(m_window->desktops(), {vd2}); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testDesktopsApplyNow() +{ + // We need at least two virtual desktop for this test. + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(VirtualDesktopManager::self()->count(), 2u); + VirtualDesktop *vd1 = VirtualDesktopManager::self()->desktops().at(0); + VirtualDesktop *vd2 = VirtualDesktopManager::self()->desktops().at(1); + + VirtualDesktopManager::self()->setCurrent(vd1); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1); + + createTestWindow(); + + QCOMPARE(m_window->desktops(), {vd1}); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1); + + setWindowRule("desktops", QStringList{vd2->id()}, int(Rules::ApplyNow)); + + // The window should have been moved to the second virtual desktop. + QCOMPARE(m_window->desktops(), {vd2}); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1); + + // One should still be able to move the window between desktops. + m_window->setDesktops({vd1}); + QCOMPARE(m_window->desktops(), {vd1}); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1); + + // The rule should not be applied again. + m_window->evaluateWindowRules(); + QCOMPARE(m_window->desktops(), {vd1}); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testDesktopsForceTemporarily() +{ + // We need at least two virtual desktop for this test. + VirtualDesktopManager::self()->setCount(2); + QCOMPARE(VirtualDesktopManager::self()->count(), 2u); + VirtualDesktop *vd1 = VirtualDesktopManager::self()->desktops().at(0); + VirtualDesktop *vd2 = VirtualDesktopManager::self()->desktops().at(1); + + VirtualDesktopManager::self()->setCurrent(vd1); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1); + + setWindowRule("desktops", QStringList{vd2->id()}, int(Rules::ForceTemporarily)); + + createTestWindow(); + + // The window should appear on the second virtual desktop. + QCOMPARE(m_window->desktops(), {vd2}); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2); + + // Any attempt to move the window to another virtual desktop should fail. + m_window->setDesktops({vd1}); + QCOMPARE(m_window->desktops(), {vd2}); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2); + + // The rule should be discarded when the window is withdrawn. + destroyTestWindow(); + VirtualDesktopManager::self()->setCurrent(vd1); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1); + createTestWindow(); + + QCOMPARE(m_window->desktops(), {vd1}); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1); + + // One should be able to move the window between desktops. + m_window->setDesktops({vd2}); + QCOMPARE(m_window->desktops(), {vd2}); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1); + + m_window->setDesktops({vd1}); + QCOMPARE(m_window->desktops(), {vd1}); + QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testMinimizeDontAffect() +{ + setWindowRule("minimize", true, int(Rules::DontAffect)); + + createTestWindow(); + QVERIFY(m_window->isMinimizable()); + + // The window should not be minimized. + QVERIFY(!m_window->isMinimized()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testMinimizeApply() +{ + setWindowRule("minimize", true, int(Rules::Apply)); + + createTestWindow(ClientShouldBeInactive); + QVERIFY(m_window->isMinimizable()); + + // The window should be minimized. + QVERIFY(m_window->isMinimized()); + + // We should still be able to unminimize the window. + m_window->setMinimized(false); + QVERIFY(!m_window->isMinimized()); + + // If we re-open the window, it should be minimized back again. + destroyTestWindow(); + createTestWindow(ClientShouldBeInactive); + QVERIFY(m_window->isMinimizable()); + QVERIFY(m_window->isMinimized()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testMinimizeRemember() +{ + setWindowRule("minimize", false, int(Rules::Remember)); + + createTestWindow(); + QVERIFY(m_window->isMinimizable()); + QVERIFY(!m_window->isMinimized()); + + // Minimize the window. + m_window->setMinimized(true); + QVERIFY(m_window->isMinimized()); + + // If we open the window again, it should be minimized. + destroyTestWindow(); + createTestWindow(ClientShouldBeInactive); + QVERIFY(m_window->isMinimizable()); + QVERIFY(m_window->isMinimized()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testMinimizeForce() +{ + setWindowRule("minimize", false, int(Rules::Force)); + + createTestWindow(); + QVERIFY(!m_window->isMinimizable()); + QVERIFY(!m_window->isMinimized()); + + // Any attempt to minimize the window should fail. + m_window->setMinimized(true); + QVERIFY(!m_window->isMinimized()); + + // If we re-open the window, the minimized state should still be forced. + destroyTestWindow(); + createTestWindow(); + QVERIFY(!m_window->isMinimizable()); + QVERIFY(!m_window->isMinimized()); + m_window->setMinimized(true); + QVERIFY(!m_window->isMinimized()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testMinimizeApplyNow() +{ + createTestWindow(); + QVERIFY(m_window->isMinimizable()); + QVERIFY(!m_window->isMinimized()); + + setWindowRule("minimize", true, int(Rules::ApplyNow)); + + // The window should be minimized now. + QVERIFY(m_window->isMinimizable()); + QVERIFY(m_window->isMinimized()); + + // One is still able to unminimize the window. + m_window->setMinimized(false); + QVERIFY(!m_window->isMinimized()); + + // The rule should not be applied again. + m_window->evaluateWindowRules(); + QVERIFY(m_window->isMinimizable()); + QVERIFY(!m_window->isMinimized()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testMinimizeForceTemporarily() +{ + setWindowRule("minimize", false, int(Rules::ForceTemporarily)); + + createTestWindow(); + QVERIFY(!m_window->isMinimizable()); + QVERIFY(!m_window->isMinimized()); + + // Any attempt to minimize the window should fail until the window is closed. + m_window->setMinimized(true); + QVERIFY(!m_window->isMinimized()); + + // The rule should be discarded when the window is closed. + destroyTestWindow(); + createTestWindow(); + QVERIFY(m_window->isMinimizable()); + QVERIFY(!m_window->isMinimized()); + m_window->setMinimized(true); + QVERIFY(m_window->isMinimized()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSkipTaskbarDontAffect() +{ + setWindowRule("skiptaskbar", true, int(Rules::DontAffect)); + + createTestWindow(); + + // The window should not be affected by the rule. + QVERIFY(!m_window->skipTaskbar()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSkipTaskbarApply() +{ + setWindowRule("skiptaskbar", true, int(Rules::Apply)); + + createTestWindow(); + + // The window should not be included on a taskbar. + QVERIFY(m_window->skipTaskbar()); + + // Though one can change that. + m_window->setOriginalSkipTaskbar(false); + QVERIFY(!m_window->skipTaskbar()); + + // Reopen the window, the rule should be applied again. + destroyTestWindow(); + createTestWindow(); + QVERIFY(m_window->skipTaskbar()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSkipTaskbarRemember() +{ + setWindowRule("skiptaskbar", true, int(Rules::Remember)); + + createTestWindow(); + + // The window should not be included on a taskbar. + QVERIFY(m_window->skipTaskbar()); + + // Change the skip-taskbar state. + m_window->setOriginalSkipTaskbar(false); + QVERIFY(!m_window->skipTaskbar()); + + // Reopen the window. + destroyTestWindow(); + createTestWindow(); + + // The window should be included on a taskbar. + QVERIFY(!m_window->skipTaskbar()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSkipTaskbarForce() +{ + setWindowRule("skiptaskbar", true, int(Rules::Force)); + + createTestWindow(); + + // The window should not be included on a taskbar. + QVERIFY(m_window->skipTaskbar()); + + // Any attempt to change the skip-taskbar state should not succeed. + m_window->setOriginalSkipTaskbar(false); + QVERIFY(m_window->skipTaskbar()); + + // Reopen the window. + destroyTestWindow(); + createTestWindow(); + + // The skip-taskbar state should be still forced. + QVERIFY(m_window->skipTaskbar()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSkipTaskbarApplyNow() +{ + createTestWindow(); + QVERIFY(!m_window->skipTaskbar()); + + setWindowRule("skiptaskbar", true, int(Rules::ApplyNow)); + + // The window should not be on a taskbar now. + QVERIFY(m_window->skipTaskbar()); + + // Also, one change the skip-taskbar state. + m_window->setOriginalSkipTaskbar(false); + QVERIFY(!m_window->skipTaskbar()); + + // The rule should not be applied again. + m_window->evaluateWindowRules(); + QVERIFY(!m_window->skipTaskbar()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSkipTaskbarForceTemporarily() +{ + setWindowRule("skiptaskbar", true, int(Rules::ForceTemporarily)); + + createTestWindow(); + + // The window should not be included on a taskbar. + QVERIFY(m_window->skipTaskbar()); + + // Any attempt to change the skip-taskbar state should not succeed. + m_window->setOriginalSkipTaskbar(false); + QVERIFY(m_window->skipTaskbar()); + + // The rule should be discarded when the window is closed. + destroyTestWindow(); + createTestWindow(); + QVERIFY(!m_window->skipTaskbar()); + + // The skip-taskbar state is no longer forced. + m_window->setOriginalSkipTaskbar(true); + QVERIFY(m_window->skipTaskbar()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSkipPagerDontAffect() +{ + setWindowRule("skippager", true, int(Rules::DontAffect)); + + createTestWindow(); + + // The window should not be affected by the rule. + QVERIFY(!m_window->skipPager()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSkipPagerApply() +{ + setWindowRule("skippager", true, int(Rules::Apply)); + + createTestWindow(); + + // The window should not be included on a pager. + QVERIFY(m_window->skipPager()); + + // Though one can change that. + m_window->setSkipPager(false); + QVERIFY(!m_window->skipPager()); + + // Reopen the window, the rule should be applied again. + destroyTestWindow(); + createTestWindow(); + QVERIFY(m_window->skipPager()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSkipPagerRemember() +{ + setWindowRule("skippager", true, int(Rules::Remember)); + + createTestWindow(); + + // The window should not be included on a pager. + QVERIFY(m_window->skipPager()); + + // Change the skip-pager state. + m_window->setSkipPager(false); + QVERIFY(!m_window->skipPager()); + + // Reopen the window. + destroyTestWindow(); + createTestWindow(); + + // The window should be included on a pager. + QVERIFY(!m_window->skipPager()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSkipPagerForce() +{ + setWindowRule("skippager", true, int(Rules::Force)); + + createTestWindow(); + + // The window should not be included on a pager. + QVERIFY(m_window->skipPager()); + + // Any attempt to change the skip-pager state should not succeed. + m_window->setSkipPager(false); + QVERIFY(m_window->skipPager()); + + // Reopen the window. + destroyTestWindow(); + createTestWindow(); + + // The skip-pager state should be still forced. + QVERIFY(m_window->skipPager()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSkipPagerApplyNow() +{ + createTestWindow(); + QVERIFY(!m_window->skipPager()); + + setWindowRule("skippager", true, int(Rules::ApplyNow)); + + // The window should not be on a pager now. + QVERIFY(m_window->skipPager()); + + // Also, one change the skip-pager state. + m_window->setSkipPager(false); + QVERIFY(!m_window->skipPager()); + + // The rule should not be applied again. + m_window->evaluateWindowRules(); + QVERIFY(!m_window->skipPager()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSkipPagerForceTemporarily() +{ + setWindowRule("skippager", true, int(Rules::ForceTemporarily)); + + createTestWindow(); + + // The window should not be included on a pager. + QVERIFY(m_window->skipPager()); + + // Any attempt to change the skip-pager state should not succeed. + m_window->setSkipPager(false); + QVERIFY(m_window->skipPager()); + + // The rule should be discarded when the window is closed. + destroyTestWindow(); + createTestWindow(); + QVERIFY(!m_window->skipPager()); + + // The skip-pager state is no longer forced. + m_window->setSkipPager(true); + QVERIFY(m_window->skipPager()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSkipSwitcherDontAffect() +{ + setWindowRule("skipswitcher", true, int(Rules::DontAffect)); + + createTestWindow(); + + // The window should not be affected by the rule. + QVERIFY(!m_window->skipSwitcher()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSkipSwitcherApply() +{ + setWindowRule("skipswitcher", true, int(Rules::Apply)); + + createTestWindow(); + + // The window should be excluded from window switching effects. + QVERIFY(m_window->skipSwitcher()); + + // Though one can change that. + m_window->setSkipSwitcher(false); + QVERIFY(!m_window->skipSwitcher()); + + // Reopen the window, the rule should be applied again. + destroyTestWindow(); + createTestWindow(); + QVERIFY(m_window->skipSwitcher()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSkipSwitcherRemember() +{ + setWindowRule("skipswitcher", true, int(Rules::Remember)); + + createTestWindow(); + + // The window should be excluded from window switching effects. + QVERIFY(m_window->skipSwitcher()); + + // Change the skip-switcher state. + m_window->setSkipSwitcher(false); + QVERIFY(!m_window->skipSwitcher()); + + // Reopen the window. + destroyTestWindow(); + createTestWindow(); + + // The window should be included in window switching effects. + QVERIFY(!m_window->skipSwitcher()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSkipSwitcherForce() +{ + setWindowRule("skipswitcher", true, int(Rules::Force)); + + createTestWindow(); + + // The window should be excluded from window switching effects. + QVERIFY(m_window->skipSwitcher()); + + // Any attempt to change the skip-switcher state should not succeed. + m_window->setSkipSwitcher(false); + QVERIFY(m_window->skipSwitcher()); + + // Reopen the window. + destroyTestWindow(); + createTestWindow(); + + // The skip-switcher state should be still forced. + QVERIFY(m_window->skipSwitcher()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSkipSwitcherApplyNow() +{ + createTestWindow(); + QVERIFY(!m_window->skipSwitcher()); + + setWindowRule("skipswitcher", true, int(Rules::ApplyNow)); + + // The window should be excluded from window switching effects now. + QVERIFY(m_window->skipSwitcher()); + + // Also, one change the skip-switcher state. + m_window->setSkipSwitcher(false); + QVERIFY(!m_window->skipSwitcher()); + + // The rule should not be applied again. + m_window->evaluateWindowRules(); + QVERIFY(!m_window->skipSwitcher()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testSkipSwitcherForceTemporarily() +{ + setWindowRule("skipswitcher", true, int(Rules::ForceTemporarily)); + + createTestWindow(); + + // The window should be excluded from window switching effects. + QVERIFY(m_window->skipSwitcher()); + + // Any attempt to change the skip-switcher state should not succeed. + m_window->setSkipSwitcher(false); + QVERIFY(m_window->skipSwitcher()); + + // The rule should be discarded when the window is closed. + destroyTestWindow(); + createTestWindow(); + QVERIFY(!m_window->skipSwitcher()); + + // The skip-switcher state is no longer forced. + m_window->setSkipSwitcher(true); + QVERIFY(m_window->skipSwitcher()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testKeepAboveDontAffect() +{ + setWindowRule("above", true, int(Rules::DontAffect)); + + createTestWindow(); + + // The keep-above state of the window should not be affected by the rule. + QVERIFY(!m_window->keepAbove()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testKeepAboveApply() +{ + setWindowRule("above", true, int(Rules::Apply)); + + createTestWindow(); + + // Initially, the window should be kept above. + QVERIFY(m_window->keepAbove()); + + // One should also be able to alter the keep-above state. + m_window->setKeepAbove(false); + QVERIFY(!m_window->keepAbove()); + + // If one re-opens the window, it should be kept above back again. + destroyTestWindow(); + createTestWindow(); + QVERIFY(m_window->keepAbove()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testKeepAboveRemember() +{ + setWindowRule("above", true, int(Rules::Remember)); + + createTestWindow(); + + // Initially, the window should be kept above. + QVERIFY(m_window->keepAbove()); + + // Unset the keep-above state. + m_window->setKeepAbove(false); + QVERIFY(!m_window->keepAbove()); + destroyTestWindow(); + + // Re-open the window, it should not be kept above. + createTestWindow(); + QVERIFY(!m_window->keepAbove()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testKeepAboveForce() +{ + setWindowRule("above", true, int(Rules::Force)); + + createTestWindow(); + + // Initially, the window should be kept above. + QVERIFY(m_window->keepAbove()); + + // Any attempt to unset the keep-above should not succeed. + m_window->setKeepAbove(false); + QVERIFY(m_window->keepAbove()); + + // If we re-open the window, it should still be kept above. + destroyTestWindow(); + createTestWindow(); + QVERIFY(m_window->keepAbove()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testKeepAboveApplyNow() +{ + createTestWindow(); + QVERIFY(!m_window->keepAbove()); + + setWindowRule("above", true, int(Rules::ApplyNow)); + + // The window should now be kept above other windows. + QVERIFY(m_window->keepAbove()); + + // One is still able to change the keep-above state of the window. + m_window->setKeepAbove(false); + QVERIFY(!m_window->keepAbove()); + + // The rule should not be applied again. + m_window->evaluateWindowRules(); + QVERIFY(!m_window->keepAbove()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testKeepAboveForceTemporarily() +{ + setWindowRule("above", true, int(Rules::ForceTemporarily)); + + createTestWindow(); + + // Initially, the window should be kept above. + QVERIFY(m_window->keepAbove()); + + // Any attempt to alter the keep-above state should not succeed. + m_window->setKeepAbove(false); + QVERIFY(m_window->keepAbove()); + + // The rule should be discarded when the window is closed. + destroyTestWindow(); + createTestWindow(); + QVERIFY(!m_window->keepAbove()); + + // The keep-above state is no longer forced. + m_window->setKeepAbove(true); + QVERIFY(m_window->keepAbove()); + m_window->setKeepAbove(false); + QVERIFY(!m_window->keepAbove()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testKeepBelowDontAffect() +{ + setWindowRule("below", true, int(Rules::DontAffect)); + + createTestWindow(); + + // The keep-below state of the window should not be affected by the rule. + QVERIFY(!m_window->keepBelow()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testKeepBelowApply() +{ + setWindowRule("below", true, int(Rules::Apply)); + + createTestWindow(); + + // Initially, the window should be kept below. + QVERIFY(m_window->keepBelow()); + + // One should also be able to alter the keep-below state. + m_window->setKeepBelow(false); + QVERIFY(!m_window->keepBelow()); + + // If one re-opens the window, it should be kept above back again. + destroyTestWindow(); + createTestWindow(); + QVERIFY(m_window->keepBelow()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testKeepBelowRemember() +{ + setWindowRule("below", true, int(Rules::Remember)); + + createTestWindow(); + + // Initially, the window should be kept below. + QVERIFY(m_window->keepBelow()); + + // Unset the keep-below state. + m_window->setKeepBelow(false); + QVERIFY(!m_window->keepBelow()); + destroyTestWindow(); + + // Re-open the window, it should not be kept below. + createTestWindow(); + QVERIFY(!m_window->keepBelow()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testKeepBelowForce() +{ + setWindowRule("below", true, int(Rules::Force)); + + createTestWindow(); + + // Initially, the window should be kept below. + QVERIFY(m_window->keepBelow()); + + // Any attempt to unset the keep-below should not succeed. + m_window->setKeepBelow(false); + QVERIFY(m_window->keepBelow()); + + // If we re-open the window, it should still be kept below. + destroyTestWindow(); + createTestWindow(); + QVERIFY(m_window->keepBelow()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testKeepBelowApplyNow() +{ + createTestWindow(); + QVERIFY(!m_window->keepBelow()); + + setWindowRule("below", true, int(Rules::ApplyNow)); + + // The window should now be kept below other windows. + QVERIFY(m_window->keepBelow()); + + // One is still able to change the keep-below state of the window. + m_window->setKeepBelow(false); + QVERIFY(!m_window->keepBelow()); + + // The rule should not be applied again. + m_window->evaluateWindowRules(); + QVERIFY(!m_window->keepBelow()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testKeepBelowForceTemporarily() +{ + setWindowRule("below", true, int(Rules::ForceTemporarily)); + + createTestWindow(); + + // Initially, the window should be kept below. + QVERIFY(m_window->keepBelow()); + + // Any attempt to alter the keep-below state should not succeed. + m_window->setKeepBelow(false); + QVERIFY(m_window->keepBelow()); + + // The rule should be discarded when the window is closed. + destroyTestWindow(); + createTestWindow(); + QVERIFY(!m_window->keepBelow()); + + // The keep-below state is no longer forced. + m_window->setKeepBelow(true); + QVERIFY(m_window->keepBelow()); + m_window->setKeepBelow(false); + QVERIFY(!m_window->keepBelow()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testShortcutDontAffect() +{ +#if !KWIN_BUILD_GLOBALSHORTCUTS + QSKIP("Can't test shortcuts without shortcuts"); + return; +#endif + + setWindowRule("shortcut", "Ctrl+Alt+1", int(Rules::DontAffect)); + + createTestWindow(); + QCOMPARE(m_window->shortcut(), QKeySequence()); + m_window->setMinimized(true); + QVERIFY(m_window->isMinimized()); + + // If we press the window shortcut, nothing should happen. + QSignalSpy minimizedChangedSpy(m_window, &Window::minimizedChanged); + quint32 timestamp = 1; + Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + Test::keyboardKeyPressed(KEY_1, timestamp++); + Test::keyboardKeyReleased(KEY_1, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(!minimizedChangedSpy.wait(100)); + QVERIFY(m_window->isMinimized()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testShortcutApply() +{ +#if !KWIN_BUILD_GLOBALSHORTCUTS + QSKIP("Can't test shortcuts without shortcuts"); + return; +#endif + setWindowRule("shortcut", "Ctrl+Alt+1", int(Rules::Apply)); + + createTestWindow(); + + // If we press the window shortcut, the window should be brought back to user. + QSignalSpy minimizedChangedSpy(m_window, &Window::minimizedChanged); + quint32 timestamp = 1; + QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_1})); + m_window->setMinimized(true); + QVERIFY(m_window->isMinimized()); + Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + Test::keyboardKeyPressed(KEY_1, timestamp++); + Test::keyboardKeyReleased(KEY_1, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(minimizedChangedSpy.wait()); + QVERIFY(!m_window->isMinimized()); + + // One can also change the shortcut. + m_window->setShortcut(QStringLiteral("Ctrl+Alt+2")); + QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_2})); + m_window->setMinimized(true); + QVERIFY(m_window->isMinimized()); + Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + Test::keyboardKeyPressed(KEY_2, timestamp++); + Test::keyboardKeyReleased(KEY_2, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(minimizedChangedSpy.wait()); + QVERIFY(!m_window->isMinimized()); + + // The old shortcut should do nothing. + m_window->setMinimized(true); + QVERIFY(m_window->isMinimized()); + Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + Test::keyboardKeyPressed(KEY_1, timestamp++); + Test::keyboardKeyReleased(KEY_1, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(!minimizedChangedSpy.wait(100)); + QVERIFY(m_window->isMinimized()); + + // Reopen the window. + destroyTestWindow(); + createTestWindow(); + + // The window shortcut should be set back to Ctrl+Alt+1. + QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_1})); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testShortcutRemember() +{ + QSKIP("KWin core doesn't try to save the last used window shortcut"); + + setWindowRule("shortcut", "Ctrl+Alt+1", int(Rules::Remember)); + + createTestWindow(); + + // If we press the window shortcut, the window should be brought back to user. + QSignalSpy minimizedChangedSpy(m_window, &Window::minimizedChanged); + quint32 timestamp = 1; + QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_1})); + m_window->setMinimized(true); + QVERIFY(m_window->isMinimized()); + Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + Test::keyboardKeyPressed(KEY_1, timestamp++); + Test::keyboardKeyReleased(KEY_1, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(minimizedChangedSpy.wait()); + QVERIFY(!m_window->isMinimized()); + + // Change the window shortcut to Ctrl+Alt+2. + m_window->setShortcut(QStringLiteral("Ctrl+Alt+2")); + QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_2})); + m_window->setMinimized(true); + QVERIFY(m_window->isMinimized()); + Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + Test::keyboardKeyPressed(KEY_2, timestamp++); + Test::keyboardKeyReleased(KEY_2, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(minimizedChangedSpy.wait()); + QVERIFY(!m_window->isMinimized()); + + // Reopen the window. + destroyTestWindow(); + createTestWindow(); + + // The window shortcut should be set to the last known value. + QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_2})); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testShortcutForce() +{ + QSKIP("KWin core can't release forced window shortcuts"); + + setWindowRule("shortcut", "Ctrl+Alt+1", int(Rules::Force)); + + createTestWindow(); + + // If we press the window shortcut, the window should be brought back to user. + QSignalSpy minimizedChangedSpy(m_window, &Window::minimizedChanged); + quint32 timestamp = 1; + QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_1})); + m_window->setMinimized(true); + QVERIFY(m_window->isMinimized()); + Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + Test::keyboardKeyPressed(KEY_1, timestamp++); + Test::keyboardKeyReleased(KEY_1, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(minimizedChangedSpy.wait()); + QVERIFY(!m_window->isMinimized()); + + // Any attempt to change the window shortcut should not succeed. + m_window->setShortcut(QStringLiteral("Ctrl+Alt+2")); + QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_1})); + m_window->setMinimized(true); + QVERIFY(m_window->isMinimized()); + Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + Test::keyboardKeyPressed(KEY_2, timestamp++); + Test::keyboardKeyReleased(KEY_2, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(!minimizedChangedSpy.wait(100)); + QVERIFY(m_window->isMinimized()); + + // Reopen the window. + destroyTestWindow(); + createTestWindow(); + + // The window shortcut should still be forced. + QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_1})); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testShortcutApplyNow() +{ +#if !KWIN_BUILD_GLOBALSHORTCUTS + QSKIP("Can't test shortcuts without shortcuts"); + return; +#endif + + createTestWindow(); + QVERIFY(m_window->shortcut().isEmpty()); + + setWindowRule("shortcut", "Ctrl+Alt+1", int(Rules::ApplyNow)); + + // The window should now have a window shortcut assigned. + QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_1})); + QSignalSpy minimizedChangedSpy(m_window, &Window::minimizedChanged); + quint32 timestamp = 1; + m_window->setMinimized(true); + QVERIFY(m_window->isMinimized()); + Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + Test::keyboardKeyPressed(KEY_1, timestamp++); + Test::keyboardKeyReleased(KEY_1, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(minimizedChangedSpy.wait()); + QVERIFY(!m_window->isMinimized()); + + // Assign a different shortcut. + m_window->setShortcut(QStringLiteral("Ctrl+Alt+2")); + QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_2})); + m_window->setMinimized(true); + QVERIFY(m_window->isMinimized()); + Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + Test::keyboardKeyPressed(KEY_2, timestamp++); + Test::keyboardKeyReleased(KEY_2, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(minimizedChangedSpy.wait()); + QVERIFY(!m_window->isMinimized()); + + // The rule should not be applied again. + m_window->evaluateWindowRules(); + QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_2})); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testShortcutForceTemporarily() +{ + QSKIP("KWin core can't release forced window shortcuts"); + + setWindowRule("shortcut", "Ctrl+Alt+1", int(Rules::ForceTemporarily)); + + createTestWindow(); + + // If we press the window shortcut, the window should be brought back to user. + QSignalSpy minimizedChangedSpy(m_window, &Window::minimizedChanged); + quint32 timestamp = 1; + QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_1})); + m_window->setMinimized(true); + QVERIFY(m_window->isMinimized()); + Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + Test::keyboardKeyPressed(KEY_1, timestamp++); + Test::keyboardKeyReleased(KEY_1, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(minimizedChangedSpy.wait()); + QVERIFY(!m_window->isMinimized()); + + // Any attempt to change the window shortcut should not succeed. + m_window->setShortcut(QStringLiteral("Ctrl+Alt+2")); + QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_1})); + m_window->setMinimized(true); + QVERIFY(m_window->isMinimized()); + Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); + Test::keyboardKeyPressed(KEY_2, timestamp++); + Test::keyboardKeyReleased(KEY_2, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); + Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); + QVERIFY(!minimizedChangedSpy.wait(100)); + QVERIFY(m_window->isMinimized()); + + // The rule should be discarded when the window is closed. + destroyTestWindow(); + createTestWindow(); + QVERIFY(m_window->shortcut().isEmpty()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testDesktopFileDontAffect() +{ + // Currently, the desktop file name is derived from the app id. If the app id is + // changed, then the old rules will be lost. Either setDesktopFileName should + // be exposed or the desktop file name rule should be removed for wayland windows. + QSKIP("Needs changes in KWin core to pass"); +} + +void TestXdgShellWindowRules::testDesktopFileApply() +{ + // Currently, the desktop file name is derived from the app id. If the app id is + // changed, then the old rules will be lost. Either setDesktopFileName should + // be exposed or the desktop file name rule should be removed for wayland windows. + QSKIP("Needs changes in KWin core to pass"); +} + +void TestXdgShellWindowRules::testDesktopFileRemember() +{ + // Currently, the desktop file name is derived from the app id. If the app id is + // changed, then the old rules will be lost. Either setDesktopFileName should + // be exposed or the desktop file name rule should be removed for wayland windows. + QSKIP("Needs changes in KWin core to pass"); +} + +void TestXdgShellWindowRules::testDesktopFileForce() +{ + // Currently, the desktop file name is derived from the app id. If the app id is + // changed, then the old rules will be lost. Either setDesktopFileName should + // be exposed or the desktop file name rule should be removed for wayland windows. + QSKIP("Needs changes in KWin core to pass"); +} + +void TestXdgShellWindowRules::testDesktopFileApplyNow() +{ + // Currently, the desktop file name is derived from the app id. If the app id is + // changed, then the old rules will be lost. Either setDesktopFileName should + // be exposed or the desktop file name rule should be removed for wayland windows. + QSKIP("Needs changes in KWin core to pass"); +} + +void TestXdgShellWindowRules::testDesktopFileForceTemporarily() +{ + // Currently, the desktop file name is derived from the app id. If the app id is + // changed, then the old rules will be lost. Either setDesktopFileName should + // be exposed or the desktop file name rule should be removed for wayland windows. + QSKIP("Needs changes in KWin core to pass"); +} + +void TestXdgShellWindowRules::testActiveOpacityDontAffect() +{ + setWindowRule("opacityactive", 90, int(Rules::DontAffect)); + + createTestWindow(); + QVERIFY(m_window->isActive()); + + // The opacity should not be affected by the rule. + QCOMPARE(m_window->opacity(), 1.0); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testActiveOpacityForce() +{ + setWindowRule("opacityactive", 90, int(Rules::Force)); + + createTestWindow(); + QVERIFY(m_window->isActive()); + QCOMPARE(m_window->opacity(), 0.9); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testActiveOpacityForceTemporarily() +{ + setWindowRule("opacityactive", 90, int(Rules::ForceTemporarily)); + + createTestWindow(); + QVERIFY(m_window->isActive()); + QCOMPARE(m_window->opacity(), 0.9); + + // The rule should be discarded when the window is closed. + destroyTestWindow(); + createTestWindow(); + QVERIFY(m_window->isActive()); + QCOMPARE(m_window->opacity(), 1.0); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testInactiveOpacityDontAffect() +{ + setWindowRule("opacityinactive", 80, int(Rules::DontAffect)); + + createTestWindow(); + QVERIFY(m_window->isActive()); + + // Make the window inactive. + workspace()->activateWindow(nullptr); + QVERIFY(!m_window->isActive()); + + // The opacity of the window should not be affected by the rule. + QCOMPARE(m_window->opacity(), 1.0); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testInactiveOpacityForce() +{ + setWindowRule("opacityinactive", 80, int(Rules::Force)); + + createTestWindow(); + QVERIFY(m_window->isActive()); + QCOMPARE(m_window->opacity(), 1.0); + + // Make the window inactive. + workspace()->activateWindow(nullptr); + QVERIFY(!m_window->isActive()); + + // The opacity should be forced by the rule. + QCOMPARE(m_window->opacity(), 0.8); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testInactiveOpacityForceTemporarily() +{ + setWindowRule("opacityinactive", 80, int(Rules::ForceTemporarily)); + + createTestWindow(); + QVERIFY(m_window->isActive()); + QCOMPARE(m_window->opacity(), 1.0); + + // Make the window inactive. + workspace()->activateWindow(nullptr); + QVERIFY(!m_window->isActive()); + + // The opacity should be forced by the rule. + QCOMPARE(m_window->opacity(), 0.8); + + // The rule should be discarded when the window is closed. + destroyTestWindow(); + createTestWindow(); + + QVERIFY(m_window->isActive()); + QCOMPARE(m_window->opacity(), 1.0); + workspace()->activateWindow(nullptr); + QVERIFY(!m_window->isActive()); + QCOMPARE(m_window->opacity(), 1.0); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testNoBorderDontAffect() +{ + setWindowRule("noborder", true, int(Rules::DontAffect)); + createTestWindow(ServerSideDecoration); + + // The window should not be affected by the rule. + QVERIFY(!m_window->noBorder()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testNoBorderApply() +{ + setWindowRule("noborder", true, int(Rules::Apply)); + createTestWindow(ServerSideDecoration); + + // Initially, the window should not be decorated. + QVERIFY(m_window->noBorder()); + QVERIFY(!m_window->isDecorated()); + + // But you should be able to change "no border" property afterwards. + QVERIFY(m_window->userCanSetNoBorder()); + m_window->setNoBorder(false); + QVERIFY(!m_window->noBorder()); + + // If one re-opens the window, it should have no border again. + destroyTestWindow(); + createTestWindow(ServerSideDecoration); + QVERIFY(m_window->noBorder()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testNoBorderRemember() +{ + setWindowRule("noborder", true, int(Rules::Remember)); + createTestWindow(ServerSideDecoration); + + // Initially, the window should not be decorated. + QVERIFY(m_window->noBorder()); + QVERIFY(!m_window->isDecorated()); + + // Unset the "no border" property. + QVERIFY(m_window->userCanSetNoBorder()); + m_window->setNoBorder(false); + QVERIFY(!m_window->noBorder()); + + // Re-open the window, it should be decorated. + destroyTestWindow(); + createTestWindow(ServerSideDecoration); + QVERIFY(m_window->isDecorated()); + QVERIFY(!m_window->noBorder()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testNoBorderForce() +{ + setWindowRule("noborder", true, int(Rules::Force)); + createTestWindow(ServerSideDecoration); + + // The window should not be decorated. + QVERIFY(m_window->noBorder()); + QVERIFY(!m_window->isDecorated()); + + // And the user should not be able to change the "no border" property. + m_window->setNoBorder(false); + QVERIFY(m_window->noBorder()); + + // Reopen the window. + destroyTestWindow(); + createTestWindow(ServerSideDecoration); + + // The "no border" property should be still forced. + QVERIFY(m_window->noBorder()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testNoBorderApplyNow() +{ + createTestWindow(ServerSideDecoration); + QVERIFY(!m_window->noBorder()); + + // Initialize RuleBook with the test rule. + setWindowRule("noborder", true, int(Rules::ApplyNow)); + + // The "no border" property should be set now. + QVERIFY(m_window->noBorder()); + + // One should be still able to change the "no border" property. + m_window->setNoBorder(false); + QVERIFY(!m_window->noBorder()); + + // The rule should not be applied again. + m_window->evaluateWindowRules(); + QVERIFY(!m_window->noBorder()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testNoBorderForceTemporarily() +{ + setWindowRule("noborder", true, int(Rules::ForceTemporarily)); + createTestWindow(ServerSideDecoration); + + // The "no border" property should be set. + QVERIFY(m_window->noBorder()); + + // And you should not be able to change it. + m_window->setNoBorder(false); + QVERIFY(m_window->noBorder()); + + // The rule should be discarded when the window is closed. + destroyTestWindow(); + createTestWindow(ServerSideDecoration); + QVERIFY(!m_window->noBorder()); + + // The "no border" property is no longer forced. + m_window->setNoBorder(true); + QVERIFY(m_window->noBorder()); + m_window->setNoBorder(false); + QVERIFY(!m_window->noBorder()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testScreenDontAffect() +{ + const QList outputs = workspace()->outputs(); + + setWindowRule("screen", int(1), int(Rules::DontAffect)); + + createTestWindow(); + + // The window should not be affected by the rule. + QCOMPARE(m_window->output()->name(), outputs.at(0)->name()); + + // The user can still move the window to another screen. + m_window->sendToOutput(outputs.at(1)); + QCOMPARE(m_window->output()->name(), outputs.at(1)->name()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testScreenApply() +{ + const QList outputs = workspace()->outputs(); + + setWindowRule("screen", int(1), int(Rules::Apply)); + + createTestWindow(); + + // The window should be in the screen specified by the rule. + QEXPECT_FAIL("", "Applying a screen rule on a new client fails on Wayland", Continue); + QCOMPARE(m_window->output()->name(), outputs.at(1)->name()); + + // The user can move the window to another screen. + m_window->sendToOutput(outputs.at(0)); + QCOMPARE(m_window->output()->name(), outputs.at(0)->name()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testScreenRemember() +{ + const QList outputs = workspace()->outputs(); + + setWindowRule("screen", int(1), int(Rules::Remember)); + + createTestWindow(); + + // Initially, the window should be in the first screen + QCOMPARE(m_window->output()->name(), outputs.at(0)->name()); + + // Move the window to the second screen. + m_window->sendToOutput(outputs.at(1)); + QCOMPARE(m_window->output()->name(), outputs.at(1)->name()); + + // Close and reopen the window. + destroyTestWindow(); + workspace()->setActiveOutput(outputs.at(0)); + createTestWindow(); + + QEXPECT_FAIL("", "Applying a screen rule on a new client fails on Wayland", Continue); + QCOMPARE(m_window->output()->name(), outputs.at(1)->name()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testScreenForce() +{ + const QList outputs = kwinApp()->outputBackend()->outputs(); + + createTestWindow(); + QVERIFY(m_window->isActive()); + + setWindowRule("screen", int(1), int(Rules::Force)); + + // The window should be forced to the screen specified by the rule. + QCOMPARE(m_window->output()->name(), outputs.at(1)->name()); + + // User should not be able to move the window to another screen. + m_window->sendToOutput(workspace()->findOutput(outputs.at(0))); + QCOMPARE(m_window->output()->name(), outputs.at(1)->name()); + + // Disable the output where the window is on, so the window is moved the other screen + OutputConfiguration config; + auto changeSet = config.changeSet(outputs.at(1)); + changeSet->enabled = false; + workspace()->applyOutputConfiguration(config); + + QVERIFY(!outputs.at(1)->isEnabled()); + QCOMPARE(m_window->output()->name(), outputs.at(0)->name()); + + // Enable the output and check that the window is moved there again + changeSet->enabled = true; + workspace()->applyOutputConfiguration(config); + + QVERIFY(outputs.at(1)->isEnabled()); + QCOMPARE(m_window->output()->name(), outputs.at(1)->name()); + + // Close and reopen the window. + destroyTestWindow(); + createTestWindow(); + + QEXPECT_FAIL("", "Applying a screen rule on a new client fails on Wayland", Continue); + QCOMPARE(m_window->output()->name(), outputs.at(1)->name()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testScreenApplyNow() +{ + const QList outputs = workspace()->outputs(); + + createTestWindow(); + + QCOMPARE(m_window->output()->name(), outputs.at(0)->name()); + + // Set the rule so the window should move to the screen specified by the rule. + setWindowRule("screen", int(1), int(Rules::ApplyNow)); + QCOMPARE(m_window->output()->name(), outputs.at(1)->name()); + + // The user can move the window to another screen. + m_window->sendToOutput(outputs.at(0)); + QCOMPARE(m_window->output()->name(), outputs.at(0)->name()); + + // The rule should not be applied again. + m_window->evaluateWindowRules(); + QCOMPARE(m_window->output()->name(), outputs.at(0)->name()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testScreenForceTemporarily() +{ + const QList outputs = workspace()->outputs(); + + createTestWindow(); + + setWindowRule("screen", int(1), int(Rules::ForceTemporarily)); + + // The window should be forced the second screen + QCOMPARE(m_window->output()->name(), outputs.at(1)->name()); + + // User is not allowed to move it + m_window->sendToOutput(outputs.at(0)); + QCOMPARE(m_window->output()->name(), outputs.at(1)->name()); + + // Close and reopen the window. + destroyTestWindow(); + workspace()->setActiveOutput(outputs.at(0)); + createTestWindow(); + + // The rule should be discarded now + QCOMPARE(m_window->output()->name(), outputs.at(0)->name()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testMatchAfterNameChange() +{ + setWindowRule("above", true, int(Rules::Force)); + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(window->keepAbove(), false); + + QSignalSpy desktopFileNameSpy(window, &Window::desktopFileNameChanged); + + shellSurface->set_app_id(QStringLiteral("org.kde.foo")); + QVERIFY(desktopFileNameSpy.wait()); + QCOMPARE(window->keepAbove(), true); +} + +void TestXdgShellWindowRules::testNotEnabled() +{ + setWindowRule("above", true, int(Rules::Force)); + setWindowRule("Enabled", false, int(Rules::Unused)); + + createTestWindow(); + + // The rule should not be applied as it is not enabled + QCOMPARE(m_window->keepAbove(), false); + + // Now enable it and check again + setWindowRule("Enabled", true, int(Rules::Unused)); + QCOMPARE(m_window->keepAbove(), true); +} + +void TestXdgShellWindowRules::testLayerDontAffect() +{ + setWindowRule("layer", QStringLiteral("overlay"), int(Rules::DontAffect)); + + createTestWindow(); + + // The layer should not be affected by the rule. + QCOMPARE(m_window->layer(), NormalLayer); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testLayerForce() +{ + setWindowRule("layer", QStringLiteral("overlay"), int(Rules::Force)); + + createTestWindow(); + QCOMPARE(m_window->layer(), OverlayLayer); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testLayerForceTemporarily() +{ + setWindowRule("layer", QStringLiteral("overlay"), int(Rules::ForceTemporarily)); + + createTestWindow(); + QCOMPARE(m_window->layer(), OverlayLayer); + + // The rule should be discarded when the window is closed. + destroyTestWindow(); + createTestWindow(); + QCOMPARE(m_window->layer(), NormalLayer); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testCloseableDontAffect() +{ + setWindowRule("closeable", false, int(Rules::DontAffect)); + + createTestWindow(); + + QVERIFY(m_window->isCloseable()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testCloseableForce() +{ + setWindowRule("closeable", false, int(Rules::Force)); + + createTestWindow(); + QVERIFY(!m_window->isCloseable()); + + destroyTestWindow(); +} + +void TestXdgShellWindowRules::testCloseableForceTemporarily() +{ + setWindowRule("closeable", false, int(Rules::ForceTemporarily)); + + createTestWindow(); + QVERIFY(!m_window->isCloseable()); + + // The rule should be discarded when the window is closed. + destroyTestWindow(); + createTestWindow(); + QVERIFY(m_window->isCloseable()); + + destroyTestWindow(); +} + +WAYLANDTEST_MAIN(TestXdgShellWindowRules) +#include "xdgshellwindow_rules_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/xdgshellwindow_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/xdgshellwindow_test.cpp new file mode 100644 index 0000000000..6011bdeec1 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/xdgshellwindow_test.cpp @@ -0,0 +1,2808 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + SPDX-FileCopyrightText: 2019 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "core/outputconfiguration.h" +#include "decorations/decorationbridge.h" +#include "decorations/settings.h" +#include "pointer_input.h" +#include "virtualdesktops.h" +#include "wayland/clientconnection.h" +#include "wayland/display.h" +#include "wayland/surface.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +// system +#include +#include +#include + +#include + +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_xdgshellwindow-0"); + +class TestXdgShellWindow : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testMapUnmap(); + void testWindowOutputs(); + void testMinimizeActiveWindow(); + void testFullscreen_data(); + void testFullscreen(); + void testUserCanSetFullscreen(); + void testSendFullScreenWindowToAnotherOutput(); + + void testMaximizeHorizontal(); + void testMaximizeVertical(); + void testMaximizeFull(); + void testMaximizedToFullscreen_data(); + void testMaximizedToFullscreen(); + void testSendMaximizedWindowToAnotherOutput(); + void testInteractiveMoveUnmaximizeFull(); + void testInteractiveMoveUnmaximizeInitiallyFull(); + void testInteractiveMoveUnmaximizeHorizontal(); + void testInteractiveMoveUnmaximizeVertical(); + void testFullscreenMultipleOutputs(); + void testHidden(); + void testDesktopFileName(); + void testCaptionSimplified(); + void testUnresponsiveWindow_data(); + void testUnresponsiveWindow(); + void testAppMenu(); + void testSendClientWithTransientToDesktop(); + void testMinimizeWindowWithTransients(); + void testXdgDecoration_data(); + void testXdgDecoration(); + void testXdgNeverCommitted(); + void testXdgInitialState(); + void testXdgInitiallyMaximised(); + void testXdgInitiallyFullscreen(); + void testXdgInitiallyMinimized(); + void testXdgWindowGeometryIsntSet(); + void testXdgWindowGeometryAttachBuffer(); + void testXdgWindowGeometryAttachSubSurface(); + void testXdgWindowGeometryInteractiveResize(); + void testXdgWindowGeometryFullScreen(); + void testXdgWindowGeometryMaximize(); + void testXdgPopupReactive_data(); + void testXdgPopupReactive(); + void testXdgPopupReposition(); + void testXdgPopupRepositionBeforeInitialCommit(); + void testPointerInputTransform(); + void testReentrantSetFrameGeometry(); + void testDoubleMaximize(); + void testDoubleFullscreenSeparatedByCommit(); + void testMaximizeAndChangeDecorationModeAfterInitialCommit(); + void testFullScreenAndChangeDecorationModeAfterInitialCommit(); + void testChangeDecorationModeAfterInitialCommit(); + void testModal(); + void testCloseModal(); + void testCloseModalPreSetup(); + void testCloseInactiveModal(); + void testClosePopupOnParentUnmapped(); + void testPopupWithDismissedParent(); + void testPopupDismissedOnFocusChange(); + void testMinimumSize(); + void testNoMinimumSize(); + void testMaximumSize(); + void testNoMaximumSize(); + void testUnconfiguredBufferToplevel(); + void testUnconfiguredBufferPopup(); + void testRemoveActiveDesktopBeforeInitialCommit(); + void testRemoveActiveDesktopBeforeMap(); + void testRemoveActiveOutputBeforeInitialCommit(); + void testRemoveActiveOutputBeforeMap(); +}; + +void TestXdgShellWindow::testXdgPopupReactive_data() +{ + QTest::addColumn("reactive"); + QTest::addColumn("parentPos"); + QTest::addColumn("popupPos"); + + QTest::addRow("reactive") << true << QPointF(0, 1024) << QPointF(50, 1024 - 10); + QTest::addRow("not reactive") << false << QPointF(0, 1024) << QPointF(50, 1024 + 40); +} + +void TestXdgShellWindow::testXdgPopupReactive() +{ + // This test verifies the behavior of reactive popups. If a popup is not reactive, + // it only has to move together with its parent. If a popup is reactive, it moves + // with its parent and it's reconstrained as needed. + + std::unique_ptr positioner(Test::createXdgPositioner()); + positioner->set_size(10, 10); + positioner->set_anchor_rect(10, 10, 10, 10); + positioner->set_anchor_rect(0, 0, 50, 40); + positioner->set_anchor(Test::XdgPositioner::anchor_bottom_right); + positioner->set_gravity(Test::XdgPositioner::gravity_bottom_right); + positioner->set_constraint_adjustment(Test::XdgPositioner::constraint_adjustment_slide_y); + + QFETCH(bool, reactive); + if (reactive) { + positioner->set_reactive(); + } + + std::unique_ptr rootSurface(Test::createSurface()); + std::unique_ptr root(Test::createXdgToplevelSurface(rootSurface.get())); + auto rootWindow = Test::renderAndWaitForShown(rootSurface.get(), QSize(100, 100), Qt::cyan); + QVERIFY(rootWindow); + + std::unique_ptr childSurface(Test::createSurface()); + std::unique_ptr popup(Test::createXdgPopupSurface(childSurface.get(), root->xdgSurface(), positioner.get())); + auto childWindow = Test::renderAndWaitForShown(childSurface.get(), QSize(10, 10), Qt::cyan); + QVERIFY(childWindow); + + QFETCH(QPointF, parentPos); + QFETCH(QPointF, popupPos); + + QSignalSpy popupConfigureRequested(popup.get(), &Test::XdgPopup::configureRequested); + rootWindow->move(parentPos); + QVERIFY(popupConfigureRequested.wait()); + QCOMPARE(popupConfigureRequested.count(), 1); + + QCOMPARE(childWindow->pos(), popupPos); +} + +void TestXdgShellWindow::testXdgPopupReposition() +{ + std::unique_ptr positioner(Test::createXdgPositioner()); + positioner->set_size(10, 10); + positioner->set_anchor_rect(10, 10, 10, 10); + + std::unique_ptr otherPositioner(Test::createXdgPositioner()); + otherPositioner->set_size(50, 50); + otherPositioner->set_anchor_rect(10, 10, 10, 10); + + std::unique_ptr rootSurface(Test::createSurface()); + std::unique_ptr root(Test::createXdgToplevelSurface(rootSurface.get())); + auto rootWindow = Test::renderAndWaitForShown(rootSurface.get(), QSize(100, 100), Qt::cyan); + QVERIFY(rootWindow); + + std::unique_ptr childSurface(Test::createSurface()); + std::unique_ptr popup(Test::createXdgPopupSurface(childSurface.get(), root->xdgSurface(), positioner.get())); + auto childWindow = Test::renderAndWaitForShown(childSurface.get(), QSize(10, 10), Qt::cyan); + QVERIFY(childWindow); + + QSignalSpy reconfigureSpy(popup.get(), &Test::XdgPopup::configureRequested); + QSignalSpy repositionedSpy(popup.get(), &Test::XdgPopup::repositioned); + + popup->reposition(otherPositioner->object(), 500000); + + QVERIFY(reconfigureSpy.wait()); + QCOMPARE(reconfigureSpy.count(), 1); + QCOMPARE(repositionedSpy.count(), 1); + QCOMPARE(repositionedSpy.last().at(0).toUInt(), 500000); +} + +void TestXdgShellWindow::testXdgPopupRepositionBeforeInitialCommit() +{ + // This test verifies that reposition requests before the initial commit are handled in a reasonable + // way. How this case should be handled is left out of the xdg-shell spec. Also, with explicit sync, + // due to transaction fences, the reposition request may be reordered after the initial commit. + + std::unique_ptr positioner(Test::createXdgPositioner()); + positioner->set_size(10, 10); + positioner->set_anchor_rect(10, 10, 10, 10); + + std::unique_ptr otherPositioner(Test::createXdgPositioner()); + otherPositioner->set_size(50, 50); + otherPositioner->set_anchor_rect(10, 10, 10, 10); + + // Create the parent surface. + std::unique_ptr rootSurface(Test::createSurface()); + std::unique_ptr root(Test::createXdgToplevelSurface(rootSurface.get())); + auto rootWindow = Test::renderAndWaitForShown(rootSurface.get(), QSize(100, 100), Qt::cyan); + QVERIFY(rootWindow); + + // Create a popup surface. + std::unique_ptr childSurface(Test::createSurface()); + std::unique_ptr popup(Test::createXdgPopupSurface(childSurface.get(), root->xdgSurface(), positioner.get(), Test::CreationSetup::CreateOnly)); + popup->reposition(otherPositioner->object(), 666); + + QSignalSpy popupConfigureRequestedSpy(popup.get(), &Test::XdgPopup::configureRequested); + QSignalSpy popupRepositionedSpy(popup.get(), &Test::XdgPopup::repositioned); + QSignalSpy surfaceConfigureRequestedSpy(popup->xdgSurface(), &Test::XdgSurface::configureRequested); + childSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(popupRepositionedSpy.count(), 1); + QCOMPARE(popupRepositionedSpy.last().at(0).toUInt(), 666); + + popup->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).toUInt()); + auto childWindow = Test::renderAndWaitForShown(childSurface.get(), QSize(10, 10), Qt::cyan); + QVERIFY(childWindow); +} + +void TestXdgShellWindow::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->start(); +} + +void TestXdgShellWindow::init() +{ + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); + + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::XdgDecorationV1 | Test::AdditionalWaylandInterface::AppMenu | Test::AdditionalWaylandInterface::XdgDialogV1)); + QVERIFY(Test::waitForWaylandPointer()); + + workspace()->setActiveOutput(QPoint(640, 512)); + // put mouse in the middle of screen one + KWin::input()->pointer()->warp(QPoint(640, 512)); +} + +void TestXdgShellWindow::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TestXdgShellWindow::testMapUnmap() +{ + // This test verifies that the compositor destroys XdgToplevelWindow when the + // associated xdg_toplevel surface is unmapped. + + // Create a wl_surface and an xdg_toplevel, but don't commit them yet! + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + + QSignalSpy configureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + // Tell the compositor that we want to map the surface. + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // The compositor will respond with a configure event. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + + // Now we can attach a buffer with actual data to the surface. + Test::render(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(windowAddedSpy.wait()); + QCOMPARE(windowAddedSpy.count(), 1); + Window *window = windowAddedSpy.last().first().value(); + QVERIFY(window); + QCOMPARE(window->readyForPainting(), true); + + // When the window becomes active, the compositor will send another configure event. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + + // Unmap the xdg_toplevel surface by committing a null buffer. + surface->attachBuffer(KWayland::Client::Buffer::Ptr()); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(Test::waitForWindowClosed(window)); + + // Tell the compositor that we want to re-map the xdg_toplevel surface. + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // The compositor will respond with a configure event. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + + // Now we can attach a buffer with actual data to the surface. + Test::render(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(windowAddedSpy.wait()); + QCOMPARE(windowAddedSpy.count(), 2); + window = windowAddedSpy.last().first().value(); + QVERIFY(window); + QCOMPARE(window->readyForPainting(), true); + + // The compositor will respond with a configure event. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 4); + + // Destroy the test window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void TestXdgShellWindow::testWindowOutputs() +{ + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto size = QSize(200, 200); + + QSignalSpy outputEnteredSpy(surface.get(), &KWayland::Client::Surface::outputEntered); + QSignalSpy outputLeftSpy(surface.get(), &KWayland::Client::Surface::outputLeft); + + auto window = Test::renderAndWaitForShown(surface.get(), size, Qt::blue); + // assumption: window is initially placed on first screen + QVERIFY(outputEnteredSpy.wait()); + QCOMPARE(outputEnteredSpy.count(), 1); + QCOMPARE(surface->outputs().count(), 1); + QCOMPARE(surface->outputs().first()->globalPosition(), QPoint(0, 0)); + + // move to overlapping both first and second screen + window->moveResize(RectF(QPoint(1250, 100), size)); + QVERIFY(outputEnteredSpy.wait()); + QCOMPARE(outputEnteredSpy.count(), 2); + QCOMPARE(outputLeftSpy.count(), 0); + QCOMPARE(surface->outputs().count(), 2); + QVERIFY(surface->outputs()[0] != surface->outputs()[1]); + + // move entirely into second screen + window->moveResize(RectF(QPoint(1400, 100), size)); + QVERIFY(outputLeftSpy.wait()); + QCOMPARE(outputEnteredSpy.count(), 2); + QCOMPARE(outputLeftSpy.count(), 1); + QCOMPARE(surface->outputs().count(), 1); + QCOMPARE(surface->outputs().first()->globalPosition(), QPoint(1280, 0)); +} + +void TestXdgShellWindow::testMinimizeActiveWindow() +{ + // this test verifies that when minimizing the active window it gets deactivated + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(workspace()->activeWindow(), window); + QVERIFY(window->wantsInput()); + QVERIFY(window->wantsTabFocus()); + QVERIFY(window->isShown()); + + workspace()->slotWindowMinimize(); + QVERIFY(!window->isShown()); + QVERIFY(window->wantsInput()); + QVERIFY(window->wantsTabFocus()); + QVERIFY(!window->isActive()); + QVERIFY(!workspace()->activeWindow()); + QVERIFY(window->isMinimized()); + + // unminimize again + window->setMinimized(false); + QVERIFY(!window->isMinimized()); + QVERIFY(!window->isActive()); + QVERIFY(window->wantsInput()); + QVERIFY(window->wantsTabFocus()); + QVERIFY(window->isShown()); + QCOMPARE(workspace()->activeWindow(), nullptr); +} + +void TestXdgShellWindow::testFullscreen_data() +{ + QTest::addColumn("decoMode"); + + QTest::newRow("client-side deco") << Test::XdgToplevelDecorationV1::mode_client_side; + QTest::newRow("server-side deco") << Test::XdgToplevelDecorationV1::mode_server_side; +} + +void TestXdgShellWindow::testFullscreen() +{ + // this test verifies that a window can be properly fullscreened + + Test::XdgToplevel::States states; + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + std::unique_ptr decoration(Test::createXdgToplevelDecorationV1(shellSurface.get())); + QSignalSpy decorationConfigureRequestedSpy(decoration.get(), &Test::XdgToplevelDecorationV1::configureRequested); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + // Initialize the xdg-toplevel surface. + QFETCH(Test::XdgToplevelDecorationV1::mode, decoMode); + decoration->set_mode(decoMode); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(500, 250), Qt::blue); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(window->layer(), NormalLayer); + QVERIFY(!window->isFullScreen()); + QCOMPARE(window->clientSize(), QSize(500, 250)); + QCOMPARE(window->isDecorated(), decoMode == Test::XdgToplevelDecorationV1::mode_server_side); + QCOMPARE(window->clientSizeToFrameSize(window->clientSize()), window->size()); + + QSignalSpy fullScreenChangedSpy(window, &Window::fullScreenChanged); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + + // Wait for the compositor to send a configure event with the Activated state. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states & Test::XdgToplevel::State::Activated); + + // Ask the compositor to show the window in full screen mode. + shellSurface->set_fullscreen(nullptr); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states & Test::XdgToplevel::State::Fullscreen); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), window->output()->geometry().size()); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::red); + + QVERIFY(fullScreenChangedSpy.wait()); + QCOMPARE(fullScreenChangedSpy.count(), 1); + QVERIFY(window->isFullScreen()); + QVERIFY(!window->isDecorated()); + QCOMPARE(window->layer(), ActiveLayer); + QCOMPARE(window->frameGeometry(), RectF(QPoint(0, 0), window->output()->geometry().size())); + + // Ask the compositor to show the window in normal mode. + shellSurface->unset_fullscreen(); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 4); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(!(states & Test::XdgToplevel::State::Fullscreen)); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(500, 250)); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::blue); + + QVERIFY(fullScreenChangedSpy.wait()); + QCOMPARE(fullScreenChangedSpy.count(), 2); + QCOMPARE(window->clientSize(), QSize(500, 250)); + QVERIFY(!window->isFullScreen()); + QCOMPARE(window->isDecorated(), decoMode == Test::XdgToplevelDecorationV1::mode_server_side); + QCOMPARE(window->layer(), NormalLayer); + + // Destroy the window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void TestXdgShellWindow::testUserCanSetFullscreen() +{ + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(window->isActive()); + QVERIFY(!window->isFullScreen()); + QVERIFY(window->isFullScreenable()); +} + +void TestXdgShellWindow::testSendFullScreenWindowToAnotherOutput() +{ + // This test verifies that the fullscreen window will have correct geometry restore + // after it's sent to another output. + + const auto outputs = workspace()->outputs(); + + // Create the window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // Wait for the compositor to send a configure event with the activated state. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + // Move the window to the left monitor. + window->move(QPointF(10, 20)); + QCOMPARE(window->frameGeometry(), RectF(10, 20, 100, 50)); + QCOMPARE(window->output(), outputs[0]); + + // Make the window fullscreen. + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + shellSurface->set_fullscreen(nullptr); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->isFullScreen(), true); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); + QCOMPARE(window->fullscreenGeometryRestore(), RectF(10, 20, 100, 50)); + QCOMPARE(window->output(), outputs[0]); + + // Send the window to another output. + window->sendToOutput(outputs[1]); + QCOMPARE(window->isFullScreen(), true); + QCOMPARE(window->frameGeometry(), RectF(1280, 0, 1280, 1024)); + QCOMPARE(window->fullscreenGeometryRestore(), RectF(1280 + 10, 20, 100, 50)); + QCOMPARE(window->output(), outputs[1]); +} + +void TestXdgShellWindow::testMaximizedToFullscreen_data() +{ + QTest::addColumn("decoMode"); + + QTest::newRow("client-side deco") << Test::XdgToplevelDecorationV1::mode_client_side; + QTest::newRow("server-side deco") << Test::XdgToplevelDecorationV1::mode_server_side; +} + +void TestXdgShellWindow::testMaximizedToFullscreen() +{ + // this test verifies that a window can be properly fullscreened after maximizing + + Test::XdgToplevel::States states; + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + std::unique_ptr decoration(Test::createXdgToplevelDecorationV1(shellSurface.get())); + QSignalSpy decorationConfigureRequestedSpy(decoration.get(), &Test::XdgToplevelDecorationV1::configureRequested); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + // Initialize the xdg-toplevel surface. + QFETCH(Test::XdgToplevelDecorationV1::mode, decoMode); + decoration->set_mode(decoMode); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(window->isActive()); + QVERIFY(!window->isFullScreen()); + QCOMPARE(window->clientSize(), QSize(100, 50)); + QCOMPARE(window->isDecorated(), decoMode == Test::XdgToplevelDecorationV1::mode_server_side); + + QSignalSpy fullscreenChangedSpy(window, &Window::fullScreenChanged); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + + // Wait for the compositor to send a configure event with the Activated state. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states & Test::XdgToplevel::State::Activated); + + // Ask the compositor to maximize the window. + shellSurface->set_maximized(); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states & Test::XdgToplevel::State::Maximized); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->maximizeMode(), MaximizeFull); + + // Ask the compositor to show the window in full screen mode. + shellSurface->set_fullscreen(nullptr); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 4); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), window->output()->geometry().size()); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states & Test::XdgToplevel::State::Maximized); + QVERIFY(states & Test::XdgToplevel::State::Fullscreen); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::red); + + QVERIFY(fullscreenChangedSpy.wait()); + QCOMPARE(fullscreenChangedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QVERIFY(window->isFullScreen()); + QVERIFY(!window->isDecorated()); + + // Switch back to normal mode. + shellSurface->unset_fullscreen(); + shellSurface->unset_maximized(); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 5); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(100, 50)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(!(states & Test::XdgToplevel::State::Maximized)); + QVERIFY(!(states & Test::XdgToplevel::State::Fullscreen)); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::red); + + QVERIFY(frameGeometryChangedSpy.wait()); + QVERIFY(!window->isFullScreen()); + QCOMPARE(window->isDecorated(), decoMode == Test::XdgToplevelDecorationV1::mode_server_side); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + + // Destroy the window. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void TestXdgShellWindow::testFullscreenMultipleOutputs() +{ + // this test verifies that kwin will place fullscreen windows in the outputs its instructed to + + const auto outputs = workspace()->outputs(); + for (KWin::LogicalOutput *output : outputs) { + Test::XdgToplevel::States states; + + std::unique_ptr surface = Test::createSurface(); + QVERIFY(surface); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface); + + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(window->isActive()); + QVERIFY(!window->isFullScreen()); + QCOMPARE(window->clientSize(), QSize(100, 50)); + QVERIFY(!window->isDecorated()); + + QSignalSpy fullscreenChangedSpy(window, &Window::fullScreenChanged); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + // Wait for the compositor to send a configure event with the Activated state. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + + // Ask the compositor to show the window in full screen mode. + shellSurface->set_fullscreen(*Test::waylandOutput(output->name())); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), output->geometry().size()); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::red); + + QVERIFY(!fullscreenChangedSpy.isEmpty() || fullscreenChangedSpy.wait()); + QCOMPARE(fullscreenChangedSpy.count(), 1); + + QVERIFY(!frameGeometryChangedSpy.isEmpty() || frameGeometryChangedSpy.wait()); + + QVERIFY(window->isFullScreen()); + + QCOMPARE(window->frameGeometry(), RectF(output->geometry())); + } +} + +void TestXdgShellWindow::testHidden() +{ + // this test verifies that when hiding window it doesn't get shown + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(workspace()->activeWindow(), window); + QVERIFY(window->wantsInput()); + QVERIFY(window->wantsTabFocus()); + QVERIFY(window->isShown()); + + window->setHidden(true); + QVERIFY(!window->isShown()); + QVERIFY(!window->isActive()); + QVERIFY(window->wantsInput()); + QVERIFY(window->wantsTabFocus()); + + // unhide again + window->setHidden(false); + QVERIFY(window->isShown()); + QVERIFY(window->wantsInput()); + QVERIFY(window->wantsTabFocus()); + + // QCOMPARE(workspace()->activeClient(), c); +} + +void TestXdgShellWindow::testDesktopFileName() +{ + QIcon::setThemeName(QStringLiteral("breeze")); + // this test verifies that desktop file name is passed correctly to the window + std::unique_ptr surface(Test::createSurface()); + // only xdg-shell as ShellSurface misses the setter + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // A client that never call set_app_id still gets the default icon + QCOMPARE(window->desktopFileName(), QString()); + QVERIFY(window->resourceClass().startsWith("testXdgShellWindow")); + QVERIFY(window->resourceName().startsWith("testXdgShellWindow")); + QCOMPARE(window->icon().name(), QStringLiteral("wayland")); + + QSignalSpy desktopFileNameChangedSpy(window, &Window::desktopFileNameChanged); + + shellSurface->set_app_id(QStringLiteral("org.kde.foo")); + QVERIFY(desktopFileNameChangedSpy.wait()); + QCOMPARE(window->desktopFileName(), QStringLiteral("org.kde.foo")); + QCOMPARE(window->resourceClass(), QStringLiteral("org.kde.foo")); + QVERIFY(window->resourceName().startsWith("testXdgShellWindow")); + // the desktop file does not exist, so icon should be generic Wayland + QCOMPARE(window->icon().name(), QStringLiteral("wayland")); + + QSignalSpy iconChangedSpy(window, &Window::iconChanged); + shellSurface->set_app_id(QStringLiteral("org.kde.bar")); + QVERIFY(desktopFileNameChangedSpy.wait()); + QCOMPARE(window->desktopFileName(), QStringLiteral("org.kde.bar")); + QCOMPARE(window->resourceClass(), QStringLiteral("org.kde.bar")); + QVERIFY(window->resourceName().startsWith("testXdgShellWindow")); + // icon should still be wayland + QCOMPARE(window->icon().name(), QStringLiteral("wayland")); + QVERIFY(iconChangedSpy.isEmpty()); + + const QString dfPath = QFINDTESTDATA("data/example.desktop"); + shellSurface->set_app_id(dfPath.toUtf8()); + QVERIFY(desktopFileNameChangedSpy.wait()); + QCOMPARE(iconChangedSpy.count(), 1); + QCOMPARE(window->desktopFileName(), dfPath); + QCOMPARE(window->icon().name(), QStringLiteral("kwin")); +} + +void TestXdgShellWindow::testCaptionSimplified() +{ + // this test verifies that caption is properly trimmed + // see BUG 323798 comment #12 + std::unique_ptr surface(Test::createSurface()); + // only done for xdg-shell as ShellSurface misses the setter + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + const QString origTitle = QString::fromUtf8(QByteArrayLiteral("Was tun, wenn Schüler Autismus haben?\342\200\250\342\200\250\342\200\250 – Marlies Hübner - Mozilla Firefox")); + shellSurface->set_title(origTitle); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(window->caption() != origTitle); + QCOMPARE(window->caption(), origTitle.simplified()); +} + +void TestXdgShellWindow::testUnresponsiveWindow_data() +{ + QTest::addColumn("shellInterface"); // see env selection in qwaylandintegration.cpp + QTest::addColumn("socketMode"); + + QTest::newRow("xdg display") << "xdg-shell" << false; + QTest::newRow("xdg socket") << "xdg-shell" << true; +} + +void TestXdgShellWindow::testUnresponsiveWindow() +{ + // this test verifies that killWindow properly terminates a process + // for this an external binary is launched + const QString kill = QFINDTESTDATA(QStringLiteral("kill")); + QVERIFY(!kill.isEmpty()); + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + + std::unique_ptr process(new QProcess); + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + + QFETCH(QString, shellInterface); + QFETCH(bool, socketMode); + env.insert("QT_WAYLAND_SHELL_INTEGRATION", shellInterface); + if (socketMode) { + int sx[2]; + QVERIFY(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sx) >= 0); + waylandServer()->display()->createClient(sx[0]); + int socket = dup(sx[1]); + QVERIFY(socket != -1); + env.insert(QStringLiteral("WAYLAND_SOCKET"), QByteArray::number(socket)); + env.remove("WAYLAND_DISPLAY"); + } else { + env.insert("WAYLAND_DISPLAY", s_socketName); + } + process->setProcessEnvironment(env); + process->setProcessChannelMode(QProcess::ForwardedChannels); + process->setProgram(kill); + QSignalSpy processStartedSpy{process.get(), &QProcess::started}; + process->start(); + QVERIFY(processStartedSpy.wait()); + + Window *killWindow = nullptr; + if (windowAddedSpy.isEmpty()) { + QVERIFY(windowAddedSpy.wait()); + } + ::kill(process->processId(), SIGUSR1); // send a signal to freeze the process + + killWindow = windowAddedSpy.first().first().value(); + QVERIFY(killWindow); + QSignalSpy unresponsiveSpy(killWindow, &Window::unresponsiveChanged); + QSignalSpy killedSpy(process.get(), static_cast(&QProcess::finished)); + QSignalSpy deletedSpy(killWindow, &QObject::destroyed); + + qint64 startTime = QDateTime::currentMSecsSinceEpoch(); + + // wait for the process to be frozen + QTest::qWait(10); + + // pretend the user clicked the close button + killWindow->closeWindow(); + + // window should not yet be marked unresponsive nor killed + QVERIFY(!killWindow->unresponsive()); + QVERIFY(killedSpy.isEmpty()); + + QVERIFY(unresponsiveSpy.wait()); + // window should be marked unresponsive but not killed + auto elapsed1 = QDateTime::currentMSecsSinceEpoch() - startTime; + const int timeout = options->killPingTimeout() / 2; // first timeout at half the time is for "unresponsive". + QVERIFY(elapsed1 > timeout - 200 && elapsed1 < timeout + 200); // coarse timers on a test across two processes means we need a fuzzy compare + QVERIFY(killWindow->unresponsive()); + QVERIFY(killedSpy.isEmpty()); + + // TODO verify that kill prompt works. + killWindow->killWindow(); + process->kill(); + + QVERIFY(killedSpy.wait()); + + if (deletedSpy.isEmpty()) { + QVERIFY(deletedSpy.wait()); + } +} + +void TestXdgShellWindow::testAppMenu() +{ + // register a faux appmenu client + QVERIFY(QDBusConnection::sessionBus().registerService("org.kde.kappmenu")); + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + std::unique_ptr menu(Test::waylandAppMenuManager()->create(surface.get())); + QSignalSpy spy(window, &Window::hasApplicationMenuChanged); + menu->setAddress("service.name", "object/path"); + spy.wait(); + QCOMPARE(window->hasApplicationMenu(), true); + QCOMPARE(window->applicationMenuServiceName(), QString("service.name")); + QCOMPARE(window->applicationMenuObjectPath(), QString("object/path")); + + QVERIFY(QDBusConnection::sessionBus().unregisterService("org.kde.kappmenu")); +} + +void TestXdgShellWindow::testSendClientWithTransientToDesktop() +{ + // this test verifies that when sending a window to a desktop all transients are also send to that desktop + + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + vds->setCount(2); + const QList desktops = vds->desktops(); + + std::unique_ptr surface{Test::createSurface()}; + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // let's create a transient window + std::unique_ptr transientSurface{Test::createSurface()}; + std::unique_ptr transientShellSurface(Test::createXdgToplevelSurface(transientSurface.get())); + transientShellSurface->set_parent(shellSurface->object()); + + auto transient = Test::renderAndWaitForShown(transientSurface.get(), QSize(100, 50), Qt::blue); + QVERIFY(transient); + QCOMPARE(workspace()->activeWindow(), transient); + QCOMPARE(transient->transientFor(), window); + QVERIFY(window->transients().contains(transient)); + + // initially, the parent and the transient are on the first virtual desktop + QCOMPARE(window->desktops(), QList{desktops[0]}); + QVERIFY(!window->isOnAllDesktops()); + QCOMPARE(transient->desktops(), QList{desktops[0]}); + QVERIFY(!transient->isOnAllDesktops()); + + // send the transient to the second virtual desktop + workspace()->slotWindowToDesktop(desktops[1]); + QCOMPARE(window->desktops(), QList{desktops[0]}); + QCOMPARE(transient->desktops(), QList{desktops[1]}); + + // activate c + workspace()->activateWindow(window); + QCOMPARE(workspace()->activeWindow(), window); + QVERIFY(window->isActive()); + + // and send it to the desktop it's already on + QCOMPARE(window->desktops(), QList{desktops[0]}); + QCOMPARE(transient->desktops(), QList{desktops[1]}); + workspace()->slotWindowToDesktop(desktops[0]); + + // which should move the transient back to the desktop + QCOMPARE(window->desktops(), QList{desktops[0]}); + QCOMPARE(transient->desktops(), QList{desktops[0]}); +} + +void TestXdgShellWindow::testMinimizeWindowWithTransients() +{ + // this test verifies that when minimizing/unminimizing a window all its + // transients will be minimized/unminimized as well + + // create the main window + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(!window->isMinimized()); + + // create a transient window + std::unique_ptr transientSurface(Test::createSurface()); + std::unique_ptr transientShellSurface(Test::createXdgToplevelSurface(transientSurface.get())); + transientShellSurface->set_parent(shellSurface->object()); + auto transient = Test::renderAndWaitForShown(transientSurface.get(), QSize(100, 50), Qt::red); + QVERIFY(transient); + QVERIFY(!transient->isMinimized()); + QCOMPARE(transient->transientFor(), window); + QVERIFY(window->hasTransient(transient, false)); + + // minimize the main window, the transient should be minimized as well + window->setMinimized(true); + QVERIFY(window->isMinimized()); + QVERIFY(transient->isMinimized()); + + // unminimize the main window, the transient should be unminimized as well + window->setMinimized(false); + QVERIFY(!window->isMinimized()); + QVERIFY(!transient->isMinimized()); +} + +void TestXdgShellWindow::testXdgDecoration_data() +{ + QTest::addColumn("requestedMode"); + QTest::addColumn("expectedMode"); + + QTest::newRow("client side requested") << Test::XdgToplevelDecorationV1::mode_client_side << Test::XdgToplevelDecorationV1::mode_client_side; + QTest::newRow("server side requested") << Test::XdgToplevelDecorationV1::mode_server_side << Test::XdgToplevelDecorationV1::mode_server_side; +} + +void TestXdgShellWindow::testXdgDecoration() +{ + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + std::unique_ptr deco(Test::createXdgToplevelDecorationV1(shellSurface.get())); + + QSignalSpy decorationConfigureRequestedSpy(deco.get(), &Test::XdgToplevelDecorationV1::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + QFETCH(Test::XdgToplevelDecorationV1::mode, requestedMode); + QFETCH(Test::XdgToplevelDecorationV1::mode, expectedMode); + + // request a mode + deco->set_mode(requestedMode); + + // kwin will send a configure + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + QCOMPARE(decorationConfigureRequestedSpy.count(), 1); + QCOMPARE(decorationConfigureRequestedSpy.last()[0].value(), expectedMode); + QVERIFY(decorationConfigureRequestedSpy.count() > 0); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last()[0].toInt()); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QCOMPARE(window->isDecorated(), expectedMode == Test::XdgToplevelDecorationV1::mode_server_side); +} + +void TestXdgShellWindow::testXdgNeverCommitted() +{ + // check we don't crash if we create a shell object but delete the XdgShellClient before committing it + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); +} + +void TestXdgShellWindow::testXdgInitialState() +{ + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + surfaceConfigureRequestedSpy.wait(); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + const auto size = toplevelConfigureRequestedSpy.first()[0].value(); + + QCOMPARE(size, QSize(0, 0)); // window should chose it's preferred size + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.first()[0].toUInt()); + + auto window = Test::renderAndWaitForShown(surface.get(), QSize(200, 100), Qt::blue); + QCOMPARE(window->size(), QSize(200, 100)); +} + +void TestXdgShellWindow::testXdgInitiallyMaximised() +{ + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + shellSurface->set_maximized(); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + surfaceConfigureRequestedSpy.wait(); + + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + const auto size = toplevelConfigureRequestedSpy.first()[0].value(); + const auto state = toplevelConfigureRequestedSpy.first()[1].value(); + + QCOMPARE(size, QSize(1280, 1024)); + QVERIFY(state & Test::XdgToplevel::State::Maximized); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.first()[0].toUInt()); + + auto window = Test::renderAndWaitForShown(surface.get(), size, Qt::blue); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->size(), QSize(1280, 1024)); +} + +void TestXdgShellWindow::testXdgInitiallyFullscreen() +{ + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + shellSurface->set_fullscreen(nullptr); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + surfaceConfigureRequestedSpy.wait(); + + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + const auto size = toplevelConfigureRequestedSpy.first()[0].value(); + const auto state = toplevelConfigureRequestedSpy.first()[1].value(); + + QCOMPARE(size, QSize(1280, 1024)); + QVERIFY(state & Test::XdgToplevel::State::Fullscreen); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.first()[0].toUInt()); + + auto window = Test::renderAndWaitForShown(surface.get(), size, Qt::blue); + QCOMPARE(window->isFullScreen(), true); + QCOMPARE(window->size(), QSize(1280, 1024)); +} + +void TestXdgShellWindow::testXdgInitiallyMinimized() +{ + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + shellSurface->set_minimized(); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + surfaceConfigureRequestedSpy.wait(); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + const auto size = toplevelConfigureRequestedSpy.first()[0].value(); + const auto state = toplevelConfigureRequestedSpy.first()[1].value(); + + QCOMPARE(size, QSize(0, 0)); + QCOMPARE(state, 0); + + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.first()[0].toUInt()); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + QVERIFY(window->isMinimized()); +} + +void TestXdgShellWindow::testXdgWindowGeometryIsntSet() +{ + // This test verifies that the effective window geometry corresponds to the + // bounding rectangle of the main surface and its sub-surfaces if no window + // geometry is set by the window. + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(200, 100), Qt::red); + QVERIFY(window); + QCOMPARE(window->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(window->frameGeometry().size(), QSize(200, 100)); + + const QPointF oldPosition = window->pos(); + + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + Test::render(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry().topLeft(), oldPosition); + QCOMPARE(window->frameGeometry().size(), QSize(100, 50)); + QCOMPARE(window->bufferGeometry().topLeft(), oldPosition); + QCOMPARE(window->bufferGeometry().size(), QSize(100, 50)); + + std::unique_ptr childSurface(Test::createSurface()); + std::unique_ptr subSurface(Test::createSubSurface(childSurface.get(), surface.get())); + QVERIFY(subSurface); + subSurface->setPosition(QPoint(-20, -10)); + Test::render(childSurface.get(), QSize(100, 50), Qt::blue); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry().topLeft(), oldPosition); + QCOMPARE(window->frameGeometry().size(), QSize(120, 60)); + QCOMPARE(window->bufferGeometry().topLeft(), oldPosition + QPoint(20, 10)); + QCOMPARE(window->bufferGeometry().size(), QSize(100, 50)); +} + +void TestXdgShellWindow::testXdgWindowGeometryAttachBuffer() +{ + // This test verifies that the effective window geometry remains the same when + // a new buffer is attached and xdg_surface.set_window_geometry is not called + // again. Notice that the window geometry must remain the same even if the new + // buffer is smaller. + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(200, 100), Qt::red); + QVERIFY(window); + QCOMPARE(window->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(window->frameGeometry().size(), QSize(200, 100)); + + const QPointF oldPosition = window->pos(); + + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + shellSurface->xdgSurface()->set_window_geometry(10, 10, 180, 80); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 1); + QCOMPARE(window->frameGeometry().topLeft(), oldPosition); + QCOMPARE(window->frameGeometry().size(), QSize(180, 80)); + QCOMPARE(window->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); + QCOMPARE(window->bufferGeometry().size(), QSize(200, 100)); + + Test::render(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 2); + QCOMPARE(window->frameGeometry().topLeft(), oldPosition); + QCOMPARE(window->frameGeometry().size(), QSize(90, 40)); + QCOMPARE(window->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); + QCOMPARE(window->bufferGeometry().size(), QSize(100, 50)); + + shellSurface->xdgSurface()->set_window_geometry(0, 0, 100, 50); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 3); + QCOMPARE(window->frameGeometry().topLeft(), oldPosition); + QCOMPARE(window->frameGeometry().size(), QSize(100, 50)); + QCOMPARE(window->bufferGeometry().topLeft(), oldPosition); + QCOMPARE(window->bufferGeometry().size(), QSize(100, 50)); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void TestXdgShellWindow::testXdgWindowGeometryAttachSubSurface() +{ + // This test verifies that the effective window geometry remains the same + // when a new sub-surface is added and xdg_surface.set_window_geometry is + // not called again. + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(200, 100), Qt::red); + QVERIFY(window); + QCOMPARE(window->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(window->frameGeometry().size(), QSize(200, 100)); + + const QPointF oldPosition = window->pos(); + + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + shellSurface->xdgSurface()->set_window_geometry(10, 10, 180, 80); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry().topLeft(), oldPosition); + QCOMPARE(window->frameGeometry().size(), QSize(180, 80)); + QCOMPARE(window->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); + QCOMPARE(window->bufferGeometry().size(), QSize(200, 100)); + + std::unique_ptr childSurface(Test::createSurface()); + std::unique_ptr subSurface(Test::createSubSurface(childSurface.get(), surface.get())); + QVERIFY(subSurface); + subSurface->setPosition(QPoint(-20, -20)); + Test::render(childSurface.get(), QSize(100, 50), Qt::blue); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QCOMPARE(window->frameGeometry().topLeft(), oldPosition); + QCOMPARE(window->frameGeometry().size(), QSize(180, 80)); + QCOMPARE(window->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); + QCOMPARE(window->bufferGeometry().size(), QSize(200, 100)); + + shellSurface->xdgSurface()->set_window_geometry(-15, -15, 50, 40); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->frameGeometry().topLeft(), oldPosition); + QCOMPARE(window->frameGeometry().size(), QSize(50, 40)); + QCOMPARE(window->bufferGeometry().topLeft(), oldPosition - QPoint(-15, -15)); + QCOMPARE(window->bufferGeometry().size(), QSize(200, 100)); +} + +void TestXdgShellWindow::testXdgWindowGeometryInteractiveResize() +{ + // This test verifies that correct window geometry is provided along each + // configure event when an xdg-shell is being interactively resized. + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(200, 100), Qt::red); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(window->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(window->frameGeometry().size(), QSize(200, 100)); + + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + shellSurface->xdgSurface()->set_window_geometry(10, 10, 180, 80); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(window->frameGeometry().size(), QSize(180, 80)); + + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + + // Start interactively resizing the window. + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->moveResizeWindow(), window); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + Test::XdgToplevel::States states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing)); + + // Go right. + QPointF cursorPos = KWin::Cursors::self()->mouse()->pos(); + window->keyPressEvent(Qt::Key_Right); + window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing)); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(188, 80)); + shellSurface->xdgSurface()->set_window_geometry(10, 10, 188, 80); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(208, 100), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->bufferGeometry().size(), QSize(208, 100)); + QCOMPARE(window->frameGeometry().size(), QSize(188, 80)); + + // Go down. + cursorPos = KWin::Cursors::self()->mouse()->pos(); + window->keyPressEvent(Qt::Key_Down); + window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers()); + QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(0, 8)); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 4); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing)); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(188, 88)); + shellSurface->xdgSurface()->set_window_geometry(10, 10, 188, 88); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(208, 108), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 2); + QCOMPARE(window->bufferGeometry().size(), QSize(208, 108)); + QCOMPARE(window->frameGeometry().size(), QSize(188, 88)); + + // Finish resizing the window. + window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); + QCOMPARE(workspace()->moveResizeWindow(), nullptr); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 5); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Resizing)); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void TestXdgShellWindow::testXdgWindowGeometryFullScreen() +{ + // This test verifies that an xdg-shell receives correct window geometry when + // its fullscreen state gets changed. + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(200, 100), Qt::red); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(window->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(window->frameGeometry().size(), QSize(200, 100)); + + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + shellSurface->xdgSurface()->set_window_geometry(10, 10, 180, 80); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(window->frameGeometry().size(), QSize(180, 80)); + + workspace()->slotWindowFullScreen(); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); + Test::XdgToplevel::States states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Fullscreen)); + shellSurface->xdgSurface()->set_window_geometry(0, 0, 1280, 1024); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(1280, 1024), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->bufferGeometry().size(), QSize(1280, 1024)); + QCOMPARE(window->frameGeometry().size(), QSize(1280, 1024)); + + workspace()->slotWindowFullScreen(); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(180, 80)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Fullscreen)); + shellSurface->xdgSurface()->set_window_geometry(10, 10, 180, 80); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(200, 100), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(window->frameGeometry().size(), QSize(180, 80)); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void TestXdgShellWindow::testXdgWindowGeometryMaximize() +{ + // This test verifies that an xdg-shell receives correct window geometry when + // its maximized state gets changed. + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(200, 100), Qt::red); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(window->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(window->frameGeometry().size(), QSize(200, 100)); + + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + shellSurface->xdgSurface()->set_window_geometry(10, 10, 180, 80); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(window->frameGeometry().size(), QSize(180, 80)); + + workspace()->slotWindowMaximize(); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); + Test::XdgToplevel::States states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + shellSurface->xdgSurface()->set_window_geometry(0, 0, 1280, 1024); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(1280, 1024), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->bufferGeometry().size(), QSize(1280, 1024)); + QCOMPARE(window->frameGeometry().size(), QSize(1280, 1024)); + + workspace()->slotWindowMaximize(); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(180, 80)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + shellSurface->xdgSurface()->set_window_geometry(10, 10, 180, 80); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(200, 100), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(window->frameGeometry().size(), QSize(180, 80)); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void TestXdgShellWindow::testPointerInputTransform() +{ + // This test verifies that XdgToplevelWindow provides correct input transform matrix. + // The input transform matrix is used by seat to map pointer events from the global + // screen coordinates to the surface-local coordinates. + + // Get a wl_pointer object on the client side. + std::unique_ptr pointer(Test::waylandSeat()->createPointer()); + QVERIFY(pointer); + QVERIFY(pointer->isValid()); + QSignalSpy pointerEnteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); + QSignalSpy pointerMotionSpy(pointer.get(), &KWayland::Client::Pointer::motion); + + // Create an xdg_toplevel surface and wait for the compositor to catch up. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(200, 100), Qt::red); + QVERIFY(window); + QVERIFY(window->isActive()); + QCOMPARE(window->bufferGeometry().size(), QSize(200, 100)); + QCOMPARE(window->frameGeometry().size(), QSize(200, 100)); + + // Enter the surface. + quint32 timestamp = 0; + Test::pointerMotion(window->pos(), timestamp++); + QVERIFY(pointerEnteredSpy.wait()); + + // Move the pointer to (10, 5) relative to the upper left frame corner, which is located + // at (0, 0) in the surface-local coordinates. + Test::pointerMotion(window->pos() + QPointF(10, 5), timestamp++); + QVERIFY(pointerMotionSpy.wait()); + QCOMPARE(pointerMotionSpy.last().first().toPointF(), QPointF(10, 5)); + + // Let's pretend that the window has changed the extents of the client-side drop-shadow + // but the frame geometry didn't change. + QSignalSpy bufferGeometryChangedSpy(window, &Window::bufferGeometryChanged); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + shellSurface->xdgSurface()->set_window_geometry(10, 20, 200, 100); + Test::render(surface.get(), QSize(220, 140), Qt::blue); + QVERIFY(bufferGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 0); + QCOMPARE(window->frameGeometry().size(), QSize(200, 100)); + QCOMPARE(window->bufferGeometry().size(), QSize(220, 140)); + + // Move the pointer to (20, 50) relative to the upper left frame corner, which is located + // at (10, 20) in the surface-local coordinates. + Test::pointerMotion(window->pos() + QPointF(20, 50), timestamp++); + QVERIFY(pointerMotionSpy.wait()); + QCOMPARE(pointerMotionSpy.last().first().toPointF(), QPointF(10, 20) + QPointF(20, 50)); + + // Destroy the xdg-toplevel surface. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void TestXdgShellWindow::testReentrantSetFrameGeometry() +{ + // This test verifies that calling moveResize() from a slot connected directly + // to the frameGeometryChanged() signal won't cause an infinite recursion. + + // Create an xdg-toplevel surface and wait for the compositor to catch up. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(200, 100), Qt::red); + QVERIFY(window); + QCOMPARE(window->pos(), QPoint(0, 0)); + + // Let's pretend that there is a script that really wants the window to be at (100, 100). + connect(window, &Window::frameGeometryChanged, this, [window]() { + window->moveResize(RectF(QPointF(100, 100), window->size())); + }); + + // Trigger the lambda above. + window->move(QPoint(40, 50)); + + // Eventually, the window will end up at (100, 100). + QCOMPARE(window->pos(), QPoint(100, 100)); + + // Destroy the xdg-toplevel surface. + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void TestXdgShellWindow::testDoubleMaximize() +{ + // This test verifies that the case where a window issues two set_maximized() requests + // separated by the initial commit is handled properly. + + // Create the test surface. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + shellSurface->set_maximized(); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the compositor to respond with a configure event. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + + QSize size = toplevelConfigureRequestedSpy.last().at(0).toSize(); + QCOMPARE(size, QSize(1280, 1024)); + Test::XdgToplevel::States states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Send another set_maximized() request, but do not attach any buffer yet. + shellSurface->set_maximized(); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // The compositor must respond with another configure event even if the state hasn't changed. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + size = toplevelConfigureRequestedSpy.last().at(0).toSize(); + QCOMPARE(size, QSize(1280, 1024)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); +} + +void TestXdgShellWindow::testDoubleFullscreenSeparatedByCommit() +{ + // Some applications do weird things at startup and this is one of them. This test verifies + // that the window will have good frame geometry if the window has issued several + // xdg_toplevel.set_fullscreen requests and they are separated by a surface commit with + // no attached buffer. + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + // Tell the compositor that we want the window to be shown in fullscreen mode. + shellSurface->set_fullscreen(nullptr); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(1280, 1024)); + QVERIFY(toplevelConfigureRequestedSpy.last().at(1).value() & Test::XdgToplevel::State::Fullscreen); + + // Ask again. + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + shellSurface->set_fullscreen(nullptr); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(1280, 1024)); + QVERIFY(toplevelConfigureRequestedSpy.last().at(1).value() & Test::XdgToplevel::State::Fullscreen); + + // Map the window. + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::blue); + QVERIFY(window->isFullScreen()); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); +} + +void TestXdgShellWindow::testMaximizeHorizontal() +{ + // Create the test window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the initial configure event. + Test::XdgToplevel::States states; + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(0, 0)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Map the window. + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(800, 600), Qt::blue); + QVERIFY(window); + QVERIFY(window->isActive()); + QVERIFY(window->isMaximizable()); + QCOMPARE(window->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(window->size(), QSize(800, 600)); + + // We should receive a configure event when the window becomes active. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Maximize the test window in horizontal direction. + workspace()->slotWindowMaximizeHorizontal(); + QCOMPARE(window->requestedMaximizeMode(), MaximizeHorizontal); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(1280, 600)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Draw contents of the maximized window. + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(1280, 600), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->size(), QSize(1280, 600)); + QCOMPARE(window->requestedMaximizeMode(), MaximizeHorizontal); + QCOMPARE(window->maximizeMode(), MaximizeHorizontal); + + // Restore the window. + workspace()->slotWindowMaximizeHorizontal(); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->maximizeMode(), MaximizeHorizontal); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 4); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(800, 600)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Draw contents of the restored window. + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(800, 600), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->size(), QSize(800, 600)); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + + // Destroy the window. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void TestXdgShellWindow::testMaximizeVertical() +{ + // Create the test window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the initial configure event. + Test::XdgToplevel::States states; + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(0, 0)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Map the window. + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(800, 600), Qt::blue); + QVERIFY(window); + QVERIFY(window->isActive()); + QVERIFY(window->isMaximizable()); + QCOMPARE(window->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(window->size(), QSize(800, 600)); + + // We should receive a configure event when the window becomes active. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Maximize the test window in vertical direction. + workspace()->slotWindowMaximizeVertical(); + QCOMPARE(window->requestedMaximizeMode(), MaximizeVertical); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(800, 1024)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Draw contents of the maximized window. + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(800, 1024), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->size(), QSize(800, 1024)); + QCOMPARE(window->requestedMaximizeMode(), MaximizeVertical); + QCOMPARE(window->maximizeMode(), MaximizeVertical); + + // Restore the window. + workspace()->slotWindowMaximizeVertical(); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->maximizeMode(), MaximizeVertical); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 4); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(800, 600)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Draw contents of the restored window. + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(800, 600), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->size(), QSize(800, 600)); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + + // Destroy the window. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void TestXdgShellWindow::testMaximizeFull() +{ + // Create the test window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // Wait for the initial configure event. + Test::XdgToplevel::States states; + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(0, 0)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Map the window. + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(800, 600), Qt::blue); + QVERIFY(window); + QVERIFY(window->isActive()); + QVERIFY(window->isMaximizable()); + QCOMPARE(window->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(window->size(), QSize(800, 600)); + + // We should receive a configure event when the window becomes active. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Maximize the test window. + workspace()->slotWindowMaximize(); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Draw contents of the maximized window. + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(1280, 1024), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->size(), QSize(1280, 1024)); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + QCOMPARE(window->maximizeMode(), MaximizeFull); + + // Restore the window. + workspace()->slotWindowMaximize(); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(surfaceConfigureRequestedSpy.count(), 4); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(800, 600)); + states = toplevelConfigureRequestedSpy.last().at(1).value(); + QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); + + // Draw contents of the restored window. + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), QSize(800, 600), Qt::blue); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->size(), QSize(800, 600)); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + + // Destroy the window. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); +} + +void TestXdgShellWindow::testSendMaximizedWindowToAnotherOutput() +{ + // This test verifies that the maximized window will have correct geometry restore + // after it's sent to another output. + + const auto outputs = workspace()->outputs(); + + // Create the window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + QVERIFY(window); + + // Wait for the compositor to send a configure event with the activated state. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + // Move the window to the left monitor. + window->move(QPointF(10, 20)); + QCOMPARE(window->frameGeometry(), RectF(10, 20, 100, 50)); + QCOMPARE(window->output(), outputs[0]); + + // Make the window maximized. + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + shellSurface->set_maximized(); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->frameGeometry(), RectF(0, 0, 1280, 1024)); + QCOMPARE(window->geometryRestore(), RectF(10, 20, 100, 50)); + QCOMPARE(window->output(), outputs[0]); + + // Send the window to another output. + window->sendToOutput(outputs[1]); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->frameGeometry(), RectF(1280, 0, 1280, 1024)); + QCOMPARE(window->geometryRestore(), RectF(1280 + 10, 20, 100, 50)); + QCOMPARE(window->output(), outputs[1]); +} + +void TestXdgShellWindow::testInteractiveMoveUnmaximizeFull() +{ + // This test verifies that a maximized xdg-toplevel is going to be properly unmaximized when it's dragged. + + // Create the window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + // Wait for the compositor to send a configure event with the activated state. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + // Make the window maximized. + const RectF originalGeometry = window->frameGeometry(); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + window->maximize(MaximizeFull); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + + // Start interactive move. + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + const qreal xOffset = 0.25; + const qreal yOffset = 0.5; + quint32 timestamp = 0; + Test::pointerMotion(QPointF(window->x() + window->width() * xOffset, window->y() + window->height() * yOffset), timestamp++); + window->performMousePressCommand(Options::MouseMove, input()->pointer()->pos()); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + + // Move the window to unmaximize it. + const RectF maximizedGeometry = window->frameGeometry(); + Test::pointerMotionRelative(QPointF(0, 100), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), maximizedGeometry); + + // Move the window a tiny bit more. + Test::pointerMotionRelative(QPointF(0, 10), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), maximizedGeometry); + + // Render the window at the new size. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), RectF(input()->pointer()->pos() - QPointF(originalGeometry.width() * xOffset, originalGeometry.height() * yOffset), originalGeometry.size())); + + // Move the window again. + const RectF normalGeometry = window->frameGeometry(); + Test::pointerMotionRelative(QPointF(0, 10), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), normalGeometry.translated(0, 10)); + + // Finish interactive move. + window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); +} + +void TestXdgShellWindow::testInteractiveMoveUnmaximizeInitiallyFull() +{ + // This test verifies that an initially maximized xdg-toplevel will be properly unmaximized when it's dragged. + + // Create the window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), [](Test::XdgToplevel *toplevel) { + toplevel->set_maximized(); + })); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + // Wait for the compositor to send a configure event with the activated state. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + // Start interactive move. + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + const qreal xOffset = 0.25; + const qreal yOffset = 0.5; + quint32 timestamp = 0; + Test::pointerMotion(QPointF(window->x() + window->width() * xOffset, window->y() + window->height() * yOffset), timestamp++); + window->performMousePressCommand(Options::MouseMove, input()->pointer()->pos()); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeFull); + + // Move the window to unmaximize it. + const RectF maximizedGeometry = window->frameGeometry(); + Test::pointerMotionRelative(QPointF(0, 100), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), maximizedGeometry); + + // Move the window a tiny bit more. + Test::pointerMotionRelative(QPointF(0, 10), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 0); + QCOMPARE(window->maximizeMode(), MaximizeFull); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), maximizedGeometry); + + // Render the window at the new size. + const QSize restoredSize(100, 50); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(0, 0)); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), restoredSize, Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), RectF(input()->pointer()->pos() - QPointF(restoredSize.width() * xOffset, restoredSize.height() * yOffset), restoredSize)); + + // Move the window again. + const RectF normalGeometry = window->frameGeometry(); + Test::pointerMotionRelative(QPointF(0, 10), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), normalGeometry.translated(0, 10)); + + // Finish interactive move. + window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); +} + +void TestXdgShellWindow::testInteractiveMoveUnmaximizeHorizontal() +{ + // This test verifies that a maximized horizontally xdg-toplevel is going to be properly unmaximized when it's dragged horizontally. + + // Create the window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + // Wait for the compositor to send a configure event with the activated state. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + // Make the window maximized. + const RectF originalGeometry = window->frameGeometry(); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + window->maximize(MaximizeHorizontal); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->maximizeMode(), MaximizeHorizontal); + QCOMPARE(window->requestedMaximizeMode(), MaximizeHorizontal); + + // Start interactive move. + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + const qreal xOffset = 0.25; + const qreal yOffset = 0.5; + quint32 timestamp = 0; + Test::pointerMotion(QPointF(window->x() + window->width() * xOffset, window->y() + window->height() * yOffset), timestamp++); + window->performMousePressCommand(Options::MouseMove, input()->pointer()->pos()); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeHorizontal); + QCOMPARE(window->requestedMaximizeMode(), MaximizeHorizontal); + + // Move the window vertically, it's not going to be unmaximized. + const RectF maximizedGeometry = window->frameGeometry(); + Test::pointerMotionRelative(QPointF(0, 100), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeHorizontal); + QCOMPARE(window->requestedMaximizeMode(), MaximizeHorizontal); + QCOMPARE(window->frameGeometry(), maximizedGeometry.translated(0, 100)); + + // Move the window horizontally. + Test::pointerMotionRelative(QPointF(100, 0), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeHorizontal); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), maximizedGeometry.translated(0, 100)); + + // Move the window to the right a bit more. + Test::pointerMotionRelative(QPointF(10, 0), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeHorizontal); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), maximizedGeometry.translated(0, 100)); + + // Render the window at the new size. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), RectF(input()->pointer()->pos() - QPointF(originalGeometry.width() * xOffset, originalGeometry.height() * yOffset), originalGeometry.size())); + + // Move the window again. + const RectF normalGeometry = window->frameGeometry(); + Test::pointerMotionRelative(QPointF(10, 0), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 2); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), normalGeometry.translated(10, 0)); + + // Finish interactive move. + window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); +} + +void TestXdgShellWindow::testInteractiveMoveUnmaximizeVertical() +{ + // This test verifies that a maximized vertically xdg-toplevel is going to be properly unmaximized when it's dragged vertically. + + // Create the window. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); + + // Wait for the compositor to send a configure event with the activated state. + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + // Make the window maximized. + const RectF originalGeometry = window->frameGeometry(); + QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged); + window->maximize(MaximizeVertical); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->maximizeMode(), MaximizeVertical); + QCOMPARE(window->requestedMaximizeMode(), MaximizeVertical); + + // Start interactive move. + QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted); + QSignalSpy interactiveMoveResizeSteppedSpy(window, &Window::interactiveMoveResizeStepped); + QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished); + const qreal xOffset = 0.25; + const qreal yOffset = 0.5; + quint32 timestamp = 0; + Test::pointerMotion(QPointF(window->x() + window->width() * xOffset, window->y() + window->height() * yOffset), timestamp++); + window->performMousePressCommand(Options::MouseMove, input()->pointer()->pos()); + QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeVertical); + QCOMPARE(window->requestedMaximizeMode(), MaximizeVertical); + + // Move the window to the right, it's not going to be unmaximized. + const RectF maximizedGeometry = window->frameGeometry(); + Test::pointerMotionRelative(QPointF(100, 0), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeVertical); + QCOMPARE(window->requestedMaximizeMode(), MaximizeVertical); + QCOMPARE(window->frameGeometry(), maximizedGeometry.translated(100, 0)); + + // Move the window vertically. + Test::pointerMotionRelative(QPointF(0, 100), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeVertical); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), maximizedGeometry.translated(100, 0)); + + // Move the window down a bit more. + Test::pointerMotionRelative(QPointF(0, 10), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1); + QCOMPARE(window->maximizeMode(), MaximizeVertical); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), maximizedGeometry.translated(100, 0)); + + // Render the window at the new size. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::red); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), RectF(input()->pointer()->pos() - QPointF(originalGeometry.width() * xOffset, originalGeometry.height() * yOffset), originalGeometry.size())); + + // Move the window again. + const RectF normalGeometry = window->frameGeometry(); + Test::pointerMotionRelative(QPointF(0, 10), timestamp++); + QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 2); + QCOMPARE(window->maximizeMode(), MaximizeRestore); + QCOMPARE(window->requestedMaximizeMode(), MaximizeRestore); + QCOMPARE(window->frameGeometry(), normalGeometry.translated(0, 10)); + + // Finish interactive move. + window->keyPressEvent(Qt::Key_Enter); + QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1); +} + +void TestXdgShellWindow::testMaximizeAndChangeDecorationModeAfterInitialCommit() +{ + // Ideally, the app would initialize the xdg-toplevel surface before the initial commit, but + // many don't do it. They initialize the surface after the first commit. + // This test verifies that the window will receive a configure event with correct size + // if an xdg-toplevel surface is set maximized and decoration mode changes after initial commit. + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + std::unique_ptr decoration(Test::createXdgToplevelDecorationV1(shellSurface.get())); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + // Commit the initial state. + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(0, 0)); + + // Request maximized mode and set decoration mode, i.e. perform late initialization. + shellSurface->set_maximized(); + decoration->set_mode(Test::XdgToplevelDecorationV1::mode_client_side); + + // The compositor will respond with a new configure event, which should contain maximized state. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(1280, 1024)); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(1).value(), Test::XdgToplevel::State::Maximized); +} + +void TestXdgShellWindow::testFullScreenAndChangeDecorationModeAfterInitialCommit() +{ + // Ideally, the app would initialize the xdg-toplevel surface before the initial commit, but + // many don't do it. They initialize the surface after the first commit. + // This test verifies that the window will receive a configure event with correct size + // if an xdg-toplevel surface is set fullscreen and decoration mode changes after initial commit. + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + std::unique_ptr decoration(Test::createXdgToplevelDecorationV1(shellSurface.get())); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + // Commit the initial state. + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(0, 0)); + + // Request fullscreen mode and set decoration mode, i.e. perform late initialization. + shellSurface->set_fullscreen(nullptr); + decoration->set_mode(Test::XdgToplevelDecorationV1::mode_client_side); + + // The compositor will respond with a new configure event, which should contain fullscreen state. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(1280, 1024)); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(1).value(), Test::XdgToplevel::State::Fullscreen); +} + +void TestXdgShellWindow::testChangeDecorationModeAfterInitialCommit() +{ + // This test verifies that the compositor will respond with a good configure event when + // the decoration mode changes after the first surface commit but before the surface is mapped. + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + std::unique_ptr decoration(Test::createXdgToplevelDecorationV1(shellSurface.get())); + QSignalSpy decorationConfigureRequestedSpy(decoration.get(), &Test::XdgToplevelDecorationV1::configureRequested); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + // Perform the initial commit. + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(0, 0)); + QCOMPARE(decorationConfigureRequestedSpy.last().at(0).value(), Test::XdgToplevelDecorationV1::mode_server_side); + + // Change decoration mode. + decoration->set_mode(Test::XdgToplevelDecorationV1::mode_client_side); + + // The configure event should still have 0x0 size. + QVERIFY(surfaceConfigureRequestedSpy.wait()); + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(0, 0)); + QCOMPARE(decorationConfigureRequestedSpy.last().at(0).value(), Test::XdgToplevelDecorationV1::mode_client_side); +} + +void TestXdgShellWindow::testModal() +{ + auto parentSurface = Test::createSurface(); + auto parentToplevel = Test::createXdgToplevelSurface(parentSurface.get()); + auto parentWindow = Test::renderAndWaitForShown(parentSurface.get(), {200, 200}, Qt::cyan); + QVERIFY(parentWindow); + + auto childSurface = Test::createSurface(); + auto childToplevel = Test::createXdgToplevelSurface(childSurface.get(), [&parentToplevel](Test::XdgToplevel *toplevel) { + toplevel->set_parent(parentToplevel->object()); + }); + auto childWindow = Test::renderAndWaitForShown(childSurface.get(), {200, 200}, Qt::yellow); + QVERIFY(childWindow); + QVERIFY(!childWindow->isModal()); + QCOMPARE(childWindow->transientFor(), parentWindow); + + auto dialog = Test::createXdgDialogV1(childToplevel.get()); + QVERIFY(Test::waylandSync()); + QVERIFY(dialog); + QVERIFY(!childWindow->isModal()); + + QSignalSpy modalChangedSpy(childWindow, &Window::modalChanged); + + dialog->set_modal(); + Test::flushWaylandConnection(); + QVERIFY(modalChangedSpy.wait()); + QVERIFY(childWindow->isModal()); + + dialog->unset_modal(); + Test::flushWaylandConnection(); + QVERIFY(modalChangedSpy.wait()); + QVERIFY(!childWindow->isModal()); + + dialog->set_modal(); + Test::flushWaylandConnection(); + QVERIFY(modalChangedSpy.wait()); + Workspace::self()->activateWindow(parentWindow); + QCOMPARE(Workspace::self()->activeWindow(), childWindow); + + dialog.reset(); + Test::flushWaylandConnection(); + QVERIFY(modalChangedSpy.wait()); + QVERIFY(!childWindow->isModal()); +} + +void TestXdgShellWindow::testCloseModal() +{ + // This test verifies that the parent window will be activated when an active modal dialog is closed. + + // Create a parent and a child windows. + auto parentSurface = Test::createSurface(); + auto parentToplevel = Test::createXdgToplevelSurface(parentSurface.get()); + auto parent = Test::renderAndWaitForShown(parentSurface.get(), {200, 200}, Qt::cyan); + QVERIFY(parent); + + auto childSurface = Test::createSurface(); + auto childToplevel = Test::createXdgToplevelSurface(childSurface.get(), [&parentToplevel](Test::XdgToplevel *toplevel) { + toplevel->set_parent(parentToplevel->object()); + }); + auto child = Test::renderAndWaitForShown(childSurface.get(), {200, 200}, Qt::yellow); + QVERIFY(child); + QVERIFY(!child->isModal()); + QCOMPARE(child->transientFor(), parent); + + // Set modal state. + auto dialog = Test::createXdgDialogV1(childToplevel.get()); + QSignalSpy modalChangedSpy(child, &Window::modalChanged); + dialog->set_modal(); + Test::flushWaylandConnection(); + QVERIFY(modalChangedSpy.wait()); + QVERIFY(child->isModal()); + QCOMPARE(workspace()->activeWindow(), child); + + // Close the child. + QSignalSpy childClosedSpy(child, &Window::closed); + childToplevel.reset(); + childSurface.reset(); + dialog.reset(); + Test::flushWaylandConnection(); + QVERIFY(childClosedSpy.wait()); + QCOMPARE(workspace()->activeWindow(), parent); +} + +void TestXdgShellWindow::testCloseModalPreSetup() +{ + // This test verifies that the parent window will be activated when an active modal dialog is closed + // even if the modality existed before mapping the parent + + // Create a parent and a child windows. + auto parentSurface = Test::createSurface(); + auto parentToplevel = Test::createXdgToplevelSurface(parentSurface.get()); + + auto childSurface = Test::createSurface(); + auto childToplevel = Test::createXdgToplevelSurface(childSurface.get(), [&parentToplevel](Test::XdgToplevel *toplevel) { + toplevel->set_parent(parentToplevel->object()); + }); + auto dialog = Test::createXdgDialogV1(childToplevel.get()); + dialog->set_modal(); + + auto parent = Test::renderAndWaitForShown(parentSurface.get(), {200, 200}, Qt::cyan); + QVERIFY(parent); + auto child = Test::renderAndWaitForShown(childSurface.get(), {200, 200}, Qt::yellow); + QVERIFY(child); + QCOMPARE(child->transientFor(), parent); + + Test::flushWaylandConnection(); + QVERIFY(child->isModal()); + QCOMPARE(workspace()->activeWindow(), child); + + // Close the child. + QSignalSpy childClosedSpy(child, &Window::closed); + childToplevel.reset(); + childSurface.reset(); + Test::flushWaylandConnection(); + QVERIFY(childClosedSpy.wait()); + QCOMPARE(workspace()->activeWindow(), parent); +} + +void TestXdgShellWindow::testCloseInactiveModal() +{ + // This test verifies that the parent window will not be activated when an inactive modal dialog is closed. + + // Create a parent and a child windows. + auto parentSurface = Test::createSurface(); + auto parentToplevel = Test::createXdgToplevelSurface(parentSurface.get()); + auto parent = Test::renderAndWaitForShown(parentSurface.get(), {200, 200}, Qt::cyan); + QVERIFY(parent); + + auto childSurface = Test::createSurface(); + auto childToplevel = Test::createXdgToplevelSurface(childSurface.get(), [&parentToplevel](Test::XdgToplevel *toplevel) { + toplevel->set_parent(parentToplevel->object()); + }); + auto child = Test::renderAndWaitForShown(childSurface.get(), {200, 200}, Qt::yellow); + QVERIFY(child); + QVERIFY(!child->isModal()); + QCOMPARE(child->transientFor(), parent); + + // Set modal state. + auto dialog = Test::createXdgDialogV1(childToplevel.get()); + QSignalSpy modalChangedSpy(child, &Window::modalChanged); + dialog->set_modal(); + Test::flushWaylandConnection(); + QVERIFY(modalChangedSpy.wait()); + QVERIFY(child->isModal()); + QCOMPARE(workspace()->activeWindow(), child); + + // Show another window. + auto otherSurface = Test::createSurface(); + auto otherToplevel = Test::createXdgToplevelSurface(otherSurface.get()); + auto otherWindow = Test::renderAndWaitForShown(otherSurface.get(), {200, 200}, Qt::magenta); + QVERIFY(otherWindow); + workspace()->activateWindow(otherWindow); + QCOMPARE(workspace()->activeWindow(), otherWindow); + + // Close the child. + QSignalSpy childClosedSpy(child, &Window::closed); + childToplevel.reset(); + childSurface.reset(); + dialog.reset(); + Test::flushWaylandConnection(); + QVERIFY(childClosedSpy.wait()); + QCOMPARE(workspace()->activeWindow(), otherWindow); +} + +void TestXdgShellWindow::testClosePopupOnParentUnmapped() +{ + // This test verifies that a popup window will be closed when the parent window is closed. + + std::unique_ptr parentSurface = Test::createSurface(); + std::unique_ptr parentToplevel = Test::createXdgToplevelSurface(parentSurface.get()); + Window *parent = Test::renderAndWaitForShown(parentSurface.get(), QSize(200, 200), Qt::cyan); + QVERIFY(parent); + + std::unique_ptr positioner = Test::createXdgPositioner(); + positioner->set_size(10, 10); + positioner->set_anchor_rect(10, 10, 10, 10); + + std::unique_ptr childSurface = Test::createSurface(); + std::unique_ptr popup = Test::createXdgPopupSurface(childSurface.get(), parentToplevel->xdgSurface(), positioner.get()); + Window *child = Test::renderAndWaitForShown(childSurface.get(), QSize(10, 10), Qt::cyan); + QVERIFY(child); + + QSignalSpy childClosedSpy(child, &Window::closed); + parentToplevel.reset(); + parentSurface.reset(); + QVERIFY(childClosedSpy.wait()); +} + +void TestXdgShellWindow::testPopupWithDismissedParent() +{ + // This test verifies that a popup window will be closed when the parent window is already dismissed by the compositor. + + std::unique_ptr parentSurface = Test::createSurface(); + std::unique_ptr parentToplevel = Test::createXdgToplevelSurface(parentSurface.get()); + Window *parent = Test::renderAndWaitForShown(parentSurface.get(), QSize(200, 200), Qt::cyan); + QVERIFY(parent); + + std::unique_ptr positioner = Test::createXdgPositioner(); + positioner->set_size(10, 10); + positioner->set_anchor_rect(10, 10, 10, 10); + + std::unique_ptr childSurface = Test::createSurface(); + std::unique_ptr popup = Test::createXdgPopupSurface(childSurface.get(), parentToplevel->xdgSurface(), positioner.get()); + Window *child = Test::renderAndWaitForShown(childSurface.get(), QSize(10, 10), Qt::cyan); + QVERIFY(child); + + QSignalSpy childDoneSpy(popup.get(), &Test::XdgPopup::doneReceived); + child->popupDone(); + QVERIFY(childDoneSpy.wait()); + + // A nested popup will be dismissed immediately if its parent popup is already dismissed. + std::unique_ptr grandChildSurface = Test::createSurface(); + std::unique_ptr grandChildPopup = Test::createXdgPopupSurface(grandChildSurface.get(), popup->xdgSurface(), positioner.get(), Test::CreationSetup::CreateOnly); + QSignalSpy grandChildDoneSpy(grandChildPopup.get(), &Test::XdgPopup::doneReceived); + grandChildPopup->xdgSurface()->surface()->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(grandChildDoneSpy.wait()); +} + +void TestXdgShellWindow::testPopupDismissedOnFocusChange() +{ + // This test verifies that a popup window will be dismissed when the focus changes. + + std::unique_ptr parentSurface = Test::createSurface(); + std::unique_ptr parentToplevel = Test::createXdgToplevelSurface(parentSurface.get()); + std::unique_ptr pointer(Test::waylandSeat()->createPointer()); + Window *parent = Test::renderAndWaitForShown(parentSurface.get(), QSize(200, 200), Qt::cyan); + QVERIFY(parent); + + QSignalSpy buttonSpy(pointer.get(), &KWayland::Client::Pointer::buttonStateChanged); + input()->pointer()->warp(parent->frameGeometry().center()); + // simulate press + quint32 timestamp = 1; + Test::pointerButtonPressed(BTN_LEFT, timestamp++); + QVERIFY(buttonSpy.wait()); + + std::unique_ptr positioner = Test::createXdgPositioner(); + positioner->set_size(10, 10); + positioner->set_anchor_rect(10, 10, 10, 10); + + std::unique_ptr childSurface = Test::createSurface(); + std::unique_ptr popup = Test::createXdgPopupSurface(childSurface.get(), parentToplevel->xdgSurface(), positioner.get()); + popup->grab(*Test::waylandSeat(), buttonSpy.first().first().value()); + QPointer child = Test::renderAndWaitForShown(childSurface.get(), QSize(10, 10), Qt::cyan); + QVERIFY(child); + + QSignalSpy popupDismissedSpy(popup.get(), &Test::XdgPopup::doneReceived); + + // create another toplevel that gets focus + std::unique_ptr otherSurface = Test::createSurface(); + std::unique_ptr otherToplevel = Test::createXdgToplevelSurface(otherSurface.get()); + Window *other = Test::renderAndWaitForShown(otherSurface.get(), QSize(200, 200), Qt::cyan); + QVERIFY(other); + + workspace()->activateWindow(other); + + QVERIFY(popupDismissedSpy.wait()); + QVERIFY(!child); // and the server-side window closed immediately too +} + +void TestXdgShellWindow::testMinimumSize() +{ + // NOTE: a minimum size of 20px is forced by the compositor + + auto surface = Test::createSurface(); + auto shellSurface = Test::createXdgToplevelSurface(surface.get(), [](Test::XdgToplevel *toplevel) { + toplevel->set_min_size(200, 250); + }); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(300, 300), Qt::cyan); + QCOMPARE(window->minSize(), QSizeF(200, 250)); + + QSignalSpy committedSpy(window->surface(), &SurfaceInterface::committed); + + shellSurface->set_min_size(100, 100); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(committedSpy.wait()); + QCOMPARE(window->minSize(), QSizeF(100, 100)); + + shellSurface->set_min_size(0, 100); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(committedSpy.wait()); + QCOMPARE(window->minSize(), QSizeF(20, 100)); + + shellSurface->set_min_size(100, 0); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(committedSpy.wait()); + QCOMPARE(window->minSize(), QSizeF(100, 20)); + + shellSurface->set_min_size(0, 0); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(committedSpy.wait()); + QCOMPARE(window->minSize(), QSizeF(20, 20)); +} + +void TestXdgShellWindow::testNoMinimumSize() +{ + // NOTE: a minimum size of 20px is forced by the compositor + + auto surface = Test::createSurface(); + auto shellSurface = Test::createXdgToplevelSurface(surface.get()); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(300, 300), Qt::cyan); + QCOMPARE(window->minSize(), QSizeF(20, 20)); +} + +void TestXdgShellWindow::testMaximumSize() +{ + auto surface = Test::createSurface(); + auto shellSurface = Test::createXdgToplevelSurface(surface.get(), [](Test::XdgToplevel *toplevel) { + toplevel->set_max_size(300, 350); + }); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(200, 200), Qt::cyan); + QCOMPARE(window->maxSize(), QSizeF(300, 350)); + + QSignalSpy committedSpy(window->surface(), &SurfaceInterface::committed); + + shellSurface->set_max_size(400, 400); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(committedSpy.wait()); + QCOMPARE(window->maxSize(), QSizeF(400, 400)); + + shellSurface->set_max_size(0, 400); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(committedSpy.wait()); + QCOMPARE(window->maxSize(), QSizeF(INT_MAX, 400)); + + shellSurface->set_max_size(400, 0); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(committedSpy.wait()); + QCOMPARE(window->maxSize(), QSizeF(400, INT_MAX)); + + shellSurface->set_max_size(0, 0); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(committedSpy.wait()); + QCOMPARE(window->maxSize(), QSizeF(INT_MAX, INT_MAX)); +} + +void TestXdgShellWindow::testNoMaximumSize() +{ + auto surface = Test::createSurface(); + auto shellSurface = Test::createXdgToplevelSurface(surface.get()); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(300, 300), Qt::cyan); + QCOMPARE(window->maxSize(), QSizeF(INT_MAX, INT_MAX)); +} + +void TestXdgShellWindow::testUnconfiguredBufferToplevel() +{ + // This test verifies that a protocol error is posted when a client attaches a buffer to + // the initial xdg-toplevel commit. + + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + Test::render(surface.get(), QSize(100, 50), Qt::blue); + + QSignalSpy connectionErrorSpy(Test::waylandConnection(), &KWayland::Client::ConnectionThread::errorOccurred); + QVERIFY(connectionErrorSpy.wait()); +} + +void TestXdgShellWindow::testUnconfiguredBufferPopup() +{ + // This test verifies that a protocol error is posted when a client attaches a buffer to + // the initial xdg-popup commit. + + std::unique_ptr parentSurface = Test::createSurface(); + std::unique_ptr parentToplevel = Test::createXdgToplevelSurface(parentSurface.get()); + Test::renderAndWaitForShown(parentSurface.get(), QSize(200, 200), Qt::cyan); + + std::unique_ptr positioner = Test::createXdgPositioner(); + positioner->set_size(10, 10); + positioner->set_anchor_rect(10, 10, 10, 10); + + std::unique_ptr childSurface = Test::createSurface(); + std::unique_ptr popup = Test::createXdgPopupSurface(childSurface.get(), parentToplevel->xdgSurface(), positioner.get(), Test::CreationSetup::CreateOnly); + Test::render(childSurface.get(), QSize(100, 50), Qt::blue); + + QSignalSpy connectionErrorSpy(Test::waylandConnection(), &KWayland::Client::ConnectionThread::errorOccurred); + QVERIFY(connectionErrorSpy.wait()); +} + +void TestXdgShellWindow::testRemoveActiveDesktopBeforeInitialCommit() +{ + // This test verifies that a window will be placed on the right desktop if the current desktop + // is removed before the client has a chance to commit the initial state. + + VirtualDesktopManager *virtualDesktopManager = VirtualDesktopManager::self(); + virtualDesktopManager->setCount(2); + + const auto virtualDesktops = virtualDesktopManager->desktops(); + virtualDesktopManager->setCurrent(virtualDesktops[1]); + + // Create an xdg-toplevel surface. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + // Wait for the corresponding Window object to be created on the compositor side. + QSignalSpy windowCreatedSpy(waylandServer(), &WaylandServer::windowCreated); + QVERIFY(windowCreatedSpy.wait()); + Window *window = windowCreatedSpy.last().at(0).value(); + QCOMPARE(window->desktops(), QList{virtualDesktops[1]}); + + // Remove the current desktop. + virtualDesktopManager->removeVirtualDesktop(virtualDesktops[1]); + + // Commit the initial state. + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + // Map the window. + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(0, 0)); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Window *mapped = Test::renderAndWaitForShown(surface.get(), QSize(800, 600), Qt::blue); + QCOMPARE(mapped, window); + + // The window should have been evacuated from the second virtual desktop to the first virtual desktop. + QCOMPARE(window->desktops(), QList{virtualDesktops[0]}); +} + +void TestXdgShellWindow::testRemoveActiveDesktopBeforeMap() +{ + // This test verifies that a window will be placed on the right desktop if the current desktop + // is removed before the client has a chance to map the surface (after committing the initial state). + + VirtualDesktopManager *virtualDesktopManager = VirtualDesktopManager::self(); + virtualDesktopManager->setCount(2); + + const auto virtualDesktops = virtualDesktopManager->desktops(); + virtualDesktopManager->setCurrent(virtualDesktops[1]); + + // Create an xdg-toplevel surface. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + // Wait for the corresponding Window object to be created on the compositor side. + QSignalSpy windowCreatedSpy(waylandServer(), &WaylandServer::windowCreated); + QVERIFY(windowCreatedSpy.wait()); + Window *window = windowCreatedSpy.last().at(0).value(); + QCOMPARE(window->desktops(), QList{virtualDesktops[1]}); + + // Commit the initial state. + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + // Remove the current desktop. + virtualDesktopManager->removeVirtualDesktop(virtualDesktops[1]); + + // Map the window. + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(0, 0)); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Window *mapped = Test::renderAndWaitForShown(surface.get(), QSize(800, 600), Qt::blue); + QCOMPARE(mapped, window); + + // The window should have been evacuated from the second virtual desktop to the first virtual desktop. + QCOMPARE(window->desktops(), QList{virtualDesktops[0]}); +} + +void TestXdgShellWindow::testRemoveActiveOutputBeforeInitialCommit() +{ + // This test verifies that a window will be placed on the right output if the active output + // is removed before the client has a chance to commit the initial state. + + const auto outputs = workspace()->outputs(); + workspace()->setActiveOutput(outputs[1]); + + // Create an xdg-toplevel surface. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + // Wait for the corresponding Window object to be created on the compositor side. + QSignalSpy windowCreatedSpy(waylandServer(), &WaylandServer::windowCreated); + QVERIFY(windowCreatedSpy.wait()); + Window *window = windowCreatedSpy.last().at(0).value(); + QCOMPARE(window->output(), outputs[1]); + QCOMPARE(window->moveResizeOutput(), outputs[1]); + + // Disable the active output. + OutputConfiguration config; + { + auto changeSet = config.changeSet(outputs[1]->backendOutput()); + changeSet->enabled = false; + } + workspace()->applyOutputConfiguration(config); + + // Commit the initial state. + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + // Map the window. + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(0, 0)); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Window *mapped = Test::renderAndWaitForShown(surface.get(), QSize(800, 600), Qt::blue); + QCOMPARE(mapped, window); + + // The window should have been evacuated from the second output to the first output. + QCOMPARE(window->output(), outputs[0]); + QCOMPARE(window->moveResizeOutput(), outputs[0]); +} + +void TestXdgShellWindow::testRemoveActiveOutputBeforeMap() +{ + // This test verifies that a window will be placed on the right output if the current output + // is removed before the client has a chance to map the surface (after committing the initial state). + + const auto outputs = workspace()->outputs(); + workspace()->setActiveOutput(outputs[1]); + + // Create an xdg-toplevel surface. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly)); + QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested); + QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); + + // Wait for the corresponding Window object to be created on the compositor side. + QSignalSpy windowCreatedSpy(waylandServer(), &WaylandServer::windowCreated); + QVERIFY(windowCreatedSpy.wait()); + Window *window = windowCreatedSpy.last().at(0).value(); + QCOMPARE(window->output(), outputs[1]); + QCOMPARE(window->moveResizeOutput(), outputs[1]); + + // Commit the initial state. + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(surfaceConfigureRequestedSpy.wait()); + + // Disable the active output. + OutputConfiguration config; + { + auto changeSet = config.changeSet(outputs[1]->backendOutput()); + changeSet->enabled = false; + } + workspace()->applyOutputConfiguration(config); + + // Map the window. + QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(0, 0)); + shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); + Window *mapped = Test::renderAndWaitForShown(surface.get(), QSize(800, 600), Qt::blue); + QCOMPARE(mapped, window); + + // The window should have been evacuated from the second output to the first output. + QCOMPARE(window->output(), outputs[0]); + QCOMPARE(window->moveResizeOutput(), outputs[0]); +} + +WAYLANDTEST_MAIN(TestXdgShellWindow) +#include "xdgshellwindow_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/xinerama_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/xinerama_test.cpp new file mode 100644 index 0000000000..e700a51ff7 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/xinerama_test.cpp @@ -0,0 +1,71 @@ +/* + SPDX-FileCopyrightText: 2024 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "wayland_server.h" +#include "workspace.h" +#include "core/outputconfiguration.h" + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_xinerama-0"); + +class XineramaTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void indexToOutput(); +}; + +void XineramaTest::initTestCase() +{ + QVERIFY(waylandServer()->init(s_socketName)); + kwinApp()->start(); +} + +void XineramaTest::indexToOutput() +{ + Test::setOutputConfig({ + Test::OutputInfo{ + .geometry = Rect(0, 0, 1280, 1024), + .scale = 1.5, + }, + Test::OutputInfo{ + .geometry = Rect(1280, 0, 1280, 1024), + .scale = 1.5, + }, + }); + kwinApp()->setXwaylandScale(1.5); + + // Start Xwayland + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + + const auto outputs = workspace()->outputs(); + + OutputConfiguration config; + config.changeSet(outputs[0]->backendOutput())->priority = 0; + config.changeSet(outputs[1]->backendOutput())->priority = 1; + QCOMPARE(workspace()->applyOutputConfiguration(config), OutputConfigurationError::None); + QCOMPARE(workspace()->xineramaIndexToOutput(0), outputs.at(0)); + QCOMPARE(workspace()->xineramaIndexToOutput(1), outputs.at(1)); + + config.changeSet(outputs[0]->backendOutput())->priority = 1; + config.changeSet(outputs[1]->backendOutput())->priority = 0; + QCOMPARE(workspace()->applyOutputConfiguration(config), OutputConfigurationError::None); + QCOMPARE(workspace()->xineramaIndexToOutput(0), outputs.at(1)); + QCOMPARE(workspace()->xineramaIndexToOutput(1), outputs.at(0)); +} + +} // namespace KWin + +WAYLANDTEST_MAIN(KWin::XineramaTest) +#include "xinerama_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/xwayland_input_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/xwayland_input_test.cpp new file mode 100644 index 0000000000..1692a96805 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/xwayland_input_test.cpp @@ -0,0 +1,270 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kwin_wayland_test.h" + +#include "core/output.h" +#include "pointer_input.h" +#include "wayland/seat.h" +#include "wayland_server.h" +#include "workspace.h" +#include "x11window.h" + +#include +#include + +#include +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_xwayland_input-0"); + +class XWaylandInputTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void testPointerEnterLeaveSsd(); + void testPointerEventLeaveCsd(); +}; + +void XWaylandInputTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); + setenv("QT_QPA_PLATFORM", "wayland", true); +} + +void XWaylandInputTest::init() +{ + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); + + QVERIFY(waylandServer()->windows().isEmpty()); +} + +class X11EventReaderHelper : public QObject +{ + Q_OBJECT +public: + X11EventReaderHelper(xcb_connection_t *c); + +Q_SIGNALS: + void entered(const QPoint &localPoint); + void left(const QPoint &localPoint); + +private: + void processXcbEvents(); + xcb_connection_t *m_connection; + QSocketNotifier *m_notifier; +}; + +X11EventReaderHelper::X11EventReaderHelper(xcb_connection_t *c) + : QObject() + , m_connection(c) + , m_notifier(new QSocketNotifier(xcb_get_file_descriptor(m_connection), QSocketNotifier::Read, this)) +{ + connect(m_notifier, &QSocketNotifier::activated, this, &X11EventReaderHelper::processXcbEvents); + connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::aboutToBlock, this, &X11EventReaderHelper::processXcbEvents); + connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::awake, this, &X11EventReaderHelper::processXcbEvents); +} + +void X11EventReaderHelper::processXcbEvents() +{ + while (auto event = xcb_poll_for_event(m_connection)) { + const uint8_t eventType = event->response_type & ~0x80; + switch (eventType) { + case XCB_ENTER_NOTIFY: { + auto enterEvent = reinterpret_cast(event); + Q_EMIT entered(QPoint(enterEvent->event_x, enterEvent->event_y)); + break; + } + case XCB_LEAVE_NOTIFY: { + auto leaveEvent = reinterpret_cast(event); + Q_EMIT left(QPoint(leaveEvent->event_x, leaveEvent->event_y)); + break; + } + } + free(event); + } + xcb_flush(m_connection); +} + +void XWaylandInputTest::testPointerEnterLeaveSsd() +{ + // this test simulates a pointer enter and pointer leave on a server-side decorated X11 window + + // create the test window + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + if (xcb_get_setup(c.get())->release_number < 11800000) { + QSKIP("XWayland 1.18 required"); + } + + xcb_warp_pointer(connection(), XCB_WINDOW_NONE, kwinApp()->x11RootWindow(), 0, 0, 0, 0, 640, 512); + xcb_flush(connection()); + + X11EventReaderHelper eventReader(c.get()); + QSignalSpy enteredSpy(&eventReader, &X11EventReaderHelper::entered); + QSignalSpy leftSpy(&eventReader, &X11EventReaderHelper::left); + + xcb_window_t windowId = xcb_generate_id(c.get()); + const Rect windowGeometry = Rect(0, 0, 100, 200); + const uint32_t values[] = { + XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW}; + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, XCB_CW_EVENT_MASK, values); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + NETWinInfo info(c.get(), windowId, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); + info.setWindowType(NET::Normal); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.last().first().value(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + QVERIFY(!window->hasStrut()); + QVERIFY(!window->readyForPainting()); + + QMetaObject::invokeMethod(window, "setReadyForPainting"); + QVERIFY(window->readyForPainting()); + QVERIFY(Test::waitForWaylandSurface(window)); + + // move pointer into the window, should trigger an enter + QVERIFY(!window->frameGeometry().contains(Cursors::self()->mouse()->pos())); + QVERIFY(enteredSpy.isEmpty()); + input()->pointer()->warp(window->frameGeometry().center().toPoint()); + QCOMPARE(waylandServer()->seat()->focusedPointerSurface(), window->surface()); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.last().first().toPoint(), (window->frameGeometry().center() - QPointF(window->frameMargins().left(), window->frameMargins().top())).toPoint()); + + // move out of window + input()->pointer()->warp(window->frameGeometry().bottomRight() + QPointF(10, 10)); + QVERIFY(leftSpy.wait()); + QCOMPARE(leftSpy.last().first().toPoint(), (window->frameGeometry().center() - QPointF(window->frameMargins().left(), window->frameMargins().top())).toPoint()); + + // destroy window again + QSignalSpy windowClosedSpy(window, &X11Window::closed); + xcb_unmap_window(c.get(), windowId); + xcb_destroy_window(c.get(), windowId); + xcb_flush(c.get()); + QVERIFY(windowClosedSpy.wait()); +} + +void XWaylandInputTest::testPointerEventLeaveCsd() +{ + // this test simulates a pointer enter and pointer leave on a client-side decorated X11 window + + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + + xcb_warp_pointer(connection(), XCB_WINDOW_NONE, kwinApp()->x11RootWindow(), 0, 0, 0, 0, 640, 512); + xcb_flush(connection()); + + if (xcb_get_setup(c.get())->release_number < 11800000) { + QSKIP("XWayland 1.18 required"); + } + + X11EventReaderHelper eventReader(c.get()); + QSignalSpy enteredSpy(&eventReader, &X11EventReaderHelper::entered); + QSignalSpy leftSpy(&eventReader, &X11EventReaderHelper::left); + + // Extents of the client-side drop-shadow. + NETStrut clientFrameExtent; + clientFrameExtent.left = 10; + clientFrameExtent.right = 10; + clientFrameExtent.top = 5; + clientFrameExtent.bottom = 20; + + xcb_rectangle_t boundingRect; + boundingRect.x = 0; + boundingRect.y = 0; + boundingRect.width = 100 + clientFrameExtent.left + clientFrameExtent.right; + boundingRect.height = 200 + clientFrameExtent.top + clientFrameExtent.bottom; + + xcb_window_t windowId = xcb_generate_id(c.get()); + const uint32_t values[] = { + XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW}; + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + boundingRect.x, boundingRect.y, boundingRect.width, boundingRect.height, + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, XCB_CW_EVENT_MASK, values); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, boundingRect.x, boundingRect.y); + xcb_icccm_size_hints_set_size(&hints, 1, boundingRect.width, boundingRect.height); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + Test::applyMotifHints(c.get(), windowId, Test::MotifHints{ + .flags = Test::MWM_HINTS_DECORATIONS, + .decorations = 0, + }); + NETWinInfo info(c.get(), windowId, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); + info.setWindowType(NET::Normal); + info.setGtkFrameExtents(clientFrameExtent); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.last().first().value(); + QVERIFY(window); + QVERIFY(!window->isDecorated()); + QVERIFY(window->isClientSideDecorated()); + QCOMPARE(window->bufferGeometry(), RectF(0, 0, 120, 225)); + QCOMPARE(window->frameGeometry(), RectF(10, 5, 100, 200)); + + QMetaObject::invokeMethod(window, "setReadyForPainting"); + QVERIFY(window->readyForPainting()); + QVERIFY(Test::waitForWaylandSurface(window)); + + // Move pointer into the window, should trigger an enter. + QVERIFY(!window->frameGeometry().contains(Cursors::self()->mouse()->pos())); + QVERIFY(enteredSpy.isEmpty()); + input()->pointer()->warp(window->frameGeometry().center().toPoint()); + QCOMPARE(waylandServer()->seat()->focusedPointerSurface(), window->surface()); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.last().first().toPoint(), QPoint(60, 105)); + + // Move out of the window, should trigger a leave. + QVERIFY(leftSpy.isEmpty()); + input()->pointer()->warp(window->frameGeometry().bottomRight() + QPoint(100, 100)); + QVERIFY(leftSpy.wait()); + QCOMPARE(leftSpy.last().first().toPoint(), QPoint(60, 105)); + + // Destroy the window. + QSignalSpy windowClosedSpy(window, &X11Window::closed); + xcb_unmap_window(c.get(), windowId); + xcb_destroy_window(c.get(), windowId); + xcb_flush(c.get()); + QVERIFY(windowClosedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::XWaylandInputTest) +#include "xwayland_input_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/xwaylandserver_crash_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/xwaylandserver_crash_test.cpp new file mode 100644 index 0000000000..1a26539d22 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/xwaylandserver_crash_test.cpp @@ -0,0 +1,124 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "compositor.h" +#include "core/output.h" +#include "main.h" +#include "scene/workspacescene.h" +#include "wayland_server.h" +#include "workspace.h" +#include "x11window.h" +#include "xwayland/xwayland.h" +#include "xwayland/xwaylandlauncher.h" + +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_xwayland_server_crash-0"); + +class XwaylandServerCrashTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void testCrash(); +}; + +void XwaylandServerCrashTest::initTestCase() +{ + qRegisterMetaType(); + QVERIFY(waylandServer()->init(s_socketName)); + + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup xwaylandGroup = config->group(QStringLiteral("Xwayland")); + xwaylandGroup.writeEntry(QStringLiteral("XwaylandCrashPolicy"), QStringLiteral("Stop")); + xwaylandGroup.sync(); + kwinApp()->setConfig(config); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); + const auto outputs = workspace()->outputs(); + QCOMPARE(outputs.count(), 2); + QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); + QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); +} + +void XwaylandServerCrashTest::testCrash() +{ + // This test verifies that all connected X11 clients get destroyed when Xwayland crashes. + + // Create a normal window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + const Rect windowGeometry(0, 0, 100, 200); + xcb_window_t windowId1 = xcb_generate_id(c.get()); + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId1, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_size_hints_set_min_size(&hints, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId1, &hints); + xcb_map_window(c.get(), windowId1); + xcb_flush(c.get()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + QPointer window = windowCreatedSpy.last().first().value(); + QVERIFY(window); + QVERIFY(window->isDecorated()); + + // Create an override-redirect window. + xcb_window_t windowId2 = xcb_generate_id(c.get()); + const uint32_t values[] = {true}; + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId2, rootWindow(), + windowGeometry.x(), windowGeometry.y(), + windowGeometry.width(), windowGeometry.height(), 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, + XCB_CW_OVERRIDE_REDIRECT, values); + xcb_map_window(c.get(), windowId2); + xcb_flush(c.get()); + + QSignalSpy unmanagedAddedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(unmanagedAddedSpy.wait()); + QPointer unmanaged = unmanagedAddedSpy.last().first().value(); + QVERIFY(unmanaged); + + // Let's pretend that the Xwayland process has crashed. + QSignalSpy x11ConnectionChangedSpy(kwinApp(), &Application::x11ConnectionChanged); + Xwl::Xwayland *xwayland = static_cast(kwinApp()->xwayland()); + xwayland->xwaylandLauncher()->process()->terminate(); + QVERIFY(x11ConnectionChangedSpy.wait()); + + // When Xwayland crashes, the compositor should tear down the XCB connection and destroy + // all connected X11 clients. + QTRY_VERIFY(!window); + QTRY_VERIFY(!unmanaged); + QCOMPARE(kwinApp()->x11Connection(), nullptr); + QCOMPARE(kwinApp()->x11RootWindow(), XCB_WINDOW_NONE); + + // Render a frame to ensure that the compositor doesn't crash. + Compositor::self()->scene()->addRepaintFull(); + QSignalSpy frameRenderedSpy(Compositor::self()->scene(), &WorkspaceScene::frameRendered); + QVERIFY(frameRenderedSpy.wait()); +} + +} // namespace KWin + +WAYLANDTEST_MAIN(KWin::XwaylandServerCrashTest) +#include "xwaylandserver_crash_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/integration/xwaylandserver_restart_test.cpp b/local/recipes/kde/kwin/source/autotests/integration/xwaylandserver_restart_test.cpp new file mode 100644 index 0000000000..6275b09f8a --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/integration/xwaylandserver_restart_test.cpp @@ -0,0 +1,113 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwin_wayland_test.h" + +#include "compositor.h" +#include "main.h" +#include "scene/workspacescene.h" +#include "wayland_server.h" +#include "workspace.h" +#include "x11window.h" +#include "xwayland/xwayland.h" +#include "xwayland/xwaylandlauncher.h" + +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_xwayland_server_restart-0"); + +class XwaylandServerRestartTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void testRestart(); +}; + +void XwaylandServerRestartTest::initTestCase() +{ + QVERIFY(waylandServer()->init(s_socketName)); + + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup xwaylandGroup = config->group(QStringLiteral("Xwayland")); + xwaylandGroup.writeEntry(QStringLiteral("XwaylandCrashPolicy"), QStringLiteral("Restart")); + xwaylandGroup.sync(); + kwinApp()->setConfig(config); + + kwinApp()->start(); + Test::setOutputConfig({ + Rect(0, 0, 1280, 1024), + Rect(1280, 0, 1280, 1024), + }); +} + +static void kwin_safe_kill(QProcess *process) +{ + // The SIGKILL signal must be sent when the event loop is spinning. + QTimer::singleShot(1, process, &QProcess::kill); +} + +void XwaylandServerRestartTest::testRestart() +{ + // This test verifies that the Xwayland server will be restarted after a crash. + + Xwl::Xwayland *xwayland = static_cast(kwinApp()->xwayland()); + + QSignalSpy startedSpy(xwayland, &Xwl::Xwayland::started); + QSignalSpy stoppedSpy(xwayland, &Xwl::Xwayland::errorOccurred); + + Test::createX11Connection(); // trigger an X11 start + QTRY_COMPARE(startedSpy.count(), 1); + + // Pretend that the Xwayland process has crashed by sending a SIGKILL to it. + kwin_safe_kill(xwayland->xwaylandLauncher()->process()); + + QTRY_COMPARE(stoppedSpy.count(), 1); + + // Check that the compositor still accepts new X11 clients. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + + QTRY_COMPARE(startedSpy.count(), 2); + const Rect rect(0, 0, 100, 200); + xcb_window_t windowId = xcb_generate_id(c.get()); + xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), + rect.x(), rect.y(), rect.width(), rect.height(), 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints{}; + xcb_icccm_size_hints_set_position(&hints, 1, rect.x(), rect.y()); + xcb_icccm_size_hints_set_size(&hints, 1, rect.width(), rect.height()); + xcb_icccm_size_hints_set_min_size(&hints, rect.width(), rect.height()); + xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); + xcb_map_window(c.get(), windowId); + xcb_flush(c.get()); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); + QVERIFY(windowCreatedSpy.wait()); + X11Window *window = windowCreatedSpy.last().first().value(); + QVERIFY(window); + QCOMPARE(window->window(), windowId); + QVERIFY(window->isDecorated()); + + // Render a frame to ensure that the compositor doesn't crash. + Compositor::self()->scene()->addRepaintFull(); + QSignalSpy frameRenderedSpy(Compositor::self()->scene(), &WorkspaceScene::frameRendered); + QVERIFY(frameRenderedSpy.wait()); + + // Destroy the test window. + xcb_destroy_window(c.get(), windowId); + xcb_flush(c.get()); + QVERIFY(Test::waitForWindowClosed(window)); +} + +} // namespace KWin + +WAYLANDTEST_MAIN(KWin::XwaylandServerRestartTest) +#include "xwaylandserver_restart_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/libinput/CMakeLists.txt b/local/recipes/kde/kwin/source/autotests/libinput/CMakeLists.txt new file mode 100644 index 0000000000..a8ae8a42be --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/libinput/CMakeLists.txt @@ -0,0 +1,46 @@ +include_directories(${Libinput_INCLUDE_DIRS}) + +add_definitions(-DKWIN_BUILD_TESTING) +add_library(LibInputTestObjects STATIC ../../src/backends/libinput/device.cpp ../../src/backends/libinput/events.cpp ../../src/core/inputdevice.cpp ../../src/mousebuttons.cpp mock_libinput.cpp) +target_link_libraries(LibInputTestObjects Qt::Test Qt::Widgets Qt::DBus Qt::Gui KF6::ConfigCore) +target_include_directories(LibInputTestObjects PUBLIC ${CMAKE_SOURCE_DIR}/src) + +######################################################## +# Test Devices +######################################################## +add_executable(testLibinputDevice device_test.cpp) +target_link_libraries(testLibinputDevice Qt::Test Qt::DBus Qt::Gui KF6::ConfigCore LibInputTestObjects) +add_test(NAME kwin-testLibinputDevice COMMAND testLibinputDevice) +ecm_mark_as_test(testLibinputDevice) + +######################################################## +# Test Key Event +######################################################## +add_executable(testLibinputKeyEvent key_event_test.cpp) +target_link_libraries(testLibinputKeyEvent Qt::Test Qt::DBus Qt::Widgets KF6::ConfigCore LibInputTestObjects) +add_test(NAME kwin-testLibinputKeyEvent COMMAND testLibinputKeyEvent) +ecm_mark_as_test(testLibinputKeyEvent) + +######################################################## +# Test Pointer Event +######################################################## +add_executable(testLibinputPointerEvent pointer_event_test.cpp) +target_link_libraries(testLibinputPointerEvent Qt::Test Qt::DBus Qt::Widgets KF6::ConfigCore LibInputTestObjects) +add_test(NAME kwin-testLibinputPointerEvent COMMAND testLibinputPointerEvent) +ecm_mark_as_test(testLibinputPointerEvent) + +######################################################## +# Test Touch Event +######################################################## +add_executable(testLibinputTouchEvent touch_event_test.cpp) +target_link_libraries(testLibinputTouchEvent Qt::Test Qt::DBus Qt::Widgets KF6::ConfigCore LibInputTestObjects) +add_test(NAME kwin-testLibinputTouchEvent COMMAND testLibinputTouchEvent) +ecm_mark_as_test(testLibinputTouchEvent) + +######################################################## +# Test Gesture Event +######################################################## +add_executable(testLibinputGestureEvent gesture_event_test.cpp) +target_link_libraries(testLibinputGestureEvent Qt::Test Qt::DBus Qt::Widgets KF6::ConfigCore LibInputTestObjects) +add_test(NAME kwin-testLibinputGestureEvent COMMAND testLibinputGestureEvent) +ecm_mark_as_test(testLibinputGestureEvent) diff --git a/local/recipes/kde/kwin/source/autotests/libinput/device_test.cpp b/local/recipes/kde/kwin/source/autotests/libinput/device_test.cpp new file mode 100644 index 0000000000..88e37e8b44 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/libinput/device_test.cpp @@ -0,0 +1,2489 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include + +#include "backends/libinput/device.h" +#include "mock_libinput.h" + +#include + +#include +#include +#include +#include + +#include + +using namespace KWin::LibInput; + +class TestLibinputDevice : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testDeviceType_data(); + void testDeviceType(); + void testGestureSupport_data(); + void testGestureSupport(); + void testNames_data(); + void testNames(); + void testProduct(); + void testVendor(); + void testTapFingerCount(); + void testSize_data(); + void testSize(); + void testDefaultPointerAcceleration_data(); + void testDefaultPointerAcceleration(); + void testDefaultPointerAccelerationProfileFlat_data(); + void testDefaultPointerAccelerationProfileFlat(); + void testDefaultPointerAccelerationProfileAdaptive_data(); + void testDefaultPointerAccelerationProfileAdaptive(); + void testDefaultClickMethodAreas_data(); + void testDefaultClickMethodAreas(); + void testDefaultClickMethodClickfinger_data(); + void testDefaultClickMethodClickfinger(); + void testLeftHandedEnabledByDefault_data(); + void testLeftHandedEnabledByDefault(); + void testTapEnabledByDefault_data(); + void testTapEnabledByDefault(); + void testMiddleEmulationEnabledByDefault_data(); + void testMiddleEmulationEnabledByDefault(); + void testNaturalScrollEnabledByDefault_data(); + void testNaturalScrollEnabledByDefault(); + void testScrollTwoFingerEnabledByDefault_data(); + void testScrollTwoFingerEnabledByDefault(); + void testScrollEdgeEnabledByDefault_data(); + void testScrollEdgeEnabledByDefault(); + void testScrollOnButtonDownEnabledByDefault_data(); + void testScrollOnButtonDownEnabledByDefault(); + void testDisableWhileTypingEnabledByDefault_data(); + void testDisableWhileTypingEnabledByDefault(); + void testLmrTapButtonMapEnabledByDefault_data(); + void testLmrTapButtonMapEnabledByDefault(); + void testSupportsDisableWhileTyping_data(); + void testSupportsDisableWhileTyping(); + void testSupportsPointerAcceleration_data(); + void testSupportsPointerAcceleration(); + void testSupportsLeftHanded_data(); + void testSupportsLeftHanded(); + void testSupportsCalibrationMatrix_data(); + void testSupportsCalibrationMatrix(); + void testSupportsDisableEvents_data(); + void testSupportsDisableEvents(); + void testSupportsDisableEventsOnExternalMouse_data(); + void testSupportsDisableEventsOnExternalMouse(); + void testSupportsMiddleEmulation_data(); + void testSupportsMiddleEmulation(); + void testSupportsNaturalScroll_data(); + void testSupportsNaturalScroll(); + void testSupportsScrollTwoFinger_data(); + void testSupportsScrollTwoFinger(); + void testSupportsScrollEdge_data(); + void testSupportsScrollEdge(); + void testSupportsScrollOnButtonDown_data(); + void testSupportsScrollOnButtonDown(); + void testDefaultScrollButton_data(); + void testDefaultScrollButton(); + void testPointerAcceleration_data(); + void testPointerAcceleration(); + void testLeftHanded_data(); + void testLeftHanded(); + void testSupportedButtons_data(); + void testSupportedButtons(); + void testAlphaNumericKeyboard_data(); + void testAlphaNumericKeyboard(); + void testEnabled_data(); + void testEnabled(); + void testDisableEventsOnExternalMouseEnabledByDefault_data(); + void testDisableEventsOnExternalMouseEnabledByDefault(); + void testDisableEventsOnExternalMouse_data(); + void testDisableEventsOnExternalMouse(); + void testTapToClick_data(); + void testTapToClick(); + void testTapAndDragEnabledByDefault_data(); + void testTapAndDragEnabledByDefault(); + void testTapAndDrag_data(); + void testTapAndDrag(); + void testTapDragLockEnabledByDefault_data(); + void testTapDragLockEnabledByDefault(); + void testTapDragLock_data(); + void testTapDragLock(); + void testMiddleEmulation_data(); + void testMiddleEmulation(); + void testNaturalScroll_data(); + void testNaturalScroll(); + void testScrollFactor(); + void testScrollTwoFinger_data(); + void testScrollTwoFinger(); + void testScrollEdge_data(); + void testScrollEdge(); + void testScrollButtonDown_data(); + void testScrollButtonDown(); + void testScrollButton_data(); + void testScrollButton(); + void testDisableWhileTyping_data(); + void testDisableWhileTyping(); + void testLmrTapButtonMap_data(); + void testLmrTapButtonMap(); + void testLoadEnabled_data(); + void testLoadEnabled(); + void testLoadPointerAcceleration_data(); + void testLoadPointerAcceleration(); + void testLoadPointerAccelerationProfile_data(); + void testLoadPointerAccelerationProfile(); + void testLoadClickMethod_data(); + void testLoadClickMethod(); + void testLoadTapToClick_data(); + void testLoadTapToClick(); + void testLoadTapAndDrag_data(); + void testLoadTapAndDrag(); + void testLoadTapDragLock_data(); + void testLoadTapDragLock(); + void testLoadMiddleButtonEmulation_data(); + void testLoadMiddleButtonEmulation(); + void testLoadNaturalScroll_data(); + void testLoadNaturalScroll(); + void testLoadScrollMethod_data(); + void testLoadScrollMethod(); + void testLoadScrollButton_data(); + void testLoadScrollButton(); + void testLoadDisableWhileTyping_data(); + void testLoadDisableWhileTyping(); + void testLoadLmrTapButtonMap_data(); + void testLoadLmrTapButtonMap(); + void testLoadLeftHanded_data(); + void testLoadLeftHanded(); + void testLoadPressureCurve_data(); + void testLoadPressureCurve(); + void testOrientation_data(); + void testOrientation(); + void testCalibrationWithDefault(); + void testSwitch_data(); + void testSwitch(); +}; + +namespace +{ +template +T dbusProperty(const QString &name, const char *property) +{ + QDBusInterface interface{ + QStringLiteral("org.kde.kwin.tests.libinputdevice"), + QStringLiteral("/org/kde/KWin/InputDevice/") + name, + QStringLiteral("org.kde.KWin.InputDevice")}; + return interface.property(property).value(); +} +} + +void TestLibinputDevice::initTestCase() +{ + QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.kwin.tests.libinputdevice")); +} + +void TestLibinputDevice::testDeviceType_data() +{ + QTest::addColumn("keyboard"); + QTest::addColumn("pointer"); + QTest::addColumn("touch"); + QTest::addColumn("tabletTool"); + QTest::addColumn("switchDevice"); + + QTest::newRow("keyboard") << true << false << false << false << false; + QTest::newRow("pointer") << false << true << false << false << false; + QTest::newRow("touch") << false << false << true << false << false; + QTest::newRow("keyboard/pointer") << true << true << false << false << false; + QTest::newRow("keyboard/touch") << true << false << true << false << false; + QTest::newRow("pointer/touch") << false << true << true << false << false; + QTest::newRow("keyboard/pointer/touch") << true << true << true << false << false; + QTest::newRow("tabletTool") << false << false << false << true << false; + QTest::newRow("switch") << false << false << false << false << true; +} + +void TestLibinputDevice::testDeviceType() +{ + // this test verifies that the device type is recognized correctly + QFETCH(bool, keyboard); + QFETCH(bool, pointer); + QFETCH(bool, touch); + QFETCH(bool, tabletTool); + QFETCH(bool, switchDevice); + + libinput_device device; + device.keyboard = keyboard; + device.pointer = pointer; + device.touch = touch; + device.tabletTool = tabletTool; + device.switchDevice = switchDevice; + + Device d(&device); + QCOMPARE(d.isKeyboard(), keyboard); + QCOMPARE(d.property("keyboard").toBool(), keyboard); + QCOMPARE(dbusProperty(d.sysName(), "keyboard"), keyboard); + QCOMPARE(d.isPointer(), pointer); + QCOMPARE(d.property("pointer").toBool(), pointer); + QCOMPARE(dbusProperty(d.sysName(), "pointer"), pointer); + QCOMPARE(d.isTouch(), touch); + QCOMPARE(d.property("touch").toBool(), touch); + QCOMPARE(dbusProperty(d.sysName(), "touch"), touch); + QCOMPARE(d.isTabletPad(), false); + QCOMPARE(d.property("tabletPad").toBool(), false); + QCOMPARE(dbusProperty(d.sysName(), "tabletPad"), false); + QCOMPARE(d.isTabletTool(), tabletTool); + QCOMPARE(d.property("tabletTool").toBool(), tabletTool); + QCOMPARE(dbusProperty(d.sysName(), "tabletTool"), tabletTool); + QCOMPARE(d.isSwitch(), switchDevice); + QCOMPARE(d.property("switchDevice").toBool(), switchDevice); + QCOMPARE(dbusProperty(d.sysName(), "switchDevice"), switchDevice); + + QCOMPARE(d.device(), &device); +} + +void TestLibinputDevice::testGestureSupport_data() +{ + QTest::addColumn("supported"); + + QTest::newRow("supported") << true; + QTest::newRow("not supported") << false; +} + +void TestLibinputDevice::testGestureSupport() +{ + // this test verifies whether the Device supports gestures + QFETCH(bool, supported); + libinput_device device; + device.gestureSupported = supported; + + Device d(&device); + QCOMPARE(d.supportsGesture(), supported); + QCOMPARE(d.property("gestureSupport").toBool(), supported); + QCOMPARE(dbusProperty(d.sysName(), "gestureSupport"), supported); +} + +void TestLibinputDevice::testNames_data() +{ + QTest::addColumn("name"); + QTest::addColumn("sysName"); + QTest::addColumn("outputName"); + + QTest::newRow("empty") << QByteArray() << QByteArrayLiteral("event1") << QByteArray(); + QTest::newRow("set") << QByteArrayLiteral("awesome test device") << QByteArrayLiteral("event0") << QByteArrayLiteral("hdmi0"); +} + +void TestLibinputDevice::testNames() +{ + // this test verifies the various name properties of the Device + QFETCH(QByteArray, name); + QFETCH(QByteArray, sysName); + QFETCH(QByteArray, outputName); + libinput_device device; + device.name = name; + device.sysName = sysName; + device.outputName = outputName; + + Device d(&device); + QCOMPARE(d.name().toUtf8(), name); + QCOMPARE(d.property("name").toString().toUtf8(), name); + QCOMPARE(dbusProperty(d.sysName(), "name"), name); + QCOMPARE(d.sysName().toUtf8(), sysName); + QCOMPARE(d.property("sysName").toString().toUtf8(), sysName); + QCOMPARE(dbusProperty(d.sysName(), "sysName"), sysName); + QCOMPARE(d.outputName().toUtf8(), outputName); + QCOMPARE(d.property("outputName").toString().toUtf8(), outputName); + QCOMPARE(dbusProperty(d.sysName(), "outputName"), outputName); +} + +void TestLibinputDevice::testProduct() +{ + // this test verifies the product property + libinput_device device; + device.product = 100u; + Device d(&device); + QCOMPARE(d.product(), 100u); + QCOMPARE(d.property("product").toUInt(), 100u); + QCOMPARE(dbusProperty(d.sysName(), "product"), 100u); +} + +void TestLibinputDevice::testVendor() +{ + // this test verifies the vendor property + libinput_device device; + device.vendor = 200u; + Device d(&device); + QCOMPARE(d.vendor(), 200u); + QCOMPARE(d.property("vendor").toUInt(), 200u); + QCOMPARE(dbusProperty(d.sysName(), "vendor"), 200u); +} + +void TestLibinputDevice::testTapFingerCount() +{ + // this test verifies the tap finger count property + libinput_device device; + device.tapFingerCount = 3; + Device d(&device); + QCOMPARE(d.tapFingerCount(), 3); + QCOMPARE(d.property("tapFingerCount").toInt(), 3); + QCOMPARE(dbusProperty(d.sysName(), "tapFingerCount"), 3); +} + +void TestLibinputDevice::testSize_data() +{ + QTest::addColumn("setSize"); + QTest::addColumn("returnValue"); + QTest::addColumn("expectedSize"); + + QTest::newRow("10/20") << QSizeF(10.5, 20.2) << 0 << QSizeF(10.5, 20.2); + QTest::newRow("failure") << QSizeF(10, 20) << 1 << QSizeF(); +} + +void TestLibinputDevice::testSize() +{ + // this test verifies that getting the size works correctly including failures + QFETCH(QSizeF, setSize); + QFETCH(int, returnValue); + libinput_device device; + device.deviceSize = setSize; + device.deviceSizeReturnValue = returnValue; + + Device d(&device); + QTEST(d.size(), "expectedSize"); + QTEST(d.property("size").toSizeF(), "expectedSize"); + QTEST(dbusProperty(d.sysName(), "size"), "expectedSize"); +} + +void TestLibinputDevice::testLeftHandedEnabledByDefault_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testLeftHandedEnabledByDefault() +{ + QFETCH(bool, enabled); + libinput_device device; + device.leftHandedEnabledByDefault = enabled; + + Device d(&device); + QCOMPARE(d.leftHandedEnabledByDefault(), enabled); + QCOMPARE(d.property("leftHandedEnabledByDefault").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "leftHandedEnabledByDefault"), enabled); +} + +void TestLibinputDevice::testTapEnabledByDefault_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testTapEnabledByDefault() +{ + QFETCH(bool, enabled); + libinput_device device; + device.tapEnabledByDefault = enabled; + + Device d(&device); + QCOMPARE(d.tapToClickEnabledByDefault(), true); + QCOMPARE(d.property("tapToClickEnabledByDefault").toBool(), true); + QCOMPARE(dbusProperty(d.sysName(), "tapToClickEnabledByDefault"), true); +} + +void TestLibinputDevice::testMiddleEmulationEnabledByDefault_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testMiddleEmulationEnabledByDefault() +{ + QFETCH(bool, enabled); + libinput_device device; + device.middleEmulationEnabledByDefault = enabled; + + Device d(&device); + QCOMPARE(d.middleEmulationEnabledByDefault(), enabled); + QCOMPARE(d.property("middleEmulationEnabledByDefault").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "middleEmulationEnabledByDefault"), enabled); +} + +void TestLibinputDevice::testNaturalScrollEnabledByDefault_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testNaturalScrollEnabledByDefault() +{ + QFETCH(bool, enabled); + libinput_device device; + device.naturalScrollEnabledByDefault = enabled; + + Device d(&device); + QCOMPARE(d.naturalScrollEnabledByDefault(), enabled); + QCOMPARE(d.property("naturalScrollEnabledByDefault").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "naturalScrollEnabledByDefault"), enabled); +} + +void TestLibinputDevice::testScrollTwoFingerEnabledByDefault_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testScrollTwoFingerEnabledByDefault() +{ + QFETCH(bool, enabled); + libinput_device device; + device.defaultScrollMethod = enabled ? LIBINPUT_CONFIG_SCROLL_2FG : LIBINPUT_CONFIG_SCROLL_NO_SCROLL; + + Device d(&device); + QCOMPARE(d.scrollTwoFingerEnabledByDefault(), enabled); + QCOMPARE(d.property("scrollTwoFingerEnabledByDefault").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "scrollTwoFingerEnabledByDefault"), enabled); +} + +void TestLibinputDevice::testScrollEdgeEnabledByDefault_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testScrollEdgeEnabledByDefault() +{ + QFETCH(bool, enabled); + libinput_device device; + device.defaultScrollMethod = enabled ? LIBINPUT_CONFIG_SCROLL_EDGE : LIBINPUT_CONFIG_SCROLL_NO_SCROLL; + + Device d(&device); + QCOMPARE(d.scrollEdgeEnabledByDefault(), enabled); + QCOMPARE(d.property("scrollEdgeEnabledByDefault").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "scrollEdgeEnabledByDefault"), enabled); +} + +void TestLibinputDevice::testDefaultPointerAccelerationProfileFlat_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testDefaultPointerAccelerationProfileFlat() +{ + QFETCH(bool, enabled); + libinput_device device; + device.defaultPointerAccelerationProfile = enabled ? LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT : LIBINPUT_CONFIG_ACCEL_PROFILE_NONE; + + Device d(&device); + QCOMPARE(d.defaultPointerAccelerationProfileFlat(), enabled); + QCOMPARE(d.property("defaultPointerAccelerationProfileFlat").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "defaultPointerAccelerationProfileFlat"), enabled); +} + +void TestLibinputDevice::testDefaultPointerAccelerationProfileAdaptive_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testDefaultPointerAccelerationProfileAdaptive() +{ + QFETCH(bool, enabled); + libinput_device device; + device.defaultPointerAccelerationProfile = enabled ? LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE : LIBINPUT_CONFIG_ACCEL_PROFILE_NONE; + + Device d(&device); + QCOMPARE(d.defaultPointerAccelerationProfileAdaptive(), enabled); + QCOMPARE(d.property("defaultPointerAccelerationProfileAdaptive").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "defaultPointerAccelerationProfileAdaptive"), enabled); +} + +void TestLibinputDevice::testDefaultClickMethodAreas_data() +{ + QTest::addColumn("enabled"); + + QTest::addRow("enabled") << true; + QTest::addRow("disabled") << false; +} + +void TestLibinputDevice::testDefaultClickMethodAreas() +{ + QFETCH(bool, enabled); + libinput_device device; + device.defaultClickMethod = enabled ? LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS : LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER; + + Device d(&device); + QCOMPARE(d.defaultClickMethodAreas(), enabled); + QCOMPARE(d.property("defaultClickMethodAreas").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "defaultClickMethodAreas"), enabled); +} + +void TestLibinputDevice::testDefaultClickMethodClickfinger_data() +{ + QTest::addColumn("enabled"); + + QTest::addRow("enabled") << true; + QTest::addRow("disabled") << false; +} + +void TestLibinputDevice::testDefaultClickMethodClickfinger() +{ + QFETCH(bool, enabled); + libinput_device device; + device.defaultClickMethod = enabled ? LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER : LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS; + + Device d(&device); + QCOMPARE(d.defaultClickMethodClickfinger(), enabled); + QCOMPARE(d.property("defaultClickMethodClickfinger").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "defaultClickMethodClickfinger"), enabled); +} + +void TestLibinputDevice::testScrollOnButtonDownEnabledByDefault_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testScrollOnButtonDownEnabledByDefault() +{ + QFETCH(bool, enabled); + libinput_device device; + device.defaultScrollMethod = enabled ? LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN : LIBINPUT_CONFIG_SCROLL_NO_SCROLL; + + Device d(&device); + QCOMPARE(d.scrollOnButtonDownEnabledByDefault(), enabled); + QCOMPARE(d.property("scrollOnButtonDownEnabledByDefault").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "scrollOnButtonDownEnabledByDefault"), enabled); +} + +void TestLibinputDevice::testDefaultScrollButton_data() +{ + QTest::addColumn("button"); + + QTest::newRow("0") << 0u; + QTest::newRow("BTN_LEFT") << quint32(BTN_LEFT); + QTest::newRow("BTN_RIGHT") << quint32(BTN_RIGHT); + QTest::newRow("BTN_MIDDLE") << quint32(BTN_MIDDLE); + QTest::newRow("BTN_SIDE") << quint32(BTN_SIDE); + QTest::newRow("BTN_EXTRA") << quint32(BTN_EXTRA); + QTest::newRow("BTN_FORWARD") << quint32(BTN_FORWARD); + QTest::newRow("BTN_BACK") << quint32(BTN_BACK); + QTest::newRow("BTN_TASK") << quint32(BTN_TASK); +} + +void TestLibinputDevice::testDefaultScrollButton() +{ + libinput_device device; + QFETCH(quint32, button); + device.defaultScrollButton = button; + + Device d(&device); + QCOMPARE(d.defaultScrollButton(), button); + QCOMPARE(d.property("defaultScrollButton").value(), button); + QCOMPARE(dbusProperty(d.sysName(), "defaultScrollButton"), button); +} + +void TestLibinputDevice::testSupportsDisableWhileTyping_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testSupportsDisableWhileTyping() +{ + QFETCH(bool, enabled); + libinput_device device; + device.supportsDisableWhileTyping = enabled; + + Device d(&device); + QCOMPARE(d.supportsDisableWhileTyping(), enabled); + QCOMPARE(d.property("supportsDisableWhileTyping").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "supportsDisableWhileTyping"), enabled); +} + +void TestLibinputDevice::testSupportsPointerAcceleration_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testSupportsPointerAcceleration() +{ + QFETCH(bool, enabled); + libinput_device device; + device.supportsPointerAcceleration = enabled; + + Device d(&device); + QCOMPARE(d.supportsPointerAcceleration(), enabled); + QCOMPARE(d.property("supportsPointerAcceleration").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "supportsPointerAcceleration"), enabled); +} + +void TestLibinputDevice::testSupportsLeftHanded_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testSupportsLeftHanded() +{ + QFETCH(bool, enabled); + libinput_device device; + device.supportsLeftHanded = enabled; + + Device d(&device); + QCOMPARE(d.supportsLeftHanded(), enabled); + QCOMPARE(d.property("supportsLeftHanded").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "supportsLeftHanded"), enabled); +} + +void TestLibinputDevice::testSupportsCalibrationMatrix_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testSupportsCalibrationMatrix() +{ + QFETCH(bool, enabled); + libinput_device device; + device.supportsCalibrationMatrix = enabled; + + Device d(&device); + QCOMPARE(d.supportsCalibrationMatrix(), enabled); + QCOMPARE(d.property("supportsCalibrationMatrix").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "supportsCalibrationMatrix"), enabled); +} + +void TestLibinputDevice::testSupportsDisableEvents_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testSupportsDisableEvents() +{ + QFETCH(bool, enabled); + libinput_device device; + device.supportsDisableEvents = enabled; + + Device d(&device); + QCOMPARE(d.supportsDisableEvents(), enabled); + QCOMPARE(d.property("supportsDisableEvents").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "supportsDisableEvents"), enabled); +} + +void TestLibinputDevice::testSupportsDisableEventsOnExternalMouse_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testSupportsDisableEventsOnExternalMouse() +{ + QFETCH(bool, enabled); + libinput_device device; + device.supportsDisableEventsOnExternalMouse = enabled; + + Device d(&device); + QCOMPARE(d.supportsDisableEventsOnExternalMouse(), enabled); + QCOMPARE(d.property("supportsDisableEventsOnExternalMouse").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "supportsDisableEventsOnExternalMouse"), enabled); +} + +void TestLibinputDevice::testSupportsMiddleEmulation_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testSupportsMiddleEmulation() +{ + QFETCH(bool, enabled); + libinput_device device; + device.supportsMiddleEmulation = enabled; + + Device d(&device); + QCOMPARE(d.supportsMiddleEmulation(), enabled); + QCOMPARE(d.property("supportsMiddleEmulation").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "supportsMiddleEmulation"), enabled); +} + +void TestLibinputDevice::testSupportsNaturalScroll_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testSupportsNaturalScroll() +{ + QFETCH(bool, enabled); + libinput_device device; + device.supportsNaturalScroll = enabled; + + Device d(&device); + QCOMPARE(d.supportsNaturalScroll(), enabled); + QCOMPARE(d.property("supportsNaturalScroll").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "supportsNaturalScroll"), enabled); +} + +void TestLibinputDevice::testSupportsScrollTwoFinger_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testSupportsScrollTwoFinger() +{ + QFETCH(bool, enabled); + libinput_device device; + device.supportedScrollMethods = enabled ? LIBINPUT_CONFIG_SCROLL_2FG : LIBINPUT_CONFIG_SCROLL_NO_SCROLL; + + Device d(&device); + QCOMPARE(d.supportsScrollTwoFinger(), enabled); + QCOMPARE(d.property("supportsScrollTwoFinger").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "supportsScrollTwoFinger"), enabled); +} + +void TestLibinputDevice::testSupportsScrollEdge_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testSupportsScrollEdge() +{ + QFETCH(bool, enabled); + libinput_device device; + device.supportedScrollMethods = enabled ? LIBINPUT_CONFIG_SCROLL_EDGE : LIBINPUT_CONFIG_SCROLL_NO_SCROLL; + + Device d(&device); + QCOMPARE(d.supportsScrollEdge(), enabled); + QCOMPARE(d.property("supportsScrollEdge").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "supportsScrollEdge"), enabled); +} + +void TestLibinputDevice::testSupportsScrollOnButtonDown_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testSupportsScrollOnButtonDown() +{ + QFETCH(bool, enabled); + libinput_device device; + device.supportedScrollMethods = enabled ? LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN : LIBINPUT_CONFIG_SCROLL_NO_SCROLL; + + Device d(&device); + QCOMPARE(d.supportsScrollOnButtonDown(), enabled); + QCOMPARE(d.property("supportsScrollOnButtonDown").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "supportsScrollOnButtonDown"), enabled); +} + +void TestLibinputDevice::testDefaultPointerAcceleration_data() +{ + QTest::addColumn("accel"); + + QTest::newRow("-1.0") << -1.0; + QTest::newRow("-0.5") << -0.5; + QTest::newRow("0.0") << 0.0; + QTest::newRow("0.3") << 0.3; + QTest::newRow("1.0") << 1.0; +} + +void TestLibinputDevice::testDefaultPointerAcceleration() +{ + QFETCH(qreal, accel); + libinput_device device; + device.defaultPointerAcceleration = accel; + + Device d(&device); + QCOMPARE(d.defaultPointerAcceleration(), accel); + QCOMPARE(d.property("defaultPointerAcceleration").toReal(), accel); + QCOMPARE(dbusProperty(d.sysName(), "defaultPointerAcceleration"), accel); +} + +void TestLibinputDevice::testPointerAcceleration_data() +{ + QTest::addColumn("supported"); + QTest::addColumn("setShouldFail"); + QTest::addColumn("accel"); + QTest::addColumn("setAccel"); + QTest::addColumn("expectedAccel"); + QTest::addColumn("expectedChanged"); + + QTest::newRow("-1 -> 2.0") << true << false << -1.0 << 2.0 << 1.0 << true; + QTest::newRow("0 -> -1.0") << true << false << 0.0 << -1.0 << -1.0 << true; + QTest::newRow("1 -> 1") << true << false << 1.0 << 1.0 << 1.0 << false; + QTest::newRow("unsupported") << false << false << 0.0 << 1.0 << 0.0 << false; + QTest::newRow("set fails") << true << true << -1.0 << 1.0 << -1.0 << false; +} + +void TestLibinputDevice::testPointerAcceleration() +{ + QFETCH(bool, supported); + QFETCH(bool, setShouldFail); + QFETCH(qreal, accel); + libinput_device device; + device.supportsPointerAcceleration = supported; + device.pointerAcceleration = accel; + device.setPointerAccelerationReturnValue = setShouldFail; + + Device d(&device); + QCOMPARE(d.pointerAcceleration(), accel); + QCOMPARE(d.property("pointerAcceleration").toReal(), accel); + QCOMPARE(dbusProperty(d.sysName(), "pointerAcceleration"), accel); + + QSignalSpy pointerAccelChangedSpy(&d, &Device::pointerAccelerationChanged); + QFETCH(qreal, setAccel); + d.setPointerAcceleration(setAccel); + QTEST(d.pointerAcceleration(), "expectedAccel"); + QTEST(!pointerAccelChangedSpy.isEmpty(), "expectedChanged"); + QTEST(dbusProperty(d.sysName(), "pointerAcceleration"), "expectedAccel"); +} + +void TestLibinputDevice::testLeftHanded_data() +{ + QTest::addColumn("supported"); + QTest::addColumn("setShouldFail"); + QTest::addColumn("initValue"); + QTest::addColumn("setValue"); + QTest::addColumn("expectedValue"); + + QTest::newRow("unsupported/true") << false << false << true << false << false; + QTest::newRow("unsupported/false") << false << false << false << true << false; + QTest::newRow("true -> false") << true << false << true << false << false; + QTest::newRow("false -> true") << true << false << false << true << true; + QTest::newRow("set fails") << true << true << true << false << true; + QTest::newRow("true -> true") << true << false << true << true << true; + QTest::newRow("false -> false") << true << false << false << false << false; +} + +void TestLibinputDevice::testLeftHanded() +{ + QFETCH(bool, supported); + QFETCH(bool, setShouldFail); + QFETCH(bool, initValue); + libinput_device device; + device.supportsLeftHanded = supported; + device.leftHanded = initValue; + device.setLeftHandedReturnValue = setShouldFail; + + Device d(&device); + QCOMPARE(d.isLeftHanded(), supported && initValue); + QCOMPARE(d.property("leftHanded").toBool(), supported && initValue); + QCOMPARE(dbusProperty(d.sysName(), "leftHanded"), supported && initValue); + + QSignalSpy leftHandedChangedSpy(&d, &Device::leftHandedChanged); + QFETCH(bool, setValue); + d.setLeftHanded(setValue); + QFETCH(bool, expectedValue); + QCOMPARE(d.isLeftHanded(), expectedValue); + QCOMPARE(leftHandedChangedSpy.isEmpty(), (supported && initValue) == expectedValue); + QCOMPARE(dbusProperty(d.sysName(), "leftHanded"), expectedValue); +} + +void TestLibinputDevice::testSupportedButtons_data() +{ + QTest::addColumn("isPointer"); + QTest::addColumn("setButtons"); + QTest::addColumn("expectedButtons"); + + QTest::newRow("left") << true << Qt::MouseButtons(Qt::LeftButton) << Qt::MouseButtons(Qt::LeftButton); + QTest::newRow("right") << true << Qt::MouseButtons(Qt::RightButton) << Qt::MouseButtons(Qt::RightButton); + QTest::newRow("middle") << true << Qt::MouseButtons(Qt::MiddleButton) << Qt::MouseButtons(Qt::MiddleButton); + QTest::newRow("extra1") << true << Qt::MouseButtons(Qt::ExtraButton1) << Qt::MouseButtons(Qt::ExtraButton1); + QTest::newRow("extra2") << true << Qt::MouseButtons(Qt::ExtraButton2) << Qt::MouseButtons(Qt::ExtraButton2); + QTest::newRow("back") << true << Qt::MouseButtons(Qt::BackButton) << Qt::MouseButtons(Qt::BackButton); + QTest::newRow("forward") << true << Qt::MouseButtons(Qt::ForwardButton) << Qt::MouseButtons(Qt::ForwardButton); + QTest::newRow("task") << true << Qt::MouseButtons(Qt::TaskButton) << Qt::MouseButtons(Qt::TaskButton); + + QTest::newRow("no pointer/left") << false << Qt::MouseButtons(Qt::LeftButton) << Qt::MouseButtons(); + QTest::newRow("no pointer/right") << false << Qt::MouseButtons(Qt::RightButton) << Qt::MouseButtons(); + QTest::newRow("no pointer/middle") << false << Qt::MouseButtons(Qt::MiddleButton) << Qt::MouseButtons(); + QTest::newRow("no pointer/extra1") << false << Qt::MouseButtons(Qt::ExtraButton1) << Qt::MouseButtons(); + QTest::newRow("no pointer/extra2") << false << Qt::MouseButtons(Qt::ExtraButton2) << Qt::MouseButtons(); + QTest::newRow("no pointer/back") << false << Qt::MouseButtons(Qt::BackButton) << Qt::MouseButtons(); + QTest::newRow("no pointer/forward") << false << Qt::MouseButtons(Qt::ForwardButton) << Qt::MouseButtons(); + QTest::newRow("no pointer/task") << false << Qt::MouseButtons(Qt::TaskButton) << Qt::MouseButtons(); + + QTest::newRow("all") << true + << Qt::MouseButtons(Qt::LeftButton | Qt::RightButton | Qt::MiddleButton | Qt::ExtraButton1 | Qt::ExtraButton2 | Qt::BackButton | Qt::ForwardButton | Qt::TaskButton) + << Qt::MouseButtons(Qt::LeftButton | Qt::RightButton | Qt::MiddleButton | Qt::ExtraButton1 | Qt::ExtraButton2 | Qt::BackButton | Qt::ForwardButton | Qt::TaskButton); +} + +void TestLibinputDevice::testSupportedButtons() +{ + libinput_device device; + QFETCH(bool, isPointer); + device.pointer = isPointer; + QFETCH(Qt::MouseButtons, setButtons); + device.supportedButtons = setButtons; + + Device d(&device); + QCOMPARE(d.isPointer(), isPointer); + QTEST(d.supportedButtons(), "expectedButtons"); + QTEST(Qt::MouseButtons(dbusProperty(d.sysName(), "supportedButtons")), "expectedButtons"); +} + +void TestLibinputDevice::testAlphaNumericKeyboard_data() +{ + QTest::addColumn>("supportedKeys"); + QTest::addColumn("isAlpha"); + + QList keys; + + for (int i = KEY_1; i <= KEY_0; i++) { + keys << i; + QByteArray row = QByteArrayLiteral("number"); + row.append(QByteArray::number(i)); + QTest::newRow(row.constData()) << keys << false; + } + for (int i = KEY_Q; i <= KEY_P; i++) { + keys << i; + QByteArray row = QByteArrayLiteral("alpha"); + row.append(QByteArray::number(i)); + QTest::newRow(row.constData()) << keys << false; + } + for (int i = KEY_A; i <= KEY_L; i++) { + keys << i; + QByteArray row = QByteArrayLiteral("alpha"); + row.append(QByteArray::number(i)); + QTest::newRow(row.constData()) << keys << false; + } + for (int i = KEY_Z; i < KEY_M; i++) { + keys << i; + QByteArray row = QByteArrayLiteral("alpha"); + row.append(QByteArray::number(i)); + QTest::newRow(row.constData()) << keys << false; + } + // adding a different key should not result in it becoming alphanumeric keyboard + keys << KEY_SEMICOLON; + QTest::newRow("semicolon") << keys << false; + + // last but not least the M which should turn everything on + keys << KEY_M; + QTest::newRow("alphanumeric") << keys << true; +} + +void TestLibinputDevice::testAlphaNumericKeyboard() +{ + QFETCH(QList, supportedKeys); + libinput_device device; + device.keyboard = true; + device.keys = supportedKeys; + + Device d(&device); + QCOMPARE(d.isKeyboard(), true); + QTEST(d.isAlphaNumericKeyboard(), "isAlpha"); + QTEST(dbusProperty(d.sysName(), "alphaNumericKeyboard"), "isAlpha"); +} + +void TestLibinputDevice::testEnabled_data() +{ + QTest::addColumn("supported"); + QTest::addColumn("setShouldFail"); + QTest::addColumn("initValue"); + QTest::addColumn("setValue"); + QTest::addColumn("expectedValue"); + QTest::addColumn("disableEventsOnExternalMouse"); + + QTest::newRow("unsupported/true") << false << false << true << false << true << false; + QTest::newRow("unsupported/false") << false << false << false << true << true << false; + QTest::newRow("true -> false") << true << false << true << false << false << false; + QTest::newRow("false -> true") << true << false << false << true << true << false; + QTest::newRow("true w/ disableEventsOnExternalMouse -> false") << true << false << true << false << false << true; + QTest::newRow("set fails") << true << true << true << false << true << false; + QTest::newRow("true -> true") << true << false << true << true << true << false; + QTest::newRow("true w/ disableEventsOnExternalMouse -> true") << true << false << true << true << true << true; + QTest::newRow("false -> false") << true << false << false << false << false << false; +} + +void TestLibinputDevice::testEnabled() +{ + libinput_device device; + QFETCH(bool, supported); + QFETCH(bool, setShouldFail); + QFETCH(bool, initValue); + QFETCH(bool, disableEventsOnExternalMouse); + device.supportsDisableEvents = supported; + device.enabled = initValue; + device.supportsDisableEventsOnExternalMouse = disableEventsOnExternalMouse; + device.disableEventsOnExternalMouse = disableEventsOnExternalMouse; + device.setEnableModeReturnValue = setShouldFail; + + Device d(&device); + QCOMPARE(d.isEnabled(), !supported || initValue); + QCOMPARE(d.property("enabled").toBool(), !supported || initValue); + QCOMPARE(dbusProperty(d.sysName(), "enabled"), !supported || initValue); + + QSignalSpy enabledChangedSpy(&d, &Device::enabledChanged); + QFETCH(bool, setValue); + d.setEnabled(setValue); + QFETCH(bool, expectedValue); + QCOMPARE(enabledChangedSpy.isEmpty(), !supported || initValue == expectedValue); + QCOMPARE(d.isEnabled(), expectedValue); + QCOMPARE(dbusProperty(d.sysName(), "enabled"), expectedValue); + + // make sure we didn't mess up the other part of Send Events Mode + QCOMPARE(d.isDisableEventsOnExternalMouse(), disableEventsOnExternalMouse); + QCOMPARE(dbusProperty(d.sysName(), "disableEventsOnExternalMouse"), disableEventsOnExternalMouse); +} + +void TestLibinputDevice::testDisableEventsOnExternalMouseEnabledByDefault_data() +{ + QTest::addColumn("supported"); + QTest::addColumn("expectedValue"); + + QTest::newRow("supported") << true << false; + QTest::newRow("unsupported") << false << false; +} + +void TestLibinputDevice::testDisableEventsOnExternalMouseEnabledByDefault() +{ + QFETCH(bool, supported); + QFETCH(bool, expectedValue); + libinput_device device; + device.supportsDisableEventsOnExternalMouse = supported; + + Device d(&device); + QCOMPARE(d.disableEventsOnExternalMouseEnabledByDefault(), expectedValue); + QCOMPARE(d.property("disableEventsOnExternalMouseEnabledByDefault").toBool(), expectedValue); + QCOMPARE(dbusProperty(d.sysName(), "disableEventsOnExternalMouseEnabledByDefault"), expectedValue); +} + +void TestLibinputDevice::testDisableEventsOnExternalMouse_data() +{ + QTest::addColumn("supported"); + QTest::addColumn("setShouldFail"); + QTest::addColumn("initValue"); + QTest::addColumn("setValue"); + QTest::addColumn("expectedValue"); + QTest::addColumn("deviceEnabled"); + + QTest::newRow("unsupported/true") << false << false << true << false << false << true; + QTest::newRow("unsupported/false") << false << false << false << true << false << true; + QTest::newRow("true -> false") << true << false << true << false << false << true; + QTest::newRow("false -> true") << true << false << false << true << true << true; + QTest::newRow("true -> false with device disabled") << true << false << true << false << false << false; + QTest::newRow("false -> true with device disabled") << true << false << false << true << true << false; + QTest::newRow("set fails") << true << true << true << false << true << true; + QTest::newRow("true -> true") << true << false << true << true << true << true; + QTest::newRow("false -> false") << true << false << false << false << false << true; +} + +void TestLibinputDevice::testDisableEventsOnExternalMouse() +{ + libinput_device device; + QFETCH(bool, supported); + QFETCH(bool, setShouldFail); + QFETCH(bool, initValue); + QFETCH(bool, deviceEnabled); + device.supportsDisableEvents = !deviceEnabled; + device.enabled = deviceEnabled; + device.supportsDisableEventsOnExternalMouse = supported; + device.disableEventsOnExternalMouse = initValue; + device.setEnableModeReturnValue = setShouldFail; + + Device d(&device); + QCOMPARE(d.isDisableEventsOnExternalMouse(), supported && initValue); + QCOMPARE(d.property("disableEventsOnExternalMouse").toBool(), supported && initValue); + QCOMPARE(dbusProperty(d.sysName(), "disableEventsOnExternalMouse"), supported && initValue); + + QSignalSpy disableEventsOnExternalMouseChangedSpy(&d, &Device::disableEventsOnExternalMouseChanged); + QFETCH(bool, setValue); + d.setDisableEventsOnExternalMouse(setValue); + QFETCH(bool, expectedValue); + QCOMPARE(d.isDisableEventsOnExternalMouse(), expectedValue); + QCOMPARE(disableEventsOnExternalMouseChangedSpy.isEmpty(), !supported || initValue == expectedValue); + QCOMPARE(dbusProperty(d.sysName(), "disableEventsOnExternalMouse"), expectedValue); + + // make sure we didn't mess up the other part of Send Events Mode + QCOMPARE(d.isEnabled(), deviceEnabled); + QCOMPARE(dbusProperty(d.sysName(), "enabled"), deviceEnabled); +} + +void TestLibinputDevice::testTapToClick_data() +{ + QTest::addColumn("fingerCount"); + QTest::addColumn("initValue"); + QTest::addColumn("setValue"); + QTest::addColumn("setShouldFail"); + QTest::addColumn("expectedValue"); + + QTest::newRow("unsupported") << 0 << false << true << true << false; + QTest::newRow("true -> false") << 1 << true << false << false << false; + QTest::newRow("false -> true") << 2 << false << true << false << true; + QTest::newRow("set fails") << 3 << true << false << true << true; + QTest::newRow("true -> true") << 2 << true << true << false << true; + QTest::newRow("false -> false") << 1 << false << false << false << false; +} + +void TestLibinputDevice::testTapToClick() +{ + libinput_device device; + QFETCH(int, fingerCount); + QFETCH(bool, initValue); + QFETCH(bool, setShouldFail); + device.tapFingerCount = fingerCount; + device.tapToClick = initValue; + device.setTapToClickReturnValue = setShouldFail; + + Device d(&device); + QCOMPARE(d.tapFingerCount(), fingerCount); + QCOMPARE(d.isTapToClick(), initValue); + QCOMPARE(d.property("tapToClick").toBool(), initValue); + QCOMPARE(dbusProperty(d.sysName(), "tapToClick"), initValue); + + QSignalSpy tapToClickChangedSpy(&d, &Device::tapToClickChanged); + QFETCH(bool, setValue); + d.setTapToClick(setValue); + QFETCH(bool, expectedValue); + QCOMPARE(d.isTapToClick(), expectedValue); + QCOMPARE(tapToClickChangedSpy.isEmpty(), initValue == expectedValue); + QCOMPARE(dbusProperty(d.sysName(), "tapToClick"), expectedValue); +} + +void TestLibinputDevice::testTapAndDragEnabledByDefault_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testTapAndDragEnabledByDefault() +{ + QFETCH(bool, enabled); + libinput_device device; + device.tapAndDragEnabledByDefault = enabled; + + Device d(&device); + QCOMPARE(d.tapAndDragEnabledByDefault(), true); + QCOMPARE(d.property("tapAndDragEnabledByDefault").toBool(), true); + QCOMPARE(dbusProperty(d.sysName(), "tapAndDragEnabledByDefault"), true); +} + +void TestLibinputDevice::testTapAndDrag_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("setValue"); + QTest::addColumn("setShouldFail"); + QTest::addColumn("expectedValue"); + + QTest::newRow("true -> false") << true << false << false << false; + QTest::newRow("false -> true") << false << true << false << true; + QTest::newRow("set fails") << true << false << true << true; + QTest::newRow("true -> true") << true << true << false << true; + QTest::newRow("false -> false") << false << false << false << false; +} + +void TestLibinputDevice::testTapAndDrag() +{ + libinput_device device; + QFETCH(bool, initValue); + QFETCH(bool, setShouldFail); + device.tapAndDrag = initValue; + device.setTapAndDragReturnValue = setShouldFail; + + Device d(&device); + QCOMPARE(d.isTapAndDrag(), initValue); + QCOMPARE(d.property("tapAndDrag").toBool(), initValue); + QCOMPARE(dbusProperty(d.sysName(), "tapAndDrag"), initValue); + + QSignalSpy tapAndDragChangedSpy(&d, &Device::tapAndDragChanged); + QFETCH(bool, setValue); + d.setTapAndDrag(setValue); + QFETCH(bool, expectedValue); + QCOMPARE(d.isTapAndDrag(), expectedValue); + QCOMPARE(tapAndDragChangedSpy.isEmpty(), initValue == expectedValue); + QCOMPARE(dbusProperty(d.sysName(), "tapAndDrag"), expectedValue); +} + +void TestLibinputDevice::testTapDragLockEnabledByDefault_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testTapDragLockEnabledByDefault() +{ + QFETCH(bool, enabled); + libinput_device device; + device.tapDragLockEnabledByDefault = enabled; + + Device d(&device); + QCOMPARE(d.tapDragLockEnabledByDefault(), enabled); + QCOMPARE(d.property("tapDragLockEnabledByDefault").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "tapDragLockEnabledByDefault"), enabled); +} + +void TestLibinputDevice::testTapDragLock_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("setValue"); + QTest::addColumn("setShouldFail"); + QTest::addColumn("expectedValue"); + + QTest::newRow("true -> false") << true << false << false << false; + QTest::newRow("false -> true") << false << true << false << true; + QTest::newRow("set fails") << true << false << true << true; + QTest::newRow("true -> true") << true << true << false << true; + QTest::newRow("false -> false") << false << false << false << false; +} + +void TestLibinputDevice::testTapDragLock() +{ + libinput_device device; + QFETCH(bool, initValue); + QFETCH(bool, setShouldFail); + device.tapDragLock = initValue; + device.setTapDragLockReturnValue = setShouldFail; + + Device d(&device); + QCOMPARE(d.isTapDragLock(), initValue); + QCOMPARE(d.property("tapDragLock").toBool(), initValue); + QCOMPARE(dbusProperty(d.sysName(), "tapDragLock"), initValue); + + QSignalSpy tapDragLockChangedSpy(&d, &Device::tapDragLockChanged); + QFETCH(bool, setValue); + d.setTapDragLock(setValue); + QFETCH(bool, expectedValue); + QCOMPARE(d.isTapDragLock(), expectedValue); + QCOMPARE(tapDragLockChangedSpy.isEmpty(), initValue == expectedValue); + QCOMPARE(dbusProperty(d.sysName(), "tapDragLock"), expectedValue); +} + +void TestLibinputDevice::testMiddleEmulation_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("setValue"); + QTest::addColumn("setShouldFail"); + QTest::addColumn("expectedValue"); + QTest::addColumn("supportsMiddleButton"); + + QTest::newRow("true -> false") << true << false << false << false << true; + QTest::newRow("false -> true") << false << true << false << true << true; + QTest::newRow("set fails") << true << false << true << true << true; + QTest::newRow("true -> true") << true << true << false << true << true; + QTest::newRow("false -> false") << false << false << false << false << true; + + QTest::newRow("false -> true, unsupported") << false << true << true << false << false; +} + +void TestLibinputDevice::testMiddleEmulation() +{ + libinput_device device; + QFETCH(bool, initValue); + QFETCH(bool, setShouldFail); + QFETCH(bool, supportsMiddleButton); + device.supportsMiddleEmulation = supportsMiddleButton; + device.middleEmulation = initValue; + device.setMiddleEmulationReturnValue = setShouldFail; + + Device d(&device); + QCOMPARE(d.isMiddleEmulation(), initValue); + QCOMPARE(d.property("middleEmulation").toBool(), initValue); + QCOMPARE(dbusProperty(d.sysName(), "middleEmulation"), initValue); + + QSignalSpy middleEmulationChangedSpy(&d, &Device::middleEmulationChanged); + QFETCH(bool, setValue); + d.setMiddleEmulation(setValue); + QFETCH(bool, expectedValue); + QCOMPARE(d.isMiddleEmulation(), expectedValue); + QCOMPARE(d.property("middleEmulation").toBool(), expectedValue); + QCOMPARE(middleEmulationChangedSpy.isEmpty(), initValue == expectedValue); + QCOMPARE(dbusProperty(d.sysName(), "middleEmulation"), expectedValue); +} + +void TestLibinputDevice::testNaturalScroll_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("setValue"); + QTest::addColumn("setShouldFail"); + QTest::addColumn("expectedValue"); + QTest::addColumn("supportsNaturalScroll"); + + QTest::newRow("true -> false") << true << false << false << false << true; + QTest::newRow("false -> true") << false << true << false << true << true; + QTest::newRow("set fails") << true << false << true << true << true; + QTest::newRow("true -> true") << true << true << false << true << true; + QTest::newRow("false -> false") << false << false << false << false << true; + + QTest::newRow("false -> true, unsupported") << false << true << true << false << false; +} + +void TestLibinputDevice::testNaturalScroll() +{ + libinput_device device; + QFETCH(bool, initValue); + QFETCH(bool, setShouldFail); + QFETCH(bool, supportsNaturalScroll); + device.supportsNaturalScroll = supportsNaturalScroll; + device.naturalScroll = initValue; + device.setNaturalScrollReturnValue = setShouldFail; + + Device d(&device); + QCOMPARE(d.isNaturalScroll(), initValue); + QCOMPARE(d.property("naturalScroll").toBool(), initValue); + QCOMPARE(dbusProperty(d.sysName(), "naturalScroll"), initValue); + + QSignalSpy naturalScrollChangedSpy(&d, &Device::naturalScrollChanged); + QFETCH(bool, setValue); + d.setNaturalScroll(setValue); + QFETCH(bool, expectedValue); + QCOMPARE(d.isNaturalScroll(), expectedValue); + QCOMPARE(d.property("naturalScroll").toBool(), expectedValue); + QCOMPARE(naturalScrollChangedSpy.isEmpty(), initValue == expectedValue); + QCOMPARE(dbusProperty(d.sysName(), "naturalScroll"), expectedValue); +} + +void TestLibinputDevice::testScrollFactor() +{ + libinput_device device; + + qreal initValue = 1.0; + + Device d(&device); + QCOMPARE(d.scrollFactor(), initValue); + QCOMPARE(d.property("scrollFactor").toReal(), initValue); + QCOMPARE(dbusProperty(d.sysName(), "scrollFactor"), initValue); + + QSignalSpy scrollFactorChangedSpy(&d, &Device::scrollFactorChanged); + + qreal expectedValue = 2.0; + + d.setScrollFactor(expectedValue); + QCOMPARE(d.scrollFactor(), expectedValue); + QCOMPARE(d.property("scrollFactor").toReal(), expectedValue); + QCOMPARE(scrollFactorChangedSpy.isEmpty(), false); + QCOMPARE(dbusProperty(d.sysName(), "scrollFactor"), expectedValue); +} + +void TestLibinputDevice::testScrollTwoFinger_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("otherValue"); + QTest::addColumn("setValue"); + QTest::addColumn("setShouldFail"); + QTest::addColumn("expectedValue"); + QTest::addColumn("supportsScrollTwoFinger"); + + QTest::newRow("true -> false") << true << false << false << false << false << true; + QTest::newRow("other -> false") << false << true << false << false << false << true; + QTest::newRow("false -> true") << false << false << true << false << true << true; + QTest::newRow("set fails") << true << false << false << true << true << true; + QTest::newRow("true -> true") << true << false << true << false << true << true; + QTest::newRow("false -> false") << false << false << false << false << false << true; + + QTest::newRow("false -> true, unsupported") << false << false << true << true << false << false; +} + +void TestLibinputDevice::testScrollTwoFinger() +{ + libinput_device device; + QFETCH(bool, initValue); + QFETCH(bool, otherValue); + QFETCH(bool, setShouldFail); + QFETCH(bool, supportsScrollTwoFinger); + device.supportedScrollMethods = (supportsScrollTwoFinger ? LIBINPUT_CONFIG_SCROLL_2FG : LIBINPUT_CONFIG_SCROLL_NO_SCROLL) | LIBINPUT_CONFIG_SCROLL_EDGE; + device.scrollMethod = initValue ? LIBINPUT_CONFIG_SCROLL_2FG : otherValue ? LIBINPUT_CONFIG_SCROLL_EDGE + : LIBINPUT_CONFIG_SCROLL_NO_SCROLL; + device.setScrollMethodReturnValue = setShouldFail; + + Device d(&device); + QCOMPARE(d.isScrollTwoFinger(), initValue); + QCOMPARE(d.property("scrollTwoFinger").toBool(), initValue); + QCOMPARE(d.property("scrollEdge").toBool(), otherValue); + QCOMPARE(dbusProperty(d.sysName(), "scrollTwoFinger"), initValue); + QCOMPARE(dbusProperty(d.sysName(), "scrollEdge"), otherValue); + + QSignalSpy scrollMethodChangedSpy(&d, &Device::scrollMethodChanged); + QFETCH(bool, setValue); + d.setScrollTwoFinger(setValue); + QFETCH(bool, expectedValue); + QCOMPARE(d.isScrollTwoFinger(), expectedValue); + QCOMPARE(d.property("scrollTwoFinger").toBool(), expectedValue); + QCOMPARE(scrollMethodChangedSpy.isEmpty(), initValue == expectedValue); + QCOMPARE(dbusProperty(d.sysName(), "scrollTwoFinger"), expectedValue); +} + +void TestLibinputDevice::testScrollEdge_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("otherValue"); + QTest::addColumn("setValue"); + QTest::addColumn("setShouldFail"); + QTest::addColumn("expectedValue"); + QTest::addColumn("supportsScrollEdge"); + + QTest::newRow("true -> false") << true << false << false << false << false << true; + QTest::newRow("other -> false") << false << true << false << false << false << true; + QTest::newRow("false -> true") << false << false << true << false << true << true; + QTest::newRow("set fails") << true << false << false << true << true << true; + QTest::newRow("true -> true") << true << false << true << false << true << true; + QTest::newRow("false -> false") << false << false << false << false << false << true; + + QTest::newRow("false -> true, unsupported") << false << false << true << true << false << false; +} + +void TestLibinputDevice::testScrollEdge() +{ + libinput_device device; + QFETCH(bool, initValue); + QFETCH(bool, otherValue); + QFETCH(bool, setShouldFail); + QFETCH(bool, supportsScrollEdge); + device.supportedScrollMethods = (supportsScrollEdge ? LIBINPUT_CONFIG_SCROLL_EDGE : LIBINPUT_CONFIG_SCROLL_NO_SCROLL) | LIBINPUT_CONFIG_SCROLL_2FG; + device.scrollMethod = initValue ? LIBINPUT_CONFIG_SCROLL_EDGE : otherValue ? LIBINPUT_CONFIG_SCROLL_2FG + : LIBINPUT_CONFIG_SCROLL_NO_SCROLL; + device.setScrollMethodReturnValue = setShouldFail; + + Device d(&device); + QCOMPARE(d.isScrollEdge(), initValue); + QCOMPARE(d.property("scrollEdge").toBool(), initValue); + QCOMPARE(d.property("scrollTwoFinger").toBool(), otherValue); + QCOMPARE(dbusProperty(d.sysName(), "scrollEdge"), initValue); + QCOMPARE(dbusProperty(d.sysName(), "scrollTwoFinger"), otherValue); + + QSignalSpy scrollMethodChangedSpy(&d, &Device::scrollMethodChanged); + QFETCH(bool, setValue); + d.setScrollEdge(setValue); + QFETCH(bool, expectedValue); + QCOMPARE(d.isScrollEdge(), expectedValue); + QCOMPARE(d.property("scrollEdge").toBool(), expectedValue); + QCOMPARE(scrollMethodChangedSpy.isEmpty(), initValue == expectedValue); + QCOMPARE(dbusProperty(d.sysName(), "scrollEdge"), expectedValue); +} + +void TestLibinputDevice::testScrollButtonDown_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("otherValue"); + QTest::addColumn("setValue"); + QTest::addColumn("setShouldFail"); + QTest::addColumn("expectedValue"); + QTest::addColumn("supportsScrollButtonDown"); + + QTest::newRow("true -> false") << true << false << false << false << false << true; + QTest::newRow("other -> false") << false << true << false << false << false << true; + QTest::newRow("false -> true") << false << false << true << false << true << true; + QTest::newRow("set fails") << true << false << false << true << true << true; + QTest::newRow("true -> true") << true << false << true << false << true << true; + QTest::newRow("false -> false") << false << false << false << false << false << true; + + QTest::newRow("false -> true, unsupported") << false << false << true << true << false << false; +} + +void TestLibinputDevice::testScrollButtonDown() +{ + libinput_device device; + QFETCH(bool, initValue); + QFETCH(bool, otherValue); + QFETCH(bool, setShouldFail); + QFETCH(bool, supportsScrollButtonDown); + device.supportedScrollMethods = (supportsScrollButtonDown ? LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN : LIBINPUT_CONFIG_SCROLL_NO_SCROLL) | LIBINPUT_CONFIG_SCROLL_2FG; + device.scrollMethod = initValue ? LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN : otherValue ? LIBINPUT_CONFIG_SCROLL_2FG + : LIBINPUT_CONFIG_SCROLL_NO_SCROLL; + device.setScrollMethodReturnValue = setShouldFail; + + Device d(&device); + QCOMPARE(d.isScrollOnButtonDown(), initValue); + QCOMPARE(d.property("scrollOnButtonDown").toBool(), initValue); + QCOMPARE(d.property("scrollTwoFinger").toBool(), otherValue); + QCOMPARE(dbusProperty(d.sysName(), "scrollOnButtonDown"), initValue); + QCOMPARE(dbusProperty(d.sysName(), "scrollTwoFinger"), otherValue); + + QSignalSpy scrollMethodChangedSpy(&d, &Device::scrollMethodChanged); + QFETCH(bool, setValue); + d.setScrollOnButtonDown(setValue); + QFETCH(bool, expectedValue); + QCOMPARE(d.isScrollOnButtonDown(), expectedValue); + QCOMPARE(d.property("scrollOnButtonDown").toBool(), expectedValue); + QCOMPARE(scrollMethodChangedSpy.isEmpty(), initValue == expectedValue); + QCOMPARE(dbusProperty(d.sysName(), "scrollOnButtonDown"), expectedValue); +} + +void TestLibinputDevice::testScrollButton_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("setValue"); + QTest::addColumn("expectedValue"); + QTest::addColumn("setShouldFail"); + QTest::addColumn("scrollOnButton"); + + QTest::newRow("BTN_LEFT -> BTN_RIGHT") << quint32(BTN_LEFT) << quint32(BTN_RIGHT) << quint32(BTN_RIGHT) << false << true; + QTest::newRow("BTN_LEFT -> BTN_LEFT") << quint32(BTN_LEFT) << quint32(BTN_LEFT) << quint32(BTN_LEFT) << false << true; + QTest::newRow("set should fail") << quint32(BTN_LEFT) << quint32(BTN_RIGHT) << quint32(BTN_LEFT) << true << true; + QTest::newRow("not scroll on button") << quint32(BTN_LEFT) << quint32(BTN_RIGHT) << quint32(BTN_LEFT) << false << false; +} + +void TestLibinputDevice::testScrollButton() +{ + libinput_device device; + QFETCH(quint32, initValue); + QFETCH(bool, setShouldFail); + QFETCH(bool, scrollOnButton); + device.scrollButton = initValue; + device.supportedScrollMethods = scrollOnButton ? LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN : LIBINPUT_CONFIG_SCROLL_NO_SCROLL; + device.setScrollButtonReturnValue = setShouldFail; + + Device d(&device); + QCOMPARE(d.scrollButton(), initValue); + QCOMPARE(d.property("scrollButton").value(), initValue); + QCOMPARE(dbusProperty(d.sysName(), "scrollButton"), initValue); + + QSignalSpy scrollButtonChangedSpy(&d, &Device::scrollButtonChanged); + QFETCH(quint32, setValue); + d.setScrollButton(setValue); + QFETCH(quint32, expectedValue); + QCOMPARE(d.scrollButton(), expectedValue); + QCOMPARE(d.property("scrollButton").value(), expectedValue); + QCOMPARE(scrollButtonChangedSpy.isEmpty(), initValue == expectedValue); + QCOMPARE(dbusProperty(d.sysName(), "scrollButton"), expectedValue); +} + +void TestLibinputDevice::testDisableWhileTypingEnabledByDefault_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testDisableWhileTypingEnabledByDefault() +{ + QFETCH(bool, enabled); + libinput_device device; + device.disableWhileTypingEnabledByDefault = enabled ? LIBINPUT_CONFIG_DWT_ENABLED : LIBINPUT_CONFIG_DWT_DISABLED; + + Device d(&device); + QCOMPARE(d.disableWhileTypingEnabledByDefault(), enabled); + QCOMPARE(d.property("disableWhileTypingEnabledByDefault").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "disableWhileTypingEnabledByDefault"), enabled); +} + +void TestLibinputDevice::testLmrTapButtonMapEnabledByDefault_data() +{ + QTest::addColumn("enabled"); + + QTest::newRow("enabled") << true; + QTest::newRow("disabled") << false; +} + +void TestLibinputDevice::testLmrTapButtonMapEnabledByDefault() +{ + QFETCH(bool, enabled); + libinput_device device; + device.defaultTapButtonMap = enabled ? LIBINPUT_CONFIG_TAP_MAP_LMR : LIBINPUT_CONFIG_TAP_MAP_LRM; + + Device d(&device); + QCOMPARE(d.lmrTapButtonMapEnabledByDefault(), enabled); + QCOMPARE(d.property("lmrTapButtonMapEnabledByDefault").toBool(), enabled); + QCOMPARE(dbusProperty(d.sysName(), "lmrTapButtonMapEnabledByDefault"), enabled); +} + +void TestLibinputDevice::testLmrTapButtonMap_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("setValue"); + QTest::addColumn("setShouldFail"); + QTest::addColumn("expectedValue"); + QTest::addColumn("fingerCount"); + + QTest::newRow("true -> false") << true << false << false << false << 3; + QTest::newRow("false -> true") << false << true << false << true << 3; + QTest::newRow("true -> false") << true << false << false << false << 2; + QTest::newRow("false -> true") << false << true << false << true << 2; + + QTest::newRow("set fails") << true << false << true << true << 3; + + QTest::newRow("true -> true") << true << true << false << true << 3; + QTest::newRow("false -> false") << false << false << false << false << 3; + QTest::newRow("true -> true") << true << true << false << true << 2; + QTest::newRow("false -> false") << false << false << false << false << 2; + + QTest::newRow("false -> true, fingerCount 0") << false << true << true << false << 0; + + // TODO: is this a fail in libinput? + // QTest::newRow("false -> true, fingerCount 1") << false << true << true << false << 1; +} + +void TestLibinputDevice::testLmrTapButtonMap() +{ + libinput_device device; + QFETCH(bool, initValue); + QFETCH(bool, setShouldFail); + QFETCH(int, fingerCount); + device.tapFingerCount = fingerCount; + device.tapButtonMap = initValue ? LIBINPUT_CONFIG_TAP_MAP_LMR : LIBINPUT_CONFIG_TAP_MAP_LRM; + device.setTapButtonMapReturnValue = setShouldFail; + + Device d(&device); + QCOMPARE(d.lmrTapButtonMap(), initValue); + QCOMPARE(d.property("lmrTapButtonMap").toBool(), initValue); + + QSignalSpy tapButtonMapChangedSpy(&d, &Device::tapButtonMapChanged); + QFETCH(bool, setValue); + d.setLmrTapButtonMap(setValue); + QFETCH(bool, expectedValue); + QCOMPARE(d.lmrTapButtonMap(), expectedValue); + QCOMPARE(d.property("lmrTapButtonMap").toBool(), expectedValue); + QCOMPARE(tapButtonMapChangedSpy.isEmpty(), initValue == expectedValue); +} + +void TestLibinputDevice::testDisableWhileTyping_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("setValue"); + QTest::addColumn("setShouldFail"); + QTest::addColumn("expectedValue"); + QTest::addColumn("supportsDisableWhileTyping"); + + QTest::newRow("true -> false") << true << false << false << false << true; + QTest::newRow("false -> true") << false << true << false << true << true; + QTest::newRow("set fails") << true << false << true << true << true; + QTest::newRow("true -> true") << true << true << false << true << true; + QTest::newRow("false -> false") << false << false << false << false << true; + + QTest::newRow("false -> true, unsupported") << false << true << true << false << false; +} + +void TestLibinputDevice::testDisableWhileTyping() +{ + libinput_device device; + QFETCH(bool, initValue); + QFETCH(bool, setShouldFail); + QFETCH(bool, supportsDisableWhileTyping); + device.supportsDisableWhileTyping = supportsDisableWhileTyping; + device.disableWhileTyping = initValue ? LIBINPUT_CONFIG_DWT_ENABLED : LIBINPUT_CONFIG_DWT_DISABLED; + device.setDisableWhileTypingReturnValue = setShouldFail; + + Device d(&device); + QCOMPARE(d.isDisableWhileTyping(), initValue); + QCOMPARE(d.property("disableWhileTyping").toBool(), initValue); + QCOMPARE(dbusProperty(d.sysName(), "disableWhileTyping"), initValue); + + QSignalSpy disableWhileTypingChangedSpy(&d, &Device::disableWhileTypingChanged); + QFETCH(bool, setValue); + d.setDisableWhileTyping(setValue); + QFETCH(bool, expectedValue); + QCOMPARE(d.isDisableWhileTyping(), expectedValue); + QCOMPARE(d.property("disableWhileTyping").toBool(), expectedValue); + QCOMPARE(disableWhileTypingChangedSpy.isEmpty(), initValue == expectedValue); + QCOMPARE(dbusProperty(d.sysName(), "disableWhileTyping"), expectedValue); +} + +void TestLibinputDevice::testLoadEnabled_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("configValue"); + + QTest::newRow("false -> true") << false << true; + QTest::newRow("true -> false") << true << false; + QTest::newRow("true -> true") << true << true; + QTest::newRow("false -> false") << false << false; +} + +void TestLibinputDevice::testLoadEnabled() +{ + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup inputConfig(config, QStringLiteral("Test")); + QFETCH(bool, configValue); + QFETCH(bool, initValue); + inputConfig.writeEntry("Enabled", configValue); + + libinput_device device; + device.supportsDisableEvents = true; + device.enabled = initValue; + device.setEnableModeReturnValue = false; + + Device d(&device); + QCOMPARE(d.isEnabled(), initValue); + // no config group set, should not change + d.loadConfiguration(); + QCOMPARE(d.isEnabled(), initValue); + + // set the group + d.setConfig(inputConfig); + d.loadConfiguration(); + QCOMPARE(d.isEnabled(), configValue); + + // and try to store + if (configValue != initValue) { + d.setEnabled(initValue); + QCOMPARE(inputConfig.readEntry("Enabled", configValue), initValue); + } +} + +void TestLibinputDevice::testLoadPointerAcceleration_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("configValue"); + + QTest::newRow("-0.2 -> 0.9") << -0.2 << 0.9; + QTest::newRow("0.0 -> -1.0") << 0.0 << -1.0; + QTest::newRow("0.123 -> -0.456") << 0.123 << -0.456; + QTest::newRow("0.7 -> 0.7") << 0.7 << 0.7; +} + +void TestLibinputDevice::testLoadPointerAcceleration() +{ + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup inputConfig(config, QStringLiteral("Test")); + QFETCH(qreal, configValue); + QFETCH(qreal, initValue); + inputConfig.writeEntry("PointerAcceleration", configValue); + + libinput_device device; + device.supportsPointerAcceleration = true; + device.pointerAcceleration = initValue; + device.setPointerAccelerationReturnValue = false; + + Device d(&device); + QCOMPARE(d.pointerAcceleration(), initValue); + QCOMPARE(d.property("pointerAcceleration").toReal(), initValue); + // no config group set, should not change + d.loadConfiguration(); + QCOMPARE(d.pointerAcceleration(), initValue); + QCOMPARE(d.property("pointerAcceleration").toReal(), initValue); + + // set the group + d.setConfig(inputConfig); + d.loadConfiguration(); + QCOMPARE(d.pointerAcceleration(), configValue); + QCOMPARE(d.property("pointerAcceleration").toReal(), configValue); + + // and try to store + if (configValue != initValue) { + d.setPointerAcceleration(initValue); + QCOMPARE(inputConfig.readEntry("PointerAcceleration", configValue), initValue); + } +} + +void TestLibinputDevice::testLoadPointerAccelerationProfile_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("initValuePropNameString"); + QTest::addColumn("configValue"); + QTest::addColumn("configValuePropNameString"); + + QTest::newRow("pointerAccelerationProfileFlat -> pointerAccelerationProfileAdaptive") + << (quint32)LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT << "pointerAccelerationProfileFlat" + << (quint32)LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE << "pointerAccelerationProfileAdaptive"; + QTest::newRow("pointerAccelerationProfileAdaptive -> pointerAccelerationProfileFlat") + << (quint32)LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE << "pointerAccelerationProfileAdaptive" + << (quint32)LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT << "pointerAccelerationProfileFlat"; + QTest::newRow("pointerAccelerationProfileAdaptive -> pointerAccelerationProfileAdaptive") + << (quint32)LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE << "pointerAccelerationProfileAdaptive" << (quint32)LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE << "pointerAccelerationProfileAdaptive"; +} + +void TestLibinputDevice::testLoadPointerAccelerationProfile() +{ + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup inputConfig(config, QStringLiteral("Test")); + QFETCH(quint32, initValue); + QFETCH(quint32, configValue); + QFETCH(QString, initValuePropNameString); + QFETCH(QString, configValuePropNameString); + + QByteArray initValuePropName = initValuePropNameString.toLatin1(); + QByteArray configValuePropName = configValuePropNameString.toLatin1(); + + inputConfig.writeEntry("PointerAccelerationProfile", configValue); + + libinput_device device; + device.supportedPointerAccelerationProfiles = LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT | LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE; + device.pointerAccelerationProfile = (libinput_config_accel_profile)initValue; + device.setPointerAccelerationProfileReturnValue = false; + + Device d(&device); + QCOMPARE(d.property(initValuePropName).toBool(), true); + QCOMPARE(d.property(configValuePropName).toBool(), initValue == configValue); + // no config group set, should not change + d.loadConfiguration(); + QCOMPARE(d.property(initValuePropName).toBool(), true); + QCOMPARE(d.property(configValuePropName).toBool(), initValue == configValue); + + // set the group + d.setConfig(inputConfig); + d.loadConfiguration(); + QCOMPARE(d.property(initValuePropName).toBool(), initValue == configValue); + QCOMPARE(d.property(configValuePropName).toBool(), true); + QCOMPARE(dbusProperty(d.sysName(), initValuePropName), initValue == configValue); + QCOMPARE(dbusProperty(d.sysName(), configValuePropName), true); + + // and try to store + if (configValue != initValue) { + d.setProperty(initValuePropName, true); + QCOMPARE(inputConfig.readEntry("PointerAccelerationProfile", configValue), initValue); + } +} + +void TestLibinputDevice::testLoadClickMethod_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("initValuePropNameString"); + QTest::addColumn("configValue"); + QTest::addColumn("configValuePropNameString"); + + QTest::newRow("clickMethodAreas -> clickMethodClickfinger") + << static_cast(LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS) << "clickMethodAreas" + << static_cast(LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER) << "clickMethodClickfinger"; + QTest::newRow("clickMethodClickfinger -> clickMethodAreas") + << static_cast(LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER) << "clickMethodClickfinger" + << static_cast(LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS) << "clickMethodAreas"; + QTest::newRow("clickMethodAreas -> clickMethodAreas") + << static_cast(LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS) << "clickMethodAreas" + << static_cast(LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS) << "clickMethodAreas"; + QTest::newRow("clickMethodClickfinger -> clickMethodClickfinger") + << static_cast(LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER) << "clickMethodClickfinger" + << static_cast(LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER) << "clickMethodClickfinger"; +} + +void TestLibinputDevice::testLoadClickMethod() +{ + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup inputConfig(config, QStringLiteral("Test")); + QFETCH(quint32, initValue); + QFETCH(quint32, configValue); + QFETCH(QString, initValuePropNameString); + QFETCH(QString, configValuePropNameString); + + QByteArray initValuePropName = initValuePropNameString.toLatin1(); + QByteArray configValuePropName = configValuePropNameString.toLatin1(); + + inputConfig.writeEntry("ClickMethod", configValue); + + libinput_device device; + device.supportedClickMethods = LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS | LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER; + device.clickMethod = (libinput_config_click_method)initValue; + device.setClickMethodReturnValue = false; + + Device d(&device); + QCOMPARE(d.property(initValuePropName).toBool(), true); + QCOMPARE(d.property(configValuePropName).toBool(), initValue == configValue); + // no config group set, should not change + d.loadConfiguration(); + QCOMPARE(d.property(initValuePropName).toBool(), true); + QCOMPARE(d.property(configValuePropName).toBool(), initValue == configValue); + + // set the group + d.setConfig(inputConfig); + d.loadConfiguration(); + QCOMPARE(d.property(initValuePropName).toBool(), initValue == configValue); + QCOMPARE(d.property(configValuePropName).toBool(), true); + QCOMPARE(dbusProperty(d.sysName(), initValuePropName), initValue == configValue); + QCOMPARE(dbusProperty(d.sysName(), configValuePropName), true); + + // and try to store + if (configValue != initValue) { + d.setProperty(initValuePropName, true); + QCOMPARE(inputConfig.readEntry("ClickMethod", configValue), initValue); + } +} + +void TestLibinputDevice::testLoadTapToClick_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("configValue"); + + QTest::newRow("false -> true") << false << true; + QTest::newRow("true -> false") << true << false; + QTest::newRow("true -> true") << true << true; + QTest::newRow("false -> false") << false << false; +} + +void TestLibinputDevice::testLoadTapToClick() +{ + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup inputConfig(config, QStringLiteral("Test")); + QFETCH(bool, configValue); + QFETCH(bool, initValue); + inputConfig.writeEntry("TapToClick", configValue); + + libinput_device device; + device.tapFingerCount = 2; + device.tapToClick = initValue; + device.setTapToClickReturnValue = false; + + Device d(&device); + QCOMPARE(d.isTapToClick(), initValue); + // no config group set, should not change + d.loadConfiguration(); + QCOMPARE(d.isTapToClick(), initValue); + + // set the group + d.setConfig(inputConfig); + d.loadConfiguration(); + QCOMPARE(d.isTapToClick(), configValue); + + // and try to store + if (configValue != initValue) { + d.setTapToClick(initValue); + QCOMPARE(inputConfig.readEntry("TapToClick", configValue), initValue); + } +} + +void TestLibinputDevice::testLoadTapAndDrag_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("configValue"); + + QTest::newRow("false -> true") << false << true; + QTest::newRow("true -> false") << true << false; + QTest::newRow("true -> true") << true << true; + QTest::newRow("false -> false") << false << false; +} + +void TestLibinputDevice::testLoadTapAndDrag() +{ + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup inputConfig(config, QStringLiteral("Test")); + QFETCH(bool, configValue); + QFETCH(bool, initValue); + inputConfig.writeEntry("TapAndDrag", configValue); + + libinput_device device; + device.tapAndDrag = initValue; + device.setTapAndDragReturnValue = false; + + Device d(&device); + QCOMPARE(d.isTapAndDrag(), initValue); + // no config group set, should not change + d.loadConfiguration(); + QCOMPARE(d.isTapAndDrag(), initValue); + + // set the group + d.setConfig(inputConfig); + d.loadConfiguration(); + QCOMPARE(d.isTapAndDrag(), configValue); + + // and try to store + if (configValue != initValue) { + d.setTapAndDrag(initValue); + QCOMPARE(inputConfig.readEntry("TapAndDrag", configValue), initValue); + } +} + +void TestLibinputDevice::testLoadTapDragLock_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("configValue"); + + QTest::newRow("false -> true") << false << true; + QTest::newRow("true -> false") << true << false; + QTest::newRow("true -> true") << true << true; + QTest::newRow("false -> false") << false << false; +} + +void TestLibinputDevice::testLoadTapDragLock() +{ + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup inputConfig(config, QStringLiteral("Test")); + QFETCH(bool, configValue); + QFETCH(bool, initValue); + inputConfig.writeEntry("TapDragLock", configValue); + + libinput_device device; + device.tapDragLock = initValue; + device.setTapDragLockReturnValue = false; + + Device d(&device); + QCOMPARE(d.isTapDragLock(), initValue); + // no config group set, should not change + d.loadConfiguration(); + QCOMPARE(d.isTapDragLock(), initValue); + + // set the group + d.setConfig(inputConfig); + d.loadConfiguration(); + QCOMPARE(d.isTapDragLock(), configValue); + + // and try to store + if (configValue != initValue) { + d.setTapDragLock(initValue); + QCOMPARE(inputConfig.readEntry("TapDragLock", configValue), initValue); + } +} + +void TestLibinputDevice::testLoadMiddleButtonEmulation_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("configValue"); + + QTest::newRow("false -> true") << false << true; + QTest::newRow("true -> false") << true << false; + QTest::newRow("true -> true") << true << true; + QTest::newRow("false -> false") << false << false; +} + +void TestLibinputDevice::testLoadMiddleButtonEmulation() +{ + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup inputConfig(config, QStringLiteral("Test")); + QFETCH(bool, configValue); + QFETCH(bool, initValue); + inputConfig.writeEntry("MiddleButtonEmulation", configValue); + + libinput_device device; + device.supportsMiddleEmulation = true; + device.middleEmulation = initValue; + device.setMiddleEmulationReturnValue = false; + + Device d(&device); + QCOMPARE(d.isMiddleEmulation(), initValue); + // no config group set, should not change + d.loadConfiguration(); + QCOMPARE(d.isMiddleEmulation(), initValue); + + // set the group + d.setConfig(inputConfig); + d.loadConfiguration(); + QCOMPARE(d.isMiddleEmulation(), configValue); + + // and try to store + if (configValue != initValue) { + d.setMiddleEmulation(initValue); + QCOMPARE(inputConfig.readEntry("MiddleButtonEmulation", configValue), initValue); + } +} + +void TestLibinputDevice::testLoadNaturalScroll_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("configValue"); + + QTest::newRow("false -> true") << false << true; + QTest::newRow("true -> false") << true << false; + QTest::newRow("true -> true") << true << true; + QTest::newRow("false -> false") << false << false; +} + +void TestLibinputDevice::testLoadNaturalScroll() +{ + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup inputConfig(config, QStringLiteral("Test")); + QFETCH(bool, configValue); + QFETCH(bool, initValue); + inputConfig.writeEntry("NaturalScroll", configValue); + + libinput_device device; + device.supportsNaturalScroll = true; + device.naturalScroll = initValue; + device.setNaturalScrollReturnValue = false; + + Device d(&device); + QCOMPARE(d.isNaturalScroll(), initValue); + // no config group set, should not change + d.loadConfiguration(); + QCOMPARE(d.isNaturalScroll(), initValue); + + // set the group + d.setConfig(inputConfig); + d.loadConfiguration(); + QCOMPARE(d.isNaturalScroll(), configValue); + + // and try to store + if (configValue != initValue) { + d.setNaturalScroll(initValue); + QCOMPARE(inputConfig.readEntry("NaturalScroll", configValue), initValue); + } +} + +void TestLibinputDevice::testLoadScrollMethod_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("initValuePropNameString"); + QTest::addColumn("configValue"); + QTest::addColumn("configValuePropNameString"); + + QTest::newRow("scrollTwoFinger -> scrollEdge") << (quint32)LIBINPUT_CONFIG_SCROLL_2FG << "scrollTwoFinger" << (quint32)LIBINPUT_CONFIG_SCROLL_EDGE << "scrollEdge"; + QTest::newRow("scrollOnButtonDown -> scrollTwoFinger") << (quint32)LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN << "scrollOnButtonDown" << (quint32)LIBINPUT_CONFIG_SCROLL_2FG << "scrollTwoFinger"; + QTest::newRow("scrollEdge -> scrollEdge") << (quint32)LIBINPUT_CONFIG_SCROLL_EDGE << "scrollEdge" << (quint32)LIBINPUT_CONFIG_SCROLL_EDGE << "scrollEdge"; +} + +void TestLibinputDevice::testLoadScrollMethod() +{ + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup inputConfig(config, QStringLiteral("Test")); + QFETCH(quint32, initValue); + QFETCH(quint32, configValue); + QFETCH(QString, initValuePropNameString); + QFETCH(QString, configValuePropNameString); + + QByteArray initValuePropName = initValuePropNameString.toLatin1(); + QByteArray configValuePropName = configValuePropNameString.toLatin1(); + + inputConfig.writeEntry("ScrollMethod", configValue); + + libinput_device device; + device.supportedScrollMethods = LIBINPUT_CONFIG_SCROLL_2FG | LIBINPUT_CONFIG_SCROLL_EDGE | LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN; + device.scrollMethod = (libinput_config_scroll_method)initValue; + device.setScrollMethodReturnValue = false; + + Device d(&device); + QCOMPARE(d.property(initValuePropName).toBool(), true); + QCOMPARE(d.property(configValuePropName).toBool(), initValue == configValue); + // no config group set, should not change + d.loadConfiguration(); + QCOMPARE(d.property(initValuePropName).toBool(), true); + QCOMPARE(d.property(configValuePropName).toBool(), initValue == configValue); + + // set the group + d.setConfig(inputConfig); + d.loadConfiguration(); + QCOMPARE(d.property(initValuePropName).toBool(), initValue == configValue); + QCOMPARE(d.property(configValuePropName).toBool(), true); + + // and try to store + if (configValue != initValue) { + d.setProperty(initValuePropName, true); + QCOMPARE(inputConfig.readEntry("ScrollMethod", configValue), initValue); + } +} + +void TestLibinputDevice::testLoadScrollButton_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("configValue"); + + QTest::newRow("BTN_LEFT -> BTN_RIGHT") << quint32(BTN_LEFT) << quint32(BTN_RIGHT); + QTest::newRow("BTN_LEFT -> BTN_LEFT") << quint32(BTN_LEFT) << quint32(BTN_LEFT); +} + +void TestLibinputDevice::testLoadScrollButton() +{ + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup inputConfig(config, QStringLiteral("Test")); + QFETCH(quint32, configValue); + QFETCH(quint32, initValue); + inputConfig.writeEntry("ScrollButton", configValue); + + libinput_device device; + device.supportedScrollMethods = LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN; + device.scrollMethod = LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN; + device.scrollButton = initValue; + device.setScrollButtonReturnValue = false; + + Device d(&device); + QCOMPARE(d.isScrollOnButtonDown(), true); + QCOMPARE(d.scrollButton(), initValue); + // no config group set, should not change + d.loadConfiguration(); + QCOMPARE(d.isScrollOnButtonDown(), true); + QCOMPARE(d.scrollButton(), initValue); + + // set the group + d.setConfig(inputConfig); + d.loadConfiguration(); + QCOMPARE(d.isScrollOnButtonDown(), true); + QCOMPARE(d.scrollButton(), configValue); + + // and try to store + if (configValue != initValue) { + d.setScrollButton(initValue); + QCOMPARE(inputConfig.readEntry("ScrollButton", configValue), initValue); + } +} + +void TestLibinputDevice::testLoadLeftHanded_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("configValue"); + + QTest::newRow("false -> true") << false << true; + QTest::newRow("true -> false") << true << false; + QTest::newRow("true -> true") << true << true; + QTest::newRow("false -> false") << false << false; +} + +void TestLibinputDevice::testLoadLeftHanded() +{ + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup inputConfig(config, QStringLiteral("Test")); + QFETCH(bool, configValue); + QFETCH(bool, initValue); + inputConfig.writeEntry("LeftHanded", configValue); + + libinput_device device; + device.supportsLeftHanded = true; + device.leftHanded = initValue; + device.setLeftHandedReturnValue = false; + + Device d(&device); + QCOMPARE(d.isLeftHanded(), initValue); + // no config group set, should not change + d.loadConfiguration(); + QCOMPARE(d.isLeftHanded(), initValue); + + // set the group + d.setConfig(inputConfig); + d.loadConfiguration(); + QCOMPARE(d.isLeftHanded(), configValue); + + // and try to store + if (configValue != initValue) { + d.setLeftHanded(initValue); + QCOMPARE(inputConfig.readEntry("LeftHanded", configValue), initValue); + } +} + +void TestLibinputDevice::testLoadPressureCurve_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("configValue"); + + const auto defaultCurve = Device::deserializePressureCurve("0.0,0.0;1.0,1.0;"); + const auto modifiedCurve = Device::deserializePressureCurve("1.0,0.0;1.0,1.0;"); + + QTest::newRow("default -> modified") << defaultCurve << modifiedCurve; + QTest::newRow("modified -> default") << modifiedCurve << defaultCurve; + QTest::newRow("default -> default") << defaultCurve << defaultCurve; +} + +void TestLibinputDevice::testLoadPressureCurve() +{ + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup inputConfig(config, QStringLiteral("Test")); + + QFETCH(QEasingCurve, configValue); + const QString configValueString = Device::serializePressureCurve(configValue); + QFETCH(QEasingCurve, initValue); + const QString initValueString = Device::serializePressureCurve(initValue); + + inputConfig.writeEntry("TabletToolPressureCurve", configValueString); + + libinput_device device; + Device d(&device); + d.setPressureCurve(initValueString); + QCOMPARE(d.pressureCurve(), initValue); + // no config group set, should not change + d.loadConfiguration(); + QCOMPARE(d.pressureCurve(), initValue); + + // set the group + d.setConfig(inputConfig); + d.loadConfiguration(); + QCOMPARE(d.pressureCurve(), configValue); + + // and try to store + if (configValue != initValue) { + d.setPressureCurve(initValueString); + QCOMPARE(inputConfig.readEntry("TabletToolPressureCurve", configValueString), initValueString); + } +} + +void TestLibinputDevice::testLoadDisableWhileTyping_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("configValue"); + + QTest::newRow("false -> true") << false << true; + QTest::newRow("true -> false") << true << false; + QTest::newRow("true -> true") << true << true; + QTest::newRow("false -> false") << false << false; +} + +void TestLibinputDevice::testLoadDisableWhileTyping() +{ + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup inputConfig(config, QStringLiteral("Test")); + QFETCH(bool, configValue); + QFETCH(bool, initValue); + inputConfig.writeEntry("DisableWhileTyping", configValue); + + libinput_device device; + device.supportsDisableWhileTyping = true; + device.disableWhileTyping = initValue ? LIBINPUT_CONFIG_DWT_ENABLED : LIBINPUT_CONFIG_DWT_DISABLED; + device.setDisableWhileTypingReturnValue = false; + + Device d(&device); + QCOMPARE(d.isDisableWhileTyping(), initValue); + // no config group set, should not change + d.loadConfiguration(); + QCOMPARE(d.isDisableWhileTyping(), initValue); + + // set the group + d.setConfig(inputConfig); + d.loadConfiguration(); + QCOMPARE(d.isDisableWhileTyping(), configValue); + + // and try to store + if (configValue != initValue) { + d.setDisableWhileTyping(initValue); + QCOMPARE(inputConfig.readEntry("DisableWhileTyping", configValue), initValue); + } +} + +void TestLibinputDevice::testLoadLmrTapButtonMap_data() +{ + QTest::addColumn("initValue"); + QTest::addColumn("configValue"); + + QTest::newRow("false -> true") << false << true; + QTest::newRow("true -> false") << true << false; + QTest::newRow("true -> true") << true << true; + QTest::newRow("false -> false") << false << false; +} + +void TestLibinputDevice::testLoadLmrTapButtonMap() +{ + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + KConfigGroup inputConfig(config, QStringLiteral("Test")); + QFETCH(bool, configValue); + QFETCH(bool, initValue); + inputConfig.writeEntry("LmrTapButtonMap", configValue); + + libinput_device device; + device.tapFingerCount = 3; + device.tapButtonMap = initValue ? LIBINPUT_CONFIG_TAP_MAP_LMR : LIBINPUT_CONFIG_TAP_MAP_LRM; + device.setTapButtonMapReturnValue = false; + + Device d(&device); + QCOMPARE(d.lmrTapButtonMap(), initValue); + // no config group set, should not change + d.loadConfiguration(); + QCOMPARE(d.lmrTapButtonMap(), initValue); + QCOMPARE(dbusProperty(d.sysName(), "lmrTapButtonMap"), initValue); + + // set the group + d.setConfig(inputConfig); + d.loadConfiguration(); + QCOMPARE(d.lmrTapButtonMap(), configValue); + QCOMPARE(dbusProperty(d.sysName(), "lmrTapButtonMap"), configValue); + + // and try to store + if (configValue != initValue) { + d.setLmrTapButtonMap(initValue); + QCOMPARE(inputConfig.readEntry("LmrTapButtonMap", configValue), initValue); + } +} + +void TestLibinputDevice::testOrientation_data() +{ + QTest::addColumn("orientation"); + QTest::addColumn("m11"); + QTest::addColumn("m12"); + QTest::addColumn("m13"); + QTest::addColumn("m21"); + QTest::addColumn("m22"); + QTest::addColumn("m23"); + QTest::addColumn("defaultIsIdentity"); + + QTest::newRow("Primary") << Qt::PrimaryOrientation << 1.0f << 2.0f << 3.0f << 4.0f << 5.0f << 6.0f << false; + QTest::newRow("Landscape") << Qt::LandscapeOrientation << 1.0f << 2.0f << 3.0f << 4.0f << 5.0f << 6.0f << false; + QTest::newRow("Portrait") << Qt::PortraitOrientation << 0.0f << -1.0f << 1.0f << 1.0f << 0.0f << 0.0f << true; + QTest::newRow("InvertedLandscape") << Qt::InvertedLandscapeOrientation << -1.0f << 0.0f << 1.0f << 0.0f << -1.0f << 1.0f << true; + QTest::newRow("InvertedPortrait") << Qt::InvertedPortraitOrientation << 0.0f << 1.0f << 0.0f << -1.0f << 0.0f << 1.0f << true; +} + +void TestLibinputDevice::testOrientation() +{ + libinput_device device; + device.supportsCalibrationMatrix = true; + device.defaultCalibrationMatrix = std::array{{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}}; + QFETCH(bool, defaultIsIdentity); + device.defaultCalibrationMatrixIsIdentity = defaultIsIdentity; + Device d(&device); + QFETCH(Qt::ScreenOrientation, orientation); + d.setOrientation(orientation); + QTEST(device.calibrationMatrix[0], "m11"); + QTEST(device.calibrationMatrix[1], "m12"); + QTEST(device.calibrationMatrix[2], "m13"); + QTEST(device.calibrationMatrix[3], "m21"); + QTEST(device.calibrationMatrix[4], "m22"); + QTEST(device.calibrationMatrix[5], "m23"); +} + +void TestLibinputDevice::testCalibrationWithDefault() +{ + libinput_device device; + device.supportsCalibrationMatrix = true; + device.defaultCalibrationMatrix = std::array{{2.0, 3.0, 0.0, 4.0, 5.0, 0.0}}; + device.defaultCalibrationMatrixIsIdentity = false; + Device d(&device); + d.setOrientation(Qt::PortraitOrientation); + QCOMPARE(device.calibrationMatrix[0], 3.0f); + QCOMPARE(device.calibrationMatrix[1], -2.0f); + QCOMPARE(device.calibrationMatrix[2], 2.0f); + QCOMPARE(device.calibrationMatrix[3], 5.0f); + QCOMPARE(device.calibrationMatrix[4], -4.0f); + QCOMPARE(device.calibrationMatrix[5], 4.0f); +} + +void TestLibinputDevice::testSwitch_data() +{ + QTest::addColumn("lid"); + QTest::addColumn("tablet"); + + QTest::newRow("lid") << true << false; + QTest::newRow("tablet") << false << true; +} + +void TestLibinputDevice::testSwitch() +{ + libinput_device device; + device.switchDevice = true; + QFETCH(bool, lid); + QFETCH(bool, tablet); + device.lidSwitch = lid; + device.tabletModeSwitch = tablet; + + Device d(&device); + QCOMPARE(d.isSwitch(), true); + QCOMPARE(d.isLidSwitch(), lid); + QCOMPARE(d.property("lidSwitch").toBool(), lid); + QCOMPARE(dbusProperty(d.sysName(), "lidSwitch"), lid); + QCOMPARE(d.isTabletModeSwitch(), tablet); + QCOMPARE(d.property("tabletModeSwitch").toBool(), tablet); + QCOMPARE(dbusProperty(d.sysName(), "tabletModeSwitch"), tablet); +} + +QTEST_GUILESS_MAIN(TestLibinputDevice) +#include "device_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/libinput/gesture_event_test.cpp b/local/recipes/kde/kwin/source/autotests/libinput/gesture_event_test.cpp new file mode 100644 index 0000000000..a99708539d --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/libinput/gesture_event_test.cpp @@ -0,0 +1,205 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "mock_libinput.h" + +#include "backends/libinput/device.h" +#include "backends/libinput/events.h" + +#include + +#include + +Q_DECLARE_METATYPE(libinput_event_type) + +using namespace KWin::LibInput; +using namespace std::literals; + +class TestLibinputGestureEvent : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void init(); + void cleanup(); + + void testType_data(); + void testType(); + + void testStart_data(); + void testStart(); + + void testSwipeUpdate(); + void testPinchUpdate(); + + void testEnd_data(); + void testEnd(); + +private: + libinput_device *m_nativeDevice = nullptr; + Device *m_device = nullptr; +}; + +void TestLibinputGestureEvent::init() +{ + m_nativeDevice = new libinput_device; + m_nativeDevice->pointer = true; + m_nativeDevice->gestureSupported = true; + m_nativeDevice->deviceSize = QSizeF(12.5, 13.8); + m_device = new Device(m_nativeDevice); +} + +void TestLibinputGestureEvent::cleanup() +{ + delete m_device; + m_device = nullptr; + + delete m_nativeDevice; + m_nativeDevice = nullptr; +} + +void TestLibinputGestureEvent::testType_data() +{ + QTest::addColumn("type"); + + QTest::newRow("pinch-start") << LIBINPUT_EVENT_GESTURE_PINCH_BEGIN; + QTest::newRow("pinch-update") << LIBINPUT_EVENT_GESTURE_PINCH_UPDATE; + QTest::newRow("pinch-end") << LIBINPUT_EVENT_GESTURE_PINCH_END; + QTest::newRow("swipe-start") << LIBINPUT_EVENT_GESTURE_SWIPE_BEGIN; + QTest::newRow("swipe-update") << LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE; + QTest::newRow("swipe-end") << LIBINPUT_EVENT_GESTURE_SWIPE_END; +} + +void TestLibinputGestureEvent::testType() +{ + // this test verifies the initialization of a PointerEvent and the parent Event class + libinput_event_gesture *gestureEvent = new libinput_event_gesture; + QFETCH(libinput_event_type, type); + gestureEvent->type = type; + gestureEvent->device = m_nativeDevice; + + std::unique_ptr event(Event::create(gestureEvent)); + // API of event + QCOMPARE(event->type(), type); + QCOMPARE(event->device(), m_device); + QCOMPARE(event->nativeDevice(), m_nativeDevice); + QCOMPARE((libinput_event *)(*event.get()), gestureEvent); + // verify it's a pointer event + QVERIFY(dynamic_cast(event.get())); + QCOMPARE((libinput_event_gesture *)(*dynamic_cast(event.get())), gestureEvent); +} + +void TestLibinputGestureEvent::testStart_data() +{ + QTest::addColumn("type"); + + QTest::newRow("pinch") << LIBINPUT_EVENT_GESTURE_PINCH_BEGIN; + QTest::newRow("swipe") << LIBINPUT_EVENT_GESTURE_SWIPE_BEGIN; +} + +void TestLibinputGestureEvent::testStart() +{ + libinput_event_gesture *gestureEvent = new libinput_event_gesture; + gestureEvent->device = m_nativeDevice; + QFETCH(libinput_event_type, type); + gestureEvent->type = type; + gestureEvent->fingerCount = 3; + gestureEvent->time = 100ms; + + std::unique_ptr event(Event::create(gestureEvent)); + auto ge = dynamic_cast(event.get()); + QVERIFY(ge); + QCOMPARE(ge->fingerCount(), gestureEvent->fingerCount); + QVERIFY(!ge->isCancelled()); + QCOMPARE(ge->time(), gestureEvent->time); + QCOMPARE(ge->delta(), QPointF(0, 0)); + if (ge->type() == LIBINPUT_EVENT_GESTURE_PINCH_BEGIN) { + auto pe = dynamic_cast(event.get()); + QCOMPARE(pe->scale(), 1.0); + QCOMPARE(pe->angleDelta(), 0.0); + } +} + +void TestLibinputGestureEvent::testSwipeUpdate() +{ + libinput_event_gesture *gestureEvent = new libinput_event_gesture; + gestureEvent->device = m_nativeDevice; + gestureEvent->type = LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE; + gestureEvent->fingerCount = 2; + gestureEvent->time = 200ms; + gestureEvent->delta = QPointF(2, 3); + + std::unique_ptr event(Event::create(gestureEvent)); + auto se = dynamic_cast(event.get()); + QVERIFY(se); + QCOMPARE(se->fingerCount(), gestureEvent->fingerCount); + QVERIFY(!se->isCancelled()); + QCOMPARE(se->time(), gestureEvent->time); + QCOMPARE(se->delta(), QPointF(2, 3)); +} + +void TestLibinputGestureEvent::testPinchUpdate() +{ + libinput_event_gesture *gestureEvent = new libinput_event_gesture; + gestureEvent->device = m_nativeDevice; + gestureEvent->type = LIBINPUT_EVENT_GESTURE_PINCH_UPDATE; + gestureEvent->fingerCount = 4; + gestureEvent->time = 600ms; + gestureEvent->delta = QPointF(5, 4); + gestureEvent->scale = 2; + gestureEvent->angleDelta = -30; + + std::unique_ptr event(Event::create(gestureEvent)); + auto pe = dynamic_cast(event.get()); + QVERIFY(pe); + QCOMPARE(pe->fingerCount(), gestureEvent->fingerCount); + QVERIFY(!pe->isCancelled()); + QCOMPARE(pe->time(), gestureEvent->time); + QCOMPARE(pe->delta(), QPointF(5, 4)); + QCOMPARE(pe->scale(), gestureEvent->scale); + QCOMPARE(pe->angleDelta(), gestureEvent->angleDelta); +} + +void TestLibinputGestureEvent::testEnd_data() +{ + QTest::addColumn("type"); + QTest::addColumn("cancelled"); + + QTest::newRow("pinch/not cancelled") << LIBINPUT_EVENT_GESTURE_PINCH_END << false; + QTest::newRow("pinch/cancelled") << LIBINPUT_EVENT_GESTURE_PINCH_END << true; + QTest::newRow("swipe/not cancelled") << LIBINPUT_EVENT_GESTURE_SWIPE_END << false; + QTest::newRow("swipe/cancelled") << LIBINPUT_EVENT_GESTURE_SWIPE_END << true; +} + +void TestLibinputGestureEvent::testEnd() +{ + libinput_event_gesture *gestureEvent = new libinput_event_gesture; + gestureEvent->device = m_nativeDevice; + QFETCH(libinput_event_type, type); + gestureEvent->type = type; + gestureEvent->fingerCount = 4; + QFETCH(bool, cancelled); + gestureEvent->cancelled = cancelled; + gestureEvent->time = 300ms; + gestureEvent->scale = 3; + + std::unique_ptr event(Event::create(gestureEvent)); + auto ge = dynamic_cast(event.get()); + QVERIFY(ge); + QCOMPARE(ge->fingerCount(), gestureEvent->fingerCount); + QCOMPARE(ge->isCancelled(), cancelled); + QCOMPARE(ge->time(), gestureEvent->time); + QCOMPARE(ge->delta(), QPointF(0, 0)); + if (ge->type() == LIBINPUT_EVENT_GESTURE_PINCH_END) { + auto pe = dynamic_cast(event.get()); + QCOMPARE(pe->scale(), gestureEvent->scale); + QCOMPARE(pe->angleDelta(), 0.0); + } +} + +QTEST_GUILESS_MAIN(TestLibinputGestureEvent) +#include "gesture_event_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/libinput/key_event_test.cpp b/local/recipes/kde/kwin/source/autotests/libinput/key_event_test.cpp new file mode 100644 index 0000000000..62974aa4aa --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/libinput/key_event_test.cpp @@ -0,0 +1,106 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "mock_libinput.h" + +#include "backends/libinput/device.h" +#include "backends/libinput/events.h" + +#include + +#include + +Q_DECLARE_METATYPE(libinput_key_state) + +using namespace KWin::LibInput; + +class TestLibinputKeyEvent : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void init(); + void cleanup(); + + void testCreate(); + void testEvent_data(); + void testEvent(); + +private: + libinput_device *m_nativeDevice = nullptr; + Device *m_device = nullptr; +}; + +void TestLibinputKeyEvent::init() +{ + m_nativeDevice = new libinput_device; + m_nativeDevice->keyboard = true; + m_device = new Device(m_nativeDevice); +} + +void TestLibinputKeyEvent::cleanup() +{ + delete m_device; + m_device = nullptr; + + delete m_nativeDevice; + m_nativeDevice = nullptr; +} + +void TestLibinputKeyEvent::testCreate() +{ + // this test verifies the initialisation of a KeyEvent and the parent Event class + libinput_event_keyboard *keyEvent = new libinput_event_keyboard; + keyEvent->device = m_nativeDevice; + + std::unique_ptr event{Event::create(keyEvent)}; + // API of event + QCOMPARE(event->type(), LIBINPUT_EVENT_KEYBOARD_KEY); + QCOMPARE(event->device(), m_device); + QCOMPARE(event->nativeDevice(), m_nativeDevice); + QCOMPARE((libinput_event *)(*event.get()), keyEvent); + // verify it's a key event + QVERIFY(dynamic_cast(event.get())); + QCOMPARE((libinput_event_keyboard *)(*dynamic_cast(event.get())), keyEvent); + + // verify that a nullptr passed to Event::create returns a nullptr + QVERIFY(!Event::create(nullptr)); +} + +void TestLibinputKeyEvent::testEvent_data() +{ + QTest::addColumn("keyState"); + QTest::addColumn("expectedKeyState"); + QTest::addColumn("key"); + QTest::addColumn("time"); + + QTest::newRow("pressed") << LIBINPUT_KEY_STATE_PRESSED << KWin::KeyboardKeyState::Pressed << quint32(KEY_A) << 100u; + QTest::newRow("released") << LIBINPUT_KEY_STATE_RELEASED << KWin::KeyboardKeyState::Released << quint32(KEY_B) << 200u; +} + +void TestLibinputKeyEvent::testEvent() +{ + // this test verifies the key press/release + libinput_event_keyboard *keyEvent = new libinput_event_keyboard; + keyEvent->device = m_nativeDevice; + QFETCH(libinput_key_state, keyState); + keyEvent->state = keyState; + QFETCH(quint32, key); + keyEvent->key = key; + QFETCH(quint32, time); + keyEvent->time = std::chrono::milliseconds(time); + + std::unique_ptr event(Event::create(keyEvent)); + auto ke = dynamic_cast(event.get()); + QVERIFY(ke); + QTEST(ke->state(), "expectedKeyState"); + QCOMPARE(ke->key(), key); + QCOMPARE(ke->time(), keyEvent->time); +} + +QTEST_GUILESS_MAIN(TestLibinputKeyEvent) +#include "key_event_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/libinput/mock_libinput.cpp b/local/recipes/kde/kwin/source/autotests/libinput/mock_libinput.cpp new file mode 100644 index 0000000000..58f5432b23 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/libinput/mock_libinput.cpp @@ -0,0 +1,1154 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include + +#include "mock_libinput.h" + +#include + +int libinput_device_keyboard_has_key(struct libinput_device *device, uint32_t code) +{ + return device->keys.contains(code); +} + +int libinput_device_has_capability(struct libinput_device *device, enum libinput_device_capability capability) +{ + switch (capability) { + case LIBINPUT_DEVICE_CAP_KEYBOARD: + return device->keyboard; + case LIBINPUT_DEVICE_CAP_POINTER: + return device->pointer; + case LIBINPUT_DEVICE_CAP_TOUCH: + return device->touch; + case LIBINPUT_DEVICE_CAP_GESTURE: + return device->gestureSupported; + case LIBINPUT_DEVICE_CAP_TABLET_TOOL: + return device->tabletTool; + case LIBINPUT_DEVICE_CAP_SWITCH: + return device->switchDevice; + default: + return 0; + } +} + +const char *libinput_device_get_name(struct libinput_device *device) +{ + return device->name.constData(); +} + +const char *libinput_device_get_sysname(struct libinput_device *device) +{ + return device->sysName.constData(); +} + +const char *libinput_device_get_output_name(struct libinput_device *device) +{ + return device->outputName.constData(); +} + +unsigned int libinput_device_get_id_product(struct libinput_device *device) +{ + return device->product; +} + +unsigned int libinput_device_get_id_vendor(struct libinput_device *device) +{ + return device->vendor; +} + +unsigned int libinput_device_get_id_bustype(struct libinput_device *device) +{ + return device->busType; +} + +int libinput_device_config_tap_get_finger_count(struct libinput_device *device) +{ + return device->tapFingerCount; +} + +enum libinput_config_tap_state libinput_device_config_tap_get_enabled(struct libinput_device *device) +{ + if (device->tapToClick) { + return LIBINPUT_CONFIG_TAP_ENABLED; + } else { + return LIBINPUT_CONFIG_TAP_DISABLED; + } +} + +enum libinput_config_status libinput_device_config_tap_set_enabled(struct libinput_device *device, enum libinput_config_tap_state enable) +{ + if (device->setTapToClickReturnValue == 0) { + device->tapToClick = (enable == LIBINPUT_CONFIG_TAP_ENABLED); + return LIBINPUT_CONFIG_STATUS_SUCCESS; + } + return LIBINPUT_CONFIG_STATUS_INVALID; +} + +enum libinput_config_tap_state libinput_device_config_tap_get_default_enabled(struct libinput_device *device) +{ + if (device->tapEnabledByDefault) { + return LIBINPUT_CONFIG_TAP_ENABLED; + } else { + return LIBINPUT_CONFIG_TAP_DISABLED; + } +} + +enum libinput_config_drag_state libinput_device_config_tap_get_default_drag_enabled(struct libinput_device *device) +{ + if (device->tapAndDragEnabledByDefault) { + return LIBINPUT_CONFIG_DRAG_ENABLED; + } else { + return LIBINPUT_CONFIG_DRAG_DISABLED; + } +} + +enum libinput_config_drag_state libinput_device_config_tap_get_drag_enabled(struct libinput_device *device) +{ + if (device->tapAndDrag) { + return LIBINPUT_CONFIG_DRAG_ENABLED; + } else { + return LIBINPUT_CONFIG_DRAG_DISABLED; + } +} + +enum libinput_config_status libinput_device_config_tap_set_drag_enabled(struct libinput_device *device, enum libinput_config_drag_state enable) +{ + if (device->setTapAndDragReturnValue == 0) { + device->tapAndDrag = (enable == LIBINPUT_CONFIG_DRAG_ENABLED); + return LIBINPUT_CONFIG_STATUS_SUCCESS; + } + return LIBINPUT_CONFIG_STATUS_INVALID; +} + +enum libinput_config_drag_lock_state libinput_device_config_tap_get_default_drag_lock_enabled(struct libinput_device *device) +{ + if (device->tapDragLockEnabledByDefault) { + return LIBINPUT_CONFIG_DRAG_LOCK_ENABLED; + } else { + return LIBINPUT_CONFIG_DRAG_LOCK_DISABLED; + } +} + +enum libinput_config_drag_lock_state libinput_device_config_tap_get_drag_lock_enabled(struct libinput_device *device) +{ + if (device->tapDragLock) { + return LIBINPUT_CONFIG_DRAG_LOCK_ENABLED; + } else { + return LIBINPUT_CONFIG_DRAG_LOCK_DISABLED; + } +} + +enum libinput_config_status libinput_device_config_tap_set_drag_lock_enabled(struct libinput_device *device, enum libinput_config_drag_lock_state enable) +{ + if (device->setTapDragLockReturnValue == 0) { + device->tapDragLock = (enable == LIBINPUT_CONFIG_DRAG_LOCK_ENABLED); + return LIBINPUT_CONFIG_STATUS_SUCCESS; + } + return LIBINPUT_CONFIG_STATUS_INVALID; +} + +int libinput_device_config_dwt_is_available(struct libinput_device *device) +{ + return device->supportsDisableWhileTyping; +} + +enum libinput_config_status libinput_device_config_dwt_set_enabled(struct libinput_device *device, enum libinput_config_dwt_state state) +{ + if (device->setDisableWhileTypingReturnValue == 0) { + if (!device->supportsDisableWhileTyping) { + return LIBINPUT_CONFIG_STATUS_INVALID; + } + device->disableWhileTyping = state; + return LIBINPUT_CONFIG_STATUS_SUCCESS; + } + return LIBINPUT_CONFIG_STATUS_INVALID; +} + +enum libinput_config_dwt_state libinput_device_config_dwt_get_enabled(struct libinput_device *device) +{ + return device->disableWhileTyping; +} + +enum libinput_config_dwt_state libinput_device_config_dwt_get_default_enabled(struct libinput_device *device) +{ + return device->disableWhileTypingEnabledByDefault; +} + +int libinput_device_config_accel_is_available(struct libinput_device *device) +{ + return device->supportsPointerAcceleration; +} + +int libinput_device_config_calibration_has_matrix(struct libinput_device *device) +{ + return device->supportsCalibrationMatrix; +} + +enum libinput_config_status libinput_device_config_calibration_set_matrix(struct libinput_device *device, const float matrix[6]) +{ + for (std::size_t i = 0; i < 6; i++) { + device->calibrationMatrix[i] = matrix[i]; + } + return LIBINPUT_CONFIG_STATUS_SUCCESS; +} + +int libinput_device_config_calibration_get_default_matrix(struct libinput_device *device, float matrix[6]) +{ + for (std::size_t i = 0; i < 6; i++) { + matrix[i] = device->defaultCalibrationMatrix[i]; + } + return device->defaultCalibrationMatrixIsIdentity ? 0 : 1; +} + +int libinput_device_config_calibration_get_matrix(struct libinput_device *device, float matrix[6]) +{ + for (std::size_t i = 0; i < 6; i++) { + matrix[i] = device->calibrationMatrix[i]; + } + return device->calibrationMatrixIsIdentity ? 0 : 1; +} + +int libinput_device_config_left_handed_is_available(struct libinput_device *device) +{ + return device->supportsLeftHanded; +} + +uint32_t libinput_device_config_send_events_get_modes(struct libinput_device *device) +{ + uint32_t modes = LIBINPUT_CONFIG_SEND_EVENTS_ENABLED; + if (device->supportsDisableEvents) { + modes |= LIBINPUT_CONFIG_SEND_EVENTS_DISABLED; + } + if (device->supportsDisableEventsOnExternalMouse) { + modes |= LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE; + } + return modes; +} + +int libinput_device_config_left_handed_get(struct libinput_device *device) +{ + return device->leftHanded; +} + +double libinput_device_config_accel_get_default_speed(struct libinput_device *device) +{ + return device->defaultPointerAcceleration; +} + +int libinput_device_config_left_handed_get_default(struct libinput_device *device) +{ + return device->leftHandedEnabledByDefault; +} + +double libinput_device_config_accel_get_speed(struct libinput_device *device) +{ + return device->pointerAcceleration; +} + +uint32_t libinput_device_config_accel_get_profiles(struct libinput_device *device) +{ + return device->supportedPointerAccelerationProfiles; +} + +enum libinput_config_accel_profile libinput_device_config_accel_get_default_profile(struct libinput_device *device) +{ + return device->defaultPointerAccelerationProfile; +} + +enum libinput_config_status libinput_device_config_accel_set_profile(struct libinput_device *device, enum libinput_config_accel_profile profile) +{ + if (device->setPointerAccelerationProfileReturnValue == 0) { + if (!(device->supportedPointerAccelerationProfiles & profile) && profile != LIBINPUT_CONFIG_ACCEL_PROFILE_NONE) { + return LIBINPUT_CONFIG_STATUS_INVALID; + } + device->pointerAccelerationProfile = profile; + return LIBINPUT_CONFIG_STATUS_SUCCESS; + } + return LIBINPUT_CONFIG_STATUS_INVALID; +} + +enum libinput_config_accel_profile libinput_device_config_accel_get_profile(struct libinput_device *device) +{ + return device->pointerAccelerationProfile; +} + +uint32_t libinput_device_config_click_get_methods(struct libinput_device *device) +{ + return device->supportedClickMethods; +} + +enum libinput_config_click_method libinput_device_config_click_get_default_method(struct libinput_device *device) +{ + return device->defaultClickMethod; +} + +enum libinput_config_click_method libinput_device_config_click_get_method(struct libinput_device *device) +{ + return device->clickMethod; +} + +enum libinput_config_status libinput_device_config_click_set_method(struct libinput_device *device, enum libinput_config_click_method method) +{ + if (device->setClickMethodReturnValue == 0) { + if (!(device->supportedClickMethods & method) && method != LIBINPUT_CONFIG_CLICK_METHOD_NONE) { + return LIBINPUT_CONFIG_STATUS_INVALID; + } + device->clickMethod = method; + return LIBINPUT_CONFIG_STATUS_SUCCESS; + } + return LIBINPUT_CONFIG_STATUS_INVALID; +} + +uint32_t libinput_device_config_send_events_get_mode(struct libinput_device *device) +{ + const uint32_t enabledBits = device->enabled ? LIBINPUT_CONFIG_SEND_EVENTS_ENABLED : LIBINPUT_CONFIG_SEND_EVENTS_DISABLED; + const uint32_t externalMouseBits = device->disableEventsOnExternalMouse ? LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE : 0; + return enabledBits | externalMouseBits; +} + +uint32_t libinput_device_config_send_events_get_default_mode(struct libinput_device *device) +{ + return LIBINPUT_CONFIG_SEND_EVENTS_ENABLED; +} + +struct libinput_device *libinput_device_ref(struct libinput_device *device) +{ + return device; +} + +struct libinput_device *libinput_device_unref(struct libinput_device *device) +{ + return device; +} + +int libinput_device_get_size(struct libinput_device *device, double *width, double *height) +{ + if (device->deviceSizeReturnValue) { + return device->deviceSizeReturnValue; + } + if (width) { + *width = device->deviceSize.width(); + } + if (height) { + *height = device->deviceSize.height(); + } + return device->deviceSizeReturnValue; +} + +int libinput_device_pointer_has_button(struct libinput_device *device, uint32_t code) +{ + switch (code) { + case BTN_LEFT: + return device->supportedButtons.testFlag(Qt::LeftButton); + case BTN_MIDDLE: + return device->supportedButtons.testFlag(Qt::MiddleButton); + case BTN_RIGHT: + return device->supportedButtons.testFlag(Qt::RightButton); + case BTN_SIDE: + return device->supportedButtons.testFlag(Qt::ExtraButton1); + case BTN_EXTRA: + return device->supportedButtons.testFlag(Qt::ExtraButton2); + case BTN_FORWARD: + return device->supportedButtons.testFlag(Qt::ExtraButton3); + case BTN_BACK: + return device->supportedButtons.testFlag(Qt::ExtraButton4); + case BTN_TASK: + return device->supportedButtons.testFlag(Qt::ExtraButton5); + default: + return 0; + } +} + +enum libinput_config_status libinput_device_config_left_handed_set(struct libinput_device *device, int left_handed) +{ + if (device->setLeftHandedReturnValue == 0) { + device->leftHanded = left_handed; + return LIBINPUT_CONFIG_STATUS_SUCCESS; + } + return LIBINPUT_CONFIG_STATUS_INVALID; +} + +enum libinput_config_status libinput_device_config_accel_set_speed(struct libinput_device *device, double speed) +{ + if (device->setPointerAccelerationReturnValue == 0) { + device->pointerAcceleration = speed; + return LIBINPUT_CONFIG_STATUS_SUCCESS; + } + return LIBINPUT_CONFIG_STATUS_INVALID; +} + +enum libinput_config_status libinput_device_config_send_events_set_mode(struct libinput_device *device, uint32_t mode) +{ + if (device->setEnableModeReturnValue == 0) { + device->enabled = (mode & LIBINPUT_CONFIG_SEND_EVENTS_DISABLED) == 0; + device->disableEventsOnExternalMouse = mode & LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE; + return LIBINPUT_CONFIG_STATUS_SUCCESS; + } + return LIBINPUT_CONFIG_STATUS_INVALID; +} + +enum libinput_event_type libinput_event_get_type(struct libinput_event *event) +{ + return event->type; +} + +struct libinput_device *libinput_event_get_device(struct libinput_event *event) +{ + return event->device; +} + +void libinput_event_destroy(struct libinput_event *event) +{ + delete event; +} + +struct libinput_event_keyboard *libinput_event_get_keyboard_event(struct libinput_event *event) +{ + if (event->type == LIBINPUT_EVENT_KEYBOARD_KEY) { + return reinterpret_cast(event); + } + return nullptr; +} + +struct libinput_event_pointer *libinput_event_get_pointer_event(struct libinput_event *event) +{ + switch (event->type) { + case LIBINPUT_EVENT_POINTER_MOTION: + case LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE: + case LIBINPUT_EVENT_POINTER_BUTTON: + case LIBINPUT_EVENT_POINTER_SCROLL_WHEEL: + case LIBINPUT_EVENT_POINTER_SCROLL_FINGER: + case LIBINPUT_EVENT_POINTER_SCROLL_CONTINUOUS: + return reinterpret_cast(event); + default: + return nullptr; + } +} + +struct libinput_event_touch *libinput_event_get_touch_event(struct libinput_event *event) +{ + switch (event->type) { + case LIBINPUT_EVENT_TOUCH_DOWN: + case LIBINPUT_EVENT_TOUCH_UP: + case LIBINPUT_EVENT_TOUCH_MOTION: + case LIBINPUT_EVENT_TOUCH_CANCEL: + case LIBINPUT_EVENT_TOUCH_FRAME: + return reinterpret_cast(event); + default: + return nullptr; + } +} + +struct libinput_event_gesture *libinput_event_get_gesture_event(struct libinput_event *event) +{ + switch (event->type) { + case LIBINPUT_EVENT_GESTURE_PINCH_BEGIN: + case LIBINPUT_EVENT_GESTURE_PINCH_UPDATE: + case LIBINPUT_EVENT_GESTURE_PINCH_END: + case LIBINPUT_EVENT_GESTURE_SWIPE_BEGIN: + case LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE: + case LIBINPUT_EVENT_GESTURE_SWIPE_END: + return reinterpret_cast(event); + default: + return nullptr; + } +} + +int libinput_event_gesture_get_cancelled(struct libinput_event_gesture *event) +{ + if (event->type == LIBINPUT_EVENT_GESTURE_PINCH_END || event->type == LIBINPUT_EVENT_GESTURE_SWIPE_END) { + return event->cancelled; + } + return 0; +} + +uint64_t libinput_event_gesture_get_time_usec(struct libinput_event_gesture *event) +{ + return event->time.count(); +} + +int libinput_event_gesture_get_finger_count(struct libinput_event_gesture *event) +{ + return event->fingerCount; +} + +double libinput_event_gesture_get_dx(struct libinput_event_gesture *event) +{ + if (event->type == LIBINPUT_EVENT_GESTURE_PINCH_UPDATE || event->type == LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE) { + return event->delta.x(); + } + return 0.0; +} + +double libinput_event_gesture_get_dy(struct libinput_event_gesture *event) +{ + if (event->type == LIBINPUT_EVENT_GESTURE_PINCH_UPDATE || event->type == LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE) { + return event->delta.y(); + } + return 0.0; +} + +double libinput_event_gesture_get_dx_unaccelerated(struct libinput_event_gesture *event) +{ + return libinput_event_gesture_get_dx(event); +} + +double libinput_event_gesture_get_dy_unaccelerated(struct libinput_event_gesture *event) +{ + return libinput_event_gesture_get_dy(event); +} + +double libinput_event_gesture_get_scale(struct libinput_event_gesture *event) +{ + switch (event->type) { + case LIBINPUT_EVENT_GESTURE_PINCH_BEGIN: + return 1.0; + case LIBINPUT_EVENT_GESTURE_PINCH_UPDATE: + case LIBINPUT_EVENT_GESTURE_PINCH_END: + return event->scale; + default: + return 0.0; + } +} + +double libinput_event_gesture_get_angle_delta(struct libinput_event_gesture *event) +{ + if (event->type == LIBINPUT_EVENT_GESTURE_PINCH_UPDATE) { + return event->angleDelta; + } + return 0.0; +} + +uint32_t libinput_event_keyboard_get_key(struct libinput_event_keyboard *event) +{ + return event->key; +} + +enum libinput_key_state libinput_event_keyboard_get_key_state(struct libinput_event_keyboard *event) +{ + return event->state; +} + +uint64_t libinput_event_keyboard_get_time_usec(struct libinput_event_keyboard *event) +{ + return event->time.count(); +} + +double libinput_event_pointer_get_absolute_x(struct libinput_event_pointer *event) +{ + return event->absolutePos.x(); +} + +double libinput_event_pointer_get_absolute_y(struct libinput_event_pointer *event) +{ + return event->absolutePos.y(); +} + +double libinput_event_pointer_get_absolute_x_transformed(struct libinput_event_pointer *event, uint32_t width) +{ + double deviceWidth = 0.0; + double deviceHeight = 0.0; + libinput_device_get_size(event->device, &deviceWidth, &deviceHeight); + return event->absolutePos.x() / deviceWidth * width; +} + +double libinput_event_pointer_get_absolute_y_transformed(struct libinput_event_pointer *event, uint32_t height) +{ + double deviceWidth = 0.0; + double deviceHeight = 0.0; + libinput_device_get_size(event->device, &deviceWidth, &deviceHeight); + return event->absolutePos.y() / deviceHeight * height; +} + +double libinput_event_pointer_get_dx(struct libinput_event_pointer *event) +{ + return event->delta.x(); +} + +double libinput_event_pointer_get_dy(struct libinput_event_pointer *event) +{ + return event->delta.y(); +} + +double libinput_event_pointer_get_dx_unaccelerated(struct libinput_event_pointer *event) +{ + return event->delta.x(); +} + +double libinput_event_pointer_get_dy_unaccelerated(struct libinput_event_pointer *event) +{ + return event->delta.y(); +} + +uint64_t libinput_event_pointer_get_time_usec(struct libinput_event_pointer *event) +{ + return event->time.count(); +} + +uint32_t libinput_event_pointer_get_button(struct libinput_event_pointer *event) +{ + return event->button; +} + +enum libinput_button_state libinput_event_pointer_get_button_state(struct libinput_event_pointer *event) +{ + return event->buttonState; +} + +int libinput_event_pointer_has_axis(struct libinput_event_pointer *event, enum libinput_pointer_axis axis) +{ + if (axis == LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL) { + return event->verticalAxis; + } else { + return event->horizontalAxis; + } +} + +double libinput_event_pointer_get_scroll_value(struct libinput_event_pointer *event, enum libinput_pointer_axis axis) +{ + if (axis == LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL) { + return event->verticalScrollValue; + } else { + return event->horizontalScrollValue; + } +} + +double libinput_event_pointer_get_scroll_value_v120(struct libinput_event_pointer *event, enum libinput_pointer_axis axis) +{ + if (axis == LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL) { + return event->verticalScrollValueV120; + } else { + return event->horizontalScrollValueV120; + } +} + +uint64_t libinput_event_touch_get_time_usec(struct libinput_event_touch *event) +{ + return event->time.count(); +} + +double libinput_event_touch_get_x(struct libinput_event_touch *event) +{ + return event->absolutePos.x(); +} + +double libinput_event_touch_get_y(struct libinput_event_touch *event) +{ + return event->absolutePos.y(); +} + +double libinput_event_touch_get_x_transformed(struct libinput_event_touch *event, uint32_t width) +{ + double deviceWidth = 0.0; + double deviceHeight = 0.0; + libinput_device_get_size(event->device, &deviceWidth, &deviceHeight); + return event->absolutePos.x() / deviceWidth * width; +} + +double libinput_event_touch_get_y_transformed(struct libinput_event_touch *event, uint32_t height) +{ + double deviceWidth = 0.0; + double deviceHeight = 0.0; + libinput_device_get_size(event->device, &deviceWidth, &deviceHeight); + return event->absolutePos.y() / deviceHeight * height; +} + +int32_t libinput_event_touch_get_seat_slot(struct libinput_event_touch *event) +{ + return event->slot; +} + +struct libinput *libinput_udev_create_context(const struct libinput_interface *interface, void *user_data, struct udev *udev) +{ + if (!udev) { + return nullptr; + } + return new libinput; +} + +void libinput_log_set_priority(struct libinput *libinput, enum libinput_log_priority priority) +{ +} + +void libinput_log_set_handler(struct libinput *libinput, libinput_log_handler log_handler) +{ +} + +struct libinput *libinput_unref(struct libinput *libinput) +{ + libinput->refCount--; + if (libinput->refCount == 0) { + delete libinput; + return nullptr; + } + return libinput; +} + +int libinput_udev_assign_seat(struct libinput *libinput, const char *seat_id) +{ + if (libinput->assignSeatRetVal == 0) { + libinput->seat = QByteArray(seat_id); + } + return libinput->assignSeatRetVal; +} + +int libinput_get_fd(struct libinput *libinput) +{ + return -1; +} + +int libinput_dispatch(struct libinput *libinput) +{ + return 0; +} + +struct libinput_event *libinput_get_event(struct libinput *libinput) +{ + return nullptr; +} + +void libinput_suspend(struct libinput *libinput) +{ +} + +int libinput_resume(struct libinput *libinput) +{ + return 0; +} + +int libinput_device_config_middle_emulation_is_available(struct libinput_device *device) +{ + return device->supportsMiddleEmulation; +} + +enum libinput_config_status libinput_device_config_middle_emulation_set_enabled(struct libinput_device *device, enum libinput_config_middle_emulation_state enable) +{ + if (device->setMiddleEmulationReturnValue == 0) { + if (!device->supportsMiddleEmulation) { + return LIBINPUT_CONFIG_STATUS_INVALID; + } + device->middleEmulation = (enable == LIBINPUT_CONFIG_MIDDLE_EMULATION_ENABLED); + return LIBINPUT_CONFIG_STATUS_SUCCESS; + } + return LIBINPUT_CONFIG_STATUS_INVALID; +} + +enum libinput_config_middle_emulation_state libinput_device_config_middle_emulation_get_enabled(struct libinput_device *device) +{ + if (device->middleEmulation) { + return LIBINPUT_CONFIG_MIDDLE_EMULATION_ENABLED; + } else { + return LIBINPUT_CONFIG_MIDDLE_EMULATION_DISABLED; + } +} + +enum libinput_config_middle_emulation_state libinput_device_config_middle_emulation_get_default_enabled(struct libinput_device *device) +{ + if (device->middleEmulationEnabledByDefault) { + return LIBINPUT_CONFIG_MIDDLE_EMULATION_ENABLED; + } else { + return LIBINPUT_CONFIG_MIDDLE_EMULATION_DISABLED; + } +} + +int libinput_device_config_scroll_has_natural_scroll(struct libinput_device *device) +{ + return device->supportsNaturalScroll; +} + +enum libinput_config_status libinput_device_config_scroll_set_natural_scroll_enabled(struct libinput_device *device, int enable) +{ + if (device->setNaturalScrollReturnValue == 0) { + if (!device->supportsNaturalScroll) { + return LIBINPUT_CONFIG_STATUS_INVALID; + } + device->naturalScroll = enable; + return LIBINPUT_CONFIG_STATUS_SUCCESS; + } + return LIBINPUT_CONFIG_STATUS_INVALID; +} + +int libinput_device_config_scroll_get_natural_scroll_enabled(struct libinput_device *device) +{ + return device->naturalScroll; +} + +int libinput_device_config_scroll_get_default_natural_scroll_enabled(struct libinput_device *device) +{ + return device->naturalScrollEnabledByDefault; +} + +enum libinput_config_tap_button_map libinput_device_config_tap_get_default_button_map(struct libinput_device *device) +{ + return device->defaultTapButtonMap; +} + +enum libinput_config_status libinput_device_config_tap_set_button_map(struct libinput_device *device, enum libinput_config_tap_button_map map) +{ + if (device->setTapButtonMapReturnValue == 0) { + if (device->tapFingerCount == 0) { + return LIBINPUT_CONFIG_STATUS_INVALID; + } + device->tapButtonMap = map; + return LIBINPUT_CONFIG_STATUS_SUCCESS; + } + return LIBINPUT_CONFIG_STATUS_INVALID; +} + +enum libinput_config_tap_button_map libinput_device_config_tap_get_button_map(struct libinput_device *device) +{ + return device->tapButtonMap; +} + +uint32_t libinput_device_config_scroll_get_methods(struct libinput_device *device) +{ + return device->supportedScrollMethods; +} + +enum libinput_config_scroll_method libinput_device_config_scroll_get_default_method(struct libinput_device *device) +{ + return device->defaultScrollMethod; +} + +enum libinput_config_status libinput_device_config_scroll_set_method(struct libinput_device *device, enum libinput_config_scroll_method method) +{ + if (device->setScrollMethodReturnValue == 0) { + if (!(device->supportedScrollMethods & method) && method != LIBINPUT_CONFIG_SCROLL_NO_SCROLL) { + return LIBINPUT_CONFIG_STATUS_INVALID; + } + device->scrollMethod = method; + return LIBINPUT_CONFIG_STATUS_SUCCESS; + } + return LIBINPUT_CONFIG_STATUS_INVALID; +} + +enum libinput_config_scroll_method libinput_device_config_scroll_get_method(struct libinput_device *device) +{ + return device->scrollMethod; +} + +enum libinput_config_status libinput_device_config_scroll_set_button(struct libinput_device *device, uint32_t button) +{ + if (device->setScrollButtonReturnValue == 0) { + if (!(device->supportedScrollMethods & LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN)) { + return LIBINPUT_CONFIG_STATUS_UNSUPPORTED; + } + device->scrollButton = button; + return LIBINPUT_CONFIG_STATUS_SUCCESS; + } + return LIBINPUT_CONFIG_STATUS_INVALID; +} + +uint32_t libinput_device_config_scroll_get_button(struct libinput_device *device) +{ + return device->scrollButton; +} + +uint32_t libinput_device_config_scroll_get_default_button(struct libinput_device *device) +{ + return device->defaultScrollButton; +} + +int libinput_device_switch_has_switch(struct libinput_device *device, enum libinput_switch sw) +{ + switch (sw) { + case LIBINPUT_SWITCH_LID: + return device->lidSwitch; + case LIBINPUT_SWITCH_TABLET_MODE: + return device->tabletModeSwitch; + default: + Q_UNREACHABLE(); + } + return 0; +} + +struct libinput_event_switch *libinput_event_get_switch_event(struct libinput_event *event) +{ + if (event->type == LIBINPUT_EVENT_SWITCH_TOGGLE) { + return reinterpret_cast(event); + } else { + return nullptr; + } +} + +enum libinput_switch_state libinput_event_switch_get_switch_state(struct libinput_event_switch *event) +{ + switch (event->state) { + case libinput_event_switch::State::On: + return LIBINPUT_SWITCH_STATE_ON; + case libinput_event_switch::State::Off: + return LIBINPUT_SWITCH_STATE_OFF; + default: + Q_UNREACHABLE(); + } +} + +uint64_t libinput_event_switch_get_time_usec(struct libinput_event_switch *event) +{ + return event->time.count(); +} + +struct libinput_event_tablet_pad *libinput_event_get_tablet_pad_event(struct libinput_event *event) +{ + if (event->type == LIBINPUT_EVENT_TABLET_PAD_BUTTON) { + return reinterpret_cast(event); + } + return nullptr; +} + +struct libinput_event_tablet_tool * +libinput_event_get_tablet_tool_event(struct libinput_event *event) +{ + switch (event->type) { + case LIBINPUT_EVENT_TABLET_TOOL_AXIS: + case LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY: + case LIBINPUT_EVENT_TABLET_TOOL_TIP: + return reinterpret_cast(event); + default: + return nullptr; + } +} + +int libinput_device_tablet_pad_get_num_strips(struct libinput_device *device) +{ + return device->stripCount; +} + +int libinput_device_tablet_pad_get_num_rings(struct libinput_device *device) +{ + return device->ringCount; +} + +int libinput_device_tablet_pad_get_num_dials(struct libinput_device *device) +{ + return device->dialCount; +} + +int libinput_device_tablet_pad_get_num_buttons(struct libinput_device *device) +{ + return device->buttonCount; +} + +int libinput_device_tablet_pad_get_num_mode_groups(struct libinput_device *device) +{ + return 1; +} + +struct libinput_tablet_pad_mode_group * +libinput_device_tablet_pad_get_mode_group(struct libinput_device *device, unsigned int index) +{ + return nullptr; +} + +unsigned int +libinput_tablet_pad_mode_group_get_mode(struct libinput_tablet_pad_mode_group *group) +{ + return 0; +} + +unsigned int +libinput_tablet_pad_mode_group_get_num_modes(struct libinput_tablet_pad_mode_group *group) +{ + return 1; +} + +int libinput_tablet_pad_mode_group_has_button(struct libinput_tablet_pad_mode_group *group, + unsigned int button) +{ + return 1; +} + +int libinput_tablet_pad_mode_group_has_ring(struct libinput_tablet_pad_mode_group *group, + unsigned int ring) +{ + return 1; +} + +int libinput_tablet_pad_mode_group_has_strip(struct libinput_tablet_pad_mode_group *group, + unsigned int strip) +{ + return 1; +} + +int libinput_tablet_pad_mode_group_has_dial(struct libinput_tablet_pad_mode_group *group, + unsigned int dial) +{ + return 1; +} + +struct libinput_device_group * +libinput_device_get_device_group(struct libinput_device *device) +{ + return nullptr; +} + +void * +libinput_device_group_get_user_data(struct libinput_device_group *group) +{ + return nullptr; +} + +void libinput_device_led_update(struct libinput_device *device, + enum libinput_led leds) +{ +} + +void libinput_device_set_user_data(struct libinput_device *device, void *user_data) +{ + device->userData = user_data; +} + +void * +libinput_device_get_user_data(struct libinput_device *device) +{ + return device->userData; +} + +udev_device *libinput_device_get_udev_device(struct libinput_device *device) +{ + return nullptr; +} + +double +libinput_event_tablet_tool_get_x_transformed(struct libinput_event_tablet_tool *event, + uint32_t width) +{ + // it's unused at the moment, it doesn't really matter what we return + return 0; +} + +double +libinput_event_tablet_tool_get_y_transformed(struct libinput_event_tablet_tool *event, + uint32_t height) +{ + return 4; +} + +const char *udev_device_get_syspath(struct udev_device *device) +{ + return ""; +} + +const char *udev_device_get_devpath(struct udev_device *device) +{ + return ""; +} + +struct libinput_tablet_tool * +libinput_tablet_tool_ref(struct libinput_tablet_tool *tool) +{ + return tool; +} + +struct libinput_tablet_tool * +libinput_tablet_tool_unref(struct libinput_tablet_tool *tool) +{ + return tool; +} + +uint64_t +libinput_tablet_tool_get_serial(struct libinput_tablet_tool *tool) +{ + return 0; +} + +uint64_t +libinput_tablet_tool_get_tool_id(struct libinput_tablet_tool *tool) +{ + return 0; +} + +enum libinput_tablet_tool_type +libinput_tablet_tool_get_type(struct libinput_tablet_tool *tool) +{ + return LIBINPUT_TABLET_TOOL_TYPE_PEN; +} + +int libinput_tablet_tool_has_pressure(struct libinput_tablet_tool *tool) +{ + return 0; +} + +int libinput_tablet_tool_has_distance(struct libinput_tablet_tool *tool) +{ + return 0; +} + +int libinput_tablet_tool_has_tilt(struct libinput_tablet_tool *tool) +{ + return 0; +} + +int libinput_tablet_tool_has_rotation(struct libinput_tablet_tool *tool) +{ + return 0; +} + +int libinput_tablet_tool_has_slider(struct libinput_tablet_tool *tool) +{ + return 0; +} + +int libinput_tablet_tool_has_wheel(struct libinput_tablet_tool *tool) +{ + return 0; +} + +const char *udev_device_get_property_value(struct udev_device *udev_device, + const char *key) +{ + return ""; +} + +libinput_config_status +libinput_device_config_area_set_rectangle(struct libinput_device *device, + const struct libinput_config_area_rectangle *rect) +{ + return LIBINPUT_CONFIG_STATUS_UNSUPPORTED; +} + +int libinput_device_config_rotation_is_available(struct libinput_device *device) +{ + return device->pointer; +} + +unsigned int libinput_device_config_rotation_get_angle(struct libinput_device *device) +{ + return device->pointer ? device->rotation : 0; +} + +enum libinput_config_status +libinput_device_config_rotation_set_angle(struct libinput_device *device, + unsigned int degrees_cw) +{ + if (!device->pointer) { + return LIBINPUT_CONFIG_STATUS_UNSUPPORTED; + } + if (degrees_cw >= 360) { + return LIBINPUT_CONFIG_STATUS_INVALID; + } + device->rotation = degrees_cw; + return LIBINPUT_CONFIG_STATUS_SUCCESS; +} + +unsigned int +libinput_device_config_rotation_get_default_angle(struct libinput_device *device) +{ + return 0u; +} + +udev_device *udev_device_unref(struct udev_device *udev_device) +{ + return udev_device; +} + +int libinput_device_config_area_has_rectangle(struct libinput_device *device) +{ + return 0; +} diff --git a/local/recipes/kde/kwin/source/autotests/libinput/mock_libinput.h b/local/recipes/kde/kwin/source/autotests/libinput/mock_libinput.h new file mode 100644 index 0000000000..3eb4c53aeb --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/libinput/mock_libinput.h @@ -0,0 +1,175 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef MOCK_LIBINPUT_H +#define MOCK_LIBINPUT_H +#include + +#include +#include +#include +#include + +#include +#include + +struct libinput_device +{ + void *userData = nullptr; + bool keyboard = false; + bool pointer = false; + bool touch = false; + bool tabletTool = false; + bool gestureSupported = false; + bool switchDevice = false; + QByteArray name; + QByteArray sysName = QByteArrayLiteral("event0"); + QByteArray outputName; + quint32 product = 0; + quint32 vendor = 0; + quint32 busType = 3; // BUS_USB + int tapFingerCount = 0; + QSizeF deviceSize; + int deviceSizeReturnValue = 0; + bool tapEnabledByDefault = false; + bool tapToClick = false; + bool tapAndDragEnabledByDefault = false; + bool tapAndDrag = false; + bool tapDragLockEnabledByDefault = false; + bool tapDragLock = false; + bool supportsDisableWhileTyping = false; + bool supportsPointerAcceleration = false; + bool supportsLeftHanded = false; + bool supportsCalibrationMatrix = false; + bool supportsDisableEvents = false; + bool supportsDisableEventsOnExternalMouse = false; + bool supportsMiddleEmulation = false; + bool supportsNaturalScroll = false; + quint32 supportedScrollMethods = 0; + bool middleEmulationEnabledByDefault = false; + bool middleEmulation = false; + enum libinput_config_tap_button_map defaultTapButtonMap = LIBINPUT_CONFIG_TAP_MAP_LRM; + enum libinput_config_tap_button_map tapButtonMap = LIBINPUT_CONFIG_TAP_MAP_LRM; + int setTapButtonMapReturnValue = 0; + enum libinput_config_dwt_state disableWhileTypingEnabledByDefault = LIBINPUT_CONFIG_DWT_DISABLED; + enum libinput_config_dwt_state disableWhileTyping = LIBINPUT_CONFIG_DWT_DISABLED; + int setDisableWhileTypingReturnValue = 0; + qreal defaultPointerAcceleration = 0.0; + qreal pointerAcceleration = 0.0; + int setPointerAccelerationReturnValue = 0; + bool leftHandedEnabledByDefault = false; + bool leftHanded = false; + int setLeftHandedReturnValue = 0; + bool naturalScrollEnabledByDefault = false; + bool naturalScroll = false; + int setNaturalScrollReturnValue = 0; + enum libinput_config_scroll_method defaultScrollMethod = LIBINPUT_CONFIG_SCROLL_NO_SCROLL; + enum libinput_config_scroll_method scrollMethod = LIBINPUT_CONFIG_SCROLL_NO_SCROLL; + int setScrollMethodReturnValue = 0; + quint32 defaultScrollButton = 0; + quint32 scrollButton = 0; + int setScrollButtonReturnValue = 0; + Qt::MouseButtons supportedButtons; + QList keys; + bool enabled = true; + bool disableEventsOnExternalMouse = false; + int setEnableModeReturnValue = 0; + int setTapToClickReturnValue = 0; + int setTapAndDragReturnValue = 0; + int setTapDragLockReturnValue = 0; + int setMiddleEmulationReturnValue = 0; + quint32 supportedPointerAccelerationProfiles = 0; + enum libinput_config_accel_profile defaultPointerAccelerationProfile = LIBINPUT_CONFIG_ACCEL_PROFILE_NONE; + enum libinput_config_accel_profile pointerAccelerationProfile = LIBINPUT_CONFIG_ACCEL_PROFILE_NONE; + bool setPointerAccelerationProfileReturnValue = 0; + std::array defaultCalibrationMatrix{{1.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f}}; + std::array calibrationMatrix{{1.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f}}; + bool defaultCalibrationMatrixIsIdentity = true; + bool calibrationMatrixIsIdentity = true; + + bool lidSwitch = false; + bool tabletModeSwitch = false; + quint32 supportedClickMethods = 0; + enum libinput_config_click_method defaultClickMethod = LIBINPUT_CONFIG_CLICK_METHOD_NONE; + enum libinput_config_click_method clickMethod = LIBINPUT_CONFIG_CLICK_METHOD_NONE; + bool setClickMethodReturnValue = 0; + uint32_t buttonCount = 0; + uint32_t stripCount = 0; + uint32_t ringCount = 0; + uint32_t dialCount = 0; + uint32_t rotation = 0; +}; + +struct libinput_event +{ + virtual ~libinput_event() + { + } + libinput_device *device = nullptr; + libinput_event_type type = LIBINPUT_EVENT_NONE; + std::chrono::microseconds time = std::chrono::microseconds::zero(); +}; + +struct libinput_event_keyboard : libinput_event +{ + libinput_event_keyboard() + { + type = LIBINPUT_EVENT_KEYBOARD_KEY; + } + libinput_key_state state = LIBINPUT_KEY_STATE_RELEASED; + quint32 key = 0; +}; + +struct libinput_event_pointer : libinput_event +{ + libinput_button_state buttonState = LIBINPUT_BUTTON_STATE_RELEASED; + quint32 button = 0; + bool verticalAxis = false; + bool horizontalAxis = false; + qreal horizontalScrollValue = 0.0; + qreal verticalScrollValue = 0.0; + qreal horizontalScrollValueV120 = 0.0; + qreal verticalScrollValueV120 = 0.0; + QPointF delta; + QPointF absolutePos; +}; + +struct libinput_event_touch : libinput_event +{ + qint32 slot = -1; + QPointF absolutePos; +}; + +struct libinput_event_gesture : libinput_event +{ + int fingerCount = 0; + bool cancelled = false; + QPointF delta = QPointF(0, 0); + qreal scale = 0.0; + qreal angleDelta = 0.0; +}; + +struct libinput_event_switch : libinput_event +{ + enum class State { + Off, + On + }; + State state = State::Off; +}; + +struct libinput +{ + int refCount = 1; + QByteArray seat; + int assignSeatRetVal = 0; +}; + +#endif diff --git a/local/recipes/kde/kwin/source/autotests/libinput/pointer_event_test.cpp b/local/recipes/kde/kwin/source/autotests/libinput/pointer_event_test.cpp new file mode 100644 index 0000000000..cac5036700 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/libinput/pointer_event_test.cpp @@ -0,0 +1,292 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "mock_libinput.h" + +#include "backends/libinput/device.h" +#include "backends/libinput/events.h" + +#include + +#include + +Q_DECLARE_METATYPE(libinput_event_type) +Q_DECLARE_METATYPE(libinput_button_state) + +using namespace KWin::LibInput; +using namespace std::literals; + +class TestLibinputPointerEvent : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void init(); + void cleanup(); + + void testType_data(); + void testType(); + void testButton_data(); + void testButton(); + void testScrollWheel_data(); + void testScrollWheel(); + void testScrollFinger_data(); + void testScrollFinger(); + void testScrollContinuous_data(); + void testScrollContinuous(); + void testMotion(); + void testAbsoluteMotion(); + +private: + libinput_device *m_nativeDevice = nullptr; + Device *m_device = nullptr; +}; + +void TestLibinputPointerEvent::init() +{ + m_nativeDevice = new libinput_device; + m_nativeDevice->pointer = true; + m_nativeDevice->deviceSize = QSizeF(12.5, 13.8); + m_device = new Device(m_nativeDevice); +} + +void TestLibinputPointerEvent::cleanup() +{ + delete m_device; + m_device = nullptr; + + delete m_nativeDevice; + m_nativeDevice = nullptr; +} + +void TestLibinputPointerEvent::testType_data() +{ + QTest::addColumn("type"); + + QTest::newRow("motion") << LIBINPUT_EVENT_POINTER_MOTION; + QTest::newRow("absolute motion") << LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE; + QTest::newRow("button") << LIBINPUT_EVENT_POINTER_BUTTON; + QTest::newRow("scroll wheel") << LIBINPUT_EVENT_POINTER_SCROLL_WHEEL; + QTest::newRow("scroll finger") << LIBINPUT_EVENT_POINTER_SCROLL_FINGER; + QTest::newRow("scroll continuous") << LIBINPUT_EVENT_POINTER_SCROLL_CONTINUOUS; +} + +void TestLibinputPointerEvent::testType() +{ + // this test verifies the initialization of a PointerEvent and the parent Event class + libinput_event_pointer *pointerEvent = new libinput_event_pointer; + QFETCH(libinput_event_type, type); + pointerEvent->type = type; + pointerEvent->device = m_nativeDevice; + + std::unique_ptr event(Event::create(pointerEvent)); + // API of event + QCOMPARE(event->type(), type); + QCOMPARE(event->device(), m_device); + QCOMPARE(event->nativeDevice(), m_nativeDevice); + QCOMPARE((libinput_event *)(*event.get()), pointerEvent); + // verify it's a pointer event + QVERIFY(dynamic_cast(event.get())); + QCOMPARE((libinput_event_pointer *)(*dynamic_cast(event.get())), pointerEvent); +} + +void TestLibinputPointerEvent::testButton_data() +{ + QTest::addColumn("buttonState"); + QTest::addColumn("expectedButtonState"); + QTest::addColumn("button"); + QTest::addColumn("time"); + + QTest::newRow("pressed") << LIBINPUT_BUTTON_STATE_RELEASED << KWin::PointerButtonState::Released << quint32(BTN_RIGHT) << 100u; + QTest::newRow("released") << LIBINPUT_BUTTON_STATE_PRESSED << KWin::PointerButtonState::Pressed << quint32(BTN_LEFT) << 200u; +} + +void TestLibinputPointerEvent::testButton() +{ + // this test verifies the button press/release + libinput_event_pointer *pointerEvent = new libinput_event_pointer; + pointerEvent->device = m_nativeDevice; + pointerEvent->type = LIBINPUT_EVENT_POINTER_BUTTON; + QFETCH(libinput_button_state, buttonState); + pointerEvent->buttonState = buttonState; + QFETCH(quint32, button); + pointerEvent->button = button; + QFETCH(quint32, time); + pointerEvent->time = std::chrono::milliseconds(time); + + std::unique_ptr event(Event::create(pointerEvent)); + auto pe = dynamic_cast(event.get()); + QVERIFY(pe); + QCOMPARE(pe->type(), LIBINPUT_EVENT_POINTER_BUTTON); + QTEST(pe->buttonState(), "expectedButtonState"); + QCOMPARE(pe->button(), button); + QCOMPARE(pe->time(), pointerEvent->time); +} + +void TestLibinputPointerEvent::testScrollWheel_data() +{ + QTest::addColumn("horizontal"); + QTest::addColumn("vertical"); + QTest::addColumn("value"); + QTest::addColumn("valueV120"); + QTest::addColumn("time"); + + QTest::newRow("wheel/horizontal") << true << false << QPointF(3.0, 0.0) << QPoint(120, 0) << 100u; + QTest::newRow("wheel/vertical") << false << true << QPointF(0.0, 2.5) << QPoint(0, 120) << 200u; + QTest::newRow("wheel/both") << true << true << QPointF(1.1, 4.2) << QPoint(120, 120) << 300u; +} + +void TestLibinputPointerEvent::testScrollWheel() +{ + // this test verifies pointer axis functionality + libinput_event_pointer *pointerEvent = new libinput_event_pointer; + pointerEvent->device = m_nativeDevice; + pointerEvent->type = LIBINPUT_EVENT_POINTER_SCROLL_WHEEL; + QFETCH(bool, horizontal); + QFETCH(bool, vertical); + QFETCH(QPointF, value); + QFETCH(QPoint, valueV120); + QFETCH(quint32, time); + pointerEvent->horizontalAxis = horizontal; + pointerEvent->verticalAxis = vertical; + pointerEvent->horizontalScrollValue = value.x(); + pointerEvent->verticalScrollValue = value.y(); + pointerEvent->horizontalScrollValueV120 = valueV120.x(); + pointerEvent->verticalScrollValueV120 = valueV120.y(); + pointerEvent->time = std::chrono::milliseconds(time); + + std::unique_ptr event(Event::create(pointerEvent)); + auto pe = dynamic_cast(event.get()); + QVERIFY(pe); + QCOMPARE(pe->type(), LIBINPUT_EVENT_POINTER_SCROLL_WHEEL); + QCOMPARE(pe->axis().contains(KWin::PointerAxis::Horizontal), horizontal); + QCOMPARE(pe->axis().contains(KWin::PointerAxis::Vertical), vertical); + QCOMPARE(pe->scrollValue(KWin::PointerAxis::Horizontal), value.x()); + QCOMPARE(pe->scrollValue(KWin::PointerAxis::Vertical), value.y()); + QCOMPARE(pe->scrollValueV120(KWin::PointerAxis::Horizontal), valueV120.x()); + QCOMPARE(pe->scrollValueV120(KWin::PointerAxis::Vertical), valueV120.y()); + QCOMPARE(pe->time(), pointerEvent->time); +} + +void TestLibinputPointerEvent::testScrollFinger_data() +{ + QTest::addColumn("horizontal"); + QTest::addColumn("vertical"); + QTest::addColumn("value"); + QTest::addColumn("time"); + + QTest::newRow("finger/horizontal") << true << false << QPointF(3.0, 0.0) << 400u; + QTest::newRow("stop finger/horizontal") << true << false << QPointF(0.0, 0.0) << 500u; + QTest::newRow("finger/vertical") << false << true << QPointF(0.0, 2.5) << 600u; + QTest::newRow("stop finger/vertical") << false << true << QPointF(0.0, 0.0) << 700u; + QTest::newRow("finger/both") << true << true << QPointF(1.1, 4.2) << 800u; + QTest::newRow("stop finger/both") << true << true << QPointF(0.0, 0.0) << 900u; +} + +void TestLibinputPointerEvent::testScrollFinger() +{ + // this test verifies pointer axis functionality + libinput_event_pointer *pointerEvent = new libinput_event_pointer; + pointerEvent->device = m_nativeDevice; + pointerEvent->type = LIBINPUT_EVENT_POINTER_SCROLL_FINGER; + QFETCH(bool, horizontal); + QFETCH(bool, vertical); + QFETCH(QPointF, value); + QFETCH(quint32, time); + pointerEvent->horizontalAxis = horizontal; + pointerEvent->verticalAxis = vertical; + pointerEvent->horizontalScrollValue = value.x(); + pointerEvent->verticalScrollValue = value.y(); + pointerEvent->time = std::chrono::milliseconds(time); + + std::unique_ptr event(Event::create(pointerEvent)); + auto pe = dynamic_cast(event.get()); + QVERIFY(pe); + QCOMPARE(pe->type(), LIBINPUT_EVENT_POINTER_SCROLL_FINGER); + QCOMPARE(pe->axis().contains(KWin::PointerAxis::Horizontal), horizontal); + QCOMPARE(pe->axis().contains(KWin::PointerAxis::Vertical), vertical); + QCOMPARE(pe->scrollValue(KWin::PointerAxis::Horizontal), value.x()); + QCOMPARE(pe->scrollValue(KWin::PointerAxis::Vertical), value.y()); + QCOMPARE(pe->time(), pointerEvent->time); +} + +void TestLibinputPointerEvent::testScrollContinuous_data() +{ + QTest::addColumn("horizontal"); + QTest::addColumn("vertical"); + QTest::addColumn("value"); + QTest::addColumn("time"); + + QTest::newRow("continuous/horizontal") << true << false << QPointF(3.0, 0.0) << 1000u; + QTest::newRow("continuous/vertical") << false << true << QPointF(0.0, 2.5) << 1100u; + QTest::newRow("continuous/both") << true << true << QPointF(1.1, 4.2) << 1200u; +} + +void TestLibinputPointerEvent::testScrollContinuous() +{ + // this test verifies pointer axis functionality + libinput_event_pointer *pointerEvent = new libinput_event_pointer; + pointerEvent->device = m_nativeDevice; + pointerEvent->type = LIBINPUT_EVENT_POINTER_SCROLL_CONTINUOUS; + QFETCH(bool, horizontal); + QFETCH(bool, vertical); + QFETCH(QPointF, value); + QFETCH(quint32, time); + pointerEvent->horizontalAxis = horizontal; + pointerEvent->verticalAxis = vertical; + pointerEvent->horizontalScrollValue = value.x(); + pointerEvent->verticalScrollValue = value.y(); + pointerEvent->time = std::chrono::milliseconds(time); + + std::unique_ptr event(Event::create(pointerEvent)); + auto pe = dynamic_cast(event.get()); + QVERIFY(pe); + QCOMPARE(pe->type(), LIBINPUT_EVENT_POINTER_SCROLL_CONTINUOUS); + QCOMPARE(pe->axis().contains(KWin::PointerAxis::Horizontal), horizontal); + QCOMPARE(pe->axis().contains(KWin::PointerAxis::Vertical), vertical); + QCOMPARE(pe->scrollValue(KWin::PointerAxis::Horizontal), value.x()); + QCOMPARE(pe->scrollValue(KWin::PointerAxis::Vertical), value.y()); + QCOMPARE(pe->time(), pointerEvent->time); +} + +void TestLibinputPointerEvent::testMotion() +{ + // this test verifies pointer motion (delta) + libinput_event_pointer *pointerEvent = new libinput_event_pointer; + pointerEvent->device = m_nativeDevice; + pointerEvent->type = LIBINPUT_EVENT_POINTER_MOTION; + pointerEvent->delta = QPointF(2.1, 4.5); + pointerEvent->time = 500ms; + + std::unique_ptr event(Event::create(pointerEvent)); + auto pe = dynamic_cast(event.get()); + QVERIFY(pe); + QCOMPARE(pe->type(), LIBINPUT_EVENT_POINTER_MOTION); + QCOMPARE(pe->time(), pointerEvent->time); + QCOMPARE(pe->delta(), QPointF(2.1, 4.5)); +} + +void TestLibinputPointerEvent::testAbsoluteMotion() +{ + // this test verifies absolute pointer motion + libinput_event_pointer *pointerEvent = new libinput_event_pointer; + pointerEvent->device = m_nativeDevice; + pointerEvent->type = LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE; + pointerEvent->absolutePos = QPointF(6.25, 6.9); + pointerEvent->time = 500ms; + + std::unique_ptr event(Event::create(pointerEvent)); + auto pe = dynamic_cast(event.get()); + QVERIFY(pe); + QCOMPARE(pe->type(), LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE); + QCOMPARE(pe->time(), pointerEvent->time); + QCOMPARE(pe->absolutePos(), QPointF(6.25, 6.9)); + QCOMPARE(pe->absolutePos(QSize(1280, 1024)), QPointF(640, 512)); +} + +QTEST_GUILESS_MAIN(TestLibinputPointerEvent) +#include "pointer_event_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/libinput/touch_event_test.cpp b/local/recipes/kde/kwin/source/autotests/libinput/touch_event_test.cpp new file mode 100644 index 0000000000..5c740f1a56 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/libinput/touch_event_test.cpp @@ -0,0 +1,121 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "mock_libinput.h" + +#include "backends/libinput/device.h" +#include "backends/libinput/events.h" + +#include + +#include + +Q_DECLARE_METATYPE(libinput_event_type) + +using namespace KWin::LibInput; +using namespace std::literals; + +class TestLibinputTouchEvent : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void init(); + void cleanup(); + + void testType_data(); + void testType(); + void testAbsoluteMotion_data(); + void testAbsoluteMotion(); + +private: + libinput_device *m_nativeDevice = nullptr; + Device *m_device = nullptr; +}; + +void TestLibinputTouchEvent::init() +{ + m_nativeDevice = new libinput_device; + m_nativeDevice->touch = true; + m_nativeDevice->deviceSize = QSizeF(12.5, 13.8); + m_device = new Device(m_nativeDevice); +} + +void TestLibinputTouchEvent::cleanup() +{ + delete m_device; + m_device = nullptr; + + delete m_nativeDevice; + m_nativeDevice = nullptr; +} + +void TestLibinputTouchEvent::testType_data() +{ + QTest::addColumn("type"); + QTest::addColumn("hasId"); + + QTest::newRow("down") << LIBINPUT_EVENT_TOUCH_DOWN << true; + QTest::newRow("up") << LIBINPUT_EVENT_TOUCH_UP << true; + QTest::newRow("motion") << LIBINPUT_EVENT_TOUCH_MOTION << true; + QTest::newRow("cancel") << LIBINPUT_EVENT_TOUCH_CANCEL << false; + QTest::newRow("frame") << LIBINPUT_EVENT_TOUCH_FRAME << false; +} + +void TestLibinputTouchEvent::testType() +{ + // this test verifies the initialization of a PointerEvent and the parent Event class + libinput_event_touch *touchEvent = new libinput_event_touch; + QFETCH(libinput_event_type, type); + touchEvent->type = type; + touchEvent->device = m_nativeDevice; + touchEvent->slot = 0; + + std::unique_ptr event(Event::create(touchEvent)); + // API of event + QCOMPARE(event->type(), type); + QCOMPARE(event->device(), m_device); + QCOMPARE(event->nativeDevice(), m_nativeDevice); + QCOMPARE((libinput_event *)(*event.get()), touchEvent); + // verify it's a pointer event + QVERIFY(dynamic_cast(event.get())); + QCOMPARE((libinput_event_touch *)(*dynamic_cast(event.get())), touchEvent); + QFETCH(bool, hasId); + if (hasId) { + QCOMPARE(dynamic_cast(event.get())->id(), 0); + } +} + +void TestLibinputTouchEvent::testAbsoluteMotion_data() +{ + QTest::addColumn("type"); + QTest::newRow("down") << LIBINPUT_EVENT_TOUCH_DOWN; + QTest::newRow("motion") << LIBINPUT_EVENT_TOUCH_MOTION; +} + +void TestLibinputTouchEvent::testAbsoluteMotion() +{ + // this test verifies absolute touch points (either down or motion) + libinput_event_touch *touchEvent = new libinput_event_touch; + touchEvent->device = m_nativeDevice; + QFETCH(libinput_event_type, type); + touchEvent->type = type; + touchEvent->absolutePos = QPointF(6.25, 6.9); + touchEvent->time = 500ms; + touchEvent->slot = 1; + + std::unique_ptr event(Event::create(touchEvent)); + auto te = dynamic_cast(event.get()); + QVERIFY(te); + QCOMPARE(te->type(), type); + QCOMPARE(te->time(), touchEvent->time); + QCOMPARE(te->absolutePos(), QPointF(6.25, 6.9)); + QCOMPARE(te->absolutePos(QSize(1280, 1024)), QPointF(640, 512)); +} + +QTEST_GUILESS_MAIN(TestLibinputTouchEvent) +#include "touch_event_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/onscreennotificationtest.cpp b/local/recipes/kde/kwin/source/autotests/onscreennotificationtest.cpp new file mode 100644 index 0000000000..f4d373721e --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/onscreennotificationtest.cpp @@ -0,0 +1,124 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Graesslin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +*/ + +#include "onscreennotificationtest.h" + +#include "input.h" +#include "onscreennotification.h" + +#include +#include + +#include +#include +#include + +QTEST_MAIN(OnScreenNotificationTest); + +namespace KWin +{ + +void InputRedirection::installInputEventSpy(InputEventSpy *spy) +{ +} + +void InputRedirection::uninstallInputEventSpy(InputEventSpy *spy) +{ +} + +InputRedirection *InputRedirection::s_self = nullptr; + +} + +using KWin::OnScreenNotification; + +void OnScreenNotificationTest::show() +{ + OnScreenNotification notification; + auto config = KSharedConfig::openConfig(QString(), KSharedConfig::SimpleConfig); + KConfigGroup group = config->group(QStringLiteral("OnScreenNotification")); + group.writeEntry(QStringLiteral("QmlPath"), QString("/does/not/exist.qml")); + group.sync(); + notification.setConfig(config); + notification.setEngine(new QQmlEngine(¬ification)); + notification.setMessage(QStringLiteral("Some text so that we see it in the test")); + + QSignalSpy visibleChangedSpy(¬ification, &OnScreenNotification::visibleChanged); + QCOMPARE(notification.isVisible(), false); + notification.setVisible(true); + QCOMPARE(notification.isVisible(), true); + QCOMPARE(visibleChangedSpy.count(), 1); + + // show again should not trigger + notification.setVisible(true); + QCOMPARE(visibleChangedSpy.count(), 1); + + // timer should not have hidden + QTest::qWait(500); + QCOMPARE(notification.isVisible(), true); + + // hide again + notification.setVisible(false); + QCOMPARE(notification.isVisible(), false); + QCOMPARE(visibleChangedSpy.count(), 2); + + // now show with timer + notification.setTimeout(250); + notification.setVisible(true); + QCOMPARE(notification.isVisible(), true); + QCOMPARE(visibleChangedSpy.count(), 3); + QVERIFY(visibleChangedSpy.wait()); + QCOMPARE(notification.isVisible(), false); + QCOMPARE(visibleChangedSpy.count(), 4); +} + +void OnScreenNotificationTest::timeout() +{ + OnScreenNotification notification; + QSignalSpy timeoutChangedSpy(¬ification, &OnScreenNotification::timeoutChanged); + QCOMPARE(notification.timeout(), 0); + notification.setTimeout(1000); + QCOMPARE(notification.timeout(), 1000); + QCOMPARE(timeoutChangedSpy.count(), 1); + notification.setTimeout(1000); + QCOMPARE(timeoutChangedSpy.count(), 1); + notification.setTimeout(0); + QCOMPARE(notification.timeout(), 0); + QCOMPARE(timeoutChangedSpy.count(), 2); +} + +void OnScreenNotificationTest::iconName() +{ + OnScreenNotification notification; + QSignalSpy iconNameChangedSpy(¬ification, &OnScreenNotification::iconNameChanged); + QCOMPARE(notification.iconName(), QString()); + notification.setIconName(QStringLiteral("foo")); + QCOMPARE(notification.iconName(), QStringLiteral("foo")); + QCOMPARE(iconNameChangedSpy.count(), 1); + notification.setIconName(QStringLiteral("foo")); + QCOMPARE(iconNameChangedSpy.count(), 1); + notification.setIconName(QStringLiteral("bar")); + QCOMPARE(notification.iconName(), QStringLiteral("bar")); + QCOMPARE(iconNameChangedSpy.count(), 2); +} + +void OnScreenNotificationTest::message() +{ + OnScreenNotification notification; + QSignalSpy messageChangedSpy(¬ification, &OnScreenNotification::messageChanged); + QCOMPARE(notification.message(), QString()); + notification.setMessage(QStringLiteral("foo")); + QCOMPARE(notification.message(), QStringLiteral("foo")); + QCOMPARE(messageChangedSpy.count(), 1); + notification.setMessage(QStringLiteral("foo")); + QCOMPARE(messageChangedSpy.count(), 1); + notification.setMessage(QStringLiteral("bar")); + QCOMPARE(notification.message(), QStringLiteral("bar")); + QCOMPARE(messageChangedSpy.count(), 2); +} + +#include "moc_onscreennotificationtest.cpp" diff --git a/local/recipes/kde/kwin/source/autotests/onscreennotificationtest.h b/local/recipes/kde/kwin/source/autotests/onscreennotificationtest.h new file mode 100644 index 0000000000..6c1583cf95 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/onscreennotificationtest.h @@ -0,0 +1,24 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Graesslin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +*/ + +#ifndef ONSCREENNOTIFICATIONTEST_H +#define ONSCREENNOTIFICATIONTEST_H + +#include + +class OnScreenNotificationTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + + void show(); + void timeout(); + void iconName(); + void message(); +}; + +#endif // ONSCREENNOTIFICATIONTEST_H diff --git a/local/recipes/kde/kwin/source/autotests/opengl_context_attribute_builder_test.cpp b/local/recipes/kde/kwin/source/autotests/opengl_context_attribute_builder_test.cpp new file mode 100644 index 0000000000..872b6274d1 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/opengl_context_attribute_builder_test.cpp @@ -0,0 +1,379 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "opengl/abstract_opengl_context_attribute_builder.h" +#include "opengl/egl_context_attribute_builder.h" +#include +#include + +#include "config-kwin.h" + +using namespace KWin; + +class OpenGLContextAttributeBuilderTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testCtor(); + void testRobust(); + void testForwardCompatible(); + void testProfile(); + void testResetOnVideoMemoryPurge(); + void testVersionMajor(); + void testVersionMajorAndMinor(); + void testHighPriority(); + void testEgl_data(); + void testEgl(); + void testGles_data(); + void testGles(); +}; + +class MockOpenGLContextAttributeBuilder : public AbstractOpenGLContextAttributeBuilder +{ +public: + std::vector build() const override; +}; + +std::vector MockOpenGLContextAttributeBuilder::build() const +{ + return std::vector(); +} + +void OpenGLContextAttributeBuilderTest::testCtor() +{ + MockOpenGLContextAttributeBuilder builder; + QCOMPARE(builder.isVersionRequested(), false); + QCOMPARE(builder.majorVersion(), 0); + QCOMPARE(builder.minorVersion(), 0); + QCOMPARE(builder.isRobust(), false); + QCOMPARE(builder.isForwardCompatible(), false); + QCOMPARE(builder.isCoreProfile(), false); + QCOMPARE(builder.isCompatibilityProfile(), false); + QCOMPARE(builder.isResetOnVideoMemoryPurge(), false); + QCOMPARE(builder.isHighPriority(), false); +} + +void OpenGLContextAttributeBuilderTest::testRobust() +{ + MockOpenGLContextAttributeBuilder builder; + QCOMPARE(builder.isRobust(), false); + builder.setRobust(true); + QCOMPARE(builder.isRobust(), true); + builder.setRobust(false); + QCOMPARE(builder.isRobust(), false); +} + +void OpenGLContextAttributeBuilderTest::testForwardCompatible() +{ + MockOpenGLContextAttributeBuilder builder; + QCOMPARE(builder.isForwardCompatible(), false); + builder.setForwardCompatible(true); + QCOMPARE(builder.isForwardCompatible(), true); + builder.setForwardCompatible(false); + QCOMPARE(builder.isForwardCompatible(), false); +} + +void OpenGLContextAttributeBuilderTest::testProfile() +{ + MockOpenGLContextAttributeBuilder builder; + QCOMPARE(builder.isCoreProfile(), false); + QCOMPARE(builder.isCompatibilityProfile(), false); + builder.setCoreProfile(true); + QCOMPARE(builder.isCoreProfile(), true); + QCOMPARE(builder.isCompatibilityProfile(), false); + builder.setCompatibilityProfile(true); + QCOMPARE(builder.isCoreProfile(), false); + QCOMPARE(builder.isCompatibilityProfile(), true); + builder.setCoreProfile(true); + QCOMPARE(builder.isCoreProfile(), true); + QCOMPARE(builder.isCompatibilityProfile(), false); +} + +void OpenGLContextAttributeBuilderTest::testResetOnVideoMemoryPurge() +{ + MockOpenGLContextAttributeBuilder builder; + QCOMPARE(builder.isResetOnVideoMemoryPurge(), false); + builder.setResetOnVideoMemoryPurge(true); + QCOMPARE(builder.isResetOnVideoMemoryPurge(), true); + builder.setResetOnVideoMemoryPurge(false); + QCOMPARE(builder.isResetOnVideoMemoryPurge(), false); +} + +void OpenGLContextAttributeBuilderTest::testHighPriority() +{ + MockOpenGLContextAttributeBuilder builder; + QCOMPARE(builder.isHighPriority(), false); + builder.setHighPriority(true); + QCOMPARE(builder.isHighPriority(), true); + builder.setHighPriority(false); + QCOMPARE(builder.isHighPriority(), false); +} + +void OpenGLContextAttributeBuilderTest::testVersionMajor() +{ + MockOpenGLContextAttributeBuilder builder; + builder.setVersion(2); + QCOMPARE(builder.isVersionRequested(), true); + QCOMPARE(builder.majorVersion(), 2); + QCOMPARE(builder.minorVersion(), 0); + builder.setVersion(3); + QCOMPARE(builder.isVersionRequested(), true); + QCOMPARE(builder.majorVersion(), 3); + QCOMPARE(builder.minorVersion(), 0); +} + +void OpenGLContextAttributeBuilderTest::testVersionMajorAndMinor() +{ + MockOpenGLContextAttributeBuilder builder; + builder.setVersion(2, 1); + QCOMPARE(builder.isVersionRequested(), true); + QCOMPARE(builder.majorVersion(), 2); + QCOMPARE(builder.minorVersion(), 1); + builder.setVersion(3, 2); + QCOMPARE(builder.isVersionRequested(), true); + QCOMPARE(builder.majorVersion(), 3); + QCOMPARE(builder.minorVersion(), 2); +} + +void OpenGLContextAttributeBuilderTest::testEgl_data() +{ + QTest::addColumn("requestVersion"); + QTest::addColumn("major"); + QTest::addColumn("minor"); + QTest::addColumn("robust"); + QTest::addColumn("forwardCompatible"); + QTest::addColumn("coreProfile"); + QTest::addColumn("compatibilityProfile"); + QTest::addColumn("highPriority"); + QTest::addColumn>("expectedAttribs"); + + QTest::newRow("fallback") << false << 0 << 0 << false << false << false << false << false << std::vector{EGL_NONE}; + QTest::newRow("legacy/robust") + << false << 0 << 0 << true << false << false << false << false + << std::vector{ + EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_KHR, EGL_LOSE_CONTEXT_ON_RESET_KHR, + EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR, + EGL_NONE}; + QTest::newRow("legacy/robust/high priority") + << false << 0 << 0 << true << false << false << false << true + << std::vector{ + EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_KHR, EGL_LOSE_CONTEXT_ON_RESET_KHR, + EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR, + EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG, + EGL_NONE}; + QTest::newRow("core") + << true << 3 << 1 << false << false << false << false << false + << std::vector{ + EGL_CONTEXT_MAJOR_VERSION_KHR, 3, + EGL_CONTEXT_MINOR_VERSION_KHR, 1, + EGL_NONE}; + QTest::newRow("core/high priority") + << true << 3 << 1 << false << false << false << false << true + << std::vector{ + EGL_CONTEXT_MAJOR_VERSION_KHR, 3, + EGL_CONTEXT_MINOR_VERSION_KHR, 1, + EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG, + EGL_NONE}; + QTest::newRow("core/robust") + << true << 3 << 1 << true << false << false << false << false + << std::vector{ + EGL_CONTEXT_MAJOR_VERSION_KHR, 3, + EGL_CONTEXT_MINOR_VERSION_KHR, 1, + EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_KHR, EGL_LOSE_CONTEXT_ON_RESET_KHR, + EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR, + EGL_NONE}; + QTest::newRow("core/robust/high priority") + << true << 3 << 1 << true << false << false << false << true + << std::vector{ + EGL_CONTEXT_MAJOR_VERSION_KHR, 3, + EGL_CONTEXT_MINOR_VERSION_KHR, 1, + EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_KHR, EGL_LOSE_CONTEXT_ON_RESET_KHR, + EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR, + EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG, + EGL_NONE}; + QTest::newRow("core/robust/forward compatible") + << true << 3 << 1 << true << true << false << false << false + << std::vector{ + EGL_CONTEXT_MAJOR_VERSION_KHR, 3, + EGL_CONTEXT_MINOR_VERSION_KHR, 1, + EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_KHR, EGL_LOSE_CONTEXT_ON_RESET_KHR, + EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR | EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR, + EGL_NONE}; + QTest::newRow("core/robust/forward compatible/high priority") + << true << 3 << 1 << true << true << false << false << true + << std::vector{ + EGL_CONTEXT_MAJOR_VERSION_KHR, 3, + EGL_CONTEXT_MINOR_VERSION_KHR, 1, + EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_KHR, EGL_LOSE_CONTEXT_ON_RESET_KHR, + EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR | EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR, + EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG, + EGL_NONE}; + QTest::newRow("core/forward compatible") + << true << 3 << 1 << false << true << false << false << false + << std::vector{ + EGL_CONTEXT_MAJOR_VERSION_KHR, 3, + EGL_CONTEXT_MINOR_VERSION_KHR, 1, + EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR, + EGL_NONE}; + QTest::newRow("core/forward compatible/high priority") + << true << 3 << 1 << false << true << false << false << true + << std::vector{ + EGL_CONTEXT_MAJOR_VERSION_KHR, 3, + EGL_CONTEXT_MINOR_VERSION_KHR, 1, + EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR, + EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG, + EGL_NONE}; + QTest::newRow("core profile/forward compatible") + << true << 3 << 2 << false << true << true << false << false + << std::vector{ + EGL_CONTEXT_MAJOR_VERSION_KHR, 3, + EGL_CONTEXT_MINOR_VERSION_KHR, 2, + EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR, + EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR, + EGL_NONE}; + QTest::newRow("core profile/forward compatible/high priority") + << true << 3 << 2 << false << true << true << false << true + << std::vector{ + EGL_CONTEXT_MAJOR_VERSION_KHR, 3, + EGL_CONTEXT_MINOR_VERSION_KHR, 2, + EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR, + EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR, + EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG, + EGL_NONE}; + QTest::newRow("compatibility profile/forward compatible") + << true << 3 << 2 << false << true << false << true << false + << std::vector{ + EGL_CONTEXT_MAJOR_VERSION_KHR, 3, + EGL_CONTEXT_MINOR_VERSION_KHR, 2, + EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR, + EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR, EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT_KHR, + EGL_NONE}; + QTest::newRow("compatibility profile/forward compatible/high priority") + << true << 3 << 2 << false << true << false << true << true + << std::vector{ + EGL_CONTEXT_MAJOR_VERSION_KHR, 3, + EGL_CONTEXT_MINOR_VERSION_KHR, 2, + EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR, + EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR, EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT_KHR, + EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG, + EGL_NONE}; + QTest::newRow("core profile/robust/forward compatible") + << true << 3 << 2 << true << true << true << false << false + << std::vector{ + EGL_CONTEXT_MAJOR_VERSION_KHR, 3, + EGL_CONTEXT_MINOR_VERSION_KHR, 2, + EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_KHR, EGL_LOSE_CONTEXT_ON_RESET_KHR, + EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR | EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR, + EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR, + EGL_NONE}; + QTest::newRow("core profile/robust/forward compatible/high priority") + << true << 3 << 2 << true << true << true << false << true + << std::vector{ + EGL_CONTEXT_MAJOR_VERSION_KHR, 3, + EGL_CONTEXT_MINOR_VERSION_KHR, 2, + EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_KHR, EGL_LOSE_CONTEXT_ON_RESET_KHR, + EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR | EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR, + EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR, + EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG, + EGL_NONE}; + QTest::newRow("compatibility profile/robust/forward compatible") + << true << 3 << 2 << true << true << false << true << false + << std::vector{ + EGL_CONTEXT_MAJOR_VERSION_KHR, 3, + EGL_CONTEXT_MINOR_VERSION_KHR, 2, + EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_KHR, EGL_LOSE_CONTEXT_ON_RESET_KHR, + EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR | EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR, + EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR, EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT_KHR, + EGL_NONE}; + QTest::newRow("compatibility profile/robust/forward compatible/high priority") + << true << 3 << 2 << true << true << false << true << true + << std::vector{ + EGL_CONTEXT_MAJOR_VERSION_KHR, 3, + EGL_CONTEXT_MINOR_VERSION_KHR, 2, + EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_KHR, EGL_LOSE_CONTEXT_ON_RESET_KHR, + EGL_CONTEXT_FLAGS_KHR, EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR | EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR, + EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR, EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT_KHR, + EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG, + EGL_NONE}; +} + +void OpenGLContextAttributeBuilderTest::testEgl() +{ + QFETCH(bool, requestVersion); + QFETCH(int, major); + QFETCH(int, minor); + QFETCH(bool, robust); + QFETCH(bool, forwardCompatible); + QFETCH(bool, coreProfile); + QFETCH(bool, compatibilityProfile); + QFETCH(bool, highPriority); + + EglContextAttributeBuilder builder; + if (requestVersion) { + builder.setVersion(major, minor); + } + builder.setRobust(robust); + builder.setForwardCompatible(forwardCompatible); + builder.setCoreProfile(coreProfile); + builder.setCompatibilityProfile(compatibilityProfile); + builder.setHighPriority(highPriority); + + auto attribs = builder.build(); + QTEST(attribs, "expectedAttribs"); +} + +void OpenGLContextAttributeBuilderTest::testGles_data() +{ + QTest::addColumn("robust"); + QTest::addColumn("highPriority"); + QTest::addColumn>("expectedAttribs"); + + QTest::newRow("robust") + << true << false + << std::vector{ + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_CONTEXT_OPENGL_ROBUST_ACCESS_EXT, EGL_TRUE, + EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_EXT, EGL_LOSE_CONTEXT_ON_RESET_EXT, + EGL_NONE}; + QTest::newRow("robust/high priority") + << true << true + << std::vector{ + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_CONTEXT_OPENGL_ROBUST_ACCESS_EXT, EGL_TRUE, + EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_EXT, EGL_LOSE_CONTEXT_ON_RESET_EXT, + EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG, + EGL_NONE}; + QTest::newRow("normal") + << false << false + << std::vector{ + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE}; + QTest::newRow("normal/high priority") + << false << true + << std::vector{ + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_CONTEXT_PRIORITY_LEVEL_IMG, EGL_CONTEXT_PRIORITY_HIGH_IMG, + EGL_NONE}; +} + +void OpenGLContextAttributeBuilderTest::testGles() +{ + QFETCH(bool, robust); + QFETCH(bool, highPriority); + + EglOpenGLESContextAttributeBuilder builder; + builder.setVersion(2); + builder.setRobust(robust); + builder.setHighPriority(highPriority); + + auto attribs = builder.build(); + QTEST(attribs, "expectedAttribs"); +} + +QTEST_GUILESS_MAIN(OpenGLContextAttributeBuilderTest) +#include "opengl_context_attribute_builder_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/output_transform_test.cpp b/local/recipes/kde/kwin/source/autotests/output_transform_test.cpp new file mode 100644 index 0000000000..952015739d --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/output_transform_test.cpp @@ -0,0 +1,477 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +#include "core/output.h" + +using namespace KWin; + +class TestOutputTransform : public QObject +{ + Q_OBJECT + +public: + TestOutputTransform(); + +private Q_SLOTS: + void mapSizeF_data(); + void mapSizeF(); + void mapSize_data(); + void mapSize(); + void mapRectF_data(); + void mapRectF(); + void mapRect_data(); + void mapRect(); + void mapPointF_data(); + void mapPointF(); + void mapPoint_data(); + void mapPoint(); + void inverted_data(); + void inverted(); + void combine_data(); + void combine(); + void matrix_data(); + void matrix(); +}; + +TestOutputTransform::TestOutputTransform() +{ +} + +void TestOutputTransform::mapSizeF_data() +{ + QTest::addColumn("kind"); + QTest::addColumn("source"); + QTest::addColumn("target"); + + QTest::addRow("rotate-0") << OutputTransform::Normal << QSizeF(10, 20) << QSizeF(10, 20); + QTest::addRow("rotate-90") << OutputTransform::Rotate90 << QSizeF(10, 20) << QSizeF(20, 10); + QTest::addRow("rotate-180") << OutputTransform::Rotate180 << QSizeF(10, 20) << QSizeF(10, 20); + QTest::addRow("rotate-270") << OutputTransform::Rotate270 << QSizeF(10, 20) << QSizeF(20, 10); + QTest::addRow("flip-x-0") << OutputTransform::FlipX << QSizeF(10, 20) << QSizeF(10, 20); + QTest::addRow("flip-x-90") << OutputTransform::FlipX90 << QSizeF(10, 20) << QSizeF(20, 10); + QTest::addRow("flip-x-180") << OutputTransform::FlipX180 << QSizeF(10, 20) << QSizeF(10, 20); + QTest::addRow("flip-x-270") << OutputTransform::FlipX270 << QSizeF(10, 20) << QSizeF(20, 10); + QTest::addRow("flip-y-0") << OutputTransform::FlipY << QSizeF(10, 20) << QSizeF(10, 20); + QTest::addRow("flip-y-90") << OutputTransform::FlipY90 << QSizeF(10, 20) << QSizeF(20, 10); + QTest::addRow("flip-y-180") << OutputTransform::FlipY180 << QSizeF(10, 20) << QSizeF(10, 20); + QTest::addRow("flip-y-270") << OutputTransform::FlipY270 << QSizeF(10, 20) << QSizeF(20, 10); +} + +void TestOutputTransform::mapSizeF() +{ + QFETCH(OutputTransform::Kind, kind); + QFETCH(QSizeF, source); + QFETCH(QSizeF, target); + + QCOMPARE(OutputTransform(kind).map(source), target); +} + +void TestOutputTransform::mapSize_data() +{ + QTest::addColumn("kind"); + QTest::addColumn("source"); + QTest::addColumn("target"); + + QTest::addRow("rotate-0") << OutputTransform::Normal << QSize(10, 20) << QSize(10, 20); + QTest::addRow("rotate-90") << OutputTransform::Rotate90 << QSize(10, 20) << QSize(20, 10); + QTest::addRow("rotate-180") << OutputTransform::Rotate180 << QSize(10, 20) << QSize(10, 20); + QTest::addRow("rotate-270") << OutputTransform::Rotate270 << QSize(10, 20) << QSize(20, 10); + QTest::addRow("flip-x-0") << OutputTransform::FlipX << QSize(10, 20) << QSize(10, 20); + QTest::addRow("flip-x-90") << OutputTransform::FlipX90 << QSize(10, 20) << QSize(20, 10); + QTest::addRow("flip-x-180") << OutputTransform::FlipX180 << QSize(10, 20) << QSize(10, 20); + QTest::addRow("flip-x-270") << OutputTransform::FlipX270 << QSize(10, 20) << QSize(20, 10); + QTest::addRow("flip-y-0") << OutputTransform::FlipY << QSize(10, 20) << QSize(10, 20); + QTest::addRow("flip-y-90") << OutputTransform::FlipY90 << QSize(10, 20) << QSize(20, 10); + QTest::addRow("flip-y-180") << OutputTransform::FlipY180 << QSize(10, 20) << QSize(10, 20); + QTest::addRow("flip-y-270") << OutputTransform::FlipY270 << QSize(10, 20) << QSize(20, 10); +} + +void TestOutputTransform::mapSize() +{ + QFETCH(OutputTransform::Kind, kind); + QFETCH(QSize, source); + QFETCH(QSize, target); + + QCOMPARE(OutputTransform(kind).map(source), target); +} + +void TestOutputTransform::mapRectF_data() +{ + QTest::addColumn("kind"); + QTest::addColumn("source"); + QTest::addColumn("target"); + + QTest::addRow("rotate-0") << OutputTransform::Normal << RectF(10, 20, 30, 40) << RectF(10, 20, 30, 40); + QTest::addRow("rotate-90") << OutputTransform::Rotate90 << RectF(10, 20, 30, 40) << RectF(20, 60, 40, 30); + QTest::addRow("rotate-180") << OutputTransform::Rotate180 << RectF(10, 20, 30, 40) << RectF(60, 140, 30, 40); + QTest::addRow("rotate-270") << OutputTransform::Rotate270 << RectF(10, 20, 30, 40) << RectF(140, 10, 40, 30); + QTest::addRow("flip-x-0") << OutputTransform::FlipX << RectF(10, 20, 30, 40) << RectF(60, 20, 30, 40); + QTest::addRow("flip-x-90") << OutputTransform::FlipX90 << RectF(10, 20, 30, 40) << RectF(20, 10, 40, 30); + QTest::addRow("flip-x-180") << OutputTransform::FlipX180 << RectF(10, 20, 30, 40) << RectF(10, 140, 30, 40); + QTest::addRow("flip-x-270") << OutputTransform::FlipX270 << RectF(10, 20, 30, 40) << RectF(140, 60, 40, 30); + QTest::addRow("flip-y-0") << OutputTransform::FlipY << RectF(10, 20, 30, 40) << RectF(10, 140, 30, 40); + QTest::addRow("flip-y-90") << OutputTransform::FlipY90 << RectF(10, 20, 30, 40) << RectF(140, 60, 40, 30); + QTest::addRow("flip-y-180") << OutputTransform::FlipY180 << RectF(10, 20, 30, 40) << RectF(60, 20, 30, 40); + QTest::addRow("flip-y-270") << OutputTransform::FlipY270 << RectF(10, 20, 30, 40) << RectF(20, 10, 40, 30); +} + +void TestOutputTransform::mapRectF() +{ + QFETCH(OutputTransform::Kind, kind); + QFETCH(RectF, source); + QFETCH(RectF, target); + + QCOMPARE(OutputTransform(kind).map(source, QSizeF(100, 200)), target); +} + +void TestOutputTransform::mapRect_data() +{ + QTest::addColumn("kind"); + QTest::addColumn("source"); + QTest::addColumn("target"); + + QTest::addRow("rotate-0") << OutputTransform::Normal << Rect(10, 20, 30, 40) << Rect(10, 20, 30, 40); + QTest::addRow("rotate-90") << OutputTransform::Rotate90 << Rect(10, 20, 30, 40) << Rect(20, 60, 40, 30); + QTest::addRow("rotate-180") << OutputTransform::Rotate180 << Rect(10, 20, 30, 40) << Rect(60, 140, 30, 40); + QTest::addRow("rotate-270") << OutputTransform::Rotate270 << Rect(10, 20, 30, 40) << Rect(140, 10, 40, 30); + QTest::addRow("flip-x-0") << OutputTransform::FlipX << Rect(10, 20, 30, 40) << Rect(60, 20, 30, 40); + QTest::addRow("flip-x-90") << OutputTransform::FlipX90 << Rect(10, 20, 30, 40) << Rect(20, 10, 40, 30); + QTest::addRow("flip-x-180") << OutputTransform::FlipX180 << Rect(10, 20, 30, 40) << Rect(10, 140, 30, 40); + QTest::addRow("flip-x-270") << OutputTransform::FlipX270 << Rect(10, 20, 30, 40) << Rect(140, 60, 40, 30); + QTest::addRow("flip-y-0") << OutputTransform::FlipY << Rect(10, 20, 30, 40) << Rect(10, 140, 30, 40); + QTest::addRow("flip-y-90") << OutputTransform::FlipY90 << Rect(10, 20, 30, 40) << Rect(140, 60, 40, 30); + QTest::addRow("flip-y-180") << OutputTransform::FlipY180 << Rect(10, 20, 30, 40) << Rect(60, 20, 30, 40); + QTest::addRow("flip-y-270") << OutputTransform::FlipY270 << Rect(10, 20, 30, 40) << Rect(20, 10, 40, 30); +} + +void TestOutputTransform::mapRect() +{ + QFETCH(OutputTransform::Kind, kind); + QFETCH(Rect, source); + QFETCH(Rect, target); + + QCOMPARE(OutputTransform(kind).map(source, QSize(100, 200)), target); +} + +void TestOutputTransform::mapPointF_data() +{ + QTest::addColumn("kind"); + QTest::addColumn("source"); + QTest::addColumn("target"); + + QTest::addRow("rotate-0") << OutputTransform::Normal << QPointF(10, 20) << QPointF(10, 20); + QTest::addRow("rotate-90") << OutputTransform::Rotate90 << QPointF(10, 20) << QPointF(20, 90); + QTest::addRow("rotate-180") << OutputTransform::Rotate180 << QPointF(10, 20) << QPointF(90, 180); + QTest::addRow("rotate-270") << OutputTransform::Rotate270 << QPointF(10, 20) << QPointF(180, 10); + QTest::addRow("flip-x-0") << OutputTransform::FlipX << QPointF(10, 20) << QPointF(90, 20); + QTest::addRow("flip-x-90") << OutputTransform::FlipX90 << QPointF(10, 20) << QPointF(20, 10); + QTest::addRow("flip-x-180") << OutputTransform::FlipX180 << QPointF(10, 20) << QPointF(10, 180); + QTest::addRow("flip-x-270") << OutputTransform::FlipX270 << QPointF(10, 20) << QPointF(180, 90); + QTest::addRow("flip-y-0") << OutputTransform::FlipY << QPointF(10, 20) << QPointF(10, 180); + QTest::addRow("flip-y-90") << OutputTransform::FlipY90 << QPointF(10, 20) << QPointF(180, 90); + QTest::addRow("flip-y-180") << OutputTransform::FlipY180 << QPointF(10, 20) << QPointF(90, 20); + QTest::addRow("flip-y-270") << OutputTransform::FlipY270 << QPointF(10, 20) << QPointF(20, 10); +} + +void TestOutputTransform::mapPointF() +{ + QFETCH(OutputTransform::Kind, kind); + QFETCH(QPointF, source); + QFETCH(QPointF, target); + + const OutputTransform transform(kind); + + QCOMPARE(transform.map(source, QSizeF(100, 200)), target); + QCOMPARE(transform.map(RectF(source, QSizeF(0, 0)), QSizeF(100, 200)), RectF(target, QSizeF(0, 0))); +} + +void TestOutputTransform::mapPoint_data() +{ + QTest::addColumn("kind"); + QTest::addColumn("source"); + QTest::addColumn("target"); + + QTest::addRow("rotate-0") << OutputTransform::Normal << QPoint(10, 20) << QPoint(10, 20); + QTest::addRow("rotate-90") << OutputTransform::Rotate90 << QPoint(10, 20) << QPoint(20, 90); + QTest::addRow("rotate-180") << OutputTransform::Rotate180 << QPoint(10, 20) << QPoint(90, 180); + QTest::addRow("rotate-270") << OutputTransform::Rotate270 << QPoint(10, 20) << QPoint(180, 10); + QTest::addRow("flip-x-0") << OutputTransform::FlipX << QPoint(10, 20) << QPoint(90, 20); + QTest::addRow("flip-x-90") << OutputTransform::FlipX90 << QPoint(10, 20) << QPoint(20, 10); + QTest::addRow("flip-x-180") << OutputTransform::FlipX180 << QPoint(10, 20) << QPoint(10, 180); + QTest::addRow("flip-x-270") << OutputTransform::FlipX270 << QPoint(10, 20) << QPoint(180, 90); + QTest::addRow("flip-y-0") << OutputTransform::FlipY << QPoint(10, 20) << QPoint(10, 180); + QTest::addRow("flip-y-90") << OutputTransform::FlipY90 << QPoint(10, 20) << QPoint(180, 90); + QTest::addRow("flip-y-180") << OutputTransform::FlipY180 << QPoint(10, 20) << QPoint(90, 20); + QTest::addRow("flip-y-270") << OutputTransform::FlipY270 << QPoint(10, 20) << QPoint(20, 10); +} + +void TestOutputTransform::mapPoint() +{ + QFETCH(OutputTransform::Kind, kind); + QFETCH(QPoint, source); + QFETCH(QPoint, target); + + const OutputTransform transform(kind); + + QCOMPARE(transform.map(source, QSize(100, 200)), target); + QCOMPARE(transform.map(Rect(source, QSize(0, 0)), QSize(100, 200)), Rect(target, QSize(0, 0))); +} + +void TestOutputTransform::inverted_data() +{ + QTest::addColumn("kind"); + QTest::addColumn("inverted"); + + QTest::addRow("rotate-0") << OutputTransform::Normal << OutputTransform::Normal; + QTest::addRow("rotate-90") << OutputTransform::Rotate90 << OutputTransform::Rotate270; + QTest::addRow("rotate-180") << OutputTransform::Rotate180 << OutputTransform::Rotate180; + QTest::addRow("rotate-270") << OutputTransform::Rotate270 << OutputTransform::Rotate90; + QTest::addRow("flip-x-0") << OutputTransform::FlipX << OutputTransform::FlipX; + QTest::addRow("flip-x-90") << OutputTransform::FlipX90 << OutputTransform::FlipX90; + QTest::addRow("flip-x-180") << OutputTransform::FlipX180 << OutputTransform::FlipX180; + QTest::addRow("flip-x-270") << OutputTransform::FlipX270 << OutputTransform::FlipX270; + QTest::addRow("flip-y-0") << OutputTransform::FlipY << OutputTransform::FlipY; + QTest::addRow("flip-y-90") << OutputTransform::FlipY90 << OutputTransform::FlipY90; + QTest::addRow("flip-y-180") << OutputTransform::FlipY180 << OutputTransform::FlipY180; + QTest::addRow("flip-y-270") << OutputTransform::FlipY270 << OutputTransform::FlipY270; +} + +void TestOutputTransform::inverted() +{ + QFETCH(OutputTransform::Kind, kind); + QFETCH(OutputTransform::Kind, inverted); + + QCOMPARE(OutputTransform(kind).inverted(), OutputTransform(inverted)); +} + +void TestOutputTransform::combine_data() +{ + QTest::addColumn("first"); + QTest::addColumn("second"); + QTest::addColumn("result"); + + QTest::addRow("rotate-0 | rotate-0") << OutputTransform::Normal << OutputTransform::Normal << OutputTransform::Normal; + QTest::addRow("rotate-90 | rotate-0") << OutputTransform::Rotate90 << OutputTransform::Normal << OutputTransform::Rotate90; + QTest::addRow("rotate-180 | rotate-0") << OutputTransform::Rotate180 << OutputTransform::Normal << OutputTransform::Rotate180; + QTest::addRow("rotate-270 | rotate-0") << OutputTransform::Rotate270 << OutputTransform::Normal << OutputTransform::Rotate270; + QTest::addRow("flip-x-0 | rotate-0") << OutputTransform::FlipX << OutputTransform::Normal << OutputTransform::FlipX; + QTest::addRow("flip-x-90 | rotate-0") << OutputTransform::FlipX90 << OutputTransform::Normal << OutputTransform::FlipX90; + QTest::addRow("flip-x-180 | rotate-0") << OutputTransform::FlipX180 << OutputTransform::Normal << OutputTransform::FlipX180; + QTest::addRow("flip-x-270 | rotate-0") << OutputTransform::FlipX270 << OutputTransform::Normal << OutputTransform::FlipX270; + QTest::addRow("flip-y-0 | rotate-0") << OutputTransform::FlipY << OutputTransform::Normal << OutputTransform::FlipY; + QTest::addRow("flip-y-90 | rotate-0") << OutputTransform::FlipY90 << OutputTransform::Normal << OutputTransform::FlipY90; + QTest::addRow("flip-y-180 | rotate-0") << OutputTransform::FlipY180 << OutputTransform::Normal << OutputTransform::FlipY180; + QTest::addRow("flip-y-270 | rotate-0") << OutputTransform::FlipY270 << OutputTransform::Normal << OutputTransform::FlipY270; + + QTest::addRow("rotate-0 | rotate-90") << OutputTransform::Normal << OutputTransform::Rotate90 << OutputTransform::Rotate90; + QTest::addRow("rotate-90 | rotate-90") << OutputTransform::Rotate90 << OutputTransform::Rotate90 << OutputTransform::Rotate180; + QTest::addRow("rotate-180 | rotate-90") << OutputTransform::Rotate180 << OutputTransform::Rotate90 << OutputTransform::Rotate270; + QTest::addRow("rotate-270 | rotate-90") << OutputTransform::Rotate270 << OutputTransform::Rotate90 << OutputTransform::Normal; + QTest::addRow("flip-x-0 | rotate-90") << OutputTransform::FlipX << OutputTransform::Rotate90 << OutputTransform::FlipX90; + QTest::addRow("flip-x-90 | rotate-90") << OutputTransform::FlipX90 << OutputTransform::Rotate90 << OutputTransform::FlipX180; + QTest::addRow("flip-x-180 | rotate-90") << OutputTransform::FlipX180 << OutputTransform::Rotate90 << OutputTransform::FlipX270; + QTest::addRow("flip-x-270 | rotate-90") << OutputTransform::FlipX270 << OutputTransform::Rotate90 << OutputTransform::FlipX; + QTest::addRow("flip-y-0 | rotate-90") << OutputTransform::FlipY << OutputTransform::Rotate90 << OutputTransform::FlipY90; + QTest::addRow("flip-y-90 | rotate-90") << OutputTransform::FlipY90 << OutputTransform::Rotate90 << OutputTransform::FlipY180; + QTest::addRow("flip-y-180 | rotate-90") << OutputTransform::FlipY180 << OutputTransform::Rotate90 << OutputTransform::FlipY270; + QTest::addRow("flip-y-270 | rotate-90") << OutputTransform::FlipY270 << OutputTransform::Rotate90 << OutputTransform::FlipY; + + QTest::addRow("rotate-0 | rotate-180") << OutputTransform::Normal << OutputTransform::Rotate180 << OutputTransform::Rotate180; + QTest::addRow("rotate-90 | rotate-180") << OutputTransform::Rotate90 << OutputTransform::Rotate180 << OutputTransform::Rotate270; + QTest::addRow("rotate-180 | rotate-180") << OutputTransform::Rotate180 << OutputTransform::Rotate180 << OutputTransform::Normal; + QTest::addRow("rotate-270 | rotate-180") << OutputTransform::Rotate270 << OutputTransform::Rotate180 << OutputTransform::Rotate90; + QTest::addRow("flip-x-0 | rotate-180") << OutputTransform::FlipX << OutputTransform::Rotate180 << OutputTransform::FlipX180; + QTest::addRow("flip-x-90 | rotate-180") << OutputTransform::FlipX90 << OutputTransform::Rotate180 << OutputTransform::FlipX270; + QTest::addRow("flip-x-180 | rotate-180") << OutputTransform::FlipX180 << OutputTransform::Rotate180 << OutputTransform::FlipX; + QTest::addRow("flip-x-270 | rotate-180") << OutputTransform::FlipX270 << OutputTransform::Rotate180 << OutputTransform::FlipX90; + QTest::addRow("flip-y-0 | rotate-180") << OutputTransform::FlipY << OutputTransform::Rotate180 << OutputTransform::FlipY180; + QTest::addRow("flip-y-90 | rotate-180") << OutputTransform::FlipY90 << OutputTransform::Rotate180 << OutputTransform::FlipY270; + QTest::addRow("flip-y-180 | rotate-180") << OutputTransform::FlipY180 << OutputTransform::Rotate180 << OutputTransform::FlipY; + QTest::addRow("flip-y-270 | rotate-180") << OutputTransform::FlipY270 << OutputTransform::Rotate180 << OutputTransform::FlipY90; + + QTest::addRow("rotate-0 | rotate-270") << OutputTransform::Normal << OutputTransform::Rotate270 << OutputTransform::Rotate270; + QTest::addRow("rotate-90 | rotate-270") << OutputTransform::Rotate90 << OutputTransform::Rotate270 << OutputTransform::Normal; + QTest::addRow("rotate-180 | rotate-270") << OutputTransform::Rotate180 << OutputTransform::Rotate270 << OutputTransform::Rotate90; + QTest::addRow("rotate-270 | rotate-270") << OutputTransform::Rotate270 << OutputTransform::Rotate270 << OutputTransform::Rotate180; + QTest::addRow("flip-x-0 | rotate-270") << OutputTransform::FlipX << OutputTransform::Rotate270 << OutputTransform::FlipX270; + QTest::addRow("flip-x-90 | rotate-270") << OutputTransform::FlipX90 << OutputTransform::Rotate270 << OutputTransform::FlipX; + QTest::addRow("flip-x-180 | rotate-270") << OutputTransform::FlipX180 << OutputTransform::Rotate270 << OutputTransform::FlipX90; + QTest::addRow("flip-x-270 | rotate-270") << OutputTransform::FlipX270 << OutputTransform::Rotate270 << OutputTransform::FlipX180; + QTest::addRow("flip-y-0 | rotate-270") << OutputTransform::FlipY << OutputTransform::Rotate270 << OutputTransform::FlipY270; + QTest::addRow("flip-y-90 | rotate-270") << OutputTransform::FlipY90 << OutputTransform::Rotate270 << OutputTransform::FlipY; + QTest::addRow("flip-y-180 | rotate-270") << OutputTransform::FlipY180 << OutputTransform::Rotate270 << OutputTransform::FlipY90; + QTest::addRow("flip-y-270 | rotate-270") << OutputTransform::FlipY270 << OutputTransform::Rotate270 << OutputTransform::FlipY180; + + QTest::addRow("rotate-0 | flip-x-0") << OutputTransform::Normal << OutputTransform::FlipX << OutputTransform::FlipX; + QTest::addRow("rotate-90 | flip-x-0") << OutputTransform::Rotate90 << OutputTransform::FlipX << OutputTransform::FlipX270; + QTest::addRow("rotate-180 | flip-x-0") << OutputTransform::Rotate180 << OutputTransform::FlipX << OutputTransform::FlipX180; + QTest::addRow("rotate-270 | flip-x-0") << OutputTransform::Rotate270 << OutputTransform::FlipX << OutputTransform::FlipX90; + QTest::addRow("flip-x-0 | flip-x-0") << OutputTransform::FlipX << OutputTransform::FlipX << OutputTransform::Normal; + QTest::addRow("flip-x-90 | flip-x-0") << OutputTransform::FlipX90 << OutputTransform::FlipX << OutputTransform::Rotate270; + QTest::addRow("flip-x-180 | flip-x-0") << OutputTransform::FlipX180 << OutputTransform::FlipX << OutputTransform::Rotate180; + QTest::addRow("flip-x-270 | flip-x-0") << OutputTransform::FlipX270 << OutputTransform::FlipX << OutputTransform::Rotate90; + QTest::addRow("flip-y-0 | flip-x-0") << OutputTransform::FlipY << OutputTransform::FlipX << OutputTransform::Rotate180; + QTest::addRow("flip-y-90 | flip-x-0") << OutputTransform::FlipY90 << OutputTransform::FlipX << OutputTransform::Rotate90; + QTest::addRow("flip-y-180 | flip-x-0") << OutputTransform::FlipY180 << OutputTransform::FlipX << OutputTransform::Normal; + QTest::addRow("flip-y-270 | flip-x-0") << OutputTransform::FlipY270 << OutputTransform::FlipX << OutputTransform::Rotate270; + + QTest::addRow("rotate-0 | flip-x-90") << OutputTransform::Normal << OutputTransform::FlipX90 << OutputTransform::FlipX90; + QTest::addRow("rotate-90 | flip-x-90") << OutputTransform::Rotate90 << OutputTransform::FlipX90 << OutputTransform::FlipX; + QTest::addRow("rotate-180 | flip-x-90") << OutputTransform::Rotate180 << OutputTransform::FlipX90 << OutputTransform::FlipX270; + QTest::addRow("rotate-270 | flip-x-90") << OutputTransform::Rotate270 << OutputTransform::FlipX90 << OutputTransform::FlipX180; + QTest::addRow("flip-x-0 | flip-x-90") << OutputTransform::FlipX << OutputTransform::FlipX90 << OutputTransform::Rotate90; + QTest::addRow("flip-x-90 | flip-x-90") << OutputTransform::FlipX90 << OutputTransform::FlipX90 << OutputTransform::Normal; + QTest::addRow("flip-x-180 | flip-x-90") << OutputTransform::FlipX180 << OutputTransform::FlipX90 << OutputTransform::Rotate270; + QTest::addRow("flip-x-270 | flip-x-90") << OutputTransform::FlipX270 << OutputTransform::FlipX90 << OutputTransform::Rotate180; + QTest::addRow("flip-y-0 | flip-x-90") << OutputTransform::FlipY << OutputTransform::FlipX90 << OutputTransform::Rotate270; + QTest::addRow("flip-y-90 | flip-x-90") << OutputTransform::FlipY90 << OutputTransform::FlipX90 << OutputTransform::Rotate180; + QTest::addRow("flip-y-180 | flip-x-90") << OutputTransform::FlipY180 << OutputTransform::FlipX90 << OutputTransform::Rotate90; + QTest::addRow("flip-y-270 | flip-x-90") << OutputTransform::FlipY270 << OutputTransform::FlipX90 << OutputTransform::Normal; + + QTest::addRow("rotate-0 | flip-x-180") << OutputTransform::Normal << OutputTransform::FlipX180 << OutputTransform::FlipX180; + QTest::addRow("rotate-90 | flip-x-180") << OutputTransform::Rotate90 << OutputTransform::FlipX180 << OutputTransform::FlipX90; + QTest::addRow("rotate-180 | flip-x-180") << OutputTransform::Rotate180 << OutputTransform::FlipX180 << OutputTransform::FlipX; + QTest::addRow("rotate-270 | flip-x-180") << OutputTransform::Rotate270 << OutputTransform::FlipX180 << OutputTransform::FlipX270; + QTest::addRow("flip-x-0 | flip-x-180") << OutputTransform::FlipX << OutputTransform::FlipX180 << OutputTransform::Rotate180; + QTest::addRow("flip-x-90 | flip-x-180") << OutputTransform::FlipX90 << OutputTransform::FlipX180 << OutputTransform::Rotate90; + QTest::addRow("flip-x-180 | flip-x-180") << OutputTransform::FlipX180 << OutputTransform::FlipX180 << OutputTransform::Normal; + QTest::addRow("flip-x-270 | flip-x-180") << OutputTransform::FlipX270 << OutputTransform::FlipX180 << OutputTransform::Rotate270; + QTest::addRow("flip-y-0 | flip-x-180") << OutputTransform::FlipY << OutputTransform::FlipX180 << OutputTransform::Normal; + QTest::addRow("flip-y-90 | flip-x-180") << OutputTransform::FlipY90 << OutputTransform::FlipX180 << OutputTransform::Rotate270; + QTest::addRow("flip-y-180 | flip-x-180") << OutputTransform::FlipY180 << OutputTransform::FlipX180 << OutputTransform::Rotate180; + QTest::addRow("flip-y-270 | flip-x-180") << OutputTransform::FlipY270 << OutputTransform::FlipX180 << OutputTransform::Rotate90; + + QTest::addRow("rotate-0 | flip-x-270") << OutputTransform::Normal << OutputTransform::FlipX270 << OutputTransform::FlipX270; + QTest::addRow("rotate-90 | flip-x-270") << OutputTransform::Rotate90 << OutputTransform::FlipX270 << OutputTransform::FlipX180; + QTest::addRow("rotate-180 | flip-x-270") << OutputTransform::Rotate180 << OutputTransform::FlipX270 << OutputTransform::FlipX90; + QTest::addRow("rotate-270 | flip-x-270") << OutputTransform::Rotate270 << OutputTransform::FlipX270 << OutputTransform::FlipX; + QTest::addRow("flip-x-0 | flip-x-270") << OutputTransform::FlipX << OutputTransform::FlipX270 << OutputTransform::Rotate270; + QTest::addRow("flip-x-90 | flip-x-270") << OutputTransform::FlipX90 << OutputTransform::FlipX270 << OutputTransform::Rotate180; + QTest::addRow("flip-x-180 | flip-x-270") << OutputTransform::FlipX180 << OutputTransform::FlipX270 << OutputTransform::Rotate90; + QTest::addRow("flip-x-270 | flip-x-270") << OutputTransform::FlipX270 << OutputTransform::FlipX270 << OutputTransform::Normal; + QTest::addRow("flip-y-0 | flip-x-270") << OutputTransform::FlipY << OutputTransform::FlipX270 << OutputTransform::Rotate90; + QTest::addRow("flip-y-90 | flip-x-270") << OutputTransform::FlipY90 << OutputTransform::FlipX270 << OutputTransform::Normal; + QTest::addRow("flip-y-180 | flip-x-270") << OutputTransform::FlipY180 << OutputTransform::FlipX270 << OutputTransform::Rotate270; + QTest::addRow("flip-y-270 | flip-x-270") << OutputTransform::FlipY270 << OutputTransform::FlipX270 << OutputTransform::Rotate180; + + QTest::addRow("rotate-0 | flip-y-0") << OutputTransform::Normal << OutputTransform::FlipY << OutputTransform::FlipY; + QTest::addRow("rotate-90 | flip-y-0") << OutputTransform::Rotate90 << OutputTransform::FlipY << OutputTransform::FlipY270; + QTest::addRow("rotate-180 | flip-y-0") << OutputTransform::Rotate180 << OutputTransform::FlipY << OutputTransform::FlipY180; + QTest::addRow("rotate-270 | flip-y-0") << OutputTransform::Rotate270 << OutputTransform::FlipY << OutputTransform::FlipY90; + QTest::addRow("flip-x-0 | flip-y-0") << OutputTransform::FlipX << OutputTransform::FlipY << OutputTransform::Rotate180; + QTest::addRow("flip-x-90 | flip-y-0") << OutputTransform::FlipX90 << OutputTransform::FlipY << OutputTransform::Rotate90; + QTest::addRow("flip-x-180 | flip-y-0") << OutputTransform::FlipX180 << OutputTransform::FlipY << OutputTransform::Normal; + QTest::addRow("flip-x-270 | flip-y-0") << OutputTransform::FlipX270 << OutputTransform::FlipY << OutputTransform::Rotate270; + QTest::addRow("flip-y-0 | flip-y-0") << OutputTransform::FlipY << OutputTransform::FlipY << OutputTransform::Normal; + QTest::addRow("flip-y-90 | flip-y-0") << OutputTransform::FlipY90 << OutputTransform::FlipY << OutputTransform::Rotate270; + QTest::addRow("flip-y-180 | flip-y-0") << OutputTransform::FlipY180 << OutputTransform::FlipY << OutputTransform::Rotate180; + QTest::addRow("flip-y-270 | flip-y-0") << OutputTransform::FlipY270 << OutputTransform::FlipY << OutputTransform::Rotate90; + + QTest::addRow("rotate-0 | flip-y-90") << OutputTransform::Normal << OutputTransform::FlipY90 << OutputTransform::FlipY90; + QTest::addRow("rotate-90 | flip-y-90") << OutputTransform::Rotate90 << OutputTransform::FlipY90 << OutputTransform::FlipY; + QTest::addRow("rotate-180 | flip-y-90") << OutputTransform::Rotate180 << OutputTransform::FlipY90 << OutputTransform::FlipY270; + QTest::addRow("rotate-270 | flip-y-90") << OutputTransform::Rotate270 << OutputTransform::FlipY90 << OutputTransform::FlipY180; + QTest::addRow("flip-x-0 | flip-y-90") << OutputTransform::FlipX << OutputTransform::FlipY90 << OutputTransform::Rotate270; + QTest::addRow("flip-x-90 | flip-y-90") << OutputTransform::FlipX90 << OutputTransform::FlipY90 << OutputTransform::Rotate180; + QTest::addRow("flip-x-180 | flip-y-90") << OutputTransform::FlipX180 << OutputTransform::FlipY90 << OutputTransform::Rotate90; + QTest::addRow("flip-x-270 | flip-y-90") << OutputTransform::FlipX270 << OutputTransform::FlipY90 << OutputTransform::Normal; + QTest::addRow("flip-y-0 | flip-y-90") << OutputTransform::FlipY << OutputTransform::FlipY90 << OutputTransform::Rotate90; + QTest::addRow("flip-y-90 | flip-y-90") << OutputTransform::FlipY90 << OutputTransform::FlipY90 << OutputTransform::Normal; + QTest::addRow("flip-y-180 | flip-y-90") << OutputTransform::FlipY180 << OutputTransform::FlipY90 << OutputTransform::Rotate270; + QTest::addRow("flip-y-270 | flip-y-90") << OutputTransform::FlipY270 << OutputTransform::FlipY90 << OutputTransform::Rotate180; + + QTest::addRow("rotate-0 | flip-y-180") << OutputTransform::Normal << OutputTransform::FlipY180 << OutputTransform::FlipY180; + QTest::addRow("rotate-90 | flip-y-180") << OutputTransform::Rotate90 << OutputTransform::FlipY180 << OutputTransform::FlipY90; + QTest::addRow("rotate-180 | flip-y-180") << OutputTransform::Rotate180 << OutputTransform::FlipY180 << OutputTransform::FlipY; + QTest::addRow("rotate-270 | flip-y-180") << OutputTransform::Rotate270 << OutputTransform::FlipY180 << OutputTransform::FlipY270; + QTest::addRow("flip-x-0 | flip-y-180") << OutputTransform::FlipX << OutputTransform::FlipY180 << OutputTransform::Normal; + QTest::addRow("flip-x-90 | flip-y-180") << OutputTransform::FlipX90 << OutputTransform::FlipY180 << OutputTransform::Rotate270; + QTest::addRow("flip-x-180 | flip-y-180") << OutputTransform::FlipX180 << OutputTransform::FlipY180 << OutputTransform::Rotate180; + QTest::addRow("flip-x-270 | flip-y-180") << OutputTransform::FlipX270 << OutputTransform::FlipY180 << OutputTransform::Rotate90; + QTest::addRow("flip-y-0 | flip-y-180") << OutputTransform::FlipY << OutputTransform::FlipY180 << OutputTransform::Rotate180; + QTest::addRow("flip-y-90 | flip-y-180") << OutputTransform::FlipY90 << OutputTransform::FlipY180 << OutputTransform::Rotate90; + QTest::addRow("flip-y-180 | flip-y-180") << OutputTransform::FlipY180 << OutputTransform::FlipY180 << OutputTransform::Normal; + QTest::addRow("flip-y-270 | flip-y-180") << OutputTransform::FlipY270 << OutputTransform::FlipY180 << OutputTransform::Rotate270; + + QTest::addRow("rotate-0 | flip-y-270") << OutputTransform::Normal << OutputTransform::FlipY270 << OutputTransform::FlipY270; + QTest::addRow("rotate-90 | flip-y-270") << OutputTransform::Rotate90 << OutputTransform::FlipY270 << OutputTransform::FlipY180; + QTest::addRow("rotate-180 | flip-y-270") << OutputTransform::Rotate180 << OutputTransform::FlipY270 << OutputTransform::FlipY90; + QTest::addRow("rotate-270 | flip-y-270") << OutputTransform::Rotate270 << OutputTransform::FlipY270 << OutputTransform::FlipY; + QTest::addRow("flip-x-0 | flip-y-270") << OutputTransform::FlipX << OutputTransform::FlipY270 << OutputTransform::Rotate90; + QTest::addRow("flip-x-90 | flip-y-270") << OutputTransform::FlipX90 << OutputTransform::FlipY270 << OutputTransform::Normal; + QTest::addRow("flip-x-180 | flip-y-270") << OutputTransform::FlipX180 << OutputTransform::FlipY270 << OutputTransform::Rotate270; + QTest::addRow("flip-x-270 | flip-y-270") << OutputTransform::FlipX270 << OutputTransform::FlipY270 << OutputTransform::Rotate180; + QTest::addRow("flip-y-0 | flip-y-270") << OutputTransform::FlipY << OutputTransform::FlipY270 << OutputTransform::Rotate270; + QTest::addRow("flip-y-90 | flip-y-270") << OutputTransform::FlipY90 << OutputTransform::FlipY270 << OutputTransform::Rotate180; + QTest::addRow("flip-y-180 | flip-y-270") << OutputTransform::FlipY180 << OutputTransform::FlipY270 << OutputTransform::Rotate90; + QTest::addRow("flip-y-270 | flip-y-270") << OutputTransform::FlipY270 << OutputTransform::FlipY270 << OutputTransform::Normal; +} + +void TestOutputTransform::combine() +{ + QFETCH(OutputTransform::Kind, first); + QFETCH(OutputTransform::Kind, second); + QFETCH(OutputTransform::Kind, result); + + const OutputTransform firstTransform(first); + const OutputTransform secondTransform(second); + const OutputTransform combinedTransform = firstTransform.combine(secondTransform); + QCOMPARE(combinedTransform.kind(), result); + + const RectF box(10, 20, 30, 40); + const QSizeF bounds(100, 200); + QCOMPARE(combinedTransform.map(box, bounds), secondTransform.map(firstTransform.map(box, bounds), firstTransform.map(bounds))); +} + +void TestOutputTransform::matrix_data() +{ + QTest::addColumn("kind"); + QTest::addColumn("source"); + QTest::addColumn("target"); + + QTest::addRow("rotate-0") << OutputTransform::Normal << RectF(10, 20, 30, 40) << RectF(10, 20, 30, 40); + QTest::addRow("rotate-90") << OutputTransform::Rotate90 << RectF(10, 20, 30, 40) << RectF(20, 60, 40, 30); + QTest::addRow("rotate-180") << OutputTransform::Rotate180 << RectF(10, 20, 30, 40) << RectF(60, 140, 30, 40); + QTest::addRow("rotate-270") << OutputTransform::Rotate270 << RectF(10, 20, 30, 40) << RectF(140, 10, 40, 30); + QTest::addRow("flip-x-0") << OutputTransform::FlipX << RectF(10, 20, 30, 40) << RectF(60, 20, 30, 40); + QTest::addRow("flip-x-90") << OutputTransform::FlipX90 << RectF(10, 20, 30, 40) << RectF(20, 10, 40, 30); + QTest::addRow("flip-x-180") << OutputTransform::FlipX180 << RectF(10, 20, 30, 40) << RectF(10, 140, 30, 40); + QTest::addRow("flip-x-270") << OutputTransform::FlipX270 << RectF(10, 20, 30, 40) << RectF(140, 60, 40, 30); + QTest::addRow("flip-y-0") << OutputTransform::FlipY << RectF(10, 20, 30, 40) << RectF(10, 140, 30, 40); + QTest::addRow("flip-y-90") << OutputTransform::FlipY90 << RectF(10, 20, 30, 40) << RectF(140, 60, 40, 30); + QTest::addRow("flip-y-180") << OutputTransform::FlipY180 << RectF(10, 20, 30, 40) << RectF(60, 20, 30, 40); + QTest::addRow("flip-y-270") << OutputTransform::FlipY270 << RectF(10, 20, 30, 40) << RectF(20, 10, 40, 30); +} + +void TestOutputTransform::matrix() +{ + QFETCH(OutputTransform::Kind, kind); + QFETCH(RectF, source); + QFETCH(RectF, target); + + const OutputTransform transform = kind; + const QSizeF sourceBounds = QSizeF(100, 200); + const QSizeF targetBounds = transform.map(sourceBounds); + + QMatrix4x4 matrix; + matrix.scale(targetBounds.width(), targetBounds.height()); + matrix.translate(0.5, 0.5); + matrix.scale(0.5, -0.5); + matrix.scale(1, -1); // flip the y axis back + matrix *= transform.toMatrix(); + matrix.scale(1, -1); // undo ortho() flipping the y axis + matrix.ortho(RectF(0, 0, sourceBounds.width(), sourceBounds.height())); + + const RectF mapped = matrix.mapRect(source); + QCOMPARE(mapped, target); + QCOMPARE(mapped, transform.map(source, sourceBounds)); +} + +QTEST_MAIN(TestOutputTransform) + +#include "output_transform_test.moc" diff --git a/local/recipes/kde/kwin/source/autotests/test_client_machine.cpp b/local/recipes/kde/kwin/source/autotests/test_client_machine.cpp new file mode 100644 index 0000000000..603b90b32e --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/test_client_machine.cpp @@ -0,0 +1,147 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "testutils.h" +// KWin +#include "client_machine.h" +#include "utils/xcbutils.h" +// Qt +#include +#include +#include +#include +#include +// xcb +#include +// system +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(KWIN_CORE, "kwin_core") + +using namespace KWin; + +class TestClientMachine : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + void hostName_data(); + void hostName(); + void emptyHostName(); + +private: + void setClientMachineProperty(xcb_window_t window, const QString &hostname); + QString m_hostName; + QString m_fqdn; +}; + +void TestClientMachine::setClientMachineProperty(xcb_window_t window, const QString &hostname) +{ + xcb_change_property(connection(), XCB_PROP_MODE_REPLACE, window, + XCB_ATOM_WM_CLIENT_MACHINE, XCB_ATOM_STRING, 8, + hostname.length(), hostname.toLocal8Bit().constData()); +} + +void TestClientMachine::initTestCase() +{ +#ifdef HOST_NAME_MAX + char hostnamebuf[HOST_NAME_MAX]; +#else + char hostnamebuf[256]; +#endif + if (gethostname(hostnamebuf, sizeof hostnamebuf) >= 0) { + hostnamebuf[sizeof(hostnamebuf) - 1] = 0; + m_hostName = hostnamebuf; + } + addrinfo *res; + addrinfo addressHints{}; + addressHints.ai_family = PF_UNSPEC; + addressHints.ai_socktype = SOCK_STREAM; + addressHints.ai_flags |= AI_CANONNAME; + if (getaddrinfo(m_hostName.toLocal8Bit().constData(), nullptr, &addressHints, &res) == 0) { + if (res->ai_canonname) { + m_fqdn = QString::fromLocal8Bit(res->ai_canonname); + } + } + freeaddrinfo(res); + + qApp->setProperty("x11RootWindow", QVariant::fromValue(QX11Info::appRootWindow())); + qApp->setProperty("x11Connection", QVariant::fromValue(QX11Info::connection())); +} + +void TestClientMachine::cleanupTestCase() +{ +} + +void TestClientMachine::hostName_data() +{ + QTest::addColumn("hostName"); + QTest::addColumn("expectedHost"); + QTest::addColumn("local"); + + QTest::newRow("empty") << QString() << QStringLiteral("localhost") << true; + QTest::newRow("localhost") << QStringLiteral("localhost") << QStringLiteral("localhost") << true; + QTest::newRow("hostname") << m_hostName << m_hostName << true; + QTest::newRow("HOSTNAME") << m_hostName.toUpper() << m_hostName.toUpper() << true; + QString cut(m_hostName); + cut.remove(0, 1); + QTest::newRow("ostname") << cut << cut << false; + QString domain("random.name.not.exist.tld"); + QTest::newRow("domain") << domain << domain << false; + QTest::newRow("fqdn") << m_fqdn << m_fqdn << true; + QTest::newRow("FQDN") << m_fqdn.toUpper() << m_fqdn.toUpper() << true; + cut = m_fqdn; + cut.remove(0, 1); + QTest::newRow("qdn") << cut << cut << false; +} + +void TestClientMachine::hostName() +{ + const Rect geometry(0, 0, 10, 10); + const uint32_t values[] = {true}; + Xcb::Window window(geometry, XCB_WINDOW_CLASS_INPUT_ONLY, XCB_CW_OVERRIDE_REDIRECT, values); + QFETCH(QString, hostName); + QFETCH(bool, local); + setClientMachineProperty(window, hostName); + + ClientMachine clientMachine; + QSignalSpy spy(&clientMachine, &ClientMachine::localhostChanged); + clientMachine.resolve(window, XCB_WINDOW_NONE); + QTEST(clientMachine.hostName(), "expectedHost"); + + int i = 0; + while (clientMachine.isResolving() && i++ < 50) { + // name is being resolved in an external thread, so let's wait a little bit + QTest::qWait(250); + } + + QCOMPARE(clientMachine.isLocal(), local); + QCOMPARE(spy.isEmpty(), !local); +} + +void TestClientMachine::emptyHostName() +{ + const Rect geometry(0, 0, 10, 10); + const uint32_t values[] = {true}; + Xcb::Window window(geometry, XCB_WINDOW_CLASS_INPUT_ONLY, XCB_CW_OVERRIDE_REDIRECT, values); + ClientMachine clientMachine; + QSignalSpy spy(&clientMachine, &ClientMachine::localhostChanged); + clientMachine.resolve(window, XCB_WINDOW_NONE); + QCOMPARE(clientMachine.hostName(), ClientMachine::localhost()); + QVERIFY(clientMachine.isLocal()); + // should be local + QCOMPARE(spy.isEmpty(), false); +} + +Q_CONSTRUCTOR_FUNCTION(forceXcb) +QTEST_MAIN(TestClientMachine) +#include "test_client_machine.moc" diff --git a/local/recipes/kde/kwin/source/autotests/test_colorspaces.cpp b/local/recipes/kde/kwin/source/autotests/test_colorspaces.cpp new file mode 100644 index 0000000000..b87140773a --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/test_colorspaces.cpp @@ -0,0 +1,694 @@ +/* + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +#include + +#include "core/colorpipeline.h" +#include "core/colorspace.h" +#include "core/iccprofile.h" +#include "opengl/eglcontext.h" +#include "opengl/egldisplay.h" +#include "opengl/glframebuffer.h" +#include "opengl/glshader.h" +#include "opengl/glshadermanager.h" +#include "opengl/icc_shader.h" + +#include + +using namespace KWin; + +class TestColorspaces : public QObject +{ + Q_OBJECT + +public: + TestColorspaces() = default; + +private Q_SLOTS: + void roundtripConversion_data(); + void roundtripConversion(); + void testXYZ_XYconversions(); + void testIdentityTransformation_data(); + void testIdentityTransformation(); + void testColorPipeline_data(); + void testColorPipeline(); + void testXYZ(); + void testOpenglShader_data(); + void testOpenglShader(); + void testIccShader_data(); + void testIccShader(); + void dontCrashWithWeirdHdrMetadata(); + void testColorimetryCheck_data(); + void testColorimetryCheck(); + void testYCbCr_data(); + void testYCbCr(); + void testBlackPointCompensation(); + void testSCRGB(); + void testNightLightNoTonemapping(); +}; + +static bool compareVectors(const QVector3D &one, const QVector3D &two, float maxDifference) +{ + const bool ret = std::abs(one.x() - two.x()) <= maxDifference + && std::abs(one.y() - two.y()) <= maxDifference + && std::abs(one.z() - two.z()) <= maxDifference; + if (!ret) { + qWarning() << one << "!=" << two << "within" << maxDifference; + } + return ret; +} + +static const double s_resolution10bit = std::pow(1.0 / 2.0, 10); + +void TestColorspaces::roundtripConversion_data() +{ + QTest::addColumn("srcColorimetry"); + QTest::addColumn("srcTransferFunction"); + QTest::addColumn("dstColorimetry"); + QTest::addColumn("dstTransferFunction"); + QTest::addColumn("requiredAccuracy"); + + QTest::addRow("BT709 (sRGB) <-> BT2020 (linear)") << Colorimetry::BT709 << TransferFunction::sRGB << Colorimetry::BT2020 << TransferFunction::linear << s_resolution10bit; + QTest::addRow("BT709 (gamma 2.2) <-> BT2020 (linear)") << Colorimetry::BT709 << TransferFunction::gamma22 << Colorimetry::BT2020 << TransferFunction::linear << s_resolution10bit; + QTest::addRow("BT709 (linear) <-> BT2020 (linear)") << Colorimetry::BT709 << TransferFunction::linear << Colorimetry::BT2020 << TransferFunction::linear << s_resolution10bit; + QTest::addRow("BT709 (PQ) <-> BT2020 (linear)") << Colorimetry::BT709 << TransferFunction::PerceptualQuantizer << Colorimetry::BT2020 << TransferFunction::linear << 3 * s_resolution10bit; +} + +void TestColorspaces::roundtripConversion() +{ + QFETCH(Colorimetry, srcColorimetry); + QFETCH(TransferFunction::Type, srcTransferFunction); + QFETCH(Colorimetry, dstColorimetry); + QFETCH(TransferFunction::Type, dstTransferFunction); + QFETCH(double, requiredAccuracy); + + const auto src = ColorDescription(srcColorimetry, TransferFunction(srcTransferFunction), 100, 0, 100, 100); + const auto dst = ColorDescription(dstColorimetry, TransferFunction(dstTransferFunction), 100, 0, 100, 100); + + const QVector3D red(1, 0, 0); + const QVector3D green(0, 1, 0); + const QVector3D blue(0, 0, 1); + const QVector3D white(1, 1, 1); + constexpr std::array renderingIntents = { + RenderingIntent::RelativeColorimetric, + RenderingIntent::AbsoluteColorimetricNoAdaptation, + }; + for (const RenderingIntent intent : renderingIntents) { + QVERIFY(compareVectors(dst.mapTo(src.mapTo(red, dst, intent), src, intent), red, requiredAccuracy)); + QVERIFY(compareVectors(dst.mapTo(src.mapTo(green, dst, intent), src, intent), green, requiredAccuracy)); + QVERIFY(compareVectors(dst.mapTo(src.mapTo(blue, dst, intent), src, intent), blue, requiredAccuracy)); + QVERIFY(compareVectors(dst.mapTo(src.mapTo(white, dst, intent), src, intent), white, requiredAccuracy)); + } +} + +void TestColorspaces::testXYZ_XYconversions() +{ + // this test ensures that xyY<->XYZ conversions can handle weird inputs + // and don't cause crashes + QCOMPARE(XYZ(0, 0, 0).toxyY(), xyY(0, 0, 1)); + QCOMPARE_LE(XYZ(100, 100, 100).toxyY().y, 1); + QCOMPARE(xyY(0, 0, 1).toXYZ(), XYZ(0, 0, 0)); + QCOMPARE(xyY(1, 0, 1).toXYZ(), XYZ(0, 0, 0)); +} + +void TestColorspaces::testIdentityTransformation_data() +{ + QTest::addColumn("colorimetry"); + QTest::addColumn("transferFunction"); + + QTest::addRow("BT709 (sRGB)") << Colorimetry::BT709 << TransferFunction::sRGB; + QTest::addRow("BT709 (gamma22)") << Colorimetry::BT709 << TransferFunction::gamma22; + QTest::addRow("BT709 (PQ)") << Colorimetry::BT709 << TransferFunction::PerceptualQuantizer; + QTest::addRow("BT709 (linear)") << Colorimetry::BT709 << TransferFunction::linear; + QTest::addRow("BT2020 (sRGB)") << Colorimetry::BT2020 << TransferFunction::sRGB; + QTest::addRow("BT2020 (gamma22)") << Colorimetry::BT2020 << TransferFunction::gamma22; + QTest::addRow("BT2020 (PQ)") << Colorimetry::BT2020 << TransferFunction::PerceptualQuantizer; + QTest::addRow("BT2020 (linear)") << Colorimetry::BT2020 << TransferFunction::linear; +} + +void TestColorspaces::testIdentityTransformation() +{ + QFETCH(Colorimetry, colorimetry); + QFETCH(TransferFunction::Type, transferFunction); + const TransferFunction tf(transferFunction); + const auto src = std::make_shared(ColorDescription{ + colorimetry, + tf, + 100, + tf.minLuminance, + tf.maxLuminance, + tf.maxLuminance, + }); + const TransferFunction tf2(transferFunction, tf.minLuminance * 1.1, tf.maxLuminance * 1.1); + const auto dst = std::make_shared(ColorDescription{ + colorimetry, + tf2, + 110, + tf2.minLuminance, + tf2.maxLuminance, + tf2.maxLuminance, + }); + + constexpr std::array renderingIntents = { + RenderingIntent::Perceptual, + RenderingIntent::RelativeColorimetric, + RenderingIntent::AbsoluteColorimetricNoAdaptation, + RenderingIntent::RelativeColorimetricWithBPC, + }; + for (const RenderingIntent intent : renderingIntents) { + const auto pipeline = ColorPipeline::create(src, dst, intent); + if (!pipeline.isIdentity()) { + qWarning() << pipeline; + } + QVERIFY(pipeline.isIdentity()); + } +} + +void TestColorspaces::testColorPipeline_data() +{ + QTest::addColumn>("srcColor"); + QTest::addColumn>("dstColor"); + QTest::addColumn("dstBlack"); + QTest::addColumn("dstGray"); + QTest::addColumn("dstWhite"); + QTest::addColumn("intent"); + + QTest::addRow("sRGB -> rec.2020 relative colorimetric") + << std::make_shared(ColorDescription{ + Colorimetry::BT709, + TransferFunction(TransferFunction::gamma22), + TransferFunction::defaultReferenceLuminanceFor(TransferFunction::gamma22), + 0, + std::nullopt, + std::nullopt, + }) + << std::make_shared(ColorDescription{ + Colorimetry::BT2020, + TransferFunction(TransferFunction::PerceptualQuantizer), + 500, + 0, + std::nullopt, + std::nullopt, + }) + << QVector3D(0.161408, 0.161408, 0.161408) + << QVector3D(0.517483, 0.517483, 0.517483) + << QVector3D(0.67658, 0.67658, 0.67658) + << RenderingIntent::RelativeColorimetric; + QTest::addRow("sRGB -> scRGB relative colorimetric") + << std::make_shared(ColorDescription{ + Colorimetry::BT709, + TransferFunction(TransferFunction::gamma22), + TransferFunction::defaultReferenceLuminanceFor(TransferFunction::gamma22), + 0, + std::nullopt, + std::nullopt, + }) + << std::make_shared(ColorDescription{ + Colorimetry::BT709, + TransferFunction(TransferFunction::linear, 0, 80), + 80, + 0, + std::nullopt, + std::nullopt, + }) + << QVector3D(0.0025, 0.0025, 0.0025) + << QVector3D(0.219594, 0.219594, 0.219594) + << QVector3D(1, 1, 1) + << RenderingIntent::RelativeColorimetric; + QTest::addRow("sRGB -> rec.2020 relative colorimetric with bpc") + << std::make_shared(ColorDescription{ + Colorimetry::BT709, + TransferFunction(TransferFunction::gamma22, 0.2, 80), + 80, + 0.2, + std::nullopt, + std::nullopt, + }) + << std::make_shared(ColorDescription{ + Colorimetry::BT2020, + TransferFunction(TransferFunction::PerceptualQuantizer, 0.005, 10'000), + 500, + 0.005, + std::nullopt, + std::nullopt, + }) + << QVector3D(0, 0, 0) + << QVector3D(0.51667, 0.51667, 0.51667) + << QVector3D(0.67658, 0.67658, 0.67658) + << RenderingIntent::RelativeColorimetricWithBPC; + QTest::addRow("scRGB -> scRGB relative colorimetric with bpc") + << std::make_shared(ColorDescription{ + Colorimetry::BT709, + TransferFunction(TransferFunction::linear, 0, 80), + TransferFunction::defaultReferenceLuminanceFor(TransferFunction::gamma22), + 0, + std::nullopt, + std::nullopt, + }) + << std::make_shared(ColorDescription{ + Colorimetry::BT709, + TransferFunction(TransferFunction::linear, 8, 80), + 80, + 8, + std::nullopt, + std::nullopt, + }) + << QVector3D(0, 0, 0) + << QVector3D(0.5, 0.5, 0.5) + << QVector3D(1, 1, 1) + << RenderingIntent::RelativeColorimetricWithBPC; +} + +void TestColorspaces::testColorPipeline() +{ + QFETCH(std::shared_ptr, srcColor); + QFETCH(std::shared_ptr, dstColor); + QFETCH(QVector3D, dstBlack); + QFETCH(QVector3D, dstGray); + QFETCH(QVector3D, dstWhite); + QFETCH(RenderingIntent, intent); + + const auto pipeline = ColorPipeline::create(srcColor, dstColor, intent); + QVERIFY(compareVectors(pipeline.evaluate(QVector3D(0, 0, 0)), dstBlack, s_resolution10bit)); + QVERIFY(compareVectors(pipeline.evaluate(QVector3D(0.5, 0.5, 0.5)), dstGray, s_resolution10bit)); + QVERIFY(compareVectors(pipeline.evaluate(QVector3D(1, 1, 1)), dstWhite, s_resolution10bit)); + + const auto inversePipeline = ColorPipeline::create(dstColor, srcColor, intent); + QVERIFY(compareVectors(inversePipeline.evaluate(dstBlack), QVector3D(0, 0, 0), s_resolution10bit)); + QVERIFY(compareVectors(inversePipeline.evaluate(dstGray), QVector3D(0.5, 0.5, 0.5), s_resolution10bit)); + QVERIFY(compareVectors(inversePipeline.evaluate(dstWhite), QVector3D(1, 1, 1), s_resolution10bit)); +} + +void TestColorspaces::testXYZ() +{ + Colorimetry xyz = Colorimetry::CIEXYZ; + QVERIFY(isFuzzyIdentity(xyz.toXYZ())); + QVERIFY(isFuzzyIdentity(xyz.fromXYZ())); +} + +void TestColorspaces::testOpenglShader_data() +{ + QTest::addColumn("intent"); + QTest::addColumn("maxError"); + + // the allowed error here needs to be this high because of llvmpipe. With real GPU drivers it's lower + QTest::addRow("Perceptual") << RenderingIntent::Perceptual << 7.0; + QTest::addRow("RelativeColorimetric") << RenderingIntent::RelativeColorimetric << 1.5; + QTest::addRow("AbsoluteColorimetricNoAdaptation") << RenderingIntent::AbsoluteColorimetricNoAdaptation << 1.5; + QTest::addRow("RelativeColorimetricWithBPC") << RenderingIntent::RelativeColorimetricWithBPC << 1.5; +} + +void TestColorspaces::testOpenglShader() +{ + QFETCH(RenderingIntent, intent); + QFETCH(double, maxError); + + const auto display = EglDisplay::create(eglGetDisplay(EGL_DEFAULT_DISPLAY)); + const auto context = EglContext::create(display.get(), EGL_NO_CONFIG_KHR, EGL_NO_CONTEXT); + + const auto src = std::make_shared(ColorDescription{ + Colorimetry::BT709, + TransferFunction(TransferFunction::linear, 0, 400), + 100, + 0, + 200, + 400, + }); + const auto dst = std::make_shared(ColorDescription{ + Colorimetry::BT709, + TransferFunction(TransferFunction::gamma22), + 100, + 0, + 100, + 100, + }); + + QImage input(255, 255, QImage::Format_RGBA8888_Premultiplied); + for (int x = 0; x < input.width(); x++) { + for (int y = 0; y < input.height(); y++) { + input.setPixel(x, y, qRgba(x, y, 0, 255)); + } + } + + QImage openGlResult; + { + ShaderBinder binder(ShaderTrait::MapTexture | ShaderTrait::TransformColorspace); + QMatrix4x4 proj; + proj.ortho(QRectF(0, 0, input.width(), input.height())); + binder.shader()->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, proj); + binder.shader()->setColorspaceUniforms(src, dst, intent); + const auto target = GLTexture::allocate(GL_RGBA8, input.size()); + GLFramebuffer buffer(target.get()); + context->pushFramebuffer(&buffer); + + const auto srcColor = GLTexture::upload(input); + srcColor->render(input.size()); + + context->popFramebuffer(); + openGlResult = target->toImage(); + openGlResult.mirror(); + } + QImage pipelineResult(input.width(), input.height(), QImage::Format_RGBA8888_Premultiplied); + { + const auto pipeline = ColorPipeline::create(src, dst, intent); + for (int x = 0; x < input.width(); x++) { + for (int y = 0; y < input.height(); y++) { + const auto pixel = input.pixel(x, y); + const QVector3D colors = QVector3D(qRed(pixel), qGreen(pixel), qBlue(pixel)) / 255; + const QVector3D output = pipeline.evaluate(colors) * 255; + pipelineResult.setPixel(x, y, qRgba(std::round(output.x()), std::round(output.y()), std::round(output.z()), 255)); + } + } + } + + for (int x = 0; x < input.width(); x++) { + for (int y = 0; y < input.height(); y++) { + const auto glPixel = openGlResult.pixel(x, y); + const QVector3D glColors = QVector3D(qRed(glPixel), qGreen(glPixel), qBlue(glPixel)); + const auto pipePixel = pipelineResult.pixel(x, y); + const QVector3D pipeColors = QVector3D(qRed(pipePixel), qGreen(pipePixel), qBlue(pipePixel)); + const QVector3D difference = (glColors - pipeColors); + QCOMPARE_LE(difference.length(), maxError); + } + } +} + +void TestColorspaces::testIccShader_data() +{ + QTest::addColumn("iccProfilePath"); + QTest::addColumn("lcmsIccProfilePath"); + QTest::addColumn("intent"); + QTest::addColumn("lcmsIntent"); + QTest::addColumn("maxAllowedError"); + + const auto F13 = QFINDTESTDATA("data/Framework 13.icc"); + const auto Samsung = QFINDTESTDATA("data/Samsung CRG49 Shaper Matrix.icc"); + QTest::addRow("relative colorimetric Framework 13") << F13 << F13 << RenderingIntent::RelativeColorimetric << uint32_t(INTENT_RELATIVE_COLORIMETRIC) << 5; + QTest::addRow("absolute colorimetric Framework 13") << F13 << F13 << RenderingIntent::AbsoluteColorimetricNoAdaptation << uint32_t(INTENT_ABSOLUTE_COLORIMETRIC) << 4; + QTest::addRow("relative colorimetric CRG49") << Samsung << Samsung << RenderingIntent::RelativeColorimetric << uint32_t(INTENT_RELATIVE_COLORIMETRIC) << 2; + QTest::addRow("absolute colorimetric CRG49") << Samsung << Samsung << RenderingIntent::AbsoluteColorimetricNoAdaptation << uint32_t(INTENT_ABSOLUTE_COLORIMETRIC) << 2; + + // NOTE that LCMS doesn't apply the MHC2 tag, so we compare with the native profile instead + // The margin for error has to be a bit higher because of that + QTest::addRow("absolute colorimetry with MHC2") << QFINDTESTDATA("data/HP 'sRGB' profile with MHC2.icc") << QFINDTESTDATA("data/HP 'Native' profile.icc") + << RenderingIntent::AbsoluteColorimetricNoAdaptation << uint32_t(INTENT_ABSOLUTE_COLORIMETRIC) << 7; +} + +void TestColorspaces::testIccShader() +{ + const auto display = EglDisplay::create(eglGetDisplay(EGL_DEFAULT_DISPLAY)); + const auto context = EglContext::create(display.get(), EGL_NO_CONFIG_KHR, EGL_NO_CONTEXT); + + QImage input(255, 255, QImage::Format_RGBA8888_Premultiplied); + for (int x = 0; x < input.width(); x++) { + for (int y = 0; y < input.height(); y++) { + input.setPixel(x, y, qRgba(x, y, (x + y) / 2, 255)); + } + } + const auto imageColorspace = ColorDescription::sRGB; + + QFETCH(QString, iccProfilePath); + QFETCH(QString, lcmsIccProfilePath); + QFETCH(RenderingIntent, intent); + QFETCH(uint32_t, lcmsIntent); + + const std::shared_ptr profile = IccProfile::load(iccProfilePath).value_or(nullptr); + QVERIFY(profile); + + QImage pipelineResult(input.width(), input.height(), QImage::Format_RGBA8888_Premultiplied); + { + // by default, LCMS uses adaption state 1.0 for absolute colorimetric, + // also known as relative colorimetric... we don't want that + cmsSetAdaptationState(0); + + const auto toCmsxyY = [](const xyY &primary) { + return cmsCIExyY{ + .x = primary.x, + .y = primary.y, + .Y = primary.Y, + }; + }; + const cmsCIExyY sRGBWhite = toCmsxyY(imageColorspace->containerColorimetry().white().toxyY()); + const cmsCIExyYTRIPLE sRGBPrimaries{ + .Red = toCmsxyY(imageColorspace->containerColorimetry().red().toxyY()), + .Green = toCmsxyY(imageColorspace->containerColorimetry().green().toxyY()), + .Blue = toCmsxyY(imageColorspace->containerColorimetry().blue().toxyY()), + }; + // parametric curve 6 is Y = (a * X + b) ^ gamma + c + // Y and X are normalized, so c must be the relative black level lift + // b is zero, so it can be simplified to Y = a^gamma * X^gamma + c + // Y(1.0) = 1.0, so a = ((max - min) / max) ^ (1/gamma) + // or a = (1.0 - min / max) ^ (1 / gamma) + const std::array params = { + 2.2, // gamma + std::pow(1.0 - imageColorspace->transferFunction().minLuminance / imageColorspace->transferFunction().maxLuminance, 1.0 / 2.2), // a + 0, // b + imageColorspace->transferFunction().minLuminance / imageColorspace->transferFunction().maxLuminance, // c + }; + const std::array toneCurves = { + cmsBuildParametricToneCurve(nullptr, 6, params.data()), + cmsBuildParametricToneCurve(nullptr, 6, params.data()), + cmsBuildParametricToneCurve(nullptr, 6, params.data()), + }; + // note that we can't just use cmsCreate_sRGBProfile here + // as that uses the sRGB piece-wise transfer function, which is not correct for our use case + cmsHPROFILE sRGBHandle = cmsCreateRGBProfile(&sRGBWhite, &sRGBPrimaries, toneCurves.data()); + + cmsHPROFILE handle = cmsOpenProfileFromFile(lcmsIccProfilePath.toUtf8(), "r"); + QVERIFY(handle); + + const auto transform = cmsCreateTransform(sRGBHandle, TYPE_RGB_8, handle, TYPE_RGB_8, lcmsIntent, cmsFLAGS_NOOPTIMIZE); + QVERIFY(transform); + + for (int x = 0; x < input.width(); x++) { + for (int y = 0; y < input.height(); y++) { + const auto pixel = input.pixel(x, y); + std::array in = { + uint8_t(qRed(pixel)), + uint8_t(qGreen(pixel)), + uint8_t(qBlue(pixel)), + }; + std::array out = {0, 0, 0}; + cmsDoTransform(transform, in.data(), out.data(), 1); + pipelineResult.setPixel(x, y, qRgba(out[0], out[1], out[2], 255)); + } + } + cmsDeleteTransform(transform); + cmsCloseProfile(sRGBHandle); + cmsCloseProfile(handle); + } + + QImage openGlResult; + { + IccShader shader; + ShaderBinder binder{shader.shader()}; + shader.setUniforms(profile, imageColorspace, intent); + + QMatrix4x4 proj; + proj.ortho(QRectF(0, 0, input.width(), input.height())); + binder.shader()->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, proj); + const auto target = GLTexture::allocate(GL_RGBA8, input.size()); + GLFramebuffer buffer(target.get()); + context->pushFramebuffer(&buffer); + + const auto srcColor = GLTexture::upload(input); + srcColor->render(input.size()); + + context->popFramebuffer(); + openGlResult = target->toImage(); + openGlResult.mirror(); + } + + float maxError = 0; + for (int x = 0; x < input.width(); x++) { + for (int y = 0; y < input.height(); y++) { + const auto glPixel = openGlResult.pixel(x, y); + const QVector3D glColors = QVector3D(qRed(glPixel), qGreen(glPixel), qBlue(glPixel)); + const auto pipePixel = pipelineResult.pixel(x, y); + const QVector3D pipeColors = QVector3D(qRed(pipePixel), qGreen(pipePixel), qBlue(pipePixel)); + const QVector3D difference = (glColors - pipeColors); + + maxError = std::max(difference.length(), maxError); + } + } + + qWarning() << "Max ICC shader error:" << maxError; + QFETCH(int, maxAllowedError); + QCOMPARE_LE(maxError, maxAllowedError); +} + +void TestColorspaces::dontCrashWithWeirdHdrMetadata() +{ + // verify that weird display metadata with max. luminance < reference luminance + // doesn't crash KWin + const auto in = std::make_shared(ColorDescription{ + Colorimetry::BT709, + TransferFunction(TransferFunction::linear, 0, 80), + 80, + 0, + 60, + 60, + }); + const auto out = std::make_shared(ColorDescription{ + Colorimetry::BT709, + TransferFunction(TransferFunction::linear, 0, 80), + 80, + 0, + 40, + 40, + }); + const auto pipeline = ColorPipeline::create(in, out, RenderingIntent::Perceptual); + QVERIFY(compareVectors(pipeline.evaluate(QVector3D()), QVector3D(), 0.000001)); +} + +void TestColorspaces::testColorimetryCheck_data() +{ + QTest::addColumn("expectedResult"); + QTest::addColumn("red"); + QTest::addColumn("green"); + QTest::addColumn("blue"); + QTest::addColumn("white"); + + QTest::addRow("PAL M") << true << xy{0.67, 0.33} << xy{0.21, 0.71} << xy{0.14, 0.08} << xy{0.310, 0.316}; + QTest::addRow("sRGB") << true << xy{0.64, 0.33} << xy{0.30, 0.60} << xy{0.15, 0.06} << xy{0.3127, 0.3290}; + QTest::addRow("sRGB with one negative data point") << false << xy{-1, 0.33} << xy{0.30, 0.60} << xy{0.15, 0.06} << xy{0.3127, 0.3290}; + QTest::addRow("sRGB with out of bounds white point") << false << xy{0.64, 0.33} << xy{0.30, 0.60} << xy{0.15, 0.06} << xy{0.9, 0.9}; + QTest::addRow("all zeros") << false << xy{0, 0} << xy{0, 0} << xy{0, 0} << xy{0, 0}; + QTest::addRow("all ones") << false << xy{1, 1} << xy{1, 1} << xy{1, 1} << xy{1, 1}; + QTest::addRow("BT2020") << true << xy{0.708, 0.292} << xy{0.170, 0.797} << xy{0.131, 0.046} << xy{0.3127, 0.3290}; + QTest::addRow("Display P3") << true << xy{0.680, 0.320} << xy{0.265, 0.690} << xy{0.150, 0.060} << xy{0.3127, 0.3290}; + QTest::addRow("AUO 44944") << true << xy{0.5664, 0.3388} << xy{0.3505, 0.5683} << xy{0.1582, 0.1210} << xy{0.3134, 0.3291}; + QTest::addRow("SAM 3996") << true << xy{0.6777, 0.3105} << xy{0.2734, 0.6542} << xy{0.1425, 0.0566} << xy{0.3125, 0.3291}; + QTest::addRow("BOE 3252") << true << xy{0.6523, 0.3320} << xy{0.2949, 0.6230} << xy{0.1464, 0.0488} << xy{0.3134, 0.3291}; +} + +void TestColorspaces::testColorimetryCheck() +{ + QFETCH(bool, expectedResult); + QFETCH(xy, red); + QFETCH(xy, green); + QFETCH(xy, blue); + QFETCH(xy, white); + QCOMPARE(Colorimetry::isValid(red, green, blue, white), expectedResult); +} + +void TestColorspaces::testYCbCr_data() +{ + QTest::addColumn("yuvCoefficients"); + + QTest::addRow("BT601") << YUVMatrixCoefficients::BT601; + QTest::addRow("BT709") << YUVMatrixCoefficients::BT709; + QTest::addRow("BT2020") << YUVMatrixCoefficients::BT2020; +} + +static float limitedLuma(float value) +{ + return (16 + 219 * value) / 255.0; +} +static float limitedChroma(float value) +{ + return (128 + 224 * value) / 255.0; +}; + +void TestColorspaces::testYCbCr() +{ + QFETCH(YUVMatrixCoefficients, yuvCoefficients); + ColorDescription limitedRange{Colorimetry::BT709, TransferFunction(TransferFunction::gamma22), yuvCoefficients, EncodingRange::Limited}; + QVERIFY(compareVectors(limitedRange.yuvMatrix() * QVector3D(limitedLuma(1), limitedChroma(0), limitedChroma(0)), QVector3D(1, 1, 1), 0.005)); + QVERIFY(compareVectors(limitedRange.yuvMatrix() * QVector3D(limitedLuma(0), limitedChroma(0), limitedChroma(0)), QVector3D(0, 0, 0), 0.005)); + + ColorDescription fullRange{Colorimetry::BT709, TransferFunction(TransferFunction::gamma22), yuvCoefficients, EncodingRange::Full}; + QVERIFY(compareVectors(fullRange.yuvMatrix() * QVector3D(1, 0.5, 0.5), QVector3D(1, 1, 1), 0.005)); + QVERIFY(compareVectors(fullRange.yuvMatrix() * QVector3D(0, 0.5, 0.5), QVector3D(0, 0, 0), 0.005)); +} + +void TestColorspaces::testBlackPointCompensation() +{ + // this test verifies that black point compensation both works and is optimized + const auto src = ColorDescription::sRGB; + const auto dst = std::make_shared(ColorDescription{ + src->containerColorimetry(), + TransferFunction(TransferFunction::gamma22, 0.01, 200), + 200, + 0.01, + 200, + 200, + }); + + QVERIFY(ColorPipeline::create(src, dst, RenderingIntent::Perceptual).isIdentity()); + QVERIFY(ColorPipeline::create(dst, src, RenderingIntent::Perceptual).isIdentity()); + + QVERIFY(ColorPipeline::create(src, dst, RenderingIntent::RelativeColorimetricWithBPC).isIdentity()); + QVERIFY(ColorPipeline::create(dst, src, RenderingIntent::RelativeColorimetricWithBPC).isIdentity()); +} + +static QVector3D clamp(const QVector3D &value, float min, float max) +{ + return QVector3D(std::clamp(value.x(), min, max), std::clamp(value.y(), min, max), std::clamp(value.z(), min, max)); +} + +void TestColorspaces::testSCRGB() +{ + const auto scRGB = std::make_shared(ColorDescription{ + Colorimetry::BT709, + TransferFunction(TransferFunction::linear, 0, 80), + 203, + 0, + 500, + 1000, + Colorimetry::BT2020, + Colorimetry::BT709, + }); + const auto hdrOutput = std::make_shared(ColorDescription{ + Colorimetry::BT2020, + TransferFunction(TransferFunction::PerceptualQuantizer), + 203, + 0.005, + 500, + 1000, + }); + const ColorPipeline toOutput = ColorPipeline::create(scRGB, hdrOutput, RenderingIntent::RelativeColorimetricWithBPC); + QCOMPARE(toOutput.ops.size(), 2); + // this is roughly the range of values required to represent BT2020 primaries at 1000cd/m² in scRGB + QCOMPARE_LE(toOutput.inputRange.min, -7); + QCOMPARE_GE(toOutput.inputRange.max, 12.5); + + for (const auto test : {QVector3D(1000, 0, 0), QVector3D(0, 1000, 0), QVector3D(0, 0, 1000), QVector3D(1000, 1000, 1000), QVector3D(0, 0, 0)}) { + QVector3D out = scRGB->transferFunction().nitsToEncoded(test); + for (const auto &op : toOutput.ops) { + out = clamp(out, op.input.min, op.input.max); + out = op.apply(out); + out = clamp(out, op.output.min, op.output.max); + } + out = hdrOutput->transferFunction().encodedToNits(out); + const QVector3D direct = scRGB->toOther(*hdrOutput, RenderingIntent::RelativeColorimetricWithBPC) * test; + QCOMPARE_LE(out.x(), direct.x() + 1); + QCOMPARE_GE(out.x(), direct.x() - 1); + QCOMPARE_LE(out.y(), direct.y() + 1); + QCOMPARE_GE(out.y(), direct.y() - 1); + QCOMPARE_LE(out.z(), direct.z() + 1); + QCOMPARE_GE(out.z(), direct.z() - 1); + } +} + +void TestColorspaces::testNightLightNoTonemapping() +{ + const auto src = ColorDescription::sRGB; + const xyY newWhite = XYZ::fromVector(src->containerColorimetry().toXYZ() * QVector3D(1.0, 0.8, 0.5)).toxyY(); + const auto dst = src->withWhitepoint(newWhite)->dimmed(newWhite.Y); + + // the color pipeline should not have any tonemapping steps in it + const auto pipeline = ColorPipeline::create(src, dst, RenderingIntent::Perceptual); + QCOMPARE(pipeline.ops.size(), 3); + QVERIFY(std::holds_alternative(pipeline.ops[0].operation)); + QVERIFY(std::holds_alternative(pipeline.ops[1].operation)); + QVERIFY(std::holds_alternative(pipeline.ops[2].operation)); +} + +QTEST_MAIN(TestColorspaces) + +#include "test_colorspaces.moc" diff --git a/local/recipes/kde/kwin/source/autotests/test_ftrace.cpp b/local/recipes/kde/kwin/source/autotests/test_ftrace.cpp new file mode 100644 index 0000000000..d460e4b2a9 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/test_ftrace.cpp @@ -0,0 +1,72 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2021 David Edmundson + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include +#include +#include + +#include "ftrace.h" + +class TestFTrace : public QObject +{ + Q_OBJECT +public: + TestFTrace(); +private Q_SLOTS: + void benchmarkTraceOff(); + void benchmarkTraceDurationOff(); + void enable(); + +private: + QTemporaryFile m_tempFile; +}; + +TestFTrace::TestFTrace() +{ + m_tempFile.open(); + qputenv("KWIN_PERF_FTRACE_FILE", m_tempFile.fileName().toLatin1()); + + KWin::FTraceLogger::create(); +} + +void TestFTrace::benchmarkTraceOff() +{ + // this macro should no-op, so take no time at all + QBENCHMARK { + fTrace("BENCH", 123, "foo"); + } +} + +void TestFTrace::benchmarkTraceDurationOff() +{ + QBENCHMARK { + fTraceDuration("BENCH", 123, "foo"); + } +} + +void TestFTrace::enable() +{ + KWin::FTraceLogger::self()->setEnabled(true); + QVERIFY(KWin::FTraceLogger::self()->isEnabled()); + + { + fTrace("TEST", 123, "foo"); + fTraceDuration("TEST_DURATION", "boo"); + fTrace("TEST", 123, "foo"); + } + + QCOMPARE(m_tempFile.readLine(), "TEST123foo\n"); + QCOMPARE(m_tempFile.readLine(), "TEST_DURATIONboo begin_ctx=1\n"); + QCOMPARE(m_tempFile.readLine(), "TEST123foo\n"); + QCOMPARE(m_tempFile.readLine(), "TEST_DURATIONboo end_ctx=1\n"); +} + +QTEST_MAIN(TestFTrace) + +#include "test_ftrace.moc" diff --git a/local/recipes/kde/kwin/source/autotests/test_gestures.cpp b/local/recipes/kde/kwin/source/autotests/test_gestures.cpp new file mode 100644 index 0000000000..8e98e3c320 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/test_gestures.cpp @@ -0,0 +1,309 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "gestures.h" + +#include +#include +#include +#include + +using namespace KWin; + +Q_DECLARE_METATYPE(SwipeDirection); +Q_DECLARE_METATYPE(PinchDirection); + +class GestureTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testMinimumScaleDelta(); + void testUnregisterSwipeCancels(); + void testUnregisterPinchCancels(); + void testDeleteSwipeCancels(); + void testSwipeCancel_data(); + void testSwipeCancel(); + void testSwipeUpdateTrigger_data(); + void testSwipeUpdateTrigger(); + void testMinimumDeltaReached_data(); + void testMinimumDeltaReached(); + void testNotEmitCallbacksBeforeDirectionDecided(); +}; + +void GestureTest::testMinimumDeltaReached_data() +{ + QTest::addColumn("direction"); + QTest::addColumn("delta"); + QTest::addColumn("reached"); + QTest::addColumn("progress"); + + QTest::newRow("Up (more)") << SwipeDirection::Up << QPointF(0, -SwipeGesture::s_minimumDelta * 2) << true << 1.0; + QTest::newRow("Up (exact)") << SwipeDirection::Up << QPointF(0, -SwipeGesture::s_minimumDelta) << true << 1.0; + QTest::newRow("Up (less)") << SwipeDirection::Up << QPointF(0, -SwipeGesture::s_minimumDelta / 2) << false << 0.5; + QTest::newRow("Left (more)") << SwipeDirection::Left << QPointF(-SwipeGesture::s_minimumDelta * 2, 0) << true << 1.0; + QTest::newRow("Left (exact)") << SwipeDirection::Left << QPointF(-SwipeGesture::s_minimumDelta, 0) << true << 1.0; + QTest::newRow("Left (less)") << SwipeDirection::Left << QPointF(-SwipeGesture::s_minimumDelta / 2, 0) << false << 0.5; + QTest::newRow("Right (more)") << SwipeDirection::Right << QPointF(SwipeGesture::s_minimumDelta * 2, 0) << true << 1.0; + QTest::newRow("Right (exact)") << SwipeDirection::Right << QPointF(SwipeGesture::s_minimumDelta, 0) << true << 1.0; + QTest::newRow("Right (less)") << SwipeDirection::Right << QPointF(SwipeGesture::s_minimumDelta / 2, 0) << false << 0.5; + QTest::newRow("Down (more)") << SwipeDirection::Down << QPointF(0, SwipeGesture::s_minimumDelta * 2) << true << 1.0; + QTest::newRow("Down (exact)") << SwipeDirection::Down << QPointF(0, SwipeGesture::s_minimumDelta) << true << 1.0; + QTest::newRow("Down (less)") << SwipeDirection::Down << QPointF(0, SwipeGesture::s_minimumDelta / 2) << false << 0.5; +} + +void GestureTest::testMinimumDeltaReached() +{ + GestureRecognizer recognizer; + + // swipe gesture + SwipeGesture gesture(1); + QFETCH(SwipeDirection, direction); + gesture.setDirection(direction); + QFETCH(QPointF, delta); + QFETCH(bool, reached); + QCOMPARE(gesture.minimumDeltaReached(delta), reached); + + recognizer.registerSwipeGesture(&gesture); + + QSignalSpy startedSpy(&gesture, &SwipeGesture::started); + QSignalSpy triggeredSpy(&gesture, &SwipeGesture::triggered); + QSignalSpy cancelledSpy(&gesture, &SwipeGesture::cancelled); + QSignalSpy progressSpy(&gesture, &SwipeGesture::progress); + + recognizer.startSwipeGesture(1); + QCOMPARE(startedSpy.count(), 1); + QCOMPARE(triggeredSpy.count(), 0); + QCOMPARE(cancelledSpy.count(), 0); + QCOMPARE(progressSpy.count(), 0); + + recognizer.updateSwipeGesture(delta); + QCOMPARE(startedSpy.count(), 1); + QCOMPARE(triggeredSpy.count(), 0); + QCOMPARE(cancelledSpy.count(), 0); + QCOMPARE(progressSpy.count(), 1); + QTEST(progressSpy.first().first().value(), "progress"); + + recognizer.endSwipeGesture(); + QCOMPARE(startedSpy.count(), 1); + QCOMPARE(progressSpy.count(), 1); + QCOMPARE(triggeredSpy.isEmpty(), !reached); + QCOMPARE(cancelledSpy.isEmpty(), reached); +} + +void GestureTest::testMinimumScaleDelta() +{ + // pinch gesture + PinchGesture gesture(4); + gesture.setDirection(PinchDirection::Contracting); + + QCOMPARE(gesture.minimumScaleDeltaReached(1.1), false); + QCOMPARE(gesture.minimumScaleDeltaReached(1.3), true); + + GestureRecognizer recognizer; + recognizer.registerPinchGesture(&gesture); + + QSignalSpy startedSpy(&gesture, &PinchGesture::started); + QSignalSpy triggeredSpy(&gesture, &PinchGesture::triggered); + QSignalSpy cancelledSpy(&gesture, &PinchGesture::cancelled); + QSignalSpy progressSpy(&gesture, &PinchGesture::progress); + + recognizer.startPinchGesture(4); + QCOMPARE(startedSpy.count(), 1); + QCOMPARE(triggeredSpy.count(), 0); + QCOMPARE(cancelledSpy.count(), 0); + QCOMPARE(progressSpy.count(), 0); +} + +void GestureTest::testUnregisterSwipeCancels() +{ + GestureRecognizer recognizer; + auto gesture = std::make_unique(1); + QSignalSpy startedSpy(gesture.get(), &SwipeGesture::started); + QSignalSpy cancelledSpy(gesture.get(), &SwipeGesture::cancelled); + + recognizer.registerSwipeGesture(gesture.get()); + recognizer.startSwipeGesture(1); + QCOMPARE(startedSpy.count(), 1); + QCOMPARE(cancelledSpy.count(), 0); + recognizer.unregisterSwipeGesture(gesture.get()); + QCOMPARE(cancelledSpy.count(), 1); + + // delete the gesture should not trigger cancel + gesture.reset(); + QCOMPARE(cancelledSpy.count(), 1); +} + +void GestureTest::testUnregisterPinchCancels() +{ + GestureRecognizer recognizer; + auto gesture = std::make_unique(1); + QSignalSpy startedSpy(gesture.get(), &PinchGesture::started); + QSignalSpy cancelledSpy(gesture.get(), &PinchGesture::cancelled); + + recognizer.registerPinchGesture(gesture.get()); + recognizer.startPinchGesture(1); + QCOMPARE(startedSpy.count(), 1); + QCOMPARE(cancelledSpy.count(), 0); + recognizer.unregisterPinchGesture(gesture.get()); + QCOMPARE(cancelledSpy.count(), 1); + + // delete the gesture should not trigger cancel + gesture.reset(); + QCOMPARE(cancelledSpy.count(), 1); +} + +void GestureTest::testDeleteSwipeCancels() +{ + GestureRecognizer recognizer; + auto gesture = std::make_unique(1); + QSignalSpy startedSpy(gesture.get(), &SwipeGesture::started); + QSignalSpy cancelledSpy(gesture.get(), &SwipeGesture::cancelled); + + recognizer.registerSwipeGesture(gesture.get()); + recognizer.startSwipeGesture(1); + QCOMPARE(startedSpy.count(), 1); + QCOMPARE(cancelledSpy.count(), 0); + gesture.reset(); + QCOMPARE(cancelledSpy.count(), 1); +} + +void GestureTest::testSwipeCancel_data() +{ + QTest::addColumn("direction"); + + QTest::newRow("Up") << SwipeDirection::Up; + QTest::newRow("Left") << SwipeDirection::Left; + QTest::newRow("Right") << SwipeDirection::Right; + QTest::newRow("Down") << SwipeDirection::Down; +} + +void GestureTest::testSwipeCancel() +{ + GestureRecognizer recognizer; + auto gesture = std::make_unique(1); + QFETCH(SwipeDirection, direction); + gesture->setDirection(direction); + QSignalSpy startedSpy(gesture.get(), &SwipeGesture::started); + QSignalSpy cancelledSpy(gesture.get(), &SwipeGesture::cancelled); + QSignalSpy triggeredSpy(gesture.get(), &SwipeGesture::triggered); + + recognizer.registerSwipeGesture(gesture.get()); + recognizer.startSwipeGesture(1); + QCOMPARE(startedSpy.count(), 1); + QCOMPARE(cancelledSpy.count(), 0); + recognizer.cancelSwipeGesture(); + QCOMPARE(cancelledSpy.count(), 1); + QCOMPARE(triggeredSpy.count(), 0); +} + +void GestureTest::testSwipeUpdateTrigger_data() +{ + QTest::addColumn("direction"); + QTest::addColumn("delta"); + + QTest::newRow("Up") << SwipeDirection::Up << QPointF(2, -300); + QTest::newRow("Left") << SwipeDirection::Left << QPointF(-200, 1); + QTest::newRow("Right") << SwipeDirection::Right << QPointF(400, -19); + QTest::newRow("Down") << SwipeDirection::Down << QPointF(0, 500); +} + +void GestureTest::testSwipeUpdateTrigger() +{ + GestureRecognizer recognizer; + SwipeGesture gesture(1); + QFETCH(SwipeDirection, direction); + gesture.setDirection(direction); + + QSignalSpy triggeredSpy(&gesture, &SwipeGesture::triggered); + QSignalSpy cancelledSpy(&gesture, &SwipeGesture::cancelled); + + recognizer.registerSwipeGesture(&gesture); + + recognizer.startSwipeGesture(1); + QFETCH(QPointF, delta); + recognizer.updateSwipeGesture(delta); + QCOMPARE(cancelledSpy.count(), 0); + QCOMPARE(triggeredSpy.count(), 0); + + recognizer.endSwipeGesture(); + QCOMPARE(cancelledSpy.count(), 0); + QCOMPARE(triggeredSpy.count(), 1); +} + +void GestureTest::testNotEmitCallbacksBeforeDirectionDecided() +{ + GestureRecognizer recognizer; + SwipeGesture up{4}; + SwipeGesture down{4}; + SwipeGesture right{4}; + PinchGesture expand{4}; + PinchGesture contract{4}; + up.setDirection(SwipeDirection::Up); + down.setDirection(SwipeDirection::Down); + right.setDirection(SwipeDirection::Right); + expand.setDirection(PinchDirection::Expanding); + contract.setDirection(PinchDirection::Contracting); + recognizer.registerSwipeGesture(&up); + recognizer.registerSwipeGesture(&down); + recognizer.registerSwipeGesture(&right); + recognizer.registerPinchGesture(&expand); + recognizer.registerPinchGesture(&contract); + + QSignalSpy upSpy(&up, &SwipeGesture::progress); + QSignalSpy downSpy(&down, &SwipeGesture::progress); + QSignalSpy rightSpy(&right, &SwipeGesture::progress); + QSignalSpy expandSpy(&expand, &PinchGesture::progress); + QSignalSpy contractSpy(&contract, &PinchGesture::progress); + + // don't release callback until we know the direction of swipe gesture + recognizer.startSwipeGesture(4); + QCOMPARE(upSpy.count(), 0); + QCOMPARE(downSpy.count(), 0); + QCOMPARE(rightSpy.count(), 0); + + // up (negative y) + recognizer.updateSwipeGesture(QPointF(0, -1.5)); + QCOMPARE(upSpy.count(), 1); + QCOMPARE(downSpy.count(), 0); + QCOMPARE(rightSpy.count(), 0); + + // down (positive y) + // recognizer.updateSwipeGesture(QPointF(0, 0)); + recognizer.updateSwipeGesture(QPointF(0, 3)); + QCOMPARE(upSpy.count(), 1); + QCOMPARE(downSpy.count(), 1); + QCOMPARE(rightSpy.count(), 0); + + // right + recognizer.cancelSwipeGesture(); + recognizer.startSwipeGesture(4); + recognizer.updateSwipeGesture(QPointF(1, 0)); + QCOMPARE(upSpy.count(), 1); + QCOMPARE(downSpy.count(), 1); + QCOMPARE(rightSpy.count(), 1); + + recognizer.cancelSwipeGesture(); + + // same test for pinch gestures + recognizer.startPinchGesture(4); + QCOMPARE(expandSpy.count(), 0); + QCOMPARE(contractSpy.count(), 0); + + // contracting + recognizer.updatePinchGesture(.5, 0, QPointF(0, 0)); + QCOMPARE(expandSpy.count(), 0); + QCOMPARE(contractSpy.count(), 1); + + // expanding + recognizer.updatePinchGesture(1.5, 0, QPointF(0, 0)); + QCOMPARE(expandSpy.count(), 1); + QCOMPARE(contractSpy.count(), 1); +} + +QTEST_MAIN(GestureTest) +#include "test_gestures.moc" diff --git a/local/recipes/kde/kwin/source/autotests/test_utils.cpp b/local/recipes/kde/kwin/source/autotests/test_utils.cpp new file mode 100644 index 0000000000..ee4c3f6acb --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/test_utils.cpp @@ -0,0 +1,71 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 MBition GmbH + SPDX-FileContributor: Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include +#include + +#include "utils/ramfile.h" + +#include + +using namespace KWin; + +class TestUtils : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testRamFile(); + void testSealedRamFile(); +}; + +static const QByteArray s_testByteArray = QByteArrayLiteral("Test Data \0\1\2\3"); +static const char s_writeTestArray[] = "test"; + +void TestUtils::testRamFile() +{ + KWin::RamFile file("test", s_testByteArray.constData(), s_testByteArray.size()); + QVERIFY(file.isValid()); + QCOMPARE(file.size(), s_testByteArray.size()); + + QVERIFY(file.fd() != -1); + + char buf[20]; + int num = read(file.fd(), buf, sizeof buf); + QCOMPARE(num, file.size()); + + QCOMPARE(qstrcmp(s_testByteArray.constData(), buf), 0); +} + +void TestUtils::testSealedRamFile() +{ +#if HAVE_MEMFD + KWin::RamFile file("test", s_testByteArray.constData(), s_testByteArray.size(), KWin::RamFile::Flag::SealWrite); + QVERIFY(file.isValid()); + QVERIFY(file.effectiveFlags().testFlag(KWin::RamFile::Flag::SealWrite)); + + // Writing should not work. + auto written = write(file.fd(), s_writeTestArray, strlen(s_writeTestArray)); + QCOMPARE(written, -1); + + // Cannot use MAP_SHARED on sealed file descriptor. + void *data = mmap(nullptr, file.size(), PROT_WRITE, MAP_SHARED, file.fd(), 0); + QCOMPARE(data, MAP_FAILED); + + data = mmap(nullptr, file.size(), PROT_WRITE, MAP_PRIVATE, file.fd(), 0); + QVERIFY(data != MAP_FAILED); +#else + QSKIP("Sealing requires memfd support."); +#endif +} + +QTEST_MAIN(TestUtils) +#include "test_utils.moc" diff --git a/local/recipes/kde/kwin/source/autotests/test_virtual_desktops.cpp b/local/recipes/kde/kwin/source/autotests/test_virtual_desktops.cpp new file mode 100644 index 0000000000..ec25c18246 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/test_virtual_desktops.cpp @@ -0,0 +1,624 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "input.h" +#include "virtualdesktops.h" +// KDE +#include + +#include +#include +#include + +namespace KWin +{ + +InputRedirection *InputRedirection::s_self = nullptr; + +void InputRedirection::registerAxisShortcut(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis, QAction *action) +{ +} + +void InputRedirection::registerTouchpadSwipeShortcut(SwipeDirection direction, uint32_t fingerCount, QAction *onUp, std::function progressCallback) +{ +} + +void InputRedirection::registerTouchpadPinchShortcut(PinchDirection direction, uint32_t fingerCount, QAction *onUp, std::function progressCallback) +{ +} + +void InputRedirection::registerTouchscreenSwipeShortcut(SwipeDirection direction, uint32_t fingerCount, QAction *action, std::function progressCallback) +{ +} + +} + +Q_DECLARE_METATYPE(Qt::Orientation) + +using namespace KWin; + +class TestVirtualDesktops : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void init(); + void cleanup(); + void count_data(); + void count(); + void navigationWrapsAround_data(); + void navigationWrapsAround(); + void current_data(); + void current(); + void currentChangeOnCountChange_data(); + void currentChangeOnCountChange(); + void next_data(); + void next(); + void previous_data(); + void previous(); + void left_data(); + void left(); + void right_data(); + void right(); + void above_data(); + void above(); + void below_data(); + void below(); + void updateGrid_data(); + void updateGrid(); + void updateLayout_data(); + void updateLayout(); + void name_data(); + void name(); + void switchToShortcuts(); + void changeRows(); + void load(); + void save(); + +private: + void addDirectionColumns(); + void testDirection(const QString &actionName, VirtualDesktopManager::Direction direction); +}; + +void TestVirtualDesktops::init() +{ + VirtualDesktopManager::create(); +} + +void TestVirtualDesktops::cleanup() +{ + delete VirtualDesktopManager::self(); +} + +static const uint s_countInitValue = 2; + +void TestVirtualDesktops::count_data() +{ + QTest::addColumn("request"); + QTest::addColumn("result"); + QTest::addColumn("signal"); + QTest::addColumn("removedSignal"); + + QTest::newRow("Minimum") << (uint)1 << (uint)1 << true << true; + QTest::newRow("Below Minimum") << (uint)0 << (uint)1 << true << true; + QTest::newRow("Normal Value") << (uint)10 << (uint)10 << true << false; + QTest::newRow("Maximum") << VirtualDesktopManager::maximum() << VirtualDesktopManager::maximum() << true << false; + QTest::newRow("Above Maximum") << VirtualDesktopManager::maximum() + 1 << VirtualDesktopManager::maximum() << true << false; + QTest::newRow("Unchanged") << s_countInitValue << s_countInitValue << false << false; +} + +void TestVirtualDesktops::count() +{ + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + QCOMPARE(vds->count(), (uint)0); + // start with a useful desktop count + vds->setCount(s_countInitValue); + + QSignalSpy spy(vds, &VirtualDesktopManager::countChanged); + QSignalSpy desktopsRemoved(vds, &VirtualDesktopManager::desktopRemoved); + + auto vdToRemove = vds->desktops().last(); + + QFETCH(uint, request); + QFETCH(uint, result); + QFETCH(bool, signal); + QFETCH(bool, removedSignal); + vds->setCount(request); + QCOMPARE(vds->count(), result); + QCOMPARE(spy.isEmpty(), !signal); + if (!spy.isEmpty()) { + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.count(), 2); + QCOMPARE(arguments.at(0).typeId(), QMetaType::UInt); + QCOMPARE(arguments.at(1).typeId(), QMetaType::UInt); + QCOMPARE(arguments.at(0).toUInt(), s_countInitValue); + QCOMPARE(arguments.at(1).toUInt(), result); + } + QCOMPARE(desktopsRemoved.isEmpty(), !removedSignal); + if (!desktopsRemoved.isEmpty()) { + QList arguments = desktopsRemoved.takeFirst(); + QCOMPARE(arguments.count(), 1); + QCOMPARE(arguments.at(0).value(), vdToRemove); + } +} + +void TestVirtualDesktops::navigationWrapsAround_data() +{ + QTest::addColumn("init"); + QTest::addColumn("request"); + QTest::addColumn("result"); + QTest::addColumn("signal"); + + QTest::newRow("enable") << false << true << true << true; + QTest::newRow("disable") << true << false << false << true; + QTest::newRow("keep enabled") << true << true << true << false; + QTest::newRow("keep disabled") << false << false << false << false; +} + +void TestVirtualDesktops::navigationWrapsAround() +{ + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + QCOMPARE(vds->isNavigationWrappingAround(), false); + QFETCH(bool, init); + QFETCH(bool, request); + QFETCH(bool, result); + QFETCH(bool, signal); + + // set to init value + vds->setNavigationWrappingAround(init); + QCOMPARE(vds->isNavigationWrappingAround(), init); + + QSignalSpy spy(vds, &VirtualDesktopManager::navigationWrappingAroundChanged); + vds->setNavigationWrappingAround(request); + QCOMPARE(vds->isNavigationWrappingAround(), result); + QCOMPARE(spy.isEmpty(), !signal); +} + +void TestVirtualDesktops::current_data() +{ + QTest::addColumn("count"); + QTest::addColumn("init"); + QTest::addColumn("request"); + QTest::addColumn("result"); + QTest::addColumn("signal"); + + QTest::newRow("lower") << (uint)4 << (uint)3 << (uint)2 << (uint)2 << true; + QTest::newRow("higher") << (uint)4 << (uint)1 << (uint)2 << (uint)2 << true; + QTest::newRow("maximum") << (uint)4 << (uint)1 << (uint)4 << (uint)4 << true; + QTest::newRow("above maximum") << (uint)4 << (uint)1 << (uint)5 << (uint)1 << false; + QTest::newRow("minimum") << (uint)4 << (uint)2 << (uint)1 << (uint)1 << true; + QTest::newRow("below minimum") << (uint)4 << (uint)2 << (uint)0 << (uint)2 << false; + QTest::newRow("unchanged") << (uint)4 << (uint)2 << (uint)2 << (uint)2 << false; +} + +void TestVirtualDesktops::current() +{ + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + QCOMPARE(vds->current(), (uint)0); + QFETCH(uint, count); + QFETCH(uint, init); + vds->setCount(count); + vds->setCurrent(init); + QCOMPARE(vds->current(), init); + + QSignalSpy spy(vds, &VirtualDesktopManager::currentChanged); + + QFETCH(uint, request); + QFETCH(uint, result); + QFETCH(bool, signal); + QCOMPARE(vds->setCurrent(request), signal); + QCOMPARE(vds->current(), result); + QCOMPARE(spy.isEmpty(), !signal); + if (!spy.isEmpty()) { + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.count(), 2); + + VirtualDesktop *previous = arguments.at(0).value(); + QCOMPARE(previous->x11DesktopNumber(), init); + + VirtualDesktop *current = arguments.at(1).value(); + QCOMPARE(current->x11DesktopNumber(), result); + } +} + +void TestVirtualDesktops::currentChangeOnCountChange_data() +{ + QTest::addColumn("initCount"); + QTest::addColumn("initCurrent"); + QTest::addColumn("request"); + QTest::addColumn("current"); + QTest::addColumn("signal"); + + QTest::newRow("increment") << (uint)4 << (uint)2 << (uint)5 << (uint)2 << false; + QTest::newRow("increment on last") << (uint)4 << (uint)4 << (uint)5 << (uint)4 << false; + QTest::newRow("decrement") << (uint)4 << (uint)2 << (uint)3 << (uint)2 << false; + QTest::newRow("decrement on second last") << (uint)4 << (uint)3 << (uint)3 << (uint)3 << false; + QTest::newRow("decrement on last") << (uint)4 << (uint)4 << (uint)3 << (uint)3 << true; + QTest::newRow("multiple decrement") << (uint)4 << (uint)2 << (uint)1 << (uint)1 << true; +} + +void TestVirtualDesktops::currentChangeOnCountChange() +{ + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + QFETCH(uint, initCount); + QFETCH(uint, initCurrent); + vds->setCount(initCount); + vds->setCurrent(initCurrent); + + QSignalSpy spy(vds, &VirtualDesktopManager::currentChanged); + + QFETCH(uint, request); + QFETCH(uint, current); + QFETCH(bool, signal); + + vds->setCount(request); + QCOMPARE(vds->current(), current); + QCOMPARE(spy.isEmpty(), !signal); +} + +void TestVirtualDesktops::addDirectionColumns() +{ + QTest::addColumn("initCount"); + QTest::addColumn("initCurrent"); + QTest::addColumn("wrap"); + QTest::addColumn("result"); +} + +void TestVirtualDesktops::testDirection(const QString &actionName, VirtualDesktopManager::Direction direction) +{ + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + QFETCH(uint, initCount); + QFETCH(uint, initCurrent); + vds->setCount(initCount); + vds->setCurrent(initCurrent); + + QFETCH(bool, wrap); + QFETCH(uint, result); + QCOMPARE(vds->inDirection(nullptr, direction, wrap)->x11DesktopNumber(), result); + + vds->setNavigationWrappingAround(wrap); + vds->initShortcuts(); + QAction *action = vds->findChild(actionName); + QVERIFY(action); + action->trigger(); + QCOMPARE(vds->current(), result); + QCOMPARE(vds->inDirection(initCurrent, direction, wrap), result); +} + +void TestVirtualDesktops::next_data() +{ + addDirectionColumns(); + + QTest::newRow("one desktop, wrap") << (uint)1 << (uint)1 << true << (uint)1; + QTest::newRow("one desktop, no wrap") << (uint)1 << (uint)1 << false << (uint)1; + QTest::newRow("desktops, wrap") << (uint)4 << (uint)1 << true << (uint)2; + QTest::newRow("desktops, no wrap") << (uint)4 << (uint)1 << false << (uint)2; + QTest::newRow("desktops at end, wrap") << (uint)4 << (uint)4 << true << (uint)1; + QTest::newRow("desktops at end, no wrap") << (uint)4 << (uint)4 << false << (uint)4; +} + +void TestVirtualDesktops::next() +{ + testDirection(QStringLiteral("Switch to Next Desktop"), VirtualDesktopManager::Direction::Next); +} + +void TestVirtualDesktops::previous_data() +{ + addDirectionColumns(); + + QTest::newRow("one desktop, wrap") << (uint)1 << (uint)1 << true << (uint)1; + QTest::newRow("one desktop, no wrap") << (uint)1 << (uint)1 << false << (uint)1; + QTest::newRow("desktops, wrap") << (uint)4 << (uint)3 << true << (uint)2; + QTest::newRow("desktops, no wrap") << (uint)4 << (uint)3 << false << (uint)2; + QTest::newRow("desktops at start, wrap") << (uint)4 << (uint)1 << true << (uint)4; + QTest::newRow("desktops at start, no wrap") << (uint)4 << (uint)1 << false << (uint)1; +} + +void TestVirtualDesktops::previous() +{ + testDirection(QStringLiteral("Switch to Previous Desktop"), VirtualDesktopManager::Direction::Previous); +} + +void TestVirtualDesktops::left_data() +{ + addDirectionColumns(); + QTest::newRow("one desktop, wrap") << (uint)1 << (uint)1 << true << (uint)1; + QTest::newRow("one desktop, no wrap") << (uint)1 << (uint)1 << false << (uint)1; + QTest::newRow("desktops, wrap, 1st row") << (uint)4 << (uint)2 << true << (uint)1; + QTest::newRow("desktops, no wrap, 1st row") << (uint)4 << (uint)2 << false << (uint)1; + QTest::newRow("desktops, wrap, 2nd row") << (uint)4 << (uint)4 << true << (uint)3; + QTest::newRow("desktops, no wrap, 2nd row") << (uint)4 << (uint)4 << false << (uint)3; + + QTest::newRow("desktops at start, wrap, 1st row") << (uint)4 << (uint)1 << true << (uint)2; + QTest::newRow("desktops at start, no wrap, 1st row") << (uint)4 << (uint)1 << false << (uint)1; + QTest::newRow("desktops at start, wrap, 2nd row") << (uint)4 << (uint)3 << true << (uint)4; + QTest::newRow("desktops at start, no wrap, 2nd row") << (uint)4 << (uint)3 << false << (uint)3; + + QTest::newRow("non symmetric, start") << (uint)5 << (uint)5 << false << (uint)4; + QTest::newRow("non symmetric, end, no wrap") << (uint)5 << (uint)4 << false << (uint)4; + QTest::newRow("non symmetric, end, wrap") << (uint)5 << (uint)4 << true << (uint)5; +} + +void TestVirtualDesktops::left() +{ + testDirection(QStringLiteral("Switch One Desktop to the Left"), VirtualDesktopManager::Direction::Left); +} + +void TestVirtualDesktops::right_data() +{ + addDirectionColumns(); + QTest::newRow("one desktop, wrap") << (uint)1 << (uint)1 << true << (uint)1; + QTest::newRow("one desktop, no wrap") << (uint)1 << (uint)1 << false << (uint)1; + QTest::newRow("desktops, wrap, 1st row") << (uint)4 << (uint)1 << true << (uint)2; + QTest::newRow("desktops, no wrap, 1st row") << (uint)4 << (uint)1 << false << (uint)2; + QTest::newRow("desktops, wrap, 2nd row") << (uint)4 << (uint)3 << true << (uint)4; + QTest::newRow("desktops, no wrap, 2nd row") << (uint)4 << (uint)3 << false << (uint)4; + + QTest::newRow("desktops at start, wrap, 1st row") << (uint)4 << (uint)2 << true << (uint)1; + QTest::newRow("desktops at start, no wrap, 1st row") << (uint)4 << (uint)2 << false << (uint)2; + QTest::newRow("desktops at start, wrap, 2nd row") << (uint)4 << (uint)4 << true << (uint)3; + QTest::newRow("desktops at start, no wrap, 2nd row") << (uint)4 << (uint)4 << false << (uint)4; + + QTest::newRow("non symmetric, start") << (uint)5 << (uint)4 << false << (uint)5; + QTest::newRow("non symmetric, end, no wrap") << (uint)5 << (uint)5 << false << (uint)5; + QTest::newRow("non symmetric, end, wrap") << (uint)5 << (uint)5 << true << (uint)4; +} + +void TestVirtualDesktops::right() +{ + testDirection(QStringLiteral("Switch One Desktop to the Right"), VirtualDesktopManager::Direction::Right); +} + +void TestVirtualDesktops::above_data() +{ + addDirectionColumns(); + QTest::newRow("one desktop, wrap") << (uint)1 << (uint)1 << true << (uint)1; + QTest::newRow("one desktop, no wrap") << (uint)1 << (uint)1 << false << (uint)1; + QTest::newRow("desktops, wrap, 1st column") << (uint)4 << (uint)3 << true << (uint)1; + QTest::newRow("desktops, no wrap, 1st column") << (uint)4 << (uint)3 << false << (uint)1; + QTest::newRow("desktops, wrap, 2nd column") << (uint)4 << (uint)4 << true << (uint)2; + QTest::newRow("desktops, no wrap, 2nd column") << (uint)4 << (uint)4 << false << (uint)2; + + QTest::newRow("desktops at start, wrap, 1st column") << (uint)4 << (uint)1 << true << (uint)3; + QTest::newRow("desktops at start, no wrap, 1st column") << (uint)4 << (uint)1 << false << (uint)1; + QTest::newRow("desktops at start, wrap, 2nd column") << (uint)4 << (uint)2 << true << (uint)4; + QTest::newRow("desktops at start, no wrap, 2nd column") << (uint)4 << (uint)2 << false << (uint)2; +} + +void TestVirtualDesktops::above() +{ + testDirection(QStringLiteral("Switch One Desktop Up"), VirtualDesktopManager::Direction::Up); +} + +void TestVirtualDesktops::below_data() +{ + addDirectionColumns(); + QTest::newRow("one desktop, wrap") << (uint)1 << (uint)1 << true << (uint)1; + QTest::newRow("one desktop, no wrap") << (uint)1 << (uint)1 << false << (uint)1; + QTest::newRow("desktops, wrap, 1st column") << (uint)4 << (uint)1 << true << (uint)3; + QTest::newRow("desktops, no wrap, 1st column") << (uint)4 << (uint)1 << false << (uint)3; + QTest::newRow("desktops, wrap, 2nd column") << (uint)4 << (uint)2 << true << (uint)4; + QTest::newRow("desktops, no wrap, 2nd column") << (uint)4 << (uint)2 << false << (uint)4; + + QTest::newRow("desktops at start, wrap, 1st column") << (uint)4 << (uint)3 << true << (uint)1; + QTest::newRow("desktops at start, no wrap, 1st column") << (uint)4 << (uint)3 << false << (uint)3; + QTest::newRow("desktops at start, wrap, 2nd column") << (uint)4 << (uint)4 << true << (uint)2; + QTest::newRow("desktops at start, no wrap, 2nd column") << (uint)4 << (uint)4 << false << (uint)4; +} + +void TestVirtualDesktops::below() +{ + testDirection(QStringLiteral("Switch One Desktop Down"), VirtualDesktopManager::Direction::Down); +} + +void TestVirtualDesktops::updateGrid_data() +{ + QTest::addColumn("initCount"); + QTest::addColumn("size"); + QTest::addColumn("coords"); + QTest::addColumn("desktop"); + + QTest::newRow("one desktop, h") << (uint)1 << QSize(1, 1) << QPoint(0, 0) << (uint)1; + QTest::newRow("one desktop, h, 0") << (uint)1 << QSize(1, 1) << QPoint(1, 0) << (uint)0; + + QTest::newRow("two desktops, h, 1") << (uint)2 << QSize(2, 1) << QPoint(0, 0) << (uint)1; + QTest::newRow("two desktops, h, 2") << (uint)2 << QSize(2, 1) << QPoint(1, 0) << (uint)2; + QTest::newRow("two desktops, h, 3") << (uint)2 << QSize(2, 1) << QPoint(0, 1) << (uint)0; + QTest::newRow("two desktops, h, 4") << (uint)2 << QSize(2, 1) << QPoint(2, 0) << (uint)0; + + QTest::newRow("four desktops, h, one row, 1") << (uint)4 << QSize(4, 1) << QPoint(0, 0) << (uint)1; + QTest::newRow("four desktops, h, one row, 2") << (uint)4 << QSize(4, 1) << QPoint(1, 0) << (uint)2; + QTest::newRow("four desktops, h, one row, 3") << (uint)4 << QSize(4, 1) << QPoint(2, 0) << (uint)3; + QTest::newRow("four desktops, h, one row, 4") << (uint)4 << QSize(4, 1) << QPoint(3, 0) << (uint)4; + + QTest::newRow("four desktops, h, grid, 1") << (uint)4 << QSize(2, 2) << QPoint(0, 0) << (uint)1; + QTest::newRow("four desktops, h, grid, 2") << (uint)4 << QSize(2, 2) << QPoint(1, 0) << (uint)2; + QTest::newRow("four desktops, h, grid, 3") << (uint)4 << QSize(2, 2) << QPoint(0, 1) << (uint)3; + QTest::newRow("four desktops, h, grid, 4") << (uint)4 << QSize(2, 2) << QPoint(1, 1) << (uint)4; + QTest::newRow("four desktops, h, grid, 0/3") << (uint)4 << QSize(2, 2) << QPoint(0, 3) << (uint)0; + + QTest::newRow("three desktops, h, grid, 1") << (uint)3 << QSize(2, 2) << QPoint(0, 0) << (uint)1; + QTest::newRow("three desktops, h, grid, 2") << (uint)3 << QSize(2, 2) << QPoint(1, 0) << (uint)2; + QTest::newRow("three desktops, h, grid, 3") << (uint)3 << QSize(2, 2) << QPoint(0, 1) << (uint)3; + QTest::newRow("three desktops, h, grid, 4") << (uint)3 << QSize(2, 2) << QPoint(1, 1) << (uint)0; +} + +void TestVirtualDesktops::updateGrid() +{ + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + QFETCH(uint, initCount); + vds->setCount(initCount); + VirtualDesktopGrid grid; + + QFETCH(QSize, size); + QCOMPARE(vds->desktops().count(), int(initCount)); + grid.update(size, vds->desktops()); + QCOMPARE(grid.size(), size); + QCOMPARE(grid.width(), size.width()); + QCOMPARE(grid.height(), size.height()); + QFETCH(QPoint, coords); + QFETCH(uint, desktop); + QCOMPARE(grid.at(coords), vds->desktopForX11Id(desktop)); + if (desktop != 0) { + QCOMPARE(grid.gridCoords(desktop), coords); + } +} + +void TestVirtualDesktops::updateLayout_data() +{ + QTest::addColumn("desktop"); + QTest::addColumn("result"); + + QTest::newRow("01") << (uint)1 << QSize(1, 1); + QTest::newRow("02") << (uint)2 << QSize(1, 2); + QTest::newRow("03") << (uint)3 << QSize(2, 2); + QTest::newRow("04") << (uint)4 << QSize(2, 2); + QTest::newRow("05") << (uint)5 << QSize(3, 2); + QTest::newRow("06") << (uint)6 << QSize(3, 2); + QTest::newRow("07") << (uint)7 << QSize(4, 2); + QTest::newRow("08") << (uint)8 << QSize(4, 2); + QTest::newRow("09") << (uint)9 << QSize(5, 2); + QTest::newRow("10") << (uint)10 << QSize(5, 2); + QTest::newRow("11") << (uint)11 << QSize(6, 2); + QTest::newRow("12") << (uint)12 << QSize(6, 2); + QTest::newRow("13") << (uint)13 << QSize(7, 2); + QTest::newRow("14") << (uint)14 << QSize(7, 2); + QTest::newRow("15") << (uint)15 << QSize(8, 2); + QTest::newRow("16") << (uint)16 << QSize(8, 2); + QTest::newRow("17") << (uint)17 << QSize(9, 2); + QTest::newRow("18") << (uint)18 << QSize(9, 2); + QTest::newRow("19") << (uint)19 << QSize(10, 2); + QTest::newRow("20") << (uint)20 << QSize(10, 2); +} + +void TestVirtualDesktops::updateLayout() +{ + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + QSignalSpy spy(vds, &VirtualDesktopManager::layoutChanged); + // call update layout - implicitly through setCount + QFETCH(uint, desktop); + QFETCH(QSize, result); + vds->setCount(desktop); + QCOMPARE(vds->grid().size(), result); + QCOMPARE(spy.count(), 1); + const QVariantList &arguments = spy.at(0); + QCOMPARE(arguments.at(0).toInt(), result.width()); + QCOMPARE(arguments.at(1).toInt(), result.height()); + // calling update layout again should not change anything + vds->updateLayout(); + QCOMPARE(vds->grid().size(), result); + QCOMPARE(spy.count(), 2); + const QVariantList &arguments2 = spy.at(1); + QCOMPARE(arguments2.at(0).toInt(), result.width()); + QCOMPARE(arguments2.at(1).toInt(), result.height()); +} + +void TestVirtualDesktops::name_data() +{ + QTest::addColumn("initCount"); + QTest::addColumn("desktop"); + QTest::addColumn("desktopName"); + + QTest::newRow("desktop 1") << (uint)5 << (uint)1 << "Desktop 1"; + QTest::newRow("desktop 2") << (uint)5 << (uint)2 << "Desktop 2"; + QTest::newRow("desktop 3") << (uint)5 << (uint)3 << "Desktop 3"; + QTest::newRow("desktop 4") << (uint)5 << (uint)4 << "Desktop 4"; + QTest::newRow("desktop 5") << (uint)5 << (uint)5 << "Desktop 5"; +} + +void TestVirtualDesktops::name() +{ + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + QFETCH(uint, initCount); + vds->setCount(initCount); + QFETCH(uint, desktop); + + const VirtualDesktop *vd = vds->desktopForX11Id(desktop); + QTEST(vd->name(), "desktopName"); +} + +void TestVirtualDesktops::switchToShortcuts() +{ + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + vds->setCount(vds->maximum()); + vds->setCurrent(vds->maximum()); + QCOMPARE(vds->current(), vds->maximum()); + vds->initShortcuts(); + const QString toDesktop = QStringLiteral("Switch to Desktop %1"); + for (uint i = 1; i <= vds->maximum(); ++i) { + const QString desktop(toDesktop.arg(i)); + QAction *action = vds->findChild(desktop); + QVERIFY2(action, desktop.toUtf8().constData()); + action->trigger(); + QCOMPARE(vds->current(), i); + } + // invoke switchTo not from a QAction + QMetaObject::invokeMethod(vds, "slotSwitchTo"); + // should still be on max + QCOMPARE(vds->current(), vds->maximum()); +} + +void TestVirtualDesktops::changeRows() +{ + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + + vds->setCount(4); + vds->setRows(4); + QCOMPARE(vds->rows(), 4); + + vds->setRows(5); + QCOMPARE(vds->rows(), 4); + + vds->setCount(2); + QCOMPARE(vds->rows(), 2); +} + +void TestVirtualDesktops::load() +{ + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + // no config yet, load should not change anything + vds->load(); + QCOMPARE(vds->count(), (uint)0); + // empty config should create one desktop + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + vds->setConfig(config); + vds->load(); + QCOMPARE(vds->count(), (uint)1); + // setting a sensible number + config->group(QStringLiteral("Desktops")).writeEntry("Number", 4); + vds->load(); + QCOMPARE(vds->count(), (uint)4); + + // setting the config value and reloading should update + config->group(QStringLiteral("Desktops")).writeEntry("Number", 5); + vds->load(); + QCOMPARE(vds->count(), (uint)5); +} + +void TestVirtualDesktops::save() +{ + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + vds->setCount(4); + // no config yet, just to ensure it actually works + vds->save(); + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + vds->setConfig(config); + + // now save should create the group "Desktops" + QCOMPARE(config->hasGroup(QStringLiteral("Desktops")), false); + vds->save(); + QCOMPARE(config->hasGroup(QStringLiteral("Desktops")), true); + KConfigGroup desktops = config->group(QStringLiteral("Desktops")); + QCOMPARE(desktops.readEntry("Number", 1), 4); + QCOMPARE(desktops.hasKey("Name_1"), false); + QCOMPARE(desktops.hasKey("Name_2"), false); + QCOMPARE(desktops.hasKey("Name_3"), false); + QCOMPARE(desktops.hasKey("Name_4"), false); +} + +QTEST_MAIN(TestVirtualDesktops) +#include "test_virtual_desktops.moc" diff --git a/local/recipes/kde/kwin/source/autotests/test_window_paint_data.cpp b/local/recipes/kde/kwin/source/autotests/test_window_paint_data.cpp new file mode 100644 index 0000000000..2098e475a3 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/test_window_paint_data.cpp @@ -0,0 +1,179 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "effect/effecthandler.h" + +#include "virtualdesktops.h" + +#include +#include +#include + +#include + +using namespace KWin; + +class TestWindowPaintData : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testCtor(); + void testCopyCtor(); + void testOperatorMultiplyAssign(); + void testOperatorPlus(); + void testMultiplyOpacity(); + void testMultiplySaturation(); + void testMultiplyBrightness(); +}; + +void TestWindowPaintData::testCtor() +{ + WindowPaintData data; + QCOMPARE(data.xScale(), 1.0); + QCOMPARE(data.yScale(), 1.0); + QCOMPARE(data.zScale(), 1.0); + QCOMPARE(data.xTranslation(), 0.0); + QCOMPARE(data.yTranslation(), 0.0); + QCOMPARE(data.zTranslation(), 0.0); + QCOMPARE(data.translation(), QVector3D()); + QCOMPARE(data.rotationAngle(), 0.0); + QCOMPARE(data.rotationOrigin(), QVector3D()); + QCOMPARE(data.rotationAxis(), QVector3D(0.0, 0.0, 1.0)); + QCOMPARE(data.opacity(), 1.0); + QCOMPARE(data.brightness(), 1.0); + QCOMPARE(data.saturation(), 1.0); +} + +void TestWindowPaintData::testCopyCtor() +{ + WindowPaintData data; + WindowPaintData data2(data); + // no value had been changed + QCOMPARE(data2.xScale(), 1.0); + QCOMPARE(data2.yScale(), 1.0); + QCOMPARE(data2.zScale(), 1.0); + QCOMPARE(data2.xTranslation(), 0.0); + QCOMPARE(data2.yTranslation(), 0.0); + QCOMPARE(data2.zTranslation(), 0.0); + QCOMPARE(data2.translation(), QVector3D()); + QCOMPARE(data2.rotationAngle(), 0.0); + QCOMPARE(data2.rotationOrigin(), QVector3D()); + QCOMPARE(data2.rotationAxis(), QVector3D(0.0, 0.0, 1.0)); + QCOMPARE(data2.opacity(), 1.0); + QCOMPARE(data2.brightness(), 1.0); + QCOMPARE(data2.saturation(), 1.0); + + data2.setScale(QVector3D(0.5, 2.0, 3.0)); + data2.translate(0.5, 2.0, 3.0); + data2.setRotationAngle(45.0); + data2.setRotationOrigin(QVector3D(1.0, 2.0, 3.0)); + data2.setRotationAxis(QVector3D(1.0, 1.0, 0.0)); + data2.setOpacity(0.1); + data2.setBrightness(0.3); + data2.setSaturation(0.4); + + WindowPaintData data3(data2); + QCOMPARE(data3.xScale(), 0.5); + QCOMPARE(data3.yScale(), 2.0); + QCOMPARE(data3.zScale(), 3.0); + QCOMPARE(data3.xTranslation(), 0.5); + QCOMPARE(data3.yTranslation(), 2.0); + QCOMPARE(data3.zTranslation(), 3.0); + QCOMPARE(data3.translation(), QVector3D(0.5, 2.0, 3.0)); + QCOMPARE(data3.rotationAngle(), 45.0); + QCOMPARE(data3.rotationOrigin(), QVector3D(1.0, 2.0, 3.0)); + QCOMPARE(data3.rotationAxis(), QVector3D(1.0, 1.0, 0.0)); + QCOMPARE(data3.opacity(), 0.1); + QCOMPARE(data3.brightness(), 0.3); + QCOMPARE(data3.saturation(), 0.4); +} + +void TestWindowPaintData::testOperatorMultiplyAssign() +{ + WindowPaintData data; + // without anything set, it's 1.0 on all axis + QCOMPARE(data.xScale(), 1.0); + QCOMPARE(data.yScale(), 1.0); + QCOMPARE(data.zScale(), 1.0); + // multiplying by a factor should set all components + data *= 2.0; + QCOMPARE(data.xScale(), 2.0); + QCOMPARE(data.yScale(), 2.0); + QCOMPARE(data.zScale(), 2.0); + // multiplying by a vector2D should set x and y components + data *= QVector2D(2.0, 3.0); + QCOMPARE(data.xScale(), 4.0); + QCOMPARE(data.yScale(), 6.0); + QCOMPARE(data.zScale(), 2.0); + // multiplying by a vector3d should set all components + data *= QVector3D(0.5, 1.5, 2.0); + QCOMPARE(data.xScale(), 2.0); + QCOMPARE(data.yScale(), 9.0); + QCOMPARE(data.zScale(), 4.0); +} + +void TestWindowPaintData::testOperatorPlus() +{ + WindowPaintData data; + QCOMPARE(data.xTranslation(), 0.0); + QCOMPARE(data.yTranslation(), 0.0); + QCOMPARE(data.zTranslation(), 0.0); + QCOMPARE(data.translation(), QVector3D()); + // test with point + data += QPoint(1, 2); + QCOMPARE(data.translation(), QVector3D(1.0, 2.0, 0.0)); + // test with pointf + data += QPointF(0.5, 0.75); + QCOMPARE(data.translation(), QVector3D(1.5, 2.75, 0.0)); + // test with QVector2D + data += QVector2D(0.25, 1.5); + QCOMPARE(data.translation(), QVector3D(1.75, 4.25, 0.0)); + // test with QVector3D + data += QVector3D(1.0, 2.0, 3.5); + QCOMPARE(data.translation(), QVector3D(2.75, 6.25, 3.5)); +} + +void TestWindowPaintData::testMultiplyBrightness() +{ + WindowPaintData data; + QCOMPARE(0.2, data.multiplyBrightness(0.2)); + QCOMPARE(0.2, data.brightness()); + QCOMPARE(0.6, data.multiplyBrightness(3.0)); + QCOMPARE(0.6, data.brightness()); + // just for safety + QCOMPARE(1.0, data.opacity()); + QCOMPARE(1.0, data.saturation()); +} + +void TestWindowPaintData::testMultiplyOpacity() +{ + WindowPaintData data; + QCOMPARE(0.2, data.multiplyOpacity(0.2)); + QCOMPARE(0.2, data.opacity()); + QCOMPARE(0.6, data.multiplyOpacity(3.0)); + QCOMPARE(0.6, data.opacity()); + // just for safety + QCOMPARE(1.0, data.brightness()); + QCOMPARE(1.0, data.saturation()); +} + +void TestWindowPaintData::testMultiplySaturation() +{ + WindowPaintData data; + QCOMPARE(0.2, data.multiplySaturation(0.2)); + QCOMPARE(0.2, data.saturation()); + QCOMPARE(0.6, data.multiplySaturation(3.0)); + QCOMPARE(0.6, data.saturation()); + // just for safety + QCOMPARE(1.0, data.brightness()); + QCOMPARE(1.0, data.opacity()); +} + +QTEST_MAIN(TestWindowPaintData) +#include "test_window_paint_data.moc" diff --git a/local/recipes/kde/kwin/source/autotests/test_xcb_size_hints.cpp b/local/recipes/kde/kwin/source/autotests/test_xcb_size_hints.cpp new file mode 100644 index 0000000000..f5569437bc --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/test_xcb_size_hints.cpp @@ -0,0 +1,362 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "testutils.h" +// KWin +#include "utils/xcbutils.h" +// Qt +#include +#include +#include +#include +// xcb +#include +#include + +using namespace KWin; + +class TestXcbSizeHints : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testSizeHints_data(); + void testSizeHints(); + void testSizeHintsEmpty(); + void testSizeHintsNotSet(); + void geometryHintsBeforeInit(); + void geometryHintsBeforeRead(); + +private: + Xcb::Window m_testWindow; +}; + +void TestXcbSizeHints::initTestCase() +{ + qApp->setProperty("x11RootWindow", QVariant::fromValue(QX11Info::appRootWindow())); + qApp->setProperty("x11Connection", QVariant::fromValue(QX11Info::connection())); +} + +void TestXcbSizeHints::init() +{ + const uint32_t values[] = {true}; + m_testWindow.create(Rect(0, 0, 10, 10), XCB_WINDOW_CLASS_INPUT_ONLY, XCB_CW_OVERRIDE_REDIRECT, values); + QVERIFY(m_testWindow.isValid()); +} + +void TestXcbSizeHints::cleanup() +{ + m_testWindow.reset(); +} + +void TestXcbSizeHints::testSizeHints_data() +{ + // set + QTest::addColumn("userPos"); + QTest::addColumn("userSize"); + QTest::addColumn("minSize"); + QTest::addColumn("maxSize"); + QTest::addColumn("resizeInc"); + QTest::addColumn("minAspect"); + QTest::addColumn("maxAspect"); + QTest::addColumn("baseSize"); + QTest::addColumn("gravity"); + // read for SizeHints + QTest::addColumn("expectedFlags"); + QTest::addColumn("expectedPad0"); + QTest::addColumn("expectedPad1"); + QTest::addColumn("expectedPad2"); + QTest::addColumn("expectedPad3"); + QTest::addColumn("expectedMinWidth"); + QTest::addColumn("expectedMinHeight"); + QTest::addColumn("expectedMaxWidth"); + QTest::addColumn("expectedMaxHeight"); + QTest::addColumn("expectedWidthInc"); + QTest::addColumn("expectedHeightInc"); + QTest::addColumn("expectedMinAspectNum"); + QTest::addColumn("expectedMinAspectDen"); + QTest::addColumn("expectedMaxAspectNum"); + QTest::addColumn("expectedMaxAspectDen"); + QTest::addColumn("expectedBaseWidth"); + QTest::addColumn("expectedBaseHeight"); + // read for GeometryHints + QTest::addColumn("expectedMinSize"); + QTest::addColumn("expectedMaxSize"); + QTest::addColumn("expectedResizeIncrements"); + QTest::addColumn("expectedMinAspect"); + QTest::addColumn("expectedMaxAspect"); + QTest::addColumn("expectedBaseSize"); + QTest::addColumn("expectedGravity"); + + QTest::newRow("userPos") << QPoint(1, 2) << QSize() << QSize() << QSize() << QSize() << QSize() << QSize() << QSize() << 0 + << 1 << 1 << 2 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 + << QSize(0, 0) << QSize(INT_MAX, INT_MAX) << QSize(1, 1) << QSize(1, INT_MAX) << QSize(INT_MAX, 1) << QSize(0, 0) << qint32(XCB_GRAVITY_NORTH_WEST); + QTest::newRow("userSize") << QPoint() << QSize(1, 2) << QSize() << QSize() << QSize() << QSize() << QSize() << QSize() << 0 + << 2 << 0 << 0 << 1 << 2 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 + << QSize(0, 0) << QSize(INT_MAX, INT_MAX) << QSize(1, 1) << QSize(1, INT_MAX) << QSize(INT_MAX, 1) << QSize(0, 0) << qint32(XCB_GRAVITY_NORTH_WEST); + QTest::newRow("minSize") << QPoint() << QSize() << QSize(1, 2) << QSize() << QSize() << QSize() << QSize() << QSize() << 0 + << 16 << 0 << 0 << 0 << 0 << 1 << 2 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 + << QSize(1, 2) << QSize(INT_MAX, INT_MAX) << QSize(1, 1) << QSize(1, INT_MAX) << QSize(INT_MAX, 1) << QSize(0, 0) << qint32(XCB_GRAVITY_NORTH_WEST); + QTest::newRow("maxSize") << QPoint() << QSize() << QSize() << QSize(1, 2) << QSize() << QSize() << QSize() << QSize() << 0 + << 32 << 0 << 0 << 0 << 0 << 0 << 0 << 1 << 2 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 + << QSize(0, 0) << QSize(1, 2) << QSize(1, 1) << QSize(1, INT_MAX) << QSize(INT_MAX, 1) << QSize(0, 0) << qint32(XCB_GRAVITY_NORTH_WEST); + QTest::newRow("maxSize0") << QPoint() << QSize() << QSize() << QSize(0, 0) << QSize() << QSize() << QSize() << QSize() << 0 + << 32 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 + << QSize(0, 0) << QSize(1, 1) << QSize(1, 1) << QSize(1, INT_MAX) << QSize(INT_MAX, 1) << QSize(0, 0) << qint32(XCB_GRAVITY_NORTH_WEST); + QTest::newRow("min/maxSize") << QPoint() << QSize() << QSize(1, 2) << QSize(3, 4) << QSize() << QSize() << QSize() << QSize() << 0 + << 48 << 0 << 0 << 0 << 0 << 1 << 2 << 3 << 4 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 + << QSize(1, 2) << QSize(3, 4) << QSize(1, 1) << QSize(1, INT_MAX) << QSize(INT_MAX, 1) << QSize(0, 0) << qint32(XCB_GRAVITY_NORTH_WEST); + QTest::newRow("resizeInc") << QPoint() << QSize() << QSize() << QSize() << QSize(1, 2) << QSize() << QSize() << QSize() << 0 + << 64 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 1 << 2 << 0 << 0 << 0 << 0 << 0 << 0 + << QSize(0, 0) << QSize(INT_MAX, INT_MAX) << QSize(1, 2) << QSize(1, INT_MAX) << QSize(INT_MAX, 1) << QSize(0, 0) << qint32(XCB_GRAVITY_NORTH_WEST); + QTest::newRow("resizeInc0") << QPoint() << QSize() << QSize() << QSize() << QSize(0, 0) << QSize() << QSize() << QSize() << 0 + << 64 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 + << QSize(0, 0) << QSize(INT_MAX, INT_MAX) << QSize(1, 1) << QSize(1, INT_MAX) << QSize(INT_MAX, 1) << QSize(0, 0) << qint32(XCB_GRAVITY_NORTH_WEST); + QTest::newRow("aspect") << QPoint() << QSize() << QSize() << QSize() << QSize() << QSize(1, 2) << QSize(3, 4) << QSize() << 0 + << 128 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 1 << 2 << 3 << 4 << 0 << 0 + << QSize(0, 0) << QSize(INT_MAX, INT_MAX) << QSize(1, 1) << QSize(1, 2) << QSize(3, 4) << QSize(0, 0) << qint32(XCB_GRAVITY_NORTH_WEST); + QTest::newRow("aspectDivision0") << QPoint() << QSize() << QSize() << QSize() << QSize() << QSize(1, 0) << QSize(3, 0) << QSize() << 0 + << 128 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 1 << 0 << 3 << 0 << 0 << 0 + << QSize(0, 0) << QSize(INT_MAX, INT_MAX) << QSize(1, 1) << QSize(1, 1) << QSize(3, 1) << QSize(0, 0) << qint32(XCB_GRAVITY_NORTH_WEST); + QTest::newRow("baseSize") << QPoint() << QSize() << QSize() << QSize() << QSize() << QSize() << QSize() << QSize(1, 2) << 0 + << 256 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 1 << 2 + << QSize(1, 2) << QSize(INT_MAX, INT_MAX) << QSize(1, 1) << QSize(1, INT_MAX) << QSize(INT_MAX, 1) << QSize(1, 2) << qint32(XCB_GRAVITY_NORTH_WEST); + QTest::newRow("gravity") << QPoint() << QSize() << QSize() << QSize() << QSize() << QSize() << QSize() << QSize() << qint32(XCB_GRAVITY_STATIC) + << 512 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 + << QSize(0, 0) << QSize(INT_MAX, INT_MAX) << QSize(1, 1) << QSize(1, INT_MAX) << QSize(INT_MAX, 1) << QSize(0, 0) << qint32(XCB_GRAVITY_STATIC); + QTest::newRow("all") << QPoint(1, 2) << QSize(3, 4) << QSize(5, 6) << QSize(7, 8) << QSize(9, 10) << QSize(11, 12) << QSize(13, 14) << QSize(15, 16) << 1 + << 1011 << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10 << 11 << 12 << 13 << 14 << 15 << 16 + << QSize(5, 6) << QSize(7, 8) << QSize(9, 10) << QSize(11, 12) << QSize(13, 14) << QSize(15, 16) << qint32(XCB_GRAVITY_NORTH_WEST); +} + +void TestXcbSizeHints::testSizeHints() +{ + xcb_size_hints_t hints{}; + QFETCH(QPoint, userPos); + if (!userPos.isNull()) { + xcb_icccm_size_hints_set_position(&hints, 1, userPos.x(), userPos.y()); + } + QFETCH(QSize, userSize); + if (userSize.isValid()) { + xcb_icccm_size_hints_set_size(&hints, 1, userSize.width(), userSize.height()); + } + QFETCH(QSize, minSize); + if (minSize.isValid()) { + xcb_icccm_size_hints_set_min_size(&hints, minSize.width(), minSize.height()); + } + QFETCH(QSize, maxSize); + if (maxSize.isValid()) { + xcb_icccm_size_hints_set_max_size(&hints, maxSize.width(), maxSize.height()); + } + QFETCH(QSize, resizeInc); + if (resizeInc.isValid()) { + xcb_icccm_size_hints_set_resize_inc(&hints, resizeInc.width(), resizeInc.height()); + } + QFETCH(QSize, minAspect); + QFETCH(QSize, maxAspect); + if (minAspect.isValid() && maxAspect.isValid()) { + xcb_icccm_size_hints_set_aspect(&hints, minAspect.width(), minAspect.height(), maxAspect.width(), maxAspect.height()); + } + QFETCH(QSize, baseSize); + if (baseSize.isValid()) { + xcb_icccm_size_hints_set_base_size(&hints, baseSize.width(), baseSize.height()); + } + QFETCH(qint32, gravity); + if (gravity != 0) { + xcb_icccm_size_hints_set_win_gravity(&hints, (xcb_gravity_t)gravity); + } + xcb_icccm_set_wm_normal_hints(QX11Info::connection(), m_testWindow, &hints); + xcb_flush(QX11Info::connection()); + + Xcb::GeometryHints geoHints; + geoHints.init(m_testWindow); + geoHints.read(); + QCOMPARE(geoHints.hasAspect(), minAspect.isValid() && maxAspect.isValid()); + QCOMPARE(geoHints.hasBaseSize(), baseSize.isValid()); + QCOMPARE(geoHints.hasMaxSize(), maxSize.isValid()); + QCOMPARE(geoHints.hasMinSize(), minSize.isValid()); + QCOMPARE(geoHints.hasPosition(), !userPos.isNull()); + QCOMPARE(geoHints.hasResizeIncrements(), resizeInc.isValid()); + QCOMPARE(geoHints.hasSize(), userSize.isValid()); + QCOMPARE(geoHints.hasWindowGravity(), gravity != 0); + QTEST(geoHints.baseSize(), "expectedBaseSize"); + QTEST(geoHints.maxAspect(), "expectedMaxAspect"); + QTEST(geoHints.maxSize(), "expectedMaxSize"); + QTEST(geoHints.minAspect(), "expectedMinAspect"); + QTEST(geoHints.minSize(), "expectedMinSize"); + QTEST(geoHints.resizeIncrements(), "expectedResizeIncrements"); + QTEST(qint32(geoHints.windowGravity()), "expectedGravity"); + + auto sizeHints = geoHints.m_sizeHints; + QVERIFY(sizeHints); + QTEST(sizeHints->flags, "expectedFlags"); + QTEST(sizeHints->pad[0], "expectedPad0"); + QTEST(sizeHints->pad[1], "expectedPad1"); + QTEST(sizeHints->pad[2], "expectedPad2"); + QTEST(sizeHints->pad[3], "expectedPad3"); + QTEST(sizeHints->minWidth, "expectedMinWidth"); + QTEST(sizeHints->minHeight, "expectedMinHeight"); + QTEST(sizeHints->maxWidth, "expectedMaxWidth"); + QTEST(sizeHints->maxHeight, "expectedMaxHeight"); + QTEST(sizeHints->widthInc, "expectedWidthInc"); + QTEST(sizeHints->heightInc, "expectedHeightInc"); + QTEST(sizeHints->minAspect[0], "expectedMinAspectNum"); + QTEST(sizeHints->minAspect[1], "expectedMinAspectDen"); + QTEST(sizeHints->maxAspect[0], "expectedMaxAspectNum"); + QTEST(sizeHints->maxAspect[1], "expectedMaxAspectDen"); + QTEST(sizeHints->baseWidth, "expectedBaseWidth"); + QTEST(sizeHints->baseHeight, "expectedBaseHeight"); + QCOMPARE(sizeHints->winGravity, gravity); + + // copy + Xcb::GeometryHints::NormalHints sizeHints2 = *sizeHints; + QTEST(sizeHints2.flags, "expectedFlags"); + QTEST(sizeHints2.pad[0], "expectedPad0"); + QTEST(sizeHints2.pad[1], "expectedPad1"); + QTEST(sizeHints2.pad[2], "expectedPad2"); + QTEST(sizeHints2.pad[3], "expectedPad3"); + QTEST(sizeHints2.minWidth, "expectedMinWidth"); + QTEST(sizeHints2.minHeight, "expectedMinHeight"); + QTEST(sizeHints2.maxWidth, "expectedMaxWidth"); + QTEST(sizeHints2.maxHeight, "expectedMaxHeight"); + QTEST(sizeHints2.widthInc, "expectedWidthInc"); + QTEST(sizeHints2.heightInc, "expectedHeightInc"); + QTEST(sizeHints2.minAspect[0], "expectedMinAspectNum"); + QTEST(sizeHints2.minAspect[1], "expectedMinAspectDen"); + QTEST(sizeHints2.maxAspect[0], "expectedMaxAspectNum"); + QTEST(sizeHints2.maxAspect[1], "expectedMaxAspectDen"); + QTEST(sizeHints2.baseWidth, "expectedBaseWidth"); + QTEST(sizeHints2.baseHeight, "expectedBaseHeight"); + QCOMPARE(sizeHints2.winGravity, gravity); +} + +void TestXcbSizeHints::testSizeHintsEmpty() +{ + xcb_size_hints_t xcbHints{}; + xcb_icccm_set_wm_normal_hints(QX11Info::connection(), m_testWindow, &xcbHints); + xcb_flush(QX11Info::connection()); + + Xcb::GeometryHints hints; + hints.init(m_testWindow); + hints.read(); + QVERIFY(!hints.hasAspect()); + QVERIFY(!hints.hasBaseSize()); + QVERIFY(!hints.hasMaxSize()); + QVERIFY(!hints.hasMinSize()); + QVERIFY(!hints.hasPosition()); + QVERIFY(!hints.hasResizeIncrements()); + QVERIFY(!hints.hasSize()); + QVERIFY(!hints.hasWindowGravity()); + + QCOMPARE(hints.baseSize(), QSize(0, 0)); + QCOMPARE(hints.maxAspect(), QSize(INT_MAX, 1)); + QCOMPARE(hints.maxSize(), QSize(INT_MAX, INT_MAX)); + QCOMPARE(hints.minAspect(), QSize(1, INT_MAX)); + QCOMPARE(hints.minSize(), QSize(0, 0)); + QCOMPARE(hints.resizeIncrements(), QSize(1, 1)); + QCOMPARE(hints.windowGravity(), XCB_GRAVITY_NORTH_WEST); + + auto sizeHints = hints.m_sizeHints; + QVERIFY(sizeHints); + QCOMPARE(sizeHints->flags, 0); + QCOMPARE(sizeHints->pad[0], 0); + QCOMPARE(sizeHints->pad[1], 0); + QCOMPARE(sizeHints->pad[2], 0); + QCOMPARE(sizeHints->pad[3], 0); + QCOMPARE(sizeHints->minWidth, 0); + QCOMPARE(sizeHints->minHeight, 0); + QCOMPARE(sizeHints->maxWidth, 0); + QCOMPARE(sizeHints->maxHeight, 0); + QCOMPARE(sizeHints->widthInc, 0); + QCOMPARE(sizeHints->heightInc, 0); + QCOMPARE(sizeHints->minAspect[0], 0); + QCOMPARE(sizeHints->minAspect[1], 0); + QCOMPARE(sizeHints->maxAspect[0], 0); + QCOMPARE(sizeHints->maxAspect[1], 0); + QCOMPARE(sizeHints->baseWidth, 0); + QCOMPARE(sizeHints->baseHeight, 0); + QCOMPARE(sizeHints->winGravity, 0); +} + +void TestXcbSizeHints::testSizeHintsNotSet() +{ + Xcb::GeometryHints hints; + hints.init(m_testWindow); + hints.read(); + QVERIFY(!hints.m_sizeHints); + QVERIFY(!hints.hasAspect()); + QVERIFY(!hints.hasBaseSize()); + QVERIFY(!hints.hasMaxSize()); + QVERIFY(!hints.hasMinSize()); + QVERIFY(!hints.hasPosition()); + QVERIFY(!hints.hasResizeIncrements()); + QVERIFY(!hints.hasSize()); + QVERIFY(!hints.hasWindowGravity()); + + QCOMPARE(hints.baseSize(), QSize(0, 0)); + QCOMPARE(hints.maxAspect(), QSize(INT_MAX, 1)); + QCOMPARE(hints.maxSize(), QSize(INT_MAX, INT_MAX)); + QCOMPARE(hints.minAspect(), QSize(1, INT_MAX)); + QCOMPARE(hints.minSize(), QSize(0, 0)); + QCOMPARE(hints.resizeIncrements(), QSize(1, 1)); + QCOMPARE(hints.windowGravity(), XCB_GRAVITY_NORTH_WEST); +} + +void TestXcbSizeHints::geometryHintsBeforeInit() +{ + Xcb::GeometryHints hints; + QVERIFY(!hints.hasAspect()); + QVERIFY(!hints.hasBaseSize()); + QVERIFY(!hints.hasMaxSize()); + QVERIFY(!hints.hasMinSize()); + QVERIFY(!hints.hasPosition()); + QVERIFY(!hints.hasResizeIncrements()); + QVERIFY(!hints.hasSize()); + QVERIFY(!hints.hasWindowGravity()); + + QCOMPARE(hints.baseSize(), QSize(0, 0)); + QCOMPARE(hints.maxAspect(), QSize(INT_MAX, 1)); + QCOMPARE(hints.maxSize(), QSize(INT_MAX, INT_MAX)); + QCOMPARE(hints.minAspect(), QSize(1, INT_MAX)); + QCOMPARE(hints.minSize(), QSize(0, 0)); + QCOMPARE(hints.resizeIncrements(), QSize(1, 1)); + QCOMPARE(hints.windowGravity(), XCB_GRAVITY_NORTH_WEST); +} + +void TestXcbSizeHints::geometryHintsBeforeRead() +{ + xcb_size_hints_t xcbHints{}; + xcb_icccm_size_hints_set_position(&xcbHints, 1, 1, 2); + xcb_icccm_set_wm_normal_hints(QX11Info::connection(), m_testWindow, &xcbHints); + xcb_flush(QX11Info::connection()); + + Xcb::GeometryHints hints; + hints.init(m_testWindow); + QVERIFY(!hints.hasAspect()); + QVERIFY(!hints.hasBaseSize()); + QVERIFY(!hints.hasMaxSize()); + QVERIFY(!hints.hasMinSize()); + QVERIFY(!hints.hasPosition()); + QVERIFY(!hints.hasResizeIncrements()); + QVERIFY(!hints.hasSize()); + QVERIFY(!hints.hasWindowGravity()); + + QCOMPARE(hints.baseSize(), QSize(0, 0)); + QCOMPARE(hints.maxAspect(), QSize(INT_MAX, 1)); + QCOMPARE(hints.maxSize(), QSize(INT_MAX, INT_MAX)); + QCOMPARE(hints.minAspect(), QSize(1, INT_MAX)); + QCOMPARE(hints.minSize(), QSize(0, 0)); + QCOMPARE(hints.resizeIncrements(), QSize(1, 1)); + QCOMPARE(hints.windowGravity(), XCB_GRAVITY_NORTH_WEST); +} + +Q_CONSTRUCTOR_FUNCTION(forceXcb) +QTEST_MAIN(TestXcbSizeHints) +#include "test_xcb_size_hints.moc" diff --git a/local/recipes/kde/kwin/source/autotests/test_xcb_window.cpp b/local/recipes/kde/kwin/source/autotests/test_xcb_window.cpp new file mode 100644 index 0000000000..2fb6d53f34 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/test_xcb_window.cpp @@ -0,0 +1,202 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "testutils.h" +// KWin +#include "utils/xcbutils.h" +// Qt +#include +#include +#include +// xcb +#include + +using namespace KWin; + +class TestXcbWindow : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void defaultCtor(); + void ctor(); + void classCtor(); + void create(); + void mapUnmap(); + void geometry(); + void destroy(); + void destroyNotManaged(); +}; + +void TestXcbWindow::initTestCase() +{ + qApp->setProperty("x11RootWindow", QVariant::fromValue(QX11Info::appRootWindow())); + qApp->setProperty("x11Connection", QVariant::fromValue(QX11Info::connection())); +} + +void TestXcbWindow::defaultCtor() +{ + Xcb::Window window; + QCOMPARE(window.isValid(), false); + xcb_window_t wId = window; + QCOMPARE(wId, noneWindow()); + + xcb_window_t nativeWindow = createWindow(); + Xcb::Window window2(nativeWindow); + QCOMPARE(window2.isValid(), true); + wId = window2; + QCOMPARE(wId, nativeWindow); +} + +void TestXcbWindow::ctor() +{ + const Rect geometry(0, 0, 10, 10); + const uint32_t values[] = {true}; + Xcb::Window window(geometry, XCB_CW_OVERRIDE_REDIRECT, values); + QCOMPARE(window.isValid(), true); + QVERIFY(window != XCB_WINDOW_NONE); + Xcb::WindowGeometry windowGeometry(window); + QCOMPARE(windowGeometry.isNull(), false); + QCOMPARE(windowGeometry.rect(), geometry); +} + +void TestXcbWindow::classCtor() +{ + const Rect geometry(0, 0, 10, 10); + const uint32_t values[] = {true}; + Xcb::Window window(geometry, XCB_WINDOW_CLASS_INPUT_ONLY, XCB_CW_OVERRIDE_REDIRECT, values); + QCOMPARE(window.isValid(), true); + QVERIFY(window != XCB_WINDOW_NONE); + Xcb::WindowGeometry windowGeometry(window); + QCOMPARE(windowGeometry.isNull(), false); + QCOMPARE(windowGeometry.rect(), geometry); + + Xcb::WindowAttributes attribs(window); + QCOMPARE(attribs.isNull(), false); + QVERIFY(attribs->_class == XCB_WINDOW_CLASS_INPUT_ONLY); +} + +void TestXcbWindow::create() +{ + Xcb::Window window; + QCOMPARE(window.isValid(), false); + xcb_window_t wId = window; + QCOMPARE(wId, noneWindow()); + + const Rect geometry(0, 0, 10, 10); + const uint32_t values[] = {true}; + window.create(geometry, XCB_CW_OVERRIDE_REDIRECT, values); + QCOMPARE(window.isValid(), true); + QVERIFY(window != XCB_WINDOW_NONE); + // and reset again + window.reset(); + QCOMPARE(window.isValid(), false); + QVERIFY(window == XCB_WINDOW_NONE); +} + +void TestXcbWindow::mapUnmap() +{ + const Rect geometry(0, 0, 10, 10); + const uint32_t values[] = {true}; + Xcb::Window window(geometry, XCB_WINDOW_CLASS_INPUT_ONLY, XCB_CW_OVERRIDE_REDIRECT, values); + Xcb::WindowAttributes attribs(window); + QCOMPARE(attribs.isNull(), false); + QVERIFY(attribs->map_state == XCB_MAP_STATE_UNMAPPED); + + window.map(); + Xcb::WindowAttributes attribs2(window); + QCOMPARE(attribs2.isNull(), false); + QVERIFY(attribs2->map_state != XCB_MAP_STATE_UNMAPPED); + + window.unmap(); + Xcb::WindowAttributes attribs3(window); + QCOMPARE(attribs3.isNull(), false); + QVERIFY(attribs3->map_state == XCB_MAP_STATE_UNMAPPED); + + // map, unmap shouldn't fail for an invalid window, it's just ignored + window.reset(); + window.map(); + window.unmap(); +} + +void TestXcbWindow::geometry() +{ + const Rect geometry(0, 0, 10, 10); + const uint32_t values[] = {true}; + Xcb::Window window(geometry, XCB_WINDOW_CLASS_INPUT_ONLY, XCB_CW_OVERRIDE_REDIRECT, values); + Xcb::WindowGeometry windowGeometry(window); + QCOMPARE(windowGeometry.isNull(), false); + QCOMPARE(windowGeometry.rect(), geometry); + + const Rect geometry2(10, 20, 100, 200); + window.setGeometry(geometry2); + Xcb::WindowGeometry windowGeometry2(window); + QCOMPARE(windowGeometry2.isNull(), false); + QCOMPARE(windowGeometry2.rect(), geometry2); + + // setting a geometry on an invalid window should be ignored + window.reset(); + window.setGeometry(geometry2); + Xcb::WindowGeometry windowGeometry3(window); + QCOMPARE(windowGeometry3.isNull(), true); +} + +void TestXcbWindow::destroy() +{ + const Rect geometry(0, 0, 10, 10); + const uint32_t values[] = {true}; + Xcb::Window window(geometry, XCB_CW_OVERRIDE_REDIRECT, values); + QCOMPARE(window.isValid(), true); + xcb_window_t wId = window; + + window.create(geometry, XCB_CW_OVERRIDE_REDIRECT, values); + // wId should now be invalid + xcb_generic_error_t *error = nullptr; + UniqueCPtr attribs(xcb_get_window_attributes_reply( + connection(), + xcb_get_window_attributes(connection(), wId), + &error)); + QVERIFY(!attribs); + QCOMPARE(error->error_code, uint8_t(3)); + QCOMPARE(error->resource_id, wId); + free(error); + + // test the same for the dtor + { + Xcb::Window scopedWindow(geometry, XCB_CW_OVERRIDE_REDIRECT, values); + QVERIFY(scopedWindow.isValid()); + wId = scopedWindow; + } + error = nullptr; + UniqueCPtr attribs2(xcb_get_window_attributes_reply( + connection(), + xcb_get_window_attributes(connection(), wId), + &error)); + QVERIFY(!attribs2); + QCOMPARE(error->error_code, uint8_t(3)); + QCOMPARE(error->resource_id, wId); + free(error); +} + +void TestXcbWindow::destroyNotManaged() +{ + Xcb::Window window; + // just destroy the non-existing window + window.reset(); + + // now let's add a window + window.reset(createWindow(), false); + xcb_window_t w = window; + window.reset(); + Xcb::WindowAttributes attribs(w); + QVERIFY(attribs); +} + +Q_CONSTRUCTOR_FUNCTION(forceXcb) +QTEST_MAIN(TestXcbWindow) +#include "test_xcb_window.moc" diff --git a/local/recipes/kde/kwin/source/autotests/test_xcb_wrapper.cpp b/local/recipes/kde/kwin/source/autotests/test_xcb_wrapper.cpp new file mode 100644 index 0000000000..7448d457dd --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/test_xcb_wrapper.cpp @@ -0,0 +1,480 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "testutils.h" +// KWin +#include "utils/xcbutils.h" +// Qt +#include +#include +#include +#include +// xcb +#include + +using namespace KWin; + +class TestXcbWrapper : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void defaultCtor(); + void normalCtor(); + void copyCtorEmpty(); + void copyCtorBeforeRetrieve(); + void copyCtorAfterRetrieve(); + void assignementEmpty(); + void assignmentBeforeRetrieve(); + void assignmentAfterRetrieve(); + void discard(); + void testQueryTree(); + void testCurrentInput(); + void testTransientFor(); + void testPropertyByteArray(); + void testPropertyBool(); + void testAtom(); + void testMotifEmpty(); + void testMotif_data(); + void testMotif(); + +private: + void testEmpty(Xcb::WindowGeometry &geometry); + void testGeometry(Xcb::WindowGeometry &geometry, const Rect &rect); + Xcb::Window m_testWindow; +}; + +void TestXcbWrapper::initTestCase() +{ + qApp->setProperty("x11RootWindow", QVariant::fromValue(QX11Info::appRootWindow())); + qApp->setProperty("x11Connection", QVariant::fromValue(QX11Info::connection())); +} + +void TestXcbWrapper::init() +{ + const uint32_t values[] = {true}; + m_testWindow.create(Rect(0, 0, 10, 10), XCB_WINDOW_CLASS_INPUT_ONLY, XCB_CW_OVERRIDE_REDIRECT, values); + QVERIFY(m_testWindow.isValid()); +} + +void TestXcbWrapper::cleanup() +{ + m_testWindow.reset(); +} + +void TestXcbWrapper::testEmpty(Xcb::WindowGeometry &geometry) +{ + QCOMPARE(geometry.window(), KWin::noneWindow()); + QVERIFY(!geometry.data()); + QCOMPARE(geometry.isNull(), true); + QCOMPARE(geometry.rect(), Rect()); + QVERIFY(!geometry); +} + +void TestXcbWrapper::testGeometry(Xcb::WindowGeometry &geometry, const Rect &rect) +{ + QCOMPARE(geometry.window(), (xcb_window_t)m_testWindow); + // now lets retrieve some data + QCOMPARE(geometry.rect(), rect); + QVERIFY(geometry.isRetrieved()); + QCOMPARE(geometry.isNull(), false); + QVERIFY(geometry); + QVERIFY(geometry.data()); + QCOMPARE(geometry.data()->x, int16_t(rect.x())); + QCOMPARE(geometry.data()->y, int16_t(rect.y())); + QCOMPARE(geometry.data()->width, uint16_t(rect.width())); + QCOMPARE(geometry.data()->height, uint16_t(rect.height())); +} + +void TestXcbWrapper::defaultCtor() +{ + Xcb::WindowGeometry geometry; + testEmpty(geometry); + QVERIFY(!geometry.isRetrieved()); +} + +void TestXcbWrapper::normalCtor() +{ + Xcb::WindowGeometry geometry(m_testWindow); + QVERIFY(!geometry.isRetrieved()); + testGeometry(geometry, Rect(0, 0, 10, 10)); +} + +void TestXcbWrapper::copyCtorEmpty() +{ + Xcb::WindowGeometry geometry; + Xcb::WindowGeometry other(geometry); + testEmpty(geometry); + QVERIFY(geometry.isRetrieved()); + testEmpty(other); + QVERIFY(!other.isRetrieved()); +} + +void TestXcbWrapper::copyCtorBeforeRetrieve() +{ + Xcb::WindowGeometry geometry(m_testWindow); + QVERIFY(!geometry.isRetrieved()); + Xcb::WindowGeometry other(geometry); + testEmpty(geometry); + QVERIFY(geometry.isRetrieved()); + + QVERIFY(!other.isRetrieved()); + testGeometry(other, Rect(0, 0, 10, 10)); +} + +void TestXcbWrapper::copyCtorAfterRetrieve() +{ + Xcb::WindowGeometry geometry(m_testWindow); + QVERIFY(geometry); + QVERIFY(geometry.isRetrieved()); + QCOMPARE(geometry.rect(), Rect(0, 0, 10, 10)); + Xcb::WindowGeometry other(geometry); + testEmpty(geometry); + QVERIFY(geometry.isRetrieved()); + + QVERIFY(other.isRetrieved()); + testGeometry(other, Rect(0, 0, 10, 10)); +} + +void TestXcbWrapper::assignementEmpty() +{ + Xcb::WindowGeometry geometry; + Xcb::WindowGeometry other; + testEmpty(geometry); + testEmpty(other); + + other = geometry; + QVERIFY(geometry.isRetrieved()); + testEmpty(geometry); + testEmpty(other); + QVERIFY(!other.isRetrieved()); + + QT_WARNING_PUSH + QT_WARNING_DISABLE_CLANG("-Wself-assign-overloaded") + // test assignment to self + geometry = geometry; + other = other; + testEmpty(geometry); + testEmpty(other); + QT_WARNING_POP +} + +void TestXcbWrapper::assignmentBeforeRetrieve() +{ + Xcb::WindowGeometry geometry(m_testWindow); + Xcb::WindowGeometry other = geometry; + QVERIFY(geometry.isRetrieved()); + testEmpty(geometry); + + QVERIFY(!other.isRetrieved()); + testGeometry(other, Rect(0, 0, 10, 10)); + + other = Xcb::WindowGeometry(m_testWindow); + QVERIFY(!other.isRetrieved()); + QCOMPARE(other.window(), (xcb_window_t)m_testWindow); + other = Xcb::WindowGeometry(); + testEmpty(geometry); + + QT_WARNING_PUSH + QT_WARNING_DISABLE_CLANG("-Wself-assign-overloaded") + // test assignment to self + geometry = geometry; + other = other; + testEmpty(geometry); + QT_WARNING_POP +} + +void TestXcbWrapper::assignmentAfterRetrieve() +{ + Xcb::WindowGeometry geometry(m_testWindow); + QVERIFY(geometry); + QVERIFY(geometry.isRetrieved()); + Xcb::WindowGeometry other = geometry; + testEmpty(geometry); + + QVERIFY(other.isRetrieved()); + testGeometry(other, Rect(0, 0, 10, 10)); + + QT_WARNING_PUSH + QT_WARNING_DISABLE_CLANG("-Wself-assign-overloaded") + // test assignment to self + geometry = geometry; + other = other; + testEmpty(geometry); + testGeometry(other, Rect(0, 0, 10, 10)); + QT_WARNING_POP + + // set to empty again + other = Xcb::WindowGeometry(); + testEmpty(other); +} + +void TestXcbWrapper::discard() +{ + // discard of reply cannot be tested properly as we cannot check whether the reply has been discarded + // therefore it's more or less just a test to ensure that it doesn't crash and the code paths + // are taken. + Xcb::WindowGeometry *geometry = new Xcb::WindowGeometry(); + delete geometry; + + geometry = new Xcb::WindowGeometry(m_testWindow); + delete geometry; + + geometry = new Xcb::WindowGeometry(m_testWindow); + QVERIFY(geometry->data()); + delete geometry; +} + +void TestXcbWrapper::testQueryTree() +{ + Xcb::Tree tree(m_testWindow); + // should have root as parent + QCOMPARE(tree.parent(), static_cast(QX11Info::appRootWindow())); + // shouldn't have any children + QCOMPARE(tree->children_len, uint16_t(0)); + QVERIFY(!tree.children()); + + // query for root + Xcb::Tree root(QX11Info::appRootWindow()); + // shouldn't have a parent + QCOMPARE(root.parent(), xcb_window_t(XCB_WINDOW_NONE)); + QVERIFY(root->children_len > 0); + xcb_window_t *children = root.children(); + bool found = false; + for (int i = 0; i < xcb_query_tree_children_length(root.data()); ++i) { + if (children[i] == tree.window()) { + found = true; + break; + } + } + QVERIFY(found); + + // query for not existing window + Xcb::Tree doesntExist(XCB_WINDOW_NONE); + QCOMPARE(doesntExist.parent(), xcb_window_t(XCB_WINDOW_NONE)); + QVERIFY(doesntExist.isNull()); + QVERIFY(doesntExist.isRetrieved()); +} + +void TestXcbWrapper::testCurrentInput() +{ + xcb_connection_t *c = QX11Info::connection(); + m_testWindow.map(); + QX11Info::setAppTime(QX11Info::getTimestamp()); + + // let's set the input focus + m_testWindow.focus(XCB_INPUT_FOCUS_PARENT, QX11Info::appTime()); + xcb_flush(c); + + Xcb::CurrentInput input; + QCOMPARE(input.window(), (xcb_window_t)m_testWindow); + + // creating a copy should make the input object have no window any more + Xcb::CurrentInput input2(input); + QCOMPARE(input2.window(), (xcb_window_t)m_testWindow); + QCOMPARE(input.window(), xcb_window_t(XCB_WINDOW_NONE)); +} + +void TestXcbWrapper::testTransientFor() +{ + Xcb::TransientFor transient(m_testWindow); + QCOMPARE(transient.window(), (xcb_window_t)m_testWindow); + // our m_testWindow doesn't have a transient for hint + QCOMPARE(transient.getTransientFor(), std::nullopt); + + // Create a Window with a transient for hint + Xcb::Window transientWindow(KWin::createWindow()); + xcb_window_t testWindowId = m_testWindow; + transientWindow.changeProperty(XCB_ATOM_WM_TRANSIENT_FOR, XCB_ATOM_WINDOW, 32, 1, &testWindowId); + + // let's get another transient object + Xcb::TransientFor realTransient(transientWindow); + QCOMPARE(realTransient.getTransientFor(), (xcb_window_t)m_testWindow); + QCOMPARE(realTransient.value(32, XCB_ATOM_WINDOW), (xcb_window_t)m_testWindow); + QCOMPARE(realTransient.value(), (xcb_window_t)m_testWindow); + const auto opt = realTransient.array(); + QVERIFY(opt.has_value()); + QCOMPARE((*opt)[0], (xcb_window_t)m_testWindow); + + // test for a not existing window + Xcb::TransientFor doesntExist(XCB_WINDOW_NONE); + QVERIFY(!doesntExist.getTransientFor().has_value()); +} + +void TestXcbWrapper::testPropertyByteArray() +{ + Xcb::Window testWindow(KWin::createWindow()); + Xcb::Property prop(false, testWindow, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 0, 100000); + QCOMPARE(prop.toByteArray(), std::nullopt); + QVERIFY(!prop.array().has_value()); + QCOMPARE(QByteArray(Xcb::StringProperty(testWindow, XCB_ATOM_WM_NAME)), QByteArray()); + + testWindow.changeProperty(XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8, 3, "foo"); + prop = Xcb::Property(false, testWindow, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 0, 100000); + QCOMPARE(prop.toByteArray(), QByteArrayLiteral("foo")); + QVERIFY(prop.array().has_value()); + QCOMPARE(prop.array()->data(), "foo"); + QCOMPARE(QByteArray(Xcb::StringProperty(testWindow, XCB_ATOM_WM_NAME)), QByteArrayLiteral("foo")); + + // verify incorrect format and type + // TODO this should just not compile, rather than needing a test... + QCOMPARE(prop.toByteArray(32, XCB_ATOM_STRING), std::nullopt); + QCOMPARE(prop.toByteArray(8, XCB_ATOM_CARDINAL), std::nullopt); + + // verify empty property + testWindow.changeProperty(XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8, 0, nullptr); + prop = Xcb::Property(false, testWindow, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 0, 100000); + QCOMPARE(prop.toByteArray(), QByteArray("", 0)); + QCOMPARE(prop.array()->size(), 0); + QCOMPARE(QByteArray(Xcb::StringProperty(testWindow, XCB_ATOM_WM_NAME)), QByteArray()); + + // verify non existing property + Xcb::Atom invalid(QByteArrayLiteral("INVALID_ATOM")); + prop = Xcb::Property(false, testWindow, invalid, XCB_ATOM_STRING, 0, 100000); + QCOMPARE(prop.toByteArray(), std::nullopt); + QVERIFY(!prop.array()); + QCOMPARE(QByteArray(Xcb::StringProperty(testWindow, XCB_ATOM_WM_NAME)), QByteArray()); +} + +void TestXcbWrapper::testPropertyBool() +{ + Xcb::Window testWindow(KWin::createWindow()); + Xcb::Atom blockCompositing(QByteArrayLiteral("_KDE_NET_WM_BLOCK_COMPOSITING")); + QVERIFY(blockCompositing != XCB_ATOM_NONE); + NETWinInfo info(QX11Info::connection(), testWindow, QX11Info::appRootWindow(), NET::Properties(), NET::WM2BlockCompositing); + + Xcb::Property prop(false, testWindow, blockCompositing, XCB_ATOM_CARDINAL, 0, 100000); + QVERIFY(!prop.toBool().has_value()); + + info.setBlockingCompositing(true); + xcb_flush(QX11Info::connection()); + prop = Xcb::Property(false, testWindow, blockCompositing, XCB_ATOM_CARDINAL, 0, 100000); + QCOMPARE(prop.toBool(), true); + + // incorrect value: + uint32_t d[] = {1, 0}; + testWindow.changeProperty(blockCompositing, XCB_ATOM_CARDINAL, 32, 2, d); + prop = Xcb::Property(false, testWindow, blockCompositing, XCB_ATOM_CARDINAL, 0, 100000); + QVERIFY(!prop.toBool().has_value()); +} + +void TestXcbWrapper::testAtom() +{ + Xcb::Atom atom(QByteArrayLiteral("WM_CLIENT_MACHINE")); + QCOMPARE(atom.name(), QByteArrayLiteral("WM_CLIENT_MACHINE")); + QVERIFY(atom == XCB_ATOM_WM_CLIENT_MACHINE); + QVERIFY(atom.isValid()); + + // test the const paths + const Xcb::Atom &atom2(atom); + QVERIFY(atom2.isValid()); + QVERIFY(atom2 == XCB_ATOM_WM_CLIENT_MACHINE); + QCOMPARE(atom2.name(), QByteArrayLiteral("WM_CLIENT_MACHINE")); + + // destroy before retrieved + Xcb::Atom atom3(QByteArrayLiteral("WM_CLIENT_MACHINE")); + QCOMPARE(atom3.name(), QByteArrayLiteral("WM_CLIENT_MACHINE")); +} + +void TestXcbWrapper::testMotifEmpty() +{ + Xcb::Atom atom(QByteArrayLiteral("_MOTIF_WM_HINTS")); + Xcb::MotifHints hints(atom); + // pre init + QCOMPARE(hints.hasDecorationsFlag(), false); + QCOMPARE(hints.noDecorations(), false); + QCOMPARE(hints.resize(), true); + QCOMPARE(hints.move(), true); + QCOMPARE(hints.minimize(), true); + QCOMPARE(hints.maximize(), true); + QCOMPARE(hints.close(), true); + // post init, pre read + hints.init(m_testWindow); + QCOMPARE(hints.hasDecorationsFlag(), false); + QCOMPARE(hints.noDecorations(), false); + QCOMPARE(hints.resize(), true); + QCOMPARE(hints.move(), true); + QCOMPARE(hints.minimize(), true); + QCOMPARE(hints.maximize(), true); + QCOMPARE(hints.close(), true); + // post read + hints.read(); + QCOMPARE(hints.hasDecorationsFlag(), false); + QCOMPARE(hints.noDecorations(), false); + QCOMPARE(hints.resize(), true); + QCOMPARE(hints.move(), true); + QCOMPARE(hints.minimize(), true); + QCOMPARE(hints.maximize(), true); + QCOMPARE(hints.close(), true); +} + +void TestXcbWrapper::testMotif_data() +{ + QTest::addColumn("flags"); + QTest::addColumn("functions"); + QTest::addColumn("decorations"); + + QTest::addColumn("expectedHasDecorationsFlag"); + QTest::addColumn("expectedNoDecorations"); + QTest::addColumn("expectedResize"); + QTest::addColumn("expectedMove"); + QTest::addColumn("expectedMinimize"); + QTest::addColumn("expectedMaximize"); + QTest::addColumn("expectedClose"); + + QTest::newRow("none") << 0u << 0u << 0u << false << false << true << true << true << true << true; + QTest::newRow("noborder") << 2u << 5u << 0u << true << true << true << true << true << true << true; + QTest::newRow("border") << 2u << 5u << 1u << true << false << true << true << true << true << true; + QTest::newRow("resize") << 1u << 2u << 1u << false << false << true << false << false << false << false; + QTest::newRow("move") << 1u << 4u << 1u << false << false << false << true << false << false << false; + QTest::newRow("minimize") << 1u << 8u << 1u << false << false << false << false << true << false << false; + QTest::newRow("maximize") << 1u << 16u << 1u << false << false << false << false << false << true << false; + QTest::newRow("close") << 1u << 32u << 1u << false << false << false << false << false << false << true; + + QTest::newRow("resize/all") << 1u << 3u << 1u << false << false << false << true << true << true << true; + QTest::newRow("move/all") << 1u << 5u << 1u << false << false << true << false << true << true << true; + QTest::newRow("minimize/all") << 1u << 9u << 1u << false << false << true << true << false << true << true; + QTest::newRow("maximize/all") << 1u << 17u << 1u << false << false << true << true << true << false << true; + QTest::newRow("close/all") << 1u << 33u << 1u << false << false << true << true << true << true << false; + + QTest::newRow("all") << 1u << 62u << 1u << false << false << true << true << true << true << true; + QTest::newRow("all/all") << 1u << 63u << 1u << false << false << false << false << false << false << false; + QTest::newRow("all/all/deco") << 3u << 63u << 1u << true << false << false << false << false << false << false; +} + +void TestXcbWrapper::testMotif() +{ + Xcb::Atom atom(QByteArrayLiteral("_MOTIF_WM_HINTS")); + QFETCH(quint32, flags); + QFETCH(quint32, functions); + QFETCH(quint32, decorations); + quint32 data[] = { + flags, + functions, + decorations, + 0, + 0}; + xcb_change_property(QX11Info::connection(), XCB_PROP_MODE_REPLACE, m_testWindow, atom, atom, 32, 5, data); + xcb_flush(QX11Info::connection()); + Xcb::MotifHints hints(atom); + hints.init(m_testWindow); + hints.read(); + QTEST(hints.hasDecorationsFlag(), "expectedHasDecorationsFlag"); + QTEST(hints.noDecorations(), "expectedNoDecorations"); + QTEST(hints.resize(), "expectedResize"); + QTEST(hints.move(), "expectedMove"); + QTEST(hints.minimize(), "expectedMinimize"); + QTEST(hints.maximize(), "expectedMaximize"); + QTEST(hints.close(), "expectedClose"); +} + +Q_CONSTRUCTOR_FUNCTION(forceXcb) +QTEST_MAIN(TestXcbWrapper) +#include "test_xcb_wrapper.moc" diff --git a/local/recipes/kde/kwin/source/autotests/test_xkb.cpp b/local/recipes/kde/kwin/source/autotests/test_xkb.cpp new file mode 100644 index 0000000000..aca83d6e23 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/test_xkb.cpp @@ -0,0 +1,529 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "xkb.h" + +#include +#include + +using namespace KWin; + +class XkbTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testToQtKey_data(); + void testToQtKey(); + void testFromQtKey_data(); + void testFromQtKey(); +}; + +// from kwindowsystem/src/platforms/xcb/kkeyserver.cpp +// adjusted to xkb +struct TransKey +{ + xkb_keysym_t keySymX; + Qt::Key keySymQt; + Qt::KeyboardModifiers modifiers; +}; + +static const TransKey g_rgQtToSymX[] = { + {XKB_KEY_Escape, Qt::Key_Escape, Qt::KeyboardModifiers()}, + {XKB_KEY_Tab, Qt::Key_Tab, Qt::KeyboardModifiers()}, + {XKB_KEY_ISO_Left_Tab, Qt::Key_Backtab, Qt::KeyboardModifiers()}, + {XKB_KEY_BackSpace, Qt::Key_Backspace, Qt::KeyboardModifiers()}, + {XKB_KEY_Return, Qt::Key_Return, Qt::KeyboardModifiers()}, + {XKB_KEY_Insert, Qt::Key_Insert, Qt::KeyboardModifiers()}, + {XKB_KEY_Delete, Qt::Key_Delete, Qt::KeyboardModifiers()}, + {XKB_KEY_Pause, Qt::Key_Pause, Qt::KeyboardModifiers()}, + {XKB_KEY_Print, Qt::Key_Print, Qt::KeyboardModifiers()}, + {XKB_KEY_Sys_Req, Qt::Key_SysReq, Qt::KeyboardModifiers()}, + {XKB_KEY_Home, Qt::Key_Home, Qt::KeyboardModifiers()}, + {XKB_KEY_End, Qt::Key_End, Qt::KeyboardModifiers()}, + {XKB_KEY_Left, Qt::Key_Left, Qt::KeyboardModifiers()}, + {XKB_KEY_Up, Qt::Key_Up, Qt::KeyboardModifiers()}, + {XKB_KEY_Right, Qt::Key_Right, Qt::KeyboardModifiers()}, + {XKB_KEY_Down, Qt::Key_Down, Qt::KeyboardModifiers()}, + {XKB_KEY_Prior, Qt::Key_PageUp, Qt::KeyboardModifiers()}, + {XKB_KEY_Next, Qt::Key_PageDown, Qt::KeyboardModifiers()}, + {XKB_KEY_Caps_Lock, Qt::Key_CapsLock, Qt::KeyboardModifiers()}, + {XKB_KEY_Num_Lock, Qt::Key_NumLock, Qt::KeyboardModifiers()}, + {XKB_KEY_Scroll_Lock, Qt::Key_ScrollLock, Qt::KeyboardModifiers()}, + {XKB_KEY_F1, Qt::Key_F1, Qt::KeyboardModifiers()}, + {XKB_KEY_F2, Qt::Key_F2, Qt::KeyboardModifiers()}, + {XKB_KEY_F3, Qt::Key_F3, Qt::KeyboardModifiers()}, + {XKB_KEY_F4, Qt::Key_F4, Qt::KeyboardModifiers()}, + {XKB_KEY_F5, Qt::Key_F5, Qt::KeyboardModifiers()}, + {XKB_KEY_F6, Qt::Key_F6, Qt::KeyboardModifiers()}, + {XKB_KEY_F7, Qt::Key_F7, Qt::KeyboardModifiers()}, + {XKB_KEY_F8, Qt::Key_F8, Qt::KeyboardModifiers()}, + {XKB_KEY_F9, Qt::Key_F9, Qt::KeyboardModifiers()}, + {XKB_KEY_F10, Qt::Key_F10, Qt::KeyboardModifiers()}, + {XKB_KEY_F11, Qt::Key_F11, Qt::KeyboardModifiers()}, + {XKB_KEY_F12, Qt::Key_F12, Qt::KeyboardModifiers()}, + {XKB_KEY_F13, Qt::Key_F13, Qt::KeyboardModifiers()}, + {XKB_KEY_F14, Qt::Key_F14, Qt::KeyboardModifiers()}, + {XKB_KEY_F15, Qt::Key_F15, Qt::KeyboardModifiers()}, + {XKB_KEY_F16, Qt::Key_F16, Qt::KeyboardModifiers()}, + {XKB_KEY_F17, Qt::Key_F17, Qt::KeyboardModifiers()}, + {XKB_KEY_F18, Qt::Key_F18, Qt::KeyboardModifiers()}, + {XKB_KEY_F19, Qt::Key_F19, Qt::KeyboardModifiers()}, + {XKB_KEY_F20, Qt::Key_F20, Qt::KeyboardModifiers()}, + {XKB_KEY_F21, Qt::Key_F21, Qt::KeyboardModifiers()}, + {XKB_KEY_F22, Qt::Key_F22, Qt::KeyboardModifiers()}, + {XKB_KEY_F23, Qt::Key_F23, Qt::KeyboardModifiers()}, + {XKB_KEY_F24, Qt::Key_F24, Qt::KeyboardModifiers()}, + {XKB_KEY_F25, Qt::Key_F25, Qt::KeyboardModifiers()}, + {XKB_KEY_F26, Qt::Key_F26, Qt::KeyboardModifiers()}, + {XKB_KEY_F27, Qt::Key_F27, Qt::KeyboardModifiers()}, + {XKB_KEY_F28, Qt::Key_F28, Qt::KeyboardModifiers()}, + {XKB_KEY_F29, Qt::Key_F29, Qt::KeyboardModifiers()}, + {XKB_KEY_F30, Qt::Key_F30, Qt::KeyboardModifiers()}, + {XKB_KEY_F31, Qt::Key_F31, Qt::KeyboardModifiers()}, + {XKB_KEY_F32, Qt::Key_F32, Qt::KeyboardModifiers()}, + {XKB_KEY_F33, Qt::Key_F33, Qt::KeyboardModifiers()}, + {XKB_KEY_F34, Qt::Key_F34, Qt::KeyboardModifiers()}, + {XKB_KEY_F35, Qt::Key_F35, Qt::KeyboardModifiers()}, + {XKB_KEY_Super_L, Qt::Key_Meta, Qt::KeyboardModifiers()}, + {XKB_KEY_Super_R, Qt::Key_Meta, Qt::KeyboardModifiers()}, + {XKB_KEY_Menu, Qt::Key_Menu, Qt::KeyboardModifiers()}, + {XKB_KEY_Hyper_L, Qt::Key_Meta, Qt::KeyboardModifiers()}, + {XKB_KEY_Hyper_R, Qt::Key_Meta, Qt::KeyboardModifiers()}, + {XKB_KEY_Help, Qt::Key_Help, Qt::KeyboardModifiers()}, + {XKB_KEY_KP_Space, Qt::Key_Space, Qt::KeypadModifier}, + {XKB_KEY_KP_Tab, Qt::Key_Tab, Qt::KeypadModifier}, + {XKB_KEY_KP_Enter, Qt::Key_Enter, Qt::KeypadModifier}, + {XKB_KEY_KP_Home, Qt::Key_Home, Qt::KeypadModifier}, + {XKB_KEY_KP_Left, Qt::Key_Left, Qt::KeypadModifier}, + {XKB_KEY_KP_Up, Qt::Key_Up, Qt::KeypadModifier}, + {XKB_KEY_KP_Right, Qt::Key_Right, Qt::KeypadModifier}, + {XKB_KEY_KP_Down, Qt::Key_Down, Qt::KeypadModifier}, + {XKB_KEY_KP_Prior, Qt::Key_PageUp, Qt::KeypadModifier}, + {XKB_KEY_KP_Next, Qt::Key_PageDown, Qt::KeypadModifier}, + {XKB_KEY_KP_End, Qt::Key_End, Qt::KeypadModifier}, + {XKB_KEY_KP_Begin, Qt::Key_Clear, Qt::KeypadModifier}, + {XKB_KEY_KP_Insert, Qt::Key_Insert, Qt::KeypadModifier}, + {XKB_KEY_KP_Delete, Qt::Key_Delete, Qt::KeypadModifier}, + {XKB_KEY_KP_Equal, Qt::Key_Equal, Qt::KeypadModifier}, + {XKB_KEY_KP_Multiply, Qt::Key_Asterisk, Qt::KeypadModifier}, + {XKB_KEY_KP_Add, Qt::Key_Plus, Qt::KeypadModifier}, + {XKB_KEY_KP_Separator, Qt::Key_Comma, Qt::KeypadModifier}, + {XKB_KEY_KP_Subtract, Qt::Key_Minus, Qt::KeypadModifier}, + {XKB_KEY_KP_Decimal, Qt::Key_Period, Qt::KeypadModifier}, + {XKB_KEY_KP_Divide, Qt::Key_Slash, Qt::KeypadModifier}, + {XKB_KEY_XF86Back, Qt::Key_Back, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Forward, Qt::Key_Forward, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Stop, Qt::Key_Stop, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Refresh, Qt::Key_Refresh, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Favorites, Qt::Key_Favorites, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86AudioMedia, Qt::Key_LaunchMedia, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86OpenURL, Qt::Key_OpenUrl, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86HomePage, Qt::Key_HomePage, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Search, Qt::Key_Search, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86AudioLowerVolume, Qt::Key_VolumeDown, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86AudioMute, Qt::Key_VolumeMute, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86AudioRaiseVolume, Qt::Key_VolumeUp, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86AudioPlay, Qt::Key_MediaPlay, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86AudioPause, Qt::Key_MediaPause, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86AudioStop, Qt::Key_MediaStop, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86AudioPrev, Qt::Key_MediaPrevious, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86AudioNext, Qt::Key_MediaNext, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86AudioRecord, Qt::Key_MediaRecord, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Mail, Qt::Key_LaunchMail, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86MyComputer, Qt::Key_LaunchMedia, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Calculater, Qt::Key_Calculator, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Memo, Qt::Key_Memo, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86ToDoList, Qt::Key_ToDoList, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Calendar, Qt::Key_Calendar, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86PowerDown, Qt::Key_PowerDown, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86ContrastAdjust, Qt::Key_ContrastAdjust, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Standby, Qt::Key_Standby, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86MonBrightnessUp, Qt::Key_MonBrightnessUp, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86MonBrightnessDown, Qt::Key_MonBrightnessDown, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86KbdLightOnOff, Qt::Key_KeyboardLightOnOff, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86KbdBrightnessUp, Qt::Key_KeyboardBrightnessUp, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86KbdBrightnessDown, Qt::Key_KeyboardBrightnessDown, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86PowerOff, Qt::Key_PowerOff, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86WakeUp, Qt::Key_WakeUp, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Eject, Qt::Key_Eject, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86ScreenSaver, Qt::Key_ScreenSaver, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86WWW, Qt::Key_WWW, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Sleep, Qt::Key_Sleep, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86LightBulb, Qt::Key_LightBulb, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Shop, Qt::Key_Shop, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86History, Qt::Key_History, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86AddFavorite, Qt::Key_AddFavorite, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86HotLinks, Qt::Key_HotLinks, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86BrightnessAdjust, Qt::Key_BrightnessAdjust, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Finance, Qt::Key_Finance, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Community, Qt::Key_Community, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86AudioRewind, Qt::Key_AudioRewind, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86BackForward, Qt::Key_BackForward, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86ApplicationLeft, Qt::Key_ApplicationLeft, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86ApplicationRight, Qt::Key_ApplicationRight, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Book, Qt::Key_Book, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86CD, Qt::Key_CD, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Calculater, Qt::Key_Calculator, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Clear, Qt::Key_Clear, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86ClearGrab, Qt::Key_ClearGrab, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Close, Qt::Key_Close, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Copy, Qt::Key_Copy, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Cut, Qt::Key_Cut, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Display, Qt::Key_Display, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86DOS, Qt::Key_DOS, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Documents, Qt::Key_Documents, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Excel, Qt::Key_Excel, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Explorer, Qt::Key_Explorer, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Game, Qt::Key_Game, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Go, Qt::Key_Go, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86iTouch, Qt::Key_iTouch, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86LogOff, Qt::Key_LogOff, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Market, Qt::Key_Market, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Meeting, Qt::Key_Meeting, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86MenuKB, Qt::Key_MenuKB, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86MenuPB, Qt::Key_MenuPB, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86MySites, Qt::Key_MySites, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86News, Qt::Key_News, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86OfficeHome, Qt::Key_OfficeHome, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Option, Qt::Key_Option, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Paste, Qt::Key_Paste, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Phone, Qt::Key_Phone, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Reply, Qt::Key_Reply, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Reload, Qt::Key_Reload, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86RotateWindows, Qt::Key_RotateWindows, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86RotationPB, Qt::Key_RotationPB, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86RotationKB, Qt::Key_RotationKB, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Save, Qt::Key_Save, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Send, Qt::Key_Send, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Spell, Qt::Key_Spell, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86SplitScreen, Qt::Key_SplitScreen, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Support, Qt::Key_Support, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86TaskPane, Qt::Key_TaskPane, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Terminal, Qt::Key_Terminal, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Tools, Qt::Key_Tools, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Travel, Qt::Key_Travel, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Video, Qt::Key_Video, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Word, Qt::Key_Word, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Xfer, Qt::Key_Xfer, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86ZoomIn, Qt::Key_ZoomIn, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86ZoomOut, Qt::Key_ZoomOut, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Away, Qt::Key_Away, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Messenger, Qt::Key_Messenger, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86WebCam, Qt::Key_WebCam, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86MailForward, Qt::Key_MailForward, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Pictures, Qt::Key_Pictures, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Music, Qt::Key_Music, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Battery, Qt::Key_Battery, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Bluetooth, Qt::Key_Bluetooth, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86WLAN, Qt::Key_WLAN, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86UWB, Qt::Key_UWB, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86AudioForward, Qt::Key_AudioForward, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86AudioRepeat, Qt::Key_AudioRepeat, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86AudioRandomPlay, Qt::Key_AudioRandomPlay, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Subtitle, Qt::Key_Subtitle, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86AudioCycleTrack, Qt::Key_AudioCycleTrack, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Time, Qt::Key_Time, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Select, Qt::Key_Select, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86View, Qt::Key_View, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86TopMenu, Qt::Key_TopMenu, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Bluetooth, Qt::Key_Bluetooth, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Suspend, Qt::Key_Suspend, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Hibernate, Qt::Key_Hibernate, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86TouchpadToggle, Qt::Key_TouchpadToggle, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86TouchpadOn, Qt::Key_TouchpadOn, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86TouchpadOff, Qt::Key_TouchpadOff, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86AudioMicMute, Qt::Key_MicMute, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Launch0, Qt::Key_Launch0, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Launch1, Qt::Key_Launch1, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Launch2, Qt::Key_Launch2, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Launch3, Qt::Key_Launch3, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Launch4, Qt::Key_Launch4, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Launch5, Qt::Key_Launch5, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Launch6, Qt::Key_Launch6, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Launch7, Qt::Key_Launch7, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Launch8, Qt::Key_Launch8, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86Launch9, Qt::Key_Launch9, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86LaunchA, Qt::Key_LaunchA, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86LaunchB, Qt::Key_LaunchB, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86LaunchC, Qt::Key_LaunchC, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86LaunchD, Qt::Key_LaunchD, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86LaunchE, Qt::Key_LaunchE, Qt::KeyboardModifiers()}, + {XKB_KEY_XF86LaunchF, Qt::Key_LaunchF, Qt::KeyboardModifiers()}, + + // Latin-1 + {XKB_KEY_exclam, Qt::Key_Exclam, Qt::KeyboardModifiers()}, + {XKB_KEY_quotedbl, Qt::Key_QuoteDbl, Qt::KeyboardModifiers()}, + {XKB_KEY_numbersign, Qt::Key_NumberSign, Qt::KeyboardModifiers()}, + {XKB_KEY_dollar, Qt::Key_Dollar, Qt::KeyboardModifiers()}, + {XKB_KEY_percent, Qt::Key_Percent, Qt::KeyboardModifiers()}, + {XKB_KEY_ampersand, Qt::Key_Ampersand, Qt::KeyboardModifiers()}, + {XKB_KEY_apostrophe, Qt::Key_Apostrophe, Qt::KeyboardModifiers()}, + {XKB_KEY_parenleft, Qt::Key_ParenLeft, Qt::KeyboardModifiers()}, + {XKB_KEY_parenright, Qt::Key_ParenRight, Qt::KeyboardModifiers()}, + {XKB_KEY_asterisk, Qt::Key_Asterisk, Qt::KeyboardModifiers()}, + {XKB_KEY_plus, Qt::Key_Plus, Qt::KeyboardModifiers()}, + {XKB_KEY_comma, Qt::Key_Comma, Qt::KeyboardModifiers()}, + {XKB_KEY_minus, Qt::Key_Minus, Qt::KeyboardModifiers()}, + {XKB_KEY_period, Qt::Key_Period, Qt::KeyboardModifiers()}, + {XKB_KEY_slash, Qt::Key_Slash, Qt::KeyboardModifiers()}, + {XKB_KEY_0, Qt::Key_0, Qt::KeyboardModifiers()}, + {XKB_KEY_1, Qt::Key_1, Qt::KeyboardModifiers()}, + {XKB_KEY_2, Qt::Key_2, Qt::KeyboardModifiers()}, + {XKB_KEY_3, Qt::Key_3, Qt::KeyboardModifiers()}, + {XKB_KEY_4, Qt::Key_4, Qt::KeyboardModifiers()}, + {XKB_KEY_5, Qt::Key_5, Qt::KeyboardModifiers()}, + {XKB_KEY_6, Qt::Key_6, Qt::KeyboardModifiers()}, + {XKB_KEY_7, Qt::Key_7, Qt::KeyboardModifiers()}, + {XKB_KEY_8, Qt::Key_8, Qt::KeyboardModifiers()}, + {XKB_KEY_9, Qt::Key_9, Qt::KeyboardModifiers()}, + {XKB_KEY_colon, Qt::Key_Colon, Qt::KeyboardModifiers()}, + {XKB_KEY_semicolon, Qt::Key_Semicolon, Qt::KeyboardModifiers()}, + {XKB_KEY_less, Qt::Key_Less, Qt::KeyboardModifiers()}, + {XKB_KEY_equal, Qt::Key_Equal, Qt::KeyboardModifiers()}, + {XKB_KEY_greater, Qt::Key_Greater, Qt::KeyboardModifiers()}, + {XKB_KEY_question, Qt::Key_Question, Qt::KeyboardModifiers()}, + {XKB_KEY_at, Qt::Key_At, Qt::KeyboardModifiers()}, + {XKB_KEY_A, Qt::Key_A, Qt::ShiftModifier}, + {XKB_KEY_B, Qt::Key_B, Qt::ShiftModifier}, + {XKB_KEY_C, Qt::Key_C, Qt::ShiftModifier}, + {XKB_KEY_D, Qt::Key_D, Qt::ShiftModifier}, + {XKB_KEY_E, Qt::Key_E, Qt::ShiftModifier}, + {XKB_KEY_F, Qt::Key_F, Qt::ShiftModifier}, + {XKB_KEY_G, Qt::Key_G, Qt::ShiftModifier}, + {XKB_KEY_H, Qt::Key_H, Qt::ShiftModifier}, + {XKB_KEY_I, Qt::Key_I, Qt::ShiftModifier}, + {XKB_KEY_J, Qt::Key_J, Qt::ShiftModifier}, + {XKB_KEY_K, Qt::Key_K, Qt::ShiftModifier}, + {XKB_KEY_L, Qt::Key_L, Qt::ShiftModifier}, + {XKB_KEY_M, Qt::Key_M, Qt::ShiftModifier}, + {XKB_KEY_N, Qt::Key_N, Qt::ShiftModifier}, + {XKB_KEY_O, Qt::Key_O, Qt::ShiftModifier}, + {XKB_KEY_P, Qt::Key_P, Qt::ShiftModifier}, + {XKB_KEY_Q, Qt::Key_Q, Qt::ShiftModifier}, + {XKB_KEY_R, Qt::Key_R, Qt::ShiftModifier}, + {XKB_KEY_S, Qt::Key_S, Qt::ShiftModifier}, + {XKB_KEY_T, Qt::Key_T, Qt::ShiftModifier}, + {XKB_KEY_U, Qt::Key_U, Qt::ShiftModifier}, + {XKB_KEY_V, Qt::Key_V, Qt::ShiftModifier}, + {XKB_KEY_W, Qt::Key_W, Qt::ShiftModifier}, + {XKB_KEY_X, Qt::Key_X, Qt::ShiftModifier}, + {XKB_KEY_Y, Qt::Key_Y, Qt::ShiftModifier}, + {XKB_KEY_Z, Qt::Key_Z, Qt::ShiftModifier}, + {XKB_KEY_bracketleft, Qt::Key_BracketLeft, Qt::KeyboardModifiers()}, + {XKB_KEY_backslash, Qt::Key_Backslash, Qt::KeyboardModifiers()}, + {XKB_KEY_bracketright, Qt::Key_BracketRight, Qt::KeyboardModifiers()}, + {XKB_KEY_asciicircum, Qt::Key_AsciiCircum, Qt::KeyboardModifiers()}, + {XKB_KEY_underscore, Qt::Key_Underscore, Qt::KeyboardModifiers()}, + {XKB_KEY_quoteleft, Qt::Key_QuoteLeft, Qt::KeyboardModifiers()}, + {XKB_KEY_a, Qt::Key_A, Qt::KeyboardModifiers()}, + {XKB_KEY_b, Qt::Key_B, Qt::KeyboardModifiers()}, + {XKB_KEY_c, Qt::Key_C, Qt::KeyboardModifiers()}, + {XKB_KEY_d, Qt::Key_D, Qt::KeyboardModifiers()}, + {XKB_KEY_e, Qt::Key_E, Qt::KeyboardModifiers()}, + {XKB_KEY_f, Qt::Key_F, Qt::KeyboardModifiers()}, + {XKB_KEY_g, Qt::Key_G, Qt::KeyboardModifiers()}, + {XKB_KEY_h, Qt::Key_H, Qt::KeyboardModifiers()}, + {XKB_KEY_i, Qt::Key_I, Qt::KeyboardModifiers()}, + {XKB_KEY_j, Qt::Key_J, Qt::KeyboardModifiers()}, + {XKB_KEY_k, Qt::Key_K, Qt::KeyboardModifiers()}, + {XKB_KEY_l, Qt::Key_L, Qt::KeyboardModifiers()}, + {XKB_KEY_m, Qt::Key_M, Qt::KeyboardModifiers()}, + {XKB_KEY_n, Qt::Key_N, Qt::KeyboardModifiers()}, + {XKB_KEY_o, Qt::Key_O, Qt::KeyboardModifiers()}, + {XKB_KEY_p, Qt::Key_P, Qt::KeyboardModifiers()}, + {XKB_KEY_q, Qt::Key_Q, Qt::KeyboardModifiers()}, + {XKB_KEY_r, Qt::Key_R, Qt::KeyboardModifiers()}, + {XKB_KEY_s, Qt::Key_S, Qt::KeyboardModifiers()}, + {XKB_KEY_t, Qt::Key_T, Qt::KeyboardModifiers()}, + {XKB_KEY_u, Qt::Key_U, Qt::KeyboardModifiers()}, + {XKB_KEY_v, Qt::Key_V, Qt::KeyboardModifiers()}, + {XKB_KEY_w, Qt::Key_W, Qt::KeyboardModifiers()}, + {XKB_KEY_x, Qt::Key_X, Qt::KeyboardModifiers()}, + {XKB_KEY_y, Qt::Key_Y, Qt::KeyboardModifiers()}, + {XKB_KEY_z, Qt::Key_Z, Qt::KeyboardModifiers()}, + {XKB_KEY_braceleft, Qt::Key_BraceLeft, Qt::KeyboardModifiers()}, + {XKB_KEY_bar, Qt::Key_Bar, Qt::KeyboardModifiers()}, + {XKB_KEY_braceright, Qt::Key_BraceRight, Qt::KeyboardModifiers()}, + {XKB_KEY_asciitilde, Qt::Key_AsciiTilde, Qt::KeyboardModifiers()}, + + {XKB_KEY_nobreakspace, Qt::Key_nobreakspace, Qt::KeyboardModifiers()}, + {XKB_KEY_exclamdown, Qt::Key_exclamdown, Qt::KeyboardModifiers()}, + {XKB_KEY_cent, Qt::Key_cent, Qt::KeyboardModifiers()}, + {XKB_KEY_sterling, Qt::Key_sterling, Qt::KeyboardModifiers()}, + {XKB_KEY_currency, Qt::Key_currency, Qt::KeyboardModifiers()}, + {XKB_KEY_yen, Qt::Key_yen, Qt::KeyboardModifiers()}, + {XKB_KEY_brokenbar, Qt::Key_brokenbar, Qt::KeyboardModifiers()}, + {XKB_KEY_section, Qt::Key_section, Qt::KeyboardModifiers()}, + {XKB_KEY_diaeresis, Qt::Key_diaeresis, Qt::KeyboardModifiers()}, + {XKB_KEY_copyright, Qt::Key_copyright, Qt::KeyboardModifiers()}, + {XKB_KEY_ordfeminine, Qt::Key_ordfeminine, Qt::KeyboardModifiers()}, + {XKB_KEY_guillemotleft, Qt::Key_guillemotleft, Qt::KeyboardModifiers()}, + {XKB_KEY_notsign, Qt::Key_notsign, Qt::KeyboardModifiers()}, + {XKB_KEY_hyphen, Qt::Key_hyphen, Qt::KeyboardModifiers()}, + {XKB_KEY_registered, Qt::Key_registered, Qt::KeyboardModifiers()}, + {XKB_KEY_macron, Qt::Key_macron, Qt::KeyboardModifiers()}, + {XKB_KEY_degree, Qt::Key_degree, Qt::KeyboardModifiers()}, + {XKB_KEY_plusminus, Qt::Key_plusminus, Qt::KeyboardModifiers()}, + {XKB_KEY_twosuperior, Qt::Key_twosuperior, Qt::KeyboardModifiers()}, + {XKB_KEY_threesuperior, Qt::Key_threesuperior, Qt::KeyboardModifiers()}, + {XKB_KEY_acute, Qt::Key_acute, Qt::KeyboardModifiers()}, + {XKB_KEY_mu, Qt::Key_micro, Qt::KeyboardModifiers()}, + {XKB_KEY_paragraph, Qt::Key_paragraph, Qt::KeyboardModifiers()}, + {XKB_KEY_periodcentered, Qt::Key_periodcentered, Qt::KeyboardModifiers()}, + {XKB_KEY_cedilla, Qt::Key_cedilla, Qt::KeyboardModifiers()}, + {XKB_KEY_onesuperior, Qt::Key_onesuperior, Qt::KeyboardModifiers()}, + {XKB_KEY_masculine, Qt::Key_masculine, Qt::KeyboardModifiers()}, + {XKB_KEY_guillemotright, Qt::Key_guillemotright, Qt::KeyboardModifiers()}, + {XKB_KEY_onequarter, Qt::Key_onequarter, Qt::KeyboardModifiers()}, + {XKB_KEY_onehalf, Qt::Key_onehalf, Qt::KeyboardModifiers()}, + {XKB_KEY_threequarters, Qt::Key_threequarters, Qt::KeyboardModifiers()}, + {XKB_KEY_questiondown, Qt::Key_questiondown, Qt::KeyboardModifiers()}, + {XKB_KEY_Agrave, Qt::Key_Agrave, Qt::ShiftModifier}, + {XKB_KEY_Aacute, Qt::Key_Aacute, Qt::ShiftModifier}, + {XKB_KEY_Acircumflex, Qt::Key_Acircumflex, Qt::ShiftModifier}, + {XKB_KEY_Atilde, Qt::Key_Atilde, Qt::ShiftModifier}, + {XKB_KEY_Adiaeresis, Qt::Key_Adiaeresis, Qt::ShiftModifier}, + {XKB_KEY_Aring, Qt::Key_Aring, Qt::ShiftModifier}, + {XKB_KEY_AE, Qt::Key_AE, Qt::ShiftModifier}, + {XKB_KEY_Ccedilla, Qt::Key_Ccedilla, Qt::ShiftModifier}, + {XKB_KEY_Egrave, Qt::Key_Egrave, Qt::ShiftModifier}, + {XKB_KEY_Eacute, Qt::Key_Eacute, Qt::ShiftModifier}, + {XKB_KEY_Ecircumflex, Qt::Key_Ecircumflex, Qt::ShiftModifier}, + {XKB_KEY_Ediaeresis, Qt::Key_Ediaeresis, Qt::ShiftModifier}, + {XKB_KEY_Igrave, Qt::Key_Igrave, Qt::ShiftModifier}, + {XKB_KEY_Iacute, Qt::Key_Iacute, Qt::ShiftModifier}, + {XKB_KEY_Icircumflex, Qt::Key_Icircumflex, Qt::ShiftModifier}, + {XKB_KEY_Idiaeresis, Qt::Key_Idiaeresis, Qt::ShiftModifier}, + {XKB_KEY_ETH, Qt::Key_ETH, Qt::ShiftModifier}, + {XKB_KEY_Ntilde, Qt::Key_Ntilde, Qt::ShiftModifier}, + {XKB_KEY_Ograve, Qt::Key_Ograve, Qt::ShiftModifier}, + {XKB_KEY_Oacute, Qt::Key_Oacute, Qt::ShiftModifier}, + {XKB_KEY_Ocircumflex, Qt::Key_Ocircumflex, Qt::ShiftModifier}, + {XKB_KEY_Otilde, Qt::Key_Otilde, Qt::ShiftModifier}, + {XKB_KEY_Odiaeresis, Qt::Key_Odiaeresis, Qt::ShiftModifier}, + {XKB_KEY_multiply, Qt::Key_multiply, Qt::ShiftModifier}, + {XKB_KEY_Ooblique, Qt::Key_Ooblique, Qt::ShiftModifier}, + {XKB_KEY_Ugrave, Qt::Key_Ugrave, Qt::ShiftModifier}, + {XKB_KEY_Uacute, Qt::Key_Uacute, Qt::ShiftModifier}, + {XKB_KEY_Ucircumflex, Qt::Key_Ucircumflex, Qt::ShiftModifier}, + {XKB_KEY_Udiaeresis, Qt::Key_Udiaeresis, Qt::ShiftModifier}, + {XKB_KEY_Yacute, Qt::Key_Yacute, Qt::ShiftModifier}, + {XKB_KEY_THORN, Qt::Key_THORN, Qt::ShiftModifier}, + {XKB_KEY_ssharp, Qt::Key_ssharp, Qt::KeyboardModifiers()}, + {XKB_KEY_agrave, Qt::Key_Agrave, Qt::KeyboardModifiers()}, + {XKB_KEY_aacute, Qt::Key_Aacute, Qt::KeyboardModifiers()}, + {XKB_KEY_acircumflex, Qt::Key_Acircumflex, Qt::KeyboardModifiers()}, + {XKB_KEY_atilde, Qt::Key_Atilde, Qt::KeyboardModifiers()}, + {XKB_KEY_adiaeresis, Qt::Key_Adiaeresis, Qt::KeyboardModifiers()}, + {XKB_KEY_aring, Qt::Key_Aring, Qt::KeyboardModifiers()}, + {XKB_KEY_ae, Qt::Key_AE, Qt::KeyboardModifiers()}, + {XKB_KEY_ccedilla, Qt::Key_Ccedilla, Qt::KeyboardModifiers()}, + {XKB_KEY_egrave, Qt::Key_Egrave, Qt::KeyboardModifiers()}, + {XKB_KEY_eacute, Qt::Key_Eacute, Qt::KeyboardModifiers()}, + {XKB_KEY_ecircumflex, Qt::Key_Ecircumflex, Qt::KeyboardModifiers()}, + {XKB_KEY_ediaeresis, Qt::Key_Ediaeresis, Qt::KeyboardModifiers()}, + {XKB_KEY_igrave, Qt::Key_Igrave, Qt::KeyboardModifiers()}, + {XKB_KEY_iacute, Qt::Key_Iacute, Qt::KeyboardModifiers()}, + {XKB_KEY_icircumflex, Qt::Key_Icircumflex, Qt::KeyboardModifiers()}, + {XKB_KEY_idiaeresis, Qt::Key_Idiaeresis, Qt::KeyboardModifiers()}, + {XKB_KEY_eth, Qt::Key_ETH, Qt::KeyboardModifiers()}, + {XKB_KEY_ntilde, Qt::Key_Ntilde, Qt::KeyboardModifiers()}, + {XKB_KEY_ograve, Qt::Key_Ograve, Qt::KeyboardModifiers()}, + {XKB_KEY_oacute, Qt::Key_Oacute, Qt::KeyboardModifiers()}, + {XKB_KEY_ocircumflex, Qt::Key_Ocircumflex, Qt::KeyboardModifiers()}, + {XKB_KEY_otilde, Qt::Key_Otilde, Qt::KeyboardModifiers()}, + {XKB_KEY_odiaeresis, Qt::Key_Odiaeresis, Qt::KeyboardModifiers()}, + {XKB_KEY_division, Qt::Key_division, Qt::KeyboardModifiers()}, + {XKB_KEY_ooblique, Qt::Key_Ooblique, Qt::KeyboardModifiers()}, + {XKB_KEY_ugrave, Qt::Key_Ugrave, Qt::KeyboardModifiers()}, + {XKB_KEY_uacute, Qt::Key_Uacute, Qt::KeyboardModifiers()}, + {XKB_KEY_ucircumflex, Qt::Key_Ucircumflex, Qt::KeyboardModifiers()}, + {XKB_KEY_udiaeresis, Qt::Key_Udiaeresis, Qt::KeyboardModifiers()}, + {XKB_KEY_yacute, Qt::Key_Yacute, Qt::KeyboardModifiers()}, + {XKB_KEY_thorn, Qt::Key_THORN, Qt::KeyboardModifiers()}, + {XKB_KEY_ydiaeresis, Qt::Key_ydiaeresis, Qt::KeyboardModifiers()}, + + // Numpad + {XKB_KEY_KP_0, Qt::Key_0, Qt::KeypadModifier}, + {XKB_KEY_KP_1, Qt::Key_1, Qt::KeypadModifier}, + {XKB_KEY_KP_2, Qt::Key_2, Qt::KeypadModifier}, + {XKB_KEY_KP_3, Qt::Key_3, Qt::KeypadModifier}, + {XKB_KEY_KP_4, Qt::Key_4, Qt::KeypadModifier}, + {XKB_KEY_KP_5, Qt::Key_5, Qt::KeypadModifier}, + {XKB_KEY_KP_6, Qt::Key_6, Qt::KeypadModifier}, + {XKB_KEY_KP_7, Qt::Key_7, Qt::KeypadModifier}, + {XKB_KEY_KP_8, Qt::Key_8, Qt::KeypadModifier}, + {XKB_KEY_KP_9, Qt::Key_9, Qt::KeypadModifier}, + {XKB_KEY_KP_Space, Qt::Key_Space, Qt::KeypadModifier}, + {XKB_KEY_KP_Tab, Qt::Key_Tab, Qt::KeypadModifier}, + {XKB_KEY_KP_Enter, Qt::Key_Enter, Qt::KeypadModifier}, + {XKB_KEY_KP_Home, Qt::Key_Home, Qt::KeypadModifier}, + {XKB_KEY_KP_Left, Qt::Key_Left, Qt::KeypadModifier}, + {XKB_KEY_KP_Up, Qt::Key_Up, Qt::KeypadModifier}, + {XKB_KEY_KP_Right, Qt::Key_Right, Qt::KeypadModifier}, + {XKB_KEY_KP_Down, Qt::Key_Down, Qt::KeypadModifier}, + {XKB_KEY_KP_Prior, Qt::Key_PageUp, Qt::KeypadModifier}, + {XKB_KEY_KP_Next, Qt::Key_PageDown, Qt::KeypadModifier}, + {XKB_KEY_KP_End, Qt::Key_End, Qt::KeypadModifier}, + {XKB_KEY_KP_Begin, Qt::Key_Clear, Qt::KeypadModifier}, + {XKB_KEY_KP_Insert, Qt::Key_Insert, Qt::KeypadModifier}, + {XKB_KEY_KP_Delete, Qt::Key_Delete, Qt::KeypadModifier}, + {XKB_KEY_KP_Equal, Qt::Key_Equal, Qt::KeypadModifier}, + {XKB_KEY_KP_Multiply, Qt::Key_Asterisk, Qt::KeypadModifier}, + {XKB_KEY_KP_Add, Qt::Key_Plus, Qt::KeypadModifier}, + {XKB_KEY_KP_Separator, Qt::Key_Comma, Qt::KeypadModifier}, + {XKB_KEY_KP_Subtract, Qt::Key_Minus, Qt::KeypadModifier}, + {XKB_KEY_KP_Decimal, Qt::Key_Period, Qt::KeypadModifier}, + {XKB_KEY_KP_Divide, Qt::Key_Slash, Qt::KeypadModifier}, + +}; + +void XkbTest::testToQtKey_data() +{ + QTest::addColumn("qt"); + QTest::addColumn("keySym"); + for (const auto rgQtToSymX : g_rgQtToSymX) { + const QByteArray row = QByteArray::number(rgQtToSymX.keySymX, 16); + QTest::newRow(row.constData()) << rgQtToSymX.keySymQt << rgQtToSymX.keySymX; + } +} + +void XkbTest::testToQtKey() +{ + Xkb xkb; + QFETCH(xkb_keysym_t, keySym); + QTEST(xkb.toQtKey(keySym), "qt"); +} + +void XkbTest::testFromQtKey_data() +{ + QTest::addColumn("keySym"); + QTest::addColumn("keyQt"); + for (const auto rgQtToSymX : g_rgQtToSymX) { + const QByteArray row = QByteArray::number(rgQtToSymX.keySymX, 16); + QTest::newRow(row.constData()) << rgQtToSymX.keySymX << QKeyCombination{rgQtToSymX.modifiers, rgQtToSymX.keySymQt}; + } +} + +void XkbTest::testFromQtKey() +{ + Xkb xkb; + QFETCH(xkb_keysym_t, keySym); + QFETCH(QKeyCombination, keyQt); + QList keys = xkb.keysymsFromQtKey(keyQt); + + QEXPECT_FAIL(QByteArray::number(XKB_KEY_Super_L, 16), "keysymsFromQtKey doesn't map super to meta", Continue); + QEXPECT_FAIL(QByteArray::number(XKB_KEY_Super_R, 16), "keysymsFromQtKey doesn't map super to meta", Continue); + QEXPECT_FAIL(QByteArray::number(XKB_KEY_Hyper_L, 16), "keysymsFromQtKey doesn't map hyper to meta", Continue); + QEXPECT_FAIL(QByteArray::number(XKB_KEY_Hyper_R, 16), "keysymsFromQtKey doesn't map hyper to meta", Continue); + + QVERIFY(keys.contains(keySym)); +} + +QTEST_MAIN(XkbTest) +#include "test_xkb.moc" diff --git a/local/recipes/kde/kwin/source/autotests/testutils.h b/local/recipes/kde/kwin/source/autotests/testutils.h new file mode 100644 index 0000000000..4c8d002fa3 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/testutils.h @@ -0,0 +1,54 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#ifndef TESTUTILS_H +#define TESTUTILS_H +// KWin +#include "effect/globals.h" +#include "effect/xcb.h" + +namespace +{ +static void forceXcb() +{ + qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("xcb")); +} +} + +namespace KWin +{ + +/** + * Wrapper to create an 0,0x10,10 input only window for testing purposes + */ +#ifndef NO_NONE_WINDOW +static xcb_window_t createWindow() +{ + xcb_window_t w = xcb_generate_id(connection()); + const uint32_t values[] = {true}; + xcb_create_window(connection(), 0, w, rootWindow(), + 0, 0, 10, 10, + 0, XCB_WINDOW_CLASS_INPUT_ONLY, XCB_COPY_FROM_PARENT, + XCB_CW_OVERRIDE_REDIRECT, values); + return w; +} +#endif + +/** + * casts XCB_WINDOW_NONE to uint32_t. Needed to make QCOMPARE working. + */ +#ifndef NO_NONE_WINDOW +static uint32_t noneWindow() +{ + return XCB_WINDOW_NONE; +} +#endif + +} // namespace + +#endif diff --git a/local/recipes/kde/kwin/source/autotests/wayland/CMakeLists.txt b/local/recipes/kde/kwin/source/autotests/wayland/CMakeLists.txt new file mode 100644 index 0000000000..ef03fe4825 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(client) +add_subdirectory(server) diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/CMakeLists.txt b/local/recipes/kde/kwin/source/autotests/wayland/client/CMakeLists.txt new file mode 100644 index 0000000000..a29adc254b --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/CMakeLists.txt @@ -0,0 +1,292 @@ +######################################################## +# Test WaylandOutput +######################################################## +set( testWaylandOutput_SRCS + test_wayland_output.cpp + ${PROJECT_SOURCE_DIR}/tests/fakeoutput.cpp + ) +add_executable(testWaylandOutput ${testWaylandOutput_SRCS}) +target_link_libraries( testWaylandOutput Qt::Test Qt::Gui Plasma::KWaylandClient kwin Wayland::Client Wayland::Server) +add_test(NAME kwayland-testWaylandOutput COMMAND testWaylandOutput) +ecm_mark_as_test(testWaylandOutput) + +######################################################## +# Test WaylandSurface +######################################################## +set( testWaylandSurface_SRCS + test_wayland_surface.cpp + ${PROJECT_SOURCE_DIR}/tests/fakeoutput.cpp + ) +add_executable(testWaylandSurface ${testWaylandSurface_SRCS}) +target_link_libraries( testWaylandSurface Qt::Test Qt::Gui Plasma::KWaylandClient kwin Wayland::Client Wayland::Server) +add_test(NAME kwayland-testWaylandSurface COMMAND testWaylandSurface) +ecm_mark_as_test(testWaylandSurface) + +######################################################## +# Test WaylandSeat +######################################################## +add_executable(testWaylandSeat) +set( testWaylandSeat_SRCS + test_wayland_seat.cpp + ) +qt6_generate_wayland_protocol_client_sources(testWaylandSeat + PRIVATE_CODE + FILES + ${WaylandProtocols_DATADIR}/unstable/pointer-gestures/pointer-gestures-unstable-v1.xml +) +target_sources(testWaylandSeat PRIVATE ${testWaylandSeat_SRCS}) +target_link_libraries( testWaylandSeat Qt::Test Qt::Gui Plasma::KWaylandClient kwin Wayland::Client Wayland::Server) +add_test(NAME kwayland-testWaylandSeat COMMAND testWaylandSeat) +ecm_mark_as_test(testWaylandSeat) + +######################################################## +# Test ShmPool +######################################################## +set( testShmPool_SRCS + test_shm_pool.cpp + ) +add_executable(testShmPool ${testShmPool_SRCS}) +target_link_libraries( testShmPool Qt::Test Qt::Gui Plasma::KWaylandClient kwin) +add_test(NAME kwayland-testShmPool COMMAND testShmPool) +ecm_mark_as_test(testShmPool) + +######################################################## +# Test SubSurface +######################################################## +set( testSubSurface_SRCS + test_wayland_subsurface.cpp + ) +add_executable(testSubSurface ${testSubSurface_SRCS}) +target_link_libraries( testSubSurface Qt::Test Qt::Gui Plasma::KWaylandClient kwin Wayland::Client) +add_test(NAME kwayland-testSubSurface COMMAND testSubSurface) +ecm_mark_as_test(testSubSurface) + +######################################################## +# Test Blur +######################################################## +set( testBlur_SRCS + test_wayland_blur.cpp + ) +add_executable(testBlur ${testBlur_SRCS}) +target_link_libraries( testBlur Qt::Test Qt::Gui Plasma::KWaylandClient kwin) +add_test(NAME kwayland-testBlur COMMAND testBlur) +ecm_mark_as_test(testBlur) + +######################################################## +# Test Contrast +######################################################## +set( testContrast_SRCS + test_wayland_contrast.cpp + ) +add_executable(testContrast ${testContrast_SRCS}) +target_link_libraries( testContrast Qt::Test Qt::Gui Plasma::KWaylandClient kwin Wayland::Client) +add_test(NAME kwayland-testContrast COMMAND testContrast) +ecm_mark_as_test(testContrast) + +######################################################## +# Test Slide +######################################################## +set( testSlide_SRCS + test_wayland_slide.cpp + ) +add_executable(testSlide ${testSlide_SRCS}) +target_link_libraries( testSlide Qt::Test Qt::Gui Plasma::KWaylandClient kwin Wayland::Client) +add_test(NAME kwayland-testSlide COMMAND testSlide) +ecm_mark_as_test(testSlide) + +######################################################## +# Test Window Management +######################################################## +set( testWindowmanagement_SRCS + test_wayland_windowmanagement.cpp + ) +add_executable(testWindowmanagement ${testWindowmanagement_SRCS}) +target_link_libraries( testWindowmanagement Qt::Test Qt::Gui Plasma::KWaylandClient kwin Wayland::Client) +add_test(NAME kwayland-testWindowmanagement COMMAND testWindowmanagement) +ecm_mark_as_test(testWindowmanagement) + +######################################################## +# Test DataSource +######################################################## +set( testDataSource_SRCS + test_datasource.cpp + ) +add_executable(testDataSource ${testDataSource_SRCS}) +target_link_libraries( testDataSource Qt::Test Qt::Gui Plasma::KWaylandClient kwin Wayland::Client) +add_test(NAME kwayland-testDataSource COMMAND testDataSource) +ecm_mark_as_test(testDataSource) + +######################################################## +# Test ServerSideDecoration +######################################################## +set( testServerSideDecoration_SRCS + test_server_side_decoration.cpp + ) +add_executable(testServerSideDecoration ${testServerSideDecoration_SRCS}) +qt6_generate_wayland_protocol_client_sources(testServerSideDecoration + PRIVATE_CODE + FILES + ${PLASMA_WAYLAND_PROTOCOLS_DIR}/server-decoration.xml +) +target_link_libraries( testServerSideDecoration Qt::Test Qt::Gui Plasma::KWaylandClient kwin Wayland::Client) +add_test(NAME kwayland-testServerSideDecoration COMMAND testServerSideDecoration) +ecm_mark_as_test(testServerSideDecoration) + +######################################################## +# Test PlasmaShell +######################################################## +set( testPlasmaShell_SRCS + test_plasmashell.cpp + ) +add_executable(testPlasmaShell ${testPlasmaShell_SRCS}) +target_link_libraries( testPlasmaShell Qt::Test Qt::Gui Plasma::KWaylandClient kwin Wayland::Client) +add_test(NAME kwayland-testPlasmaShell COMMAND testPlasmaShell) +ecm_mark_as_test(testPlasmaShell) + +######################################################## +# Test Shadow +######################################################## +set( testShadow_SRCS + test_shadow.cpp + ) +add_executable(testShadow ${testShadow_SRCS}) +target_link_libraries( testShadow Qt::Test Qt::Gui Plasma::KWaylandClient kwin) +add_test(NAME kwayland-testShadow COMMAND testShadow) +ecm_mark_as_test(testShadow) + +######################################################## +# Test TextInputV2 +######################################################## +set( testTextInputV2_SRCS + test_text_input_v2.cpp + ) +add_executable(testTextInputV2 ${testTextInputV2_SRCS}) +target_link_libraries( testTextInputV2 Qt::Test Qt::Gui Plasma::KWaylandClient kwin) +add_test(NAME kwayland-testTextInputV2 COMMAND testTextInputV2) +ecm_mark_as_test(testTextInputV2) + +######################################################## +# Test Error +######################################################## +set( testError_SRCS + test_error.cpp + ) +add_executable(testError ${testError_SRCS}) +target_link_libraries( testError Qt::Test Qt::Gui Plasma::KWaylandClient kwin Wayland::Client) +add_test(NAME kwayland-testError COMMAND testError) +ecm_mark_as_test(testError) + +######################################################## +# Test XdgForeign +######################################################## +set( testXdgForeign_SRCS + test_xdg_foreign.cpp + ) +add_executable(testXdgForeign ${testXdgForeign_SRCS}) +target_link_libraries( testXdgForeign Qt::Test Qt::Gui kwin Plasma::KWaylandClient Wayland::Client) +add_test(NAME kwayland-testXdgForeign COMMAND testXdgForeign) +ecm_mark_as_test(testXdgForeign) + +######################################################## +# Test XdgShell +######################################################## +set(testXdgShell_SRCS + test_xdg_shell.cpp + ${PROJECT_SOURCE_DIR}/tests/fakeoutput.cpp +) +add_executable(testXdgShell ${testXdgShell_SRCS}) +target_link_libraries( testXdgShell Qt::Test Qt::Gui kwin Plasma::KWaylandClient Wayland::Client) +add_test(NAME kwayland-testXdgShell COMMAND testXdgShell) +ecm_mark_as_test(testXdgShell) + +######################################################## +# Test Pointer Constraints +######################################################## +add_executable(testPointerConstraintsInterface test_pointer_constraints.cpp) +target_link_libraries( testPointerConstraintsInterface Qt::Test Qt::Gui kwin Plasma::KWaylandClient Wayland::Client) +add_test(NAME kwayland-testPointerConstraintsInterface COMMAND testPointerConstraintsInterface) +ecm_mark_as_test(testPointerConstraintsInterface) + + +######################################################## +# Test Filter +######################################################## +set( testFilter_SRCS + test_wayland_filter.cpp + ) +add_executable(testFilter ${testFilter_SRCS}) +target_link_libraries( testFilter Qt::Test Qt::Gui Plasma::KWaylandClient kwin Wayland::Server) +add_test(NAME kwayland-testFilter COMMAND testFilter) +ecm_mark_as_test(testFilter) + +######################################################## +# Test Appmenu +######################################################## +set( testAppmenu_SRCS + test_wayland_appmenu.cpp + ) +add_executable(testAppmenu ${testAppmenu_SRCS}) +target_link_libraries( testAppmenu Qt::Test Qt::Gui Plasma::KWaylandClient kwin) +add_test(NAME kwayland-testAppmenu COMMAND testAppmenu) +ecm_mark_as_test(testAppmenu) + +######################################################## +# Test Appmenu +######################################################## +set( testServerSideDecorationPalette_SRCS + test_server_side_decoration_palette.cpp + ) +add_executable(testServerSideDecorationPalette ${testServerSideDecorationPalette_SRCS}) +qt6_generate_wayland_protocol_client_sources(testServerSideDecorationPalette + PRIVATE_CODE + FILES + ${PLASMA_WAYLAND_PROTOCOLS_DIR}/server-decoration-palette.xml +) +target_link_libraries( testServerSideDecorationPalette Qt::Test Qt::Gui Plasma::KWaylandClient Wayland::Client kwin) +add_test(NAME kwayland-testServerSideDecorationPalette COMMAND testServerSideDecorationPalette) +ecm_mark_as_test(testServerSideDecorationPalette) + +######################################################## +# Test VirtualDesktop +######################################################## +set( testPlasmaVirtualDesktop_SRCS + test_plasma_virtual_desktop.cpp + ) +add_executable(testPlasmaVirtualDesktop ${testPlasmaVirtualDesktop_SRCS}) +target_link_libraries( testPlasmaVirtualDesktop Qt::Test Qt::Gui Plasma::KWaylandClient kwin) +add_test(NAME kwayland-testPlasmaVirtualDesktop COMMAND testPlasmaVirtualDesktop) +ecm_mark_as_test(testPlasmaVirtualDesktop) + +######################################################## +# Test Activities +######################################################## +set( testPlasmaActivities_SRCS + test_plasma_activities.cpp + ) +add_executable(testPlasmaActivities ${testPlasmaActivities_SRCS}) +target_link_libraries( testPlasmaActivities Qt::Test Qt::Gui Plasma::KWaylandClient kwin) +add_test(NAME kwayland-testPlasmaActivities COMMAND testPlasmaActivities) +ecm_mark_as_test(testPlasmaActivities) + +######################################################## +# Test XDG Output +######################################################## +set( testXdgOutput_SRCS + test_xdg_output.cpp + ${PROJECT_SOURCE_DIR}/tests/fakeoutput.cpp + ) +add_executable(testXdgOutput ${testXdgOutput_SRCS}) +target_link_libraries( testXdgOutput Qt::Test Qt::Gui Plasma::KWaylandClient kwin Wayland::Client Wayland::Server) +add_test(NAME kwayland-testXdgOutput COMMAND testXdgOutput) +ecm_mark_as_test(testXdgOutput) + +######################################################## +# Test XDG Decoration +######################################################## +set( testXdgdecoration_SRCS + test_xdg_decoration.cpp + ) +add_executable(testXdgDecoration ${testXdgdecoration_SRCS}) +target_link_libraries( testXdgDecoration Qt::Test Qt::Gui Plasma::KWaylandClient kwin Wayland::Client Wayland::Server) +add_test(NAME kwayland-testXdgDecoration COMMAND testXdgDecoration) +ecm_mark_as_test(testXdgDecoration) diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_datasource.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_datasource.cpp new file mode 100644 index 0000000000..f8453527d0 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_datasource.cpp @@ -0,0 +1,259 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +#include +#include + +#include "wayland/datadevicemanager.h" +#include "wayland/datasource.h" +#include "wayland/display.h" + +// KWayland +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/datadevicemanager.h" +#include "KWayland/Client/datasource.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/registry.h" + +// Wayland +#include + +class TestDataSource : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void init(); + void cleanup(); + + void testOffer(); + void testTargetAccepts_data(); + void testTargetAccepts(); + void testRequestSend(); + void testCancel(); + void testServerGet(); + +private: + KWin::Display *m_display = nullptr; + KWin::DataDeviceManagerInterface *m_dataDeviceManagerInterface = nullptr; + KWayland::Client::ConnectionThread *m_connection = nullptr; + KWayland::Client::DataDeviceManager *m_dataDeviceManager = nullptr; + KWayland::Client::EventQueue *m_queue = nullptr; + QThread *m_thread = nullptr; +}; + +static const QString s_socketName = QStringLiteral("kwayland-test-wayland-datasource-0"); + +void TestDataSource::init() +{ + using namespace KWin; + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + KWayland::Client::Registry registry; + QSignalSpy dataDeviceManagerSpy(®istry, &KWayland::Client::Registry::dataDeviceManagerAnnounced); + QVERIFY(!registry.eventQueue()); + registry.setEventQueue(m_queue); + QCOMPARE(registry.eventQueue(), m_queue); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + + m_dataDeviceManagerInterface = new DataDeviceManagerInterface(m_display, m_display); + + QVERIFY(dataDeviceManagerSpy.wait()); + m_dataDeviceManager = + registry.createDataDeviceManager(dataDeviceManagerSpy.first().first().value(), dataDeviceManagerSpy.first().last().value(), this); +} + +void TestDataSource::cleanup() +{ + if (m_dataDeviceManager) { + delete m_dataDeviceManager; + m_dataDeviceManager = nullptr; + } + if (m_queue) { + delete m_queue; + m_queue = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + delete m_connection; + m_connection = nullptr; + + delete m_display; + m_display = nullptr; +} + +void TestDataSource::testOffer() +{ + using namespace KWin; + + qRegisterMetaType(); + QSignalSpy dataSourceCreatedSpy(m_dataDeviceManagerInterface, &KWin::DataDeviceManagerInterface::dataSourceCreated); + + std::unique_ptr dataSource(m_dataDeviceManager->createDataSource()); + QVERIFY(dataSource->isValid()); + + QVERIFY(dataSourceCreatedSpy.wait()); + QCOMPARE(dataSourceCreatedSpy.count(), 1); + + QPointer serverDataSource = dataSourceCreatedSpy.first().first().value(); + QVERIFY(!serverDataSource.isNull()); + QCOMPARE(serverDataSource->mimeTypes().count(), 0); + + QSignalSpy offeredSpy(serverDataSource.data(), &KWin::AbstractDataSource::mimeTypeOffered); + + const QString plain = QStringLiteral("text/plain"); + QMimeDatabase db; + dataSource->offer(db.mimeTypeForName(plain)); + + QVERIFY(offeredSpy.wait()); + QCOMPARE(offeredSpy.count(), 1); + QCOMPARE(offeredSpy.last().first().toString(), plain); + QCOMPARE(serverDataSource->mimeTypes().count(), 1); + QCOMPARE(serverDataSource->mimeTypes().first(), plain); + + const QString html = QStringLiteral("text/html"); + dataSource->offer(db.mimeTypeForName(html)); + + QVERIFY(offeredSpy.wait()); + QCOMPARE(offeredSpy.count(), 2); + QCOMPARE(offeredSpy.first().first().toString(), plain); + QCOMPARE(offeredSpy.last().first().toString(), html); + QCOMPARE(serverDataSource->mimeTypes().count(), 2); + QCOMPARE(serverDataSource->mimeTypes().first(), plain); + QCOMPARE(serverDataSource->mimeTypes().last(), html); + + // try destroying the client side, should trigger a destroy of server side + dataSource.reset(); + QVERIFY(!serverDataSource.isNull()); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QVERIFY(serverDataSource.isNull()); +} + +void TestDataSource::testTargetAccepts_data() +{ + QTest::addColumn("mimeType"); + + QTest::newRow("empty") << QString(); + QTest::newRow("text/plain") << QStringLiteral("text/plain"); +} + +void TestDataSource::testTargetAccepts() +{ + using namespace KWin; + + QSignalSpy dataSourceCreatedSpy(m_dataDeviceManagerInterface, &KWin::DataDeviceManagerInterface::dataSourceCreated); + + std::unique_ptr dataSource(m_dataDeviceManager->createDataSource()); + QVERIFY(dataSource->isValid()); + + QSignalSpy targetAcceptsSpy(dataSource.get(), &KWayland::Client::DataSource::targetAccepts); + + QVERIFY(dataSourceCreatedSpy.wait()); + QCOMPARE(dataSourceCreatedSpy.count(), 1); + + QFETCH(QString, mimeType); + dataSourceCreatedSpy.first().first().value()->accept(mimeType); + + QVERIFY(targetAcceptsSpy.wait()); + QCOMPARE(targetAcceptsSpy.count(), 1); + QCOMPARE(targetAcceptsSpy.first().first().toString(), mimeType); +} + +void TestDataSource::testRequestSend() +{ + using namespace KWin; + + QSignalSpy dataSourceCreatedSpy(m_dataDeviceManagerInterface, &KWin::DataDeviceManagerInterface::dataSourceCreated); + + std::unique_ptr dataSource(m_dataDeviceManager->createDataSource()); + QVERIFY(dataSource->isValid()); + QSignalSpy sendRequestedSpy(dataSource.get(), &KWayland::Client::DataSource::sendDataRequested); + + const QString plain = QStringLiteral("text/plain"); + QVERIFY(dataSourceCreatedSpy.wait()); + QCOMPARE(dataSourceCreatedSpy.count(), 1); + QTemporaryFile file; + QVERIFY(file.open()); + dataSourceCreatedSpy.first().first().value()->requestData(plain, FileDescriptor(file.handle())); + + QVERIFY(sendRequestedSpy.wait()); + QCOMPARE(sendRequestedSpy.count(), 1); + QCOMPARE(sendRequestedSpy.first().first().toString(), plain); + QCOMPARE(sendRequestedSpy.first().last().value(), file.handle()); + QVERIFY(sendRequestedSpy.first().last().value() != -1); + + QFile writeFile; + QVERIFY(writeFile.open(sendRequestedSpy.first().last().value(), QFile::WriteOnly, QFileDevice::AutoCloseHandle)); + writeFile.close(); +} + +void TestDataSource::testCancel() +{ + using namespace KWin; + + QSignalSpy dataSourceCreatedSpy(m_dataDeviceManagerInterface, &KWin::DataDeviceManagerInterface::dataSourceCreated); + + std::unique_ptr dataSource(m_dataDeviceManager->createDataSource()); + QVERIFY(dataSource->isValid()); + QSignalSpy cancelledSpy(dataSource.get(), &KWayland::Client::DataSource::cancelled); + + QVERIFY(dataSourceCreatedSpy.wait()); + + QCOMPARE(cancelledSpy.count(), 0); + dataSourceCreatedSpy.first().first().value()->cancel(); + + QVERIFY(cancelledSpy.wait()); + QCOMPARE(cancelledSpy.count(), 1); +} + +void TestDataSource::testServerGet() +{ + using namespace KWin; + + QSignalSpy dataSourceCreatedSpy(m_dataDeviceManagerInterface, &KWin::DataDeviceManagerInterface::dataSourceCreated); + + std::unique_ptr dataSource(m_dataDeviceManager->createDataSource()); + QVERIFY(dataSource->isValid()); + + QVERIFY(!DataSourceInterface::get(nullptr)); + QVERIFY(dataSourceCreatedSpy.wait()); + auto d = dataSourceCreatedSpy.first().first().value(); + + QCOMPARE(DataSourceInterface::get(d->resource()), d); + QVERIFY(!DataSourceInterface::get(nullptr)); +} + +QTEST_GUILESS_MAIN(TestDataSource) +#include "test_datasource.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_error.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_error.cpp new file mode 100644 index 0000000000..5e26e8d1e3 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_error.cpp @@ -0,0 +1,138 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include + +// server +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/plasmashell.h" + +// client +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/plasmashell.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/surface.h" + +#include + +#include // For EPROTO + +using namespace KWin; + +class ErrorTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void init(); + void cleanup(); + + void testMultiplePlasmaShellSurfacesForSurface(); + +private: + KWin::Display *m_display = nullptr; + CompositorInterface *m_ci = nullptr; + PlasmaShellInterface *m_psi = nullptr; + KWayland::Client::ConnectionThread *m_connection = nullptr; + QThread *m_thread = nullptr; + KWayland::Client::EventQueue *m_queue = nullptr; + KWayland::Client::Compositor *m_compositor = nullptr; + KWayland::Client::PlasmaShell *m_plasmaShell = nullptr; +}; + +static const QString s_socketName = QStringLiteral("kwayland-test-error-0"); + +void ErrorTest::init() +{ + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + m_display->createShm(); + m_ci = new CompositorInterface(m_display, m_display); + m_psi = new PlasmaShellInterface(m_display, m_display); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + m_queue->setup(m_connection); + + KWayland::Client::Registry registry; + QSignalSpy interfacesAnnouncedSpy(®istry, &KWayland::Client::Registry::interfacesAnnounced); + registry.setEventQueue(m_queue); + registry.create(m_connection); + QVERIFY(registry.isValid()); + registry.setup(); + QVERIFY(interfacesAnnouncedSpy.wait()); + + m_compositor = + registry.createCompositor(registry.interface(KWayland::Client::Registry::Interface::Compositor).name, registry.interface(KWayland::Client::Registry::Interface::Compositor).version, this); + QVERIFY(m_compositor); + m_plasmaShell = registry.createPlasmaShell(registry.interface(KWayland::Client::Registry::Interface::PlasmaShell).name, + registry.interface(KWayland::Client::Registry::Interface::PlasmaShell).version, + this); + QVERIFY(m_plasmaShell); +} + +void ErrorTest::cleanup() +{ +#define CLEANUP(variable) \ + if (variable) { \ + delete variable; \ + variable = nullptr; \ + } + CLEANUP(m_plasmaShell) + CLEANUP(m_compositor) + CLEANUP(m_queue) + if (m_connection) { + m_connection->deleteLater(); + m_connection = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + CLEANUP(m_display) +#undef CLEANUP + // these are the children of the display + m_psi = nullptr; + m_ci = nullptr; +} + +void ErrorTest::testMultiplePlasmaShellSurfacesForSurface() +{ + // this test verifies that creating two ShellSurfaces for the same Surface triggers a protocol error + QSignalSpy errorSpy(m_connection, &KWayland::Client::ConnectionThread::errorOccurred); + // PlasmaShell is too smart and doesn't allow us to create a second PlasmaShellSurface + // thus we need to cheat by creating a surface manually + auto surface = wl_compositor_create_surface(*m_compositor); + std::unique_ptr shellSurface1(m_plasmaShell->createSurface(surface)); + std::unique_ptr shellSurface2(m_plasmaShell->createSurface(surface)); + QVERIFY(!m_connection->hasError()); + QVERIFY(errorSpy.wait()); + QVERIFY(m_connection->hasError()); + QCOMPARE(m_connection->errorCode(), EPROTO); + wl_surface_destroy(surface); +} + +QTEST_GUILESS_MAIN(ErrorTest) +#include "test_error.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_plasma_activities.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_plasma_activities.cpp new file mode 100644 index 0000000000..e3e5d99193 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_plasma_activities.cpp @@ -0,0 +1,195 @@ +/* + SPDX-FileCopyrightText: 2021 Kevin Ottens + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// KWin +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/plasmawindowmanagement.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/plasmawindowmanagement.h" +#include "KWayland/Client/region.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/surface.h" + +class TestActivities : public QObject +{ + Q_OBJECT +public: + explicit TestActivities(QObject *parent = nullptr); +private Q_SLOTS: + void init(); + void cleanup(); + + void testEnterLeaveActivity(); + +private: + KWin::Display *m_display; + KWin::CompositorInterface *m_compositorInterface; + KWin::PlasmaWindowManagementInterface *m_windowManagementInterface; + KWin::PlasmaWindowInterface *m_windowInterface; + + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::Compositor *m_compositor; + KWayland::Client::EventQueue *m_queue; + KWayland::Client::PlasmaWindowManagement *m_windowManagement; + KWayland::Client::PlasmaWindow *m_window; + + QThread *m_thread; +}; + +static const QString s_socketName = QStringLiteral("kwayland-test-wayland-activities-0"); + +TestActivities::TestActivities(QObject *parent) + : QObject(parent) + , m_display(nullptr) + , m_compositorInterface(nullptr) + , m_connection(nullptr) + , m_compositor(nullptr) + , m_queue(nullptr) + , m_thread(nullptr) +{ +} + +void TestActivities::init() +{ + using namespace KWin; + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + KWayland::Client::Registry registry; + QSignalSpy compositorSpy(®istry, &KWayland::Client::Registry::compositorAnnounced); + + QSignalSpy windowManagementSpy(®istry, &KWayland::Client::Registry::plasmaWindowManagementAnnounced); + + QVERIFY(!registry.eventQueue()); + registry.setEventQueue(m_queue); + QCOMPARE(registry.eventQueue(), m_queue); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + + m_compositorInterface = new CompositorInterface(m_display, m_display); + QVERIFY(compositorSpy.wait()); + m_compositor = registry.createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + + m_windowManagementInterface = new PlasmaWindowManagementInterface(m_display, m_display); + + QVERIFY(windowManagementSpy.wait()); + m_windowManagement = + registry.createPlasmaWindowManagement(windowManagementSpy.first().first().value(), windowManagementSpy.first().last().value(), this); + + QSignalSpy windowSpy(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated); + m_windowInterface = m_windowManagementInterface->createWindow(this, QUuid::createUuid()); + m_windowInterface->setPid(1337); + + QVERIFY(windowSpy.wait()); + m_window = windowSpy.first().first().value(); +} + +void TestActivities::cleanup() +{ +#define CLEANUP(variable) \ + if (variable) { \ + delete variable; \ + variable = nullptr; \ + } + CLEANUP(m_compositor) + CLEANUP(m_windowInterface) + CLEANUP(m_windowManagement) + CLEANUP(m_queue) + if (m_connection) { + m_connection->deleteLater(); + m_connection = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + CLEANUP(m_compositorInterface) + CLEANUP(m_windowManagementInterface) + CLEANUP(m_display) +#undef CLEANUP +} + +void TestActivities::testEnterLeaveActivity() +{ + QSignalSpy enterRequestedSpy(m_windowInterface, &KWin::PlasmaWindowInterface::enterPlasmaActivityRequested); + m_window->requestEnterActivity(QStringLiteral("0-1")); + enterRequestedSpy.wait(); + + QCOMPARE(enterRequestedSpy.takeFirst().at(0).toString(), QStringLiteral("0-1")); + + QSignalSpy activityEnteredSpy(m_window, &KWayland::Client::PlasmaWindow::plasmaActivityEntered); + + // agree to the request + m_windowInterface->addPlasmaActivity(QStringLiteral("0-1")); + QCOMPARE(m_windowInterface->plasmaActivities().length(), 1); + QCOMPARE(m_windowInterface->plasmaActivities().first(), QStringLiteral("0-1")); + + // check if the client received the enter + activityEnteredSpy.wait(); + QCOMPARE(activityEnteredSpy.takeFirst().at(0).toString(), QStringLiteral("0-1")); + QCOMPARE(m_window->plasmaActivities().length(), 1); + QCOMPARE(m_window->plasmaActivities().first(), QStringLiteral("0-1")); + + // add another activity, server side + m_windowInterface->addPlasmaActivity(QStringLiteral("0-3")); + activityEnteredSpy.wait(); + QCOMPARE(activityEnteredSpy.takeFirst().at(0).toString(), QStringLiteral("0-3")); + QCOMPARE(m_windowInterface->plasmaActivities().length(), 2); + QCOMPARE(m_window->plasmaActivities().length(), 2); + QCOMPARE(m_window->plasmaActivities()[1], QStringLiteral("0-3")); + + // remove an activity + QSignalSpy leaveRequestedSpy(m_windowInterface, &KWin::PlasmaWindowInterface::leavePlasmaActivityRequested); + m_window->requestLeaveActivity(QStringLiteral("0-1")); + leaveRequestedSpy.wait(); + + QCOMPARE(leaveRequestedSpy.takeFirst().at(0).toString(), QStringLiteral("0-1")); + + QSignalSpy activityLeftSpy(m_window, &KWayland::Client::PlasmaWindow::plasmaActivityLeft); + + // agree to the request + m_windowInterface->removePlasmaActivity(QStringLiteral("0-1")); + QCOMPARE(m_windowInterface->plasmaActivities().length(), 1); + QCOMPARE(m_windowInterface->plasmaActivities().first(), QStringLiteral("0-3")); + + // check if the client received the leave + activityLeftSpy.wait(); + QCOMPARE(activityLeftSpy.takeFirst().at(0).toString(), QStringLiteral("0-1")); + QCOMPARE(m_window->plasmaActivities().length(), 1); + QCOMPARE(m_window->plasmaActivities().first(), QStringLiteral("0-3")); +} + +QTEST_GUILESS_MAIN(TestActivities) +#include "test_plasma_activities.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_plasma_virtual_desktop.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_plasma_virtual_desktop.cpp new file mode 100644 index 0000000000..0e9105b162 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_plasma_virtual_desktop.cpp @@ -0,0 +1,519 @@ +/* + SPDX-FileCopyrightText: 2018 Marco Martin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// KWin +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/plasmavirtualdesktop.h" +#include "wayland/plasmawindowmanagement.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/plasmavirtualdesktop.h" +#include "KWayland/Client/plasmawindowmanagement.h" +#include "KWayland/Client/region.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/surface.h" + +class TestVirtualDesktop : public QObject +{ + Q_OBJECT +public: + explicit TestVirtualDesktop(QObject *parent = nullptr); +private Q_SLOTS: + void init(); + void cleanup(); + + void testCreate(); + void testSetRows(); + void testConnectNewClient(); + void testDestroy(); + void testActivate(); + + void testEnterLeaveDesktop(); + void testAllDesktops(); + void testCreateRequested(); + void testRemoveRequested(); + +private: + KWin::Display *m_display; + KWin::CompositorInterface *m_compositorInterface; + KWin::PlasmaVirtualDesktopManagementInterface *m_plasmaVirtualDesktopManagementInterface; + KWin::PlasmaWindowManagementInterface *m_windowManagementInterface; + KWin::PlasmaWindowInterface *m_windowInterface; + + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::Compositor *m_compositor; + KWayland::Client::PlasmaVirtualDesktopManagement *m_plasmaVirtualDesktopManagement; + KWayland::Client::EventQueue *m_queue; + KWayland::Client::PlasmaWindowManagement *m_windowManagement; + KWayland::Client::PlasmaWindow *m_window; + + QThread *m_thread; +}; + +static const QString s_socketName = QStringLiteral("kwayland-test-wayland-virtual-desktop-0"); + +TestVirtualDesktop::TestVirtualDesktop(QObject *parent) + : QObject(parent) + , m_display(nullptr) + , m_compositorInterface(nullptr) + , m_connection(nullptr) + , m_compositor(nullptr) + , m_queue(nullptr) + , m_thread(nullptr) +{ +} + +void TestVirtualDesktop::init() +{ + using namespace KWin; + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + KWayland::Client::Registry registry; + QSignalSpy compositorSpy(®istry, &KWayland::Client::Registry::compositorAnnounced); + + QSignalSpy plasmaVirtualDesktopManagementSpy(®istry, &KWayland::Client::Registry::plasmaVirtualDesktopManagementAnnounced); + + QSignalSpy windowManagementSpy(®istry, &KWayland::Client::Registry::plasmaWindowManagementAnnounced); + + QVERIFY(!registry.eventQueue()); + registry.setEventQueue(m_queue); + QCOMPARE(registry.eventQueue(), m_queue); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + + m_compositorInterface = new CompositorInterface(m_display, m_display); + QVERIFY(compositorSpy.wait()); + m_compositor = registry.createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + + m_plasmaVirtualDesktopManagementInterface = new PlasmaVirtualDesktopManagementInterface(m_display, m_display); + + QVERIFY(plasmaVirtualDesktopManagementSpy.wait()); + m_plasmaVirtualDesktopManagement = registry.createPlasmaVirtualDesktopManagement(plasmaVirtualDesktopManagementSpy.first().first().value(), + plasmaVirtualDesktopManagementSpy.first().last().value(), + this); + + m_windowManagementInterface = new PlasmaWindowManagementInterface(m_display, m_display); + m_windowManagementInterface->setPlasmaVirtualDesktopManagementInterface(m_plasmaVirtualDesktopManagementInterface); + + QVERIFY(windowManagementSpy.wait()); + m_windowManagement = + registry.createPlasmaWindowManagement(windowManagementSpy.first().first().value(), windowManagementSpy.first().last().value(), this); + + QSignalSpy windowSpy(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated); + m_windowInterface = m_windowManagementInterface->createWindow(this, QUuid::createUuid()); + m_windowInterface->setPid(1337); + + QVERIFY(windowSpy.wait()); + m_window = windowSpy.first().first().value(); +} + +void TestVirtualDesktop::cleanup() +{ +#define CLEANUP(variable) \ + if (variable) { \ + delete variable; \ + variable = nullptr; \ + } + CLEANUP(m_compositor) + CLEANUP(m_plasmaVirtualDesktopManagement) + CLEANUP(m_windowInterface) + CLEANUP(m_windowManagement) + CLEANUP(m_queue) + if (m_connection) { + m_connection->deleteLater(); + m_connection = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + CLEANUP(m_compositorInterface) + CLEANUP(m_plasmaVirtualDesktopManagementInterface) + CLEANUP(m_windowManagementInterface) + CLEANUP(m_display) +#undef CLEANUP +} + +void TestVirtualDesktop::testCreate() +{ + QSignalSpy desktopCreatedSpy(m_plasmaVirtualDesktopManagement, &KWayland::Client::PlasmaVirtualDesktopManagement::desktopCreated); + QSignalSpy managementDoneSpy(m_plasmaVirtualDesktopManagement, &KWayland::Client::PlasmaVirtualDesktopManagement::done); + + // on this createDesktop bind() isn't called already, the desktopadded signals will be sent after bind happened + KWin::PlasmaVirtualDesktopInterface *desktop1Int = m_plasmaVirtualDesktopManagementInterface->createDesktop(QStringLiteral("0-1")); + desktop1Int->setName("Desktop 1"); + + QVERIFY(desktopCreatedSpy.wait()); + QList arguments = desktopCreatedSpy.takeFirst(); + QCOMPARE(arguments.at(0).toString(), QStringLiteral("0-1")); + QCOMPARE(arguments.at(1).toUInt(), (quint32)0); + m_plasmaVirtualDesktopManagementInterface->scheduleDone(); + QVERIFY(managementDoneSpy.wait()); + + QCOMPARE(m_plasmaVirtualDesktopManagement->desktops().length(), 1); + + KWayland::Client::PlasmaVirtualDesktop *desktop1 = m_plasmaVirtualDesktopManagement->desktops().first(); + QSignalSpy desktop1DoneSpy(desktop1, &KWayland::Client::PlasmaVirtualDesktop::done); + desktop1Int->sendDone(); + QVERIFY(desktop1DoneSpy.wait()); + + QCOMPARE(desktop1->id(), QStringLiteral("0-1")); + QCOMPARE(desktop1->name(), QStringLiteral("Desktop 1")); + + // on those createDesktop the bind will already be done + KWin::PlasmaVirtualDesktopInterface *desktop2Int = m_plasmaVirtualDesktopManagementInterface->createDesktop(QStringLiteral("0-2")); + desktop2Int->setName("Desktop 2"); + QVERIFY(desktopCreatedSpy.wait()); + arguments = desktopCreatedSpy.takeFirst(); + QCOMPARE(arguments.at(0).toString(), QStringLiteral("0-2")); + QCOMPARE(arguments.at(1).toUInt(), (quint32)1); + QCOMPARE(m_plasmaVirtualDesktopManagement->desktops().length(), 2); + + KWin::PlasmaVirtualDesktopInterface *desktop3Int = m_plasmaVirtualDesktopManagementInterface->createDesktop(QStringLiteral("0-3")); + desktop3Int->setName("Desktop 3"); + QVERIFY(desktopCreatedSpy.wait()); + arguments = desktopCreatedSpy.takeFirst(); + QCOMPARE(arguments.at(0).toString(), QStringLiteral("0-3")); + QCOMPARE(m_plasmaVirtualDesktopManagement->desktops().length(), 3); + + m_plasmaVirtualDesktopManagementInterface->scheduleDone(); + QVERIFY(managementDoneSpy.wait()); + + // get the clients + KWayland::Client::PlasmaVirtualDesktop *desktop2 = m_plasmaVirtualDesktopManagement->desktops()[1]; + QSignalSpy desktop2DoneSpy(desktop2, &KWayland::Client::PlasmaVirtualDesktop::done); + desktop2Int->sendDone(); + QVERIFY(desktop2DoneSpy.wait()); + + KWayland::Client::PlasmaVirtualDesktop *desktop3 = m_plasmaVirtualDesktopManagement->desktops()[2]; + QSignalSpy desktop3DoneSpy(desktop3, &KWayland::Client::PlasmaVirtualDesktop::done); + desktop3Int->sendDone(); + QVERIFY(desktop3DoneSpy.wait()); + + QCOMPARE(desktop1->id(), QStringLiteral("0-1")); + QCOMPARE(desktop1->name(), QStringLiteral("Desktop 1")); + + QCOMPARE(desktop2->id(), QStringLiteral("0-2")); + QCOMPARE(desktop2->name(), QStringLiteral("Desktop 2")); + + QCOMPARE(desktop3->id(), QStringLiteral("0-3")); + QCOMPARE(desktop3->name(), QStringLiteral("Desktop 3")); + + // coherence of order between client and server + QCOMPARE(m_plasmaVirtualDesktopManagementInterface->desktops().length(), 3); + QCOMPARE(m_plasmaVirtualDesktopManagement->desktops().length(), 3); + + for (int i = 0; i < m_plasmaVirtualDesktopManagement->desktops().length(); ++i) { + QCOMPARE(m_plasmaVirtualDesktopManagementInterface->desktops().at(i)->id(), m_plasmaVirtualDesktopManagement->desktops().at(i)->id()); + } +} + +void TestVirtualDesktop::testSetRows() +{ + // rebuild some desktops + testCreate(); + + QSignalSpy rowsChangedSpy(m_plasmaVirtualDesktopManagement, &KWayland::Client::PlasmaVirtualDesktopManagement::rowsChanged); + + m_plasmaVirtualDesktopManagementInterface->setRows(3); + QVERIFY(rowsChangedSpy.wait()); + QCOMPARE(m_plasmaVirtualDesktopManagement->rows(), 3); +} + +void TestVirtualDesktop::testConnectNewClient() +{ + // rebuild some desktops + testCreate(); + + KWayland::Client::Registry registry; + QVERIFY(!registry.eventQueue()); + registry.setEventQueue(m_queue); + QCOMPARE(registry.eventQueue(), m_queue); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + + QSignalSpy plasmaVirtualDesktopManagementSpy(®istry, &KWayland::Client::Registry::plasmaVirtualDesktopManagementAnnounced); + + QVERIFY(plasmaVirtualDesktopManagementSpy.wait()); + + KWayland::Client::PlasmaVirtualDesktopManagement *otherPlasmaVirtualDesktopManagement = + registry.createPlasmaVirtualDesktopManagement(plasmaVirtualDesktopManagementSpy.first().first().value(), + plasmaVirtualDesktopManagementSpy.first().last().value(), + this); + + QSignalSpy managementDoneSpy(otherPlasmaVirtualDesktopManagement, &KWayland::Client::PlasmaVirtualDesktopManagement::done); + + QVERIFY(managementDoneSpy.wait()); + QCOMPARE(otherPlasmaVirtualDesktopManagement->desktops().length(), 3); + + delete otherPlasmaVirtualDesktopManagement; +} + +void TestVirtualDesktop::testDestroy() +{ + // rebuild some desktops + testCreate(); + + KWin::PlasmaVirtualDesktopInterface *desktop1Int = m_plasmaVirtualDesktopManagementInterface->desktops().first(); + KWayland::Client::PlasmaVirtualDesktop *desktop1 = m_plasmaVirtualDesktopManagement->desktops().first(); + + QSignalSpy desktop1IntDestroyedSpy(desktop1Int, &QObject::destroyed); + QSignalSpy desktop1DestroyedSpy(desktop1, &QObject::destroyed); + QSignalSpy desktop1RemovedSpy(desktop1, &KWayland::Client::PlasmaVirtualDesktop::removed); + m_plasmaVirtualDesktopManagementInterface->removeDesktop(QStringLiteral("0-1")); + + // test that both server and client desktoip interfaces go away + QVERIFY(!desktop1IntDestroyedSpy.isEmpty()); + QVERIFY(desktop1RemovedSpy.wait()); + QVERIFY(desktop1DestroyedSpy.wait()); + + // coherence of order between client and server + QCOMPARE(m_plasmaVirtualDesktopManagementInterface->desktops().length(), 2); + QCOMPARE(m_plasmaVirtualDesktopManagement->desktops().length(), 2); + + for (int i = 0; i < m_plasmaVirtualDesktopManagement->desktops().length(); ++i) { + QCOMPARE(m_plasmaVirtualDesktopManagementInterface->desktops().at(i)->id(), m_plasmaVirtualDesktopManagement->desktops().at(i)->id()); + } + + // Test the desktopRemoved signal of the manager, remove another desktop as the signals can't be tested at the same time + QSignalSpy desktopManagerRemovedSpy(m_plasmaVirtualDesktopManagement, &KWayland::Client::PlasmaVirtualDesktopManagement::desktopRemoved); + m_plasmaVirtualDesktopManagementInterface->removeDesktop(QStringLiteral("0-2")); + QVERIFY(desktopManagerRemovedSpy.wait()); + QCOMPARE(desktopManagerRemovedSpy.takeFirst().at(0).toString(), QStringLiteral("0-2")); + + QCOMPARE(m_plasmaVirtualDesktopManagementInterface->desktops().length(), 1); + QCOMPARE(m_plasmaVirtualDesktopManagement->desktops().length(), 1); +} + +void TestVirtualDesktop::testActivate() +{ + // rebuild some desktops + testCreate(); + + KWin::PlasmaVirtualDesktopInterface *desktop1Int = m_plasmaVirtualDesktopManagementInterface->desktops().first(); + KWayland::Client::PlasmaVirtualDesktop *desktop1 = m_plasmaVirtualDesktopManagement->desktops().first(); + QVERIFY(desktop1->isActive()); + QVERIFY(desktop1Int->isActive()); + + KWin::PlasmaVirtualDesktopInterface *desktop2Int = m_plasmaVirtualDesktopManagementInterface->desktops()[1]; + KWayland::Client::PlasmaVirtualDesktop *desktop2 = m_plasmaVirtualDesktopManagement->desktops()[1]; + QVERIFY(!desktop2Int->isActive()); + + QSignalSpy requestActivateSpy(desktop2Int, &KWin::PlasmaVirtualDesktopInterface::activateRequested); + QSignalSpy activatedSpy(desktop2, &KWayland::Client::PlasmaVirtualDesktop::activated); + + desktop2->requestActivate(); + QVERIFY(requestActivateSpy.wait()); + + // This simulates a compositor which supports only one active desktop at a time + for (auto *deskInt : m_plasmaVirtualDesktopManagementInterface->desktops()) { + if (deskInt->id() == desktop2->id()) { + deskInt->setActive(true); + } else { + deskInt->setActive(false); + } + } + QVERIFY(activatedSpy.wait()); + + // correct state in the server + QVERIFY(desktop2Int->isActive()); + QVERIFY(!desktop1Int->isActive()); + // correct state in the client + QVERIFY(desktop2Int->isActive()); + QVERIFY(!desktop1Int->isActive()); + + // Test the deactivated signal + QSignalSpy deactivatedSpy(desktop2, &KWayland::Client::PlasmaVirtualDesktop::deactivated); + + for (auto *deskInt : m_plasmaVirtualDesktopManagementInterface->desktops()) { + if (deskInt->id() == desktop1->id()) { + deskInt->setActive(true); + } else { + deskInt->setActive(false); + } + } + QVERIFY(deactivatedSpy.wait()); +} + +void TestVirtualDesktop::testEnterLeaveDesktop() +{ + testCreate(); + + QSignalSpy enterRequestedSpy(m_windowInterface, &KWin::PlasmaWindowInterface::enterPlasmaVirtualDesktopRequested); + m_window->requestEnterVirtualDesktop(QStringLiteral("0-1")); + QVERIFY(enterRequestedSpy.wait()); + + QCOMPARE(enterRequestedSpy.takeFirst().at(0).toString(), QStringLiteral("0-1")); + + QSignalSpy virtualDesktopEnteredSpy(m_window, &KWayland::Client::PlasmaWindow::plasmaVirtualDesktopEntered); + + // agree to the request + m_windowInterface->addPlasmaVirtualDesktop(QStringLiteral("0-1")); + QCOMPARE(m_windowInterface->plasmaVirtualDesktops().length(), 1); + QCOMPARE(m_windowInterface->plasmaVirtualDesktops().first(), QStringLiteral("0-1")); + + // check if the client received the enter + QVERIFY(virtualDesktopEnteredSpy.wait()); + QCOMPARE(virtualDesktopEnteredSpy.takeFirst().at(0).toString(), QStringLiteral("0-1")); + QCOMPARE(m_window->plasmaVirtualDesktops().length(), 1); + QCOMPARE(m_window->plasmaVirtualDesktops().first(), QStringLiteral("0-1")); + + // add another desktop, server side + m_windowInterface->addPlasmaVirtualDesktop(QStringLiteral("0-3")); + QVERIFY(virtualDesktopEnteredSpy.wait()); + QCOMPARE(virtualDesktopEnteredSpy.takeFirst().at(0).toString(), QStringLiteral("0-3")); + QCOMPARE(m_windowInterface->plasmaVirtualDesktops().length(), 2); + QCOMPARE(m_window->plasmaVirtualDesktops().length(), 2); + QCOMPARE(m_window->plasmaVirtualDesktops()[1], QStringLiteral("0-3")); + + // try to add an invalid desktop + m_windowInterface->addPlasmaVirtualDesktop(QStringLiteral("invalid")); + QCOMPARE(m_window->plasmaVirtualDesktops().length(), 2); + + // remove a desktop + QSignalSpy leaveRequestedSpy(m_windowInterface, &KWin::PlasmaWindowInterface::leavePlasmaVirtualDesktopRequested); + m_window->requestLeaveVirtualDesktop(QStringLiteral("0-1")); + QVERIFY(leaveRequestedSpy.wait()); + + QCOMPARE(leaveRequestedSpy.takeFirst().at(0).toString(), QStringLiteral("0-1")); + + QSignalSpy virtualDesktopLeftSpy(m_window, &KWayland::Client::PlasmaWindow::plasmaVirtualDesktopLeft); + + // agree to the request + m_windowInterface->removePlasmaVirtualDesktop(QStringLiteral("0-1")); + QCOMPARE(m_windowInterface->plasmaVirtualDesktops().length(), 1); + QCOMPARE(m_windowInterface->plasmaVirtualDesktops().first(), QStringLiteral("0-3")); + + // check if the client received the leave + QVERIFY(virtualDesktopLeftSpy.wait()); + QCOMPARE(virtualDesktopLeftSpy.takeFirst().at(0).toString(), QStringLiteral("0-1")); + QCOMPARE(m_window->plasmaVirtualDesktops().length(), 1); + QCOMPARE(m_window->plasmaVirtualDesktops().first(), QStringLiteral("0-3")); + + // Destroy desktop 2 + m_plasmaVirtualDesktopManagementInterface->removeDesktop(QStringLiteral("0-3")); + // the window should receive a left signal from the destroyed desktop + QVERIFY(virtualDesktopLeftSpy.wait()); + + QCOMPARE(m_window->plasmaVirtualDesktops().length(), 0); +} + +void TestVirtualDesktop::testAllDesktops() +{ + testCreate(); + QSignalSpy virtualDesktopEnteredSpy(m_window, &KWayland::Client::PlasmaWindow::plasmaVirtualDesktopEntered); + QSignalSpy virtualDesktopLeftSpy(m_window, &KWayland::Client::PlasmaWindow::plasmaVirtualDesktopLeft); + + // in the beginning the window is on desktop 1 and desktop 3 + m_windowInterface->addPlasmaVirtualDesktop(QStringLiteral("0-1")); + m_windowInterface->addPlasmaVirtualDesktop(QStringLiteral("0-3")); + QVERIFY(virtualDesktopEnteredSpy.wait()); + + // setting on all desktops + QCOMPARE(m_window->plasmaVirtualDesktops().length(), 2); + m_windowInterface->setOnAllDesktops(true); + // setting on all desktops, the window will leave every desktop + + QVERIFY(virtualDesktopLeftSpy.wait()); + QCOMPARE(virtualDesktopLeftSpy.count(), 2); + QCOMPARE(m_window->plasmaVirtualDesktops().length(), 0); + QVERIFY(m_window->isOnAllDesktops()); + + QCOMPARE(m_window->plasmaVirtualDesktops().length(), 0); + QVERIFY(m_window->isOnAllDesktops()); + + // return to the active desktop (0-1) + m_windowInterface->setOnAllDesktops(false); + QVERIFY(virtualDesktopEnteredSpy.wait()); + QCOMPARE(m_window->plasmaVirtualDesktops().length(), 1); + QCOMPARE(m_windowInterface->plasmaVirtualDesktops().first(), QStringLiteral("0-1")); + QVERIFY(!m_window->isOnAllDesktops()); +} + +void TestVirtualDesktop::testCreateRequested() +{ + // rebuild some desktops + testCreate(); + + QSignalSpy desktopCreateRequestedSpy(m_plasmaVirtualDesktopManagementInterface, + &KWin::PlasmaVirtualDesktopManagementInterface::desktopCreateRequested); + QSignalSpy desktopCreatedSpy(m_plasmaVirtualDesktopManagement, &KWayland::Client::PlasmaVirtualDesktopManagement::desktopCreated); + + // listen for createdRequested + m_plasmaVirtualDesktopManagement->requestCreateVirtualDesktop(QStringLiteral("Desktop"), 1); + QVERIFY(desktopCreateRequestedSpy.wait()); + QCOMPARE(desktopCreateRequestedSpy.first().first().toString(), QStringLiteral("Desktop")); + QCOMPARE(desktopCreateRequestedSpy.first().at(1).toUInt(), (quint32)1); + + // actually create + m_plasmaVirtualDesktopManagementInterface->createDesktop(QStringLiteral("0-4"), 1); + KWin::PlasmaVirtualDesktopInterface *desktopInt = m_plasmaVirtualDesktopManagementInterface->desktops().at(1); + + QCOMPARE(desktopInt->id(), QStringLiteral("0-4")); + desktopInt->setName(QStringLiteral("Desktop")); + + QVERIFY(desktopCreatedSpy.wait()); + + QCOMPARE(desktopCreatedSpy.first().first().toString(), QStringLiteral("0-4")); + QCOMPARE(m_plasmaVirtualDesktopManagement->desktops().count(), 4); + + KWayland::Client::PlasmaVirtualDesktop *desktop = m_plasmaVirtualDesktopManagement->desktops().at(1); + QSignalSpy desktopDoneSpy(desktop, &KWayland::Client::PlasmaVirtualDesktop::done); + desktopInt->sendDone(); + // desktopDoneSpy.wait(); + // check the order is correct + QCOMPARE(m_plasmaVirtualDesktopManagement->desktops().at(0)->id(), QStringLiteral("0-1")); + QCOMPARE(desktop->id(), QStringLiteral("0-4")); + QCOMPARE(m_plasmaVirtualDesktopManagement->desktops().at(2)->id(), QStringLiteral("0-2")); + QCOMPARE(m_plasmaVirtualDesktopManagement->desktops().at(3)->id(), QStringLiteral("0-3")); +} + +void TestVirtualDesktop::testRemoveRequested() +{ + // rebuild some desktops + testCreate(); + + QSignalSpy desktopRemoveRequestedSpy(m_plasmaVirtualDesktopManagementInterface, + &KWin::PlasmaVirtualDesktopManagementInterface::desktopRemoveRequested); + + // request a remove, just check the request arrived, ignore the request. + m_plasmaVirtualDesktopManagement->requestRemoveVirtualDesktop(QStringLiteral("0-1")); + QVERIFY(desktopRemoveRequestedSpy.wait()); + QCOMPARE(desktopRemoveRequestedSpy.first().first().toString(), QStringLiteral("0-1")); +} + +QTEST_GUILESS_MAIN(TestVirtualDesktop) +#include "test_plasma_virtual_desktop.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_plasmashell.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_plasmashell.cpp new file mode 100644 index 0000000000..bd74f4f8d2 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_plasmashell.cpp @@ -0,0 +1,493 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// KWayland +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/plasmashell.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/plasmashell.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/surface.h" + +using namespace KWin; + +class TestPlasmaShell : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void init(); + void cleanup(); + + void testRole_data(); + void testRole(); + void testPosition(); + void testSkipTaskbar(); + void testSkipSwitcher(); + void testPanelBehavior_data(); + void testPanelBehavior(); + void testAutoHidePanel(); + void testPanelTakesFocus(); + void testDisconnect(); + void testWhileDestroying(); + +private: + KWin::Display *m_display = nullptr; + CompositorInterface *m_compositorInterface = nullptr; + PlasmaShellInterface *m_plasmaShellInterface = nullptr; + + KWayland::Client::ConnectionThread *m_connection = nullptr; + KWayland::Client::Compositor *m_compositor = nullptr; + KWayland::Client::EventQueue *m_queue = nullptr; + QThread *m_thread = nullptr; + KWayland::Client::Registry *m_registry = nullptr; + KWayland::Client::PlasmaShell *m_plasmaShell = nullptr; +}; + +static const QString s_socketName = QStringLiteral("kwayland-test-wayland-plasma-shell-0"); + +void TestPlasmaShell::init() +{ + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + + m_compositorInterface = new CompositorInterface(m_display, m_display); + m_display->createShm(); + + m_plasmaShellInterface = new PlasmaShellInterface(m_display, m_display); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + m_registry = new KWayland::Client::Registry(); + QSignalSpy interfacesAnnouncedSpy(m_registry, &KWayland::Client::Registry::interfaceAnnounced); + + QVERIFY(!m_registry->eventQueue()); + m_registry->setEventQueue(m_queue); + QCOMPARE(m_registry->eventQueue(), m_queue); + m_registry->create(m_connection); + QVERIFY(m_registry->isValid()); + m_registry->setup(); + + QVERIFY(interfacesAnnouncedSpy.wait()); +#define CREATE(variable, factory, iface) \ + variable = \ + m_registry->create##factory(m_registry->interface(KWayland::Client::Registry::Interface::iface).name, m_registry->interface(KWayland::Client::Registry::Interface::iface).version, this); \ + QVERIFY(variable); + + CREATE(m_compositor, Compositor, Compositor) + CREATE(m_plasmaShell, PlasmaShell, PlasmaShell) + +#undef CREATE +} + +void TestPlasmaShell::cleanup() +{ +#define DELETE(name) \ + if (name) { \ + delete name; \ + name = nullptr; \ + } + DELETE(m_plasmaShell) + DELETE(m_compositor) + DELETE(m_queue) + DELETE(m_registry) +#undef DELETE + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + delete m_connection; + m_connection = nullptr; + + delete m_display; + m_display = nullptr; +} + +void TestPlasmaShell::testRole_data() +{ + QTest::addColumn("clientRole"); + QTest::addColumn("serverRole"); + + QTest::newRow("desktop") << KWayland::Client::PlasmaShellSurface::Role::Desktop << PlasmaShellSurfaceInterface::Role::Desktop; + QTest::newRow("osd") << KWayland::Client::PlasmaShellSurface::Role::OnScreenDisplay << PlasmaShellSurfaceInterface::Role::OnScreenDisplay; + QTest::newRow("panel") << KWayland::Client::PlasmaShellSurface::Role::Panel << PlasmaShellSurfaceInterface::Role::Panel; + QTest::newRow("notification") << KWayland::Client::PlasmaShellSurface::Role::Notification << PlasmaShellSurfaceInterface::Role::Notification; + QTest::newRow("tooltip") << KWayland::Client::PlasmaShellSurface::Role::ToolTip << PlasmaShellSurfaceInterface::Role::ToolTip; + QTest::newRow("criticalnotification") << KWayland::Client::PlasmaShellSurface::Role::CriticalNotification << PlasmaShellSurfaceInterface::Role::CriticalNotification; + QTest::newRow("appletPopup") << KWayland::Client::PlasmaShellSurface::Role::AppletPopup << PlasmaShellSurfaceInterface::Role::AppletPopup; +} + +void TestPlasmaShell::testRole() +{ + // this test verifies that setting the role on a plasma shell surface works + + // first create signal spies + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + QSignalSpy plasmaSurfaceCreatedSpy(m_plasmaShellInterface, &PlasmaShellInterface::surfaceCreated); + + // create the surface + std::unique_ptr s(m_compositor->createSurface()); + // no PlasmaShellSurface for the Surface yet yet + QVERIFY(!KWayland::Client::PlasmaShellSurface::get(s.get())); + std::unique_ptr ps(m_plasmaShell->createSurface(s.get())); + QCOMPARE(ps->role(), KWayland::Client::PlasmaShellSurface::Role::Normal); + // now we should have a PlasmaShellSurface for + QCOMPARE(KWayland::Client::PlasmaShellSurface::get(s.get()), ps.get()); + + // try to create another PlasmaShellSurface for the same Surface, should return from cache + QCOMPARE(m_plasmaShell->createSurface(s.get()), ps.get()); + + // and get them on the server + QVERIFY(plasmaSurfaceCreatedSpy.wait()); + QCOMPARE(plasmaSurfaceCreatedSpy.count(), 1); + QCOMPARE(surfaceCreatedSpy.count(), 1); + + // verify that we got a plasma shell surface + auto sps = plasmaSurfaceCreatedSpy.first().first().value(); + QVERIFY(sps); + QVERIFY(sps->surface()); + QCOMPARE(sps->surface(), surfaceCreatedSpy.first().first().value()); + + // default role should be normal + QCOMPARE(sps->role(), PlasmaShellSurfaceInterface::Role::Normal); + + // now change it + QSignalSpy roleChangedSpy(sps, &PlasmaShellSurfaceInterface::roleChanged); + QFETCH(KWayland::Client::PlasmaShellSurface::Role, clientRole); + ps->setRole(clientRole); + QCOMPARE(ps->role(), clientRole); + QVERIFY(roleChangedSpy.wait()); + QCOMPARE(roleChangedSpy.count(), 1); + QTEST(sps->role(), "serverRole"); + + // try changing again should not emit the signal + ps->setRole(clientRole); + QVERIFY(!roleChangedSpy.wait(100)); + + // set role back to normal + ps->setRole(KWayland::Client::PlasmaShellSurface::Role::Normal); + QCOMPARE(ps->role(), KWayland::Client::PlasmaShellSurface::Role::Normal); + QVERIFY(roleChangedSpy.wait()); + QCOMPARE(roleChangedSpy.count(), 2); + QCOMPARE(sps->role(), PlasmaShellSurfaceInterface::Role::Normal); +} + +void TestPlasmaShell::testPosition() +{ + // this test verifies that updating the position of a PlasmaShellSurface is properly passed to the server + QSignalSpy plasmaSurfaceCreatedSpy(m_plasmaShellInterface, &PlasmaShellInterface::surfaceCreated); + + std::unique_ptr s(m_compositor->createSurface()); + std::unique_ptr ps(m_plasmaShell->createSurface(s.get())); + QVERIFY(plasmaSurfaceCreatedSpy.wait()); + QCOMPARE(plasmaSurfaceCreatedSpy.count(), 1); + + // verify that we got a plasma shell surface + auto sps = plasmaSurfaceCreatedSpy.first().first().value(); + QVERIFY(sps); + QVERIFY(sps->surface()); + + // default position should not be set + QVERIFY(!sps->isPositionSet()); + QCOMPARE(sps->position(), QPoint()); + + // now let's try to change the position + QSignalSpy positionChangedSpy(sps, &PlasmaShellSurfaceInterface::positionChanged); + ps->setPosition(QPoint(1, 2)); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(positionChangedSpy.wait()); + QCOMPARE(positionChangedSpy.count(), 1); + QVERIFY(sps->isPositionSet()); + QCOMPARE(sps->position(), QPoint(1, 2)); + + // let's try to set same position, should not trigger an update + ps->setPosition(QPoint(1, 2)); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(!positionChangedSpy.wait(100)); + + // different point should work, though + ps->setPosition(QPoint(3, 4)); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(positionChangedSpy.wait()); + QCOMPARE(positionChangedSpy.count(), 2); + QCOMPARE(sps->position(), QPoint(3, 4)); +} + +void TestPlasmaShell::testSkipTaskbar() +{ + // this test verifies that sip taskbar is properly passed to server + QSignalSpy plasmaSurfaceCreatedSpy(m_plasmaShellInterface, &PlasmaShellInterface::surfaceCreated); + + std::unique_ptr s(m_compositor->createSurface()); + std::unique_ptr ps(m_plasmaShell->createSurface(s.get())); + QVERIFY(plasmaSurfaceCreatedSpy.wait()); + QCOMPARE(plasmaSurfaceCreatedSpy.count(), 1); + + // verify that we got a plasma shell surface + auto sps = plasmaSurfaceCreatedSpy.first().first().value(); + QVERIFY(sps); + QVERIFY(sps->surface()); + QVERIFY(!sps->skipTaskbar()); + + // now change + QSignalSpy skipTaskbarChangedSpy(sps, &PlasmaShellSurfaceInterface::skipTaskbarChanged); + ps->setSkipTaskbar(true); + QVERIFY(skipTaskbarChangedSpy.wait()); + QVERIFY(sps->skipTaskbar()); + // setting to same again should not emit the signal + ps->setSkipTaskbar(true); + QEXPECT_FAIL("", "Should not be emitted if not changed", Continue); + QVERIFY(!skipTaskbarChangedSpy.wait(100)); + QVERIFY(sps->skipTaskbar()); + + // setting to false should change again + ps->setSkipTaskbar(false); + QVERIFY(skipTaskbarChangedSpy.wait()); + QVERIFY(!sps->skipTaskbar()); +} + +void TestPlasmaShell::testSkipSwitcher() +{ + // this test verifies that Skip Switcher is properly passed to server + QSignalSpy plasmaSurfaceCreatedSpy(m_plasmaShellInterface, &PlasmaShellInterface::surfaceCreated); + + std::unique_ptr s(m_compositor->createSurface()); + std::unique_ptr ps(m_plasmaShell->createSurface(s.get())); + QVERIFY(plasmaSurfaceCreatedSpy.wait()); + QCOMPARE(plasmaSurfaceCreatedSpy.count(), 1); + + // verify that we got a plasma shell surface + auto sps = plasmaSurfaceCreatedSpy.first().first().value(); + QVERIFY(sps); + QVERIFY(sps->surface()); + QVERIFY(!sps->skipSwitcher()); + + // now change + QSignalSpy skipSwitcherChangedSpy(sps, &PlasmaShellSurfaceInterface::skipSwitcherChanged); + ps->setSkipSwitcher(true); + QVERIFY(skipSwitcherChangedSpy.wait()); + QVERIFY(sps->skipSwitcher()); + // setting to same again should not emit the signal + ps->setSkipSwitcher(true); + QEXPECT_FAIL("", "Should not be emitted if not changed", Continue); + QVERIFY(!skipSwitcherChangedSpy.wait(100)); + QVERIFY(sps->skipSwitcher()); + + // setting to false should change again + ps->setSkipSwitcher(false); + QVERIFY(skipSwitcherChangedSpy.wait()); + QVERIFY(!sps->skipSwitcher()); +} + +void TestPlasmaShell::testPanelBehavior_data() +{ + QTest::addColumn("client"); + QTest::addColumn("server"); + + QTest::newRow("autohide") << KWayland::Client::PlasmaShellSurface::PanelBehavior::AutoHide << PlasmaShellSurfaceInterface::PanelBehavior::AutoHide; + QTest::newRow("can cover") << KWayland::Client::PlasmaShellSurface::PanelBehavior::WindowsCanCover << PlasmaShellSurfaceInterface::PanelBehavior::WindowsCanCover; + QTest::newRow("go below") << KWayland::Client::PlasmaShellSurface::PanelBehavior::WindowsGoBelow << PlasmaShellSurfaceInterface::PanelBehavior::WindowsGoBelow; +} + +void TestPlasmaShell::testPanelBehavior() +{ + // this test verifies that the panel behavior is properly passed to the server + QSignalSpy plasmaSurfaceCreatedSpy(m_plasmaShellInterface, &PlasmaShellInterface::surfaceCreated); + + std::unique_ptr s(m_compositor->createSurface()); + std::unique_ptr ps(m_plasmaShell->createSurface(s.get())); + ps->setRole(KWayland::Client::PlasmaShellSurface::Role::Panel); + QVERIFY(plasmaSurfaceCreatedSpy.wait()); + QCOMPARE(plasmaSurfaceCreatedSpy.count(), 1); + + // verify that we got a plasma shell surface + auto sps = plasmaSurfaceCreatedSpy.first().first().value(); + QVERIFY(sps); + QVERIFY(sps->surface()); + QCOMPARE(sps->panelBehavior(), PlasmaShellSurfaceInterface::PanelBehavior::AlwaysVisible); + + // now change the behavior + QSignalSpy behaviorChangedSpy(sps, &PlasmaShellSurfaceInterface::panelBehaviorChanged); + QFETCH(KWayland::Client::PlasmaShellSurface::PanelBehavior, client); + ps->setPanelBehavior(client); + QVERIFY(behaviorChangedSpy.wait()); + QTEST(sps->panelBehavior(), "server"); + + // changing to same should not trigger the signal + ps->setPanelBehavior(client); + QVERIFY(!behaviorChangedSpy.wait(100)); + + // but changing back to Always Visible should work + ps->setPanelBehavior(KWayland::Client::PlasmaShellSurface::PanelBehavior::AlwaysVisible); + QVERIFY(behaviorChangedSpy.wait()); + QCOMPARE(sps->panelBehavior(), PlasmaShellSurfaceInterface::PanelBehavior::AlwaysVisible); +} + +void TestPlasmaShell::testAutoHidePanel() +{ + // this test verifies that auto-hiding panels work correctly + QSignalSpy plasmaSurfaceCreatedSpy(m_plasmaShellInterface, &PlasmaShellInterface::surfaceCreated); + + std::unique_ptr s(m_compositor->createSurface()); + std::unique_ptr ps(m_plasmaShell->createSurface(s.get())); + ps->setRole(KWayland::Client::PlasmaShellSurface::Role::Panel); + ps->setPanelBehavior(KWayland::Client::PlasmaShellSurface::PanelBehavior::AutoHide); + QVERIFY(plasmaSurfaceCreatedSpy.wait()); + QCOMPARE(plasmaSurfaceCreatedSpy.count(), 1); + auto sps = plasmaSurfaceCreatedSpy.first().first().value(); + QVERIFY(sps); + QCOMPARE(sps->panelBehavior(), PlasmaShellSurfaceInterface::PanelBehavior::AutoHide); + + QSignalSpy autoHideRequestedSpy(sps, &PlasmaShellSurfaceInterface::panelAutoHideHideRequested); + QSignalSpy autoHideShowRequestedSpy(sps, &PlasmaShellSurfaceInterface::panelAutoHideShowRequested); + ps->requestHideAutoHidingPanel(); + QVERIFY(autoHideRequestedSpy.wait()); + QCOMPARE(autoHideRequestedSpy.count(), 1); + QCOMPARE(autoHideShowRequestedSpy.count(), 0); + + QSignalSpy panelShownSpy(ps.get(), &KWayland::Client::PlasmaShellSurface::autoHidePanelShown); + QSignalSpy panelHiddenSpy(ps.get(), &KWayland::Client::PlasmaShellSurface::autoHidePanelHidden); + + sps->hideAutoHidingPanel(); + QVERIFY(panelHiddenSpy.wait()); + QCOMPARE(panelHiddenSpy.count(), 1); + QCOMPARE(panelShownSpy.count(), 0); + + ps->requestShowAutoHidingPanel(); + QVERIFY(autoHideShowRequestedSpy.wait()); + QCOMPARE(autoHideRequestedSpy.count(), 1); + QCOMPARE(autoHideShowRequestedSpy.count(), 1); + + sps->showAutoHidingPanel(); + QVERIFY(panelShownSpy.wait()); + QCOMPARE(panelHiddenSpy.count(), 1); + QCOMPARE(panelShownSpy.count(), 1); + + // change panel type + ps->setPanelBehavior(KWayland::Client::PlasmaShellSurface::PanelBehavior::AlwaysVisible); + // requesting auto hide should raise error + QSignalSpy errorSpy(m_connection, &KWayland::Client::ConnectionThread::errorOccurred); + ps->requestHideAutoHidingPanel(); + QVERIFY(errorSpy.wait()); +} + +void TestPlasmaShell::testPanelTakesFocus() +{ + // this test verifies that whether a panel wants to take focus is passed through correctly + QSignalSpy plasmaSurfaceCreatedSpy(m_plasmaShellInterface, &PlasmaShellInterface::surfaceCreated); + + std::unique_ptr s(m_compositor->createSurface()); + std::unique_ptr ps(m_plasmaShell->createSurface(s.get())); + ps->setRole(KWayland::Client::PlasmaShellSurface::Role::Panel); + QVERIFY(plasmaSurfaceCreatedSpy.wait()); + QCOMPARE(plasmaSurfaceCreatedSpy.count(), 1); + auto sps = plasmaSurfaceCreatedSpy.first().first().value(); + QSignalSpy plasmaSurfaceTakesFocusSpy(sps, &PlasmaShellSurfaceInterface::panelTakesFocusChanged); + + QVERIFY(sps); + QCOMPARE(sps->role(), PlasmaShellSurfaceInterface::Role::Panel); + QCOMPARE(sps->panelTakesFocus(), false); + + ps->setPanelTakesFocus(true); + m_connection->flush(); + QVERIFY(plasmaSurfaceTakesFocusSpy.wait()); + QCOMPARE(plasmaSurfaceTakesFocusSpy.count(), 1); + QCOMPARE(sps->panelTakesFocus(), true); + ps->setPanelTakesFocus(false); + m_connection->flush(); + QVERIFY(plasmaSurfaceTakesFocusSpy.wait()); + QCOMPARE(plasmaSurfaceTakesFocusSpy.count(), 2); + QCOMPARE(sps->panelTakesFocus(), false); +} + +void TestPlasmaShell::testDisconnect() +{ + // this test verifies that a disconnect cleans up + QSignalSpy plasmaSurfaceCreatedSpy(m_plasmaShellInterface, &PlasmaShellInterface::surfaceCreated); + // create the surface + std::unique_ptr s(m_compositor->createSurface()); + std::unique_ptr ps(m_plasmaShell->createSurface(s.get())); + + // and get them on the server + QVERIFY(plasmaSurfaceCreatedSpy.wait()); + QCOMPARE(plasmaSurfaceCreatedSpy.count(), 1); + auto sps = plasmaSurfaceCreatedSpy.first().first().value(); + QVERIFY(sps); + + // disconnect + QSignalSpy surfaceDestroyedSpy(sps, &QObject::destroyed); + if (m_connection) { + m_connection->deleteLater(); + m_connection = nullptr; + } + QCOMPARE(surfaceDestroyedSpy.count(), 0); + QVERIFY(surfaceDestroyedSpy.wait()); + QCOMPARE(surfaceDestroyedSpy.count(), 1); + + s->destroy(); + ps->destroy(); + m_plasmaShell->destroy(); + m_compositor->destroy(); + m_registry->destroy(); + m_queue->destroy(); +} + +void TestPlasmaShell::testWhileDestroying() +{ + // this test tries to hit a condition that a Surface gets created with an ID which was already + // used for a previous Surface. For each Surface we try to create a PlasmaShellSurface. + // Even if there was a Surface in the past with the same ID, it should create the PlasmaShellSurface + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr s(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + // create ShellSurface + QSignalSpy shellSurfaceCreatedSpy(m_plasmaShellInterface, &PlasmaShellInterface::surfaceCreated); + std::unique_ptr ps(m_plasmaShell->createSurface(s.get())); + QVERIFY(shellSurfaceCreatedSpy.wait()); + + // now try to create more surfaces + QSignalSpy clientErrorSpy(m_connection, &KWayland::Client::ConnectionThread::errorOccurred); + for (int i = 0; i < 100; i++) { + s.reset(); + s.reset(m_compositor->createSurface()); + m_plasmaShell->createSurface(s.get(), this); + QVERIFY(surfaceCreatedSpy.wait()); + } + QVERIFY(clientErrorSpy.isEmpty()); + QVERIFY(!clientErrorSpy.wait(100)); + QVERIFY(clientErrorSpy.isEmpty()); +} + +QTEST_GUILESS_MAIN(TestPlasmaShell) +#include "test_plasmashell.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_pointer_constraints.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_pointer_constraints.cpp new file mode 100644 index 0000000000..8f6c9c624e --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_pointer_constraints.cpp @@ -0,0 +1,444 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// client +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/pointer.h" +#include "KWayland/Client/pointerconstraints.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/seat.h" +#include "KWayland/Client/shm_pool.h" +#include "KWayland/Client/surface.h" +// server +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/pointerconstraints_v1.h" +#include "wayland/seat.h" +#include "wayland/surface.h" + +using namespace KWin; + +Q_DECLARE_METATYPE(KWayland::Client::PointerConstraints::LifeTime) +Q_DECLARE_METATYPE(KWin::ConfinedPointerV1Interface::LifeTime) +Q_DECLARE_METATYPE(KWin::LockedPointerV1Interface::LifeTime) + +class TestPointerConstraints : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void init(); + void cleanup(); + + void testLockPointer_data(); + void testLockPointer(); + + void testConfinePointer_data(); + void testConfinePointer(); + void testAlreadyConstrained_data(); + void testAlreadyConstrained(); + +private: + KWin::Display *m_display = nullptr; + CompositorInterface *m_compositorInterface = nullptr; + SeatInterface *m_seatInterface = nullptr; + PointerConstraintsV1Interface *m_pointerConstraintsInterface = nullptr; + KWayland::Client::ConnectionThread *m_connection = nullptr; + QThread *m_thread = nullptr; + KWayland::Client::EventQueue *m_queue = nullptr; + KWayland::Client::Compositor *m_compositor = nullptr; + KWayland::Client::Seat *m_seat = nullptr; + KWayland::Client::ShmPool *m_shm = nullptr; + KWayland::Client::Pointer *m_pointer = nullptr; + KWayland::Client::PointerConstraints *m_pointerConstraints = nullptr; +}; + +static const QString s_socketName = QStringLiteral("kwayland-test-pointer_constraint-0"); + +void TestPointerConstraints::init() +{ + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + m_display->createShm(); + m_seatInterface = new SeatInterface(m_display, QStringLiteral("seat0"), m_display); + m_seatInterface->setHasPointer(true); + m_compositorInterface = new CompositorInterface(m_display, m_display); + m_pointerConstraintsInterface = new PointerConstraintsV1Interface(m_display, m_display); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + m_queue->setup(m_connection); + + KWayland::Client::Registry registry; + QSignalSpy interfacesAnnouncedSpy(®istry, &KWayland::Client::Registry::interfacesAnnounced); + QSignalSpy interfaceAnnouncedSpy(®istry, &KWayland::Client::Registry::interfaceAnnounced); + registry.setEventQueue(m_queue); + registry.create(m_connection); + QVERIFY(registry.isValid()); + registry.setup(); + QVERIFY(interfacesAnnouncedSpy.wait()); + + m_shm = new KWayland::Client::ShmPool(this); + m_shm->setup(registry.bindShm(registry.interface(KWayland::Client::Registry::Interface::Shm).name, + registry.interface(KWayland::Client::Registry::Interface::Shm).version)); + QVERIFY(m_shm->isValid()); + + m_compositor = + registry.createCompositor(registry.interface(KWayland::Client::Registry::Interface::Compositor).name, registry.interface(KWayland::Client::Registry::Interface::Compositor).version, this); + QVERIFY(m_compositor); + QVERIFY(m_compositor->isValid()); + + m_pointerConstraints = registry.createPointerConstraints(registry.interface(KWayland::Client::Registry::Interface::PointerConstraintsUnstableV1).name, + registry.interface(KWayland::Client::Registry::Interface::PointerConstraintsUnstableV1).version, + this); + QVERIFY(m_pointerConstraints); + QVERIFY(m_pointerConstraints->isValid()); + + m_seat = registry.createSeat(registry.interface(KWayland::Client::Registry::Interface::Seat).name, registry.interface(KWayland::Client::Registry::Interface::Seat).version, this); + QVERIFY(m_seat); + QVERIFY(m_seat->isValid()); + QSignalSpy pointerChangedSpy(m_seat, &KWayland::Client::Seat::hasPointerChanged); + QVERIFY(pointerChangedSpy.wait()); + m_pointer = m_seat->createPointer(this); + QVERIFY(m_pointer); +} + +void TestPointerConstraints::cleanup() +{ +#define CLEANUP(variable) \ + if (variable) { \ + delete variable; \ + variable = nullptr; \ + } + CLEANUP(m_compositor) + CLEANUP(m_pointerConstraints) + CLEANUP(m_pointer) + CLEANUP(m_shm) + CLEANUP(m_seat) + CLEANUP(m_queue) + if (m_connection) { + m_connection->deleteLater(); + m_connection = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + + CLEANUP(m_display) +#undef CLEANUP + + // these are the children of the display + m_compositorInterface = nullptr; + m_seatInterface = nullptr; + m_pointerConstraintsInterface = nullptr; +} + +void TestPointerConstraints::testLockPointer_data() +{ + QTest::addColumn("clientLifeTime"); + QTest::addColumn("serverLifeTime"); + QTest::addColumn("hasConstraintAfterUnlock"); + QTest::addColumn("pointerChangedCount"); + + QTest::newRow("persistent") << KWayland::Client::PointerConstraints::LifeTime::Persistent << LockedPointerV1Interface::LifeTime::Persistent << true << 1; + QTest::newRow("oneshot") << KWayland::Client::PointerConstraints::LifeTime::OneShot << LockedPointerV1Interface::LifeTime::OneShot << false << 2; +} + +void TestPointerConstraints::testLockPointer() +{ + // this test verifies the basic interaction for lock pointer + // first create a surface + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(surface->isValid()); + QVERIFY(surfaceCreatedSpy.wait()); + + QImage image(QSize(100, 100), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::black); + surface->attachBuffer(m_shm->createBuffer(image)); + surface->damage(image.rect()); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + auto serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + QVERIFY(!serverSurface->lockedPointer()); + QVERIFY(!serverSurface->confinedPointer()); + + // now create the locked pointer + QSignalSpy pointerConstraintsChangedSpy(serverSurface, &SurfaceInterface::pointerConstraintsChanged); + QFETCH(KWayland::Client::PointerConstraints::LifeTime, clientLifeTime); + std::unique_ptr lockedPointer(m_pointerConstraints->lockPointer(surface.get(), m_pointer, nullptr, clientLifeTime)); + QSignalSpy lockedSpy(lockedPointer.get(), &KWayland::Client::LockedPointer::locked); + QSignalSpy unlockedSpy(lockedPointer.get(), &KWayland::Client::LockedPointer::unlocked); + QVERIFY(lockedPointer->isValid()); + QVERIFY(pointerConstraintsChangedSpy.wait()); + + auto serverLockedPointer = serverSurface->lockedPointer(); + QVERIFY(serverLockedPointer); + QVERIFY(!serverSurface->confinedPointer()); + + QCOMPARE(serverLockedPointer->isLocked(), false); + QCOMPARE(serverLockedPointer->region(), KWin::Region(0, 0, 100, 100)); + QFETCH(LockedPointerV1Interface::LifeTime, serverLifeTime); + QCOMPARE(serverLockedPointer->lifeTime(), serverLifeTime); + // setting to unlocked now should not trigger an unlocked spy + serverLockedPointer->setLocked(false); + QVERIFY(!unlockedSpy.wait(500)); + + // try setting a region + QSignalSpy destroyedSpy(serverLockedPointer, &QObject::destroyed); + QSignalSpy regionChangedSpy(serverLockedPointer, &LockedPointerV1Interface::regionChanged); + lockedPointer->setRegion(m_compositor->createRegion(QRegion(0, 5, 10, 20), m_compositor)); + // it's double buffered + QVERIFY(!regionChangedSpy.wait(500)); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(regionChangedSpy.wait()); + QCOMPARE(serverLockedPointer->region(), KWin::Region(0, 5, 10, 20)); + // and unset region again + lockedPointer->setRegion(nullptr); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(regionChangedSpy.wait()); + QCOMPARE(serverLockedPointer->region(), KWin::Region(0, 0, 100, 100)); + + // let's lock the surface + QSignalSpy lockedChangedSpy(serverLockedPointer, &LockedPointerV1Interface::lockedChanged); + m_seatInterface->notifyPointerEnter(serverSurface, QPointF(0, 0)); + QSignalSpy pointerMotionSpy(m_pointer, &KWayland::Client::Pointer::motion); + m_seatInterface->notifyPointerMotion(QPoint(0, 1)); + m_seatInterface->notifyPointerFrame(); + QVERIFY(pointerMotionSpy.wait()); + + serverLockedPointer->setLocked(true); + QCOMPARE(serverLockedPointer->isLocked(), true); + m_seatInterface->notifyPointerMotion(QPoint(1, 1)); + m_seatInterface->notifyPointerFrame(); + QCOMPARE(lockedChangedSpy.count(), 1); + QCOMPARE(pointerMotionSpy.count(), 1); + QVERIFY(lockedSpy.isEmpty()); + QVERIFY(lockedSpy.wait()); + QVERIFY(unlockedSpy.isEmpty()); + + const QPointF hint = QPointF(1.5, 0.5); + QSignalSpy hintChangedSpy(serverLockedPointer, &LockedPointerV1Interface::cursorPositionHintChanged); + lockedPointer->setCursorPositionHint(hint); + QCOMPARE(serverLockedPointer->cursorPositionHint(), QPointF(-1., -1.)); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(hintChangedSpy.wait()); + QCOMPARE(serverLockedPointer->cursorPositionHint(), hint); + + // and unlock again + serverLockedPointer->setLocked(false); + QCOMPARE(serverLockedPointer->isLocked(), false); + QCOMPARE(serverLockedPointer->cursorPositionHint(), QPointF(-1., -1.)); + QCOMPARE(lockedChangedSpy.count(), 2); + QTEST(bool(serverSurface->lockedPointer()), "hasConstraintAfterUnlock"); + QFETCH(int, pointerChangedCount); + QCOMPARE(pointerConstraintsChangedSpy.count(), pointerChangedCount); + QVERIFY(unlockedSpy.wait()); + QCOMPARE(unlockedSpy.count(), 1); + QCOMPARE(lockedSpy.count(), 1); + + // now motion should work again + m_seatInterface->notifyPointerMotion(QPoint(0, 1)); + m_seatInterface->notifyPointerFrame(); + QVERIFY(pointerMotionSpy.wait()); + QCOMPARE(pointerMotionSpy.count(), 2); + + lockedPointer.reset(); + QVERIFY(destroyedSpy.wait()); + QCOMPARE(pointerConstraintsChangedSpy.count(), 2); +} + +void TestPointerConstraints::testConfinePointer_data() +{ + QTest::addColumn("clientLifeTime"); + QTest::addColumn("serverLifeTime"); + QTest::addColumn("hasConstraintAfterUnlock"); + QTest::addColumn("pointerChangedCount"); + + QTest::newRow("persistent") << KWayland::Client::PointerConstraints::LifeTime::Persistent << ConfinedPointerV1Interface::LifeTime::Persistent << true << 1; + QTest::newRow("oneshot") << KWayland::Client::PointerConstraints::LifeTime::OneShot << ConfinedPointerV1Interface::LifeTime::OneShot << false << 2; +} + +void TestPointerConstraints::testConfinePointer() +{ + // this test verifies the basic interaction for confined pointer + // first create a surface + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(surface->isValid()); + QVERIFY(surfaceCreatedSpy.wait()); + + QImage image(QSize(100, 100), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::black); + surface->attachBuffer(m_shm->createBuffer(image)); + surface->damage(image.rect()); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + auto serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + QVERIFY(!serverSurface->lockedPointer()); + QVERIFY(!serverSurface->confinedPointer()); + + // now create the confined pointer + QSignalSpy pointerConstraintsChangedSpy(serverSurface, &SurfaceInterface::pointerConstraintsChanged); + QFETCH(KWayland::Client::PointerConstraints::LifeTime, clientLifeTime); + std::unique_ptr confinedPointer(m_pointerConstraints->confinePointer(surface.get(), m_pointer, nullptr, clientLifeTime)); + QSignalSpy confinedSpy(confinedPointer.get(), &KWayland::Client::ConfinedPointer::confined); + QSignalSpy unconfinedSpy(confinedPointer.get(), &KWayland::Client::ConfinedPointer::unconfined); + QVERIFY(confinedPointer->isValid()); + QVERIFY(pointerConstraintsChangedSpy.wait()); + + auto serverConfinedPointer = serverSurface->confinedPointer(); + QVERIFY(serverConfinedPointer); + QVERIFY(!serverSurface->lockedPointer()); + + QCOMPARE(serverConfinedPointer->isConfined(), false); + QCOMPARE(serverConfinedPointer->region(), KWin::Region(0, 0, 100, 100)); + QFETCH(ConfinedPointerV1Interface::LifeTime, serverLifeTime); + QCOMPARE(serverConfinedPointer->lifeTime(), serverLifeTime); + // setting to unconfined now should not trigger an unconfined spy + serverConfinedPointer->setConfined(false); + QVERIFY(!unconfinedSpy.wait(500)); + + // try setting a region + QSignalSpy destroyedSpy(serverConfinedPointer, &QObject::destroyed); + QSignalSpy regionChangedSpy(serverConfinedPointer, &ConfinedPointerV1Interface::regionChanged); + confinedPointer->setRegion(m_compositor->createRegion(QRegion(0, 5, 10, 20), m_compositor)); + // it's double buffered + QVERIFY(!regionChangedSpy.wait(500)); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(regionChangedSpy.wait()); + QCOMPARE(serverConfinedPointer->region(), KWin::Region(0, 5, 10, 20)); + // and unset region again + confinedPointer->setRegion(nullptr); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(regionChangedSpy.wait()); + QCOMPARE(serverConfinedPointer->region(), KWin::Region(0, 0, 100, 100)); + + // let's confine the surface + QSignalSpy confinedChangedSpy(serverConfinedPointer, &ConfinedPointerV1Interface::confinedChanged); + m_seatInterface->notifyPointerEnter(serverSurface, QPointF(0, 0)); + serverConfinedPointer->setConfined(true); + QCOMPARE(serverConfinedPointer->isConfined(), true); + QCOMPARE(confinedChangedSpy.count(), 1); + QVERIFY(confinedSpy.isEmpty()); + QVERIFY(confinedSpy.wait()); + QVERIFY(unconfinedSpy.isEmpty()); + + // and unconfine again + serverConfinedPointer->setConfined(false); + QCOMPARE(serverConfinedPointer->isConfined(), false); + QCOMPARE(confinedChangedSpy.count(), 2); + QTEST(bool(serverSurface->confinedPointer()), "hasConstraintAfterUnlock"); + QFETCH(int, pointerChangedCount); + QCOMPARE(pointerConstraintsChangedSpy.count(), pointerChangedCount); + QVERIFY(unconfinedSpy.wait()); + QCOMPARE(unconfinedSpy.count(), 1); + QCOMPARE(confinedSpy.count(), 1); + + confinedPointer.reset(); + QVERIFY(destroyedSpy.wait()); + QCOMPARE(pointerConstraintsChangedSpy.count(), 2); +} + +enum class Constraint { + Lock, + Confine, +}; + +Q_DECLARE_METATYPE(Constraint) + +void TestPointerConstraints::testAlreadyConstrained_data() +{ + QTest::addColumn("firstConstraint"); + QTest::addColumn("secondConstraint"); + + QTest::newRow("confine-confine") << Constraint::Confine << Constraint::Confine; + QTest::newRow("lock-confine") << Constraint::Lock << Constraint::Confine; + QTest::newRow("confine-lock") << Constraint::Confine << Constraint::Lock; + QTest::newRow("lock-lock") << Constraint::Lock << Constraint::Lock; +} + +void TestPointerConstraints::testAlreadyConstrained() +{ + // this test verifies that creating a pointer constraint for an already constrained surface triggers an error + // first create a surface + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(surface->isValid()); + QFETCH(Constraint, firstConstraint); + std::unique_ptr confinedPointer; + std::unique_ptr lockedPointer; + switch (firstConstraint) { + case Constraint::Lock: + lockedPointer.reset(m_pointerConstraints->lockPointer(surface.get(), m_pointer, nullptr, KWayland::Client::PointerConstraints::LifeTime::OneShot)); + break; + case Constraint::Confine: + confinedPointer.reset(m_pointerConstraints->confinePointer(surface.get(), m_pointer, nullptr, KWayland::Client::PointerConstraints::LifeTime::OneShot)); + break; + default: + Q_UNREACHABLE(); + } + QVERIFY(confinedPointer || lockedPointer); + + QSignalSpy errorSpy(m_connection, &KWayland::Client::ConnectionThread::errorOccurred); + QFETCH(Constraint, secondConstraint); + std::unique_ptr confinedPointer2; + std::unique_ptr lockedPointer2; + switch (secondConstraint) { + case Constraint::Lock: + lockedPointer2.reset(m_pointerConstraints->lockPointer(surface.get(), m_pointer, nullptr, KWayland::Client::PointerConstraints::LifeTime::OneShot)); + break; + case Constraint::Confine: + confinedPointer2.reset(m_pointerConstraints->confinePointer(surface.get(), m_pointer, nullptr, KWayland::Client::PointerConstraints::LifeTime::OneShot)); + break; + default: + Q_UNREACHABLE(); + } + QVERIFY(errorSpy.wait()); + QVERIFY(m_connection->hasError()); + if (confinedPointer2) { + confinedPointer2->destroy(); + } + if (lockedPointer2) { + lockedPointer2->destroy(); + } + if (confinedPointer) { + confinedPointer->destroy(); + } + if (lockedPointer) { + lockedPointer->destroy(); + } + surface->destroy(); + m_compositor->destroy(); + m_pointerConstraints->destroy(); + m_pointer->destroy(); + m_seat->destroy(); + m_queue->destroy(); +} + +QTEST_GUILESS_MAIN(TestPointerConstraints) +#include "test_pointer_constraints.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_server_side_decoration.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_server_side_decoration.cpp new file mode 100644 index 0000000000..b782f8bc32 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_server_side_decoration.cpp @@ -0,0 +1,323 @@ +/* + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// KWin +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/server_decoration.h" + +#include "qwayland-server-decoration.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/surface.h" + +class ServerSideDecorationManager : public QtWayland::org_kde_kwin_server_decoration_manager +{ +}; + +class ServerSideDecoration : public QObject, public QtWayland::org_kde_kwin_server_decoration +{ + Q_OBJECT + +public: + ~ServerSideDecoration() override + { + release(); + } + +Q_SIGNALS: + void modeChanged(ServerSideDecorationManager::mode mode); + +protected: + void org_kde_kwin_server_decoration_mode(uint32_t mode) override + { + Q_EMIT modeChanged(ServerSideDecorationManager::mode(mode)); + } +}; + +class TestServerSideDecoration : public QObject +{ + Q_OBJECT +public: + explicit TestServerSideDecoration(QObject *parent = nullptr); +private Q_SLOTS: + void init(); + void cleanup(); + + void testCreate_data(); + void testCreate(); + + void testRequest_data(); + void testRequest(); + + void testSurfaceDestroy(); + +private: + KWin::Display *m_display = nullptr; + KWin::CompositorInterface *m_compositorInterface = nullptr; + KWin::ServerSideDecorationManagerInterface *m_serverSideDecorationManagerInterface = nullptr; + KWayland::Client::ConnectionThread *m_connection = nullptr; + KWayland::Client::Compositor *m_compositor = nullptr; + KWayland::Client::EventQueue *m_queue = nullptr; + ServerSideDecorationManager *m_serverSideDecorationManager = nullptr; + QThread *m_thread = nullptr; + KWayland::Client::Registry *m_registry = nullptr; +}; + +static const QString s_socketName = QStringLiteral("kwayland-test-wayland-server-side-decoration-0"); + +TestServerSideDecoration::TestServerSideDecoration(QObject *parent) + : QObject(parent) +{ +} + +void TestServerSideDecoration::init() +{ + using namespace KWin; + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + m_compositorInterface = new CompositorInterface(m_display, m_display); + m_serverSideDecorationManagerInterface = new ServerSideDecorationManagerInterface(m_display, m_display); + + m_registry = new KWayland::Client::Registry(); + connect(m_registry, &KWayland::Client::Registry::interfaceAnnounced, this, [this](const QByteArray &interfaceName, quint32 name, quint32 version) { + if (interfaceName == org_kde_kwin_server_decoration_manager_interface.name) { + m_serverSideDecorationManager = new ServerSideDecorationManager(); + m_serverSideDecorationManager->init(*m_registry, name, version); + } + }); + + QSignalSpy interfacesAnnouncedSpy(m_registry, &KWayland::Client::Registry::interfacesAnnounced); + QSignalSpy compositorSpy(m_registry, &KWayland::Client::Registry::compositorAnnounced); + + QVERIFY(!m_registry->eventQueue()); + m_registry->setEventQueue(m_queue); + QCOMPARE(m_registry->eventQueue(), m_queue); + m_registry->create(m_connection); + QVERIFY(m_registry->isValid()); + m_registry->setup(); + QVERIFY(interfacesAnnouncedSpy.wait()); + + m_compositor = m_registry->createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + + QVERIFY(m_compositor); + QVERIFY(m_serverSideDecorationManager); +} + +void TestServerSideDecoration::cleanup() +{ + if (m_compositor) { + delete m_compositor; + m_compositor = nullptr; + } + if (m_serverSideDecorationManager) { + delete m_serverSideDecorationManager; + m_serverSideDecorationManager = nullptr; + } + if (m_registry) { + delete m_registry; + m_registry = nullptr; + } + + if (m_queue) { + delete m_queue; + m_queue = nullptr; + } + + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + delete m_connection; + m_connection = nullptr; + + delete m_display; + m_display = nullptr; +} + +void TestServerSideDecoration::testCreate_data() +{ + using namespace KWin; + QTest::addColumn("serverMode"); + QTest::addColumn("clientMode"); + + QTest::newRow("none") << ServerSideDecorationManagerInterface::Mode::None << ServerSideDecorationManager::mode_None; + QTest::newRow("client") << ServerSideDecorationManagerInterface::Mode::Client << ServerSideDecorationManager::mode_Client; + QTest::newRow("server") << ServerSideDecorationManagerInterface::Mode::Server << ServerSideDecorationManager::mode_Server; +} + +void TestServerSideDecoration::testCreate() +{ + using namespace KWin; + QFETCH(KWin::ServerSideDecorationManagerInterface::Mode, serverMode); + m_serverSideDecorationManagerInterface->setDefaultMode(serverMode); + QCOMPARE(m_serverSideDecorationManagerInterface->defaultMode(), serverMode); + + QSignalSpy serverSurfaceCreated(m_compositorInterface, &CompositorInterface::surfaceCreated); + QSignalSpy decorationCreated(m_serverSideDecorationManagerInterface, &ServerSideDecorationManagerInterface::decorationCreated); + + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + + auto serverSurface = serverSurfaceCreated.first().first().value(); + QVERIFY(!ServerSideDecorationInterface::get(serverSurface)); + + // create server side deco + auto serverSideDecoration = std::make_unique(); + serverSideDecoration->init(m_serverSideDecorationManager->create(*surface.get())); + QSignalSpy modeChangedSpy(serverSideDecoration.get(), &ServerSideDecoration::modeChanged); + + QVERIFY(decorationCreated.wait()); + + auto serverDeco = decorationCreated.first().first().value(); + QVERIFY(serverDeco); + QCOMPARE(serverDeco, ServerSideDecorationInterface::get(serverSurface)); + QCOMPARE(serverDeco->surface(), serverSurface); + + // after binding the client should get the default mode + QVERIFY(modeChangedSpy.wait()); + QCOMPARE(modeChangedSpy.count(), 1); + QTEST(modeChangedSpy.last().at(0).value(), "clientMode"); + + // and destroy + QSignalSpy destroyedSpy(serverDeco, &QObject::destroyed); + serverSideDecoration.reset(); + QVERIFY(destroyedSpy.wait()); +} + +void TestServerSideDecoration::testRequest_data() +{ + using namespace KWin; + QTest::addColumn("defaultMode"); + QTest::addColumn("clientMode"); + QTest::addColumn("clientRequestMode"); + QTest::addColumn("serverRequestedMode"); + + const auto serverNone = ServerSideDecorationManagerInterface::Mode::None; + const auto serverClient = ServerSideDecorationManagerInterface::Mode::Client; + const auto serverServer = ServerSideDecorationManagerInterface::Mode::Server; + const auto clientNone = ServerSideDecorationManager::mode_None; + const auto clientClient = ServerSideDecorationManager::mode_Client; + const auto clientServer = ServerSideDecorationManager::mode_Server; + + QTest::newRow("none->none") << serverNone << clientNone << clientNone << serverNone; + QTest::newRow("none->client") << serverNone << clientNone << clientClient << serverClient; + QTest::newRow("none->server") << serverNone << clientNone << clientServer << serverServer; + QTest::newRow("client->none") << serverClient << clientClient << clientNone << serverNone; + QTest::newRow("client->client") << serverClient << clientClient << clientClient << serverClient; + QTest::newRow("client->server") << serverClient << clientClient << clientServer << serverServer; + QTest::newRow("server->none") << serverServer << clientServer << clientNone << serverNone; + QTest::newRow("server->client") << serverServer << clientServer << clientClient << serverClient; + QTest::newRow("server->server") << serverServer << clientServer << clientServer << serverServer; +} + +void TestServerSideDecoration::testRequest() +{ + using namespace KWin; + QFETCH(KWin::ServerSideDecorationManagerInterface::Mode, defaultMode); + m_serverSideDecorationManagerInterface->setDefaultMode(defaultMode); + QCOMPARE(m_serverSideDecorationManagerInterface->defaultMode(), defaultMode); + + QSignalSpy serverSurfaceCreated(m_compositorInterface, &CompositorInterface::surfaceCreated); + QSignalSpy decorationCreated(m_serverSideDecorationManagerInterface, &ServerSideDecorationManagerInterface::decorationCreated); + + // create server side deco + std::unique_ptr surface(m_compositor->createSurface()); + + auto serverSideDecoration = std::make_unique(); + serverSideDecoration->init(m_serverSideDecorationManager->create(*surface.get())); + QSignalSpy modeChangedSpy(serverSideDecoration.get(), &ServerSideDecoration::modeChanged); + QVERIFY(decorationCreated.wait()); + + auto serverDeco = decorationCreated.first().first().value(); + QVERIFY(serverDeco); + QSignalSpy preferredModeChangedSpy(serverDeco, &ServerSideDecorationInterface::preferredModeChanged); + + // after binding the client should get the default mode + QVERIFY(modeChangedSpy.wait()); + QCOMPARE(modeChangedSpy.count(), 1); + QTEST(modeChangedSpy.last().at(0).value(), "clientMode"); + + // request a change + QFETCH(ServerSideDecorationManager::mode, clientRequestMode); + serverSideDecoration->request_mode(clientRequestMode); + // mode not yet changed + QCOMPARE(modeChangedSpy.count(), 1); + + QVERIFY(preferredModeChangedSpy.wait()); + QCOMPARE(preferredModeChangedSpy.count(), 1); + QFETCH(ServerSideDecorationManagerInterface::Mode, serverRequestedMode); + QCOMPARE(serverDeco->preferredMode(), serverRequestedMode); + + // mode not yet changed + QCOMPARE(serverDeco->mode(), defaultMode); + serverDeco->setMode(serverRequestedMode); + QCOMPARE(serverDeco->mode(), serverRequestedMode); + + // should be sent to client + QVERIFY(modeChangedSpy.wait()); + QCOMPARE(modeChangedSpy.count(), 2); + QCOMPARE(modeChangedSpy.last().at(0).value(), clientRequestMode); +} + +void TestServerSideDecoration::testSurfaceDestroy() +{ + using namespace KWin; + QSignalSpy serverSurfaceCreated(m_compositorInterface, &CompositorInterface::surfaceCreated); + QSignalSpy decorationCreated(m_serverSideDecorationManagerInterface, &ServerSideDecorationManagerInterface::decorationCreated); + + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + + auto serverSurface = serverSurfaceCreated.first().first().value(); + auto serverSideDecoration = std::make_unique(); + serverSideDecoration->init(m_serverSideDecorationManager->create(*surface.get())); + QSignalSpy modeChangedSpy(serverSideDecoration.get(), &ServerSideDecoration::modeChanged); + QVERIFY(decorationCreated.wait()); + auto serverDeco = decorationCreated.first().first().value(); + QVERIFY(serverDeco); + + // destroy the parent surface + QSignalSpy surfaceDestroyedSpy(serverSurface, &QObject::destroyed); + QSignalSpy decorationDestroyedSpy(serverDeco, &QObject::destroyed); + surface.reset(); + QVERIFY(surfaceDestroyedSpy.wait()); + QVERIFY(decorationDestroyedSpy.isEmpty()); + // destroy the blur + serverSideDecoration.reset(); + QVERIFY(decorationDestroyedSpy.wait()); +} + +QTEST_GUILESS_MAIN(TestServerSideDecoration) +#include "test_server_side_decoration.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_server_side_decoration_palette.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_server_side_decoration_palette.cpp new file mode 100644 index 0000000000..74e0d53021 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_server_side_decoration_palette.cpp @@ -0,0 +1,187 @@ +/* + SPDX-FileCopyrightText: 2017 David Edmundson + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// KWin +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/server_decoration_palette.h" + +#include "qwayland-server-decoration-palette.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/surface.h" + +class ServerSideDecorationPaletteManager : public QtWayland::org_kde_kwin_server_decoration_palette_manager +{ +}; + +class ServerSideDecorationPalette : public QObject, public QtWayland::org_kde_kwin_server_decoration_palette +{ + Q_OBJECT + +public: + ~ServerSideDecorationPalette() + { + release(); + } +}; + +class TestServerSideDecorationPalette : public QObject +{ + Q_OBJECT +public: + explicit TestServerSideDecorationPalette(QObject *parent = nullptr); +private Q_SLOTS: + void init(); + void cleanup(); + + void testCreateAndSet(); + +private: + KWin::Display *m_display; + KWin::CompositorInterface *m_compositorInterface; + KWin::ServerSideDecorationPaletteManagerInterface *m_paletteManagerInterface; + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::Compositor *m_compositor; + ServerSideDecorationPaletteManager *m_paletteManager; + KWayland::Client::EventQueue *m_queue; + QThread *m_thread; +}; + +static const QString s_socketName = QStringLiteral("kwayland-test-wayland-decopalette-0"); + +TestServerSideDecorationPalette::TestServerSideDecorationPalette(QObject *parent) + : QObject(parent) + , m_display(nullptr) + , m_compositorInterface(nullptr) + , m_connection(nullptr) + , m_compositor(nullptr) + , m_queue(nullptr) + , m_thread(nullptr) +{ +} + +void TestServerSideDecorationPalette::init() +{ + using namespace KWin; + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + m_compositorInterface = new CompositorInterface(m_display, m_display); + m_paletteManagerInterface = new ServerSideDecorationPaletteManagerInterface(m_display, m_display); + + KWayland::Client::Registry registry; + + connect(®istry, &KWayland::Client::Registry::interfaceAnnounced, this, [this, ®istry](const QByteArray &interfaceName, quint32 name, quint32 version) { + if (interfaceName == org_kde_kwin_server_decoration_palette_manager_interface.name) { + m_paletteManager = new ServerSideDecorationPaletteManager(); + m_paletteManager->init(registry.registry(), name, version); + } + }); + + QSignalSpy interfacesAnnouncedSpy(®istry, &KWayland::Client::Registry::interfacesAnnounced); + QSignalSpy compositorSpy(®istry, &KWayland::Client::Registry::compositorAnnounced); + + QVERIFY(!registry.eventQueue()); + registry.setEventQueue(m_queue); + QCOMPARE(registry.eventQueue(), m_queue); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + QVERIFY(interfacesAnnouncedSpy.wait()); + + m_compositor = registry.createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + + QVERIFY(m_compositor); + QVERIFY(m_paletteManager); +} + +void TestServerSideDecorationPalette::cleanup() +{ +#define CLEANUP(variable) \ + if (variable) { \ + delete variable; \ + variable = nullptr; \ + } + CLEANUP(m_compositor) + CLEANUP(m_paletteManager) + CLEANUP(m_queue) + if (m_connection) { + m_connection->deleteLater(); + m_connection = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + CLEANUP(m_compositorInterface) + CLEANUP(m_paletteManagerInterface) + CLEANUP(m_display) +#undef CLEANUP +} + +void TestServerSideDecorationPalette::testCreateAndSet() +{ + QSignalSpy serverSurfaceCreated(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + + auto serverSurface = serverSurfaceCreated.first().first().value(); + QSignalSpy paletteCreatedSpy(m_paletteManagerInterface, &KWin::ServerSideDecorationPaletteManagerInterface::paletteCreated); + + QVERIFY(!m_paletteManagerInterface->paletteForSurface(serverSurface)); + + auto palette = std::make_unique(); + palette->init(m_paletteManager->create(*surface.get())); + QVERIFY(paletteCreatedSpy.wait()); + auto paletteInterface = paletteCreatedSpy.first().first().value(); + QCOMPARE(m_paletteManagerInterface->paletteForSurface(serverSurface), paletteInterface); + + QCOMPARE(paletteInterface->palette(), QString()); + + QSignalSpy changedSpy(paletteInterface, &KWin::ServerSideDecorationPaletteInterface::paletteChanged); + palette->set_palette(QStringLiteral("foobar")); + QVERIFY(changedSpy.wait()); + QCOMPARE(paletteInterface->palette(), QString("foobar")); + + // and destroy + QSignalSpy destroyedSpy(paletteInterface, &QObject::destroyed); + palette.reset(); + QVERIFY(destroyedSpy.wait()); + QVERIFY(!m_paletteManagerInterface->paletteForSurface(serverSurface)); +} + +QTEST_GUILESS_MAIN(TestServerSideDecorationPalette) +#include "test_server_side_decoration_palette.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_shadow.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_shadow.cpp new file mode 100644 index 0000000000..919fb73d56 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_shadow.cpp @@ -0,0 +1,266 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// client +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/shadow.h" +#include "KWayland/Client/shm_pool.h" +#include "KWayland/Client/surface.h" +// server +#include "core/graphicsbufferview.h" +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/shadow.h" + +using namespace KWin; + +class ShadowTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void init(); + void cleanup(); + + void testCreateShadow(); + void testShadowElements(); + void testSurfaceDestroy(); + +private: + KWin::Display *m_display = nullptr; + + KWayland::Client::ConnectionThread *m_connection = nullptr; + CompositorInterface *m_compositorInterface = nullptr; + ShadowManagerInterface *m_shadowInterface = nullptr; + QThread *m_thread = nullptr; + KWayland::Client::EventQueue *m_queue = nullptr; + KWayland::Client::ShmPool *m_shm = nullptr; + KWayland::Client::Compositor *m_compositor = nullptr; + KWayland::Client::ShadowManager *m_shadow = nullptr; +}; + +static const QString s_socketName = QStringLiteral("kwayland-test-shadow-0"); + +void ShadowTest::init() +{ + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + m_display->createShm(); + m_compositorInterface = new CompositorInterface(m_display, m_display); + m_shadowInterface = new ShadowManagerInterface(m_display, m_display); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + m_queue->setup(m_connection); + + KWayland::Client::Registry registry; + QSignalSpy interfacesAnnouncedSpy(®istry, &KWayland::Client::Registry::interfacesAnnounced); + registry.setEventQueue(m_queue); + registry.create(m_connection); + QVERIFY(registry.isValid()); + registry.setup(); + QVERIFY(interfacesAnnouncedSpy.wait()); + + m_shm = registry.createShmPool(registry.interface(KWayland::Client::Registry::Interface::Shm).name, registry.interface(KWayland::Client::Registry::Interface::Shm).version, this); + QVERIFY(m_shm->isValid()); + m_compositor = + registry.createCompositor(registry.interface(KWayland::Client::Registry::Interface::Compositor).name, registry.interface(KWayland::Client::Registry::Interface::Compositor).version, this); + QVERIFY(m_compositor->isValid()); + m_shadow = + registry.createShadowManager(registry.interface(KWayland::Client::Registry::Interface::Shadow).name, registry.interface(KWayland::Client::Registry::Interface::Shadow).version, this); + QVERIFY(m_shadow->isValid()); +} + +void ShadowTest::cleanup() +{ +#define CLEANUP(variable) \ + if (variable) { \ + delete variable; \ + variable = nullptr; \ + } + CLEANUP(m_shm) + CLEANUP(m_compositor) + CLEANUP(m_shadow) + CLEANUP(m_queue) + if (m_connection) { + m_connection->deleteLater(); + m_connection = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + + CLEANUP(m_display) +#undef CLEANUP + + // these are the children of the display + m_compositorInterface = nullptr; + m_shadowInterface = nullptr; +} + +void ShadowTest::testCreateShadow() +{ + // this test verifies the basic shadow behavior, create for surface, commit it, etc. + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + // a surface without anything should not have a Shadow + QVERIFY(!serverSurface->shadow()); + QSignalSpy shadowChangedSpy(serverSurface, &SurfaceInterface::shadowChanged); + + // let's create a shadow for the Surface + std::unique_ptr shadow(m_shadow->createShadow(surface.get())); + // that should not have triggered the shadowChangedSpy) + QVERIFY(!shadowChangedSpy.wait(100)); + + // now let's commit the surface, that should trigger the shadow changed + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(shadowChangedSpy.wait()); + QCOMPARE(shadowChangedSpy.count(), 1); + + // we didn't set anything on the shadow, so it should be all default values + auto serverShadow = serverSurface->shadow(); + QVERIFY(serverShadow); + QCOMPARE(serverShadow->offset(), QMarginsF()); + QVERIFY(!serverShadow->topLeft()); + QVERIFY(!serverShadow->top()); + QVERIFY(!serverShadow->topRight()); + QVERIFY(!serverShadow->right()); + QVERIFY(!serverShadow->bottomRight()); + QVERIFY(!serverShadow->bottom()); + QVERIFY(!serverShadow->bottomLeft()); + QVERIFY(!serverShadow->left()); + + // now let's remove the shadow + m_shadow->removeShadow(surface.get()); + // just removing should not remove it yet, surface needs to be committed + QVERIFY(!shadowChangedSpy.wait(100)); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(shadowChangedSpy.wait()); + QCOMPARE(shadowChangedSpy.count(), 2); + QVERIFY(!serverSurface->shadow()); +} + +static QImage bufferToImage(KWin::GraphicsBuffer *clientBuffer) +{ + if (clientBuffer) { + KWin::GraphicsBufferView view(clientBuffer); + if (QImage *image = view.image()) { + return image->copy(); + } + } + return QImage(); +} + +void ShadowTest::testShadowElements() +{ + // this test verifies that all shadow elements are correctly passed to the server + // first create surface + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + QSignalSpy shadowChangedSpy(serverSurface, &SurfaceInterface::shadowChanged); + + // now create the shadow + std::unique_ptr shadow(m_shadow->createShadow(surface.get())); + QImage topLeftImage(QSize(10, 10), QImage::Format_ARGB32_Premultiplied); + topLeftImage.fill(Qt::white); + shadow->attachTopLeft(m_shm->createBuffer(topLeftImage)); + QImage topImage(QSize(11, 11), QImage::Format_ARGB32_Premultiplied); + topImage.fill(Qt::black); + shadow->attachTop(m_shm->createBuffer(topImage)); + QImage topRightImage(QSize(12, 12), QImage::Format_ARGB32_Premultiplied); + topRightImage.fill(Qt::red); + shadow->attachTopRight(m_shm->createBuffer(topRightImage)); + QImage rightImage(QSize(13, 13), QImage::Format_ARGB32_Premultiplied); + rightImage.fill(Qt::darkRed); + shadow->attachRight(m_shm->createBuffer(rightImage)); + QImage bottomRightImage(QSize(14, 14), QImage::Format_ARGB32_Premultiplied); + bottomRightImage.fill(Qt::green); + shadow->attachBottomRight(m_shm->createBuffer(bottomRightImage)); + QImage bottomImage(QSize(15, 15), QImage::Format_ARGB32_Premultiplied); + bottomImage.fill(Qt::darkGreen); + shadow->attachBottom(m_shm->createBuffer(bottomImage)); + QImage bottomLeftImage(QSize(16, 16), QImage::Format_ARGB32_Premultiplied); + bottomLeftImage.fill(Qt::blue); + shadow->attachBottomLeft(m_shm->createBuffer(bottomLeftImage)); + QImage leftImage(QSize(17, 17), QImage::Format_ARGB32_Premultiplied); + leftImage.fill(Qt::darkBlue); + shadow->attachLeft(m_shm->createBuffer(leftImage)); + shadow->setOffsets(QMarginsF(1, 2, 3, 4)); + shadow->commit(); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + QVERIFY(shadowChangedSpy.wait()); + auto serverShadow = serverSurface->shadow(); + QVERIFY(serverShadow); + QCOMPARE(serverShadow->offset(), QMarginsF(1, 2, 3, 4)); + QCOMPARE(bufferToImage(serverShadow->topLeft()), topLeftImage); + QCOMPARE(bufferToImage(serverShadow->top()), topImage); + QCOMPARE(bufferToImage(serverShadow->topRight()), topRightImage); + QCOMPARE(bufferToImage(serverShadow->right()), rightImage); + QCOMPARE(bufferToImage(serverShadow->bottomRight()), bottomRightImage); + QCOMPARE(bufferToImage(serverShadow->bottom()), bottomImage); + QCOMPARE(bufferToImage(serverShadow->bottomLeft()), bottomLeftImage); + QCOMPARE(bufferToImage(serverShadow->left()), leftImage); +} + +void ShadowTest::testSurfaceDestroy() +{ + using namespace KWin; + QSignalSpy serverSurfaceCreated(m_compositorInterface, &CompositorInterface::surfaceCreated); + + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + auto serverSurface = serverSurfaceCreated.first().first().value(); + QSignalSpy shadowChangedSpy(serverSurface, &SurfaceInterface::shadowChanged); + + std::unique_ptr shadow(m_shadow->createShadow(surface.get())); + shadow->commit(); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(shadowChangedSpy.wait()); + auto serverShadow = serverSurface->shadow(); + QVERIFY(serverShadow); + + // destroy the parent surface + QSignalSpy surfaceDestroyedSpy(serverSurface, &QObject::destroyed); + QSignalSpy shadowDestroyedSpy(serverShadow, &QObject::destroyed); + surface.reset(); + QVERIFY(surfaceDestroyedSpy.wait()); + QVERIFY(shadowDestroyedSpy.isEmpty()); + // destroy the shadow + shadow.reset(); + QVERIFY(shadowDestroyedSpy.wait()); + QCOMPARE(shadowDestroyedSpy.count(), 1); +} + +QTEST_GUILESS_MAIN(ShadowTest) +#include "test_shadow.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_shm_pool.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_shm_pool.cpp new file mode 100644 index 0000000000..e491f118c6 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_shm_pool.cpp @@ -0,0 +1,210 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +#include +// KWin +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/surface.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/shm_pool.h" +#include "KWayland/Client/surface.h" + +class TestShmPool : public QObject +{ + Q_OBJECT +public: + explicit TestShmPool(QObject *parent = nullptr); +private Q_SLOTS: + void init(); + void cleanup(); + + void testCreateBufferNullImage(); + void testCreateBufferNullSize(); + void testCreateBufferInvalidSize(); + void testCreateBufferFromImage(); + void testCreateBufferFromImageWithAlpha(); + void testCreateBufferFromData(); + void testReuseBuffer(); + +private: + KWin::Display *m_display; + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::Compositor *m_compositor; + KWayland::Client::ShmPool *m_shmPool; + QThread *m_thread; +}; + +static const QString s_socketName = QStringLiteral("kwin-test-wayland-surface-0"); + +TestShmPool::TestShmPool(QObject *parent) + : QObject(parent) + , m_display(nullptr) + , m_connection(nullptr) + , m_compositor(nullptr) + , m_shmPool(nullptr) + , m_thread(nullptr) +{ +} + +void TestShmPool::init() +{ + using namespace KWin; + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + KWayland::Client::Registry registry; + QSignalSpy shmSpy(®istry, &KWayland::Client::Registry::shmAnnounced); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + + // here we need a shm pool + m_display->createShm(); + + QVERIFY(shmSpy.wait()); + m_shmPool = registry.createShmPool(shmSpy.first().first().value(), shmSpy.first().last().value(), this); +} + +void TestShmPool::cleanup() +{ + if (m_compositor) { + delete m_compositor; + m_compositor = nullptr; + } + if (m_shmPool) { + delete m_shmPool; + m_shmPool = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + delete m_connection; + m_connection = nullptr; + + delete m_display; + m_display = nullptr; +} + +void TestShmPool::testCreateBufferNullImage() +{ + QVERIFY(m_shmPool->isValid()); + QImage img; + QVERIFY(img.isNull()); + QVERIFY(!m_shmPool->createBuffer(img)); +} + +void TestShmPool::testCreateBufferNullSize() +{ + QVERIFY(m_shmPool->isValid()); + QSize size(0, 0); + QVERIFY(size.isNull()); + QVERIFY(!m_shmPool->createBuffer(size, 0, nullptr)); +} + +void TestShmPool::testCreateBufferInvalidSize() +{ + QVERIFY(m_shmPool->isValid()); + QSize size; + QVERIFY(!size.isValid()); + QVERIFY(!m_shmPool->createBuffer(size, 0, nullptr)); +} + +void TestShmPool::testCreateBufferFromImage() +{ + QVERIFY(m_shmPool->isValid()); + QImage img(24, 24, QImage::Format_RGB32); + img.fill(Qt::black); + QVERIFY(!img.isNull()); + auto buffer = m_shmPool->createBuffer(img).toStrongRef(); + QVERIFY(buffer); + QCOMPARE(buffer->size(), img.size()); + QImage img2(buffer->address(), img.width(), img.height(), QImage::Format_RGB32); + QCOMPARE(img2, img); +} + +void TestShmPool::testCreateBufferFromImageWithAlpha() +{ + QVERIFY(m_shmPool->isValid()); + QImage img(24, 24, QImage::Format_ARGB32_Premultiplied); + img.fill(QColor(255, 0, 0, 100)); // red with alpha + QVERIFY(!img.isNull()); + auto buffer = m_shmPool->createBuffer(img).toStrongRef(); + QVERIFY(buffer); + QCOMPARE(buffer->size(), img.size()); + QImage img2(buffer->address(), img.width(), img.height(), QImage::Format_ARGB32_Premultiplied); + QCOMPARE(img2, img); +} + +void TestShmPool::testCreateBufferFromData() +{ + QVERIFY(m_shmPool->isValid()); + QImage img(24, 24, QImage::Format_ARGB32_Premultiplied); + img.fill(Qt::black); + QVERIFY(!img.isNull()); + auto buffer = m_shmPool->createBuffer(img.size(), img.bytesPerLine(), img.constBits()).toStrongRef(); + QVERIFY(buffer); + QCOMPARE(buffer->size(), img.size()); + QImage img2(buffer->address(), img.width(), img.height(), QImage::Format_ARGB32_Premultiplied); + QCOMPARE(img2, img); +} + +void TestShmPool::testReuseBuffer() +{ + QVERIFY(m_shmPool->isValid()); + QImage img(24, 24, QImage::Format_ARGB32_Premultiplied); + img.fill(Qt::black); + QVERIFY(!img.isNull()); + auto buffer = m_shmPool->createBuffer(img).toStrongRef(); + QVERIFY(buffer); + buffer->setReleased(true); + buffer->setUsed(false); + + // same image should get the same buffer + auto buffer2 = m_shmPool->createBuffer(img).toStrongRef(); + QCOMPARE(buffer, buffer2); + buffer2->setReleased(true); + buffer2->setUsed(false); + + // image with different size should get us a new buffer + auto buffer3 = m_shmPool->getBuffer(QSize(10, 10), 8); + QVERIFY(buffer3 != buffer2); + + // image with a different format should get us a new buffer + QImage img2(24, 24, QImage::Format_RGB32); + img2.fill(Qt::black); + QVERIFY(!img2.isNull()); + auto buffer4 = m_shmPool->createBuffer(img2).toStrongRef(); + QVERIFY(buffer4); + QVERIFY(buffer4 != buffer2); + QVERIFY(buffer4 != buffer3); +} + +QTEST_GUILESS_MAIN(TestShmPool) +#include "test_shm_pool.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_text_input_v2.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_text_input_v2.cpp new file mode 100644 index 0000000000..b37c91bed8 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_text_input_v2.cpp @@ -0,0 +1,770 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// client +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/keyboard.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/seat.h" +#include "KWayland/Client/surface.h" +#include "KWayland/Client/textinput.h" +// server +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/seat.h" +#include "wayland/textinput.h" +#include "wayland/textinput_v2.h" + +using namespace KWin; +using namespace std::literals; + +class TextInputTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void init(); + void cleanup(); + + void testEnterLeave_data(); + void testEnterLeave(); + void testFocusedBeforeCreateTextInput(); + void testShowHidePanel(); + void testCursorRectangle(); + void testPreferredLanguage(); + void testReset(); + void testSurroundingText(); + void testContentHints_data(); + void testContentHints(); + void testContentPurpose_data(); + void testContentPurpose(); + void testTextDirection_data(); + void testTextDirection(); + void testLanguage(); + void testKeyEvent(); + void testPreEdit(); + void testCommit(); + +private: + SurfaceInterface *waitForSurface(); + KWayland::Client::TextInput *createTextInput(); + KWin::Display *m_display = nullptr; + SeatInterface *m_seatInterface = nullptr; + CompositorInterface *m_compositorInterface = nullptr; + TextInputManagerV2Interface *m_textInputManagerV2Interface = nullptr; + KWayland::Client::ConnectionThread *m_connection = nullptr; + QThread *m_thread = nullptr; + KWayland::Client::EventQueue *m_queue = nullptr; + KWayland::Client::Seat *m_seat = nullptr; + KWayland::Client::Keyboard *m_keyboard = nullptr; + KWayland::Client::Compositor *m_compositor = nullptr; + KWayland::Client::TextInputManager *m_textInputManagerV2 = nullptr; +}; + +static const QString s_socketName = QStringLiteral("kwayland-test-text-input-0"); + +void TextInputTest::init() +{ + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + m_display->createShm(); + m_seatInterface = new SeatInterface(m_display, QStringLiteral("seat0"), m_display); + m_seatInterface->setHasKeyboard(true); + m_seatInterface->setHasTouch(true); + m_compositorInterface = new CompositorInterface(m_display, m_display); + m_textInputManagerV2Interface = new TextInputManagerV2Interface(m_display, m_display); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + m_queue->setup(m_connection); + + KWayland::Client::Registry registry; + QSignalSpy interfacesAnnouncedSpy(®istry, &KWayland::Client::Registry::interfacesAnnounced); + registry.setEventQueue(m_queue); + registry.create(m_connection); + QVERIFY(registry.isValid()); + registry.setup(); + QVERIFY(interfacesAnnouncedSpy.wait()); + + m_seat = registry.createSeat(registry.interface(KWayland::Client::Registry::Interface::Seat).name, registry.interface(KWayland::Client::Registry::Interface::Seat).version, this); + QVERIFY(m_seat->isValid()); + QSignalSpy hasKeyboardSpy(m_seat, &KWayland::Client::Seat::hasKeyboardChanged); + QVERIFY(hasKeyboardSpy.wait()); + m_keyboard = m_seat->createKeyboard(this); + QVERIFY(m_keyboard->isValid()); + + m_compositor = + registry.createCompositor(registry.interface(KWayland::Client::Registry::Interface::Compositor).name, registry.interface(KWayland::Client::Registry::Interface::Compositor).version, this); + QVERIFY(m_compositor->isValid()); + + m_textInputManagerV2 = registry.createTextInputManager(registry.interface(KWayland::Client::Registry::Interface::TextInputManagerUnstableV2).name, + registry.interface(KWayland::Client::Registry::Interface::TextInputManagerUnstableV2).version, + this); + QVERIFY(m_textInputManagerV2->isValid()); +} + +void TextInputTest::cleanup() +{ +#define CLEANUP(variable) \ + if (variable) { \ + delete variable; \ + variable = nullptr; \ + } + CLEANUP(m_textInputManagerV2) + CLEANUP(m_keyboard) + CLEANUP(m_seat) + CLEANUP(m_compositor) + CLEANUP(m_queue) + if (m_connection) { + m_connection->deleteLater(); + m_connection = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + + CLEANUP(m_display) +#undef CLEANUP + + // these are the children of the display + m_textInputManagerV2Interface = nullptr; + m_compositorInterface = nullptr; + m_seatInterface = nullptr; +} + +SurfaceInterface *TextInputTest::waitForSurface() +{ + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + if (!surfaceCreatedSpy.isValid()) { + return nullptr; + } + if (!surfaceCreatedSpy.wait(500)) { + return nullptr; + } + if (surfaceCreatedSpy.count() != 1) { + return nullptr; + } + return surfaceCreatedSpy.first().first().value(); +} + +KWayland::Client::TextInput *TextInputTest::createTextInput() +{ + return m_textInputManagerV2->createTextInput(m_seat); +} + +void TextInputTest::testEnterLeave_data() +{ + QTest::addColumn("updatesDirectly"); + QTest::newRow("UnstableV2") << true; +} + +void TextInputTest::testEnterLeave() +{ + // this test verifies that enter leave are sent correctly + std::unique_ptr surface(m_compositor->createSurface()); + + std::unique_ptr textInput(createTextInput()); + auto serverSurface = waitForSurface(); + QVERIFY(serverSurface); + QVERIFY(textInput != nullptr); + QSignalSpy enteredSpy(textInput.get(), &KWayland::Client::TextInput::entered); + QSignalSpy leftSpy(textInput.get(), &KWayland::Client::TextInput::left); + QSignalSpy textInputChangedSpy(m_seatInterface, &SeatInterface::focusedTextInputSurfaceChanged); + + // now let's try to enter it + QVERIFY(!m_seatInterface->focusedTextInputSurface()); + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + QCOMPARE(m_seatInterface->focusedTextInputSurface(), serverSurface); + // text input not yet set for the surface + QFETCH(bool, updatesDirectly); + QCOMPARE(bool(m_seatInterface->textInputV2()), updatesDirectly); + QCOMPARE(textInputChangedSpy.isEmpty(), !updatesDirectly); + textInput->enable(surface.get()); + // this should trigger on server side + if (!updatesDirectly) { + QVERIFY(textInputChangedSpy.wait()); + } + QCOMPARE(textInputChangedSpy.count(), 1); + auto serverTextInput = m_seatInterface->textInputV2(); + QVERIFY(serverTextInput); + QSignalSpy enabledChangedSpy(serverTextInput, &TextInputV2Interface::enabledChanged); + if (updatesDirectly) { + QVERIFY(enabledChangedSpy.wait()); + enabledChangedSpy.clear(); + } + QCOMPARE(serverTextInput->surface().data(), serverSurface); + QVERIFY(serverTextInput->isEnabled()); + + // and trigger an enter + if (enteredSpy.isEmpty()) { + QVERIFY(enteredSpy.wait()); + } + QCOMPARE(enteredSpy.count(), 1); + QCOMPARE(textInput->enteredSurface(), surface.get()); + + // now trigger a leave + m_seatInterface->setFocusedKeyboardSurface(nullptr); + QCOMPARE(textInputChangedSpy.count(), 2); + QVERIFY(leftSpy.wait()); + QVERIFY(!textInput->enteredSurface()); + QVERIFY(!serverTextInput->isEnabled()); + + // if we enter again we should directly get the text input as it's still activated + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + QCOMPARE(textInputChangedSpy.count(), 3); + QVERIFY(m_seatInterface->textInputV2()); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 2); + QCOMPARE(textInput->enteredSurface(), surface.get()); + QVERIFY(serverTextInput->isEnabled()); + + // let's deactivate on client side + textInput->disable(surface.get()); + QVERIFY(enabledChangedSpy.wait()); + QCOMPARE(enabledChangedSpy.count(), 3); + QVERIFY(!serverTextInput->isEnabled()); + // does not trigger a leave + QCOMPARE(textInputChangedSpy.count(), 3); + // should still be the same text input + QCOMPARE(m_seatInterface->textInputV2(), serverTextInput); + // reset + textInput->enable(surface.get()); + QVERIFY(enabledChangedSpy.wait()); + + // delete the client and wait for the server to catch up + QSignalSpy unboundSpy(serverSurface, &QObject::destroyed); + surface.reset(); + QVERIFY(unboundSpy.wait()); + QVERIFY(leftSpy.wait()); + QVERIFY(!textInput->enteredSurface()); +} + +void TextInputTest::testFocusedBeforeCreateTextInput() +{ + // this test verifies that enter leave are sent correctly + std::unique_ptr surface(m_compositor->createSurface()); + auto serverSurface = waitForSurface(); + // now let's try to enter it + QSignalSpy textInputChangedSpy(m_seatInterface, &SeatInterface::focusedTextInputSurfaceChanged); + QVERIFY(!m_seatInterface->focusedTextInputSurface()); + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + QCOMPARE(m_seatInterface->focusedTextInputSurface(), serverSurface); + QCOMPARE(textInputChangedSpy.count(), 1); + + // This is null because there is no text input object for this client. + QCOMPARE(m_seatInterface->textInputV2()->surface(), nullptr); + + QVERIFY(serverSurface); + std::unique_ptr textInput(createTextInput()); + QVERIFY(textInput != nullptr); + QSignalSpy enteredSpy(textInput.get(), &KWayland::Client::TextInput::entered); + QSignalSpy leftSpy(textInput.get(), &KWayland::Client::TextInput::left); + + // and trigger an enter + if (enteredSpy.isEmpty()) { + QVERIFY(enteredSpy.wait()); + } + QCOMPARE(enteredSpy.count(), 1); + QCOMPARE(textInput->enteredSurface(), surface.get()); + + // This is not null anymore because there is a text input object associated with it. + QCOMPARE(m_seatInterface->textInputV2()->surface(), serverSurface); + + // now trigger a leave + m_seatInterface->setFocusedKeyboardSurface(nullptr); + QCOMPARE(textInputChangedSpy.count(), 2); + QVERIFY(leftSpy.wait()); + QVERIFY(!textInput->enteredSurface()); + + QCOMPARE(m_seatInterface->textInputV2()->surface(), nullptr); + QCOMPARE(m_seatInterface->focusedTextInputSurface(), nullptr); +} + +void TextInputTest::testShowHidePanel() +{ + // this test verifies that the requests for show/hide panel work + // and that status is properly sent to the client + std::unique_ptr surface(m_compositor->createSurface()); + auto serverSurface = waitForSurface(); + QVERIFY(serverSurface); + + std::unique_ptr textInput(createTextInput()); + QVERIFY(textInput != nullptr); + textInput->enable(surface.get()); + m_connection->flush(); + m_display->dispatchEvents(); + + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + auto ti = m_seatInterface->textInputV2(); + QVERIFY(ti); + + QSignalSpy showPanelRequestedSpy(ti, &TextInputV2Interface::requestShowInputPanel); + QSignalSpy hidePanelRequestedSpy(ti, &TextInputV2Interface::requestHideInputPanel); + QSignalSpy inputPanelStateChangedSpy(textInput.get(), &KWayland::Client::TextInput::inputPanelStateChanged); + + QCOMPARE(textInput->isInputPanelVisible(), false); + textInput->showInputPanel(); + QVERIFY(showPanelRequestedSpy.wait()); + ti->setInputPanelState(true, Rect(0, 0, 0, 0)); + QVERIFY(inputPanelStateChangedSpy.wait()); + QCOMPARE(textInput->isInputPanelVisible(), true); + + textInput->hideInputPanel(); + QVERIFY(hidePanelRequestedSpy.wait()); + ti->setInputPanelState(false, Rect(0, 0, 0, 0)); + QVERIFY(inputPanelStateChangedSpy.wait()); + QCOMPARE(textInput->isInputPanelVisible(), false); +} + +void TextInputTest::testCursorRectangle() +{ + // this test verifies that passing the cursor rectangle from client to server works + // and that setting visibility state from server to client works + std::unique_ptr surface(m_compositor->createSurface()); + auto serverSurface = waitForSurface(); + QVERIFY(serverSurface); + + std::unique_ptr textInput(createTextInput()); + QVERIFY(textInput != nullptr); + textInput->enable(surface.get()); + m_connection->flush(); + m_display->dispatchEvents(); + + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + auto ti = m_seatInterface->textInputV2(); + QVERIFY(ti); + QCOMPARE(ti->cursorRectangle(), Rect()); + QSignalSpy cursorRectangleChangedSpy(ti, &TextInputV2Interface::cursorRectangleChanged); + + textInput->setCursorRectangle(Rect(10, 20, 30, 40)); + QVERIFY(cursorRectangleChangedSpy.wait()); + QCOMPARE(ti->cursorRectangle(), Rect(10, 20, 30, 40)); +} + +void TextInputTest::testPreferredLanguage() +{ + // this test verifies that passing the preferred language from client to server works + std::unique_ptr surface(m_compositor->createSurface()); + auto serverSurface = waitForSurface(); + QVERIFY(serverSurface); + + std::unique_ptr textInput(createTextInput()); + QVERIFY(textInput != nullptr); + textInput->enable(surface.get()); + m_connection->flush(); + m_display->dispatchEvents(); + + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + auto ti = m_seatInterface->textInputV2(); + QVERIFY(ti); + QVERIFY(ti->preferredLanguage().isEmpty()); + + QSignalSpy preferredLanguageChangedSpy(ti, &TextInputV2Interface::preferredLanguageChanged); + textInput->setPreferredLanguage(QStringLiteral("foo")); + QVERIFY(preferredLanguageChangedSpy.wait()); + QCOMPARE(ti->preferredLanguage(), QStringLiteral("foo").toUtf8()); +} + +void TextInputTest::testReset() +{ + // this test verifies that the reset request is properly passed from client to server + std::unique_ptr surface(m_compositor->createSurface()); + auto serverSurface = waitForSurface(); + QVERIFY(serverSurface); + + std::unique_ptr textInput(createTextInput()); + QVERIFY(textInput != nullptr); + textInput->enable(surface.get()); + m_connection->flush(); + m_display->dispatchEvents(); + + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + auto ti = m_seatInterface->textInputV2(); + QVERIFY(ti); + + QSignalSpy stateUpdatedSpy(ti, &TextInputV2Interface::stateUpdated); + + textInput->reset(); + QVERIFY(stateUpdatedSpy.wait()); +} + +void TextInputTest::testSurroundingText() +{ + // this test verifies that surrounding text is properly passed around + std::unique_ptr surface(m_compositor->createSurface()); + auto serverSurface = waitForSurface(); + QVERIFY(serverSurface); + + std::unique_ptr textInput(createTextInput()); + QVERIFY(textInput != nullptr); + textInput->enable(surface.get()); + m_connection->flush(); + m_display->dispatchEvents(); + + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + auto ti = m_seatInterface->textInputV2(); + QVERIFY(ti); + QVERIFY(ti->surroundingText().isEmpty()); + QCOMPARE(ti->surroundingTextCursorPosition(), 0); + QCOMPARE(ti->surroundingTextSelectionAnchor(), 0); + + QSignalSpy surroundingTextChangedSpy(ti, &TextInputV2Interface::surroundingTextChanged); + + textInput->setSurroundingText(QStringLiteral("100 €, 100 $"), 5, 6); + QVERIFY(surroundingTextChangedSpy.wait()); + QCOMPARE(ti->surroundingText(), QStringLiteral("100 €, 100 $").toUtf8()); + QCOMPARE(ti->surroundingTextCursorPosition(), QStringLiteral("100 €, 100 $").toUtf8().indexOf(',')); + QCOMPARE(ti->surroundingTextSelectionAnchor(), QStringLiteral("100 €, 100 $").toUtf8().indexOf(' ', ti->surroundingTextCursorPosition())); +} + +void TextInputTest::testContentHints_data() +{ + QTest::addColumn("clientHints"); + QTest::addColumn("serverHints"); + + QTest::newRow("completion/v2") << KWayland::Client::TextInput::ContentHints(KWayland::Client::TextInput::ContentHint::AutoCompletion) + << KWin::TextInputContentHints(KWin::TextInputContentHint::AutoCompletion); + QTest::newRow("Correction/v2") << KWayland::Client::TextInput::ContentHints(KWayland::Client::TextInput::ContentHint::AutoCorrection) + << KWin::TextInputContentHints(KWin::TextInputContentHint::AutoCorrection); + QTest::newRow("Capitalization/v2") << KWayland::Client::TextInput::ContentHints(KWayland::Client::TextInput::ContentHint::AutoCapitalization) + << KWin::TextInputContentHints(KWin::TextInputContentHint::AutoCapitalization); + QTest::newRow("Lowercase/v2") << KWayland::Client::TextInput::ContentHints(KWayland::Client::TextInput::ContentHint::LowerCase) + << KWin::TextInputContentHints(KWin::TextInputContentHint::LowerCase); + QTest::newRow("Uppercase/v2") << KWayland::Client::TextInput::ContentHints(KWayland::Client::TextInput::ContentHint::UpperCase) + << KWin::TextInputContentHints(KWin::TextInputContentHint::UpperCase); + QTest::newRow("Titlecase/v2") << KWayland::Client::TextInput::ContentHints(KWayland::Client::TextInput::ContentHint::TitleCase) + << KWin::TextInputContentHints(KWin::TextInputContentHint::TitleCase); + QTest::newRow("HiddenText/v2") << KWayland::Client::TextInput::ContentHints(KWayland::Client::TextInput::ContentHint::HiddenText) + << KWin::TextInputContentHints(KWin::TextInputContentHint::HiddenText); + QTest::newRow("SensitiveData/v2") << KWayland::Client::TextInput::ContentHints(KWayland::Client::TextInput::ContentHint::SensitiveData) + << KWin::TextInputContentHints(KWin::TextInputContentHint::SensitiveData); + QTest::newRow("Latin/v2") << KWayland::Client::TextInput::ContentHints(KWayland::Client::TextInput::ContentHint::Latin) + << KWin::TextInputContentHints(KWin::TextInputContentHint::Latin); + QTest::newRow("Multiline/v2") << KWayland::Client::TextInput::ContentHints(KWayland::Client::TextInput::ContentHint::MultiLine) + << KWin::TextInputContentHints(KWin::TextInputContentHint::MultiLine); + + QTest::newRow("autos/v2") << (KWayland::Client::TextInput::ContentHint::AutoCompletion | KWayland::Client::TextInput::ContentHint::AutoCorrection | KWayland::Client::TextInput::ContentHint::AutoCapitalization) + << (KWin::TextInputContentHint::AutoCompletion | KWin::TextInputContentHint::AutoCorrection + | KWin::TextInputContentHint::AutoCapitalization); + + // all has combinations which don't make sense - what's both lowercase and uppercase? + QTest::newRow("all/v2") << (KWayland::Client::TextInput::ContentHint::AutoCompletion | KWayland::Client::TextInput::ContentHint::AutoCorrection | KWayland::Client::TextInput::ContentHint::AutoCapitalization + | KWayland::Client::TextInput::ContentHint::LowerCase | KWayland::Client::TextInput::ContentHint::UpperCase | KWayland::Client::TextInput::ContentHint::TitleCase + | KWayland::Client::TextInput::ContentHint::HiddenText | KWayland::Client::TextInput::ContentHint::SensitiveData | KWayland::Client::TextInput::ContentHint::Latin + | KWayland::Client::TextInput::ContentHint::MultiLine) + << (KWin::TextInputContentHint::AutoCompletion | KWin::TextInputContentHint::AutoCorrection + | KWin::TextInputContentHint::AutoCapitalization | KWin::TextInputContentHint::LowerCase + | KWin::TextInputContentHint::UpperCase | KWin::TextInputContentHint::TitleCase + | KWin::TextInputContentHint::HiddenText | KWin::TextInputContentHint::SensitiveData + | KWin::TextInputContentHint::Latin | KWin::TextInputContentHint::MultiLine); +} + +void TextInputTest::testContentHints() +{ + // this test verifies that content hints are properly passed from client to server + std::unique_ptr surface(m_compositor->createSurface()); + auto serverSurface = waitForSurface(); + QVERIFY(serverSurface); + + std::unique_ptr textInput(createTextInput()); + QVERIFY(textInput != nullptr); + textInput->enable(surface.get()); + m_connection->flush(); + m_display->dispatchEvents(); + + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + auto ti = m_seatInterface->textInputV2(); + QVERIFY(ti); + QCOMPARE(ti->contentHints(), KWin::TextInputContentHints()); + + QSignalSpy contentTypeChangedSpy(ti, &TextInputV2Interface::contentTypeChanged); + QFETCH(KWayland::Client::TextInput::ContentHints, clientHints); + textInput->setContentType(clientHints, KWayland::Client::TextInput::ContentPurpose::Normal); + QVERIFY(contentTypeChangedSpy.wait()); + QTEST(ti->contentHints(), "serverHints"); + + // setting to same should not trigger an update + textInput->setContentType(clientHints, KWayland::Client::TextInput::ContentPurpose::Normal); + QVERIFY(!contentTypeChangedSpy.wait(100)); + + // unsetting should work + textInput->setContentType(KWayland::Client::TextInput::ContentHints(), KWayland::Client::TextInput::ContentPurpose::Normal); + QVERIFY(contentTypeChangedSpy.wait()); + QCOMPARE(ti->contentHints(), KWin::TextInputContentHints()); +} + +void TextInputTest::testContentPurpose_data() +{ + QTest::addColumn("clientPurpose"); + QTest::addColumn("serverPurpose"); + + QTest::newRow("Alpha/v2") << KWayland::Client::TextInput::ContentPurpose::Alpha << KWin::TextInputContentPurpose::Alpha; + QTest::newRow("Digits/v2") << KWayland::Client::TextInput::ContentPurpose::Digits << KWin::TextInputContentPurpose::Digits; + QTest::newRow("Number/v2") << KWayland::Client::TextInput::ContentPurpose::Number << KWin::TextInputContentPurpose::Number; + QTest::newRow("Phone/v2") << KWayland::Client::TextInput::ContentPurpose::Phone << KWin::TextInputContentPurpose::Phone; + QTest::newRow("Url/v2") << KWayland::Client::TextInput::ContentPurpose::Url << KWin::TextInputContentPurpose::Url; + QTest::newRow("Email/v2") << KWayland::Client::TextInput::ContentPurpose::Email << KWin::TextInputContentPurpose::Email; + QTest::newRow("Name/v2") << KWayland::Client::TextInput::ContentPurpose::Name << KWin::TextInputContentPurpose::Name; + QTest::newRow("Password/v2") << KWayland::Client::TextInput::ContentPurpose::Password << KWin::TextInputContentPurpose::Password; + QTest::newRow("Date/v2") << KWayland::Client::TextInput::ContentPurpose::Date << KWin::TextInputContentPurpose::Date; + QTest::newRow("Time/v2") << KWayland::Client::TextInput::ContentPurpose::Time << KWin::TextInputContentPurpose::Time; + QTest::newRow("Datetime/v2") << KWayland::Client::TextInput::ContentPurpose::DateTime << KWin::TextInputContentPurpose::DateTime; + QTest::newRow("Terminal/v2") << KWayland::Client::TextInput::ContentPurpose::Terminal << KWin::TextInputContentPurpose::Terminal; +} + +void TextInputTest::testContentPurpose() +{ + // this test verifies that content purpose are properly passed from client to server + std::unique_ptr surface(m_compositor->createSurface()); + auto serverSurface = waitForSurface(); + QVERIFY(serverSurface); + + std::unique_ptr textInput(createTextInput()); + QVERIFY(textInput != nullptr); + textInput->enable(surface.get()); + m_connection->flush(); + m_display->dispatchEvents(); + + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + auto ti = m_seatInterface->textInputV2(); + QVERIFY(ti); + QCOMPARE(ti->contentPurpose(), KWin::TextInputContentPurpose::Normal); + + QSignalSpy contentTypeChangedSpy(ti, &TextInputV2Interface::contentTypeChanged); + QFETCH(KWayland::Client::TextInput::ContentPurpose, clientPurpose); + textInput->setContentType(KWayland::Client::TextInput::ContentHints(), clientPurpose); + QVERIFY(contentTypeChangedSpy.wait()); + QTEST(ti->contentPurpose(), "serverPurpose"); + + // setting to same should not trigger an update + textInput->setContentType(KWayland::Client::TextInput::ContentHints(), clientPurpose); + QVERIFY(!contentTypeChangedSpy.wait(100)); + + // unsetting should work + textInput->setContentType(KWayland::Client::TextInput::ContentHints(), KWayland::Client::TextInput::ContentPurpose::Normal); + QVERIFY(contentTypeChangedSpy.wait()); + QCOMPARE(ti->contentPurpose(), KWin::TextInputContentPurpose::Normal); +} + +void TextInputTest::testTextDirection_data() +{ + QTest::addColumn("textDirection"); + + QTest::newRow("ltr/v0") << Qt::LeftToRight; + QTest::newRow("rtl/v0") << Qt::RightToLeft; + + QTest::newRow("ltr/v2") << Qt::LeftToRight; + QTest::newRow("rtl/v2") << Qt::RightToLeft; +} + +void TextInputTest::testTextDirection() +{ + // this test verifies that the text direction is sent from server to client + std::unique_ptr surface(m_compositor->createSurface()); + auto serverSurface = waitForSurface(); + QVERIFY(serverSurface); + + std::unique_ptr textInput(createTextInput()); + QVERIFY(textInput != nullptr); + // default should be auto + QCOMPARE(textInput->textDirection(), Qt::LayoutDirectionAuto); + textInput->enable(surface.get()); + m_connection->flush(); + m_display->dispatchEvents(); + + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + auto ti = m_seatInterface->textInputV2(); + QVERIFY(ti); + + // let's send the new text direction + QSignalSpy textDirectionChangedSpy(textInput.get(), &KWayland::Client::TextInput::textDirectionChanged); + QFETCH(Qt::LayoutDirection, textDirection); + ti->setTextDirection(textDirection); + QVERIFY(textDirectionChangedSpy.wait()); + QCOMPARE(textInput->textDirection(), textDirection); + // setting again should not change + ti->setTextDirection(textDirection); + QVERIFY(!textDirectionChangedSpy.wait(100)); + + // setting back to auto + ti->setTextDirection(Qt::LayoutDirectionAuto); + QVERIFY(textDirectionChangedSpy.wait()); + QCOMPARE(textInput->textDirection(), Qt::LayoutDirectionAuto); +} + +void TextInputTest::testLanguage() +{ + // this test verifies that language is sent from server to client + std::unique_ptr surface(m_compositor->createSurface()); + auto serverSurface = waitForSurface(); + QVERIFY(serverSurface); + + std::unique_ptr textInput(createTextInput()); + QVERIFY(textInput != nullptr); + // default should be empty + QVERIFY(textInput->language().isEmpty()); + textInput->enable(surface.get()); + m_connection->flush(); + m_display->dispatchEvents(); + + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + auto ti = m_seatInterface->textInputV2(); + QVERIFY(ti); + + // let's send the new language + QSignalSpy langugageChangedSpy(textInput.get(), &KWayland::Client::TextInput::languageChanged); + ti->setLanguage(QByteArrayLiteral("foo")); + QVERIFY(langugageChangedSpy.wait()); + QCOMPARE(textInput->language(), QByteArrayLiteral("foo")); + // setting to same should not trigger + ti->setLanguage(QByteArrayLiteral("foo")); + QVERIFY(!langugageChangedSpy.wait(100)); + // but to something else should trigger again + ti->setLanguage(QByteArrayLiteral("bar")); + QVERIFY(langugageChangedSpy.wait()); + QCOMPARE(textInput->language(), QByteArrayLiteral("bar")); +} + +void TextInputTest::testKeyEvent() +{ + qRegisterMetaType(); + qRegisterMetaType(); + // this test verifies that key events are properly sent to the client + std::unique_ptr surface(m_compositor->createSurface()); + auto serverSurface = waitForSurface(); + QVERIFY(serverSurface); + + std::unique_ptr textInput(createTextInput()); + QVERIFY(textInput != nullptr); + textInput->enable(surface.get()); + m_connection->flush(); + m_display->dispatchEvents(); + + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + auto ti = m_seatInterface->textInputV2(); + QVERIFY(ti); + + // TODO: test modifiers + QSignalSpy keyEventSpy(textInput.get(), &KWayland::Client::TextInput::keyEvent); + m_seatInterface->setTimestamp(100ms); + ti->keysymPressed(2); + QVERIFY(keyEventSpy.wait()); + QCOMPARE(keyEventSpy.count(), 1); + QCOMPARE(keyEventSpy.last().at(0).value(), 2u); + QCOMPARE(keyEventSpy.last().at(1).value(), KWayland::Client::TextInput::KeyState::Pressed); + QCOMPARE(keyEventSpy.last().at(2).value(), Qt::KeyboardModifiers()); + QCOMPARE(keyEventSpy.last().at(3).value(), 100u); + m_seatInterface->setTimestamp(101ms); + ti->keysymReleased(2); + QVERIFY(keyEventSpy.wait()); + QCOMPARE(keyEventSpy.count(), 2); + QCOMPARE(keyEventSpy.last().at(0).value(), 2u); + QCOMPARE(keyEventSpy.last().at(1).value(), KWayland::Client::TextInput::KeyState::Released); + QCOMPARE(keyEventSpy.last().at(2).value(), Qt::KeyboardModifiers()); + QCOMPARE(keyEventSpy.last().at(3).value(), 101u); +} + +void TextInputTest::testPreEdit() +{ + // this test verifies that pre-edit is correctly passed to the client + std::unique_ptr surface(m_compositor->createSurface()); + auto serverSurface = waitForSurface(); + QVERIFY(serverSurface); + + std::unique_ptr textInput(createTextInput()); + QVERIFY(textInput != nullptr); + // verify default values + QVERIFY(textInput->composingText().isEmpty()); + QVERIFY(textInput->composingFallbackText().isEmpty()); + QCOMPARE(textInput->composingTextCursorPosition(), 0); + + textInput->enable(surface.get()); + m_connection->flush(); + m_display->dispatchEvents(); + + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + auto ti = m_seatInterface->textInputV2(); + QVERIFY(ti); + + // now let's pass through some pre-edit events + QSignalSpy composingTextChangedSpy(textInput.get(), &KWayland::Client::TextInput::composingTextChanged); + ti->setPreEditCursor(1); + ti->preEdit(QByteArrayLiteral("foo"), QByteArrayLiteral("bar")); + QVERIFY(composingTextChangedSpy.wait()); + QCOMPARE(composingTextChangedSpy.count(), 1); + QCOMPARE(textInput->composingText(), QByteArrayLiteral("foo")); + QCOMPARE(textInput->composingFallbackText(), QByteArrayLiteral("bar")); + QCOMPARE(textInput->composingTextCursorPosition(), 1); + + // when no pre edit cursor is sent, it's at end of text + ti->preEdit(QByteArrayLiteral("foobar"), QByteArray()); + QVERIFY(composingTextChangedSpy.wait()); + QCOMPARE(composingTextChangedSpy.count(), 2); + QCOMPARE(textInput->composingText(), QByteArrayLiteral("foobar")); + QCOMPARE(textInput->composingFallbackText(), QByteArray()); + QCOMPARE(textInput->composingTextCursorPosition(), 6); +} + +void TextInputTest::testCommit() +{ + // this test verifies that the commit is handled correctly by the client + std::unique_ptr surface(m_compositor->createSurface()); + auto serverSurface = waitForSurface(); + QVERIFY(serverSurface); + + std::unique_ptr textInput(createTextInput()); + QVERIFY(textInput != nullptr); + // verify default values + QCOMPARE(textInput->commitText(), QByteArray()); + QCOMPARE(textInput->cursorPosition(), 0); + QCOMPARE(textInput->anchorPosition(), 0); + QCOMPARE(textInput->deleteSurroundingText().beforeLength, 0u); + QCOMPARE(textInput->deleteSurroundingText().afterLength, 0u); + + textInput->enable(surface.get()); + m_connection->flush(); + m_display->dispatchEvents(); + + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + auto ti = m_seatInterface->textInputV2(); + QVERIFY(ti); + + // now let's commit + QSignalSpy committedSpy(textInput.get(), &KWayland::Client::TextInput::committed); + ti->setCursorPosition(3, 4); + ti->deleteSurroundingText(2, 1); + ti->commitString(QByteArrayLiteral("foo")); + + QVERIFY(committedSpy.wait()); + QCOMPARE(textInput->commitText(), QByteArrayLiteral("foo")); + QCOMPARE(textInput->cursorPosition(), 3); + QCOMPARE(textInput->anchorPosition(), 4); + QCOMPARE(textInput->deleteSurroundingText().beforeLength, 2u); + QCOMPARE(textInput->deleteSurroundingText().afterLength, 1u); +} + +QTEST_GUILESS_MAIN(TextInputTest) +#include "test_text_input_v2.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_appmenu.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_appmenu.cpp new file mode 100644 index 0000000000..df540cedd6 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_appmenu.cpp @@ -0,0 +1,171 @@ +/* + SPDX-FileCopyrightText: 2017 David Edmundson + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// KWin +#include "wayland/appmenu.h" +#include "wayland/compositor.h" +#include "wayland/display.h" + +#include "KWayland/Client/appmenu.h" +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/region.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/surface.h" + +Q_DECLARE_METATYPE(KWin::AppMenuInterface::InterfaceAddress) + +class TestAppmenu : public QObject +{ + Q_OBJECT +public: + explicit TestAppmenu(QObject *parent = nullptr); +private Q_SLOTS: + void init(); + void cleanup(); + + void testCreateAndSet(); + +private: + KWin::Display *m_display; + KWin::CompositorInterface *m_compositorInterface; + KWin::AppMenuManagerInterface *m_appmenuManagerInterface; + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::Compositor *m_compositor; + KWayland::Client::AppMenuManager *m_appmenuManager; + KWayland::Client::EventQueue *m_queue; + QThread *m_thread; +}; + +static const QString s_socketName = QStringLiteral("kwayland-test-wayland-appmenu-0"); + +TestAppmenu::TestAppmenu(QObject *parent) + : QObject(parent) + , m_display(nullptr) + , m_compositorInterface(nullptr) + , m_connection(nullptr) + , m_compositor(nullptr) + , m_queue(nullptr) + , m_thread(nullptr) +{ +} + +void TestAppmenu::init() +{ + using namespace KWin; + qRegisterMetaType(); + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + KWayland::Client::Registry registry; + QSignalSpy compositorSpy(®istry, &KWayland::Client::Registry::compositorAnnounced); + + QSignalSpy appmenuSpy(®istry, &KWayland::Client::Registry::appMenuAnnounced); + + QVERIFY(!registry.eventQueue()); + registry.setEventQueue(m_queue); + QCOMPARE(registry.eventQueue(), m_queue); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + + m_compositorInterface = new CompositorInterface(m_display, m_display); + QVERIFY(compositorSpy.wait()); + m_compositor = registry.createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + + m_appmenuManagerInterface = new AppMenuManagerInterface(m_display, m_display); + + QVERIFY(appmenuSpy.wait()); + m_appmenuManager = registry.createAppMenuManager(appmenuSpy.first().first().value(), appmenuSpy.first().last().value(), this); +} + +void TestAppmenu::cleanup() +{ +#define CLEANUP(variable) \ + if (variable) { \ + delete variable; \ + variable = nullptr; \ + } + CLEANUP(m_compositor) + CLEANUP(m_appmenuManager) + CLEANUP(m_queue) + if (m_connection) { + m_connection->deleteLater(); + m_connection = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + CLEANUP(m_compositorInterface) + CLEANUP(m_appmenuManagerInterface) + CLEANUP(m_display) +#undef CLEANUP +} + +void TestAppmenu::testCreateAndSet() +{ + QSignalSpy serverSurfaceCreated(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + + auto serverSurface = serverSurfaceCreated.first().first().value(); + QSignalSpy appMenuCreated(m_appmenuManagerInterface, &KWin::AppMenuManagerInterface::appMenuCreated); + + QVERIFY(!m_appmenuManagerInterface->appMenuForSurface(serverSurface)); + + auto appmenu = m_appmenuManager->create(surface.get(), surface.get()); + QVERIFY(appMenuCreated.wait()); + auto appMenuInterface = appMenuCreated.first().first().value(); + QCOMPARE(m_appmenuManagerInterface->appMenuForSurface(serverSurface), appMenuInterface); + + QCOMPARE(appMenuInterface->address().serviceName, QString()); + QCOMPARE(appMenuInterface->address().objectPath, QString()); + + QSignalSpy appMenuChangedSpy(appMenuInterface, &KWin::AppMenuInterface::addressChanged); + + appmenu->setAddress("net.somename", "/test/path"); + + QVERIFY(appMenuChangedSpy.wait()); + QCOMPARE(appMenuInterface->address().serviceName, QString("net.somename")); + QCOMPARE(appMenuInterface->address().objectPath, QString("/test/path")); + + // and destroy + QSignalSpy destroyedSpy(appMenuInterface, &QObject::destroyed); + delete appmenu; + QVERIFY(destroyedSpy.wait()); + QVERIFY(!m_appmenuManagerInterface->appMenuForSurface(serverSurface)); +} + +QTEST_GUILESS_MAIN(TestAppmenu) +#include "test_wayland_appmenu.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_blur.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_blur.cpp new file mode 100644 index 0000000000..8936de9e89 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_blur.cpp @@ -0,0 +1,187 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// KWin +#include "wayland/blur.h" +#include "wayland/compositor.h" +#include "wayland/display.h" + +#include "KWayland/Client/blur.h" +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/region.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/surface.h" + +class TestBlur : public QObject +{ + Q_OBJECT +public: + explicit TestBlur(QObject *parent = nullptr); +private Q_SLOTS: + void init(); + void cleanup(); + + void testCreate(); + void testSurfaceDestroy(); + +private: + KWin::Display *m_display; + KWin::CompositorInterface *m_compositorInterface; + KWin::BlurManagerInterface *m_blurManagerInterface; + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::Compositor *m_compositor; + KWayland::Client::BlurManager *m_blurManager; + KWayland::Client::EventQueue *m_queue; + QThread *m_thread; +}; + +static const QString s_socketName = QStringLiteral("kwayland-test-wayland-blur-0"); + +TestBlur::TestBlur(QObject *parent) + : QObject(parent) + , m_display(nullptr) + , m_compositorInterface(nullptr) + , m_connection(nullptr) + , m_compositor(nullptr) + , m_queue(nullptr) + , m_thread(nullptr) +{ +} + +void TestBlur::init() +{ + using namespace KWin; + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + KWayland::Client::Registry registry; + QSignalSpy compositorSpy(®istry, &KWayland::Client::Registry::compositorAnnounced); + + QSignalSpy blurSpy(®istry, &KWayland::Client::Registry::blurAnnounced); + + QVERIFY(!registry.eventQueue()); + registry.setEventQueue(m_queue); + QCOMPARE(registry.eventQueue(), m_queue); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + + m_compositorInterface = new CompositorInterface(m_display, m_display); + QVERIFY(compositorSpy.wait()); + m_compositor = registry.createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + + m_blurManagerInterface = new BlurManagerInterface(m_display, m_display); + QVERIFY(blurSpy.wait()); + m_blurManager = registry.createBlurManager(blurSpy.first().first().value(), blurSpy.first().last().value(), this); +} + +void TestBlur::cleanup() +{ +#define CLEANUP(variable) \ + if (variable) { \ + delete variable; \ + variable = nullptr; \ + } + CLEANUP(m_compositor) + CLEANUP(m_blurManager) + CLEANUP(m_queue) + if (m_connection) { + m_connection->deleteLater(); + m_connection = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + CLEANUP(m_display) +#undef CLEANUP + + // these are the children of the display + m_compositorInterface = nullptr; + m_blurManagerInterface = nullptr; +} + +void TestBlur::testCreate() +{ + QSignalSpy serverSurfaceCreated(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + + auto serverSurface = serverSurfaceCreated.first().first().value(); + QSignalSpy blurChanged(serverSurface, &KWin::SurfaceInterface::blurChanged); + + auto blur = m_blurManager->createBlur(surface.get(), surface.get()); + blur->setRegion(m_compositor->createRegion(QRegion(0, 0, 10, 20), nullptr)); + blur->commit(); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + QVERIFY(blurChanged.wait()); + QCOMPARE(serverSurface->blur()->region(), KWin::Region(0, 0, 10, 20)); + + // and destroy + QSignalSpy destroyedSpy(serverSurface->blur(), &QObject::destroyed); + delete blur; + QVERIFY(destroyedSpy.wait()); +} + +void TestBlur::testSurfaceDestroy() +{ + QSignalSpy serverSurfaceCreated(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + + auto serverSurface = serverSurfaceCreated.first().first().value(); + QSignalSpy blurChanged(serverSurface, &KWin::SurfaceInterface::blurChanged); + + std::unique_ptr blur(m_blurManager->createBlur(surface.get())); + blur->setRegion(m_compositor->createRegion(QRegion(0, 0, 10, 20), nullptr)); + blur->commit(); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + QVERIFY(blurChanged.wait()); + QCOMPARE(serverSurface->blur()->region(), KWin::Region(0, 0, 10, 20)); + + // destroy the parent surface + QSignalSpy surfaceDestroyedSpy(serverSurface, &QObject::destroyed); + QSignalSpy blurDestroyedSpy(serverSurface->blur(), &QObject::destroyed); + surface.reset(); + QVERIFY(surfaceDestroyedSpy.wait()); + QVERIFY(blurDestroyedSpy.isEmpty()); + // destroy the blur + blur.reset(); + QVERIFY(blurDestroyedSpy.wait()); +} + +QTEST_GUILESS_MAIN(TestBlur) +#include "test_wayland_blur.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_contrast.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_contrast.cpp new file mode 100644 index 0000000000..c6472dbd10 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_contrast.cpp @@ -0,0 +1,199 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2015 Marco Martin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// KWin +#include "wayland/compositor.h" +#include "wayland/contrast.h" +#include "wayland/display.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/contrast.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/region.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/surface.h" + +#include + +class TestContrast : public QObject +{ + Q_OBJECT +public: + explicit TestContrast(QObject *parent = nullptr); +private Q_SLOTS: + void init(); + void cleanup(); + + void testCreate(); + void testSurfaceDestroy(); + +private: + KWin::Display *m_display; + KWin::CompositorInterface *m_compositorInterface; + KWin::ContrastManagerInterface *m_contrastManagerInterface; + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::Compositor *m_compositor; + KWayland::Client::ContrastManager *m_contrastManager; + KWayland::Client::EventQueue *m_queue; + QThread *m_thread; +}; + +static const QString s_socketName = QStringLiteral("kwayland-test-wayland-contrast-0"); + +TestContrast::TestContrast(QObject *parent) + : QObject(parent) + , m_display(nullptr) + , m_compositorInterface(nullptr) + , m_connection(nullptr) + , m_compositor(nullptr) + , m_queue(nullptr) + , m_thread(nullptr) +{ +} + +void TestContrast::init() +{ + using namespace KWin; + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + KWayland::Client::Registry registry; + QSignalSpy compositorSpy(®istry, &KWayland::Client::Registry::compositorAnnounced); + + QSignalSpy contrastSpy(®istry, &KWayland::Client::Registry::contrastAnnounced); + + QVERIFY(!registry.eventQueue()); + registry.setEventQueue(m_queue); + QCOMPARE(registry.eventQueue(), m_queue); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + + m_compositorInterface = new CompositorInterface(m_display, m_display); + QVERIFY(compositorSpy.wait()); + m_compositor = registry.createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + + m_contrastManagerInterface = new ContrastManagerInterface(m_display, m_display); + + QVERIFY(contrastSpy.wait()); + m_contrastManager = registry.createContrastManager(contrastSpy.first().first().value(), contrastSpy.first().last().value(), this); +} + +void TestContrast::cleanup() +{ +#define CLEANUP(variable) \ + if (variable) { \ + delete variable; \ + variable = nullptr; \ + } + CLEANUP(m_compositor) + CLEANUP(m_contrastManager) + CLEANUP(m_queue) + if (m_connection) { + m_connection->deleteLater(); + m_connection = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + CLEANUP(m_display) +#undef CLEANUP + + // these are the children of the display + m_compositorInterface = nullptr; + m_contrastManagerInterface = nullptr; +} + +void TestContrast::testCreate() +{ + QSignalSpy serverSurfaceCreated(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + + auto serverSurface = serverSurfaceCreated.first().first().value(); + QSignalSpy contrastChanged(serverSurface, &KWin::SurfaceInterface::contrastChanged); + + auto contrast = m_contrastManager->createContrast(surface.get(), surface.get()); + contrast->setRegion(m_compositor->createRegion(QRegion(0, 0, 10, 20), nullptr)); + + contrast->setContrast(0.2); + contrast->setIntensity(2.0); + contrast->setSaturation(1.7); + + contrast->commit(); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + QVERIFY(contrastChanged.wait()); + QCOMPARE(serverSurface->contrast()->region(), KWin::Region(0, 0, 10, 20)); + QCOMPARE(wl_fixed_from_double(serverSurface->contrast()->contrast()), wl_fixed_from_double(0.2)); + QCOMPARE(wl_fixed_from_double(serverSurface->contrast()->intensity()), wl_fixed_from_double(2.0)); + QCOMPARE(wl_fixed_from_double(serverSurface->contrast()->saturation()), wl_fixed_from_double(1.7)); + + // and destroy + QSignalSpy destroyedSpy(serverSurface->contrast(), &QObject::destroyed); + delete contrast; + QVERIFY(destroyedSpy.wait()); +} + +void TestContrast::testSurfaceDestroy() +{ + QSignalSpy serverSurfaceCreated(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + + auto serverSurface = serverSurfaceCreated.first().first().value(); + QSignalSpy contrastChanged(serverSurface, &KWin::SurfaceInterface::contrastChanged); + + std::unique_ptr contrast(m_contrastManager->createContrast(surface.get())); + contrast->setRegion(m_compositor->createRegion(QRegion(0, 0, 10, 20), nullptr)); + contrast->commit(); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + QVERIFY(contrastChanged.wait()); + QCOMPARE(serverSurface->contrast()->region(), KWin::Region(0, 0, 10, 20)); + + // destroy the parent surface + QSignalSpy surfaceDestroyedSpy(serverSurface, &QObject::destroyed); + QSignalSpy contrastDestroyedSpy(serverSurface->contrast(), &QObject::destroyed); + surface.reset(); + QVERIFY(surfaceDestroyedSpy.wait()); + QVERIFY(contrastDestroyedSpy.isEmpty()); + // destroy the blur + contrast.reset(); + QVERIFY(contrastDestroyedSpy.wait()); +} + +QTEST_GUILESS_MAIN(TestContrast) +#include "test_wayland_contrast.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_filter.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_filter.cpp new file mode 100644 index 0000000000..e3fa58ad5d --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_filter.cpp @@ -0,0 +1,149 @@ +/* + SPDX-FileCopyrightText: 2017 David Edmundson + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// KWin +#include "wayland/blur.h" +#include "wayland/clientconnection.h" +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/filtered_display.h" + +#include "KWayland/Client/blur.h" +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/region.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/surface.h" + +#include + +class TestDisplay; + +class TestFilter : public QObject +{ + Q_OBJECT +public: + explicit TestFilter(QObject *parent = nullptr); +private Q_SLOTS: + void init(); + void cleanup(); + void testFilter_data(); + void testFilter(); + +private: + TestDisplay *m_display; + KWin::CompositorInterface *m_compositorInterface; + KWin::BlurManagerInterface *m_blurManagerInterface; +}; + +static const QString s_socketName = QStringLiteral("kwayland-test-wayland-blur-0"); + +// The following non-realistic class allows only clients in the m_allowedClients list to access the blur interface +// all other interfaces are allowed +class TestDisplay : public KWin::FilteredDisplay +{ +public: + TestDisplay(QObject *parent); + bool allowInterface(KWin::ClientConnection *client, const QByteArray &interfaceName) override; + QList m_allowedClients; +}; + +TestDisplay::TestDisplay(QObject *parent) + : KWin::FilteredDisplay(parent) +{ +} + +bool TestDisplay::allowInterface(KWin::ClientConnection *client, const QByteArray &interfaceName) +{ + if (interfaceName == "org_kde_kwin_blur_manager") { + return m_allowedClients.contains(*client); + } + return true; +} + +TestFilter::TestFilter(QObject *parent) + : QObject(parent) + , m_display(nullptr) + , m_compositorInterface(nullptr) +{ +} + +void TestFilter::init() +{ + using namespace KWin; + delete m_display; + m_display = new TestDisplay(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + + m_compositorInterface = new CompositorInterface(m_display, m_display); + m_blurManagerInterface = new BlurManagerInterface(m_display, m_display); +} + +void TestFilter::cleanup() +{ +} + +void TestFilter::testFilter_data() +{ + QTest::addColumn("accessAllowed"); + QTest::newRow("granted") << true; + QTest::newRow("denied") << false; +} + +void TestFilter::testFilter() +{ + QFETCH(bool, accessAllowed); + + // setup connection + std::unique_ptr connection(new KWayland::Client::ConnectionThread()); + QSignalSpy connectedSpy(connection.get(), &KWayland::Client::ConnectionThread::connected); + connection->setSocketName(s_socketName); + + std::unique_ptr thread(new QThread(this)); + connection->moveToThread(thread.get()); + thread->start(); + + connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + // use low level API as Server::Display::connections only lists connections which have + // been previous fetched via getConnection() + if (accessAllowed) { + wl_client *clientConnection; + wl_client_for_each(clientConnection, wl_display_get_client_list(*m_display)) + { + m_display->m_allowedClients << clientConnection; + } + } + + KWayland::Client::EventQueue queue; + queue.setup(connection.get()); + + KWayland::Client::Registry registry; + QSignalSpy registryDoneSpy(®istry, &KWayland::Client::Registry::interfacesAnnounced); + QSignalSpy compositorSpy(®istry, &KWayland::Client::Registry::compositorAnnounced); + QSignalSpy blurSpy(®istry, &KWayland::Client::Registry::blurAnnounced); + + registry.setEventQueue(&queue); + registry.create(connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + + QVERIFY(registryDoneSpy.wait()); + QVERIFY(compositorSpy.count() == 1); + QVERIFY(blurSpy.count() == accessAllowed ? 1 : 0); + + thread->quit(); + thread->wait(); +} + +QTEST_GUILESS_MAIN(TestFilter) +#include "test_wayland_filter.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_output.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_output.cpp new file mode 100644 index 0000000000..256b1726f5 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_output.cpp @@ -0,0 +1,335 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// KWin +#include "wayland/display.h" +#include "wayland/output.h" + +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/output.h" +#include "KWayland/Client/registry.h" + +#include "../../../tests/fakeoutput.h" + +// Wayland +#include + +class TestWaylandOutput : public QObject +{ + Q_OBJECT +public: + explicit TestWaylandOutput(QObject *parent = nullptr); +private Q_SLOTS: + void init(); + void cleanup(); + + void testRegistry(); + void testModeChange(); + void testScaleChange(); + + void testSubPixel_data(); + void testSubPixel(); + + void testTransform_data(); + void testTransform(); + +private: + KWin::Display *m_display; + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::EventQueue *m_queue; + QThread *m_thread; +}; + +static const QString s_socketName = QStringLiteral("kwin-test-wayland-output-0"); + +TestWaylandOutput::TestWaylandOutput(QObject *parent) + : QObject(parent) + , m_display(nullptr) + , m_connection(nullptr) + , m_thread(nullptr) +{ +} + +void TestWaylandOutput::init() +{ + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); +} + +void TestWaylandOutput::cleanup() +{ + if (m_queue) { + delete m_queue; + m_queue = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + delete m_connection; + m_connection = nullptr; + + delete m_display; + m_display = nullptr; +} + +void TestWaylandOutput::testRegistry() +{ + auto fakeOutput = std::make_unique(); + fakeOutput->setMode(QSize(1024, 768), 60000); + fakeOutput->moveTo(QPoint(100, 50)); + fakeOutput->setPhysicalSize(QSize(200, 100)); + auto outputHandle = std::make_unique(fakeOutput.get()); + + auto outputInterface = std::make_unique(m_display, outputHandle.get()); + + KWayland::Client::Registry registry; + QSignalSpy announced(®istry, &KWayland::Client::Registry::outputAnnounced); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + wl_display_flush(m_connection->display()); + QVERIFY(announced.wait()); + + KWayland::Client::Output output; + QVERIFY(!output.isValid()); + QCOMPARE(output.geometry(), QRect()); + QCOMPARE(output.globalPosition(), QPoint()); + QCOMPARE(output.manufacturer(), QString()); + QCOMPARE(output.model(), QString()); + QCOMPARE(output.physicalSize(), QSize()); + QCOMPARE(output.pixelSize(), QSize()); + QCOMPARE(output.refreshRate(), 0); + QCOMPARE(output.scale(), 1); + QCOMPARE(output.subPixel(), KWayland::Client::Output::SubPixel::Unknown); + QCOMPARE(output.transform(), KWayland::Client::Output::Transform::Normal); + + QSignalSpy outputChanged(&output, &KWayland::Client::Output::changed); + auto o = registry.bindOutput(announced.first().first().value(), announced.first().last().value()); + QVERIFY(!KWayland::Client::Output::get(o)); + output.setup(o); + QCOMPARE(KWayland::Client::Output::get(o), &output); + wl_display_flush(m_connection->display()); + QVERIFY(outputChanged.wait()); + + QCOMPARE(output.geometry(), QRect(100, 50, 1024, 768)); + QCOMPARE(output.globalPosition(), QPoint(100, 50)); + QCOMPARE(output.manufacturer(), QString()); + QCOMPARE(output.model(), QString()); + QCOMPARE(output.physicalSize(), QSize(200, 100)); + QCOMPARE(output.pixelSize(), QSize(1024, 768)); + QCOMPARE(output.refreshRate(), 60000); + QCOMPARE(output.scale(), 1); + // for xwayland output it's unknown + QCOMPARE(output.subPixel(), KWayland::Client::Output::SubPixel::Unknown); + // for xwayland transform is normal + QCOMPARE(output.transform(), KWayland::Client::Output::Transform::Normal); +} + +void TestWaylandOutput::testModeChange() +{ + auto fakeOutput = std::make_unique(); + fakeOutput->setMode(QSize(1024, 768), 60000); + auto outputHandle = std::make_unique(fakeOutput.get()); + + auto outputInterface = std::make_unique(m_display, outputHandle.get()); + + KWayland::Client::Registry registry; + QSignalSpy announced(®istry, &KWayland::Client::Registry::outputAnnounced); + registry.setEventQueue(m_queue); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + wl_display_flush(m_connection->display()); + QVERIFY(announced.wait()); + + KWayland::Client::Output output; + QSignalSpy outputChanged(&output, &KWayland::Client::Output::changed); + QSignalSpy modeAddedSpy(&output, &KWayland::Client::Output::modeAdded); + output.setup(registry.bindOutput(announced.first().first().value(), announced.first().last().value())); + wl_display_flush(m_connection->display()); + QVERIFY(outputChanged.wait()); + QCOMPARE(modeAddedSpy.count(), 1); + QCOMPARE(modeAddedSpy.at(0).first().value().size, QSize(1024, 768)); + QCOMPARE(modeAddedSpy.at(0).first().value().refreshRate, 60000); + QCOMPARE(modeAddedSpy.at(0).first().value().flags, KWayland::Client::Output::Mode::Flags(KWayland::Client::Output::Mode::Flag::Current)); + QCOMPARE(modeAddedSpy.at(0).first().value().output, QPointer(&output)); + QCOMPARE(output.pixelSize(), QSize(1024, 768)); + QCOMPARE(output.refreshRate(), 60000); + + // change once more + fakeOutput->setMode(QSize(1280, 1024), 90000); + QVERIFY(outputChanged.wait()); + QCOMPARE(modeAddedSpy.count(), 2); + QCOMPARE(modeAddedSpy.at(1).first().value().size, QSize(1280, 1024)); + QCOMPARE(modeAddedSpy.at(1).first().value().refreshRate, 90000); + QCOMPARE(modeAddedSpy.at(1).first().value().flags, KWayland::Client::Output::Mode::Flags(KWayland::Client::Output::Mode::Flag::Current)); + QCOMPARE(modeAddedSpy.at(1).first().value().output, QPointer(&output)); + QCOMPARE(output.pixelSize(), QSize(1280, 1024)); + QCOMPARE(output.refreshRate(), 90000); +} + +void TestWaylandOutput::testScaleChange() +{ + auto fakeOutput = std::make_unique(); + fakeOutput->setMode(QSize(1024, 768), 60000); + auto outputHandle = std::make_unique(fakeOutput.get()); + + auto outputInterface = std::make_unique(m_display, outputHandle.get()); + + KWayland::Client::Registry registry; + QSignalSpy announced(®istry, &KWayland::Client::Registry::outputAnnounced); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + wl_display_flush(m_connection->display()); + QVERIFY(announced.wait()); + + KWayland::Client::Output output; + QSignalSpy outputChanged(&output, &KWayland::Client::Output::changed); + output.setup(registry.bindOutput(announced.first().first().value(), announced.first().last().value())); + wl_display_flush(m_connection->display()); + QVERIFY(outputChanged.wait()); + QCOMPARE(output.scale(), 1); + + // change the scale + outputChanged.clear(); + fakeOutput->setScale(2); + QVERIFY(outputChanged.wait()); + QCOMPARE(output.scale(), 2); + // changing to same value should not trigger + fakeOutput->setScale(2); + QVERIFY(!outputChanged.wait(100)); + + // change once more + outputChanged.clear(); + fakeOutput->setScale(4); + QVERIFY(outputChanged.wait()); + QCOMPARE(output.scale(), 4); +} + +void TestWaylandOutput::testSubPixel_data() +{ + QTest::addColumn("expected"); + QTest::addColumn("actual"); + + QTest::newRow("none") << KWayland::Client::Output::SubPixel::None << KWin::BackendOutput::SubPixel::None; + QTest::newRow("horizontal/rgb") << KWayland::Client::Output::SubPixel::HorizontalRGB << KWin::BackendOutput::SubPixel::Horizontal_RGB; + QTest::newRow("horizontal/bgr") << KWayland::Client::Output::SubPixel::HorizontalBGR << KWin::BackendOutput::SubPixel::Horizontal_BGR; + QTest::newRow("vertical/rgb") << KWayland::Client::Output::SubPixel::VerticalRGB << KWin::BackendOutput::SubPixel::Vertical_RGB; + QTest::newRow("vertical/bgr") << KWayland::Client::Output::SubPixel::VerticalBGR << KWin::BackendOutput::SubPixel::Vertical_BGR; +} + +void TestWaylandOutput::testSubPixel() +{ + QFETCH(KWin::BackendOutput::SubPixel, actual); + + auto fakeOutput = std::make_unique(); + fakeOutput->setMode(QSize(1024, 768), 60000); + fakeOutput->setSubPixel(actual); + auto outputHandle = std::make_unique(fakeOutput.get()); + + auto outputInterface = std::make_unique(m_display, outputHandle.get()); + + KWayland::Client::Registry registry; + QSignalSpy announced(®istry, &KWayland::Client::Registry::outputAnnounced); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + wl_display_flush(m_connection->display()); + QVERIFY(announced.wait()); + + KWayland::Client::Output output; + QSignalSpy outputChanged(&output, &KWayland::Client::Output::changed); + output.setup(registry.bindOutput(announced.first().first().value(), announced.first().last().value())); + wl_display_flush(m_connection->display()); + if (outputChanged.isEmpty()) { + QVERIFY(outputChanged.wait()); + } + + QTEST(output.subPixel(), "expected"); +} + +void TestWaylandOutput::testTransform_data() +{ + QTest::addColumn("expected"); + QTest::addColumn("actual"); + + QTest::newRow("90") << KWayland::Client::Output::Transform::Rotated90 << KWin::OutputTransform::Rotate90; + QTest::newRow("180") << KWayland::Client::Output::Transform::Rotated180 << KWin::OutputTransform::Rotate180; + QTest::newRow("270") << KWayland::Client::Output::Transform::Rotated270 << KWin::OutputTransform::Rotate270; + QTest::newRow("Flipped") << KWayland::Client::Output::Transform::Flipped << KWin::OutputTransform::FlipX; + QTest::newRow("Flipped 90") << KWayland::Client::Output::Transform::Flipped90 << KWin::OutputTransform::FlipX90; + QTest::newRow("Flipped 180") << KWayland::Client::Output::Transform::Flipped180 << KWin::OutputTransform::FlipX180; + QTest::newRow("Flipped 280") << KWayland::Client::Output::Transform::Flipped270 << KWin::OutputTransform::FlipX270; +} + +void TestWaylandOutput::testTransform() +{ + QFETCH(KWin::OutputTransform::Kind, actual); + + auto fakeOutput = std::make_unique(); + fakeOutput->setMode(QSize(1024, 768), 60000); + fakeOutput->setTransform(actual); + auto outputHandle = std::make_unique(fakeOutput.get()); + + auto outputInterface = std::make_unique(m_display, outputHandle.get()); + + using namespace KWin; + + KWayland::Client::Registry registry; + QSignalSpy announced(®istry, &KWayland::Client::Registry::outputAnnounced); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + wl_display_flush(m_connection->display()); + QVERIFY(announced.wait()); + + KWayland::Client::Output *output = registry.createOutput(announced.first().first().value(), announced.first().last().value(), ®istry); + QSignalSpy outputChanged(output, &KWayland::Client::Output::changed); + wl_display_flush(m_connection->display()); + if (outputChanged.isEmpty()) { + QVERIFY(outputChanged.wait()); + } + + QTEST(output->transform(), "expected"); + + // change back to normal + outputChanged.clear(); + fakeOutput->setTransform(KWin::OutputTransform::Normal); + if (outputChanged.isEmpty()) { + QVERIFY(outputChanged.wait()); + } + QCOMPARE(output->transform(), KWayland::Client::Output::Transform::Normal); +} + +QTEST_GUILESS_MAIN(TestWaylandOutput) +#include "test_wayland_output.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_seat.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_seat.cpp new file mode 100644 index 0000000000..c2650f7778 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_seat.cpp @@ -0,0 +1,1957 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +#include +// KWin +#include "wayland/compositor.h" +#include "wayland/datadevicemanager.h" +#include "wayland/datasource.h" +#include "wayland/display.h" +#include "wayland/keyboard.h" +#include "wayland/pointer.h" +#include "wayland/pointergestures_v1.h" +#include "wayland/relativepointer_v1.h" +#include "wayland/seat.h" +#include "wayland/subcompositor.h" +#include "wayland/surface.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/datadevice.h" +#include "KWayland/Client/datadevicemanager.h" +#include "KWayland/Client/datasource.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/keyboard.h" +#include "KWayland/Client/pointer.h" +#include "KWayland/Client/pointergestures.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/relativepointer.h" +#include "KWayland/Client/seat.h" +#include "KWayland/Client/shm_pool.h" +#include "KWayland/Client/subcompositor.h" +#include "KWayland/Client/subsurface.h" +#include "KWayland/Client/surface.h" +#include "KWayland/Client/touch.h" + +// Wayland +#include "qwayland-pointer-gestures-unstable-v1.h" +#include + +#include +// System +#include +#include + +using namespace std::literals; + +class WaylandSyncPoint : public QObject +{ + Q_OBJECT + +public: + explicit WaylandSyncPoint(KWayland::Client::ConnectionThread *connection, KWayland::Client::EventQueue *eventQueue) + { + static const wl_callback_listener listener = { + .done = [](void *data, wl_callback *callback, uint32_t callback_data) { + auto syncPoint = static_cast(data); + Q_EMIT syncPoint->done(); + }, + }; + + m_callback = wl_display_sync(connection->display()); + eventQueue->addProxy(m_callback); + wl_callback_add_listener(m_callback, &listener, this); + } + + ~WaylandSyncPoint() override + { + wl_callback_destroy(m_callback); + } + +Q_SIGNALS: + void done(); + +private: + wl_callback *m_callback; +}; + +class TestWaylandSeat : public QObject +{ + Q_OBJECT +public: + explicit TestWaylandSeat(QObject *parent = nullptr); +private Q_SLOTS: + void init(); + void cleanup(); + + void testCapabilities_data(); + void testCapabilities(); + void testPointer(); + void testPointerTransformation_data(); + void testPointerTransformation(); + void testPointerButton_data(); + void testPointerButton(); + void testPointerSubSurfaceTree(); + void testPointerSwipeGesture_data(); + void testPointerSwipeGesture(); + void testPointerPinchGesture_data(); + void testPointerPinchGesture(); + void testPointerHoldGesture_data(); + void testPointerHoldGesture(); + void testPointerAxis(); + void testCursor(); + void testKeyboard(); + void testSelection(); + void testDataDeviceForKeyboardSurface(); + void testTouch(); + void testKeymap(); + +private: + bool sync(); + + KWin::Display *m_display; + KWin::CompositorInterface *m_compositorInterface; + KWin::SeatInterface *m_seatInterface; + KWin::SubCompositorInterface *m_subCompositorInterface; + KWin::RelativePointerManagerV1Interface *m_relativePointerManagerV1Interface; + KWin::PointerGesturesV1Interface *m_pointerGesturesV1Interface; + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::Compositor *m_compositor; + KWayland::Client::Seat *m_seat; + KWayland::Client::ShmPool *m_shm; + KWayland::Client::SubCompositor *m_subCompositor; + KWayland::Client::RelativePointerManager *m_relativePointerManager; + KWayland::Client::PointerGestures *m_pointerGestures; + KWayland::Client::EventQueue *m_queue; + QThread *m_thread; +}; + +static const QString s_socketName = QStringLiteral("kwin-test-wayland-seat-0"); + +TestWaylandSeat::TestWaylandSeat(QObject *parent) + : QObject(parent) + , m_display(nullptr) + , m_compositorInterface(nullptr) + , m_seatInterface(nullptr) + , m_subCompositorInterface(nullptr) + , m_relativePointerManagerV1Interface(nullptr) + , m_pointerGesturesV1Interface(nullptr) + , m_connection(nullptr) + , m_compositor(nullptr) + , m_seat(nullptr) + , m_shm(nullptr) + , m_subCompositor(nullptr) + , m_relativePointerManager(nullptr) + , m_pointerGestures(nullptr) + , m_queue(nullptr) + , m_thread(nullptr) +{ +} + +void TestWaylandSeat::init() +{ + using namespace KWin; + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + m_display->createShm(); + + m_compositorInterface = new CompositorInterface(m_display, m_display); + m_subCompositorInterface = new SubCompositorInterface(m_display, m_display); + QVERIFY(m_subCompositorInterface); + + m_relativePointerManagerV1Interface = new RelativePointerManagerV1Interface(m_display, m_display); + m_pointerGesturesV1Interface = new PointerGesturesV1Interface(m_display, m_display); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + m_queue->setup(m_connection); + + KWayland::Client::Registry registry; + QSignalSpy compositorSpy(®istry, &KWayland::Client::Registry::compositorAnnounced); + QSignalSpy seatSpy(®istry, &KWayland::Client::Registry::seatAnnounced); + QSignalSpy shmSpy(®istry, &KWayland::Client::Registry::shmAnnounced); + registry.setEventQueue(m_queue); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + QVERIFY(compositorSpy.wait()); + + m_seatInterface = new SeatInterface(m_display, QStringLiteral("seat0"), m_display); + QVERIFY(m_seatInterface); + QVERIFY(seatSpy.wait()); + + m_compositor = new KWayland::Client::Compositor(this); + m_compositor->setup(registry.bindCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value())); + QVERIFY(m_compositor->isValid()); + + m_seat = registry.createSeat(seatSpy.first().first().value(), seatSpy.first().last().value(), this); + QSignalSpy nameSpy(m_seat, &KWayland::Client::Seat::nameChanged); + QVERIFY(nameSpy.wait()); + + m_shm = new KWayland::Client::ShmPool(this); + m_shm->setup(registry.bindShm(shmSpy.first().first().value(), shmSpy.first().last().value())); + QVERIFY(m_shm->isValid()); + + m_subCompositor = registry.createSubCompositor(registry.interface(KWayland::Client::Registry::Interface::SubCompositor).name, + registry.interface(KWayland::Client::Registry::Interface::SubCompositor).version, + this); + QVERIFY(m_subCompositor->isValid()); + + m_relativePointerManager = + registry.createRelativePointerManager(registry.interface(KWayland::Client::Registry::Interface::RelativePointerManagerUnstableV1).name, + registry.interface(KWayland::Client::Registry::Interface::RelativePointerManagerUnstableV1).version, + this); + QVERIFY(m_relativePointerManager->isValid()); + + m_pointerGestures = registry.createPointerGestures(registry.interface(KWayland::Client::Registry::Interface::PointerGesturesUnstableV1).name, + registry.interface(KWayland::Client::Registry::Interface::PointerGesturesUnstableV1).version, + this); + QVERIFY(m_pointerGestures->isValid()); +} + +void TestWaylandSeat::cleanup() +{ + if (m_pointerGestures) { + delete m_pointerGestures; + m_pointerGestures = nullptr; + } + if (m_relativePointerManager) { + delete m_relativePointerManager; + m_relativePointerManager = nullptr; + } + if (m_subCompositor) { + delete m_subCompositor; + m_subCompositor = nullptr; + } + if (m_shm) { + delete m_shm; + m_shm = nullptr; + } + if (m_seat) { + delete m_seat; + m_seat = nullptr; + } + if (m_compositor) { + delete m_compositor; + m_compositor = nullptr; + } + if (m_queue) { + delete m_queue; + m_queue = nullptr; + } + if (m_connection) { + m_connection->deleteLater(); + m_connection = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + + delete m_display; + m_display = nullptr; + + // these are the children of the display + m_compositorInterface = nullptr; + m_seatInterface = nullptr; + m_subCompositorInterface = nullptr; + m_relativePointerManagerV1Interface = nullptr; + m_pointerGesturesV1Interface = nullptr; +} + +bool TestWaylandSeat::sync() +{ + WaylandSyncPoint syncPoint(m_connection, m_queue); + QSignalSpy doneSpy(&syncPoint, &WaylandSyncPoint::done); + return doneSpy.wait(); +} + +void TestWaylandSeat::testCapabilities_data() +{ + QTest::addColumn("pointer"); + QTest::addColumn("keyboard"); + QTest::addColumn("touch"); + + QTest::newRow("none") << false << false << false; + QTest::newRow("pointer") << true << false << false; + QTest::newRow("keyboard") << false << true << false; + QTest::newRow("touch") << false << false << true; + QTest::newRow("pointer/keyboard") << true << true << false; + QTest::newRow("pointer/touch") << true << false << true; + QTest::newRow("keyboard/touch") << false << true << true; + QTest::newRow("all") << true << true << true; +} + +void TestWaylandSeat::testCapabilities() +{ + QVERIFY(!m_seat->hasPointer()); + QVERIFY(!m_seat->hasKeyboard()); + QVERIFY(!m_seat->hasTouch()); + + QFETCH(bool, pointer); + QFETCH(bool, keyboard); + QFETCH(bool, touch); + + QSignalSpy pointerSpy(m_seat, &KWayland::Client::Seat::hasPointerChanged); + QSignalSpy keyboardSpy(m_seat, &KWayland::Client::Seat::hasKeyboardChanged); + QSignalSpy touchSpy(m_seat, &KWayland::Client::Seat::hasTouchChanged); + + m_seatInterface->setHasPointer(pointer); + m_seatInterface->setHasKeyboard(keyboard); + m_seatInterface->setHasTouch(touch); + + QVERIFY(sync()); + + // do processing + QCOMPARE(pointerSpy.isEmpty(), !pointer); + if (!pointerSpy.isEmpty()) { + QCOMPARE(pointerSpy.first().first().toBool(), pointer); + } + + QCOMPARE(keyboardSpy.isEmpty(), !keyboard); + if (!keyboardSpy.isEmpty()) { + QCOMPARE(keyboardSpy.first().first().toBool(), keyboard); + } + + QCOMPARE(touchSpy.isEmpty(), !touch); + if (!touchSpy.isEmpty()) { + QCOMPARE(touchSpy.first().first().toBool(), touch); + } + + QCOMPARE(m_seat->hasPointer(), pointer); + QCOMPARE(m_seat->hasKeyboard(), keyboard); + QCOMPARE(m_seat->hasTouch(), touch); +} + +void TestWaylandSeat::testPointer() +{ + using namespace KWin; + + QSignalSpy pointerSpy(m_seat, &KWayland::Client::Seat::hasPointerChanged); + m_seatInterface->setHasPointer(true); + QVERIFY(pointerSpy.wait()); + + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + KWayland::Client::Surface *s = m_compositor->createSurface(m_compositor); + QVERIFY(surfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + QImage image(QSize(100, 100), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::black); + s->attachBuffer(m_shm->createBuffer(image)); + s->damage(image.rect()); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QSignalSpy committedSpy(serverSurface, &KWin::SurfaceInterface::committed); + QVERIFY(committedSpy.wait()); + + m_seatInterface->notifyPointerEnter(serverSurface, QPointF(20, 18), QPointF(10, 15)); + + KWayland::Client::Pointer *p = m_seat->createPointer(m_seat); + QSignalSpy frameSpy(p, &KWayland::Client::Pointer::frame); + const KWayland::Client::Pointer &cp = *p; + QVERIFY(p->isValid()); + std::unique_ptr relativePointer(m_relativePointerManager->createRelativePointer(p)); + QVERIFY(relativePointer->isValid()); + QVERIFY(frameSpy.wait()); + QCOMPARE(frameSpy.count(), 1); + + m_seatInterface->notifyPointerLeave(); + serverSurface->client()->flush(); + QVERIFY(frameSpy.wait()); + QCOMPARE(frameSpy.count(), 2); + + QSignalSpy enteredSpy(p, &KWayland::Client::Pointer::entered); + + QSignalSpy leftSpy(p, &KWayland::Client::Pointer::left); + + QSignalSpy motionSpy(p, &KWayland::Client::Pointer::motion); + + QSignalSpy axisSpy(p, &KWayland::Client::Pointer::axisChanged); + + QSignalSpy buttonSpy(p, &KWayland::Client::Pointer::buttonStateChanged); + + QSignalSpy relativeMotionSpy(relativePointer.get(), &KWayland::Client::RelativePointer::relativeMotion); + + QVERIFY(!p->enteredSurface()); + QVERIFY(!cp.enteredSurface()); + uint32_t serial = m_display->serial(); + m_seatInterface->notifyPointerEnter(serverSurface, QPointF(20, 18), QPointF(10, 15)); + QCOMPARE(m_seatInterface->focusedPointerSurface(), serverSurface); + QVERIFY(enteredSpy.wait()); + QCOMPARE_GT(enteredSpy.first().first().value(), serial); + QCOMPARE(enteredSpy.first().last().toPoint(), QPoint(10, 3)); + QCOMPARE(frameSpy.count(), 3); + QCOMPARE(p->enteredSurface(), s); + QCOMPARE(cp.enteredSurface(), s); + + auto timestamp = 1ms; + // test motion + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyPointerMotion(QPoint(10, 16)); + m_seatInterface->notifyPointerFrame(); + QVERIFY(motionSpy.wait()); + QCOMPARE(frameSpy.count(), 4); + QCOMPARE(motionSpy.first().first().toPoint(), QPoint(0, 1)); + QCOMPARE(motionSpy.first().last().value(), quint32(1)); + + // test relative motion + m_seatInterface->relativePointerMotion(QPointF(1, 2), QPointF(3, 4), 1234us); + m_seatInterface->notifyPointerFrame(); + QVERIFY(relativeMotionSpy.wait()); + QCOMPARE(relativeMotionSpy.count(), 1); + QCOMPARE(frameSpy.count(), 5); + QCOMPARE(relativeMotionSpy.first().at(0).toSizeF(), QSizeF(1, 2)); + QCOMPARE(relativeMotionSpy.first().at(1).toSizeF(), QSizeF(3, 4)); + QCOMPARE(relativeMotionSpy.first().at(2).value(), 1234); + + // test axis + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyPointerAxis(Qt::Horizontal, 10, 120, PointerAxisSource::Wheel); + m_seatInterface->notifyPointerFrame(); + QVERIFY(axisSpy.wait()); + QCOMPARE(frameSpy.count(), 6); + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyPointerAxis(Qt::Vertical, 20, 240, PointerAxisSource::Wheel); + m_seatInterface->notifyPointerFrame(); + QVERIFY(axisSpy.wait()); + QCOMPARE(frameSpy.count(), 7); + QCOMPARE(axisSpy.first().at(0).value(), quint32(2)); + QCOMPARE(axisSpy.first().at(1).value(), KWayland::Client::Pointer::Axis::Horizontal); + QCOMPARE(axisSpy.first().at(2).value(), qreal(10)); + + QCOMPARE(axisSpy.last().at(0).value(), quint32(3)); + QCOMPARE(axisSpy.last().at(1).value(), KWayland::Client::Pointer::Axis::Vertical); + QCOMPARE(axisSpy.last().at(2).value(), qreal(20)); + + // test button + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyPointerButton(1, PointerButtonState::Pressed); + m_seatInterface->notifyPointerFrame(); + QVERIFY(buttonSpy.wait()); + QCOMPARE(frameSpy.count(), 8); + QCOMPARE(buttonSpy.at(0).at(0).value(), m_display->serial()); + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyPointerButton(2, PointerButtonState::Pressed); + m_seatInterface->notifyPointerFrame(); + QVERIFY(buttonSpy.wait()); + QCOMPARE(frameSpy.count(), 9); + QCOMPARE(buttonSpy.at(1).at(0).value(), m_display->serial()); + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyPointerButton(2, PointerButtonState::Released); + m_seatInterface->notifyPointerFrame(); + QVERIFY(buttonSpy.wait()); + QCOMPARE(frameSpy.count(), 10); + QCOMPARE(buttonSpy.at(2).at(0).value(), m_display->serial()); + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyPointerButton(1, PointerButtonState::Released); + m_seatInterface->notifyPointerFrame(); + QVERIFY(buttonSpy.wait()); + QCOMPARE(frameSpy.count(), 11); + QCOMPARE(buttonSpy.count(), 4); + + // timestamp + QCOMPARE(buttonSpy.at(0).at(1).value(), quint32(4)); + // button + QCOMPARE(buttonSpy.at(0).at(2).value(), quint32(1)); + QCOMPARE(buttonSpy.at(0).at(3).value(), KWayland::Client::Pointer::ButtonState::Pressed); + + // timestamp + QCOMPARE(buttonSpy.at(1).at(1).value(), quint32(5)); + // button + QCOMPARE(buttonSpy.at(1).at(2).value(), quint32(2)); + QCOMPARE(buttonSpy.at(1).at(3).value(), KWayland::Client::Pointer::ButtonState::Pressed); + + QCOMPARE(buttonSpy.at(2).at(0).value(), m_seatInterface->pointerButtonSerial(2)); + // timestamp + QCOMPARE(buttonSpy.at(2).at(1).value(), quint32(6)); + // button + QCOMPARE(buttonSpy.at(2).at(2).value(), quint32(2)); + QCOMPARE(buttonSpy.at(2).at(3).value(), KWayland::Client::Pointer::ButtonState::Released); + + QCOMPARE(buttonSpy.at(3).at(0).value(), m_seatInterface->pointerButtonSerial(1)); + // timestamp + QCOMPARE(buttonSpy.at(3).at(1).value(), quint32(7)); + // button + QCOMPARE(buttonSpy.at(3).at(2).value(), quint32(1)); + QCOMPARE(buttonSpy.at(3).at(3).value(), KWayland::Client::Pointer::ButtonState::Released); + + // leave the surface + serial = m_display->serial(); + m_seatInterface->notifyPointerLeave(); + QVERIFY(leftSpy.wait()); + QCOMPARE(frameSpy.count(), 12); + QCOMPARE_GT(leftSpy.first().first().value(), serial); + QVERIFY(!p->enteredSurface()); + QVERIFY(!cp.enteredSurface()); + + // now a relative motion should not be sent to the relative pointer + m_seatInterface->relativePointerMotion(QPointF(1, 2), QPointF(3, 4), std::chrono::milliseconds::zero()); + QVERIFY(sync()); + QCOMPARE(relativeMotionSpy.count(), 1); + + // enter it again + m_seatInterface->notifyPointerEnter(serverSurface, QPointF(10, 16), QPointF(0, 0)); + QVERIFY(enteredSpy.wait()); + QCOMPARE(frameSpy.count(), 13); + QCOMPARE(p->enteredSurface(), s); + QCOMPARE(cp.enteredSurface(), s); + + // send another relative motion event + m_seatInterface->relativePointerMotion(QPointF(4, 5), QPointF(6, 7), 1234us); + QVERIFY(relativeMotionSpy.wait()); + QCOMPARE(relativeMotionSpy.count(), 2); + QCOMPARE(relativeMotionSpy.last().at(0).toSizeF(), QSizeF(4, 5)); + QCOMPARE(relativeMotionSpy.last().at(1).toSizeF(), QSizeF(6, 7)); + QCOMPARE(relativeMotionSpy.last().at(2).value(), 1234); +} + +void TestWaylandSeat::testPointerTransformation_data() +{ + QTest::addColumn("enterTransformation"); + // global position at 20/18 + QTest::addColumn("expectedEnterPoint"); + // global position at 10/16 + QTest::addColumn("expectedMovePoint"); + + QMatrix4x4 tm; + tm.translate(-10, -15); + QTest::newRow("translation") << tm << QPointF(10, 3) << QPointF(0, 1); + QMatrix4x4 sm; + sm.scale(2, 2); + QTest::newRow("scale") << sm << QPointF(40, 36) << QPointF(20, 32); + QMatrix4x4 rotate; + rotate.rotate(90, 0, 0, 1); + QTest::newRow("rotate") << rotate << QPointF(-18, 20) << QPointF(-16, 10); +} + +void TestWaylandSeat::testPointerTransformation() +{ + using namespace KWin; + + QSignalSpy pointerSpy(m_seat, &KWayland::Client::Seat::hasPointerChanged); + m_seatInterface->setHasPointer(true); + QVERIFY(pointerSpy.wait()); + + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + KWayland::Client::Surface *s = m_compositor->createSurface(m_compositor); + QVERIFY(surfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + QImage image(QSize(100, 100), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::black); + s->attachBuffer(m_shm->createBuffer(image)); + s->damage(image.rect()); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QSignalSpy committedSpy(serverSurface, &KWin::SurfaceInterface::committed); + QVERIFY(committedSpy.wait()); + + KWayland::Client::Pointer *p = m_seat->createPointer(m_seat); + QVERIFY(p->isValid()); + const KWayland::Client::Pointer &cp = *p; + + QSignalSpy enteredSpy(p, &KWayland::Client::Pointer::entered); + QSignalSpy leftSpy(p, &KWayland::Client::Pointer::left); + QSignalSpy motionSpy(p, &KWayland::Client::Pointer::motion); + + QVERIFY(!p->enteredSurface()); + QVERIFY(!cp.enteredSurface()); + uint32_t serial = m_display->serial(); + QFETCH(QMatrix4x4, enterTransformation); + m_seatInterface->notifyPointerEnter(serverSurface, QPointF(20, 18), enterTransformation); + QCOMPARE(m_seatInterface->focusedPointerSurface(), serverSurface); + QVERIFY(enteredSpy.wait()); + QCOMPARE_GT(enteredSpy.first().first().value(), serial); + QTEST(enteredSpy.first().last().toPointF(), "expectedEnterPoint"); + QCOMPARE(p->enteredSurface(), s); + QCOMPARE(cp.enteredSurface(), s); + + // test motion + m_seatInterface->setTimestamp(std::chrono::milliseconds(1)); + m_seatInterface->notifyPointerMotion(QPoint(10, 16)); + m_seatInterface->notifyPointerFrame(); + QVERIFY(motionSpy.wait()); + QTEST(motionSpy.first().first().toPointF(), "expectedMovePoint"); + QCOMPARE(motionSpy.first().last().value(), quint32(1)); + + // leave the surface + serial = m_display->serial(); + m_seatInterface->notifyPointerLeave(); + QVERIFY(leftSpy.wait()); + QCOMPARE_GT(leftSpy.first().first().value(), serial); + QVERIFY(!p->enteredSurface()); + QVERIFY(!cp.enteredSurface()); + + // enter it again + m_seatInterface->notifyPointerEnter(serverSurface, QPointF(10, 16)); + QVERIFY(enteredSpy.wait()); + QCOMPARE(p->enteredSurface(), s); + QCOMPARE(cp.enteredSurface(), s); + + QSignalSpy serverSurfaceDestroyedSpy(serverSurface, &QObject::destroyed); + delete s; + QVERIFY(serverSurfaceDestroyedSpy.wait()); + QVERIFY(!m_seatInterface->focusedPointerSurface()); +} + +Q_DECLARE_METATYPE(Qt::MouseButton) + +void TestWaylandSeat::testPointerButton_data() +{ + QTest::addColumn("qtButton"); + QTest::addColumn("waylandButton"); + + QTest::newRow("left") << Qt::LeftButton << quint32(BTN_LEFT); + QTest::newRow("right") << Qt::RightButton << quint32(BTN_RIGHT); + QTest::newRow("middle") << Qt::MiddleButton << quint32(BTN_MIDDLE); + QTest::newRow("back") << Qt::BackButton << quint32(BTN_BACK); + QTest::newRow("x1") << Qt::XButton1 << quint32(BTN_BACK); + QTest::newRow("extra1") << Qt::ExtraButton1 << quint32(BTN_BACK); + QTest::newRow("forward") << Qt::ForwardButton << quint32(BTN_FORWARD); + QTest::newRow("x2") << Qt::XButton2 << quint32(BTN_FORWARD); + QTest::newRow("extra2") << Qt::ExtraButton2 << quint32(BTN_FORWARD); + QTest::newRow("task") << Qt::TaskButton << quint32(BTN_TASK); + QTest::newRow("extra3") << Qt::ExtraButton3 << quint32(BTN_TASK); + QTest::newRow("extra4") << Qt::ExtraButton4 << quint32(BTN_EXTRA); + QTest::newRow("extra5") << Qt::ExtraButton5 << quint32(BTN_SIDE); + QTest::newRow("extra6") << Qt::ExtraButton6 << quint32(0x118); + QTest::newRow("extra7") << Qt::ExtraButton7 << quint32(0x119); + QTest::newRow("extra8") << Qt::ExtraButton8 << quint32(0x11a); + QTest::newRow("extra9") << Qt::ExtraButton9 << quint32(0x11b); + QTest::newRow("extra10") << Qt::ExtraButton10 << quint32(0x11c); + QTest::newRow("extra11") << Qt::ExtraButton11 << quint32(0x11d); + QTest::newRow("extra12") << Qt::ExtraButton12 << quint32(0x11e); + QTest::newRow("extra13") << Qt::ExtraButton13 << quint32(0x11f); +} + +void TestWaylandSeat::testPointerButton() +{ + using namespace KWin; + + QSignalSpy pointerSpy(m_seat, &KWayland::Client::Seat::hasPointerChanged); + m_seatInterface->setHasPointer(true); + QVERIFY(pointerSpy.wait()); + + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + KWayland::Client::Surface *s = m_compositor->createSurface(m_compositor); + QVERIFY(surfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + QImage image(QSize(100, 100), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::black); + s->attachBuffer(m_shm->createBuffer(image)); + s->damage(image.rect()); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QSignalSpy committedSpy(serverSurface, &KWin::SurfaceInterface::committed); + QVERIFY(committedSpy.wait()); + + std::unique_ptr p(m_seat->createPointer()); + QVERIFY(p->isValid()); + QSignalSpy buttonChangedSpy(p.get(), &KWayland::Client::Pointer::buttonStateChanged); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + + m_seatInterface->notifyPointerEnter(serverSurface, QPointF(20, 18), QPointF(10, 15)); + QVERIFY(m_seatInterface->focusedPointerSurface()); + + QCoreApplication::processEvents(); + + QFETCH(Qt::MouseButton, qtButton); + QFETCH(quint32, waylandButton); + std::chrono::milliseconds timestamp(1); + QCOMPARE(m_seatInterface->isPointerButtonPressed(waylandButton), false); + QCOMPARE(m_seatInterface->isPointerButtonPressed(qtButton), false); + m_seatInterface->setTimestamp(timestamp); + m_seatInterface->notifyPointerButton(qtButton, PointerButtonState::Pressed); + m_seatInterface->notifyPointerFrame(); + QCOMPARE(m_seatInterface->isPointerButtonPressed(waylandButton), true); + QCOMPARE(m_seatInterface->isPointerButtonPressed(qtButton), true); + QVERIFY(buttonChangedSpy.wait()); + QCOMPARE(buttonChangedSpy.count(), 1); + QCOMPARE(buttonChangedSpy.last().at(0).value(), m_seatInterface->pointerButtonSerial(waylandButton)); + QCOMPARE(buttonChangedSpy.last().at(0).value(), m_seatInterface->pointerButtonSerial(qtButton)); + QCOMPARE(buttonChangedSpy.last().at(1).value(), timestamp.count()); + QCOMPARE(buttonChangedSpy.last().at(2).value(), waylandButton); + QCOMPARE(buttonChangedSpy.last().at(3).value(), KWayland::Client::Pointer::ButtonState::Pressed); + timestamp++; + m_seatInterface->setTimestamp(timestamp); + m_seatInterface->notifyPointerButton(qtButton, PointerButtonState::Released); + m_seatInterface->notifyPointerFrame(); + QCOMPARE(m_seatInterface->isPointerButtonPressed(waylandButton), false); + QCOMPARE(m_seatInterface->isPointerButtonPressed(qtButton), false); + QVERIFY(buttonChangedSpy.wait()); + QCOMPARE(buttonChangedSpy.count(), 2); + QCOMPARE(buttonChangedSpy.last().at(0).value(), m_seatInterface->pointerButtonSerial(waylandButton)); + QCOMPARE(buttonChangedSpy.last().at(0).value(), m_seatInterface->pointerButtonSerial(qtButton)); + QCOMPARE(buttonChangedSpy.last().at(1).value(), timestamp.count()); + QCOMPARE(buttonChangedSpy.last().at(2).value(), waylandButton); + QCOMPARE(buttonChangedSpy.last().at(3).value(), KWayland::Client::Pointer::ButtonState::Released); +} + +void TestWaylandSeat::testPointerSubSurfaceTree() +{ + // this test verifies that pointer motion on a surface with sub-surfaces sends motion enter/leave to the sub-surface + using namespace KWin; + + // first create the pointer + QSignalSpy hasPointerChangedSpy(m_seat, &KWayland::Client::Seat::hasPointerChanged); + m_seatInterface->setHasPointer(true); + QVERIFY(hasPointerChangedSpy.wait()); + std::unique_ptr pointer(m_seat->createPointer()); + + // create a sub surface tree + // parent surface (100, 100) with one sub surface taking the half of it's size (50, 100) + // which has two further children (50, 50) which are overlapping + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr parentSurface(m_compositor->createSurface()); + std::unique_ptr childSurface(m_compositor->createSurface()); + std::unique_ptr grandChild1Surface(m_compositor->createSurface()); + std::unique_ptr grandChild2Surface(m_compositor->createSurface()); + std::unique_ptr childSubSurface(m_subCompositor->createSubSurface(childSurface.get(), parentSurface.get())); + std::unique_ptr grandChild1SubSurface(m_subCompositor->createSubSurface(grandChild1Surface.get(), childSurface.get())); + std::unique_ptr grandChild2SubSurface(m_subCompositor->createSubSurface(grandChild2Surface.get(), childSurface.get())); + grandChild2SubSurface->setPosition(QPoint(0, 25)); + + // let's map the surfaces + auto render = [this](KWayland::Client::Surface *s, const QSize &size) { + QImage image(size, QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::black); + s->attachBuffer(m_shm->createBuffer(image)); + s->damage(QRect(QPoint(0, 0), size)); + s->commit(KWayland::Client::Surface::CommitFlag::None); + }; + render(grandChild2Surface.get(), QSize(50, 50)); + render(grandChild1Surface.get(), QSize(50, 50)); + render(childSurface.get(), QSize(50, 100)); + render(parentSurface.get(), QSize(100, 100)); + + QVERIFY(surfaceCreatedSpy.wait()); + auto serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface->isMapped()); + + // send in pointer events + QSignalSpy enteredSpy(pointer.get(), &KWayland::Client::Pointer::entered); + QSignalSpy leftSpy(pointer.get(), &KWayland::Client::Pointer::left); + QSignalSpy motionSpy(pointer.get(), &KWayland::Client::Pointer::motion); + // first to the grandChild2 in the overlapped area + std::chrono::milliseconds timestamp(1); + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyPointerEnter(serverSurface, QPointF(25, 50)); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 1); + QCOMPARE(leftSpy.count(), 0); + QCOMPARE(motionSpy.count(), 0); + QCOMPARE(enteredSpy.last().last().toPointF(), QPointF(25, 25)); + QCOMPARE(pointer->enteredSurface(), grandChild2Surface.get()); + // a motion on grandchild2 + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyPointerMotion(QPointF(25, 60)); + m_seatInterface->notifyPointerFrame(); + QVERIFY(motionSpy.wait()); + QCOMPARE(enteredSpy.count(), 1); + QCOMPARE(leftSpy.count(), 0); + QCOMPARE(motionSpy.count(), 1); + QCOMPARE(motionSpy.last().first().toPointF(), QPointF(25, 35)); + // motion which changes to childSurface + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyPointerMotion(QPointF(25, 80)); + m_seatInterface->notifyPointerFrame(); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 2); + QCOMPARE(leftSpy.count(), 1); + QCOMPARE(motionSpy.count(), 2); + QCOMPARE(enteredSpy.last().last().toPointF(), QPointF(25, 80)); + QCOMPARE(pointer->enteredSurface(), childSurface.get()); + // a leave for the whole surface + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyPointerLeave(); + QVERIFY(leftSpy.wait()); + QCOMPARE(enteredSpy.count(), 2); + QCOMPARE(leftSpy.count(), 2); + QCOMPARE(motionSpy.count(), 2); + // a new enter on the main surface + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyPointerEnter(serverSurface, QPointF(75, 50)); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 3); + QCOMPARE(leftSpy.count(), 2); + QCOMPARE(motionSpy.count(), 2); + QCOMPARE(enteredSpy.last().last().toPointF(), QPointF(75, 50)); + QCOMPARE(pointer->enteredSurface(), parentSurface.get()); +} + +void TestWaylandSeat::testPointerSwipeGesture_data() +{ + QTest::addColumn("cancel"); + QTest::addColumn("expectedEndCount"); + QTest::addColumn("expectedCancelCount"); + + QTest::newRow("end") << false << 1 << 0; + QTest::newRow("cancel") << true << 0 << 1; +} + +void TestWaylandSeat::testPointerSwipeGesture() +{ + using namespace KWin; + + // first create the pointer and pointer swipe gesture + QSignalSpy hasPointerChangedSpy(m_seat, &KWayland::Client::Seat::hasPointerChanged); + m_seatInterface->setHasPointer(true); + QVERIFY(hasPointerChangedSpy.wait()); + std::unique_ptr pointer(m_seat->createPointer()); + std::unique_ptr gesture(m_pointerGestures->createSwipeGesture(pointer.get())); + QVERIFY(gesture); + QVERIFY(gesture->isValid()); + QVERIFY(gesture->surface().isNull()); + QCOMPARE(gesture->fingerCount(), 0u); + + QSignalSpy startSpy(gesture.get(), &KWayland::Client::PointerSwipeGesture::started); + QSignalSpy updateSpy(gesture.get(), &KWayland::Client::PointerSwipeGesture::updated); + QSignalSpy endSpy(gesture.get(), &KWayland::Client::PointerSwipeGesture::ended); + QSignalSpy cancelledSpy(gesture.get(), &KWayland::Client::PointerSwipeGesture::cancelled); + + // now create a surface + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + QImage image(QSize(100, 100), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::black); + surface->attachBuffer(m_shm->createBuffer(image)); + surface->damage(image.rect()); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QSignalSpy committedSpy(serverSurface, &KWin::SurfaceInterface::committed); + QVERIFY(committedSpy.wait()); + + m_seatInterface->notifyPointerEnter(serverSurface, QPointF(0, 0)); + QCOMPARE(m_seatInterface->focusedPointerSurface(), serverSurface); + QVERIFY(m_seatInterface->pointer()); + + // send in the start + std::chrono::milliseconds timestamp(1); + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->startPointerSwipeGesture(2); + QVERIFY(startSpy.wait()); + QCOMPARE(startSpy.count(), 1); + QCOMPARE(startSpy.first().at(0).value(), m_display->serial()); + QCOMPARE(startSpy.first().at(1).value(), 1u); + QCOMPARE(gesture->fingerCount(), 2u); + QCOMPARE(gesture->surface().data(), surface.get()); + + // another start should not be possible + m_seatInterface->startPointerSwipeGesture(2); + QVERIFY(sync()); + QCOMPARE(startSpy.count(), 1); + + // send in some updates + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->updatePointerSwipeGesture(QPointF(2, 3)); + QVERIFY(updateSpy.wait()); + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->updatePointerSwipeGesture(QPointF(4, 5)); + QVERIFY(updateSpy.wait()); + QCOMPARE(updateSpy.count(), 2); + QCOMPARE(updateSpy.at(0).at(0).toSizeF(), QSizeF(2, 3)); + QCOMPARE(updateSpy.at(0).at(1).value(), 2u); + QCOMPARE(updateSpy.at(1).at(0).toSizeF(), QSizeF(4, 5)); + QCOMPARE(updateSpy.at(1).at(1).value(), 3u); + + // now end or cancel + QFETCH(bool, cancel); + QSignalSpy *spy; + m_seatInterface->setTimestamp(timestamp++); + if (cancel) { + m_seatInterface->cancelPointerSwipeGesture(); + spy = &cancelledSpy; + } else { + m_seatInterface->endPointerSwipeGesture(); + spy = &endSpy; + } + QVERIFY(spy->wait()); + QFETCH(int, expectedEndCount); + QCOMPARE(endSpy.count(), expectedEndCount); + QFETCH(int, expectedCancelCount); + QCOMPARE(cancelledSpy.count(), expectedCancelCount); + QCOMPARE(spy->count(), 1); + QCOMPARE(spy->first().at(0).value(), m_display->serial()); + QCOMPARE(spy->first().at(1).value(), 4u); + + QCOMPARE(gesture->fingerCount(), 0u); + QVERIFY(gesture->surface().isNull()); + + // now a start should be possible again + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->startPointerSwipeGesture(2); + QVERIFY(startSpy.wait()); + + // unsetting the focused pointer surface should not change anything + m_seatInterface->notifyPointerLeave(); + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->updatePointerSwipeGesture(QPointF(6, 7)); + QVERIFY(updateSpy.wait()); + // and end + m_seatInterface->setTimestamp(timestamp++); + if (cancel) { + m_seatInterface->cancelPointerSwipeGesture(); + } else { + m_seatInterface->endPointerSwipeGesture(); + } + QVERIFY(spy->wait()); +} + +void TestWaylandSeat::testPointerPinchGesture_data() +{ + QTest::addColumn("cancel"); + QTest::addColumn("expectedEndCount"); + QTest::addColumn("expectedCancelCount"); + + QTest::newRow("end") << false << 1 << 0; + QTest::newRow("cancel") << true << 0 << 1; +} + +void TestWaylandSeat::testPointerPinchGesture() +{ + using namespace KWin; + + // first create the pointer and pointer swipe gesture + QSignalSpy hasPointerChangedSpy(m_seat, &KWayland::Client::Seat::hasPointerChanged); + m_seatInterface->setHasPointer(true); + QVERIFY(hasPointerChangedSpy.wait()); + std::unique_ptr pointer(m_seat->createPointer()); + std::unique_ptr gesture(m_pointerGestures->createPinchGesture(pointer.get())); + QVERIFY(gesture); + QVERIFY(gesture->isValid()); + QVERIFY(gesture->surface().isNull()); + QCOMPARE(gesture->fingerCount(), 0u); + + QSignalSpy startSpy(gesture.get(), &KWayland::Client::PointerPinchGesture::started); + QSignalSpy updateSpy(gesture.get(), &KWayland::Client::PointerPinchGesture::updated); + QSignalSpy endSpy(gesture.get(), &KWayland::Client::PointerPinchGesture::ended); + QSignalSpy cancelledSpy(gesture.get(), &KWayland::Client::PointerPinchGesture::cancelled); + + // now create a surface + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + QImage image(QSize(100, 100), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::black); + surface->attachBuffer(m_shm->createBuffer(image)); + surface->damage(image.rect()); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QSignalSpy committedSpy(serverSurface, &KWin::SurfaceInterface::committed); + QVERIFY(committedSpy.wait()); + + m_seatInterface->notifyPointerEnter(serverSurface, QPointF(0, 0)); + QCOMPARE(m_seatInterface->focusedPointerSurface(), serverSurface); + QVERIFY(m_seatInterface->pointer()); + + // send in the start + std::chrono::milliseconds timestamp(1); + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->startPointerPinchGesture(3); + QVERIFY(startSpy.wait()); + QCOMPARE(startSpy.count(), 1); + QCOMPARE(startSpy.first().at(0).value(), m_display->serial()); + QCOMPARE(startSpy.first().at(1).value(), 1u); + QCOMPARE(gesture->fingerCount(), 3u); + QCOMPARE(gesture->surface().data(), surface.get()); + + // another start should not be possible + m_seatInterface->startPointerPinchGesture(3); + QVERIFY(sync()); + QCOMPARE(startSpy.count(), 1); + + // send in some updates + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->updatePointerPinchGesture(QPointF(2, 3), 2, 45); + QVERIFY(updateSpy.wait()); + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->updatePointerPinchGesture(QPointF(4, 5), 1, 90); + QVERIFY(updateSpy.wait()); + QCOMPARE(updateSpy.count(), 2); + QCOMPARE(updateSpy.at(0).at(0).toSizeF(), QSizeF(2, 3)); + QCOMPARE(updateSpy.at(0).at(1).value(), 2u); + QCOMPARE(updateSpy.at(0).at(2).value(), 45u); + QCOMPARE(updateSpy.at(0).at(3).value(), 2u); + QCOMPARE(updateSpy.at(1).at(0).toSizeF(), QSizeF(4, 5)); + QCOMPARE(updateSpy.at(1).at(1).value(), 1u); + QCOMPARE(updateSpy.at(1).at(2).value(), 90u); + QCOMPARE(updateSpy.at(1).at(3).value(), 3u); + + // now end or cancel + QFETCH(bool, cancel); + QSignalSpy *spy; + m_seatInterface->setTimestamp(timestamp++); + if (cancel) { + m_seatInterface->cancelPointerPinchGesture(); + spy = &cancelledSpy; + } else { + m_seatInterface->endPointerPinchGesture(); + spy = &endSpy; + } + QVERIFY(spy->wait()); + QFETCH(int, expectedEndCount); + QCOMPARE(endSpy.count(), expectedEndCount); + QFETCH(int, expectedCancelCount); + QCOMPARE(cancelledSpy.count(), expectedCancelCount); + QCOMPARE(spy->count(), 1); + QCOMPARE(spy->first().at(0).value(), m_display->serial()); + QCOMPARE(spy->first().at(1).value(), 4u); + + QCOMPARE(gesture->fingerCount(), 0u); + QVERIFY(gesture->surface().isNull()); + + // now a start should be possible again + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->startPointerPinchGesture(3); + QVERIFY(startSpy.wait()); + + // unsetting the focused pointer surface should not change anything + m_seatInterface->notifyPointerLeave(); + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->updatePointerPinchGesture(QPointF(6, 7), 2, -45); + QVERIFY(updateSpy.wait()); + // and end + m_seatInterface->setTimestamp(timestamp++); + if (cancel) { + m_seatInterface->cancelPointerPinchGesture(); + } else { + m_seatInterface->endPointerPinchGesture(); + } + QVERIFY(spy->wait()); +} + +void TestWaylandSeat::testPointerHoldGesture_data() +{ + QTest::addColumn("cancel"); + QTest::addColumn("expectedEndCount"); + QTest::addColumn("expectedCancelCount"); + + QTest::newRow("end") << false << 1 << 0; + QTest::newRow("cancel") << true << 0 << 1; +} + +class PointerHoldGesture : public QObject, public QtWayland::zwp_pointer_gesture_hold_v1 +{ + using zwp_pointer_gesture_hold_v1::zwp_pointer_gesture_hold_v1; + Q_OBJECT + void zwp_pointer_gesture_hold_v1_begin(uint32_t serial, uint32_t time, wl_surface *surface, uint32_t fingers) override + { + Q_EMIT started(serial, time, surface, fingers); + } + + void zwp_pointer_gesture_hold_v1_end(uint32_t serial, uint32_t time, int32_t cancelled) override + { + cancelled ? Q_EMIT this->cancelled(serial, time) : Q_EMIT ended(serial, time); + } +Q_SIGNALS: + void started(quint32 serial, quint32 time, void *surface, quint32 fingers); + void ended(quint32 serial, quint32 time); + void cancelled(quint32 serial, quint32 time); +}; + +void TestWaylandSeat::testPointerHoldGesture() +{ + using namespace KWin; + + // first create the pointer and pointer swipe gesture + QSignalSpy hasPointerChangedSpy(m_seat, &KWayland::Client::Seat::hasPointerChanged); + m_seatInterface->setHasPointer(true); + QVERIFY(hasPointerChangedSpy.wait()); + std::unique_ptr pointer(m_seat->createPointer()); + KWayland::Client::Registry registry; + QSignalSpy gesturesAnnoucedSpy(®istry, &KWayland::Client::Registry::pointerGesturesUnstableV1Announced); + registry.create(m_connection); + registry.setup(); + QVERIFY(gesturesAnnoucedSpy.wait()); + QtWayland::zwp_pointer_gestures_v1 gestures(registry, gesturesAnnoucedSpy.first().at(0).value(), gesturesAnnoucedSpy.first().at(1).value()); + PointerHoldGesture gesture(gestures.get_hold_gesture(*pointer)); + QVERIFY(gesture.isInitialized()); + + QSignalSpy startSpy(&gesture, &PointerHoldGesture::started); + QSignalSpy endSpy(&gesture, &PointerHoldGesture::ended); + QSignalSpy cancelledSpy(&gesture, &PointerHoldGesture::cancelled); + + // now create a surface + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + QImage image(QSize(100, 100), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::black); + surface->attachBuffer(m_shm->createBuffer(image)); + surface->damage(image.rect()); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QSignalSpy committedSpy(serverSurface, &KWin::SurfaceInterface::committed); + QVERIFY(committedSpy.wait()); + + m_seatInterface->notifyPointerEnter(serverSurface, QPointF(0, 0)); + QCOMPARE(m_seatInterface->focusedPointerSurface(), serverSurface); + QVERIFY(m_seatInterface->pointer()); + + // send in the start + std::chrono::milliseconds timestamp(1); + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->startPointerHoldGesture(3); + QVERIFY(startSpy.wait()); + QCOMPARE(startSpy.count(), 1); + QCOMPARE(startSpy.first().at(0).value(), m_display->serial()); + QCOMPARE(startSpy.first().at(1).value(), 1u); + QCOMPARE(startSpy.first().at(2).value(), *surface.get()); + QCOMPARE(startSpy.first().at(3).value(), 3); + + // another start should not be possible + m_seatInterface->startPointerPinchGesture(3); + QVERIFY(sync()); + QCOMPARE(startSpy.count(), 1); + + // now end or cancel + QFETCH(bool, cancel); + QSignalSpy *spy; + m_seatInterface->setTimestamp(timestamp++); + if (cancel) { + m_seatInterface->cancelPointerHoldGesture(); + spy = &cancelledSpy; + } else { + m_seatInterface->endPointerHoldGesture(); + spy = &endSpy; + } + QVERIFY(spy->wait()); + QFETCH(int, expectedEndCount); + QCOMPARE(endSpy.count(), expectedEndCount); + QFETCH(int, expectedCancelCount); + QCOMPARE(cancelledSpy.count(), expectedCancelCount); + QCOMPARE(spy->count(), 1); + QCOMPARE(spy->first().at(0).value(), m_display->serial()); + QCOMPARE(spy->first().at(1).value(), 2); + + // now a start should be possible again + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->startPointerHoldGesture(3); + QVERIFY(startSpy.wait()); + + // and end + m_seatInterface->setTimestamp(timestamp++); + if (cancel) { + m_seatInterface->cancelPointerHoldGesture(); + } else { + m_seatInterface->endPointerHoldGesture(); + } + QVERIFY(spy->wait()); +} + +void TestWaylandSeat::testPointerAxis() +{ + using namespace KWin; + + // first create the pointer + QSignalSpy hasPointerChangedSpy(m_seat, &KWayland::Client::Seat::hasPointerChanged); + m_seatInterface->setHasPointer(true); + QVERIFY(hasPointerChangedSpy.wait()); + std::unique_ptr pointer(m_seat->createPointer()); + QVERIFY(pointer); + + // now create a surface + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + QImage image(QSize(100, 100), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::black); + surface->attachBuffer(m_shm->createBuffer(image)); + surface->damage(image.rect()); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QSignalSpy committedSpy(serverSurface, &KWin::SurfaceInterface::committed); + QVERIFY(committedSpy.wait()); + + m_seatInterface->notifyPointerEnter(serverSurface, QPointF(0, 0)); + QCOMPARE(m_seatInterface->focusedPointerSurface(), serverSurface); + QSignalSpy frameSpy(pointer.get(), &KWayland::Client::Pointer::frame); + QVERIFY(frameSpy.wait()); + QCOMPARE(frameSpy.count(), 1); + + // let's scroll vertically + QSignalSpy axisSourceSpy(pointer.get(), &KWayland::Client::Pointer::axisSourceChanged); + QSignalSpy axisSpy(pointer.get(), &KWayland::Client::Pointer::axisChanged); + QSignalSpy axisDiscreteSpy(pointer.get(), &KWayland::Client::Pointer::axisDiscreteChanged); + QSignalSpy axisStoppedSpy(pointer.get(), &KWayland::Client::Pointer::axisStopped); + + std::chrono::milliseconds timestamp(1); + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyPointerAxis(Qt::Vertical, 10, 120, PointerAxisSource::Wheel); + m_seatInterface->notifyPointerFrame(); + QVERIFY(frameSpy.wait()); + QCOMPARE(frameSpy.count(), 2); + QCOMPARE(axisSourceSpy.count(), 1); + QCOMPARE(axisSourceSpy.last().at(0).value(), KWayland::Client::Pointer::AxisSource::Wheel); + QCOMPARE(axisDiscreteSpy.count(), 1); + QCOMPARE(axisDiscreteSpy.last().at(0).value(), KWayland::Client::Pointer::Axis::Vertical); + QCOMPARE(axisDiscreteSpy.last().at(1).value(), 1); + QCOMPARE(axisSpy.count(), 1); + QCOMPARE(axisSpy.last().at(0).value(), quint32(1)); + QCOMPARE(axisSpy.last().at(1).value(), KWayland::Client::Pointer::Axis::Vertical); + QCOMPARE(axisSpy.last().at(2).value(), 10.0); + QCOMPARE(axisStoppedSpy.count(), 0); + + // let's scroll using fingers + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyPointerAxis(Qt::Horizontal, 42, 0, PointerAxisSource::Finger); + m_seatInterface->notifyPointerFrame(); + QVERIFY(frameSpy.wait()); + QCOMPARE(frameSpy.count(), 3); + QCOMPARE(axisSourceSpy.count(), 2); + QCOMPARE(axisSourceSpy.last().at(0).value(), KWayland::Client::Pointer::AxisSource::Finger); + QCOMPARE(axisDiscreteSpy.count(), 1); + QCOMPARE(axisSpy.count(), 2); + QCOMPARE(axisSpy.last().at(0).value(), quint32(2)); + QCOMPARE(axisSpy.last().at(1).value(), KWayland::Client::Pointer::Axis::Horizontal); + QCOMPARE(axisSpy.last().at(2).value(), 42.0); + QCOMPARE(axisStoppedSpy.count(), 0); + + // lift the fingers off the device + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyPointerAxis(Qt::Horizontal, 0, 0, PointerAxisSource::Finger); + m_seatInterface->notifyPointerFrame(); + QVERIFY(frameSpy.wait()); + QCOMPARE(frameSpy.count(), 4); + QCOMPARE(axisSourceSpy.count(), 3); + QCOMPARE(axisSourceSpy.last().at(0).value(), KWayland::Client::Pointer::AxisSource::Finger); + QCOMPARE(axisDiscreteSpy.count(), 1); + QCOMPARE(axisSpy.count(), 2); + QCOMPARE(axisStoppedSpy.count(), 1); + QCOMPARE(axisStoppedSpy.last().at(0).value(), 3); + QCOMPARE(axisStoppedSpy.last().at(1).value(), KWayland::Client::Pointer::Axis::Horizontal); + + // if the device is unknown, no axis_source event should be sent + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyPointerAxis(Qt::Horizontal, 42, 120, PointerAxisSource::Unknown); + m_seatInterface->notifyPointerFrame(); + QVERIFY(frameSpy.wait()); + QCOMPARE(frameSpy.count(), 5); + QCOMPARE(axisSourceSpy.count(), 3); + QCOMPARE(axisDiscreteSpy.count(), 2); + QCOMPARE(axisDiscreteSpy.last().at(0).value(), KWayland::Client::Pointer::Axis::Horizontal); + QCOMPARE(axisDiscreteSpy.last().at(1).value(), 1); + QCOMPARE(axisSpy.count(), 3); + QCOMPARE(axisSpy.last().at(0).value(), quint32(4)); + QCOMPARE(axisSpy.last().at(1).value(), KWayland::Client::Pointer::Axis::Horizontal); + QCOMPARE(axisSpy.last().at(2).value(), 42.0); + QCOMPARE(axisStoppedSpy.count(), 1); +} + +void TestWaylandSeat::testCursor() +{ + using namespace KWin; + + QSignalSpy pointerSpy(m_seat, &KWayland::Client::Seat::hasPointerChanged); + m_seatInterface->setHasPointer(true); + QVERIFY(pointerSpy.wait()); + + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + KWayland::Client::Surface *surface = m_compositor->createSurface(m_compositor); + QVERIFY(surfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + QImage image(QSize(100, 100), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::black); + surface->attachBuffer(m_shm->createBuffer(image)); + surface->damage(image.rect()); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QSignalSpy committedSpy(serverSurface, &KWin::SurfaceInterface::committed); + QVERIFY(committedSpy.wait()); + + std::unique_ptr p(m_seat->createPointer()); + QVERIFY(p->isValid()); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + + QSignalSpy enteredSpy(p.get(), &KWayland::Client::Pointer::entered); + + uint32_t serial = m_seatInterface->display()->serial(); + m_seatInterface->notifyPointerEnter(serverSurface, QPointF(20, 18), QPointF(10, 15)); + QVERIFY(enteredSpy.wait()); + QCOMPARE_GT(enteredSpy.first().first().value(), serial); + QVERIFY(m_seatInterface->focusedPointerSurface()); + + QSignalSpy cursorChangedSpy(m_seatInterface->pointer(), &KWin::PointerInterface::cursorChanged); + // just remove the pointer + p->setCursor(nullptr); + QVERIFY(cursorChangedSpy.wait()); + QCOMPARE(cursorChangedSpy.count(), 1); + auto cursor = std::get(cursorChangedSpy.last().first().value()); + QVERIFY(cursor); + QVERIFY(!cursor->surface()); + QCOMPARE(cursor->hotspot(), QPoint()); + + // test changing hotspot + p->setCursor(nullptr, QPoint(1, 2)); + QVERIFY(cursorChangedSpy.wait()); + QCOMPARE(cursorChangedSpy.count(), 2); + QCOMPARE(cursor->surface(), nullptr); + QCOMPARE(cursor->hotspot(), QPoint(1, 2)); + + // set surface + QImage img(QSize(10, 20), QImage::Format_RGB32); + img.fill(Qt::red); + auto cursorSurface = m_compositor->createSurface(m_compositor); + cursorSurface->attachBuffer(m_shm->createBuffer(img)); + cursorSurface->damage(QRect(0, 0, 10, 20)); + cursorSurface->commit(KWayland::Client::Surface::CommitFlag::None); + p->setCursor(cursorSurface, QPoint(1, 2)); + QVERIFY(cursorChangedSpy.wait()); + QCOMPARE(cursorChangedSpy.count(), 3); + QCOMPARE(cursor->hotspot(), QPoint(1, 2)); + QVERIFY(cursor->surface()); + + p->hideCursor(); + QVERIFY(cursorChangedSpy.wait()); + QCOMPARE(cursorChangedSpy.count(), 4); + QVERIFY(!cursor->surface()); +} + +void TestWaylandSeat::testKeyboard() +{ + using namespace KWin; + + QSignalSpy keyboardSpy(m_seat, &KWayland::Client::Seat::hasKeyboardChanged); + m_seatInterface->setHasKeyboard(true); + QVERIFY(keyboardSpy.wait()); + + // update modifiers before any surface focused + m_seatInterface->notifyKeyboardModifiers(4, 3, 2, 1); + + // create the surface + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + KWayland::Client::Surface *s = m_compositor->createSurface(m_compositor); + QVERIFY(surfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + KWayland::Client::Keyboard *keyboard = m_seat->createKeyboard(m_seat); + QSignalSpy repeatInfoSpy(keyboard, &KWayland::Client::Keyboard::keyRepeatChanged); + const KWayland::Client::Keyboard &ckeyboard = *keyboard; + QVERIFY(keyboard->isValid()); + QCOMPARE(keyboard->isKeyRepeatEnabled(), false); + QCOMPARE(keyboard->keyRepeatDelay(), 0); + QCOMPARE(keyboard->keyRepeatRate(), 0); + QVERIFY(repeatInfoSpy.wait()); + + auto serverKeyboard = m_seatInterface->keyboard(); + QVERIFY(serverKeyboard); + + // we should get the repeat info announced + QCOMPARE(repeatInfoSpy.count(), 1); + QCOMPARE(keyboard->isKeyRepeatEnabled(), false); + QCOMPARE(keyboard->keyRepeatDelay(), 0); + QCOMPARE(keyboard->keyRepeatRate(), 0); + + // let's change repeat in server + m_seatInterface->keyboard()->setRepeatInfo(25, 660); + QVERIFY(repeatInfoSpy.wait()); + QCOMPARE(repeatInfoSpy.count(), 2); + QCOMPARE(keyboard->isKeyRepeatEnabled(), true); + QCOMPARE(keyboard->keyRepeatRate(), 25); + QCOMPARE(keyboard->keyRepeatDelay(), 660); + + QSignalSpy modifierSpy(keyboard, &KWayland::Client::Keyboard::modifiersChanged); + QSignalSpy enteredSpy(keyboard, &KWayland::Client::Keyboard::entered); + m_seatInterface->setFocusedKeyboardSurface(serverSurface, {KEY_K, KEY_D, KEY_E}); + QCOMPARE(m_seatInterface->focusedKeyboardSurface(), serverSurface); + QCOMPARE(m_seatInterface->keyboard()->focusedSurface(), serverSurface); + + // we get the modifiers sent after the enter + QVERIFY(modifierSpy.wait()); + QCOMPARE(modifierSpy.count(), 1); + QCOMPARE(modifierSpy.first().at(0).value(), quint32(4)); + QCOMPARE(modifierSpy.first().at(1).value(), quint32(3)); + QCOMPARE(modifierSpy.first().at(2).value(), quint32(2)); + QCOMPARE(modifierSpy.first().at(3).value(), quint32(1)); + QCOMPARE(enteredSpy.count(), 1); + // TODO: get through API + QCOMPARE(enteredSpy.first().first().value(), m_display->serial() - 1); + + QSignalSpy keyChangedSpy(keyboard, &KWayland::Client::Keyboard::keyChanged); + + std::chrono::milliseconds time(1); + m_seatInterface->setTimestamp(time++); + m_seatInterface->notifyKeyboardKey(KEY_E, KeyboardKeyState::Released, m_display->nextSerial()); + QVERIFY(keyChangedSpy.wait()); + m_seatInterface->setTimestamp(time++); + m_seatInterface->notifyKeyboardKey(KEY_D, KeyboardKeyState::Released, m_display->nextSerial()); + QVERIFY(keyChangedSpy.wait()); + m_seatInterface->setTimestamp(time++); + m_seatInterface->notifyKeyboardKey(KEY_K, KeyboardKeyState::Released, m_display->nextSerial()); + QVERIFY(keyChangedSpy.wait()); + m_seatInterface->setTimestamp(time++); + m_seatInterface->notifyKeyboardKey(KEY_F1, KeyboardKeyState::Pressed, m_display->nextSerial()); + QVERIFY(keyChangedSpy.wait()); + m_seatInterface->setTimestamp(time++); + m_seatInterface->notifyKeyboardKey(KEY_F1, KeyboardKeyState::Released, m_display->nextSerial()); + QVERIFY(keyChangedSpy.wait()); + + QCOMPARE(keyChangedSpy.count(), 5); + QCOMPARE(keyChangedSpy.at(0).at(0).value(), quint32(KEY_E)); + QCOMPARE(keyChangedSpy.at(0).at(1).value(), KWayland::Client::Keyboard::KeyState::Released); + QCOMPARE(keyChangedSpy.at(0).at(2).value(), quint32(1)); + QCOMPARE(keyChangedSpy.at(1).at(0).value(), quint32(KEY_D)); + QCOMPARE(keyChangedSpy.at(1).at(1).value(), KWayland::Client::Keyboard::KeyState::Released); + QCOMPARE(keyChangedSpy.at(1).at(2).value(), quint32(2)); + QCOMPARE(keyChangedSpy.at(2).at(0).value(), quint32(KEY_K)); + QCOMPARE(keyChangedSpy.at(2).at(1).value(), KWayland::Client::Keyboard::KeyState::Released); + QCOMPARE(keyChangedSpy.at(2).at(2).value(), quint32(3)); + QCOMPARE(keyChangedSpy.at(3).at(0).value(), quint32(KEY_F1)); + QCOMPARE(keyChangedSpy.at(3).at(1).value(), KWayland::Client::Keyboard::KeyState::Pressed); + QCOMPARE(keyChangedSpy.at(3).at(2).value(), quint32(4)); + QCOMPARE(keyChangedSpy.at(4).at(0).value(), quint32(KEY_F1)); + QCOMPARE(keyChangedSpy.at(4).at(1).value(), KWayland::Client::Keyboard::KeyState::Released); + QCOMPARE(keyChangedSpy.at(4).at(2).value(), quint32(5)); + + // releasing a key which is already released should not set a key changed + m_seatInterface->notifyKeyboardKey(KEY_F1, KeyboardKeyState::Released, m_display->nextSerial()); + QVERIFY(sync()); + QCOMPARE(keyChangedSpy.count(), 5); + // let's press it again + m_seatInterface->notifyKeyboardKey(KEY_F1, KeyboardKeyState::Pressed, m_display->nextSerial()); + QVERIFY(keyChangedSpy.wait()); + QCOMPARE(keyChangedSpy.count(), 6); + // press again should be ignored + m_seatInterface->notifyKeyboardKey(KEY_F1, KeyboardKeyState::Pressed, m_display->nextSerial()); + QVERIFY(sync()); + QCOMPARE(keyChangedSpy.count(), 6); + // and release + m_seatInterface->notifyKeyboardKey(KEY_F1, KeyboardKeyState::Released, m_display->nextSerial()); + QVERIFY(keyChangedSpy.wait()); + QCOMPARE(keyChangedSpy.count(), 7); + + m_seatInterface->notifyKeyboardModifiers(1, 2, 3, 4); + QVERIFY(modifierSpy.wait()); + QCOMPARE(modifierSpy.count(), 2); + QCOMPARE(modifierSpy.last().at(0).value(), quint32(1)); + QCOMPARE(modifierSpy.last().at(1).value(), quint32(2)); + QCOMPARE(modifierSpy.last().at(2).value(), quint32(3)); + QCOMPARE(modifierSpy.last().at(3).value(), quint32(4)); + + QSignalSpy leftSpy(keyboard, &KWayland::Client::Keyboard::left); + m_seatInterface->setFocusedKeyboardSurface(nullptr); + QVERIFY(!m_seatInterface->focusedKeyboardSurface()); + QVERIFY(leftSpy.wait()); + QCOMPARE(leftSpy.count(), 1); + // TODO: get through API + QCOMPARE(leftSpy.first().first().value(), m_display->serial() - 1); + + QVERIFY(!keyboard->enteredSurface()); + QVERIFY(!ckeyboard.enteredSurface()); + + // enter it again + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + QVERIFY(modifierSpy.wait()); + QCOMPARE(m_seatInterface->focusedKeyboardSurface(), serverSurface); + QCOMPARE(m_seatInterface->keyboard()->focusedSurface(), serverSurface); + QCOMPARE(enteredSpy.count(), 2); + + QCOMPARE(keyboard->enteredSurface(), s); + QCOMPARE(ckeyboard.enteredSurface(), s); + + QSignalSpy serverSurfaceDestroyedSpy(serverSurface, &QObject::destroyed); + QCOMPARE(keyboard->enteredSurface(), s); + delete s; + QVERIFY(!keyboard->enteredSurface()); + QVERIFY(leftSpy.wait()); + QCOMPARE(serverSurfaceDestroyedSpy.count(), 1); + QVERIFY(!m_seatInterface->focusedKeyboardSurface()); + QVERIFY(!serverKeyboard->focusedSurface()); + + // let's create a Surface again + std::unique_ptr s2(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + QCOMPARE(surfaceCreatedSpy.count(), 2); + serverSurface = surfaceCreatedSpy.last().first().value(); + QVERIFY(serverSurface); + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + QCOMPARE(m_seatInterface->focusedKeyboardSurface(), serverSurface); + QCOMPARE(m_seatInterface->keyboard(), serverKeyboard); +} + +void TestWaylandSeat::testSelection() +{ + using namespace KWin; + std::unique_ptr ddmi(new DataDeviceManagerInterface(m_display)); + KWayland::Client::Registry registry; + QSignalSpy dataDeviceManagerSpy(®istry, &KWayland::Client::Registry::dataDeviceManagerAnnounced); + m_seatInterface->setHasKeyboard(true); + registry.setEventQueue(m_queue); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + + QVERIFY(dataDeviceManagerSpy.wait()); + std::unique_ptr ddm( + registry.createDataDeviceManager(dataDeviceManagerSpy.first().first().value(), dataDeviceManagerSpy.first().last().value())); + QVERIFY(ddm->isValid()); + + std::unique_ptr dd1(ddm->getDataDevice(m_seat)); + QVERIFY(dd1->isValid()); + QSignalSpy selectionSpy(dd1.get(), &KWayland::Client::DataDevice::selectionOffered); + QSignalSpy selectionClearedSpy(dd1.get(), &KWayland::Client::DataDevice::selectionCleared); + + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(surface->isValid()); + QVERIFY(surfaceCreatedSpy.wait()); + auto serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(!m_seatInterface->selection()); + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + QCOMPARE(m_seatInterface->focusedKeyboardSurface(), serverSurface); + QVERIFY(selectionClearedSpy.wait()); + QVERIFY(selectionSpy.isEmpty()); + QVERIFY(!selectionClearedSpy.isEmpty()); + selectionClearedSpy.clear(); + QVERIFY(!m_seatInterface->selection()); + + // now let's try to set a selection - we have keyboard focus, so it should be sent to us + std::unique_ptr ds(ddm->createDataSource()); + QVERIFY(ds->isValid()); + ds->offer(QStringLiteral("text/plain")); + dd1->setSelection(m_display->nextSerial(), ds.get()); + QVERIFY(selectionSpy.wait()); + QCOMPARE(selectionSpy.count(), 1); + auto ddi = m_seatInterface->selection(); + QVERIFY(ddi); + auto df = selectionSpy.first().first().value(); + QCOMPARE(df->offeredMimeTypes().count(), 1); + QCOMPARE(df->offeredMimeTypes().first().name(), QStringLiteral("text/plain")); + + // try to clear + dd1->setSelection(m_display->nextSerial()); + QVERIFY(selectionClearedSpy.wait()); + QCOMPARE(selectionClearedSpy.count(), 1); + QCOMPARE(selectionSpy.count(), 1); + + // unset the keyboard focus + m_seatInterface->setFocusedKeyboardSurface(nullptr); + QVERIFY(!m_seatInterface->focusedKeyboardSurface()); + serverSurface->client()->flush(); + QCoreApplication::processEvents(); + QCoreApplication::processEvents(); + + // try to set Selection + dd1->setSelection(m_display->nextSerial(), ds.get()); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCoreApplication::processEvents(); + QCOMPARE(selectionSpy.count(), 1); + + // let's unset the selection on the seat + m_seatInterface->setSelection(nullptr, m_display->nextSerial()); + // and pass focus back on our surface + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + // we don't have a selection, so it should not send a selection + QVERIFY(sync()); + QCOMPARE(selectionSpy.count(), 1); + // now let's set it manually + m_seatInterface->setSelection(ddi, m_display->nextSerial()); + QCOMPARE(m_seatInterface->selection(), ddi); + QVERIFY(selectionSpy.wait()); + QCOMPARE(selectionSpy.count(), 2); + // setting the same again should not change + m_seatInterface->setSelection(ddi, m_display->nextSerial()); + QVERIFY(sync()); + QCOMPARE(selectionSpy.count(), 2); + // now clear it manually + m_seatInterface->setSelection(nullptr, m_display->nextSerial()); + QVERIFY(selectionClearedSpy.wait()); + QCOMPARE(selectionSpy.count(), 2); + + // create a second ddi and a data source + std::unique_ptr dd2(ddm->getDataDevice(m_seat)); + QVERIFY(dd2->isValid()); + std::unique_ptr ds2(ddm->createDataSource()); + QVERIFY(ds2->isValid()); + ds2->offer(QStringLiteral("text/plain")); + dd2->setSelection(m_display->nextSerial(), ds2.get()); + QVERIFY(selectionSpy.wait()); + QSignalSpy cancelledSpy(ds2.get(), &KWayland::Client::DataSource::cancelled); + m_seatInterface->setSelection(ddi, m_display->nextSerial()); + QVERIFY(cancelledSpy.wait()); +} + +void TestWaylandSeat::testDataDeviceForKeyboardSurface() +{ + // this test verifies that the server does not crash when creating a datadevice for the focused keyboard surface + // and the currentSelection does not have a DataSource. + // to properly test the functionality this test requires a second client + using namespace KWin; + // create the DataDeviceManager + std::unique_ptr ddmi(new DataDeviceManagerInterface(m_display)); + QSignalSpy ddiCreatedSpy(ddmi.get(), &DataDeviceManagerInterface::dataDeviceCreated); + m_seatInterface->setHasKeyboard(true); + + // create a second Wayland client connection to use it for setSelection + auto c = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(c, &KWayland::Client::ConnectionThread::connected); + c->setSocketName(s_socketName); + + auto thread = new QThread(this); + c->moveToThread(thread); + thread->start(); + + c->initConnection(); + QVERIFY(connectedSpy.wait()); + + std::unique_ptr queue(new KWayland::Client::EventQueue); + queue->setup(c); + + std::unique_ptr registry(new KWayland::Client::Registry); + QSignalSpy interfacesAnnouncedSpy(registry.get(), &KWayland::Client::Registry::interfacesAnnounced); + registry->setEventQueue(queue.get()); + registry->create(c); + QVERIFY(registry->isValid()); + registry->setup(); + + QVERIFY(interfacesAnnouncedSpy.wait()); + std::unique_ptr seat( + registry->createSeat(registry->interface(KWayland::Client::Registry::Interface::Seat).name, registry->interface(KWayland::Client::Registry::Interface::Seat).version)); + QVERIFY(seat->isValid()); + std::unique_ptr ddm1(registry->createDataDeviceManager(registry->interface(KWayland::Client::Registry::Interface::DataDeviceManager).name, + registry->interface(KWayland::Client::Registry::Interface::DataDeviceManager).version)); + QVERIFY(ddm1->isValid()); + + // now create our first datadevice + std::unique_ptr dd1(ddm1->getDataDevice(seat.get())); + QVERIFY(ddiCreatedSpy.wait()); + auto ddi = ddiCreatedSpy.first().first().value(); + QVERIFY(ddi); + + // switch to other client + // create a surface and pass it keyboard focus + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(surface->isValid()); + QVERIFY(surfaceCreatedSpy.wait()); + auto serverSurface = surfaceCreatedSpy.first().first().value(); + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + QCOMPARE(m_seatInterface->focusedKeyboardSurface(), serverSurface); + + // now create a DataDevice + KWayland::Client::Registry registry2; + QSignalSpy dataDeviceManagerSpy(®istry2, &KWayland::Client::Registry::dataDeviceManagerAnnounced); + registry2.setEventQueue(m_queue); + registry2.create(m_connection->display()); + QVERIFY(registry2.isValid()); + registry2.setup(); + + QVERIFY(dataDeviceManagerSpy.wait()); + std::unique_ptr ddm( + registry2.createDataDeviceManager(dataDeviceManagerSpy.first().first().value(), dataDeviceManagerSpy.first().last().value())); + QVERIFY(ddm->isValid()); + + std::unique_ptr dd(ddm->getDataDevice(m_seat)); + QVERIFY(dd->isValid()); + QVERIFY(ddiCreatedSpy.wait()); + + // unset surface and set again + m_seatInterface->setFocusedKeyboardSurface(nullptr); + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + + // and delete the connection thread again + dd1.reset(); + ddm1.reset(); + seat.reset(); + registry.reset(); + queue.reset(); + c->deleteLater(); + thread->quit(); + thread->wait(); + delete thread; +} + +void TestWaylandSeat::testTouch() +{ + using namespace KWin; + + QSignalSpy touchSpy(m_seat, &KWayland::Client::Seat::hasTouchChanged); + m_seatInterface->setHasTouch(true); + QVERIFY(touchSpy.wait()); + + // create the surface + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + KWayland::Client::Surface *s = m_compositor->createSurface(m_compositor); + QVERIFY(surfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + KWayland::Client::Touch *touch = m_seat->createTouch(m_seat); + QVERIFY(touch->isValid()); + + // Process wl_touch bind request. + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + + QSignalSpy sequenceStartedSpy(touch, &KWayland::Client::Touch::sequenceStarted); + QSignalSpy sequenceEndedSpy(touch, &KWayland::Client::Touch::sequenceEnded); + QSignalSpy sequenceCanceledSpy(touch, &KWayland::Client::Touch::sequenceCanceled); + QSignalSpy frameEndedSpy(touch, &KWayland::Client::Touch::frameEnded); + QSignalSpy pointAddedSpy(touch, &KWayland::Client::Touch::pointAdded); + QSignalSpy pointMovedSpy(touch, &KWayland::Client::Touch::pointMoved); + QSignalSpy pointRemovedSpy(touch, &KWayland::Client::Touch::pointRemoved); + + std::chrono::milliseconds timestamp(1); + + // try a few things + const QPointF surfacePosition(10, 20); + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyTouchDown(serverSurface, QPointF(10, 20), 0, QPointF(15, 26)); + QVERIFY(sequenceStartedSpy.wait()); + QCOMPARE(sequenceStartedSpy.count(), 1); + QCOMPARE(sequenceEndedSpy.count(), 0); + QCOMPARE(sequenceCanceledSpy.count(), 0); + QCOMPARE(frameEndedSpy.count(), 0); + QCOMPARE(pointAddedSpy.count(), 0); + QCOMPARE(pointMovedSpy.count(), 0); + QCOMPARE(pointRemovedSpy.count(), 0); + KWayland::Client::TouchPoint *tp = sequenceStartedSpy.first().first().value(); + QVERIFY(tp); + QCOMPARE(tp->downSerial(), m_seatInterface->display()->serial()); + QCOMPARE(tp->id(), 0); + QVERIFY(tp->isDown()); + QCOMPARE(tp->position(), QPointF(5, 6)); + QCOMPARE(tp->positions().size(), 1); + QCOMPARE(tp->time(), 1u); + QCOMPARE(tp->timestamps().count(), 1); + QCOMPARE(tp->upSerial(), 0u); + QCOMPARE(tp->surface().data(), s); + QCOMPARE(touch->sequence().count(), 1); + QCOMPARE(touch->sequence().first(), tp); + + // let's end the frame + m_seatInterface->notifyTouchFrame(); + QVERIFY(frameEndedSpy.wait()); + QCOMPARE(frameEndedSpy.count(), 1); + + // move the one point + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyTouchMotion(0, QPointF(10, 20)); + m_seatInterface->notifyTouchFrame(); + QVERIFY(frameEndedSpy.wait()); + QCOMPARE(sequenceStartedSpy.count(), 1); + QCOMPARE(sequenceEndedSpy.count(), 0); + QCOMPARE(sequenceCanceledSpy.count(), 0); + QCOMPARE(frameEndedSpy.count(), 2); + QCOMPARE(pointAddedSpy.count(), 0); + QCOMPARE(pointMovedSpy.count(), 1); + QCOMPARE(pointRemovedSpy.count(), 0); + QCOMPARE(pointMovedSpy.first().first().value(), tp); + + QCOMPARE(tp->id(), 0); + QVERIFY(tp->isDown()); + QCOMPARE(tp->position(), QPointF(0, 0)); + QCOMPARE(tp->positions().size(), 2); + QCOMPARE(tp->time(), 2u); + QCOMPARE(tp->timestamps().count(), 2); + QCOMPARE(tp->upSerial(), 0u); + QCOMPARE(tp->surface().data(), s); + + // add onther point + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyTouchDown(serverSurface, surfacePosition, 1, QPointF(15, 26)); + m_seatInterface->notifyTouchFrame(); + QVERIFY(frameEndedSpy.wait()); + QCOMPARE(sequenceStartedSpy.count(), 1); + QCOMPARE(sequenceEndedSpy.count(), 0); + QCOMPARE(sequenceCanceledSpy.count(), 0); + QCOMPARE(frameEndedSpy.count(), 3); + QCOMPARE(pointAddedSpy.count(), 1); + QCOMPARE(pointMovedSpy.count(), 1); + QCOMPARE(pointRemovedSpy.count(), 0); + QCOMPARE(touch->sequence().count(), 2); + QCOMPARE(touch->sequence().first(), tp); + KWayland::Client::TouchPoint *tp2 = pointAddedSpy.first().first().value(); + QVERIFY(tp2); + QCOMPARE(touch->sequence().last(), tp2); + QCOMPARE(tp2->id(), 1); + QVERIFY(tp2->isDown()); + QCOMPARE(tp2->position(), QPointF(5, 6)); + QCOMPARE(tp2->positions().size(), 1); + QCOMPARE(tp2->time(), 3u); + QCOMPARE(tp2->timestamps().count(), 1); + QCOMPARE(tp2->upSerial(), 0u); + QCOMPARE(tp2->surface().data(), s); + + // send it an up + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyTouchUp(1); + m_seatInterface->notifyTouchFrame(); + QVERIFY(frameEndedSpy.wait()); + QCOMPARE(sequenceStartedSpy.count(), 1); + QCOMPARE(sequenceEndedSpy.count(), 0); + QCOMPARE(sequenceCanceledSpy.count(), 0); + QCOMPARE(frameEndedSpy.count(), 4); + QCOMPARE(pointAddedSpy.count(), 1); + QCOMPARE(pointMovedSpy.count(), 1); + QCOMPARE(pointRemovedSpy.count(), 1); + QCOMPARE(pointRemovedSpy.first().first().value(), tp2); + QCOMPARE(tp2->id(), 1); + QVERIFY(!tp2->isDown()); + QCOMPARE(tp2->position(), QPointF(5, 6)); + QCOMPARE(tp2->positions().size(), 1); + QCOMPARE(tp2->time(), 4u); + QCOMPARE(tp2->timestamps().count(), 2); + QCOMPARE(tp2->upSerial(), m_seatInterface->display()->serial()); + QCOMPARE(tp2->surface().data(), s); + + // send another down and up + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyTouchDown(serverSurface, surfacePosition, 1, QPointF(15, 26)); + m_seatInterface->notifyTouchFrame(); + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyTouchUp(1); + // and send an up for the first point + m_seatInterface->notifyTouchUp(0); + m_seatInterface->notifyTouchFrame(); + QVERIFY(frameEndedSpy.wait()); + QCOMPARE(sequenceStartedSpy.count(), 1); + QCOMPARE(sequenceEndedSpy.count(), 1); + QCOMPARE(sequenceCanceledSpy.count(), 0); + QCOMPARE(frameEndedSpy.count(), 6); + QCOMPARE(pointAddedSpy.count(), 2); + QCOMPARE(pointMovedSpy.count(), 1); + QCOMPARE(pointRemovedSpy.count(), 3); + QCOMPARE(touch->sequence().count(), 3); + QVERIFY(!touch->sequence().at(0)->isDown()); + QVERIFY(!touch->sequence().at(1)->isDown()); + QVERIFY(!touch->sequence().at(2)->isDown()); + QVERIFY(!m_seatInterface->isTouchSequence()); + + // try cancel + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyTouchDown(serverSurface, QPointF(15, 26), 0, QPointF(15, 26)); + m_seatInterface->notifyTouchFrame(); + m_seatInterface->notifyTouchCancel(); + QVERIFY(sequenceCanceledSpy.wait()); + QCOMPARE(sequenceStartedSpy.count(), 2); + QCOMPARE(sequenceEndedSpy.count(), 1); + QCOMPARE(sequenceCanceledSpy.count(), 1); + QCOMPARE(frameEndedSpy.count(), 7); + QCOMPARE(pointAddedSpy.count(), 2); + QCOMPARE(pointMovedSpy.count(), 1); + QCOMPARE(pointRemovedSpy.count(), 3); + QCOMPARE(touch->sequence().first()->position(), QPointF(0, 0)); + + // destroy touched surface + QSignalSpy serverSurfaceDestroyedSpy(serverSurface, &QObject::destroyed); + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyTouchDown(serverSurface, surfacePosition, 2, QPointF(10, 15)); + m_seatInterface->notifyTouchFrame(); + QVERIFY(frameEndedSpy.wait()); + QCOMPARE(sequenceStartedSpy.count(), 3); + QCOMPARE(sequenceEndedSpy.count(), 1); + QCOMPARE(sequenceCanceledSpy.count(), 1); + QCOMPARE(frameEndedSpy.count(), 8); + QCOMPARE(pointAddedSpy.count(), 2); + QCOMPARE(pointMovedSpy.count(), 1); + QCOMPARE(pointRemovedSpy.count(), 3); + + delete s; + QVERIFY(serverSurfaceDestroyedSpy.wait()); + + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyTouchMotion(2, QPointF(10, 20)); + m_seatInterface->notifyTouchFrame(); + QVERIFY(!frameEndedSpy.wait(10)); + + m_seatInterface->setTimestamp(timestamp++); + m_seatInterface->notifyTouchUp(2); + m_seatInterface->notifyTouchFrame(); + QVERIFY(frameEndedSpy.wait()); + QCOMPARE(sequenceStartedSpy.count(), 3); + QCOMPARE(sequenceEndedSpy.count(), 2); + QCOMPARE(sequenceCanceledSpy.count(), 1); + QCOMPARE(frameEndedSpy.count(), 9); + QCOMPARE(pointAddedSpy.count(), 2); + QCOMPARE(pointMovedSpy.count(), 1); + QCOMPARE(pointRemovedSpy.count(), 4); +} + +void TestWaylandSeat::testKeymap() +{ + using namespace KWin; + + m_seatInterface->setHasKeyboard(true); + QSignalSpy keyboardChangedSpy(m_seat, &KWayland::Client::Seat::hasKeyboardChanged); + QVERIFY(keyboardChangedSpy.wait()); + + std::unique_ptr keyboard(m_seat->createKeyboard()); + + // create surface + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(surface->isValid()); + QVERIFY(surfaceCreatedSpy.wait()); + auto serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(!m_seatInterface->selection()); + m_seatInterface->setFocusedKeyboardSurface(serverSurface); + + QSignalSpy keymapChangedSpy(keyboard.get(), &KWayland::Client::Keyboard::keymapChanged); + + m_seatInterface->keyboard()->setKeymap(QByteArrayLiteral("foo")); + QVERIFY(keymapChangedSpy.wait()); + int fd = keymapChangedSpy.first().first().toInt(); + QVERIFY(fd != -1); + // Account for null terminator. + QCOMPARE(keymapChangedSpy.first().last().value(), 4u); + QFile file; + QVERIFY(file.open(fd, QIODevice::ReadOnly)); + const char *address = reinterpret_cast(file.map(0, keymapChangedSpy.first().last().value())); + QVERIFY(address); + QCOMPARE(qstrcmp(address, "foo"), 0); + file.close(); + + // change the keymap + keymapChangedSpy.clear(); + m_seatInterface->keyboard()->setKeymap(QByteArrayLiteral("bar")); + QVERIFY(keymapChangedSpy.wait()); + fd = keymapChangedSpy.first().first().toInt(); + QVERIFY(fd != -1); + // Account for null terminator. + QCOMPARE(keymapChangedSpy.first().last().value(), 4u); + QVERIFY(file.open(fd, QIODevice::ReadWrite)); + address = reinterpret_cast(file.map(0, keymapChangedSpy.first().last().value())); + QVERIFY(address); + QCOMPARE(qstrcmp(address, "bar"), 0); +} + +QTEST_GUILESS_MAIN(TestWaylandSeat) +#include "test_wayland_seat.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_slide.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_slide.cpp new file mode 100644 index 0000000000..44bb3bda27 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_slide.cpp @@ -0,0 +1,187 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// KWin +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/slide.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/region.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/slide.h" +#include "KWayland/Client/surface.h" + +class TestSlide : public QObject +{ + Q_OBJECT +public: + explicit TestSlide(QObject *parent = nullptr); +private Q_SLOTS: + void init(); + void cleanup(); + + void testCreate(); + void testSurfaceDestroy(); + +private: + KWin::Display *m_display; + KWin::CompositorInterface *m_compositorInterface; + KWin::SlideManagerInterface *m_slideManagerInterface; + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::Compositor *m_compositor; + KWayland::Client::SlideManager *m_slideManager; + KWayland::Client::EventQueue *m_queue; + QThread *m_thread; +}; + +static const QString s_socketName = QStringLiteral("kwayland-test-wayland-slide-0"); + +TestSlide::TestSlide(QObject *parent) + : QObject(parent) + , m_display(nullptr) + , m_compositorInterface(nullptr) + , m_connection(nullptr) + , m_compositor(nullptr) + , m_queue(nullptr) + , m_thread(nullptr) +{ +} + +void TestSlide::init() +{ + using namespace KWin; + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + KWayland::Client::Registry registry; + QSignalSpy compositorSpy(®istry, &KWayland::Client::Registry::compositorAnnounced); + + QSignalSpy slideSpy(®istry, &KWayland::Client::Registry::slideAnnounced); + + QVERIFY(!registry.eventQueue()); + registry.setEventQueue(m_queue); + QCOMPARE(registry.eventQueue(), m_queue); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + + m_compositorInterface = new CompositorInterface(m_display, m_display); + QVERIFY(compositorSpy.wait()); + m_compositor = registry.createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + + m_slideManagerInterface = new SlideManagerInterface(m_display, m_display); + + QVERIFY(slideSpy.wait()); + m_slideManager = registry.createSlideManager(slideSpy.first().first().value(), slideSpy.first().last().value(), this); +} + +void TestSlide::cleanup() +{ +#define CLEANUP(variable) \ + if (variable) { \ + delete variable; \ + variable = nullptr; \ + } + CLEANUP(m_compositor) + CLEANUP(m_slideManager) + CLEANUP(m_queue) + if (m_connection) { + m_connection->deleteLater(); + m_connection = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + CLEANUP(m_display) +#undef CLEANUP + // these are the children of the display + m_compositorInterface = nullptr; + m_slideManagerInterface = nullptr; +} + +void TestSlide::testCreate() +{ + QSignalSpy serverSurfaceCreated(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + + auto serverSurface = serverSurfaceCreated.first().first().value(); + QSignalSpy slideChanged(serverSurface, &KWin::SurfaceInterface::slideOnShowHideChanged); + + auto slide = m_slideManager->createSlide(surface.get(), surface.get()); + slide->setLocation(KWayland::Client::Slide::Location::Top); + slide->setOffset(15); + slide->commit(); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + QVERIFY(slideChanged.wait()); + QCOMPARE(serverSurface->slideOnShowHide()->location(), KWin::SlideInterface::Location::Top); + QCOMPARE(serverSurface->slideOnShowHide()->offset(), 15); + + // and destroy + QSignalSpy destroyedSpy(serverSurface->slideOnShowHide(), &QObject::destroyed); + delete slide; + QVERIFY(destroyedSpy.wait()); +} + +void TestSlide::testSurfaceDestroy() +{ + using namespace KWin; + QSignalSpy serverSurfaceCreated(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + + auto serverSurface = serverSurfaceCreated.first().first().value(); + QSignalSpy slideChanged(serverSurface, &SurfaceInterface::slideOnShowHideChanged); + + std::unique_ptr slide(m_slideManager->createSlide(surface.get())); + slide->commit(); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(slideChanged.wait()); + auto serverSlide = serverSurface->slideOnShowHide(); + QVERIFY(serverSlide); + + // destroy the parent surface + QSignalSpy surfaceDestroyedSpy(serverSurface, &QObject::destroyed); + QSignalSpy slideDestroyedSpy(serverSlide, &QObject::destroyed); + surface.reset(); + QVERIFY(surfaceDestroyedSpy.wait()); + QVERIFY(slideDestroyedSpy.isEmpty()); + // destroy the slide + slide.reset(); + QVERIFY(slideDestroyedSpy.wait()); +} + +QTEST_GUILESS_MAIN(TestSlide) +#include "test_wayland_slide.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_subsurface.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_subsurface.cpp new file mode 100644 index 0000000000..7832ab15bd --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_subsurface.cpp @@ -0,0 +1,1086 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// KWin +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/subcompositor.h" +#include "wayland/surface.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/region.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/shm_pool.h" +#include "KWayland/Client/subcompositor.h" +#include "KWayland/Client/subsurface.h" +#include "KWayland/Client/surface.h" + +// Wayland +#include + +Q_DECLARE_METATYPE(KWayland::Client::SubSurface::Mode) + +class TestSubSurface : public QObject +{ + Q_OBJECT +public: + explicit TestSubSurface(QObject *parent = nullptr); +private Q_SLOTS: + void init(); + void cleanup(); + + void testCreate(); + void testMode(); + void testPosition_data(); + void testPosition(); + void testPlaceAbove(); + void testPlaceBelow(); + void testSyncMode(); + void testDeSyncMode(); + void testMainSurfaceFromTree(); + void testRemoveSurface(); + void testMappingOfSurfaceTree(); + void testSurfaceAt(); + void testDestroyAttachedBuffer(); + void testDestroyParentSurface(); + void testDestroyCommittedSubSurfaceBeforeParent(); + +private: + KWin::Display *m_display; + KWin::CompositorInterface *m_compositorInterface; + KWin::SubCompositorInterface *m_subcompositorInterface; + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::Compositor *m_compositor; + KWayland::Client::ShmPool *m_shm; + KWayland::Client::SubCompositor *m_subCompositor; + KWayland::Client::EventQueue *m_queue; + QThread *m_thread; +}; + +static const QString s_socketName = QStringLiteral("kwayland-test-wayland-subsurface-0"); + +TestSubSurface::TestSubSurface(QObject *parent) + : QObject(parent) + , m_display(nullptr) + , m_compositorInterface(nullptr) + , m_subcompositorInterface(nullptr) + , m_connection(nullptr) + , m_compositor(nullptr) + , m_shm(nullptr) + , m_subCompositor(nullptr) + , m_queue(nullptr) + , m_thread(nullptr) +{ +} + +void TestSubSurface::init() +{ + using namespace KWin; + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + m_display->createShm(); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + KWayland::Client::Registry registry; + QSignalSpy compositorSpy(®istry, &KWayland::Client::Registry::compositorAnnounced); + QSignalSpy subCompositorSpy(®istry, &KWayland::Client::Registry::subCompositorAnnounced); + QVERIFY(!registry.eventQueue()); + registry.setEventQueue(m_queue); + QCOMPARE(registry.eventQueue(), m_queue); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + + m_compositorInterface = new CompositorInterface(m_display, m_display); + m_subcompositorInterface = new SubCompositorInterface(m_display, m_display); + QVERIFY(m_subcompositorInterface); + + QVERIFY(subCompositorSpy.wait()); + m_subCompositor = registry.createSubCompositor(subCompositorSpy.first().first().value(), subCompositorSpy.first().last().value(), this); + + if (compositorSpy.isEmpty()) { + QVERIFY(compositorSpy.wait()); + } + m_compositor = registry.createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + + m_shm = registry.createShmPool(registry.interface(KWayland::Client::Registry::Interface::Shm).name, + registry.interface(KWayland::Client::Registry::Interface::Shm).version, + this); + QVERIFY(m_shm->isValid()); +} + +void TestSubSurface::cleanup() +{ + if (m_shm) { + delete m_shm; + m_shm = nullptr; + } + if (m_subCompositor) { + delete m_subCompositor; + m_subCompositor = nullptr; + } + if (m_compositor) { + delete m_compositor; + m_compositor = nullptr; + } + if (m_queue) { + delete m_queue; + m_queue = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + delete m_connection; + m_connection = nullptr; + + delete m_display; + m_display = nullptr; +} + +void TestSubSurface::testCreate() +{ + using namespace KWin; + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + + // create two Surfaces + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + surfaceCreatedSpy.clear(); + std::unique_ptr parent(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + SurfaceInterface *serverParentSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverParentSurface); + + QSignalSpy subSurfaceCreatedSpy(m_subcompositorInterface, &KWin::SubCompositorInterface::subSurfaceCreated); + + // create subSurface for surface of parent + std::unique_ptr subSurface(m_subCompositor->createSubSurface(surface.get(), parent.get())); + + QVERIFY(subSurfaceCreatedSpy.wait()); + SubSurfaceInterface *serverSubSurface = subSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSubSurface); + QVERIFY(serverSubSurface->parentSurface()); + QCOMPARE(serverSubSurface->parentSurface(), serverParentSurface); + QCOMPARE(serverSubSurface->surface(), serverSurface); + QCOMPARE(serverSurface->subSurface(), serverSubSurface); + QCOMPARE(serverSubSurface->mainSurface(), serverParentSurface); + // children are only added after committing the surface + QCOMPARE(serverParentSurface->below().count(), 0); + QEXPECT_FAIL("", "Incorrect adding of child windows to workaround QtWayland behavior", Continue); + QCOMPARE(serverParentSurface->above().count(), 0); + // so let's commit the surface, to apply the stacking change + parent->commit(KWayland::Client::Surface::CommitFlag::None); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(serverParentSurface->below().count(), 0); + QCOMPARE(serverParentSurface->above().count(), 1); + QCOMPARE(serverParentSurface->above().constFirst(), serverSubSurface); + + // and let's destroy it again + QSignalSpy destroyedSpy(serverSubSurface, &QObject::destroyed); + subSurface.reset(); + QVERIFY(destroyedSpy.wait()); + QCOMPARE(serverSurface->subSurface(), QPointer()); + // only applied after next commit + QCOMPARE(serverParentSurface->below().count(), 0); + QEXPECT_FAIL("", "Incorrect removing of child windows to workaround QtWayland behavior", Continue); + QCOMPARE(serverParentSurface->above().count(), 1); + // but the surface should be invalid + if (!serverParentSurface->above().isEmpty()) { + QVERIFY(!serverParentSurface->above().constFirst()); + } + // committing the state should solve it + parent->commit(KWayland::Client::Surface::CommitFlag::None); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(serverParentSurface->below().count(), 0); + QCOMPARE(serverParentSurface->above().count(), 0); +} + +void TestSubSurface::testMode() +{ + using namespace KWin; + // create two Surface + std::unique_ptr surface(m_compositor->createSurface()); + std::unique_ptr parent(m_compositor->createSurface()); + + QSignalSpy subSurfaceCreatedSpy(m_subcompositorInterface, &KWin::SubCompositorInterface::subSurfaceCreated); + + // create the SubSurface for surface of parent + std::unique_ptr subSurface(m_subCompositor->createSubSurface(surface.get(), parent.get())); + QVERIFY(subSurfaceCreatedSpy.wait()); + SubSurfaceInterface *serverSubSurface = subSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSubSurface); + + // both client and server subsurface should be in synchronized mode + QCOMPARE(subSurface->mode(), KWayland::Client::SubSurface::Mode::Synchronized); + QCOMPARE(serverSubSurface->mode(), SubSurfaceInterface::Mode::Synchronized); + + // verify that we can change to desynchronized + QSignalSpy modeChangedSpy(serverSubSurface, &KWin::SubSurfaceInterface::modeChanged); + + subSurface->setMode(KWayland::Client::SubSurface::Mode::Desynchronized); + QCOMPARE(subSurface->mode(), KWayland::Client::SubSurface::Mode::Desynchronized); + + QVERIFY(modeChangedSpy.wait()); + QCOMPARE(modeChangedSpy.first().first().value(), SubSurfaceInterface::Mode::Desynchronized); + QCOMPARE(serverSubSurface->mode(), SubSurfaceInterface::Mode::Desynchronized); + + // setting the same again won't change + subSurface->setMode(KWayland::Client::SubSurface::Mode::Desynchronized); + QCOMPARE(subSurface->mode(), KWayland::Client::SubSurface::Mode::Desynchronized); + // not testing the signal, we do that after changing to synchronized + + // and change back to synchronized + subSurface->setMode(KWayland::Client::SubSurface::Mode::Synchronized); + QCOMPARE(subSurface->mode(), KWayland::Client::SubSurface::Mode::Synchronized); + + QVERIFY(modeChangedSpy.wait()); + QCOMPARE(modeChangedSpy.count(), 2); + QCOMPARE(modeChangedSpy.first().first().value(), SubSurfaceInterface::Mode::Desynchronized); + QCOMPARE(modeChangedSpy.last().first().value(), SubSurfaceInterface::Mode::Synchronized); + QCOMPARE(serverSubSurface->mode(), SubSurfaceInterface::Mode::Synchronized); +} + +void TestSubSurface::testPosition_data() +{ + QTest::addColumn("commitMode"); + + QTest::addRow("sync") << KWayland::Client::SubSurface::Mode::Synchronized; + QTest::addRow("desync") << KWayland::Client::SubSurface::Mode::Desynchronized; +} + +void TestSubSurface::testPosition() +{ + using namespace KWin; + // create two Surface + std::unique_ptr surface(m_compositor->createSurface()); + std::unique_ptr parent(m_compositor->createSurface()); + + QSignalSpy subSurfaceCreatedSpy(m_subcompositorInterface, &KWin::SubCompositorInterface::subSurfaceCreated); + + // create the SubSurface for surface of parent + std::unique_ptr subSurface(m_subCompositor->createSubSurface(surface.get(), parent.get())); + QVERIFY(subSurfaceCreatedSpy.wait()); + SubSurfaceInterface *serverSubSurface = subSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSubSurface); + + // put the subsurface in the desired commit mode + QFETCH(KWayland::Client::SubSurface::Mode, commitMode); + subSurface->setMode(commitMode); + + // both client and server should have a default position + QCOMPARE(subSurface->position(), QPoint()); + QCOMPARE(serverSubSurface->position(), QPoint()); + + QSignalSpy positionChangedSpy(serverSubSurface, &KWin::SubSurfaceInterface::positionChanged); + + // changing the position should not trigger a direct update on server side + subSurface->setPosition(QPoint(10, 20)); + QCOMPARE(subSurface->position(), QPoint(10, 20)); + // ensure it's processed on server side + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(serverSubSurface->position(), QPoint()); + // changing once more + subSurface->setPosition(QPoint(20, 30)); + QCOMPARE(subSurface->position(), QPoint(20, 30)); + // ensure it's processed on server side + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(serverSubSurface->position(), QPoint()); + + // committing the parent surface should update the position + QSignalSpy parentCommittedSpy(serverSubSurface->parentSurface(), &SurfaceInterface::committed); + parent->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(parentCommittedSpy.wait()); + QCOMPARE(positionChangedSpy.count(), 1); + QCOMPARE(positionChangedSpy.first().first().toPoint(), QPoint(20, 30)); + QCOMPARE(serverSubSurface->position(), QPoint(20, 30)); +} + +void TestSubSurface::testPlaceAbove() +{ + using namespace KWin; + // create needed Surfaces (one parent, three client + std::unique_ptr surface1(m_compositor->createSurface()); + std::unique_ptr surface2(m_compositor->createSurface()); + std::unique_ptr surface3(m_compositor->createSurface()); + std::unique_ptr parent(m_compositor->createSurface()); + + QSignalSpy subSurfaceCreatedSpy(m_subcompositorInterface, &KWin::SubCompositorInterface::subSurfaceCreated); + + // create the SubSurfaces for surface of parent + std::unique_ptr subSurface1(m_subCompositor->createSubSurface(surface1.get(), parent.get())); + QVERIFY(subSurfaceCreatedSpy.wait()); + SubSurfaceInterface *serverSubSurface1 = subSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSubSurface1); + subSurfaceCreatedSpy.clear(); + std::unique_ptr subSurface2(m_subCompositor->createSubSurface(surface2.get(), parent.get())); + QVERIFY(subSurfaceCreatedSpy.wait()); + SubSurfaceInterface *serverSubSurface2 = subSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSubSurface2); + subSurfaceCreatedSpy.clear(); + std::unique_ptr subSurface3(m_subCompositor->createSubSurface(surface3.get(), parent.get())); + QVERIFY(subSurfaceCreatedSpy.wait()); + SubSurfaceInterface *serverSubSurface3 = subSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSubSurface3); + subSurfaceCreatedSpy.clear(); + + // so far the stacking order should still be empty + QVERIFY(serverSubSurface1->parentSurface()->below().isEmpty()); + QEXPECT_FAIL("", "Incorrect adding of child windows to workaround QtWayland behavior", Continue); + QVERIFY(serverSubSurface1->parentSurface()->above().isEmpty()); + + // committing the parent should create the stacking order + parent->commit(KWayland::Client::Surface::CommitFlag::None); + // ensure it's processed on server side + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(serverSubSurface1->parentSurface()->below().count(), 0); + QCOMPARE(serverSubSurface1->parentSurface()->above().count(), 3); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(0), serverSubSurface1); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(1), serverSubSurface2); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(2), serverSubSurface3); + + // raising subsurface1 should place it to top of stack + subSurface1->placeAbove(subSurface3.get()); + // ensure it's processed on server side + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + // but as long as parent is not committed it shouldn't change on server side + QCOMPARE(serverSubSurface1->parentSurface()->above().at(0), serverSubSurface1); + // after commit it's changed + parent->commit(KWayland::Client::Surface::CommitFlag::None); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(serverSubSurface1->parentSurface()->above().count(), 3); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(0), serverSubSurface2); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(1), serverSubSurface3); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(2), serverSubSurface1); + + // try placing 3 above 1, should result in 2, 1, 3 + subSurface3->placeAbove(subSurface1.get()); + parent->commit(KWayland::Client::Surface::CommitFlag::None); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(serverSubSurface1->parentSurface()->above().count(), 3); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(0), serverSubSurface2); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(1), serverSubSurface1); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(2), serverSubSurface3); + + // try placing 3 above 2, should result in 2, 3, 1 + subSurface3->placeAbove(subSurface2.get()); + parent->commit(KWayland::Client::Surface::CommitFlag::None); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(serverSubSurface1->parentSurface()->above().count(), 3); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(0), serverSubSurface2); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(1), serverSubSurface3); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(2), serverSubSurface1); + + // try placing 1 above 3 - shouldn't change + subSurface1->placeAbove(subSurface3.get()); + parent->commit(KWayland::Client::Surface::CommitFlag::None); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(serverSubSurface1->parentSurface()->above().count(), 3); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(0), serverSubSurface2); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(1), serverSubSurface3); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(2), serverSubSurface1); + + // and 2 above 3 - > 3, 2, 1 + subSurface2->placeAbove(subSurface3.get()); + parent->commit(KWayland::Client::Surface::CommitFlag::None); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(serverSubSurface1->parentSurface()->above().count(), 3); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(0), serverSubSurface3); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(1), serverSubSurface2); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(2), serverSubSurface1); +} + +void TestSubSurface::testPlaceBelow() +{ + using namespace KWin; + // create needed Surfaces (one parent, three client + std::unique_ptr surface1(m_compositor->createSurface()); + std::unique_ptr surface2(m_compositor->createSurface()); + std::unique_ptr surface3(m_compositor->createSurface()); + std::unique_ptr parent(m_compositor->createSurface()); + + QSignalSpy subSurfaceCreatedSpy(m_subcompositorInterface, &KWin::SubCompositorInterface::subSurfaceCreated); + + // create the SubSurfaces for surface of parent + std::unique_ptr subSurface1(m_subCompositor->createSubSurface(surface1.get(), parent.get())); + QVERIFY(subSurfaceCreatedSpy.wait()); + SubSurfaceInterface *serverSubSurface1 = subSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSubSurface1); + subSurfaceCreatedSpy.clear(); + std::unique_ptr subSurface2(m_subCompositor->createSubSurface(surface2.get(), parent.get())); + QVERIFY(subSurfaceCreatedSpy.wait()); + SubSurfaceInterface *serverSubSurface2 = subSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSubSurface2); + subSurfaceCreatedSpy.clear(); + std::unique_ptr subSurface3(m_subCompositor->createSubSurface(surface3.get(), parent.get())); + QVERIFY(subSurfaceCreatedSpy.wait()); + SubSurfaceInterface *serverSubSurface3 = subSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSubSurface3); + subSurfaceCreatedSpy.clear(); + + // so far the stacking order should still be empty + QVERIFY(serverSubSurface1->parentSurface()->below().isEmpty()); + QEXPECT_FAIL("", "Incorrect adding of child windows to workaround QtWayland behavior", Continue); + QVERIFY(serverSubSurface1->parentSurface()->above().isEmpty()); + + // committing the parent should create the stacking order + parent->commit(KWayland::Client::Surface::CommitFlag::None); + // ensure it's processed on server side + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(serverSubSurface1->parentSurface()->above().count(), 3); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(0), serverSubSurface1); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(1), serverSubSurface2); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(2), serverSubSurface3); + + // lowering subsurface3 should place it to the bottom of stack + subSurface3->lower(); + // ensure it's processed on server side + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + // but as long as parent is not committed it shouldn't change on server side + QCOMPARE(serverSubSurface1->parentSurface()->above().at(0), serverSubSurface1); + // after commit it's changed + parent->commit(KWayland::Client::Surface::CommitFlag::None); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(serverSubSurface1->parentSurface()->below().count(), 1); + QCOMPARE(serverSubSurface1->parentSurface()->below().at(0), serverSubSurface3); + QCOMPARE(serverSubSurface1->parentSurface()->above().count(), 2); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(0), serverSubSurface1); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(1), serverSubSurface2); + + // place 1 below 3 -> 1, 3, 2 + subSurface1->placeBelow(subSurface3.get()); + parent->commit(KWayland::Client::Surface::CommitFlag::None); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(serverSubSurface1->parentSurface()->below().count(), 2); + QCOMPARE(serverSubSurface1->parentSurface()->below().at(0), serverSubSurface1); + QCOMPARE(serverSubSurface1->parentSurface()->below().at(1), serverSubSurface3); + QCOMPARE(serverSubSurface1->parentSurface()->above().count(), 1); + QCOMPARE(serverSubSurface1->parentSurface()->above().at(0), serverSubSurface2); + + // 2 below 3 -> 1, 2, 3 + subSurface2->placeBelow(subSurface3.get()); + parent->commit(KWayland::Client::Surface::CommitFlag::None); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(serverSubSurface1->parentSurface()->below().count(), 3); + QCOMPARE(serverSubSurface1->parentSurface()->below().at(0), serverSubSurface1); + QCOMPARE(serverSubSurface1->parentSurface()->below().at(1), serverSubSurface2); + QCOMPARE(serverSubSurface1->parentSurface()->below().at(2), serverSubSurface3); + QCOMPARE(serverSubSurface1->parentSurface()->above().count(), 0); + + // 1 below 2 -> shouldn't change + subSurface1->placeBelow(subSurface2.get()); + parent->commit(KWayland::Client::Surface::CommitFlag::None); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(serverSubSurface1->parentSurface()->below().count(), 3); + QCOMPARE(serverSubSurface1->parentSurface()->below().at(0), serverSubSurface1); + QCOMPARE(serverSubSurface1->parentSurface()->below().at(1), serverSubSurface2); + QCOMPARE(serverSubSurface1->parentSurface()->below().at(2), serverSubSurface3); + QCOMPARE(serverSubSurface1->parentSurface()->above().count(), 0); + + // and 3 below 1 -> 3, 1, 2 + subSurface3->placeBelow(subSurface1.get()); + parent->commit(KWayland::Client::Surface::CommitFlag::None); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(serverSubSurface1->parentSurface()->below().count(), 3); + QCOMPARE(serverSubSurface1->parentSurface()->below().at(0), serverSubSurface3); + QCOMPARE(serverSubSurface1->parentSurface()->below().at(1), serverSubSurface1); + QCOMPARE(serverSubSurface1->parentSurface()->below().at(2), serverSubSurface2); + QCOMPARE(serverSubSurface1->parentSurface()->above().count(), 0); +} + +void TestSubSurface::testSyncMode() +{ + // this test verifies that state is only applied when the parent surface commits its pending state + using namespace KWin; + + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto childSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(childSurface); + + std::unique_ptr parent(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto parentSurface = surfaceCreatedSpy.last().first().value(); + QVERIFY(parentSurface); + // create subSurface for surface of parent + std::unique_ptr subSurface(m_subCompositor->createSubSurface(surface.get(), parent.get())); + + // let's damage the child surface + QSignalSpy childDamagedSpy(childSurface, &SurfaceInterface::damaged); + + QImage image(QSize(200, 200), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::black); + surface->attachBuffer(m_shm->createBuffer(image)); + surface->damage(QRect(0, 0, 200, 200)); + surface->commit(); + + // state should be applied when the parent surface's state gets applied + QVERIFY(!childDamagedSpy.wait(100)); + QVERIFY(!childSurface->buffer()); + + QVERIFY(!childSurface->isMapped()); + QVERIFY(!parentSurface->isMapped()); + + QImage image2(QSize(400, 400), QImage::Format_ARGB32_Premultiplied); + image2.fill(Qt::red); + parent->attachBuffer(m_shm->createBuffer(image2)); + parent->damage(QRect(0, 0, 400, 400)); + parent->commit(); + QVERIFY(childDamagedSpy.wait()); + QCOMPARE(childDamagedSpy.count(), 1); + QVERIFY(childSurface->isMapped()); + QVERIFY(parentSurface->isMapped()); +} + +void TestSubSurface::testDeSyncMode() +{ + // this test verifies that state gets applied immediately in desync mode + using namespace KWin; + + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto childSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(childSurface); + + std::unique_ptr parent(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto parentSurface = surfaceCreatedSpy.last().first().value(); + QVERIFY(parentSurface); + // create subSurface for surface of parent + std::unique_ptr subSurface(m_subCompositor->createSubSurface(surface.get(), parent.get())); + + // let's damage the child surface + QSignalSpy childDamagedSpy(childSurface, &SurfaceInterface::damaged); + + QImage image(QSize(200, 200), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::black); + surface->attachBuffer(m_shm->createBuffer(image)); + surface->damage(QRect(0, 0, 200, 200)); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + + // state should be applied when the parent surface's state gets applied or when the subsurface switches to desync + QVERIFY(!childDamagedSpy.wait(100)); + QVERIFY(!childSurface->isMapped()); + QVERIFY(!parentSurface->isMapped()); + + // setting to desync should apply the state directly + subSurface->setMode(KWayland::Client::SubSurface::Mode::Desynchronized); + QVERIFY(childDamagedSpy.wait()); + QVERIFY(!childSurface->isMapped()); + QVERIFY(!parentSurface->isMapped()); + + // and damaging again, should directly be applied + image.fill(Qt::red); + surface->attachBuffer(m_shm->createBuffer(image)); + surface->damage(QRect(0, 0, 200, 200)); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(childDamagedSpy.wait()); +} + +void TestSubSurface::testMainSurfaceFromTree() +{ + // this test verifies that in a tree of surfaces every surface has the same main surface + using namespace KWin; + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + + std::unique_ptr parentSurface(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto parentServerSurface = surfaceCreatedSpy.last().first().value(); + QVERIFY(parentServerSurface); + std::unique_ptr childLevel1Surface(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto childLevel1ServerSurface = surfaceCreatedSpy.last().first().value(); + QVERIFY(childLevel1ServerSurface); + std::unique_ptr childLevel2Surface(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto childLevel2ServerSurface = surfaceCreatedSpy.last().first().value(); + QVERIFY(childLevel2ServerSurface); + std::unique_ptr childLevel3Surface(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto childLevel3ServerSurface = surfaceCreatedSpy.last().first().value(); + QVERIFY(childLevel3ServerSurface); + + m_subCompositor->createSubSurface(childLevel1Surface.get(), parentSurface.get()); + m_subCompositor->createSubSurface(childLevel2Surface.get(), childLevel1Surface.get()); + m_subCompositor->createSubSurface(childLevel3Surface.get(), childLevel2Surface.get()); + + QSignalSpy parentCommittedSpy(parentServerSurface, &SurfaceInterface::committed); + parentSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(parentCommittedSpy.wait()); + + QCOMPARE(parentServerSurface->below().count(), 0); + QCOMPARE(parentServerSurface->above().count(), 1); + auto child = parentServerSurface->above().constFirst(); + QCOMPARE(child->parentSurface(), parentServerSurface); + QCOMPARE(child->mainSurface(), parentServerSurface); + QCOMPARE(child->surface()->below().count(), 0); + QCOMPARE(child->surface()->above().count(), 1); + auto child2 = child->surface()->above().constFirst(); + QCOMPARE(child2->parentSurface(), child->surface()); + QCOMPARE(child2->mainSurface(), parentServerSurface); + QCOMPARE(child2->surface()->below().count(), 0); + QCOMPARE(child2->surface()->above().count(), 1); + auto child3 = child2->surface()->above().constFirst(); + QCOMPARE(child3->parentSurface(), child2->surface()); + QCOMPARE(child3->mainSurface(), parentServerSurface); + QCOMPARE(child3->surface()->below().count(), 0); + QCOMPARE(child3->surface()->above().count(), 0); +} + +void TestSubSurface::testRemoveSurface() +{ + // this test verifies that removing the surface also removes the sub-surface from the parent + using namespace KWin; + + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + + std::unique_ptr parentSurface(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto parentServerSurface = surfaceCreatedSpy.last().first().value(); + QVERIFY(parentServerSurface); + std::unique_ptr childSurface(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto childServerSurface = surfaceCreatedSpy.last().first().value(); + QVERIFY(childServerSurface); + + QSignalSpy childrenChangedSpy(parentServerSurface, &SurfaceInterface::childSubSurfacesChanged); + + m_subCompositor->createSubSurface(childSurface.get(), parentSurface.get()); + parentSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(childrenChangedSpy.wait()); + + QCOMPARE(parentServerSurface->below().count(), 0); + QCOMPARE(parentServerSurface->above().count(), 1); + + // destroy surface, takes place immediately + childSurface.reset(); + QVERIFY(childrenChangedSpy.wait()); + QCOMPARE(parentServerSurface->below().count(), 0); + QCOMPARE(parentServerSurface->above().count(), 0); +} + +void TestSubSurface::testMappingOfSurfaceTree() +{ + // this test verifies mapping and unmapping of a sub-surface tree + using namespace KWin; + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + + std::unique_ptr parentSurface(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto parentServerSurface = surfaceCreatedSpy.last().first().value(); + QVERIFY(parentServerSurface); + std::unique_ptr childLevel1Surface(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto childLevel1ServerSurface = surfaceCreatedSpy.last().first().value(); + QVERIFY(childLevel1ServerSurface); + std::unique_ptr childLevel2Surface(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto childLevel2ServerSurface = surfaceCreatedSpy.last().first().value(); + QVERIFY(childLevel2ServerSurface); + std::unique_ptr childLevel3Surface(m_compositor->createSurface()); + QVERIFY(surfaceCreatedSpy.wait()); + auto childLevel3ServerSurface = surfaceCreatedSpy.last().first().value(); + QVERIFY(childLevel3ServerSurface); + + auto subSurfaceLevel1 = m_subCompositor->createSubSurface(childLevel1Surface.get(), parentSurface.get()); + auto subSurfaceLevel2 = m_subCompositor->createSubSurface(childLevel2Surface.get(), childLevel1Surface.get()); + auto subSurfaceLevel3 = m_subCompositor->createSubSurface(childLevel3Surface.get(), childLevel2Surface.get()); + + QSignalSpy parentCommittedSpy(parentServerSurface, &SurfaceInterface::committed); + parentSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(parentCommittedSpy.wait()); + + QCOMPARE(parentServerSurface->below().count(), 0); + QCOMPARE(parentServerSurface->above().count(), 1); + auto child = parentServerSurface->above().constFirst(); + QCOMPARE(child->surface()->below().count(), 0); + QCOMPARE(child->surface()->above().count(), 1); + auto child2 = child->surface()->above().constFirst(); + QCOMPARE(child2->surface()->below().count(), 0); + QCOMPARE(child2->surface()->above().count(), 1); + auto child3 = child2->surface()->above().constFirst(); + QCOMPARE(child3->parentSurface(), child2->surface()); + QCOMPARE(child3->mainSurface(), parentServerSurface); + QCOMPARE(child3->surface()->below().count(), 0); + QCOMPARE(child3->surface()->above().count(), 0); + + // so far no surface is mapped + QVERIFY(!parentServerSurface->isMapped()); + QVERIFY(!child->surface()->isMapped()); + QVERIFY(!child2->surface()->isMapped()); + QVERIFY(!child3->surface()->isMapped()); + + // first set all subsurfaces to desync, to simplify + subSurfaceLevel1->setMode(KWayland::Client::SubSurface::Mode::Desynchronized); + subSurfaceLevel2->setMode(KWayland::Client::SubSurface::Mode::Desynchronized); + subSurfaceLevel3->setMode(KWayland::Client::SubSurface::Mode::Desynchronized); + + // first map the child, should not map it + QSignalSpy child3DamageSpy(child3->surface(), &SurfaceInterface::damaged); + QImage image(QSize(200, 200), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::black); + childLevel3Surface->attachBuffer(m_shm->createBuffer(image)); + childLevel3Surface->damage(QRect(0, 0, 200, 200)); + childLevel3Surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(child3DamageSpy.wait()); + QVERIFY(child3->surface()->buffer()); + QVERIFY(!child3->surface()->isMapped()); + + // let's map the top level + QSignalSpy parentSpy(parentServerSurface, &SurfaceInterface::damaged); + parentSurface->attachBuffer(m_shm->createBuffer(image)); + parentSurface->damage(QRect(0, 0, 200, 200)); + parentSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(parentSpy.wait()); + QVERIFY(parentServerSurface->isMapped()); + // children should not yet be mapped + QVERIFY(!child->surface()->isMapped()); + QVERIFY(!child2->surface()->isMapped()); + QVERIFY(!child3->surface()->isMapped()); + + // next level + QSignalSpy child2DamageSpy(child2->surface(), &SurfaceInterface::damaged); + childLevel2Surface->attachBuffer(m_shm->createBuffer(image)); + childLevel2Surface->damage(QRect(0, 0, 200, 200)); + childLevel2Surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(child2DamageSpy.wait()); + QVERIFY(parentServerSurface->isMapped()); + // children should not yet be mapped + QVERIFY(!child->surface()->isMapped()); + QVERIFY(!child2->surface()->isMapped()); + QVERIFY(!child3->surface()->isMapped()); + + // last but not least the first child level, which should map all our subsurfaces + QSignalSpy child1DamageSpy(child->surface(), &SurfaceInterface::damaged); + childLevel1Surface->attachBuffer(m_shm->createBuffer(image)); + childLevel1Surface->damage(QRect(0, 0, 200, 200)); + childLevel1Surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(child1DamageSpy.wait()); + + // everything is mapped + QVERIFY(parentServerSurface->isMapped()); + QVERIFY(child->surface()->isMapped()); + QVERIFY(child2->surface()->isMapped()); + QVERIFY(child3->surface()->isMapped()); + + // unmapping a parent should unmap the complete tree + QSignalSpy unmappedSpy(child->surface(), &SurfaceInterface::unmapped); + childLevel1Surface->attachBuffer(KWayland::Client::Buffer::Ptr()); + childLevel1Surface->damage(QRect(0, 0, 200, 200)); + childLevel1Surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(unmappedSpy.wait()); + + QVERIFY(parentServerSurface->isMapped()); + QVERIFY(!child->surface()->isMapped()); + QVERIFY(!child2->surface()->isMapped()); + QVERIFY(!child3->surface()->isMapped()); +} + +void TestSubSurface::testSurfaceAt() +{ + // this test verifies that the correct surface is picked in a sub-surface tree + using namespace KWin; + // first create a parent surface and map it + QSignalSpy serverSurfaceCreated(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr parent(m_compositor->createSurface()); + QImage image(QSize(100, 100), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::red); + parent->attachBuffer(m_shm->createBuffer(image)); + parent->damage(QRect(0, 0, 100, 100)); + parent->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(serverSurfaceCreated.wait()); + SurfaceInterface *parentServerSurface = serverSurfaceCreated.last().first().value(); + + // directChild1 occupies the top-left quarter of the parent surface + QImage directImage(QSize(50, 50), QImage::Format_ARGB32_Premultiplied); + std::unique_ptr directChild1(m_compositor->createSurface()); + directChild1->attachBuffer(m_shm->createBuffer(directImage)); + directChild1->damage(QRect(0, 0, 50, 50)); + directChild1->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(serverSurfaceCreated.wait()); + SurfaceInterface *directChild1ServerSurface = serverSurfaceCreated.last().first().value(); + QVERIFY(directChild1ServerSurface); + + // directChild2 occupies the bottom-right quarter of the parent surface + std::unique_ptr directChild2(m_compositor->createSurface()); + directChild2->attachBuffer(m_shm->createBuffer(directImage)); + directChild2->damage(QRect(0, 0, 50, 50)); + directChild2->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(serverSurfaceCreated.wait()); + SurfaceInterface *directChild2ServerSurface = serverSurfaceCreated.last().first().value(); + QVERIFY(directChild2ServerSurface); + + // create the sub surfaces for them + std::unique_ptr directChild1SubSurface(m_subCompositor->createSubSurface(directChild1.get(), parent.get())); + directChild1SubSurface->setMode(KWayland::Client::SubSurface::Mode::Desynchronized); + std::unique_ptr directChild2SubSurface(m_subCompositor->createSubSurface(directChild2.get(), parent.get())); + directChild2SubSurface->setMode(KWayland::Client::SubSurface::Mode::Desynchronized); + directChild2SubSurface->setPosition(QPoint(50, 50)); + + // unset input regions for direct children + QSignalSpy directChild1CommittedSpy(directChild1ServerSurface, &SurfaceInterface::committed); + directChild1->setInputRegion(m_compositor->createRegion(QRegion()).get()); + directChild1->commit(KWayland::Client::Surface::CommitFlag::None); + parent->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(directChild1CommittedSpy.wait()); + + QSignalSpy directChild2CommittedSpy(directChild2ServerSurface, &SurfaceInterface::committed); + directChild2->setInputRegion(m_compositor->createRegion(QRegion()).get()); + directChild2->commit(KWayland::Client::Surface::CommitFlag::None); + parent->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(directChild2CommittedSpy.wait()); + + // each of the children gets a child + std::unique_ptr childFor1(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + SurfaceInterface *childFor1ServerSurface = serverSurfaceCreated.last().first().value(); + std::unique_ptr childFor2(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + SurfaceInterface *childFor2ServerSurface = serverSurfaceCreated.last().first().value(); + + // create sub surfaces for them + std::unique_ptr childFor1SubSurface(m_subCompositor->createSubSurface(childFor1.get(), directChild1.get())); + childFor1SubSurface->setMode(KWayland::Client::SubSurface::Mode::Desynchronized); + std::unique_ptr childFor2SubSurface(m_subCompositor->createSubSurface(childFor2.get(), directChild2.get())); + childFor2SubSurface->setMode(KWayland::Client::SubSurface::Mode::Desynchronized); + + // now let's render both grand children + QImage partImage(QSize(50, 50), QImage::Format_ARGB32_Premultiplied); + partImage.fill(Qt::green); + childFor1->attachBuffer(m_shm->createBuffer(partImage)); + childFor1->damage(QRect(0, 0, 50, 50)); + childFor1->commit(KWayland::Client::Surface::CommitFlag::None); + partImage.fill(Qt::blue); + + QSignalSpy childFor2CommittedSpy(childFor2ServerSurface, &SurfaceInterface::committed); + childFor2->attachBuffer(m_shm->createBuffer(partImage)); + // child for 2's input region is subdivided into quadrants, with input mask on the top left and bottom right + QRegion region; + region += QRect(0, 0, 25, 25); + region += QRect(25, 25, 25, 25); + childFor2->setInputRegion(m_compositor->createRegion(region).get()); + childFor2->damage(QRect(0, 0, 50, 50)); + childFor2->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(childFor2CommittedSpy.wait()); + + QCOMPARE(directChild1ServerSurface->subSurface()->parentSurface(), parentServerSurface); + QCOMPARE(directChild2ServerSurface->subSurface()->parentSurface(), parentServerSurface); + QCOMPARE(childFor1ServerSurface->subSurface()->parentSurface(), directChild1ServerSurface); + QCOMPARE(childFor2ServerSurface->subSurface()->parentSurface(), directChild2ServerSurface); + + // now let's test a few positions + QCOMPARE(parentServerSurface->surfaceAt(QPointF(0, 0)), childFor1ServerSurface); + QCOMPARE(parentServerSurface->surfaceAt(QPointF(49, 49)), childFor1ServerSurface); + QCOMPARE(parentServerSurface->surfaceAt(QPointF(50, 50)), childFor2ServerSurface); + QCOMPARE(parentServerSurface->surfaceAt(QPointF(99, 99)), childFor2ServerSurface); + QCOMPARE(parentServerSurface->surfaceAt(QPointF(99, 50)), childFor2ServerSurface); + QCOMPARE(parentServerSurface->surfaceAt(QPointF(50, 99)), childFor2ServerSurface); + QCOMPARE(parentServerSurface->surfaceAt(QPointF(25, 75)), parentServerSurface); + QCOMPARE(parentServerSurface->surfaceAt(QPointF(75, 25)), parentServerSurface); + + QCOMPARE(parentServerSurface->inputSurfaceAt(QPointF(0, 0)), childFor1ServerSurface); + QCOMPARE(parentServerSurface->inputSurfaceAt(QPointF(49, 49)), childFor1ServerSurface); + QCOMPARE(parentServerSurface->inputSurfaceAt(QPointF(50, 50)), childFor2ServerSurface); + QCOMPARE(parentServerSurface->inputSurfaceAt(QPointF(99, 99)), childFor2ServerSurface); + QCOMPARE(parentServerSurface->inputSurfaceAt(QPointF(99, 50)), parentServerSurface); + QCOMPARE(parentServerSurface->inputSurfaceAt(QPointF(50, 99)), parentServerSurface); + QCOMPARE(parentServerSurface->inputSurfaceAt(QPointF(25, 75)), parentServerSurface); + QCOMPARE(parentServerSurface->inputSurfaceAt(QPointF(75, 25)), parentServerSurface); + + // outside the geometries should be no surface + QVERIFY(!parentServerSurface->surfaceAt(QPointF(-1, -1))); + QVERIFY(!parentServerSurface->surfaceAt(QPointF(101, 101))); + + // on the surface edge right/bottom edges should not trigger as contained + QCOMPARE(parentServerSurface->surfaceAt(QPointF(50, 25)), parentServerSurface); + QCOMPARE(parentServerSurface->surfaceAt(QPointF(25, 50)), parentServerSurface); + QCOMPARE(parentServerSurface->inputSurfaceAt(QPointF(50, 25)), parentServerSurface); + QCOMPARE(parentServerSurface->inputSurfaceAt(QPointF(25, 50)), parentServerSurface); +} + +void TestSubSurface::testDestroyAttachedBuffer() +{ + // this test verifies that destroying of a buffer attached to a sub-surface works + using namespace KWin; + // create surface + QSignalSpy serverSurfaceCreated(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr parent(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + std::unique_ptr child(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + SurfaceInterface *serverChildSurface = serverSurfaceCreated.last().first().value(); + // create sub-surface + m_subCompositor->createSubSurface(child.get(), parent.get()); + + // let's damage this surface, will be in sub-surface pending state + QImage image(QSize(100, 100), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::red); + child->attachBuffer(m_shm->createBuffer(image)); + child->damage(QRect(0, 0, 100, 100)); + child->commit(KWayland::Client::Surface::CommitFlag::None); + m_connection->flush(); + + // Let's try to destroy it + QSignalSpy destroySpy(serverChildSurface, &QObject::destroyed); + delete m_shm; + m_shm = nullptr; + child.reset(); + QVERIFY(destroySpy.wait()); +} + +void TestSubSurface::testDestroyParentSurface() +{ + // this test verifies that destroying a parent surface does not create problems + // see BUG 389231 + using namespace KWin; + // create surface + QSignalSpy serverSurfaceCreated(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr parent(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + SurfaceInterface *serverParentSurface = serverSurfaceCreated.last().first().value(); + std::unique_ptr child(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + SurfaceInterface *serverChildSurface = serverSurfaceCreated.last().first().value(); + std::unique_ptr grandChild(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + SurfaceInterface *serverGrandChildSurface = serverSurfaceCreated.last().first().value(); + // create sub-surface in desynchronized mode as Qt uses them + auto sub1 = m_subCompositor->createSubSurface(child.get(), parent.get()); + sub1->setMode(KWayland::Client::SubSurface::Mode::Desynchronized); + auto sub2 = m_subCompositor->createSubSurface(grandChild.get(), child.get()); + sub2->setMode(KWayland::Client::SubSurface::Mode::Desynchronized); + + // let's damage this surface + // and at the same time delete the parent surface + parent.reset(); + QSignalSpy parentDestroyedSpy(serverParentSurface, &QObject::destroyed); + QVERIFY(parentDestroyedSpy.wait()); + QImage image(QSize(100, 100), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::red); + grandChild->attachBuffer(m_shm->createBuffer(image)); + grandChild->damage(QRect(0, 0, 100, 100)); + grandChild->commit(KWayland::Client::Surface::CommitFlag::None); + QSignalSpy damagedSpy(serverGrandChildSurface, &SurfaceInterface::damaged); + QVERIFY(damagedSpy.wait()); + + // Let's try to destroy it + QSignalSpy destroySpy(serverChildSurface, &QObject::destroyed); + child.reset(); + QVERIFY(destroySpy.wait()); +} + +void TestSubSurface::testDestroyCommittedSubSurfaceBeforeParent() +{ + // this test verifies that no crash occurs when a committed synchronized subsurface is destroyed + // before the parent is committed + + using namespace KWin; + + QSignalSpy serverSurfaceCreated(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr parent(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + SurfaceInterface *serverParentSurface = serverSurfaceCreated.last().first().value(); + + std::unique_ptr child(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + SurfaceInterface *serverChildSurface = serverSurfaceCreated.last().first().value(); + + std::unique_ptr grandChild(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + + auto firstSubSurface = m_subCompositor->createSubSurface(child.get(), parent.get()); + firstSubSurface->setMode(KWayland::Client::SubSurface::Mode::Synchronized); + auto secondSubSurface = m_subCompositor->createSubSurface(grandChild.get(), child.get()); + secondSubSurface->setMode(KWayland::Client::SubSurface::Mode::Synchronized); + + { + QImage image(QSize(100, 100), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::red); + grandChild->attachBuffer(m_shm->createBuffer(image)); + grandChild->damage(QRect(0, 0, 100, 100)); + grandChild->commit(KWayland::Client::Surface::CommitFlag::None); + } + + { + QImage image(QSize(75, 75), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::red); + child->attachBuffer(m_shm->createBuffer(image)); + child->damage(QRect(0, 0, 75, 75)); + child->commit(KWayland::Client::Surface::CommitFlag::None); + } + + { + delete secondSubSurface; + grandChild.reset(); + } + + { + QImage image(QSize(60, 60), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::red); + parent->attachBuffer(m_shm->createBuffer(image)); + parent->damage(QRect(0, 0, 60, 60)); + parent->commit(KWayland::Client::Surface::CommitFlag::None); + } + + QSignalSpy parentCommittedSpy(serverParentSurface, &SurfaceInterface::committed); + QVERIFY(parentCommittedSpy.wait()); + + QCOMPARE(serverParentSurface->size(), QSize(60, 60)); + QCOMPARE(serverChildSurface->size(), QSize(75, 75)); +} + +QTEST_GUILESS_MAIN(TestSubSurface) +#include "test_wayland_subsurface.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_surface.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_surface.cpp new file mode 100644 index 0000000000..b205e93476 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_surface.cpp @@ -0,0 +1,946 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +#include +#include +// KWin +#include "core/graphicsbuffer.h" +#include "core/graphicsbufferview.h" +#include "core/region.h" +#include "wayland/clientconnection.h" +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/idleinhibit_v1.h" +#include "wayland/output.h" +#include "wayland/surface.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/idleinhibit.h" +#include "KWayland/Client/output.h" +#include "KWayland/Client/region.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/shm_pool.h" +#include "KWayland/Client/surface.h" + +#include "../../../tests/fakeoutput.h" + +// Wayland +#include + +class TestWaylandSurface : public QObject +{ + Q_OBJECT +public: + explicit TestWaylandSurface(QObject *parent = nullptr); +private Q_SLOTS: + void init(); + void cleanup(); + + void testStaticAccessor(); + void testDamage(); + void testFrameCallback(); + void testAttachBuffer(); + void testOpaque(); + void testInput(); + void testScale(); + void testUnmapOfNotMappedSurface(); + void testSurfaceAt(); + void testDestroyAttachedBuffer(); + void testDestroyWithPendingCallback(); + void testOutput(); + void testDisconnect(); + void testInhibit(); + +private: + KWin::Display *m_display; + KWin::CompositorInterface *m_compositorInterface; + KWin::IdleInhibitManagerV1Interface *m_idleInhibitInterface; + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::Compositor *m_compositor; + KWayland::Client::ShmPool *m_shm; + KWayland::Client::EventQueue *m_queue; + KWayland::Client::IdleInhibitManager *m_idleInhibitManager; + QThread *m_thread; +}; + +static const QString s_socketName = QStringLiteral("kwin-test-wayland-surface-0"); + +TestWaylandSurface::TestWaylandSurface(QObject *parent) + : QObject(parent) + , m_display(nullptr) + , m_compositorInterface(nullptr) + , m_connection(nullptr) + , m_compositor(nullptr) + , m_thread(nullptr) +{ +} + +void TestWaylandSurface::init() +{ + using namespace KWin; + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + m_display->createShm(); + + m_compositorInterface = new CompositorInterface(m_display, m_display); + QVERIFY(m_compositorInterface); + + m_idleInhibitInterface = new IdleInhibitManagerV1Interface(m_display, m_display); + QVERIFY(m_idleInhibitInterface); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + /*connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::aboutToBlock, m_connection, + [this]() { + if (m_connection->display()) { + wl_display_flush(m_connection->display()); + } + } + );*/ + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + KWayland::Client::Registry registry; + registry.setEventQueue(m_queue); + QSignalSpy compositorSpy(®istry, &KWayland::Client::Registry::compositorAnnounced); + QSignalSpy shmSpy(®istry, &KWayland::Client::Registry::shmAnnounced); + QSignalSpy allAnnounced(®istry, &KWayland::Client::Registry::interfacesAnnounced); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + QVERIFY(allAnnounced.wait()); + QVERIFY(!compositorSpy.isEmpty()); + QVERIFY(!shmSpy.isEmpty()); + + m_compositor = registry.createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + QVERIFY(m_compositor->isValid()); + m_shm = registry.createShmPool(shmSpy.first().first().value(), shmSpy.first().last().value(), this); + QVERIFY(m_shm->isValid()); + + m_idleInhibitManager = registry.createIdleInhibitManager(registry.interface(KWayland::Client::Registry::Interface::IdleInhibitManagerUnstableV1).name, + registry.interface(KWayland::Client::Registry::Interface::IdleInhibitManagerUnstableV1).version, + this); + QVERIFY(m_idleInhibitManager->isValid()); +} + +void TestWaylandSurface::cleanup() +{ + if (m_compositor) { + delete m_compositor; + m_compositor = nullptr; + } + if (m_idleInhibitManager) { + delete m_idleInhibitManager; + m_idleInhibitManager = nullptr; + } + if (m_shm) { + delete m_shm; + m_shm = nullptr; + } + if (m_queue) { + delete m_queue; + m_queue = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + delete m_connection; + m_connection = nullptr; + + delete m_display; + m_display = nullptr; + + // these are the children of the display + m_compositorInterface = nullptr; + m_idleInhibitInterface = nullptr; +} + +void TestWaylandSurface::testStaticAccessor() +{ + QSignalSpy serverSurfaceCreated(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + + QVERIFY(!KWin::SurfaceInterface::get(nullptr)); + QVERIFY(KWayland::Client::Surface::all().isEmpty()); + std::unique_ptr s1(m_compositor->createSurface()); + QVERIFY(s1->isValid()); + QCOMPARE(KWayland::Client::Surface::all().count(), 1); + QCOMPARE(KWayland::Client::Surface::all().first(), s1.get()); + QCOMPARE(KWayland::Client::Surface::get(*s1), s1.get()); + QVERIFY(serverSurfaceCreated.wait()); + auto serverSurface1 = serverSurfaceCreated.first().first().value(); + QVERIFY(serverSurface1); + QCOMPARE(KWin::SurfaceInterface::get(serverSurface1->resource()), serverSurface1); + + QVERIFY(!s1->size().isValid()); + QSignalSpy sizeChangedSpy(s1.get(), &KWayland::Client::Surface::sizeChanged); + const QSize testSize(200, 300); + s1->setSize(testSize); + QCOMPARE(s1->size(), testSize); + QCOMPARE(sizeChangedSpy.count(), 1); + QCOMPARE(sizeChangedSpy.first().first().toSize(), testSize); + + // add another surface + std::unique_ptr s2(m_compositor->createSurface()); + QVERIFY(s2->isValid()); + QCOMPARE(KWayland::Client::Surface::all().count(), 2); + QCOMPARE(KWayland::Client::Surface::all().first(), s1.get()); + QCOMPARE(KWayland::Client::Surface::all().last(), s2.get()); + QCOMPARE(KWayland::Client::Surface::get(*s1), s1.get()); + QCOMPARE(KWayland::Client::Surface::get(*s2), s2.get()); + serverSurfaceCreated.clear(); + QVERIFY(serverSurfaceCreated.wait()); + auto serverSurface2 = serverSurfaceCreated.first().first().value(); + QVERIFY(serverSurface2); + QCOMPARE(KWin::SurfaceInterface::get(serverSurface1->resource()), serverSurface1); + QCOMPARE(KWin::SurfaceInterface::get(serverSurface2->resource()), serverSurface2); + + // delete s2 again + s2.reset(); + QCOMPARE(KWayland::Client::Surface::all().count(), 1); + QCOMPARE(KWayland::Client::Surface::all().first(), s1.get()); + QCOMPARE(KWayland::Client::Surface::get(*s1), s1.get()); + + // and finally delete the last one + s1.reset(); + QVERIFY(KWayland::Client::Surface::all().isEmpty()); + QVERIFY(!KWayland::Client::Surface::get(nullptr)); + QSignalSpy destroyedSpy(serverSurface1, &KWin::SurfaceInterface::destroyed); + QVERIFY(destroyedSpy.wait()); + QVERIFY(!KWin::SurfaceInterface::get(nullptr)); +} + +void TestWaylandSurface::testDamage() +{ + QSignalSpy serverSurfaceCreated(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + std::unique_ptr s(m_compositor->createSurface()); + s->setScale(2); + QVERIFY(serverSurfaceCreated.wait()); + KWin::SurfaceInterface *serverSurface = serverSurfaceCreated.first().first().value(); + QVERIFY(serverSurface); + QCOMPARE(serverSurface->bufferDamage(), KWin::Region()); + QVERIFY(!serverSurface->isMapped()); + + QSignalSpy committedSpy(serverSurface, &KWin::SurfaceInterface::committed); + QSignalSpy damageSpy(serverSurface, &KWin::SurfaceInterface::damaged); + + // send damage without a buffer + { + s->damage(QRect(0, 0, 100, 100)); + s->commit(KWayland::Client::Surface::CommitFlag::None); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCoreApplication::processEvents(); + QVERIFY(damageSpy.isEmpty()); + QVERIFY(!serverSurface->isMapped()); + QCOMPARE(committedSpy.count(), 1); + } + + // surface damage + { + QImage img(QSize(10, 10), QImage::Format_ARGB32_Premultiplied); + img.fill(Qt::black); + auto b = m_shm->createBuffer(img); + s->attachBuffer(b, QPoint(55, 55)); + s->damage(QRect(0, 0, 10, 10)); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(damageSpy.wait()); + QCOMPARE(serverSurface->offset(), QPoint(55, 55)); // offset is surface local so scale doesn't change this + QCOMPARE(serverSurface->bufferDamage(), KWin::Region(0, 0, 10, 10)); + QCOMPARE(damageSpy.first().first().value(), KWin::Region(0, 0, 10, 10)); + QVERIFY(serverSurface->isMapped()); + QCOMPARE(committedSpy.count(), 2); + } + + // damage multiple times + { + const QRegion surfaceDamage = QRegion(5, 8, 3, 6).united(QRect(10, 11, 6, 1)); + const KWin::Region expectedDamage = KWin::Region(10, 16, 6, 12).united(QRect(20, 22, 12, 2)); + QImage img(QSize(80, 70), QImage::Format_ARGB32_Premultiplied); + img.fill(Qt::black); + auto b = m_shm->createBuffer(img); + s->attachBuffer(b); + s->damage(surfaceDamage); + damageSpy.clear(); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(damageSpy.wait()); + QCOMPARE(serverSurface->bufferDamage(), expectedDamage); + QCOMPARE(damageSpy.first().first().value(), expectedDamage); + QVERIFY(serverSurface->isMapped()); + QCOMPARE(committedSpy.count(), 3); + } + + // damage buffer + { + const QRegion damage(30, 40, 22, 4); + const KWin::Region expectedDamage(30, 40, 22, 4); + QImage img(QSize(80, 70), QImage::Format_ARGB32_Premultiplied); + img.fill(Qt::black); + auto b = m_shm->createBuffer(img); + s->attachBuffer(b); + s->damageBuffer(damage); + damageSpy.clear(); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(damageSpy.wait()); + QCOMPARE(serverSurface->bufferDamage(), expectedDamage); + QCOMPARE(damageSpy.first().first().value(), expectedDamage); + QVERIFY(serverSurface->isMapped()); + } + + // combined regular damage and damaged buffer + { + const QRegion surfaceDamage(10, 20, 5, 5); + const QRegion bufferDamage(30, 50, 50, 20); + const KWin::Region expectedDamage = KWin::Region(20, 40, 10, 10).united(QRect(30, 50, 50, 20)); + QImage img(QSize(80, 70), QImage::Format_ARGB32_Premultiplied); + img.fill(Qt::black); + auto b = m_shm->createBuffer(img); + s->attachBuffer(b); + s->damage(surfaceDamage); + s->damageBuffer(bufferDamage); + damageSpy.clear(); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(damageSpy.wait()); + QCOMPARE(serverSurface->bufferDamage(), expectedDamage); + QCOMPARE(damageSpy.first().first().value(), expectedDamage); + QVERIFY(serverSurface->isMapped()); + } +} + +void TestWaylandSurface::testFrameCallback() +{ + QSignalSpy serverSurfaceCreated(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + std::unique_ptr s(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + KWin::SurfaceInterface *serverSurface = serverSurfaceCreated.first().first().value(); + QVERIFY(serverSurface); + + QSignalSpy damageSpy(serverSurface, &KWin::SurfaceInterface::damaged); + + QSignalSpy frameRenderedSpy(s.get(), &KWayland::Client::Surface::frameRendered); + QImage img(QSize(10, 10), QImage::Format_ARGB32_Premultiplied); + img.fill(Qt::black); + auto b = m_shm->createBuffer(img); + s->attachBuffer(b); + s->damage(QRect(0, 0, 10, 10)); + s->commit(); + QVERIFY(damageSpy.wait()); + serverSurface->frameRendered(10); + QVERIFY(frameRenderedSpy.isEmpty()); + QVERIFY(frameRenderedSpy.wait()); + QVERIFY(!frameRenderedSpy.isEmpty()); +} + +void TestWaylandSurface::testAttachBuffer() +{ + // create the surface + QSignalSpy serverSurfaceCreated(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + std::unique_ptr s(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + KWin::SurfaceInterface *serverSurface = serverSurfaceCreated.first().first().value(); + QVERIFY(serverSurface); + + // create three images + QImage black(24, 24, QImage::Format_RGB32); + black.fill(Qt::black); + QImage red(24, 24, QImage::Format_ARGB32); // Note - deliberately not premultiplied + red.fill(QColor(255, 0, 0, 128)); + QImage blue(24, 24, QImage::Format_ARGB32_Premultiplied); + blue.fill(QColor(0, 0, 255, 128)); + + QSharedPointer blackBufferPtr = m_shm->createBuffer(black).toStrongRef(); + QVERIFY(blackBufferPtr); + wl_buffer *blackBuffer = *(blackBufferPtr.data()); + QSharedPointer redBuffer = m_shm->createBuffer(red).toStrongRef(); + QVERIFY(redBuffer); + QSharedPointer blueBuffer = m_shm->createBuffer(blue).toStrongRef(); + QVERIFY(blueBuffer); + + QCOMPARE(blueBuffer->format(), KWayland::Client::Buffer::Format::ARGB32); + QCOMPARE(blueBuffer->size(), blue.size()); + QVERIFY(!blueBuffer->isReleased()); + QVERIFY(!blueBuffer->isUsed()); + QCOMPARE(blueBuffer->stride(), blue.bytesPerLine()); + + s->attachBuffer(redBuffer.data()); + s->attachBuffer(blackBuffer); + s->damage(QRect(0, 0, 24, 24)); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QSignalSpy damageSpy(serverSurface, &KWin::SurfaceInterface::damaged); + QSignalSpy mappedSpy(serverSurface, &KWin::SurfaceInterface::mapped); + QSignalSpy unmappedSpy(serverSurface, &KWin::SurfaceInterface::unmapped); + QVERIFY(damageSpy.wait()); + QCOMPARE(mappedSpy.count(), 1); + QVERIFY(unmappedSpy.isEmpty()); + + // now the ServerSurface should have the black image attached as a buffer + KWin::GraphicsBuffer *buffer = serverSurface->buffer(); + buffer->ref(); + { + KWin::GraphicsBufferView view(buffer); + QVERIFY(view.image()); + QCOMPARE(*view.image(), black); + QCOMPARE(view.image()->format(), QImage::Format_RGB32); + QCOMPARE(view.image()->size(), QSize(24, 24)); + } + + // render another frame + s->attachBuffer(redBuffer); + s->damage(QRect(0, 0, 24, 24)); + s->commit(KWayland::Client::Surface::CommitFlag::None); + damageSpy.clear(); + QVERIFY(damageSpy.wait()); + QCOMPARE(mappedSpy.count(), 1); + QVERIFY(unmappedSpy.isEmpty()); + KWin::GraphicsBuffer *buffer2 = serverSurface->buffer(); + buffer2->ref(); + { + KWin::GraphicsBufferView view(buffer2); + QVERIFY(view.image()); + QCOMPARE(view.image()->format(), QImage::Format_ARGB32_Premultiplied); + QCOMPARE(view.image()->size(), QSize(24, 24)); + for (int i = 0; i < 24; ++i) { + for (int j = 0; j < 24; ++j) { + // it's premultiplied in the format + QCOMPARE(view.image()->pixel(i, j), qRgba(128, 0, 0, 128)); + } + } + } + buffer2->unref(); + QVERIFY(buffer2->isReferenced()); + QVERIFY(!redBuffer.data()->isReleased()); + + // render another frame + blueBuffer->setUsed(true); + QVERIFY(blueBuffer->isUsed()); + s->attachBuffer(blueBuffer.data()); + s->damage(QRect(0, 0, 24, 24)); + QSignalSpy frameRenderedSpy(s.get(), &KWayland::Client::Surface::frameRendered); + s->commit(); + damageSpy.clear(); + QVERIFY(damageSpy.wait()); + QCOMPARE(mappedSpy.count(), 1); + QVERIFY(unmappedSpy.isEmpty()); + QVERIFY(!buffer2->isReferenced()); + // TODO: we should have a signal on when the Buffer gets released + QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents); + if (!redBuffer.data()->isReleased()) { + QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents); + } + QVERIFY(redBuffer.data()->isReleased()); + + KWin::GraphicsBuffer *buffer3 = serverSurface->buffer(); + buffer3->ref(); + { + KWin::GraphicsBufferView view(buffer3); + QVERIFY(view.image()); + QCOMPARE(view.image()->format(), QImage::Format_ARGB32_Premultiplied); + QCOMPARE(view.image()->size(), QSize(24, 24)); + for (int i = 0; i < 24; ++i) { + for (int j = 0; j < 24; ++j) { + // it's premultiplied in the format + QCOMPARE(view.image()->pixel(i, j), qRgba(0, 0, 128, 128)); + } + } + } + buffer3->unref(); + QVERIFY(buffer3->isReferenced()); + + serverSurface->frameRendered(1); + QVERIFY(frameRenderedSpy.wait()); + + // commit a different value shouldn't change our buffer + QCOMPARE(serverSurface->buffer(), buffer3); + damageSpy.clear(); + s->setInputRegion(m_compositor->createRegion(QRegion(0, 0, 24, 24)).get()); + s->commit(KWayland::Client::Surface::CommitFlag::None); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCoreApplication::processEvents(); + QCOMPARE(serverSurface->buffer(), buffer3); + QVERIFY(damageSpy.isEmpty()); + QCOMPARE(mappedSpy.count(), 1); + QVERIFY(unmappedSpy.isEmpty()); + QVERIFY(serverSurface->isMapped()); + + // clear the surface + s->attachBuffer(blackBuffer); + s->damage(QRect(0, 0, 1, 1)); + // TODO: better method + s->attachBuffer((wl_buffer *)nullptr); + s->damage(QRect(0, 0, 10, 10)); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(unmappedSpy.wait()); + QCOMPARE(mappedSpy.count(), 1); + QCOMPARE(unmappedSpy.count(), 1); + QVERIFY(damageSpy.isEmpty()); + QVERIFY(!serverSurface->isMapped()); + + // TODO: add signal test on release + buffer->unref(); +} + +void TestWaylandSurface::testOpaque() +{ + using namespace KWin; + QSignalSpy serverSurfaceCreated(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + std::unique_ptr s(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreated.first().first().value(); + QVERIFY(serverSurface); + QSignalSpy opaqueRegionChangedSpy(serverSurface, &KWin::SurfaceInterface::opaqueChanged); + + // by default there should be an empty opaque region + QCOMPARE(serverSurface->opaque(), Region()); + + // let's install an opaque region + s->setOpaqueRegion(m_compositor->createRegion(QRegion(0, 10, 20, 30)).get()); + // the region should only be applied after the surface got committed + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(serverSurface->opaque(), Region()); + QCOMPARE(opaqueRegionChangedSpy.count(), 0); + + // so let's commit to get the new region + QImage black(20, 40, QImage::Format_ARGB32_Premultiplied); + black.fill(Qt::black); + QSharedPointer buffer1 = m_shm->createBuffer(black).toStrongRef(); + s->attachBuffer(buffer1); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(opaqueRegionChangedSpy.wait()); + QCOMPARE(opaqueRegionChangedSpy.count(), 1); + QCOMPARE(opaqueRegionChangedSpy.last().first().value(), KWin::Region(0, 10, 20, 30)); + QCOMPARE(serverSurface->opaque(), KWin::Region(0, 10, 20, 30)); + + // committing without setting a new region shouldn't change + s->commit(KWayland::Client::Surface::CommitFlag::None); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(opaqueRegionChangedSpy.count(), 1); + QCOMPARE(serverSurface->opaque(), KWin::Region(0, 10, 20, 30)); + + // let's change the opaque region, it will be clipped with rect(0, 0, 20, 40) + s->setOpaqueRegion(m_compositor->createRegion(QRegion(10, 20, 30, 40)).get()); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(opaqueRegionChangedSpy.wait()); + QCOMPARE(opaqueRegionChangedSpy.count(), 2); + QCOMPARE(opaqueRegionChangedSpy.last().first().value(), KWin::Region(10, 20, 10, 20)); + QCOMPARE(serverSurface->opaque(), KWin::Region(10, 20, 10, 20)); + + // and let's go back to an empty region + s->setOpaqueRegion(); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(opaqueRegionChangedSpy.wait()); + QCOMPARE(opaqueRegionChangedSpy.count(), 3); + QCOMPARE(opaqueRegionChangedSpy.last().first().value(), KWin::Region()); + QCOMPARE(serverSurface->opaque(), KWin::Region()); +} + +void TestWaylandSurface::testInput() +{ + using namespace KWin; + QSignalSpy serverSurfaceCreated(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + std::unique_ptr s(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreated.first().first().value(); + QVERIFY(serverSurface); + QSignalSpy inputRegionChangedSpy(serverSurface, &KWin::SurfaceInterface::inputChanged); + QSignalSpy committedSpy(serverSurface, &SurfaceInterface::committed); + + // the input region should be empty if the surface has no buffer + QVERIFY(!serverSurface->isMapped()); + QCOMPARE(serverSurface->input(), KWin::Region()); + + // the default input region is infinite + QImage black(100, 50, QImage::Format_RGB32); + black.fill(Qt::black); + QSharedPointer buffer1 = m_shm->createBuffer(black).toStrongRef(); + QVERIFY(buffer1); + s->attachBuffer(buffer1); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(committedSpy.wait()); + QVERIFY(serverSurface->isMapped()); + QCOMPARE(inputRegionChangedSpy.count(), 1); + QCOMPARE(serverSurface->input(), KWin::Region(0, 0, 100, 50)); + + // let's install an input region + s->setInputRegion(m_compositor->createRegion(QRegion(0, 10, 20, 30)).get()); + // the region should only be applied after the surface got committed + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(inputRegionChangedSpy.count(), 1); + QCOMPARE(serverSurface->input(), KWin::Region(0, 0, 100, 50)); + + // so let's commit to get the new region + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(committedSpy.wait()); + QCOMPARE(inputRegionChangedSpy.count(), 2); + QCOMPARE(serverSurface->input(), KWin::Region(0, 10, 20, 30)); + + // committing without setting a new region shouldn't change + s->commit(KWayland::Client::Surface::CommitFlag::None); + wl_display_flush(m_connection->display()); + QCoreApplication::processEvents(); + QCOMPARE(inputRegionChangedSpy.count(), 2); + QCOMPARE(serverSurface->input(), KWin::Region(0, 10, 20, 30)); + + // let's change the input region, note that the new input region is cropped + s->setInputRegion(m_compositor->createRegion(QRegion(10, 20, 30, 40)).get()); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(committedSpy.wait()); + QCOMPARE(inputRegionChangedSpy.count(), 3); + QCOMPARE(serverSurface->input(), KWin::Region(10, 20, 30, 30)); + + // and let's go back to an empty region + s->setInputRegion(); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(committedSpy.wait()); + QCOMPARE(inputRegionChangedSpy.count(), 4); + QCOMPARE(serverSurface->input(), KWin::Region(0, 0, 100, 50)); +} + +void TestWaylandSurface::testScale() +{ + // this test verifies that updating the scale factor is correctly passed to the Wayland server + using namespace KWin; + // create surface + QSignalSpy serverSurfaceCreated(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr s(m_compositor->createSurface()); + QCOMPARE(s->scale(), 1); + QVERIFY(serverSurfaceCreated.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreated.first().first().value(); + QVERIFY(serverSurface); + + // changing the scale implicitly changes the size + QSignalSpy sizeChangedSpy(serverSurface, &SurfaceInterface::sizeChanged); + + // attach a buffer of 100x100 + QImage red(100, 100, QImage::Format_ARGB32_Premultiplied); + red.fill(QColor(255, 0, 0, 128)); + QSharedPointer redBuffer = m_shm->createBuffer(red).toStrongRef(); + QVERIFY(redBuffer); + s->attachBuffer(redBuffer.data()); + s->damageBuffer(QRect(0, 0, 100, 100)); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(sizeChangedSpy.wait()); + QCOMPARE(sizeChangedSpy.count(), 1); + QCOMPARE(serverSurface->size(), QSize(100, 100)); + + // set the scale to 2, buffer is still 100x100 so size should change to 50x50 + s->setScale(2); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(sizeChangedSpy.wait()); + QCOMPARE(sizeChangedSpy.count(), 2); + QCOMPARE(serverSurface->size(), QSize(50, 50)); + + // set scale and size in one commit, buffer is 60x60 at scale 3 so size should be 20x20 + QImage blue(60, 60, QImage::Format_ARGB32_Premultiplied); + red.fill(QColor(255, 0, 0, 128)); + QSharedPointer blueBuffer = m_shm->createBuffer(blue).toStrongRef(); + QVERIFY(blueBuffer); + s->attachBuffer(blueBuffer.data()); + s->setScale(3); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(sizeChangedSpy.wait()); + QCOMPARE(sizeChangedSpy.count(), 3); + QCOMPARE(serverSurface->size(), QSize(20, 20)); +} + +void TestWaylandSurface::testUnmapOfNotMappedSurface() +{ + // this test verifies that a surface which doesn't have a buffer attached doesn't trigger the unmapped signal + using namespace KWin; + // create surface + QSignalSpy serverSurfaceCreated(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr s(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreated.first().first().value(); + + QSignalSpy unmappedSpy(serverSurface, &SurfaceInterface::unmapped); + QSignalSpy committedSpy(serverSurface, &SurfaceInterface::committed); + + // let's map a null buffer and change scale to trigger a signal we can wait for + s->attachBuffer(KWayland::Client::Buffer::Ptr()); + s->commit(KWayland::Client::Surface::CommitFlag::None); + + QVERIFY(committedSpy.wait()); + QVERIFY(unmappedSpy.isEmpty()); +} + +void TestWaylandSurface::testSurfaceAt() +{ + // this test verifies that surfaceAt(const QPointF&) works as expected for the case of no children + using namespace KWin; + // create surface + QSignalSpy serverSurfaceCreated(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr s(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreated.first().first().value(); + + // a newly created surface should not be mapped and not provide a surface at a position + QVERIFY(!serverSurface->isMapped()); + QVERIFY(!serverSurface->surfaceAt(QPointF(0, 0))); + + // let's damage this surface + QSignalSpy sizeChangedSpy(serverSurface, &SurfaceInterface::sizeChanged); + QImage image(QSize(100, 100), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::red); + s->attachBuffer(m_shm->createBuffer(image)); + s->damage(QRect(0, 0, 100, 100)); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(sizeChangedSpy.wait()); + + // now the surface is mapped and surfaceAt should give the surface + QVERIFY(serverSurface->isMapped()); + QCOMPARE(serverSurface->surfaceAt(QPointF(0, 0)), serverSurface); + QCOMPARE(serverSurface->surfaceAt(QPointF(99, 99)), serverSurface); + // outside the geometry it should not give a surface + QVERIFY(!serverSurface->surfaceAt(QPointF(100, 100))); + QVERIFY(!serverSurface->surfaceAt(QPointF(-1, -1))); +} + +void TestWaylandSurface::testDestroyAttachedBuffer() +{ + // this test verifies that destroying of a buffer attached to a surface works + using namespace KWin; + // create surface + QSignalSpy serverSurfaceCreated(m_compositorInterface, &CompositorInterface::surfaceCreated); + std::unique_ptr s(m_compositor->createSurface()); + QVERIFY(serverSurfaceCreated.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreated.first().first().value(); + + // let's damage this surface + QSignalSpy damagedSpy(serverSurface, &SurfaceInterface::damaged); + QImage image(QSize(100, 100), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::red); + s->attachBuffer(m_shm->createBuffer(image)); + s->damage(QRect(0, 0, 100, 100)); + s->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(damagedSpy.wait()); + QVERIFY(serverSurface->buffer()); + + // attach another buffer + image.fill(Qt::blue); + s->attachBuffer(m_shm->createBuffer(image)); + m_connection->flush(); + + // Let's try to destroy it + delete m_shm; + m_shm = nullptr; + QTRY_VERIFY(serverSurface->buffer()->isDropped()); +} + +void TestWaylandSurface::testDestroyWithPendingCallback() +{ + // this test tries to verify that destroying a surface with a pending callback works correctly + // first create surface + using namespace KWin; + std::unique_ptr s(m_compositor->createSurface()); + QVERIFY(s != nullptr); + QVERIFY(s->isValid()); + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + QVERIFY(surfaceCreatedSpy.wait()); + auto serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + // now render to it + QImage img(QSize(10, 10), QImage::Format_ARGB32_Premultiplied); + img.fill(Qt::black); + auto b = m_shm->createBuffer(img); + s->attachBuffer(b); + s->damage(QRect(0, 0, 10, 10)); + // add some frame callbacks + for (int i = 0; i < 1000; i++) { + wl_surface_frame(*s); + } + s->commit(KWayland::Client::Surface::CommitFlag::FrameCallback); + QSignalSpy damagedSpy(serverSurface, &SurfaceInterface::damaged); + QVERIFY(damagedSpy.wait()); + + // now try to destroy the Surface again + QSignalSpy destroyedSpy(serverSurface, &QObject::destroyed); + s.reset(); + QVERIFY(destroyedSpy.wait()); +} + +void TestWaylandSurface::testDisconnect() +{ + // this test verifies that the server side correctly tears down the resources when the client disconnects + using namespace KWin; + std::unique_ptr s(m_compositor->createSurface()); + QVERIFY(s != nullptr); + QVERIFY(s->isValid()); + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + QVERIFY(surfaceCreatedSpy.wait()); + auto serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + // destroy client + QSignalSpy clientDestroyedSpy(serverSurface->client(), &ClientConnection::destroyed); + QSignalSpy surfaceDestroyedSpy(serverSurface, &QObject::destroyed); + if (m_connection) { + m_connection->deleteLater(); + m_connection = nullptr; + } + QVERIFY(clientDestroyedSpy.wait()); + QCOMPARE(clientDestroyedSpy.count(), 1); + if (surfaceDestroyedSpy.isEmpty()) { + QVERIFY(surfaceDestroyedSpy.wait()); + } + QTRY_COMPARE(surfaceDestroyedSpy.count(), 1); + + s->destroy(); + m_shm->destroy(); + m_compositor->destroy(); + m_queue->destroy(); + m_idleInhibitManager->destroy(); +} + +void TestWaylandSurface::testOutput() +{ + // This test verifies that the enter/leave are sent correctly to the Client + using namespace KWin; + qRegisterMetaType(); + std::unique_ptr s(m_compositor->createSurface()); + QVERIFY(s != nullptr); + QVERIFY(s->isValid()); + QVERIFY(s->outputs().isEmpty()); + QSignalSpy enteredSpy(s.get(), &KWayland::Client::Surface::outputEntered); + QSignalSpy leftSpy(s.get(), &KWayland::Client::Surface::outputLeft); + // wait for the surface on the Server side + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + QVERIFY(surfaceCreatedSpy.wait()); + auto serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + QCOMPARE(serverSurface->outputs(), QList()); + + // create another registry to get notified about added outputs + KWayland::Client::Registry registry; + registry.setEventQueue(m_queue); + QSignalSpy allAnnounced(®istry, &KWayland::Client::Registry::interfacesAnnounced); + registry.create(m_connection); + QVERIFY(registry.isValid()); + registry.setup(); + QVERIFY(allAnnounced.wait()); + QSignalSpy outputAnnouncedSpy(®istry, &KWayland::Client::Registry::outputAnnounced); + + auto fakeOutput = std::make_unique(); + auto outputHandle = std::make_unique(fakeOutput.get()); + auto serverOutput = std::make_unique(m_display, outputHandle.get()); + QVERIFY(outputAnnouncedSpy.wait()); + std::unique_ptr clientOutput( + registry.createOutput(outputAnnouncedSpy.first().first().value(), outputAnnouncedSpy.first().last().value())); + QVERIFY(clientOutput->isValid()); + m_connection->flush(); + m_display->dispatchEvents(); + + // now enter it + serverSurface->setOutputs(QList{serverOutput.get()}, serverOutput.get()); + QCOMPARE(serverSurface->outputs(), QList{serverOutput.get()}); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 1); + QCOMPARE(enteredSpy.first().first().value(), clientOutput.get()); + QCOMPARE(s->outputs(), QList{clientOutput.get()}); + + // adding to same should not trigger + serverSurface->setOutputs(QList{serverOutput.get()}, serverOutput.get()); + + // leave again + serverSurface->setOutputs(QList(), nullptr); + QCOMPARE(serverSurface->outputs(), QList()); + QVERIFY(leftSpy.wait()); + QCOMPARE(enteredSpy.count(), 1); + QCOMPARE(leftSpy.count(), 1); + QCOMPARE(leftSpy.first().first().value(), clientOutput.get()); + QCOMPARE(s->outputs(), QList()); + + // leave again should not trigger + serverSurface->setOutputs(QList(), nullptr); + + // and enter again, just to verify + serverSurface->setOutputs(QList{serverOutput.get()}, serverOutput.get()); + QCOMPARE(serverSurface->outputs(), QList{serverOutput.get()}); + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 2); + QCOMPARE(leftSpy.count(), 1); + + // delete output client is on. + // client should get an exit and be left on no outputs (which is allowed) + serverOutput.reset(); + outputHandle.reset(); + QVERIFY(leftSpy.wait()); + QCOMPARE(serverSurface->outputs(), QList()); +} + +void TestWaylandSurface::testInhibit() +{ + using namespace KWin; + std::unique_ptr s(m_compositor->createSurface()); + // wait for the surface on the Server side + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + QVERIFY(surfaceCreatedSpy.wait()); + auto serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + QCOMPARE(serverSurface->inhibitsIdle(), false); + + QSignalSpy inhibitsChangedSpy(serverSurface, &SurfaceInterface::inhibitsIdleChanged); + + // now create an idle inhibition + std::unique_ptr inhibitor1(m_idleInhibitManager->createInhibitor(s.get())); + QVERIFY(inhibitsChangedSpy.wait()); + QCOMPARE(serverSurface->inhibitsIdle(), true); + + // creating a second idle inhibition should not trigger the signal + std::unique_ptr inhibitor2(m_idleInhibitManager->createInhibitor(s.get())); + QVERIFY(!inhibitsChangedSpy.wait(500)); + QCOMPARE(serverSurface->inhibitsIdle(), true); + + // and also deleting the first inhibitor should not yet change the inhibition + inhibitor1.reset(); + QVERIFY(!inhibitsChangedSpy.wait(500)); + QCOMPARE(serverSurface->inhibitsIdle(), true); + + // but deleting also the second inhibitor should trigger + inhibitor2.reset(); + QVERIFY(inhibitsChangedSpy.wait()); + QCOMPARE(serverSurface->inhibitsIdle(), false); + QCOMPARE(inhibitsChangedSpy.count(), 2); + + // recreate inhibitor1 should inhibit again + inhibitor1.reset(m_idleInhibitManager->createInhibitor(s.get())); + QVERIFY(inhibitsChangedSpy.wait()); + QCOMPARE(serverSurface->inhibitsIdle(), true); + // and destroying should uninhibit + inhibitor1.reset(); + QVERIFY(inhibitsChangedSpy.wait()); + QCOMPARE(serverSurface->inhibitsIdle(), false); + QCOMPARE(inhibitsChangedSpy.count(), 4); +} + +QTEST_GUILESS_MAIN(TestWaylandSurface) +#include "test_wayland_surface.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_windowmanagement.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_windowmanagement.cpp new file mode 100644 index 0000000000..fedb24f82f --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_wayland_windowmanagement.cpp @@ -0,0 +1,605 @@ +/* + SPDX-FileCopyrightText: 2015 Marco Martin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// KWin +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/plasmawindowmanagement.h" +#include "wayland/surface.h" +#include + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/plasmawindowmanagement.h" +#include "KWayland/Client/region.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/surface.h" + +typedef void (KWin::PlasmaWindowInterface::*ServerWindowSignal)(); +Q_DECLARE_METATYPE(ServerWindowSignal) +typedef void (KWin::PlasmaWindowInterface::*ServerWindowBooleanSignal)(bool); +Q_DECLARE_METATYPE(ServerWindowBooleanSignal) +typedef void (KWayland::Client::PlasmaWindow::*ClientWindowVoidSetter)(); +Q_DECLARE_METATYPE(ClientWindowVoidSetter) + +class TestWindowManagement : public QObject +{ + Q_OBJECT +public: + explicit TestWindowManagement(QObject *parent = nullptr); +private Q_SLOTS: + void init(); + + void testWindowTitle(); + void testReallyLongTitle(); + void testMinimizedGeometry(); + void testUseAfterUnmap(); + void testServerDelete(); + void testActiveWindowOnUnmapped(); + void testDeleteActiveWindow(); + void testCreateAfterUnmap(); + void testRequests_data(); + void testRequests(); + void testRequestsBoolean_data(); + void testRequestsBoolean(); + void testKeepAbove(); + void testKeepBelow(); + void testShowingDesktop(); + void testRequestShowingDesktop_data(); + void testRequestShowingDesktop(); + void testParentWindow(); + void testGeometry(); + void testIcon(); + void testPid(); + void testApplicationMenu(); + + void cleanup(); + +private: + KWin::Display *m_display; + KWin::CompositorInterface *m_compositorInterface; + KWin::PlasmaWindowManagementInterface *m_windowManagementInterface; + KWin::PlasmaWindowInterface *m_windowInterface; + QPointer m_surfaceInterface; + + KWayland::Client::Surface *m_surface = nullptr; + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::Compositor *m_compositor; + KWayland::Client::EventQueue *m_queue; + KWayland::Client::PlasmaWindowManagement *m_windowManagement; + KWayland::Client::PlasmaWindow *m_window; + QThread *m_thread; + KWayland::Client::Registry *m_registry; +}; + +static const QString s_socketName = QStringLiteral("kwayland-test-wayland-windowmanagement-0"); + +TestWindowManagement::TestWindowManagement(QObject *parent) + : QObject(parent) + , m_display(nullptr) + , m_compositorInterface(nullptr) + , m_connection(nullptr) + , m_compositor(nullptr) + , m_queue(nullptr) + , m_thread(nullptr) +{ +} + +void TestWindowManagement::init() +{ + using namespace KWin; + qRegisterMetaType("ShowingDesktopState"); + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + m_registry = new KWayland::Client::Registry(this); + QSignalSpy compositorSpy(m_registry, &KWayland::Client::Registry::compositorAnnounced); + + QSignalSpy windowManagementSpy(m_registry, &KWayland::Client::Registry::plasmaWindowManagementAnnounced); + + QVERIFY(!m_registry->eventQueue()); + m_registry->setEventQueue(m_queue); + QCOMPARE(m_registry->eventQueue(), m_queue); + m_registry->create(m_connection->display()); + QVERIFY(m_registry->isValid()); + m_registry->setup(); + + m_compositorInterface = new CompositorInterface(m_display, m_display); + QVERIFY(compositorSpy.wait()); + m_compositor = m_registry->createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + + m_windowManagementInterface = new PlasmaWindowManagementInterface(m_display, m_display); + + QVERIFY(windowManagementSpy.wait()); + m_windowManagement = m_registry->createPlasmaWindowManagement(windowManagementSpy.first().first().value(), + windowManagementSpy.first().last().value(), + this); + + QSignalSpy windowSpy(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated); + m_windowInterface = m_windowManagementInterface->createWindow(this, QUuid::createUuid()); + m_windowInterface->setPid(1337); + + QVERIFY(windowSpy.wait()); + m_window = windowSpy.first().first().value(); + + QSignalSpy serverSurfaceCreated(m_compositorInterface, &KWin::CompositorInterface::surfaceCreated); + m_surface = m_compositor->createSurface(this); + QVERIFY(m_surface); + + QVERIFY(serverSurfaceCreated.wait()); + m_surfaceInterface = serverSurfaceCreated.first().first().value(); + QVERIFY(m_surfaceInterface); +} + +void TestWindowManagement::testWindowTitle() +{ + m_windowInterface->setTitle(QStringLiteral("Test Title")); + + QSignalSpy titleSpy(m_window, &KWayland::Client::PlasmaWindow::titleChanged); + + QVERIFY(titleSpy.wait()); + + QCOMPARE(m_window->title(), QString::fromUtf8("Test Title")); +} + +void TestWindowManagement::testReallyLongTitle() +{ + QString title; + title.fill(QLatin1Char('t'), 500000); + m_windowInterface->setTitle(title); + + QSignalSpy titleSpy(m_window, &KWayland::Client::PlasmaWindow::titleChanged); + + QVERIFY(titleSpy.wait()); + QVERIFY(m_window->title().startsWith("t")); +} + +void TestWindowManagement::testMinimizedGeometry() +{ + m_window->setMinimizedGeometry(m_surface, QRect(5, 10, 100, 200)); + + QSignalSpy geometrySpy(m_windowInterface, &KWin::PlasmaWindowInterface::minimizedGeometriesChanged); + + QVERIFY(geometrySpy.wait()); + QCOMPARE(m_windowInterface->minimizedGeometries().values().first(), QRect(5, 10, 100, 200)); + + m_window->unsetMinimizedGeometry(m_surface); + QVERIFY(geometrySpy.wait()); + QVERIFY(m_windowInterface->minimizedGeometries().isEmpty()); +} + +void TestWindowManagement::cleanup() +{ + if (m_surface) { + delete m_surface; + m_surface = nullptr; + } + if (m_compositor) { + delete m_compositor; + m_compositor = nullptr; + } + if (m_queue) { + delete m_queue; + m_queue = nullptr; + } + if (m_windowManagement) { + delete m_windowManagement; + m_windowManagement = nullptr; + } + if (m_registry) { + delete m_registry; + m_registry = nullptr; + } + if (m_thread) { + if (m_connection) { + m_connection->flush(); + m_connection->deleteLater(); + } + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + m_connection = nullptr; + + m_display->dispatchEvents(); + + QVERIFY(m_surfaceInterface.isNull()); + + delete m_display; + m_display = nullptr; + + // these are the children of the display + m_windowManagementInterface = nullptr; + m_windowInterface = nullptr; +} + +void TestWindowManagement::testUseAfterUnmap() +{ + // this test verifies that when the client uses a window after it's unmapped, things don't break + QSignalSpy unmappedSpy(m_window, &KWayland::Client::PlasmaWindow::unmapped); + QSignalSpy destroyedSpy(m_window, &QObject::destroyed); + m_windowInterface->deleteLater(); + m_windowInterface = nullptr; + m_window->requestClose(); + QVERIFY(unmappedSpy.wait()); + QVERIFY(destroyedSpy.wait()); + m_window = nullptr; +} + +void TestWindowManagement::testServerDelete() +{ + QSignalSpy unmappedSpy(m_window, &KWayland::Client::PlasmaWindow::unmapped); + QSignalSpy destroyedSpy(m_window, &QObject::destroyed); + delete m_windowInterface; + m_windowInterface = nullptr; + QVERIFY(unmappedSpy.wait()); + QVERIFY(destroyedSpy.wait()); + m_window = nullptr; +} + +void TestWindowManagement::testActiveWindowOnUnmapped() +{ + // This test verifies that unmapping the active window changes the active window. + // first make the window active + QVERIFY(!m_windowManagement->activeWindow()); + QVERIFY(!m_window->isActive()); + QSignalSpy activeWindowChangedSpy(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::activeWindowChanged); + m_windowInterface->setActive(true); + QVERIFY(activeWindowChangedSpy.wait()); + QCOMPARE(m_windowManagement->activeWindow(), m_window); + QVERIFY(m_window->isActive()); + + // now unmap should change the active window + QSignalSpy destroyedSpy(m_window, &QObject::destroyed); + QSignalSpy serverDestroyedSpy(m_windowInterface, &QObject::destroyed); + delete m_windowInterface; + m_windowInterface = nullptr; + QVERIFY(activeWindowChangedSpy.wait()); + QVERIFY(!m_windowManagement->activeWindow()); +} + +void TestWindowManagement::testDeleteActiveWindow() +{ + // This test verifies that deleting the active window on client side changes the active window + // first make the window active + QVERIFY(!m_windowManagement->activeWindow()); + QVERIFY(!m_window->isActive()); + QSignalSpy activeWindowChangedSpy(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::activeWindowChanged); + m_windowInterface->setActive(true); + QVERIFY(activeWindowChangedSpy.wait()); + QCOMPARE(activeWindowChangedSpy.count(), 1); + QCOMPARE(m_windowManagement->activeWindow(), m_window); + QVERIFY(m_window->isActive()); + + // delete the client side window - that's semantically kind of wrong, but shouldn't make our code crash + delete m_window; + m_window = nullptr; + QCOMPARE(activeWindowChangedSpy.count(), 2); + QVERIFY(!m_windowManagement->activeWindow()); +} + +void TestWindowManagement::testCreateAfterUnmap() +{ + // this test verifies that we don't get a protocol error on client side when creating an already unmapped window. + QCOMPARE(m_windowManagement->children().count(), 1); + + QSignalSpy windowAddedSpy(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated); + + // create and unmap in one go + // client will first handle the create, the unmap will be sent once the server side is bound + auto serverWindow = m_windowManagementInterface->createWindow(m_windowManagementInterface, QUuid::createUuid()); + delete serverWindow; + QCOMPARE(m_windowManagementInterface->children().count(), 0); + + windowAddedSpy.wait(); + auto window = dynamic_cast(m_windowManagement->children().last()); + QVERIFY(window); + // now this is not yet on the server, on the server it will be after next roundtrip + // which we can trigger by waiting for destroy of the newly created window. + // why destroy? Because we will get the unmap which triggers a destroy + QSignalSpy clientDestroyedSpy(window, &QObject::destroyed); + QVERIFY(clientDestroyedSpy.wait()); + // Verify that any wrappers created for our temporary window are now gone + QCOMPARE(m_windowManagement->children().count(), 1); +} + +void TestWindowManagement::testRequests_data() +{ + using namespace KWin; + QTest::addColumn("changedSignal"); + QTest::addColumn("requester"); + + QTest::newRow("close") << &PlasmaWindowInterface::closeRequested << &KWayland::Client::PlasmaWindow::requestClose; + QTest::newRow("move") << &PlasmaWindowInterface::moveRequested << &KWayland::Client::PlasmaWindow::requestMove; + QTest::newRow("resize") << &PlasmaWindowInterface::resizeRequested << &KWayland::Client::PlasmaWindow::requestResize; +} + +void TestWindowManagement::testRequests() +{ + // this test case verifies all the different requests on a PlasmaWindow + QFETCH(ServerWindowSignal, changedSignal); + QSignalSpy requestSpy(m_windowInterface, changedSignal); + QFETCH(ClientWindowVoidSetter, requester); + (m_window->*(requester))(); + QVERIFY(requestSpy.wait()); +} + +void TestWindowManagement::testRequestsBoolean_data() +{ + using namespace KWin; + QTest::addColumn("changedSignal"); + QTest::addColumn("flag"); + + QTest::newRow("activate") << &PlasmaWindowInterface::activeRequested << int(ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_ACTIVE); + QTest::newRow("minimized") << &PlasmaWindowInterface::minimizedRequested << int(ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_MINIMIZED); + QTest::newRow("maximized") << &PlasmaWindowInterface::maximizedRequested << int(ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_MAXIMIZED); + QTest::newRow("fullscreen") << &PlasmaWindowInterface::fullscreenRequested << int(ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_FULLSCREEN); + QTest::newRow("keepAbove") << &PlasmaWindowInterface::keepAboveRequested << int(ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_KEEP_ABOVE); + QTest::newRow("keepBelow") << &PlasmaWindowInterface::keepBelowRequested << int(ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_KEEP_BELOW); + QTest::newRow("demandsAttention") << &PlasmaWindowInterface::demandsAttentionRequested << int(ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_DEMANDS_ATTENTION); + QTest::newRow("closeable") << &PlasmaWindowInterface::closeableRequested << int(ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_CLOSEABLE); + QTest::newRow("minimizable") << &PlasmaWindowInterface::minimizeableRequested << int(ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_MINIMIZABLE); + QTest::newRow("maximizable") << &PlasmaWindowInterface::maximizeableRequested << int(ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_MAXIMIZABLE); + QTest::newRow("fullscreenable") << &PlasmaWindowInterface::fullscreenableRequested << int(ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_FULLSCREENABLE); + QTest::newRow("skiptaskbar") << &PlasmaWindowInterface::skipTaskbarRequested << int(ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_SKIPTASKBAR); + QTest::newRow("skipSwitcher") << &PlasmaWindowInterface::skipSwitcherRequested << int(ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_SKIPSWITCHER); + QTest::newRow("shadeable") << &PlasmaWindowInterface::shadeableRequested << int(ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_SHADEABLE); + QTest::newRow("shaded") << &PlasmaWindowInterface::shadedRequested << int(ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_SHADED); + QTest::newRow("movable") << &PlasmaWindowInterface::movableRequested << int(ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_MOVABLE); + QTest::newRow("resizable") << &PlasmaWindowInterface::resizableRequested << int(ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_RESIZABLE); + QTest::newRow("virtualDesktopChangeable") << &PlasmaWindowInterface::virtualDesktopChangeableRequested + << int(ORG_KDE_PLASMA_WINDOW_MANAGEMENT_STATE_VIRTUAL_DESKTOP_CHANGEABLE); +} + +void TestWindowManagement::testRequestsBoolean() +{ + // this test case verifies all the different requests on a PlasmaWindow + QFETCH(ServerWindowBooleanSignal, changedSignal); + QSignalSpy requestSpy(m_windowInterface, changedSignal); + QFETCH(int, flag); + org_kde_plasma_window_set_state(*m_window, flag, flag); + QVERIFY(requestSpy.wait()); + QCOMPARE(requestSpy.count(), 1); + QCOMPARE(requestSpy.first().first().toBool(), true); + org_kde_plasma_window_set_state(*m_window, flag, 0); + QVERIFY(requestSpy.wait()); + QCOMPARE(requestSpy.count(), 2); + QCOMPARE(requestSpy.last().first().toBool(), false); +} + +void TestWindowManagement::testShowingDesktop() +{ + using namespace KWin; + // this test verifies setting the showing desktop state + QVERIFY(!m_windowManagement->isShowingDesktop()); + QSignalSpy showingDesktopChangedSpy(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::showingDesktopChanged); + m_windowManagementInterface->setShowingDesktopState(PlasmaWindowManagementInterface::ShowingDesktopState::Enabled); + QVERIFY(showingDesktopChangedSpy.wait()); + QCOMPARE(showingDesktopChangedSpy.count(), 1); + QCOMPARE(showingDesktopChangedSpy.first().first().toBool(), true); + QVERIFY(m_windowManagement->isShowingDesktop()); + // setting to same should not change + m_windowManagementInterface->setShowingDesktopState(PlasmaWindowManagementInterface::ShowingDesktopState::Enabled); + QVERIFY(!showingDesktopChangedSpy.wait(100)); + QCOMPARE(showingDesktopChangedSpy.count(), 1); + // setting to other state should change + m_windowManagementInterface->setShowingDesktopState(PlasmaWindowManagementInterface::ShowingDesktopState::Disabled); + QVERIFY(showingDesktopChangedSpy.wait()); + QCOMPARE(showingDesktopChangedSpy.count(), 2); + QCOMPARE(showingDesktopChangedSpy.first().first().toBool(), true); + QCOMPARE(showingDesktopChangedSpy.last().first().toBool(), false); + QVERIFY(!m_windowManagement->isShowingDesktop()); +} + +void TestWindowManagement::testRequestShowingDesktop_data() +{ + using namespace KWin; + QTest::addColumn("value"); + QTest::addColumn("expectedValue"); + + QTest::newRow("enable") << true << PlasmaWindowManagementInterface::ShowingDesktopState::Enabled; + QTest::newRow("disable") << false << PlasmaWindowManagementInterface::ShowingDesktopState::Disabled; +} + +void TestWindowManagement::testRequestShowingDesktop() +{ + // this test verifies requesting show desktop state + using namespace KWin; + QSignalSpy requestSpy(m_windowManagementInterface, &PlasmaWindowManagementInterface::requestChangeShowingDesktop); + QFETCH(bool, value); + m_windowManagement->setShowingDesktop(value); + QVERIFY(requestSpy.wait()); + QCOMPARE(requestSpy.count(), 1); + QTEST(requestSpy.first().first().value(), "expectedValue"); +} + +void TestWindowManagement::testKeepAbove() +{ + using namespace KWin; + // this test verifies setting the keep above state + QVERIFY(!m_window->isKeepAbove()); + QSignalSpy keepAboveChangedSpy(m_window, &KWayland::Client::PlasmaWindow::keepAboveChanged); + m_windowInterface->setKeepAbove(true); + QVERIFY(keepAboveChangedSpy.wait()); + QCOMPARE(keepAboveChangedSpy.count(), 1); + QVERIFY(m_window->isKeepAbove()); + // setting to same should not change + m_windowInterface->setKeepAbove(true); + QVERIFY(!keepAboveChangedSpy.wait(100)); + QCOMPARE(keepAboveChangedSpy.count(), 1); + // setting to other state should change + m_windowInterface->setKeepAbove(false); + QVERIFY(keepAboveChangedSpy.wait()); + QCOMPARE(keepAboveChangedSpy.count(), 2); + QVERIFY(!m_window->isKeepAbove()); +} + +void TestWindowManagement::testKeepBelow() +{ + using namespace KWin; + // this test verifies setting the keep below state + QVERIFY(!m_window->isKeepBelow()); + QSignalSpy keepBelowChangedSpy(m_window, &KWayland::Client::PlasmaWindow::keepBelowChanged); + m_windowInterface->setKeepBelow(true); + QVERIFY(keepBelowChangedSpy.wait()); + QCOMPARE(keepBelowChangedSpy.count(), 1); + QVERIFY(m_window->isKeepBelow()); + // setting to same should not change + m_windowInterface->setKeepBelow(true); + QVERIFY(!keepBelowChangedSpy.wait(100)); + QCOMPARE(keepBelowChangedSpy.count(), 1); + // setting to other state should change + m_windowInterface->setKeepBelow(false); + QVERIFY(keepBelowChangedSpy.wait()); + QCOMPARE(keepBelowChangedSpy.count(), 2); + QVERIFY(!m_window->isKeepBelow()); +} + +void TestWindowManagement::testParentWindow() +{ + // this test verifies the functionality of ParentWindows + QCOMPARE(m_windowManagement->windows().count(), 1); + auto parentWindow = m_windowManagement->windows().first(); + QVERIFY(parentWindow); + QVERIFY(parentWindow->parentWindow().isNull()); + + // now let's create a second window + QSignalSpy windowAddedSpy(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated); + std::unique_ptr serverTransient(m_windowManagementInterface->createWindow(this, QUuid::createUuid())); + serverTransient->setParentWindow(m_windowInterface); + QVERIFY(windowAddedSpy.wait()); + auto transient = windowAddedSpy.first().first().value(); + QCOMPARE(transient->parentWindow().data(), parentWindow); + + // let's unset the parent + QSignalSpy parentWindowChangedSpy(transient, &KWayland::Client::PlasmaWindow::parentWindowChanged); + serverTransient->setParentWindow(nullptr); + QVERIFY(parentWindowChangedSpy.wait()); + QVERIFY(transient->parentWindow().isNull()); + + // and set it again + serverTransient->setParentWindow(m_windowInterface); + QVERIFY(parentWindowChangedSpy.wait()); + QCOMPARE(transient->parentWindow().data(), parentWindow); + + // now let's try to unmap the parent + m_windowInterface->deleteLater(); + m_window = nullptr; + m_windowInterface = nullptr; + QVERIFY(parentWindowChangedSpy.wait()); + QVERIFY(transient->parentWindow().isNull()); +} + +void TestWindowManagement::testGeometry() +{ + QVERIFY(m_window); + QCOMPARE(m_window->geometry(), QRect()); + QSignalSpy windowGeometryChangedSpy(m_window, &KWayland::Client::PlasmaWindow::geometryChanged); + m_windowInterface->setGeometry(QRect(20, -10, 30, 40)); + QVERIFY(windowGeometryChangedSpy.wait()); + QCOMPARE(m_window->geometry(), QRect(20, -10, 30, 40)); + // setting an empty geometry should not be sent to the client + m_windowInterface->setGeometry(QRect()); + QVERIFY(!windowGeometryChangedSpy.wait(10)); + // setting to the geometry which the client still has should not trigger signal + m_windowInterface->setGeometry(QRect(20, -10, 30, 40)); + QVERIFY(!windowGeometryChangedSpy.wait(10)); + // setting another geometry should work, though + m_windowInterface->setGeometry(QRect(0, 0, 35, 45)); + QVERIFY(windowGeometryChangedSpy.wait()); + QCOMPARE(windowGeometryChangedSpy.count(), 2); + QCOMPARE(m_window->geometry(), QRect(0, 0, 35, 45)); + + // let's bind a second PlasmaWindowManagement to verify the initial setting + std::unique_ptr pm( + m_registry->createPlasmaWindowManagement(m_registry->interface(KWayland::Client::Registry::Interface::PlasmaWindowManagement).name, + m_registry->interface(KWayland::Client::Registry::Interface::PlasmaWindowManagement).version)); + QVERIFY(pm != nullptr); + QSignalSpy windowAddedSpy(pm.get(), &KWayland::Client::PlasmaWindowManagement::windowCreated); + QVERIFY(windowAddedSpy.wait()); + auto window = pm->windows().first(); + QCOMPARE(window->geometry(), QRect(0, 0, 35, 45)); +} + +void TestWindowManagement::testIcon() +{ + // initially, there shouldn't be any icon + QSignalSpy iconChangedSpy(m_window, &KWayland::Client::PlasmaWindow::iconChanged); + QVERIFY(m_window->icon().isNull()); + + // create an icon with a pixmap + QImage p(32, 32, QImage::Format_ARGB32_Premultiplied); + p.fill(Qt::red); + const QIcon dummyIcon(QPixmap::fromImage(p)); + m_windowInterface->setIcon(dummyIcon); + QVERIFY(iconChangedSpy.wait()); + QCOMPARE(iconChangedSpy.count(), 1); + QCOMPARE(m_window->icon().pixmap(32, 32), dummyIcon.pixmap(32, 32)); + + // let's set a themed icon + m_windowInterface->setIcon(QIcon::fromTheme(QStringLiteral("wayland"))); + QVERIFY(iconChangedSpy.wait()); + QCOMPARE(iconChangedSpy.count(), 2); + if (!QIcon::hasThemeIcon(QStringLiteral("wayland"))) { + QEXPECT_FAIL("", "no wayland icon", Continue); + } + QCOMPARE(m_window->icon().name(), QStringLiteral("wayland")); +} + +void TestWindowManagement::testPid() +{ + QVERIFY(m_window); + QVERIFY(m_window->pid() == 1337); + + // test server not setting a PID for whatever reason + std::unique_ptr newWindowInterface(m_windowManagementInterface->createWindow(this, QUuid::createUuid())); + QSignalSpy windowSpy(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated); + QVERIFY(windowSpy.wait()); + std::unique_ptr newWindow(windowSpy.first().first().value()); + QVERIFY(newWindow); + QVERIFY(newWindow->pid() == 0); +} + +void TestWindowManagement::testApplicationMenu() +{ + const auto serviceName = QStringLiteral("org.kde.foo"); + const auto objectPath = QStringLiteral("/org/kde/bar"); + + m_windowInterface->setApplicationMenuPaths(serviceName, objectPath); + + QSignalSpy applicationMenuChangedSpy(m_window, &KWayland::Client::PlasmaWindow::applicationMenuChanged); + QVERIFY(applicationMenuChangedSpy.wait()); + + QCOMPARE(m_window->applicationMenuServiceName(), serviceName); + QCOMPARE(m_window->applicationMenuObjectPath(), objectPath); +} + +QTEST_MAIN(TestWindowManagement) +#include "test_wayland_windowmanagement.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_xdg_decoration.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_xdg_decoration.cpp new file mode 100644 index 0000000000..8d29edc32d --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_xdg_decoration.cpp @@ -0,0 +1,225 @@ +/* + SPDX-FileCopyrightText: 2018 David Edmundson + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// KWin +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/xdgdecoration_v1.h" +#include "wayland/xdgshell.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/surface.h" +#include "KWayland/Client/xdgdecoration.h" +#include "KWayland/Client/xdgshell.h" + +class TestXdgDecoration : public QObject +{ + Q_OBJECT +public: + explicit TestXdgDecoration(QObject *parent = nullptr); +private Q_SLOTS: + void init(); + void cleanup(); + + void testDecoration_data(); + void testDecoration(); + +private: + KWin::Display *m_display = nullptr; + KWin::CompositorInterface *m_compositorInterface = nullptr; + KWin::XdgShellInterface *m_xdgShellInterface = nullptr; + KWin::XdgDecorationManagerV1Interface *m_xdgDecorationManagerInterface = nullptr; + + KWayland::Client::ConnectionThread *m_connection = nullptr; + KWayland::Client::Compositor *m_compositor = nullptr; + KWayland::Client::EventQueue *m_queue = nullptr; + KWayland::Client::XdgShell *m_xdgShell = nullptr; + KWayland::Client::XdgDecorationManager *m_xdgDecorationManager = nullptr; + + QThread *m_thread = nullptr; + KWayland::Client::Registry *m_registry = nullptr; +}; + +static const QString s_socketName = QStringLiteral("kwayland-test-wayland-server-side-decoration-0"); + +TestXdgDecoration::TestXdgDecoration(QObject *parent) + : QObject(parent) +{ +} + +void TestXdgDecoration::init() +{ + using namespace KWin; + + qRegisterMetaType(); + qRegisterMetaType(); + + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + m_registry = new KWayland::Client::Registry(); + QSignalSpy compositorSpy(m_registry, &KWayland::Client::Registry::compositorAnnounced); + QSignalSpy xdgShellSpy(m_registry, &KWayland::Client::Registry::xdgShellStableAnnounced); + QSignalSpy xdgDecorationManagerSpy(m_registry, &KWayland::Client::Registry::xdgDecorationAnnounced); + + QVERIFY(!m_registry->eventQueue()); + m_registry->setEventQueue(m_queue); + QCOMPARE(m_registry->eventQueue(), m_queue); + m_registry->create(m_connection); + QVERIFY(m_registry->isValid()); + m_registry->setup(); + + m_compositorInterface = new CompositorInterface(m_display, m_display); + QVERIFY(compositorSpy.wait()); + m_compositor = m_registry->createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + + m_xdgShellInterface = new XdgShellInterface(m_display, m_display); + QVERIFY(xdgShellSpy.wait()); + m_xdgShell = m_registry->createXdgShell(xdgShellSpy.first().first().value(), xdgShellSpy.first().last().value(), this); + + m_xdgDecorationManagerInterface = new XdgDecorationManagerV1Interface(m_display, m_display); + + QVERIFY(xdgDecorationManagerSpy.wait()); + m_xdgDecorationManager = m_registry->createXdgDecorationManager(xdgDecorationManagerSpy.first().first().value(), + xdgDecorationManagerSpy.first().last().value(), + this); +} + +void TestXdgDecoration::cleanup() +{ + if (m_compositor) { + delete m_compositor; + m_compositor = nullptr; + } + if (m_xdgShell) { + delete m_xdgShell; + m_xdgShell = nullptr; + } + if (m_xdgDecorationManager) { + delete m_xdgDecorationManager; + m_xdgDecorationManager = nullptr; + } + if (m_queue) { + delete m_queue; + m_queue = nullptr; + } + if (m_registry) { + delete m_registry; + m_registry = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + delete m_connection; + m_connection = nullptr; + + delete m_display; + m_display = nullptr; +} + +void TestXdgDecoration::testDecoration_data() +{ + using namespace KWin; + QTest::addColumn("configuredMode"); + QTest::addColumn("configuredModeExp"); + QTest::addColumn("setMode"); + QTest::addColumn("setModeExp"); + + const auto serverClient = XdgToplevelDecorationV1Interface::Mode::Client; + const auto serverServer = XdgToplevelDecorationV1Interface::Mode::Server; + const auto clientClient = KWayland::Client::XdgDecoration::Mode::ClientSide; + const auto clientServer = KWayland::Client::XdgDecoration::Mode::ServerSide; + + QTest::newRow("client->client") << serverClient << clientClient << clientClient << serverClient; + QTest::newRow("client->server") << serverClient << clientClient << clientServer << serverServer; + QTest::newRow("server->client") << serverServer << clientServer << clientClient << serverClient; + QTest::newRow("server->server") << serverServer << clientServer << clientServer << serverServer; +} + +void TestXdgDecoration::testDecoration() +{ + using namespace KWin; + + QFETCH(KWin::XdgToplevelDecorationV1Interface::Mode, configuredMode); + QFETCH(KWayland::Client::XdgDecoration::Mode, configuredModeExp); + QFETCH(KWayland::Client::XdgDecoration::Mode, setMode); + QFETCH(KWin::XdgToplevelDecorationV1Interface::Mode, setModeExp); + + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + QSignalSpy shellSurfaceCreatedSpy(m_xdgShellInterface, &XdgShellInterface::toplevelCreated); + QSignalSpy decorationCreatedSpy(m_xdgDecorationManagerInterface, &XdgDecorationManagerV1Interface::decorationCreated); + + // create shell surface and deco object + std::unique_ptr surface(m_compositor->createSurface()); + std::unique_ptr shellSurface(m_xdgShell->createSurface(surface.get())); + std::unique_ptr decoration(m_xdgDecorationManager->getToplevelDecoration(shellSurface.get())); + + // and receive all these on the "server" + QVERIFY(surfaceCreatedSpy.count() || surfaceCreatedSpy.wait()); + QVERIFY(shellSurfaceCreatedSpy.count() || shellSurfaceCreatedSpy.wait()); + QVERIFY(decorationCreatedSpy.count() || decorationCreatedSpy.wait()); + + auto shellSurfaceIface = shellSurfaceCreatedSpy.first().first().value(); + auto decorationIface = decorationCreatedSpy.first().first().value(); + + QVERIFY(decorationIface); + QVERIFY(shellSurfaceIface); + QCOMPARE(decorationIface->toplevel(), shellSurfaceIface); + QCOMPARE(decorationIface->preferredMode(), XdgToplevelDecorationV1Interface::Mode::Undefined); + + QSignalSpy clientConfiguredSpy(decoration.get(), &KWayland::Client::XdgDecoration::modeChanged); + QSignalSpy modeRequestedSpy(decorationIface, &XdgToplevelDecorationV1Interface::preferredModeChanged); + + // server configuring a client + decorationIface->sendConfigure(configuredMode); + quint32 serial = shellSurfaceIface->sendConfigure(QSize(0, 0), {}); + QVERIFY(clientConfiguredSpy.wait()); + QCOMPARE(clientConfiguredSpy.first().first().value(), configuredModeExp); + + shellSurface->ackConfigure(serial); + + // client requesting another mode + decoration->setMode(setMode); + QVERIFY(modeRequestedSpy.wait()); + QCOMPARE(modeRequestedSpy.first().first().value(), setModeExp); + QCOMPARE(decorationIface->preferredMode(), setModeExp); + modeRequestedSpy.clear(); + + decoration->unsetMode(); + QVERIFY(modeRequestedSpy.wait()); + QCOMPARE(modeRequestedSpy.first().first().value(), XdgToplevelDecorationV1Interface::Mode::Undefined); +} + +QTEST_GUILESS_MAIN(TestXdgDecoration) +#include "test_xdg_decoration.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_xdg_foreign.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_xdg_foreign.cpp new file mode 100644 index 0000000000..ebdd9a5214 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_xdg_foreign.cpp @@ -0,0 +1,355 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2017 Marco Martin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// KWin +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/surface.h" +#include "wayland/xdgforeign_v2.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/region.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/surface.h" +#include "KWayland/Client/xdgforeign.h" + +class TestForeign : public QObject +{ + Q_OBJECT +public: + explicit TestForeign(QObject *parent = nullptr); +private Q_SLOTS: + void init(); + void cleanup(); + + void testExport(); + void testDeleteImported(); + void testDeleteChildSurface(); + void testDeleteParentSurface(); + void testDeleteExported(); + void testExportTwoTimes(); + void testImportTwoTimes(); + void testImportInvalidToplevel(); + +private: + void doExport(); + + KWin::Display *m_display; + QPointer m_compositorInterface; + KWin::XdgForeignV2Interface *m_foreignInterface; + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::Compositor *m_compositor; + KWayland::Client::EventQueue *m_queue; + KWayland::Client::XdgExporter *m_exporter; + KWayland::Client::XdgImporter *m_importer; + + QPointer m_exportedSurface; + QPointer m_exportedSurfaceInterface; + + QPointer m_exported; + QPointer m_imported; + + QPointer m_childSurface; + QPointer m_childSurfaceInterface; + + QThread *m_thread; +}; + +static const QString s_socketName = QStringLiteral("kwayland-test-xdg-foreign-0"); + +TestForeign::TestForeign(QObject *parent) + : QObject(parent) + , m_display(nullptr) + , m_compositorInterface(nullptr) + , m_connection(nullptr) + , m_compositor(nullptr) + , m_queue(nullptr) + , m_exporter(nullptr) + , m_importer(nullptr) + , m_thread(nullptr) +{ +} + +void TestForeign::init() +{ + using namespace KWin; + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + + qRegisterMetaType(); + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + KWayland::Client::Registry registry; + QSignalSpy compositorSpy(®istry, &KWayland::Client::Registry::compositorAnnounced); + + QSignalSpy exporterSpy(®istry, &KWayland::Client::Registry::exporterUnstableV2Announced); + + QSignalSpy importerSpy(®istry, &KWayland::Client::Registry::importerUnstableV2Announced); + + QVERIFY(!registry.eventQueue()); + registry.setEventQueue(m_queue); + QCOMPARE(registry.eventQueue(), m_queue); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + + m_compositorInterface = new CompositorInterface(m_display, m_display); + QVERIFY(compositorSpy.wait()); + m_compositor = registry.createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + + m_foreignInterface = new XdgForeignV2Interface(m_display, m_display); + + QVERIFY(exporterSpy.wait()); + // Both importer and exporter should have been triggered by now + QCOMPARE(exporterSpy.count(), 1); + QCOMPARE(importerSpy.count(), 1); + + m_exporter = registry.createXdgExporter(exporterSpy.first().first().value(), exporterSpy.first().last().value(), this); + m_importer = registry.createXdgImporter(importerSpy.first().first().value(), importerSpy.first().last().value(), this); +} + +void TestForeign::cleanup() +{ +#define CLEANUP(variable) \ + if (variable) { \ + delete variable; \ + variable = nullptr; \ + } + + CLEANUP(m_compositor) + CLEANUP(m_exporter) + CLEANUP(m_importer) + CLEANUP(m_queue) + if (m_connection) { + m_connection->deleteLater(); + m_connection = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + delete m_display; + m_display = nullptr; +#undef CLEANUP + + // these are the children of the display + m_foreignInterface = nullptr; +} + +void TestForeign::doExport() +{ + QSignalSpy serverSurfaceCreated(m_compositorInterface.data(), &KWin::CompositorInterface::surfaceCreated); + + m_exportedSurface = m_compositor->createSurface(); + QVERIFY(serverSurfaceCreated.wait()); + + m_exportedSurfaceInterface = serverSurfaceCreated.first().first().value(); + + // Export a window + m_exported = m_exporter->exportTopLevel(m_exportedSurface); + QVERIFY(m_exported->handle().isEmpty()); + QSignalSpy doneSpy(m_exported.data(), &KWayland::Client::XdgExported::done); + QVERIFY(doneSpy.wait()); + QVERIFY(!m_exported->handle().isEmpty()); + + QSignalSpy transientSpy(m_foreignInterface, &KWin::XdgForeignV2Interface::transientChanged); + + // Import the just exported window + m_imported = m_importer->importTopLevel(m_exported->handle()); + QVERIFY(m_imported->isValid()); + + QSignalSpy childSurfaceInterfaceCreated(m_compositorInterface.data(), &KWin::CompositorInterface::surfaceCreated); + m_childSurface = m_compositor->createSurface(); + QVERIFY(childSurfaceInterfaceCreated.wait()); + m_childSurfaceInterface = childSurfaceInterfaceCreated.first().first().value(); + m_childSurface->commit(KWayland::Client::Surface::CommitFlag::None); + + m_imported->setParentOf(m_childSurface); + QVERIFY(transientSpy.wait()); + + QCOMPARE(transientSpy.first().first().value(), m_childSurfaceInterface.data()); + QCOMPARE(transientSpy.first().at(1).value(), m_exportedSurfaceInterface.data()); + + // transientFor api + QCOMPARE(m_foreignInterface->transientFor(m_childSurfaceInterface), m_exportedSurfaceInterface.data()); +} + +void TestForeign::testExport() +{ + doExport(); +} + +void TestForeign::testDeleteImported() +{ + doExport(); + + QSignalSpy transientSpy(m_foreignInterface, &KWin::XdgForeignV2Interface::transientChanged); + + m_imported->deleteLater(); + m_imported = nullptr; + + QVERIFY(transientSpy.wait()); + + QCOMPARE(transientSpy.first().first().value(), m_childSurfaceInterface.data()); + QVERIFY(!transientSpy.first().at(1).value()); + QVERIFY(!m_foreignInterface->transientFor(m_childSurfaceInterface)); +} + +void TestForeign::testDeleteChildSurface() +{ + doExport(); + + QSignalSpy transientSpy(m_foreignInterface, &KWin::XdgForeignV2Interface::transientChanged); + + m_childSurface->deleteLater(); + + QVERIFY(transientSpy.wait()); + + QVERIFY(!transientSpy.first().at(0).value()); + QCOMPARE(transientSpy.first().at(1).value(), m_exportedSurfaceInterface.data()); +} + +void TestForeign::testDeleteParentSurface() +{ + doExport(); + + QSignalSpy transientSpy(m_foreignInterface, &KWin::XdgForeignV2Interface::transientChanged); + m_exportedSurface->deleteLater(); + QVERIFY(transientSpy.wait()); + + QCOMPARE(transientSpy.first().first().value(), m_childSurfaceInterface.data()); + QVERIFY(!transientSpy.first().at(1).value()); + QVERIFY(!m_foreignInterface->transientFor(m_childSurfaceInterface)); +} + +void TestForeign::testDeleteExported() +{ + doExport(); + + QSignalSpy transientSpy(m_foreignInterface, &KWin::XdgForeignV2Interface::transientChanged); + QSignalSpy destroyedSpy(m_imported.data(), &KWayland::Client::XdgImported::importedDestroyed); + + m_exported->deleteLater(); + m_exported = nullptr; + + QVERIFY(transientSpy.wait()); + QVERIFY(destroyedSpy.wait()); + + QCOMPARE(transientSpy.first().first().value(), m_childSurfaceInterface.data()); + QVERIFY(!transientSpy.first().at(1).value()); + QVERIFY(!m_foreignInterface->transientFor(m_childSurfaceInterface)); + + QVERIFY(!m_imported->isValid()); +} + +void TestForeign::testExportTwoTimes() +{ + doExport(); + + // Export second window + KWayland::Client::XdgExported *exported2 = m_exporter->exportTopLevel(m_exportedSurface); + QVERIFY(exported2->handle().isEmpty()); + QSignalSpy doneSpy(exported2, &KWayland::Client::XdgExported::done); + QVERIFY(doneSpy.wait()); + QVERIFY(!exported2->handle().isEmpty()); + + QSignalSpy transientSpy(m_foreignInterface, &KWin::XdgForeignV2Interface::transientChanged); + + // Import the just exported window + KWayland::Client::XdgImported *imported2 = m_importer->importTopLevel(exported2->handle()); + QVERIFY(imported2->isValid()); + + // create a second child surface + QSignalSpy serverSurfaceCreated(m_compositorInterface.data(), &KWin::CompositorInterface::surfaceCreated); + + KWayland::Client::Surface *childSurface2 = m_compositor->createSurface(); + QVERIFY(serverSurfaceCreated.wait()); + + KWin::SurfaceInterface *childSurface2Interface = serverSurfaceCreated.first().first().value(); + + imported2->setParentOf(childSurface2); + QVERIFY(transientSpy.wait()); + + QCOMPARE(transientSpy.first().first().value(), childSurface2Interface); + QCOMPARE(transientSpy.first().at(1).value(), m_exportedSurfaceInterface.data()); + + // transientFor api + // check the old relationship is still here + QCOMPARE(m_foreignInterface->transientFor(m_childSurfaceInterface), m_exportedSurfaceInterface.data()); + // check the new relationship + QCOMPARE(m_foreignInterface->transientFor(childSurface2Interface), m_exportedSurfaceInterface.data()); +} + +void TestForeign::testImportTwoTimes() +{ + doExport(); + + QSignalSpy transientSpy(m_foreignInterface, &KWin::XdgForeignV2Interface::transientChanged); + + // Import another time the exported window + KWayland::Client::XdgImported *imported2 = m_importer->importTopLevel(m_exported->handle()); + QVERIFY(imported2->isValid()); + + // create a second child surface + QSignalSpy serverSurfaceCreated(m_compositorInterface.data(), &KWin::CompositorInterface::surfaceCreated); + + KWayland::Client::Surface *childSurface2 = m_compositor->createSurface(); + QVERIFY(serverSurfaceCreated.wait()); + + KWin::SurfaceInterface *childSurface2Interface = serverSurfaceCreated.first().first().value(); + + imported2->setParentOf(childSurface2); + QVERIFY(transientSpy.wait()); + + QCOMPARE(transientSpy.first().first().value(), childSurface2Interface); + QCOMPARE(transientSpy.first().at(1).value(), m_exportedSurfaceInterface.data()); + + // transientFor api + // check the old relationship is still here + QCOMPARE(m_foreignInterface->transientFor(m_childSurfaceInterface), m_exportedSurfaceInterface.data()); + // check the new relationship + QCOMPARE(m_foreignInterface->transientFor(childSurface2Interface), m_exportedSurfaceInterface.data()); +} + +void TestForeign::testImportInvalidToplevel() +{ + // This test verifies that the compositor properly handles the case where a client + // attempts to import a toplevel with an invalid handle. + + KWayland::Client::XdgImported *imported = m_importer->importTopLevel(QStringLiteral("foobar")); + QVERIFY(imported->isValid()); + + QSignalSpy importedDestroySpy(imported, &KWayland::Client::XdgImported::importedDestroyed); + QVERIFY(importedDestroySpy.wait()); +} + +QTEST_GUILESS_MAIN(TestForeign) +#include "test_xdg_foreign.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_xdg_output.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_xdg_output.cpp new file mode 100644 index 0000000000..0889963b56 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_xdg_output.cpp @@ -0,0 +1,173 @@ +/* + SPDX-FileCopyrightText: 2018 David Edmundson + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// KWin +#include "wayland/display.h" +#include "wayland/output.h" +#include "wayland/xdgoutput_v1.h" + +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/output.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/xdgoutput.h" + +#include "../../../tests/fakeoutput.h" + +class TestXdgOutput : public QObject +{ + Q_OBJECT +public: + explicit TestXdgOutput(QObject *parent = nullptr); +private Q_SLOTS: + void init(); + void cleanup(); + void testChanges(); + +private: + KWin::Display *m_display; + std::unique_ptr m_fakeOutput; + std::unique_ptr m_outputHandle; + KWin::OutputInterface *m_serverOutput; + KWin::XdgOutputManagerV1Interface *m_serverXdgOutputManager; + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::EventQueue *m_queue; + QThread *m_thread; +}; + +static const QString s_socketName = QStringLiteral("kwin-test-xdg-output-0"); + +TestXdgOutput::TestXdgOutput(QObject *parent) + : QObject(parent) + , m_display(nullptr) + , m_serverOutput(nullptr) + , m_connection(nullptr) + , m_thread(nullptr) +{ +} + +void TestXdgOutput::init() +{ + using namespace KWin; + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + + m_fakeOutput = std::make_unique(); + m_fakeOutput->setMode(QSize(1920, 1080), 60000); + m_fakeOutput->moveTo(QPoint(11, 12)); // not a sensible value for one monitor, but works for this test + m_fakeOutput->setScale(1.5); + m_fakeOutput->setName("testName"); + m_fakeOutput->setManufacturer("foo"); + m_fakeOutput->setModel("bar"); + m_outputHandle = std::make_unique(m_fakeOutput.get()); + + m_serverOutput = new OutputInterface(m_display, m_outputHandle.get(), this); + + m_serverXdgOutputManager = new XdgOutputManagerV1Interface(m_display, this); + m_serverXdgOutputManager->offer(m_serverOutput); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); +} + +void TestXdgOutput::cleanup() +{ + if (m_queue) { + delete m_queue; + m_queue = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + delete m_connection; + m_connection = nullptr; + + delete m_serverOutput; + m_serverOutput = nullptr; + m_outputHandle.reset(); + + delete m_display; + m_display = nullptr; +} + +void TestXdgOutput::testChanges() +{ + // verify the server modes + using namespace KWin; + KWayland::Client::Registry registry; + QSignalSpy announced(®istry, &KWayland::Client::Registry::outputAnnounced); + QSignalSpy xdgOutputAnnounced(®istry, &KWayland::Client::Registry::xdgOutputAnnounced); + + registry.setEventQueue(m_queue); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + QVERIFY(announced.wait()); + if (xdgOutputAnnounced.count() != 1) { + QVERIFY(xdgOutputAnnounced.wait()); + } + + KWayland::Client::Output output; + QSignalSpy outputChanged(&output, &KWayland::Client::Output::changed); + + output.setup(registry.bindOutput(announced.first().first().value(), announced.first().last().value())); + QVERIFY(outputChanged.wait()); + + std::unique_ptr xdgOutputManager( + registry.createXdgOutputManager(xdgOutputAnnounced.first().first().value(), xdgOutputAnnounced.first().last().value(), this)); + + std::unique_ptr xdgOutput(xdgOutputManager->getXdgOutput(&output, this)); + QSignalSpy xdgOutputChanged(xdgOutput.get(), &KWayland::Client::XdgOutput::changed); + + // check details are sent on client bind + QVERIFY(xdgOutputChanged.wait()); + xdgOutputChanged.clear(); + QCOMPARE(xdgOutput->logicalPosition(), QPoint(11, 12)); + QCOMPARE(xdgOutput->logicalSize(), QSize(1280, 720)); + QCOMPARE(xdgOutput->name(), "testName"); + QCOMPARE(xdgOutput->description(), "foo bar"); + + // change the logical position + m_fakeOutput->moveTo(QPoint(1000, 2000)); + QVERIFY(xdgOutputChanged.wait()); + QCOMPARE(xdgOutputChanged.count(), 1); + QCOMPARE(xdgOutput->logicalPosition(), QPoint(1000, 2000)); + QEXPECT_FAIL("", "KWayland::Client::XdgOutput incorrectly handles partial updates", Continue); + QCOMPARE(xdgOutput->logicalSize(), QSize(1280, 720)); + + // change the logical size + m_fakeOutput->setScale(2); + QVERIFY(xdgOutputChanged.wait()); + QCOMPARE(xdgOutputChanged.count(), 2); + QEXPECT_FAIL("", "KWayland::Client::XdgOutput incorrectly handles partial updates", Continue); + QCOMPARE(xdgOutput->logicalPosition(), QPoint(1000, 2000)); + QCOMPARE(xdgOutput->logicalSize(), QSize(960, 540)); +} + +QTEST_GUILESS_MAIN(TestXdgOutput) +#include "test_xdg_output.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/client/test_xdg_shell.cpp b/local/recipes/kde/kwin/source/autotests/wayland/client/test_xdg_shell.cpp new file mode 100644 index 0000000000..153a25e73d --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/client/test_xdg_shell.cpp @@ -0,0 +1,597 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Gräßlin + SPDX-FileCopyrightText: 2017 David Edmundson + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +// Qt +#include +#include +// client +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/output.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/seat.h" +#include "KWayland/Client/shm_pool.h" +#include "KWayland/Client/surface.h" +#include "KWayland/Client/xdgshell.h" +// server +#include "utils/gravity.h" +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/output.h" +#include "wayland/seat.h" +#include "wayland/surface.h" +#include "wayland/xdgshell.h" + +#include "../../../tests/fakeoutput.h" + +using namespace KWin; + +Q_DECLARE_METATYPE(Qt::MouseButton) + +static const QString s_socketName = QStringLiteral("kwayland-test-xdg_shell-0"); + +class XdgShellTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void init(); + void cleanup(); + + void testCreateSurface(); + void testTitle(); + void testWindowClass(); + void testMaximize(); + void testMinimize(); + void testFullscreen(); + void testShowWindowMenu(); + void testMove(); + void testResize_data(); + void testResize(); + void testTransient(); + void testPing(); + void testClose(); + void testConfigureStates_data(); + void testConfigureStates(); + void testConfigureMultipleAcks(); + +private: + XdgShellInterface *m_xdgShellInterface = nullptr; + KWayland::Client::Compositor *m_compositor = nullptr; + KWayland::Client::XdgShell *m_xdgShell = nullptr; + KWin::Display *m_display = nullptr; + CompositorInterface *m_compositorInterface = nullptr; + std::unique_ptr m_fakeOutput1; + std::unique_ptr m_output1Handle; + OutputInterface *m_output1Interface = nullptr; + std::unique_ptr m_fakeOutput2; + std::unique_ptr m_output2Handle; + OutputInterface *m_output2Interface = nullptr; + SeatInterface *m_seatInterface = nullptr; + KWayland::Client::ConnectionThread *m_connection = nullptr; + QThread *m_thread = nullptr; + KWayland::Client::EventQueue *m_queue = nullptr; + KWayland::Client::ShmPool *m_shmPool = nullptr; + KWayland::Client::Output *m_output1 = nullptr; + KWayland::Client::Output *m_output2 = nullptr; + KWayland::Client::Seat *m_seat = nullptr; +}; + +#define SURFACE \ + QSignalSpy xdgSurfaceCreatedSpy(m_xdgShellInterface, &XdgShellInterface::toplevelCreated); \ + std::unique_ptr surface(m_compositor->createSurface()); \ + std::unique_ptr xdgSurface(m_xdgShell->createSurface(surface.get())); \ + QCOMPARE(xdgSurface->size(), QSize()); \ + QVERIFY(xdgSurfaceCreatedSpy.wait()); \ + auto serverXdgToplevel = xdgSurfaceCreatedSpy.first().first().value(); \ + QVERIFY(serverXdgToplevel); + +void XdgShellTest::init() +{ + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + m_display->createShm(); + m_fakeOutput1 = std::make_unique(); + m_fakeOutput1->setMode(QSize(1024, 768), 60000); + m_output1Handle = std::make_unique(m_fakeOutput1.get()); + m_output1Interface = new OutputInterface(m_display, m_output1Handle.get(), m_display); + m_fakeOutput2 = std::make_unique(); + m_fakeOutput2->setMode(QSize(1024, 768), 60000); + m_output2Handle = std::make_unique(m_fakeOutput2.get()); + m_output2Interface = new OutputInterface(m_display, m_output2Handle.get(), m_display); + m_seatInterface = new SeatInterface(m_display, QStringLiteral("seat0"), m_display); + m_seatInterface->setHasKeyboard(true); + m_seatInterface->setHasPointer(true); + m_seatInterface->setHasTouch(true); + m_compositorInterface = new CompositorInterface(m_display, m_display); + m_xdgShellInterface = new XdgShellInterface(m_display, m_display); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + m_queue->setup(m_connection); + + KWayland::Client::Registry registry; + QSignalSpy interfacesAnnouncedSpy(®istry, &KWayland::Client::Registry::interfacesAnnounced); + QSignalSpy interfaceAnnouncedSpy(®istry, &KWayland::Client::Registry::interfaceAnnounced); + QSignalSpy outputAnnouncedSpy(®istry, &KWayland::Client::Registry::outputAnnounced); + + QSignalSpy xdgShellAnnouncedSpy(®istry, &KWayland::Client::Registry::xdgShellStableAnnounced); + registry.setEventQueue(m_queue); + registry.create(m_connection); + QVERIFY(registry.isValid()); + registry.setup(); + QVERIFY(interfacesAnnouncedSpy.wait()); + + QCOMPARE(outputAnnouncedSpy.count(), 2); + m_output1 = registry.createOutput(outputAnnouncedSpy.first().at(0).value(), outputAnnouncedSpy.first().at(1).value(), this); + m_output2 = registry.createOutput(outputAnnouncedSpy.last().at(0).value(), outputAnnouncedSpy.last().at(1).value(), this); + + m_shmPool = registry.createShmPool(registry.interface(KWayland::Client::Registry::Interface::Shm).name, registry.interface(KWayland::Client::Registry::Interface::Shm).version, this); + QVERIFY(m_shmPool); + QVERIFY(m_shmPool->isValid()); + + m_compositor = + registry.createCompositor(registry.interface(KWayland::Client::Registry::Interface::Compositor).name, registry.interface(KWayland::Client::Registry::Interface::Compositor).version, this); + QVERIFY(m_compositor); + QVERIFY(m_compositor->isValid()); + + m_seat = registry.createSeat(registry.interface(KWayland::Client::Registry::Interface::Seat).name, registry.interface(KWayland::Client::Registry::Interface::Seat).version, this); + QVERIFY(m_seat); + QVERIFY(m_seat->isValid()); + + QCOMPARE(xdgShellAnnouncedSpy.count(), 1); + + m_xdgShell = registry.createXdgShell(registry.interface(KWayland::Client::Registry::Interface::XdgShellStable).name, + registry.interface(KWayland::Client::Registry::Interface::XdgShellStable).version, + this); + QVERIFY(m_xdgShell); + QVERIFY(m_xdgShell->isValid()); +} + +void XdgShellTest::cleanup() +{ +#define CLEANUP(variable) \ + if (variable) { \ + delete variable; \ + variable = nullptr; \ + } + CLEANUP(m_xdgShell) + CLEANUP(m_compositor) + CLEANUP(m_shmPool) + CLEANUP(m_output1) + CLEANUP(m_output2) + CLEANUP(m_seat) + CLEANUP(m_queue) + if (m_connection) { + m_connection->deleteLater(); + m_connection = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + + CLEANUP(m_display) +#undef CLEANUP + + // these are the children of the display + m_compositorInterface = nullptr; + m_xdgShellInterface = nullptr; + m_output1Handle.reset(); + m_output1Interface = nullptr; + m_output2Handle.reset(); + m_output2Interface = nullptr; + m_seatInterface = nullptr; +} + +void XdgShellTest::testCreateSurface() +{ + // this test verifies that we can create a surface + // first created the signal spies for the server + QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated); + QSignalSpy xdgSurfaceCreatedSpy(m_xdgShellInterface, &XdgShellInterface::toplevelCreated); + + // create surface + std::unique_ptr surface(m_compositor->createSurface()); + QVERIFY(surface != nullptr); + QVERIFY(surfaceCreatedSpy.wait()); + auto serverSurface = surfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + // create shell surface + std::unique_ptr xdgSurface(m_xdgShell->createSurface(surface.get())); + QVERIFY(xdgSurface != nullptr); + QVERIFY(xdgSurfaceCreatedSpy.wait()); + // verify base things + auto serverToplevel = xdgSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverToplevel); + QCOMPARE(serverToplevel->title(), QString()); + QCOMPARE(serverToplevel->appId(), QByteArray()); + QCOMPARE(serverToplevel->parentXdgToplevel(), nullptr); + QCOMPARE(serverToplevel->surface(), serverSurface); + + // now let's destroy it + QSignalSpy destroyedSpy(serverToplevel, &QObject::destroyed); + xdgSurface.reset(); + QVERIFY(destroyedSpy.wait()); +} + +void XdgShellTest::testTitle() +{ + // this test verifies that we can change the title of a shell surface + // first create surface + SURFACE + + // should not have a title yet + QCOMPARE(serverXdgToplevel->title(), QString()); + + // lets' change the title + QSignalSpy titleChangedSpy(serverXdgToplevel, &XdgToplevelInterface::titleChanged); + xdgSurface->setTitle(QStringLiteral("foo")); + QVERIFY(titleChangedSpy.wait()); + QCOMPARE(titleChangedSpy.count(), 1); + QCOMPARE(titleChangedSpy.first().first().toString(), QStringLiteral("foo")); + QCOMPARE(serverXdgToplevel->title(), QStringLiteral("foo")); +} + +void XdgShellTest::testWindowClass() +{ + // this test verifies that we can change the window class/app id of a shell surface + // first create surface + SURFACE + + // should not have a window class yet + QCOMPARE(serverXdgToplevel->appId(), QByteArray()); + + // let's change the window class + QSignalSpy windowClassChanged(serverXdgToplevel, &XdgToplevelInterface::appIdChanged); + xdgSurface->setAppId(QByteArrayLiteral("org.kde.xdgsurfacetest")); + QVERIFY(windowClassChanged.wait()); + QCOMPARE(windowClassChanged.count(), 1); + QCOMPARE(windowClassChanged.first().first().toByteArray(), QByteArrayLiteral("org.kde.xdgsurfacetest")); + QCOMPARE(serverXdgToplevel->appId(), QByteArrayLiteral("org.kde.xdgsurfacetest")); +} + +void XdgShellTest::testMaximize() +{ + // this test verifies that the maximize/unmaximize calls work + SURFACE + + QSignalSpy maximizeRequestedSpy(serverXdgToplevel, &XdgToplevelInterface::maximizeRequested); + QSignalSpy unmaximizeRequestedSpy(serverXdgToplevel, &XdgToplevelInterface::unmaximizeRequested); + + xdgSurface->setMaximized(true); + QVERIFY(maximizeRequestedSpy.wait()); + QCOMPARE(maximizeRequestedSpy.count(), 1); + + xdgSurface->setMaximized(false); + QVERIFY(unmaximizeRequestedSpy.wait()); + QCOMPARE(unmaximizeRequestedSpy.count(), 1); +} + +void XdgShellTest::testMinimize() +{ + // this test verifies that the minimize request is delivered + SURFACE + + QSignalSpy minimizeRequestedSpy(serverXdgToplevel, &XdgToplevelInterface::minimizeRequested); + + xdgSurface->requestMinimize(); + QVERIFY(minimizeRequestedSpy.wait()); + QCOMPARE(minimizeRequestedSpy.count(), 1); +} + +void XdgShellTest::testFullscreen() +{ + qRegisterMetaType(); + // this test verifies going to/from fullscreen + SURFACE + + QSignalSpy fullscreenRequestedSpy(serverXdgToplevel, &XdgToplevelInterface::fullscreenRequested); + QSignalSpy unfullscreenRequestedSpy(serverXdgToplevel, &XdgToplevelInterface::unfullscreenRequested); + + // without an output + xdgSurface->setFullscreen(true, nullptr); + QVERIFY(fullscreenRequestedSpy.wait()); + QCOMPARE(fullscreenRequestedSpy.count(), 1); + QVERIFY(!fullscreenRequestedSpy.last().at(0).value()); + + // unset + xdgSurface->setFullscreen(false); + QVERIFY(unfullscreenRequestedSpy.wait()); + QCOMPARE(unfullscreenRequestedSpy.count(), 1); + + // with outputs + xdgSurface->setFullscreen(true, m_output1); + QVERIFY(fullscreenRequestedSpy.wait()); + QCOMPARE(fullscreenRequestedSpy.count(), 2); + QCOMPARE(fullscreenRequestedSpy.last().at(0).value(), m_output1Interface); + + // now other output + xdgSurface->setFullscreen(true, m_output2); + QVERIFY(fullscreenRequestedSpy.wait()); + QCOMPARE(fullscreenRequestedSpy.count(), 3); + QCOMPARE(fullscreenRequestedSpy.last().at(0).value(), m_output2Interface); +} + +void XdgShellTest::testShowWindowMenu() +{ + qRegisterMetaType(); + // this test verifies that the show window menu request works + SURFACE + + // hack: pretend that the xdg-surface had been configured + serverXdgToplevel->sendConfigure(QSize(0, 0), XdgToplevelInterface::States()); + + QSignalSpy windowMenuSpy(serverXdgToplevel, &XdgToplevelInterface::windowMenuRequested); + + // TODO: the serial needs to be a proper one + xdgSurface->requestShowWindowMenu(m_seat, 20, QPoint(30, 40)); + QVERIFY(windowMenuSpy.wait()); + QCOMPARE(windowMenuSpy.count(), 1); + QCOMPARE(windowMenuSpy.first().at(0).value(), m_seatInterface); + QCOMPARE(windowMenuSpy.first().at(1).toPoint(), QPoint(30, 40)); + QCOMPARE(windowMenuSpy.first().at(2).value(), 20u); +} + +void XdgShellTest::testMove() +{ + qRegisterMetaType(); + // this test verifies that the move request works + SURFACE + + // hack: pretend that the xdg-surface had been configured + serverXdgToplevel->sendConfigure(QSize(0, 0), XdgToplevelInterface::States()); + + QSignalSpy moveSpy(serverXdgToplevel, &XdgToplevelInterface::moveRequested); + + // TODO: the serial needs to be a proper one + xdgSurface->requestMove(m_seat, 50); + QVERIFY(moveSpy.wait()); + QCOMPARE(moveSpy.count(), 1); + QCOMPARE(moveSpy.first().at(0).value(), m_seatInterface); + QCOMPARE(moveSpy.first().at(1).value(), 50u); +} + +void XdgShellTest::testResize_data() +{ + QTest::addColumn("edges"); + QTest::addColumn("gravity"); + + QTest::newRow("none") << Qt::Edges() << KWin::Gravity::None; + QTest::newRow("top") << Qt::Edges(Qt::TopEdge) << KWin::Gravity::Top; + QTest::newRow("bottom") << Qt::Edges(Qt::BottomEdge) << KWin::Gravity::Bottom; + QTest::newRow("left") << Qt::Edges(Qt::LeftEdge) << KWin::Gravity::Left; + QTest::newRow("top left") << Qt::Edges(Qt::TopEdge | Qt::LeftEdge) << KWin::Gravity::TopLeft; + QTest::newRow("bottom left") << Qt::Edges(Qt::BottomEdge | Qt::LeftEdge) << KWin::Gravity::BottomLeft; + QTest::newRow("right") << Qt::Edges(Qt::RightEdge) << KWin::Gravity::Right; + QTest::newRow("top right") << Qt::Edges(Qt::TopEdge | Qt::RightEdge) << KWin::Gravity::TopRight; + QTest::newRow("bottom right") << Qt::Edges(Qt::BottomEdge | Qt::RightEdge) << KWin::Gravity::BottomRight; +} + +void XdgShellTest::testResize() +{ + qRegisterMetaType(); + // this test verifies that the resize request works + SURFACE + + // hack: pretend that the xdg-surface had been configured + serverXdgToplevel->sendConfigure(QSize(0, 0), XdgToplevelInterface::States()); + + QSignalSpy resizeSpy(serverXdgToplevel, &XdgToplevelInterface::resizeRequested); + + // TODO: the serial needs to be a proper one + QFETCH(Qt::Edges, edges); + QFETCH(KWin::Gravity::Kind, gravity); + xdgSurface->requestResize(m_seat, 60, edges); + QVERIFY(resizeSpy.wait()); + QCOMPARE(resizeSpy.count(), 1); + QCOMPARE(resizeSpy.first().at(0).value(), m_seatInterface); + QCOMPARE(resizeSpy.first().at(1).value(), gravity); + QCOMPARE(resizeSpy.first().at(2).value(), 60u); +} + +void XdgShellTest::testTransient() +{ + // this test verifies that setting the transient for works + SURFACE + std::unique_ptr surface2(m_compositor->createSurface()); + std::unique_ptr xdgSurface2(m_xdgShell->createSurface(surface2.get())); + QVERIFY(xdgSurfaceCreatedSpy.wait()); + auto serverXdgToplevel2 = xdgSurfaceCreatedSpy.last().first().value(); + QVERIFY(serverXdgToplevel2); + + QVERIFY(!serverXdgToplevel->parentXdgToplevel()); + QVERIFY(!serverXdgToplevel2->parentXdgToplevel()); + + // now make xdsgSurface2 a transient for xdgSurface + QSignalSpy transientForSpy(serverXdgToplevel2, &XdgToplevelInterface::parentXdgToplevelChanged); + xdgSurface2->setTransientFor(xdgSurface.get()); + + QVERIFY(transientForSpy.wait()); + QCOMPARE(transientForSpy.count(), 1); + QCOMPARE(serverXdgToplevel2->parentXdgToplevel(), serverXdgToplevel); + QVERIFY(!serverXdgToplevel->parentXdgToplevel()); + + // unset the transient for + xdgSurface2->setTransientFor(nullptr); + QVERIFY(transientForSpy.wait()); + QCOMPARE(transientForSpy.count(), 2); + QVERIFY(!serverXdgToplevel2->parentXdgToplevel()); + QVERIFY(!serverXdgToplevel->parentXdgToplevel()); +} + +void XdgShellTest::testPing() +{ + // this test verifies that a ping request is sent to the client + SURFACE + + QSignalSpy pingSpy(m_xdgShellInterface, &XdgShellInterface::pongReceived); + + quint32 serial = m_xdgShellInterface->ping(serverXdgToplevel->xdgSurface()); + QVERIFY(pingSpy.wait()); + QCOMPARE(pingSpy.count(), 1); + QCOMPARE(pingSpy.takeFirst().at(0).value(), serial); + + // test of a ping failure + // disconnecting the connection thread to the queue will break the connection and pings will do a timeout + disconnect(m_connection, &KWayland::Client::ConnectionThread::eventsRead, m_queue, &KWayland::Client::EventQueue::dispatch); + m_xdgShellInterface->ping(serverXdgToplevel->xdgSurface()); + QSignalSpy pingDelayedSpy(m_xdgShellInterface, &XdgShellInterface::pingDelayed); + QVERIFY(pingDelayedSpy.wait()); + + QSignalSpy pingTimeoutSpy(m_xdgShellInterface, &XdgShellInterface::pingTimeout); + QVERIFY(pingTimeoutSpy.wait()); +} + +void XdgShellTest::testClose() +{ + // this test verifies that a close request is sent to the client + SURFACE + + QSignalSpy closeSpy(xdgSurface.get(), &KWayland::Client::XdgShellSurface::closeRequested); + + serverXdgToplevel->sendClose(); + QVERIFY(closeSpy.wait()); + QCOMPARE(closeSpy.count(), 1); + + QSignalSpy destroyedSpy(serverXdgToplevel, &XdgToplevelInterface::destroyed); + xdgSurface.reset(); + QVERIFY(destroyedSpy.wait()); +} + +void XdgShellTest::testConfigureStates_data() +{ + QTest::addColumn("serverStates"); + QTest::addColumn("clientStates"); + + const auto sa = XdgToplevelInterface::States(XdgToplevelInterface::State::Activated); + const auto sm = XdgToplevelInterface::States(XdgToplevelInterface::State::Maximized); + const auto sf = XdgToplevelInterface::States(XdgToplevelInterface::State::FullScreen); + const auto sr = XdgToplevelInterface::States(XdgToplevelInterface::State::Resizing); + + const auto ca = KWayland::Client::XdgShellSurface::States(KWayland::Client::XdgShellSurface::State::Activated); + const auto cm = KWayland::Client::XdgShellSurface::States(KWayland::Client::XdgShellSurface::State::Maximized); + const auto cf = KWayland::Client::XdgShellSurface::States(KWayland::Client::XdgShellSurface::State::Fullscreen); + const auto cr = KWayland::Client::XdgShellSurface::States(KWayland::Client::XdgShellSurface::State::Resizing); + + QTest::newRow("none") << XdgToplevelInterface::States() << KWayland::Client::XdgShellSurface::States(); + QTest::newRow("Active") << sa << ca; + QTest::newRow("Maximize") << sm << cm; + QTest::newRow("Fullscreen") << sf << cf; + QTest::newRow("Resizing") << sr << cr; + + QTest::newRow("Active/Maximize") << (sa | sm) << (ca | cm); + QTest::newRow("Active/Fullscreen") << (sa | sf) << (ca | cf); + QTest::newRow("Active/Resizing") << (sa | sr) << (ca | cr); + QTest::newRow("Maximize/Fullscreen") << (sm | sf) << (cm | cf); + QTest::newRow("Maximize/Resizing") << (sm | sr) << (cm | cr); + QTest::newRow("Fullscreen/Resizing") << (sf | sr) << (cf | cr); + + QTest::newRow("Active/Maximize/Fullscreen") << (sa | sm | sf) << (ca | cm | cf); + QTest::newRow("Active/Maximize/Resizing") << (sa | sm | sr) << (ca | cm | cr); + QTest::newRow("Maximize/Fullscreen|Resizing") << (sm | sf | sr) << (cm | cf | cr); + + QTest::newRow("Active/Maximize/Fullscreen/Resizing") << (sa | sm | sf | sr) << (ca | cm | cf | cr); +} + +void XdgShellTest::testConfigureStates() +{ + qRegisterMetaType(); + // this test verifies that configure states works + SURFACE + + QSignalSpy configureSpy(xdgSurface.get(), &KWayland::Client::XdgShellSurface::configureRequested); + + QFETCH(XdgToplevelInterface::States, serverStates); + serverXdgToplevel->sendConfigure(QSize(0, 0), serverStates); + QVERIFY(configureSpy.wait()); + QCOMPARE(configureSpy.count(), 1); + QCOMPARE(configureSpy.first().at(0).toSize(), QSize(0, 0)); + QTEST(configureSpy.first().at(1).value(), "clientStates"); + QCOMPARE(configureSpy.first().at(2).value(), m_display->serial()); + + QSignalSpy ackSpy(serverXdgToplevel->xdgSurface(), &XdgSurfaceInterface::configureAcknowledged); + + xdgSurface->ackConfigure(configureSpy.first().at(2).value()); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(ackSpy.wait()); + QCOMPARE(ackSpy.count(), 1); + QCOMPARE(ackSpy.first().first().value(), configureSpy.first().at(2).value()); +} + +void XdgShellTest::testConfigureMultipleAcks() +{ + qRegisterMetaType(); + // this test verifies that with multiple configure requests the last acknowledged one acknowledges all + SURFACE + + QSignalSpy configureSpy(xdgSurface.get(), &KWayland::Client::XdgShellSurface::configureRequested); + QSignalSpy sizeChangedSpy(xdgSurface.get(), &KWayland::Client::XdgShellSurface::sizeChanged); + QSignalSpy ackSpy(serverXdgToplevel->xdgSurface(), &XdgSurfaceInterface::configureAcknowledged); + + serverXdgToplevel->sendConfigure(QSize(10, 20), XdgToplevelInterface::States()); + const quint32 serial1 = m_display->serial(); + serverXdgToplevel->sendConfigure(QSize(20, 30), XdgToplevelInterface::States()); + const quint32 serial2 = m_display->serial(); + QVERIFY(serial1 != serial2); + serverXdgToplevel->sendConfigure(QSize(30, 40), XdgToplevelInterface::States()); + const quint32 serial3 = m_display->serial(); + QVERIFY(serial1 != serial3); + QVERIFY(serial2 != serial3); + + QVERIFY(configureSpy.wait()); + QCOMPARE(configureSpy.count(), 3); + QCOMPARE(configureSpy.at(0).at(0).toSize(), QSize(10, 20)); + QCOMPARE(configureSpy.at(0).at(1).value(), KWayland::Client::XdgShellSurface::States()); + QCOMPARE(configureSpy.at(0).at(2).value(), serial1); + QCOMPARE(configureSpy.at(1).at(0).toSize(), QSize(20, 30)); + QCOMPARE(configureSpy.at(1).at(1).value(), KWayland::Client::XdgShellSurface::States()); + QCOMPARE(configureSpy.at(1).at(2).value(), serial2); + QCOMPARE(configureSpy.at(2).at(0).toSize(), QSize(30, 40)); + QCOMPARE(configureSpy.at(2).at(1).value(), KWayland::Client::XdgShellSurface::States()); + QCOMPARE(configureSpy.at(2).at(2).value(), serial3); + QCOMPARE(sizeChangedSpy.count(), 3); + QCOMPARE(sizeChangedSpy.at(0).at(0).toSize(), QSize(10, 20)); + QCOMPARE(sizeChangedSpy.at(1).at(0).toSize(), QSize(20, 30)); + QCOMPARE(sizeChangedSpy.at(2).at(0).toSize(), QSize(30, 40)); + QCOMPARE(xdgSurface->size(), QSize(30, 40)); + + xdgSurface->ackConfigure(serial3); + surface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(ackSpy.wait()); + QCOMPARE(ackSpy.count(), 1); + QCOMPARE(ackSpy.last().first().value(), serial3); + + // configure once more with a null size + serverXdgToplevel->sendConfigure(QSize(0, 0), XdgToplevelInterface::States()); + // should not change size + QVERIFY(configureSpy.wait()); + QCOMPARE(configureSpy.count(), 4); + QCOMPARE(sizeChangedSpy.count(), 3); + QCOMPARE(xdgSurface->size(), QSize(30, 40)); +} + +QTEST_GUILESS_MAIN(XdgShellTest) +#include "test_xdg_shell.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/server/CMakeLists.txt b/local/recipes/kde/kwin/source/autotests/wayland/server/CMakeLists.txt new file mode 100644 index 0000000000..ce7cb5464b --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/server/CMakeLists.txt @@ -0,0 +1,162 @@ +######################################################## +# Test WaylandServerDisplay +######################################################## +set( testWaylandServerDisplay_SRCS + test_display.cpp + ) +add_executable(testWaylandServerDisplay ${testWaylandServerDisplay_SRCS}) +target_link_libraries( testWaylandServerDisplay Qt::Test Qt::Gui kwin Wayland::Server) +add_test(NAME kwayland-testWaylandServerDisplay COMMAND testWaylandServerDisplay) +ecm_mark_as_test(testWaylandServerDisplay) + +######################################################## +# Test WaylandServerSeat +######################################################## +set( testWaylandServerSeat_SRCS + test_seat.cpp + ) +add_executable(testWaylandServerSeat ${testWaylandServerSeat_SRCS}) +target_link_libraries( testWaylandServerSeat Qt::Test Qt::Gui kwin Wayland::Server) +add_test(NAME kwayland-testWaylandServerSeat COMMAND testWaylandServerSeat) +ecm_mark_as_test(testWaylandServerSeat) + +######################################################## +# Test No XDG_RUNTIME_DIR +######################################################## +add_executable(testNoXdgRuntimeDir test_no_xdg_runtime_dir.cpp) +target_link_libraries( testNoXdgRuntimeDir Qt::Test kwin) +add_test(NAME kwayland-testNoXdgRuntimeDir COMMAND testNoXdgRuntimeDir) +ecm_mark_as_test(testNoXdgRuntimeDir) + +######################################################## +# Test Tablet Interface +######################################################## +add_executable(testTabletInterface) +qt6_generate_wayland_protocol_client_sources(testTabletInterface + PRIVATE_CODE + FILES + ${WaylandProtocols_DATADIR}/stable/tablet/tablet-v2.xml + ) +target_sources(testTabletInterface PRIVATE test_tablet_interface.cpp ${TABLET_SRCS}) +target_link_libraries( testTabletInterface Qt::Test kwin Plasma::KWaylandClient Wayland::Client) +add_test(NAME kwayland-testTabletInterface COMMAND testTabletInterface) +ecm_mark_as_test(testTabletInterface) + +######################################################## +# Test DataControlInterface +######################################################## +add_executable(testDataControlInterface test_datacontrol_interface.cpp ${DATACONTROL_SRCS}) +qt6_generate_wayland_protocol_client_sources(testDataControlInterface + PRIVATE_CODE + FILES + ${WaylandProtocols_DATADIR}/staging/ext-data-control/ext-data-control-v1.xml +) +target_sources(testDataControlInterface PRIVATE test_datacontrol_interface.cpp ${DATACONTROL_SRCS}) +target_link_libraries( testDataControlInterface Qt::Test kwin Plasma::KWaylandClient Wayland::Client) +add_test(NAME kwayland-testDataControlInterface COMMAND testDataControlInterface) +ecm_mark_as_test(testDataControlInterface) + +######################################################## +# Test Keyboard Shortcuts Inhibitor Interface +######################################################## +add_executable(testKeyboardShortcutsInhibitorInterface) +qt6_generate_wayland_protocol_client_sources(testKeyboardShortcutsInhibitorInterface + PRIVATE_CODE + FILES + ${WaylandProtocols_DATADIR}/unstable/keyboard-shortcuts-inhibit/keyboard-shortcuts-inhibit-unstable-v1.xml +) +target_sources(testKeyboardShortcutsInhibitorInterface PRIVATE test_keyboard_shortcuts_inhibitor_interface.cpp ${KEYBOARD_SHORTCUTS_INHIBITOR_SRCS}) +target_link_libraries(testKeyboardShortcutsInhibitorInterface Qt::Test kwin Plasma::KWaylandClient Wayland::Client) +add_test(NAME kwayland-testKeyboardShortcutsInhibitorInterface COMMAND testKeyboardShortcutsInhibitorInterface) +ecm_mark_as_test(testKeyboardShortcutsInhibitorInterface) + +######################################################## +# Test Viewporter Interface +######################################################## +add_executable(testViewporterInterface) +qt6_generate_wayland_protocol_client_sources(testViewporterInterface + PRIVATE_CODE + FILES + ${WaylandProtocols_DATADIR}/stable/viewporter/viewporter.xml +) +target_sources(testViewporterInterface PRIVATE test_viewporter_interface.cpp ${VIEWPORTER_SRCS}) +target_link_libraries(testViewporterInterface Qt::Test kwin Plasma::KWaylandClient Wayland::Client) +add_test(NAME kwayland-testViewporterInterface COMMAND testViewporterInterface) +ecm_mark_as_test(testViewporterInterface) + +######################################################## +# Test ScreencastV1Interface +######################################################## +add_executable(testScreencastV1Interface) +qt6_generate_wayland_protocol_client_sources(testScreencastV1Interface + PRIVATE_CODE + FILES + ${PLASMA_WAYLAND_PROTOCOLS_DIR}/zkde-screencast-unstable-v1.xml +) +target_sources(testScreencastV1Interface PRIVATE test_screencast.cpp ${SCREENCAST_SRCS}) +target_link_libraries(testScreencastV1Interface Qt::Test kwin Wayland::Client Plasma::KWaylandClient) +add_test(NAME kwayland-testScreencastV1Interface COMMAND testScreencastV1Interface) +ecm_mark_as_test(testScreencastV1Interface) + +######################################################## +# Test InputMethod Interface +######################################################## +add_executable(testInputMethodInterface) +qt6_generate_wayland_protocol_client_sources(testInputMethodInterface + NO_INCLUDE_CORE_ONLY + FILES + ${WaylandProtocols_DATADIR}/unstable/input-method/input-method-unstable-v1.xml + PRIVATE_CODE +) +target_sources(testInputMethodInterface PRIVATE + test_inputmethod_interface.cpp + ${PROJECT_SOURCE_DIR}/tests/fakeoutput.cpp + ${INPUTMETHOD_SRCS} +) +target_link_libraries(testInputMethodInterface Qt::Test kwin Plasma::KWaylandClient Wayland::Client) +add_test(NAME kwayland-testInputMethodInterface COMMAND testInputMethodInterface) +ecm_mark_as_test(testInputMethodInterface) + +######################################################## +# Test LayerShellV1 Interface +######################################################## +add_executable(testLayerShellV1Interface) +qt6_generate_wayland_protocol_client_sources(testLayerShellV1Interface + PRIVATE_CODE + FILES + ${PROJECT_SOURCE_DIR}/src/wayland/protocols/wlr-layer-shell-unstable-v1.xml + ${WaylandProtocols_DATADIR}/stable/xdg-shell/xdg-shell.xml +) +target_sources(testLayerShellV1Interface PRIVATE test_layershellv1_interface.cpp ${LAYERSHELLV1_SRCS}) +target_link_libraries(testLayerShellV1Interface Qt::Test kwin Plasma::KWaylandClient Wayland::Client) +add_test(NAME kwayland-testLayerShellV1Interface COMMAND testLayerShellV1Interface) +ecm_mark_as_test(testLayerShellV1Interface) + + +######################################################## +# Test TextInputV3 Interface +######################################################## +add_executable(testTextInputV3Interface) +qt6_generate_wayland_protocol_client_sources(testTextInputV3Interface + PRIVATE_CODE + FILES + ${WaylandProtocols_DATADIR}/unstable/text-input/text-input-unstable-v3.xml +) +target_sources(testTextInputV3Interface PRIVATE test_textinputv3_interface.cpp ${TEXTINPUTV3_SRCS}) +target_link_libraries(testTextInputV3Interface Qt::Test kwin Plasma::KWaylandClient Wayland::Client) +add_test(NAME kwayland-testTextInputV3Interface COMMAND testTextInputV3Interface) +ecm_mark_as_test(testTextInputV3Interface) + +######################################################## +# Test TextInputV1 Interface +######################################################## +add_executable(testTextInputV1Interface) +qt6_generate_wayland_protocol_client_sources(testTextInputV1Interface + PRIVATE_CODE + FILES + ${WaylandProtocols_DATADIR}/unstable/text-input/text-input-unstable-v1.xml +) +target_sources(testTextInputV1Interface PRIVATE test_textinputv1_interface.cpp ${TEXTINPUTV1_SRCS}) +target_link_libraries(testTextInputV1Interface Qt::Test kwin Plasma::KWaylandClient Wayland::Client) +add_test(NAME kwayland-testTextInputV1Interface COMMAND testTextInputV1Interface) +ecm_mark_as_test(testTextInputV1Interface) diff --git a/local/recipes/kde/kwin/source/autotests/wayland/server/test_datacontrol_interface.cpp b/local/recipes/kde/kwin/source/autotests/wayland/server/test_datacontrol_interface.cpp new file mode 100644 index 0000000000..149a76b32e --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/server/test_datacontrol_interface.cpp @@ -0,0 +1,391 @@ +/* + SPDX-FileCopyrightText: 2020 David Edmundson + SPDX-FileCopyrightText: 2021 David Redondo + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +// Qt +#include +#include +#include +#include + +// WaylandServer +#include "wayland/compositor.h" +#include "wayland/datacontroldevice_v1.h" +#include "wayland/datacontroldevicemanager_v1.h" +#include "wayland/datacontrolsource_v1.h" +#include "wayland/display.h" +#include "wayland/seat.h" + +#include +#include +#include +#include +#include + +#include "qwayland-ext-data-control-v1.h" + +using namespace KWin; + +// Faux-client API for tests + +Q_DECLARE_OPAQUE_POINTER(::ext_data_control_offer_v1 *) +Q_DECLARE_METATYPE(::ext_data_control_offer_v1 *) + +class DataControlDeviceManager : public QObject, public QtWayland::ext_data_control_manager_v1 +{ + Q_OBJECT +}; + +class DataControlOffer : public QObject, public QtWayland::ext_data_control_offer_v1 +{ + Q_OBJECT +public: + ~DataControlOffer() + { + destroy(); + } + QStringList receivedOffers() + { + return m_receivedOffers; + } + +protected: + virtual void ext_data_control_offer_v1_offer(const QString &mime_type) override + { + m_receivedOffers << mime_type; + } + +private: + QStringList m_receivedOffers; +}; + +class DataControlDevice : public QObject, public QtWayland::ext_data_control_device_v1 +{ + Q_OBJECT +public: + ~DataControlDevice() + { + destroy(); + } +Q_SIGNALS: + void dataControlOffer(DataControlOffer *offer); // our event receives a new ID, so we make a new object + void selection(struct ::ext_data_control_offer_v1 *id); + void primary_selection(struct ::ext_data_control_offer_v1 *id); + +protected: + void ext_data_control_device_v1_data_offer(struct ::ext_data_control_offer_v1 *id) override + { + auto offer = new DataControlOffer; + offer->init(id); + Q_EMIT dataControlOffer(offer); + } + + void ext_data_control_device_v1_selection(struct ::ext_data_control_offer_v1 *id) override + { + Q_EMIT selection(id); + } + + void ext_data_control_device_v1_primary_selection(struct ::ext_data_control_offer_v1 *id) override + { + Q_EMIT primary_selection(id); + } +}; + +class DataControlSource : public QObject, public QtWayland::ext_data_control_source_v1 +{ + Q_OBJECT +public: + ~DataControlSource() + { + destroy(); + } + +public: +}; + +class TestDataSource : public AbstractDataSource +{ + Q_OBJECT +public: + TestDataSource() + : AbstractDataSource(nullptr) + { + } + ~TestDataSource() + { + Q_EMIT aboutToBeDestroyed(); + } + void requestData(const QString &mimeType, FileDescriptor fd) override + { + } + void cancel() override{}; + QStringList mimeTypes() const override + { + return {"text/test1", "text/test2"}; + } +}; + +// The test itself + +class DataControlInterfaceTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testCopyToControl(); + void testCopyToControlPrimarySelection(); + void testCopyFromControl(); + void testCopyFromControlPrimarySelection(); + void testKlipperCase(); + +private: + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::EventQueue *m_queue; + KWayland::Client::Compositor *m_clientCompositor; + KWayland::Client::Seat *m_clientSeat = nullptr; + + QThread *m_thread; + KWin::Display *m_display; + SeatInterface *m_seat; + CompositorInterface *m_serverCompositor; + + DataControlDeviceManagerV1Interface *m_dataControlDeviceManagerInterface; + + DataControlDeviceManager *m_dataControlDeviceManager; + + QList m_surfaces; +}; + +static const QString s_socketName = QStringLiteral("kwin-wayland-datacontrol-test-0"); + +void DataControlInterfaceTest::initTestCase() +{ + qRegisterMetaType<::ext_data_control_offer_v1 *>(); +} + +void DataControlInterfaceTest::init() +{ + m_display = new KWin::Display(); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + + m_seat = new SeatInterface(m_display, QStringLiteral("seat0"), this); + m_serverCompositor = new CompositorInterface(m_display, this); + m_dataControlDeviceManagerInterface = new DataControlDeviceManagerV1Interface(m_display, this); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + QVERIFY(!m_connection->connections().isEmpty()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + KWayland::Client::Registry registry; + connect(®istry, &KWayland::Client::Registry::interfaceAnnounced, this, [this, ®istry](const QByteArray &interface, quint32 name, quint32 version) { + if (interface == "ext_data_control_manager_v1") { + m_dataControlDeviceManager = new DataControlDeviceManager; + m_dataControlDeviceManager->init(registry.registry(), name, version); + } + }); + connect(®istry, &KWayland::Client::Registry::seatAnnounced, this, [this, ®istry](quint32 name, quint32 version) { + m_clientSeat = registry.createSeat(name, version); + }); + registry.setEventQueue(m_queue); + QSignalSpy compositorSpy(®istry, &KWayland::Client::Registry::compositorAnnounced); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + wl_display_flush(m_connection->display()); + + QVERIFY(compositorSpy.wait()); + m_clientCompositor = registry.createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + QVERIFY(m_clientCompositor->isValid()); + + QVERIFY(m_dataControlDeviceManager); +} + +void DataControlInterfaceTest::cleanup() +{ +#define CLEANUP(variable) \ + if (variable) { \ + delete variable; \ + variable = nullptr; \ + } + CLEANUP(m_dataControlDeviceManager) + CLEANUP(m_clientSeat) + CLEANUP(m_clientCompositor) + CLEANUP(m_queue) + if (m_connection) { + m_connection->deleteLater(); + m_connection = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + CLEANUP(m_display) +#undef CLEANUP + + // these are the children of the display + m_seat = nullptr; + m_serverCompositor = nullptr; +} + +void DataControlInterfaceTest::testCopyToControl() +{ + // we set a dummy data source on the seat using abstract client directly + // then confirm we receive the offer despite not having a surface + + std::unique_ptr dataControlDevice(new DataControlDevice); + dataControlDevice->init(m_dataControlDeviceManager->get_data_device(*m_clientSeat)); + + QSignalSpy newOfferSpy(dataControlDevice.get(), &DataControlDevice::dataControlOffer); + QSignalSpy selectionSpy(dataControlDevice.get(), &DataControlDevice::selection); + + std::unique_ptr testSelection(new TestDataSource); + m_seat->setSelection(testSelection.get(), m_display->nextSerial()); + + // selection will be sent after we've been sent a new offer object and the mimes have been sent to that object + selectionSpy.wait(); + + QCOMPARE(newOfferSpy.count(), 1); + std::unique_ptr offer(newOfferSpy.first().first().value()); + QCOMPARE(selectionSpy.first().first().value(), offer->object()); + + QCOMPARE(offer->receivedOffers().count(), 2); + QCOMPARE(offer->receivedOffers()[0], "text/test1"); + QCOMPARE(offer->receivedOffers()[1], "text/test2"); +} + +void DataControlInterfaceTest::testCopyToControlPrimarySelection() +{ + // we set a dummy data source on the seat using abstract client directly + // then confirm we receive the offer despite not having a surface + + std::unique_ptr dataControlDevice(new DataControlDevice); + dataControlDevice->init(m_dataControlDeviceManager->get_data_device(*m_clientSeat)); + + QSignalSpy newOfferSpy(dataControlDevice.get(), &DataControlDevice::dataControlOffer); + QSignalSpy selectionSpy(dataControlDevice.get(), &DataControlDevice::primary_selection); + + std::unique_ptr testSelection(new TestDataSource); + m_seat->setPrimarySelection(testSelection.get(), m_display->nextSerial()); + + // selection will be sent after we've been sent a new offer object and the mimes have been sent to that object + selectionSpy.wait(); + + QCOMPARE(newOfferSpy.count(), 1); + std::unique_ptr offer(newOfferSpy.first().first().value()); + QCOMPARE(selectionSpy.first().first().value(), offer->object()); + + QCOMPARE(offer->receivedOffers().count(), 2); + QCOMPARE(offer->receivedOffers()[0], "text/test1"); + QCOMPARE(offer->receivedOffers()[1], "text/test2"); +} + +void DataControlInterfaceTest::testCopyFromControl() +{ + // we create a data device and set a selection + // then confirm the server sees the new selection + QSignalSpy serverSelectionChangedSpy(m_seat, &SeatInterface::selectionChanged); + + std::unique_ptr dataControlDevice(new DataControlDevice); + dataControlDevice->init(m_dataControlDeviceManager->get_data_device(*m_clientSeat)); + + std::unique_ptr source(new DataControlSource); + source->init(m_dataControlDeviceManager->create_data_source()); + source->offer("cheese/test1"); + source->offer("cheese/test2"); + + dataControlDevice->set_selection(source->object()); + + serverSelectionChangedSpy.wait(); + QVERIFY(m_seat->selection()); + QCOMPARE(m_seat->selection()->mimeTypes(), QStringList({"cheese/test1", "cheese/test2"})); +} + +void DataControlInterfaceTest::testCopyFromControlPrimarySelection() +{ + // we create a data device and set a selection + // then confirm the server sees the new selection + QSignalSpy serverSelectionChangedSpy(m_seat, &SeatInterface::primarySelectionChanged); + + std::unique_ptr dataControlDevice(new DataControlDevice); + dataControlDevice->init(m_dataControlDeviceManager->get_data_device(*m_clientSeat)); + + std::unique_ptr source(new DataControlSource); + source->init(m_dataControlDeviceManager->create_data_source()); + source->offer("cheese/test1"); + source->offer("cheese/test2"); + + dataControlDevice->set_primary_selection(source->object()); + + serverSelectionChangedSpy.wait(); + QVERIFY(m_seat->primarySelection()); + QCOMPARE(m_seat->primarySelection()->mimeTypes(), QStringList({"cheese/test1", "cheese/test2"})); +} + +void DataControlInterfaceTest::testKlipperCase() +{ + // This tests the setup of klipper's real world operation and a race with a common pattern seen between clients and klipper + // The client's behaviour is faked with direct access to the seat + + std::unique_ptr dataControlDevice(new DataControlDevice); + dataControlDevice->init(m_dataControlDeviceManager->get_data_device(*m_clientSeat)); + + QSignalSpy newOfferSpy(dataControlDevice.get(), &DataControlDevice::dataControlOffer); + QSignalSpy selectionSpy(dataControlDevice.get(), &DataControlDevice::selection); + QSignalSpy serverSelectionChangedSpy(m_seat, &SeatInterface::selectionChanged); + + // Client A has a data source + std::unique_ptr testSelection(new TestDataSource); + m_seat->setSelection(testSelection.get(), m_display->nextSerial()); + + // klipper gets it + selectionSpy.wait(); + + // Client A deletes it + testSelection.reset(); + + // klipper gets told + selectionSpy.wait(); + + // Client A sets something else + std::unique_ptr testSelection2(new TestDataSource); + m_seat->setSelection(testSelection2.get(), m_display->nextSerial()); + + // Meanwhile klipper updates with the old content + std::unique_ptr source(new DataControlSource); + source->init(m_dataControlDeviceManager->create_data_source()); + source->offer("fromKlipper/test1"); + source->offer("application/x-kde-onlyReplaceEmpty"); + + dataControlDevice->set_selection(source->object()); + + QVERIFY(!serverSelectionChangedSpy.wait(10)); + QCOMPARE(m_seat->selection(), testSelection2.get()); +} + +QTEST_GUILESS_MAIN(DataControlInterfaceTest) + +#include "test_datacontrol_interface.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/server/test_display.cpp b/local/recipes/kde/kwin/source/autotests/wayland/server/test_display.cpp new file mode 100644 index 0000000000..fe994370a9 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/server/test_display.cpp @@ -0,0 +1,172 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// WaylandServer +#include "wayland/clientconnection.h" +#include "wayland/display.h" +// Wayland +#include +// system +#include +#include +#include + +using namespace KWin; + +class TestWaylandServerDisplay : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testSocketName(); + void testStartStop(); + void testClientConnection(); + void testConnectNoSocket(); + void testAutoSocketName(); +}; + +void TestWaylandServerDisplay::testSocketName() +{ + KWin::Display display; + QSignalSpy changedSpy(&display, &KWin::Display::socketNamesChanged); + QCOMPARE(display.socketNames(), QStringList()); + const QString testSName = QStringLiteral("fooBar"); + display.addSocketName(testSName); + QCOMPARE(display.socketNames(), QStringList{testSName}); + QCOMPARE(changedSpy.count(), 1); + + // changing to same name again should not emit signal + display.addSocketName(testSName); + QCOMPARE(changedSpy.count(), 1); +} + +void TestWaylandServerDisplay::testStartStop() +{ + const QString testSocketName = QStringLiteral("kwin-wayland-server-display-test-0"); + QDir runtimeDir(qgetenv("XDG_RUNTIME_DIR")); + QVERIFY(runtimeDir.exists()); + QVERIFY(!runtimeDir.exists(testSocketName)); + + std::unique_ptr display(new KWin::Display); + QSignalSpy runningSpy(display.get(), &KWin::Display::runningChanged); + display->addSocketName(testSocketName); + QVERIFY(!display->isRunning()); + display->start(); + // QVERIFY(runningSpy.wait()); + QCOMPARE(runningSpy.count(), 1); + QVERIFY(runningSpy.first().first().toBool()); + QVERIFY(display->isRunning()); + QVERIFY(runtimeDir.exists(testSocketName)); + + display.reset(); + QVERIFY(!runtimeDir.exists(testSocketName)); +} + +void TestWaylandServerDisplay::testClientConnection() +{ + KWin::Display display; + display.addSocketName(QStringLiteral("kwin-wayland-server-display-test-client-connection")); + display.start(); + QSignalSpy connectedSpy(&display, &KWin::Display::clientConnected); + + int sv[2]; + QVERIFY(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) >= 0); + + auto client = wl_client_create(display, sv[0]); + QVERIFY(client); + QCOMPARE(connectedSpy.count(), 1); + + ClientConnection *connection = ClientConnection::get(client); + QVERIFY(connection); + QCOMPARE(connectedSpy.first().first().value(), connection); + QCOMPARE(connection->client(), client); + if (getuid() == 0) { + QEXPECT_FAIL("", "Please don't run test as root", Continue); + } + QVERIFY(connection->userId() != 0); + if (getgid() == 0) { + QEXPECT_FAIL("", "Please don't run test as root", Continue); + } + QVERIFY(connection->groupId() != 0); + QVERIFY(connection->processId() != 0); + QCOMPARE(connection->display(), &display); + QCOMPARE(connection->executablePath(), QCoreApplication::applicationFilePath()); + QCOMPARE((wl_client *)*connection, client); + const ClientConnection &constRef = *connection; + QCOMPARE((wl_client *)constRef, client); + + QCOMPARE(connection, ClientConnection::get(client)); + QCOMPARE(connectedSpy.count(), 1); + + // create a second client + int sv2[2]; + QVERIFY(socketpair(AF_UNIX, SOCK_STREAM, 0, sv2) >= 0); + auto client2 = display.createClient(sv2[0]); + QVERIFY(client2); + ClientConnection *connection2 = ClientConnection::get(client2->client()); + QVERIFY(connection2); + QCOMPARE(connection2, client2); + QCOMPARE(connectedSpy.count(), 2); + QCOMPARE(connectedSpy.first().first().value(), connection); + QCOMPARE(connectedSpy.last().first().value(), connection2); + QCOMPARE(connectedSpy.last().first().value(), client2); + + // and destroy + QSignalSpy clientDestroyedSpy(connection, &QObject::destroyed); + wl_client_destroy(client); + QCOMPARE(clientDestroyedSpy.count(), 1); + QSignalSpy client2DestroyedSpy(client2, &QObject::destroyed); + client2->destroy(); + QCOMPARE(client2DestroyedSpy.count(), 1); + close(sv[0]); + close(sv[1]); + close(sv2[0]); + close(sv2[1]); +} + +void TestWaylandServerDisplay::testConnectNoSocket() +{ + KWin::Display display; + display.start(); + QVERIFY(display.isRunning()); + + // let's try connecting a client + int sv[2]; + QVERIFY(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) >= 0); + auto client = display.createClient(sv[0]); + QVERIFY(client); + + wl_client_destroy(client->client()); + close(sv[0]); + close(sv[1]); +} + +void TestWaylandServerDisplay::testAutoSocketName() +{ + QTemporaryDir runtimeDir; + QVERIFY(runtimeDir.isValid()); + QVERIFY(qputenv("XDG_RUNTIME_DIR", runtimeDir.path().toUtf8())); + + KWin::Display display0; + QSignalSpy socketNameChangedSpy0(&display0, &KWin::Display::socketNamesChanged); + QVERIFY(socketNameChangedSpy0.isValid()); + QVERIFY(display0.addSocketName()); + display0.start(); + QVERIFY(display0.isRunning()); + QCOMPARE(socketNameChangedSpy0.count(), 1); + + KWin::Display display1; + QSignalSpy socketNameChangedSpy1(&display1, &KWin::Display::socketNamesChanged); + QVERIFY(socketNameChangedSpy1.isValid()); + QVERIFY(display1.addSocketName()); + display1.start(); + QVERIFY(display1.isRunning()); + QCOMPARE(socketNameChangedSpy1.count(), 1); +} + +QTEST_GUILESS_MAIN(TestWaylandServerDisplay) +#include "test_display.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/server/test_inputmethod_interface.cpp b/local/recipes/kde/kwin/source/autotests/wayland/server/test_inputmethod_interface.cpp new file mode 100644 index 0000000000..1c16bec5b1 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/server/test_inputmethod_interface.cpp @@ -0,0 +1,640 @@ +/* + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + SPDX-FileCopyrightText: 2020 Bhushan Shah + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +#include +#include + +#include "../../../tests/fakeoutput.h" + +// WaylandServer +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/inputmethod_v1.h" +#include "wayland/output.h" +#include "wayland/seat.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/keyboard.h" +#include "KWayland/Client/output.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/seat.h" +#include "KWayland/Client/surface.h" + +#include "qwayland-input-method-unstable-v1.h" +#include "qwayland-server-text-input-unstable-v1.h" + +#include + +using namespace KWin; + +class InputPanelSurface : public QObject, public QtWayland::zwp_input_panel_surface_v1 +{ + Q_OBJECT +public: + InputPanelSurface(::zwp_input_panel_surface_v1 *t) + : QtWayland::zwp_input_panel_surface_v1(t) + { + } +}; + +class InputPanel : public QtWayland::zwp_input_panel_v1 +{ +public: + InputPanel(struct ::wl_registry *registry, int id, int version) + : QtWayland::zwp_input_panel_v1(registry, id, version) + { + } + + InputPanelSurface *panelForSurface(KWayland::Client::Surface *surface) + { + auto panelSurface = new InputPanelSurface(get_input_panel_surface(*surface)); + QObject::connect(surface, &QObject::destroyed, panelSurface, &QObject::deleteLater); + return panelSurface; + } +}; + +class InputMethodV1Context : public QObject, public QtWayland::zwp_input_method_context_v1 +{ + Q_OBJECT +public: + quint32 contentPurpose() + { + return imPurpose; + } + quint32 contentHints() + { + return imHint; + } +Q_SIGNALS: + void content_type_changed(); + void invoke_action(quint32 button, quint32 index); + void preferred_language(QString lang); + void surrounding_text(QString lang, quint32 cursor, quint32 anchor); + void reset(); + +protected: + void zwp_input_method_context_v1_content_type(uint32_t hint, uint32_t purpose) override + { + imHint = hint; + imPurpose = purpose; + Q_EMIT content_type_changed(); + } + void zwp_input_method_context_v1_invoke_action(uint32_t button, uint32_t index) override + { + Q_EMIT invoke_action(button, index); + } + void zwp_input_method_context_v1_preferred_language(const QString &language) override + { + Q_EMIT preferred_language(language); + } + void zwp_input_method_context_v1_surrounding_text(const QString &text, uint32_t cursor, uint32_t anchor) override + { + Q_EMIT surrounding_text(text, cursor, anchor); + } + void zwp_input_method_context_v1_reset() override + { + Q_EMIT reset(); + } + +private: + quint32 imHint = 0; + quint32 imPurpose = 0; +}; + +class InputMethodV1 : public QObject, public QtWayland::zwp_input_method_v1 +{ + Q_OBJECT +public: + InputMethodV1(struct ::wl_registry *registry, int id, int version) + : QtWayland::zwp_input_method_v1(registry, id, version) + { + } + InputMethodV1Context *context() + { + return m_context; + } + +Q_SIGNALS: + void activated(); + void deactivated(); + +protected: + void zwp_input_method_v1_activate(struct ::zwp_input_method_context_v1 *context) override + { + m_context = new InputMethodV1Context(); + m_context->init(context); + Q_EMIT activated(); + }; + void zwp_input_method_v1_deactivate(struct ::zwp_input_method_context_v1 *context) override + { + delete m_context; + m_context = nullptr; + Q_EMIT deactivated(); + }; + +private: + InputMethodV1Context *m_context; +}; + +class TestInputMethodInterface : public QObject +{ + Q_OBJECT +public: + TestInputMethodInterface() + { + } + ~TestInputMethodInterface() override; + +private Q_SLOTS: + void initTestCase(); + void testAdd(); + void testActivate(); + void testContext(); + void testGrabkeyboard(); + void testContentHints_data(); + void testContentHints(); + void testContentPurpose_data(); + void testContentPurpose(); + void testKeyboardGrab(); + +private: + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::EventQueue *m_queue; + KWayland::Client::Compositor *m_clientCompositor; + KWayland::Client::Seat *m_clientSeat = nullptr; + KWayland::Client::Output *m_output = nullptr; + + InputMethodV1 *m_inputMethod; + InputPanel *m_inputPanel; + QThread *m_thread; + KWin::Display m_display; + SeatInterface *m_seat; + CompositorInterface *m_serverCompositor; + std::unique_ptr m_fakeOutput; + std::unique_ptr m_outputHandle; + std::unique_ptr m_outputInterface; + + KWin::InputMethodV1Interface *m_inputMethodIface; + KWin::InputPanelV1Interface *m_inputPanelIface; + + QList m_surfaces; +}; + +static const QString s_socketName = QStringLiteral("kwin-wayland-server-inputmethod-test-0"); + +void TestInputMethodInterface::initTestCase() +{ + m_display.addSocketName(s_socketName); + m_display.start(); + QVERIFY(m_display.isRunning()); + + m_seat = new SeatInterface(&m_display, QStringLiteral("seat0"), this); + m_serverCompositor = new CompositorInterface(&m_display, this); + m_inputMethodIface = new InputMethodV1Interface(&m_display, this); + m_inputPanelIface = new InputPanelV1Interface(&m_display, this); + + m_fakeOutput = std::make_unique(); + m_outputHandle = std::make_unique(m_fakeOutput.get()); + m_outputInterface = std::make_unique(&m_display, m_outputHandle.get()); + + connect(m_serverCompositor, &CompositorInterface::surfaceCreated, this, [this](SurfaceInterface *surface) { + m_surfaces += surface; + }); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + QVERIFY(!m_connection->connections().isEmpty()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + auto registry = new KWayland::Client::Registry(this); + QSignalSpy interfacesSpy(registry, &KWayland::Client::Registry::interfacesAnnounced); + connect(registry, &KWayland::Client::Registry::outputAnnounced, this, [this, registry](quint32 name, quint32 version) { + m_output = new KWayland::Client::Output(this); + m_output->setup(registry->bindOutput(name, version)); + }); + connect(registry, &KWayland::Client::Registry::interfaceAnnounced, this, [this, registry](const QByteArray &interface, quint32 name, quint32 version) { + if (interface == "zwp_input_panel_v1") { + m_inputPanel = new InputPanel(registry->registry(), name, version); + } else if (interface == "zwp_input_method_v1") { + m_inputMethod = new InputMethodV1(registry->registry(), name, version); + } + }); + connect(registry, &KWayland::Client::Registry::seatAnnounced, this, [this, registry](quint32 name, quint32 version) { + m_clientSeat = registry->createSeat(name, version); + }); + registry->setEventQueue(m_queue); + QSignalSpy compositorSpy(registry, &KWayland::Client::Registry::compositorAnnounced); + registry->create(m_connection->display()); + QVERIFY(registry->isValid()); + registry->setup(); + wl_display_flush(m_connection->display()); + + QVERIFY(compositorSpy.wait()); + m_clientCompositor = registry->createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + QVERIFY(m_clientCompositor->isValid()); + + QVERIFY(interfacesSpy.count() || interfacesSpy.wait()); + + QSignalSpy surfaceSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + for (int i = 0; i < 3; ++i) { + m_clientCompositor->createSurface(this); + } + QVERIFY(surfaceSpy.count() < 3 && surfaceSpy.wait(200)); + QVERIFY(m_surfaces.count() == 3); + QVERIFY(m_inputPanel); + QVERIFY(m_output); +} + +TestInputMethodInterface::~TestInputMethodInterface() +{ + if (m_queue) { + delete m_queue; + m_queue = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + delete m_inputPanel; + delete m_inputMethod; + delete m_inputMethodIface; + delete m_inputPanelIface; + m_connection->deleteLater(); + m_connection = nullptr; +} + +void TestInputMethodInterface::testAdd() +{ + QSignalSpy panelSpy(m_inputPanelIface, &InputPanelV1Interface::inputPanelSurfaceAdded); + QPointer panelSurfaceIface; + connect(m_inputPanelIface, &InputPanelV1Interface::inputPanelSurfaceAdded, this, [&panelSurfaceIface](InputPanelSurfaceV1Interface *surface) { + panelSurfaceIface = surface; + }); + + auto surface = m_clientCompositor->createSurface(this); + auto panelSurface = m_inputPanel->panelForSurface(surface); + + QVERIFY(panelSpy.wait() || panelSurfaceIface); + Q_ASSERT(panelSurfaceIface); + Q_ASSERT(panelSurfaceIface->surface() == m_surfaces.constLast()); + + QSignalSpy panelTopLevelSpy(panelSurfaceIface, &InputPanelSurfaceV1Interface::topLevel); + panelSurface->set_toplevel(*m_output, InputPanelSurface::position_center_bottom); + QVERIFY(panelTopLevelSpy.wait()); +} + +void TestInputMethodInterface::testActivate() +{ + QVERIFY(m_inputMethodIface); + QSignalSpy inputMethodActivateSpy(m_inputMethod, &InputMethodV1::activated); + QSignalSpy inputMethodDeactivateSpy(m_inputMethod, &InputMethodV1::deactivated); + + // before sending activate the context should be null + QVERIFY(!m_inputMethodIface->context()); + + // send activate now + m_inputMethodIface->sendActivate(); + QVERIFY(inputMethodActivateSpy.wait()); + QCOMPARE(inputMethodActivateSpy.count(), 1); + QVERIFY(m_inputMethodIface->context()); + + // send deactivate and verify server interface resets context + m_inputMethodIface->sendDeactivate(); + QVERIFY(inputMethodDeactivateSpy.wait()); + QCOMPARE(inputMethodActivateSpy.count(), 1); + QVERIFY(!m_inputMethodIface->context()); +} + +void TestInputMethodInterface::testContext() +{ + QVERIFY(m_inputMethodIface); + QSignalSpy inputMethodActivateSpy(m_inputMethod, &InputMethodV1::activated); + QSignalSpy inputMethodDeactivateSpy(m_inputMethod, &InputMethodV1::deactivated); + + // before sending activate the context should be null + QVERIFY(!m_inputMethodIface->context()); + + // send activate now + m_inputMethodIface->sendActivate(); + QVERIFY(inputMethodActivateSpy.wait()); + QCOMPARE(inputMethodActivateSpy.count(), 1); + + KWin::InputMethodContextV1Interface *serverContext = m_inputMethodIface->context(); + QVERIFY(serverContext); + + InputMethodV1Context *imContext = m_inputMethod->context(); + QVERIFY(imContext); + + quint32 serial = 1; + + // commit some text + QSignalSpy commitStringSpy(serverContext, &KWin::InputMethodContextV1Interface::commitString); + imContext->commit_string(serial, "hello"); + QVERIFY(commitStringSpy.wait()); + QCOMPARE(commitStringSpy.count(), serial); + QCOMPARE(commitStringSpy.last().at(0).value(), serial); + QCOMPARE(commitStringSpy.last().at(1).value(), "hello"); + serial++; + + // preedit styling event + QSignalSpy preeditStylingSpy(serverContext, &KWin::InputMethodContextV1Interface::preeditStyling); + // protocol does not document 3rd argument mean in much details (styling) + imContext->preedit_styling(0, 5, 1); + QVERIFY(preeditStylingSpy.wait()); + QCOMPARE(preeditStylingSpy.count(), 1); + QCOMPARE(preeditStylingSpy.last().at(0).value(), 0); + QCOMPARE(preeditStylingSpy.last().at(1).value(), 5); + QCOMPARE(preeditStylingSpy.last().at(2).value(), 1); + + // preedit cursor event + QSignalSpy preeditCursorSpy(serverContext, &KWin::InputMethodContextV1Interface::preeditCursor); + imContext->preedit_cursor(3); + QVERIFY(preeditCursorSpy.wait()); + QCOMPARE(preeditCursorSpy.count(), 1); + QCOMPARE(preeditCursorSpy.last().at(0).value(), 3); + + // commit preedit_string + QSignalSpy preeditStringSpy(serverContext, &KWin::InputMethodContextV1Interface::preeditString); + imContext->preedit_string(serial, "hello", "kde"); + QVERIFY(preeditStringSpy.wait()); + QCOMPARE(preeditStringSpy.count(), 1); + QCOMPARE(preeditStringSpy.last().at(0).value(), serial); + QCOMPARE(preeditStringSpy.last().at(1).value(), "hello"); + QCOMPARE(preeditStringSpy.last().at(2).value(), "kde"); + serial++; + + // delete surrounding text + QSignalSpy deleteSurroundingSpy(serverContext, &KWin::InputMethodContextV1Interface::deleteSurroundingText); + imContext->delete_surrounding_text(0, 5); + QVERIFY(deleteSurroundingSpy.wait()); + QCOMPARE(deleteSurroundingSpy.count(), 1); + QCOMPARE(deleteSurroundingSpy.last().at(0).value(), 0); + QCOMPARE(deleteSurroundingSpy.last().at(1).value(), 5); + + // set cursor position + QSignalSpy cursorPositionSpy(serverContext, &KWin::InputMethodContextV1Interface::cursorPosition); + imContext->cursor_position(2, 4); + QVERIFY(cursorPositionSpy.wait()); + QCOMPARE(cursorPositionSpy.count(), 1); + QCOMPARE(cursorPositionSpy.last().at(0).value(), 2); + QCOMPARE(cursorPositionSpy.last().at(1).value(), 4); + + // invoke action + QSignalSpy invokeActionSpy(imContext, &InputMethodV1Context::invoke_action); + serverContext->sendInvokeAction(3, 5); + QVERIFY(invokeActionSpy.wait()); + QCOMPARE(invokeActionSpy.count(), 1); + QCOMPARE(invokeActionSpy.last().at(0).value(), 3); + QCOMPARE(invokeActionSpy.last().at(1).value(), 5); + + // preferred language + QSignalSpy preferredLanguageSpy(imContext, &InputMethodV1Context::preferred_language); + serverContext->sendPreferredLanguage("gu_IN"); + QVERIFY(preferredLanguageSpy.wait()); + QCOMPARE(preferredLanguageSpy.count(), 1); + QCOMPARE(preferredLanguageSpy.last().at(0).value(), "gu_IN"); + + // surrounding text + QSignalSpy surroundingTextSpy(imContext, &InputMethodV1Context::surrounding_text); + serverContext->sendSurroundingText("Hello Plasma!", 2, 4); + QVERIFY(surroundingTextSpy.wait()); + QCOMPARE(surroundingTextSpy.count(), 1); + QCOMPARE(surroundingTextSpy.last().at(0).value(), "Hello Plasma!"); + QCOMPARE(surroundingTextSpy.last().at(1).value(), 2); + QCOMPARE(surroundingTextSpy.last().at(2).value(), 4); + + // reset + QSignalSpy resetSpy(imContext, &InputMethodV1Context::reset); + serverContext->sendReset(); + QVERIFY(resetSpy.wait()); + QCOMPARE(resetSpy.count(), 1); + + // send deactivate and verify server interface resets context + m_inputMethodIface->sendDeactivate(); + QVERIFY(inputMethodDeactivateSpy.wait()); + QCOMPARE(inputMethodActivateSpy.count(), 1); + QVERIFY(!m_inputMethodIface->context()); + QVERIFY(!m_inputMethod->context()); +} + +void TestInputMethodInterface::testGrabkeyboard() +{ + QVERIFY(m_inputMethodIface); + QSignalSpy inputMethodActivateSpy(m_inputMethod, &InputMethodV1::activated); + QSignalSpy inputMethodDeactivateSpy(m_inputMethod, &InputMethodV1::deactivated); + + // before sending activate the context should be null + QVERIFY(!m_inputMethodIface->context()); + + // send activate now + m_inputMethodIface->sendActivate(); + QVERIFY(inputMethodActivateSpy.wait()); + QCOMPARE(inputMethodActivateSpy.count(), 1); + + KWin::InputMethodContextV1Interface *serverContext = m_inputMethodIface->context(); + QVERIFY(serverContext); + + InputMethodV1Context *imContext = m_inputMethod->context(); + QVERIFY(imContext); + + QSignalSpy keyEventSpy(serverContext, &KWin::InputMethodContextV1Interface::key); + imContext->key(0, 123, 56, 1); + QEXPECT_FAIL("", "We should be not get key event if keyboard is not grabbed", Continue); + QVERIFY(!keyEventSpy.wait(200)); + + QSignalSpy modifierEventSpy(serverContext, &KWin::InputMethodContextV1Interface::modifiers); + imContext->modifiers(1234, 0, 0, 0, 0); + QEXPECT_FAIL("", "We should be not get modifiers event if keyboard is not grabbed", Continue); + QVERIFY(!modifierEventSpy.wait(200)); + + // grab the keyboard + wl_keyboard *keyboard = imContext->grab_keyboard(); + QVERIFY(keyboard); + + // TODO: add more tests about keyboard grab here + + // send deactivate and verify server interface resets context + m_inputMethodIface->sendDeactivate(); + QVERIFY(inputMethodDeactivateSpy.wait()); + QCOMPARE(inputMethodActivateSpy.count(), 1); + QVERIFY(!m_inputMethodIface->context()); + QVERIFY(!m_inputMethod->context()); +} + +void TestInputMethodInterface::testContentHints_data() +{ + QTest::addColumn("serverHints"); + QTest::addColumn("imHint"); + QTest::addRow("Spellcheck") << TextInputContentHints(TextInputContentHint::AutoCorrection) + << quint32(QtWaylandServer::zwp_text_input_v1::content_hint_auto_correction); + QTest::addRow("AutoCapital") << TextInputContentHints(TextInputContentHint::AutoCapitalization) + << quint32(QtWaylandServer::zwp_text_input_v1::content_hint_auto_capitalization); + QTest::addRow("Lowercase") << TextInputContentHints(TextInputContentHint::LowerCase) << quint32(QtWaylandServer::zwp_text_input_v1::content_hint_lowercase); + QTest::addRow("Uppercase") << TextInputContentHints(TextInputContentHint::UpperCase) << quint32(QtWaylandServer::zwp_text_input_v1::content_hint_uppercase); + QTest::addRow("Titlecase") << TextInputContentHints(TextInputContentHint::TitleCase) << quint32(QtWaylandServer::zwp_text_input_v1::content_hint_titlecase); + QTest::addRow("HiddenText") << TextInputContentHints(TextInputContentHint::HiddenText) + << quint32(QtWaylandServer::zwp_text_input_v1::content_hint_hidden_text); + QTest::addRow("SensitiveData") << TextInputContentHints(TextInputContentHint::SensitiveData) + << quint32(QtWaylandServer::zwp_text_input_v1::content_hint_sensitive_data); + QTest::addRow("Latin") << TextInputContentHints(TextInputContentHint::Latin) << quint32(QtWaylandServer::zwp_text_input_v1::content_hint_latin); + QTest::addRow("Multiline") << TextInputContentHints(TextInputContentHint::MultiLine) << quint32(QtWaylandServer::zwp_text_input_v1::content_hint_multiline); + QTest::addRow("Auto") << TextInputContentHints(TextInputContentHint::AutoCorrection | TextInputContentHint::AutoCapitalization) + << quint32(QtWaylandServer::zwp_text_input_v1::content_hint_auto_correction + | QtWaylandServer::zwp_text_input_v1::content_hint_auto_capitalization); +} + +void TestInputMethodInterface::testContentHints() +{ + QVERIFY(m_inputMethodIface); + QSignalSpy inputMethodActivateSpy(m_inputMethod, &InputMethodV1::activated); + QSignalSpy inputMethodDeactivateSpy(m_inputMethod, &InputMethodV1::deactivated); + + // before sending activate the context should be null + QVERIFY(!m_inputMethodIface->context()); + + // send activate now + m_inputMethodIface->sendActivate(); + QVERIFY(inputMethodActivateSpy.wait()); + QCOMPARE(inputMethodActivateSpy.count(), 1); + + KWin::InputMethodContextV1Interface *serverContext = m_inputMethodIface->context(); + QVERIFY(serverContext); + + InputMethodV1Context *imContext = m_inputMethod->context(); + QVERIFY(imContext); + + QSignalSpy contentTypeChangedSpy(imContext, &InputMethodV1Context::content_type_changed); + + QFETCH(KWin::TextInputContentHints, serverHints); + serverContext->sendContentType(serverHints, KWin::TextInputContentPurpose::Normal); + QVERIFY(contentTypeChangedSpy.wait()); + QCOMPARE(contentTypeChangedSpy.count(), 1); + QEXPECT_FAIL("SensitiveData", "SensitiveData content hint need fixing", Continue); + QTEST(imContext->contentHints(), "imHint"); + + // send deactivate and verify server interface resets context + m_inputMethodIface->sendDeactivate(); + QVERIFY(inputMethodDeactivateSpy.wait()); + QCOMPARE(inputMethodActivateSpy.count(), 1); + QVERIFY(!m_inputMethodIface->context()); + QVERIFY(!m_inputMethod->context()); +} + +void TestInputMethodInterface::testContentPurpose_data() +{ + QTest::addColumn("serverPurpose"); + QTest::addColumn("imPurpose"); + + QTest::newRow("Alpha") << TextInputContentPurpose::Alpha << quint32(QtWaylandServer::zwp_text_input_v1::content_purpose_alpha); + QTest::newRow("Digits") << TextInputContentPurpose::Digits << quint32(QtWaylandServer::zwp_text_input_v1::content_purpose_digits); + QTest::newRow("Number") << TextInputContentPurpose::Number << quint32(QtWaylandServer::zwp_text_input_v1::content_purpose_number); + QTest::newRow("Phone") << TextInputContentPurpose::Phone << quint32(QtWaylandServer::zwp_text_input_v1::content_purpose_phone); + QTest::newRow("Url") << TextInputContentPurpose::Url << quint32(QtWaylandServer::zwp_text_input_v1::content_purpose_url); + QTest::newRow("Email") << TextInputContentPurpose::Email << quint32(QtWaylandServer::zwp_text_input_v1::content_purpose_email); + QTest::newRow("Name") << TextInputContentPurpose::Name << quint32(QtWaylandServer::zwp_text_input_v1::content_purpose_name); + QTest::newRow("Password") << TextInputContentPurpose::Password << quint32(QtWaylandServer::zwp_text_input_v1::content_purpose_password); + QTest::newRow("Date") << TextInputContentPurpose::Date << quint32(QtWaylandServer::zwp_text_input_v1::content_purpose_date); + QTest::newRow("Time") << TextInputContentPurpose::Time << quint32(QtWaylandServer::zwp_text_input_v1::content_purpose_time); + QTest::newRow("DateTime") << TextInputContentPurpose::DateTime << quint32(QtWaylandServer::zwp_text_input_v1::content_purpose_datetime); + QTest::newRow("Terminal") << TextInputContentPurpose::Terminal << quint32(QtWaylandServer::zwp_text_input_v1::content_purpose_terminal); + QTest::newRow("Normal") << TextInputContentPurpose::Normal << quint32(QtWaylandServer::zwp_text_input_v1::content_purpose_normal); + QTest::newRow("Pin") << TextInputContentPurpose::Pin << quint32(QtWaylandServer::zwp_text_input_v1::content_purpose_password); +} + +void TestInputMethodInterface::testContentPurpose() +{ + QVERIFY(m_inputMethodIface); + QSignalSpy inputMethodActivateSpy(m_inputMethod, &InputMethodV1::activated); + QSignalSpy inputMethodDeactivateSpy(m_inputMethod, &InputMethodV1::deactivated); + + // before sending activate the context should be null + QVERIFY(!m_inputMethodIface->context()); + + // send activate now + m_inputMethodIface->sendActivate(); + QVERIFY(inputMethodActivateSpy.wait()); + QCOMPARE(inputMethodActivateSpy.count(), 1); + + KWin::InputMethodContextV1Interface *serverContext = m_inputMethodIface->context(); + QVERIFY(serverContext); + + InputMethodV1Context *imContext = m_inputMethod->context(); + QVERIFY(imContext); + + QSignalSpy contentTypeChangedSpy(imContext, &InputMethodV1Context::content_type_changed); + + QFETCH(KWin::TextInputContentPurpose, serverPurpose); + serverContext->sendContentType(KWin::TextInputContentHints(KWin::TextInputContentHint::None), serverPurpose); + QVERIFY(contentTypeChangedSpy.wait()); + QCOMPARE(contentTypeChangedSpy.count(), 1); + QEXPECT_FAIL("Pin", "Pin should return content_purpose_password", Continue); + QTEST(imContext->contentPurpose(), "imPurpose"); + + // send deactivate and verify server interface resets context + m_inputMethodIface->sendDeactivate(); + QVERIFY(inputMethodDeactivateSpy.wait()); + QCOMPARE(inputMethodActivateSpy.count(), 1); + QVERIFY(!m_inputMethodIface->context()); + QVERIFY(!m_inputMethod->context()); +} + +void TestInputMethodInterface::testKeyboardGrab() +{ + QVERIFY(m_inputMethodIface); + QSignalSpy inputMethodActivateSpy(m_inputMethod, &InputMethodV1::activated); + + m_inputMethodIface->sendActivate(); + QVERIFY(inputMethodActivateSpy.wait()); + + QSignalSpy keyboardGrabSpy(m_inputMethodIface->context(), &InputMethodContextV1Interface::keyboardGrabRequested); + InputMethodV1Context *imContext = m_inputMethod->context(); + QVERIFY(imContext); + KWayland::Client::Keyboard *keyboard = new KWayland::Client::Keyboard(this); + keyboard->setup(imContext->grab_keyboard()); + QVERIFY(keyboard->isValid()); + QVERIFY(keyboardGrabSpy.count() || keyboardGrabSpy.wait()); + + QPointer serverKeyboardGrab = m_inputMethodIface->context()->keyboardGrab(); + QVERIFY(serverKeyboardGrab); + QSignalSpy keyboardGrabDestroyedSpy(serverKeyboardGrab, &QObject::destroyed); + + QSignalSpy keyboardSpy(keyboard, &KWayland::Client::Keyboard::keyChanged); + m_inputMethodIface->context()->keyboardGrab()->sendKey(0, 0, KEY_F1, KeyboardKeyState::Pressed); + m_inputMethodIface->context()->keyboardGrab()->sendKey(0, 0, KEY_F1, KeyboardKeyState::Released); + keyboardSpy.wait(); + QCOMPARE(keyboardSpy.count(), 2); + + delete keyboard; + wl_display_flush(m_connection->display()); + QVERIFY(keyboardGrabDestroyedSpy.wait()); + QVERIFY(!serverKeyboardGrab); + QVERIFY(!m_inputMethodIface->context()->keyboardGrab()); + + m_inputMethodIface->sendDeactivate(); +} + +QTEST_GUILESS_MAIN(TestInputMethodInterface) +#include "test_inputmethod_interface.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/server/test_keyboard_shortcuts_inhibitor_interface.cpp b/local/recipes/kde/kwin/source/autotests/wayland/server/test_keyboard_shortcuts_inhibitor_interface.cpp new file mode 100644 index 0000000000..bb2ca31043 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/server/test_keyboard_shortcuts_inhibitor_interface.cpp @@ -0,0 +1,212 @@ +/* + SPDX-FileCopyrightText: 2020 Benjamin Port + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +// Qt +#include +#include +#include +#include +// WaylandServer +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/keyboard_shortcuts_inhibit_v1.h" +#include "wayland/seat.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/seat.h" +#include "KWayland/Client/surface.h" + +#include "qwayland-keyboard-shortcuts-inhibit-unstable-v1.h" + +using namespace KWin; + +class KeyboardShortcutsInhibitManager : public QObject, public QtWayland::zwp_keyboard_shortcuts_inhibit_manager_v1 +{ + Q_OBJECT +public: + KeyboardShortcutsInhibitManager(wl_registry *registry, quint32 id, quint32 version) + : QtWayland::zwp_keyboard_shortcuts_inhibit_manager_v1(registry, id, version) + { + } +}; + +class KeyboardShortcutsInhibitor : public QObject, public QtWayland::zwp_keyboard_shortcuts_inhibitor_v1 +{ + Q_OBJECT +public: + KeyboardShortcutsInhibitor(::zwp_keyboard_shortcuts_inhibitor_v1 *inhibitorV1) + : QtWayland::zwp_keyboard_shortcuts_inhibitor_v1(inhibitorV1) + { + } + + void zwp_keyboard_shortcuts_inhibitor_v1_active() override + { + Q_EMIT inhibitorActive(); + } + + void zwp_keyboard_shortcuts_inhibitor_v1_inactive() override + { + Q_EMIT inhibitorInactive(); + } + +Q_SIGNALS: + void inhibitorActive(); + void inhibitorInactive(); +}; + +class TestKeyboardShortcutsInhibitorInterface : public QObject +{ + Q_OBJECT +public: + TestKeyboardShortcutsInhibitorInterface() + { + } + ~TestKeyboardShortcutsInhibitorInterface() override; + +private Q_SLOTS: + void initTestCase(); + void testKeyboardShortcuts(); + +private: + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::EventQueue *m_queue; + KWayland::Client::Compositor *m_clientCompositor; + KWayland::Client::Seat *m_clientSeat = nullptr; + + QThread *m_thread; + KWin::Display m_display; + SeatInterface *m_seat; + CompositorInterface *m_serverCompositor; + + KeyboardShortcutsInhibitManagerV1Interface *m_manager; + QList m_surfaces; + QList m_clientSurfaces; + KeyboardShortcutsInhibitManager *m_inhibitManagerClient = nullptr; +}; + +static const QString s_socketName = QStringLiteral("kwin-wayland-server-keyboard-shortcuts-inhibitor-test-0"); + +void TestKeyboardShortcutsInhibitorInterface::initTestCase() +{ + m_display.addSocketName(s_socketName); + m_display.start(); + QVERIFY(m_display.isRunning()); + + m_seat = new SeatInterface(&m_display, QStringLiteral("seat0"), this); + m_serverCompositor = new CompositorInterface(&m_display, this); + m_manager = new KeyboardShortcutsInhibitManagerV1Interface(&m_display, this); + + connect(m_serverCompositor, &CompositorInterface::surfaceCreated, this, [this](SurfaceInterface *surface) { + m_surfaces += surface; + }); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + QVERIFY(!m_connection->connections().isEmpty()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + auto registry = new KWayland::Client::Registry(this); + connect(registry, &KWayland::Client::Registry::interfaceAnnounced, this, [this, registry](const QByteArray &interface, quint32 id, quint32 version) { + if (interface == "zwp_keyboard_shortcuts_inhibit_manager_v1") { + m_inhibitManagerClient = new KeyboardShortcutsInhibitManager(registry->registry(), id, version); + } + }); + connect(registry, &KWayland::Client::Registry::seatAnnounced, this, [this, registry](quint32 name, quint32 version) { + m_clientSeat = registry->createSeat(name, version); + }); + registry->setEventQueue(m_queue); + QSignalSpy compositorSpy(registry, &KWayland::Client::Registry::compositorAnnounced); + registry->create(m_connection->display()); + QVERIFY(registry->isValid()); + registry->setup(); + wl_display_flush(m_connection->display()); + + QVERIFY(compositorSpy.wait()); + m_clientCompositor = registry->createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + QVERIFY(m_clientCompositor->isValid()); + + QSignalSpy surfaceSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + for (int i = 0; i < 3; ++i) { + KWayland::Client::Surface *s = m_clientCompositor->createSurface(this); + m_clientSurfaces += s->operator wl_surface *(); + } + QVERIFY(surfaceSpy.count() < 3 && surfaceSpy.wait(200)); + QVERIFY(m_surfaces.count() == 3); + QVERIFY(m_inhibitManagerClient); +} + +TestKeyboardShortcutsInhibitorInterface::~TestKeyboardShortcutsInhibitorInterface() +{ + if (m_queue) { + delete m_queue; + m_queue = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + m_connection->deleteLater(); + m_connection = nullptr; +} + +void TestKeyboardShortcutsInhibitorInterface::testKeyboardShortcuts() +{ + auto clientSurface = m_clientSurfaces[0]; + auto surface = m_surfaces[0]; + + // Test creation + auto inhibitorClientV1 = m_inhibitManagerClient->inhibit_shortcuts(clientSurface, m_clientSeat->operator wl_seat *()); + auto inhibitorClient = new KeyboardShortcutsInhibitor(inhibitorClientV1); + QSignalSpy inhibitorActiveSpy(inhibitorClient, &KeyboardShortcutsInhibitor::inhibitorActive); + QSignalSpy inhibitorInactiveSpy(inhibitorClient, &KeyboardShortcutsInhibitor::inhibitorInactive); + QSignalSpy inhibitorCreatedSpy(m_manager, &KeyboardShortcutsInhibitManagerV1Interface::inhibitorCreated); + QVERIFY(inhibitorCreatedSpy.wait() || inhibitorCreatedSpy.count() == 1); + auto inhibitorServer = m_manager->findInhibitor(surface, m_seat); + + // Test deactivate + inhibitorServer->setActive(false); + QVERIFY(inhibitorInactiveSpy.wait() || inhibitorInactiveSpy.count() == 1); + + // Test activate + inhibitorServer->setActive(true); + QVERIFY(inhibitorActiveSpy.wait() || inhibitorActiveSpy.count() == 1); + + // Test creating for another surface + m_inhibitManagerClient->inhibit_shortcuts(m_clientSurfaces[1], m_clientSeat->operator wl_seat *()); + QVERIFY(inhibitorCreatedSpy.wait() || inhibitorCreatedSpy.count() == 2); + + // Test destroy is working + inhibitorClient->destroy(); + m_inhibitManagerClient->inhibit_shortcuts(clientSurface, m_clientSeat->operator wl_seat *()); + QVERIFY(inhibitorCreatedSpy.wait() || inhibitorCreatedSpy.count() == 3); + + // Test creating with same surface / seat (expect error) + QSignalSpy errorOccured(m_connection, &KWayland::Client::ConnectionThread::errorOccurred); + m_inhibitManagerClient->inhibit_shortcuts(m_clientSurfaces[0], m_clientSeat->operator wl_seat *()); + QVERIFY(errorOccured.wait() || errorOccured.count() == 1); +} + +QTEST_GUILESS_MAIN(TestKeyboardShortcutsInhibitorInterface) + +#include "test_keyboard_shortcuts_inhibitor_interface.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/server/test_layershellv1_interface.cpp b/local/recipes/kde/kwin/source/autotests/wayland/server/test_layershellv1_interface.cpp new file mode 100644 index 0000000000..c331c1adc8 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/server/test_layershellv1_interface.cpp @@ -0,0 +1,504 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include +#include +#include + +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/layershell_v1.h" +#include "wayland/surface.h" +#include "wayland/xdgshell.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/surface.h" + +#include "qwayland-wlr-layer-shell-unstable-v1.h" +#include "qwayland-xdg-shell.h" + +Q_DECLARE_METATYPE(KWin::LayerSurfaceV1Interface::Layer) +Q_DECLARE_METATYPE(KWin::LayerSurfaceV1Interface *) + +using namespace KWin; + +class LayerShellV1 : public QtWayland::zwlr_layer_shell_v1 +{ +public: + ~LayerShellV1() override + { + destroy(); + } +}; + +class LayerSurfaceV1 : public QtWayland::zwlr_layer_surface_v1 +{ +public: + ~LayerSurfaceV1() override + { + destroy(); + } +}; + +class XdgShell : public QtWayland::xdg_wm_base +{ +public: + ~XdgShell() + { + destroy(); + } +}; + +class XdgSurface : public QtWayland::xdg_surface +{ +public: + ~XdgSurface() + { + destroy(); + } +}; + +class XdgPositioner : public QtWayland::xdg_positioner +{ +public: + ~XdgPositioner() + { + destroy(); + } +}; + +class XdgPopup : public QtWayland::xdg_popup +{ +public: + ~XdgPopup() + { + destroy(); + } +}; + +class TestLayerShellV1Interface : public QObject +{ + Q_OBJECT + +public: + ~TestLayerShellV1Interface() override; + +private Q_SLOTS: + void initTestCase(); + void testDesiredSize(); + void testScope(); + void testAnchor_data(); + void testAnchor(); + void testMargins(); + void testExclusiveZone(); + void testExclusiveEdge_data(); + void testExclusiveEdge(); + void testLayer_data(); + void testLayer(); + void testPopup(); + +private: + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::EventQueue *m_queue; + KWayland::Client::Compositor *m_clientCompositor; + + QThread *m_thread; + KWin::Display m_display; + CompositorInterface *m_serverCompositor; + LayerShellV1 *m_clientLayerShell = nullptr; + LayerShellV1Interface *m_serverLayerShell = nullptr; + XdgShell *m_clientXdgShell = nullptr; + XdgShellInterface *m_serverXdgShell = nullptr; +}; + +static const QString s_socketName = QStringLiteral("kwin-wayland-server-layer-shell-v1-test-0"); + +void TestLayerShellV1Interface::initTestCase() +{ + m_display.addSocketName(s_socketName); + m_display.start(); + QVERIFY(m_display.isRunning()); + + m_serverLayerShell = new LayerShellV1Interface(&m_display, this); + m_serverXdgShell = new XdgShellInterface(&m_display, this); + m_serverCompositor = new CompositorInterface(&m_display, this); + + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + QVERIFY(!m_connection->connections().isEmpty()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + auto registry = new KWayland::Client::Registry(this); + connect(registry, &KWayland::Client::Registry::interfaceAnnounced, this, [this, registry](const QByteArray &interface, quint32 id, quint32 version) { + if (interface == QByteArrayLiteral("zwlr_layer_shell_v1")) { + m_clientLayerShell = new LayerShellV1(); + m_clientLayerShell->init(*registry, id, version); + } + if (interface == QByteArrayLiteral("xdg_wm_base")) { + m_clientXdgShell = new XdgShell(); + m_clientXdgShell->init(*registry, id, version); + } + }); + QSignalSpy allAnnouncedSpy(registry, &KWayland::Client::Registry::interfaceAnnounced); + QSignalSpy compositorSpy(registry, &KWayland::Client::Registry::compositorAnnounced); + QSignalSpy shmSpy(registry, &KWayland::Client::Registry::shmAnnounced); + registry->setEventQueue(m_queue); + registry->create(m_connection->display()); + QVERIFY(registry->isValid()); + registry->setup(); + QVERIFY(allAnnouncedSpy.wait()); + + m_clientCompositor = registry->createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + QVERIFY(m_clientCompositor->isValid()); +} + +TestLayerShellV1Interface::~TestLayerShellV1Interface() +{ + if (m_clientXdgShell) { + delete m_clientXdgShell; + m_clientXdgShell = nullptr; + } + if (m_clientLayerShell) { + delete m_clientLayerShell; + m_clientLayerShell = nullptr; + } + if (m_queue) { + delete m_queue; + m_queue = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + m_connection->deleteLater(); + m_connection = nullptr; +} + +void TestLayerShellV1Interface::testDesiredSize() +{ + // Create a test wl_surface object. + QSignalSpy serverSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + std::unique_ptr clientSurface(m_clientCompositor->createSurface(this)); + QVERIFY(serverSurfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + // Create a test wlr_layer_surface_v1 object. + std::unique_ptr clientShellSurface(new LayerSurfaceV1); + clientShellSurface->init(m_clientLayerShell->get_layer_surface(*clientSurface, nullptr, LayerShellV1::layer_top, QStringLiteral("test"))); + QSignalSpy layerSurfaceCreatedSpy(m_serverLayerShell, &LayerShellV1Interface::surfaceCreated); + QVERIFY(layerSurfaceCreatedSpy.wait()); + auto serverShellSurface = layerSurfaceCreatedSpy.last().first().value(); + QVERIFY(serverShellSurface); + + clientShellSurface->set_size(10, 20); + clientSurface->commit(KWayland::Client::Surface::CommitFlag::None); + + QSignalSpy desiredSizeChangedSpy(serverShellSurface, &LayerSurfaceV1Interface::desiredSizeChanged); + QVERIFY(desiredSizeChangedSpy.wait()); + + QCOMPARE(serverShellSurface->desiredSize(), QSize(10, 20)); +} + +void TestLayerShellV1Interface::testScope() +{ + // Create a test wl_surface object. + QSignalSpy serverSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + std::unique_ptr clientSurface(m_clientCompositor->createSurface(this)); + QVERIFY(serverSurfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + // Create a test wlr_layer_surface_v1 object. + std::unique_ptr clientShellSurface(new LayerSurfaceV1); + clientShellSurface->init(m_clientLayerShell->get_layer_surface(*clientSurface, nullptr, LayerShellV1::layer_top, QStringLiteral("foobar"))); + clientShellSurface->set_size(100, 50); + QSignalSpy layerSurfaceCreatedSpy(m_serverLayerShell, &LayerShellV1Interface::surfaceCreated); + QVERIFY(layerSurfaceCreatedSpy.wait()); + auto serverShellSurface = layerSurfaceCreatedSpy.last().first().value(); + QVERIFY(serverShellSurface); + + QCOMPARE(serverShellSurface->scope(), QStringLiteral("foobar")); +} + +void TestLayerShellV1Interface::testAnchor_data() +{ + QTest::addColumn("anchor"); + QTest::addColumn("expected"); + + QTest::addRow("left") << int(QtWayland::zwlr_layer_surface_v1::anchor_left) << Qt::LeftEdge; + QTest::addRow("right") << int(QtWayland::zwlr_layer_surface_v1::anchor_right) << Qt::RightEdge; + QTest::addRow("top") << int(QtWayland::zwlr_layer_surface_v1::anchor_top) << Qt::TopEdge; + QTest::addRow("bottom") << int(QtWayland::zwlr_layer_surface_v1::anchor_bottom) << Qt::BottomEdge; +} + +void TestLayerShellV1Interface::testAnchor() +{ + // Create a test wl_surface object. + QSignalSpy serverSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + std::unique_ptr clientSurface(m_clientCompositor->createSurface(this)); + QVERIFY(serverSurfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + // Create a test wlr_layer_surface_v1 object. + std::unique_ptr clientShellSurface(new LayerSurfaceV1); + clientShellSurface->init(m_clientLayerShell->get_layer_surface(*clientSurface, nullptr, LayerShellV1::layer_top, QStringLiteral("test"))); + QSignalSpy layerSurfaceCreatedSpy(m_serverLayerShell, &LayerShellV1Interface::surfaceCreated); + QVERIFY(layerSurfaceCreatedSpy.wait()); + auto serverShellSurface = layerSurfaceCreatedSpy.last().first().value(); + QVERIFY(serverShellSurface); + + QFETCH(int, anchor); + QFETCH(Qt::Edge, expected); + + clientShellSurface->set_anchor(anchor); + clientShellSurface->set_size(100, 50); + clientSurface->commit(KWayland::Client::Surface::CommitFlag::None); + + QSignalSpy anchorChangedSpy(serverShellSurface, &LayerSurfaceV1Interface::anchorChanged); + QVERIFY(anchorChangedSpy.wait()); + + QCOMPARE(serverShellSurface->anchor(), expected); +} + +void TestLayerShellV1Interface::testMargins() +{ + // Create a test wl_surface object. + QSignalSpy serverSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + std::unique_ptr clientSurface(m_clientCompositor->createSurface(this)); + QVERIFY(serverSurfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + // Create a test wlr_layer_surface_v1 object. + std::unique_ptr clientShellSurface(new LayerSurfaceV1); + clientShellSurface->init(m_clientLayerShell->get_layer_surface(*clientSurface, nullptr, LayerShellV1::layer_top, QStringLiteral("test"))); + QSignalSpy layerSurfaceCreatedSpy(m_serverLayerShell, &LayerShellV1Interface::surfaceCreated); + QVERIFY(layerSurfaceCreatedSpy.wait()); + auto serverShellSurface = layerSurfaceCreatedSpy.last().first().value(); + QVERIFY(serverShellSurface); + + clientShellSurface->set_margin(10, 20, 30, 40); + clientShellSurface->set_size(100, 50); + clientSurface->commit(KWayland::Client::Surface::CommitFlag::None); + + QSignalSpy marginsChangedSpy(serverShellSurface, &LayerSurfaceV1Interface::marginsChanged); + QVERIFY(marginsChangedSpy.wait()); + + QCOMPARE(serverShellSurface->margins(), QMargins(40, 10, 20, 30)); +} + +void TestLayerShellV1Interface::testExclusiveZone() +{ + // Create a test wl_surface object. + QSignalSpy serverSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + std::unique_ptr clientSurface(m_clientCompositor->createSurface(this)); + QVERIFY(serverSurfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + // Create a test wlr_layer_surface_v1 object. + std::unique_ptr clientShellSurface(new LayerSurfaceV1); + clientShellSurface->init(m_clientLayerShell->get_layer_surface(*clientSurface, nullptr, LayerShellV1::layer_top, QStringLiteral("test"))); + QSignalSpy layerSurfaceCreatedSpy(m_serverLayerShell, &LayerShellV1Interface::surfaceCreated); + QVERIFY(layerSurfaceCreatedSpy.wait()); + auto serverShellSurface = layerSurfaceCreatedSpy.last().first().value(); + QVERIFY(serverShellSurface); + + clientShellSurface->set_exclusive_zone(10); + clientShellSurface->set_size(100, 50); + clientSurface->commit(KWayland::Client::Surface::CommitFlag::None); + + QSignalSpy exclusiveZoneChangedSpy(serverShellSurface, &LayerSurfaceV1Interface::exclusiveZoneChanged); + QVERIFY(exclusiveZoneChangedSpy.wait()); + + QCOMPARE(serverShellSurface->exclusiveZone(), 10); +} + +void TestLayerShellV1Interface::testExclusiveEdge_data() +{ + QTest::addColumn("anchor"); + QTest::addColumn("expected"); + + QTest::addRow("left (singular)") << int(QtWayland::zwlr_layer_surface_v1::anchor_left) << Qt::LeftEdge; + + QTest::addRow("left (triplet)") << (QtWayland::zwlr_layer_surface_v1::anchor_bottom | QtWayland::zwlr_layer_surface_v1::anchor_left + | QtWayland::zwlr_layer_surface_v1::anchor_top) + << Qt::LeftEdge; + + QTest::addRow("right (singular)") << int(QtWayland::zwlr_layer_surface_v1::anchor_right) << Qt::RightEdge; + + QTest::addRow("right (triplet)") << (QtWayland::zwlr_layer_surface_v1::anchor_top | QtWayland::zwlr_layer_surface_v1::anchor_right + | QtWayland::zwlr_layer_surface_v1::anchor_bottom) + << Qt::RightEdge; + + QTest::addRow("top (singular)") << int(QtWayland::zwlr_layer_surface_v1::anchor_top) << Qt::TopEdge; + + QTest::addRow("top (triplet)") << (QtWayland::zwlr_layer_surface_v1::anchor_left | QtWayland::zwlr_layer_surface_v1::anchor_top + | QtWayland::zwlr_layer_surface_v1::anchor_right) + << Qt::TopEdge; + + QTest::addRow("bottom (singular)") << int(QtWayland::zwlr_layer_surface_v1::anchor_bottom) << Qt::BottomEdge; + + QTest::addRow("bottom (triplet)") << (QtWayland::zwlr_layer_surface_v1::anchor_right | QtWayland::zwlr_layer_surface_v1::anchor_bottom + | QtWayland::zwlr_layer_surface_v1::anchor_left) + << Qt::BottomEdge; + + QTest::addRow("all") << (QtWayland::zwlr_layer_surface_v1::anchor_left | QtWayland::zwlr_layer_surface_v1::anchor_right + | QtWayland::zwlr_layer_surface_v1::anchor_top | QtWayland::zwlr_layer_surface_v1::anchor_bottom) + << Qt::Edge(); +} + +void TestLayerShellV1Interface::testExclusiveEdge() +{ + // Create a test wl_surface object. + QSignalSpy serverSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + std::unique_ptr clientSurface(m_clientCompositor->createSurface(this)); + QVERIFY(serverSurfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + // Create a test wlr_layer_surface_v1 object. + std::unique_ptr clientShellSurface(new LayerSurfaceV1); + clientShellSurface->init(m_clientLayerShell->get_layer_surface(*clientSurface, nullptr, LayerShellV1::layer_top, QStringLiteral("test"))); + QSignalSpy layerSurfaceCreatedSpy(m_serverLayerShell, &LayerShellV1Interface::surfaceCreated); + QVERIFY(layerSurfaceCreatedSpy.wait()); + auto serverShellSurface = layerSurfaceCreatedSpy.last().first().value(); + QVERIFY(serverShellSurface); + + QFETCH(int, anchor); + QFETCH(Qt::Edge, expected); + + clientShellSurface->set_exclusive_zone(10); + clientShellSurface->set_size(100, 50); + clientShellSurface->set_anchor(anchor); + clientSurface->commit(KWayland::Client::Surface::CommitFlag::None); + + QSignalSpy anchorChangedSpy(serverShellSurface, &LayerSurfaceV1Interface::anchorChanged); + QVERIFY(anchorChangedSpy.wait()); + + QCOMPARE(serverShellSurface->exclusiveEdge(), expected); +} + +void TestLayerShellV1Interface::testLayer_data() +{ + QTest::addColumn("layer"); + QTest::addColumn("expected"); + + QTest::addRow("overlay") << int(QtWayland::zwlr_layer_shell_v1::layer_overlay) << LayerSurfaceV1Interface::OverlayLayer; + QTest::addRow("top") << int(QtWayland::zwlr_layer_shell_v1::layer_top) << LayerSurfaceV1Interface::TopLayer; + QTest::addRow("bottom") << int(QtWayland::zwlr_layer_shell_v1::layer_bottom) << LayerSurfaceV1Interface::BottomLayer; + QTest::addRow("background") << int(QtWayland::zwlr_layer_shell_v1::layer_background) << LayerSurfaceV1Interface::BackgroundLayer; +} + +void TestLayerShellV1Interface::testLayer() +{ + // Create a test wl_surface object. + QSignalSpy serverSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + std::unique_ptr clientSurface(m_clientCompositor->createSurface(this)); + QVERIFY(serverSurfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + // Create a test wlr_layer_surface_v1 object. + std::unique_ptr clientShellSurface(new LayerSurfaceV1); + clientShellSurface->init(m_clientLayerShell->get_layer_surface(*clientSurface, nullptr, LayerShellV1::layer_top, QStringLiteral("test"))); + QSignalSpy layerSurfaceCreatedSpy(m_serverLayerShell, &LayerShellV1Interface::surfaceCreated); + QVERIFY(layerSurfaceCreatedSpy.wait()); + auto serverShellSurface = layerSurfaceCreatedSpy.last().first().value(); + QVERIFY(serverShellSurface); + + QFETCH(int, layer); + QFETCH(LayerSurfaceV1Interface::Layer, expected); + + clientShellSurface->set_layer(layer); + clientShellSurface->set_size(100, 50); + clientSurface->commit(KWayland::Client::Surface::CommitFlag::None); + + QSignalSpy committedSpy(serverSurface, &SurfaceInterface::committed); + QVERIFY(committedSpy.wait()); + + QCOMPARE(serverShellSurface->layer(), expected); +} + +void TestLayerShellV1Interface::testPopup() +{ + // Create a test wl_surface object for the panel. + QSignalSpy serverPanelSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + std::unique_ptr clientPanelSurface(m_clientCompositor->createSurface(this)); + QVERIFY(serverPanelSurfaceCreatedSpy.wait()); + SurfaceInterface *serverPanelSurface = serverPanelSurfaceCreatedSpy.last().first().value(); + QVERIFY(serverPanelSurface); + + // Create a test wlr_layer_surface_v1 object for the panel.. + std::unique_ptr clientPanelShellSurface(new LayerSurfaceV1); + clientPanelShellSurface->init(m_clientLayerShell->get_layer_surface(*clientPanelSurface, nullptr, LayerShellV1::layer_top, QStringLiteral("panel"))); + clientPanelShellSurface->set_size(100, 50); + QSignalSpy layerSurfaceCreatedSpy(m_serverLayerShell, &LayerShellV1Interface::surfaceCreated); + QVERIFY(layerSurfaceCreatedSpy.wait()); + auto serverPanelShellSurface = layerSurfaceCreatedSpy.last().first().value(); + QVERIFY(serverPanelShellSurface); + + // Create a wl_surface object for the popup. + std::unique_ptr clientPopupSurface(m_clientCompositor->createSurface(this)); + QVERIFY(serverPanelSurfaceCreatedSpy.wait()); + SurfaceInterface *serverPopupSurface = serverPanelSurfaceCreatedSpy.last().first().value(); + QVERIFY(serverPopupSurface); + + // Create an xdg_surface object for the popup. + std::unique_ptr clientXdgSurface(new XdgSurface); + clientXdgSurface->init(m_clientXdgShell->get_xdg_surface(*clientPopupSurface)); + + // Create an xdg_positioner object for the popup. + std::unique_ptr<::XdgPositioner> positioner(new ::XdgPositioner); + positioner->init(m_clientXdgShell->create_positioner()); + positioner->set_size(100, 100); + positioner->set_anchor_rect(0, 0, 10, 10); + + // Create an xdg_popup surface. + std::unique_ptr clientXdgPopup(new XdgPopup); + clientXdgPopup->init(clientXdgSurface->get_popup(nullptr, positioner->object())); + + // Wait for the server side to catch up. + QSignalSpy popupCreatedSpy(m_serverXdgShell, &XdgShellInterface::popupCreated); + QVERIFY(popupCreatedSpy.wait()); + XdgPopupInterface *serverPopupShellSurface = popupCreatedSpy.last().first().value(); + QVERIFY(serverPopupShellSurface); + QCOMPARE(serverPopupShellSurface->parentSurface(), nullptr); + + // Make the xdg_popup surface a child of the panel. + clientPanelShellSurface->get_popup(clientXdgPopup->object()); + + // Commit the initial state of the xdg_popup surface. + clientPopupSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QSignalSpy initializeRequestedSpy(serverPopupShellSurface, &XdgPopupInterface::initializeRequested); + QVERIFY(initializeRequestedSpy.wait()); + + // The popup should be a transient for the panel. + QCOMPARE(serverPopupShellSurface->parentSurface(), serverPanelSurface); +} + +QTEST_GUILESS_MAIN(TestLayerShellV1Interface) + +#include "test_layershellv1_interface.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/server/test_no_xdg_runtime_dir.cpp b/local/recipes/kde/kwin/source/autotests/wayland/server/test_no_xdg_runtime_dir.cpp new file mode 100644 index 0000000000..54d921507b --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/server/test_no_xdg_runtime_dir.cpp @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// WaylandServer +#include "wayland/display.h" + +using namespace KWin; + +class NoXdgRuntimeDirTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void testCreate(); +}; + +void NoXdgRuntimeDirTest::initTestCase() +{ + qunsetenv("XDG_RUNTIME_DIR"); +} + +void NoXdgRuntimeDirTest::testCreate() +{ + // this test verifies that not having an XDG_RUNTIME_DIR is handled gracefully + // the server cannot start, but should not crash + const QString testSocketName = QStringLiteral("kwayland-test-no-xdg-runtime-dir-0"); + KWin::Display display; + QSignalSpy runningSpy(&display, &KWin::Display::runningChanged); + QVERIFY(!display.addSocketName(testSocketName)); + display.start(); + + // call into dispatchEvents should not crash + display.dispatchEvents(); +} + +QTEST_GUILESS_MAIN(NoXdgRuntimeDirTest) +#include "test_no_xdg_runtime_dir.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/server/test_screencast.cpp b/local/recipes/kde/kwin/source/autotests/wayland/server/test_screencast.cpp new file mode 100644 index 0000000000..b5dd6720e1 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/server/test_screencast.cpp @@ -0,0 +1,184 @@ +/* + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +// Qt +#include +#include +#include +#include + +#include + +// WaylandServer +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/screencast_v1.h" +#include "wayland/seat.h" + +#include +#include +#include +#include +#include + +#include "qwayland-zkde-screencast-unstable-v1.h" + +class ScreencastStreamV1 : public QObject, public QtWayland::zkde_screencast_stream_unstable_v1 +{ + Q_OBJECT + +public: + ScreencastStreamV1(::zkde_screencast_stream_unstable_v1 *obj, QObject *parent) + : QObject(parent) + , zkde_screencast_stream_unstable_v1(obj) + { + } + + void zkde_screencast_stream_unstable_v1_created(uint32_t node) override + { + Q_EMIT created(node); + } + +Q_SIGNALS: + void created(quint32 node); +}; + +class ScreencastV1 : public QObject, public QtWayland::zkde_screencast_unstable_v1 +{ + Q_OBJECT + +public: + ScreencastV1(QObject *parent) + : QObject(parent) + { + } + + ScreencastStreamV1 *createWindowStream(const QString &uuid) + { + return new ScreencastStreamV1(stream_window(uuid, 2), this); + } +}; + +class TestScreencastV1Interface : public QObject +{ + Q_OBJECT + +public: + TestScreencastV1Interface() + { + } + + ~TestScreencastV1Interface() override; + +private Q_SLOTS: + void initTestCase(); + void testCreate(); + +private: + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::EventQueue *m_queue = nullptr; + ScreencastV1 *m_screencast = nullptr; + + KWin::ScreencastV1Interface *m_screencastInterface = nullptr; + + QPointer m_triggered = nullptr; + QThread *m_thread; + KWin::Display *m_display = nullptr; +}; + +static const QString s_socketName = QStringLiteral("kwin-wayland-server-screencast-test-0"); + +void TestScreencastV1Interface::initTestCase() +{ + delete m_display; + m_display = new KWin::Display(this); + m_display->addSocketName(s_socketName); + m_display->start(); + QVERIFY(m_display->isRunning()); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + KWayland::Client::Registry registry; + + QSignalSpy screencastSpy(®istry, &KWayland::Client::Registry::interfacesAnnounced); + m_screencastInterface = new KWin::ScreencastV1Interface(m_display, this); + connect(m_screencastInterface, + &KWin::ScreencastV1Interface::windowScreencastRequested, + this, + [this](KWin::ScreencastStreamV1Interface *stream, const QString &winid) { + stream->sendCreated(123); + m_triggered = stream; + }); + + connect(®istry, + &KWayland::Client::Registry::interfaceAnnounced, + this, + [this, ®istry](const QByteArray &interfaceName, quint32 name, quint32 version) { + if (interfaceName != "zkde_screencast_unstable_v1") + return; + m_screencast = new ScreencastV1(this); + m_screencast->init(&*registry, name, version); + }); + registry.setEventQueue(m_queue); + registry.create(m_connection->display()); + QVERIFY(registry.isValid()); + registry.setup(); + wl_display_flush(m_connection->display()); + + QVERIFY(m_screencastInterface); + QVERIFY(m_screencast || screencastSpy.wait()); + QVERIFY(m_screencast); +} + +TestScreencastV1Interface::~TestScreencastV1Interface() +{ + delete m_queue; + m_queue = nullptr; + + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + m_connection->deleteLater(); + m_connection = nullptr; + + delete m_display; +} + +void TestScreencastV1Interface::testCreate() +{ + auto stream = m_screencast->createWindowStream("3"); + QVERIFY(stream); + + QSignalSpy spyWorking(stream, &ScreencastStreamV1::created); + QVERIFY(spyWorking.count() || spyWorking.wait()); + QVERIFY(m_triggered); + + QSignalSpy spyStop(m_triggered, &KWin::ScreencastStreamV1Interface::finished); + stream->close(); + QVERIFY(spyStop.count() || spyStop.wait()); +} + +QTEST_GUILESS_MAIN(TestScreencastV1Interface) + +#include "test_screencast.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/server/test_seat.cpp b/local/recipes/kde/kwin/source/autotests/wayland/server/test_seat.cpp new file mode 100644 index 0000000000..08b02a4fa1 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/server/test_seat.cpp @@ -0,0 +1,188 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +// WaylandServer +#include "wayland/display.h" +#include "wayland/keyboard.h" +#include "wayland/pointer.h" +#include "wayland/seat.h" + +using namespace KWin; + +class TestWaylandServerSeat : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testCapabilities(); + void testPointerButton(); + void testPointerPos(); + void testRepeatInfo(); + void testMultiple(); +}; + +static const QString s_socketName = QStringLiteral("kwin-wayland-server-seat-test-0"); + +void TestWaylandServerSeat::testCapabilities() +{ + KWin::Display display; + display.addSocketName(s_socketName); + display.start(); + SeatInterface *seat = new SeatInterface(&display, QStringLiteral("seat0"), &display); + QVERIFY(!seat->hasKeyboard()); + QVERIFY(!seat->hasPointer()); + QVERIFY(!seat->hasTouch()); + + QSignalSpy keyboardSpy(seat, &SeatInterface::hasKeyboardChanged); + seat->setHasKeyboard(true); + QCOMPARE(keyboardSpy.count(), 1); + QVERIFY(keyboardSpy.last().first().toBool()); + QVERIFY(seat->hasKeyboard()); + seat->setHasKeyboard(false); + QCOMPARE(keyboardSpy.count(), 2); + QVERIFY(!keyboardSpy.last().first().toBool()); + QVERIFY(!seat->hasKeyboard()); + seat->setHasKeyboard(false); + QCOMPARE(keyboardSpy.count(), 2); + + QSignalSpy pointerSpy(seat, &SeatInterface::hasPointerChanged); + seat->setHasPointer(true); + QCOMPARE(pointerSpy.count(), 1); + QVERIFY(pointerSpy.last().first().toBool()); + QVERIFY(seat->hasPointer()); + seat->setHasPointer(false); + QCOMPARE(pointerSpy.count(), 2); + QVERIFY(!pointerSpy.last().first().toBool()); + QVERIFY(!seat->hasPointer()); + seat->setHasPointer(false); + QCOMPARE(pointerSpy.count(), 2); + + QSignalSpy touchSpy(seat, &SeatInterface::hasTouchChanged); + seat->setHasTouch(true); + QCOMPARE(touchSpy.count(), 1); + QVERIFY(touchSpy.last().first().toBool()); + QVERIFY(seat->hasTouch()); + seat->setHasTouch(false); + QCOMPARE(touchSpy.count(), 2); + QVERIFY(!touchSpy.last().first().toBool()); + QVERIFY(!seat->hasTouch()); + seat->setHasTouch(false); + QCOMPARE(touchSpy.count(), 2); +} + +void TestWaylandServerSeat::testPointerButton() +{ + KWin::Display display; + display.addSocketName(s_socketName); + display.start(); + SeatInterface *seat = new SeatInterface(&display, QStringLiteral("seat0"), &display); + seat->setHasPointer(true); + + // no button pressed yet, should be released and no serial + QVERIFY(!seat->isPointerButtonPressed(0)); + QVERIFY(!seat->isPointerButtonPressed(1)); + QCOMPARE(seat->pointerButtonSerial(0), quint32(0)); + QCOMPARE(seat->pointerButtonSerial(1), quint32(0)); + + // mark the button as pressed + seat->notifyPointerButton(0, PointerButtonState::Pressed); + seat->notifyPointerFrame(); + QVERIFY(seat->isPointerButtonPressed(0)); + QCOMPARE(seat->pointerButtonSerial(0), display.serial()); + + // other button should still be unpressed + QVERIFY(!seat->isPointerButtonPressed(1)); + QCOMPARE(seat->pointerButtonSerial(1), quint32(0)); + + // release it again + seat->notifyPointerButton(0, PointerButtonState::Released); + seat->notifyPointerFrame(); + QVERIFY(!seat->isPointerButtonPressed(0)); + QCOMPARE(seat->pointerButtonSerial(0), display.serial()); +} + +void TestWaylandServerSeat::testPointerPos() +{ + KWin::Display display; + display.addSocketName(s_socketName); + display.start(); + SeatInterface *seat = new SeatInterface(&display, QStringLiteral("seat0"), &display); + seat->setHasPointer(true); + QSignalSpy seatPosSpy(seat, &SeatInterface::pointerPosChanged); + + QCOMPARE(seat->pointerPos(), QPointF()); + + seat->notifyPointerMotion(QPointF(10, 15)); + seat->notifyPointerFrame(); + QCOMPARE(seat->pointerPos(), QPointF(10, 15)); + QCOMPARE(seatPosSpy.count(), 1); + QCOMPARE(seatPosSpy.first().first().toPointF(), QPointF(10, 15)); + + seat->notifyPointerMotion(QPointF(10, 15)); + seat->notifyPointerFrame(); + QCOMPARE(seatPosSpy.count(), 1); + + seat->notifyPointerMotion(QPointF(5, 7)); + seat->notifyPointerFrame(); + QCOMPARE(seat->pointerPos(), QPointF(5, 7)); + QCOMPARE(seatPosSpy.count(), 2); + QCOMPARE(seatPosSpy.first().first().toPointF(), QPointF(10, 15)); + QCOMPARE(seatPosSpy.last().first().toPointF(), QPointF(5, 7)); +} + +void TestWaylandServerSeat::testRepeatInfo() +{ + KWin::Display display; + display.addSocketName(s_socketName); + display.start(); + SeatInterface *seat = new SeatInterface(&display, QStringLiteral("seat0"), &display); + seat->setHasKeyboard(true); + QCOMPARE(seat->keyboard()->keyRepeatRate(), 0); + QCOMPARE(seat->keyboard()->keyRepeatDelay(), 0); + seat->keyboard()->setRepeatInfo(25, 660); + QCOMPARE(seat->keyboard()->keyRepeatRate(), 25); + QCOMPARE(seat->keyboard()->keyRepeatDelay(), 660); + // setting negative values should result in 0 + seat->keyboard()->setRepeatInfo(-25, -660); + QCOMPARE(seat->keyboard()->keyRepeatRate(), 0); + QCOMPARE(seat->keyboard()->keyRepeatDelay(), 0); +} + +void TestWaylandServerSeat::testMultiple() +{ + KWin::Display display; + display.addSocketName(s_socketName); + display.start(); + QVERIFY(display.seats().isEmpty()); + SeatInterface *seat1 = new SeatInterface(&display, QStringLiteral("seat0"), &display); + QCOMPARE(display.seats().count(), 1); + QCOMPARE(display.seats().at(0), seat1); + SeatInterface *seat2 = new SeatInterface(&display, QStringLiteral("seat0"), &display); + QCOMPARE(display.seats().count(), 2); + QCOMPARE(display.seats().at(0), seat1); + QCOMPARE(display.seats().at(1), seat2); + SeatInterface *seat3 = new SeatInterface(&display, QStringLiteral("seat0"), &display); + QCOMPARE(display.seats().count(), 3); + QCOMPARE(display.seats().at(0), seat1); + QCOMPARE(display.seats().at(1), seat2); + QCOMPARE(display.seats().at(2), seat3); + + delete seat3; + QCOMPARE(display.seats().count(), 2); + QCOMPARE(display.seats().at(0), seat1); + QCOMPARE(display.seats().at(1), seat2); + + delete seat2; + QCOMPARE(display.seats().count(), 1); + QCOMPARE(display.seats().at(0), seat1); + + delete seat1; + QCOMPARE(display.seats().count(), 0); +} + +QTEST_GUILESS_MAIN(TestWaylandServerSeat) +#include "test_seat.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/server/test_tablet_interface.cpp b/local/recipes/kde/kwin/source/autotests/wayland/server/test_tablet_interface.cpp new file mode 100644 index 0000000000..b71cc3b44a --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/server/test_tablet_interface.cpp @@ -0,0 +1,596 @@ +/* + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ +// Qt +#include +#include +#include +#include +// WaylandServer +#include "core/inputdevice.h" +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/seat.h" +#include "wayland/tablet_v2.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/seat.h" +#include "KWayland/Client/surface.h" + +#include "qwayland-tablet-v2.h" + +using namespace KWin; +using namespace std::literals; + +class Tablet : public QtWayland::zwp_tablet_v2 +{ +public: + Tablet(::zwp_tablet_v2 *t) + : QtWayland::zwp_tablet_v2(t) + { + } +}; + +class TabletPad : public QObject, public QtWayland::zwp_tablet_pad_v2 +{ + Q_OBJECT +public: + TabletPad(::zwp_tablet_pad_v2 *t) + : QtWayland::zwp_tablet_pad_v2(t) + { + } + + void zwp_tablet_pad_v2_done() override + { + Q_ASSERT(!doneCalled); + doneCalled = true; + } + + void zwp_tablet_pad_v2_buttons(uint32_t buttons) override + { + Q_ASSERT(buttons == 1); + } + + void zwp_tablet_pad_v2_enter(uint32_t /*serial*/, struct ::zwp_tablet_v2 * /*tablet*/, struct ::wl_surface *surface) override + { + m_currentSurface = surface; + } + + void zwp_tablet_pad_v2_button(uint32_t /*time*/, uint32_t button, uint32_t state) override + { + buttonStates[m_currentSurface][button] = state; + Q_EMIT buttonReceived(); + } + + ::wl_surface *m_currentSurface = nullptr; + + bool doneCalled = false; + QHash<::wl_surface *, QHash> buttonStates; + +Q_SIGNALS: + void buttonReceived(); +}; + +class Tool : public QObject, public QtWayland::zwp_tablet_tool_v2 +{ + Q_OBJECT +public: + Tool(::zwp_tablet_tool_v2 *t) + : QtWayland::zwp_tablet_tool_v2(t) + { + } + + void zwp_tablet_tool_v2_proximity_in(uint32_t /*serial*/, struct ::zwp_tablet_v2 * /*tablet*/, struct ::wl_surface *surface) override + { + surfaceApproximated[surface]++; + } + + void zwp_tablet_tool_v2_frame(uint32_t time) override + { + Q_EMIT frame(time); + } + + QHash surfaceApproximated; +Q_SIGNALS: + void frame(quint32 time); +}; + +class TabletSeat : public QObject, public QtWayland::zwp_tablet_seat_v2 +{ + Q_OBJECT +public: + TabletSeat(::zwp_tablet_seat_v2 *seat) + : QtWayland::zwp_tablet_seat_v2(seat) + { + } + + void zwp_tablet_seat_v2_tablet_added(struct ::zwp_tablet_v2 *id) override + { + m_tablets << new Tablet(id); + Q_EMIT tabletAdded(); + } + void zwp_tablet_seat_v2_tool_added(struct ::zwp_tablet_tool_v2 *id) override + { + m_tools << new Tool(id); + Q_EMIT toolAdded(); + } + + void zwp_tablet_seat_v2_pad_added(struct ::zwp_tablet_pad_v2 *id) override + { + m_pads << new TabletPad(id); + Q_EMIT padAdded(); + } + + QList m_tablets; + QList m_pads; + QList m_tools; + +Q_SIGNALS: + void padAdded(); + void toolAdded(); + void tabletAdded(); +}; + +class DummyInputDevice : public InputDevice +{ + Q_OBJECT + +public: + explicit DummyInputDevice(QObject *parent = nullptr) + : InputDevice(parent) + { + } + + QString name() const override + { + return m_name; + } + + QString sysPath() const override + { + return m_sysPath; + } + + quint32 vendor() const override + { + return m_vendorId; + } + + quint32 product() const override + { + return m_productId; + } + + bool isEnabled() const override + { + return true; + } + + void setEnabled(bool enabled) override + { + } + + bool isKeyboard() const override + { + return false; + } + + bool isPointer() const override + { + return false; + } + + bool isTouchpad() const override + { + return false; + } + + bool isTouch() const override + { + return false; + } + + bool isTabletTool() const override + { + return m_tabletTool; + } + + bool isTabletPad() const override + { + return m_tabletPad; + } + + bool isTabletModeSwitch() const override + { + return false; + } + + bool isLidSwitch() const override + { + return false; + } + + int tabletPadButtonCount() const override + { + return 1; + } + + void setName(const QString &name) + { + m_name = name; + } + + void setSysPath(const QString &path) + { + m_sysPath = path; + } + + void setVendor(quint32 vendor) + { + m_vendorId = vendor; + } + + void setProduct(quint32 product) + { + m_productId = product; + } + + void setTabletTool(bool tool) + { + m_tabletTool = tool; + } + + void setTabletPad(bool pad) + { + m_tabletPad = pad; + } + + QList modeGroups() const override + { + return { + InputDeviceTabletPadModeGroup{ + .modeCount = 1, + .buttons = {0}, + .rings = {0}, + .strips = {0}, + }}; + } + +private: + QString m_name; + QString m_sysPath; + quint32 m_vendorId = 0; + quint32 m_productId = 0; + bool m_tabletTool = false; + bool m_tabletPad = false; +}; + +class DummyInputDeviceTabletTool : public InputDeviceTabletTool +{ + Q_OBJECT + +public: + explicit DummyInputDeviceTabletTool(QObject *parent = nullptr) + : InputDeviceTabletTool(parent) + { + } + + void setSerialId(quint64 serialId) + { + m_serialId = serialId; + } + + void setUniqueId(quint64 uniqueId) + { + m_uniqueId = uniqueId; + } + + void setType(Type type) + { + m_type = type; + } + + void setCapabilities(const QList &capabilities) + { + m_capabilities = capabilities; + } + + quint64 serialId() const override + { + return m_serialId; + } + + quint64 uniqueId() const override + { + return m_uniqueId; + } + + Type type() const override + { + return m_type; + } + + QList capabilities() const override + { + return m_capabilities; + } + +private: + quint64 m_serialId = 0; + quint64 m_uniqueId = 0; + Type m_type = Type::Pen; + QList m_capabilities; +}; + +class TestTabletInterface : public QObject +{ + Q_OBJECT +public: + TestTabletInterface() + { + } + ~TestTabletInterface() override; + +private Q_SLOTS: + void initTestCase(); + void testAdd(); + void testAddPad(); + void testInteractSimple_data(); + void testInteractSimple(); + void testInteractSurfaceChange_data(); + void testInteractSurfaceChange(); + +private: + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::EventQueue *m_queue; + KWayland::Client::Compositor *m_clientCompositor; + KWayland::Client::Seat *m_clientSeat = nullptr; + + QThread *m_thread; + KWin::Display m_display; + SeatInterface *m_seat; + CompositorInterface *m_serverCompositor; + + TabletSeat *m_tabletSeatClient = nullptr; + TabletSeat *m_tabletSeatClient2 = nullptr; + + TabletManagerV2Interface *m_tabletManager; + QList m_surfacesClient; + + DummyInputDevice *m_tabletDevice = nullptr; + TabletV2Interface *m_tablet; + DummyInputDevice *m_tabletPadDevice = nullptr; + TabletPadV2Interface *m_tabletPad = nullptr; + DummyInputDeviceTabletTool *m_toolDevice = nullptr; + TabletToolV2Interface *m_tool = nullptr; + + QList m_surfaces; +}; + +static const QString s_socketName = QStringLiteral("kwin-wayland-server-tablet-test-0"); + +void TestTabletInterface::initTestCase() +{ + m_display.addSocketName(s_socketName); + m_display.start(); + QVERIFY(m_display.isRunning()); + + m_seat = new SeatInterface(&m_display, QStringLiteral("seat0"), this); + m_serverCompositor = new CompositorInterface(&m_display, this); + m_tabletManager = new TabletManagerV2Interface(&m_display, this); + + connect(m_serverCompositor, &CompositorInterface::surfaceCreated, this, [this](SurfaceInterface *surface) { + m_surfaces += surface; + }); + + // setup connection + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + QVERIFY(!m_connection->connections().isEmpty()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + auto registry = new KWayland::Client::Registry(this); + connect(registry, &KWayland::Client::Registry::interfaceAnnounced, this, [this, registry](const QByteArray &interface, quint32 name, quint32 version) { + if (interface == "zwp_tablet_manager_v2") { + auto tabletClient = new QtWayland::zwp_tablet_manager_v2(registry->registry(), name, version); + auto _seat = tabletClient->get_tablet_seat(*m_clientSeat); + m_tabletSeatClient = new TabletSeat(_seat); + auto _seat2 = tabletClient->get_tablet_seat(*m_clientSeat); + m_tabletSeatClient2 = new TabletSeat(_seat2); + } + }); + connect(registry, &KWayland::Client::Registry::seatAnnounced, this, [this, registry](quint32 name, quint32 version) { + m_clientSeat = registry->createSeat(name, version); + }); + registry->setEventQueue(m_queue); + QSignalSpy compositorSpy(registry, &KWayland::Client::Registry::compositorAnnounced); + registry->create(m_connection->display()); + QVERIFY(registry->isValid()); + registry->setup(); + wl_display_flush(m_connection->display()); + + QVERIFY(compositorSpy.wait()); + m_clientCompositor = registry->createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + QVERIFY(m_clientCompositor->isValid()); + + QSignalSpy surfaceSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + for (int i = 0; i < 3; ++i) { + m_surfacesClient += m_clientCompositor->createSurface(this); + } + QVERIFY(surfaceSpy.count() < 3 && surfaceSpy.wait(200)); + QVERIFY(m_surfaces.count() == 3); + QVERIFY(m_tabletSeatClient); +} + +TestTabletInterface::~TestTabletInterface() +{ + if (m_queue) { + delete m_queue; + m_queue = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + delete m_tabletSeatClient; + delete m_tabletSeatClient2; + m_connection->deleteLater(); + m_connection = nullptr; +} + +void TestTabletInterface::testAdd() +{ + TabletSeatV2Interface *seatInterface = m_tabletManager->seat(m_seat); + QVERIFY(seatInterface); + + QSignalSpy tabletSpy(m_tabletSeatClient, &TabletSeat::tabletAdded); + m_tabletDevice = new DummyInputDevice(); + m_tabletDevice->setTabletTool(true); + m_tabletDevice->setVendor(1); + m_tabletDevice->setProduct(2); + m_tabletDevice->setName(QStringLiteral("my tablet")); + m_tabletDevice->setSysPath(QStringLiteral("/test/event33")); + m_tablet = seatInterface->addTablet(m_tabletDevice); + QVERIFY(m_tablet); + QVERIFY(tabletSpy.wait() || tabletSpy.count() == 1); + QCOMPARE(m_tabletSeatClient->m_tablets.count(), 1); + + QSignalSpy toolSpy(m_tabletSeatClient, &TabletSeat::toolAdded); + m_toolDevice = new DummyInputDeviceTabletTool(); + m_toolDevice->setSerialId(0); + m_toolDevice->setUniqueId(0); + m_toolDevice->setType(InputDeviceTabletTool::Pen); + m_toolDevice->setCapabilities({InputDeviceTabletTool::Tilt, InputDeviceTabletTool::Pressure}); + m_tool = seatInterface->addTool(m_toolDevice); + QVERIFY(m_tool); + QVERIFY(toolSpy.wait() || toolSpy.count() == 1); + QCOMPARE(m_tabletSeatClient->m_tools.count(), 1); + + QVERIFY(!m_tool->isClientSupported()); // There's no surface in it yet + m_tool->setCurrentSurface(nullptr); + QVERIFY(!m_tool->isClientSupported()); // There's no surface in it + + QCOMPARE(m_surfaces.count(), 3); + for (SurfaceInterface *surface : m_surfaces) { + m_tool->setCurrentSurface(surface); + } + m_tool->setCurrentSurface(nullptr); +} + +void TestTabletInterface::testAddPad() +{ + TabletSeatV2Interface *seatInterface = m_tabletManager->seat(m_seat); + QVERIFY(seatInterface); + + QSignalSpy tabletPadSpy(m_tabletSeatClient, &TabletSeat::padAdded); + m_tabletPadDevice = new DummyInputDevice(); + m_tabletPadDevice->setTabletPad(true); + m_tabletPadDevice->setName(QStringLiteral("tabletpad")); + m_tabletPadDevice->setSysPath(QStringLiteral("/test/event33")); + m_tabletPad = seatInterface->addPad(m_tabletPadDevice); + QVERIFY(m_tabletPad); + QVERIFY(tabletPadSpy.wait() || tabletPadSpy.count() == 1); + QCOMPARE(m_tabletSeatClient->m_pads.count(), 1); + QVERIFY(m_tabletSeatClient->m_pads[0]); + + QVERIFY(m_tabletPad->group(0)->ring(0)); + QVERIFY(m_tabletPad->group(0)->strip(0)); + + QCOMPARE(m_surfaces.count(), 3); + QVERIFY(m_tabletSeatClient->m_pads[0]->buttonStates.isEmpty()); + QSignalSpy buttonSpy(m_tabletSeatClient->m_pads[0], &TabletPad::buttonReceived); + m_tabletPad->setCurrentSurface(m_surfaces[0], m_tablet); + m_tabletPad->sendButton(123ms, 0, QtWayland::zwp_tablet_pad_v2::button_state_pressed); + QVERIFY(buttonSpy.count() || buttonSpy.wait(100)); + QCOMPARE(m_tabletSeatClient->m_pads[0]->doneCalled, true); + QCOMPARE(m_tabletSeatClient->m_pads[0]->buttonStates.count(), 1); + QCOMPARE(m_tabletSeatClient->m_pads[0]->buttonStates[*m_surfacesClient[0]][0], QtWayland::zwp_tablet_pad_v2::button_state_pressed); +} + +static uint s_serial = 0; + +void TestTabletInterface::testInteractSimple_data() +{ + QTest::addColumn("tabletSeatClient"); + QTest::newRow("first client") << m_tabletSeatClient; + QTest::newRow("second client") << m_tabletSeatClient2; +} + +void TestTabletInterface::testInteractSimple() +{ + QFETCH(TabletSeat *, tabletSeatClient); + tabletSeatClient->m_tools[0]->surfaceApproximated.clear(); + QSignalSpy frameSpy(tabletSeatClient->m_tools[0], &Tool::frame); + + QVERIFY(!m_tool->isClientSupported()); + m_tool->setCurrentSurface(m_surfaces[0]); + QVERIFY(m_tool->isClientSupported() && m_tablet->isSurfaceSupported(m_surfaces[0])); + m_tool->sendProximityIn(m_tablet); + m_tool->sendPressure(0); + m_tool->sendFrame(s_serial++); + m_tool->sendMotion({3, 3}); + m_tool->sendFrame(s_serial++); + m_tool->sendProximityOut(); + QVERIFY(m_tool->isClientSupported()); + m_tool->sendFrame(s_serial++); + QVERIFY(!m_tool->isClientSupported()); + + QVERIFY(frameSpy.wait(500)); + QCOMPARE(tabletSeatClient->m_tools[0]->surfaceApproximated.count(), 1); +} + +void TestTabletInterface::testInteractSurfaceChange_data() +{ + QTest::addColumn("tabletSeatClient"); + QTest::newRow("first client") << m_tabletSeatClient; + QTest::newRow("second client") << m_tabletSeatClient2; +} + +void TestTabletInterface::testInteractSurfaceChange() +{ + QFETCH(TabletSeat *, tabletSeatClient); + tabletSeatClient->m_tools[0]->surfaceApproximated.clear(); + QSignalSpy frameSpy(tabletSeatClient->m_tools[0], &Tool::frame); + + QVERIFY(!m_tool->isClientSupported()); + m_tool->setCurrentSurface(m_surfaces[0]); + QVERIFY(m_tool->isClientSupported() && m_tablet->isSurfaceSupported(m_surfaces[0])); + m_tool->sendProximityIn(m_tablet); + m_tool->sendPressure(0); + m_tool->sendFrame(s_serial++); + + m_tool->setCurrentSurface(m_surfaces[1]); + QVERIFY(m_tool->isClientSupported()); + + m_tool->sendMotion({3, 3}); + m_tool->sendFrame(s_serial++); + m_tool->sendProximityOut(); + QVERIFY(m_tool->isClientSupported()); + m_tool->sendFrame(s_serial++); + QVERIFY(!m_tool->isClientSupported()); + + QVERIFY(frameSpy.wait(500)); + QCOMPARE(tabletSeatClient->m_tools[0]->surfaceApproximated.count(), 2); +} + +QTEST_GUILESS_MAIN(TestTabletInterface) +#include "test_tablet_interface.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/server/test_textinputv1_interface.cpp b/local/recipes/kde/kwin/source/autotests/wayland/server/test_textinputv1_interface.cpp new file mode 100644 index 0000000000..662b6114da --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/server/test_textinputv1_interface.cpp @@ -0,0 +1,462 @@ +/* + SPDX-FileCopyrightText: 2020 Bhushan Shah + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include +#include +#include + +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/seat.h" +#include "wayland/surface.h" +#include "wayland/textinput_v1.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/seat.h" +#include "KWayland/Client/surface.h" + +#include "qwayland-text-input-unstable-v1.h" + +using namespace KWin; + +Q_DECLARE_METATYPE(QtWayland::zwp_text_input_v1::content_purpose) +Q_DECLARE_METATYPE(QtWayland::zwp_text_input_v1::content_hint) + +class TextInputV1 : public QObject, public QtWayland::zwp_text_input_v1 +{ + Q_OBJECT +Q_SIGNALS: + void surface_enter(wl_surface *surface); + void surface_leave(); + void commit_string(const QString &text); + void delete_surrounding_text(qint32 index, quint32 length); + void preedit_string(const QString &text, const QString &commit); + +public: + void zwp_text_input_v1_enter(struct ::wl_surface *surface) override + { + Q_EMIT surface_enter(surface); + } + void zwp_text_input_v1_leave() override + { + Q_EMIT surface_leave(); + } + void zwp_text_input_v1_commit_string(uint32_t serial, const QString &text) override + { + Q_EMIT commit_string(text); + } + void zwp_text_input_v1_delete_surrounding_text(int32_t index, uint32_t length) override + { + Q_EMIT delete_surrounding_text(index, length); + } + void zwp_text_input_v1_preedit_string(uint32_t serial, const QString &text, const QString &commit) override + { + Q_EMIT preedit_string(text, commit); + } +}; + +class TextInputManagerV1 : public QtWayland::zwp_text_input_manager_v1 +{ +public: + ~TextInputManagerV1() override + { + } +}; + +class TestTextInputV1Interface : public QObject +{ + Q_OBJECT + +public: + ~TestTextInputV1Interface() override; + +private Q_SLOTS: + void initTestCase(); + void testEnableDisable(); + void testEvents(); + void testContentPurpose_data(); + void testContentPurpose(); + void testContentHints_data(); + void testContentHints(); + +private: + KWayland::Client::ConnectionThread *m_connection = nullptr; + KWayland::Client::EventQueue *m_queue = nullptr; + KWayland::Client::Compositor *m_clientCompositor = nullptr; + KWayland::Client::Seat *m_clientSeat = nullptr; + + SeatInterface *m_seat; + QThread *m_thread; + KWin::Display m_display; + TextInputV1 *m_clientTextInputV1; + CompositorInterface *m_serverCompositor; + TextInputV1Interface *m_serverTextInputV1; + TextInputManagerV1 *m_clientTextInputManagerV1; + + quint32 m_totalCommits = 0; +}; + +static const QString s_socketName = QStringLiteral("kwin-wayland-server-text-input-v1-test-0"); + +void TestTextInputV1Interface::initTestCase() +{ + m_display.addSocketName(s_socketName); + m_display.start(); + QVERIFY(m_display.isRunning()); + + m_seat = new SeatInterface(&m_display, QStringLiteral("seat0"), this); + m_seat->setHasKeyboard(true); + + m_serverCompositor = new CompositorInterface(&m_display, this); + new TextInputManagerV1Interface(&m_display); + + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + QVERIFY(!m_connection->connections().isEmpty()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + auto registry = new KWayland::Client::Registry(this); + connect(registry, &KWayland::Client::Registry::interfaceAnnounced, this, [this, registry](const QByteArray &interface, quint32 id, quint32 version) { + if (interface == QByteArrayLiteral("zwp_text_input_manager_v1")) { + m_clientTextInputManagerV1 = new TextInputManagerV1(); + m_clientTextInputManagerV1->init(*registry, id, version); + } + }); + + connect(registry, &KWayland::Client::Registry::seatAnnounced, this, [this, registry](quint32 name, quint32 version) { + m_clientSeat = registry->createSeat(name, version); + }); + + QSignalSpy allAnnouncedSpy(registry, &KWayland::Client::Registry::interfaceAnnounced); + QSignalSpy compositorSpy(registry, &KWayland::Client::Registry::compositorAnnounced); + QSignalSpy shmSpy(registry, &KWayland::Client::Registry::shmAnnounced); + registry->setEventQueue(m_queue); + registry->create(m_connection->display()); + QVERIFY(registry->isValid()); + registry->setup(); + QVERIFY(allAnnouncedSpy.wait()); + + m_clientCompositor = registry->createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + QVERIFY(m_clientCompositor->isValid()); + // create a text input v1 + m_clientTextInputV1 = new TextInputV1(); + m_clientTextInputV1->init(m_clientTextInputManagerV1->create_text_input()); + QVERIFY(m_clientTextInputV1); +} + +TestTextInputV1Interface::~TestTextInputV1Interface() +{ + if (m_clientTextInputV1) { + delete m_clientTextInputV1; + m_clientTextInputV1 = nullptr; + } + if (m_clientTextInputManagerV1) { + delete m_clientTextInputManagerV1; + m_clientTextInputManagerV1 = nullptr; + } + if (m_queue) { + delete m_queue; + m_queue = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + m_connection->deleteLater(); + m_connection = nullptr; +} + +// Ensures that enable disable events don't fire without commit +void TestTextInputV1Interface::testEnableDisable() +{ + // create a surface + QSignalSpy serverSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + std::unique_ptr clientSurface(m_clientCompositor->createSurface(this)); + QVERIFY(serverSurfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + m_serverTextInputV1 = m_seat->textInputV1(); + QVERIFY(m_serverTextInputV1); + + QSignalSpy focusedSurfaceChangedSpy(m_seat, &SeatInterface::focusedTextInputSurfaceChanged); + QSignalSpy textInputEnabledSpy(m_serverTextInputV1, &TextInputV1Interface::enabledChanged); + QSignalSpy cursorRectangleChangedSpy(m_serverTextInputV1, &TextInputV1Interface::cursorRectangleChanged); + + QSignalSpy surfaceEnterSpy(m_clientTextInputV1, &TextInputV1::surface_enter); + QSignalSpy surfaceLeaveSpy(m_clientTextInputV1, &TextInputV1::surface_leave); + + // Enter the textinput + + QCOMPARE(focusedSurfaceChangedSpy.count(), 0); + + // Make sure that entering surface does not trigger the text input + m_seat->setFocusedTextInputSurface(serverSurface); + QCOMPARE(focusedSurfaceChangedSpy.count(), 1); + QCOMPARE(surfaceEnterSpy.count(), 0); + QCOMPARE(textInputEnabledSpy.count(), 0); + + // Now enable the textInput, we should not get event just yet + m_clientTextInputV1->activate(*m_clientSeat, *clientSurface); + m_clientTextInputV1->set_cursor_rectangle(0, 0, 20, 20); + m_clientTextInputV1->set_surrounding_text("KDE Plasma Desktop", 0, 3); + QVERIFY(surfaceEnterSpy.wait()); + + QCOMPARE(textInputEnabledSpy.count(), 1); + QCOMPARE(cursorRectangleChangedSpy.count(), 1); + QCOMPARE(m_serverTextInputV1->cursorRectangle(), Rect(0, 0, 20, 20)); + QCOMPARE(m_serverTextInputV1->surroundingText(), QString("KDE Plasma Desktop")); + QCOMPARE(m_serverTextInputV1->surroundingTextCursorPosition(), 0); + QCOMPARE(m_serverTextInputV1->surroundingTextSelectionAnchor(), 3); + + // disabling we should get the event + m_clientTextInputV1->deactivate(*m_clientSeat); + QVERIFY(textInputEnabledSpy.wait()); + QCOMPARE(textInputEnabledSpy.count(), 2); + QVERIFY(surfaceLeaveSpy.wait()); + + // Lets try leaving the surface and make sure event propogage + m_seat->setFocusedTextInputSurface(nullptr); + QCOMPARE(surfaceLeaveSpy.count(), 1); +} + +void TestTextInputV1Interface::testEvents() +{ + // create a surface + QSignalSpy serverSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + std::unique_ptr clientSurface(m_clientCompositor->createSurface(this)); + QVERIFY(serverSurfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + m_serverTextInputV1 = m_seat->textInputV1(); + QVERIFY(m_serverTextInputV1); + + QSignalSpy focusedSurfaceChangedSpy(m_seat, &SeatInterface::focusedTextInputSurfaceChanged); + QSignalSpy textInputEnabledSpy(m_serverTextInputV1, &TextInputV1Interface::enabledChanged); + + // Enter the textinput + QCOMPARE(focusedSurfaceChangedSpy.count(), 0); + + // Make sure that entering surface does not trigger the text input + m_seat->setFocusedTextInputSurface(serverSurface); + // FIXME: somehow this triggers BEFORE setFocusedTextInputSurface returns :( + // QVERIFY(focusedSurfaceChangedSpy.wait()); + QCOMPARE(focusedSurfaceChangedSpy.count(), 1); + + // Now enable the textInput + m_clientTextInputV1->activate(*m_clientSeat, *clientSurface); + QVERIFY(textInputEnabledSpy.wait()); + + QSignalSpy preEditSpy(m_clientTextInputV1, &TextInputV1::preedit_string); + QSignalSpy commitStringSpy(m_clientTextInputV1, &TextInputV1::commit_string); + QSignalSpy deleteSurroundingSpy(m_clientTextInputV1, &TextInputV1::delete_surrounding_text); + + m_serverTextInputV1->preEdit("Hello KDE community!", "Hello"); + m_serverTextInputV1->deleteSurroundingText(6, 10); + m_serverTextInputV1->commitString("Plasma"); + + // Wait for the last update + QVERIFY(commitStringSpy.wait()); + + QCOMPARE(preEditSpy.last().at(0).value(), "Hello KDE community!"); + QCOMPARE(preEditSpy.last().at(1).value(), "Hello"); + QCOMPARE(commitStringSpy.last().at(0).value(), "Plasma"); + QCOMPARE(deleteSurroundingSpy.last().at(0).value(), 6); + QCOMPARE(deleteSurroundingSpy.last().at(1).value(), 10); + + // Now disable the textInput + m_clientTextInputV1->deactivate(*m_clientSeat); + QVERIFY(textInputEnabledSpy.wait()); +} + +void TestTextInputV1Interface::testContentPurpose_data() +{ + QTest::addColumn("clientPurpose"); + QTest::addColumn("serverPurpose"); + + QTest::newRow("Alpha") << QtWayland::zwp_text_input_v1::content_purpose_alpha << TextInputContentPurpose::Alpha; + QTest::newRow("Digits") << QtWayland::zwp_text_input_v1::content_purpose_digits << TextInputContentPurpose::Digits; + QTest::newRow("Number") << QtWayland::zwp_text_input_v1::content_purpose_number << TextInputContentPurpose::Number; + QTest::newRow("Phone") << QtWayland::zwp_text_input_v1::content_purpose_phone << TextInputContentPurpose::Phone; + QTest::newRow("Url") << QtWayland::zwp_text_input_v1::content_purpose_url << TextInputContentPurpose::Url; + QTest::newRow("Email") << QtWayland::zwp_text_input_v1::content_purpose_email << TextInputContentPurpose::Email; + QTest::newRow("Name") << QtWayland::zwp_text_input_v1::content_purpose_name << TextInputContentPurpose::Name; + QTest::newRow("Password") << QtWayland::zwp_text_input_v1::content_purpose_password << TextInputContentPurpose::Password; + QTest::newRow("Date") << QtWayland::zwp_text_input_v1::content_purpose_date << TextInputContentPurpose::Date; + QTest::newRow("Time") << QtWayland::zwp_text_input_v1::content_purpose_time << TextInputContentPurpose::Time; + QTest::newRow("DateTime") << QtWayland::zwp_text_input_v1::content_purpose_datetime << TextInputContentPurpose::DateTime; + QTest::newRow("Terminal") << QtWayland::zwp_text_input_v1::content_purpose_terminal << TextInputContentPurpose::Terminal; +} + +void TestTextInputV1Interface::testContentPurpose() +{ + // create a surface + QSignalSpy serverSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + std::unique_ptr clientSurface(m_clientCompositor->createSurface(this)); + QVERIFY(serverSurfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + m_serverTextInputV1 = m_seat->textInputV1(); + QVERIFY(m_serverTextInputV1); + + QSignalSpy focusedSurfaceChangedSpy(m_seat, &SeatInterface::focusedTextInputSurfaceChanged); + QSignalSpy textInputEnabledSpy(m_serverTextInputV1, &TextInputV1Interface::enabledChanged); + + // Enter the textinput + QCOMPARE(focusedSurfaceChangedSpy.count(), 0); + + // Make sure that entering surface does not trigger the text input + m_seat->setFocusedTextInputSurface(serverSurface); + // FIXME: somehow this triggers BEFORE setFocusedTextInputSurface returns :( + // QVERIFY(focusedSurfaceChangedSpy.wait()); + QCOMPARE(focusedSurfaceChangedSpy.count(), 1); + + // Now enable the textInput + m_clientTextInputV1->activate(*m_clientSeat, *clientSurface); + QVERIFY(textInputEnabledSpy.wait()); + m_totalCommits++; + + // Default should be normal content purpose + QCOMPARE(m_serverTextInputV1->contentPurpose(), TextInputContentPurpose::Normal); + + QSignalSpy contentTypeChangedSpy(m_serverTextInputV1, &TextInputV1Interface::contentTypeChanged); + + QFETCH(QtWayland::zwp_text_input_v1::content_purpose, clientPurpose); + m_clientTextInputV1->activate(*m_clientSeat, *clientSurface); + m_clientTextInputV1->set_content_type(QtWayland::zwp_text_input_v1::content_hint_none, clientPurpose); + QVERIFY(contentTypeChangedSpy.wait()); + QTEST(m_serverTextInputV1->contentPurpose(), "serverPurpose"); + m_totalCommits++; + + // Setting same thing should not trigger update + m_clientTextInputV1->activate(*m_clientSeat, *clientSurface); + m_clientTextInputV1->set_content_type(QtWayland::zwp_text_input_v1::content_hint_none, clientPurpose); + QVERIFY(!contentTypeChangedSpy.wait(100)); + m_totalCommits++; + + // unset to normal + m_clientTextInputV1->activate(*m_clientSeat, *clientSurface); + m_clientTextInputV1->set_content_type(QtWayland::zwp_text_input_v1::content_hint_none, QtWayland::zwp_text_input_v1::content_purpose_normal); + QVERIFY(contentTypeChangedSpy.wait()); + m_totalCommits++; + QCOMPARE(m_serverTextInputV1->contentPurpose(), TextInputContentPurpose::Normal); + + // Now disable the textInput + m_clientTextInputV1->deactivate(*m_clientSeat); + m_totalCommits++; + QVERIFY(textInputEnabledSpy.wait()); +} + +void TestTextInputV1Interface::testContentHints_data() +{ + QTest::addColumn("clientHint"); + QTest::addColumn("serverHints"); + + QTest::addRow("Spellcheck") << quint32(QtWayland::zwp_text_input_v1::content_hint_auto_correction) + << TextInputContentHints(TextInputContentHint::AutoCorrection); + QTest::addRow("Completion") << quint32(QtWayland::zwp_text_input_v1::content_hint_auto_completion) + << TextInputContentHints(TextInputContentHint::AutoCompletion); + QTest::addRow("AutoCapital") << quint32(QtWayland::zwp_text_input_v1::content_hint_auto_capitalization) + << TextInputContentHints(TextInputContentHint::AutoCapitalization); + QTest::addRow("Lowercase") << quint32(QtWayland::zwp_text_input_v1::content_hint_lowercase) << TextInputContentHints(TextInputContentHint::LowerCase); + QTest::addRow("Uppercase") << quint32(QtWayland::zwp_text_input_v1::content_hint_uppercase) << TextInputContentHints(TextInputContentHint::UpperCase); + QTest::addRow("Titlecase") << quint32(QtWayland::zwp_text_input_v1::content_hint_titlecase) << TextInputContentHints(TextInputContentHint::TitleCase); + QTest::addRow("HiddenText") << quint32(QtWayland::zwp_text_input_v1::content_hint_hidden_text) << TextInputContentHints(TextInputContentHint::HiddenText); + QTest::addRow("SensitiveData") << quint32(QtWayland::zwp_text_input_v1::content_hint_sensitive_data) + << TextInputContentHints(TextInputContentHint::SensitiveData); + QTest::addRow("Latin") << quint32(QtWayland::zwp_text_input_v1::content_hint_latin) << TextInputContentHints(TextInputContentHint::Latin); + QTest::addRow("Multiline") << quint32(QtWayland::zwp_text_input_v1::content_hint_multiline) << TextInputContentHints(TextInputContentHint::MultiLine); + QTest::addRow("Auto") << quint32(QtWayland::zwp_text_input_v1::content_hint_auto_completion | QtWayland::zwp_text_input_v1::content_hint_auto_correction + | QtWayland::zwp_text_input_v1::content_hint_auto_capitalization) + << TextInputContentHints(TextInputContentHint::AutoCompletion | TextInputContentHint::AutoCorrection + | TextInputContentHint::AutoCapitalization); +} + +void TestTextInputV1Interface::testContentHints() +{ + // create a surface + QSignalSpy serverSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + std::unique_ptr clientSurface(m_clientCompositor->createSurface(this)); + QVERIFY(serverSurfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + m_serverTextInputV1 = m_seat->textInputV1(); + QVERIFY(m_serverTextInputV1); + + QSignalSpy focusedSurfaceChangedSpy(m_seat, &SeatInterface::focusedTextInputSurfaceChanged); + QSignalSpy textInputEnabledSpy(m_serverTextInputV1, &TextInputV1Interface::enabledChanged); + + // Enter the textinput + QCOMPARE(focusedSurfaceChangedSpy.count(), 0); + + // Make sure that entering surface does not trigger the text input + m_seat->setFocusedTextInputSurface(serverSurface); + // FIXME: somehow this triggers BEFORE setFocusedTextInputSurface returns :( + // QVERIFY(focusedSurfaceChangedSpy.wait()); + QCOMPARE(focusedSurfaceChangedSpy.count(), 1); + + // Now enable the textInput + m_clientTextInputV1->activate(*m_clientSeat, *clientSurface); + QVERIFY(textInputEnabledSpy.wait()); + m_totalCommits++; + + QCOMPARE(m_serverTextInputV1->contentHints(), TextInputContentHint::None); + + // Now disable the textInput + m_clientTextInputV1->deactivate(*m_clientSeat); + QVERIFY(textInputEnabledSpy.wait()); + m_totalCommits++; + + QSignalSpy contentTypeChangedSpy(m_serverTextInputV1, &TextInputV1Interface::contentTypeChanged); + + QFETCH(quint32, clientHint); + m_clientTextInputV1->activate(*m_clientSeat, *clientSurface); + m_clientTextInputV1->set_content_type(clientHint, QtWayland::zwp_text_input_v1::content_purpose_normal); + QVERIFY(contentTypeChangedSpy.wait()); + QTEST(m_serverTextInputV1->contentHints(), "serverHints"); + m_totalCommits++; + + // Setting same thing should not trigger update + m_clientTextInputV1->activate(*m_clientSeat, *clientSurface); + m_clientTextInputV1->set_content_type(clientHint, QtWayland::zwp_text_input_v1::content_purpose_normal); + QVERIFY(!contentTypeChangedSpy.wait(100)); + m_totalCommits++; + + // unset to normal + m_clientTextInputV1->activate(*m_clientSeat, *clientSurface); + m_clientTextInputV1->set_content_type(QtWayland::zwp_text_input_v1::content_hint_none, QtWayland::zwp_text_input_v1::content_purpose_normal); + QVERIFY(contentTypeChangedSpy.wait()); + m_totalCommits++; + + // Now disable the textInput + m_clientTextInputV1->deactivate(*m_clientSeat); + QVERIFY(textInputEnabledSpy.wait()); + m_totalCommits++; +} + +QTEST_GUILESS_MAIN(TestTextInputV1Interface) + +#include "test_textinputv1_interface.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/server/test_textinputv3_interface.cpp b/local/recipes/kde/kwin/source/autotests/wayland/server/test_textinputv3_interface.cpp new file mode 100644 index 0000000000..2b9d9dd1b8 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/server/test_textinputv3_interface.cpp @@ -0,0 +1,796 @@ +/* + SPDX-FileCopyrightText: 2020 Bhushan Shah + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include + +#include +#include +#include +#include + +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/seat.h" +#include "wayland/surface.h" +#include "wayland/textinput_v3.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/seat.h" +#include "KWayland/Client/surface.h" + +#include "qwayland-text-input-unstable-v3.h" + +using namespace KWin; + +Q_DECLARE_METATYPE(QtWayland::zwp_text_input_v3::content_purpose) +Q_DECLARE_METATYPE(QtWayland::zwp_text_input_v3::content_hint) + +class TextInputV3 : public QObject, public QtWayland::zwp_text_input_v3 +{ + Q_OBJECT +Q_SIGNALS: + void surface_enter(wl_surface *surface); + void surface_leave(wl_surface *surface); + void commit_string(const QString &text); + void delete_surrounding_text(quint32 before_length, quint32 after_length); + void preedit_string(const QString &text, quint32 cursor_begin, quint32 cursor_end); + void done(quint32 serial); + +public: + ~TextInputV3() override + { + destroy(); + } + void zwp_text_input_v3_enter(struct ::wl_surface *surface) override + { + Q_EMIT surface_enter(surface); + } + void zwp_text_input_v3_leave(struct ::wl_surface *surface) override + { + Q_EMIT surface_leave(surface); + } + void zwp_text_input_v3_commit_string(const QString &text) override + { + commitText = text; + } + void zwp_text_input_v3_delete_surrounding_text(uint32_t before_length, uint32_t after_length) override + { + before = before_length; + after = after_length; + } + void zwp_text_input_v3_done(uint32_t serial) override + { + Q_EMIT commit_string(commitText); + Q_EMIT preedit_string(preeditText, cursorBegin, cursorEnd); + Q_EMIT delete_surrounding_text(before, after); + Q_EMIT done(serial); + } + void zwp_text_input_v3_preedit_string(const QString &text, int32_t cursor_begin, int32_t cursor_end) override + { + preeditText = text; + cursorBegin = cursor_begin; + cursorEnd = cursor_end; + } + +private: + QString preeditText; + QString commitText; + uint32_t cursorBegin, cursorEnd; + uint32_t before, after; +}; + +class TextInputManagerV3 : public QtWayland::zwp_text_input_manager_v3 +{ +public: + ~TextInputManagerV3() override + { + destroy(); + } +}; + +class TestTextInputV3Interface : public QObject +{ + Q_OBJECT + +public: + ~TestTextInputV3Interface() override; + +private Q_SLOTS: + void initTestCase(); + void testEnableDisable(); + void testEvents(); + void testContentPurpose_data(); + void testContentPurpose(); + void testContentHints_data(); + void testContentHints(); + void testMultipleTextinputs(); + void testMultipleTextinputsOverlappedUpdate(); + +private: + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::EventQueue *m_queue; + KWayland::Client::Compositor *m_clientCompositor; + KWayland::Client::Seat *m_clientSeat = nullptr; + + SeatInterface *m_seat; + QThread *m_thread; + KWin::Display m_display; + TextInputV3 *m_clientTextInputV3; + CompositorInterface *m_serverCompositor; + TextInputV3Interface *m_serverTextInputV3; + TextInputManagerV3 *m_clientTextInputManagerV3; + + quint32 m_totalCommits = 0; +}; + +static const QString s_socketName = QStringLiteral("kwin-wayland-server-text-input-v3-test-0"); + +void TestTextInputV3Interface::initTestCase() +{ + m_display.addSocketName(s_socketName); + m_display.start(); + QVERIFY(m_display.isRunning()); + + m_seat = new SeatInterface(&m_display, QStringLiteral("seat0"), this); + m_seat->setHasKeyboard(true); + + m_serverCompositor = new CompositorInterface(&m_display, this); + new TextInputManagerV3Interface(&m_display); + + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + QVERIFY(!m_connection->connections().isEmpty()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + auto registry = new KWayland::Client::Registry(this); + connect(registry, &KWayland::Client::Registry::interfaceAnnounced, this, [this, registry](const QByteArray &interface, quint32 id, quint32 version) { + if (interface == QByteArrayLiteral("zwp_text_input_manager_v3")) { + m_clientTextInputManagerV3 = new TextInputManagerV3(); + m_clientTextInputManagerV3->init(*registry, id, version); + } + }); + + connect(registry, &KWayland::Client::Registry::seatAnnounced, this, [this, registry](quint32 name, quint32 version) { + m_clientSeat = registry->createSeat(name, version); + }); + + QSignalSpy allAnnouncedSpy(registry, &KWayland::Client::Registry::interfaceAnnounced); + QSignalSpy compositorSpy(registry, &KWayland::Client::Registry::compositorAnnounced); + QSignalSpy shmSpy(registry, &KWayland::Client::Registry::shmAnnounced); + registry->setEventQueue(m_queue); + registry->create(m_connection->display()); + QVERIFY(registry->isValid()); + registry->setup(); + QVERIFY(allAnnouncedSpy.wait()); + + m_clientCompositor = registry->createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + QVERIFY(m_clientCompositor->isValid()); + // create a text input v3 + m_clientTextInputV3 = new TextInputV3(); + m_clientTextInputV3->init(m_clientTextInputManagerV3->get_text_input(*m_clientSeat)); + QVERIFY(m_clientTextInputV3); + + delete registry; +} + +TestTextInputV3Interface::~TestTextInputV3Interface() +{ + if (m_clientTextInputV3) { + delete m_clientTextInputV3; + m_clientTextInputV3 = nullptr; + } + if (m_clientTextInputManagerV3) { + delete m_clientTextInputManagerV3; + m_clientTextInputManagerV3 = nullptr; + } + if (m_clientCompositor) { + delete m_clientCompositor; + m_clientCompositor = nullptr; + } + if (m_clientSeat) { + delete m_clientSeat; + m_clientSeat = nullptr; + } + if (m_queue) { + delete m_queue; + m_queue = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + m_connection->deleteLater(); + m_connection = nullptr; +} + +// Ensures that enable disable events don't fire without commit +void TestTextInputV3Interface::testEnableDisable() +{ + // create a surface + QSignalSpy serverSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + std::unique_ptr clientSurface(m_clientCompositor->createSurface(this)); + QVERIFY(serverSurfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + m_serverTextInputV3 = m_seat->textInputV3(); + QVERIFY(m_serverTextInputV3); + + QSignalSpy focusedSurfaceChangedSpy(m_seat, &SeatInterface::focusedTextInputSurfaceChanged); + QSignalSpy textInputEnabledSpy(m_serverTextInputV3, &TextInputV3Interface::enabledChanged); + QSignalSpy cursorRectangleChangedSpy(m_serverTextInputV3, &TextInputV3Interface::cursorRectangleChanged); + QSignalSpy enableRequestedSpy(m_serverTextInputV3, &TextInputV3Interface::enableRequested); + + QSignalSpy surfaceEnterSpy(m_clientTextInputV3, &TextInputV3::surface_enter); + QSignalSpy surfaceLeaveSpy(m_clientTextInputV3, &TextInputV3::surface_leave); + + // Enter the textinput + + QCOMPARE(focusedSurfaceChangedSpy.count(), 0); + + // Make sure that entering surface does not trigger the text input + m_seat->setFocusedTextInputSurface(serverSurface); + QVERIFY(surfaceEnterSpy.wait()); + QCOMPARE(surfaceEnterSpy.count(), 1); + QCOMPARE(focusedSurfaceChangedSpy.count(), 1); + QCOMPARE(textInputEnabledSpy.count(), 0); + + // Now enable the textInput, we should not get event just yet + m_clientTextInputV3->enable(); + m_clientTextInputV3->set_cursor_rectangle(0, 0, 20, 20); + m_clientTextInputV3->set_surrounding_text("KDE Plasma Desktop", 0, 3); + QCOMPARE(textInputEnabledSpy.count(), 0); + QCOMPARE(cursorRectangleChangedSpy.count(), 0); + + // after we do commit we should get event + m_clientTextInputV3->commit(); + QVERIFY(textInputEnabledSpy.wait()); + m_totalCommits++; + + QCOMPARE(enableRequestedSpy.count(), 0); + QCOMPARE(textInputEnabledSpy.count(), 1); + QCOMPARE(cursorRectangleChangedSpy.count(), 1); + QCOMPARE(m_serverTextInputV3->cursorRectangle(), Rect(0, 0, 20, 20)); + QCOMPARE(m_serverTextInputV3->surroundingText(), QString("KDE Plasma Desktop")); + QCOMPARE(m_serverTextInputV3->surroundingTextCursorPosition(), 0); + QCOMPARE(m_serverTextInputV3->surroundingTextSelectionAnchor(), 3); + + // Do another enable when it's already enabled. + m_clientTextInputV3->enable(); + m_clientTextInputV3->set_cursor_rectangle(0, 0, 20, 20); + m_clientTextInputV3->set_surrounding_text("KDE Plasma Desktop", 0, 3); + m_clientTextInputV3->commit(); + QVERIFY(enableRequestedSpy.wait()); + QCOMPARE(textInputEnabledSpy.count(), 1); + QCOMPARE(cursorRectangleChangedSpy.count(), 1); + QCOMPARE(m_serverTextInputV3->cursorRectangle(), Rect(0, 0, 20, 20)); + QCOMPARE(m_serverTextInputV3->surroundingText(), QString("KDE Plasma Desktop")); + QCOMPARE(m_serverTextInputV3->surroundingTextCursorPosition(), 0); + QCOMPARE(m_serverTextInputV3->surroundingTextSelectionAnchor(), 3); + m_totalCommits++; + + // disabling we should not get the event + m_clientTextInputV3->disable(); + QCOMPARE(textInputEnabledSpy.count(), 1); + + // after we do commit we should get event + m_clientTextInputV3->commit(); + QVERIFY(textInputEnabledSpy.wait()); + QCOMPARE(textInputEnabledSpy.count(), 2); + m_totalCommits++; + + // Lets try leaving the surface and make sure event propogage + m_seat->setFocusedTextInputSurface(nullptr); + QVERIFY(surfaceLeaveSpy.wait()); + QCOMPARE(surfaceLeaveSpy.count(), 1); +} + +void TestTextInputV3Interface::testEvents() +{ + // create a surface + QSignalSpy serverSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + std::unique_ptr clientSurface(m_clientCompositor->createSurface(this)); + QVERIFY(serverSurfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + m_serverTextInputV3 = m_seat->textInputV3(); + QVERIFY(m_serverTextInputV3); + + QSignalSpy focusedSurfaceChangedSpy(m_seat, &SeatInterface::focusedTextInputSurfaceChanged); + QSignalSpy textInputEnabledSpy(m_serverTextInputV3, &TextInputV3Interface::enabledChanged); + QSignalSpy doneSpy(m_clientTextInputV3, &TextInputV3::done); + + // Enter the textinput + QCOMPARE(focusedSurfaceChangedSpy.count(), 0); + + // Make sure that entering surface does not trigger the text input + m_seat->setFocusedTextInputSurface(serverSurface); + // FIXME: somehow this triggers BEFORE setFocusedTextInputSurface returns :( + // QVERIFY(focusedSurfaceChangedSpy.wait()); + QCOMPARE(focusedSurfaceChangedSpy.count(), 1); + + // Now enable the textInput + m_clientTextInputV3->enable(); + m_clientTextInputV3->commit(); + m_totalCommits++; + QVERIFY(textInputEnabledSpy.wait()); + QVERIFY(doneSpy.wait()); + QCOMPARE(doneSpy.count(), 1); + + QSignalSpy preEditSpy(m_clientTextInputV3, &TextInputV3::preedit_string); + QSignalSpy commitStringSpy(m_clientTextInputV3, &TextInputV3::commit_string); + QSignalSpy deleteSurroundingSpy(m_clientTextInputV3, &TextInputV3::delete_surrounding_text); + + m_serverTextInputV3->sendPreEditString("Hello KDE community!", 1, 2); + m_serverTextInputV3->deleteSurroundingText(6, 10); + m_serverTextInputV3->commitString("Plasma"); + m_serverTextInputV3->done(); + + QVERIFY(doneSpy.wait()); + QCOMPARE(doneSpy.count(), 2); + QCOMPARE(preEditSpy.count(), 1); + QCOMPARE(commitStringSpy.count(), 1); + QCOMPARE(deleteSurroundingSpy.count(), 1); + + QCOMPARE(preEditSpy.last().at(0).value(), "Hello KDE community!"); + QCOMPARE(preEditSpy.last().at(1).value(), 1); + QCOMPARE(preEditSpy.last().at(2).value(), 2); + QCOMPARE(commitStringSpy.last().at(0).value(), "Plasma"); + QCOMPARE(deleteSurroundingSpy.last().at(0).value(), 6); + QCOMPARE(deleteSurroundingSpy.last().at(1).value(), 10); + + // zwp_text_input_v3.done event have serial of total commits + QCOMPARE(doneSpy.last().at(0).value(), m_totalCommits); + + // Now disable the textInput + m_clientTextInputV3->disable(); + m_clientTextInputV3->commit(); + m_totalCommits++; + QVERIFY(textInputEnabledSpy.wait()); +} + +void TestTextInputV3Interface::testContentPurpose_data() +{ + QTest::addColumn("clientPurpose"); + QTest::addColumn("serverPurpose"); + + QTest::newRow("Alpha") << QtWayland::zwp_text_input_v3::content_purpose_alpha << TextInputContentPurpose::Alpha; + QTest::newRow("Digits") << QtWayland::zwp_text_input_v3::content_purpose_digits << TextInputContentPurpose::Digits; + QTest::newRow("Number") << QtWayland::zwp_text_input_v3::content_purpose_number << TextInputContentPurpose::Number; + QTest::newRow("Phone") << QtWayland::zwp_text_input_v3::content_purpose_phone << TextInputContentPurpose::Phone; + QTest::newRow("Url") << QtWayland::zwp_text_input_v3::content_purpose_url << TextInputContentPurpose::Url; + QTest::newRow("Email") << QtWayland::zwp_text_input_v3::content_purpose_email << TextInputContentPurpose::Email; + QTest::newRow("Name") << QtWayland::zwp_text_input_v3::content_purpose_name << TextInputContentPurpose::Name; + QTest::newRow("Password") << QtWayland::zwp_text_input_v3::content_purpose_password << TextInputContentPurpose::Password; + QTest::newRow("Pin") << QtWayland::zwp_text_input_v3::content_purpose_pin << TextInputContentPurpose::Pin; + QTest::newRow("Date") << QtWayland::zwp_text_input_v3::content_purpose_date << TextInputContentPurpose::Date; + QTest::newRow("Time") << QtWayland::zwp_text_input_v3::content_purpose_time << TextInputContentPurpose::Time; + QTest::newRow("DateTime") << QtWayland::zwp_text_input_v3::content_purpose_datetime << TextInputContentPurpose::DateTime; + QTest::newRow("Terminal") << QtWayland::zwp_text_input_v3::content_purpose_terminal << TextInputContentPurpose::Terminal; +} + +void TestTextInputV3Interface::testContentPurpose() +{ + // create a surface + QSignalSpy serverSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + std::unique_ptr clientSurface(m_clientCompositor->createSurface(this)); + QVERIFY(serverSurfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + m_serverTextInputV3 = m_seat->textInputV3(); + QVERIFY(m_serverTextInputV3); + + QSignalSpy focusedSurfaceChangedSpy(m_seat, &SeatInterface::focusedTextInputSurfaceChanged); + QSignalSpy textInputEnabledSpy(m_serverTextInputV3, &TextInputV3Interface::enabledChanged); + + // Enter the textinput + QCOMPARE(focusedSurfaceChangedSpy.count(), 0); + + // Make sure that entering surface does not trigger the text input + m_seat->setFocusedTextInputSurface(serverSurface); + // FIXME: somehow this triggers BEFORE setFocusedTextInputSurface returns :( + // QVERIFY(focusedSurfaceChangedSpy.wait()); + QCOMPARE(focusedSurfaceChangedSpy.count(), 1); + + // Now enable the textInput + m_clientTextInputV3->enable(); + m_clientTextInputV3->commit(); + QVERIFY(textInputEnabledSpy.wait()); + m_totalCommits++; + + // Default should be normal content purpose + QCOMPARE(m_serverTextInputV3->contentPurpose(), TextInputContentPurpose::Normal); + + QSignalSpy contentTypeChangedSpy(m_serverTextInputV3, &TextInputV3Interface::contentTypeChanged); + + QFETCH(QtWayland::zwp_text_input_v3::content_purpose, clientPurpose); + m_clientTextInputV3->enable(); + m_clientTextInputV3->set_content_type(QtWayland::zwp_text_input_v3::content_hint_none, clientPurpose); + m_clientTextInputV3->commit(); + QVERIFY(contentTypeChangedSpy.wait()); + QTEST(m_serverTextInputV3->contentPurpose(), "serverPurpose"); + m_totalCommits++; + + // Setting same thing should not trigger update + m_clientTextInputV3->enable(); + m_clientTextInputV3->set_content_type(QtWayland::zwp_text_input_v3::content_hint_none, clientPurpose); + m_clientTextInputV3->commit(); + QVERIFY(!contentTypeChangedSpy.wait(100)); + m_totalCommits++; + + // unset to normal + m_clientTextInputV3->enable(); + m_clientTextInputV3->set_content_type(QtWayland::zwp_text_input_v3::content_hint_none, QtWayland::zwp_text_input_v3::content_purpose_normal); + m_clientTextInputV3->commit(); + QVERIFY(contentTypeChangedSpy.wait()); + m_totalCommits++; + QCOMPARE(m_serverTextInputV3->contentPurpose(), TextInputContentPurpose::Normal); + + // Now disable the textInput + m_clientTextInputV3->disable(); + m_clientTextInputV3->commit(); + m_totalCommits++; + QVERIFY(textInputEnabledSpy.wait()); +} + +void TestTextInputV3Interface::testContentHints_data() +{ + QTest::addColumn("clientHint"); + QTest::addColumn("serverHints"); + + QTest::addRow("Spellcheck") << quint32(QtWayland::zwp_text_input_v3::content_hint_spellcheck) + << TextInputContentHints(TextInputContentHint::AutoCorrection); + QTest::addRow("Completion") << quint32(QtWayland::zwp_text_input_v3::content_hint_completion) + << TextInputContentHints(TextInputContentHint::AutoCompletion); + QTest::addRow("AutoCapital") << quint32(QtWayland::zwp_text_input_v3::content_hint_auto_capitalization) + << TextInputContentHints(TextInputContentHint::AutoCapitalization); + QTest::addRow("Lowercase") << quint32(QtWayland::zwp_text_input_v3::content_hint_lowercase) << TextInputContentHints(TextInputContentHint::LowerCase); + QTest::addRow("Uppercase") << quint32(QtWayland::zwp_text_input_v3::content_hint_uppercase) << TextInputContentHints(TextInputContentHint::UpperCase); + QTest::addRow("Titlecase") << quint32(QtWayland::zwp_text_input_v3::content_hint_titlecase) << TextInputContentHints(TextInputContentHint::TitleCase); + QTest::addRow("HiddenText") << quint32(QtWayland::zwp_text_input_v3::content_hint_hidden_text) << TextInputContentHints(TextInputContentHint::HiddenText); + QTest::addRow("SensitiveData") << quint32(QtWayland::zwp_text_input_v3::content_hint_sensitive_data) + << TextInputContentHints(TextInputContentHint::SensitiveData); + QTest::addRow("Latin") << quint32(QtWayland::zwp_text_input_v3::content_hint_latin) << TextInputContentHints(TextInputContentHint::Latin); + QTest::addRow("Multiline") << quint32(QtWayland::zwp_text_input_v3::content_hint_multiline) << TextInputContentHints(TextInputContentHint::MultiLine); + QTest::addRow("Auto") << quint32(QtWayland::zwp_text_input_v3::content_hint_completion | QtWayland::zwp_text_input_v3::content_hint_spellcheck + | QtWayland::zwp_text_input_v3::content_hint_auto_capitalization) + << TextInputContentHints(TextInputContentHint::AutoCompletion | TextInputContentHint::AutoCorrection + | TextInputContentHint::AutoCapitalization); +} + +void TestTextInputV3Interface::testContentHints() +{ + // create a surface + QSignalSpy serverSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + std::unique_ptr clientSurface(m_clientCompositor->createSurface(this)); + QVERIFY(serverSurfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + m_serverTextInputV3 = m_seat->textInputV3(); + QVERIFY(m_serverTextInputV3); + + QSignalSpy focusedSurfaceChangedSpy(m_seat, &SeatInterface::focusedTextInputSurfaceChanged); + QSignalSpy textInputEnabledSpy(m_serverTextInputV3, &TextInputV3Interface::enabledChanged); + + // Enter the textinput + QCOMPARE(focusedSurfaceChangedSpy.count(), 0); + + // Make sure that entering surface does not trigger the text input + m_seat->setFocusedTextInputSurface(serverSurface); + // FIXME: somehow this triggers BEFORE setFocusedTextInputSurface returns :( + // QVERIFY(focusedSurfaceChangedSpy.wait()); + QCOMPARE(focusedSurfaceChangedSpy.count(), 1); + + // Now enable the textInput + m_clientTextInputV3->enable(); + m_clientTextInputV3->commit(); + QVERIFY(textInputEnabledSpy.wait()); + m_totalCommits++; + + QCOMPARE(m_serverTextInputV3->contentHints(), TextInputContentHint::None); + + // Now disable the textInput + m_clientTextInputV3->disable(); + m_clientTextInputV3->commit(); + QVERIFY(textInputEnabledSpy.wait()); + m_totalCommits++; + + QSignalSpy contentTypeChangedSpy(m_serverTextInputV3, &TextInputV3Interface::contentTypeChanged); + + QFETCH(quint32, clientHint); + m_clientTextInputV3->enable(); + m_clientTextInputV3->set_content_type(clientHint, QtWayland::zwp_text_input_v3::content_purpose_normal); + m_clientTextInputV3->commit(); + QVERIFY(contentTypeChangedSpy.wait()); + QTEST(m_serverTextInputV3->contentHints(), "serverHints"); + m_totalCommits++; + + // Setting same thing should not trigger update + m_clientTextInputV3->enable(); + m_clientTextInputV3->set_content_type(clientHint, QtWayland::zwp_text_input_v3::content_purpose_normal); + m_clientTextInputV3->commit(); + QVERIFY(!contentTypeChangedSpy.wait(100)); + m_totalCommits++; + + // unset to normal + m_clientTextInputV3->enable(); + m_clientTextInputV3->set_content_type(QtWayland::zwp_text_input_v3::content_hint_none, QtWayland::zwp_text_input_v3::content_purpose_normal); + m_clientTextInputV3->commit(); + QVERIFY(contentTypeChangedSpy.wait()); + m_totalCommits++; + + // Now disable the textInput + m_clientTextInputV3->disable(); + m_clientTextInputV3->commit(); + QVERIFY(textInputEnabledSpy.wait()); + m_totalCommits++; +} + +void TestTextInputV3Interface::testMultipleTextinputs() +{ + // create two more text inputs + TextInputV3 *ti1 = new TextInputV3(); + ti1->init(m_clientTextInputManagerV3->get_text_input(*m_clientSeat)); + QVERIFY(ti1); + + TextInputV3 *ti2 = new TextInputV3(); + ti2->init(m_clientTextInputManagerV3->get_text_input(*m_clientSeat)); + QVERIFY(ti2); + + // create a surface + QSignalSpy serverSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + std::unique_ptr clientSurface(m_clientCompositor->createSurface(this)); + QVERIFY(serverSurfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + QSignalSpy focusedSurfaceChangedSpy(m_seat, &SeatInterface::focusedTextInputSurfaceChanged); + // Make sure that entering surface does not trigger the text input + m_seat->setFocusedTextInputSurface(serverSurface); + QCOMPARE(focusedSurfaceChangedSpy.count(), 1); + + m_serverTextInputV3 = m_seat->textInputV3(); + QVERIFY(m_serverTextInputV3); + QVERIFY(!m_serverTextInputV3->isEnabled()); + + QSignalSpy doneSpy1(ti1, &TextInputV3::done); + QSignalSpy committedSpy(m_serverTextInputV3, &TextInputV3Interface::stateCommitted); + // Enable ti1 + ti1->enable(); + ti1->commit(); + QVERIFY(committedSpy.wait()); + QCOMPARE(committedSpy.last().at(0).value(), 1); + QVERIFY(m_serverTextInputV3->isEnabled()); + QVERIFY(doneSpy1.wait()); + + // Send another three commits on ti1 + ti1->enable(); + ti1->set_surrounding_text("hello", 0, 1); + ti1->commit(); + QVERIFY(committedSpy.wait()); + QCOMPARE(committedSpy.last().at(0).value(), 2); + QVERIFY(m_serverTextInputV3->isEnabled()); + QVERIFY(doneSpy1.wait()); + + ti1->enable(); + ti1->set_content_type(QtWayland::zwp_text_input_v3::content_hint_none, QtWayland::zwp_text_input_v3::content_purpose_normal); + ti1->commit(); + QVERIFY(committedSpy.wait()); + QCOMPARE(committedSpy.last().at(0).value(), 3); + QVERIFY(m_serverTextInputV3->isEnabled()); + QVERIFY(doneSpy1.wait()); + + // at this point total commit count to ti1 is 3 + QSignalSpy doneSpy2(ti2, &TextInputV3::done); + + m_serverTextInputV3->commitString("Hello"); + m_serverTextInputV3->done(); + QVERIFY(doneSpy1.wait()); + + // zwp_text_input_v3.done event have serial of total commits + QCOMPARE(doneSpy1.last().at(0).value(), 3); + + // now ti1 is at 4 commit, while ti2 is still 0 + ti1->disable(); + ti1->commit(); + QVERIFY(committedSpy.wait()); + QCOMPARE(committedSpy.last().at(0).value(), 4); + QVERIFY(!m_serverTextInputV3->isEnabled()); + + // first commit to ti2 + ti2->enable(); + ti2->commit(); + QVERIFY(committedSpy.wait()); + QCOMPARE(committedSpy.last().at(0).value(), 1); + QVERIFY(m_serverTextInputV3->isEnabled()); + + // send commit string + m_serverTextInputV3->commitString("Hello world"); + m_serverTextInputV3->done(); + QVERIFY(doneSpy2.wait()); + + // ti2 is at one commit + QCOMPARE(doneSpy2.last().at(0).value(), 1); + ti2->disable(); + ti2->commit(); + QVERIFY(committedSpy.wait()); + QCOMPARE(committedSpy.last().at(0).value(), 2); + QVERIFY(!m_serverTextInputV3->isEnabled()); + + // now re-enable the ti1 and verify sending commits to t2 hasn't affected it's serial + // Enable ti1 : 5 commits now + ti1->enable(); + ti1->commit(); + QVERIFY(committedSpy.wait()); + QCOMPARE(committedSpy.last().at(0).value(), 5); + QVERIFY(m_serverTextInputV3->isEnabled()); + + // send done signal + m_serverTextInputV3->commitString("Hello"); + m_serverTextInputV3->done(); + QVERIFY(doneSpy1.wait()); + QCOMPARE(doneSpy1.last().at(0).value(), 5); + + // cleanup + if (ti1) { + delete ti1; + ti1 = nullptr; + } + if (ti2) { + delete ti2; + ti2 = nullptr; + } +} + +struct TextInputV3TestClient +{ + TextInputV3TestClient(QObject *parent, KWin::Display *display, const QString &socketName) + { + display->addSocketName(socketName); + connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(connection, &KWayland::Client::ConnectionThread::connected); + connection->setSocketName(socketName); + + thread = new QThread(parent); + connection->moveToThread(thread); + thread->start(); + + connection->initConnection(); + QVERIFY(connectedSpy.wait()); + QVERIFY(!connection->connections().isEmpty()); + + queue = std::make_unique(parent); + QVERIFY(!queue->isValid()); + queue->setup(connection); + QVERIFY(queue->isValid()); + + registry = std::make_unique(parent); + QObject::connect(registry.get(), &KWayland::Client::Registry::interfaceAnnounced, parent, [this](const QByteArray &interface, quint32 id, quint32 version) { + if (interface == QByteArrayLiteral("zwp_text_input_manager_v3")) { + textInputManagerV3 = std::make_unique(); + textInputManagerV3->init(*registry, id, version); + } + }); + QObject::connect(registry.get(), &KWayland::Client::Registry::seatAnnounced, parent, [this](quint32 name, quint32 version) { + seat.reset(registry->createSeat(name, version)); + }); + + QSignalSpy allAnnouncedSpy(registry.get(), &KWayland::Client::Registry::interfaceAnnounced); + QSignalSpy compositorSpy(registry.get(), &KWayland::Client::Registry::compositorAnnounced); + registry->setEventQueue(queue.get()); + registry->create(connection->display()); + QVERIFY(registry->isValid()); + registry->setup(); + QVERIFY(allAnnouncedSpy.wait()); + + clientCompositor.reset(registry->createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), parent)); + QVERIFY(clientCompositor->isValid()); + + textInput = std::make_unique(); + textInput->init(textInputManagerV3->get_text_input(*seat)); + } + + ~TextInputV3TestClient() + { + textInput.reset(); + clientCompositor.reset(); + textInputManagerV3.reset(); + seat.reset(); + registry.reset(); + queue.reset(); + if (thread) { + thread->quit(); + thread->wait(); + delete thread; + thread = nullptr; + } + connection->deleteLater(); + connection = nullptr; + } + + QThread *thread = nullptr; + KWayland::Client::ConnectionThread *connection = nullptr; + std::unique_ptr queue; + std::unique_ptr registry; + std::unique_ptr seat; + std::unique_ptr textInputManagerV3; + std::unique_ptr clientCompositor; + std::unique_ptr textInput; +}; + +void TestTextInputV3Interface::testMultipleTextinputsOverlappedUpdate() +{ + TextInputV3TestClient client1(this, &m_display, QString("%1_%2").arg(qAppName()).arg(1)); + TextInputV3TestClient client2(this, &m_display, QString("%1_%2").arg(qAppName()).arg(2)); + + // create a two surface + QSignalSpy serverSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + std::unique_ptr clientSurface(client1.clientCompositor->createSurface(this)); + QVERIFY(serverSurfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + serverSurfaceCreatedSpy.clear(); + std::unique_ptr clientSurface2(client2.clientCompositor->createSurface(this)); + QVERIFY(serverSurfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface2 = serverSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface2); + + QSignalSpy focusedSurfaceChangedSpy(m_seat, &SeatInterface::focusedTextInputSurfaceChanged); + // Make sure that entering surface does not trigger the text input + m_seat->setFocusedTextInputSurface(serverSurface); + QCOMPARE(focusedSurfaceChangedSpy.count(), 1); + + m_serverTextInputV3 = m_seat->textInputV3(); + QVERIFY(m_serverTextInputV3); + QVERIFY(!m_serverTextInputV3->isEnabled()); + + QSignalSpy doneSpy1(client1.textInput.get(), &TextInputV3::done); + QSignalSpy doneSpy2(client2.textInput.get(), &TextInputV3::done); + QSignalSpy committedSpy(m_serverTextInputV3, &TextInputV3Interface::stateCommitted); + // Send enable and disable at the same time. + client1.textInput->enable(); + client2.textInput->disable(); + client1.textInput->commit(); + QVERIFY(committedSpy.wait()); + QCOMPARE(committedSpy.last().at(0).value(), 1); + QVERIFY(m_serverTextInputV3->isEnabled()); + QVERIFY(doneSpy1.wait()); + + client2.textInput->commit(); + QVERIFY(committedSpy.wait()); + QCOMPARE(committedSpy.last().at(0).value(), 1); + QVERIFY(m_serverTextInputV3->isEnabled()); + QVERIFY(doneSpy2.wait()); + + m_seat->setFocusedTextInputSurface(serverSurface2); + QCOMPARE(focusedSurfaceChangedSpy.count(), 2); + QVERIFY(!m_serverTextInputV3->isEnabled()); +} + +QTEST_GUILESS_MAIN(TestTextInputV3Interface) + +#include "test_textinputv3_interface.moc" diff --git a/local/recipes/kde/kwin/source/autotests/wayland/server/test_viewporter_interface.cpp b/local/recipes/kde/kwin/source/autotests/wayland/server/test_viewporter_interface.cpp new file mode 100644 index 0000000000..1baf284195 --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/wayland/server/test_viewporter_interface.cpp @@ -0,0 +1,192 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL +*/ + +#include +#include +#include + +#include "wayland/compositor.h" +#include "wayland/display.h" +#include "wayland/surface.h" +#include "wayland/viewporter.h" + +#include "KWayland/Client/compositor.h" +#include "KWayland/Client/connection_thread.h" +#include "KWayland/Client/event_queue.h" +#include "KWayland/Client/registry.h" +#include "KWayland/Client/shm_pool.h" +#include "KWayland/Client/surface.h" + +#include "qwayland-viewporter.h" + +using namespace KWin; + +class Viewporter : public QtWayland::wp_viewporter +{ +}; + +class Viewport : public QtWayland::wp_viewport +{ +}; + +class TestViewporterInterface : public QObject +{ + Q_OBJECT + +public: + ~TestViewporterInterface() override; + +private Q_SLOTS: + void initTestCase(); + void testCropScale(); + +private: + KWayland::Client::ConnectionThread *m_connection; + KWayland::Client::EventQueue *m_queue; + KWayland::Client::Compositor *m_clientCompositor; + KWayland::Client::ShmPool *m_shm; + + QThread *m_thread; + KWin::Display m_display; + CompositorInterface *m_serverCompositor; + Viewporter *m_viewporter; +}; + +static const QString s_socketName = QStringLiteral("kwin-wayland-server-viewporter-test-0"); + +void TestViewporterInterface::initTestCase() +{ + m_display.addSocketName(s_socketName); + m_display.start(); + QVERIFY(m_display.isRunning()); + + m_display.createShm(); + new ViewporterInterface(&m_display); + + m_serverCompositor = new CompositorInterface(&m_display, this); + + m_connection = new KWayland::Client::ConnectionThread; + QSignalSpy connectedSpy(m_connection, &KWayland::Client::ConnectionThread::connected); + m_connection->setSocketName(s_socketName); + + m_thread = new QThread(this); + m_connection->moveToThread(m_thread); + m_thread->start(); + + m_connection->initConnection(); + QVERIFY(connectedSpy.wait()); + QVERIFY(!m_connection->connections().isEmpty()); + + m_queue = new KWayland::Client::EventQueue(this); + QVERIFY(!m_queue->isValid()); + m_queue->setup(m_connection); + QVERIFY(m_queue->isValid()); + + auto registry = new KWayland::Client::Registry(this); + connect(registry, &KWayland::Client::Registry::interfaceAnnounced, this, [this, registry](const QByteArray &interface, quint32 id, quint32 version) { + if (interface == QByteArrayLiteral("wp_viewporter")) { + m_viewporter = new Viewporter(); + m_viewporter->init(*registry, id, version); + } + }); + QSignalSpy allAnnouncedSpy(registry, &KWayland::Client::Registry::interfaceAnnounced); + QSignalSpy compositorSpy(registry, &KWayland::Client::Registry::compositorAnnounced); + QSignalSpy shmSpy(registry, &KWayland::Client::Registry::shmAnnounced); + registry->setEventQueue(m_queue); + registry->create(m_connection->display()); + QVERIFY(registry->isValid()); + registry->setup(); + QVERIFY(allAnnouncedSpy.wait()); + + m_clientCompositor = registry->createCompositor(compositorSpy.first().first().value(), compositorSpy.first().last().value(), this); + QVERIFY(m_clientCompositor->isValid()); + + m_shm = registry->createShmPool(shmSpy.first().first().value(), shmSpy.first().last().value(), this); + QVERIFY(m_shm->isValid()); +} + +TestViewporterInterface::~TestViewporterInterface() +{ + if (m_viewporter) { + delete m_viewporter; + m_viewporter = nullptr; + } + if (m_shm) { + delete m_shm; + m_shm = nullptr; + } + if (m_queue) { + delete m_queue; + m_queue = nullptr; + } + if (m_thread) { + m_thread->quit(); + m_thread->wait(); + delete m_thread; + m_thread = nullptr; + } + m_connection->deleteLater(); + m_connection = nullptr; +} + +void TestViewporterInterface::testCropScale() +{ + // Create a test surface. + QSignalSpy serverSurfaceCreatedSpy(m_serverCompositor, &CompositorInterface::surfaceCreated); + std::unique_ptr clientSurface(m_clientCompositor->createSurface(this)); + QVERIFY(serverSurfaceCreatedSpy.wait()); + SurfaceInterface *serverSurface = serverSurfaceCreatedSpy.first().first().value(); + QVERIFY(serverSurface); + + QSignalSpy serverSurfaceMappedSpy(serverSurface, &SurfaceInterface::mapped); + QSignalSpy serverSurfaceSizeChangedSpy(serverSurface, &SurfaceInterface::sizeChanged); + QSignalSpy bufferSourceBoxChangedSpy(serverSurface, &SurfaceInterface::bufferSourceBoxChanged); + + // Map the surface. + QImage image(QSize(200, 100), QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::black); + KWayland::Client::Buffer::Ptr buffer = m_shm->createBuffer(image); + clientSurface->attachBuffer(buffer); + clientSurface->setScale(2); + clientSurface->damage(image.rect()); + clientSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(serverSurfaceMappedSpy.wait()); + QCOMPARE(bufferSourceBoxChangedSpy.count(), 1); + QCOMPARE(serverSurface->size(), QSize(100, 50)); + QCOMPARE(serverSurface->bufferSourceBox(), RectF(0, 0, 200, 100)); + + // Create a viewport for the surface. + std::unique_ptr clientViewport(new Viewport); + clientViewport->init(m_viewporter->get_viewport(*clientSurface)); + + // Crop the surface. + clientViewport->set_source(wl_fixed_from_double(10), wl_fixed_from_double(10), wl_fixed_from_double(30), wl_fixed_from_double(20)); + clientSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(serverSurfaceSizeChangedSpy.wait()); + QCOMPARE(bufferSourceBoxChangedSpy.count(), 2); + QCOMPARE(serverSurface->size(), QSize(30, 20)); + QCOMPARE(serverSurface->bufferSourceBox(), RectF(20, 20, 60, 40)); + + // Scale the surface. + clientViewport->set_destination(500, 250); + clientSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(serverSurfaceSizeChangedSpy.wait()); + QCOMPARE(bufferSourceBoxChangedSpy.count(), 2); + QCOMPARE(serverSurface->size(), QSize(500, 250)); + QCOMPARE(serverSurface->bufferSourceBox(), RectF(20, 20, 60, 40)); + + // If the viewport is destroyed, the crop and scale state will be unset on a next commit. + clientViewport->destroy(); + clientSurface->commit(KWayland::Client::Surface::CommitFlag::None); + QVERIFY(serverSurfaceSizeChangedSpy.wait()); + QCOMPARE(bufferSourceBoxChangedSpy.count(), 3); + QCOMPARE(serverSurface->size(), QSize(100, 50)); + QCOMPARE(serverSurface->bufferSourceBox(), RectF(0, 0, 200, 100)); +} + +QTEST_GUILESS_MAIN(TestViewporterInterface) + +#include "test_viewporter_interface.moc" diff --git a/local/recipes/kde/kwin/source/autotests/xcb_scaling_mock.cpp b/local/recipes/kde/kwin/source/autotests/xcb_scaling_mock.cpp new file mode 100644 index 0000000000..eac72b0eed --- /dev/null +++ b/local/recipes/kde/kwin/source/autotests/xcb_scaling_mock.cpp @@ -0,0 +1,55 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +namespace KWin +{ + +uint32_t Xcb::toXNative(qreal value) +{ + return value; +} + +Rect Xcb::toXNative(const RectF &rect) +{ + return rect.toRect(); +} + +qreal Xcb::fromXNative(int value) +{ + return value; +} + +QSizeF Xcb::fromXNative(const QSize &value) +{ + return value; +} + +RectF Xcb::nativeFloor(const RectF &value) +{ + return value; +} +} + +QDebug operator<<(QDebug dbg, const KWin::Rect &rect) +{ + QDebugStateSaver saver(dbg); + dbg.nospace(); + dbg << "KWin::Rect(" << rect.x() << "," << rect.y() << " " << rect.width() << "x" << rect.height() << ")"; + return dbg; +} + +QDebug operator<<(QDebug dbg, const KWin::RectF &rect) +{ + QDebugStateSaver saver(dbg); + dbg.nospace(); + dbg << "KWin::RectF(" << rect.x() << "," << rect.y() << " " << rect.width() << "x" << rect.height() << ")"; + return dbg; +} diff --git a/local/recipes/kde/kwin/source/cmake/modules/FindLibdrm.cmake b/local/recipes/kde/kwin/source/cmake/modules/FindLibdrm.cmake new file mode 100644 index 0000000000..cf6e061349 --- /dev/null +++ b/local/recipes/kde/kwin/source/cmake/modules/FindLibdrm.cmake @@ -0,0 +1,105 @@ +#.rst: +# FindLibdrm +# ------- +# +# Try to find libdrm on a Unix system. +# +# This will define the following variables: +# +# ``Libdrm_FOUND`` +# True if (the requested version of) libdrm is available +# ``Libdrm_VERSION`` +# The version of libdrm +# ``Libdrm_LIBRARIES`` +# This can be passed to target_link_libraries() instead of the ``Libdrm::Libdrm`` +# target +# ``Libdrm_INCLUDE_DIRS`` +# This should be passed to target_include_directories() if the target is not +# used for linking +# ``Libdrm_DEFINITIONS`` +# This should be passed to target_compile_options() if the target is not +# used for linking +# +# If ``Libdrm_FOUND`` is TRUE, it will also define the following imported target: +# +# ``Libdrm::Libdrm`` +# The libdrm library +# +# In general we recommend using the imported target, as it is easier to use. +# Bear in mind, however, that if the target is in the link interface of an +# exported library, it must be made available by the package config file. + +#============================================================================= +# SPDX-FileCopyrightText: 2014 Alex Merry +# SPDX-FileCopyrightText: 2014 Martin Gräßlin +# +# SPDX-License-Identifier: BSD-3-Clause +#============================================================================= + +if(CMAKE_VERSION VERSION_LESS 2.8.12) + message(FATAL_ERROR "CMake 2.8.12 is required by FindLibdrm.cmake") +endif() +if(CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 2.8.12) + message(AUTHOR_WARNING "Your project should require at least CMake 2.8.12 to use FindLibdrm.cmake") +endif() + +if(NOT WIN32) + # Use pkg-config to get the directories and then use these values + # in the FIND_PATH() and FIND_LIBRARY() calls + find_package(PkgConfig) + pkg_check_modules(PKG_Libdrm QUIET libdrm) + + set(Libdrm_DEFINITIONS ${PKG_Libdrm_CFLAGS_OTHER}) + set(Libdrm_VERSION ${PKG_Libdrm_VERSION}) + + find_path(Libdrm_INCLUDE_DIR + NAMES + xf86drm.h + HINTS + ${PKG_Libdrm_INCLUDE_DIRS} + ) + find_library(Libdrm_LIBRARY + NAMES + drm + HINTS + ${PKG_Libdrm_LIBRARY_DIRS} + ) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(Libdrm + FOUND_VAR + Libdrm_FOUND + REQUIRED_VARS + Libdrm_LIBRARY + Libdrm_INCLUDE_DIR + VERSION_VAR + Libdrm_VERSION + ) + + if(Libdrm_FOUND AND NOT TARGET Libdrm::Libdrm) + add_library(Libdrm::Libdrm UNKNOWN IMPORTED) + set_target_properties(Libdrm::Libdrm PROPERTIES + IMPORTED_LOCATION "${Libdrm_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${Libdrm_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${Libdrm_INCLUDE_DIR}" + INTERFACE_INCLUDE_DIRECTORIES "${Libdrm_INCLUDE_DIR}/libdrm" + ) + endif() + + mark_as_advanced(Libdrm_LIBRARY Libdrm_INCLUDE_DIR) + + # compatibility variables + set(Libdrm_LIBRARIES ${Libdrm_LIBRARY}) + set(Libdrm_INCLUDE_DIRS ${Libdrm_INCLUDE_DIR} "${Libdrm_INCLUDE_DIR}/libdrm") + set(Libdrm_VERSION_STRING ${Libdrm_VERSION}) + +else() + message(STATUS "FindLibdrm.cmake cannot find libdrm on Windows systems.") + set(Libdrm_FOUND FALSE) +endif() + +include(FeatureSummary) +set_package_properties(Libdrm PROPERTIES + URL "https://wiki.freedesktop.org/dri/" + DESCRIPTION "Userspace interface to kernel DRM services" +) diff --git a/local/recipes/kde/kwin/source/cmake/modules/FindLibeis-1.0.cmake b/local/recipes/kde/kwin/source/cmake/modules/FindLibeis-1.0.cmake new file mode 100644 index 0000000000..e1a841d5fa --- /dev/null +++ b/local/recipes/kde/kwin/source/cmake/modules/FindLibeis-1.0.cmake @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2024 David Redondo +# SPDX-License-Identifier: BSD-3-Clause + +find_package(PkgConfig) +pkg_check_modules(PKG_Libeis QUIET libeis-1.0) + +find_path(Libeis_INCLUDE_DIR + NAMES libeis.h + HINTS ${PKG_Libeis_INCLUDE_DIRS} +) +find_library(Libeis_LIBRARY + NAMES eis + PATHS ${PKG_Libeis_LIBRARY_DIRS} +) + +set(Libeis_VERSION ${PKG_Libeis_VERSION}) + + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Libeis-1.0 + FOUND_VAR Libeis-1.0_FOUND + REQUIRED_VARS + Libeis_LIBRARY + Libeis_INCLUDE_DIR + VERSION_VAR Libeis_VERSION +) + +if(Libeis-1.0_FOUND AND NOT TARGET Libeis::Libeis) + add_library(Libeis::Libeis UNKNOWN IMPORTED) + set_target_properties(Libeis::Libeis PROPERTIES + IMPORTED_LOCATION "${Libeis_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${PKG_Libeis_CFLAGS_OTHER}" + INTERFACE_INCLUDE_DIRECTORIES "${Libeis_INCLUDE_DIR}" + ) +endif() + +mark_as_advanced(Libeis_INCLUDE_DIR Libeis_LIBRARY) diff --git a/local/recipes/kde/kwin/source/cmake/modules/FindXKB.cmake b/local/recipes/kde/kwin/source/cmake/modules/FindXKB.cmake new file mode 100644 index 0000000000..2a2e45110b --- /dev/null +++ b/local/recipes/kde/kwin/source/cmake/modules/FindXKB.cmake @@ -0,0 +1,89 @@ +#.rst: +# FindXKB +# ------- +# +# Try to find xkbcommon on a Unix system +# If found, this will define the following variables: +# +# ``XKB_FOUND`` +# True if XKB is available +# ``XKB_LIBRARIES`` +# Link these to use XKB +# ``XKB_INCLUDE_DIRS`` +# Include directory for XKB +# ``XKB_DEFINITIONS`` +# Compiler flags for using XKB +# +# Additionally, the following imported targets will be defined: +# +# ``XKB::XKB`` +# The XKB library + +#============================================================================= +# SPDX-FileCopyrightText: 2014 Martin Gräßlin +# +# SPDX-License-Identifier: BSD-3-Clause +#============================================================================= + +if(CMAKE_VERSION VERSION_LESS 2.8.12) + message(FATAL_ERROR "CMake 2.8.12 is required by FindXKB.cmake") +endif() +if(CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 2.8.12) + message(AUTHOR_WARNING "Your project should require at least CMake 2.8.12 to use FindXKB.cmake") +endif() + +if(NOT WIN32) + # Use pkg-config to get the directories and then use these values + # in the FIND_PATH() and FIND_LIBRARY() calls + find_package(PkgConfig) + pkg_check_modules(PKG_XKB QUIET xkbcommon) + + set(XKB_DEFINITIONS ${PKG_XKB_CFLAGS_OTHER}) + + find_path(XKB_INCLUDE_DIR + NAMES + xkbcommon/xkbcommon.h + HINTS + ${PKG_XKB_INCLUDE_DIRS} + ) + find_library(XKB_LIBRARY + NAMES + xkbcommon + HINTS + ${PKG_XKB_LIBRARY_DIRS} + ) + + set(XKB_LIBRARIES ${XKB_LIBRARY}) + set(XKB_INCLUDE_DIRS ${XKB_INCLUDE_DIR}) + set(XKB_VERSION ${PKG_XKB_VERSION}) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(XKB + FOUND_VAR + XKB_FOUND + REQUIRED_VARS + XKB_LIBRARY + XKB_INCLUDE_DIR + VERSION_VAR + XKB_VERSION + ) + + if(XKB_FOUND AND NOT TARGET XKB::XKB) + add_library(XKB::XKB UNKNOWN IMPORTED) + set_target_properties(XKB::XKB PROPERTIES + IMPORTED_LOCATION "${XKB_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${XKB_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${XKB_INCLUDE_DIR}" + ) + endif() + +else() + message(STATUS "FindXKB.cmake cannot find XKB on Windows systems.") + set(XKB_FOUND FALSE) +endif() + +include(FeatureSummary) +set_package_properties(XKB PROPERTIES + URL "https://xkbcommon.org" + DESCRIPTION "XKB API common to servers and clients" +) diff --git a/local/recipes/kde/kwin/source/cmake/modules/FindXwayland.cmake b/local/recipes/kde/kwin/source/cmake/modules/FindXwayland.cmake new file mode 100644 index 0000000000..1e28dc64e7 --- /dev/null +++ b/local/recipes/kde/kwin/source/cmake/modules/FindXwayland.cmake @@ -0,0 +1,42 @@ +#.rst: +# FindXwayland +# ------- +# +# Try to find Xwayland on a Unix system. +# +# This will define the following variables: +# +# ``Xwayland_FOUND`` +# True if (the requested version of) Xwayland is available +# ``Xwayland_VERSION`` +# The version of Xwayland +# ``Xwayland_HAVE_LISTENFD`` +# True if (the requested version of) Xwayland has -listenfd option +# ``Xwayland_HAVE_ENABLE_EI_PORTAL`` +# True if (the requested version of) Xwayland has -enable-ei-portal option + +#============================================================================= +# SPDX-FileCopyrightText: 2016 Martin Gräßlin +# SPDX-FileCopyrightText: 2021 Vlad Zahorodnii +# +# SPDX-License-Identifier: BSD-3-Clause +#============================================================================= + +find_package(PkgConfig) +pkg_check_modules(PKG_xwayland QUIET xwayland) + +set(Xwayland_VERSION ${PKG_xwayland_VERSION}) +pkg_get_variable(Xwayland_HAVE_LISTENFD xwayland have_listenfd) +pkg_get_variable(Xwayland_HAVE_ENABLE_EI_PORTAL xwayland have_enable_ei_portal) + +find_program(Xwayland_EXECUTABLE NAMES Xwayland) +find_package_handle_standard_args(Xwayland + FOUND_VAR Xwayland_FOUND + REQUIRED_VARS Xwayland_EXECUTABLE + VERSION_VAR Xwayland_VERSION +) +mark_as_advanced( + Xwayland_EXECUTABLE + Xwayland_HAVE_LISTENFD + Xwayland_VERSION +) diff --git a/local/recipes/kde/kwin/source/cmake/modules/Findgbm.cmake b/local/recipes/kde/kwin/source/cmake/modules/Findgbm.cmake new file mode 100644 index 0000000000..0f4ef3771b --- /dev/null +++ b/local/recipes/kde/kwin/source/cmake/modules/Findgbm.cmake @@ -0,0 +1,104 @@ +#.rst: +# Findgbm +# ------- +# +# Try to find gbm on a Unix system. +# +# This will define the following variables: +# +# ``gbm_FOUND`` +# True if (the requested version of) gbm is available +# ``gbm_VERSION`` +# The version of gbm +# ``gbm_LIBRARIES`` +# This can be passed to target_link_libraries() instead of the ``gbm::gbm`` +# target +# ``gbm_INCLUDE_DIRS`` +# This should be passed to target_include_directories() if the target is not +# used for linking +# ``gbm_DEFINITIONS`` +# This should be passed to target_compile_options() if the target is not +# used for linking +# +# If ``gbm_FOUND`` is TRUE, it will also define the following imported target: +# +# ``gbm::gbm`` +# The gbm library +# +# In general we recommend using the imported target, as it is easier to use. +# Bear in mind, however, that if the target is in the link interface of an +# exported library, it must be made available by the package config file. + +#============================================================================= +# SPDX-FileCopyrightText: 2014 Alex Merry +# SPDX-FileCopyrightText: 2014 Martin Gräßlin +# +# SPDX-License-Identifier: BSD-3-Clause +#============================================================================= + +if(CMAKE_VERSION VERSION_LESS 2.8.12) + message(FATAL_ERROR "CMake 2.8.12 is required by Findgbm.cmake") +endif() +if(CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 2.8.12) + message(AUTHOR_WARNING "Your project should require at least CMake 2.8.12 to use Findgbm.cmake") +endif() + +if(NOT WIN32) + # Use pkg-config to get the directories and then use these values + # in the FIND_PATH() and FIND_LIBRARY() calls + find_package(PkgConfig) + pkg_check_modules(PKG_gbm QUIET gbm) + + set(gbm_DEFINITIONS ${PKG_gbm_CFLAGS_OTHER}) + set(gbm_VERSION ${PKG_gbm_VERSION}) + + find_path(gbm_INCLUDE_DIR + NAMES + gbm.h + HINTS + ${PKG_gbm_INCLUDE_DIRS} + ) + find_library(gbm_LIBRARY + NAMES + gbm + HINTS + ${PKG_gbm_LIBRARY_DIRS} + ) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(gbm + FOUND_VAR + gbm_FOUND + REQUIRED_VARS + gbm_LIBRARY + gbm_INCLUDE_DIR + VERSION_VAR + gbm_VERSION + ) + + if(gbm_FOUND AND NOT TARGET gbm::gbm) + add_library(gbm::gbm UNKNOWN IMPORTED) + set_target_properties(gbm::gbm PROPERTIES + IMPORTED_LOCATION "${gbm_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${gbm_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${gbm_INCLUDE_DIR}" + ) + endif() + + mark_as_advanced(gbm_LIBRARY gbm_INCLUDE_DIR) + + # compatibility variables + set(gbm_LIBRARIES ${gbm_LIBRARY}) + set(gbm_INCLUDE_DIRS ${gbm_INCLUDE_DIR}) + set(gbm_VERSION_STRING ${gbm_VERSION}) + +else() + message(STATUS "Findgbm.cmake cannot find gbm on Windows systems.") + set(gbm_FOUND FALSE) +endif() + +include(FeatureSummary) +set_package_properties(gbm PROPERTIES + URL "https://www.mesa3d.org" + DESCRIPTION "Mesa gbm library" +) diff --git a/local/recipes/kde/kwin/source/cmake/modules/Findhwdata.cmake b/local/recipes/kde/kwin/source/cmake/modules/Findhwdata.cmake new file mode 100644 index 0000000000..3525173be9 --- /dev/null +++ b/local/recipes/kde/kwin/source/cmake/modules/Findhwdata.cmake @@ -0,0 +1,25 @@ +# - Try to find hwdata +# Once done this will define +# +# hwdata_DIR - The hwdata directory +# hwdata_PNPIDS_FILE - File with mapping of hw vendor IDs to names +# hwdata_FOUND - The hwdata directory exists and contains pnp.ids file + +# SPDX-FileCopyrightText: 2020 Daniel Vrátil +# +# SPDX-License-Identifier: BSD-3-Clause + +if (UNIX AND NOT APPLE) + find_path(hwdata_DIR NAMES hwdata/pnp.ids HINTS /usr/share ENV XDG_DATA_DIRS) + find_file(hwdata_PNPIDS_FILE NAMES hwdata/pnp.ids HINTS /usr/share) + if (NOT hwdata_DIR OR NOT hwdata_PNPIDS_FILE) + set(hwdata_FOUND FALSE) + else() + set(hwdata_FOUND TRUE) + endif() + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(hwdata DEFAULT_MSG hwdata_FOUND hwdata_DIR hwdata_PNPIDS_FILE) + + mark_as_advanced(hwdata_FOUND hwdata_DIR hwdata_PNPIDS_FILE) +endif() diff --git a/local/recipes/kde/kwin/source/cmake/modules/Findlcms2.cmake b/local/recipes/kde/kwin/source/cmake/modules/Findlcms2.cmake new file mode 100644 index 0000000000..c488a0d9a2 --- /dev/null +++ b/local/recipes/kde/kwin/source/cmake/modules/Findlcms2.cmake @@ -0,0 +1,75 @@ +#.rst: +# Findlcms2 +# ------- +# +# Try to find lcms2 on a Unix system. +# +# This will define the following variables: +# +# ``lcms2_FOUND`` +# True if (the requested version of) lcms2 is available +# ``lcms2_VERSION`` +# The version of lcms2 +# ``lcms2_LIBRARIES`` +# This should be passed to target_link_libraries() if the target is not +# used for linking +# ``lcms2_INCLUDE_DIRS`` +# This should be passed to target_include_directories() if the target is not +# used for linking +# ``lcms2_DEFINITIONS`` +# This should be passed to target_compile_options() if the target is not +# used for linking +# +# If ``lcms2_FOUND`` is TRUE, it will also define the following imported target: +# +# ``lcms2::lcms2`` +# The lcms2 library +# +# In general we recommend using the imported target, as it is easier to use. +# Bear in mind, however, that if the target is in the link interface of an +# exported library, it must be made available by the package config file. + +# SPDX-FileCopyrightText: 2020 Vlad Zahorodnii +# SPDX-License-Identifier: BSD-3-Clause + +find_package(PkgConfig) +pkg_check_modules(PKG_lcms2 QUIET lcms2) + +set(lcms2_VERSION ${PKG_lcms2_VERSION}) +set(lcms2_DEFINITIONS ${PKG_lcms2_CFLAGS_OTHER}) + +find_path(lcms2_INCLUDE_DIR + NAMES lcms2.h + HINTS ${PKG_lcms2_INCLUDE_DIRS} +) + +find_library(lcms2_LIBRARY + NAMES lcms2 + HINTS ${PKG_lcms2_LIBRARY_DIRS} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(lcms2 + FOUND_VAR lcms2_FOUND + REQUIRED_VARS lcms2_LIBRARY + lcms2_INCLUDE_DIR + VERSION_VAR lcms2_VERSION +) + +if (lcms2_FOUND AND NOT TARGET lcms2::lcms2) + add_library(lcms2::lcms2 UNKNOWN IMPORTED) + set_target_properties(lcms2::lcms2 PROPERTIES + IMPORTED_LOCATION "${lcms2_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${lcms2_DEFINITIONS}" + # Don't use the register keyword to allow compiling in C++17 mode. + # See https://github.com/mm2/Little-CMS/issues/243 + INTERFACE_COMPILE_DEFINITIONS "CMS_NO_REGISTER_KEYWORD=1" + INTERFACE_INCLUDE_DIRECTORIES "${lcms2_INCLUDE_DIR}" + ) +endif() + +set(lcms2_INCLUDE_DIRS ${lcms2_INCLUDE_DIR}) +set(lcms2_LIBRARIES ${lcms2_LIBRARY}) + +mark_as_advanced(lcms2_INCLUDE_DIR) +mark_as_advanced(lcms2_LIBRARY) diff --git a/local/recipes/kde/kwin/source/data/CMakeLists.txt b/local/recipes/kde/kwin/source/data/CMakeLists.txt new file mode 100644 index 0000000000..322459beff --- /dev/null +++ b/local/recipes/kde/kwin/source/data/CMakeLists.txt @@ -0,0 +1,14 @@ +add_subdirectory(icons) + +########### next target ############### +add_executable(kwin5_update_default_rules update_default_rules.cpp) +target_link_libraries(kwin5_update_default_rules + KF6::ConfigCore + Qt::Core + Qt::DBus +) +install(TARGETS kwin5_update_default_rules DESTINATION ${KDE_INSTALL_LIBDIR}/kconf_update_bin/) + +########### install files ############### + +install(FILES org_kde_kwin.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}) diff --git a/local/recipes/kde/kwin/source/data/icons/16-apps-kwin.png b/local/recipes/kde/kwin/source/data/icons/16-apps-kwin.png new file mode 100644 index 0000000000..101cda87b9 Binary files /dev/null and b/local/recipes/kde/kwin/source/data/icons/16-apps-kwin.png differ diff --git a/local/recipes/kde/kwin/source/data/icons/32-apps-kwin.png b/local/recipes/kde/kwin/source/data/icons/32-apps-kwin.png new file mode 100644 index 0000000000..7bdd7fe20b Binary files /dev/null and b/local/recipes/kde/kwin/source/data/icons/32-apps-kwin.png differ diff --git a/local/recipes/kde/kwin/source/data/icons/48-apps-kwin.png b/local/recipes/kde/kwin/source/data/icons/48-apps-kwin.png new file mode 100644 index 0000000000..0d5838a9cf Binary files /dev/null and b/local/recipes/kde/kwin/source/data/icons/48-apps-kwin.png differ diff --git a/local/recipes/kde/kwin/source/data/icons/CMakeLists.txt b/local/recipes/kde/kwin/source/data/icons/CMakeLists.txt new file mode 100644 index 0000000000..6690f28131 --- /dev/null +++ b/local/recipes/kde/kwin/source/data/icons/CMakeLists.txt @@ -0,0 +1,11 @@ +ecm_install_icons( + ICONS + 16-apps-kwin.png + 32-apps-kwin.png + 48-apps-kwin.png + sc-apps-kwin.svgz + DESTINATION + ${KDE_INSTALL_ICONDIR} + THEME + hicolor +) diff --git a/local/recipes/kde/kwin/source/data/icons/sc-apps-kwin.svgz b/local/recipes/kde/kwin/source/data/icons/sc-apps-kwin.svgz new file mode 100644 index 0000000000..5248264d2a Binary files /dev/null and b/local/recipes/kde/kwin/source/data/icons/sc-apps-kwin.svgz differ diff --git a/local/recipes/kde/kwin/source/data/org_kde_kwin.categories b/local/recipes/kde/kwin/source/data/org_kde_kwin.categories new file mode 100644 index 0000000000..b2d7be49e0 --- /dev/null +++ b/local/recipes/kde/kwin/source/data/org_kde_kwin.categories @@ -0,0 +1,22 @@ +kwin_core KWin Core DEFAULT_SEVERITY [WARNING] IDENTIFIER [KWIN_CORE] +kwin_utils KWin utils DEFAULT_SEVERITY [WARNING] IDENTIFIER [KWIN_UTILS] +kwin_virtualkeyboard KWin Virtual Keyboard Integration DEFAULT_SEVERITY [WARNING] IDENTIFIER [KWIN_VIRTUALKEYBOARD] +kwineffects KWin Effects DEFAULT_SEVERITY [WARNING] IDENTIFIER [KWINEFFECTS] +libkwineffects KWin Effects Library DEFAULT_SEVERITY [WARNING] IDENTIFIER [LIBKWINEFFECTS] +libkwinglutils KWin OpenGL utility Library DEFAULT_SEVERITY [WARNING] IDENTIFIER [LIBKWINGLUTILS] +libkwinxrenderutils KWin XRender utility Library DEFAULT_SEVERITY [WARNING] IDENTIFIER [LIBKWINXRENDERUTILS] +kwin_wayland_drm KWin Wayland (DRM backend) DEFAULT_SEVERITY [WARNING] IDENTIFIER [KWIN_DRM] +kwin_wayland_framebuffer KWin Wayland (Framebuffer backend) DEFAULT_SEVERITY [WARNING] IDENTIFIER [KWIN_FB] +kwin_wayland_backend KWin Wayland (Wayland backend) DEFAULT_SEVERITY [WARNING] IDENTIFIER [KWIN_WAYLAND_BACKEND] +kwin_wayland_x11windowed KWin Wayland (X11 backend) DEFAULT_SEVERITY [WARNING] IDENTIFIER [KWIN_X11WINDOWED] +kwin_libinput KWin Libinput Integration DEFAULT_SEVERITY [WARNING] IDENTIFIER [KWIN_LIBINPUT] +kwin_tabbox KWin Window Switcher DEFAULT_SEVERITY [WARNING] IDENTIFIER [KWIN_TABBOX] +kwin_decorations KWin Decorations DEFAULT_SEVERITY [WARNING] IDENTIFIER [KWIN_DECORATIONS] +kwin_scripting KWin Scripting DEFAULT_SEVERITY [WARNING] IDENTIFIER [KWIN_SCRIPTING] +aurorae KWin Aurorae Window Decoration Engine DEFAULT_SEVERITY [WARNING] IDENTIFIER [AURORAE] +kwin_xkbcommon KWin xkbcommon integration DEFAULT_SEVERITY [WARNING] IDENTIFIER [KWIN_XKB] +kwin_qpa_plugin KWin QtPlatformAbstraction plugin DEFAULT_SEVERITY [WARNING] IDENTIFIER [KWIN_QPA] +kwin_scene_qpainter KWin QPainter based compositor scene plugin DEFAULT_SEVERITY [WARNING] IDENTIFIER [KWIN_QPAINTER] +kwin_scene_opengl KWin OpenGL based compositor scene plugins DEFAULT_SEVERITY [WARNING] IDENTIFIER [KWIN_OPENGL] +kwin_screencast KWin Screen Cast Service DEFAULT_SEVERITY [WARNING] IDENTIFIER [KWIN_SCREENCAST] +kwin_xwl KWin Xwayland Server DEFAULT_SEVERITY [WARNING] IDENTIFIER [KWIN_XWL] diff --git a/local/recipes/kde/kwin/source/data/update_default_rules.cpp b/local/recipes/kde/kwin/source/data/update_default_rules.cpp new file mode 100644 index 0000000000..c6d8e7754a --- /dev/null +++ b/local/recipes/kde/kwin/source/data/update_default_rules.cpp @@ -0,0 +1,61 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2005 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +// read additional window rules and add them to kwinrulesrc + +#include "config-kwin.h" + +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char *argv[]) +{ + if (argc != 2) { + return 1; + } + + QCoreApplication::setApplicationName("kwin_update_default_rules"); + + QString file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QString("kwin-wayland/default_rules/%1").arg(argv[1])); + if (file.isEmpty()) { + qWarning() << "File " << argv[1] << " not found!"; + return 1; + } + KConfig src_cfg(file); + KConfig dest_cfg(QStringLiteral("kwinrulesrc"), KConfig::NoGlobals); + KConfigGroup scg(&src_cfg, QStringLiteral("General")); + KConfigGroup dcg(&dest_cfg, QStringLiteral("General")); + int count = scg.readEntry("count", 0); + int pos = dcg.readEntry("count", 0); + for (int group = 1; + group <= count; + ++group) { + QMap entries = src_cfg.entryMap(QString::number(group)); + ++pos; + dest_cfg.deleteGroup(QString::number(pos)); + KConfigGroup dcg2(&dest_cfg, QString::number(pos)); + for (QMap::ConstIterator it = entries.constBegin(); + it != entries.constEnd(); + ++it) { + dcg2.writeEntry(it.key(), *it); + } + } + dcg.writeEntry("count", pos); + scg.sync(); + dcg.sync(); + // Send signal to all kwin instances + QDBusMessage message = + QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); +} diff --git a/local/recipes/kde/kwin/source/doc/CMakeLists.txt b/local/recipes/kde/kwin/source/doc/CMakeLists.txt new file mode 100644 index 0000000000..c305cdd48c --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/CMakeLists.txt @@ -0,0 +1,9 @@ +ecm_optional_add_subdirectory(desktop) +ecm_optional_add_subdirectory(kwindecoration) +ecm_optional_add_subdirectory(kwinscreenedges) +ecm_optional_add_subdirectory(kwintabbox) +ecm_optional_add_subdirectory(kwintouchscreen) +ecm_optional_add_subdirectory(kwinvirtualkeyboard) +ecm_optional_add_subdirectory(windowbehaviour) +ecm_optional_add_subdirectory(windowspecific) +ecm_optional_add_subdirectory(kwineffects) diff --git a/local/recipes/kde/kwin/source/doc/TESTING.md b/local/recipes/kde/kwin/source/doc/TESTING.md new file mode 100644 index 0000000000..ff5ff54573 --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/TESTING.md @@ -0,0 +1,48 @@ +# Testing in KWin +KWin provides a unit and integration test suite for X11 and Wayland. The source code for the tests can be found in the subdirectory autotests. The test suite should be run prior to any merge to KWin. + +# Dependencies +The following additional software needs to be installed for running the test suite: + +* Xvfb +* Xephyr +* glxgears +* DMZ-white cursor theme +* breeze window decoration + +# Preparing OpenGL +Some of the tests require OpenGL. The test suite is implemented against Mesa and uses the Mesa specific EGL extension +EGL_MESA_platform_surfaceless. This extension supports rendering without any real GPU using llvmpipe as software +emulation. This gives the tests a stable base removing variance introduced by different hardware and drivers. + +Users of non-Mesa drivers (e.g. proprietary NVIDIA driver) need to ensure that Mesa is also installed. If your system +uses libglvnd this should work out of the box, if not you might need to tune LD_LIBRARY_PATH. + +# Preventing side effects +To prevent side effects with the running session it is recommended to run tests +in a dedicated dbus session. This can be achieved by prefixing the test command +with `dbus-run-session`, as shown in the examples below. + +# Running tests +Tests are more likely to succeed when run from ssh, as the environment is +further isolated from the user's session. For example: + +```bash +ssh localhost +``` + +Then, run the tests as described below. + +## Running the test suite +The test suite can be run from the build directory. Best is to do: + + cd path/to/build/directory + dbus-run-session xvfb-run ctest + +## Running individual tests +All tests executables are created in the directory "bin" in the build directory. Each test can be executed by just starting it from within the test directory: + + cd path/to/build/directory/bin + dbus-run-session ./testFoo + +For tests relying on X11 one should also either start a dedicated Xvfb and export DISPLAY or use xvfb-run as described above. diff --git a/local/recipes/kde/kwin/source/doc/coding-conventions.md b/local/recipes/kde/kwin/source/doc/coding-conventions.md new file mode 100644 index 0000000000..42bb4bfbd4 --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/coding-conventions.md @@ -0,0 +1,86 @@ +# Coding Conventions + +This document describes some of the recommended coding conventions that should be followed in KWin. + +For KWin, it is recommended to follow the KDE Frameworks Coding Style. + + +## `auto` Keyword + +Optionally, you can use the `auto` keyword in the following cases. If in doubt, for example if using +`auto` could make the code less readable, do not use `auto`. Keep in mind that code is read much more +often than written. + +* When it avoids repetition of a type in the same statement. + + ``` + auto something = new MyCustomType; + auto keyEvent = static_cast(event); + auto myList = QStringList({ "FooThing", "BarThing" }); + ``` + +* When assigning iterator types. + + ``` + auto it = myList.const_iterator(); + ``` + + +## `QRect::right()` and `QRect::bottom()` + +For historical reasons, the `QRect::right()` and `QRect::bottom()` functions deviate from the true +bottom-right corner of the rectangle. Note that this is not the case for the `QRectF` class. + +As a general rule, avoid using `QRect::right()` and `QRect::bottom()` as well methods that operate +on them. There are exceptions, though. + +Exception 1: you can use `QRect::moveRight()` and `QRect::moveBottom()` to snap a `QRect` to +another `QRect` as long as the corresponding borders match, for example + +``` +// Ok +rect.moveRight(anotherRect.right()); +rect.moveBottom(anotherRect.bottom()); +rect.moveBottomRight(anotherRect.bottomRight()); + +// Bad +rect.moveRight(anotherRect.left() - 1); // must be rect.moveLeft(anotherRect.left() - rect.width()); +rect.moveBottom(anotherRect.top() - 1); // must be rect.moveTop(anotherRect.top() - rect.height()); +rect.moveBottomRight(anotherRect.topLeft() - QPoint(1, 1)); +``` + +Exception 2: you can use `QRect::setRight()` and `QRect::setBottom()` to clip a `QRect` by another +`QRect` as long as the corresponding borders match, for example + +``` +// Ok +rect.setRight(anotherRect.right()); +rect.setBottom(anotherRect.bottom()); +rect.setBottomRight(anotherRect.bottomRight()); + +// Bad +rect.setRight(anotherRect.left()); +rect.setBottom(anotherRect.top()); +rect.setBottomRight(anotherRect.topLeft()); +``` + +Exception 3: you can use `QRect::right()` and `QRect::bottom()` in conditional statements as long +as the compared borders are the same, for example + +``` +// Ok +if (rect.right() > anotherRect.right()) { + return; +} +if (rect.bottom() > anotherRect.bottom()) { + return; +} + +// Bad +if (rect.right() > anotherRect.left()) { + return; +} +if (rect.bottom() > anotherRect.top()) { + return; +} +``` diff --git a/local/recipes/kde/kwin/source/doc/desktop/CMakeLists.txt b/local/recipes/kde/kwin/source/doc/desktop/CMakeLists.txt new file mode 100644 index 0000000000..3a9098121b --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/desktop/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR kcontrol/desktop) diff --git a/local/recipes/kde/kwin/source/doc/desktop/index.docbook b/local/recipes/kde/kwin/source/doc/desktop/index.docbook new file mode 100644 index 0000000000..72bbe359f4 --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/desktop/index.docbook @@ -0,0 +1,84 @@ + + + +]> + +
+Virtual Desktops + + + +&Mike.McBride; &Mike.McBride.mail; +&Jost.Schenck; &Jost.Schenck.mail; + + + +2021-04-09 +Plasma 5.20 + + +KDE +Systemsettings +virtual +desktop + + + + + &kde; offers you the possibility to have several virtual desktops. + + + You can change the names to the desktops by clicking on the Rename button (appears when you hover the desktop item in the list) and entering text into the text field. + + + If you want to remove a desktop, use the trash overlay icon at the right of the desktop item. + + + Press the Add button to add a virtual desktop (default name is New Desktop) to the list. + + + You can configure the number of rows in the pager item on the panel. Just use the rows input box below the list to adjust the number of desktops. The desktops will be rearranged by the rows automatically. + + + + +Navigation wraps around +Enable this option if you want a keyboard or active desktop border navigation +beyond the edge of a desktop to take you to the opposite edge of the new desktop. + + + + +Show animation when switching +Select Slide, +Desktop Cube Animation, or Fade Desktop +from the drop-down box or switch animations off by unchecking the item. If the selected animation has settings options, click on the +tools icon on the right of the drop-down box to launch a configuration dialog. + + + + +Show on-screen display when switching +Enable this option if you want to have an on-screen display for desktop switching. + + + + +Show desktop layout indicators +Enabling this option will show a small preview of the desktop layout +indicating the selected desktop. + + + + + +Scrolling the mouse wheel over an empty space on the +desktop or on the Pager icon in the panel will change to the next +virtual desktop numerically, in the direction you scrolled (either up or down). +You can change this default behavior on the page Mouse Actions in +the Desktop Settings (&Alt;D, +&Alt;S). + +
diff --git a/local/recipes/kde/kwin/source/doc/kwindecoration/CMakeLists.txt b/local/recipes/kde/kwin/source/doc/kwindecoration/CMakeLists.txt new file mode 100644 index 0000000000..932e9b3973 --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/kwindecoration/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR kcontrol/kwindecoration) diff --git a/local/recipes/kde/kwin/source/doc/kwindecoration/button.png b/local/recipes/kde/kwin/source/doc/kwindecoration/button.png new file mode 100644 index 0000000000..8363bb65fd Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/kwindecoration/button.png differ diff --git a/local/recipes/kde/kwin/source/doc/kwindecoration/configure.png b/local/recipes/kde/kwin/source/doc/kwindecoration/configure.png new file mode 100644 index 0000000000..eb153247d8 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/kwindecoration/configure.png differ diff --git a/local/recipes/kde/kwin/source/doc/kwindecoration/decoration.png b/local/recipes/kde/kwin/source/doc/kwindecoration/decoration.png new file mode 100644 index 0000000000..5c1dd152b2 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/kwindecoration/decoration.png differ diff --git a/local/recipes/kde/kwin/source/doc/kwindecoration/index.docbook b/local/recipes/kde/kwin/source/doc/kwindecoration/index.docbook new file mode 100644 index 0000000000..b2fe949f40 --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/kwindecoration/index.docbook @@ -0,0 +1,151 @@ + + + +]> + +
+ +Window Decorations + +&Rik.Hemsley; &Rik.Hemsley.mail; +&Anne-Marie.Mahfouf; &Anne-Marie.Mahfouf.mail; + + + +2021-04-09 +Plasma 5.21 + + +KDE +Systemsettings +kwin +window +border +theme +style + + + +Window Decorations + +This module allows you to select a style for the buttons and borders around +the windows. + + +Window Decorations + + + +Window Decoration Configuration Module + + + + + + Window Decoration Configuration Module + + + + + +Choose a window decoration style from the preview list, or +download a new style using the Get New Window Decorations +button. + +The default window decoration is called Breeze. + +Each style has a different look, but also a different +feel. Some have (sometimes invisible) +resize borders all around the edge, which make resizing +easier but moving more difficult. Some have no borders on certain +edges. + +You are encouraged to experiment with the different styles until +you find one which best suits your pattern of work. + +In the preview of each style you find a + +configure button to open configuration dialogs for the decoration. + +The options in this configuration dialog are applied to all windows. +Some window decorations (⪚ Breeze) +provide a Window-Specific Overrides tab. +On this tab you can change the border size and the visibility +of the window titlebar for particular windows. + +Different options for particular windows you find in the &systemsettings; +module Window Rules. + + + +For accessibility purposes, some window decorations support +extra wide borders. If this is available, you can also choose a +Window border size here. The large borders are easier to see for low +vision users, and easier to grab for people with limited mobility or +difficulty using a mouse. + + + +If you are interested in creating your own window decoration, you can learn more about it in this tutorial. + + + + + +Decorations + +In this dialog you can change the decoration of the window. + +The available options depend on the selected style. + + + +Breeze Decoration Options + + + + + + Breeze Decoration Options + + + + + + + +Titlebar Buttons + +This page allows you to customize the button location on the titlebar. +You can drag buttons ⪚ the Application menu into the +titlebar, remove them or drag around the +buttons until you have the order that makes you comfortable. + + + +Button Options + + + + + + Button Options + + + + +Enable Close windows by double clicking the window menu button +to have an additional option to the Close button or if you have removed the Close button from the titlebar. + + +Check the Show titlebar buttons tooltips item if you +want to see the default tooltips when you hover the titlebar buttons with the mouse pointer. + + + + + + +
diff --git a/local/recipes/kde/kwin/source/doc/kwindecoration/main.png b/local/recipes/kde/kwin/source/doc/kwindecoration/main.png new file mode 100644 index 0000000000..36717327cf Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/kwindecoration/main.png differ diff --git a/local/recipes/kde/kwin/source/doc/kwineffects/CMakeLists.txt b/local/recipes/kde/kwin/source/doc/kwineffects/CMakeLists.txt new file mode 100644 index 0000000000..265b6ce6c6 --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/kwineffects/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR kcontrol/kwineffects) diff --git a/local/recipes/kde/kwin/source/doc/kwineffects/configure-effects.png b/local/recipes/kde/kwin/source/doc/kwineffects/configure-effects.png new file mode 100644 index 0000000000..f6a5f8bc8c Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/kwineffects/configure-effects.png differ diff --git a/local/recipes/kde/kwin/source/doc/kwineffects/dialog-information.png b/local/recipes/kde/kwin/source/doc/kwineffects/dialog-information.png new file mode 100644 index 0000000000..f2d21888cd Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/kwineffects/dialog-information.png differ diff --git a/local/recipes/kde/kwin/source/doc/kwineffects/index.docbook b/local/recipes/kde/kwin/source/doc/kwineffects/index.docbook new file mode 100644 index 0000000000..7fc865de2b --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/kwineffects/index.docbook @@ -0,0 +1,86 @@ + + + +]> + +
+ + +Desktop Effects + +&Mike.McBride; &Mike.McBride.mail; + + + +2021-04-09 +Plasma 5.20 + + +KDE +KControl +desktop +effects + + + + + +This module is used to enable and configure desktop effects +for Plasma. + +The main part of this page is a list of all available effects grouped +by Accessibility, Appearance, +Focus, Peek at Desktop Animation, Tools, +Virtual Desktop Switching Animation, Window Management, +and Window Open/Close Animation. +Use the incremental search bar above the list window to find items in the list. + +Normally there is no reason for users to change that, but +there is a + configuration button to modify the filtering of the list to show +also those effects. + + +The easiest way of installing new effects is by using the built-in +KNewStuff support in &kwin;. Press the Get New Desktop Effects button to open +a dialog with a list of available effects from the Internet and to install and uninstall effects. +Please keep in mind that changing these sensible defaults can break your system. + + +Check an effect in the list to enable it. Display information about Author and License by +clicking the + info button at the right side of the list item. + +Some effects have settings options, in this case there is a + configure button +at the left of the info button. Click it to open a configuration dialog. +To see a video preview of an effect click on the + button. + +Some effects are mutual exclusive to other effects. For example one would only want to activate the +Maximize or the Magic Lamp effect. Both activated at the same +time result in broken animations. + + +For effects in a mutual exclusive group the &GUI; uses radio buttons and manages that only one of these +effects can be activated. + + +All effects which are not supported by the currently used compositing backend +are hidden by default (⪚ OpenGL effects when using software renderer). + + +Also all internal or helper effects are hidden by default. These are effects which replace +functionality from KWin Core or provide interaction with other elements of the desktop shell. + + + + +
diff --git a/local/recipes/kde/kwin/source/doc/kwineffects/video.png b/local/recipes/kde/kwin/source/doc/kwineffects/video.png new file mode 100644 index 0000000000..992d13e35d Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/kwineffects/video.png differ diff --git a/local/recipes/kde/kwin/source/doc/kwinscreenedges/CMakeLists.txt b/local/recipes/kde/kwin/source/doc/kwinscreenedges/CMakeLists.txt new file mode 100644 index 0000000000..b9fb42afef --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/kwinscreenedges/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR kcontrol/kwinscreenedges) diff --git a/local/recipes/kde/kwin/source/doc/kwinscreenedges/index.docbook b/local/recipes/kde/kwin/source/doc/kwinscreenedges/index.docbook new file mode 100644 index 0000000000..1881bf5c48 --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/kwinscreenedges/index.docbook @@ -0,0 +1,71 @@ + + + +]> + +
+ + +Screen Edges + +&Mike.McBride; &Mike.McBride.mail; + + + +2023-01-30 +Plasma 5.27 + + +KDE +Systemsettings +desktop +effects +screen +edge + + + +Active screen edges allow you to activate effects by pushing your mouse +cursor against the edge of the screen. Here you can configure which effect +will get activated on each edge and corner of the screen. + + +Click with any mouse button onto a square and select an effect +in the context menu. Edges with a blue square have already an attached effect, +a grey-colored square indicates that no effect is selected for this edge. + +The number of accessible items in the context menu depends on the settings in the module + +Desktop Effects in the Workspace +category. Select your favorite effects from the Window Management +group. This activates the corresponding items in the context menu. + +If you are looking for the setting to enable switching of desktops by +pushing your mouse cursor against the edge of the screen choose one of the Present +Windows effects from the context menu. + +You can enable Maximize: Windows dragged to top edge, Tile: Windows dragged to left or right edge or Trigger quarter tiling in: +and set a percentage of the screen to trigger the tiling. + + + + +In the Movement tab of the Window Behavior settings module in the Window Management section of the system settings, you can configure snap zones for windows to moved to the screen edges, center or other windows when they get near them. + + + +Disable the Remain active when windows are fullscreen option to suppress triggering screen edge actions when an application is running in fullscreen. + +Using the Switch desktop on edge item, configure if you want to switch +to another desktop when pushing the mouse cursor to an edge of the screen, ⪚ only when +moving windows. + +Activation delay is the amount of time required for the mouse cursor +to be pushed against the edge of the screen before the action is triggered. + +Reactivation delay is the amount of time required after triggering +an action until the next trigger can occur. + +
diff --git a/local/recipes/kde/kwin/source/doc/kwintabbox/CMakeLists.txt b/local/recipes/kde/kwin/source/doc/kwintabbox/CMakeLists.txt new file mode 100644 index 0000000000..46313f8a21 --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/kwintabbox/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR kcontrol/kwintabbox) diff --git a/local/recipes/kde/kwin/source/doc/kwintabbox/index.docbook b/local/recipes/kde/kwin/source/doc/kwintabbox/index.docbook new file mode 100644 index 0000000000..1d343e91e7 --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/kwintabbox/index.docbook @@ -0,0 +1,117 @@ + + + +]> +
+ +Task Switcher + +&Martin.Graesslin;&Martin.Graesslin.mail; + + + +2023-01-30 +&plasma; 5.27 + + +KDE +System Settings +desktop +window +navigation +switch +alt-tab + + + + +Navigating through Windows + +The Task Switcher allows the user to easily switch between currently open windows using the keyboard. It is highly configurable, and allows the user to control its behavior, visual appearance, keyboard shortcuts, and window filtering. + + + + Screenshot of the default Task Switcher + + + + + + + + +The Task Switcher is often invoked using the key combination &Alt; , but this can be changed. When invoked, it shows a list of all the currently open windows, optionally filtered and augmented according to the configuration settings. For example, the list may be filtered to show only windows that meet certain criteria, such as windows that are currently visible. Once the window list is shown, the user can cycle forward and backward through all the listed windows by repeatedly hitting the Task Switcher key combination. Releasing the Task Switcher key combination will activate the window that was selected in the list. + +Because the Task Switcher offers so many configuration options, two distinct collections of configuration settings can be defined. These collections are called Main and Alternative, and each can have a unique set of key combinations assigned to them. + +The configuration options for each of the Main and Alternative collections are presented in four groupings, as follows: + + +Visualization +This group of configuration options controls how the list of windows is displayed on the screen. The default visualization is called Breeze. It lists all open windows along the left-hand side of the screen. Other visualizations include Cover Switch (a 3D carousel), Flip Switch (a 3D stack of cards), and Medium Rounded (a Microsoft &Windows;-style list of icons). Many more visualizations can be downloaded and installed by clicking the Get New Task Switchers... button at the bottom right of the dialog box. + +Once a visualization has been selected from the drop-down list, the button to the right of the list can be clicked to see a preview or to configure visualization-specific options. + +The Show selected window checkbox determines how clearly the user will see which window will be activated. If this box is checked, then all windows will be hidden except for the one that is currently highlighted in the Task Switcher. + +There may be cases where the desired Task Switcher visualization cannot be shown. One of these situations can be when a process called 'compositing' is turned off or disabled. If this ever happens, the window list will still be shown, but in a very simple format. + + + +Shortcuts +This section allows you to define up to four Task Switcher keyboard shortcuts for the Main configuration and four more for the Alternative configuration. The Main shortcuts are predefined, while the Alternative shortcuts need to be defined manually. +In the All Windows section, the Forward and Reverse shortcuts will cycle forward and backward through the list of open windows. +In the Current Application section, the Forward and Reverse shortcuts can be set to cycle through the windows of the currently active application. For example, if you have three &dolphin; file browser windows open, then you would be able to use these shortcuts to just cycle among the three &dolphin; windows. +To change a keyboard shortcut, click the Forward or Reverse button and type the desired shortcut combination. Be sure to use a modifier key like &Ctrl; or &Alt; as part of the shortcut, otherwise you might not be able to cycle through the window list properly. + +Any of the defined keyboard shortcuts can be used to invoke the Task Switcher. To invoke the Task Switcher without using the keyboard, you can define screen edge actions in the &systemsettings; module Screen Edges. + + + +Content +The options in this section partially control which windows will appear in the Task Switcher list. + +The Sort Order drop-down list specifies whether the windows should be listed in Stacking Order or Recently Used order. Stacking Order is the order in which the windows appear on top of each other on the screen, while Recently Used order is the order in which the windows have been used. Recently Used order makes it very easy to switch between the two most frequently used windows because they will always appear in the top 2 positions in the list. + +The Include "Peek at Desktop" icon option will add a Peek at Desktop option to the window list. This allows the user to easily select the Desktop as the 'window' to show. + +The Only one window per application option reduces clutter by only showing one window for each open application. If an application has multiple windows open, then its most recently activated window will be shown in the list and the others will not be shown. + +The Order minimized windows after unminimized windows option will show you all unminimized windows first, even if they are less recent or further at the bottom than some of the minimized windows. + + + +Filter Windows By +This section contains options for additionally filtering the Task Switcher's list of windows. + +The Virtual Desktops option filters the list of windows according to which virtual desktop is currently active. If you consistently put specific windows on specific virtual desktops, then this filtering option can make it easy to switch to windows within or across those virtual desktops. Select Current desktop to only show windows on the current virtual desktop. Select All other desktops to show only the windows on the virtual desktops that are not currently active. + +The Activities option filters the list of windows according to which Activity is currently active. As with Virtual Desktop filtering, this option can make it easier to switch to applications within or across all Activities. Select Current activity to only show windows that are part of the current Activity. Select All other activities to only show windows that are part of the Activities that are not currently active. + +The Screens option filters the list of windows according to which display screen is currently active. Select Current screen to only show windows that are on the display that currently has the mouse pointer on it. Select All other screens to show the windows that are on all other displays. This option can be useful to users who want to quickly switch between windows that are on the same monitor in a multi-monitor setup. +The active screen is the one that the mouse pointer is currently on, not the screen that the currently active window is on. + +The Minimization option filters the list of windows according to whether they are hidden or not. Select Visible windows to only show windows that have not been minimized. Select Hidden windows to only show the minimized windows. + + If you uncheck an option in this section, then no filtering will be applied for that option. For example, if you check the Screens option and clear the other three options, then the Task Switcher window list will only be filtered according to which windows are on the current display. +All of the options described in the above sections work together to provide very fine-grained control of the Task Switcher's behavior and appearance. For example, you could define the Main settings collection to be invoked with the &Alt; key combination, to show the open windows in a carousel, to only show one window per application, and to only list windows that are on the current desktop and on the currently active screen. This can provide very fast context-sensitive window switching if you have both 'work' and 'home' virtual desktops, and then keep all of your spreadsheets for work and home on the same monitor. + +The availability of the Alternative Task Switcher configuration gives you a second way to easily filter and browse through the window lists. With eight key combinations available across the two Task Switcher configurations, it should be possible to easily and quickly navigate through large numbers of windows. + + +
+ + diff --git a/local/recipes/kde/kwin/source/doc/kwintabbox/taskswitcher.png b/local/recipes/kde/kwin/source/doc/kwintabbox/taskswitcher.png new file mode 100644 index 0000000000..d5a82d95ab Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/kwintabbox/taskswitcher.png differ diff --git a/local/recipes/kde/kwin/source/doc/kwintouchscreen/CMakeLists.txt b/local/recipes/kde/kwin/source/doc/kwintouchscreen/CMakeLists.txt new file mode 100644 index 0000000000..3c3bb44975 --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/kwintouchscreen/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR kcontrol/kwintouchscreen) diff --git a/local/recipes/kde/kwin/source/doc/kwintouchscreen/index.docbook b/local/recipes/kde/kwin/source/doc/kwintouchscreen/index.docbook new file mode 100644 index 0000000000..6989ce703a --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/kwintouchscreen/index.docbook @@ -0,0 +1,40 @@ + + + +]> + +
+ + +Touchscreen Gestures + +&Mike.McBride; &Mike.McBride.mail; + + + +2021-04-10 +Plasma 5.20 + + +KDE +Systemsettings +touch +screen + + + +Swiping from the screen edge towards the center of the screen allow you to activate effects. Here you can configure which effect will get activated on each edge of the screen. + + +Click with any mouse button onto a square and select an effect +in the context menu. Edges with a blue square have already an attached effect, +a grey-colored square indicates that no effect is selected for this edge. + +The number of accessible items in the context menu depends on the settings in the module + +Desktop Effects in the Workspace +category. Select your favorite effects from the Window Management +group. This activates the corresponding items in the context menu. +
diff --git a/local/recipes/kde/kwin/source/doc/kwinvirtualkeyboard/CMakeLists.txt b/local/recipes/kde/kwin/source/doc/kwinvirtualkeyboard/CMakeLists.txt new file mode 100644 index 0000000000..ec32b7efaa --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/kwinvirtualkeyboard/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR kcontrol/kwinvirtualkeyboard) diff --git a/local/recipes/kde/kwin/source/doc/kwinvirtualkeyboard/index.docbook b/local/recipes/kde/kwin/source/doc/kwinvirtualkeyboard/index.docbook new file mode 100644 index 0000000000..40d18ec2f1 --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/kwinvirtualkeyboard/index.docbook @@ -0,0 +1,44 @@ + + + +]> + +
+ + +Virtual Keyboard + + +Yuri +Chornoivan + + + + +2021-04-27 +Plasma 5.22 + + +KDE +Systemsettings +virtual +keyboard + + + + + This module lets you choose the virtual keyboard to use. The virtual keyboard will be automatically enabled when there is no hardware keyboard detected. + + + + Select a virtual keyboard + from the list or choose None if you do not want to use any virtual keyboard. + + + + It is advisable to install corresponding input method engines before using this module. + + +
diff --git a/local/recipes/kde/kwin/source/doc/moveresizerestriction/moveresizerestriction.pdf b/local/recipes/kde/kwin/source/doc/moveresizerestriction/moveresizerestriction.pdf new file mode 100644 index 0000000000..3fd87fc4f8 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/moveresizerestriction/moveresizerestriction.pdf differ diff --git a/local/recipes/kde/kwin/source/doc/moveresizerestriction/moveresizerestriction.tex b/local/recipes/kde/kwin/source/doc/moveresizerestriction/moveresizerestriction.tex new file mode 100755 index 0000000000..bbd91e6c35 --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/moveresizerestriction/moveresizerestriction.tex @@ -0,0 +1,232 @@ +% SPDX-FileCopyrightText: 2025 Yifan Zhu +% SPDX-License-Identifier: CC0-1.0 +\documentclass[8pt]{beamer} + +\usepackage{tikz} +\usetikzlibrary{patterns} % LaTeX and plain TeX when using TikZ + +\newcommand{\questionMark}{\usefont{T1}{cmr}{m}{n}\selectfont\color{red}{?}} + +\title{moveResize restriction algorithm} + +\begin{document} + +\begin{frame} + \maketitle +\end{frame} + +\begin{frame} + \only<1>{ + The big rectangle represents the overall available area -- windows are not visible outside. + The grey rectangles represent the struts (think of them as obstacles -- windows are not visible in these areas). + } + \only<2>{ + Since the titlebar need to have certain number of continuous visible pixels, extend each strut to the left (by requiredPixels) and to the top (by titlebarHeight). + Shrink the overall available area from the right and bottom by the same amount. + These are the areas where the top left corner of the visible titlebar subrect cannot be placed (red diagonal lines). + The remaining white area is availableRegion. + } + \only<3>{ + Since availableRegion is a QRegion, it is automatically split into rectangles (green with dashed borders, and ``availableRect'' in center). + } + \only<4>{ + The next step depends on window location (shown in blue) and move/resize type. + } + \only<5>{ + \frametitle{Move} + Assume \emph{move} for now. + Recall availableRect stores possible locations of the top left corner of the visible titlebar subrect. + For each availableRect (stopping early if availableRect becomes empty): + \begin{itemize} + \item Apply restrictions to visible subrect top-left + \begin{itemize} + \item None needed for move. + \end{itemize} + \item Convert visible subrect top-left to window top-left + \begin{itemize} + \item + Extend each availableRect to the left by windowWidth - requiredPixels (gray dots, with text anchor candidate). + \end{itemize} + \end{itemize} + + For proposed anchor point (top left corner of the window, calculated from user input, red question mark), find closest anchor candidate point (green circle). + } + \only<6>{ + \frametitle{Move} + We can visually inspect the solution. + } + \only<7>{ + \frametitle{Resize Left} + Now assume user is resizing the \emph{left} of the window. + For this case anchor is also top left. + For each availableRect (stopping early if availableRect becomes empty): + \begin{itemize} + \item Apply restrictions to visible subrect top-left + \begin{itemize} + \item clip bottom to windowBottom - titlebarHeight (always performed for resize); + \item clip right to windowRight - requiredPixels; + \end{itemize} + \item Convert visible subrect top-left to window top-left + \begin{itemize} + \item extend left to overall available area left. + \end{itemize} + \end{itemize} + For proposed anchor point (red question mark), find closest anchor candidate point (green circle), while only allowing horizontal movement. + } + \only<8>{ + \frametitle{Resize Left} + We can visually inspect the solution. + } + \only<9>{ + \frametitle{Resize Top Right} + Now assume user is resizing the \emph{top right} of the window. + For this case anchor is top right. + Transform availableRect and convert to possible locations of the top right corner of the window. + For each availableRect (stopping early if availableRect becomes empty): + \begin{itemize} + \item Apply restrictions to visible subrect top-left + \begin{itemize} + \item clip bottom to windowBottom - titlebarHeight (always performed for resize); + \item clip left to windowLeft (top-left of visible subrect must be right of windowLeft); + \end{itemize} + \item Convert visible subrect top-left to window top-right: + \begin{itemize} + \item extend right to overall available area right; + \item move left right by requiredPixels; + \end{itemize} + \end{itemize} + For proposed anchor point (red question mark), find closest anchor candidate point (green circle). + } + \only<10>{ + \frametitle{Resize Top Right} + We can visually inspect the solution. + } + + \vfill + + \begin{center} + \resizebox{0.8\textwidth}{!}{ + \begin{tikzpicture}[yscale=-1,every node/.style={scale=1.5,thick}] + + % Draw the main blank rectangle canvas + \draw[thick] (0,0) rectangle (16,12); + \tikzstyle{strut} = [fill=gray!30, draw=black, thick] + \tikzstyle{forbidden} = [ + draw=black, thick, fill opacity=0.3, text opacity=1, pattern=north east lines, pattern color=red] + \tikzstyle{available} = [fill=green!30, dashed, fill opacity=.3, text opacity=1, draw=black, thick] + \tikzstyle{window} = [fill=blue!50, fill opacity=.5, text opacity=1, draw=black, thick] + \tikzstyle{final} = [ + draw=black, thick, fill opacity=0.3, text opacity=1, pattern=dots, pattern color=black] + \tikzstyle{result} = [green, fill=green, radius=0.15cm] + \tikzstyle{windowFinal} = [fill opacity=.3, text opacity=1, draw=black, thick, pattern=north west lines, pattern color=blue] + + \only<1-3,6,8,10>{ + % Draw rectangles on four sides + \draw[strut] (6,0) rectangle (10,1); + \draw[strut] (6,11) rectangle (10,12); + \draw[strut] (0,2) rectangle (1,10); + \draw[strut] (15,3) rectangle (16,9); + + % \draw node at (8, 6) {Screen}; + + \draw[strut] (17,0) rectangle (19,1) node[midway] {Strut}; + } + + \only<2-3>{ + \draw[forbidden] (17,1) rectangle (20,2) node[midway] {Visible subrect}; + \draw[forbidden] (3,-1) rectangle (10,1); + \draw[forbidden] (3,10) rectangle (10,12); + \draw[forbidden] (-3,1) rectangle (1,10); + \draw[forbidden] (12,2) rectangle (16,9); + + \draw[forbidden] (13,0) rectangle (16,12); + \draw[forbidden] (0,11) rectangle (16,12); + } + \only<3-4>{ + \draw[available] (17,2) rectangle (21,3) node[midway] {availableRect}; + \draw[available] (0,0) rectangle (3,1) node[midway] {availableRect}; + \draw[available] (0,10) rectangle (3,11) node[midway] {availableRect}; + \draw[available] (10,0) rectangle (13,2) node[midway] {availableRect}; + \draw[available] (10,9) rectangle (13,11) node[midway] {availableRect}; + \draw[available] (1,1) rectangle (10,10) node[midway] {availableRect}; + \draw[available] (10,2) rectangle (12,9) node[midway,align=left] {available\\Rect}; + } + \only<5,7,9>{ + \draw[available] (17,2) rectangle (21,3) node[midway] {availableRect}; + \draw[available] (0,0) rectangle (3,1); + \draw[available] (0,10) rectangle (3,11); + \draw[available] (10,0) rectangle (13,2); + \draw[available] (10,9) rectangle (13,11); + \draw[available] (1,1) rectangle (10,10); + \draw[available] (10,2) rectangle (12,9); + } + \only<4->{ + \draw[window] (17,3) rectangle (19,4) node[midway] {Window}; + \draw[window] (5, 4) rectangle (13, 8) node[midway] {Window}; + } + \only<5>{ + \draw[final] (17,4) rectangle (21,5) node[midway] {anchor candidate}; + \draw[final] (-5,0) rectangle (3,1) node[midway] {anchor candidate}; + \draw[final] (-5,10) rectangle (3,11) node[midway] {anchor candidate}; + \draw[final] (5,0) rectangle (13,2) node[midway] {anchor candidate}; + \draw[final] (5,9) rectangle (13,11) node[midway] {anchor candidate}; + \draw[final] (-4,1) rectangle (10,10) node[midway] {anchor candidate}; + \draw[final] (5,2) rectangle (12,9) node[midway,align=left] {anchor candidate}; + \draw node at (19, 5.5) {\questionMark : proposed anchor}; + \draw node at (14, 8) {\questionMark}; + \draw[result] (18, 6.5) circle node[right] {closestPoint}; + \draw[result] (13, 9) circle; + } + \only<6>{ + \draw node at (19, 5.5) {\questionMark : proposed anchor}; + \draw node at (14, 8) {\questionMark}; + \draw[result] (18, 6.5) circle node[right] {closestPoint}; + \draw[result] (13, 9) circle; + \draw[windowFinal] (17,7) rectangle (20,8) node[midway] {Final Window}; + \draw[windowFinal] (13, 9) rectangle (21, 13) node[midway] {Final Window}; + } + \only<7>{ + \draw[final] (17,4) rectangle (21,5) node[midway] {anchor candidate}; + \draw[final] (0,0) rectangle (3,1) node[midway] {anchor candidate}; + % \draw[final] (0,10) rectangle (3,11) node[midway] {anchor candidate}; + \draw[final] (0,1) rectangle (10,7) node[midway] {anchor candidate}; + \draw node at (19, 5.5) {\questionMark : proposed anchor}; + \draw node at (11, 4) {\questionMark}; + \draw[result] (18, 6.5) circle node[right] {closestPoint}; + \draw[result] (10, 4) circle; + } + \only<8>{ + \draw node at (19, 5.5) {\questionMark : proposed anchor}; + \draw node at (11, 4) {\questionMark}; + \draw[result] (18, 6.5) circle node[right] {closestPoint}; + \draw[result] (10, 4) circle; + \draw[windowFinal] (17,7) rectangle (20,8) node[midway] {Final Window}; + \draw[windowFinal] (10, 4) rectangle (13, 8) node[midway] {Final Window}; + } + \only<9>{ + \draw[final] (17,4) rectangle (21,5) node[midway] {anchor candidate}; + % \draw[final] (0,0) rectangle (3,1) node[midway] {anchor candidate}; + % \draw[final] (0,10) rectangle (3,11) node[midway] {anchor candidate}; + \draw[final] (13,0) rectangle (16,2) node[midway] {anchor candidate}; + % \draw[final] (10,9) rectangle (13,11) node[midway] {anchor candidate}; + \draw[final] (8,1) rectangle (16,7) node[midway] {anchor candidate}; + \draw[final] (13,2) rectangle (16,7) node[midway,align=left] {anchor candidate}; + \draw node at (19, 5.5) {\questionMark : proposed anchor}; + \draw node at (11, 0) {\questionMark}; + \draw[result] (18, 6.5) circle node[right] {closestPoint}; + \draw[result] (11, 1) circle; + } + \only<10>{ + \draw node at (19, 5.5) {\questionMark : proposed anchor}; + \draw node at (11, 0) {\questionMark}; + \draw[result] (18, 6.5) circle node[right] {closestPoint}; + \draw[result] (11, 1) circle; + \draw[windowFinal] (17,7) rectangle (20,8) node[midway] {Final Window}; + \draw[windowFinal] (5, 1) rectangle (11, 8) node[midway] {Final Window}; + } + \end{tikzpicture} + } + \end{center} +\end{frame} + +\end{document} diff --git a/local/recipes/kde/kwin/source/doc/windowbehaviour/CMakeLists.txt b/local/recipes/kde/kwin/source/doc/windowbehaviour/CMakeLists.txt new file mode 100644 index 0000000000..1f48a01ea8 --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/windowbehaviour/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR kcontrol/windowbehaviour) diff --git a/local/recipes/kde/kwin/source/doc/windowbehaviour/index.docbook b/local/recipes/kde/kwin/source/doc/windowbehaviour/index.docbook new file mode 100644 index 0000000000..3ac69256be --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/windowbehaviour/index.docbook @@ -0,0 +1,1214 @@ + + + +]> + + + +
+ +Window Behavior + +&Mike.McBride; &Mike.McBride.mail; +&Jost.Schenck; &Jost.Schenck.mail; +NatalieClariusnatalie_clarius@yahoo.de + + + +2022-08-31 +Plasma 5.26 + + +KDE +KControl +system settings +actions +window +window placement +window size +window management +window behavior +focus +raise +titlebar +screen +snap + + + + +Window Behavior + + In the upper part of this control module you can see several +tabs: Focus, Titlebar Actions, +Window Actions, Movement and +Advanced. In the +Focus panel you can configure how windows gain or +lose focus, &ie; become active or inactive. Using +Titlebar Actions and Window Actions +you can configure how titlebars and windows react to +mouse clicks. Movement allows you to configure how +windows move and place themselves when started. The +Advanced options cover some specialized options +like window shading. + + + + +Please note that the configuration in this module will not take effect +if you do not use &plasma;'s native window manager, &kwin;. If you do use a +different window manager, please refer to its documentation for how to +customize window behavior. + + + + +Focus + + +The focus of the workspace refers to the window which the +user is currently working on. The window with focus is often referred to +as the active window. + + + +Focus does not necessarily mean the window is the one at the +front — this is referred to as raised, and +although this is configured here as well, focus and raising of windows +are configured independently. + + + +Windows activation policy + + +There are six methods &kwin; can use to determine the current focus: + + + + + +Click to focus + + +A window becomes active when you click into it. +This behaviour is common on other operating systems and likely what you want. + + + + +Click to focus (mouse precedence) + + +This is mostly the same as Click to focus. +If an active window has to be chosen by the system +(⪚ because the currently active one was closed) +the window under the mouse is the preferred candidate. +Unusual, but possible variant of Click to focus. + + + + + +Focus follows mouse + + +Moving the mouse pointer actively over a normal window activates it. New +windows such as the mini command line invoked with +&Alt;F2 will receive the focus, +without you having to point the mouse at them explicitly. +⪚ windows randomly appearing under the mouse will not gain the focus. +Focus stealing prevention takes place as usual. +Think as Click to focus just without having to actually click. + + +In other window managers, this is sometimes known as Sloppy focus +follows mouse. + + + + + +Focus follows mouse (mouse precedence) + + +This is mostly the same as Focus follows mouse. +If an active window has to be chosen by the system +(⪚ because the currently active one was closed) +the window under the mouse is the preferred candidate. +Choose this, if you want a hover controlled focus. + + + + + +Focus under mouse + + +The window that happens to be under the mouse pointer becomes active. If +the mouse is not over a window (for instance, it's over the desktop wallpaper) the last +window that was under the mouse has focus. New windows such as the mini +command line invoked with &Alt;F2 will +not receive the focus, you must move the mouse over them to type. + + + + + +Focus strictly under mouse + + +Similar to Focus under mouse, but even more +strict with its interpretation. Only the window under the mouse pointer is +active. If the mouse pointer is not over a window, no window has focus. +New windows such as the mini command line invoked with +&Alt;F2 will not receive the focus, +you must move the mouse over them to type. + + + + + + + + +Note that Focus under mouse and +Focus strictly under mouse prevent certain +features, such as Focus stealing prevention and the +&Alt; +walk-through-windows dialog, from working properly. + + + + + + +Delay focus by + + +This is the delay after which the window the mouse pointer is over will automatically receive focus. + + + + + +Focus stealing prevention + + +This option specifies how much KWin will try to prevent unwanted focus +stealing caused by unexpected activation of new windows. + + + + + +None +Prevention is turned off and new windows always become activated. + + + +Low +Prevention is enabled; when some window does not have support +for the underlying mechanism and KWin cannot reliably decide whether to activate +the window or not, it will be activated. This setting may have both worse and better +results than the medium level, depending on the applications. + + + +Medium +Prevention is enabled. + + + +High +New windows get activated only +if no window is currently active or if they belong to the currently active +application. This setting is probably not really usable when not using mouse +focus policy. + + + +Extreme +All windows must be explicitly activated by the user. + + + + + +Windows that are prevented from stealing focus are marked as demanding +attention, which by default means their taskbar entry will be highlighted. +This can be changed in the Notifications control module. + + + + + +Raising windows + + +Besides receiving focus, you can also control under which conditions windows get raised, &ie; brought to the front. + + + +You should make sure that at least one of the raising options is enabled, otherwise windows will not be raised at all. + + + +Click raises active window will bring a window to the front when it is clicked on. This is enabled by default with a click to focus policy. + + + +By activating Raise on hover, delayed by you can alternatively bring a window to the front if the mouse pointer is over that window for a specified period of time. You can determine the delay for this option by using the spin box control. This auto-raising option is only available with a hover to focus policy. + + + + +Setting the delay too short will cause a rapid fire changing of +windows, which can be quite distracting. Most people will like a delay +of 100-300 ms. This is responsive, but it will let you slide over the +corners of a window on your way to your destination without bringing +that window to the front. + + + + + + +Multiscreen behavior + + +This controls the behavior of window focus with multiple screens. Note that these options appear only when more than one screen is currently connected. + + + + + +Separate screen focus + + +When this option is enabled, focus operations are limited only to the active screen. For instance, when you close a window, then the next window to receive focus will be a window on the active screen, even if there is a more recently used window on a different screen. + + + + + + + + + + + +Titlebar Actions + + +In this panel you can configure what happens to windows when a mousebutton is +clicked on their titlebars. + + + +<guilabel>Titlebar Actions</guilabel> + + +This section allows you to determine what happens when you double-click +or scroll the mouse wheel on the titlebar of a window. + + + +The following actions are available for Double-click: + + + + + +Maximize + + +Resizes the window to fill the height and width of the screen. + + + + + +Vertically maximize + + +Resizes the window to the height of the screen. + + + + + +Horizontally maximize + + +Resizes the window to the width of the screen. + + + + + +Minimize + + +Hides the window into its minimized state, from which it can be restored ⪚ via the Task Manager or Task Switcher. + + + + + +Shade + + +Causes the window to be +reduced to simply the titlebar. Double-clicking on the titlebar again +restores the window to its normal size. + + + + + +Close + + +Closes the window. + + + + + +Show on all desktops + + +Makes the window be visible on all Virtual Desktops. + + + + + +Do nothing + + +Nothing happens on double-click. + + + + + + + +The Mouse wheel can be used to trigger an action depending on whether it is scrolled up or down: + + + + + +Raise/lower + + +Scrolling up will move the window on top of other windows. + + +Scrolling down will move the window below other windows. + + + + + +Shade/unshade + + +Scrolling up will collapse the window to just its titlebar. + + +Scrolling down will restore the window to its normal size. + + + + + +Maximize/restore + + +Scrolling up will maximize the window to fill the whole screen. + + +Scrolling down will restore it to its previous size. + + + + + +Keep above/below + + +Scrolling up will make the window stay on top, covering other windows. + + +Scrolling down will make the window stay covered below other windows. + + + + + +Move to previous/next desktop + + +Scrolling up will move the window to the previous Virtual Desktop. + + +Scrolling down will move the window to the next Virtual Desktop. + + + + + +Change opacity + + +Scrolling up will make the window less transparent. + + +Scrolling down will make the window more transparent. + + + + + +Do nothing + + +Nothing happens when scrolling up or down on the window's titlebar. + + + + + + + + +You can have windows automatically unshade when you simply place the +mouse over their shaded titlebar. Just check the Window +unshading check box in the Advanced tab of +this module. This is a great way to reclaim screen space when you are +cutting and pasting between a lot of windows, for example. + + + + + + +<guilabel>Titlebar and Frame Actions</guilabel> + + +This section allows you to determine what happens when you single click +on the titlebar or frame of a window. Notice that you can have +different actions associated with the same click depending on whether +the window is active or not. + + + For each combination of mousebuttons, Active and +Inactive, you can select the most appropriate choice. The actions are +as follows: + + + + + +Raise + + +Will bring the window to the top of the window stack. All other windows +which overlap with this one will be hidden below it. + + + + + +Lower + + +Will move this window to the bottom of the window stack. This will get the +window out of the way. + + + + + +Toggle raise and lower + + +This will raise windows which are not on top, and lower windows which +are already on top. + + + + + +Minimize + + +Hides the window into its minimized state, from which it can be restored ⪚ via the Task Manager or Task Switcher. + + + + + +Shade + + +Causes the window to be +reduced to simply the titlebar. Double-clicking on the titlebar again +restores the window to its normal size. + + + + + +Close + + +Closes the window. + + + + + +Window menu + + +Will bring up a small submenu where you can choose window related +commands (&ie; Move to Desktop, Move to Screen, Maximize, Minimize, Close, &etc;). + + + + + +Do nothing + + +Nothing happens on click. + + + + + + + + + +<guilabel>Maximize Button Actions</guilabel> + + +This section allows you to determine the behavior of the three mouse buttons +onto the maximize button. + + + + + +Maximize + + +Resizes the window to the height and width of the screen. + + + + + +Vertically maximize + + +Resizes the window to the height of the screen. + + + + + +Horizontally maximize + + +Resizes the window to the width of the screen. + + + + + + + + + + + +Window Actions + + +<guilabel>Inactive Inner Window</guilabel> + + +This part of the module, allows you to configure what happens when you +click on an inactive window, with any of the three mouse buttons or use +the mouse wheel. + + + +Your choices are as follows: + + + + + +Activate, raise and pass click + + +This makes the clicked window active, raises it to the top of the +display, and passes a mouse click to the application within the window. + + + + + +Activate and pass click + + +This makes the clicked window active and passes a mouse click to the +application within the window. + + + + + +Activate + + +This simply makes the clicked window active. The mouse click is not +passed on to the application within the window. + + + + + +Activate and raise + + +This makes the clicked window active and raises the window to the top of +the display. The mouse click is not passed on to the application within +the window. + + + + + + + +Your choices for Mouse wheel are as follows: + + + + + +Scroll + + +Just scrolls the content within the window. + + + + + +Activate and scroll + + +This makes the clicked window active and scrolls the content. + + + + + +Activate, raise and scroll + + +This makes the clicked window active, raises the window to the top of +the display, and scrolls the content. + + + + + + + + + +<guilabel>Inner Window, Titlebar and Frame</guilabel> + + +This bottom section allows you to configure additional actions when +clicking on a window with a modifier key pressed. + + + +As a Modifier key, you can select between Meta (default) or Alt. + + + +Once again, you can select different actions for +Left, Middle and +Right button clicks and the Mouse +wheel. + + + +Your choices for the mouse buttons are: + + + + + +Move + + +Allows you to drag the selected window around the workspace. + + + + + +Activate, raise and move + + +This makes the clicked window active, raises it to the top of the +window stack, and drags the window around the workspace. + + + + + +Toggle raise and lower + + +This will raise windows which are not on top, and lower windows which +are already on top. + + + + + +Resize + + +Allows you to change the size of the selected window. + + + + + +Raise + + +Will bring the window to the top of the window stack. All other windows +which overlap with this one will be hidden below it. + + + + + +Lower + + + Will move this window to the bottom of the window stack. This will get the +window out of the way. + + + + + +Minimize + + +Hides the window into its minimized state, from which it can be restored ⪚ via the Task Manager or Task Switcher. + + + + + +Decrease opacity + + +Makes the window more transparent. + + + + + +Increase opacity + + +Makes the window less transparent. + + + + + +Window menu + + +Will bring up a small submenu where you can choose window related +commands (&ie; Move to Desktop, Move to Screen, Maximize, Minimize, Close, &etc;). + + + + + +Do nothing + + +Nothing happens on click. + + + + + + + +Your choices for the mouse wheel are: + + + + + +Raise/lower + + +Scrolling up will move the window on top of other windows. + + +Scrolling down will move the window below other windows. + + + + + +Shade/unshade + + +Scrolling up will collapse the window to just its titlebar. + + +Scrolling down will restore the window to its normal size. + + + + + +Maximize/restore + + +Scrolling up will maximize the window to fill the whole screen. + + +Scrolling down will restore it to its previous size. + + + + + +Keep above/below + + +Scrolling up will make the window stay on top, covering other windows. + + +Scrolling down will make the window stay covered below other windows. + + + + + +Move to previous/next desktop + + +Scrolling up will move the window to the previous Virtual Desktop. + + +Scrolling down will move the window to the next Virtual Desktop. + + + + + +Change opacity + + +Scrolling up will make the window less transparent. + + +Scrolling down will make the window more transparent. + + + + + +Do nothing + + +Nothing happens on when scrolling up or down the window's titlebar. + + + + + + + + + + + +Movement + +This page allows you to configure the Snap +Zones. These are like a magnetic field along the side of +the screen and each window, which will make windows snap alongside +when moved near. + + + + + +Screen edge snap zone + +Here you can set the snap zone for screen borders. Moving a +window within the configured distance will make it snap to the edge of +the screen. + + + + + +Window snap zone + + +Here you can set the snap zone for windows. As with screen +borders, moving a window near to another will make it snap to the edge +as if the windows were magnetized. + + + + + +Center snap zone + + +Here you can set the snap zone for the screen center, &ie; the +strength of the magnetic field which will make windows snap +to the center of the screen when moved near it. + + + + + +Snap windows: Only when overlapping + + +If checked, windows will not snap together if they are only near +each other, they must be overlapping, by the configured amount or +less. + + + + + + + + +In the Screen Edges settings module in the Workspace Behavior section of the system settings, you can configure windows to be quick-tiled to the whole, half, or quarter of the screen when dragged near the screen edges. + + + + + + +Advanced + + +In the Advanced panel you can do more advanced fine +tuning to the window behavior. + + + +Window unshading + + + +On titlebar hover after + + +If this option is enabled, a shaded window will un-shade automatically +when the mouse pointer has been over the titlebar for some time. Use +the spinbox to configure the delay un-shading. + + + + + + + +Window placement + + +The placement policy determines where a new window will appear +on the screen. + + + +In a multi-monitor setup, the screen for windows to appear on is always the active screen (that is, the screen that has the mouse pointer or the focused window; see Multiscreen behavior), with the exception of windows remembering their previous position (see below). + + + + + +Minimal Overlapping + + +Will place all new windows in such a manner as to overlap existing windows as little as possible. + + + + + +Maximized + + +Will try to maximize all new windows to fill the whole screen. + + + + + +Random + + +Will place all new windows in random locations. + + + + + +Centered + + +Will place all new windows in the center of the screen. + + + + + +In Top-Left Corner + + +Will place all new windows with their top left corner in the top left corner of +the screen. + + + + + +Under Mouse + + +Will place all new windows centered under the mouse pointer. + + + + + + + +Check the Allow apps to remember the positions of +their own windows item to open windows where they previously were rather than by the placement method chosen above. Note that this remembered position includes the screen assignment, so windows may open on a screen other than the active one if this is where they were last located. Note also that this option is only available on X11, not on Wayland, and is only supported by some KDE applications. + + + + +If you would like some windows to appear on specific positions, screens, or Virtual Desktops, you can set up Window Rules to configure special window or application settings. You can find this by right-clicking on the titlebar of a window and choosing More Actions, or in the Window Rules module in the Window Management section of system settings. + + + + + + + +Special windows + + + + +Hide utility windows for inactive applications + + +When turned on, utility windows (tool windows, torn-off menus, ...) of +inactive applications will be hidden and will be shown only when the +application becomes active. Note that applications have to mark the windows +with the proper window type for this feature to work. + + + + + + + + + +Virtual Desktop behavior + + +Sometimes calling an application will activate an existing window rather than opening a new window. This setting controls what should happen if that activated window is located on a Virtual Desktop other than the current one. + + + + + +Switch to that Virtual Desktop + + +Will switch to the Virtual Desktop where the window is currently located. + + +Choose this option if you would like the active Virtual Desktop to automatically follow windows to their assigned Virtual Desktop. + + + + + +Bring window to current Virtual Desktop + + +Will cause the window to jump to the active Virtual Desktop. + + +Choose this option if you would like windows to always open on the current Virtual Desktop, and the active Virtual Desktop to only switch when navigating there manually. + + + + + +Do nothing + + +The window stays on the desktop it currently is and the current desktop doesn't change. + + +Choose this option if you would like to keep the windows and desktops as they are. + + + + + + + + + + + + +
diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/CMakeLists.txt b/local/recipes/kde/kwin/source/doc/windowspecific/CMakeLists.txt new file mode 100644 index 0000000000..732014926b --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/windowspecific/CMakeLists.txt @@ -0,0 +1,2 @@ +########### install files ############### +kdoctools_create_handbook(index.docbook INSTALL_DESTINATION ${KDE_INSTALL_DOCBUNDLEDIR}/en SUBDIR kcontrol/windowspecific) diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/Face-smile.png b/local/recipes/kde/kwin/source/doc/windowspecific/Face-smile.png new file mode 100644 index 0000000000..8e26cba799 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/Face-smile.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/akgregator-info.png b/local/recipes/kde/kwin/source/doc/windowspecific/akgregator-info.png new file mode 100644 index 0000000000..cec27134b3 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/akgregator-info.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/akregator-attributes.png b/local/recipes/kde/kwin/source/doc/windowspecific/akregator-attributes.png new file mode 100644 index 0000000000..1efa8ea46e Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/akregator-attributes.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/akregator-fav.png b/local/recipes/kde/kwin/source/doc/windowspecific/akregator-fav.png new file mode 100644 index 0000000000..42aed2ed82 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/akregator-fav.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/config-win-behavior.png b/local/recipes/kde/kwin/source/doc/windowspecific/config-win-behavior.png new file mode 100644 index 0000000000..307120443e Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/config-win-behavior.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/emacs-attribute.png b/local/recipes/kde/kwin/source/doc/windowspecific/emacs-attribute.png new file mode 100644 index 0000000000..638f9e8a00 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/emacs-attribute.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/emacs-info.png b/local/recipes/kde/kwin/source/doc/windowspecific/emacs-info.png new file mode 100644 index 0000000000..7787f1a89a Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/emacs-info.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/focus-stealing-pop2top-attribute.png b/local/recipes/kde/kwin/source/doc/windowspecific/focus-stealing-pop2top-attribute.png new file mode 100644 index 0000000000..233a917859 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/focus-stealing-pop2top-attribute.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/index.docbook b/local/recipes/kde/kwin/source/doc/windowspecific/index.docbook new file mode 100644 index 0000000000..e0b8dfcd67 --- /dev/null +++ b/local/recipes/kde/kwin/source/doc/windowspecific/index.docbook @@ -0,0 +1,1020 @@ + + + +]> + + +Window Rules + +&Lauri.Watts; &Lauri.Watts.mail; + + + Parts of this documentation was converted from the KDE UserBase KWin Rules page and updated by the &kde; Documentation team to Plasma 5.8. + + + + + +&FDLNotice; +2016-06-23 + Plasma 5.8 + +Here you can customize window settings specifically only for +some windows. + + +KDE +KControl +window settings +window placement +window size + + + +Window Specific Settings: Quick Start + +Here you can customize window settings specifically only for +some windows. + + +Please note that this configuration will not take effect if you +do not use &kwin; as your window manager. If you do use a different +window manager, please refer to its documentation for how to customize +window behavior. + + +Many of the settings you can configure here are those you can +configure on a global basis in the Window Behavior +&systemsettings; module, however some of them are even more detailed. + +They encompass geometry, placement, whether a window should be +kept above or below others, focus stealing prevention, and translucency +settings. + +You can access this module in two ways: from the titlebar of the +application you wish to configure, or from the &systemsettings;. If you +start it from within &systemsettings; you can use the +New... to create a window profile, and the +Detect Window Properties button on the resulting dialog to +partially fill in the required information for the application +you wish to configure. + +You can also at any time Modify... or +Delete any stored settings profile, and +reorder the list. Reordering the list using the Move Up +and Move Down buttons effects on how they are applied. + + + + + +Overview +&kwin; allows the end-user to define rules to alter an application's window attributes. + +For example, when an application is started, it can be forced to always run on Virtual Desktop 2. Or a defect in an application can be worked-around to force the window above others. + +Step-by-step examples are provided along with detailed information on using the &kwin; Rule Editor to specify Window Matching and Window Attributes. + + +Examples and Application Workaround +To see what's possible, detailed examples are provided which can also be used to model your own rules. + +A special page is dedicated to address Application Workaround. + + +KWin Rule Editor +Invoking the KWin Rule Editor + + + + + + + + + + + + + +There are several ways to invoke the &kwin; Rule Editor. Below are two: + + +Right-click on the title-bar of any window, choose More ActionsWindow Manager Settings... and in the Configure window, select Window Rules or + + +System SettingsWindow BehaviorWindow Rules + +The main window is used to: + + +Affect rules with New..., Modify... and Delete +Share rules with others via Import and Export +Ensure desired rule evaluation using Move Up and Move Down + +Rule Evaluation +When an application starts (or the rules are modified), &kwin; evaluates the rules from the top of the list to the bottom. For all rules which match a window, the collective set of attributes are applied to the window, then the window is displayed. + +Should two or more matching rules enable the same attribute, the setting in the first rule in the list is used. + +You can tailor children windows for the application by placing the more restrictive rules first - see the Kopete and Kopete Chat Window example. + + + +Rule Editor + + + + + + + + + + + + + +The editor is composed of four tabs: + + +Window matching +Size & Position +Arrangement & Access +Appearance & Fixes + +As the name implies, Window matching is used to specify criteria to match one or more windows. The other three tabs are used to alter the attributes of the matching windows. + +Panels can also be affected. + +Window Matching +Each window rule has user specified Window Matching criteria. &kwin; uses the criteria to determine whether the rule is applicable for an application. + + +Window Attributes +Along with Window Matching criteria, each window rule has a set of Window Attributes. The attributes override the corresponding application's settings and are applied before the window is displayed by &kwin;. + + + + + +Window Matching + + + + + + + + + + + + + +The Window Matching tab is used to specify the criteria &kwin; uses to evaluate whether the rule is applicable for a given window. + +Zero (match any window) or more of the following may be specified: + + +Window class (application) - match the class. +Match whole window class - include matching the secondary class. + + +Window role - restrict the match to the function of the window (⪚ a main window, a chat window, &etc;) +Window types - restrict the match to the type of window: Normal Window, Dialog Window, &etc; +Window title - restrict the match to the title of the window. +Machine (hostname) - restrict the match to the host name associated with the window. + +While it's possible to manually enter the above information, the preferred method is to use the Detect Window Properties button. + +For each field, the following operators can be applied against the field value: + + +Unimportant - ignore the field. +Exact Match +Substring Match + +Both Exact Match and Substring Match implement case insensitive matching. For example, AB matches the string AB, ab, Ab and aB. + + +Regular Expression - Qt's regular expressions are implemented - see pattern matching using regular expressions. + +Detect Window Properties + + + + + + + + + + + + + +The Detect Window Properties function simplifies the process of entering the matching-criteria. + + +For the application you'd like to create a rule, start the application. +Next, in the Window matching tab, set the number of seconds of delay before the Detect Window Properties function starts. The default is zero seconds. +Click on Detect Window Properties and +When the mouse-cursor turns to cross-hairs, place it inside the application window (not the title bar) and left-click. +A new window is presented with information about the selected window. Select the desired fields: +Secondary class name - some applications have a secondary class name. This value can be used to restrict windows by this value. +Window role +Window type +Window title + + + +Click the OK button to back-fill the Window Matching criteria. + +By using a combination of the information, a rule can apply to an entire application (by Class) or a to a specific window Type within the Class - say a Toolbar. + + + +Window Attributes + + + + + + + + + + + + + +The attributes which can be set are grouped by function in three tabs: + + +Size & Position +Arrangement & Access +Appearance & Fixes + +Each attribute has a set of parameters which determines its disposition. + +Parameters +Each attribute, minimally, accepts one of the following parameters. Additional, attribute-specific arguments are listed within each attribute definition. + + + Do Not Affect + + Ensure a subsequent rule, which matches the window, does not affect the attribute. + + + Apply Initially + + Start the window with the attribute and allow it to be changed at run-time. + + + Remember + + Use the attribute setting as defined in the rule and if changed at run-time, save and use the new value instead. + + + Force + + The setting cannot be changed at run-time. + + + Apply Now, Force Temporarily + + Apply/Force the setting once and unset the attribute.The difference between the two is at run-time, Apply Now allows the attribute to be changed and Force Temporarily prohibits it to be altered until all affected windows exit. + + + +For Apply Now, if the rule has no other attributes set, the rule is deleted after evaluation whereas Force Temporarily, the rule is deleted after the last affected window terminates. + + +Attributes +The Detect Window Properties button back-fills attribute-specific values - for more information see Window Matching. For example the height and width values of the Size attribute is set to the height and width of the detected window. + +Yes/No arguments are used to toggle on or off attributes. Leniency with grammar helps one understand how a setting will be processed. For example, the attribute Skip taskbar, when set to No means do not skip the taskbar. In other words, show the window in the taskbar. + +Size & Position + + Position + + Position the window's upper left corner at the specified x,y coordinate. + + + +&kwin;'s origin, (0,0), is the upper left of the desktop. + + + Size + + The width and height of the window. + + + Maximized horizontally, Maximized vertically + + These attributes are used to toggle the maximum horizontal/minimum horizontal window attribute. + + + Desktop, Activity, Screen + + Place the window on the specified (Virtual) Desktop, Activity or Screen. Use All Desktops to place the window on all Virtual Desktops. + + + Fullscreen, Minimized, Shaded + + Toggle the Fullscreen, Minimize and Shading window attribute. For example, a window can be started Minimized or if it is started Minimized, it can be forced to not. + + + +Maximized attribute is emulated by using both Maximized horizontally and Maximized vertically or Initial placement with the Maximizing argument. + + + Initial placement + + Override the global window placement strategy with one of the following: + +Default - use the global window placement strategy. +No Placement - top-left corner. +Minimal Overlapping - place where no other window exists. +Maximized - start the window maximized. +Centered - center of the desktop. +Random +In Top-Left Corner +Under Mouse +On Main Window - restrict placement of a child window to the boundaries of the parent window. + + + + + Ignore requested geometry + + Toggle whether to accept or ignore the window's requested geometry position. To avoid conflicts between the default placement strategy and the window's request, the placement strategy is ignored when the window's request is accepted. + + + Minimum size, Maximum size + + The minimum and maximum size allowed for the window. + + + Obey geometry restrictions + + Toggle whether to adhere to the window's requested aspect ratio or base increment.In order to understand this attribute, some background is required. Briefly, windows must request from the Window Manager, a base increment: the minimum number of height X width pixels per re-size request. Typically, it's 1x1. Other windows though, for example terminal emulators or editors, use fixed-fonts and request their base-increment according to the size of one character. + + + + +Arrangement & Access + + Keep above, Keep below + + Toggle whether to keep the window above/below all others. + + + Autogroup with identical + + Toggle the grouping (commonly known as tabbing) of windows. + + + Autogroup in foreground + + Toggle whether to make the window active when it is added to the current Autogroup. + + + Autogroup by ID + + Create a group via a user-defined ID. More than one rule can share the same ID to allow for seemingly unrelated windows to be grouped. + + + + Skip taskbar + + Toggle whether to display the window in the taskbar. + + + Skip pager + + Toggle whether to display the window in pager. + + + + + + + + + + + + + + + + + + Skip switcher + + Toggle whether to display the window in the ALT+TAB list. + + + Shortcut + + Assign a shortcut to the window. When Edit... is clicked, additional instructions are presented. + + + + +Appearance & Fixes + + No titlebar and frame + + Toggle whether to display the titlebar and frame around the window. + + + Titlebar color scheme + + Select a color scheme for the titlebar of the window. + + + Active/Inactive opacity + + When the window is active/inactive, set its opacity to the percentage specified. + + + +Active/Inactive opacity can only be affected when Desktop Effects are enabled. + + + Focus stealing prevention + + When a window wants focus, control on a scale (from None to Extreme) whether to honor the request and place above all other windows, or ignore its request (potentially leaving the window behind other windows): + +None - Always grant focus to the window. +Low +Normal +High +Extreme - The window's focus request is denied. Focus is only granted by explicitly requesting via the mousing. + + + + + +See Accept focus to make a window read-only - not accept any keyboard input. + + + Accept focus + + Toggle whether the window accepts keyboard input. Make the window read-only. + + + Ignore global shortcuts + + Toggle whether to ignore global shortcuts (as defined by System SettingsShortcuts and GesturesGlobal Shortcuts or by running kcmshell6 keys in konsole) while the window is active. + + + Closeable + + Toggle whether to display the Close button on the title bar. + + + +A terminal window may still be closed by the end user by ending the shell session however using Accept focus to disable keyboard input will make it more difficult to close the window. + + + Window type + + Change the window to another type and inherit the characteristics of that window: + +Normal Window +Dialog Window +Utility Window +Dock (panel) +Toolbar +Torn-Off Menu +Splash Screen +Desktop +Standalone Menubar +On Screen Display + + + + + +Use with care because unwanted results may be introduced. For example, a Splash Screen is a automatically closed by &kwin; when clicked. + + + Block compositing + + Toggle whether to disable compositing while the window exists. If compositing is enabled and the rule specifies to disable compositing, while any matching window exists, compositing will be disabled. Compositing is re-enabled when the last matching window terminates. + + + + + + +Examples +The first example details all the necessary steps to create the rules. In order to keep this page a manageable size, subsequent examples only list steps specific to the example. + +The Pager attribute refers to the Virtual Desktop Manager: + + + + + + + + + + + + +Pin a Window to a Desktop and set other Attributes +Pin &akregator; to Virtual Desktop 2. Additionally, start the application with a preferred size and position. For each attribute, use the Apply Initially parameter so it can be overridden at run-time. + +The &kwin; rule is created as follows: + + +Start &akregator; on desktop two, size and position it to suit: + + + + + + + + + + + +Right-click on the titlebar and select More ActionsWindow Manager Settings...: + + + + + + + + + + + +Select the Window Rules in the left column and click on New...: + + + + + + + + + + + +The Edit Window-Specific Settings window is displayed. Window matching is the default tab: + + + + + + + + + + + +Click Detect Window Properties with 0s delay the cursor immediately turns into cross-hairs. Click (anywhere) inside the &akregator; window (but not the title bar). The window criteria are presented. Match only by primary class name so leave the check boxes unchecked - for additional information see window matching: + + + + + + + + + + + +Clicking OK the previous window back-fills the results in the Window Matching tab. Enter a meaningful text in the Description field (which is displayed in the KWin Rule window): + + + + + + + + + + + +Enable the window attributes: Position, Size and Desktop. The initial values are set by Detect Window Properties and can be overridden: + + + + + + + + + + + +Clicking OK in the previous window returns to the main KWin Rules. The new rule with its description is listed: + + + + + + + + + + + +Click OK to close the window. +Done. + + +Application on all Desktops and Handle One Child Window Uniquely +Except for conversation windows, display &kopete; and its children windows on all desktops and skip the systray and pager. For children conversation windows, treat them as the parent window except show them in systray. + +For each attribute, use the Force parameter so it can not be overridden. + +In order to implement the above, two rules need to be created: + + +A rule for Kopete Chat and +A rule for &kopete; + +The Kopete Chat rule's matching-criteria is more restrictive than the Kopete rule as it needs to match a specific Window Role: the chat window. Due to rule evaluation processing, the Kopete Chat rule must precede the &kopete; rule in the KWin Rule list for Kopete. + +Kopete Chat Rule +Assuming a Kopete Chat window is open: + + +Use Detect Window Properties and select the Kopete Chat window. Check the Window role box to restrict the criteria to chat windows - for additional information see window matching: + + + + + + + + + + + +Clicking OK in the previous window back-fills the results in the Window Matching tab. Enter a meaningful text in the Description box: + + + + + + + + + + + +Enable the following attributes: + + + + + + + + + + + +Click through to complete entry of the rule. + +The Skip taskbar attribute is set to No to display the window in the taskbar which loosely translates to: no do not skip taskbar . + + +Kopete Rule +Assuming &kopete; is open: + + +Use Detect Window Properties and select the &kopete; window. Match only by primary class name so leave the check boxes unchecked - for additional information see window matching: + + + + + + + + + + + +Clicking OK in the previous window back-fills the results in the Window Matching tab. Enter a meaningful text in the Description box: + + + + + + + + + + + +Enable the following attributes: + + + + + + + + + + + +Click through to complete entry of the rule. + + +Kopete KWin Rule List +As mentioned, due to rule evaluation processing, the Kopete Chat rule must precede the &kopete; rule: + + + + + + + + + + + + + + +Suppress a Window from showing on Pager +KNotes currently does not allow for its notes to skip the pager however a rule easily solves this shortcoming. + +Assuming a sticky note' window is available: + + +Use Detect Window Properties and select any sticky note window. Match only by primary class name so leave the check boxes unchecked - for additional information see window matching: + + + + + + + + + + + +Clicking OK in the previous window back-fills the results in the Window Matching tab. Enter a meaningful text in the Description box: + + + + + + + + + + + +Enable the Skip Pager attribute with the Force parameter: + + + + + + + + + + + +Click through to complete entry of the rule. + + +Force a Window to the Top +To pop an active window to the top, set its Focus stealing prevention attribute to None, typically, in conjunction with the Force parameter: + + + + + + + + + + + + + +Multiple Rules per Application +Thunderbird has several different child windows. This example: + + +Pin Thunderbird's main window on Virtual Desktop 1 with a specific size and location on the desktop. +Allow the Thunderbird composer window to reside on any desktop and when activated, force focus and pop it to the top of all windows. +Pop the Thunderbird reminder to the top and do not give it focus so it isn't inadvertently dismissed. + +Each rule's matching criteria is sufficiently restrictive so their order within the main &kwin; window is not important to affect rule evaluation. + +Thunderbird - Main +Assuming the Thunderbird Main window is open, sized and position to suit: + + +Use Detect Window Properties and select the Thunderbird Main window. Check the Window role box to restrict the criteria to the main window - for additional information see window matching: + + + + + + + + + + + +Clicking OK in the previous window back-fills the results in the Window Matching tab. Enter a meaningful text in the Description box: + + + + + + + + + + + +Enable the following attributes: + + + + + + + + + + + +Click through to complete entry of the rule. + + +Thunderbird - Composer +Assuming a Thunderbird Composer window is open: + + +Use Detect Window Properties and select the Thunderbird Compose window. Check the Window role and Window type boxes to restrict the criteria to composition windows - for additional information see window matching: + + + + + + + + + + + +Clicking OK in the previous window back-fills the results in the Window Matching tab. Enter a meaningful text in the Description box: + + + + + + + + + + + +Enable the following attributes: + + + + + + + + + + + +Click through to complete entry of the rule. + + +Thunderbird - Reminder +Assuming a Thunderbird Reminder window is open: + + +Use Detect Window Properties and select the Thunderbird Reminder window. Check the Secondary class name and Window Type boxes to restrict the criteria to reminder windows - for additional information see window matching: + + + + + + + + + + + +Clicking OK in the previous window back-fills the results in the Window Matching tab. Enter a meaningful text in the Description box: + + + + + + + + + + + +Enable the following attributes: + + + + + + + + + + + +Click through to complete entry of the rule. + + + + + +Application Workarounds +Below are Workarounds for misbehaving applications. + +If you are unfamiliar with creating &kwin; Rules, see this detailed example to base your new rule. + +Full-screen Re-size Error +&Emacs; and gVim, when maximized (full-screen mode) and under certain conditions may encounter window re-sizing issues - see Emacs window resizes ... A &kwin; Rule will work-around the issue. + +Assuming an &Emacs; window is open: + + +Use Detect Window Properties and select the &Emacs; window. Match only by primary class name so leave the check boxes unchecked - for additional information see window matching + + + + + + + + + + + +Clicking OK in the previous window back-fills the results in the Window Matching tab. Enter a meaningful text in the Description text box: + + + + + + + + + + + +Ignore &Emacs;'s full-screen request by enabling the Obey geometry restrictions attribute, toggling it to off (No) to ignore and selecting the Force parameter: + + + + + + + + + + + +Click through to complete entry of the rule. + + + + + + +Credits and License + +Documentation Copyright see the UserBase + KWin Rules page history + +&underFDL; + +&documentation.index; + diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/knotes-attribute.png b/local/recipes/kde/kwin/source/doc/windowspecific/knotes-attribute.png new file mode 100644 index 0000000000..eeaf3ae931 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/knotes-attribute.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/knotes-info.png b/local/recipes/kde/kwin/source/doc/windowspecific/knotes-info.png new file mode 100644 index 0000000000..4769a1d9b2 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/knotes-info.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/kopete-attribute-2.png b/local/recipes/kde/kwin/source/doc/windowspecific/kopete-attribute-2.png new file mode 100644 index 0000000000..19a546e62c Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/kopete-attribute-2.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/kopete-chat-attribute.png b/local/recipes/kde/kwin/source/doc/windowspecific/kopete-chat-attribute.png new file mode 100644 index 0000000000..b6b2fa5a47 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/kopete-chat-attribute.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/kopete-chat-info.png b/local/recipes/kde/kwin/source/doc/windowspecific/kopete-chat-info.png new file mode 100644 index 0000000000..f704e7f649 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/kopete-chat-info.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/kopete-info.png b/local/recipes/kde/kwin/source/doc/windowspecific/kopete-info.png new file mode 100644 index 0000000000..7f09d42888 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/kopete-info.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/kwin-detect-window.png b/local/recipes/kde/kwin/source/doc/windowspecific/kwin-detect-window.png new file mode 100644 index 0000000000..b1efe5a87e Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/kwin-detect-window.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/kwin-kopete-rules.png b/local/recipes/kde/kwin/source/doc/windowspecific/kwin-kopete-rules.png new file mode 100644 index 0000000000..7fe6b18514 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/kwin-kopete-rules.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/kwin-rule-editor.png b/local/recipes/kde/kwin/source/doc/windowspecific/kwin-rule-editor.png new file mode 100644 index 0000000000..076091356a Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/kwin-rule-editor.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/kwin-rules-main-n-akregator.png b/local/recipes/kde/kwin/source/doc/windowspecific/kwin-rules-main-n-akregator.png new file mode 100644 index 0000000000..5b95582205 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/kwin-rules-main-n-akregator.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/kwin-rules-main.png b/local/recipes/kde/kwin/source/doc/windowspecific/kwin-rules-main.png new file mode 100644 index 0000000000..37b7303a29 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/kwin-rules-main.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/kwin-rules-ordering.png b/local/recipes/kde/kwin/source/doc/windowspecific/kwin-rules-ordering.png new file mode 100644 index 0000000000..1248b0149c Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/kwin-rules-ordering.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/kwin-window-attributes.png b/local/recipes/kde/kwin/source/doc/windowspecific/kwin-window-attributes.png new file mode 100644 index 0000000000..4bc0fe3068 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/kwin-window-attributes.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/kwin-window-matching.png b/local/recipes/kde/kwin/source/doc/windowspecific/kwin-window-matching.png new file mode 100644 index 0000000000..6aa1c475bc Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/kwin-window-matching.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/pager-4-desktops.png b/local/recipes/kde/kwin/source/doc/windowspecific/pager-4-desktops.png new file mode 100644 index 0000000000..9d1b26fe62 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/pager-4-desktops.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/tbird-compose-attribute.png b/local/recipes/kde/kwin/source/doc/windowspecific/tbird-compose-attribute.png new file mode 100644 index 0000000000..a33a010711 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/tbird-compose-attribute.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/tbird-compose-info.png b/local/recipes/kde/kwin/source/doc/windowspecific/tbird-compose-info.png new file mode 100644 index 0000000000..fae5d6b5c1 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/tbird-compose-info.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/tbird-main-attribute.png b/local/recipes/kde/kwin/source/doc/windowspecific/tbird-main-attribute.png new file mode 100644 index 0000000000..6260d5723f Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/tbird-main-attribute.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/tbird-main-info.png b/local/recipes/kde/kwin/source/doc/windowspecific/tbird-main-info.png new file mode 100644 index 0000000000..e0de5ea42d Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/tbird-main-info.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/tbird-reminder-attribute-2.png b/local/recipes/kde/kwin/source/doc/windowspecific/tbird-reminder-attribute-2.png new file mode 100644 index 0000000000..094892141d Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/tbird-reminder-attribute-2.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/tbird-reminder-info.png b/local/recipes/kde/kwin/source/doc/windowspecific/tbird-reminder-info.png new file mode 100644 index 0000000000..6bc2bee562 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/tbird-reminder-info.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-emacs.png b/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-emacs.png new file mode 100644 index 0000000000..d6dfcea97e Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-emacs.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-init.png b/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-init.png new file mode 100644 index 0000000000..338b959b39 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-init.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-knotes.png b/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-knotes.png new file mode 100644 index 0000000000..7fc4f8e750 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-knotes.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-kopete-chat.png b/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-kopete-chat.png new file mode 100644 index 0000000000..83061c46e1 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-kopete-chat.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-kopete.png b/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-kopete.png new file mode 100644 index 0000000000..34a6b4d62f Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-kopete.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-ready-akregator.png b/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-ready-akregator.png new file mode 100644 index 0000000000..f5b6c6bc05 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-ready-akregator.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-tbird-compose.png b/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-tbird-compose.png new file mode 100644 index 0000000000..62bde9fc65 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-tbird-compose.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-tbird-main.png b/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-tbird-main.png new file mode 100644 index 0000000000..e06019c947 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-tbird-main.png differ diff --git a/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-tbird-reminder.png b/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-tbird-reminder.png new file mode 100644 index 0000000000..d8e13c1da3 Binary files /dev/null and b/local/recipes/kde/kwin/source/doc/windowspecific/window-matching-tbird-reminder.png differ diff --git a/local/recipes/kde/kwin/source/examples/plugin/.gitignore b/local/recipes/kde/kwin/source/examples/plugin/.gitignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/plugin/.gitignore @@ -0,0 +1 @@ +build diff --git a/local/recipes/kde/kwin/source/examples/plugin/CMakeLists.txt b/local/recipes/kde/kwin/source/examples/plugin/CMakeLists.txt new file mode 100644 index 0000000000..5f4dce1b78 --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/plugin/CMakeLists.txt @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: None +# SPDX-License-Identifier: CC0-1.0 + +cmake_minimum_required(VERSION 3.20) +project(quick-effect) + +set(KF6_MIN_VERSION "6.0.0") + +find_package(ECM ${KF6_MIN_VERSION} REQUIRED NO_MODULE) +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) + +include(FeatureSummary) +include(KDEInstallDirs) +include(KDECMakeSettings) +include(KDECompilerSettings NO_POLICY_SCOPE) + +find_package(Qt6 CONFIG REQUIRED COMPONENTS + Core + Widgets +) + +find_package(KWin REQUIRED COMPONENTS + kwin +) + +kcoreaddons_add_plugin(eventlistener INSTALL_NAMESPACE "kwin/plugins") +target_sources(eventlistener PRIVATE + main.cpp + eventlistener.cpp +) + +target_link_libraries(eventlistener PRIVATE + KWin::kwin +) + +feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/local/recipes/kde/kwin/source/examples/plugin/eventlistener.cpp b/local/recipes/kde/kwin/source/examples/plugin/eventlistener.cpp new file mode 100644 index 0000000000..5a13b9e6b8 --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/plugin/eventlistener.cpp @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2024 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "eventlistener.h" +#include "kwin/input.h" +#include "kwin/input_event.h" + +#include + +namespace KWin +{ + +EventListener::EventListener() +{ + qDebug() << "Loaded demo event listener plugin"; + input()->installInputEventSpy(this); +} + +void EventListener::keyEvent(KeyEvent *event) +{ + qDebug() << event; +} + +void EventListener::pointerEvent(MouseEvent *event) +{ + qDebug() << event; +} + +} // namespace KWin + +#include "moc_eventlistener.cpp" diff --git a/local/recipes/kde/kwin/source/examples/plugin/eventlistener.h b/local/recipes/kde/kwin/source/examples/plugin/eventlistener.h new file mode 100644 index 0000000000..810b040ad2 --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/plugin/eventlistener.h @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2024 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include "kwin/plugin.h" +#include "kwin/input_event_spy.h" + +namespace KWin +{ + +class EventListener : public Plugin, public InputEventSpy +{ + Q_OBJECT + +public: + EventListener(); + + void keyEvent(KeyEvent *event) override; + void pointerEvent(MouseEvent *event) override; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/examples/plugin/main.cpp b/local/recipes/kde/kwin/source/examples/plugin/main.cpp new file mode 100644 index 0000000000..43d10c4885 --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/plugin/main.cpp @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2024 Vlad Zahorodnii + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "eventlistener.h" + +#include + +namespace KWin +{ + +class KWIN_EXPORT EventListenerFactory : public PluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID PluginFactory_iid FILE "metadata.json") + Q_INTERFACES(KWin::PluginFactory) + +public: + explicit EventListenerFactory() = default; + + std::unique_ptr create() const override + { + return std::make_unique(); + } +}; + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/examples/plugin/metadata.json b/local/recipes/kde/kwin/source/examples/plugin/metadata.json new file mode 100644 index 0000000000..aa304f4093 --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/plugin/metadata.json @@ -0,0 +1,5 @@ +{ + "KPlugin": { + "EnabledByDefault": true + } +} diff --git a/local/recipes/kde/kwin/source/examples/plugin/metadata.json.license b/local/recipes/kde/kwin/source/examples/plugin/metadata.json.license new file mode 100644 index 0000000000..cf53d2bce1 --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/plugin/metadata.json.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: None +SPDX-License-Identifier: CC0-1.0 diff --git a/local/recipes/kde/kwin/source/examples/quick-effect/.gitignore b/local/recipes/kde/kwin/source/examples/quick-effect/.gitignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/quick-effect/.gitignore @@ -0,0 +1 @@ +build diff --git a/local/recipes/kde/kwin/source/examples/quick-effect/CMakeLists.txt b/local/recipes/kde/kwin/source/examples/quick-effect/CMakeLists.txt new file mode 100644 index 0000000000..e4d2d9e67c --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/quick-effect/CMakeLists.txt @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: None +# SPDX-License-Identifier: CC0-1.0 + +cmake_minimum_required(VERSION 3.20) +project(quick-effect) + +set(KF6_MIN_VERSION "6.0.0") + +find_package(ECM ${KF6_MIN_VERSION} REQUIRED NO_MODULE) +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) + +find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS + Package +) + +kpackage_install_package(package quick-effect effects kwin) diff --git a/local/recipes/kde/kwin/source/examples/quick-effect/package/contents/config/main.xml b/local/recipes/kde/kwin/source/examples/quick-effect/package/contents/config/main.xml new file mode 100644 index 0000000000..47bbc2c975 --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/quick-effect/package/contents/config/main.xml @@ -0,0 +1,12 @@ + + + + + + #ff00ff + + + diff --git a/local/recipes/kde/kwin/source/examples/quick-effect/package/contents/config/main.xml.license b/local/recipes/kde/kwin/source/examples/quick-effect/package/contents/config/main.xml.license new file mode 100644 index 0000000000..cf53d2bce1 --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/quick-effect/package/contents/config/main.xml.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: None +SPDX-License-Identifier: CC0-1.0 diff --git a/local/recipes/kde/kwin/source/examples/quick-effect/package/contents/ui/config.ui b/local/recipes/kde/kwin/source/examples/quick-effect/package/contents/ui/config.ui new file mode 100644 index 0000000000..57ed22e80c --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/quick-effect/package/contents/ui/config.ui @@ -0,0 +1,39 @@ + + + QuickEffectConfig + + + + 0 + 0 + 455 + 177 + + + + + + + Background color: + + + + + + + false + + + + + + + + KColorButton + QPushButton +
kcolorbutton.h
+
+
+ + +
diff --git a/local/recipes/kde/kwin/source/examples/quick-effect/package/contents/ui/config.ui.license b/local/recipes/kde/kwin/source/examples/quick-effect/package/contents/ui/config.ui.license new file mode 100644 index 0000000000..cf53d2bce1 --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/quick-effect/package/contents/ui/config.ui.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: None +SPDX-License-Identifier: CC0-1.0 diff --git a/local/recipes/kde/kwin/source/examples/quick-effect/package/contents/ui/main.qml b/local/recipes/kde/kwin/source/examples/quick-effect/package/contents/ui/main.qml new file mode 100644 index 0000000000..e8b3907679 --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/quick-effect/package/contents/ui/main.qml @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: MIT +*/ + +import QtQuick +import org.kde.kwin + +SceneEffect { + id: effect + + delegate: Rectangle { + color: effect.configuration.BackgroundColor + + Text { + anchors.centerIn: parent + text: SceneView.screen.name + } + + MouseArea { + anchors.fill: parent + onClicked: effect.visible = false + } + } + + ScreenEdgeHandler { + enabled: true + edge: ScreenEdgeHandler.TopEdge + onActivated: effect.visible = !effect.visible + } + + ShortcutHandler { + name: "Toggle Quick Effect" + text: "Toggle Quick Effect" + sequence: "Meta+Ctrl+Q" + onActivated: effect.visible = !effect.visible + } + + PinchGestureHandler { + direction: PinchGestureHandler.Direction.Contracting + fingerCount: 3 + onActivated: effect.visible = !effect.visible + } +} diff --git a/local/recipes/kde/kwin/source/examples/quick-effect/package/metadata.json b/local/recipes/kde/kwin/source/examples/quick-effect/package/metadata.json new file mode 100644 index 0000000000..db4715e592 --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/quick-effect/package/metadata.json @@ -0,0 +1,20 @@ +{ + "KPackageStructure": "KWin/Effect", + "KPlugin": { + "Authors": [ + { + "Email": "user@example.com", + "Name": "Real Name" + } + ], + "Category": "Appearance", + "Description": "Quick Effect", + "EnabledByDefault": true, + "Id": "quick-effect", + "License": "MIT", + "Name": "Quick Effect" + }, + "X-KDE-ConfigModule": "kcm_kwin4_genericscripted", + "X-KDE-Ordering": 60, + "X-Plasma-API": "declarativescript" +} diff --git a/local/recipes/kde/kwin/source/examples/quick-effect/package/metadata.json.license b/local/recipes/kde/kwin/source/examples/quick-effect/package/metadata.json.license new file mode 100644 index 0000000000..cf53d2bce1 --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/quick-effect/package/metadata.json.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: None +SPDX-License-Identifier: CC0-1.0 diff --git a/local/recipes/kde/kwin/source/examples/quick-script/.gitignore b/local/recipes/kde/kwin/source/examples/quick-script/.gitignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/quick-script/.gitignore @@ -0,0 +1 @@ +build diff --git a/local/recipes/kde/kwin/source/examples/quick-script/CMakeLists.txt b/local/recipes/kde/kwin/source/examples/quick-script/CMakeLists.txt new file mode 100644 index 0000000000..daa5c27eb0 --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/quick-script/CMakeLists.txt @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: None +# SPDX-License-Identifier: CC0-1.0 + +cmake_minimum_required(VERSION 3.20) +project(quick-script) + +set(KF6_MIN_VERSION "6.0.0") + +find_package(ECM ${KF6_MIN_VERSION} REQUIRED NO_MODULE) +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) + +find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS + Package +) + +kpackage_install_package(package quick-script scripts kwin) diff --git a/local/recipes/kde/kwin/source/examples/quick-script/package/contents/ui/main.qml b/local/recipes/kde/kwin/source/examples/quick-script/package/contents/ui/main.qml new file mode 100644 index 0000000000..e8233fc5fe --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/quick-script/package/contents/ui/main.qml @@ -0,0 +1,94 @@ +/* + SPDX-FileCopyrightText: 2024 Vlad Zahorodnii + + SPDX-License-Identifier: MIT +*/ + +import QtQuick +import QtQuick.Window +import org.kde.kirigami as Kirigami +import org.kde.kwin as KWinComponents + +Window { + id: root + + readonly property int thumbnailWidth: Kirigami.Units.gridUnit * 10 + readonly property int thumbnailHeight: Kirigami.Units.gridUnit * 10 + readonly property bool _q_showWithoutActivating: true + + color: "transparent" + flags: Qt.BypassWindowManagerHint | Qt.FramelessWindowHint + visible: listModel.count > 0 + width: thumbnailWidth + height: Math.min(listModel.count * thumbnailHeight, Screen.height) + x: (Screen.virtualX + Screen.width) - width + y: Screen.virtualY + 0.5 * (Screen.height - height) + + ListView { + id: listView + anchors.fill: parent + model: listModel + opacity: 0.5 + delegate: KWinComponents.WindowThumbnail { + client: model.window + width: thumbnailWidth + height: thumbnailHeight + + TapHandler { + onTapped: KWinComponents.Workspace.activeWindow = client; + } + } + transform: Rotation { + axis { + x: 0 + y: 1 + z: 0 + } + origin { + x: width + y: height / 2 + } + angle: -30 + } + } + + ListModel { + id: listModel + } + + function toggle() { + const window = KWinComponents.Workspace.activeWindow; + if (!window) { + return; + } + + for (let i = 0; i < listModel.count; ++i) { + if (listModel.get(i)["window"] == window) { + listModel.remove(i); + return; + } + } + + listModel.append({"window": window}); + } + + function remove(window) { + for (let i = 0; i < listModel.count; ++i) { + const item = listModel.get(i); + if (item["window"] == window) { + listModel.remove(i); + } + } + } + + KWinComponents.ShortcutHandler { + name: "Toggle Current Thumbnail" + text: "Toggle Current Thumbnail" + sequence: "Meta+Ctrl+T" + onActivated: toggle() + } + + Component.onCompleted: { + KWinComponents.Workspace.windowRemoved.connect(remove); + } +} diff --git a/local/recipes/kde/kwin/source/examples/quick-script/package/metadata.json b/local/recipes/kde/kwin/source/examples/quick-script/package/metadata.json new file mode 100644 index 0000000000..1cd3aab1b4 --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/quick-script/package/metadata.json @@ -0,0 +1,18 @@ +{ + "KPackageStructure": "KWin/Script", + "KPlugin": { + "Authors": [ + { + "Email": "user@example.com", + "Name": "Your Name" + } + ], + "Description": "An example QtQuick script", + "EnabledByDefault": false, + "Icon": "preferences-system-windows-script-test", + "Id": "quick-script", + "License": "MIT", + "Name": "Quick Script" + }, + "X-Plasma-API": "declarativescript" +} diff --git a/local/recipes/kde/kwin/source/examples/quick-script/package/metadata.json.license b/local/recipes/kde/kwin/source/examples/quick-script/package/metadata.json.license new file mode 100644 index 0000000000..cf53d2bce1 --- /dev/null +++ b/local/recipes/kde/kwin/source/examples/quick-script/package/metadata.json.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: None +SPDX-License-Identifier: CC0-1.0 diff --git a/local/recipes/kde/kwin/source/kconf_update/CMakeLists.txt b/local/recipes/kde/kwin/source/kconf_update/CMakeLists.txt new file mode 100644 index 0000000000..e830d0c5f1 --- /dev/null +++ b/local/recipes/kde/kwin/source/kconf_update/CMakeLists.txt @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2023 Niccolò Venerandi +# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +add_executable(kwin-6.0-delete-desktop-switching-shortcuts) +target_sources(kwin-6.0-delete-desktop-switching-shortcuts PRIVATE kwin-6.0-delete-desktop-switching-shortcuts.cpp) +target_link_libraries(kwin-6.0-delete-desktop-switching-shortcuts PRIVATE KF6::GlobalAccel) +install(TARGETS kwin-6.0-delete-desktop-switching-shortcuts DESTINATION ${KDE_INSTALL_LIBDIR}/kconf_update_bin/) + +add_executable(kwin-6.0-reset-active-mouse-screen) +target_sources(kwin-6.0-reset-active-mouse-screen PRIVATE kwin-6.0-reset-active-mouse-screen.cpp) +target_link_libraries(kwin-6.0-reset-active-mouse-screen PRIVATE KF6::ConfigCore) +install(TARGETS kwin-6.0-reset-active-mouse-screen DESTINATION ${KDE_INSTALL_LIBDIR}/kconf_update_bin/) + +add_executable(kwin-6.0-remove-breeze-tabbox-default) +target_sources(kwin-6.0-remove-breeze-tabbox-default PRIVATE kwin-6.0-remove-breeze-tabbox-default.cpp) +target_link_libraries(kwin-6.0-remove-breeze-tabbox-default PRIVATE KF6::ConfigCore) +install(TARGETS kwin-6.0-remove-breeze-tabbox-default DESTINATION ${KDE_INSTALL_LIBDIR}/kconf_update_bin/) + +add_executable(kwin-6.1-remove-gridview-expose-shortcuts) +target_sources(kwin-6.1-remove-gridview-expose-shortcuts PRIVATE kwin-6.1-remove-gridview-expose-shortcuts.cpp) +target_link_libraries(kwin-6.1-remove-gridview-expose-shortcuts PRIVATE KF6::GlobalAccel) +install(TARGETS kwin-6.1-remove-gridview-expose-shortcuts DESTINATION ${KDE_INSTALL_LIBDIR}/kconf_update_bin/) + +add_executable(kwin-6.5-showpaint-changes) +target_sources(kwin-6.5-showpaint-changes PRIVATE kwin-6.5-showpaint-changes.cpp) +target_link_libraries(kwin-6.5-showpaint-changes PRIVATE KF6::ConfigCore KF6::GlobalAccel) +install(TARGETS kwin-6.5-showpaint-changes DESTINATION ${KDE_INSTALL_LIBDIR}/kconf_update_bin/) + +install(FILES kwin.upd + DESTINATION ${KDE_INSTALL_KCONFUPDATEDIR}) diff --git a/local/recipes/kde/kwin/source/kconf_update/kwin-6.0-delete-desktop-switching-shortcuts.cpp b/local/recipes/kde/kwin/source/kconf_update/kwin-6.0-delete-desktop-switching-shortcuts.cpp new file mode 100644 index 0000000000..5acff0ed5e --- /dev/null +++ b/local/recipes/kde/kwin/source/kconf_update/kwin-6.0-delete-desktop-switching-shortcuts.cpp @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2023 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include +#include +#include + +int main(int argc, char **argv) +{ + QGuiApplication app(argc, argv); + + const QStringList actionNames{ + QStringLiteral("Walk Through Desktops"), + QStringLiteral("Walk Through Desktops (Reverse)"), + QStringLiteral("Walk Through Desktop List"), + QStringLiteral("Walk Through Desktop List (Reverse)"), + }; + + for (const QString &actionName : actionNames) { + QAction action; + action.setObjectName(actionName); + action.setProperty("componentName", QStringLiteral("kwin")); + action.setProperty("componentDisplayName", QStringLiteral("KWin")); + KGlobalAccel::self()->setShortcut(&action, {QKeySequence()}, KGlobalAccel::NoAutoloading); + KGlobalAccel::self()->removeAllShortcuts(&action); + } + + return 0; +} diff --git a/local/recipes/kde/kwin/source/kconf_update/kwin-6.0-remove-breeze-tabbox-default.cpp b/local/recipes/kde/kwin/source/kconf_update/kwin-6.0-remove-breeze-tabbox-default.cpp new file mode 100644 index 0000000000..dd536ab0b3 --- /dev/null +++ b/local/recipes/kde/kwin/source/kconf_update/kwin-6.0-remove-breeze-tabbox-default.cpp @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2024 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include + +int main() +{ + KConfig config(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String("/kdedefaults/kwinrc"), KConfig::SimpleConfig); + + KConfigGroup windows = config.group(QStringLiteral("TabBox")); + bool needsSync = false; + + if (!windows.exists()) { + return EXIT_SUCCESS; + } + + if (windows.hasKey(QStringLiteral("LayoutName")) && windows.readEntry(QStringLiteral("LayoutName"), QString()) == QString("org.kde.breeze.desktop")) { + windows.deleteEntry(QStringLiteral("LayoutName")); + needsSync = true; + } + + if (windows.hasKey(QStringLiteral("DesktopListLayout"))) { + windows.deleteEntry(QStringLiteral("DesktopListLayout")); + needsSync = true; + } + if (windows.hasKey(QStringLiteral("DesktopLayout"))) { + windows.deleteEntry(QStringLiteral("DesktopLayout")); + needsSync = true; + } + + if (needsSync) { + return windows.sync() ? EXIT_SUCCESS : EXIT_FAILURE; + } else { + return EXIT_SUCCESS; + } +} diff --git a/local/recipes/kde/kwin/source/kconf_update/kwin-6.0-reset-active-mouse-screen.cpp b/local/recipes/kde/kwin/source/kconf_update/kwin-6.0-reset-active-mouse-screen.cpp new file mode 100644 index 0000000000..0efcc1dd60 --- /dev/null +++ b/local/recipes/kde/kwin/source/kconf_update/kwin-6.0-reset-active-mouse-screen.cpp @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2024 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include + +int main() +{ + auto config = KSharedConfig::openConfig(QStringLiteral("kwinrc")); + + KConfigGroup windows = config->group(QStringLiteral("Windows")); + if (!windows.exists()) { + return EXIT_SUCCESS; + } + + if (!windows.hasKey(QStringLiteral("ActiveMouseScreen"))) { + return EXIT_SUCCESS; + } + + windows.deleteEntry(QStringLiteral("ActiveMouseScreen")); + + return windows.sync() ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/local/recipes/kde/kwin/source/kconf_update/kwin-6.1-remove-gridview-expose-shortcuts.cpp b/local/recipes/kde/kwin/source/kconf_update/kwin-6.1-remove-gridview-expose-shortcuts.cpp new file mode 100644 index 0000000000..2568979c02 --- /dev/null +++ b/local/recipes/kde/kwin/source/kconf_update/kwin-6.1-remove-gridview-expose-shortcuts.cpp @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2023 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include + +#include +#include +#include + +int main(int argc, char **argv) +{ + QGuiApplication app(argc, argv); + + const QStringList actionNames{ + QStringLiteral("ShowDesktopGrid"), + QStringLiteral("Expose"), + QStringLiteral("ExposeAll"), + QStringLiteral("ExposeClass"), + QStringLiteral("ExposeClassCurrentDesktop"), + }; + + for (const QString &actionName : actionNames) { + QAction action; + action.setObjectName(actionName); + action.setProperty("componentName", QStringLiteral("kwin")); + action.setProperty("componentDisplayName", QStringLiteral("KWin")); + KGlobalAccel::self()->setShortcut(&action, {QKeySequence()}, KGlobalAccel::NoAutoloading); + KGlobalAccel::self()->removeAllShortcuts(&action); + } + + return 0; +} diff --git a/local/recipes/kde/kwin/source/kconf_update/kwin.upd b/local/recipes/kde/kwin/source/kconf_update/kwin.upd new file mode 100644 index 0000000000..61a694e39d --- /dev/null +++ b/local/recipes/kde/kwin/source/kconf_update/kwin.upd @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2023 Niccolò Venerandi +# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +Version=6 + +# Reset ActiveMouseScreen config option. +Id=kwin-6.0-reset-active-mouse-screen +Script=kwin-6.0-reset-active-mouse-screen + +# Delete old desktop switching shortcuts. +Id=kwin-6.0-delete-desktop-switching-shortcuts +Script=kwin-6.0-delete-desktop-switching-shortcuts + +# Delete old tabbox defaults +Id=kwin-6.0-remove-breeze-tabbox-default +Script=kwin-6.0-remove-breeze-tabbox-default + +# Delete old gridview and expose defaults +Id=kwin-6.1-remove-gridview-expose-shortcuts +Script=kwin-6.1-remove-gridview-expose-shortcuts + +# Unload showpaint effect and delete old shortcut +Id=kwin-6.5-showpaint-changes +Script=kwin-6.5-showpaint-changes diff --git a/local/recipes/kde/kwin/source/logo.png b/local/recipes/kde/kwin/source/logo.png new file mode 100644 index 0000000000..71b690c290 Binary files /dev/null and b/local/recipes/kde/kwin/source/logo.png differ diff --git a/local/recipes/kde/kwin/source/plasma-kwin_wayland.service.in b/local/recipes/kde/kwin/source/plasma-kwin_wayland.service.in new file mode 100644 index 0000000000..e04ba2ed9a --- /dev/null +++ b/local/recipes/kde/kwin/source/plasma-kwin_wayland.service.in @@ -0,0 +1,8 @@ +[Unit] +Description=KDE Wayland Compositor +PartOf=graphical-session.target + +[Service] +ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/kwin_wayland_wrapper --xwayland +BusName=org.kde.KWinWrapper +Slice=session.slice diff --git a/local/recipes/kde/kwin/source/src/3rdparty/colortemperature.h b/local/recipes/kde/kwin/source/src/3rdparty/colortemperature.h new file mode 100644 index 0000000000..3dc6cb63ca --- /dev/null +++ b/local/recipes/kde/kwin/source/src/3rdparty/colortemperature.h @@ -0,0 +1,289 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once +#include +#include + +#include "core/colorspace.h" + +namespace KWin +{ + +/** + * Whitepoint values for temperatures at 100K intervals. + * These will be interpolated for the actual temperature. + * This table was provided by Ingo Thies, 2013. + * See the following file for more information: + * https://github.com/jonls/redshift/blob/master/README-colorramp + */ +static const std::array s_blackbodyColor = { + QVector3D(1.00000000, 0.18172716, 0.00000000), /* 1000K */ + QVector3D(1.00000000, 0.25503671, 0.00000000), /* 1100K */ + QVector3D(1.00000000, 0.30942099, 0.00000000), /* 1200K */ + QVector3D(1.00000000, 0.35357379, 0.00000000), /* ... */ + QVector3D(1.00000000, 0.39091524, 0.00000000), + QVector3D(1.00000000, 0.42322816, 0.00000000), + QVector3D(1.00000000, 0.45159884, 0.00000000), + QVector3D(1.00000000, 0.47675916, 0.00000000), + QVector3D(1.00000000, 0.49923747, 0.00000000), + QVector3D(1.00000000, 0.51943421, 0.00000000), + QVector3D(1.00000000, 0.54360078, 0.08679949), /* 2000K */ + QVector3D(1.00000000, 0.56618736, 0.14065513), + QVector3D(1.00000000, 0.58734976, 0.18362641), + QVector3D(1.00000000, 0.60724493, 0.22137978), + QVector3D(1.00000000, 0.62600248, 0.25591950), + QVector3D(1.00000000, 0.64373109, 0.28819679), + QVector3D(1.00000000, 0.66052319, 0.31873863), + QVector3D(1.00000000, 0.67645822, 0.34786758), + QVector3D(1.00000000, 0.69160518, 0.37579588), + QVector3D(1.00000000, 0.70602449, 0.40267128), + QVector3D(1.00000000, 0.71976951, 0.42860152), /* 3000K */ + QVector3D(1.00000000, 0.73288760, 0.45366838), + QVector3D(1.00000000, 0.74542112, 0.47793608), + QVector3D(1.00000000, 0.75740814, 0.50145662), + QVector3D(1.00000000, 0.76888303, 0.52427322), + QVector3D(1.00000000, 0.77987699, 0.54642268), + QVector3D(1.00000000, 0.79041843, 0.56793692), + QVector3D(1.00000000, 0.80053332, 0.58884417), + QVector3D(1.00000000, 0.81024551, 0.60916971), + QVector3D(1.00000000, 0.81957693, 0.62893653), + QVector3D(1.00000000, 0.82854786, 0.64816570), /* 4000K */ + QVector3D(1.00000000, 0.83717703, 0.66687674), + QVector3D(1.00000000, 0.84548188, 0.68508786), + QVector3D(1.00000000, 0.85347859, 0.70281616), + QVector3D(1.00000000, 0.86118227, 0.72007777), + QVector3D(1.00000000, 0.86860704, 0.73688797), /* 4500K */ + QVector3D(1.00000000, 0.87576611, 0.75326132), + QVector3D(1.00000000, 0.88267187, 0.76921169), + QVector3D(1.00000000, 0.88933596, 0.78475236), + QVector3D(1.00000000, 0.89576933, 0.79989606), + QVector3D(1.00000000, 0.90198230, 0.81465502), /* 5000K */ + QVector3D(1.00000000, 0.90963069, 0.82838210), + QVector3D(1.00000000, 0.91710889, 0.84190889), + QVector3D(1.00000000, 0.92441842, 0.85523742), + QVector3D(1.00000000, 0.93156127, 0.86836903), + QVector3D(1.00000000, 0.93853986, 0.88130458), + QVector3D(1.00000000, 0.94535695, 0.89404470), + QVector3D(1.00000000, 0.95201559, 0.90658983), + QVector3D(1.00000000, 0.95851906, 0.91894041), + QVector3D(1.00000000, 0.96487079, 0.93109690), + QVector3D(1.00000000, 0.97107439, 0.94305985), /* 6000K */ + QVector3D(1.00000000, 0.97713351, 0.95482993), + QVector3D(1.00000000, 0.98305189, 0.96640795), + QVector3D(1.00000000, 0.98883326, 0.97779486), + QVector3D(1.00000000, 0.99448139, 0.98899179), + QVector3D(1.00000000, 1.00000000, 1.00000000), /* 6500K */ + QVector3D(0.98947904, 0.99348723, 1.00000000), + QVector3D(0.97940448, 0.98722715, 1.00000000), + QVector3D(0.96975025, 0.98120637, 1.00000000), + QVector3D(0.96049223, 0.97541240, 1.00000000), + QVector3D(0.95160805, 0.96983355, 1.00000000), /* 7000K */ + QVector3D(0.94303638, 0.96443333, 1.00000000), + QVector3D(0.93480451, 0.95923080, 1.00000000), + QVector3D(0.92689056, 0.95421394, 1.00000000), + QVector3D(0.91927697, 0.94937330, 1.00000000), + QVector3D(0.91194747, 0.94470005, 1.00000000), + QVector3D(0.90488690, 0.94018594, 1.00000000), + QVector3D(0.89808115, 0.93582323, 1.00000000), + QVector3D(0.89151710, 0.93160469, 1.00000000), + QVector3D(0.88518247, 0.92752354, 1.00000000), + QVector3D(0.87906581, 0.92357340, 1.00000000), /* 8000K */ + QVector3D(0.87315640, 0.91974827, 1.00000000), + QVector3D(0.86744421, 0.91604254, 1.00000000), + QVector3D(0.86191983, 0.91245088, 1.00000000), + QVector3D(0.85657444, 0.90896831, 1.00000000), + QVector3D(0.85139976, 0.90559011, 1.00000000), + QVector3D(0.84638799, 0.90231183, 1.00000000), + QVector3D(0.84153180, 0.89912926, 1.00000000), + QVector3D(0.83682430, 0.89603843, 1.00000000), + QVector3D(0.83225897, 0.89303558, 1.00000000), + QVector3D(0.82782969, 0.89011714, 1.00000000), /* 9000K */ + QVector3D(0.82353066, 0.88727974, 1.00000000), + QVector3D(0.81935641, 0.88452017, 1.00000000), + QVector3D(0.81530175, 0.88183541, 1.00000000), + QVector3D(0.81136180, 0.87922257, 1.00000000), + QVector3D(0.80753191, 0.87667891, 1.00000000), + QVector3D(0.80380769, 0.87420182, 1.00000000), + QVector3D(0.80018497, 0.87178882, 1.00000000), + QVector3D(0.79665980, 0.86943756, 1.00000000), + QVector3D(0.79322843, 0.86714579, 1.00000000), + QVector3D(0.78988728, 0.86491137, 1.00000000), /* 10000K */ + QVector3D(0.78663296, 0.86273225, 1.00000000), + QVector3D(0.78346225, 0.86060650, 1.00000000), + QVector3D(0.78037207, 0.85853224, 1.00000000), + QVector3D(0.77735950, 0.85650771, 1.00000000), + QVector3D(0.77442176, 0.85453121, 1.00000000), + QVector3D(0.77155617, 0.85260112, 1.00000000), + QVector3D(0.76876022, 0.85071588, 1.00000000), + QVector3D(0.76603147, 0.84887402, 1.00000000), + QVector3D(0.76336762, 0.84707411, 1.00000000), + QVector3D(0.76076645, 0.84531479, 1.00000000), /* 11000K */ + QVector3D(0.75822586, 0.84359476, 1.00000000), + QVector3D(0.75574383, 0.84191277, 1.00000000), + QVector3D(0.75331843, 0.84026762, 1.00000000), + QVector3D(0.75094780, 0.83865816, 1.00000000), + QVector3D(0.74863017, 0.83708329, 1.00000000), + QVector3D(0.74636386, 0.83554194, 1.00000000), + QVector3D(0.74414722, 0.83403311, 1.00000000), + QVector3D(0.74197871, 0.83255582, 1.00000000), + QVector3D(0.73985682, 0.83110912, 1.00000000), + QVector3D(0.73778012, 0.82969211, 1.00000000), /* 12000K */ + QVector3D(0.73574723, 0.82830393, 1.00000000), + QVector3D(0.73375683, 0.82694373, 1.00000000), + QVector3D(0.73180765, 0.82561071, 1.00000000), + QVector3D(0.72989845, 0.82430410, 1.00000000), + QVector3D(0.72802807, 0.82302316, 1.00000000), + QVector3D(0.72619537, 0.82176715, 1.00000000), + QVector3D(0.72439927, 0.82053539, 1.00000000), + QVector3D(0.72263872, 0.81932722, 1.00000000), + QVector3D(0.72091270, 0.81814197, 1.00000000), + QVector3D(0.71922025, 0.81697905, 1.00000000), /* 13000K */ + QVector3D(0.71756043, 0.81583783, 1.00000000), + QVector3D(0.71593234, 0.81471775, 1.00000000), + QVector3D(0.71433510, 0.81361825, 1.00000000), + QVector3D(0.71276788, 0.81253878, 1.00000000), + QVector3D(0.71122987, 0.81147883, 1.00000000), + QVector3D(0.70972029, 0.81043789, 1.00000000), + QVector3D(0.70823838, 0.80941546, 1.00000000), + QVector3D(0.70678342, 0.80841109, 1.00000000), + QVector3D(0.70535469, 0.80742432, 1.00000000), + QVector3D(0.70395153, 0.80645469, 1.00000000), /* 14000K */ + QVector3D(0.70257327, 0.80550180, 1.00000000), + QVector3D(0.70121928, 0.80456522, 1.00000000), + QVector3D(0.69988894, 0.80364455, 1.00000000), + QVector3D(0.69858167, 0.80273941, 1.00000000), + QVector3D(0.69729688, 0.80184943, 1.00000000), + QVector3D(0.69603402, 0.80097423, 1.00000000), + QVector3D(0.69479255, 0.80011347, 1.00000000), + QVector3D(0.69357196, 0.79926681, 1.00000000), + QVector3D(0.69237173, 0.79843391, 1.00000000), + QVector3D(0.69119138, 0.79761446, 1.00000000), /* 15000K */ + QVector3D(0.69003044, 0.79680814, 1.00000000), + QVector3D(0.68888844, 0.79601466, 1.00000000), + QVector3D(0.68776494, 0.79523371, 1.00000000), + QVector3D(0.68665951, 0.79446502, 1.00000000), + QVector3D(0.68557173, 0.79370830, 1.00000000), + QVector3D(0.68450119, 0.79296330, 1.00000000), + QVector3D(0.68344751, 0.79222975, 1.00000000), + QVector3D(0.68241029, 0.79150740, 1.00000000), + QVector3D(0.68138918, 0.79079600, 1.00000000), + QVector3D(0.68038380, 0.79009531, 1.00000000), /* 16000K */ + QVector3D(0.67939381, 0.78940511, 1.00000000), + QVector3D(0.67841888, 0.78872517, 1.00000000), + QVector3D(0.67745866, 0.78805526, 1.00000000), + QVector3D(0.67651284, 0.78739518, 1.00000000), + QVector3D(0.67558112, 0.78674472, 1.00000000), + QVector3D(0.67466317, 0.78610368, 1.00000000), + QVector3D(0.67375872, 0.78547186, 1.00000000), + QVector3D(0.67286748, 0.78484907, 1.00000000), + QVector3D(0.67198916, 0.78423512, 1.00000000), + QVector3D(0.67112350, 0.78362984, 1.00000000), /* 17000K */ + QVector3D(0.67027024, 0.78303305, 1.00000000), + QVector3D(0.66942911, 0.78244457, 1.00000000), + QVector3D(0.66859988, 0.78186425, 1.00000000), + QVector3D(0.66778228, 0.78129191, 1.00000000), + QVector3D(0.66697610, 0.78072740, 1.00000000), + QVector3D(0.66618110, 0.78017057, 1.00000000), + QVector3D(0.66539706, 0.77962127, 1.00000000), + QVector3D(0.66462376, 0.77907934, 1.00000000), + QVector3D(0.66386098, 0.77854465, 1.00000000), + QVector3D(0.66310852, 0.77801705, 1.00000000), /* 18000K */ + QVector3D(0.66236618, 0.77749642, 1.00000000), + QVector3D(0.66163375, 0.77698261, 1.00000000), + QVector3D(0.66091106, 0.77647551, 1.00000000), + QVector3D(0.66019791, 0.77597498, 1.00000000), + QVector3D(0.65949412, 0.77548090, 1.00000000), + QVector3D(0.65879952, 0.77499315, 1.00000000), + QVector3D(0.65811392, 0.77451161, 1.00000000), + QVector3D(0.65743716, 0.77403618, 1.00000000), + QVector3D(0.65676908, 0.77356673, 1.00000000), + QVector3D(0.65610952, 0.77310316, 1.00000000), /* 19000K */ + QVector3D(0.65545831, 0.77264537, 1.00000000), + QVector3D(0.65481530, 0.77219324, 1.00000000), + QVector3D(0.65418036, 0.77174669, 1.00000000), + QVector3D(0.65355332, 0.77130560, 1.00000000), + QVector3D(0.65293404, 0.77086988, 1.00000000), + QVector3D(0.65232240, 0.77043944, 1.00000000), + QVector3D(0.65171824, 0.77001419, 1.00000000), + QVector3D(0.65112144, 0.76959404, 1.00000000), + QVector3D(0.65053187, 0.76917889, 1.00000000), + QVector3D(0.64994941, 0.76876866, 1.00000000), /* 20000K */ + QVector3D(0.64937392, 0.76836326, 1.00000000), + QVector3D(0.64880528, 0.76796263, 1.00000000), + QVector3D(0.64824339, 0.76756666, 1.00000000), + QVector3D(0.64768812, 0.76717529, 1.00000000), + QVector3D(0.64713935, 0.76678844, 1.00000000), + QVector3D(0.64659699, 0.76640603, 1.00000000), + QVector3D(0.64606092, 0.76602798, 1.00000000), + QVector3D(0.64553103, 0.76565424, 1.00000000), + QVector3D(0.64500722, 0.76528472, 1.00000000), + QVector3D(0.64448939, 0.76491935, 1.00000000), /* 21000K */ + QVector3D(0.64397745, 0.76455808, 1.00000000), + QVector3D(0.64347129, 0.76420082, 1.00000000), + QVector3D(0.64297081, 0.76384753, 1.00000000), + QVector3D(0.64247594, 0.76349813, 1.00000000), + QVector3D(0.64198657, 0.76315256, 1.00000000), + QVector3D(0.64150261, 0.76281076, 1.00000000), + QVector3D(0.64102399, 0.76247267, 1.00000000), + QVector3D(0.64055061, 0.76213824, 1.00000000), + QVector3D(0.64008239, 0.76180740, 1.00000000), + QVector3D(0.63961926, 0.76148010, 1.00000000), /* 22000K */ + QVector3D(0.63916112, 0.76115628, 1.00000000), + QVector3D(0.63870790, 0.76083590, 1.00000000), + QVector3D(0.63825953, 0.76051890, 1.00000000), + QVector3D(0.63781592, 0.76020522, 1.00000000), + QVector3D(0.63737701, 0.75989482, 1.00000000), + QVector3D(0.63694273, 0.75958764, 1.00000000), + QVector3D(0.63651299, 0.75928365, 1.00000000), + QVector3D(0.63608774, 0.75898278, 1.00000000), + QVector3D(0.63566691, 0.75868499, 1.00000000), + QVector3D(0.63525042, 0.75839025, 1.00000000), /* 23000K */ + QVector3D(0.63483822, 0.75809849, 1.00000000), + QVector3D(0.63443023, 0.75780969, 1.00000000), + QVector3D(0.63402641, 0.75752379, 1.00000000), + QVector3D(0.63362667, 0.75724075, 1.00000000), + QVector3D(0.63323097, 0.75696053, 1.00000000), + QVector3D(0.63283925, 0.75668310, 1.00000000), + QVector3D(0.63245144, 0.75640840, 1.00000000), + QVector3D(0.63206749, 0.75613641, 1.00000000), + QVector3D(0.63168735, 0.75586707, 1.00000000), + QVector3D(0.63131096, 0.75560036, 1.00000000), /* 24000K */ + QVector3D(0.63093826, 0.75533624, 1.00000000), + QVector3D(0.63056920, 0.75507467, 1.00000000), + QVector3D(0.63020374, 0.75481562, 1.00000000), + QVector3D(0.62984181, 0.75455904, 1.00000000), + QVector3D(0.62948337, 0.75430491, 1.00000000), + QVector3D(0.62912838, 0.75405319, 1.00000000), + QVector3D(0.62877678, 0.75380385, 1.00000000), + QVector3D(0.62842852, 0.75355685, 1.00000000), + QVector3D(0.62808356, 0.75331217, 1.00000000), + QVector3D(0.62774186, 0.75306977, 1.00000000), /* 25000K */ + QVector3D(0.62740336, 0.75282962, 1.00000000), +}; + +static QVector3D sampleColorTemperature(int temperature) +{ + if (temperature >= 6500) { + // TODO also support color temperatures above 6500K? + return QVector3D(1, 1, 1); + } else { + // Note that cmsWhitePointFromTemp() returns a slightly green-ish white point. + const int blackBodyColorIndex = (temperature - 1000) / 100; + const qreal blendFactor = (temperature % 100) / 100.0; + + const QVector3D &low = s_blackbodyColor[blackBodyColorIndex]; + const QVector3D &high = s_blackbodyColor[blackBodyColorIndex + 1]; + const QVector3D whitePoint = low * (1 - blendFactor) + high * blendFactor; + // the values in the blackbodyColor array are "gamma corrected", but we need a linear value + return TransferFunction(TransferFunction::gamma22, 0, 1).encodedToNits(whitePoint); + } +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/3rdparty/xcursor.c b/local/recipes/kde/kwin/source/src/3rdparty/xcursor.c new file mode 100644 index 0000000000..436f124665 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/3rdparty/xcursor.c @@ -0,0 +1,519 @@ +/* + * Copyright © 2002 Keith Packard + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#define _DEFAULT_SOURCE +#include "xcursor.h" +#include +#include +#include +#include + +/* + * From libXcursor/include/X11/extensions/Xcursor.h + */ + +#define XcursorTrue 1 +#define XcursorFalse 0 + +/* + * Cursor files start with a header. The header + * contains a magic number, a version number and a + * table of contents which has type and offset information + * for the remaining tables in the file. + * + * File minor versions increment for compatible changes + * File major versions increment for incompatible changes (never, we hope) + * + * Chunks of the same type are always upward compatible. Incompatible + * changes are made with new chunk types; the old data can remain under + * the old type. Upward compatible changes can add header data as the + * header lengths are specified in the file. + * + * File: + * FileHeader + * LISTofChunk + * + * FileHeader: + * CARD32 magic magic number + * CARD32 header bytes in file header + * CARD32 version file version + * CARD32 ntoc number of toc entries + * LISTofFileToc toc table of contents + * + * FileToc: + * CARD32 type entry type + * CARD32 subtype entry subtype (size for images) + * CARD32 position absolute file position + */ + +#define XCURSOR_MAGIC 0x72756358 /* "Xcur" LSBFirst */ + +/* + * Current Xcursor version number. Will be substituted by configure + * from the version in the libXcursor configure.ac file. + */ + +#define XCURSOR_LIB_MAJOR 1 +#define XCURSOR_LIB_MINOR 1 +#define XCURSOR_LIB_REVISION 13 +#define XCURSOR_LIB_VERSION ((XCURSOR_LIB_MAJOR * 10000) + (XCURSOR_LIB_MINOR * 100) + (XCURSOR_LIB_REVISION)) + +/* + * This version number is stored in cursor files; changes to the + * file format require updating this version number + */ +#define XCURSOR_FILE_MAJOR 1 +#define XCURSOR_FILE_MINOR 0 +#define XCURSOR_FILE_VERSION ((XCURSOR_FILE_MAJOR << 16) | (XCURSOR_FILE_MINOR)) +#define XCURSOR_FILE_HEADER_LEN (4 * 4) +#define XCURSOR_FILE_TOC_LEN (3 * 4) + +typedef struct _XcursorFileToc +{ + XcursorUInt type; /* chunk type */ + XcursorUInt subtype; /* subtype (size for images) */ + XcursorUInt position; /* absolute position in file */ +} XcursorFileToc; + +typedef struct _XcursorFileHeader +{ + XcursorUInt magic; /* magic number */ + XcursorUInt header; /* byte length of header */ + XcursorUInt version; /* file version number */ + XcursorUInt ntoc; /* number of toc entries */ + XcursorFileToc *tocs; /* table of contents */ +} XcursorFileHeader; + +/* + * The rest of the file is a list of chunks, each tagged by type + * and version. + * + * Chunk: + * ChunkHeader + * + * + * + * ChunkHeader: + * CARD32 header bytes in chunk header + type header + * CARD32 type chunk type + * CARD32 subtype chunk subtype + * CARD32 version chunk type version + */ + +#define XCURSOR_CHUNK_HEADER_LEN (4 * 4) + +typedef struct _XcursorChunkHeader +{ + XcursorUInt header; /* bytes in chunk header */ + XcursorUInt type; /* chunk type */ + XcursorUInt subtype; /* chunk subtype (size for images) */ + XcursorUInt version; /* version of this type */ +} XcursorChunkHeader; + +/* + * Here's a list of the known chunk types + */ + +/* + * Comments consist of a 4-byte length field followed by + * UTF-8 encoded text + * + * Comment: + * ChunkHeader header chunk header + * CARD32 length bytes in text + * LISTofCARD8 text UTF-8 encoded text + */ + +#define XCURSOR_COMMENT_TYPE 0xfffe0001 +#define XCURSOR_COMMENT_VERSION 1 +#define XCURSOR_COMMENT_HEADER_LEN (XCURSOR_CHUNK_HEADER_LEN + (1 * 4)) +#define XCURSOR_COMMENT_COPYRIGHT 1 +#define XCURSOR_COMMENT_LICENSE 2 +#define XCURSOR_COMMENT_OTHER 3 +#define XCURSOR_COMMENT_MAX_LEN 0x100000 + +typedef struct _XcursorComment +{ + XcursorUInt version; + XcursorUInt comment_type; + char *comment; +} XcursorComment; + +/* + * Each cursor image occupies a separate image chunk. + * The length of the image header follows the chunk header + * so that future versions can extend the header without + * breaking older applications + * + * Image: + * ChunkHeader header chunk header + * CARD32 width actual width + * CARD32 height actual height + * CARD32 xhot hot spot x + * CARD32 yhot hot spot y + * CARD32 delay animation delay + * LISTofCARD32 pixels ARGB pixels + */ + +#define XCURSOR_IMAGE_TYPE 0xfffd0002 +#define XCURSOR_IMAGE_VERSION 1 +#define XCURSOR_IMAGE_HEADER_LEN (XCURSOR_CHUNK_HEADER_LEN + (5 * 4)) +#define XCURSOR_IMAGE_MAX_SIZE 0x7fff /* 32767x32767 max cursor size */ + +typedef struct _XcursorComments +{ + int ncomment; /* number of comments */ + XcursorComment **comments; /* array of XcursorComment pointers */ +} XcursorComments; + +/* + * From libXcursor/src/file.c + */ + +static XcursorImage * +XcursorImageCreate(int width, int height) +{ + XcursorImage *image; + + if (width < 0 || height < 0) + return NULL; + if (width > XCURSOR_IMAGE_MAX_SIZE || height > XCURSOR_IMAGE_MAX_SIZE) + return NULL; + + image = malloc(sizeof(XcursorImage) + width * height * sizeof(XcursorPixel)); + if (!image) + return NULL; + image->version = XCURSOR_IMAGE_VERSION; + image->pixels = (XcursorPixel *)(image + 1); + image->size = width > height ? width : height; + image->width = width; + image->height = height; + image->delay = 0; + return image; +} + +static void +XcursorImageDestroy(XcursorImage *image) +{ + free(image); +} + +static XcursorImages * +XcursorImagesCreate(int size) +{ + XcursorImages *images; + + images = malloc(sizeof(XcursorImages) + size * sizeof(XcursorImage *)); + if (!images) + return NULL; + images->nimage = 0; + images->images = (XcursorImage **)(images + 1); + return images; +} + +void XcursorImagesDestroy(XcursorImages *images) +{ + int n; + + if (!images) + return; + + for (n = 0; n < images->nimage; n++) + XcursorImageDestroy(images->images[n]); + free(images); +} + +static XcursorBool +_XcursorReadUInt(XcursorFile *file, XcursorUInt *u) +{ + uint8_t bytes[4]; + + if (!file || !u) + return XcursorFalse; + + if ((*file->read)(file, bytes, 4) != 4) + return XcursorFalse; + + *u = ((XcursorUInt)(bytes[0]) << 0) | ((XcursorUInt)(bytes[1]) << 8) | ((XcursorUInt)(bytes[2]) << 16) | ((XcursorUInt)(bytes[3]) << 24); + return XcursorTrue; +} + +static void +_XcursorFileHeaderDestroy(XcursorFileHeader *fileHeader) +{ + free(fileHeader); +} + +static XcursorFileHeader * +_XcursorFileHeaderCreate(XcursorUInt ntoc) +{ + XcursorFileHeader *fileHeader; + + if (ntoc > 0x10000) + return NULL; + fileHeader = malloc(sizeof(XcursorFileHeader) + ntoc * sizeof(XcursorFileToc)); + if (!fileHeader) + return NULL; + fileHeader->magic = XCURSOR_MAGIC; + fileHeader->header = XCURSOR_FILE_HEADER_LEN; + fileHeader->version = XCURSOR_FILE_VERSION; + fileHeader->ntoc = ntoc; + fileHeader->tocs = (XcursorFileToc *)(fileHeader + 1); + return fileHeader; +} + +static XcursorFileHeader * +_XcursorReadFileHeader(XcursorFile *file) +{ + XcursorFileHeader head, *fileHeader; + XcursorUInt skip; + unsigned int n; + + if (!file) + return NULL; + + if (!_XcursorReadUInt(file, &head.magic)) + return NULL; + if (head.magic != XCURSOR_MAGIC) + return NULL; + if (!_XcursorReadUInt(file, &head.header)) + return NULL; + if (!_XcursorReadUInt(file, &head.version)) + return NULL; + if (!_XcursorReadUInt(file, &head.ntoc)) + return NULL; + skip = head.header - XCURSOR_FILE_HEADER_LEN; + if (skip) + if (!(*file->skip)(file, skip)) + return NULL; + fileHeader = _XcursorFileHeaderCreate(head.ntoc); + if (!fileHeader) + return NULL; + fileHeader->magic = head.magic; + fileHeader->header = head.header; + fileHeader->version = head.version; + fileHeader->ntoc = head.ntoc; + for (n = 0; n < fileHeader->ntoc; n++) { + if (!_XcursorReadUInt(file, &fileHeader->tocs[n].type)) + break; + if (!_XcursorReadUInt(file, &fileHeader->tocs[n].subtype)) + break; + if (!_XcursorReadUInt(file, &fileHeader->tocs[n].position)) + break; + } + if (n != fileHeader->ntoc) { + _XcursorFileHeaderDestroy(fileHeader); + return NULL; + } + return fileHeader; +} + +static XcursorBool +_XcursorSeekToToc(XcursorFile *file, + XcursorFileHeader *fileHeader, + int toc) +{ + if (!file || !fileHeader) + return XcursorFalse; + return (*file->seek)(file, fileHeader->tocs[toc].position); +} + +static XcursorBool +_XcursorFileReadChunkHeader(XcursorFile *file, + XcursorFileHeader *fileHeader, + int toc, + XcursorChunkHeader *chunkHeader) +{ + if (!file || !fileHeader || !chunkHeader) + return XcursorFalse; + if (!_XcursorSeekToToc(file, fileHeader, toc)) + return XcursorFalse; + if (!_XcursorReadUInt(file, &chunkHeader->header)) + return XcursorFalse; + if (!_XcursorReadUInt(file, &chunkHeader->type)) + return XcursorFalse; + if (!_XcursorReadUInt(file, &chunkHeader->subtype)) + return XcursorFalse; + if (!_XcursorReadUInt(file, &chunkHeader->version)) + return XcursorFalse; + /* sanity check */ + if (chunkHeader->type != fileHeader->tocs[toc].type || chunkHeader->subtype != fileHeader->tocs[toc].subtype) + return XcursorFalse; + return XcursorTrue; +} + +#define dist(a, b) ((a) > (b) ? (a) - (b) : (b) - (a)) + +static XcursorDim +_XcursorFindBestSize(XcursorFileHeader *fileHeader, + XcursorDim size, + int *nsizesp) +{ + unsigned int n; + int nsizes = 0; + XcursorDim bestSize = 0; + XcursorDim thisSize; + + if (!fileHeader || !nsizesp) + return 0; + + for (n = 0; n < fileHeader->ntoc; n++) { + if (fileHeader->tocs[n].type != XCURSOR_IMAGE_TYPE) + continue; + thisSize = fileHeader->tocs[n].subtype; + if (!bestSize || dist(thisSize, size) < dist(bestSize, size)) { + bestSize = thisSize; + nsizes = 1; + } else if (thisSize == bestSize) + nsizes++; + } + *nsizesp = nsizes; + return bestSize; +} + +static int +_XcursorFindImageToc(XcursorFileHeader *fileHeader, + XcursorDim size, + int count) +{ + unsigned int toc; + XcursorDim thisSize; + + if (!fileHeader) + return 0; + + for (toc = 0; toc < fileHeader->ntoc; toc++) { + if (fileHeader->tocs[toc].type != XCURSOR_IMAGE_TYPE) + continue; + thisSize = fileHeader->tocs[toc].subtype; + if (thisSize != size) + continue; + if (!count) + break; + count--; + } + if (toc == fileHeader->ntoc) + return -1; + return toc; +} + +static XcursorImage * +_XcursorReadImage(XcursorFile *file, + XcursorFileHeader *fileHeader, + int toc) +{ + XcursorChunkHeader chunkHeader; + XcursorImage head; + XcursorImage *image; + int n; + XcursorPixel *p; + + if (!file || !fileHeader) + return NULL; + + if (!_XcursorFileReadChunkHeader(file, fileHeader, toc, &chunkHeader)) + return NULL; + if (!_XcursorReadUInt(file, &head.width)) + return NULL; + if (!_XcursorReadUInt(file, &head.height)) + return NULL; + if (!_XcursorReadUInt(file, &head.xhot)) + return NULL; + if (!_XcursorReadUInt(file, &head.yhot)) + return NULL; + if (!_XcursorReadUInt(file, &head.delay)) + return NULL; + /* sanity check data */ + if (head.width > XCURSOR_IMAGE_MAX_SIZE || head.height > XCURSOR_IMAGE_MAX_SIZE) + return NULL; + if (head.width == 0 || head.height == 0) + return NULL; + if (head.xhot > head.width || head.yhot > head.height) + return NULL; + + /* Create the image and initialize it */ + image = XcursorImageCreate(head.width, head.height); + if (image == NULL) + return NULL; + if (chunkHeader.version < image->version) + image->version = chunkHeader.version; + image->size = chunkHeader.subtype; + image->xhot = head.xhot; + image->yhot = head.yhot; + image->delay = head.delay; + n = image->width * image->height; + p = image->pixels; + while (n--) { + if (!_XcursorReadUInt(file, p)) { + XcursorImageDestroy(image); + return NULL; + } + p++; + } + return image; +} + +XcursorImages * +XcursorXcFileLoadImages(XcursorFile *file, int size) +{ + XcursorFileHeader *fileHeader; + XcursorDim bestSize; + int nsize; + XcursorImages *images; + int n; + int toc; + + if (!file || size < 0) + return NULL; + fileHeader = _XcursorReadFileHeader(file); + if (!fileHeader) + return NULL; + bestSize = _XcursorFindBestSize(fileHeader, (XcursorDim)size, &nsize); + if (!bestSize) { + _XcursorFileHeaderDestroy(fileHeader); + return NULL; + } + images = XcursorImagesCreate(nsize); + if (!images) { + _XcursorFileHeaderDestroy(fileHeader); + return NULL; + } + for (n = 0; n < nsize; n++) { + toc = _XcursorFindImageToc(fileHeader, bestSize, n); + if (toc < 0) + break; + images->images[images->nimage] = _XcursorReadImage(file, fileHeader, + toc); + if (!images->images[images->nimage]) + break; + images->nimage++; + } + _XcursorFileHeaderDestroy(fileHeader); + if (images->nimage != nsize) { + XcursorImagesDestroy(images); + images = NULL; + } + return images; +} diff --git a/local/recipes/kde/kwin/source/src/3rdparty/xcursor.h b/local/recipes/kde/kwin/source/src/3rdparty/xcursor.h new file mode 100644 index 0000000000..d4c689e9e8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/3rdparty/xcursor.h @@ -0,0 +1,81 @@ +/* + * Copyright © 2002 Keith Packard + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice (including the + * next paragraph) shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef XCURSOR_H +#define XCURSOR_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef int XcursorBool; +typedef uint32_t XcursorUInt; + +typedef XcursorUInt XcursorDim; +typedef XcursorUInt XcursorPixel; + +typedef struct _XcursorImage +{ + XcursorUInt version; /* version of the image data */ + XcursorDim size; /* nominal size for matching */ + XcursorDim width; /* actual width */ + XcursorDim height; /* actual height */ + XcursorDim xhot; /* hot spot x (must be inside image) */ + XcursorDim yhot; /* hot spot y (must be inside image) */ + XcursorUInt delay; /* animation delay to next frame (ms) */ + XcursorPixel *pixels; /* pointer to pixels */ +} XcursorImage; + +/* + * Other data structures exposed by the library API + */ +typedef struct _XcursorImages +{ + int nimage; /* number of images */ + XcursorImage **images; /* array of XcursorImage pointers */ +} XcursorImages; + +typedef struct _XcursorFile XcursorFile; + +struct _XcursorFile +{ + void *closure; + int (*read)(XcursorFile *file, uint8_t *buf, int len); + XcursorBool (*skip)(XcursorFile *file, long offset); + XcursorBool (*seek)(XcursorFile *file, long offset); +}; + +XcursorImages * +XcursorXcFileLoadImages(XcursorFile *file, int size); + +void XcursorImagesDestroy(XcursorImages *images); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/local/recipes/kde/kwin/source/src/CMakeLists.txt b/local/recipes/kde/kwin/source/src/CMakeLists.txt new file mode 100644 index 0000000000..5429e7e5e4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/CMakeLists.txt @@ -0,0 +1,649 @@ +set(KWIN_KILLER_BIN ${CMAKE_INSTALL_FULL_LIBEXECDIR}/kwin_killer_helper) + +configure_file(config-kwin.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kwin.h) + +set(kwin_effects_dbus_xml ${CMAKE_CURRENT_SOURCE_DIR}/org.kde.kwin.Effects.xml) +qt_add_dbus_interface(effects_interface_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +add_library(KWinEffectsInterface STATIC ${effects_interface_SRCS}) +set_property(TARGET KWinEffectsInterface PROPERTY POSITION_INDEPENDENT_CODE ON) + +target_link_libraries(KWinEffectsInterface Qt::DBus) + +add_subdirectory(helpers) +add_subdirectory(qml) + +if (KWIN_BUILD_KCMS) + add_subdirectory(kcms) +endif() + +add_library(kwin SHARED) +target_include_directories(kwin INTERFACE "$") +set_target_properties(kwin PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION 6 +) + +target_compile_definitions(kwin PRIVATE + -DTRANSLATION_DOMAIN=\"kwin\" +) + +target_sources(kwin PRIVATE + 3rdparty/xcursor.c + a11ykeyboardmonitor.cpp + activation.cpp + appmenu.cpp + client_machine.cpp + compositor.cpp + core/backendoutput.cpp + core/colorlut3d.cpp + core/colorpipeline.cpp + core/colorpipelinestage.cpp + core/colorspace.cpp + core/colortransformation.cpp + core/drmdevice.cpp + core/gbmgraphicsbufferallocator.cpp + core/graphicsbuffer.cpp + core/graphicsbufferallocator.cpp + core/graphicsbufferview.cpp + core/iccprofile.cpp + core/inputbackend.cpp + core/inputdevice.cpp + core/output.cpp + core/outputbackend.cpp + core/outputconfiguration.cpp + core/outputlayer.cpp + core/rect.cpp + core/region.cpp + core/renderbackend.cpp + core/renderjournal.cpp + core/renderloop.cpp + core/rendertarget.cpp + core/renderviewport.cpp + core/session.cpp + core/session_consolekit.cpp + core/session_logind.cpp + core/session_noop.cpp + core/shmgraphicsbufferallocator.cpp + core/syncobjtimeline.cpp + cursor.cpp + cursorsource.cpp + dbusinterface.cpp + debug_console.cpp + decorations/decoratedwindow.cpp + decorations/decorationbridge.cpp + decorations/decorationpalette.cpp + decorations/decorations_logging.cpp + decorations/settings.cpp + dpmsinputeventfilter.cpp + effect/anidata.cpp + effect/animationeffect.cpp + effect/effect.cpp + effect/effectframe.cpp + effect/effecthandler.cpp + effect/effectloader.cpp + effect/effecttogglablestate.cpp + effect/effectwindow.cpp + effect/logging.cpp + effect/offscreeneffect.cpp + effect/offscreenquickview.cpp + effect/quickeffect.cpp + effect/timeline.cpp + focuschain.cpp + ftrace.cpp + gestures.cpp + globalshortcuts.cpp + hide_cursor_spy.cpp + idle_inhibition.cpp + idledetector.cpp + input.cpp + input_event.cpp + input_event_spy.cpp + inputmethod.cpp + inputpanelv1integration.cpp + inputpanelv1window.cpp + internalinputmethodcontext.cpp + internalwindow.cpp + keyboard_input.cpp + keyboard_layout.cpp + keyboard_layout_switching.cpp + keyboard_repeat.cpp + killprompt.cpp + killwindow.cpp + kscreenintegration.cpp + layers.cpp + layershellv1integration.cpp + layershellv1window.cpp + lidswitchtracker.cpp + main.cpp + mousebuttons.cpp + onscreennotification.cpp + opengl/abstract_opengl_context_attribute_builder.cpp + opengl/egl_context_attribute_builder.cpp + opengl/eglbackend.cpp + opengl/eglcontext.cpp + opengl/egldisplay.cpp + opengl/eglimagetexture.cpp + opengl/eglnativefence.cpp + opengl/eglswapchain.cpp + opengl/glframebuffer.cpp + opengl/gllut.cpp + opengl/gllut3D.cpp + opengl/glplatform.cpp + opengl/glrendertimequery.cpp + opengl/glshader.cpp + opengl/glshadermanager.cpp + opengl/gltexture.cpp + opengl/glutils.cpp + opengl/glvertexbuffer.cpp + opengl/icc_shader.cpp + options.cpp + osd.cpp + outline.cpp + outputconfigurationstore.cpp + placeholderinputeventfilter.cpp + placeholderoutput.cpp + placement.cpp + placementtracker.cpp + plugin.cpp + pluginmanager.cpp + pointer_input.cpp + popup_input_filter.cpp + qpainter/qpainterbackend.cpp + qpainter/qpainterswapchain.cpp + renderloopdrivenqanimationdriver.cpp + resources.qrc + rulebooksettings.cpp + rules.cpp + scene/backgroundeffectitem.cpp + scene/borderoutline.cpp + scene/borderradius.cpp + scene/cursoritem.cpp + scene/decorationitem.cpp + scene/dndiconitem.cpp + scene/imageitem.cpp + scene/item.cpp + scene/itemgeometry.cpp + scene/itemrenderer.cpp + scene/itemrenderer_opengl.cpp + scene/itemrenderer_qpainter.cpp + scene/outlinedborderitem.cpp + scene/rootitem.cpp + scene/scene.cpp + scene/shadowitem.cpp + scene/surfaceitem.cpp + scene/surfaceitem_internal.cpp + scene/surfaceitem_wayland.cpp + scene/windowitem.cpp + scene/workspacescene.cpp + screenedge.cpp + screenedgegestures.cpp + scripting/dbuscall.cpp + scripting/desktopbackgrounditem.cpp + scripting/gesturehandler.cpp + scripting/screenedgehandler.cpp + scripting/scriptedeffect.cpp + scripting/scriptedquicksceneeffect.cpp + scripting/scripting.cpp + scripting/scripting_logging.cpp + scripting/scriptingutils.cpp + scripting/shortcuthandler.cpp + scripting/tilemodel.cpp + scripting/virtualdesktopmodel.cpp + scripting/windowmodel.cpp + scripting/windowthumbnailitem.cpp + scripting/workspace_wrapper.cpp + shadow.cpp + sm.cpp + tablet_input.cpp + tabletmodemanager.cpp + tiles/customtile.cpp + tiles/quicktile.cpp + tiles/tile.cpp + tiles/tilemanager.cpp + touch_input.cpp + useractions.cpp + utils/svgcursorreader.cpp + utils/version.cpp + utils/xcursorreader.cpp + virtualdesktops.cpp + virtualdesktopsdbustypes.cpp + virtualkeyboard_dbus.cpp + wayland_server.cpp + waylandshellintegration.cpp + waylandwindow.cpp + window.cpp + workspace.cpp + xdgactivationv1.cpp + xdgshellintegration.cpp + xdgshellwindow.cpp + xkb.cpp + xxpipv1integration.cpp + xxpipv1window.cpp +) + +target_link_libraries(kwin + PUBLIC + Qt::DBus + Qt::Quick + Qt::Widgets + Wayland::Server + KF6::ConfigCore + KF6::CoreAddons + KF6::WindowSystem + epoxy::epoxy + Libdrm::Libdrm + + PRIVATE + Qt::Concurrent + Qt::GuiPrivate + Qt::Svg + + KF6::ColorScheme + KF6::ConfigGui + KF6::ConfigQml + KF6::Crash + KF6::GlobalAccel + KF6::I18n + KF6::I18nQml + KF6::Package + KF6::Service + + KDecoration3::KDecoration + KDecoration3::KDecoration3Private + + UDev::UDev + XKB::XKB + EGL::EGL + epoxy::epoxy + + Threads::Threads + lcms2::lcms2 + PkgConfig::libdisplayinfo +) + +if (TARGET K::KGlobalAccelD) + target_link_libraries(kwin PRIVATE K::KGlobalAccelD) +endif() + +if (KWIN_BUILD_X11) + target_sources(kwin + PRIVATE + atoms.cpp + events.cpp + group.cpp + netinfo.cpp + rootinfo_filter.cpp + syncalarmx11filter.cpp + window_property_notify_x11_filter.cpp + x11eventfilter.cpp + x11window.cpp + ) + target_link_libraries(kwin + PRIVATE + XCB::COMPOSITE + XCB::ICCCM + XCB::KEYSYMS + XCB::RANDR + XCB::RENDER + XCB::RES + XCB::SHAPE + XCB::SHM + XCB::SYNC + XCB::XCB + XCB::XFIXES + XCB::XINERAMA + ) +endif() + +if (KWIN_BUILD_NOTIFICATIONS) + target_link_libraries(kwin PRIVATE KF6::Notifications) +endif() + +kconfig_add_kcfg_files(kwin + settings.kcfgc + rulesettings.kcfgc + rulebooksettingsbase.kcfgc +) + +ki18n_wrap_ui(kwin + debug_console.ui + shortcutdialog.ui +) + +set(kwin_dbus_SRCS) +qt_add_dbus_adaptor(kwin_dbus_SRCS scripting/org.kde.kwin.Script.xml scripting/scripting.h KWin::AbstractScript) +qt_add_dbus_adaptor(kwin_dbus_SRCS org.kde.KWin.xml dbusinterface.h KWin::DBusInterface) +qt_add_dbus_adaptor(kwin_dbus_SRCS org.kde.kwin.Compositing.xml dbusinterface.h KWin::CompositorDBusInterface) +qt_add_dbus_adaptor(kwin_dbus_SRCS ${kwin_effects_dbus_xml} effect/effecthandler.h KWin::EffectsHandler) +qt_add_dbus_adaptor(kwin_dbus_SRCS org.kde.KWin.VirtualDesktopManager.xml dbusinterface.h KWin::VirtualDesktopManagerDBusInterface) +qt_add_dbus_adaptor(kwin_dbus_SRCS org.kde.KWin.Session.xml sm.h KWin::SessionManager) +qt_add_dbus_adaptor(kwin_dbus_SRCS org.kde.KWin.Plugins.xml dbusinterface.h KWin::PluginManagerDBusInterface) +qt_add_dbus_interface(kwin_dbus_SRCS org.freedesktop.DBus.Properties.xml dbusproperties_interface) + +set_source_files_properties(net.hadess.SensorProxy.xml PROPERTIES + NO_NAMESPACE ON +) +qt_add_dbus_interface(kwin_dbus_SRCS net.hadess.SensorProxy.xml sensorproxy_interface) + +if (KWIN_BUILD_SCREENLOCKER) + qt_add_dbus_interface(kwin_dbus_SRCS ${KSCREENLOCKER_DBUS_INTERFACES_DIR}/kf6_org.freedesktop.ScreenSaver.xml screenlocker_interface) +endif() + +qt_add_dbus_interface(kwin_dbus_SRCS org.kde.kappmenu.xml appmenu_interface) + +target_sources(kwin PRIVATE + ${kwin_dbus_SRCS} +) + +add_subdirectory(backends) +add_subdirectory(plugins) +add_subdirectory(utils) +add_subdirectory(wayland) +add_subdirectory(wayland-client) + +if (KWIN_BUILD_X11) + add_subdirectory(xwayland) +endif() + +if (KWIN_BUILD_ACTIVITIES) + target_sources(kwin PRIVATE activities.cpp) + target_link_libraries(kwin PRIVATE Plasma::Activities) +endif() + +if (KWIN_BUILD_SCREENLOCKER) + target_link_libraries(kwin PRIVATE PW::KScreenLocker) +endif() + +if (KWIN_BUILD_TABBOX) + target_sources(kwin PRIVATE + tabbox/clientmodel.cpp + tabbox/switcheritem.cpp + tabbox/tabbox.cpp + tabbox/tabbox_logging.cpp + tabbox/tabboxconfig.cpp + tabbox/tabboxhandler.cpp + ) + add_subdirectory(tabbox/switchers) +endif() + +qt_generate_dbus_interface(virtualkeyboard_dbus.h org.kde.kwin.VirtualKeyboard.xml OPTIONS -A) +qt_generate_dbus_interface(tabletmodemanager.h org.kde.KWin.TabletModeManager.xml OPTIONS -A) + +generate_export_header(kwin EXPORT_FILE_NAME kwin_export.h) + +install(TARGETS kwin EXPORT KWinTargets ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) + +ecm_qt_declare_logging_category(kwin + HEADER outputconfiglogging.h + IDENTIFIER KWIN_OUTPUT_CONFIG + CATEGORY_NAME kwin_output_config +) + +add_executable(kwin_wayland main_wayland.cpp) + +if(TARGET PkgConfig::libsystemd) + ecm_qt_declare_logging_category(kwin_wayland + HEADER watchdoglogging.h + IDENTIFIER KWIN_WATCHDOG + CATEGORY_NAME kwin_watchdog + DEFAULT_SEVERITY Info + ) + + target_sources(kwin_wayland PRIVATE watchdog.cpp) + target_link_libraries(kwin_wayland PkgConfig::libsystemd) +endif() + +target_link_libraries(kwin_wayland + kwin + KF6::Crash + KF6::I18n +) +if (KWIN_BUILD_X11) + target_link_libraries(kwin_wayland KWinXwaylandServerModule) +endif() +target_compile_definitions(kwin_wayland PRIVATE + -DTRANSLATION_DOMAIN=\"kwin\" +) +kcoreaddons_target_static_plugins(kwin_wayland NAMESPACE "kwin/effects/plugins") + +install(TARGETS kwin_wayland ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) + +target_link_libraries(kwin_wayland + KWinQpaPlugin + KF6WindowSystemKWinPlugin + KF6IdleTimeKWinPlugin +) + +if (TARGET KF6GlobalAccelKWinPlugin) + target_link_libraries(kwin_wayland KF6GlobalAccelKWinPlugin) +endif() + +add_custom_target( + KWinDBusInterfaces + ALL + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.kwin.VirtualKeyboard.xml + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.KWin.TabletModeManager.xml +) + +install(FILES kwin.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) +if (KWIN_BUILD_NOTIFICATIONS) + install(FILES kwin.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR}) +endif() +install( + FILES + org.kde.KWin.VirtualDesktopManager.xml + org.kde.KWin.xml + org.kde.kwin.Compositing.xml + org.kde.kwin.Effects.xml + org.kde.KWin.Plugins.xml + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.kwin.VirtualKeyboard.xml + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.KWin.TabletModeManager.xml + DESTINATION + ${KDE_INSTALL_DBUSINTERFACEDIR} +) + +install(EXPORT KWinTargets DESTINATION "${KDE_INSTALL_CMAKEPACKAGEDIR}/KWin" FILE KWinTargets.cmake NAMESPACE KWin:: ) + +if (KWIN_BUILD_X11) + install(FILES atoms.h DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kwin COMPONENT Devel) +endif() + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/config-kwin.h + ${CMAKE_CURRENT_BINARY_DIR}/kwin_export.h + a11ykeyboardmonitor.h + activities.h + appmenu.h + client_machine.h + compositor.h + cursor.h + cursorsource.h + dbusinterface.h + debug_console.h + focuschain.h + ftrace.h + gestures.h + globalshortcuts.h + group.h + idle_inhibition.h + idledetector.h + input.h + input_event.h + input_event_spy.h + inputmethod.h + inputpanelv1integration.h + inputpanelv1window.h + internalwindow.h + keyboard_input.h + keyboard_layout.h + keyboard_layout_switching.h + keyboard_repeat.h + killwindow.h + kscreenintegration.h + layershellv1integration.h + layershellv1window.h + lidswitchtracker.h + main.h + mousebuttons.h + netinfo.h + onscreennotification.h + options.h + osd.h + outline.h + outputconfigurationstore.h + placeholderoutput.h + placement.h + placementtracker.h + plugin.h + pluginmanager.h + pointer_input.h + rulebooksettings.h + rules.h + screenedge.h + screenedgegestures.h + shadow.h + sm.h + tablet_input.h + tabletmodemanager.h + touch_input.h + useractions.h + virtualdesktops.h + virtualdesktopsdbustypes.h + virtualkeyboard_dbus.h + wayland_server.h + waylandshellintegration.h + waylandwindow.h + window.h + workspace.h + x11eventfilter.h + x11window.h + xdgactivationv1.h + xdgshellintegration.h + xdgshellwindow.h + xkb.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kwin COMPONENT Devel) + +install(FILES + core/backendoutput.h + core/colorlut3d.h + core/colorpipeline.h + core/colorpipelinestage.h + core/colorspace.h + core/colortransformation.h + core/drmdevice.h + core/gbmgraphicsbufferallocator.h + core/graphicsbuffer.h + core/graphicsbufferallocator.h + core/graphicsbufferview.h + core/iccprofile.h + core/inputbackend.h + core/inputdevice.h + core/output.h + core/outputbackend.h + core/outputconfiguration.h + core/outputlayer.h + core/pixelgrid.h + core/rect.h + core/region.h + core/renderbackend.h + core/renderjournal.h + core/renderloop.h + core/renderloop_p.h + core/rendertarget.h + core/renderviewport.h + core/session.h + core/session_consolekit.h + core/session_logind.h + core/session_noop.h + core/shmgraphicsbufferallocator.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kwin/core COMPONENT Devel) + +install(FILES + utils/c_ptr.h + utils/common.h + utils/cursortheme.h + utils/damagejournal.h + utils/drm_format_helper.h + utils/edid.h + utils/executable_path.h + utils/filedescriptor.h + utils/gravity.h + utils/kernel.h + utils/memorymap.h + utils/orientationsensor.h + utils/ramfile.h + utils/realtime.h + utils/resource.h + utils/serial.h + utils/serviceutils.h + utils/softwarevsyncmonitor.h + utils/subsurfacemonitor.h + utils/udev.h + utils/version.h + utils/vsyncmonitor.h + utils/xcbutils.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kwin/utils COMPONENT Devel) + +install(FILES + effect/animationeffect.h + effect/effect.h + effect/effecthandler.h + effect/effecttogglablestate.h + effect/effectwindow.h + effect/globals.h + effect/offscreeneffect.h + effect/offscreenquickview.h + effect/quickeffect.h + effect/timeline.h + effect/xcb.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kwin/effect COMPONENT Devel) + +install(FILES + opengl/abstract_opengl_context_attribute_builder.h + opengl/egl_context_attribute_builder.h + opengl/eglcontext.h + opengl/egldisplay.h + opengl/eglimagetexture.h + opengl/eglnativefence.h + opengl/eglswapchain.h + opengl/eglutils_p.h + opengl/glframebuffer.h + opengl/gllut3D.h + opengl/gllut.h + opengl/glplatform.h + opengl/glrendertimequery.h + opengl/glshader.h + opengl/glshadermanager.h + opengl/gltexture.h + opengl/gltexture_p.h + opengl/glutils.h + opengl/glvertexbuffer.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kwin/opengl COMPONENT Devel +) + +install(FILES + scene/backgroundeffectitem.h + scene/borderoutline.h + scene/borderradius.h + scene/cursoritem.h + scene/decorationitem.h + scene/dndiconitem.h + scene/imageitem.h + scene/item.h + scene/itemgeometry.h + scene/itemrenderer.h + scene/itemrenderer_opengl.h + scene/itemrenderer_qpainter.h + scene/outlinedborderitem.h + scene/rootitem.h + scene/scene.h + scene/shadowitem.h + scene/surfaceitem.h + scene/surfaceitem_internal.h + scene/surfaceitem_wayland.h + scene/windowitem.h + scene/workspacescene.h + DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kwin/scene COMPONENT Devel +) + +ecm_generate_qdoc(kwin kwin.qdocconf) diff --git a/local/recipes/kde/kwin/source/src/Messages.sh b/local/recipes/kde/kwin/source/src/Messages.sh new file mode 100644 index 0000000000..e5f5bdad29 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/Messages.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -not -path "./kcms/*" \( -name \*.kcfg -o -name \*.ui \)` >> rc.cpp || exit 11 +$XGETTEXT `find . -not -path "./kcms/*" \( -name \*.cpp -o -name \*.qml \)` -o $podir/kwin.pot +rm -f rc.cpp diff --git a/local/recipes/kde/kwin/source/src/activation.cpp b/local/recipes/kde/kwin/source/src/activation.cpp new file mode 100644 index 0000000000..3017eb6b6e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/activation.cpp @@ -0,0 +1,636 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* + + This file contains things relevant to window activation and focus + stealing prevention. + +*/ + +#include "cursor.h" +#include "focuschain.h" +#include "workspace.h" +#if KWIN_BUILD_ACTIVITIES +#include "activities.h" +#endif +#include "input.h" +#include "layershellv1window.h" +#include "rules.h" +#include "useractions.h" +#include "virtualdesktops.h" +#include "wayland/layershell_v1.h" +#include "waylandwindow.h" +#include "window.h" + +#if KWIN_BUILD_X11 +#include "atoms.h" +#include "group.h" +#include "netinfo.h" +#include "x11window.h" +#include +#endif + +#include +#include +#include + +namespace KWin +{ + +/* + Prevention of focus stealing: + + KWin tries to prevent unwanted changes of focus, that would result + from mapping a new window. Also, some nasty applications may try + to force focus change even in cases when ICCCM 4.2.7 doesn't allow it + (e.g. they may try to activate their main window because the user + definitely "needs" to see something happened - misusing + of QWidget::setActiveWindow() may be such case). + + There are 4 ways how a window may become active: + - the user changes the active window (e.g. focus follows mouse, clicking + on some window's titlebar) - the change of focus will + be done by KWin, so there's nothing to solve in this case + - the change of active window will be requested using the _NET_ACTIVE_WINDOW + message (handled in RootInfo::changeActiveWindow()) - such requests + will be obeyed, because this request is meant mainly for e.g. taskbar + asking the WM to change the active window as a result of some user action. + Normal applications should use this request only rarely in special cases. + See also below the discussion of _NET_ACTIVE_WINDOW_TRANSFER. + - the change of active window will be done by performing XSetInputFocus() + on a window that's not currently active. ICCCM 4.2.7 describes when + the application may perform change of input focus. In order to handle + misbehaving applications, KWin will try to detect focus changes to + windows that don't belong to currently active application, and restore + focus back to the currently active window, instead of activating the window + that got focus (unfortunately there's no way to FocusChangeRedirect similar + to e.g. SubstructureRedirect, so there will be short time when the focus + will be changed). The check itself that's done is + Workspace::allowWindowActivation() (see below). + - a new window will be mapped - this is the most complicated case. If + the new window belongs to the currently active application, it may be safely + mapped on top and activated. The same if there's no active window, + or the active window is the desktop. These checks are done by + Workspace::allowWindowActivation(). + Following checks need to compare times. One time is the timestamp + of last user action in the currently active window, the other time is + the timestamp of the action that originally caused mapping of the new window + (e.g. when the application was started). If the first time is newer than + the second one, the window will not be activated, as that indicates + further user actions took place after the action leading to this new + mapped window. This check is done by Workspace::allowWindowActivation(). + There are several ways how to get the timestamp of action that caused + the new mapped window (done in X11Window::readUserTimeMapTimestamp()) : + - the window may have the _NET_WM_USER_TIME property. This way + the application may either explicitly request that the window is not + activated (by using 0 timestamp), or the property contains the time + of last user action in the application. + - KWin itself tries to detect time of last user action in every window, + by watching KeyPress and ButtonPress events on windows. This way some + events may be missed (if they don't propagate to the toplevel window), + but it's good as a fallback for applications that don't provide + _NET_WM_USER_TIME, and missing some events may at most lead + to unwanted focus stealing. + - the timestamp may come from application startup notification. + Application startup notification, if it exists for the new mapped window, + should include time of the user action that caused it. + - if there's no timestamp available, it's checked whether the new window + belongs to some already running application - if yes, the timestamp + will be 0 (i.e. refuse activation) + - if the window is from session restored window, the timestamp will + be 0 too, unless this application was the active one at the time + when the session was saved, in which case the window will be + activated if there wasn't any user interaction since the time + KWin was started. + - as the last resort, the _KDE_NET_USER_CREATION_TIME timestamp + is used. For every toplevel window that is created (see CreateNotify + handling), this property is set to the at that time current time. + Since at this time it's known that the new window doesn't belong + to any existing application (better said, the application doesn't + have any other window mapped), it is either the very first window + of the application, or it is the only window of the application + that was hidden before. The latter case is handled by removing + the property from windows before withdrawing them, making + the timestamp empty for next mapping of the window. In the sooner + case, the timestamp will be used. This helps in case when + an application is launched without application startup notification, + it creates its mainwindow, and starts its initialization (that + may possibly take long time). The timestamp used will be older + than any user action done after launching this application. + - if no timestamp is found at all, the window is activated. + The check whether two windows belong to the same application (same + process) is done in X11Window::belongToSameApplication(). Not 100% reliable, + but hopefully 99,99% reliable. + + As a somewhat special case, window activation is always enabled when + session saving is in progress. When session saving, the session + manager allows only one application to interact with the user. + Not allowing window activation in such case would result in e.g. dialogs + not becoming active, so focus stealing prevention would cause here + more harm than good. + + Windows that attempted to become active but KWin prevented this will + be marked as demanding user attention. They'll get + the _NET_WM_STATE_DEMANDS_ATTENTION state, and the taskbar should mark + them specially (blink, etc.). The state will be reset when the window + eventually really becomes active. + + There are two more ways how a window can become obtrusive, window stealing + focus: By showing above the active window, by either raising itself, + or by moving itself on the active desktop. + - KWin will refuse raising non-active window above the active one, + unless they belong to the same application. Applications shouldn't + raise their windows anyway (unless the app wants to raise one + of its windows above another of its windows). + - KWin activates windows moved to the current desktop (as that seems + logical from the user's point of view, after sending the window + there directly from KWin, or e.g. using pager). This means + applications shouldn't send their windows to another desktop + (SELI TODO - but what if they do?) + + Special cases I can think of: + - konqueror reusing, i.e. kfmclient tells running Konqueror instance + to open new window + - without focus stealing prevention - no problem + - with ASN (application startup notification) - ASN is forwarded, + and because it's newer than the instance's user timestamp, + it takes precedence + - without ASN - user timestamp needs to be reset, otherwise it would + be used, and it's old; moreover this new window mustn't be detected + as window belonging to already running application, or it wouldn't + be activated - see X11Window::sameAppWindowRoleMatch() for the (rather ugly) + hack + - konqueror preloading, i.e. window is created in advance, and kfmclient + tells this Konqueror instance to show it later + - without focus stealing prevention - no problem + - with ASN - ASN is forwarded, and because it's newer than the instance's + user timestamp, it takes precedence + - without ASN - user timestamp needs to be reset, otherwise it would + be used, and it's old; also, creation timestamp is changed to + the time the instance starts (re-)initializing the window, + this ensures creation timestamp will still work somewhat even in this case + - KUniqueApplication - when the window is already visible, and the new instance + wants it to activate + - without focus stealing prevention - _NET_ACTIVE_WINDOW - no problem + - with ASN - ASN is forwarded, and set on the already visible window, KWin + treats the window as new with that ASN + - without ASN - _NET_ACTIVE_WINDOW as application request is used, + and there's no really usable timestamp, only timestamp + from the time the (new) application instance was started, + so KWin will activate the window *sigh* + - the bad thing here is that there's absolutely no chance to recognize + the case of starting this KUniqueApp from Konsole (and thus wanting + the already visible window to become active) from the case + when something started this KUniqueApp without ASN (in which case + the already visible window shouldn't become active) + - the only solution is using ASN for starting applications, at least silent + (i.e. without feedback) + - when one application wants to activate another application's window (e.g. KMail + activating already running KAddressBook window ?) + - without focus stealing prevention - _NET_ACTIVE_WINDOW - no problem + - with ASN - can't be here, it's the KUniqueApp case then + - without ASN - _NET_ACTIVE_WINDOW as application request should be used, + KWin will activate the new window depending on the timestamp and + whether it belongs to the currently active application + + _NET_ACTIVE_WINDOW usage: + data.l[0]= 1 ->app request + = 2 ->pager request + = 0 - backwards compatibility + data.l[1]= timestamp +*/ + +//**************************************** +// Workspace +//**************************************** + +/** + * Informs the workspace about the active window, i.e. the window that + * has the focus (or None if no window has the focus). This functions + * is called by the window itself that gets focus. It has no other + * effect than fixing the focus chain and the return value of + * activeWindow(). And of course, to propagate the active window to the + * world. + */ +void Workspace::setActiveWindow(Window *window) +{ + if (m_activeWindow == window) { + return; + } + + if (active_popup && m_activePopupWindow != window && m_setActiveWindowRecursion == 0) { + closeActivePopup(); + } + if (m_userActionsMenu->hasWindow() && !m_userActionsMenu->isMenuWindow(window) && m_setActiveWindowRecursion == 0) { + m_userActionsMenu->close(); + } + StackingUpdatesBlocker blocker(this); + ++m_setActiveWindowRecursion; + updateFocusMousePosition(Cursors::self()->mouse()->pos()); + + if (qobject_cast(window)) { + focusToNull(); + } + + Window *previousActiveWindow = m_activeWindow; + m_activeWindow = window; + + if (previousActiveWindow) { + previousActiveWindow->setActive(false); + } + + if (m_activeWindow) { + m_lastActiveWindow = m_activeWindow; + m_focusChain->update(m_activeWindow, FocusChain::MakeFirst); + m_activeWindow->demandAttention(false); + m_activeWindow->setActive(true); + + // activating a client can cause a non active fullscreen window to loose the ActiveLayer status on > 1 screens + if (outputs().count() > 1) { + for (auto it = m_windows.begin(); it != m_windows.end(); ++it) { + if (*it != m_activeWindow && (*it)->layer() == ActiveLayer && (*it)->output() == m_activeWindow->output()) { + (*it)->updateLayer(); + } + } + } + } + + if (window) { + setActiveOutput(window->output()); + disableGlobalShortcutsForClient(window->rules()->checkDisableGlobalShortcuts(false)); + } else { + disableGlobalShortcutsForClient(false); + } + +#if KWIN_BUILD_X11 + if (rootInfo()) { + rootInfo()->setActiveClient(m_activeWindow); + } +#endif + + Q_EMIT windowActivated(m_activeWindow); + --m_setActiveWindowRecursion; +} + +/** + * Tries to activate the window \a window. This function performs what you + * expect when clicking the respective entry in a taskbar: showing and + * raising the window (this may imply switching to the another virtual + * desktop) and putting the focus onto it. Once X really gave focus to + * the window window as requested, the window itself will call + * setActiveWindow() and the operation is complete. This may not happen + * with certain focus policies, though. + * + * @see setActiveWindow + * @see requestFocus + */ +void Workspace::activateWindow(Window *window, bool force) +{ + if (window == nullptr) { + resetFocus(); + return; + } + if (!window->isClient() || window->isDeleted() || !window->wantsInput()) { + return; + } + if (window->isHiddenByShowDesktop()) { + ++block_focus; + setShowingDesktop(false); + --block_focus; + } + raiseWindow(window); + if (!window->isOnCurrentDesktop()) { + ++block_focus; + switch (options->activationDesktopPolicy()) { + case Options::ActivationDesktopPolicy::SwitchToOtherDesktop: + VirtualDesktopManager::self()->setCurrent(window->desktops().constLast()); + break; + case Options::ActivationDesktopPolicy::BringToCurrentDesktop: + window->enterDesktop(VirtualDesktopManager::self()->currentDesktop()); + break; + case Options::ActivationDesktopPolicy::DoNothing: + break; + } + --block_focus; + } +#if KWIN_BUILD_ACTIVITIES + if (!window->isOnCurrentActivity()) { + ++block_focus; + // DBUS! + // first isn't necessarily best, but it's easiest + m_activities->setCurrent(window->activities().constFirst(), window->isOnAllDesktops() ? nullptr : window->desktops().front()); + --block_focus; + } +#endif + if (window->isMinimized()) { + window->setMinimized(false); + } + + // ensure the window is really visible - could eg. be a hidden utility window, see bug #348083 + window->setHidden(false); + + // TODO force should perhaps allow this only if the window already contains the mouse + if (options->focusPolicyIsReasonable() || force) { + requestFocus(window, force); + } + +#if KWIN_BUILD_X11 + // Don't update user time for windows that have focus stealing workaround. + // As they usually belong to the current active window but fail to provide + // this information, updating their user time would make the user time + // of the currently active window old, and reject further activation for it. + // E.g. typing URL in minicli which will show kio_uiserver dialog (with workaround), + // and then kdesktop shows dialog about SSL certificate. + // This needs also avoiding user creation time in X11Window::readUserTimeMapTimestamp(). + if (X11Window *x11Window = dynamic_cast(window)) { + // updateUserTime is X11 specific + x11Window->updateUserTime(); + } +#endif +} + +/** + * Tries to activate the window by asking X for the input focus. This + * function does not perform any show, raise or desktop switching. See + * Workspace::activateWindow() instead. + * + * @see activateWindow + */ +bool Workspace::requestFocus(Window *window, bool force) +{ + // the 'if ( window == m_activeWindow ) return;' optimization mustn't be done here + if (!focusChangeEnabled() && (window != m_activeWindow)) { + return false; + } + + Window *modal = window->findModal(); + if (modal != nullptr && modal != window) { + if (modal->desktops() != window->desktops()) { + modal->setDesktops(window->desktops()); + } + if (!modal->isShown() && !modal->isMinimized()) { // forced desktop or utility window + activateWindow(modal); // activating a minimized blocked window will unminimize its modal implicitly + } + window = modal; + } + cancelDelayFocus(); + + if (!force && window->isSplash()) { + return false; // toplevel menus don't take focus if not forced + } + + if (!window->isShown()) { // shouldn't happen, call activateWindow() if needed + qCWarning(KWIN_CORE) << "Cannot focus a window that is hidden"; + return false; + } + + if (!window->wantsInput()) { + return false; + } + + window->takeFocus(); + setActiveWindow(window); + + return true; +} + +void Workspace::resetFocus() +{ + focusToNull(); + setActiveWindow(nullptr); +} + +Window *Workspace::windowUnderMouse(LogicalOutput *output) const +{ + auto it = stackingOrder().constEnd(); + while (it != stackingOrder().constBegin()) { + auto window = *(--it); + if (!window->isClient()) { + continue; + } + + // rule out windows which are not really visible. + // the screen test is rather superfluous for xrandr & twinview since the geometry would differ -> TODO: might be dropped + if (!(window->isShown() && window->isOnCurrentDesktop() && window->isOnCurrentActivity() && window->isOnOutput(output))) { + continue; + } + + if (window->frameGeometry().contains(Cursors::self()->mouse()->pos())) { + return window; + } + } + return nullptr; +} + +// deactivates 'window' and activates next window +void Workspace::activateNextWindow(Window *window) +{ + if (m_activeWindow != window) { + return; + } + + closeActivePopup(); + + Window *focusCandidate = nullptr; + + if (options->focusPolicyIsReasonable()) { + VirtualDesktop *desktop = VirtualDesktopManager::self()->currentDesktop(); + LogicalOutput *output = window ? window->output() : workspace()->activeOutput(); + + if (!focusCandidate && showingDesktop()) { + focusCandidate = findDesktop(desktop, output); // to not break the state + } + + if (!focusCandidate && options->isNextFocusPrefersMouse()) { + focusCandidate = windowUnderMouse(output); + if (focusCandidate && (focusCandidate == window || focusCandidate->isDesktop())) { + // should rather not happen, but it cannot get the focus. rest of usability is tested above + focusCandidate = nullptr; + } + } + + if (!focusCandidate) { // no suitable window under the mouse -> find sth. else + // first try to pass the focus to the (former) active clients leader + if (window && window->isTransient()) { + auto leaders = window->mainWindows(); + if (leaders.count() == 1 && m_focusChain->isUsableFocusCandidate(leaders.at(0), window)) { + focusCandidate = leaders.at(0); + raiseWindow(focusCandidate); // also raise - we don't know where it came from + } + } + if (!focusCandidate) { + // nope, ask the focus chain for the next candidate + focusCandidate = m_focusChain->nextForDesktop(window, desktop); + } + } + + if (focusCandidate == nullptr) { // last chance: focus the desktop + focusCandidate = findDesktop(desktop, output); + } + } + + if (focusCandidate) { + if (requestFocus(focusCandidate)) { + return; + } + } + + resetFocus(); +} + +void Workspace::switchToOutput(LogicalOutput *output) +{ + if (!options->focusPolicyIsReasonable()) { + return; + } + closeActivePopup(); + VirtualDesktop *desktop = VirtualDesktopManager::self()->currentDesktop(); + Window *get_focus = m_focusChain->getForActivation(desktop, output); + if (get_focus == nullptr) { + get_focus = findDesktop(desktop, output); + } + if (get_focus != nullptr && get_focus != activeWindow()) { + requestFocus(get_focus); + } + setActiveOutput(output); +} + +// basically the same like allowWindowActivation(), this time allowing +// a window to be fully raised upon its own request (XRaiseWindow), +// if refused, it will be raised only on top of windows belonging +// to the same application +bool Workspace::allowFullClientRaising(const KWin::Window *window, uint32_t time) +{ + FocusStealingPreventionLevel level = window->rules()->checkFSP(options->focusStealingPreventionLevel()); + if (sessionManager()->state() == SessionState::Saving && level <= FocusStealingPreventionLevel::Medium) { + return true; + } + Window *ac = activeWindow(); + if (level == FocusStealingPreventionLevel::None) { + return true; + } + if (level == FocusStealingPreventionLevel::Extreme) { + return false; + } + if (ac == nullptr || ac->isDesktop()) { + qCDebug(KWIN_CORE) << "Raising: No window active, allowing"; + return true; // no active window -> always allow + } + // TODO window urgency -> return true? + if (Window::belongToSameApplication(window, ac, Window::SameApplicationCheck::RelaxedForActive)) { + qCDebug(KWIN_CORE) << "Raising: Belongs to active application"; + return true; + } + if (level == FocusStealingPreventionLevel::High) { + return false; + } +#if KWIN_BUILD_X11 + xcb_timestamp_t user_time = ac->userTime(); + qCDebug(KWIN_CORE) << "Raising, compared:" << time << ":" << user_time + << ":" << (NET::timestampCompare(time, user_time) >= 0); + return NET::timestampCompare(time, user_time) >= 0; // time >= user_time +#else + return true; +#endif +} + +/** + * Called from X11Window after FocusIn that wasn't initiated by KWin and the window wasn't + * allowed to activate. + * + * Returns @c true if the focus has been restored successfully; otherwise returns @c false. + */ +bool Workspace::restoreFocus() +{ + if (m_activeWindow) { + return requestFocus(m_activeWindow); + } else if (m_lastActiveWindow) { + return requestFocus(m_lastActiveWindow); + } + return true; +} + +void Workspace::windowAttentionChanged(Window *window, bool set) +{ + if (window->isDeleted()) { + return; + } + if (set) { + attention_chain.removeAll(window); + attention_chain.prepend(window); + } else { + attention_chain.removeAll(window); + } +} + +void Workspace::setActivationToken(const QString &token, UInt32Serial serial, const QString &appId) +{ + m_activationToken = token; + m_activationTokenSerial = serial; + m_activationTokenAppId = appId; +} + +bool Workspace::mayActivate(Window *window, const QString &token) const +{ + if (!m_activeWindow) { + return true; + } + if (m_activeWindow->hasTransient(window, true)) { + return true; + } + if (auto parentWindow = window->transientFor()) { + const bool allow = mayActivate(parentWindow, m_activationToken); + if (allow) { + return true; + } + } + const FocusStealingPreventionLevel focusStealingPreventionLevel = window->rules()->checkFSP(options->focusStealingPreventionLevel()); + if (focusStealingPreventionLevel == FocusStealingPreventionLevel::None) { + return true; + } + if (!m_activationToken.isEmpty() && token == m_activationToken && input()->lastInteractionSerial() <= m_activationTokenSerial) { + return true; + } else if (focusStealingPreventionLevel == FocusStealingPreventionLevel::Extreme) { + // "Extreme" only accepts proper activation tokens + return false; + } + // with focus stealing prevention below "Extreme" + // also allow activation if the app id matches with the last activation token + if (!m_activationToken.isEmpty() + && input()->lastInteractionSerial() <= m_activationTokenSerial + && m_activationTokenAppId == window->desktopFileName()) { + return true; + } + + // If it is a fullscreen overlay window, i.e. a window placed above other normal windows, + // allow window activation even without a token to prevent keyboard focus going to an occluded + // window. An example of such an overlay window is the logout greeter. + // + // If a layer shell surface could be closed by the compositor because it didn't get input + // focus, then perhaps we wouldn't need this special case. + if (const auto layerShellWindow = qobject_cast(window)) { + if (m_activeWindow->output() == window->output()) { + const LayerSurfaceV1Interface *layerSurface = layerShellWindow->shellSurface(); + switch (layerSurface->layer()) { + case LayerSurfaceV1Interface::BackgroundLayer: + case LayerSurfaceV1Interface::BottomLayer: + return false; + case LayerSurfaceV1Interface::TopLayer: + case LayerSurfaceV1Interface::OverlayLayer: + return layerSurface->anchor() == Qt::Edges(Qt::TopEdge | Qt::RightEdge | Qt::BottomEdge | Qt::LeftEdge); + } + } + } + + return false; +} + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/activities.cpp b/local/recipes/kde/kwin/source/src/activities.cpp new file mode 100644 index 0000000000..a68633ec4d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/activities.cpp @@ -0,0 +1,175 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "activities.h" +// KWin +#include "virtualdesktops.h" +#include "window.h" +#include "workspace.h" +#if KWIN_BUILD_X11 +#include "x11window.h" +#endif +// KDE +#include +// Qt +#include +#include +#include +#include + +namespace KWin +{ + +Activities::Activities() + : m_controller(new KActivities::Controller(this)) +{ + connect(m_controller, &KActivities::Controller::activityRemoved, this, &Activities::slotRemoved); + connect(m_controller, &KActivities::Controller::activityRemoved, this, &Activities::removed); + connect(m_controller, &KActivities::Controller::activityAdded, this, &Activities::added); + connect(m_controller, &KActivities::Controller::currentActivityChanged, this, &Activities::slotCurrentChanged); + connect(m_controller, &KActivities::Controller::serviceStatusChanged, this, &Activities::slotServiceStatusChanged); + + m_config = KSharedConfig::openStateConfig(); + auto lastDesktopConfig = m_config->group("Activities").group("LastVirtualDesktop"); + + // migrate old config + kwinApp()->config()->group("Activities").group("LastVirtualDesktop").moveValuesTo(lastDesktopConfig); + + for (const auto &activity : lastDesktopConfig.keyList()) { + const QString desktop = lastDesktopConfig.readEntry(activity); + if (!desktop.isEmpty()) { + m_lastVirtualDesktop[activity] = desktop; + } + } + + // Clean up no longer used subsession data + const auto sessionsConfig = KSharedConfig::openConfig(); + const auto groups = sessionsConfig->groupList(); + for (const QString &groupName : groups) { + if (groupName.startsWith(QLatin1String("SubSession: "))) { + sessionsConfig->deleteGroup(groupName); + } + } +} + +KActivities::Consumer::ServiceStatus Activities::serviceStatus() const +{ + return m_controller->serviceStatus(); +} + +void Activities::slotServiceStatusChanged() +{ + if (m_controller->serviceStatus() != KActivities::Consumer::Running) { + return; + } + const auto windows = Workspace::self()->windows(); + for (auto *const window : windows) { + if (!window->isClient()) { + continue; + } + if (window->isDesktop()) { + continue; + } + window->checkActivities(); + } +} + +void Activities::setCurrent(const QString &activity, VirtualDesktop *desktop) +{ + if (desktop) { + m_lastVirtualDesktop[activity] = desktop->id(); + } + m_controller->setCurrentActivity(activity); +} + +void Activities::notifyCurrentDesktopChanged(VirtualDesktop *desktop) +{ + m_lastVirtualDesktop[m_current] = desktop->id(); + auto lastDesktopConfig = m_config->group("Activities").group("LastVirtualDesktop"); + lastDesktopConfig.writeEntry(m_current, desktop->id()); +} + +void Activities::slotCurrentChanged(const QString &newActivity) +{ + if (m_current == newActivity) { + return; + } + Q_EMIT currentAboutToChange(); + m_previous = m_current; + m_current = newActivity; + + if (m_previous != nullUuid()) { + m_lastVirtualDesktop[m_previous] = VirtualDesktopManager::self()->currentDesktop()->id(); + } + const auto it = m_lastVirtualDesktop.find(m_current); + if (it != m_lastVirtualDesktop.end()) { + VirtualDesktop *desktop = VirtualDesktopManager::self()->desktopForId(it->second); + if (desktop) { + VirtualDesktopManager::self()->setCurrent(desktop); + } + } + + Q_EMIT currentChanged(newActivity); +} + +void Activities::slotRemoved(const QString &activity) +{ + const auto windows = Workspace::self()->windows(); + for (auto *const window : windows) { + if (!window->isClient()) { + continue; + } + if (window->isDesktop()) { + continue; + } + window->setOnActivity(activity, false); + } + + m_lastVirtualDesktop.erase(activity); + m_config->group("Activities").group("LastVirtualDesktop").deleteEntry(activity); +} + +void Activities::toggleWindowOnActivity(Window *window, const QString &activity, bool dont_activate) +{ + // int old_desktop = window->desktop(); + bool was_on_activity = window->isOnActivity(activity); + bool was_on_all = window->isOnAllActivities(); + // note: all activities === no activities + bool enable = was_on_all || !was_on_activity; + window->setOnActivity(activity, enable); + if (window->isOnActivity(activity) == was_on_activity && window->isOnAllActivities() == was_on_all) { // No change + return; + } + + Workspace *ws = Workspace::self(); + if (window->isOnCurrentActivity()) { + if (window->wantsTabFocus() && options->focusPolicyIsReasonable() && !was_on_activity && // for stickiness changes + // FIXME not sure if the line above refers to the correct activity + !dont_activate) { + ws->requestFocus(window); + } else { + ws->restackWindowUnderActive(window); + } + } else { + ws->raiseWindow(window); + } + + // notifyWindowDesktopChanged( c, old_desktop ); + + const auto transients_stacking_order = ws->ensureStackingOrder(window->transients()); + for (auto *const window : transients_stacking_order) { + if (!window) { + continue; + } + toggleWindowOnActivity(window, activity, dont_activate); + } + ws->rearrange(); +} +} // namespace + +#include "moc_activities.cpp" diff --git a/local/recipes/kde/kwin/source/src/activities.h b/local/recipes/kde/kwin/source/src/activities.h new file mode 100644 index 0000000000..cdaeda7e67 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/activities.h @@ -0,0 +1,116 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include +#include +#include + +#include +#include + +namespace KActivities +{ +class Controller; +} + +namespace KWin +{ +class Window; +class VirtualDesktop; + +class KWIN_EXPORT Activities : public QObject +{ + Q_OBJECT + +public: + explicit Activities(); + + /** + * Sets the current activity to @param activity, and if desktop isn't nullptr, + * ensures that this doesn't interfere with virtual desktop switching + */ + void setCurrent(const QString &activity, VirtualDesktop *desktop); + /** + * Adds/removes window \a window to/from \a activity. + * + * Takes care of transients as well. + */ + void toggleWindowOnActivity(Window *window, const QString &activity, bool dont_activate); + + QStringList all() const; + const QString ¤t() const; + const QString &previous() const; + + static QString nullUuid(); + + KActivities::Controller::ServiceStatus serviceStatus() const; + +Q_SIGNALS: + /** + * emitted before the current activity actually changes + */ + void currentAboutToChange(); + /** + * This signal is emitted when the global + * activity is changed + * @param id id of the new current activity + */ + void currentChanged(const QString &id); + /** + * This signal is emitted when a new activity is added + * @param id id of the new activity + */ + void added(const QString &id); + /** + * This signal is emitted when the activity + * is removed + * @param id id of the removed activity + */ + void removed(const QString &id); + +public Q_SLOTS: + void notifyCurrentDesktopChanged(VirtualDesktop *desktop); + +private Q_SLOTS: + void slotServiceStatusChanged(); + void slotRemoved(const QString &activity); + void slotCurrentChanged(const QString &newActivity); + +private: + QString m_previous; + QString m_current; + KActivities::Controller *m_controller; + std::unordered_map m_lastVirtualDesktop; + KSharedConfig::Ptr m_config; +}; + +inline QStringList Activities::all() const +{ + return m_controller->activities(); +} + +inline const QString &Activities::current() const +{ + return m_current; +} + +inline const QString &Activities::previous() const +{ + return m_previous; +} + +inline QString Activities::nullUuid() +{ + // cloned from kactivities/src/lib/core/consumer.cpp + return QStringLiteral("00000000-0000-0000-0000-000000000000"); +} + +} diff --git a/local/recipes/kde/kwin/source/src/appmenu.cpp b/local/recipes/kde/kwin/source/src/appmenu.cpp new file mode 100644 index 0000000000..91afc6e324 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/appmenu.cpp @@ -0,0 +1,115 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Lionel Chauvin + SPDX-FileCopyrightText: 2011, 2012 Cédric Bellegarde + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "appmenu.h" +#include "window.h" +#include "workspace.h" +#include + +#include +#include + +#include "decorations/decorationbridge.h" +#include + +namespace KWin +{ + +static const QString s_viewService(QStringLiteral("org.kde.kappmenuview")); + +ApplicationMenu::ApplicationMenu() + : m_appmenuInterface(new OrgKdeKappmenuInterface(QStringLiteral("org.kde.kappmenu"), QStringLiteral("/KAppMenu"), QDBusConnection::sessionBus(), this)) +{ + connect(m_appmenuInterface, &OrgKdeKappmenuInterface::showRequest, this, &ApplicationMenu::slotShowRequest); + connect(m_appmenuInterface, &OrgKdeKappmenuInterface::menuShown, this, &ApplicationMenu::slotMenuShown); + connect(m_appmenuInterface, &OrgKdeKappmenuInterface::menuHidden, this, &ApplicationMenu::slotMenuHidden); + + m_kappMenuWatcher = new QDBusServiceWatcher(QStringLiteral("org.kde.kappmenu"), QDBusConnection::sessionBus(), + QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration, this); + + connect(m_kappMenuWatcher, &QDBusServiceWatcher::serviceRegistered, this, [this]() { + m_applicationMenuEnabled = true; + Q_EMIT applicationMenuEnabledChanged(true); + }); + connect(m_kappMenuWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this]() { + m_applicationMenuEnabled = false; + Q_EMIT applicationMenuEnabledChanged(false); + }); + + m_applicationMenuEnabled = QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.kappmenu")); +} + +bool ApplicationMenu::applicationMenuEnabled() const +{ + return m_applicationMenuEnabled; +} + +void ApplicationMenu::setViewEnabled(bool enabled) +{ + if (enabled) { + QDBusConnection::sessionBus().interface()->registerService(s_viewService, + QDBusConnectionInterface::QueueService, + QDBusConnectionInterface::DontAllowReplacement); + } else { + QDBusConnection::sessionBus().interface()->unregisterService(s_viewService); + } +} + +void ApplicationMenu::slotShowRequest(const QString &serviceName, const QDBusObjectPath &menuObjectPath, int actionId) +{ + // Ignore show request when user has not configured the application menu title bar button + auto decorationSettings = Workspace::self()->decorationBridge()->settings(); + if (decorationSettings && !decorationSettings->decorationButtonsLeft().contains(KDecoration3::DecorationButtonType::ApplicationMenu) + && !decorationSettings->decorationButtonsRight().contains(KDecoration3::DecorationButtonType::ApplicationMenu)) { + return; + } + + if (Window *window = findWindowWithApplicationMenu(serviceName, menuObjectPath)) { + window->showApplicationMenu(actionId); + } +} + +void ApplicationMenu::slotMenuShown(const QString &serviceName, const QDBusObjectPath &menuObjectPath) +{ + if (Window *window = findWindowWithApplicationMenu(serviceName, menuObjectPath)) { + window->setApplicationMenuActive(true); + } +} + +void ApplicationMenu::slotMenuHidden(const QString &serviceName, const QDBusObjectPath &menuObjectPath) +{ + if (Window *window = findWindowWithApplicationMenu(serviceName, menuObjectPath)) { + window->setApplicationMenuActive(false); + } +} + +void ApplicationMenu::showApplicationMenu(const QPoint &p, Window *c, int actionId) +{ + if (!c->hasApplicationMenu()) { + return; + } + m_appmenuInterface->showMenu(p.x(), p.y(), c->applicationMenuServiceName(), QDBusObjectPath(c->applicationMenuObjectPath()), actionId); +} + +Window *ApplicationMenu::findWindowWithApplicationMenu(const QString &serviceName, const QDBusObjectPath &menuObjectPath) +{ + if (serviceName.isEmpty() || menuObjectPath.path().isEmpty()) { + return nullptr; + } + + return Workspace::self()->findWindow([&](const Window *window) { + return window->applicationMenuServiceName() == serviceName + && window->applicationMenuObjectPath() == menuObjectPath.path(); + }); +} + +} // namespace KWin + +#include "moc_appmenu.cpp" diff --git a/local/recipes/kde/kwin/source/src/appmenu.h b/local/recipes/kde/kwin/source/src/appmenu.h new file mode 100644 index 0000000000..3b81dfcfaa --- /dev/null +++ b/local/recipes/kde/kwin/source/src/appmenu.h @@ -0,0 +1,55 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Lionel Chauvin + SPDX-FileCopyrightText: 2011, 2012 Cédric Bellegarde + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +// Qt +#include + +class QPoint; +class OrgKdeKappmenuInterface; +class QDBusObjectPath; +class QDBusServiceWatcher; + +namespace KWin +{ + +class Window; + +class ApplicationMenu : public QObject +{ + Q_OBJECT + +public: + explicit ApplicationMenu(); + + void showApplicationMenu(const QPoint &pos, Window *c, int actionId); + + bool applicationMenuEnabled() const; + + void setViewEnabled(bool enabled); + +Q_SIGNALS: + void applicationMenuEnabledChanged(bool enabled); + +private Q_SLOTS: + void slotShowRequest(const QString &serviceName, const QDBusObjectPath &menuObjectPath, int actionId); + void slotMenuShown(const QString &serviceName, const QDBusObjectPath &menuObjectPath); + void slotMenuHidden(const QString &serviceName, const QDBusObjectPath &menuObjectPath); + +private: + OrgKdeKappmenuInterface *m_appmenuInterface; + QDBusServiceWatcher *m_kappMenuWatcher; + + Window *findWindowWithApplicationMenu(const QString &serviceName, const QDBusObjectPath &menuObjectPath); + + bool m_applicationMenuEnabled = false; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/atoms.cpp b/local/recipes/kde/kwin/source/src/atoms.cpp new file mode 100644 index 0000000000..31bea40b82 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/atoms.cpp @@ -0,0 +1,90 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "atoms.h" + +namespace KWin +{ + +Atoms::Atoms() + : activities(QByteArrayLiteral("_KDE_NET_WM_ACTIVITIES")) + , wm_protocols(QByteArrayLiteral("WM_PROTOCOLS")) + , wm_delete_window(QByteArrayLiteral("WM_DELETE_WINDOW")) + , wm_take_focus(QByteArrayLiteral("WM_TAKE_FOCUS")) + , wm_change_state(QByteArrayLiteral("WM_CHANGE_STATE")) + , wm_client_leader(QByteArrayLiteral("WM_CLIENT_LEADER")) + , wm_window_role(QByteArrayLiteral("WM_WINDOW_ROLE")) + , wm_state(QByteArrayLiteral("WM_STATE")) + , sm_client_id(QByteArrayLiteral("SM_CLIENT_ID")) + , motif_wm_hints(QByteArrayLiteral("_MOTIF_WM_HINTS")) + , net_wm_context_help(QByteArrayLiteral("_NET_WM_CONTEXT_HELP")) + , net_wm_ping(QByteArrayLiteral("_NET_WM_PING")) + , net_wm_user_time(QByteArrayLiteral("_NET_WM_USER_TIME")) + , kde_net_wm_user_creation_time(QByteArrayLiteral("_KDE_NET_WM_USER_CREATION_TIME")) + , net_wm_take_activity(QByteArrayLiteral("_NET_WM_TAKE_ACTIVITY")) + , net_wm_window_opacity(QByteArrayLiteral("_NET_WM_WINDOW_OPACITY")) + , xdnd_selection(QByteArrayLiteral("XdndSelection")) + , xdnd_aware(QByteArrayLiteral("XdndAware")) + , xdnd_enter(QByteArrayLiteral("XdndEnter")) + , xdnd_type_list(QByteArrayLiteral("XdndTypeList")) + , xdnd_position(QByteArrayLiteral("XdndPosition")) + , xdnd_status(QByteArrayLiteral("XdndStatus")) + , xdnd_action_copy(QByteArrayLiteral("XdndActionCopy")) + , xdnd_action_move(QByteArrayLiteral("XdndActionMove")) + , xdnd_action_ask(QByteArrayLiteral("XdndActionAsk")) + , xdnd_drop(QByteArrayLiteral("XdndDrop")) + , xdnd_leave(QByteArrayLiteral("XdndLeave")) + , xdnd_finished(QByteArrayLiteral("XdndFinished")) + , net_frame_extents(QByteArrayLiteral("_NET_FRAME_EXTENTS")) + , kde_net_wm_frame_strut(QByteArrayLiteral("_KDE_NET_WM_FRAME_STRUT")) + , net_wm_sync_request_counter(QByteArrayLiteral("_NET_WM_SYNC_REQUEST_COUNTER")) + , net_wm_sync_request(QByteArrayLiteral("_NET_WM_SYNC_REQUEST")) + , kde_net_wm_shadow(QByteArrayLiteral("_KDE_NET_WM_SHADOW")) + , kde_color_sheme(QByteArrayLiteral("_KDE_NET_WM_COLOR_SCHEME")) + , kde_skip_close_animation(QByteArrayLiteral("_KDE_NET_WM_SKIP_CLOSE_ANIMATION")) + , utf8_string(QByteArrayLiteral("UTF8_STRING")) + , text(QByteArrayLiteral("TEXT")) + , uri_list(QByteArrayLiteral("text/uri-list")) + , netscape_url(QByteArrayLiteral("_NETSCAPE_URL")) + , moz_url(QByteArrayLiteral("text/x-moz-url")) + , wl_surface_serial(QByteArrayLiteral("WL_SURFACE_SERIAL")) + , kde_net_wm_appmenu_service_name(QByteArrayLiteral("_KDE_NET_WM_APPMENU_SERVICE_NAME")) + , kde_net_wm_appmenu_object_path(QByteArrayLiteral("_KDE_NET_WM_APPMENU_OBJECT_PATH")) + , clipboard(QByteArrayLiteral("CLIPBOARD")) + , timestamp(QByteArrayLiteral("TIMESTAMP")) + , targets(QByteArrayLiteral("TARGETS")) + , multiple(QByteArrayLiteral("MULTIPLE")) + , save_targets(QByteArrayLiteral("SAVE_TARGETS")) + , delete_atom(QByteArrayLiteral("DELETE")) + , incr(QByteArrayLiteral("INCR")) + , wl_selection(QByteArrayLiteral("WL_SELECTION")) + , primary(QByteArrayLiteral("PRIMARY")) + , edid(QByteArrayLiteral("EDID")) + , xwayland_allow_commits(QByteArrayLiteral("_XWAYLAND_ALLOW_COMMITS")) + , xwayland_xrandr_emulation(QByteArrayLiteral("_XWAYLAND_RANDR_EMU_MONITOR_RECTS")) + , m_dtSmWindowInfo(QByteArrayLiteral("_DT_SM_WINDOW_INFO")) + , m_motifSupport(QByteArrayLiteral("_MOTIF_WM_INFO")) + , m_helpersRetrieved(false) +{ +} + +void Atoms::retrieveHelpers() +{ + if (m_helpersRetrieved) { + return; + } + // just retrieve the atoms once, all others are retrieved when being accessed + m_dtSmWindowInfo.getReply(); + m_motifSupport.getReply(); + m_helpersRetrieved = true; +} + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/atoms.h b/local/recipes/kde/kwin/source/src/atoms.h new file mode 100644 index 0000000000..68fe8d1998 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/atoms.h @@ -0,0 +1,102 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "config-kwin.h" + +#if !KWIN_BUILD_X11 +#error Do not include on non-X11 builds +#endif + +#include "utils/xcbutils.h" + +namespace KWin +{ + +class KWIN_EXPORT Atoms +{ +public: + Atoms(); + + Xcb::Atom activities; + + Xcb::Atom wm_protocols; + Xcb::Atom wm_delete_window; + Xcb::Atom wm_take_focus; + Xcb::Atom wm_change_state; + Xcb::Atom wm_client_leader; + Xcb::Atom wm_window_role; + Xcb::Atom wm_state; + Xcb::Atom sm_client_id; + + Xcb::Atom motif_wm_hints; + Xcb::Atom net_wm_context_help; + Xcb::Atom net_wm_ping; + Xcb::Atom net_wm_user_time; + Xcb::Atom kde_net_wm_user_creation_time; + Xcb::Atom net_wm_take_activity; + Xcb::Atom net_wm_window_opacity; + Xcb::Atom xdnd_selection; + Xcb::Atom xdnd_aware; + Xcb::Atom xdnd_enter; + Xcb::Atom xdnd_type_list; + Xcb::Atom xdnd_position; + Xcb::Atom xdnd_status; + Xcb::Atom xdnd_action_copy; + Xcb::Atom xdnd_action_move; + Xcb::Atom xdnd_action_ask; + Xcb::Atom xdnd_drop; + Xcb::Atom xdnd_leave; + Xcb::Atom xdnd_finished; + Xcb::Atom net_frame_extents; + Xcb::Atom kde_net_wm_frame_strut; + Xcb::Atom net_wm_sync_request_counter; + Xcb::Atom net_wm_sync_request; + Xcb::Atom kde_net_wm_shadow; + Xcb::Atom kde_color_sheme; + Xcb::Atom kde_skip_close_animation; + Xcb::Atom utf8_string; + Xcb::Atom text; + Xcb::Atom uri_list; + Xcb::Atom netscape_url; + Xcb::Atom moz_url; + Xcb::Atom wl_surface_serial; + Xcb::Atom kde_net_wm_appmenu_service_name; + Xcb::Atom kde_net_wm_appmenu_object_path; + Xcb::Atom clipboard; + Xcb::Atom timestamp; + Xcb::Atom targets; + Xcb::Atom multiple; + Xcb::Atom save_targets; + Xcb::Atom delete_atom; + Xcb::Atom incr; + Xcb::Atom wl_selection; + Xcb::Atom primary; + Xcb::Atom edid; + Xcb::Atom xwayland_allow_commits; + Xcb::Atom xwayland_xrandr_emulation; + + /** + * @internal + */ + void retrieveHelpers(); + +private: + // helper atoms we need to resolve to "announce" support (see #172028) + Xcb::Atom m_dtSmWindowInfo; + Xcb::Atom m_motifSupport; + bool m_helpersRetrieved; +}; + +extern KWIN_EXPORT Atoms *atoms; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/backends/CMakeLists.txt b/local/recipes/kde/kwin/source/src/backends/CMakeLists.txt new file mode 100644 index 0000000000..e94a1c61fe --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/CMakeLists.txt @@ -0,0 +1,8 @@ +add_subdirectory(drm) +add_subdirectory(fakeinput) +add_subdirectory(libinput) +add_subdirectory(virtual) +add_subdirectory(wayland) +if (X11_XCB_FOUND) + add_subdirectory(x11) +endif() diff --git a/local/recipes/kde/kwin/source/src/backends/drm/CMakeLists.txt b/local/recipes/kde/kwin/source/src/backends/drm/CMakeLists.txt new file mode 100644 index 0000000000..48619e548d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/CMakeLists.txt @@ -0,0 +1,29 @@ +target_sources(kwin PRIVATE + drm_abstract_output.cpp + drm_backend.cpp + drm_blob.cpp + drm_buffer.cpp + drm_colorop.cpp + drm_commit.cpp + drm_commit_thread.cpp + drm_connector.cpp + drm_crtc.cpp + drm_egl_backend.cpp + drm_egl_layer.cpp + drm_egl_layer_surface.cpp + drm_gpu.cpp + drm_layer.cpp + drm_logging.cpp + drm_object.cpp + drm_output.cpp + drm_pipeline.cpp + drm_pipeline_legacy.cpp + drm_plane.cpp + drm_property.cpp + drm_qpainter_backend.cpp + drm_qpainter_layer.cpp + drm_virtual_egl_layer.cpp + drm_virtual_output.cpp +) + +target_link_libraries(kwin PRIVATE gbm::gbm PkgConfig::Libxcvt) diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_abstract_output.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_abstract_output.cpp new file mode 100644 index 0000000000..874925bee2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_abstract_output.cpp @@ -0,0 +1,29 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2021 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_abstract_output.h" +#include "core/renderbackend.h" +#include "drm_backend.h" +#include "drm_gpu.h" +#include "drm_layer.h" + +namespace KWin +{ + +DrmAbstractOutput::DrmAbstractOutput() + : m_renderLoop(std::make_unique(this)) +{ +} + +RenderLoop *DrmAbstractOutput::renderLoop() const +{ + return m_renderLoop.get(); +} +} + +#include "moc_drm_abstract_output.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_abstract_output.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_abstract_output.h new file mode 100644 index 0000000000..92c8a8965e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_abstract_output.h @@ -0,0 +1,34 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2021 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "core/backendoutput.h" + +namespace KWin +{ + +class DrmBackend; +class DrmOutputLayer; +class OutputFrame; + +class DrmAbstractOutput : public BackendOutput +{ + Q_OBJECT +public: + explicit DrmAbstractOutput(); + + RenderLoop *renderLoop() const override; + +protected: + friend class DrmGpu; + + std::unique_ptr m_renderLoop; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_backend.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_backend.cpp new file mode 100644 index 0000000000..a6fabc6ba2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_backend.cpp @@ -0,0 +1,476 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_backend.h" + +#include "config-kwin.h" + +#include "backends/libinput/libinputbackend.h" +#include "core/outputconfiguration.h" +#include "core/session.h" +#include "drm_egl_backend.h" +#include "drm_gpu.h" +#include "drm_layer.h" +#include "drm_logging.h" +#include "drm_output.h" +#include "drm_pipeline.h" +#include "drm_qpainter_backend.h" +#include "drm_render_backend.h" +#include "drm_virtual_output.h" +#include "utils/envvar.h" +#include "utils/udev.h" +// KF5 +#include +#include +// Qt +#include +#include +#include +#include +#include +// system +#include +#include +#include +#include +#include +#include +// drm +#include +#include +#include + +using namespace std::chrono_literals; + +namespace KWin +{ + +static QStringList splitPathList(const QString &input, const QChar delimiter) +{ + QStringList ret; + QString tmp; + for (int i = 0; i < input.size(); i++) { + if (input[i] == delimiter) { + if (i > 0 && input[i - 1] == '\\') { + tmp[tmp.size() - 1] = delimiter; + } else if (!tmp.isEmpty()) { + ret.append(tmp); + tmp = QString(); + } + } else { + tmp.append(input[i]); + } + } + if (!tmp.isEmpty()) { + ret.append(tmp); + } + return ret; +} + +DrmBackend::DrmBackend(Session *session, QObject *parent) + : OutputBackend(parent) + , m_udev(std::make_unique()) + , m_udevMonitor(m_udev->monitor()) + , m_session(session) + , m_explicitGpus(splitPathList(qEnvironmentVariable("KWIN_DRM_DEVICES"), ':')) +{ +} + +DrmBackend::~DrmBackend() = default; + +Session *DrmBackend::session() const +{ + return m_session; +} + +QList DrmBackend::outputs() const +{ + return m_outputs | std::ranges::to>(); +} + +bool DrmBackend::initialize() +{ + connect(m_session, &Session::devicePaused, this, [this](dev_t deviceId) { + if (const auto gpu = findGpu(deviceId)) { + gpu->setActive(false); + } + }); + connect(m_session, &Session::deviceResumed, this, [this](dev_t deviceId) { + if (const auto gpu = findGpu(deviceId); gpu && !gpu->isActive()) { + gpu->setActive(true); + // the output list might've changed while the device was inactive + // note that this might delete the gpu! + updateOutputs(gpu); + } + }); + connect(m_session, &Session::awoke, this, [this]() { + // some drivers for old GPUs have problems after suspend, which + // triggering a modeset works around. + for (const auto &gpu : m_gpus) { + if (gpu->atomicModeSetting()) { + continue; + } + const auto outputs = gpu->drmOutputs(); + for (const auto &output : outputs) { + output->pipeline()->forceLegacyModeset(); + } + } + }); + + if (!m_explicitGpus.isEmpty()) { + for (const QString &fileName : m_explicitGpus) { + addGpu(fileName); + } + } else { + const auto devices = m_udev->listGPUs(); + for (const auto &device : devices) { + if (device->seat() == m_session->seat()) { + addGpu(device->devNode()); + } + } + } + + if (m_gpus.empty()) { + qCWarning(KWIN_DRM) << "No suitable DRM devices have been found"; + return false; + } + + // setup udevMonitor + if (m_udevMonitor) { + m_udevMonitor->filterSubsystemDevType("drm"); + const int fd = m_udevMonitor->fd(); + if (fd != -1) { + m_socketNotifier = std::make_unique(fd, QSocketNotifier::Read); + connect(m_socketNotifier.get(), &QSocketNotifier::activated, this, &DrmBackend::handleUdevEvent); + m_udevMonitor->enable(); + } + } + updateOutputs(); + + if (m_explicitGpus.empty() && m_gpus.size() > 1) { + std::ranges::sort(m_gpus, [](const auto &gpu1, const auto &gpu2) { + const size_t internalOutputs1 = std::ranges::count_if(gpu1->drmOutputs(), &BackendOutput::isInternal); + const size_t internalOutputs2 = std::ranges::count_if(gpu2->drmOutputs(), &BackendOutput::isInternal); + if (internalOutputs1 != internalOutputs2) { + return internalOutputs1 > internalOutputs2; + } + const size_t desktopOutputs1 = std::ranges::count_if(gpu1->drmOutputs(), std::not_fn(&BackendOutput::isNonDesktop)); + const size_t desktopOutputs2 = std::ranges::count_if(gpu2->drmOutputs(), std::not_fn(&BackendOutput::isNonDesktop)); + if (desktopOutputs1 != desktopOutputs2) { + return desktopOutputs1 > desktopOutputs2; + } + return gpu1->drmOutputs().size() > gpu2->drmOutputs().size(); + }); + qCDebug(KWIN_DRM) << "chose" << m_gpus.front()->drmDevice()->path() << "as the primary GPU"; + } + return true; +} + +void DrmBackend::handleUdevEvent() +{ + while (auto device = m_udevMonitor->getDevice()) { + // Ignore the device seat if the KWIN_DRM_DEVICES envvar is set. + if (!m_explicitGpus.isEmpty()) { + const auto canonicalPath = QFileInfo(device->devNode()).canonicalFilePath(); + const bool foundMatch = std::ranges::any_of(m_explicitGpus, [&canonicalPath](const QString &explicitPath) { + return QFileInfo(explicitPath).canonicalFilePath() == canonicalPath; + }); + if (!foundMatch) { + continue; + } + } else { + if (device->seat() != m_session->seat()) { + continue; + } + } + + if (device->action() == QLatin1StringView("add")) { + DrmGpu *gpu = findGpu(device->devNum()); + if (gpu) { + qCWarning(KWIN_DRM) << "Received unexpected add udev event for:" << device->devNode(); + continue; + } + if (DrmGpu *gpu = addGpu(device->devNode())) { + updateOutputs(gpu); + } + } else if (device->action() == QLatin1StringView("remove")) { + DrmGpu *gpu = findGpu(device->devNum()); + if (gpu) { + if (primaryGpu() == gpu) { + qCCritical(KWIN_DRM) << "Primary gpu has been removed! Quitting..."; + QCoreApplication::exit(1); + return; + } else { + gpu->setRemoved(); + updateOutputs(gpu); + } + } + } else if (device->action() == QLatin1StringView("change")) { + DrmGpu *gpu = findGpu(device->devNum()); + if (!gpu) { + gpu = addGpu(device->devNode()); + } + if (gpu && gpu->isActive()) { + qCDebug(KWIN_DRM) << "Received change event for monitored drm device" << gpu->drmDevice()->path(); + updateOutputs(gpu); + } + } + } +} + +DrmGpu *DrmBackend::addGpu(const QString &fileName) +{ + std::expected fd = m_session->openRestricted(fileName); + QElapsedTimer timer; + timer.start(); + // Switching between sessions / drm masters seems to be racy in some situations. + // Lacking a proper solution for that, retry opening the node for up to 5s. + while (!fd.has_value() && fd.error() == Session::Error::EBusy && timer.durationElapsed() < 5s) { + qCDebug(KWIN_DRM, "Retrying openRestricted(%s)", qPrintable(fileName)); + std::this_thread::sleep_for(100ms); + fd = m_session->openRestricted(fileName); + } + if (!fd.has_value()) { + qCWarning(KWIN_DRM, "Failed to open drm device %s", qPrintable(fileName)); + return nullptr; + } + + if (!drmIsKMS(*fd)) { + qCDebug(KWIN_DRM) << "Skipping KMS incapable drm device node at" << fileName; + m_session->closeRestricted(*fd); + return nullptr; + } + + auto drmDevice = DrmDevice::openWithAuthentication(fileName, *fd); + if (!drmDevice) { + m_session->closeRestricted(*fd); + return nullptr; + } + + m_gpus.push_back(std::make_unique(this, *fd, std::move(drmDevice))); + auto gpu = m_gpus.back().get(); + qCDebug(KWIN_DRM, "adding GPU %s", qPrintable(fileName)); + connect(gpu, &DrmGpu::outputAdded, this, &DrmBackend::addOutput); + connect(gpu, &DrmGpu::outputRemoved, this, &DrmBackend::removeOutput); + if (m_renderBackend) { + gpu->createLayers(); + } + Q_EMIT gpuAdded(gpu); + return gpu; +} + +void DrmBackend::addOutput(DrmAbstractOutput *o) +{ + m_outputs.append(o); + Q_EMIT outputAdded(o); +} + +void DrmBackend::removeOutput(DrmAbstractOutput *o) +{ + m_outputs.removeOne(o); + Q_EMIT outputRemoved(o); +} + +void DrmBackend::updateOutputs(DrmGpu *onlyUpdate) +{ + for (auto it = m_gpus.begin(); it != m_gpus.end(); ++it) { + if ((*it)->isRemoved()) { + (*it)->removeOutputs(); + } else if (!onlyUpdate || onlyUpdate == it->get()) { + (*it)->updateOutputs(); + } + } + + Q_EMIT outputsQueried(); + + for (auto it = m_gpus.begin(); it != m_gpus.end();) { + DrmGpu *gpu = it->get(); + if (gpu->isRemoved() || (gpu != primaryGpu() && gpu->drmOutputs().isEmpty())) { + qCDebug(KWIN_DRM) << "Removing GPU" << it->get(); + const std::unique_ptr keepAlive = std::move(*it); + it = m_gpus.erase(it); + Q_EMIT gpuRemoved(keepAlive.get()); + } else { + it++; + } + } +} + +std::unique_ptr DrmBackend::createInputBackend() +{ + return std::make_unique(m_session); +} + +std::unique_ptr DrmBackend::createQPainterBackend() +{ + return std::make_unique(this); +} + +std::unique_ptr DrmBackend::createOpenGLBackend() +{ + return std::make_unique(this); +} + +QList DrmBackend::supportedCompositors() const +{ + return QList{OpenGLCompositing, QPainterCompositing}; +} + +QString DrmBackend::supportInformation() const +{ + QString supportInfo; + QDebug s(&supportInfo); + s.nospace(); + s << "Name: " + << "DRM" << Qt::endl; + for (size_t g = 0; g < m_gpus.size(); g++) { + s << "Atomic Mode Setting on GPU " << g << ": " << m_gpus.at(g)->atomicModeSetting() << Qt::endl; + } + return supportInfo; +} + +BackendOutput *DrmBackend::createVirtualOutput(const QString &name, const QString &description, const QSize &size, double scale) +{ + const auto ret = new DrmVirtualOutput(this, name, description, size, scale); + m_virtualOutputs.push_back(ret); + addOutput(ret); + Q_EMIT outputsQueried(); + return ret; +} + +void DrmBackend::removeVirtualOutput(BackendOutput *output) +{ + auto virtualOutput = qobject_cast(output); + Q_ASSERT(virtualOutput); + if (!m_virtualOutputs.removeOne(virtualOutput)) { + return; + } + removeOutput(virtualOutput); + Q_EMIT outputsQueried(); + virtualOutput->unref(); +} + +DrmGpu *DrmBackend::primaryGpu() const +{ + return m_gpus.empty() ? nullptr : m_gpus.front().get(); +} + +DrmGpu *DrmBackend::findGpu(dev_t deviceId) const +{ + auto it = std::ranges::find_if(m_gpus, [deviceId](const auto &gpu) { + return gpu->drmDevice()->deviceId() == deviceId; + }); + return it == m_gpus.end() ? nullptr : it->get(); +} + +size_t DrmBackend::gpuCount() const +{ + return m_gpus.size(); +} + +OutputConfigurationError DrmBackend::applyOutputChanges(const OutputConfiguration &config) +{ + QList toBeEnabled; + QList toBeDisabled; + for (const auto &gpu : m_gpus) { + const auto outputs = gpu->drmOutputs(); + for (DrmOutput *output : outputs) { + if (output->isNonDesktop()) { + continue; + } + if (const auto changeset = config.constChangeSet(output)) { + output->queueChanges(changeset); + if (changeset->enabled.value_or(output->isEnabled())) { + toBeEnabled << output; + } else { + toBeDisabled << output; + } + } + } + const auto error = gpu->testPendingConfiguration(); + if (error != DrmPipeline::Error::None) { + for (DrmOutput *output : std::as_const(toBeEnabled)) { + output->revertQueuedChanges(); + } + for (DrmOutput *output : std::as_const(toBeDisabled)) { + output->revertQueuedChanges(); + } + if (error == DrmPipeline::Error::NotEnoughCrtcs) { + // TODO make this more specific, this is per GPU! + return OutputConfigurationError::TooManyEnabledOutputs; + } else if (error == DrmPipeline::Error::Timeout) { + return OutputConfigurationError::Timeout; + } else { + return OutputConfigurationError::Unknown; + } + } + } + // first, apply changes to drm outputs. + // This may remove the placeholder output and thus change m_outputs! + for (DrmOutput *output : std::as_const(toBeEnabled)) { + if (const auto changeset = config.constChangeSet(output)) { + output->applyQueuedChanges(changeset); + } + } + for (DrmOutput *output : std::as_const(toBeDisabled)) { + if (const auto changeset = config.constChangeSet(output)) { + output->applyQueuedChanges(changeset); + } + } + for (const auto &gpu : m_gpus) { + gpu->releaseUnusedBuffers(); + } + // only then apply changes to the virtual outputs + for (DrmVirtualOutput *output : std::as_const(m_virtualOutputs)) { + output->applyChanges(config); + } + return OutputConfigurationError::None; +} + +void DrmBackend::setRenderBackend(DrmRenderBackend *backend) +{ + m_renderBackend = backend; +} + +DrmRenderBackend *DrmBackend::renderBackend() const +{ + return m_renderBackend; +} + +void DrmBackend::createLayers() +{ + for (const auto &gpu : m_gpus) { + gpu->createLayers(); + } + for (DrmVirtualOutput *virt : std::as_const(m_virtualOutputs)) { + virt->recreateSurface(); + } +} + +void DrmBackend::releaseBuffers() +{ + for (const auto &gpu : m_gpus) { + gpu->releaseBuffers(); + } + for (const DrmVirtualOutput *virt : std::as_const(m_virtualOutputs)) { + virt->primaryLayer()->releaseBuffers(); + } +} + +const std::vector> &DrmBackend::gpus() const +{ + return m_gpus; +} + +EglDisplay *DrmBackend::sceneEglDisplayObject() const +{ + return m_gpus.front()->eglDisplay(); +} +} + +#include "moc_drm_backend.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_backend.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_backend.h new file mode 100644 index 0000000000..13449a3fe3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_backend.h @@ -0,0 +1,97 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "core/outputbackend.h" + +#include +#include +#include +#include + +#include +#include + +namespace KWin +{ + +class Session; +class Udev; +class UdevMonitor; +class UdevDevice; + +class DrmAbstractOutput; +class Cursor; +class DrmGpu; +class DrmVirtualOutput; +class DrmRenderBackend; + +class KWIN_EXPORT DrmBackend : public OutputBackend +{ + Q_OBJECT + +public: + explicit DrmBackend(Session *session, QObject *parent = nullptr); + ~DrmBackend() override; + + std::unique_ptr createInputBackend() override; + std::unique_ptr createQPainterBackend() override; + std::unique_ptr createOpenGLBackend() override; + EglDisplay *sceneEglDisplayObject() const override; + + bool initialize() override; + + QList outputs() const override; + Session *session() const override; + + QList supportedCompositors() const override; + + QString supportInformation() const override; + BackendOutput *createVirtualOutput(const QString &name, const QString &description, const QSize &size, double scale) override; + void removeVirtualOutput(BackendOutput *output) override; + + DrmGpu *primaryGpu() const; + DrmGpu *findGpu(dev_t deviceId) const; + size_t gpuCount() const; + + void setRenderBackend(DrmRenderBackend *backend); + DrmRenderBackend *renderBackend() const; + + void createLayers(); + void releaseBuffers(); + void updateOutputs(DrmGpu *onlyUpdate = nullptr); + + const std::vector> &gpus() const; + +Q_SIGNALS: + void gpuAdded(DrmGpu *gpu); + void gpuRemoved(DrmGpu *gpu); + +protected: + OutputConfigurationError applyOutputChanges(const OutputConfiguration &config) override; + +private: + friend class DrmGpu; + void addOutput(DrmAbstractOutput *output); + void removeOutput(DrmAbstractOutput *output); + void handleUdevEvent(); + DrmGpu *addGpu(const QString &fileName); + + std::unique_ptr m_udev; + std::unique_ptr m_udevMonitor; + std::unique_ptr m_socketNotifier; + Session *m_session; + QList m_outputs; + + const QStringList m_explicitGpus; + std::vector> m_gpus; + QList m_virtualOutputs; + DrmRenderBackend *m_renderBackend = nullptr; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_blob.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_blob.cpp new file mode 100644 index 0000000000..bf63067b23 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_blob.cpp @@ -0,0 +1,42 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_blob.h" +#include "drm_gpu.h" + +namespace KWin +{ + +DrmBlob::DrmBlob(DrmGpu *gpu, uint32_t blobId) + : m_gpu(gpu) + , m_blobId(blobId) +{ +} + +DrmBlob::~DrmBlob() +{ + if (m_blobId) { + drmModeDestroyPropertyBlob(m_gpu->fd(), m_blobId); + } +} + +uint32_t DrmBlob::blobId() const +{ + return m_blobId; +} + +std::shared_ptr DrmBlob::create(DrmGpu *gpu, const void *data, uint32_t dataSize) +{ + uint32_t id = 0; + if (drmModeCreatePropertyBlob(gpu->fd(), data, dataSize, &id) == 0) { + return std::make_shared(gpu, id); + } else { + return nullptr; + } +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_blob.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_blob.h new file mode 100644 index 0000000000..23d2a70476 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_blob.h @@ -0,0 +1,33 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include +#include + +namespace KWin +{ + +class DrmGpu; + +class DrmBlob +{ +public: + DrmBlob(DrmGpu *gpu, uint32_t blobId); + ~DrmBlob(); + + uint32_t blobId() const; + + static std::shared_ptr create(DrmGpu *gpu, const void *data, uint32_t dataSize); + +protected: + DrmGpu *const m_gpu; + const uint32_t m_blobId; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_buffer.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_buffer.cpp new file mode 100644 index 0000000000..9fb0b3212f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_buffer.cpp @@ -0,0 +1,126 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_buffer.h" + +#include "core/graphicsbuffer.h" +#include "drm_gpu.h" +#include "utils/envvar.h" + +// system +#include +#if defined(Q_OS_LINUX) +#include +#include +#endif +// drm +#include +#include +#include +#include +#include + +namespace KWin +{ + +static std::optional s_disableBufferWait = environmentVariableBoolValue("KWIN_DRM_DISABLE_BUFFER_READABILITY_CHECKS"); + +DrmFramebufferData::DrmFramebufferData(DrmGpu *gpu, uint32_t fbid, GraphicsBuffer *buffer) + : m_gpu(gpu) + , m_framebufferId(fbid) + , m_buffer(buffer) +{ +} + +DrmFramebufferData::~DrmFramebufferData() +{ + if (drmModeCloseFB(m_gpu->fd(), m_framebufferId) != 0) { + drmModeRmFB(m_gpu->fd(), m_framebufferId); + } + if (m_buffer) { + m_gpu->forgetBuffer(m_buffer); + } +} + +DrmFramebuffer::DrmFramebuffer(const std::shared_ptr &data, GraphicsBuffer *buffer, FileDescriptor &&readFence) + : m_data(data) + , m_bufferRef(buffer) +{ + if (s_disableBufferWait.value_or(data->m_gpu->isVmwgfx())) { + // buffer readability checks cause frames to be wrongly delayed on Virtual Machines running vmwgfx + m_readable = true; + } + m_syncFd = std::move(readFence); +#if defined(Q_OS_LINUX) + if (!m_syncFd.isValid()) { + dma_buf_export_sync_file req{ + .flags = DMA_BUF_SYNC_READ, + .fd = -1, + }; + if (drmIoctl(buffer->dmabufAttributes()->fd[0].get(), DMA_BUF_IOCTL_EXPORT_SYNC_FILE, &req) == 0) { + m_syncFd = FileDescriptor{req.fd}; + } + } +#endif +} + +uint32_t DrmFramebuffer::framebufferId() const +{ + return m_data->m_framebufferId; +} + +GraphicsBuffer *DrmFramebuffer::buffer() const +{ + return *m_bufferRef; +} + +void DrmFramebuffer::releaseBuffer() +{ + m_bufferRef = nullptr; +} + +const FileDescriptor &DrmFramebuffer::syncFd() const +{ + return m_syncFd; +} + +bool DrmFramebuffer::isReadable() +{ + if (m_readable) { + return true; + } else if (m_syncFd.isValid()) { + return m_readable = m_syncFd.isReadable(); + } else { + const auto &fds = m_bufferRef->dmabufAttributes()->fd; + m_readable = std::ranges::all_of(fds, [](const auto &fd) { + return !fd.isValid() || fd.isReadable(); + }); + return m_readable; + } +} + +void DrmFramebuffer::setDeadline(std::chrono::steady_clock::time_point deadline) +{ +#ifdef SYNC_IOC_SET_DEADLINE + if (!m_syncFd.isValid()) { + return; + } + sync_set_deadline args{ + .deadline_ns = uint64_t(deadline.time_since_epoch().count()), + .pad = 0, + }; + drmIoctl(m_syncFd.get(), SYNC_IOC_SET_DEADLINE, &args); +#endif +} + +std::shared_ptr DrmFramebuffer::data() const +{ + return m_data; +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_buffer.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_buffer.h new file mode 100644 index 0000000000..4a9aa12b0b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_buffer.h @@ -0,0 +1,61 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "core/graphicsbuffer.h" +#include "utils/filedescriptor.h" + +#include +#include + +namespace KWin +{ + +class DrmGpu; + +class DrmFramebufferData +{ +public: + DrmFramebufferData(DrmGpu *gpu, uint32_t fbid, GraphicsBuffer *buffer); + ~DrmFramebufferData(); + + DrmGpu *const m_gpu; + const uint32_t m_framebufferId; + const QPointer m_buffer; +}; + +class DrmFramebuffer +{ +public: + DrmFramebuffer(const std::shared_ptr &data, GraphicsBuffer *buffer, FileDescriptor &&readFence); + + uint32_t framebufferId() const; + + /** + * may be nullptr + */ + GraphicsBuffer *buffer() const; + + void releaseBuffer(); + bool isReadable(); + + const FileDescriptor &syncFd() const; + void setDeadline(std::chrono::steady_clock::time_point deadline); + + std::shared_ptr data() const; + +protected: + const std::shared_ptr m_data; + GraphicsBufferRef m_bufferRef; + bool m_readable = false; + FileDescriptor m_syncFd; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_colorop.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_colorop.cpp new file mode 100644 index 0000000000..f5dd39dcf0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_colorop.cpp @@ -0,0 +1,591 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2024 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_colorop.h" +#include "drm_blob.h" +#include "drm_commit.h" +#include "drm_gpu.h" +#include "drm_object.h" +#include "utils/envvar.h" + +#include + +namespace KWin +{ + +DrmAbstractColorOp::DrmAbstractColorOp(DrmAbstractColorOp *next, Features features, const QString &name) + : m_next(next) + , m_features(features) + , m_name(name) +{ +} + +DrmAbstractColorOp::~DrmAbstractColorOp() +{ +} + +DrmAbstractColorOp *DrmAbstractColorOp::next() const +{ + return m_next; +} + +static const auto s_disableAmdgpuWorkaround = environmentVariableBoolValue("KWIN_DRM_DISABLE_AMD_GAMMA_WORKAROUND"); + +bool DrmAbstractColorOp::canBypass() const +{ + return m_features & Feature::Bypass; +} + +bool DrmAbstractColorOp::supportsMultipleOps() const +{ + return m_features & Feature::MultipleOps; +} + +QString DrmAbstractColorOp::name() const +{ + return m_name; +} + +bool DrmAbstractColorOp::matchPipeline(DrmAtomicCommit *commit, const ColorPipeline &pipeline) +{ + if (m_cachedPipeline && *m_cachedPipeline == pipeline) { + commit->merge(m_cache.get()); + return true; + } + if (pipeline.isIdentity() && s_disableAmdgpuWorkaround.value_or(!commit->gpu()->isAmdgpu())) { + // Applying this config is very simple and cheap, so do it directly + // and avoid invalidating the cache + DrmAbstractColorOp *currentOp = this; + while (currentOp) { + currentOp->bypass(commit); + currentOp = currentOp->next(); + } + return true; + } + + DrmAbstractColorOp *currentOp = this; + const auto needsLimitedRange = [](const ColorOp &op) { + // KMS LUTs have an input and output range of [0, 1] + return std::holds_alternative(op.operation) + || std::holds_alternative(op.operation); + }; + + // first, only check if the pipeline can be programmed in the first place + // don't calculate LUTs just yet + + std::unordered_map> assignments; + + double valueScaling = 1; + if (!pipeline.ops.empty() && needsLimitedRange(pipeline.ops.front()) && pipeline.ops.front().input.max > 1) { + valueScaling = 1.0 / pipeline.ops.front().input.max; + ColorOp initialOp{ + .input = pipeline.ops.front().input, + .operation = ColorMultiplier{valueScaling}, + .output = ValueRange{ + .min = pipeline.ops.front().input.min * valueScaling, + .max = 1.0, + }, + }; + while (currentOp && !currentOp->canBeUsedFor(initialOp, false)) { + currentOp = currentOp->next(); + } + if (!currentOp) { + return false; + } + assignments[currentOp].push_back(initialOp.operation); + } + auto ops = std::span(pipeline.ops); + while (!ops.empty()) { + while (currentOp && !currentOp->canBeUsedFor(ops.front(), true)) { + currentOp = currentOp->next(); + } + if (!currentOp) { + return false; + } + auto &hwOps = assignments[currentOp]; + if (valueScaling != 1) { + // un-do the previous scaling, so we get the original values out again + hwOps.push_back(ColorMultiplier{1.0 / valueScaling}); + } + hwOps.push_back(ops.front().operation); + if (ops.size() > 1 && ops.front().output.max > 1 && (needsLimitedRange(ops[0]) || needsLimitedRange(ops[1]))) { + // scale values so that LUTs with their [0; 1] range can represent the values + valueScaling = 1.0 / ops.front().output.max; + hwOps.push_back(ColorMultiplier{valueScaling}); + } else { + valueScaling = 1; + } + ops = ops.subspan(1); + } + + // now actually program the properties + currentOp = this; + m_cache = std::make_unique(commit->gpu()); + currentOp = this; + while (currentOp) { + const auto it = assignments.find(currentOp); + if (it != assignments.end()) { + const auto &[op, program] = *it; + currentOp->program(m_cache.get(), program); + } else { + currentOp->bypass(m_cache.get()); + } + currentOp = currentOp->next(); + } + commit->merge(m_cache.get()); + m_cachedPipeline = pipeline; + return true; +} + +DrmLutColorOp16::DrmLutColorOp16(DrmAbstractColorOp *next, DrmProperty *prop, DrmEnumProperty *interpolation, uint32_t maxSize, DrmProperty *bypass) + : DrmAbstractColorOp(next, Features{Feature::MultipleOps} | Feature::Bypass, QStringLiteral("1D LUT")) + , m_prop(prop) + , m_bypass(bypass) + , m_interpolationMode(interpolation) + , m_maxSize(maxSize) + , m_components(m_maxSize) +{ +} + +bool DrmLutColorOp16::canBeUsedFor(const ColorOp &op, bool normalizedInput) +{ + constexpr double eta = 0.0001; + // if normalizedInput is true, we can assume the input to be bounded to [0; 1] already + if ((!normalizedInput && op.input.max > 1 + eta) || op.input.min < -eta) { + return false; + } + if (std::holds_alternative(op.operation) || std::holds_alternative(op.operation) + || std::holds_alternative(op.operation) || std::holds_alternative>(op.operation)) { + // the required resolution depends heavily on the function and on the input and output ranges / multipliers + // but this is good enough for now + return m_maxSize >= 256; + } + return std::holds_alternative(op.operation) + || std::holds_alternative(op.operation); +} + +void DrmLutColorOp16::program(DrmAtomicCommit *commit, std::span operations) +{ + for (uint32_t i = 0; i < m_maxSize; i++) { + const double input = i / double(m_maxSize - 1); + QVector3D output(input, input, input); + for (const auto &op : operations) { + output = ColorOp::applyOperation(op, output); + } + m_components[i] = { + .red = uint16_t(std::round(std::clamp(output.x(), 0.0f, 1.0f) * std::numeric_limits::max())), + .green = uint16_t(std::round(std::clamp(output.y(), 0.0f, 1.0f) * std::numeric_limits::max())), + .blue = uint16_t(std::round(std::clamp(output.z(), 0.0f, 1.0f) * std::numeric_limits::max())), + .reserved = 0, + }; + } + commit->addBlob(*m_prop, DrmBlob::create(m_prop->drmObject()->gpu(), m_components.data(), sizeof(drm_color_lut) * m_maxSize)); + if (m_bypass) { + commit->addProperty(*m_bypass, 0); + } +} + +void DrmLutColorOp16::bypass(DrmAtomicCommit *commit) +{ + commit->addBlob(*m_prop, nullptr); + if (m_bypass) { + commit->addProperty(*m_bypass, 1); + } +} + +DrmLutColorOp32::DrmLutColorOp32(DrmAbstractColorOp *next, DrmProperty *prop, DrmEnumProperty *interpolation, uint32_t maxSize, DrmProperty *bypass) + : DrmAbstractColorOp(next, Features{Feature::MultipleOps} | Feature::Bypass, QStringLiteral("1D LUT")) + , m_prop(prop) + , m_bypass(bypass) + , m_interpolationMode(interpolation) + , m_maxSize(maxSize) + , m_components(m_maxSize) +{ +} + +bool DrmLutColorOp32::canBeUsedFor(const ColorOp &op, bool normalizedInput) +{ + constexpr double eta = 0.0001; + // if normalizedInput is true, we can assume the input to be bounded to [0; 1] already + if ((!normalizedInput && op.input.max > 1 + eta) || op.input.min < -eta) { + return false; + } + if (std::holds_alternative(op.operation) || std::holds_alternative(op.operation) + || std::holds_alternative(op.operation) || std::holds_alternative>(op.operation)) { + // the required resolution depends heavily on the function and on the input and output ranges / multipliers + // but this is good enough for now + return m_maxSize >= 256; + } + return std::holds_alternative(op.operation) + || std::holds_alternative(op.operation); +} + +void DrmLutColorOp32::program(DrmAtomicCommit *commit, std::span operations) +{ + for (uint32_t i = 0; i < m_maxSize; i++) { + const double input = i / double(m_maxSize - 1); + QVector3D output(input, input, input); + for (const auto &op : operations) { + output = ColorOp::applyOperation(op, output); + } + m_components[i] = { + .red = uint32_t(std::round(std::clamp(output.x(), 0.0, 1.0) * std::numeric_limits::max())), + .green = uint32_t(std::round(std::clamp(output.y(), 0.0, 1.0) * std::numeric_limits::max())), + .blue = uint32_t(std::round(std::clamp(output.z(), 0.0, 1.0) * std::numeric_limits::max())), + .reserved = 0, + }; + } + commit->addBlob(*m_prop, DrmBlob::create(m_prop->drmObject()->gpu(), m_components.data(), sizeof(LutComponent32) * m_maxSize)); + if (m_bypass) { + commit->addProperty(*m_bypass, 0); + } +} + +void DrmLutColorOp32::bypass(DrmAtomicCommit *commit) +{ + commit->addBlob(*m_prop, nullptr); + if (m_bypass) { + commit->addProperty(*m_bypass, 1); + } +} + +LegacyMatrixColorOp::LegacyMatrixColorOp(DrmAbstractColorOp *next, DrmProperty *prop) + : DrmAbstractColorOp(next, Features{Feature::Bypass} | Feature::MultipleOps, QStringLiteral("legacy matrix")) + , m_prop(prop) +{ +} + +bool LegacyMatrixColorOp::canBeUsedFor(const ColorOp &op, bool normalizedInput) +{ + // this isn't necessarily true, but let's keep things simple for now + if (auto matrix = std::get_if(&op.operation)) { + return std::abs(matrix->mat(3, 0) - 0) < ColorPipeline::s_maxResolution + && std::abs(matrix->mat(3, 1) - 0) < ColorPipeline::s_maxResolution + && std::abs(matrix->mat(3, 2) - 0) < ColorPipeline::s_maxResolution + && std::abs(matrix->mat(3, 3) - 1) < ColorPipeline::s_maxResolution + && std::abs(matrix->mat(0, 3) - 0) < ColorPipeline::s_maxResolution + && std::abs(matrix->mat(1, 3) - 0) < ColorPipeline::s_maxResolution + && std::abs(matrix->mat(2, 3) - 0) < ColorPipeline::s_maxResolution; + } else if (std::holds_alternative(op.operation)) { + return true; + } + return false; +} + +static uint64_t doubleToFixed(double value) +{ + // ctm values are in S31.32 sign-magnitude format + uint64_t ret = std::abs(value) * (1ull << 32); + if (value < 0) { + ret |= 1ull << 63; + } + return ret; +} + +void LegacyMatrixColorOp::program(DrmAtomicCommit *commit, std::span operations) +{ + // NOTE that matrix operations have to be added in reverse order to get the correct result! + QMatrix4x4 result; + for (const auto &op : operations | std::views::reverse) { + if (auto matrix = std::get_if(&op)) { + result *= matrix->mat; + } else if (auto mult = std::get_if(&op)) { + result.scale(mult->factors.x(), mult->factors.y(), mult->factors.z()); + } else { + Q_UNREACHABLE(); + } + } + drm_color_ctm data = { + .matrix = { + doubleToFixed(result(0, 0)), doubleToFixed(result(0, 1)), doubleToFixed(result(0, 2)), // + doubleToFixed(result(1, 0)), doubleToFixed(result(1, 1)), doubleToFixed(result(1, 2)), // + doubleToFixed(result(2, 0)), doubleToFixed(result(2, 1)), doubleToFixed(result(2, 2)), // + }, + }; + commit->addBlob(*m_prop, DrmBlob::create(m_prop->drmObject()->gpu(), &data, sizeof(data))); +} + +void LegacyMatrixColorOp::bypass(DrmAtomicCommit *commit) +{ + commit->addBlob(*m_prop, nullptr); +} + +Matrix3x4ColorOp::Matrix3x4ColorOp(DrmAbstractColorOp *next, DrmProperty *prop, DrmProperty *bypass) + : DrmAbstractColorOp(next, Features{Feature::MultipleOps} | (bypass ? Feature::Bypass : Features{}), QStringLiteral("3x4 matrix")) + , m_prop(prop) + , m_bypass(bypass) +{ +} + +bool Matrix3x4ColorOp::canBeUsedFor(const ColorOp &op, bool normalizedInput) +{ + // TODO check the resolution of the matrix too? + // -> values above abs(5) should be re-scaled with a different (previous?) operation + if (auto matrix = std::get_if(&op.operation)) { + return matrix->mat(3, 0) == 0 + && matrix->mat(3, 1) == 0 + && matrix->mat(3, 2) == 0 + && matrix->mat(3, 3) == 1; + } else if (std::holds_alternative(op.operation)) { + return true; + } + return false; +} + +void Matrix3x4ColorOp::program(DrmAtomicCommit *commit, std::span operations) +{ + // NOTE that matrix operations have to be added in reverse order to get the correct result! + QMatrix4x4 result; + for (const auto &op : operations | std::views::reverse) { + if (auto matrix = std::get_if(&op)) { + result *= matrix->mat; + } else if (auto mult = std::get_if(&op)) { + result.scale(mult->factors.x(), mult->factors.y(), mult->factors.z()); + } else { + Q_UNREACHABLE(); + } + } + std::array data = { + doubleToFixed(result(0, 0)), doubleToFixed(result(0, 1)), doubleToFixed(result(0, 2)), doubleToFixed(result(0, 3)), // + doubleToFixed(result(1, 0)), doubleToFixed(result(1, 1)), doubleToFixed(result(1, 2)), doubleToFixed(result(1, 3)), // + doubleToFixed(result(2, 0)), doubleToFixed(result(2, 1)), doubleToFixed(result(2, 2)), doubleToFixed(result(2, 3)), // + }; + commit->addBlob(*m_prop, DrmBlob::create(m_prop->drmObject()->gpu(), data.data(), sizeof(uint64_t) * data.size())); + if (m_bypass) { + commit->addProperty(*m_bypass, 0); + } +} + +void Matrix3x4ColorOp::bypass(DrmAtomicCommit *commit) +{ + commit->addProperty(*m_bypass, 1); +} + +UnknownColorOp::UnknownColorOp(DrmAbstractColorOp *next, DrmProperty *bypass) + : DrmAbstractColorOp(next, bypass ? Feature::Bypass : Features{}, "unknown") + , m_bypass(bypass) +{ +} + +bool UnknownColorOp::canBeUsedFor(const ColorOp &op, bool normalizedInput) +{ + return false; +} + +void UnknownColorOp::program(DrmAtomicCommit *commit, std::span operations) +{ + Q_UNREACHABLE(); +} + +void UnknownColorOp::bypass(DrmAtomicCommit *commit) +{ + commit->addProperty(*m_bypass, 1); +} + +DrmLut3DColorOp::DrmLut3DColorOp(DrmAbstractColorOp *next, DrmProperty *value, DrmProperty *bypass, size_t size, DrmEnumProperty *interpolation) + : DrmAbstractColorOp(next, Features{Feature::MultipleOps} | (bypass ? Feature::Bypass : Features{}), QStringLiteral("3D LUT")) + , m_value(value) + , m_bypass(bypass) + , m_size(size) + , m_interpolation(interpolation) + , m_components(m_size * m_size * m_size) +{ +} + +void DrmLut3DColorOp::program(DrmAtomicCommit *commit, std::span operations) +{ + for (size_t r = 0; r < m_size; r++) { + for (size_t g = 0; g < m_size; g++) { + for (size_t b = 0; b < m_size; b++) { + QVector3D output = QVector3D(r, g, b) / float(m_size - 1); + for (const auto &op : operations) { + output = ColorOp::applyOperation(op, output); + } + const size_t index = b * m_size * m_size + g * m_size + r; + m_components[index] = LutComponent32{ + .red = uint32_t(std::round(std::clamp(output.x(), 0.0, 1.0) * std::numeric_limits::max())), + .green = uint32_t(std::round(std::clamp(output.y(), 0.0, 1.0) * std::numeric_limits::max())), + .blue = uint32_t(std::round(std::clamp(output.z(), 0.0, 1.0) * std::numeric_limits::max())), + .reserved = 0, + }; + } + } + } + commit->addBlob(*m_value, DrmBlob::create(m_value->drmObject()->gpu(), m_components.data(), m_components.size() * sizeof(LutComponent32))); + if (m_bypass) { + commit->addProperty(*m_bypass, 0); + } +} + +void DrmLut3DColorOp::bypass(DrmAtomicCommit *commit) +{ + commit->addProperty(*m_bypass, 1); +} + +bool DrmLut3DColorOp::canBeUsedFor(const ColorOp &op, bool normalizedInput) +{ + constexpr double eta = 0.0001; + // if normalizedInput is true, we can assume the input to be bounded to [0; 1] already + if ((!normalizedInput && op.input.max > 1 + eta) || op.input.min < -eta) { + return false; + } + // restricted to simple multipliers and clamps for now, + // as everything else requires more effort to be yield good results + return std::holds_alternative(op.operation) + || std::holds_alternative(op.operation); +} + +DrmMultiplier::DrmMultiplier(DrmAbstractColorOp *next, DrmProperty *value, DrmProperty *bypass) + : DrmAbstractColorOp(next, Features{Feature::MultipleOps} | (bypass ? Feature::Bypass : Features{}), QStringLiteral("multiplier")) + , m_value(value) + , m_bypass(bypass) +{ +} + +static float commonScaling(const QMatrix4x4 &mat) +{ + return std::min({mat(0, 0), mat(1, 1), mat(2, 2)}); +} + +bool DrmMultiplier::canBeUsedFor(const ColorOp &op, bool normalizedInput) +{ + if (const auto mult = std::get_if(&op.operation)) { + const float diff1 = std::abs(mult->factors.x() - mult->factors.y()); + const float diff2 = std::abs(mult->factors.y() - mult->factors.z()); + const float diff3 = std::abs(mult->factors.z() - mult->factors.x()); + const float maxDiff = std::max({diff1, diff2, diff3}); + return maxDiff < 0.00001; + } + return false; +} + +void DrmMultiplier::program(DrmAtomicCommit *commit, std::span operations) +{ + double factor = 1; + for (const auto &op : operations) { + if (const auto mult = std::get_if(&op)) { + factor *= mult->factors.x(); + } else if (const auto mat = std::get_if(&op)) { + factor *= commonScaling(mat->mat); + } + } + commit->addProperty(*m_value, doubleToFixed(factor)); + if (m_bypass) { + commit->addProperty(*m_bypass, 0); + } +} + +void DrmMultiplier::bypass(DrmAtomicCommit *commit) +{ + commit->addProperty(*m_bypass, 1); +} + +DrmColorOp::DrmColorOp(DrmGpu *gpu, uint32_t objectId) + : DrmObject(gpu, objectId, DRM_MODE_OBJECT_ANY) + , m_next(this, QByteArrayLiteral("NEXT")) + , m_type(this, QByteArrayLiteral("TYPE"), { + QByteArrayLiteral("1D LUT"), + QByteArrayLiteral("3x4 Matrix"), + QByteArrayLiteral("3D LUT"), + QByteArrayLiteral("Multiplier"), + }) + , m_data(this, QByteArrayLiteral("DATA")) + , m_size(this, QByteArrayLiteral("SIZE")) + , m_bypass(this, QByteArrayLiteral("BYPASS")) + , m_multiplier(this, QByteArrayLiteral("MULTIPLIER")) + , m_lut1dInterpolation(this, QByteArrayLiteral("LUT1D_INTERPOLATION"), {QByteArrayLiteral("Linear")}) + , m_lut3dInterpolation(this, QByteArrayLiteral("LUT3D_INTERPOLATION"), {QByteArrayLiteral("Tetrahedal")}) +{ +} + +bool DrmColorOp::init() +{ + return updateProperties(); +} + +DrmAbstractColorOp *DrmColorOp::colorOp() const +{ + return m_op.get(); +} + +void DrmColorOp::disable(DrmAtomicCommit *commit) +{ + Q_UNREACHABLE(); +} + +bool DrmColorOp::updateProperties() +{ + DrmPropertyList props = queryProperties(); + m_next.update(props); + m_type.update(props); + m_data.update(props); + m_size.update(props); + m_bypass.update(props); + m_multiplier.update(props); + m_lut1dInterpolation.update(props); + m_lut3dInterpolation.update(props); + + if (!m_type.isValid()) { + return false; + } + if (m_next.value() != 0) { + if (!m_nextOp) { + m_nextOp = std::make_unique(gpu(), m_next.value()); + if (!m_nextOp->init()) { + return false; + } + } else if (!m_nextOp->updateProperties()) { + return false; + } + } + + if (m_op) { + return true; + } + const auto next = m_nextOp ? m_nextOp->colorOp() : nullptr; + const auto bypassProp = m_bypass.isValid() ? &m_bypass : nullptr; + if (!m_type.hasEnumForValue(m_type.value())) { + // we don't know this color operation type (yet) + // but we have a bypass, so it's fine + m_op = std::make_unique(next, bypassProp); + qCDebug(KWIN_DRM, "skipping unknown color op %lu", m_type.value()); + return true; + } + switch (m_type.enumValue()) { + case Type::Lut1D: + if (!m_size.isValid() || !m_size.value()) { + qCWarning(KWIN_DRM, "skipping 1D lut with invalid size property"); + m_op = std::make_unique(next, bypassProp); + return true; + } + m_op = std::make_unique(next, &m_data, &m_lut1dInterpolation, m_size.value(), bypassProp); + return true; + case Type::Matrix3x4: + m_op = std::make_unique(next, &m_data, bypassProp); + return true; + case Type::Lut3D: { + if (!m_size.isValid() || !m_size.value()) { + qCDebug(KWIN_DRM, "skipping 3D lut with invalid mode index, mode blob or exclusively unsupported modes"); + m_op = std::make_unique(next, bypassProp); + return true; + } + m_op = std::make_unique(next, &m_data, bypassProp, m_size.value(), &m_lut3dInterpolation); + return true; + } + case Type::Multiplier: + if (!m_multiplier.isValid()) { + qCWarning(KWIN_DRM, "Skipping multiplier with invalid multiplier property"); + m_op = std::make_unique(next, bypassProp); + return true; + } + m_op = std::make_unique(next, &m_multiplier, bypassProp); + return true; + } + Q_UNREACHABLE(); +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_colorop.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_colorop.h new file mode 100644 index 0000000000..51eb0a494d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_colorop.h @@ -0,0 +1,215 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2024 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "core/colorpipeline.h" +#include "drm_object.h" + +#include +#include +#include +#include +#include + +namespace KWin +{ + +class DrmBlob; +class DrmProperty; +class DrmAtomicCommit; + +class DrmAbstractColorOp +{ +public: + enum class Feature { + Bypass = 1 << 0, + MultipleOps = 1 << 1, + }; + Q_DECLARE_FLAGS(Features, Feature); + + enum class Priority { + Low = 0, + Normal = 1, + High = 2, + }; + + explicit DrmAbstractColorOp(DrmAbstractColorOp *next, Features features, const QString &name); + virtual ~DrmAbstractColorOp(); + + bool matchPipeline(DrmAtomicCommit *commit, const ColorPipeline &pipeline); + virtual void program(DrmAtomicCommit *commit, std::span operations) = 0; + virtual void bypass(DrmAtomicCommit *commit) = 0; + virtual bool canBeUsedFor(const ColorOp &op, bool normalizedInput) = 0; + + DrmAbstractColorOp *next() const; + bool canBypass() const; + bool supportsMultipleOps() const; + QString name() const; + +protected: + DrmAbstractColorOp *const m_next; + const Features m_features; + const QString m_name; + + std::optional m_cachedPipeline; + std::optional m_cachedPipelineFail; + std::unique_ptr m_cache; +}; + +enum class Lut1DInterpolation { + Linear, +}; + +class DrmLutColorOp16 : public DrmAbstractColorOp +{ +public: + explicit DrmLutColorOp16(DrmAbstractColorOp *next, DrmProperty *prop, DrmEnumProperty *interpolation, uint32_t maxSize, DrmProperty *bypass); + + void program(DrmAtomicCommit *commit, std::span operations) override; + void bypass(DrmAtomicCommit *commit) override; + bool canBeUsedFor(const ColorOp &op, bool normalizedInput) override; + +private: + DrmProperty *const m_prop; + DrmProperty *const m_bypass; + DrmEnumProperty *const m_interpolationMode; + const uint32_t m_maxSize; + QList m_components; +}; + +// TODO replace with drm_color_lut_32 once we can rely on it +struct LutComponent32 +{ + uint32_t red; + uint32_t green; + uint32_t blue; + uint32_t reserved; +}; + +class DrmLutColorOp32 : public DrmAbstractColorOp +{ +public: + explicit DrmLutColorOp32(DrmAbstractColorOp *next, DrmProperty *prop, DrmEnumProperty *interpolation, uint32_t maxSize, DrmProperty *bypass); + + void program(DrmAtomicCommit *commit, std::span operations) override; + void bypass(DrmAtomicCommit *commit) override; + bool canBeUsedFor(const ColorOp &op, bool normalizedInput) override; + +private: + DrmProperty *const m_prop; + DrmProperty *const m_bypass; + DrmEnumProperty *const m_interpolationMode; + const uint32_t m_maxSize; + QList m_components; +}; + +class LegacyMatrixColorOp : public DrmAbstractColorOp +{ +public: + explicit LegacyMatrixColorOp(DrmAbstractColorOp *next, DrmProperty *prop); + + void program(DrmAtomicCommit *commit, std::span operations) override; + void bypass(DrmAtomicCommit *commit) override; + bool canBeUsedFor(const ColorOp &op, bool normalizedInput) override; + +private: + DrmProperty *const m_prop; +}; + +class Matrix3x4ColorOp : public DrmAbstractColorOp +{ +public: + explicit Matrix3x4ColorOp(DrmAbstractColorOp *next, DrmProperty *prop, DrmProperty *bypass); + + void program(DrmAtomicCommit *commit, std::span operations) override; + void bypass(DrmAtomicCommit *commit) override; + bool canBeUsedFor(const ColorOp &op, bool normalizedInput) override; + +private: + DrmProperty *const m_prop; + DrmProperty *const m_bypass; +}; + +class UnknownColorOp : public DrmAbstractColorOp +{ +public: + explicit UnknownColorOp(DrmAbstractColorOp *next, DrmProperty *bypass); + + void program(DrmAtomicCommit *commit, std::span operations) override; + void bypass(DrmAtomicCommit *commit) override; + bool canBeUsedFor(const ColorOp &op, bool normalizedInput) override; + +private: + DrmProperty *const m_bypass; +}; + +enum class Lut3DInterpolation { + Tetrahedal = 0, +}; + +class DrmLut3DColorOp : public DrmAbstractColorOp +{ +public: + explicit DrmLut3DColorOp(DrmAbstractColorOp *next, DrmProperty *value, DrmProperty *bypass, size_t size, DrmEnumProperty *interpolation); + + void program(DrmAtomicCommit *commit, std::span operations) override; + void bypass(DrmAtomicCommit *commit) override; + bool canBeUsedFor(const ColorOp &op, bool normalizedInput) override; + +private: + DrmProperty *const m_value; + DrmProperty *const m_bypass; + const size_t m_size; + DrmEnumProperty *const m_interpolation; + QList m_components; +}; + +class DrmMultiplier : public DrmAbstractColorOp +{ +public: + explicit DrmMultiplier(DrmAbstractColorOp *next, DrmProperty *value, DrmProperty *bypass); + + void program(DrmAtomicCommit *commit, std::span operations) override; + void bypass(DrmAtomicCommit *commit) override; + bool canBeUsedFor(const ColorOp &op, bool normalizedInput) override; + +private: + DrmProperty *const m_value; + DrmProperty *const m_bypass; +}; + +class DrmColorOp : public DrmObject +{ +public: + explicit DrmColorOp(DrmGpu *gpu, uint32_t objectId); + + bool init(); + void disable(DrmAtomicCommit *commit) override; + bool updateProperties() override; + + DrmAbstractColorOp *colorOp() const; + +private: + enum class Type : uint64_t { + Lut1D, + Matrix3x4, + Lut3D, + Multiplier, + }; + DrmProperty m_next; + DrmEnumProperty m_type; + DrmProperty m_data; + DrmProperty m_size; + DrmProperty m_bypass; + DrmProperty m_multiplier; + DrmEnumProperty m_lut1dInterpolation; + DrmEnumProperty m_lut3dInterpolation; + std::unique_ptr m_op; + std::unique_ptr m_nextOp; +}; +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_commit.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_commit.cpp new file mode 100644 index 0000000000..c0aa9dd76b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_commit.cpp @@ -0,0 +1,322 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_commit.h" +#include "core/renderbackend.h" +#include "drm_blob.h" +#include "drm_buffer.h" +#include "drm_connector.h" +#include "drm_crtc.h" +#include "drm_gpu.h" +#include "drm_object.h" +#include "drm_property.h" + +#include +#include +#include + +using namespace std::chrono_literals; + +namespace KWin +{ + +DrmCommit::DrmCommit(DrmGpu *gpu) + : m_gpu(gpu) +{ +} + +DrmCommit::~DrmCommit() +{ + Q_ASSERT(QThread::currentThread() == QCoreApplication::instance()->thread()); +} + +DrmGpu *DrmCommit::gpu() const +{ + return m_gpu; +} + +void DrmCommit::setDefunct() +{ + m_defunct = true; +} + +DrmAtomicCommit::DrmAtomicCommit(DrmGpu *gpu) + : DrmCommit(gpu) +{ +} + +DrmAtomicCommit::DrmAtomicCommit(const QList &pipelines) + : DrmCommit(pipelines.front()->gpu()) + , m_pipelines(pipelines) +{ +} + +void DrmAtomicCommit::addProperty(const DrmProperty &prop, uint64_t value) +{ + if (Q_UNLIKELY(!prop.isValid())) { + qCWarning(KWIN_DRM) << "Trying to add an invalid property" << prop.name(); + return; + } + prop.checkValueInRange(value); + m_properties[prop.drmObject()->id()][prop.propId()] = value; +} + +void DrmAtomicCommit::addBlob(const DrmProperty &prop, const std::shared_ptr &blob) +{ + addProperty(prop, blob ? blob->blobId() : 0); + m_blobs[&prop] = blob; +} + +void DrmAtomicCommit::addBuffer(DrmPlane *plane, const std::shared_ptr &buffer, const std::shared_ptr &frame) +{ + addProperty(plane->fbId, buffer ? buffer->framebufferId() : 0); + m_buffers[plane] = buffer; + m_frames[plane] = frame; + // atomic commits with IN_FENCE_FD fail with NVidia and (as of kernel 6.9) with tearing + if (plane->inFenceFd.isValid() && !plane->gpu()->isNVidia() && !isTearing()) { + addProperty(plane->inFenceFd, buffer ? buffer->syncFd().get() : -1); + } + m_planes.emplace(plane); + if (frame) { + if (m_targetPageflipTime) { + m_targetPageflipTime = std::min(*m_targetPageflipTime, frame->targetPageflipTime()); + } else { + m_targetPageflipTime = frame->targetPageflipTime(); + } + } +} + +void DrmAtomicCommit::setVrr(DrmCrtc *crtc, bool vrr) +{ + addProperty(crtc->vrrEnabled, vrr ? 1 : 0); + m_vrr = vrr; +} + +void DrmAtomicCommit::setPresentationMode(PresentationMode mode) +{ + m_mode = mode; +} + +bool DrmAtomicCommit::test() +{ + uint32_t flags = DRM_MODE_ATOMIC_TEST_ONLY | DRM_MODE_ATOMIC_NONBLOCK; + if (isTearing()) { + flags |= DRM_MODE_PAGE_FLIP_ASYNC; + } + return doCommit(flags); +} + +bool DrmAtomicCommit::testAllowModeset() +{ + return doCommit(DRM_MODE_ATOMIC_TEST_ONLY | DRM_MODE_ATOMIC_ALLOW_MODESET); +} + +bool DrmAtomicCommit::commit() +{ + uint32_t flags = DRM_MODE_ATOMIC_NONBLOCK | DRM_MODE_PAGE_FLIP_EVENT; + if (isTearing()) { + flags |= DRM_MODE_PAGE_FLIP_ASYNC; + } + return doCommit(flags); +} + +bool DrmAtomicCommit::commitModeset() +{ + m_modeset = true; + return doCommit(DRM_MODE_ATOMIC_ALLOW_MODESET); +} + +bool DrmAtomicCommit::doCommit(uint32_t flags) +{ + std::vector objects; + std::vector propertyCounts; + std::vector propertyIds; + std::vector values; + objects.reserve(m_properties.size()); + propertyCounts.reserve(m_properties.size()); + uint64_t totalPropertiesCount = 0; + for (const auto &[object, properties] : m_properties) { + objects.push_back(object); + propertyCounts.push_back(properties.size()); + totalPropertiesCount += properties.size(); + } + propertyIds.reserve(totalPropertiesCount); + values.reserve(totalPropertiesCount); + for (const auto &[object, properties] : m_properties) { + for (const auto &[property, value] : properties) { + propertyIds.push_back(property); + values.push_back(value); + } + } + drm_mode_atomic commitData{ + .flags = flags, + .count_objs = uint32_t(objects.size()), + .objs_ptr = reinterpret_cast(objects.data()), + .count_props_ptr = reinterpret_cast(propertyCounts.data()), + .props_ptr = reinterpret_cast(propertyIds.data()), + .prop_values_ptr = reinterpret_cast(values.data()), + .reserved = 0, + .user_data = reinterpret_cast(this), + }; + return drmIoctl(m_gpu->fd(), DRM_IOCTL_MODE_ATOMIC, &commitData) == 0; +} + +void DrmAtomicCommit::pageFlipped(std::chrono::nanoseconds timestamp) +{ + Q_ASSERT(QThread::currentThread() == QCoreApplication::instance()->thread()); + for (const auto &[plane, buffer] : m_buffers) { + plane->setCurrentBuffer(buffer); + } + if (m_defunct) { + return; + } + // de-duplicate frames, so that two planes committed + // together don't cause problems + std::set frames; + for (const auto &[plane, frame] : m_frames) { + if (frame) { + frames.insert(frame.get()); + } + } + for (const auto &frame : frames) { + frame->presented(timestamp, m_mode); + } + m_frames.clear(); + for (const auto pipeline : std::as_const(m_pipelines)) { + pipeline->pageFlipped(timestamp); + } +} + +bool DrmAtomicCommit::areBuffersReadable() const +{ + return std::ranges::all_of(m_buffers, [](const auto &pair) { + const auto &[plane, buffer] = pair; + return !buffer || buffer->isReadable(); + }); +} + +void DrmAtomicCommit::setDeadline(std::chrono::steady_clock::time_point deadline) +{ + for (const auto &[plane, buffer] : m_buffers) { + if (buffer) { + buffer->setDeadline(deadline); + } + } +} + +std::optional DrmAtomicCommit::isVrr() const +{ + return m_vrr; +} + +const std::unordered_set &DrmAtomicCommit::modifiedPlanes() const +{ + return m_planes; +} + +void DrmAtomicCommit::merge(DrmAtomicCommit *onTop) +{ + for (const auto &[obj, properties] : onTop->m_properties) { + auto &ownProperties = m_properties[obj]; + for (const auto &[prop, value] : properties) { + ownProperties[prop] = value; + } + } + for (const auto &[plane, buffer] : onTop->m_buffers) { + m_buffers[plane] = buffer; + m_frames[plane] = onTop->m_frames[plane]; + m_planes.emplace(plane); + } + for (const auto &[prop, blob] : onTop->m_blobs) { + m_blobs[prop] = blob; + } + if (onTop->m_vrr) { + m_vrr = onTop->m_vrr; + } + if (!m_targetPageflipTime) { + m_targetPageflipTime = onTop->m_targetPageflipTime; + } else if (onTop->m_targetPageflipTime) { + *m_targetPageflipTime = std::min(*m_targetPageflipTime, *onTop->m_targetPageflipTime); + } + if (m_allowedVrrDelay && onTop->m_allowedVrrDelay) { + *m_allowedVrrDelay = std::min(*m_allowedVrrDelay, *onTop->m_allowedVrrDelay); + } else { + m_allowedVrrDelay.reset(); + } +} + +void DrmAtomicCommit::setAllowedVrrDelay(std::optional allowedDelay) +{ + m_allowedVrrDelay = allowedDelay; +} + +std::optional DrmAtomicCommit::allowedVrrDelay() const +{ + return m_allowedVrrDelay; +} + +std::optional DrmAtomicCommit::targetPageflipTime() const +{ + return m_targetPageflipTime; +} + +bool DrmAtomicCommit::isReadyFor(std::chrono::steady_clock::time_point pageflipTarget) const +{ + static constexpr auto s_pageflipSlop = 500us; + return (!m_targetPageflipTime || pageflipTarget + s_pageflipSlop >= *m_targetPageflipTime) && areBuffersReadable(); +} + +bool DrmAtomicCommit::isTearing() const +{ + return m_mode == PresentationMode::Async || m_mode == PresentationMode::AdaptiveAsync; +} + +DrmLegacyCommit::DrmLegacyCommit(DrmPipeline *pipeline, const std::shared_ptr &buffer, const std::shared_ptr &frame) + : DrmCommit(pipeline->gpu()) + , m_pipeline(pipeline) + , m_crtc(m_pipeline->crtc()) + , m_buffer(buffer) + , m_frame(frame) +{ +} + +bool DrmLegacyCommit::doModeset(DrmConnector *connector, DrmConnectorMode *mode) +{ + uint32_t connectorId = connector->id(); + if (drmModeSetCrtc(gpu()->fd(), m_crtc->id(), m_buffer->framebufferId(), 0, 0, &connectorId, 1, mode->nativeMode()) == 0) { + m_crtc->setCurrent(m_buffer); + return true; + } else { + return false; + } +} + +bool DrmLegacyCommit::doPageflip(PresentationMode mode) +{ + m_mode = mode; + uint32_t flags = DRM_MODE_PAGE_FLIP_EVENT; + if (mode == PresentationMode::Async || mode == PresentationMode::AdaptiveAsync) { + flags |= DRM_MODE_PAGE_FLIP_ASYNC; + } + return drmModePageFlip(gpu()->fd(), m_crtc->id(), m_buffer->framebufferId(), flags, this) == 0; +} + +void DrmLegacyCommit::pageFlipped(std::chrono::nanoseconds timestamp) +{ + Q_ASSERT(QThread::currentThread() == QCoreApplication::instance()->thread()); + m_crtc->setCurrent(m_buffer); + if (m_defunct) { + return; + } + if (m_frame) { + m_frame->presented(timestamp, m_mode); + m_frame.reset(); + } + m_pipeline->pageFlipped(timestamp); +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_commit.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_commit.h new file mode 100644 index 0000000000..337ea460ba --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_commit.h @@ -0,0 +1,125 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include "core/renderloop.h" +#include "drm_pointer.h" +#include "drm_property.h" + +namespace KWin +{ + +class DrmBlob; +class DrmConnector; +class DrmConnectorMode; +class DrmCrtc; +class DrmFramebuffer; +class DrmGpu; +class DrmPlane; +class DrmProperty; +class DrmPipeline; +class OutputFrame; + +class DrmCommit +{ +public: + virtual ~DrmCommit(); + + DrmGpu *gpu() const; + virtual void pageFlipped(std::chrono::nanoseconds timestamp) = 0; + void setDefunct(); + +protected: + DrmCommit(DrmGpu *gpu); + + DrmGpu *const m_gpu; + bool m_defunct = false; +}; + +class DrmAtomicCommit : public DrmCommit +{ +public: + explicit DrmAtomicCommit(DrmGpu *gpu); + explicit DrmAtomicCommit(const QList &pipelines); + explicit DrmAtomicCommit(const DrmAtomicCommit ©) = default; + + void addProperty(const DrmProperty &prop, uint64_t value); + template + void addEnum(const DrmEnumProperty &prop, T enumValue) + { + addProperty(prop, prop.valueForEnum(enumValue)); + } + void addBlob(const DrmProperty &prop, const std::shared_ptr &blob); + void addBuffer(DrmPlane *plane, const std::shared_ptr &buffer, const std::shared_ptr &frame); + void setVrr(DrmCrtc *crtc, bool vrr); + void setPresentationMode(PresentationMode mode); + + bool test(); + bool testAllowModeset(); + bool commit(); + bool commitModeset(); + + void pageFlipped(std::chrono::nanoseconds timestamp) override; + + bool areBuffersReadable() const; + void setDeadline(std::chrono::steady_clock::time_point deadline); + std::optional isVrr() const; + const std::unordered_set &modifiedPlanes() const; + + void merge(DrmAtomicCommit *onTop); + + void setAllowedVrrDelay(std::optional allowedDelay); + std::optional allowedVrrDelay() const; + + std::optional targetPageflipTime() const; + bool isReadyFor(std::chrono::steady_clock::time_point pageflipTarget) const; + bool isTearing() const; + +private: + bool doCommit(uint32_t flags); + + const QList m_pipelines; + std::optional m_targetPageflipTime; + std::optional m_allowedVrrDelay; + std::unordered_map> m_blobs; + std::unordered_map> m_buffers; + std::unordered_map> m_frames; + std::unordered_set m_planes; + std::optional m_vrr; + std::unordered_map> m_properties; + bool m_modeset = false; + PresentationMode m_mode = PresentationMode::VSync; +}; + +class DrmLegacyCommit : public DrmCommit +{ +public: + DrmLegacyCommit(DrmPipeline *pipeline, const std::shared_ptr &buffer, const std::shared_ptr &frame); + + bool doModeset(DrmConnector *connector, DrmConnectorMode *mode); + bool doPageflip(PresentationMode mode); + void pageFlipped(std::chrono::nanoseconds timestamp) override; + +private: + DrmPipeline *const m_pipeline; + DrmCrtc *const m_crtc; + const std::shared_ptr m_buffer; + std::shared_ptr m_frame; + PresentationMode m_mode = PresentationMode::VSync; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_commit_thread.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_commit_thread.cpp new file mode 100644 index 0000000000..7e1648932f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_commit_thread.cpp @@ -0,0 +1,415 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_commit_thread.h" +#include "drm_commit.h" +#include "drm_gpu.h" +#include "drm_logging.h" +#include "utils/envvar.h" +#include "utils/realtime.h" + +#include +#include +#include + +using namespace std::chrono_literals; + +namespace KWin +{ + +DrmCommitThread::DrmCommitThread(DrmGpu *gpu, const QString &name) + : m_gpu(gpu) + , m_targetPageflipTime(std::chrono::steady_clock::now()) +{ + if (!gpu->atomicModeSetting()) { + return; + } + + m_thread.reset(QThread::create([this]() { + const auto thread = QThread::currentThread(); + gainRealTime(); + while (true) { + if (thread->isInterruptionRequested()) { + return; + } + std::unique_lock lock(m_mutex); + bool timeout = false; + if (m_committed) { + timeout = m_commitPending.wait_for(lock, DrmGpu::s_pageflipTimeout) == std::cv_status::timeout; + } else if (m_commits.empty()) { + m_commitPending.wait(lock); + } + if (m_committed) { + if (timeout) { + // if the main thread just hung for a while, the pageflip will be processed after the wait + // but not if it's a real pageflip timeout + m_ping = false; + QMetaObject::invokeMethod(this, &DrmCommitThread::handlePing, Qt::ConnectionType::QueuedConnection); + while (!m_ping) { + m_pong.wait(lock); + } + if (m_committed) { + qCCritical(KWIN_DRM, "Pageflip timed out! This is a bug in the %s kernel driver", qPrintable(m_gpu->driverName())); + if (m_gpu->isAmdgpu()) { + qCCritical(KWIN_DRM, "Please report this at https://gitlab.freedesktop.org/drm/amd/-/issues"); + } else if (m_gpu->isNVidia()) { + qCCritical(KWIN_DRM, "Please report this at https://forums.developer.nvidia.com/c/gpu-graphics/linux"); + } else if (m_gpu->isI915()) { + qCCritical(KWIN_DRM, "Please report this at https://gitlab.freedesktop.org/drm/i915/kernel/-/issues"); + } + qCCritical(KWIN_DRM, "With the output of 'sudo dmesg' and 'journalctl --user-unit plasma-kwin_wayland --boot 0'"); + m_pageflipTimeoutDetected = true; + } else { + qCWarning(KWIN_DRM, "The main thread was hanging temporarily!"); + } + } else { + // the commit would fail with EBUSY, wait until the pageflip is done + } + continue; + } + if (m_commits.empty()) { + continue; + } + const auto now = std::chrono::steady_clock::now(); + if (m_targetPageflipTime > now + m_safetyMargin) { + lock.unlock(); + std::this_thread::sleep_until(m_targetPageflipTime - m_safetyMargin); + lock.lock(); + // the main thread might've modified the list + if (m_commits.empty()) { + continue; + } + } + optimizeCommits(m_targetPageflipTime); + if (!m_commits.front()->isReadyFor(m_targetPageflipTime)) { + // no commit is ready yet, reschedule + if (m_vrr || m_tearing) { + m_targetPageflipTime += 50us; + } else { + m_targetPageflipTime += m_minVblankInterval; + } + continue; + } + if (m_commits.front()->allowedVrrDelay() && m_vrr) { + // wait for a higher priority commit to be in, or the timeout to be hit + const bool allDelay = std::ranges::all_of(m_commits, [](const auto &commit) { + return commit->allowedVrrDelay().has_value(); + }); + auto delays = m_commits | std::views::filter([](const auto &commit) { + return commit->allowedVrrDelay().has_value(); + }) | std::views::transform([](const auto &commit) { + return *commit->allowedVrrDelay(); + }); + const std::chrono::nanoseconds lowestDelay = *std::ranges::min_element(delays); + const auto delayedTarget = m_lastPageflip + lowestDelay; + if (allDelay) { + // all commits should be delayed, just wait for the timeout + if (m_commitPending.wait_until(lock, delayedTarget) == std::cv_status::no_timeout) { + continue; + } + } else { + // TODO replace this with polling for the buffers to be ready instead + bool timeout = true; + while (std::chrono::steady_clock::now() < delayedTarget && timeout && m_commits.front()->allowedVrrDelay().has_value()) { + timeout = m_commitPending.wait_for(lock, 50us) == std::cv_status::timeout; + if (m_commits.empty()) { + break; + } + optimizeCommits(delayedTarget); + } + if (!timeout) { + // some new commit was added, process that + continue; + } + } + if (m_commits.empty()) { + continue; + } + } + submit(); + } + })); + m_thread->setObjectName(name); + m_thread->start(); +} + +static std::unique_ptr mergeCommits(std::span> commits) +{ + auto ret = std::make_unique(*commits.front()); + for (const auto &onTop : commits.subspan(1)) { + ret->merge(onTop.get()); + } + return ret; +} + +void DrmCommitThread::submit() +{ + DrmAtomicCommit *commit = m_commits.front().get(); + const auto vrr = commit->isVrr(); + const bool success = commit->commit(); + if (success) { + m_vrr = vrr.value_or(m_vrr); + m_tearing = commit->isTearing(); + m_committed = std::move(m_commits.front()); + m_commits.erase(m_commits.begin()); + + // the kernel might still take some time to actually apply the commit + // after we return from the commit ioctl, but we don't have any better + // way to know when it's done + m_lastCommitTime = std::chrono::steady_clock::now(); + // this is when we wanted to have completed the commit + const auto targetTimestamp = m_targetPageflipTime - m_baseSafetyMargin; + // this is how much safety we need to add or remove to achieve that next time + const auto safetyDifference = targetTimestamp - m_lastCommitTime; + if (safetyDifference < std::chrono::nanoseconds::zero()) { + // the commit was done later than desired, immediately add the + // required difference to make sure that it doesn't happen again + m_additionalSafetyMargin -= safetyDifference; + } else { + // we were done earlier than desired. This isn't problematic, but + // we want to keep latency at a minimum, so slowly reduce the safety margin + m_additionalSafetyMargin -= safetyDifference / 10; + } + const auto maximumReasonableMargin = std::min(3ms, m_minVblankInterval / 2); + m_additionalSafetyMargin = std::clamp(m_additionalSafetyMargin, 0ns, maximumReasonableMargin); + m_safetyMargin = m_baseSafetyMargin + m_additionalSafetyMargin; + } else { + if (m_commits.size() > 1) { + // the failure may have been because of the reordering of commits + // -> collapse all commits into one and try again with an already tested state + auto newCommit = mergeCommits(m_commits); + std::ranges::move(m_commits, std::back_inserter(m_commitsToDelete)); + m_commits.clear(); + m_commits.push_back(std::move(newCommit)); + if (m_commits.front()->test()) { + // presentation didn't fail after all, try again + submit(); + return; + } + } + for (auto &commit : m_commits) { + m_commitsToDelete.push_back(std::move(commit)); + } + m_commits.clear(); + qCWarning(KWIN_DRM) << "atomic commit failed:" << strerror(errno); + } + QMetaObject::invokeMethod(this, &DrmCommitThread::clearDroppedCommits, Qt::ConnectionType::QueuedConnection); +} + +void DrmCommitThread::optimizeCommits(TimePoint pageflipTarget) +{ + if (m_commits.size() <= 1) { + return; + } + // merge commits in the front that are already ready (regardless of which planes they modify) + if (m_commits.front()->areBuffersReadable()) { + const auto firstNotReady = std::find_if(m_commits.begin() + 1, m_commits.end(), [pageflipTarget](const auto &commit) { + return !commit->isReadyFor(pageflipTarget); + }); + if (firstNotReady != m_commits.begin() + 1) { + auto merged = mergeCommits(std::span(m_commits.begin(), firstNotReady)); + std::move(m_commits.begin(), firstNotReady, std::back_inserter(m_commitsToDelete)); + m_commits.erase(m_commits.begin() + 1, firstNotReady); + m_commits.front() = std::move(merged); + } + } + // merge commits that are ready and modify the same drm planes + for (auto it = m_commits.begin(); it != m_commits.end();) { + const auto startIt = it; + auto &startCommit = *startIt; + const auto firstNotSamePlaneNotReady = std::find_if(startIt + 1, m_commits.end(), [&startCommit, pageflipTarget](const auto &commit) { + return startCommit->modifiedPlanes() != commit->modifiedPlanes() || !commit->isReadyFor(pageflipTarget); + }); + if (firstNotSamePlaneNotReady == startIt + 1) { + it++; + continue; + } + auto merged = mergeCommits(std::span(startIt, firstNotSamePlaneNotReady)); + std::move(startIt, firstNotSamePlaneNotReady, std::back_inserter(m_commitsToDelete)); + startCommit = std::move(merged); + it = m_commits.erase(startIt + 1, firstNotSamePlaneNotReady); + } + if (m_commits.size() == 1) { + // already done + return; + } + std::unique_ptr front; + if (m_commits.front()->isReadyFor(pageflipTarget)) { + // can't just move the commit, or merging might drop the last reference + // to an OutputFrame, which should only happen in the main thread + front = std::make_unique(*m_commits.front()); + m_commitsToDelete.push_back(std::move(m_commits.front())); + m_commits.erase(m_commits.begin()); + } + // try to move commits that are ready to the front + for (auto it = m_commits.begin() + 1; it != m_commits.end();) { + auto &commit = *it; + if (!commit->isReadyFor(pageflipTarget)) { + it++; + continue; + } + // commits that target the same plane(s) need to stay in the same order + const auto &planes = commit->modifiedPlanes(); + const bool skipping = std::any_of(m_commits.begin(), it, [&planes](const auto &other) { + return std::ranges::any_of(planes, [&other](DrmPlane *plane) { + return other->modifiedPlanes().contains(plane); + }); + }); + if (skipping) { + it++; + continue; + } + // find out if the modified commit order will actually work + std::unique_ptr duplicate; + if (front) { + duplicate = std::make_unique(*front); + duplicate->merge(commit.get()); + if (!duplicate->test()) { + m_commitsToDelete.push_back(std::move(duplicate)); + it++; + continue; + } + } else { + if (!commit->test()) { + it++; + continue; + } + duplicate = std::make_unique(*commit); + } + bool success = true; + for (const auto &otherCommit : m_commits) { + if (otherCommit != commit) { + duplicate->merge(otherCommit.get()); + if (!duplicate->test()) { + success = false; + break; + } + } + } + m_commitsToDelete.push_back(std::move(duplicate)); + if (success) { + if (front) { + front->merge(commit.get()); + m_commitsToDelete.push_back(std::move(commit)); + } else { + front = std::make_unique(*commit); + m_commitsToDelete.push_back(std::move(commit)); + } + it = m_commits.erase(it); + } else { + it++; + } + } + if (front) { + m_commits.insert(m_commits.begin(), std::move(front)); + } +} + +DrmCommitThread::~DrmCommitThread() +{ + if (m_thread) { + { + std::unique_lock lock(m_mutex); + m_thread->requestInterruption(); + m_commitPending.notify_all(); + m_ping = true; + m_pong.notify_all(); + } + m_thread->wait(); + } + if (m_committed) { + m_committed->setDefunct(); + m_gpu->addDefunctCommit(std::move(m_committed)); + } +} + +void DrmCommitThread::addCommit(std::unique_ptr &&commit) +{ + std::unique_lock lock(m_mutex); + m_commits.push_back(std::move(commit)); + const auto now = std::chrono::steady_clock::now(); + TimePoint newTarget; + if (m_tearing) { + newTarget = now; + } else if (m_vrr && now >= m_lastPageflip + m_minVblankInterval) { + newTarget = now; + } else { + newTarget = estimateNextVblank(now); + } + m_targetPageflipTime = std::max(m_targetPageflipTime, newTarget); + m_commits.back()->setDeadline(m_targetPageflipTime - m_safetyMargin); + m_commitPending.notify_all(); +} + +void DrmCommitThread::setPendingCommit(std::unique_ptr &&commit) +{ + m_committed = std::move(commit); +} + +void DrmCommitThread::clearDroppedCommits() +{ + std::unique_lock lock(m_mutex); + m_commitsToDelete.clear(); +} + +// TODO reduce the default for this, once we have a more accurate way to know when an atomic commit +// is actually applied. Waiting for the commit returning seems to work on Intel and AMD, but not with NVidia +static const std::chrono::microseconds s_safetyMarginMinimum{environmentVariableIntValue("KWIN_DRM_OVERRIDE_SAFETY_MARGIN").value_or(1000)}; + +void DrmCommitThread::setModeInfo(uint32_t maximum, std::chrono::nanoseconds vblankTime) +{ + std::unique_lock lock(m_mutex); + m_minVblankInterval = std::chrono::nanoseconds(1'000'000'000'000ull / maximum); + // the kernel rejects commits that happen during vblank + // the 1.5ms on top of that was chosen experimentally, for the time it takes to commit + scheduling inaccuracies + m_baseSafetyMargin = vblankTime + s_safetyMarginMinimum; + m_safetyMargin = m_baseSafetyMargin + m_additionalSafetyMargin; +} + +void DrmCommitThread::pageFlipped(std::chrono::nanoseconds timestamp) +{ + std::unique_lock lock(m_mutex); + if (m_pageflipTimeoutDetected) { + qCCritical(KWIN_DRM, "Pageflip arrived after all, %lums after the commit", std::chrono::duration_cast(std::chrono::steady_clock::now() - m_lastCommitTime).count()); + m_pageflipTimeoutDetected = false; + } + m_lastPageflip = TimePoint(timestamp); + m_committed.reset(); + if (!m_commits.empty()) { + m_targetPageflipTime = estimateNextVblank(std::chrono::steady_clock::now()); + m_commitPending.notify_all(); + } +} + +bool DrmCommitThread::pageflipsPending() +{ + std::unique_lock lock(m_mutex); + return !m_commits.empty() || m_committed; +} + +TimePoint DrmCommitThread::estimateNextVblank(TimePoint now) const +{ + // the pageflip timestamp may be in the future + const uint64_t pageflipsSince = now >= m_lastPageflip ? (now - m_lastPageflip) / m_minVblankInterval : 0; + return m_lastPageflip + m_minVblankInterval * (pageflipsSince + 1); +} + +std::chrono::nanoseconds DrmCommitThread::safetyMargin() const +{ + return m_safetyMargin; +} + +void DrmCommitThread::handlePing() +{ + // this will process the pageflip and call pageFlipped if there is one + m_gpu->dispatchEvents(); + std::unique_lock lock(m_mutex); + m_ping = true; + m_pong.notify_one(); +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_commit_thread.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_commit_thread.h new file mode 100644 index 0000000000..aeb6ad007c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_commit_thread.h @@ -0,0 +1,74 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include +#include +#include +#include + +namespace KWin +{ + +class DrmGpu; +class DrmCommit; +class DrmAtomicCommit; +class DrmLegacyCommit; + +using TimePoint = std::chrono::steady_clock::time_point; + +class DrmCommitThread : public QObject +{ + Q_OBJECT +public: + explicit DrmCommitThread(DrmGpu *gpu, const QString &name); + ~DrmCommitThread(); + + void addCommit(std::unique_ptr &&commit); + void setPendingCommit(std::unique_ptr &&commit); + + void setModeInfo(uint32_t maximum, std::chrono::nanoseconds vblankTime); + void pageFlipped(std::chrono::nanoseconds timestamp); + bool pageflipsPending(); + /** + * @return how long before the desired presentation timestamp the commit has to be added + * in order to get presented at that timestamp + */ + std::chrono::nanoseconds safetyMargin() const; + +private: + void clearDroppedCommits(); + TimePoint estimateNextVblank(TimePoint now) const; + void optimizeCommits(TimePoint pageflipTarget); + void submit(); + void handlePing(); + + DrmGpu *const m_gpu; + std::unique_ptr m_committed; + std::vector> m_commits; + std::unique_ptr m_thread; + std::mutex m_mutex; + std::condition_variable m_commitPending; + std::condition_variable m_pong; + TimePoint m_lastPageflip; + TimePoint m_targetPageflipTime; + TimePoint m_lastCommitTime; + std::chrono::nanoseconds m_minVblankInterval; + std::vector> m_commitsToDelete; + bool m_vrr = false; + bool m_tearing = false; + std::chrono::nanoseconds m_safetyMargin{0}; + std::chrono::nanoseconds m_baseSafetyMargin{0}; + std::chrono::nanoseconds m_additionalSafetyMargin = std::chrono::milliseconds(1); + bool m_ping = false; + bool m_pageflipTimeoutDetected = false; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_connector.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_connector.cpp new file mode 100644 index 0000000000..081d6cd8ea --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_connector.cpp @@ -0,0 +1,519 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Roman Gilg + SPDX-FileCopyrightText: 2021 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_connector.h" +#include "drm_commit.h" +#include "drm_crtc.h" +#include "drm_gpu.h" +#include "drm_logging.h" +#include "drm_output.h" +#include "drm_pipeline.h" +#include "drm_pointer.h" + +#include +#include +#include + +namespace KWin +{ + +static QSize resolutionForMode(const drmModeModeInfo *info) +{ + return QSize(info->hdisplay, info->vdisplay); +} + +uint32_t DrmConnector::refreshRateForMode(_drmModeModeInfo *m) +{ + // Calculate higher precision (mHz) refresh rate + // logic based on Weston, see compositor-drm.c + uint64_t refreshRate = (m->clock * 1000000LL / m->htotal + m->vtotal / 2) / m->vtotal; + if (m->flags & DRM_MODE_FLAG_INTERLACE) { + refreshRate *= 2; + } + if (m->flags & DRM_MODE_FLAG_DBLSCAN) { + refreshRate /= 2; + } + if (m->vscan > 1) { + refreshRate /= m->vscan; + } + return refreshRate; +} + +static OutputMode::Flags flagsForMode(const drmModeModeInfo *info, OutputMode::Flags additionalFlags) +{ + OutputMode::Flags flags = additionalFlags; + if (info->type & DRM_MODE_TYPE_PREFERRED) { + flags |= OutputMode::Flag::Preferred; + } + return flags; +} + +DrmConnectorMode::DrmConnectorMode(DrmConnector *connector, drmModeModeInfo nativeMode, Flags additionalFlags) + : OutputMode(resolutionForMode(&nativeMode), DrmConnector::refreshRateForMode(&nativeMode), flagsForMode(&nativeMode, additionalFlags)) + , m_connector(connector) + , m_nativeMode(nativeMode) +{ +} + +std::shared_ptr DrmConnectorMode::blob() +{ + if (!m_blob) { + m_blob = DrmBlob::create(m_connector->gpu(), &m_nativeMode, sizeof(m_nativeMode)); + } + return m_blob; +} + +std::chrono::nanoseconds DrmConnectorMode::vblankTime() const +{ + return std::chrono::nanoseconds(((m_nativeMode.vtotal - m_nativeMode.vdisplay) * m_nativeMode.htotal * 1'000'000ULL) / m_nativeMode.clock); +} + +drmModeModeInfo *DrmConnectorMode::nativeMode() +{ + return &m_nativeMode; +} + +static inline bool checkIfEqual(const drmModeModeInfo *one, const drmModeModeInfo *two) +{ + // NOTE that + // - the struct contains a name, so doing memcmp would yield false negatives! + // - vrefresh is a redundant value that the kernel seems to round differently from us, + // so that's not checked either + return one->clock == two->clock + && one->hdisplay == two->hdisplay + && one->hsync_start == two->hsync_start + && one->hsync_end == two->hsync_end + && one->htotal == two->htotal + && one->hskew == two->hskew + && one->vdisplay == two->vdisplay + && one->vsync_start == two->vsync_start + && one->vsync_end == two->vsync_end + && one->vtotal == two->vtotal + && one->vscan == two->vscan + && one->flags == two->flags; +} + +bool DrmConnectorMode::operator==(const DrmConnectorMode &otherMode) const +{ + return checkIfEqual(&m_nativeMode, &otherMode.m_nativeMode); +} + +bool DrmConnectorMode::operator==(const drmModeModeInfo &otherMode) const +{ + return checkIfEqual(&m_nativeMode, &otherMode); +} + +DrmConnector::DrmConnector(DrmGpu *gpu, uint32_t connectorId) + : DrmObject(gpu, connectorId, DRM_MODE_OBJECT_CONNECTOR) + , crtcId(this, QByteArrayLiteral("CRTC_ID")) + , nonDesktop(this, QByteArrayLiteral("non-desktop")) + , dpms(this, QByteArrayLiteral("DPMS")) + , edidProp(this, QByteArrayLiteral("EDID")) + , overscan(this, QByteArrayLiteral("overscan")) + , vrrCapable(this, QByteArrayLiteral("vrr_capable")) + , underscan(this, QByteArrayLiteral("underscan"), { + QByteArrayLiteral("off"), + QByteArrayLiteral("on"), + QByteArrayLiteral("auto"), + }) + , underscanVBorder(this, QByteArrayLiteral("underscan vborder")) + , underscanHBorder(this, QByteArrayLiteral("underscan hborder")) + , broadcastRGB(this, QByteArrayLiteral("Broadcast RGB"), { + QByteArrayLiteral("Automatic"), + QByteArrayLiteral("Full"), + QByteArrayLiteral("Limited 16:235"), + }) + , maxBpc(this, QByteArrayLiteral("max bpc")) + , linkStatus(this, QByteArrayLiteral("link-status"), { + QByteArrayLiteral("Good"), + QByteArrayLiteral("Bad"), + }) + , contentType(this, QByteArrayLiteral("content type"), { + QByteArrayLiteral("No Data"), + QByteArrayLiteral("Graphics"), + QByteArrayLiteral("Photo"), + QByteArrayLiteral("Cinema"), + QByteArrayLiteral("Game"), + }) + , panelOrientation(this, QByteArrayLiteral("panel orientation"), { + QByteArrayLiteral("Normal"), + QByteArrayLiteral("Upside Down"), + QByteArrayLiteral("Left Side Up"), + QByteArrayLiteral("Right Side Up"), + }) + , hdrMetadata(this, QByteArrayLiteral("HDR_OUTPUT_METADATA")) + , scalingMode(this, QByteArrayLiteral("scaling mode"), { + QByteArrayLiteral("None"), + QByteArrayLiteral("Full"), + QByteArrayLiteral("Center"), + QByteArrayLiteral("Full aspect"), + }) + , colorspace(this, QByteArrayLiteral("Colorspace"), { + QByteArrayLiteral("Default"), + QByteArrayLiteral("BT709_YCC"), + QByteArrayLiteral("opRGB"), + QByteArrayLiteral("BT2020_RGB"), + QByteArrayLiteral("BT2020_YCC"), + }) + , path(this, QByteArrayLiteral("PATH")) +{ +} + +bool DrmConnector::init() +{ + if (!updateProperties()) { + return false; + } + + m_possibleCrtcs = drmModeConnectorGetPossibleCrtcs(gpu()->fd(), m_conn.get()); + + return true; +} + +bool DrmConnector::isConnected() const +{ + return !m_driverModes.empty() && m_conn && m_conn->connection == DRM_MODE_CONNECTED; +} + +QString DrmConnector::connectorName() const +{ + const char *connectorName = drmModeGetConnectorTypeName(m_conn->connector_type); + if (!connectorName) { + connectorName = "Unknown"; + } + return QStringLiteral("%1-%2").arg(connectorName).arg(m_conn->connector_type_id); +} + +QString DrmConnector::modelName() const +{ + if (m_edid.serialNumber().isEmpty()) { + return connectorName() + QLatin1Char('-') + m_edid.nameString(); + } else { + return m_edid.nameString(); + } +} + +bool DrmConnector::isInternal() const +{ + return m_conn->connector_type == DRM_MODE_CONNECTOR_LVDS || m_conn->connector_type == DRM_MODE_CONNECTOR_eDP + || m_conn->connector_type == DRM_MODE_CONNECTOR_DSI; +} + +QSize DrmConnector::physicalSize() const +{ + return m_physicalSize; +} + +QByteArray DrmConnector::mstPath() const +{ + return m_mstPath; +} + +QList> DrmConnector::modes() const +{ + return m_modes; +} + +BackendOutput::SubPixel DrmConnector::subpixel() const +{ + switch (m_conn->subpixel) { + case DRM_MODE_SUBPIXEL_UNKNOWN: + return BackendOutput::SubPixel::Unknown; + case DRM_MODE_SUBPIXEL_HORIZONTAL_RGB: + return BackendOutput::SubPixel::Horizontal_RGB; + case DRM_MODE_SUBPIXEL_HORIZONTAL_BGR: + return BackendOutput::SubPixel::Horizontal_BGR; + case DRM_MODE_SUBPIXEL_VERTICAL_RGB: + return BackendOutput::SubPixel::Vertical_RGB; + case DRM_MODE_SUBPIXEL_VERTICAL_BGR: + return BackendOutput::SubPixel::Vertical_BGR; + case DRM_MODE_SUBPIXEL_NONE: + return BackendOutput::SubPixel::None; + default: + return BackendOutput::SubPixel::Unknown; + } +} + +bool DrmConnector::updateProperties() +{ + if (auto connector = drmModeGetConnector(gpu()->fd(), id())) { + m_conn.reset(connector); + } else { + qCWarning(KWIN_DRM) << "drmModeGetConnector() failed:" << strerror(errno); + } + + if (!m_conn) { + return false; + } + DrmPropertyList props = queryProperties(); + crtcId.update(props); + nonDesktop.update(props); + dpms.update(props); + edidProp.update(props); + overscan.update(props); + vrrCapable.update(props); + underscan.update(props); + underscanVBorder.update(props); + underscanHBorder.update(props); + broadcastRGB.update(props); + maxBpc.update(props); + linkStatus.update(props); + contentType.update(props); + panelOrientation.update(props); + hdrMetadata.update(props); + scalingMode.update(props); + colorspace.update(props); + path.update(props); + + if (gpu()->atomicModeSetting() && !crtcId.isValid()) { + qCWarning(KWIN_DRM) << "Failed to update the basic connector properties (CRTC_ID)"; + return false; + } + + // parse edid + if (edidProp.immutableBlob()) { + m_edid = Edid(edidProp.immutableBlob()->data, edidProp.immutableBlob()->length); + if (!m_edid.isValid()) { + qCWarning(KWIN_DRM) << "Couldn't parse EDID for connector" << this; + } + } else { + m_edid = Edid{}; + if (m_conn->connection == DRM_MODE_CONNECTED) { + qCWarning(KWIN_DRM) << "Could not find edid for connector" << this; + } + } + + // check the physical size + if (m_edid.physicalSize().isEmpty()) { + m_physicalSize = QSize(m_conn->mmWidth, m_conn->mmHeight); + } else { + m_physicalSize = m_edid.physicalSize(); + } + + // update modes + bool equal = m_conn->count_modes == m_driverModes.count(); + for (int i = 0; equal && i < m_conn->count_modes; i++) { + equal &= checkIfEqual(m_driverModes[i]->nativeMode(), &m_conn->modes[i]); + } + if (!equal && m_conn->count_modes > 0) { + // reload modes + m_driverModes.clear(); + for (int i = 0; i < m_conn->count_modes; i++) { + m_driverModes.append(std::make_shared(this, m_conn->modes[i], OutputMode::Flags())); + } + m_modes.clear(); + m_modes.append(m_driverModes); + if (scalingMode.isValid() && scalingMode.hasEnum(ScalingMode::Full_Aspect)) { + m_modes.append(generateCommonModes()); + } + } + + m_mstPath.clear(); + if (auto blob = path.immutableBlob()) { + QByteArray value = QByteArray(static_cast(blob->data), blob->length); + if (value.startsWith("mst:")) { + // for backwards compatibility reasons the string also contains the drm connector id + // remove that to get a more stable identifier + const ssize_t firstHyphen = value.indexOf('-'); + if (firstHyphen > 0) { + m_mstPath = value.mid(firstHyphen); + } else { + qCWarning(KWIN_DRM) << "Unexpected format in path property:" << value; + } + } else { + qCWarning(KWIN_DRM) << "Unknown path type detected:" << value; + } + } + + return true; +} + +bool DrmConnector::isCrtcSupported(DrmCrtc *crtc) const +{ + return (m_possibleCrtcs & (1 << crtc->pipeIndex())); +} + +bool DrmConnector::isNonDesktop() const +{ + return nonDesktop.isValid() && nonDesktop.value() == 1; +} + +const Edid *DrmConnector::edid() const +{ + return &m_edid; +} + +void DrmConnector::disable(DrmAtomicCommit *commit) +{ + commit->addProperty(crtcId, 0); +} + +static const QList s_commonModes = { + /* 4:3 (1.33) */ + QSize(1600, 1200), + QSize(1280, 1024), /* 5:4 (1.25) */ + QSize(1024, 768), + /* 16:10 (1.6) */ + QSize(2560, 1600), + QSize(1920, 1200), + QSize(1280, 800), + /* 16:9 (1.77) */ + QSize(5120, 2880), + QSize(3840, 2160), + QSize(3200, 1800), + QSize(2880, 1620), + QSize(2560, 1440), + QSize(1920, 1080), + QSize(1600, 900), + QSize(1368, 768), + QSize(1280, 720), +}; + +QList> DrmConnector::generateCommonModes() +{ + QList> ret; + QSize maxSize; + uint64_t maxSizeRefreshRate = 0; + for (const auto &mode : std::as_const(m_driverModes)) { + if (mode->size().width() >= maxSize.width() && mode->size().height() >= maxSize.height() && mode->refreshRate() >= maxSizeRefreshRate) { + maxSize = mode->size(); + maxSizeRefreshRate = mode->refreshRate(); + } + } + const uint64_t maxBandwidthEstimation = maxSize.width() * maxSize.height() * maxSizeRefreshRate; + QList refreshRates = {60000ul}; + if (maxSizeRefreshRate > 60000ul) { + refreshRates.push_back(maxSizeRefreshRate); + } + for (const auto size : s_commonModes) { + for (uint64_t refreshRate : refreshRates) { + const uint64_t bandwidthEstimation = size.width() * size.height() * refreshRate; + if (size.width() > maxSize.width() || size.height() > maxSize.height() || bandwidthEstimation > maxBandwidthEstimation) { + continue; + } + const auto generatedMode = generateMode(size, refreshRate / 1000.0, OutputMode::Flags{}); + const bool alreadyExists = std::ranges::any_of(m_driverModes, [generatedMode](const auto &mode) { + return mode->size() == generatedMode->size() + && std::round(mode->refreshRate() / 1000.0) == std::round(generatedMode->refreshRate() / 1000.0); + }); + if (alreadyExists) { + continue; + } + ret.push_back(generatedMode); + } + } + return ret; +} + +std::shared_ptr DrmConnector::generateMode(const QSize &size, float refreshRate, OutputMode::Flags flags) +{ + auto modeInfo = libxcvt_gen_mode_info(size.width(), size.height(), refreshRate, flags & OutputMode::Flag::ReducedBlanking, false); + + drmModeModeInfo mode{ + .clock = uint32_t(modeInfo->dot_clock), + .hdisplay = uint16_t(modeInfo->hdisplay), + .hsync_start = modeInfo->hsync_start, + .hsync_end = modeInfo->hsync_end, + .htotal = modeInfo->htotal, + .vdisplay = uint16_t(modeInfo->vdisplay), + .vsync_start = modeInfo->vsync_start, + .vsync_end = modeInfo->vsync_end, + .vtotal = modeInfo->vtotal, + .vscan = 1, + .vrefresh = uint32_t(modeInfo->vrefresh), + .flags = modeInfo->mode_flags, + .type = DRM_MODE_TYPE_USERDEF, + }; + + sprintf(mode.name, "%dx%d@%d", size.width(), size.height(), mode.vrefresh); + + free(modeInfo); + return std::make_shared(this, mode, flags | OutputMode::Flag::Generated); +} + +QDebug &operator<<(QDebug &s, const KWin::DrmConnector *obj) +{ + QDebugStateSaver saver(s); + if (obj) { + + QString connState = QStringLiteral("Disconnected"); + if (!obj->m_conn || obj->m_conn->connection == DRM_MODE_UNKNOWNCONNECTION) { + connState = QStringLiteral("Unknown Connection"); + } else if (obj->m_conn->connection == DRM_MODE_CONNECTED) { + connState = QStringLiteral("Connected"); + } + + s.nospace() << "DrmConnector(id=" << obj->id() << ", gpu=" << obj->gpu() << ", name=" << obj->connectorName() << ", connection=" << connState << ", countMode=" << (obj->m_conn ? obj->m_conn->count_modes : 0) + << ')'; + } else { + s << "DrmConnector(0x0)"; + } + return s; +} + +DrmConnector::DrmContentType DrmConnector::kwinToDrmContentType(ContentType type) +{ + switch (type) { + case ContentType::None: + return DrmContentType::Graphics; + case ContentType::Photo: + return DrmContentType::Photo; + case ContentType::Video: + return DrmContentType::Cinema; + case ContentType::Game: + return DrmContentType::Game; + default: + Q_UNREACHABLE(); + } +} + +OutputTransform DrmConnector::toKWinTransform(PanelOrientation orientation) +{ + switch (orientation) { + case PanelOrientation::Normal: + return KWin::OutputTransform::Normal; + case PanelOrientation::RightUp: + return KWin::OutputTransform::Rotate270; + case PanelOrientation::LeftUp: + return KWin::OutputTransform::Rotate90; + case PanelOrientation::UpsideDown: + return KWin::OutputTransform::Rotate180; + default: + Q_UNREACHABLE(); + } +} + +DrmConnector::BroadcastRgbOptions DrmConnector::rgbRangeToBroadcastRgb(BackendOutput::RgbRange rgbRange) +{ + switch (rgbRange) { + case BackendOutput::RgbRange::Automatic: + return BroadcastRgbOptions::Automatic; + case BackendOutput::RgbRange::Full: + return BroadcastRgbOptions::Full; + case BackendOutput::RgbRange::Limited: + return BroadcastRgbOptions::Limited; + default: + Q_UNREACHABLE(); + } +} + +BackendOutput::RgbRange DrmConnector::broadcastRgbToRgbRange(BroadcastRgbOptions rgbRange) +{ + switch (rgbRange) { + case BroadcastRgbOptions::Automatic: + return BackendOutput::RgbRange::Automatic; + case BroadcastRgbOptions::Full: + return BackendOutput::RgbRange::Full; + case BroadcastRgbOptions::Limited: + return BackendOutput::RgbRange::Limited; + default: + Q_UNREACHABLE(); + } +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_connector.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_connector.h new file mode 100644 index 0000000000..34361f43ff --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_connector.h @@ -0,0 +1,162 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Roman Gilg + SPDX-FileCopyrightText: 2021 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "kwin_export.h" + +#include +#include + +#include + +#include "core/backendoutput.h" +#include "drm_blob.h" +#include "drm_object.h" +#include "drm_pointer.h" +#include "utils/edid.h" + +namespace KWin +{ + +class DrmConnector; +class DrmCrtc; + +/** + * The DrmConnectorMode class represents a native mode and the associated blob. + */ +class DrmConnectorMode : public OutputMode +{ +public: + DrmConnectorMode(DrmConnector *connector, drmModeModeInfo nativeMode, Flags additionalFlags); + + drmModeModeInfo *nativeMode(); + std::shared_ptr blob(); + std::chrono::nanoseconds vblankTime() const; + + bool operator==(const DrmConnectorMode &otherMode) const; + bool operator==(const drmModeModeInfo &otherMode) const; + +private: + DrmConnector *m_connector; + drmModeModeInfo m_nativeMode; + std::shared_ptr m_blob; +}; + +class KWIN_EXPORT DrmConnector : public DrmObject +{ +public: + DrmConnector(DrmGpu *gpu, uint32_t connectorId); + + bool init(); + + bool updateProperties() override; + void disable(DrmAtomicCommit *commit) override; + + bool isCrtcSupported(DrmCrtc *crtc) const; + bool isConnected() const; + bool isNonDesktop() const; + bool isInternal() const; + + const Edid *edid() const; + QString connectorName() const; + QString modelName() const; + QSize physicalSize() const; + /** + * @returns the mst path of the connector. Is empty if invalid + */ + QByteArray mstPath() const; + + QList> modes() const; + std::shared_ptr generateMode(const QSize &size, float refreshRate, OutputMode::Flags flags); + + BackendOutput::SubPixel subpixel() const; + + enum class UnderscanOptions : uint64_t { + Off = 0, + On = 1, + Auto = 2, + }; + enum class BroadcastRgbOptions : uint64_t { + Automatic = 0, + Full = 1, + Limited = 2 + }; + enum class LinkStatus : uint64_t { + Good = 0, + Bad = 1 + }; + enum class DrmContentType : uint64_t { + None = 0, + Graphics = 1, + Photo = 2, + Cinema = 3, + Game = 4 + }; + enum class PanelOrientation : uint64_t { + Normal = 0, + UpsideDown = 1, + LeftUp = 2, + RightUp = 3 + }; + enum class ScalingMode : uint64_t { + None = 0, + Full = 1, + Center = 2, + Full_Aspect = 3 + }; + enum class Colorspace : uint64_t { + Default, + BT709_YCC, + opRGB, + BT2020_RGB, + BT2020_YCC, + }; + + DrmProperty crtcId; + DrmProperty nonDesktop; + DrmProperty dpms; + DrmProperty edidProp; + DrmProperty overscan; + DrmProperty vrrCapable; + DrmEnumProperty underscan; + DrmProperty underscanVBorder; + DrmProperty underscanHBorder; + DrmEnumProperty broadcastRGB; + DrmProperty maxBpc; + DrmEnumProperty linkStatus; + DrmEnumProperty contentType; + DrmEnumProperty panelOrientation; + DrmProperty hdrMetadata; + DrmEnumProperty scalingMode; + DrmEnumProperty colorspace; + DrmProperty path; + + static DrmContentType kwinToDrmContentType(ContentType type); + static OutputTransform toKWinTransform(PanelOrientation orientation); + static BroadcastRgbOptions rgbRangeToBroadcastRgb(BackendOutput::RgbRange rgbRange); + static BackendOutput::RgbRange broadcastRgbToRgbRange(BroadcastRgbOptions rgbRange); + static uint32_t refreshRateForMode(_drmModeModeInfo *m); + +private: + QList> generateCommonModes(); + + DrmUniquePtr m_conn; + Edid m_edid; + QSize m_physicalSize = QSize(-1, -1); + QList> m_driverModes; + QList> m_modes; + uint32_t m_possibleCrtcs = 0; + QByteArray m_mstPath; + + friend QDebug &operator<<(QDebug &s, const KWin::DrmConnector *obj); +}; + +QDebug &operator<<(QDebug &s, const KWin::DrmConnector *obj); + +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_crtc.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_crtc.cpp new file mode 100644 index 0000000000..66da64b125 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_crtc.cpp @@ -0,0 +1,134 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_crtc.h" +#include "drm_backend.h" +#include "drm_buffer.h" +#include "drm_commit.h" +#include "drm_gpu.h" +#include "drm_output.h" +#include "drm_pointer.h" + +namespace KWin +{ + +DrmCrtc::DrmCrtc(DrmGpu *gpu, uint32_t crtcId, int pipeIndex, DrmPlane *primaryPlane) + : DrmObject(gpu, crtcId, DRM_MODE_OBJECT_CRTC) + , modeId(this, QByteArrayLiteral("MODE_ID")) + , active(this, QByteArrayLiteral("ACTIVE")) + , vrrEnabled(this, QByteArrayLiteral("VRR_ENABLED")) + , gammaLut(this, QByteArrayLiteral("GAMMA_LUT")) + , gammaLutSize(this, QByteArrayLiteral("GAMMA_LUT_SIZE")) + , ctm(this, QByteArrayLiteral("CTM")) + , degammaLut(this, QByteArrayLiteral("DEGAMMA_LUT")) + , degammaLutSize(this, QByteArrayLiteral("DEGAMMA_LUT_SIZE")) + , sharpnessStrength(this, QByteArrayLiteral("SHARPNESS_STRENGTH")) + , m_crtc(drmModeGetCrtc(gpu->fd(), crtcId)) + , m_pipeIndex(pipeIndex) + , m_primaryPlane(primaryPlane) +{ +} + +bool DrmCrtc::init() +{ + return updateProperties(); +} + +bool DrmCrtc::updateProperties() +{ + if (!m_crtc) { + return false; + } + DrmPropertyList props = queryProperties(); + modeId.update(props); + active.update(props); + vrrEnabled.update(props); + gammaLut.update(props); + gammaLutSize.update(props); + ctm.update(props); + degammaLut.update(props); + degammaLutSize.update(props); + sharpnessStrength.update(props); + + if (!postBlendingPipeline) { + DrmAbstractColorOp *next = nullptr; + if (gammaLut.isValid()) { + m_postBlendingColorOps.push_back(std::make_unique(next, &gammaLut, nullptr, gammaRampSize(), nullptr)); + next = m_postBlendingColorOps.back().get(); + } + if (!gpu()->isNVidia() && ctm.isValid()) { + m_postBlendingColorOps.push_back(std::make_unique(next, &ctm)); + next = m_postBlendingColorOps.back().get(); + } + // DEGAMMA_LUT is intentionally not part of the post blending pipeline + // as on most hardware it actually maps to pre-blending operations, + // and more importantly it's buggy on Intel, AMD and NVidia... + postBlendingPipeline = next; + } + + const bool ret = !gpu()->atomicModeSetting() || (modeId.isValid() && active.isValid()); + if (!ret) { + qCWarning(KWIN_DRM) << "Failed to update the basic crtc properties. modeId:" << modeId.isValid() << "active:" << active.isValid(); + } + return ret; +} + +drmModeModeInfo DrmCrtc::queryCurrentMode() +{ + DrmUniquePtr crtc(drmModeGetCrtc(gpu()->fd(), id())); + if (crtc) { + return crtc->mode; + } else { + return m_crtc->mode; + } +} + +int DrmCrtc::pipeIndex() const +{ + return m_pipeIndex; +} + +std::shared_ptr DrmCrtc::current() const +{ + return m_currentBuffer; +} + +void DrmCrtc::setCurrent(const std::shared_ptr &buffer) +{ + m_currentBuffer = buffer; +} + +int DrmCrtc::gammaRampSize() const +{ + if (gpu()->atomicModeSetting()) { + // limit atomic gamma ramp to 4096 to work around https://gitlab.freedesktop.org/drm/intel/-/issues/3916 + if (gammaLutSize.isValid() && gammaLutSize.value() <= 4096) { + return gammaLutSize.value(); + } + } + return m_crtc->gamma_size; +} + +DrmPlane *DrmCrtc::primaryPlane() const +{ + return m_primaryPlane; +} + +void DrmCrtc::disable(DrmAtomicCommit *commit) +{ + commit->addProperty(active, 0); + commit->addProperty(modeId, 0); +} + +void DrmCrtc::releaseCurrentBuffer() +{ + if (m_currentBuffer) { + m_currentBuffer->releaseBuffer(); + } +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_crtc.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_crtc.h new file mode 100644 index 0000000000..29409da3f7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_crtc.h @@ -0,0 +1,66 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "drm_colorop.h" +#include "drm_object.h" + +#include +#include + +namespace KWin +{ + +class DrmBackend; +class DrmFramebuffer; +class GammaRamp; +class DrmGpu; +class DrmPlane; + +class DrmCrtc : public DrmObject +{ +public: + explicit DrmCrtc(DrmGpu *gpu, uint32_t crtcId, int pipeIndex, DrmPlane *primaryPlane); + + bool init(); + + void disable(DrmAtomicCommit *commit) override; + bool updateProperties() override; + + int pipeIndex() const; + int gammaRampSize() const; + DrmPlane *primaryPlane() const; + drmModeModeInfo queryCurrentMode(); + + std::shared_ptr current() const; + void setCurrent(const std::shared_ptr &buffer); + void releaseCurrentBuffer(); + + DrmProperty modeId; + DrmProperty active; + DrmProperty vrrEnabled; + DrmProperty gammaLut; + DrmProperty gammaLutSize; + DrmProperty ctm; + DrmProperty degammaLut; + DrmProperty degammaLutSize; + DrmProperty sharpnessStrength; + + DrmAbstractColorOp *postBlendingPipeline = nullptr; + +private: + DrmUniquePtr m_crtc; + std::shared_ptr m_currentBuffer; + int m_pipeIndex; + DrmPlane *m_primaryPlane; + + std::vector> m_postBlendingColorOps; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_egl_backend.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_egl_backend.cpp new file mode 100644 index 0000000000..5d2d2c4cd0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_egl_backend.cpp @@ -0,0 +1,170 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_egl_backend.h" +// kwin +#include "core/syncobjtimeline.h" +#include "drm_abstract_output.h" +#include "drm_backend.h" +#include "drm_egl_layer.h" +#include "drm_gpu.h" +#include "drm_logging.h" +#include "drm_output.h" +#include "drm_pipeline.h" +#include "drm_virtual_egl_layer.h" +#include "drm_virtual_output.h" +// system +#include +#include +#include + +namespace KWin +{ + +EglGbmBackend::EglGbmBackend(DrmBackend *drmBackend) + : m_backend(drmBackend) +{ + drmBackend->setRenderBackend(this); + connect(m_backend, &DrmBackend::gpuRemoved, this, [this](DrmGpu *gpu) { + m_contexts.erase(gpu->eglDisplay()); + }); +} + +EglGbmBackend::~EglGbmBackend() +{ + m_backend->releaseBuffers(); + m_contexts.clear(); + cleanup(); + m_backend->setRenderBackend(nullptr); +} + +bool EglGbmBackend::initializeEgl() +{ + initClientExtensions(); + auto display = m_backend->primaryGpu()->eglDisplay(); + + // Use eglGetPlatformDisplayEXT() to get the display pointer + // if the implementation supports it. + if (!display) { + display = createEglDisplay(m_backend->primaryGpu()); + if (!display) { + return false; + } + } + setEglDisplay(display); + return true; +} + +EglDisplay *EglGbmBackend::createEglDisplay(DrmGpu *gpu) const +{ + for (const QByteArray &extension : {QByteArrayLiteral("EGL_EXT_platform_base"), QByteArrayLiteral("EGL_KHR_platform_gbm")}) { + if (!hasClientExtension(extension)) { + qCWarning(KWIN_DRM) << extension << "client extension is not supported by the platform"; + return nullptr; + } + } + + gpu->setEglDisplay(EglDisplay::create(eglGetPlatformDisplayEXT(EGL_PLATFORM_GBM_KHR, gpu->drmDevice()->gbmDevice(), nullptr))); + return gpu->eglDisplay(); +} + +void EglGbmBackend::init() +{ + if (!initializeEgl()) { + setFailed("Could not initialize egl"); + return; + } + + if (!initRenderingContext()) { + setFailed("Could not initialize rendering context"); + return; + } + initWayland(); + m_backend->createLayers(); +} + +bool EglGbmBackend::initRenderingContext() +{ + return createContext(EGL_NO_CONFIG_KHR) && openglContext()->makeCurrent(); +} + +EglDisplay *EglGbmBackend::displayForGpu(DrmGpu *gpu) +{ + if (gpu == m_backend->primaryGpu()) { + return eglDisplayObject(); + } + auto display = gpu->eglDisplay(); + if (!display) { + display = createEglDisplay(gpu); + } + return display; +} + +std::shared_ptr EglGbmBackend::contextForGpu(DrmGpu *gpu) +{ + if (gpu == m_backend->primaryGpu()) { + return m_context; + } + auto display = gpu->eglDisplay(); + if (!display) { + display = createEglDisplay(gpu); + if (!display) { + return nullptr; + } + } + auto &context = m_contexts[display]; + if (const auto c = context.lock()) { + return c; + } + const auto ret = std::shared_ptr(EglContext::create(display, EGL_NO_CONFIG_KHR, EGL_NO_CONTEXT)); + context = ret; + return ret; +} + +void EglGbmBackend::resetContextForGpu(DrmGpu *gpu) +{ + m_contexts.erase(gpu->eglDisplay()); +} + +DrmDevice *EglGbmBackend::drmDevice() const +{ + return gpu()->drmDevice(); +} + +QList EglGbmBackend::compatibleOutputLayers(BackendOutput *output) +{ + if (auto virtualOutput = qobject_cast(output)) { + return {virtualOutput->primaryLayer()}; + } else { + return static_cast(output)->pipeline()->gpu()->compatibleOutputLayers(output); + } +} + +std::unique_ptr EglGbmBackend::createDrmPlaneLayer(DrmPlane *plane) +{ + return std::make_unique(this, plane); +} + +std::unique_ptr EglGbmBackend::createDrmPlaneLayer(DrmGpu *gpu, DrmPlane::TypeIndex type) +{ + return std::make_unique(this, gpu, type); +} + +std::unique_ptr EglGbmBackend::createLayer(DrmVirtualOutput *output) +{ + return std::make_unique(this, output); +} + +DrmGpu *EglGbmBackend::gpu() const +{ + return m_backend->primaryGpu(); +} + +} // namespace KWin + +#include "moc_drm_egl_backend.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_egl_backend.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_egl_backend.h new file mode 100644 index 0000000000..b585111a64 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_egl_backend.h @@ -0,0 +1,71 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "drm_plane.h" +#include "drm_render_backend.h" +#include "opengl/eglbackend.h" +#include "opengl/glutils.h" + +#include +#include +#include + +namespace KWin +{ + +struct DmaBufAttributes; +class BackendOutput; +class DrmAbstractOutput; +class DrmOutput; +class DumbSwapchain; +class DrmBackend; +class DrmGpu; +class EglGbmLayer; +class DrmOutputLayer; +class DrmPipeline; +class EglContext; +class EglDisplay; + +/** + * @brief OpenGL Backend using Egl on a GBM surface. + */ +class EglGbmBackend : public EglBackend, public DrmRenderBackend +{ + Q_OBJECT +public: + EglGbmBackend(DrmBackend *drmBackend); + ~EglGbmBackend() override; + + DrmDevice *drmDevice() const override; + + QList compatibleOutputLayers(BackendOutput *output) override; + + void init() override; + std::unique_ptr createDrmPlaneLayer(DrmPlane *plane) override; + std::unique_ptr createDrmPlaneLayer(DrmGpu *gpu, DrmPlane::TypeIndex type) override; + std::unique_ptr createLayer(DrmVirtualOutput *output) override; + + DrmGpu *gpu() const; + + EglDisplay *displayForGpu(DrmGpu *gpu); + std::shared_ptr contextForGpu(DrmGpu *gpu); + void resetContextForGpu(DrmGpu *gpu); + +private: + bool initializeEgl(); + bool initRenderingContext(); + EglDisplay *createEglDisplay(DrmGpu *gpu) const; + + DrmBackend *m_backend; + std::map> m_contexts; + + friend class EglGbmTexture; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_egl_layer.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_egl_layer.cpp new file mode 100644 index 0000000000..ea30287da3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_egl_layer.cpp @@ -0,0 +1,139 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_egl_layer.h" +#include "core/colorpipeline.h" +#include "core/iccprofile.h" +#include "drm_backend.h" +#include "drm_buffer.h" +#include "drm_crtc.h" +#include "drm_egl_backend.h" +#include "drm_gpu.h" +#include "drm_output.h" +#include "drm_pipeline.h" +#include "scene/surfaceitem_wayland.h" +#include "utils/envvar.h" +#include "wayland/surface.h" + +#include +#include +#include +#include + +namespace KWin +{ + +static EglGbmLayerSurface::BufferTarget targetFor(DrmGpu *gpu, DrmPlane::TypeIndex planeType) +{ + if ((!gpu->atomicModeSetting() || gpu->isVirtualMachine()) && planeType == DrmPlane::TypeIndex::Cursor) { + return EglGbmLayerSurface::BufferTarget::Dumb; + } else { + return EglGbmLayerSurface::BufferTarget::Normal; + } +} + +EglGbmLayer::EglGbmLayer(EglGbmBackend *eglBackend, DrmPlane *plane) + : DrmPipelineLayer(plane) + , m_surface(plane->gpu(), eglBackend, targetFor(plane->gpu(), plane->type.enumValue())) +{ +} + +EglGbmLayer::EglGbmLayer(EglGbmBackend *eglBackend, DrmGpu *gpu, DrmPlane::TypeIndex type) + : DrmPipelineLayer(type) + , m_surface(gpu, eglBackend, targetFor(gpu, type)) +{ +} + +std::optional EglGbmLayer::doBeginFrame() +{ + m_scanoutBuffer.reset(); + return m_surface.startRendering(targetRect().size(), + drmOutput()->transform().combine(OutputTransform::FlipY), + supportedDrmFormats(), + drmOutput()->blendingColor(), + drmOutput()->layerBlendingColor(), + drmOutput()->needsShadowBuffer() ? pipeline()->iccProfile() : nullptr, + drmOutput()->scale(), + drmOutput()->colorPowerTradeoff(), + drmOutput()->needsShadowBuffer(), + m_requiredAlphaBits); +} + +bool EglGbmLayer::doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) +{ + return m_surface.endRendering(damagedDeviceRegion, frame); +} + +bool EglGbmLayer::preparePresentationTest() +{ + if (m_type != OutputLayerType::Primary && drmOutput()->shouldDisableNonPrimaryPlanes()) { + return false; + } + m_scanoutBuffer.reset(); + return m_surface.renderTestBuffer(targetRect().size(), supportedDrmFormats(), drmOutput()->nextState().colorPowerTradeoff, m_requiredAlphaBits) != nullptr; +} + +bool EglGbmLayer::importScanoutBuffer(GraphicsBuffer *buffer, const std::shared_ptr &frame) +{ + static const bool directScanoutDisabled = environmentVariableBoolValue("KWIN_DRM_NO_DIRECT_SCANOUT").value_or(false); + if (directScanoutDisabled) { + return false; + } + if (m_type != OutputLayerType::Primary && drmOutput()->shouldDisableNonPrimaryPlanes()) { + return false; + } + if (gpu()->needsModeset()) { + // don't do direct scanout with modeset, it might lead to locking + // the hardware to some buffer format we can't switch away from + return false; + } + if (drmOutput()->needsShadowBuffer()) { + // while there are cases where this could still work (if the client prepares the buffer to match the output exactly) + // it's likely not worth making this code more complicated to handle those edge cases + return false; + } + if (gpu() != gpu()->platform()->primaryGpu()) { + // Disable direct scanout between GPUs, as + // - there are some significant driver bugs with direct scanout from other GPUs, + // like https://gitlab.freedesktop.org/drm/amd/-/issues/2075 + // - with implicit modifiers, direct scanout on secondary GPUs + // is also very unlikely to yield the correct results. + // TODO once we know what buffer a GPU is meant for, loosen this check again + // Right now this just assumes all buffers are on the primary GPU + return false; + } + if (!m_colorPipeline.isIdentity() && drmOutput()->colorPowerTradeoff() == BackendOutput::ColorPowerTradeoff::PreferAccuracy) { + return false; + } + // kernel documentation says that + // "Devices that don’t support subpixel plane coordinates can ignore the fractional part." + // so we need to make sure that doesn't cause a difference vs the composited result + if (sourceRect() != sourceRect().toRect()) { + return false; + } + if (offloadTransform() != OutputTransform::Kind::Normal && (!m_plane || !m_plane->supportsTransformation(offloadTransform()))) { + return false; + } + m_scanoutBuffer = gpu()->importBuffer(buffer, FileDescriptor{}); + if (m_scanoutBuffer) { + m_surface.forgetDamage(); // TODO: Use absolute frame sequence numbers for indexing the DamageJournal. It's more flexible and less error-prone + } + return m_scanoutBuffer != nullptr; +} + +std::shared_ptr EglGbmLayer::currentBuffer() const +{ + return m_scanoutBuffer ? m_scanoutBuffer : m_surface.currentBuffer(); +} + +void EglGbmLayer::releaseBuffers() +{ + m_scanoutBuffer.reset(); + m_surface.destroyResources(); +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_egl_layer.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_egl_layer.h new file mode 100644 index 0000000000..09865c2ff7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_egl_layer.h @@ -0,0 +1,44 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "drm_layer.h" + +#include "drm_egl_layer_surface.h" + +#include +#include +#include +#include + +namespace KWin +{ + +class EglGbmBackend; +class DrmPlane; + +class EglGbmLayer : public DrmPipelineLayer +{ +public: + explicit EglGbmLayer(EglGbmBackend *eglBackend, DrmPlane *plane); + explicit EglGbmLayer(EglGbmBackend *eglBackend, DrmGpu *gpu, DrmPlane::TypeIndex type); + + std::optional doBeginFrame() override; + bool doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) override; + bool preparePresentationTest() override; + std::shared_ptr currentBuffer() const override; + void releaseBuffers() override; + +private: + bool importScanoutBuffer(GraphicsBuffer *buffer, const std::shared_ptr &frame) override; + + EglGbmLayerSurface m_surface; + std::shared_ptr m_scanoutBuffer; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_egl_layer_surface.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_egl_layer_surface.cpp new file mode 100644 index 0000000000..21bc445f5e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_egl_layer_surface.cpp @@ -0,0 +1,685 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_egl_layer_surface.h" + +#include "config-kwin.h" + +#include "core/colortransformation.h" +#include "core/graphicsbufferview.h" +#include "core/iccprofile.h" +#include "drm_egl_backend.h" +#include "drm_gpu.h" +#include "drm_logging.h" +#include "opengl/eglnativefence.h" +#include "opengl/eglswapchain.h" +#include "opengl/gllut.h" +#include "opengl/glrendertimequery.h" +#include "opengl/icc_shader.h" +#include "qpainter/qpainterswapchain.h" +#include "utils/drm_format_helper.h" +#include "utils/envvar.h" + +#include +#include +#include +#include + +namespace KWin +{ + +static const QList linearModifier = {DRM_FORMAT_MOD_LINEAR}; +static const QList implicitModifier = {DRM_FORMAT_MOD_INVALID}; +static const QList cpuCopyFormats = {DRM_FORMAT_ARGB8888, DRM_FORMAT_XRGB8888}; + +static const bool bufferAgeEnabled = environmentVariableBoolValue("KWIN_USE_BUFFER_AGE").value_or(true); +static const bool s_forceMGPUSync = environmentVariableBoolValue("KWIN_DRM_FORCE_GL_FINISH_MGPU_COPY").value_or(false); +static const bool s_forcePresentSync = environmentVariableBoolValue("KWIN_DRM_FORCE_GL_FINISH_PRESENT").value_or(false); + +static gbm_format_name_desc formatName(uint32_t format) +{ + gbm_format_name_desc ret; + gbm_format_get_name(format, &ret); + return ret; +} + +EglGbmLayerSurface::EglGbmLayerSurface(DrmGpu *gpu, EglGbmBackend *eglBackend, BufferTarget target) + : m_gpu(gpu) + , m_eglBackend(eglBackend) + , m_requestedBufferTarget(target) +{ +} + +EglGbmLayerSurface::~EglGbmLayerSurface() = default; + +EglGbmLayerSurface::Surface::~Surface() +{ + if (importContext) { + importContext->makeCurrent(); + importGbmSwapchain.reset(); + importedTextureCache.clear(); + importContext.reset(); + } + if (context) { + context->makeCurrent(); + } +} + +void EglGbmLayerSurface::destroyResources() +{ + m_surface = {}; + m_oldSurface = {}; +} + +std::optional EglGbmLayerSurface::startRendering(const QSize &bufferSize, OutputTransform transformation, + const QHash> &formats, + const std::shared_ptr &blendingColor, + const std::shared_ptr &layerBlendingColor, + const std::shared_ptr &iccProfile, double scale, + BackendOutput::ColorPowerTradeoff tradeoff, bool useShadowBuffer, + uint32_t requiredAlphaBits) +{ + if (!checkSurface(bufferSize, formats, tradeoff, requiredAlphaBits)) { + return std::nullopt; + } + m_oldSurface.reset(); + + if (!m_eglBackend->openglContext()->makeCurrent()) { + return std::nullopt; + } + + auto slot = m_surface->gbmSwapchain->acquire(); + if (!slot) { + return std::nullopt; + } + + if (slot->framebuffer()->colorAttachment()->contentTransform() != transformation) { + m_surface->damageJournal.clear(); + } + slot->framebuffer()->colorAttachment()->setContentTransform(transformation); + m_surface->currentSlot = slot; + m_surface->scale = scale; + + if (m_surface->blendingColor != blendingColor || m_surface->layerBlendingColor != layerBlendingColor || m_surface->iccProfile != iccProfile) { + m_surface->damageJournal.clear(); + m_surface->shadowDamageJournal.clear(); + m_surface->needsShadowBuffer = useShadowBuffer; + m_surface->blendingColor = blendingColor; + m_surface->layerBlendingColor = layerBlendingColor; + m_surface->iccProfile = iccProfile; + if (iccProfile) { + if (!m_surface->iccShader) { + m_surface->iccShader = std::make_unique(); + } + } else { + m_surface->iccShader.reset(); + } + } + + m_surface->compositingTimeQuery = std::make_unique(m_surface->context); + m_surface->compositingTimeQuery->begin(); + if (m_surface->needsShadowBuffer) { + if (!m_surface->shadowSwapchain || m_surface->shadowSwapchain->size() != m_surface->gbmSwapchain->size()) { + const auto formats = m_eglBackend->eglDisplayObject()->nonExternalOnlySupportedDrmFormats(); + const QList sortedFormats = OutputLayer::filterAndSortFormats(formats, requiredAlphaBits, tradeoff); + for (const auto format : sortedFormats) { + auto modifiers = formats[format.drmFormat]; + if (format.floatingPoint && m_eglBackend->gpu()->isAmdgpu() && qEnvironmentVariableIntValue("KWIN_DRM_NO_DCC_WORKAROUND") == 0) { + // using modifiers with DCC here causes glitches on amdgpu: https://gitlab.freedesktop.org/mesa/mesa/-/issues/10875 + if (!modifiers.contains(DRM_FORMAT_MOD_LINEAR)) { + continue; + } + modifiers = {DRM_FORMAT_MOD_LINEAR}; + } + m_surface->shadowSwapchain = EglSwapchain::create(m_eglBackend->drmDevice()->allocator(), m_eglBackend->openglContext(), m_surface->gbmSwapchain->size(), format.drmFormat, modifiers); + if (m_surface->shadowSwapchain) { + break; + } + } + } + if (!m_surface->shadowSwapchain) { + qCCritical(KWIN_DRM) << "Failed to create shadow swapchain!"; + return std::nullopt; + } + m_surface->currentShadowSlot = m_surface->shadowSwapchain->acquire(); + if (!m_surface->currentShadowSlot) { + return std::nullopt; + } + m_surface->currentShadowSlot->texture()->setContentTransform(m_surface->currentSlot->framebuffer()->colorAttachment()->contentTransform()); + return OutputLayerBeginFrameInfo{ + .renderTarget = RenderTarget(m_surface->currentShadowSlot->framebuffer(), m_surface->blendingColor), + .repaint = bufferAgeEnabled ? m_surface->shadowDamageJournal.accumulate(m_surface->currentShadowSlot->age(), Region::infinite()) : Region::infinite(), + }; + } else { + m_surface->shadowSwapchain.reset(); + m_surface->currentShadowSlot.reset(); + return OutputLayerBeginFrameInfo{ + .renderTarget = RenderTarget(m_surface->currentSlot->framebuffer(), m_surface->blendingColor), + .repaint = bufferAgeEnabled ? m_surface->damageJournal.accumulate(slot->age(), Region::infinite()) : Region::infinite(), + }; + } +} + +static GLVertexBuffer *uploadGeometry(const Region &devicePixels, const QSize &fboSize) +{ + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + vbo->setAttribLayout(std::span(GLVertexBuffer::GLVertex2DLayout), sizeof(GLVertex2D)); + const auto optMap = vbo->map(devicePixels.rects().size() * 6); + if (!optMap) { + return nullptr; + } + const auto map = *optMap; + size_t vboIndex = 0; + for (RectF rect : devicePixels.rects()) { + const float x0 = rect.left(); + const float y0 = rect.top(); + const float x1 = rect.right(); + const float y1 = rect.bottom(); + + const float u0 = x0 / fboSize.width(); + const float v0 = y0 / fboSize.height(); + const float u1 = x1 / fboSize.width(); + const float v1 = y1 / fboSize.height(); + + // first triangle + map[vboIndex++] = GLVertex2D{ + .position = QVector2D(x0, y0), + .texcoord = QVector2D(u0, v0), + }; + map[vboIndex++] = GLVertex2D{ + .position = QVector2D(x1, y1), + .texcoord = QVector2D(u1, v1), + }; + map[vboIndex++] = GLVertex2D{ + .position = QVector2D(x0, y1), + .texcoord = QVector2D(u0, v1), + }; + + // second triangle + map[vboIndex++] = GLVertex2D{ + .position = QVector2D(x0, y0), + .texcoord = QVector2D(u0, v0), + }; + map[vboIndex++] = GLVertex2D{ + .position = QVector2D(x1, y0), + .texcoord = QVector2D(u1, v0), + }; + map[vboIndex++] = GLVertex2D{ + .position = QVector2D(x1, y1), + .texcoord = QVector2D(u1, v1), + }; + } + vbo->unmap(); + vbo->setVertexCount(vboIndex); + return vbo; +} + +bool EglGbmLayerSurface::endRendering(const Region &damagedDeviceRegion, OutputFrame *frame) +{ + if (m_surface->needsShadowBuffer) { + const Region deviceRepaint = damagedDeviceRegion | m_surface->damageJournal.accumulate(m_surface->currentSlot->age(), Region::infinite()); + m_surface->damageJournal.add(damagedDeviceRegion); + m_surface->shadowDamageJournal.add(damagedDeviceRegion); + const auto mapping = m_surface->currentShadowSlot->framebuffer()->colorAttachment()->contentTransform().combine(OutputTransform::FlipY); + const QSize rotatedSize = mapping.map(m_surface->gbmSwapchain->size()); + const Region repaint = mapping.map(deviceRepaint & Rect(QPoint(), rotatedSize), rotatedSize); + + GLFramebuffer *fbo = m_surface->currentSlot->framebuffer(); + GLFramebuffer::pushFramebuffer(fbo); + ShaderBinder binder = m_surface->iccShader ? ShaderBinder(m_surface->iccShader->shader()) : ShaderBinder(ShaderTrait::MapTexture | ShaderTrait::TransformColorspace); + // this transform is absolute colorimetric, whitepoint adjustment is done in compositing already + if (m_surface->iccShader) { + m_surface->iccShader->setUniforms(m_surface->iccProfile, m_surface->blendingColor, RenderingIntent::AbsoluteColorimetricNoAdaptation); + } else { + binder.shader()->setColorspaceUniforms(m_surface->blendingColor, m_surface->layerBlendingColor, RenderingIntent::AbsoluteColorimetricNoAdaptation); + } + QMatrix4x4 mat; + mat.scale(1, -1); + mat.ortho(QRectF(QPointF(), fbo->size())); + binder.shader()->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, mat); + glDisable(GL_BLEND); + if (const auto vbo = uploadGeometry(repaint, m_surface->gbmSwapchain->size())) { + m_surface->currentShadowSlot->texture()->bind(); + vbo->render(GL_TRIANGLES); + m_surface->currentShadowSlot->texture()->unbind(); + } + EGLNativeFence fence(m_surface->context->displayObject()); + m_surface->shadowSwapchain->release(m_surface->currentShadowSlot, fence.takeFileDescriptor()); + GLFramebuffer::popFramebuffer(); + } else { + m_surface->damageJournal.add(damagedDeviceRegion); + } + m_surface->compositingTimeQuery->end(); + if (frame) { + frame->addRenderTimeQuery(std::move(m_surface->compositingTimeQuery)); + } + glFlush(); + EGLNativeFence sourceFence(m_eglBackend->eglDisplayObject()); + if (!sourceFence.isValid() || s_forcePresentSync) { + // llvmpipe doesn't do synchronization properly: https://gitlab.freedesktop.org/mesa/mesa/-/issues/9375 + // and NVidia doesn't support implicit sync + glFinish(); + } + m_surface->gbmSwapchain->release(m_surface->currentSlot, sourceFence.fileDescriptor().duplicate()); + const auto buffer = importBuffer(m_surface.get(), m_surface->currentSlot.get(), sourceFence.takeFileDescriptor(), frame, damagedDeviceRegion); + if (buffer) { + m_surface->currentFramebuffer = buffer; + return true; + } else { + return false; + } +} + +EglGbmBackend *EglGbmLayerSurface::eglBackend() const +{ + return m_eglBackend; +} + +std::shared_ptr EglGbmLayerSurface::currentBuffer() const +{ + return m_surface ? m_surface->currentFramebuffer : nullptr; +} + +const std::shared_ptr &EglGbmLayerSurface::colorDescription() const +{ + if (m_surface) { + return m_surface->blendingColor; + } else { + return ColorDescription::sRGB; + } +} + +std::shared_ptr EglGbmLayerSurface::renderTestBuffer(const QSize &bufferSize, const QHash> &formats, BackendOutput::ColorPowerTradeoff tradeoff, uint32_t requiredAlphaBits) +{ + EglContext *context = m_eglBackend->openglContext(); + if (!context->makeCurrent()) { + qCWarning(KWIN_DRM) << "EglGbmLayerSurface::renderTestBuffer: failed to make opengl context current"; + return nullptr; + } + + if (checkSurface(bufferSize, formats, tradeoff, requiredAlphaBits)) { + return m_surface->currentFramebuffer; + } else { + return nullptr; + } +} + +void EglGbmLayerSurface::forgetDamage() +{ + if (m_surface) { + m_surface->damageJournal.clear(); + m_surface->importDamageJournal.clear(); + m_surface->shadowDamageJournal.clear(); + } +} + +bool EglGbmLayerSurface::checkSurface(const QSize &size, const QHash> &formats, BackendOutput::ColorPowerTradeoff tradeoff, uint32_t requiredAlphaBits) +{ + if (doesSurfaceFit(m_surface.get(), size, formats, tradeoff, requiredAlphaBits)) { + return true; + } + if (doesSurfaceFit(m_oldSurface.get(), size, formats, tradeoff, requiredAlphaBits)) { + m_surface = std::move(m_oldSurface); + return true; + } + if (auto newSurface = createSurface(size, formats, tradeoff, requiredAlphaBits)) { + m_oldSurface = std::move(m_surface); + if (m_oldSurface) { + // FIXME: Use absolute frame sequence numbers for indexing the DamageJournal + m_oldSurface->damageJournal.clear(); + m_oldSurface->shadowDamageJournal.clear(); + m_oldSurface->gbmSwapchain->resetBufferAge(); + if (m_oldSurface->shadowSwapchain) { + m_oldSurface->shadowSwapchain->resetBufferAge(); + } + if (m_oldSurface->importGbmSwapchain) { + m_oldSurface->importGbmSwapchain->resetBufferAge(); + m_oldSurface->importDamageJournal.clear(); + } + } + m_surface = std::move(newSurface); + return true; + } + return false; +} + +bool EglGbmLayerSurface::doesSurfaceFit(Surface *surface, const QSize &size, const QHash> &formats, BackendOutput::ColorPowerTradeoff tradeoff, uint32_t requiredAlphaBits) const +{ + if (!surface || surface->needsRecreation || !surface->gbmSwapchain || surface->gbmSwapchain->size() != size) { + return false; + } + if (surface->tradeoff != tradeoff || surface->requiredAlphaBits != requiredAlphaBits) { + // TODO requiredAlphaBits could be a bit more conservative with reallocations? + return false; + } + if (surface->bufferTarget == BufferTarget::Dumb) { + return formats.contains(surface->importDumbSwapchain->format()); + } + switch (surface->importMode) { + case MultiGpuImportMode::None: { + const auto format = surface->gbmSwapchain->format(); + return formats.contains(format) && (surface->gbmSwapchain->modifier() == DRM_FORMAT_MOD_INVALID || formats[format].contains(surface->gbmSwapchain->modifier())); + } + case MultiGpuImportMode::DumbBuffer: + return formats.contains(surface->importDumbSwapchain->format()); + case MultiGpuImportMode::Egl: { + const auto format = surface->importGbmSwapchain->format(); + const auto it = formats.find(format); + return it != formats.end() && (surface->importGbmSwapchain->modifier() == DRM_FORMAT_MOD_INVALID || it->contains(surface->importGbmSwapchain->modifier())); + } + } + Q_UNREACHABLE(); +} + +std::unique_ptr EglGbmLayerSurface::createSurface(const QSize &size, const QHash> &formats, BackendOutput::ColorPowerTradeoff tradeoff, uint32_t requiredAlphaBits) const +{ + const QList sortedFormats = OutputLayer::filterAndSortFormats(formats, requiredAlphaBits, tradeoff); + + // special case: the cursor plane needs linear, but not all GPUs (NVidia) can render to linear + auto bufferTarget = m_requestedBufferTarget; + if (m_gpu == m_eglBackend->gpu()) { + const bool needsLinear = std::ranges::all_of(sortedFormats, [&formats](const FormatInfo &fmt) { + const auto &mods = formats[fmt.drmFormat]; + return std::ranges::all_of(mods, [](uint64_t mod) { + return mod == DRM_FORMAT_MOD_LINEAR; + }); + }); + if (needsLinear) { + const auto renderFormats = m_eglBackend->eglDisplayObject()->allSupportedDrmFormats(); + const bool noLinearSupport = std::ranges::none_of(sortedFormats, [&renderFormats](const auto &formatInfo) { + const auto it = renderFormats.constFind(formatInfo.drmFormat); + return it != renderFormats.cend() && it->nonExternalOnlyModifiers.contains(DRM_FORMAT_MOD_LINEAR); + }); + if (noLinearSupport) { + bufferTarget = BufferTarget::Dumb; + } + } + } + + const auto doTestFormats = [this, &size, &formats, bufferTarget, tradeoff, requiredAlphaBits](const QList &gbmFormats, MultiGpuImportMode importMode) -> std::unique_ptr { + for (const auto &format : gbmFormats) { + auto surface = createSurface(size, format.drmFormat, formats[format.drmFormat], importMode, bufferTarget, tradeoff, requiredAlphaBits); + if (surface) { + return surface; + } + } + return nullptr; + }; + if (m_gpu == m_eglBackend->gpu()) { + return doTestFormats(sortedFormats, MultiGpuImportMode::None); + } + // special case, we're using different display devices but the same render device + const auto display = m_eglBackend->displayForGpu(m_gpu); + if (display && !display->renderNode().isEmpty() && display->renderNode() == m_eglBackend->eglDisplayObject()->renderNode()) { + if (auto surface = doTestFormats(sortedFormats, MultiGpuImportMode::None)) { + return surface; + } + } + if (auto surface = doTestFormats(sortedFormats, MultiGpuImportMode::Egl)) { + // qCDebug(KWIN_DRM) << "chose egl import with format" << formatName(surface->gbmSwapchain->format()).name << "and modifier" << surface->gbmSwapchain->modifier(); + return surface; + } + if (auto surface = doTestFormats(sortedFormats, MultiGpuImportMode::DumbBuffer)) { + qCDebug(KWIN_DRM) << "chose cpu import with format" << formatName(surface->gbmSwapchain->format()).name << "and modifier" << surface->gbmSwapchain->modifier(); + return surface; + } + return nullptr; +} + +static QList filterModifiers(const QList &one, const QList &two) +{ + QList ret = one; + ret.erase(std::remove_if(ret.begin(), ret.end(), [&two](uint64_t mod) { + return !two.contains(mod); + }), + ret.end()); + return ret; +} + +std::unique_ptr EglGbmLayerSurface::createSurface(const QSize &size, uint32_t format, const QList &modifiers, MultiGpuImportMode importMode, BufferTarget bufferTarget, BackendOutput::ColorPowerTradeoff tradeoff, uint32_t requiredAlphaBits) const +{ + const bool cpuCopy = importMode == MultiGpuImportMode::DumbBuffer || bufferTarget == BufferTarget::Dumb; + QList renderModifiers; + auto ret = std::make_unique(); + const auto drmFormat = m_eglBackend->eglDisplayObject()->allSupportedDrmFormats()[format]; + QList eglImportModifiers; + if (importMode == MultiGpuImportMode::Egl) { + ret->importContext = m_eglBackend->contextForGpu(m_gpu); + if (!ret->importContext || ret->importContext->isSoftwareRenderer()) { + return nullptr; + } + const auto importDrmFormat = ret->importContext->displayObject()->allSupportedDrmFormats()[format]; + eglImportModifiers = filterModifiers(modifiers, importDrmFormat.nonExternalOnlyModifiers); + if (eglImportModifiers.empty()) { + return nullptr; + } + renderModifiers = filterModifiers(importDrmFormat.allModifiers, + drmFormat.nonExternalOnlyModifiers); + // transferring non-linear buffers with implicit modifiers between GPUs is likely to yield wrong results + renderModifiers.removeAll(DRM_FORMAT_MOD_INVALID); + } else if (cpuCopy) { + if (!cpuCopyFormats.contains(format)) { + return nullptr; + } + renderModifiers = drmFormat.nonExternalOnlyModifiers; + } else { + renderModifiers = filterModifiers(modifiers, drmFormat.nonExternalOnlyModifiers); + } + if (renderModifiers.empty()) { + return nullptr; + } + ret->context = m_eglBackend->contextForGpu(m_eglBackend->gpu()); + ret->bufferTarget = bufferTarget; + ret->importMode = importMode; + ret->gbmSwapchain = createGbmSwapchain(m_eglBackend->gpu(), m_eglBackend->openglContext(), size, format, renderModifiers, importMode, bufferTarget); + ret->tradeoff = tradeoff; + ret->requiredAlphaBits = requiredAlphaBits; + if (!ret->gbmSwapchain) { + return nullptr; + } + if (cpuCopy) { + ret->importDumbSwapchain = std::make_unique(m_gpu->drmDevice()->allocator(), size, format); + } else if (importMode == MultiGpuImportMode::Egl) { + ret->importGbmSwapchain = createGbmSwapchain(m_gpu, ret->importContext.get(), size, format, eglImportModifiers, MultiGpuImportMode::None, BufferTarget::Normal); + if (!ret->importGbmSwapchain) { + return nullptr; + } + } + if (!doRenderTestBuffer(ret.get())) { + return nullptr; + } + return ret; +} + +std::shared_ptr EglGbmLayerSurface::createGbmSwapchain(DrmGpu *gpu, EglContext *context, const QSize &size, uint32_t format, const QList &modifiers, MultiGpuImportMode importMode, BufferTarget bufferTarget) const +{ + const bool linearSupported = modifiers.contains(DRM_FORMAT_MOD_LINEAR); + const bool preferLinear = importMode == MultiGpuImportMode::DumbBuffer || bufferTarget == BufferTarget::Dumb; + if (linearSupported && preferLinear) { + if (const auto swapchain = EglSwapchain::create(gpu->drmDevice()->allocator(), context, size, format, linearModifier)) { + return swapchain; + } + } + return EglSwapchain::create(gpu->drmDevice()->allocator(), context, size, format, modifiers); +} + +std::shared_ptr EglGbmLayerSurface::doRenderTestBuffer(Surface *surface) const +{ + auto slot = surface->gbmSwapchain->acquire(); + if (!slot) { + return nullptr; + } + if (!m_gpu->atomicModeSetting()) { + EglContext::currentContext()->pushFramebuffer(slot->framebuffer()); + glClearColor(0, 0, 0, 0); + glClear(GL_COLOR_BUFFER_BIT); + EglContext::currentContext()->popFramebuffer(); + } + if (const auto ret = importBuffer(surface, slot.get(), FileDescriptor{}, nullptr, Region::infinite())) { + // clear the render journal, because this was just a nonsense frame + surface->importDamageJournal.clear(); + surface->currentSlot = slot; + surface->currentFramebuffer = ret; + return ret; + } else { + return nullptr; + } +} + +std::shared_ptr EglGbmLayerSurface::importBuffer(Surface *surface, EglSwapchainSlot *slot, FileDescriptor &&readFence, OutputFrame *frame, const Region &damagedDeviceRegion) const +{ + if (surface->bufferTarget == BufferTarget::Dumb || surface->importMode == MultiGpuImportMode::DumbBuffer) { + return importWithCpu(surface, slot, frame); + } else if (surface->importMode == MultiGpuImportMode::Egl) { + return importWithEgl(surface, slot, std::move(readFence), frame, damagedDeviceRegion); + } else { + const auto ret = m_gpu->importBuffer(slot->buffer(), std::move(readFence)); + if (!ret) { + qCWarning(KWIN_DRM, "Failed to create framebuffer: %s", strerror(errno)); + } + return ret; + } +} + +std::shared_ptr EglGbmLayerSurface::importWithEgl(Surface *surface, EglSwapchainSlot *source, FileDescriptor &&readFence, OutputFrame *frame, const Region &damagedDeviceRegion) const +{ + Q_ASSERT(surface->importGbmSwapchain); + + const auto display = m_eglBackend->displayForGpu(m_gpu); + // older versions of the NVidia proprietary driver support neither implicit sync nor EGL_ANDROID_native_fence_sync + if (!readFence.isValid() || !display->supportsNativeFence() || s_forceMGPUSync) { + glFinish(); + } + + if (!surface->importContext->makeCurrent()) { + qCWarning(KWIN_DRM, "Failed to make import context current"); + // this is probably caused by a GPU reset, let's not take any chances + surface->needsRecreation = true; + m_eglBackend->resetContextForGpu(m_gpu); + return nullptr; + } + const auto restoreContext = qScopeGuard([this]() { + m_eglBackend->openglContext()->makeCurrent(); + }); + if (surface->importContext->checkGraphicsResetStatus() != GL_NO_ERROR) { + qCWarning(KWIN_DRM, "Detected GPU reset on secondary GPU %s", qPrintable(m_gpu->drmDevice()->path())); + surface->needsRecreation = true; + m_eglBackend->resetContextForGpu(m_gpu); + return nullptr; + } + std::unique_ptr renderTime; + if (frame) { + renderTime = std::make_unique(surface->importContext); + renderTime->begin(); + } + + if (readFence.isValid()) { + const auto destinationFence = EGLNativeFence::importFence(surface->importContext->displayObject(), std::move(readFence)); + destinationFence.waitSync(); + } + + auto &sourceTexture = surface->importedTextureCache[source->buffer()]; + if (!sourceTexture) { + sourceTexture = surface->importContext->importDmaBufAsTexture(*source->buffer()->dmabufAttributes()); + } + if (!sourceTexture) { + qCWarning(KWIN_DRM, "failed to import the source texture!"); + return nullptr; + } + auto slot = surface->importGbmSwapchain->acquire(); + if (!slot) { + qCWarning(KWIN_DRM, "failed to import the local texture!"); + return nullptr; + } + + slot->texture()->setContentTransform(source->texture()->contentTransform()); + + const Region deviceRepaint = damagedDeviceRegion | surface->importDamageJournal.accumulate(slot->age(), Region::infinite()); + surface->importDamageJournal.add(damagedDeviceRegion); + + const auto mapping = slot->texture()->contentTransform().combine(OutputTransform::FlipY); + const QSize rotatedSize = mapping.map(slot->texture()->size()); + const Region repaint = mapping.map(deviceRepaint & Rect(QPoint(), rotatedSize), rotatedSize); + + GLFramebuffer *fbo = slot->framebuffer(); + surface->importContext->pushFramebuffer(fbo); + + const auto shader = surface->importContext->shaderManager()->pushShader(sourceTexture->target() == GL_TEXTURE_EXTERNAL_OES ? ShaderTrait::MapExternalTexture : ShaderTrait::MapTexture); + QMatrix4x4 mat; + mat.scale(1, -1); + mat.ortho(QRect(QPoint(), fbo->size())); + shader->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, mat); + + if (const auto vbo = uploadGeometry(repaint, fbo->size())) { + sourceTexture->bind(); + vbo->render(GL_TRIANGLES); + sourceTexture->unbind(); + } + + surface->importContext->popFramebuffer(); + surface->importContext->shaderManager()->popShader(); + glFlush(); + EGLNativeFence endFence(display); + if (!endFence.isValid() || s_forcePresentSync) { + glFinish(); + } + surface->importGbmSwapchain->release(slot, endFence.fileDescriptor().duplicate()); + if (frame) { + renderTime->end(); + frame->addRenderTimeQuery(std::move(renderTime)); + } + + return m_gpu->importBuffer(slot->buffer(), endFence.takeFileDescriptor()); +} + +std::shared_ptr EglGbmLayerSurface::importWithCpu(Surface *surface, EglSwapchainSlot *source, OutputFrame *frame) const +{ + std::unique_ptr copyTime; + if (frame) { + copyTime = std::make_unique(); + } + Q_ASSERT(surface->importDumbSwapchain); + const auto slot = surface->importDumbSwapchain->acquire(); + if (!slot) { + qCWarning(KWIN_DRM) << "EglGbmLayerSurface::importWithCpu: failed to get a target dumb buffer"; + return nullptr; + } + const auto size = source->buffer()->size(); + const qsizetype srcStride = 4 * size.width(); + EglContext *context = m_eglBackend->openglContext(); + GLFramebuffer::pushFramebuffer(source->framebuffer()); + QImage *const dst = slot->view()->image(); + if (dst->bytesPerLine() == srcStride) { + context->glReadnPixels(0, 0, dst->width(), dst->height(), GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, dst->sizeInBytes(), dst->bits()); + } else { + // there's padding, need to copy line by line + if (surface->cpuCopyCache.size() != dst->size()) { + surface->cpuCopyCache = QImage(dst->size(), QImage::Format_RGBA8888); + } + context->glReadnPixels(0, 0, dst->width(), dst->height(), GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, surface->cpuCopyCache.sizeInBytes(), surface->cpuCopyCache.bits()); + for (int i = 0; i < dst->height(); i++) { + std::memcpy(dst->scanLine(i), surface->cpuCopyCache.scanLine(i), srcStride); + } + } + GLFramebuffer::popFramebuffer(); + + const auto ret = m_gpu->importBuffer(slot->buffer(), FileDescriptor{}); + if (!ret) { + qCWarning(KWIN_DRM, "Failed to create a framebuffer: %s", strerror(errno)); + } + surface->importDumbSwapchain->release(slot); + if (frame) { + copyTime->end(); + frame->addRenderTimeQuery(std::move(copyTime)); + } + return ret; +} +} + +#include "moc_drm_egl_layer_surface.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_egl_layer_surface.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_egl_layer_surface.h new file mode 100644 index 0000000000..7bbc6ab855 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_egl_layer_surface.h @@ -0,0 +1,125 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include +#include +#include +#include + +#include "core/outputlayer.h" +#include "drm_plane.h" +#include "opengl/gltexture.h" +#include "utils/damagejournal.h" +#include "utils/filedescriptor.h" + +namespace KWin +{ + +class DrmFramebuffer; +class EglSwapchain; +class EglSwapchainSlot; +class QPainterSwapchain; +class ShadowBuffer; +class EglContext; +class EglGbmBackend; +class GraphicsBuffer; +class SurfaceItem; +class GLTexture; +class GLRenderTimeQuery; +class ColorTransformation; +class GlLookUpTable; +class IccProfile; +class IccShader; + +class EglGbmLayerSurface : public QObject +{ + Q_OBJECT +public: + enum class BufferTarget { + Normal, + Dumb + }; + explicit EglGbmLayerSurface(DrmGpu *gpu, EglGbmBackend *eglBackend, BufferTarget target = BufferTarget::Normal); + ~EglGbmLayerSurface(); + + std::optional startRendering(const QSize &bufferSize, OutputTransform transformation, const QHash> &formats, const std::shared_ptr &blendingColor, const std::shared_ptr &layerBlendingColor, const std::shared_ptr &iccProfile, double scale, BackendOutput::ColorPowerTradeoff tradeoff, bool useShadowBuffer, uint32_t requiredAlphaBits); + bool endRendering(const Region &damagedDeviceRegion, OutputFrame *frame); + + void destroyResources(); + EglGbmBackend *eglBackend() const; + std::shared_ptr renderTestBuffer(const QSize &bufferSize, const QHash> &formats, BackendOutput::ColorPowerTradeoff tradeoff, uint32_t requiredAlphaBits); + void forgetDamage(); + + std::shared_ptr currentBuffer() const; + const std::shared_ptr &colorDescription() const; + +private: + enum class MultiGpuImportMode { + None, + Egl, + DumbBuffer + }; + struct Surface + { + ~Surface(); + + bool needsRecreation = false; + + std::shared_ptr context; + std::shared_ptr gbmSwapchain; + std::shared_ptr currentSlot; + DamageJournal damageJournal; + std::unique_ptr importDumbSwapchain; + std::shared_ptr importContext; + std::shared_ptr importGbmSwapchain; + QHash> importedTextureCache; + QImage cpuCopyCache; + MultiGpuImportMode importMode; + DamageJournal importDamageJournal; + std::shared_ptr currentFramebuffer; + BufferTarget bufferTarget; + double scale = 1; + uint32_t requiredAlphaBits = 0; + + // for color management + bool needsShadowBuffer = false; + std::shared_ptr shadowSwapchain; + std::shared_ptr currentShadowSlot; + std::shared_ptr layerBlendingColor = ColorDescription::sRGB; + std::shared_ptr blendingColor = ColorDescription::sRGB; + double brightness = 1.0; + std::unique_ptr iccShader; + std::shared_ptr iccProfile; + DamageJournal shadowDamageJournal; + BackendOutput::ColorPowerTradeoff tradeoff = BackendOutput::ColorPowerTradeoff::PreferEfficiency; + + std::unique_ptr compositingTimeQuery; + }; + bool checkSurface(const QSize &size, const QHash> &formats, BackendOutput::ColorPowerTradeoff tradeoff, uint32_t requiredAlphaBits); + bool doesSurfaceFit(Surface *surface, const QSize &size, const QHash> &formats, BackendOutput::ColorPowerTradeoff tradeoff, uint32_t requiredAlphaBits) const; + std::unique_ptr createSurface(const QSize &size, const QHash> &formats, BackendOutput::ColorPowerTradeoff tradeoff, uint32_t requiredAlphaBits) const; + std::unique_ptr createSurface(const QSize &size, uint32_t format, const QList &modifiers, MultiGpuImportMode importMode, BufferTarget bufferTarget, BackendOutput::ColorPowerTradeoff tradeoff, uint32_t requiredAlphaBits) const; + std::shared_ptr createGbmSwapchain(DrmGpu *gpu, EglContext *context, const QSize &size, uint32_t format, const QList &modifiers, MultiGpuImportMode importMode, BufferTarget bufferTarget) const; + + std::shared_ptr doRenderTestBuffer(Surface *surface) const; + std::shared_ptr importBuffer(Surface *surface, EglSwapchainSlot *source, FileDescriptor &&readFence, OutputFrame *frame, const Region &damagedDeviceRegion) const; + std::shared_ptr importWithEgl(Surface *surface, EglSwapchainSlot *source, FileDescriptor &&readFence, OutputFrame *frame, const Region &damagedDeviceRegion) const; + std::shared_ptr importWithCpu(Surface *surface, EglSwapchainSlot *source, OutputFrame *frame) const; + + std::unique_ptr m_surface; + std::unique_ptr m_oldSurface; + + DrmGpu *const m_gpu; + EglGbmBackend *const m_eglBackend; + const BufferTarget m_requestedBufferTarget; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_gpu.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_gpu.cpp new file mode 100644 index 0000000000..7c7ef15943 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_gpu.cpp @@ -0,0 +1,1116 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_gpu.h" + +#include "config-kwin.h" + +#include "core/gbmgraphicsbufferallocator.h" +#include "core/session.h" +#include "drm_backend.h" +#include "drm_buffer.h" +#include "drm_commit.h" +#include "drm_commit_thread.h" +#include "drm_connector.h" +#include "drm_crtc.h" +#include "drm_egl_backend.h" +#include "drm_layer.h" +#include "drm_logging.h" +#include "drm_output.h" +#include "drm_pipeline.h" +#include "drm_plane.h" +#include "drm_virtual_output.h" +#include "utils/envvar.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef DRM_CLIENT_CAP_CURSOR_PLANE_HOTSPOT +#define DRM_CLIENT_CAP_CURSOR_PLANE_HOTSPOT 6 +#endif +#ifndef DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP +#define DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP 0x15 +#endif +#ifndef DRM_CLIENT_CAP_PLANE_COLOR_PIPELINE +#define DRM_CLIENT_CAP_PLANE_COLOR_PIPELINE 7 +#endif + +using namespace std::chrono_literals; + +namespace KWin +{ + +static const std::optional s_modifiersEnv = environmentVariableBoolValue("KWIN_DRM_USE_MODIFIERS"); +static const std::optional s_colorPipelineEnv = environmentVariableBoolValue("KWIN_DRM_USE_COLOR_PIPELINE"); + +DrmGpu::DrmGpu(DrmBackend *backend, int fd, std::unique_ptr &&device) + : m_fd(fd) + , m_drmDevice(std::move(device)) + , m_atomicModeSetting(false) + , m_platform(backend) +{ + uint64_t capability = 0; + + if (drmGetCap(fd, DRM_CAP_CURSOR_WIDTH, &capability) == 0) { + m_cursorSize.setWidth(capability); + } else { + m_cursorSize.setWidth(64); + } + + if (drmGetCap(fd, DRM_CAP_CURSOR_HEIGHT, &capability) == 0) { + m_cursorSize.setHeight(capability); + } else { + m_cursorSize.setHeight(64); + } + + int ret = drmGetCap(fd, DRM_CAP_TIMESTAMP_MONOTONIC, &capability); + if (ret == 0 && capability == 1) { + m_presentationClock = CLOCK_MONOTONIC; + } else { + m_presentationClock = CLOCK_REALTIME; + } + + if (s_modifiersEnv.has_value() && *s_modifiersEnv == false) { + qCDebug(KWIN_DRM, "modifier support disabled by environment variable"); + } else { + m_addFB2ModifiersSupported = drmGetCap(fd, DRM_CAP_ADDFB2_MODIFIERS, &capability) == 0 && capability == 1; + qCDebug(KWIN_DRM) << "drmModeAddFB2WithModifiers is" << (m_addFB2ModifiersSupported ? "supported" : "not supported") << "on GPU" << this; + } + + // find out what driver this kms device is using + DrmUniquePtr version(drmGetVersion(fd)); + m_isI915 = strstr(version->name, "i915"); + m_isNVidia = strstr(version->name, "nvidia-drm"); + m_isAmdgpu = strstr(version->name, "amdgpu"); + m_isVmwgfx = strstr(version->name, "vmwgfx"); + m_isVirtualMachine = strstr(version->name, "virtio") || strstr(version->name, "qxl") + || strstr(version->name, "vmwgfx") || strstr(version->name, "vboxvideo"); + if (m_isNVidia) { + QFile moduleVersion("/sys/module/nvidia_drm/version"); + if (moduleVersion.open(QIODeviceBase::OpenModeFlag::ReadOnly)) { + m_nvidiaDriverVersion = Version::parseString(moduleVersion.readLine(100)); + } + } + m_driverName = version->name; + + m_socketNotifier = std::make_unique(fd, QSocketNotifier::Read); + connect(m_socketNotifier.get(), &QSocketNotifier::activated, this, &DrmGpu::dispatchEvents); + + initDrmResources(); + + if (m_atomicModeSetting == false) { + m_asyncPageflipSupported = drmGetCap(fd, DRM_CAP_ASYNC_PAGE_FLIP, &capability) == 0 && capability == 1; + } else { + m_asyncPageflipSupported = drmGetCap(fd, DRM_CAP_ATOMIC_ASYNC_PAGE_FLIP, &capability) == 0 && capability == 1; + } + + m_colorPipelineSupported = s_colorPipelineEnv.value_or(!m_isAmdgpu) && drmSetClientCap(fd, DRM_CLIENT_CAP_PLANE_COLOR_PIPELINE, 1) == 0; + + m_delayedModesetTimer.setInterval(0); + m_delayedModesetTimer.setSingleShot(true); + connect(&m_delayedModesetTimer, &QTimer::timeout, this, &DrmGpu::doModeset); + m_sharpnessSupported = std::ranges::all_of(m_crtcs, [](const std::unique_ptr &crtc) { + return crtc->sharpnessStrength.isValid(); + }); +} + +DrmGpu::~DrmGpu() +{ + // clean up all `DrmFramebuffer`s before destroying the egl display + removeOutputs(); + m_planeLayerMap.clear(); + m_legacyLayerMap.clear(); + m_legacyCursorLayerMap.clear(); + m_pipelineMap.clear(); + m_crtcs.clear(); + m_connectors.clear(); + m_planes.clear(); + m_socketNotifier.reset(); + m_eglDisplay.reset(); + m_platform->session()->closeRestricted(m_fd); +} + +FileDescriptor DrmGpu::createNonMasterFd() const +{ + char *path = drmGetDeviceNameFromFd2(m_fd); + FileDescriptor fd{open(path, O_RDWR | O_CLOEXEC)}; + free(path); + if (!fd.isValid()) { + qCWarning(KWIN_DRM) << "Could not open DRM fd for leasing!" << strerror(errno); + } else { + if (drmIsMaster(fd.get())) { + if (drmDropMaster(fd.get()) != 0) { + qCWarning(KWIN_DRM) << "Could not create a non-master DRM fd for leasing!" << strerror(errno); + return FileDescriptor{}; + } + } + } + return fd; +} + +clockid_t DrmGpu::presentationClock() const +{ + return m_presentationClock; +} + +void DrmGpu::initDrmResources() +{ + // try atomic mode setting + bool isEnvVarSet = false; + bool noAMS = qEnvironmentVariableIntValue("KWIN_DRM_NO_AMS", &isEnvVarSet) != 0 && isEnvVarSet; + // always set the cap, so autotests can read back properties, + // even when this DrmGpu otherwise only uses legacy modesetting + const bool atomicSuccessful = drmSetClientCap(m_fd, DRM_CLIENT_CAP_ATOMIC, 1) == 0; + if (noAMS) { + qCWarning(KWIN_DRM) << "Atomic Mode Setting requested off via environment variable. Using legacy mode on GPU" << this; + } else if (atomicSuccessful) { + if (m_isVirtualMachine) { + // ATOMIC must be set before attempting CURSOR_PLANE_HOTSPOT + if (drmSetClientCap(m_fd, DRM_CLIENT_CAP_CURSOR_PLANE_HOTSPOT, 1) != 0) { + qCWarning(KWIN_DRM, "Atomic Mode Setting disabled on GPU %s because of cursor offset issues in virtual machines", qPrintable(m_drmDevice->path())); + drmSetClientCap(m_fd, DRM_CLIENT_CAP_ATOMIC, 0); + noAMS = true; + } + } + DrmUniquePtr planeResources(drmModeGetPlaneResources(m_fd)); + if (planeResources && !noAMS) { + qCDebug(KWIN_DRM) << "Using Atomic Mode Setting on gpu" << this; + qCDebug(KWIN_DRM) << "Number of planes on GPU" << this << ":" << planeResources->count_planes; + // create the plane objects + for (unsigned int i = 0; i < planeResources->count_planes; ++i) { + auto plane = std::make_unique(this, planeResources->planes[i]); + if (plane->init()) { + m_allObjects << plane.get(); + m_planes.push_back(std::move(plane)); + } + } + if (m_planes.empty()) { + qCWarning(KWIN_DRM) << "Failed to create any plane. Falling back to legacy mode on GPU " << this; + } + } else { + qCWarning(KWIN_DRM) << "Failed to get plane resources. Falling back to legacy mode on GPU " << this; + } + } else { + qCWarning(KWIN_DRM) << "drmSetClientCap for Atomic Mode Setting failed. Using legacy mode on GPU" << this; + } + + m_atomicModeSetting = !m_planes.empty(); + + DrmUniquePtr resources(drmModeGetResources(m_fd)); + if (!resources) { + qCCritical(KWIN_DRM) << "drmModeGetResources for getting CRTCs failed on GPU" << this; + return; + } + for (int i = 0; i < resources->count_crtcs; ++i) { + auto freePrimaryPlanes = m_planes | std::views::filter([this, i](const auto &plane) { + return plane->isCrtcSupported(i) + && plane->type.enumValue() == DrmPlane::TypeIndex::Primary + && std::ranges::none_of(m_crtcs, [&plane](const auto &crtc) { + return crtc->primaryPlane() == plane.get(); + }); + }); + // prefer an already connected plane + const uint32_t crtcId = resources->crtcs[i]; + auto it = std::ranges::find_if(freePrimaryPlanes, [crtcId](const auto &plane) { + return plane->crtcId.value() == crtcId; + }); + if (it == freePrimaryPlanes.end()) { + it = freePrimaryPlanes.begin(); + } + DrmPlane *primary = it == freePrimaryPlanes.end() ? nullptr : it->get(); + if (m_atomicModeSetting && !primary) { + qCWarning(KWIN_DRM) << "Could not find a suitable primary plane for crtc" << resources->crtcs[i]; + continue; + } + auto crtc = std::make_unique(this, crtcId, i, primary); + if (!crtc->init()) { + continue; + } + m_allObjects << crtc.get(); + m_crtcs.push_back(std::move(crtc)); + } +} + +bool DrmGpu::updateOutputs() +{ + if (!m_isActive) { + return false; + } + DrmUniquePtr resources(drmModeGetResources(m_fd)); + if (!resources) { + qCWarning(KWIN_DRM) << "drmModeGetResources failed:" << strerror(errno); + return false; + } + + // In principle these things are supposed to be detected through the wayland protocol. + // In practice SteamVR doesn't always behave correctly + if (DrmUniquePtr lessees{drmModeListLessees(m_fd)}) { + for (const DrmOutput *output : std::as_const(m_drmOutputs)) { + if (output->lease()) { + const bool leaseActive = std::ranges::any_of(std::span(lessees->lessees, lessees->count), [output](uint32_t id) { + return output->lease()->lesseeId() == id; + }); + if (!leaseActive) { + Q_EMIT output->lease()->revokeRequested(); + } + } + } + } else { + qCWarning(KWIN_DRM) << "drmModeListLessees() failed:" << strerror(errno); + } + + // update crtc properties + for (const auto &crtc : std::as_const(m_crtcs)) { + crtc->updateProperties(); + } + // update plane properties + for (const auto &plane : std::as_const(m_planes)) { + plane->updateProperties(); + } + + // check for added and removed connectors + QList existing; + QList addedOutputs; + for (int i = 0; i < resources->count_connectors; ++i) { + const uint32_t currentConnector = resources->connectors[i]; + const auto it = std::ranges::find_if(m_connectors, [currentConnector](const auto &connector) { + return connector->id() == currentConnector; + }); + if (it == m_connectors.end()) { + auto conn = std::make_shared(this, currentConnector); + if (!conn->init()) { + continue; + } + existing.push_back(conn.get()); + m_allObjects.push_back(conn.get()); + m_connectors.push_back(std::move(conn)); + } else { + (*it)->updateProperties(); + existing.push_back(it->get()); + } + } + for (auto it = m_connectors.begin(); it != m_connectors.end();) { + DrmConnector *conn = it->get(); + const auto output = findOutput(conn->id()); + const bool stillExists = existing.contains(conn); + if (!stillExists || !conn->isConnected()) { + if (output) { + removeOutput(output); + } + } else if (!output) { + qCDebug(KWIN_DRM, "New %soutput on GPU %s: %s", conn->isNonDesktop() ? "non-desktop " : "", qPrintable(m_drmDevice->path()), qPrintable(conn->modelName())); + auto &pipeline = m_pipelineMap[conn]; + pipeline = std::make_unique(conn); + m_pipelines.push_back(pipeline.get()); + auto output = new DrmOutput(*it, pipeline.get()); + m_drmOutputs << output; + addedOutputs << output; + Q_EMIT outputAdded(output); + pipeline->setActive(true); + pipeline->setEnable(false); + pipeline->setMode(conn->modes().front()); + pipeline->applyPendingChanges(); + } else { + output->updateConnectorProperties(); + } + if (stillExists) { + if (conn->isConnected() && conn->linkStatus.isValid() && conn->linkStatus.enumValue() == DrmConnector::LinkStatus::Bad) { + qCWarning(KWIN_DRM, "Bad link status detected on connector %s", qPrintable(conn->connectorName())); + // force a modeset, to renegotiate the connection + m_forceModeset = true; + } + it++; + } else { + m_allObjects.removeOne(it->get()); + it = m_connectors.erase(it); + } + } + return true; +} + +void DrmGpu::removeOutputs() +{ + const auto outputs = m_drmOutputs; + for (DrmOutput *output : outputs) { + removeOutput(output); + } +} + +DrmPipeline::Error DrmGpu::checkCrtcAssignment(QList connectors, const QList &crtcs, std::chrono::steady_clock::time_point deadline) +{ + if (std::chrono::steady_clock::now() > deadline) { + return DrmPipeline::Error::Timeout; + } + if (connectors.isEmpty()) { + const auto result = testPipelines(); + return result; + } + auto connector = connectors.takeFirst(); + auto pipelineIt = m_pipelineMap.find(connector); + if (pipelineIt == m_pipelineMap.end()) { + // this connector doesn't even have a connected output + return checkCrtcAssignment(connectors, crtcs, deadline); + } + auto pipeline = pipelineIt->second.get(); + if (!pipeline->enabled() || !connector->isConnected()) { + // disabled pipelines don't need CRTCs + pipeline->setCrtc(nullptr); + return checkCrtcAssignment(connectors, crtcs, deadline); + } + if (crtcs.isEmpty()) { + // we have no crtc left to drive this connector + return DrmPipeline::Error::NotEnoughCrtcs; + } + DrmCrtc *currentCrtc = nullptr; + if (m_atomicModeSetting) { + // try the crtc that this connector is already connected to first + const uint32_t id = connector->crtcId.value(); + auto it = std::ranges::find_if(crtcs, [id](const DrmCrtc *crtc) { + return id == crtc->id(); + }); + if (it != crtcs.end()) { + currentCrtc = *it; + auto crtcsLeft = crtcs; + crtcsLeft.removeOne(currentCrtc); + pipeline->setCrtc(currentCrtc); + DrmPipeline::Error err = checkCrtcAssignment(connectors, crtcsLeft, deadline); + if (err == DrmPipeline::Error::None || err == DrmPipeline::Error::NoPermission || err == DrmPipeline::Error::FramePending || err == DrmPipeline::Error::Timeout) { + return err; + } + } + } + for (DrmCrtc *crtc : std::as_const(crtcs)) { + if (connector->isCrtcSupported(crtc) && crtc != currentCrtc) { + auto crtcsLeft = crtcs; + crtcsLeft.removeOne(crtc); + pipeline->setCrtc(crtc); + DrmPipeline::Error err = checkCrtcAssignment(connectors, crtcsLeft, deadline); + if (err == DrmPipeline::Error::None || err == DrmPipeline::Error::NoPermission || err == DrmPipeline::Error::FramePending || err == DrmPipeline::Error::Timeout) { + return err; + } + } + } + return DrmPipeline::Error::InvalidArguments; +} + +static const std::chrono::milliseconds s_checkCrtcTimeout = environmentVariableIntValue("KWIN_DRM_PENDING_CONFIG_TIMEOUT").transform([](int value) { + return std::chrono::milliseconds(value); +}).value_or(3s); + +DrmPipeline::Error DrmGpu::testPendingConfiguration() +{ + QList connectors; + QList crtcs; + // only change resources that aren't currently leased away + for (const auto &conn : m_connectors) { + const bool isLeased = std::ranges::any_of(m_drmOutputs, [&conn](const auto output) { + return output->lease() && output->pipeline()->connector() == conn.get(); + }); + if (!isLeased) { + connectors.push_back(conn.get()); + } + } + for (const auto &crtc : m_crtcs) { + const bool isLeased = std::ranges::any_of(m_drmOutputs, [&crtc](const auto output) { + return output->lease() && output->pipeline()->crtc() == crtc.get(); + }); + if (!isLeased) { + crtcs.push_back(crtc.get()); + } + } + if (m_atomicModeSetting) { + // sort outputs by being already connected (to any CRTC) so that already working outputs get preferred + std::sort(connectors.begin(), connectors.end(), [](auto c1, auto c2) { + return c1->crtcId.value() > c2->crtcId.value(); + }); + } + m_forceLowBandwidthMode = false; + auto err = checkCrtcAssignment(connectors, crtcs, std::chrono::steady_clock::now() + s_checkCrtcTimeout); + if (err == DrmPipeline::Error::None || err == DrmPipeline::Error::NoPermission || err == DrmPipeline::Error::FramePending) { + return err; + } + const bool hasPreferAccuracy = std::ranges::any_of(m_drmOutputs, [](const auto &output) { + return output->colorPowerTradeoff() == BackendOutput::ColorPowerTradeoff::PreferAccuracy; + }); + if (m_addFB2ModifiersSupported || hasPreferAccuracy) { + // We currently don't have any information about why the output config + // got rejected; one possibility is missing memory bandwidth. + m_forceLowBandwidthMode = true; + err = checkCrtcAssignment(connectors, crtcs, std::chrono::steady_clock::now() + s_checkCrtcTimeout); + } + return err; +} + +void DrmGpu::releaseUnusedBuffers() +{ + const auto isLayerUsed = [this](DrmPipelineLayer *layer) { + return std::ranges::any_of(m_pipelines, [layer](const auto &pipeline) { + return pipeline->layers().contains(layer); + }); + }; + for (const auto &[plane, layer] : m_planeLayerMap) { + if (!isLayerUsed(layer.get())) { + layer->releaseBuffers(); + } + } + for (const auto &[crtc, layer] : m_legacyLayerMap) { + if (!isLayerUsed(layer.get())) { + layer->releaseBuffers(); + } + } + for (const auto &[crtc, layer] : m_legacyCursorLayerMap) { + if (!isLayerUsed(layer.get())) { + layer->releaseBuffers(); + } + } +} + +DrmPipeline::Error DrmGpu::testPipelines() +{ + if (m_pipelines.empty()) { + // nothing to do + return DrmPipeline::Error::None; + } + assignOutputLayers(); + for (DrmPipeline *pipeline : m_pipelines) { + if (pipeline->output()->lease() || !pipeline->enabled()) { + continue; + } + // reset all outputs to their most basic configuration (primary plane without scaling) + // for the test, and set the target rects appropriately + const auto layers = pipeline->layers(); + for (auto layer : layers) { + if (layer->type() == OutputLayerType::Primary) { + layer->setTargetRect(Rect(QPoint(0, 0), pipeline->mode()->size())); + layer->setSourceRect(Rect(QPoint(0, 0), pipeline->mode()->size())); + layer->setEnabled(true); + // ensure we have suitable buffers for the test + if (!layer->preparePresentationTest()) { + return DrmPipeline::Error::InvalidArguments; + } + } else { + layer->setEnabled(false); + } + } + } + return DrmPipeline::commitPipelines(m_pipelines, DrmPipeline::CommitMode::TestAllowModeset, unusedModesetObjects()); +} + +DrmOutput *DrmGpu::findOutput(quint32 connector) +{ + auto it = std::ranges::find_if(m_drmOutputs, [connector](DrmOutput *o) { + return o->connector()->id() == connector; + }); + if (it != m_drmOutputs.constEnd()) { + return *it; + } + return nullptr; +} + +bool DrmGpu::isIdle() const +{ + return std::ranges::none_of(m_pipelines, [](DrmPipeline *pipeline) { + return pipeline->commitThread()->pageflipsPending(); + }); +} + +static std::chrono::nanoseconds convertTimestamp(const timespec ×tamp) +{ + return std::chrono::seconds(timestamp.tv_sec) + std::chrono::nanoseconds(timestamp.tv_nsec); +} + +static std::chrono::nanoseconds convertTimestamp(clockid_t sourceClock, clockid_t targetClock, + const timespec ×tamp) +{ + if (sourceClock == targetClock) { + return convertTimestamp(timestamp); + } + + timespec sourceCurrentTime = {}; + timespec targetCurrentTime = {}; + + clock_gettime(sourceClock, &sourceCurrentTime); + clock_gettime(targetClock, &targetCurrentTime); + + const auto delta = convertTimestamp(sourceCurrentTime) - convertTimestamp(timestamp); + return convertTimestamp(targetCurrentTime) - delta; +} + +void DrmGpu::pageFlipHandler(int fd, unsigned int sequence, unsigned int sec, unsigned int usec, unsigned int crtc_id, void *user_data) +{ + const auto commit = static_cast(user_data); + const auto gpu = commit->gpu(); + const bool defunct = std::erase_if(gpu->m_defunctCommits, [commit](const auto &defunct) { + return defunct.get() == commit; + }) != 0; + if (defunct) { + return; + } + + // The static_cast<> here are for a 32-bit environment where + // sizeof(time_t) == sizeof(unsigned int) == 4 . Putting @p sec + // into a time_t cuts off the most-significant bit (after the + // year 2038), similarly long can't hold all the bits of an + // unsigned multiplication. + std::chrono::nanoseconds timestamp = convertTimestamp(gpu->presentationClock(), CLOCK_MONOTONIC, + {static_cast(sec), static_cast(usec * 1000)}); + if (timestamp == std::chrono::nanoseconds::zero()) { + // in some cases this can happen a lot, + // see https://gitlab.freedesktop.org/drm/amd/-/issues/4359 for example + static uint64_t s_warningCounter = 0; + s_warningCounter++; + if (s_warningCounter == 10) { + qCDebug(KWIN_DRM, "Too many invalid timestamps received, suppressing future warnings"); + } else if (s_warningCounter < 10) { + qCDebug(KWIN_DRM, "Got invalid timestamp (sec: %u, usec: %u) on gpu %s", + sec, usec, qPrintable(gpu->drmDevice()->path())); + } + timestamp = std::chrono::steady_clock::now().time_since_epoch(); + } + commit->pageFlipped(timestamp); +} + +void DrmGpu::dispatchEvents() +{ + drmEventContext context = {}; + context.version = 3; + context.page_flip_handler2 = pageFlipHandler; + drmHandleEvent(m_fd, &context); +} + +void DrmGpu::addDefunctCommit(std::unique_ptr &&commit) +{ + m_defunctCommits.push_back(std::move(commit)); +} + +void DrmGpu::removeOutput(DrmOutput *output) +{ + qCDebug(KWIN_DRM) << "Removing output" << output; + m_drmOutputs.removeOne(output); + Q_EMIT outputRemoved(output); + m_pipelines.removeOne(output->pipeline()); + m_pipelineMap.erase(output->connector()); + output->removePipeline(); + output->unref(); + // force a modeset to make sure unused objects are cleaned up + m_forceModeset = true; +} + +DrmBackend *DrmGpu::platform() const +{ + return m_platform; +} + +const QList DrmGpu::pipelines() const +{ + return m_pipelines; +} + +std::unique_ptr DrmGpu::leaseOutputs(const QList &outputs) +{ + const bool alreadyLeased = std::ranges::any_of(outputs, [](DrmOutput *output) { + return output->lease(); + }); + if (alreadyLeased) { + return nullptr; + } + + // allocate crtcs for the outputss + for (DrmOutput *output : outputs) { + output->pipeline()->setEnable(true); + output->pipeline()->setActive(false); + } + if (testPendingConfiguration() != DrmPipeline::Error::None) { + return nullptr; + } + + QList objects; + for (DrmOutput *output : outputs) { + if (!output->addLeaseObjects(objects)) { + return nullptr; + } + } + + uint32_t lesseeId; + FileDescriptor fd{drmModeCreateLease(m_fd, objects.constData(), objects.count(), 0, &lesseeId)}; + if (!fd.isValid()) { + qCWarning(KWIN_DRM) << "Could not create DRM lease!" << strerror(errno); + qCWarning(KWIN_DRM) << "Tried to lease the following" << objects.count() << "resources:"; + for (const uint32_t res : std::as_const(objects)) { + qCWarning(KWIN_DRM) << res; + } + return nullptr; + } else { + qCDebug(KWIN_DRM) << "Created lease for" << objects.count() << "resources:"; + for (const uint32_t res : std::as_const(objects)) { + qCDebug(KWIN_DRM) << res; + } + return std::make_unique(this, std::move(fd), lesseeId, outputs); + } +} + +QList DrmGpu::drmOutputs() const +{ + return m_drmOutputs; +} + +int DrmGpu::fd() const +{ + return m_fd; +} + +DrmDevice *DrmGpu::drmDevice() const +{ + return m_drmDevice.get(); +} + +bool DrmGpu::atomicModeSetting() const +{ + return m_atomicModeSetting; +} + +EglDisplay *DrmGpu::eglDisplay() const +{ + return m_eglDisplay.get(); +} + +void DrmGpu::setEglDisplay(std::unique_ptr &&display) +{ + m_eglDisplay = std::move(display); +} + +bool DrmGpu::addFB2ModifiersSupported() const +{ + return m_addFB2ModifiersSupported; +} + +bool DrmGpu::forceLowBandwidthMode() const +{ + return m_forceLowBandwidthMode; +} + +bool DrmGpu::asyncPageflipSupported() const +{ + return m_asyncPageflipSupported; +} + +bool DrmGpu::sharpnessSupported() const +{ + return m_sharpnessSupported; +} + +bool DrmGpu::colorPipelineSupported() const +{ + return m_colorPipelineSupported; +} + +bool DrmGpu::isI915() const +{ + return m_isI915; +} + +bool DrmGpu::isNVidia() const +{ + return m_isNVidia; +} + +bool DrmGpu::isAmdgpu() const +{ + return m_isAmdgpu; +} + +bool DrmGpu::isVmwgfx() const +{ + return m_isVmwgfx; +} + +bool DrmGpu::isVirtualMachine() const +{ + return m_isVirtualMachine; +} + +std::optional DrmGpu::nvidiaDriverVersion() const +{ + return m_nvidiaDriverVersion; +} + +bool DrmGpu::isRemoved() const +{ + return m_isRemoved; +} + +void DrmGpu::setRemoved() +{ + m_isRemoved = true; +} + +void DrmGpu::setActive(bool active) +{ + if (m_isActive != active) { + m_isActive = active; + if (active) { + for (const DrmOutput *output : std::as_const(m_drmOutputs)) { + output->renderLoop()->uninhibit(); + } + for (const DrmOutput *output : std::as_const(m_drmOutputs)) { + // force a modeset with legacy, we can't reliably know if one is needed + if (!atomicModeSetting()) { + output->pipeline()->forceLegacyModeset(); + } + } + } else { + for (const DrmOutput *output : std::as_const(m_drmOutputs)) { + output->renderLoop()->inhibit(); + } + } + Q_EMIT activeChanged(active); + } +} + +bool DrmGpu::isActive() const +{ + return m_isActive; +} + +bool DrmGpu::needsModeset() const +{ + return m_forceModeset + || !m_pendingModesetFrames.empty() + || std::ranges::any_of(m_pipelines, [](DrmPipeline *pipeline) { + return !pipeline->output()->lease() && pipeline->needsModeset(); + }); +} + +void DrmGpu::maybeModeset(DrmPipeline *pipeline, const std::shared_ptr &frame) +{ + if (pipeline && frame) { + m_pendingModesetFrames.emplace(pipeline, frame); + } + auto pipelines = m_pipelines; + for (const DrmOutput *output : std::as_const(m_drmOutputs)) { + if (output->lease()) { + pipelines.removeOne(output->pipeline()); + } + } + const bool presentPendingForAll = std::ranges::all_of(pipelines, [](const DrmPipeline *pipeline) { + return pipeline->modesetPresentPending() || !pipeline->activePending(); + }); + if (!presentPendingForAll) { + // commit only once all pipelines are ready for presentation + return; + } + if (!isIdle()) { + // doing a modeset with pending pageflips would crash + return; + } + if (m_inModeset) { + return; + } + // Modesets need to be done asynchronously, to match how presentation + // normally works. This is necessary because the Compositor adds presentation + // time feedbacks to the OutputFrame after calling LogicalOutput::present + m_delayedModesetTimer.start(); +} + +void DrmGpu::doModeset() +{ + auto pipelines = m_pipelines; + for (const DrmOutput *output : std::as_const(m_drmOutputs)) { + if (output->lease()) { + pipelines.removeOne(output->pipeline()); + } + } + if (pipelines.empty()) { + m_pendingModesetFrames.clear(); + m_forceModeset = false; + return; + } + m_inModeset = true; + const DrmPipeline::Error err = DrmPipeline::commitPipelines(pipelines, DrmPipeline::CommitMode::CommitModeset, unusedModesetObjects()); + for (DrmPipeline *pipeline : std::as_const(pipelines)) { + if (pipeline->modesetPresentPending()) { + pipeline->resetModesetPresentPending(); + } + } + m_forceModeset = false; + if (err == DrmPipeline::Error::None) { + for (const auto &[pipeline, frame] : m_pendingModesetFrames) { + frame->presented(std::chrono::steady_clock::now().time_since_epoch(), PresentationMode::VSync); + } + } else { + if (err != DrmPipeline::Error::FramePending) { + QTimer::singleShot(0, m_platform, [backend = m_platform]() { + backend->updateOutputs(); + }); + } + } + m_pendingModesetFrames.clear(); + m_inModeset = false; +} + +QList DrmGpu::unusedModesetObjects() const +{ + QList ret = m_allObjects; + for (const DrmPipeline *pipeline : m_pipelines) { + ret.removeOne(pipeline->connector()); + if (pipeline->crtc()) { + ret.removeOne(pipeline->crtc()); + // for modesets, only the primary plane should be enabled + ret.removeOne(pipeline->crtc()->primaryPlane()); + } + } + return ret; +} + +QSize DrmGpu::cursorSize() const +{ + return m_cursorSize; +} + +void DrmGpu::releaseBuffers() +{ + for (DrmPipeline *pipeline : std::as_const(m_pipelines)) { + pipeline->setLayers({}); + pipeline->applyPendingChanges(); + } + for (const auto &plane : std::as_const(m_planes)) { + plane->releaseCurrentBuffer(); + m_planeLayerMap.erase(plane.get()); + } + for (const auto &crtc : std::as_const(m_crtcs)) { + crtc->releaseCurrentBuffer(); + m_legacyLayerMap.erase(crtc.get()); + m_legacyCursorLayerMap.erase(crtc.get()); + } +} + +void DrmGpu::createLayers() +{ + if (m_atomicModeSetting) { + for (const auto &plane : m_planes) { + m_planeLayerMap[plane.get()] = m_platform->renderBackend()->createDrmPlaneLayer(plane.get()); + } + } else { + for (const auto &crtc : m_crtcs) { + m_legacyLayerMap[crtc.get()] = m_platform->renderBackend()->createDrmPlaneLayer(this, DrmPlane::TypeIndex::Primary); + m_legacyCursorLayerMap[crtc.get()] = m_platform->renderBackend()->createDrmPlaneLayer(this, DrmPlane::TypeIndex::Cursor); + } + } + assignOutputLayers(); + for (DrmPipeline *pipeline : std::as_const(m_pipelines)) { + pipeline->applyPendingChanges(); + } +} + +void DrmGpu::assignOutputLayers() +{ + if (m_atomicModeSetting) { + QList freePlanes = m_planes | std::views::transform([](const auto &plane) { + return plane.get(); + }) | std::ranges::to(); + const size_t enabledPipelinesCount = std::ranges::count_if(m_pipelines, &DrmPipeline::enabled); + for (DrmPipeline *pipeline : std::as_const(m_pipelines)) { + if (!pipeline->enabled()) { + pipeline->setLayers({}); + continue; + } + QList layers = {m_planeLayerMap[pipeline->crtc()->primaryPlane()].get()}; + for (DrmPlane *plane : freePlanes) { + if (plane->isCrtcSupported(pipeline->crtc()->pipeIndex()) + && plane->type.enumValue() == DrmPlane::TypeIndex::Cursor) { + layers.push_back(m_planeLayerMap[plane].get()); + freePlanes.removeOne(plane); + break; + } + } + if (enabledPipelinesCount == 1) { + // To avoid having to deal with GPU-wide bandwidth restrictions + // and switching planes between outputs, for now only use overlay + // planes with single-output setups + for (DrmPlane *plane : freePlanes) { + if (plane->isCrtcSupported(pipeline->crtc()->pipeIndex()) + && plane->type.enumValue() == DrmPlane::TypeIndex::Overlay) { + layers.push_back(m_planeLayerMap[plane].get()); + } + } + } + pipeline->setLayers(layers); + } + } else { + for (DrmPipeline *pipeline : std::as_const(m_pipelines)) { + if (!pipeline->enabled()) { + pipeline->setLayers({}); + continue; + } + pipeline->setLayers({m_legacyLayerMap[pipeline->crtc()].get(), m_legacyCursorLayerMap[pipeline->crtc()].get()}); + } + } +} + +std::shared_ptr DrmGpu::importBuffer(GraphicsBuffer *buffer, FileDescriptor &&readFence) +{ + const DmaBufAttributes *attributes = buffer->dmabufAttributes(); + if (Q_UNLIKELY(!attributes)) { + return nullptr; + } + + const auto it = m_fbCache.constFind(buffer); + if (it != m_fbCache.constEnd()) { + return std::make_shared(it->lock(), buffer, std::move(readFence)); + } + + uint32_t handles[] = {0, 0, 0, 0}; + auto cleanup = qScopeGuard([this, &handles]() { + for (int i = 0; i < 4; ++i) { + if (handles[i] == 0) { + continue; + } + bool closed = false; + for (int j = 0; j < i; ++j) { + if (handles[i] == handles[j]) { + closed = true; + break; + } + } + if (closed) { + continue; + } + drmCloseBufferHandle(m_fd, handles[i]); + } + }); + for (int i = 0; i < attributes->planeCount; ++i) { + if (drmPrimeFDToHandle(m_fd, attributes->fd[i].get(), &handles[i]) != 0) { + qCWarning(KWIN_DRM) << "drmPrimeFDToHandle() failed"; + return nullptr; + } + } + + uint32_t framebufferId = 0; + int ret; + if (addFB2ModifiersSupported() && attributes->modifier != DRM_FORMAT_MOD_INVALID) { + uint64_t modifier[4] = {0, 0, 0, 0}; + for (int i = 0; i < attributes->planeCount; ++i) { + modifier[i] = attributes->modifier; + } + ret = drmModeAddFB2WithModifiers(m_fd, + attributes->width, + attributes->height, + attributes->format, + handles, + attributes->pitch.data(), + attributes->offset.data(), + modifier, + &framebufferId, + DRM_MODE_FB_MODIFIERS); + } else { + ret = drmModeAddFB2(m_fd, + attributes->width, + attributes->height, + attributes->format, + handles, + attributes->pitch.data(), + attributes->offset.data(), + &framebufferId, + 0); + if (ret == EOPNOTSUPP && attributes->planeCount == 1) { + ret = drmModeAddFB(m_fd, + attributes->width, + attributes->height, + 24, 32, + attributes->pitch[0], + handles[0], + &framebufferId); + } + } + + if (ret != 0) { + return nullptr; + } + + auto fbData = std::make_shared(this, framebufferId, buffer); + m_fbCache[buffer] = fbData; + connect(buffer, &GraphicsBuffer::destroyed, this, &DrmGpu::forgetBufferObject); + return std::make_shared(fbData, buffer, std::move(readFence)); +} + +void DrmGpu::forgetBuffer(GraphicsBuffer *buf) +{ + disconnect(buf, &GraphicsBuffer::destroyed, this, &DrmGpu::forgetBufferObject); + m_fbCache.remove(buf); +} + +void DrmGpu::forgetBufferObject(QObject *buf) +{ + m_fbCache.remove(static_cast(buf)); +} + +QString DrmGpu::driverName() const +{ + return m_driverName; +} + +QList DrmGpu::compatibleOutputLayers(BackendOutput *output) const +{ + if (auto virt = qobject_cast(output)) { + return {virt->primaryLayer()}; + } + // TODO once dynamic ownership of layers is defined somehow, + // additionally return planes that aren't currently in use + return static_cast(output)->pipeline()->layers() | std::ranges::to>(); +} + +DrmLease::DrmLease(DrmGpu *gpu, FileDescriptor &&fd, uint32_t lesseeId, const QList &outputs) + : m_gpu(gpu) + , m_fd(std::move(fd)) + , m_lesseeId(lesseeId) + , m_outputs(outputs) +{ + for (const auto output : m_outputs) { + output->leased(this); + } +} + +DrmLease::~DrmLease() +{ + qCDebug(KWIN_DRM, "Revoking lease with leaseID %d", m_lesseeId); + drmModeRevokeLease(m_gpu->fd(), m_lesseeId); + for (DrmOutput *output : m_outputs) { + output->leaseEnded(); + output->pipeline()->setEnable(false); + } +} + +FileDescriptor &DrmLease::fd() +{ + return m_fd; +} + +uint32_t DrmLease::lesseeId() const +{ + return m_lesseeId; +} +} + +QDebug &operator<<(QDebug &s, const KWin::DrmGpu *gpu) +{ + s << gpu->drmDevice()->path(); + return s; +} + +#include "moc_drm_gpu.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_gpu.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_gpu.h new file mode 100644 index 0000000000..0f06f8ec86 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_gpu.h @@ -0,0 +1,200 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "kwin_export.h" + +#include "core/drmdevice.h" +#include "drm_buffer.h" +#include "drm_pipeline.h" +#include "utils/filedescriptor.h" +#include "utils/version.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace KWin +{ +class DrmOutput; +class DrmObject; +class DrmCrtc; +class DrmConnector; +class DrmPlane; +class DrmBackend; +class EglGbmBackend; +class DrmAbstractOutput; +class DrmRenderBackend; +class DrmVirtualOutput; +class EglDisplay; +class GraphicsBuffer; +class GraphicsBufferAllocator; +class OutputFrame; +class DrmCommit; + +class DrmLease : public QObject +{ + Q_OBJECT +public: + DrmLease(DrmGpu *gpu, FileDescriptor &&fd, uint32_t lesseeId, const QList &outputs); + ~DrmLease(); + + FileDescriptor &fd(); + uint32_t lesseeId() const; + +Q_SIGNALS: + void revokeRequested(); + +private: + DrmGpu *const m_gpu; + FileDescriptor m_fd; + const uint32_t m_lesseeId; + const QList m_outputs; +}; + +class KWIN_EXPORT DrmGpu : public QObject +{ + Q_OBJECT +public: + /** + * This should always be longer than any real pageflip can take, even with PSR and modesets + */ + static constexpr std::chrono::milliseconds s_pageflipTimeout = std::chrono::seconds(1); + + DrmGpu(DrmBackend *backend, int fd, std::unique_ptr &&device); + ~DrmGpu(); + + int fd() const; + DrmDevice *drmDevice() const; + + bool isRemoved() const; + void setRemoved(); + void setActive(bool active); + bool isActive() const; + + bool atomicModeSetting() const; + bool addFB2ModifiersSupported() const; + bool forceLowBandwidthMode() const; + bool asyncPageflipSupported() const; + bool colorPipelineSupported() const; + bool isI915() const; + bool isNVidia() const; + bool isAmdgpu() const; + bool isVmwgfx() const; + bool isVirtualMachine() const; + bool sharpnessSupported() const; + std::optional nvidiaDriverVersion() const; + QString driverName() const; + EglDisplay *eglDisplay() const; + DrmBackend *platform() const; + /** + * Returns the clock from which presentation timestamps are sourced. The returned value + * can be either CLOCK_MONOTONIC or CLOCK_REALTIME. + */ + clockid_t presentationClock() const; + QSize cursorSize() const; + + QList drmOutputs() const; + const QList pipelines() const; + + void setEglDisplay(std::unique_ptr &&display); + + bool updateOutputs(); + void removeOutputs(); + + DrmPipeline::Error testPendingConfiguration(); + void releaseUnusedBuffers(); + bool needsModeset() const; + void maybeModeset(DrmPipeline *pipeline, const std::shared_ptr &frame); + + std::shared_ptr importBuffer(GraphicsBuffer *buffer, FileDescriptor &&explicitFence); + void forgetBuffer(GraphicsBuffer *buf); + void releaseBuffers(); + void createLayers(); + QList compatibleOutputLayers(BackendOutput *output) const; + + FileDescriptor createNonMasterFd() const; + std::unique_ptr leaseOutputs(const QList &outputs); + bool isIdle() const; + void dispatchEvents(); + + void addDefunctCommit(std::unique_ptr &&commit); + +Q_SIGNALS: + void activeChanged(bool active); + void outputAdded(DrmAbstractOutput *output); + void outputRemoved(DrmAbstractOutput *output); + +private: + DrmOutput *findOutput(quint32 connector); + void removeOutput(DrmOutput *output); + void initDrmResources(); + void forgetBufferObject(QObject *buf); + void doModeset(); + + DrmPipeline::Error checkCrtcAssignment(QList connectors, const QList &crtcs, std::chrono::steady_clock::time_point deadline); + DrmPipeline::Error testPipelines(); + QList unusedModesetObjects() const; + void assignOutputLayers(); + + static void pageFlipHandler(int fd, unsigned int sequence, unsigned int sec, unsigned int usec, unsigned int crtc_id, void *user_data); + + const int m_fd; + const std::unique_ptr m_drmDevice; + bool m_atomicModeSetting; + bool m_addFB2ModifiersSupported = false; + bool m_isNVidia; + bool m_isI915; + bool m_isAmdgpu; + bool m_isVmwgfx; + bool m_isVirtualMachine; + QString m_driverName; + bool m_supportsCursorPlaneHotspot = false; + bool m_asyncPageflipSupported = false; + bool m_colorPipelineSupported = false; + bool m_isRemoved = false; + bool m_isActive = true; + bool m_forceModeset = false; + bool m_forceLowBandwidthMode = false; + bool m_forceImplicitModifiers = false; + bool m_sharpnessSupported = false; + clockid_t m_presentationClock; + std::unique_ptr m_eglDisplay; + DrmBackend *const m_platform; + std::optional m_nvidiaDriverVersion; + + std::vector> m_planes; + std::vector> m_crtcs; + std::vector> m_connectors; + std::unordered_map> m_pipelineMap; + std::unordered_map> m_planeLayerMap; + std::unordered_map> m_legacyLayerMap; + std::unordered_map> m_legacyCursorLayerMap; + QList m_allObjects; + QList m_pipelines; + + QList m_drmOutputs; + + std::unique_ptr m_socketNotifier; + QSize m_cursorSize; + std::unordered_map> m_pendingModesetFrames; + bool m_inModeset = false; + QHash> m_fbCache; + std::vector> m_defunctCommits; + QTimer m_delayedModesetTimer; +}; + +} + +QDebug &operator<<(QDebug &s, const KWin::DrmGpu *gpu); diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_layer.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_layer.cpp new file mode 100644 index 0000000000..8a210d4fab --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_layer.cpp @@ -0,0 +1,159 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_layer.h" +#include "core/graphicsbuffer.h" +#include "drm_buffer.h" +#include "drm_gpu.h" +#include "drm_output.h" +#include "drm_pipeline.h" + +#include +#include + +namespace KWin +{ + +DrmOutputLayer::DrmOutputLayer(BackendOutput *output, OutputLayerType type) + : OutputLayer(output, type) +{ +} + +DrmOutputLayer::DrmOutputLayer(BackendOutput *output, OutputLayerType type, int zpos, int minZpos, int maxZpos) + : OutputLayer(output, type, zpos, minZpos, maxZpos) +{ +} + +DrmOutputLayer::~DrmOutputLayer() = default; + +static OutputLayerType planeToLayerType(DrmPlane *plane, DrmPlane::TypeIndex type) +{ + switch (type) { + case DrmPlane::TypeIndex::Overlay: + return OutputLayerType::GenericLayer; + case DrmPlane::TypeIndex::Primary: + return OutputLayerType::Primary; + case DrmPlane::TypeIndex::Cursor: + if (!plane || plane->gpu()->isVirtualMachine()) { + return OutputLayerType::CursorOnly; + } else { + return OutputLayerType::EfficientOverlay; + } + } + Q_UNREACHABLE(); +} + +static int determineZpos(DrmPlane *plane) +{ + if (plane->zpos.isValid()) { + return plane->zpos.value(); + } else { + switch (plane->type.enumValue()) { + case DrmPlane::TypeIndex::Primary: + return 0; + case DrmPlane::TypeIndex::Overlay: + return 1; + case DrmPlane::TypeIndex::Cursor: + return 255; + } + return 0; + } +} + +static int determineMinZpos(DrmPlane *plane) +{ + if (plane->zpos.isValid()) { + return plane->zpos.minValue(); + } else { + return determineZpos(plane); + } +} + +static int determineMaxZpos(DrmPlane *plane) +{ + if (plane->zpos.isValid()) { + return plane->zpos.maxValue(); + } else { + return determineZpos(plane); + } +} + +DrmPipelineLayer::DrmPipelineLayer(DrmPlane *plane) + : DrmOutputLayer(nullptr, planeToLayerType(plane, plane->type.enumValue()), determineZpos(plane), determineMinZpos(plane), determineMaxZpos(plane)) + , m_plane(plane) +{ +} + +DrmPipelineLayer::DrmPipelineLayer(DrmPlane::TypeIndex type) + : DrmOutputLayer(nullptr, planeToLayerType(nullptr, type)) +{ +} + +DrmPlane *DrmPipelineLayer::plane() const +{ + return m_plane; +} + +DrmPipeline *DrmPipelineLayer::pipeline() const +{ + return drmOutput()->pipeline(); +} + +DrmGpu *DrmPipelineLayer::gpu() const +{ + return pipeline()->gpu(); +} + +DrmOutput *DrmPipelineLayer::drmOutput() const +{ + return static_cast(m_output.get()); +} + +DrmDevice *DrmPipelineLayer::scanoutDevice() const +{ + return pipeline()->gpu()->drmDevice(); +} + +static const QHash> s_legacyFormats = {{DRM_FORMAT_XRGB8888, {DRM_FORMAT_MOD_INVALID}}}; +static const QHash> s_legacyCursorFormats = {{DRM_FORMAT_ARGB8888, {DRM_FORMAT_MOD_LINEAR}}}; + +QHash> DrmPipelineLayer::supportedDrmFormats() const +{ + if (m_plane) { + if (m_plane->gpu()->forceLowBandwidthMode() && m_type == OutputLayerType::Primary) { + return m_plane->lowBandwidthFormats(); + } else { + return m_plane->formats(); + } + } else if (m_type == OutputLayerType::CursorOnly) { + return s_legacyCursorFormats; + } else { + return s_legacyFormats; + } +} + +QHash> DrmPipelineLayer::supportedAsyncDrmFormats() const +{ + if (m_plane) { + return m_plane->tearingFormats(); + } else { + return {}; + } +} + +QList DrmPipelineLayer::recommendedSizes() const +{ + if (m_plane) { + return m_plane->recommendedSizes(); + } else if (m_type == OutputLayerType::CursorOnly && pipeline()) { + return {gpu()->cursorSize()}; + } else { + return {}; + } +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_layer.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_layer.h new file mode 100644 index 0000000000..8ea5e64639 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_layer.h @@ -0,0 +1,56 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "core/colorpipeline.h" +#include "core/outputlayer.h" +#include "drm_plane.h" + +#include +#include + +namespace KWin +{ + +class SurfaceItem; +class DrmFramebuffer; +class GLTexture; +class DrmPipeline; +class DrmOutput; + +class DrmOutputLayer : public OutputLayer +{ +public: + explicit DrmOutputLayer(BackendOutput *output, OutputLayerType type); + explicit DrmOutputLayer(BackendOutput *output, OutputLayerType type, int zpos, int minZpos, int maxZpos); + virtual ~DrmOutputLayer(); +}; + +class DrmPipelineLayer : public DrmOutputLayer +{ +public: + explicit DrmPipelineLayer(DrmPlane *plane); + explicit DrmPipelineLayer(DrmPlane::TypeIndex type); + + DrmDevice *scanoutDevice() const override; + QHash> supportedDrmFormats() const override; + QList recommendedSizes() const override; + QHash> supportedAsyncDrmFormats() const override; + + virtual std::shared_ptr currentBuffer() const = 0; + + DrmPlane *plane() const; + +protected: + DrmPipeline *pipeline() const; + DrmGpu *gpu() const; + DrmOutput *drmOutput() const; + + DrmPlane *m_plane = nullptr; +}; +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_logging.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_logging.cpp new file mode 100644 index 0000000000..189e0aa053 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_logging.cpp @@ -0,0 +1,10 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_logging.h" +Q_LOGGING_CATEGORY(KWIN_DRM, "kwin_wayland_drm", QtWarningMsg) diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_logging.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_logging.h new file mode 100644 index 0000000000..f2f395a4c2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_logging.h @@ -0,0 +1,13 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(KWIN_DRM) diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_object.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_object.cpp new file mode 100644 index 0000000000..5a39b88f32 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_object.cpp @@ -0,0 +1,110 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_object.h" + +#include + +#include "drm_commit.h" +#include "drm_gpu.h" +#include "drm_logging.h" +#include "drm_pointer.h" + +namespace KWin +{ + +DrmObject::DrmObject(DrmGpu *gpu, uint32_t objectId, uint32_t objectType) + : m_gpu(gpu) + , m_id(objectId) + , m_objectType(objectType) +{ +} + +DrmPropertyList DrmObject::queryProperties(int fd, uint32_t objectId, uint32_t objectType) +{ + DrmUniquePtr properties(drmModeObjectGetProperties(fd, objectId, objectType)); + if (!properties) { + qCWarning(KWIN_DRM) << "Failed to get properties for object" << objectId; + return {}; + } + DrmPropertyList ret; + for (uint32_t i = 0; i < properties->count_props; i++) { + DrmUniquePtr prop(drmModeGetProperty(fd, properties->props[i])); + if (!prop) { + qCWarning(KWIN_DRM, "Getting property %d of object %d failed!", properties->props[i], objectId); + continue; + } + ret.addProperty(std::move(prop), properties->prop_values[i]); + } + return ret; +} + +DrmPropertyList DrmObject::queryProperties() const +{ + return queryProperties(m_gpu->fd(), m_id, m_objectType); +} + +uint32_t DrmObject::id() const +{ + return m_id; +} + +DrmGpu *DrmObject::gpu() const +{ + return m_gpu; +} + +uint32_t DrmObject::type() const +{ + return m_objectType; +} + +QString DrmObject::typeName() const +{ + switch (m_objectType) { + case DRM_MODE_OBJECT_CONNECTOR: + return QStringLiteral("connector"); + case DRM_MODE_OBJECT_CRTC: + return QStringLiteral("crtc"); + case DRM_MODE_OBJECT_PLANE: + return QStringLiteral("plane"); + default: + return QStringLiteral("unknown?"); + } +} + +void DrmPropertyList::addProperty(DrmUniquePtr &&prop, uint64_t value) +{ + m_properties.push_back(std::make_pair(std::move(prop), value)); +} + +std::optional, uint64_t>> DrmPropertyList::takeProperty(const QByteArray &name) +{ + const auto it = std::ranges::find_if(m_properties, [&name](const auto &pair) { + return pair.first->name == name; + }); + if (it != m_properties.end()) { + auto ret = std::move(*it); + m_properties.erase(it); + return ret; + } else { + return std::nullopt; + } +} +} + +QDebug operator<<(QDebug s, const KWin::DrmObject *obj) +{ + QDebugStateSaver saver(s); + if (obj) { + s.nospace() << "DrmObject(id=" << obj->id() << ", gpu=" << obj->gpu() << ')'; + } else { + s << "DrmObject(0x0)"; + } + return s; +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_object.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_object.h new file mode 100644 index 0000000000..37b73b1aad --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_object.h @@ -0,0 +1,75 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "kwin_export.h" + +#include +#include +#include + +#include + +// drm +#include + +#include "drm_pointer.h" +#include "drm_property.h" + +namespace KWin +{ + +class DrmBackend; +class DrmGpu; +class DrmOutput; +class DrmAtomicCommit; + +class KWIN_EXPORT DrmPropertyList +{ +public: + void addProperty(DrmUniquePtr &&prop, uint64_t value); + std::optional, uint64_t>> takeProperty(const QByteArray &name); + +private: + std::vector, uint64_t>> m_properties; +}; + +class KWIN_EXPORT DrmObject +{ +public: + virtual ~DrmObject() = default; + DrmObject(const DrmObject &) = delete; + + /** + * Set the properties in such a way that this resource won't be used anymore + */ + virtual void disable(DrmAtomicCommit *commit) = 0; + + virtual bool updateProperties() = 0; + + uint32_t id() const; + DrmGpu *gpu() const; + uint32_t type() const; + QString typeName() const; + + static DrmPropertyList queryProperties(int fd, uint32_t object, uint32_t objectType); + +protected: + DrmObject(DrmGpu *gpu, uint32_t objectId, uint32_t objectType); + + DrmPropertyList queryProperties() const; + +private: + DrmGpu *m_gpu; + const uint32_t m_id; + const uint32_t m_objectType; +}; + +} + +QDebug operator<<(QDebug stream, const KWin::DrmObject *); diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_output.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_output.cpp new file mode 100644 index 0000000000..14a30b6de1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_output.cpp @@ -0,0 +1,822 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_output.h" +#include "drm_backend.h" +#include "drm_connector.h" +#include "drm_crtc.h" +#include "drm_gpu.h" +#include "drm_pipeline.h" + +#include "core/brightnessdevice.h" +#include "core/colortransformation.h" +#include "core/iccprofile.h" +#include "core/outputconfiguration.h" +#include "core/renderbackend.h" +#include "core/renderloop.h" +#include "core/renderloop_p.h" +#include "core/session.h" +#include "drm_layer.h" +#include "drm_logging.h" +#include "utils/kernel.h" +// Qt +#include +#include +#include +// c++ +#include +// drm +#include +#include +#include + +namespace KWin +{ + +static bool s_disableTripleBufferingSet = false; +static const bool s_disableTripleBuffering = qEnvironmentVariableIntValue("KWIN_DRM_DISABLE_TRIPLE_BUFFERING", &s_disableTripleBufferingSet) == 1; + +DrmOutput::DrmOutput(const std::shared_ptr &conn, DrmPipeline *pipeline) + : m_gpu(conn->gpu()) + , m_pipeline(pipeline) + , m_connector(conn) +{ + m_pipeline->setOutput(this); + if (m_gpu->atomicModeSetting() && ((!s_disableTripleBufferingSet && !m_gpu->isNVidia()) || (s_disableTripleBufferingSet && !s_disableTripleBuffering))) { + m_renderLoop->setMaxPendingFrameCount(2); + } + + const Edid *edid = m_connector->edid(); + setInformation(Information{ + .name = m_connector->connectorName(), + .manufacturer = edid->manufacturerString(), + .model = m_connector->modelName(), + .serialNumber = edid->serialNumber(), + .eisaId = edid->eisaId(), + .physicalSize = m_connector->physicalSize(), + .edid = *edid, + .subPixel = m_connector->subpixel(), + .capabilities = computeCapabilities(), + .panelOrientation = m_connector->panelOrientation.isValid() ? DrmConnector::toKWinTransform(m_connector->panelOrientation.enumValue()) : OutputTransform::Normal, + .internal = m_connector->isInternal(), + .nonDesktop = m_connector->isNonDesktop(), + .mstPath = m_connector->mstPath(), + .maxPeakBrightness = edid->desiredMaxLuminance(), + .maxAverageBrightness = edid->desiredMaxFrameAverageLuminance(), + .minBrightness = edid->desiredMinLuminance(), + .bitsPerColorRange = BpcRange{ + .min = m_gpu->atomicModeSetting() ? uint32_t(m_connector->maxBpc.minValue()) : 8, + .max = m_gpu->atomicModeSetting() ? uint32_t(m_connector->maxBpc.maxValue()) : 8, + }, + .minVrrRefreshRateHz = edid->minVrrRefreshRateHz(), + }); + updateConnectorProperties(); +} + +bool DrmOutput::addLeaseObjects(QList &objectList) +{ + if (!m_pipeline->crtc()) { + qCWarning(KWIN_DRM) << "Can't lease connector: No suitable crtc available"; + return false; + } + qCDebug(KWIN_DRM) << "adding connector" << m_pipeline->connector()->id() << "to lease"; + objectList << m_pipeline->connector()->id(); + objectList << m_pipeline->crtc()->id(); + if (m_pipeline->crtc()->primaryPlane()) { + objectList << m_pipeline->crtc()->primaryPlane()->id(); + } + return true; +} + +void DrmOutput::leased(DrmLease *lease) +{ + m_lease = lease; +} + +void DrmOutput::leaseEnded() +{ + qCDebug(KWIN_DRM) << "ended lease for connector" << m_pipeline->connector()->id(); + m_lease = nullptr; +} + +DrmLease *DrmOutput::lease() const +{ + return m_lease; +} + +bool DrmOutput::shouldDisableNonPrimaryPlanes() const +{ + // The kernel rejects async commits that change anything but the primary plane FB_ID + // This disables the hardware cursor, so it doesn't interfere with that + return m_desiredPresentationMode == PresentationMode::Async || m_desiredPresentationMode == PresentationMode::AdaptiveAsync; +} + +bool DrmOutput::presentAsync(OutputLayer *layer, std::optional allowedVrrDelay) +{ + if (!m_pipeline) { + // this can happen when the output gets hot-unplugged + // FIXME fix output lifetimes so that this doesn't happen anymore... + return false; + } + if (m_pipeline->gpu()->atomicModeSetting() && shouldDisableNonPrimaryPlanes() && layer->isEnabled()) { + return false; + } + return m_pipeline->presentAsync(layer, allowedVrrDelay); +} + +DrmPlane::Transformations outputToPlaneTransform(OutputTransform transform) +{ + using PlaneTrans = DrmPlane::Transformation; + + switch (transform.kind()) { + case OutputTransform::Normal: + return PlaneTrans::Rotate0; + case OutputTransform::FlipX: + return PlaneTrans::ReflectX | PlaneTrans::Rotate0; + case OutputTransform::Rotate90: + return PlaneTrans::Rotate90; + case OutputTransform::FlipX90: + return PlaneTrans::ReflectX | PlaneTrans::Rotate90; + case OutputTransform::Rotate180: + return PlaneTrans::Rotate180; + case OutputTransform::FlipX180: + return PlaneTrans::ReflectX | PlaneTrans::Rotate180; + case OutputTransform::Rotate270: + return PlaneTrans::Rotate270; + case OutputTransform::FlipX270: + return PlaneTrans::ReflectX | PlaneTrans::Rotate270; + default: + Q_UNREACHABLE(); + } +} + +void DrmOutput::updateConnectorProperties() +{ + updateInformation(); + + State next = m_state; + populateModes(&next); + setState(next); +} + +void DrmOutput::populateModes(State *next) const +{ + next->modes.clear(); + + const auto drmModes = m_pipeline->connector()->modes(); + for (const auto &drmMode : drmModes) { + next->modes.append(drmMode); + } + + for (const auto &custom : next->customModes) { + next->modes.append(m_pipeline->connector()->generateMode(custom.size, custom.refreshRate / 1000.0f, custom.flags | OutputMode::Flag::Custom)); + } + + static const bool noCustomModeQuirk = qEnvironmentVariableIntValue("KWIN_DRM_NO_CUSTOM_MODE_QUIRK"); + if (!noCustomModeQuirk) { + if (!next->desiredModeSize.isEmpty() && next->desiredModeRefreshRate && next->desiredModeFlags) { + for (const auto &mode : std::as_const(next->modes)) { + const auto drmMode = std::static_pointer_cast(mode); + if (next->desiredModeSize == mode->size() && next->desiredModeRefreshRate == drmMode->refreshRate() && next->desiredModeFlags == drmMode->flags()) { + next->currentMode = drmMode; + break; + } + } + } + } + + if (!next->currentMode) { + next->currentMode = next->modes.constFirst(); + } else if (!next->modes.contains(next->currentMode)) { + const auto it = std::ranges::find_if(next->modes, [&](const auto &mode) { + return next->currentMode->size() == mode->size() + && next->currentMode->refreshRate() == mode->refreshRate() + && next->currentMode->flags() == mode->flags(); + }); + if (it != next->modes.end()) { + next->currentMode = *it; + } else { + next->currentMode->setRemoved(); + next->modes.push_front(next->currentMode); + } + } +} + +static const bool s_allowColorspaceIntel = qEnvironmentVariableIntValue("KWIN_DRM_ALLOW_INTEL_COLORSPACE") == 1; +static const bool s_allowColorspaceNVidia = qEnvironmentVariableIntValue("KWIN_DRM_ALLOW_NVIDIA_COLORSPACE") == 1; + +BackendOutput::Capabilities DrmOutput::computeCapabilities() const +{ + Capabilities capabilities = Capability::Dpms | Capability::IccProfile | Capability::CustomModes; + if (m_connector->overscan.isValid() || m_connector->underscan.isValid()) { + capabilities |= Capability::Overscan; + } + if (m_connector->vrrCapable.isValid() && m_connector->vrrCapable.value()) { + capabilities |= Capability::Vrr; + } + if (m_gpu->asyncPageflipSupported()) { + capabilities |= Capability::Tearing; + } + if (m_connector->broadcastRGB.isValid()) { + capabilities |= Capability::RgbRange; + } + if (m_connector->colorspace.isValid() && (m_connector->colorspace.hasEnum(DrmConnector::Colorspace::BT2020_RGB) || m_connector->colorspace.hasEnum(DrmConnector::Colorspace::BT2020_YCC)) && m_connector->edid()->supportsBT2020()) { + bool allowColorspace = true; + if (m_gpu->isI915()) { + allowColorspace &= s_allowColorspaceIntel || linuxKernelVersion() >= Version(6, 11); + } else if (m_gpu->isNVidia()) { + allowColorspace &= s_allowColorspaceNVidia || m_gpu->nvidiaDriverVersion() >= Version(565, 57, 1); + } + if (allowColorspace) { + capabilities |= Capability::WideColorGamut; + } + } + if (m_connector->hdrMetadata.isValid() && m_connector->edid()->supportsPQ() && (capabilities & Capability::WideColorGamut)) { + capabilities |= Capability::HighDynamicRange; + } + if (m_connector->isInternal() && m_autoRotateAvailable) { + capabilities |= Capability::AutoRotation; + } + if (m_state.highDynamicRange || m_state.brightnessDevice || m_state.allowSdrSoftwareBrightness) { + capabilities |= Capability::BrightnessControl; + // changing brightness too often with DDC/CI can cause problems on + // some displays, so automatic brightness is blocked for them + const bool ddcci = !m_state.highDynamicRange && m_state.brightnessDevice && m_state.brightnessDevice->usesDdcCi(); + if (m_autoBrightnessAvailable && !ddcci) { + capabilities |= Capability::AutomaticBrightness; + } + } + if (m_connector->edid()->isValid() && m_connector->edid()->defaultColorimetry().has_value()) { + capabilities |= Capability::BuiltInColorProfile; + } + if (m_state.detectedDdcCi) { + capabilities |= Capability::DdcCi; + } + if (m_connector->maxBpc.isValid()) { + capabilities |= Capability::MaxBitsPerColor; + } + if (m_state.brightnessDevice && isInternal()) { + capabilities |= Capability::Edr; + } + if (m_gpu->sharpnessSupported()) { + capabilities |= Capability::SharpnessControl; + } + return capabilities; +} + +void DrmOutput::updateInformation() +{ + // not all changes are currently handled by the rest of KWin + // so limit the changes to what's verified to work + const Edid *edid = m_connector->edid(); + Information nextInfo = m_information; + nextInfo.capabilities = computeCapabilities(); + nextInfo.maxPeakBrightness = edid->desiredMaxLuminance(); + nextInfo.maxAverageBrightness = edid->desiredMaxFrameAverageLuminance(); + nextInfo.minBrightness = edid->desiredMinLuminance(); + // TODO narrow that down by parsing the EDID and checking what the display supports + nextInfo.bitsPerColorRange = BpcRange{ + .min = m_gpu->atomicModeSetting() ? uint32_t(m_connector->maxBpc.minValue()) : 8, + .max = m_gpu->atomicModeSetting() ? uint32_t(m_connector->maxBpc.maxValue()) : 8, + }; + setInformation(nextInfo); +} + +bool DrmOutput::testPresentation(const std::shared_ptr &frame) +{ + m_desiredPresentationMode = frame->presentationMode(); + const auto layers = m_pipeline->layers(); + const bool nonPrimaryEnabled = std::ranges::any_of(layers, [](OutputLayer *layer) { + return layer->isEnabled() && layer->type() != OutputLayerType::Primary; + }); + if (m_gpu->needsModeset()) { + // modesets should be done with only the primary plane + // as additional planes may mean we can't power all outputs + if (nonPrimaryEnabled) { + return false; + } + // the atomic test for the modeset has already been done before + // so testing again isn't super useful + return true; + } + m_pipeline->setPresentationMode(frame->presentationMode()); + if (nonPrimaryEnabled) { + // the cursor plane needs to be disabled before we enable tearing; see DrmOutput::presentAsync + if (frame->presentationMode() == PresentationMode::AdaptiveAsync) { + m_pipeline->setPresentationMode(PresentationMode::AdaptiveSync); + } else if (frame->presentationMode() == PresentationMode::Async) { + m_pipeline->setPresentationMode(PresentationMode::VSync); + } + } + DrmPipeline::Error err = m_pipeline->testPresent(frame); + if (err != DrmPipeline::Error::None && frame->presentationMode() == PresentationMode::AdaptiveAsync) { + // tearing can fail in various circumstances, but vrr shouldn't + m_pipeline->setPresentationMode(PresentationMode::AdaptiveSync); + err = m_pipeline->testPresent(frame); + } + if (err != DrmPipeline::Error::None && frame->presentationMode() != PresentationMode::VSync) { + // retry with the most basic presentation mode + m_pipeline->setPresentationMode(PresentationMode::VSync); + err = m_pipeline->testPresent(frame); + } + return err == DrmPipeline::Error::None; +} + +bool DrmOutput::present(const QList &layersToUpdate, const std::shared_ptr &frame) +{ + m_desiredPresentationMode = frame->presentationMode(); + const bool needsModeset = m_gpu->needsModeset(); + bool success; + if (needsModeset) { + m_pipeline->setPresentationMode(PresentationMode::VSync); + m_pipeline->setContentType(DrmConnector::DrmContentType::Graphics); + m_pipeline->maybeModeset(frame); + success = true; + } else { + // the presentation mode of the pipeline is already set in testPresentation + success = m_pipeline->present(layersToUpdate, frame) == DrmPipeline::Error::None; + } + m_renderLoop->setPresentationMode(m_pipeline->presentationMode()); + if (!success) { + return false; + } + updateBrightness(frame->brightness().value_or(m_state.currentBrightness.value_or(m_state.brightnessSetting)), + frame->artificialHdrHeadroom().value_or(m_state.artificialHdrHeadroom), + frame->dimmingFactor().value_or(m_state.currentDimming)); + return true; +} + +void DrmOutput::repairPresentation() +{ + // read back drm properties, most likely our info is out of date somehow + // or we need a modeset + QTimer::singleShot(0, m_gpu->platform(), [backend = m_gpu->platform()]() { + backend->updateOutputs(); + }); +} + +bool DrmOutput::overlayLayersLikelyBroken() const +{ + return m_gpu->isNVidia(); +} + +DrmConnector *DrmOutput::connector() const +{ + return m_connector.get(); +} + +DrmPipeline *DrmOutput::pipeline() const +{ + return m_pipeline; +} + +std::optional DrmOutput::decideAutomaticBpcLimit() const +{ + static bool preferreedColorDepthIsSet = false; + static const int preferred = qEnvironmentVariableIntValue("KWIN_DRM_PREFER_COLOR_DEPTH", &preferreedColorDepthIsSet); + if (preferreedColorDepthIsSet) { + return preferred / 3; + } + if (!m_connector->mstPath().isEmpty()) { + // >8bpc is often broken with docks + return 8; + } + return std::nullopt; +} + +static QVector3D adaptChannelFactors(const std::shared_ptr &originalColor, const QVector3D &sRGBchannelFactors) +{ + QVector3D adaptedChannelFactors = ColorDescription::sRGB->containerColorimetry().relativeColorimetricTo(originalColor->containerColorimetry()) * sRGBchannelFactors; + // ensure none of the values reach zero, otherwise the white point might end up on or outside + // the edges of the gamut, which leads to terrible glitches + adaptedChannelFactors.setX(std::max(adaptedChannelFactors.x(), 0.0001f)); + adaptedChannelFactors.setY(std::max(adaptedChannelFactors.y(), 0.0001f)); + adaptedChannelFactors.setZ(std::max(adaptedChannelFactors.z(), 0.0001f)); + return adaptedChannelFactors; +} + +static std::shared_ptr applyNightLight(const std::shared_ptr &originalColor, const QVector3D &sRGBchannelFactors) +{ + const QVector3D adapted = adaptChannelFactors(originalColor, sRGBchannelFactors); + // calculate the white point + // this includes the maximum brightness we can do without clipping any color channel as well + const xyY newWhite = XYZ::fromVector(originalColor->containerColorimetry().toXYZ() * adapted).toxyY(); + return originalColor->withWhitepoint(newWhite)->dimmed(newWhite.Y); +} + +double DrmOutput::calculateMaxArtificialHdrHeadroom(const State &next) const +{ + if (!next.brightnessDevice || !isInternal() || next.edrPolicy == EdrPolicy::Never) { + return 1.0; + } + // just a rough estimate from the Framework 13 laptop. + // The less accurate this is, the more the screen will flicker during backlight changes + constexpr double relativeLuminanceAtZeroBrightness = 0.04; + // to restrict HDR videos from using all the battery and burning your eyes + // TODO make it a setting, and/or dependent on the power management state? + constexpr double maxHdrHeadroom = 3.0; + const double maxPossibleHeadroom = (1 + relativeLuminanceAtZeroBrightness) + / (relativeLuminanceAtZeroBrightness + next.currentBrightness.value_or(next.brightnessSetting)); + return std::min(maxPossibleHeadroom, maxHdrHeadroom); +} + +std::shared_ptr DrmOutput::createColorDescription(const State &next) const +{ + const bool effectiveHdr = next.highDynamicRange && (capabilities() & Capability::HighDynamicRange); + const bool effectiveWcg = next.wideColorGamut && (capabilities() & Capability::WideColorGamut); + double brightnessFactor = 1.0; + if ((!next.brightnessDevice && next.allowSdrSoftwareBrightness) || effectiveHdr) { + brightnessFactor = next.currentBrightness.value_or(next.brightnessSetting); + } + if (!next.brightnessDevice || next.brightnessDevice->usesDdcCi()) { + brightnessFactor *= next.currentDimming; + } + + if (next.colorProfileSource == ColorProfileSource::ICC && !effectiveHdr && !effectiveWcg && next.iccProfile) { + const double maxFALL = next.iccProfile->maxFALL().value_or(200); + const double minBrightness = next.iccProfile->relativeBlackPoint().value_or(0) * maxFALL; + const auto sdrColor = Colorimetry::BT709.interpolateGamutTo(next.iccProfile->colorimetry(), next.sdrGamutWideness); + const double effectiveReferenceLuminance = 5 + (maxFALL - 5) * brightnessFactor; + + return std::make_shared(ColorDescription{ + next.iccProfile->colorimetry(), + TransferFunction(TransferFunction::gamma22, minBrightness * next.artificialHdrHeadroom, maxFALL * next.artificialHdrHeadroom), + effectiveReferenceLuminance, + minBrightness * next.artificialHdrHeadroom, + maxFALL * next.maxPossibleArtificialHdrHeadroom, + maxFALL * next.maxPossibleArtificialHdrHeadroom, + next.iccProfile->colorimetry(), + sdrColor, + }); + } + + const Colorimetry nativeColorimetry = m_information.edid.nativeColorimetry().value_or(Colorimetry::BT709); + const Colorimetry containerColorimetry = effectiveWcg ? Colorimetry::BT2020 : (next.colorProfileSource == ColorProfileSource::EDID ? nativeColorimetry : Colorimetry::BT709); + const Colorimetry masteringColorimetry = (effectiveWcg || next.colorProfileSource == ColorProfileSource::EDID) ? nativeColorimetry : Colorimetry::BT709; + const Colorimetry sdrColorimetry = (effectiveWcg || next.colorProfileSource == ColorProfileSource::EDID) ? Colorimetry::BT709.interpolateGamutTo(nativeColorimetry, next.sdrGamutWideness) : Colorimetry::BT709; + // TODO the EDID can contain a gamma value, use that when available and colorSource == ColorProfileSource::EDID + const double maxAverageBrightness = effectiveHdr ? next.maxAverageBrightnessOverride.value_or(m_connector->edid()->desiredMaxFrameAverageLuminance().value_or(next.referenceLuminance)) : 200 * next.maxPossibleArtificialHdrHeadroom; + const double maxPeakBrightness = effectiveHdr ? next.maxPeakBrightnessOverride.value_or(m_connector->edid()->desiredMaxLuminance().value_or(800)) : 200 * next.maxPossibleArtificialHdrHeadroom; + const double referenceLuminance = effectiveHdr ? next.referenceLuminance : 200; + // the min luminance the Wayland protocol defines for SDR is unrealistically high for most modern displays + // normally that doesn't really matter, but with night light it can lead to increased black levels, + // which are really noticeable when they're tinted red + const double minSdrLuminance = 0.01; + const auto transferFunction = effectiveHdr ? TransferFunction{TransferFunction::PerceptualQuantizer} : TransferFunction{TransferFunction::gamma22, minSdrLuminance * next.artificialHdrHeadroom, referenceLuminance * next.artificialHdrHeadroom}; + // HDR screens are weird, sending them the min. luminance from the EDID does *not* make all of them present the darkest luminance the display can show + // to work around that, (unless overridden by the user), assume the min. luminance of the transfer function instead + const double minBrightness = effectiveHdr ? next.minBrightnessOverride.value_or(transferFunction.minLuminance) : transferFunction.minLuminance; + + const double effectiveReferenceLuminance = 5 + (referenceLuminance - 5) * brightnessFactor; + return std::make_shared(ColorDescription{ + containerColorimetry, + transferFunction, + effectiveReferenceLuminance, + minBrightness, + maxAverageBrightness, + maxPeakBrightness, + masteringColorimetry, + sdrColorimetry, + }); +} + +bool DrmOutput::queueChanges(const std::shared_ptr &props) +{ + const auto mode = props->mode.value_or(currentMode()).lock(); + if (!mode) { + return false; + } + + m_nextState = m_state; + m_nextState->enabled = props->enabled.value_or(m_state.enabled); + m_nextState->position = props->pos.value_or(m_state.position); + m_nextState->scale = props->scale.value_or(m_state.scale); + m_nextState->scaleSetting = props->scaleSetting.value_or(m_state.scaleSetting); + m_nextState->transform = props->transform.value_or(m_state.transform); + m_nextState->manualTransform = props->manualTransform.value_or(m_state.manualTransform); + m_nextState->currentMode = mode; + m_nextState->overscan = props->overscan.value_or(m_state.overscan); + m_nextState->rgbRange = props->rgbRange.value_or(m_state.rgbRange); + m_nextState->highDynamicRange = props->highDynamicRange.value_or(m_state.highDynamicRange); + m_nextState->referenceLuminance = props->referenceLuminance.value_or(m_state.referenceLuminance); + m_nextState->wideColorGamut = props->wideColorGamut.value_or(m_state.wideColorGamut); + m_nextState->autoRotatePolicy = props->autoRotationPolicy.value_or(m_state.autoRotatePolicy); + m_nextState->maxPeakBrightnessOverride = props->maxPeakBrightnessOverride.value_or(m_state.maxPeakBrightnessOverride); + m_nextState->maxAverageBrightnessOverride = props->maxAverageBrightnessOverride.value_or(m_state.maxAverageBrightnessOverride); + m_nextState->minBrightnessOverride = props->minBrightnessOverride.value_or(m_state.minBrightnessOverride); + m_nextState->sdrGamutWideness = props->sdrGamutWideness.value_or(m_state.sdrGamutWideness); + m_nextState->iccProfilePath = props->iccProfilePath.value_or(m_state.iccProfilePath); + m_nextState->iccProfile = props->iccProfile.value_or(m_state.iccProfile); + m_nextState->vrrPolicy = props->vrrPolicy.value_or(m_state.vrrPolicy); + m_nextState->colorProfileSource = props->colorProfileSource.value_or(m_state.colorProfileSource); + m_nextState->brightnessSetting = props->brightness.value_or(m_state.brightnessSetting); + m_nextState->desiredModeSize = props->desiredModeSize.value_or(m_state.desiredModeSize); + m_nextState->desiredModeRefreshRate = props->desiredModeRefreshRate.value_or(m_state.desiredModeRefreshRate); + m_nextState->desiredModeFlags = props->desiredModeFlags.value_or(m_state.desiredModeFlags); + m_nextState->allowSdrSoftwareBrightness = props->allowSdrSoftwareBrightness.value_or(m_state.allowSdrSoftwareBrightness); + m_nextState->colorPowerTradeoff = props->colorPowerTradeoff.value_or(m_state.colorPowerTradeoff); + m_nextState->dimming = props->dimming.value_or(m_state.dimming); + m_nextState->brightnessDevice = props->brightnessDevice.value_or(m_state.brightnessDevice); + if (!m_nextState->highDynamicRange && m_nextState->brightnessDevice) { + m_nextState->currentBrightness = props->currentHardwareBrightness.has_value() ? props->currentHardwareBrightness : m_state.currentBrightness; + } + m_nextState->uuid = props->uuid.value_or(m_state.uuid); + m_nextState->replicationSource = props->replicationSource.value_or(m_state.replicationSource); + m_nextState->detectedDdcCi = props->detectedDdcCi.value_or(m_state.detectedDdcCi); + m_nextState->allowDdcCi = props->allowDdcCi.value_or(m_state.allowDdcCi); + if (m_nextState->allowSdrSoftwareBrightness != m_state.allowSdrSoftwareBrightness) { + // make sure that we set the brightness again next frame + m_nextState->currentBrightness.reset(); + } + m_nextState->maxBitsPerColor = props->maxBitsPerColor.value_or(m_state.maxBitsPerColor); + m_nextState->automaticMaxBitsPerColorLimit = decideAutomaticBpcLimit(); + m_nextState->edrPolicy = props->edrPolicy.value_or(m_state.edrPolicy); + m_nextState->dpmsMode = props->dpmsMode.value_or(m_state.dpmsMode); + if (props->customModes.has_value()) { + m_nextState->customModes = *props->customModes; + populateModes(&*m_nextState); + } + m_nextState->maxPossibleArtificialHdrHeadroom = calculateMaxArtificialHdrHeadroom(*m_nextState); + m_nextState->originalColorDescription = createColorDescription(*m_nextState); + m_nextState->colorDescription = applyNightLight(m_nextState->originalColorDescription, m_sRgbChannelFactors); + m_nextState->sharpnessSetting = props->sharpness.value_or(m_state.sharpnessSetting); + m_nextState->priority = props->priority.value_or(m_state.priority); + m_nextState->deviceOffset = props->deviceOffset.value_or(m_state.deviceOffset); + m_nextState->automaticBrightness = props->automaticBrightness.value_or(m_state.automaticBrightness); + m_nextState->lastBrightnessAdjustmentReason = props->brightnessReason.value_or(m_state.lastBrightnessAdjustmentReason); + m_nextState->autoBrightnessCurve = props->autoBrightnessCurve.value_or(m_state.autoBrightnessCurve); + + const bool bt2020 = m_nextState->wideColorGamut && (capabilities() & Capability::WideColorGamut); + const bool hdr = m_nextState->highDynamicRange && (capabilities() & Capability::HighDynamicRange); + m_pipeline->setMode(std::static_pointer_cast(mode)); + m_pipeline->setOverscan(m_nextState->overscan); + m_pipeline->setRgbRange(m_nextState->rgbRange); + m_pipeline->setEnable(m_nextState->enabled); + m_pipeline->setActive(m_nextState->enabled && m_nextState->dpmsMode == DpmsMode::On); + m_pipeline->setHighDynamicRange(hdr); + m_pipeline->setWideColorGamut(bt2020); + + if (uint32_t bpcSetting = props->maxBitsPerColor.value_or(maxBitsPerColor())) { + m_pipeline->setMaxBpc(bpcSetting); + } else { + const auto tradeoff = props->colorPowerTradeoff.value_or(m_state.colorPowerTradeoff); + m_pipeline->setMaxBpc(decideAutomaticBpcLimit().value_or(tradeoff == ColorPowerTradeoff::PreferAccuracy ? 16 : 10)); + } + + if (bt2020 || hdr || props->colorProfileSource.value_or(m_state.colorProfileSource) != ColorProfileSource::ICC) { + // ICC profiles don't support HDR (yet) + m_pipeline->setIccProfile(nullptr); + } else { + m_pipeline->setIccProfile(m_nextState->iccProfile); + } + // remove the color pipeline for the atomic test + // otherwise it could potentially fail + if (m_gpu->atomicModeSetting()) { + m_pipeline->setCrtcColorPipeline(ColorPipeline{}); + } + + return true; +} + +void DrmOutput::applyQueuedChanges(const std::shared_ptr &props) +{ + Q_EMIT aboutToChange(props.get()); + m_pipeline->applyPendingChanges(); + + tryKmsColorOffloading(*m_nextState); + maybeScheduleRepaints(*m_nextState); + const bool nextOff = m_nextState->dpmsMode != DpmsMode::On; + const bool currentOff = m_state.dpmsMode != DpmsMode::On; + if (nextOff != currentOff) { + if (m_nextState->dpmsMode == DpmsMode::On) { + m_renderLoop->uninhibit(); + } else { + // NOTE that legacy modesetting applies dpms in the "test" before this + // method gets called, so we have to special case legacy vs. atomic here + if (m_state.dpmsMode == DpmsMode::On && m_gpu->atomicModeSetting()) { + m_nextState->dpmsMode = DpmsMode::TurningOff; + } + m_renderLoop->inhibit(); + } + } + setState(*m_nextState); + m_nextState.reset(); + + // allowSdrSoftwareBrightness, the brightness device or detectedDdcCi might change our capabilities + Information newInfo = m_information; + newInfo.capabilities = computeCapabilities(); + setInformation(newInfo); + + if (!m_pipeline->activePending() && m_gpu->needsModeset()) { + // If the output is active, state changes end up being committed when presenting. + // However, if it's off, that needs to be done explicitly + m_gpu->maybeModeset(nullptr, nullptr); + } + + m_renderLoop->setRefreshRate(refreshRate()); + + if (m_state.brightnessDevice && m_state.highDynamicRange && isInternal()) { + // This is usually not necessary with external monitors, as they default to 100% in HDR mode on their own, + // and is known to even cause problems with some buggy ones. + // This is however needed for laptop displays to have the desired luminance levels + m_state.brightnessDevice->setBrightness(1.0); + } + + Q_EMIT changed(); +} + +void DrmOutput::unsetBrightnessDevice() +{ + State next = m_state; + next.brightnessDevice = nullptr; + setState(next); + updateInformation(); +} + +void DrmOutput::updateBrightness(double newBrightness, double newArtificialHdrHeadroom, double newDimming) +{ + if (m_state.currentBrightness == newBrightness + && m_state.artificialHdrHeadroom == newArtificialHdrHeadroom + && m_state.currentDimming == newDimming) { + return; + } + if (m_state.brightnessDevice && !m_state.highDynamicRange) { + double brightnessFactor = newBrightness; + // With DDC/CI, dimming should be applied in software only, + // to avoid unnecessary EEPROM writes on the display side + if (!m_state.brightnessDevice->usesDdcCi()) { + brightnessFactor *= newDimming; + } + constexpr double minLuminance = 0.04; + const double effectiveBrightness = (minLuminance + brightnessFactor) * newArtificialHdrHeadroom - minLuminance; + m_state.brightnessDevice->setBrightness(effectiveBrightness); + } + State next = m_state; + next.currentBrightness = newBrightness; + next.artificialHdrHeadroom = newArtificialHdrHeadroom; + next.currentDimming = newDimming; + next.maxPossibleArtificialHdrHeadroom = calculateMaxArtificialHdrHeadroom(next); + next.originalColorDescription = createColorDescription(next); + next.colorDescription = applyNightLight(next.originalColorDescription, m_sRgbChannelFactors); + tryKmsColorOffloading(next); + setState(next); +} + +void DrmOutput::revertQueuedChanges() +{ + m_nextState.reset(); + m_pipeline->revertPendingChanges(); +} + +void DrmOutput::setChannelFactors(const QVector3D &rgb) +{ + if (rgb == m_sRgbChannelFactors) { + return; + } + m_sRgbChannelFactors = rgb; + State next = m_state; + next.colorDescription = applyNightLight(next.originalColorDescription, m_sRgbChannelFactors); + tryKmsColorOffloading(next); + setState(next); +} + +void DrmOutput::tryKmsColorOffloading(State &next) +{ + if (!m_pipeline->activePending() || m_pipeline->layers().empty() || m_lease) { + return; + } + + const auto repaints = qScopeGuard([this, &next]() { + maybeScheduleRepaints(next); + }); + + constexpr TransferFunction::Type blendingSpace = TransferFunction::gamma22; + const double maxLuminance = next.colorDescription->maxHdrLuminance().value_or(next.colorDescription->referenceLuminance()); + if (next.colorDescription->transferFunction().type == blendingSpace) { + next.blendingColor = next.colorDescription; + } else { + next.blendingColor = next.colorDescription->withTransferFunction(TransferFunction(blendingSpace, 0, maxLuminance)); + } + + // we can't use the original color description without modifications + // as that would un-do any brightness adjustments we did for night light + // note that we also can't use ColorDescription::dimmed, as we must avoid clipping to this luminance! + const auto encoding = next.originalColorDescription->withReference(next.colorDescription->referenceLuminance()); + + // absolute colorimetric to preserve the whitepoint adjustments made during compositing + ColorPipeline colorPipeline = ColorPipeline::create(next.blendingColor, encoding, RenderingIntent::AbsoluteColorimetricNoAdaptation); + + const bool hdr = next.highDynamicRange && (capabilities() & Capability::HighDynamicRange); + const bool wcg = next.wideColorGamut && (capabilities() & Capability::WideColorGamut); + const bool usesICC = next.colorProfileSource == ColorProfileSource::ICC && next.iccProfile && !hdr && !wcg; + if (next.colorPowerTradeoff == ColorPowerTradeoff::PreferAccuracy) { + next.layerBlendingColor = encoding; + m_pipeline->setCrtcColorPipeline(ColorPipeline{}); + m_pipeline->applyPendingChanges(); + m_needsShadowBuffer = usesICC + || next.colorDescription->transferFunction().type != blendingSpace + || !colorPipeline.isIdentity(); + return; + } + if (usesICC) { + colorPipeline.addTransferFunction(encoding->transferFunction(), ColorspaceType::LinearRGB); + colorPipeline.addMultiplier(1.0 / encoding->transferFunction().maxLuminance); + if (!next.iccProfile->mhc2Matrix().isIdentity()) { + // NOTE the spec assumes BT.709 or BT.2020 is used as the target colorspace, *not* the native colorspace! + const auto calibration = Colorimetry::BT709.fromXYZ() * next.iccProfile->mhc2Matrix() * encoding->containerColorimetry().toXYZ(); + colorPipeline.addMatrix(calibration, colorPipeline.currentOutputRange(), ColorspaceType::LinearRGB); + } + colorPipeline.add1DLUT(next.iccProfile->inverseTransferFunction(), ColorspaceType::NonLinearRGB); + if (next.iccProfile->vcgt()) { + colorPipeline.add1DLUT(next.iccProfile->vcgt(), ColorspaceType::NonLinearRGB); + } + } + m_pipeline->setCrtcColorPipeline(colorPipeline); + if (DrmPipeline::commitPipelines({m_pipeline}, DrmPipeline::CommitMode::Test) == DrmPipeline::Error::None) { + m_pipeline->applyPendingChanges(); + next.layerBlendingColor = next.blendingColor; + m_needsShadowBuffer = false; + return; + } + if (next.colorDescription->transferFunction().type == blendingSpace && !usesICC) { + // Allow falling back to applying night light in non-linear space. + // This isn't technically correct, but the difference is quite small and not worth + // losing a lot of performance and battery life over + ColorPipeline simplerPipeline(ValueRange{0, 1}, ColorspaceType::NonLinearRGB); + simplerPipeline.addMatrix(next.blendingColor->toOther(*encoding, RenderingIntent::AbsoluteColorimetricNoAdaptation), colorPipeline.currentOutputRange(), ColorspaceType::NonLinearRGB); + m_pipeline->setCrtcColorPipeline(simplerPipeline); + if (DrmPipeline::commitPipelines({m_pipeline}, DrmPipeline::CommitMode::Test) == DrmPipeline::Error::None) { + m_pipeline->applyPendingChanges(); + next.layerBlendingColor = next.blendingColor; + m_needsShadowBuffer = false; + return; + } + } + // fall back to using a shadow buffer for doing blending in gamma 2.2 and/or night light + m_pipeline->setCrtcColorPipeline(ColorPipeline{}); + m_pipeline->applyPendingChanges(); + next.layerBlendingColor = encoding; + m_needsShadowBuffer = usesICC + || next.colorDescription->transferFunction().type != blendingSpace + || !colorPipeline.isIdentity(); +} + +void DrmOutput::maybeScheduleRepaints(const State &next) +{ + // TODO move the output layers to BackendOutput, and have it take care of this when updating State + if (next.blendingColor != m_state.blendingColor || next.layerBlendingColor != m_state.layerBlendingColor) { + const auto layers = m_pipeline->layers(); + for (const auto &layer : layers) { + layer->addDeviceRepaint(Region::infinite()); + } + } +} + +bool DrmOutput::needsShadowBuffer() const +{ + return m_needsShadowBuffer; +} + +void DrmOutput::removePipeline() +{ + m_pipeline = nullptr; +} + +void DrmOutput::setAutoRotateAvailable(bool isAvailable) +{ + m_autoRotateAvailable = isAvailable; + Information next = m_information; + next.capabilities = computeCapabilities(); + setInformation(next); +} + +void DrmOutput::maybeUpdateDpmsState() +{ + // this is only needed for updating the state from "TurningOff" to "Off" + if (m_state.dpmsMode == DpmsMode::TurningOff && !m_pipeline->activePending()) { + State next = m_state; + next.dpmsMode = DpmsMode::Off; + setState(next); + } +} + +void DrmOutput::setAutoBrightnessAvailable(bool isAvailable) +{ + m_autoBrightnessAvailable = isAvailable; + Information next = m_information; + next.capabilities = computeCapabilities(); + setInformation(next); +} + +const BackendOutput::State &DrmOutput::nextState() const +{ + return m_nextState ? *m_nextState : m_state; +} +} + +#include "moc_drm_output.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_output.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_output.h new file mode 100644 index 0000000000..024b4f4313 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_output.h @@ -0,0 +1,105 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "drm_abstract_output.h" +#include "drm_object.h" +#include "drm_plane.h" +#include "utils/filedescriptor.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +class DrmConnector; +class DrmGpu; +class DrmPipeline; +class DumbSwapchain; +class DrmLease; +class OutputChangeSet; + +class KWIN_EXPORT DrmOutput : public DrmAbstractOutput +{ + Q_OBJECT +public: + explicit DrmOutput(const std::shared_ptr &connector, DrmPipeline *pipeline); + + DrmConnector *connector() const; + DrmPipeline *pipeline() const; + + bool testPresentation(const std::shared_ptr &frame) override; + bool present(const QList &layersToUpdate, const std::shared_ptr &frame) override; + void repairPresentation() override; + bool overlayLayersLikelyBroken() const override; + + bool queueChanges(const std::shared_ptr &properties); + void applyQueuedChanges(const std::shared_ptr &properties); + void revertQueuedChanges(); + + bool shouldDisableNonPrimaryPlanes() const; + bool presentAsync(OutputLayer *layer, std::optional allowedVrrDelay) override; + void setAutoRotateAvailable(bool isAvailable) override; + void setAutoBrightnessAvailable(bool isAvailable) override; + + DrmLease *lease() const; + bool addLeaseObjects(QList &objectList); + void leased(DrmLease *lease); + void leaseEnded(); + + void setChannelFactors(const QVector3D &rgb) override; + void updateConnectorProperties(); + + /** + * @returns whether or not the renderer should apply channel factors + */ + bool needsShadowBuffer() const; + + void removePipeline(); + void maybeUpdateDpmsState(); + + const State &nextState() const; + +private: + void tryKmsColorOffloading(State &next); + double calculateMaxArtificialHdrHeadroom(const State &next) const; + std::shared_ptr createColorDescription(const State &next) const; + Capabilities computeCapabilities() const; + void updateInformation(); + void unsetBrightnessDevice() override; + void updateBrightness(double newBrightness, double newArtificialHdrHeadroom, double newDimming); + void maybeScheduleRepaints(const State &next); + std::optional decideAutomaticBpcLimit() const; + void populateModes(State *state) const; + + DrmGpu *const m_gpu; + DrmPipeline *m_pipeline; + const std::shared_ptr m_connector; + + DrmLease *m_lease = nullptr; + + QVector3D m_sRgbChannelFactors = {1, 1, 1}; + bool m_needsShadowBuffer = false; + PresentationMode m_desiredPresentationMode = PresentationMode::VSync; + bool m_autoRotateAvailable = false; + bool m_autoBrightnessAvailable = false; + + std::optional m_nextState; +}; + +} + +Q_DECLARE_METATYPE(KWin::DrmOutput *) diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_pipeline.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_pipeline.cpp new file mode 100644 index 0000000000..856be8c4ee --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_pipeline.cpp @@ -0,0 +1,731 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2021 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "drm_pipeline.h" + +#include + +#include "core/iccprofile.h" +#include "core/session.h" +#include "drm_backend.h" +#include "drm_buffer.h" +#include "drm_commit.h" +#include "drm_commit_thread.h" +#include "drm_connector.h" +#include "drm_crtc.h" +#include "drm_egl_backend.h" +#include "drm_gpu.h" +#include "drm_layer.h" +#include "drm_logging.h" +#include "drm_output.h" +#include "drm_plane.h" +#include "utils/drm_format_helper.h" +#include "utils/envvar.h" +#include "utils/kernel.h" + +#include +#include + +using namespace std::literals; + +namespace KWin +{ + +DrmPipeline::DrmPipeline(DrmConnector *conn) + : m_connector(conn) + , m_commitThread(std::make_unique(conn->gpu(), conn->connectorName())) +{ +} + +DrmPipeline::~DrmPipeline() +{ + // the commit thread may still access the pipeline until it's stopped + // so it must be deleted before everything else + m_commitThread.reset(); +} + +DrmPipeline::Error DrmPipeline::testPresent(const std::shared_ptr &frame) +{ + if (!gpu()->atomicModeSetting()) { + // we can do nothing but hope for the best + // the compositor will have to do a fallback for when the real present fails + return Error::None; + } + // test the full state with all planes, to take pending commits into account + return DrmPipeline::commitPipelinesAtomic({this}, CommitMode::Test, frame, {}); +} + +DrmPipeline::Error DrmPipeline::present(const QList &layersToUpdate, const std::shared_ptr &frame) +{ + Q_ASSERT(m_pending.crtc); + if (gpu()->atomicModeSetting()) { + // NOTE that this assumes testPresentation has been called before and succeeded + // only give the actual state update to the commit thread, so that it can potentially reorder the commits + auto partialUpdate = std::make_unique(QList{this}); + if (Error err = prepareAtomicPresentation(partialUpdate.get(), frame); err != Error::None) { + return err; + } + for (const auto layer : layersToUpdate) { + const auto pipelineLayer = static_cast(layer); + if (Error err = prepareAtomicPlane(partialUpdate.get(), pipelineLayer->plane(), pipelineLayer, frame); err != Error::None) { + return err; + } + } + if (layersToUpdate.isEmpty()) { + // work around amdgpu not giving us a valid pageflip timestamp + // if the commit doesn't contain a drm plane + if (Error err = prepareAtomicPlane(partialUpdate.get(), m_pending.layers.front()->plane(), m_pending.layers.front(), frame); err != Error::None) { + return err; + } + } + if (m_pending.needsModesetProperties && !prepareAtomicModeset(partialUpdate.get())) { + return Error::InvalidArguments; + } + m_next.needsModesetProperties = m_pending.needsModesetProperties = false; + m_commitThread->addCommit(std::move(partialUpdate)); + return Error::None; + } else { + return presentLegacy(layersToUpdate, frame); + } +} + +void DrmPipeline::maybeModeset(const std::shared_ptr &frame) +{ + m_modesetPresentPending = true; + gpu()->maybeModeset(this, frame); +} + +DrmPipeline::Error DrmPipeline::commitPipelines(const QList &pipelines, CommitMode mode, const QList &unusedObjects) +{ + Q_ASSERT(!pipelines.isEmpty()); + if (pipelines[0]->gpu()->atomicModeSetting()) { + return commitPipelinesAtomic(pipelines, mode, nullptr, unusedObjects); + } else { + return commitPipelinesLegacy(pipelines, mode, unusedObjects); + } +} + +DrmPipeline::Error DrmPipeline::commitPipelinesAtomic(const QList &pipelines, CommitMode mode, const std::shared_ptr &frame, const QList &unusedObjects) +{ + auto commit = std::make_unique(pipelines); + if (mode == CommitMode::Test) { + // if there's a modeset pending, the tests on top of that state + // also have to allow modesets or they'll always fail + const bool wantsModeset = std::ranges::any_of(pipelines, [](DrmPipeline *pipeline) { + return pipeline->needsModeset(); + }); + if (wantsModeset) { + mode = CommitMode::TestAllowModeset; + } + } + for (DrmPipeline *pipeline : pipelines) { + if (Error err = pipeline->prepareAtomicCommit(commit.get(), mode, frame); err != Error::None) { + return err; + } + } + for (DrmObject *unused : unusedObjects) { + unused->disable(commit.get()); + } + switch (mode) { + case CommitMode::TestAllowModeset: { + if (!commit->testAllowModeset()) { + qCWarning(KWIN_DRM) << "Atomic modeset test failed!" << strerror(errno); + return errnoToError(); + } + const bool withoutModeset = std::ranges::all_of(pipelines, [&frame](DrmPipeline *pipeline) { + // always require a modeset for turning off displays, it makes other logic easier to follow + const bool oldActive = pipeline->m_next.enabled && pipeline->m_next.active; + if (oldActive && !pipeline->activePending()) { + return false; + } + auto commit = std::make_unique(QVector{pipeline}); + return pipeline->prepareAtomicCommit(commit.get(), CommitMode::TestAllowModeset, frame) == Error::None && commit->test(); + }); + for (DrmPipeline *pipeline : pipelines) { + pipeline->m_pending.needsModeset = !withoutModeset; + pipeline->m_pending.needsModesetProperties = true; + } + return Error::None; + } + case CommitMode::CommitModeset: { + // The kernel fails commits with DRM_MODE_PAGE_FLIP_EVENT when a crtc is disabled in the commit + // and already was disabled before, to work around some quirks in old userspace. + // Instead of using DRM_MODE_PAGE_FLIP_EVENT | DRM_MODE_ATOMIC_NONBLOCK, do the modeset in a blocking + // fashion without page flip events and trigger the pageflip notification directly + if (!commit->commitModeset()) { + qCCritical(KWIN_DRM) << "Atomic modeset commit failed!" << strerror(errno); + return errnoToError(); + } + for (const auto pipeline : pipelines) { + pipeline->m_next.needsModeset = pipeline->m_pending.needsModeset = false; + } + commit->pageFlipped(std::chrono::steady_clock::now().time_since_epoch()); + return Error::None; + } + case CommitMode::Test: { + if (!commit->test()) { + return errnoToError(); + } + return Error::None; + } + default: + Q_UNREACHABLE(); + } +} + +DrmPipeline::Error DrmPipeline::prepareAtomicCommit(DrmAtomicCommit *commit, CommitMode mode, const std::shared_ptr &frame) +{ + if (activePending()) { + if (Error err = prepareAtomicPresentation(commit, frame); err != Error::None) { + return err; + } + for (const auto &layer : m_pending.layers) { + if (Error err = prepareAtomicPlane(commit, layer->plane(), layer, frame); err != Error::None) { + return err; + } + } + if (mode == CommitMode::TestAllowModeset || mode == CommitMode::CommitModeset || m_pending.needsModesetProperties) { + if (!prepareAtomicModeset(commit)) { + return Error::InvalidArguments; + } + } + } else { + prepareAtomicDisable(commit); + } + return Error::None; +} + +DrmPipeline::Error DrmPipeline::prepareAtomicPresentation(DrmAtomicCommit *commit, const std::shared_ptr &frame) +{ + commit->setPresentationMode(m_pending.presentationMode); + if (m_connector->contentType.isValid()) { + commit->addEnum(m_connector->contentType, m_pending.contentType); + } + + if (m_pending.crtc->vrrEnabled.isValid()) { + commit->setVrr(m_pending.crtc, m_pending.presentationMode == PresentationMode::AdaptiveSync || m_pending.presentationMode == PresentationMode::AdaptiveAsync); + } + + const ColorPipeline &colorPipeline = m_pending.crtcColorPipeline; + if (!m_pending.crtc->postBlendingPipeline) { + if (!colorPipeline.isIdentity()) { + return Error::InvalidArguments; + } + } else { + if (!m_pending.crtc->postBlendingPipeline->matchPipeline(commit, colorPipeline)) { + return Error::InvalidArguments; + } + } + + return Error::None; +} + +DrmPipeline::Error DrmPipeline::prepareAtomicPlane(DrmAtomicCommit *commit, DrmPlane *plane, DrmPipelineLayer *layer, const std::shared_ptr &frame) +{ + if (!layer->isEnabled()) { + plane->disable(commit); + return Error::None; + } + if (!layer->currentBuffer()) { + qCWarning(KWIN_DRM) << "An enabled plane has no buffer!"; + return Error::TestBufferFailed; + } + const auto fb = layer->currentBuffer(); + if (!fb) { + return Error::InvalidArguments; + } + const auto transform = layer->offloadTransform(); + const auto planeTransform = DrmPlane::outputTransformToPlaneTransform(transform); + if (plane->rotation.isValid()) { + if (!plane->rotation.hasEnum(planeTransform)) { + return Error::InvalidArguments; + } + commit->addEnum(plane->rotation, planeTransform); + } else if (planeTransform != DrmPlane::Transformation::Rotate0) { + return Error::InvalidArguments; + } + commit->addProperty(plane->crtcId, m_pending.crtc->id()); + commit->addBuffer(plane, fb, frame); + plane->set(commit, layer->sourceRect().toRect(), layer->targetRect()); + if (plane->vmHotspotX.isValid() && plane->vmHotspotY.isValid()) { + commit->addProperty(plane->vmHotspotX, std::round(layer->hotspot().x())); + commit->addProperty(plane->vmHotspotY, std::round(layer->hotspot().y())); + } + + if (plane->alpha.isValid()) { + commit->addProperty(plane->alpha, plane->alpha.maxValue()); + } + if (plane->pixelBlendMode.isValid()) { + commit->addEnum(plane->pixelBlendMode, DrmPlane::PixelBlendMode::PreMultiplied); + } + if (plane->zpos.isValid() && !plane->zpos.isImmutable()) { + commit->addProperty(plane->zpos, layer->zpos()); + } + + const auto colorPipelines = plane->colorPipelines(); + if (layer->colorPipeline().isIdentity()) { + if (plane->colorPipeline.isValid()) { + commit->addProperty(plane->colorPipeline, 0); + } + } else { + const auto it = std::ranges::find_if(colorPipelines, [&](DrmColorOp *pipeline) { + return pipeline->colorOp()->matchPipeline(commit, layer->colorPipeline()); + }); + if (it == colorPipelines.end()) { + // TODO re-allow merging with post-blending pipeline + return DrmPipeline::Error::InvalidArguments; + } + commit->addProperty(plane->colorPipeline, (*it)->id()); + } + + if (plane->colorPipeline.isValid() && layer->colorDescription()->yuvCoefficients() != YUVMatrixCoefficients::Identity) { + // color pipelines don't support the color encoding and color range properties yet + return Error::InvalidArguments; + } + DrmPlane::ColorRange range = DrmPlane::ColorRange::Limited_YCbCr; + if (layer->colorDescription()->range() == EncodingRange::Full) { + range = DrmPlane::ColorRange::Full_YCbCr; + } + switch (layer->colorDescription()->yuvCoefficients()) { + case YUVMatrixCoefficients::Identity: + if (layer->colorDescription()->range() == EncodingRange::Limited) { + return Error::InvalidArguments; + } + // Workaround for the proprietary NVIDIA driver, which defaults to + // limited color range (16-235) when COLOR_RANGE is not explicitly + // set, even for RGB content. This makes the desktop look washed out. + // See https://github.com/NVIDIA/open-gpu-kernel-modules/discussions/1105 + if (gpu()->isNVidia() && plane->colorRange.isValid()) { + commit->addEnum(plane->colorRange, DrmPlane::ColorRange::Full_YCbCr); + } + break; + case YUVMatrixCoefficients::BT601: + if (!plane->colorEncoding.isValid() || !plane->colorRange.isValid()) { + return Error::InvalidArguments; + } + commit->addEnum(plane->colorEncoding, DrmPlane::ColorEncoding::BT601_YCbCr); + commit->addEnum(plane->colorRange, range); + break; + case YUVMatrixCoefficients::BT709: + if (!plane->colorEncoding.isValid() || !plane->colorRange.isValid()) { + return Error::InvalidArguments; + } + commit->addEnum(plane->colorEncoding, DrmPlane::ColorEncoding::BT709_YCbCr); + commit->addEnum(plane->colorRange, range); + break; + case YUVMatrixCoefficients::BT2020: + if (!plane->colorEncoding.isValid() || !plane->colorRange.isValid()) { + return Error::InvalidArguments; + } + commit->addEnum(plane->colorEncoding, DrmPlane::ColorEncoding::BT2020_YCbCr); + commit->addEnum(plane->colorRange, range); + break; + } + return Error::None; +} + +void DrmPipeline::prepareAtomicDisable(DrmAtomicCommit *commit) +{ + m_connector->disable(commit); + if (m_pending.crtc) { + m_pending.crtc->disable(commit); + for (const auto layer : m_pending.layers) { + if (DrmPlane *plane = layer->plane()) { + plane->disable(commit); + } + } + } +} + +static const auto s_forceScalingMode = []() -> std::optional { + const auto env = qEnvironmentVariable("KWIN_DRM_FORCE_SCALING_MODE"); + if (env == "NONE") { + return DrmConnector::ScalingMode::None; + } else if (env == "FULL") { + return DrmConnector::ScalingMode::Full; + } else if (env == "CENTER") { + return DrmConnector::ScalingMode::Center; + } else if (env == "FULL_ASPECT") { + return DrmConnector::ScalingMode::Full_Aspect; + } else { + return std::nullopt; + } +}(); + +bool DrmPipeline::prepareAtomicModeset(DrmAtomicCommit *commit) +{ + commit->addProperty(m_connector->crtcId, m_pending.crtc->id()); + if (m_connector->broadcastRGB.isValid()) { + commit->addEnum(m_connector->broadcastRGB, DrmConnector::rgbRangeToBroadcastRgb(m_pending.rgbRange)); + } + if (m_connector->linkStatus.isValid()) { + commit->addEnum(m_connector->linkStatus, DrmConnector::LinkStatus::Good); + } + if (m_connector->overscan.isValid()) { + commit->addProperty(m_connector->overscan, m_pending.overscan); + } else if (m_connector->underscan.isValid()) { + const uint32_t hborder = calculateUnderscan(); + commit->addEnum(m_connector->underscan, m_pending.overscan != 0 ? DrmConnector::UnderscanOptions::On : DrmConnector::UnderscanOptions::Off); + commit->addProperty(m_connector->underscanVBorder, m_pending.overscan); + commit->addProperty(m_connector->underscanHBorder, hborder); + } + if (m_connector->maxBpc.isValid()) { + commit->addProperty(m_connector->maxBpc, std::clamp(m_pending.maxBpc, m_connector->maxBpc.minValue(), m_connector->maxBpc.maxValue())); + } + if (m_connector->hdrMetadata.isValid()) { + commit->addBlob(m_connector->hdrMetadata, createHdrMetadata(m_pending.hdr ? TransferFunction::PerceptualQuantizer : TransferFunction::gamma22)); + } else if (m_pending.hdr) { + return false; + } + if (m_pending.wcg) { + if (!m_connector->colorspace.isValid() || !m_connector->colorspace.hasEnum(DrmConnector::Colorspace::BT2020_RGB)) { + return false; + } + commit->addEnum(m_connector->colorspace, DrmConnector::Colorspace::BT2020_RGB); + } else if (m_connector->colorspace.isValid()) { + commit->addEnum(m_connector->colorspace, DrmConnector::Colorspace::Default); + } + if (m_connector->scalingMode.isValid()) { + if (s_forceScalingMode.has_value()) { + if (m_connector->scalingMode.hasEnum(*s_forceScalingMode)) { + commit->addEnum(m_connector->scalingMode, *s_forceScalingMode); + } else if (m_connector->scalingMode.hasEnum(DrmConnector::ScalingMode::None)) { + commit->addEnum(m_connector->scalingMode, DrmConnector::ScalingMode::None); + } + } else if (m_connector->isInternal() + && m_connector->scalingMode.hasEnum(DrmConnector::ScalingMode::Full_Aspect) + && (m_pending.mode->flags() & OutputMode::Flag::Generated) + && !(m_pending.mode->flags() & OutputMode::Flag::Custom)) { + commit->addEnum(m_connector->scalingMode, DrmConnector::ScalingMode::Full_Aspect); + } else if (m_connector->scalingMode.hasEnum(DrmConnector::ScalingMode::None)) { + commit->addEnum(m_connector->scalingMode, DrmConnector::ScalingMode::None); + } + } + + commit->addProperty(m_pending.crtc->active, 1); + commit->addBlob(m_pending.crtc->modeId, m_pending.mode->blob()); + if (m_pending.crtc->degammaLut.isValid()) { + commit->addProperty(m_pending.crtc->degammaLut, 0); + } + + if (m_pending.crtc->sharpnessStrength.isValid()) { + const int maxValue = m_pending.crtc->sharpnessStrength.maxValue(); + const int sharpness = std::clamp(std::round(m_output->nextState().sharpnessSetting * maxValue), 0, maxValue); + commit->addProperty(m_pending.crtc->sharpnessStrength, sharpness); + } + + return true; +} + +uint32_t DrmPipeline::calculateUnderscan() +{ + const auto size = m_pending.mode->size(); + const float aspectRatio = size.width() / static_cast(size.height()); + uint32_t hborder = m_pending.overscan * aspectRatio; + if (hborder > 128) { + // overscan only goes from 0-100 so we cut off the 101-128 value range of underscan_vborder + hborder = 128; + m_pending.overscan = 128 / aspectRatio; + } + return hborder; +} + +DrmPipeline::Error DrmPipeline::errnoToError() +{ + switch (errno) { + case EINVAL: + return Error::InvalidArguments; + case EBUSY: + return Error::FramePending; + case ENOMEM: + return Error::OutofMemory; + case EACCES: + return Error::NoPermission; + default: + return Error::Unknown; + } +} + +bool DrmPipeline::presentAsync(OutputLayer *layer, std::optional allowedVrrDelay) +{ + if (needsModeset() || !m_pending.crtc || !m_pending.active) { + return false; + } + // We need to make sure that on vmwgfx software cursor is selected + // until Broadcom fixes hw cursor issues with vmwgfx. Otherwise + // the cursor is missing. + if (gpu()->isVmwgfx()) { + return false; + } + const auto drmLayer = static_cast(layer); + if (drmLayer->plane()) { + // test the full state, to take pending commits into account + if (DrmPipeline::commitPipelinesAtomic({this}, CommitMode::Test, nullptr, {}) != Error::None) { + return false; + } + // only give the actual state update to the commit thread, so that it can potentially reorder the commits + auto partialUpdate = std::make_unique(QList{this}); + prepareAtomicPlane(partialUpdate.get(), drmLayer->plane(), drmLayer, nullptr); + partialUpdate->setAllowedVrrDelay(allowedVrrDelay); + m_commitThread->addCommit(std::move(partialUpdate)); + return true; + } else { + return setCursorLegacy(drmLayer); + } +} + +void DrmPipeline::applyPendingChanges() +{ + m_next = m_pending; + m_commitThread->setModeInfo(m_pending.mode->refreshRate(), m_pending.mode->vblankTime()); + m_output->renderLoop()->setPresentationSafetyMargin(m_commitThread->safetyMargin()); + m_output->renderLoop()->setRefreshRate(m_pending.mode->refreshRate()); +} + +DrmConnector *DrmPipeline::connector() const +{ + return m_connector; +} + +DrmGpu *DrmPipeline::gpu() const +{ + return m_connector->gpu(); +} + +void DrmPipeline::pageFlipped(std::chrono::nanoseconds timestamp) +{ + RenderLoopPrivate::get(m_output->renderLoop())->notifyVblank(timestamp); + m_commitThread->pageFlipped(timestamp); + // the commit thread adjusts the safety margin on every commit + m_output->renderLoop()->setPresentationSafetyMargin(m_commitThread->safetyMargin()); + m_output->maybeUpdateDpmsState(); + if (gpu()->needsModeset()) { + gpu()->maybeModeset(nullptr, nullptr); + } +} + +void DrmPipeline::setOutput(DrmOutput *output) +{ + m_output = output; + for (const auto layer : m_pending.layers) { + layer->setOutput(output); + } +} + +DrmOutput *DrmPipeline::output() const +{ + return m_output; +} + +bool DrmPipeline::needsModeset() const +{ + return m_pending.needsModeset; +} + +bool DrmPipeline::activePending() const +{ + return m_pending.crtc && m_pending.mode && m_pending.active; +} + +void DrmPipeline::revertPendingChanges() +{ + m_pending = m_next; +} + +DrmCommitThread *DrmPipeline::commitThread() const +{ + return m_commitThread.get(); +} + +bool DrmPipeline::modesetPresentPending() const +{ + return m_modesetPresentPending; +} + +void DrmPipeline::resetModesetPresentPending() +{ + m_modesetPresentPending = false; +} + +DrmCrtc *DrmPipeline::crtc() const +{ + return m_pending.crtc; +} + +std::shared_ptr DrmPipeline::mode() const +{ + return m_pending.mode; +} + +bool DrmPipeline::active() const +{ + return m_pending.active; +} + +bool DrmPipeline::enabled() const +{ + return m_pending.enabled; +} + +QList DrmPipeline::layers() const +{ + return m_pending.layers; +} + +PresentationMode DrmPipeline::presentationMode() const +{ + return m_pending.presentationMode; +} + +uint32_t DrmPipeline::overscan() const +{ + return m_pending.overscan; +} + +BackendOutput::RgbRange DrmPipeline::rgbRange() const +{ + return m_pending.rgbRange; +} + +DrmConnector::DrmContentType DrmPipeline::contentType() const +{ + return m_pending.contentType; +} + +const std::shared_ptr &DrmPipeline::iccProfile() const +{ + return m_pending.iccProfile; +} + +void DrmPipeline::setCrtc(DrmCrtc *crtc) +{ + m_pending.crtc = crtc; +} + +void DrmPipeline::setMode(const std::shared_ptr &mode) +{ + m_pending.mode = mode; +} + +void DrmPipeline::setActive(bool active) +{ + m_pending.active = active; +} + +void DrmPipeline::setEnable(bool enable) +{ + m_pending.enabled = enable; +} + +void DrmPipeline::setLayers(const QList &layers) +{ + m_pending.layers = layers; + for (const auto layer : layers) { + layer->setOutput(m_output); + } +} + +void DrmPipeline::setPresentationMode(PresentationMode mode) +{ + m_pending.presentationMode = mode; +} + +void DrmPipeline::setOverscan(uint32_t overscan) +{ + m_pending.overscan = overscan; +} + +void DrmPipeline::setRgbRange(BackendOutput::RgbRange range) +{ + m_pending.rgbRange = range; +} + +void DrmPipeline::setCrtcColorPipeline(const ColorPipeline &pipeline) +{ + m_pending.crtcColorPipeline = pipeline; +} + +void DrmPipeline::setHighDynamicRange(bool hdr) +{ + m_pending.hdr = hdr; +} + +void DrmPipeline::setWideColorGamut(bool wcg) +{ + m_pending.wcg = wcg; +} + +void DrmPipeline::setMaxBpc(uint32_t max) +{ + m_pending.maxBpc = max; +} + +void DrmPipeline::setContentType(DrmConnector::DrmContentType type) +{ + m_pending.contentType = type; +} + +void DrmPipeline::setIccProfile(const std::shared_ptr &profile) +{ + m_pending.iccProfile = profile; +} + +std::shared_ptr DrmPipeline::createHdrMetadata(TransferFunction::Type transferFunction) const +{ + if (transferFunction != TransferFunction::PerceptualQuantizer) { + // for sRGB / gamma 2.2, don't send any metadata, to ensure the non-HDR experience stays the same + return nullptr; + } + if (!m_connector->edid()->supportsPQ()) { + return nullptr; + } + const auto colorimetry = m_connector->edid()->nativeColorimetry().value_or(Colorimetry::BT709); + const xyY red = colorimetry.red().toxyY(); + const xyY green = colorimetry.green().toxyY(); + const xyY blue = colorimetry.blue().toxyY(); + const xyY white = colorimetry.white().toxyY(); + const auto to16Bit = [](float value) { + return uint16_t(std::round(value / 0.00002)); + }; + hdr_output_metadata data{ + .metadata_type = 0, + .hdmi_metadata_type1 = hdr_metadata_infoframe{ + // eotf types (from CTA-861-G page 85): + // - 0: traditional gamma, SDR + // - 1: traditional gamma, HDR + // - 2: SMPTE ST2084 + // - 3: hybrid Log-Gamma based on BT.2100-0 + // - 4-7: reserved + .eotf = uint8_t(2), + // there's only one type. 1-7 are reserved for future use + .metadata_type = 0, + // in 0.00002 nits + .display_primaries = { + {to16Bit(red.x), to16Bit(red.y)}, + {to16Bit(green.x), to16Bit(green.y)}, + {to16Bit(blue.x), to16Bit(blue.y)}, + }, + .white_point = {to16Bit(white.x), to16Bit(white.y)}, + // in nits + .max_display_mastering_luminance = uint16_t(std::round(m_connector->edid()->desiredMaxFrameAverageLuminance().value_or(0))), + // in 0.0001 nits + .min_display_mastering_luminance = uint16_t(std::round(m_connector->edid()->desiredMinLuminance() * 10000)), + // in nits + .max_cll = uint16_t(std::round(m_connector->edid()->desiredMaxFrameAverageLuminance().value_or(0))), + .max_fall = uint16_t(std::round(m_connector->edid()->desiredMaxFrameAverageLuminance().value_or(0))), + }, + }; + return DrmBlob::create(gpu(), &data, sizeof(data)); +} + +std::chrono::nanoseconds DrmPipeline::presentationDeadline() const +{ + return m_commitThread->safetyMargin(); +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_pipeline.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_pipeline.h new file mode 100644 index 0000000000..8dace83ae9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_pipeline.h @@ -0,0 +1,177 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2021 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include +#include + +#include "core/colorpipeline.h" +#include "core/colorspace.h" +#include "core/output.h" +#include "core/renderloop_p.h" +#include "drm_blob.h" +#include "drm_connector.h" +#include "drm_plane.h" + +namespace KWin +{ + +class DrmGpu; +class DrmConnector; +class DrmCrtc; +class DrmConnectorMode; +class DrmPipelineLayer; +class DrmCommitThread; +class OutputFrame; + +class DrmPipeline +{ +public: + DrmPipeline(DrmConnector *conn); + ~DrmPipeline(); + + enum class Error { + None, + OutofMemory, + InvalidArguments, + NoPermission, + FramePending, + TestBufferFailed, + NotEnoughCrtcs, + Timeout, + Unknown, + }; + Q_ENUM(Error) + + /** + * tests the pending commit first and commits it if the test passes + * if the test fails, there is a guarantee for no lasting changes + */ + Error present(const QList &layersToUpdate, const std::shared_ptr &frame); + Error testPresent(const std::shared_ptr &frame); + void maybeModeset(const std::shared_ptr &frame); + void forceLegacyModeset(); + + bool needsModeset() const; + void applyPendingChanges(); + void revertPendingChanges(); + + bool presentAsync(OutputLayer *layer, std::optional allowedVrrDelay); + + DrmConnector *connector() const; + DrmGpu *gpu() const; + + void pageFlipped(std::chrono::nanoseconds timestamp); + bool modesetPresentPending() const; + void resetModesetPresentPending(); + DrmCommitThread *commitThread() const; + + void setOutput(DrmOutput *output); + DrmOutput *output() const; + + QList layers() const; + void setLayers(const QList &layers); + std::chrono::nanoseconds presentationDeadline() const; + + DrmCrtc *crtc() const; + std::shared_ptr mode() const; + bool active() const; + bool activePending() const; + bool enabled() const; + PresentationMode presentationMode() const; + uint32_t overscan() const; + BackendOutput::RgbRange rgbRange() const; + DrmConnector::DrmContentType contentType() const; + const std::shared_ptr &iccProfile() const; + + void setCrtc(DrmCrtc *crtc); + void setMode(const std::shared_ptr &mode); + void setActive(bool active); + void setEnable(bool enable); + void setPresentationMode(PresentationMode mode); + void setOverscan(uint32_t overscan); + void setRgbRange(BackendOutput::RgbRange range); + void setCrtcColorPipeline(const ColorPipeline &pipeline); + void setContentType(DrmConnector::DrmContentType type); + void setIccProfile(const std::shared_ptr &profile); + void setHighDynamicRange(bool hdr); + void setWideColorGamut(bool wcg); + void setMaxBpc(uint32_t max); + + enum class CommitMode { + Test, + TestAllowModeset, + CommitModeset + }; + Q_ENUM(CommitMode) + static Error commitPipelines(const QList &pipelines, CommitMode mode, const QList &unusedObjects = {}); + +private: + bool isBufferForDirectScanout() const; + uint32_t calculateUnderscan(); + static Error errnoToError(); + std::shared_ptr createHdrMetadata(TransferFunction::Type transferFunction) const; + + // legacy only + Error presentLegacy(const QList &layersToUpdate, const std::shared_ptr &frame); + Error legacyModeset(); + Error setLegacyGamma(); + Error applyPendingChangesLegacy(); + bool setCursorLegacy(DrmPipelineLayer *layer); + static Error commitPipelinesLegacy(const QList &pipelines, CommitMode mode, const QList &unusedObjects); + + // atomic modesetting only + Error prepareAtomicCommit(DrmAtomicCommit *commit, CommitMode mode, const std::shared_ptr &frame); + bool prepareAtomicModeset(DrmAtomicCommit *commit); + Error prepareAtomicPresentation(DrmAtomicCommit *commit, const std::shared_ptr &frame); + Error prepareAtomicPlane(DrmAtomicCommit *commit, DrmPlane *plane, DrmPipelineLayer *layer, const std::shared_ptr &frame); + void prepareAtomicDisable(DrmAtomicCommit *commit); + static Error commitPipelinesAtomic(const QList &pipelines, CommitMode mode, const std::shared_ptr &frame, const QList &unusedObjects); + + DrmOutput *m_output = nullptr; + DrmConnector *m_connector = nullptr; + + bool m_modesetPresentPending = false; + ColorPipeline m_currentLegacyGamma; + + struct State + { + DrmCrtc *crtc = nullptr; + bool active = true; // whether or not the pipeline should be currently used + bool enabled = true; // whether or not the pipeline needs a crtc + bool needsModeset = false; + bool needsModesetProperties = false; + std::shared_ptr mode; + uint32_t overscan = 0; + BackendOutput::RgbRange rgbRange = BackendOutput::RgbRange::Automatic; + PresentationMode presentationMode = PresentationMode::VSync; + ColorPipeline crtcColorPipeline; + DrmConnector::DrmContentType contentType = DrmConnector::DrmContentType::Graphics; + + std::shared_ptr iccProfile; + bool hdr = false; + bool wcg = false; + uint32_t maxBpc = 10; + + QList layers; + }; + // the state that is to be tested next + State m_pending; + // the state that will be applied at the next real atomic commit + State m_next; + + std::unique_ptr m_commitThread; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_pipeline_legacy.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_pipeline_legacy.cpp new file mode 100644 index 0000000000..83b748d5c0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_pipeline_legacy.cpp @@ -0,0 +1,250 @@ +/* + * KWin - the KDE window manager + * This file is part of the KDE project. + * + * SPDX-FileCopyrightText: 2021 Xaver Hugl + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "core/graphicsbuffer.h" +#include "drm_buffer.h" +#include "drm_commit.h" +#include "drm_commit_thread.h" +#include "drm_connector.h" +#include "drm_crtc.h" +#include "drm_gpu.h" +#include "drm_layer.h" +#include "drm_logging.h" +#include "drm_output.h" +#include "drm_pipeline.h" + +#include +#include + +namespace KWin +{ + +static DrmPipelineLayer *findLayer(const auto &layers, OutputLayerType type) +{ + const auto it = std::ranges::find_if(layers, [type](OutputLayer *layer) { + return layer->type() == type; + }); + return it == layers.end() ? nullptr : static_cast(*it); +} + +DrmPipeline::Error DrmPipeline::presentLegacy(const QList &layersToUpdate, const std::shared_ptr &frame) +{ + if (Error err = applyPendingChangesLegacy(); err != Error::None) { + return err; + } + if (auto cursor = findLayer(layersToUpdate, OutputLayerType::CursorOnly); cursor && !setCursorLegacy(cursor)) { + return Error::InvalidArguments; + } + // always present on the crtc, for presentation feedback + const auto primary = findLayer(m_pending.layers, OutputLayerType::Primary); + const auto buffer = primary->currentBuffer(); + if (primary->sourceRect() != primary->targetRect() || primary->targetRect() != Rect(QPoint(0, 0), buffer->buffer()->size())) { + return Error::InvalidArguments; + } + auto commit = std::make_unique(this, buffer, frame); + if (!commit->doPageflip(m_pending.presentationMode)) { + qCDebug(KWIN_DRM) << "Page flip failed:" << strerror(errno); + return errnoToError(); + } + m_commitThread->setPendingCommit(std::move(commit)); + return Error::None; +} + +void DrmPipeline::forceLegacyModeset() +{ + if (activePending()) { + legacyModeset(); + setLegacyGamma(); + } +} + +DrmPipeline::Error DrmPipeline::legacyModeset() +{ + const auto primary = findLayer(m_pending.layers, OutputLayerType::Primary); + const auto buffer = primary->currentBuffer(); + if (!buffer) { + return Error::InvalidArguments; + } + if (primary->sourceRect() != RectF(QPoint(0, 0), buffer->buffer()->size())) { + return Error::InvalidArguments; + } + auto commit = std::make_unique(this, buffer, nullptr); + if (!commit->doModeset(m_connector, m_pending.mode.get())) { + qCWarning(KWIN_DRM) << "Modeset failed!" << strerror(errno); + return errnoToError(); + } + return Error::None; +} + +DrmPipeline::Error DrmPipeline::commitPipelinesLegacy(const QList &pipelines, CommitMode mode, const QList &unusedObjects) +{ + Error err = Error::None; + for (DrmPipeline *pipeline : pipelines) { + err = pipeline->applyPendingChangesLegacy(); + if (err != Error::None) { + break; + } + } + if (err != Error::None) { + // at least try to revert the config + for (DrmPipeline *pipeline : pipelines) { + pipeline->revertPendingChanges(); + pipeline->applyPendingChangesLegacy(); + } + } else { + for (DrmPipeline *pipeline : pipelines) { + pipeline->applyPendingChanges(); + if (mode == CommitMode::CommitModeset && pipeline->activePending()) { + pipeline->pageFlipped(std::chrono::steady_clock::now().time_since_epoch()); + } + } + for (DrmObject *obj : unusedObjects) { + if (auto crtc = dynamic_cast(obj)) { + drmModeSetCrtc(pipelines.front()->gpu()->fd(), crtc->id(), 0, 0, 0, nullptr, 0, nullptr); + } + } + } + return err; +} + +DrmPipeline::Error DrmPipeline::applyPendingChangesLegacy() +{ + if (!m_pending.active && m_pending.crtc) { + drmModeSetCursor(gpu()->fd(), m_pending.crtc->id(), 0, 0, 0); + } + if (activePending()) { + const bool colorTransforms = std::ranges::any_of(m_pending.layers, [](DrmPipelineLayer *layer) { + return !layer->colorPipeline().isIdentity(); + }); + if (colorTransforms) { + // while it's technically possible to set CRTC color management properties, + // it may result in glitches + return DrmPipeline::Error::InvalidArguments; + } + const bool shouldEnableVrr = m_pending.presentationMode == PresentationMode::AdaptiveSync || m_pending.presentationMode == PresentationMode::AdaptiveAsync; + if (m_pending.crtc->vrrEnabled.isValid() && !m_pending.crtc->vrrEnabled.setPropertyLegacy(shouldEnableVrr)) { + qCWarning(KWIN_DRM) << "Setting vrr failed!" << strerror(errno); + return errnoToError(); + } + if (m_connector->broadcastRGB.isValid()) { + m_connector->broadcastRGB.setEnumLegacy(DrmConnector::rgbRangeToBroadcastRgb(m_pending.rgbRange)); + } + if (m_connector->overscan.isValid()) { + m_connector->overscan.setPropertyLegacy(m_pending.overscan); + } else if (m_connector->underscan.isValid()) { + const uint32_t hborder = calculateUnderscan(); + m_connector->underscan.setEnumLegacy(m_pending.overscan != 0 ? DrmConnector::UnderscanOptions::On : DrmConnector::UnderscanOptions::Off); + m_connector->underscanVBorder.setPropertyLegacy(m_pending.overscan); + m_connector->underscanHBorder.setPropertyLegacy(hborder); + } + if (m_connector->scalingMode.isValid() && m_connector->scalingMode.hasEnum(DrmConnector::ScalingMode::None)) { + m_connector->scalingMode.setEnumLegacy(DrmConnector::ScalingMode::None); + } + if (m_connector->hdrMetadata.isValid()) { + const auto blob = createHdrMetadata(m_pending.hdr ? TransferFunction::PerceptualQuantizer : TransferFunction::gamma22); + m_connector->hdrMetadata.setPropertyLegacy(blob ? blob->blobId() : 0); + } else if (m_pending.hdr) { + return DrmPipeline::Error::InvalidArguments; + } + if (m_connector->colorspace.isValid()) { + m_connector->colorspace.setEnumLegacy(m_pending.wcg ? DrmConnector::Colorspace::BT2020_RGB : DrmConnector::Colorspace::Default); + } else if (m_pending.wcg) { + return DrmPipeline::Error::InvalidArguments; + } + const auto currentModeContent = m_pending.crtc->queryCurrentMode(); + if (m_pending.crtc != m_next.crtc || *m_pending.mode != currentModeContent) { + qCDebug(KWIN_DRM) << "Using legacy path to set mode" << m_pending.mode->nativeMode()->name; + Error err = legacyModeset(); + if (err != Error::None) { + return err; + } + } + if (m_pending.crtcColorPipeline != m_currentLegacyGamma) { + if (Error err = setLegacyGamma(); err != Error::None) { + return err; + } + } + if (m_connector->contentType.isValid()) { + m_connector->contentType.setEnumLegacy(m_pending.contentType); + } + if (m_connector->maxBpc.isValid()) { + m_connector->maxBpc.setPropertyLegacy(8); + } + setCursorLegacy(findLayer(m_pending.layers, OutputLayerType::CursorOnly)); + } + if (!m_connector->dpms.setPropertyLegacy(activePending() ? DRM_MODE_DPMS_ON : DRM_MODE_DPMS_OFF)) { + qCWarning(KWIN_DRM) << "Setting legacy dpms failed!" << strerror(errno); + return errnoToError(); + } + return Error::None; +} + +DrmPipeline::Error DrmPipeline::setLegacyGamma() +{ + QList red(m_pending.crtc->gammaRampSize()); + QList green(m_pending.crtc->gammaRampSize()); + QList blue(m_pending.crtc->gammaRampSize()); + for (int i = 0; i < m_pending.crtc->gammaRampSize(); i++) { + const double input = i / double(m_pending.crtc->gammaRampSize() - 1); + QVector3D output = QVector3D(input, input, input); + for (const auto &op : m_pending.crtcColorPipeline.ops) { + if (auto tf = std::get_if(&op.operation)) { + output = tf->tf.encodedToNits(output); + } else if (auto tf = std::get_if(&op.operation)) { + output = tf->tf.nitsToEncoded(output); + } else if (auto mult = std::get_if(&op.operation)) { + output *= mult->factors; + } else { + // not supported + return Error::InvalidArguments; + } + } + red[i] = std::clamp(output.x(), 0.0f, 1.0f) * std::numeric_limits::max(); + green[i] = std::clamp(output.y(), 0.0f, 1.0f) * std::numeric_limits::max(); + blue[i] = std::clamp(output.z(), 0.0f, 1.0f) * std::numeric_limits::max(); + } + if (drmModeCrtcSetGamma(gpu()->fd(), m_pending.crtc->id(), m_pending.crtc->gammaRampSize(), red.data(), green.data(), blue.data()) != 0) { + qCWarning(KWIN_DRM) << "Setting gamma failed!" << strerror(errno); + return errnoToError(); + } + m_currentLegacyGamma = m_pending.crtcColorPipeline; + return DrmPipeline::Error::None; +} + +bool DrmPipeline::setCursorLegacy(DrmPipelineLayer *layer) +{ + const auto bo = layer->currentBuffer(); + uint32_t handle = 0; + if (bo && bo->buffer() && layer->isEnabled()) { + const DmaBufAttributes *attributes = bo->buffer()->dmabufAttributes(); + if (drmPrimeFDToHandle(gpu()->fd(), attributes->fd[0].get(), &handle) != 0) { + qCWarning(KWIN_DRM) << "drmPrimeFDToHandle() failed"; + return false; + } + } + + struct drm_mode_cursor2 arg = { + .flags = DRM_MODE_CURSOR_BO | DRM_MODE_CURSOR_MOVE, + .crtc_id = m_pending.crtc->id(), + .x = int32_t(layer->targetRect().x()), + .y = int32_t(layer->targetRect().y()), + .width = (uint32_t)gpu()->cursorSize().width(), + .height = (uint32_t)gpu()->cursorSize().height(), + .handle = handle, + .hot_x = int32_t(layer->hotspot().x()), + .hot_y = int32_t(layer->hotspot().y()), + }; + const int ret = drmIoctl(gpu()->fd(), DRM_IOCTL_MODE_CURSOR2, &arg); + + if (handle != 0) { + drmCloseBufferHandle(gpu()->fd(), handle); + } + return ret == 0; +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_plane.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_plane.cpp new file mode 100644 index 0000000000..41e5f9861c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_plane.cpp @@ -0,0 +1,309 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Roman Gilg + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_plane.h" + +#include "config-kwin.h" + +#include "drm_buffer.h" +#include "drm_commit.h" +#include "drm_gpu.h" +#include "drm_logging.h" +#include "drm_pointer.h" +#include "utils/drm_format_helper.h" + +#include +#include +#include + +namespace KWin +{ + +DrmPlane::DrmPlane(DrmGpu *gpu, uint32_t planeId) + : DrmObject(gpu, planeId, DRM_MODE_OBJECT_PLANE) + , type(this, QByteArrayLiteral("type"), { + QByteArrayLiteral("Overlay"), + QByteArrayLiteral("Primary"), + QByteArrayLiteral("Cursor"), + }) + , srcX(this, QByteArrayLiteral("SRC_X")) + , srcY(this, QByteArrayLiteral("SRC_Y")) + , srcW(this, QByteArrayLiteral("SRC_W")) + , srcH(this, QByteArrayLiteral("SRC_H")) + , crtcX(this, QByteArrayLiteral("CRTC_X")) + , crtcY(this, QByteArrayLiteral("CRTC_Y")) + , crtcW(this, QByteArrayLiteral("CRTC_W")) + , crtcH(this, QByteArrayLiteral("CRTC_H")) + , fbId(this, QByteArrayLiteral("FB_ID")) + , crtcId(this, QByteArrayLiteral("CRTC_ID")) + , rotation(this, QByteArrayLiteral("rotation"), { + QByteArrayLiteral("rotate-0"), + QByteArrayLiteral("rotate-90"), + QByteArrayLiteral("rotate-180"), + QByteArrayLiteral("rotate-270"), + QByteArrayLiteral("reflect-x"), + QByteArrayLiteral("reflect-y"), + }) + , inFormats(this, QByteArrayLiteral("IN_FORMATS")) + , alpha(this, QByteArrayLiteral("alpha")) + , pixelBlendMode(this, QByteArrayLiteral("pixel blend mode"), { + QByteArrayLiteral("None"), + QByteArrayLiteral("Pre-multiplied"), + QByteArrayLiteral("Coverage"), + }) + , colorEncoding(this, QByteArrayLiteral("COLOR_ENCODING"), { + QByteArrayLiteral("ITU-R BT.601 YCbCr"), + QByteArrayLiteral("ITU-R BT.709 YCbCr"), + QByteArrayLiteral("ITU-R BT.2020 YCbCr"), + }) + , colorRange(this, QByteArrayLiteral("COLOR_RANGE"), { + QByteArrayLiteral("YCbCr limited range"), + QByteArrayLiteral("YCbCr full range"), + }) + , vmHotspotX(this, QByteArrayLiteral("HOTSPOT_X")) + , vmHotspotY(this, QByteArrayLiteral("HOTSPOT_Y")) + , inFenceFd(this, QByteArrayLiteral("IN_FENCE_FD")) + , sizeHints(this, QByteArrayLiteral("SIZE_HINTS")) + , inFormatsForTearing(this, QByteArrayLiteral("IN_FORMATS_ASYNC")) + , zpos(this, QByteArrayLiteral("zpos")) + , colorPipeline(this, QByteArrayLiteral("COLOR_PIPELINE")) +{ +} + +bool DrmPlane::init() +{ + return updateProperties(); +} + +bool DrmPlane::updateProperties() +{ + DrmUniquePtr p(drmModeGetPlane(gpu()->fd(), id())); + if (!p) { + qCWarning(KWIN_DRM) << "Failed to get kernel plane" << id(); + return false; + } + DrmPropertyList props = queryProperties(); + type.update(props); + srcX.update(props); + srcY.update(props); + srcW.update(props); + srcH.update(props); + crtcX.update(props); + crtcY.update(props); + crtcW.update(props); + crtcH.update(props); + fbId.update(props); + crtcId.update(props); + rotation.update(props); + inFormats.update(props); + alpha.update(props); + pixelBlendMode.update(props); + colorEncoding.update(props); + colorRange.update(props); + vmHotspotX.update(props); + vmHotspotY.update(props); + inFenceFd.update(props); + sizeHints.update(props); + inFormatsForTearing.update(props); + zpos.update(props); + colorPipeline.update(props); + + if (!type.isValid() || !srcX.isValid() || !srcY.isValid() || !srcW.isValid() || !srcH.isValid() + || !crtcX.isValid() || !crtcY.isValid() || !crtcW.isValid() || !crtcH.isValid() || !fbId.isValid()) { + qCWarning(KWIN_DRM) << "Failed to update the basic plane properties"; + return false; + } + + m_possibleCrtcs = p->possible_crtcs; + + // read formats from blob if available and if modifiers are supported, and from the plane object if not + m_supportedFormats.clear(); + if (inFormats.isValid() && inFormats.immutableBlob() && gpu()->addFB2ModifiersSupported()) { + drmModeFormatModifierIterator iterator{}; + while (drmModeFormatModifierBlobIterNext(inFormats.immutableBlob(), &iterator)) { + m_supportedFormats[iterator.fmt].push_back(iterator.mod); + } + } else { + // if we don't have modifier support, assume the cursor needs a linear buffer + const QList modifiers = {type.enumValue() == TypeIndex::Cursor ? DRM_FORMAT_MOD_LINEAR : DRM_FORMAT_MOD_INVALID}; + for (uint32_t i = 0; i < p->count_formats; i++) { + m_supportedFormats.insert(p->formats[i], modifiers); + } + if (m_supportedFormats.isEmpty()) { + qCWarning(KWIN_DRM) << "Driver doesn't advertise any formats for this plane. Falling back to XRGB8888 without explicit modifiers"; + m_supportedFormats.insert(DRM_FORMAT_XRGB8888, modifiers); + } + } + m_lowBandwidthFormats.clear(); + for (auto it = m_supportedFormats.begin(); it != m_supportedFormats.end(); it++) { + const auto info = FormatInfo::get(it.key()); + if (info && info->bitsPerPixel <= 32) { + // Mesa usually picks the modifier with lowest bandwidth requirements, + // so prefer implicit modifiers for low bandwidth if supported + m_lowBandwidthFormats.insert(it.key(), {DRM_FORMAT_MOD_INVALID}); + } + } + + m_sizeHints.clear(); + if (sizeHints.isValid() && sizeHints.immutableBlob()) { + // TODO switch to drm_plane_size_hint once we require libdrm 2.4.122 + struct SizeHint + { + uint16_t width; + uint16_t height; + }; + std::span hints(reinterpret_cast(sizeHints.immutableBlob()->data), sizeHints.immutableBlob()->length / sizeof(SizeHint)); + std::ranges::transform(hints, std::back_inserter(m_sizeHints), [](const SizeHint &hint) { + return QSize(hint.width, hint.height); + }); + } + if (m_sizeHints.empty() && type.enumValue() == TypeIndex::Cursor) { + m_sizeHints = {gpu()->cursorSize()}; + } + + if (inFormatsForTearing.isValid() && inFormatsForTearing.immutableBlob() && gpu()->addFB2ModifiersSupported()) { + m_supportedTearingFormats.clear(); + drmModeFormatModifierIterator iterator{}; + while (drmModeFormatModifierBlobIterNext(inFormatsForTearing.immutableBlob(), &iterator)) { + m_supportedTearingFormats[iterator.fmt].push_back(iterator.mod); + } + } else { + m_supportedTearingFormats = m_supportedFormats; + } + if (colorPipeline.isValid() && m_colorPipelineObjects.empty()) { + const QList possibleValues = colorPipeline.possibleEnumValues(); + for (const uint64_t value : possibleValues) { + if (value == 0) { + // 0 is bypass + continue; + } + auto pipeline = std::make_unique(gpu(), value); + if (!pipeline->init()) { + qCWarning(KWIN_DRM, "initializing color pipeline %lu failed!", value); + continue; + } + QList opNames; + auto op = pipeline->colorOp(); + while (op) { + opNames.push_back(op->name()); + op = op->next(); + } + qCDebug(KWIN_DRM) << "Initialized color pipeline" << opNames; + m_colorPipelines.push_back(pipeline.get()); + m_colorPipelineObjects.push_back(std::move(pipeline)); + } + } + return true; +} + +void DrmPlane::set(DrmAtomicCommit *commit, const Rect &src, const Rect &dst) +{ + // Src* are in 16.16 fixed point format + commit->addProperty(srcX, src.x() << 16); + commit->addProperty(srcY, src.y() << 16); + commit->addProperty(srcW, src.width() << 16); + commit->addProperty(srcH, src.height() << 16); + commit->addProperty(crtcX, dst.x()); + commit->addProperty(crtcY, dst.y()); + commit->addProperty(crtcW, dst.width()); + commit->addProperty(crtcH, dst.height()); +} + +bool DrmPlane::isCrtcSupported(int pipeIndex) const +{ + return (m_possibleCrtcs & (1 << pipeIndex)); +} + +QHash> DrmPlane::lowBandwidthFormats() const +{ + return m_lowBandwidthFormats; +} + +QHash> DrmPlane::formats() const +{ + return m_supportedFormats; +} + +QHash> DrmPlane::tearingFormats() const +{ + return m_supportedTearingFormats; +} + +std::shared_ptr DrmPlane::currentBuffer() const +{ + return m_current; +} + +void DrmPlane::setCurrentBuffer(const std::shared_ptr &b) +{ + if (m_current == b) { + return; + } + + m_current = b; + if (b && !m_lastBuffers.contains(b->data())) { + m_lastBuffers.prepend(b->data()); + m_lastBuffers.resize(4); + } +} + +void DrmPlane::disable(DrmAtomicCommit *commit) +{ + commit->addProperty(crtcId, 0); + commit->addBuffer(this, nullptr, nullptr); +} + +void DrmPlane::releaseCurrentBuffer() +{ + if (m_current) { + m_current->releaseBuffer(); + } +} + +DrmPlane::Transformations DrmPlane::outputTransformToPlaneTransform(OutputTransform transform) +{ + // note that drm transformations are counter clockwise + switch (transform.kind()) { + case OutputTransform::Kind::Normal: + return Transformation::Rotate0; + case OutputTransform::Kind::Rotate90: + return Transformation::Rotate270; + case OutputTransform::Kind::Rotate180: + return Transformation::Rotate180; + case OutputTransform::Kind::Rotate270: + return Transformation::Rotate90; + case OutputTransform::Kind::FlipY: + return Transformation::Rotate0 | Transformation::ReflectY; + case OutputTransform::Kind::FlipY90: + return Transformation::Rotate270 | Transformation::ReflectY; + case OutputTransform::Kind::FlipY180: + return Transformation::Rotate180 | Transformation::ReflectY; + case OutputTransform::Kind::FlipY270: + return Transformation::Rotate90 | Transformation::ReflectY; + } + Q_UNREACHABLE(); +} + +bool DrmPlane::supportsTransformation(OutputTransform transform) const +{ + return rotation.isValid() && rotation.hasEnum(outputTransformToPlaneTransform(transform)); +} + +QList DrmPlane::recommendedSizes() const +{ + return m_sizeHints; +} + +QList DrmPlane::colorPipelines() const +{ + return m_colorPipelines; +} +} + +#include "moc_drm_plane.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_plane.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_plane.h new file mode 100644 index 0000000000..036e66146c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_plane.h @@ -0,0 +1,127 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Roman Gilg + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "core/output.h" +#include "drm_colorop.h" +#include "drm_object.h" + +#include +#include +#include +#include +#include + +namespace KWin +{ + +class DrmFramebuffer; +class DrmFramebufferData; +class DrmCrtc; + +class DrmPlane : public DrmObject +{ + Q_GADGET +public: + DrmPlane(DrmGpu *gpu, uint32_t planeId); + + bool init(); + + bool updateProperties() override; + void disable(DrmAtomicCommit *commit) override; + + bool isCrtcSupported(int pipeIndex) const; + QHash> lowBandwidthFormats() const; + QHash> formats() const; + QHash> tearingFormats() const; + bool supportsTransformation(OutputTransform transform) const; + + std::shared_ptr currentBuffer() const; + void setCurrentBuffer(const std::shared_ptr &b); + void releaseCurrentBuffer(); + + void set(DrmAtomicCommit *commit, const Rect &src, const Rect &dst); + + QList recommendedSizes() const; + QList colorPipelines() const; + + enum class TypeIndex : uint64_t { + Overlay = 0, + Primary = 1, + Cursor = 2 + }; + enum class Transformation : uint32_t { + Rotate0 = 1 << 0, + Rotate90 = 1 << 1, + Rotate180 = 1 << 2, + Rotate270 = 1 << 3, + ReflectX = 1 << 4, + ReflectY = 1 << 5 + }; + Q_ENUM(Transformation) + Q_DECLARE_FLAGS(Transformations, Transformation) + static Transformations outputTransformToPlaneTransform(OutputTransform transform); + enum class PixelBlendMode : uint64_t { + None, + PreMultiplied, + Coverage + }; + enum class ColorEncoding : uint64_t { + BT601_YCbCr, + BT709_YCbCr, + BT2020_YCbCr + }; + enum class ColorRange : uint64_t { + Limited_YCbCr, + Full_YCbCr + }; + + DrmEnumProperty type; + DrmProperty srcX; + DrmProperty srcY; + DrmProperty srcW; + DrmProperty srcH; + DrmProperty crtcX; + DrmProperty crtcY; + DrmProperty crtcW; + DrmProperty crtcH; + DrmProperty fbId; + DrmProperty crtcId; + DrmEnumProperty rotation; + DrmProperty inFormats; + DrmProperty alpha; + DrmEnumProperty pixelBlendMode; + DrmEnumProperty colorEncoding; + DrmEnumProperty colorRange; + DrmProperty vmHotspotX; + DrmProperty vmHotspotY; + DrmProperty inFenceFd; + DrmProperty sizeHints; + DrmProperty inFormatsForTearing; + DrmProperty zpos; + DrmProperty colorPipeline; + +private: + std::shared_ptr m_current; + QList> m_lastBuffers; + + QHash> m_supportedFormats; + QHash> m_lowBandwidthFormats; + QHash> m_supportedTearingFormats; + uint32_t m_possibleCrtcs = 0; + QList m_sizeHints; + + std::vector> m_colorPipelineObjects; + QList m_colorPipelines; +}; + +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::DrmPlane::Transformations) diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_pointer.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_pointer.h new file mode 100644 index 0000000000..b58403a245 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_pointer.h @@ -0,0 +1,150 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include +#include + +namespace KWin +{ + +template +struct DrmDeleter; + +template<> +struct DrmDeleter +{ + void operator()(drmVersion *version) + { + drmFreeVersion(version); + } +}; + +template<> +struct DrmDeleter +{ + void operator()(drmModeAtomicReq *req) + { + drmModeAtomicFree(req); + } +}; + +template<> +struct DrmDeleter +{ + void operator()(drmModeConnector *connector) + { + drmModeFreeConnector(connector); + } +}; + +template<> +struct DrmDeleter +{ + void operator()(drmModeCrtc *crtc) + { + drmModeFreeCrtc(crtc); + } +}; + +template<> +struct DrmDeleter +{ + void operator()(drmModeFB *fb) + { + drmModeFreeFB(fb); + } +}; + +template<> +struct DrmDeleter +{ + void operator()(drmModeEncoder *encoder) + { + drmModeFreeEncoder(encoder); + } +}; + +template<> +struct DrmDeleter +{ + void operator()(drmModeModeInfo *info) + { + drmModeFreeModeInfo(info); + } +}; + +template<> +struct DrmDeleter +{ + void operator()(drmModeObjectProperties *properties) + { + drmModeFreeObjectProperties(properties); + } +}; + +template<> +struct DrmDeleter +{ + void operator()(drmModePlane *plane) + { + drmModeFreePlane(plane); + } +}; + +template<> +struct DrmDeleter +{ + void operator()(drmModePlaneRes *resources) + { + drmModeFreePlaneResources(resources); + } +}; + +template<> +struct DrmDeleter +{ + void operator()(drmModePropertyRes *property) + { + drmModeFreeProperty(property); + } +}; + +template<> +struct DrmDeleter +{ + void operator()(drmModePropertyBlobRes *blob) + { + drmModeFreePropertyBlob(blob); + } +}; + +template<> +struct DrmDeleter +{ + void operator()(drmModeRes *resources) + { + drmModeFreeResources(resources); + } +}; + +template<> +struct DrmDeleter +{ + void operator()(drmModeLesseeListRes *ptr) + { + drmFree(ptr); + } +}; + +template +using DrmUniquePtr = std::unique_ptr>; +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_property.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_property.cpp new file mode 100644 index 0000000000..953e09b3cf --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_property.cpp @@ -0,0 +1,168 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Roman Gilg + SPDX-FileCopyrightText: 2021-2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "drm_property.h" +#include "drm_gpu.h" +#include "drm_logging.h" +#include "drm_object.h" +#include + +namespace KWin +{ + +DrmProperty::DrmProperty(DrmObject *obj, const QByteArray &name, const QList &enumNames) + : m_obj(obj) + , m_propName(name) + , m_enumNames(enumNames) +{ +} + +bool DrmProperty::setPropertyLegacy(uint64_t value) +{ + if (m_current == value) { + return true; + } + + const int ret = drmModeObjectSetProperty(m_obj->gpu()->fd(), m_obj->id(), m_obj->type(), m_propId, value); + if (ret == 0) { + m_current = value; + return true; + } + + qCWarning(KWIN_DRM) << "Failed to set property:" << m_propName << "for object ID:" << m_obj->id() << "to value:" << value << "error:" << strerror(-ret); + return false; +} + +void DrmProperty::update(DrmPropertyList &propertyList) +{ + if (const auto opt = propertyList.takeProperty(m_propName)) { + const auto &[prop, value] = *opt; + m_propId = prop->prop_id; + m_current = value; + m_flags = prop->flags; + if ((prop->flags & DRM_MODE_PROP_RANGE) || (prop->flags & DRM_MODE_PROP_SIGNED_RANGE)) { + Q_ASSERT(prop->count_values > 1); + m_minValue = prop->values[0]; + m_maxValue = prop->values[1]; + } + m_enumToPropertyMap.clear(); + m_propertyToEnumMap.clear(); + // bitmasks need translation too, not just enums + if (prop->flags & (DRM_MODE_PROP_ENUM | DRM_MODE_PROP_BITMASK)) { + for (int i = 0; i < prop->count_enums; i++) { + struct drm_mode_property_enum *en = &prop->enums[i]; + int j = m_enumNames.indexOf(QByteArray(en->name)); + if (j >= 0) { + if (m_flags & DRM_MODE_PROP_BITMASK) { + m_enumToPropertyMap[1 << j] = 1 << en->value; + m_propertyToEnumMap[1 << en->value] = 1 << j; + } else { + m_enumToPropertyMap[j] = en->value; + m_propertyToEnumMap[en->value] = j; + } + } + } + } + m_enumValues.clear(); + if (prop->flags & DRM_MODE_PROP_ENUM) { + QList names; + for (const drm_mode_property_enum &en : std::span(prop->enums, prop->count_enums)) { + m_enumValues.push_back(en.value); + names.push_back(en.name); + } + } + if ((m_flags & DRM_MODE_PROP_IMMUTABLE) && (m_flags & DRM_MODE_PROP_BLOB)) { + if (m_current != 0) { + m_immutableBlob.reset(drmModeGetPropertyBlob(m_obj->gpu()->fd(), m_current)); + if (m_immutableBlob && (!m_immutableBlob->data || !m_immutableBlob->length)) { + m_immutableBlob.reset(); + } + } else { + m_immutableBlob.reset(); + } + } + } else { + m_propId = 0; + m_immutableBlob.reset(); + m_enumToPropertyMap.clear(); + m_propertyToEnumMap.clear(); + } +} + +QList DrmProperty::possibleEnumValues() const +{ + return m_enumValues; +} + +uint64_t DrmProperty::value() const +{ + return m_current; +} + +bool DrmProperty::hasAllEnums() const +{ + return m_enumToPropertyMap.count() == m_enumNames.count(); +} + +uint32_t DrmProperty::propId() const +{ + return m_propId; +} + +const QByteArray &DrmProperty::name() const +{ + return m_propName; +} + +bool DrmProperty::isImmutable() const +{ + return m_flags & DRM_MODE_PROP_IMMUTABLE; +} + +bool DrmProperty::isBitmask() const +{ + return m_flags & DRM_MODE_PROP_BITMASK; +} + +uint64_t DrmProperty::maxValue() const +{ + return m_maxValue; +} + +uint64_t DrmProperty::minValue() const +{ + return m_minValue; +} + +void DrmProperty::checkValueInRange(uint64_t value) const +{ + if (Q_UNLIKELY(m_flags & DRM_MODE_PROP_RANGE && (value > m_maxValue || value < m_minValue))) { + qCWarning(KWIN_DRM) << "Range property value out of bounds." << m_propName << " value:" << value << "min:" << m_minValue << "max:" << m_maxValue; + } + if (Q_UNLIKELY(m_flags & DRM_MODE_PROP_SIGNED_RANGE && (int64_t(value) > int64_t(m_maxValue) || int64_t(value) < int64_t(m_minValue)))) { + qCWarning(KWIN_DRM) << "Signed range property value out of bounds." << m_propName << " value:" << int64_t(value) << "min:" << int64_t(m_minValue) << "max:" << int64_t(m_maxValue); + } +} + +drmModePropertyBlobRes *DrmProperty::immutableBlob() const +{ + return m_immutableBlob.get(); +} + +DrmObject *DrmProperty::drmObject() const +{ + return m_obj; +} + +bool DrmProperty::isValid() const +{ + return m_propId != 0; +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_property.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_property.h new file mode 100644 index 0000000000..51c65edc86 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_property.h @@ -0,0 +1,148 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Roman Gilg + SPDX-FileCopyrightText: 2021-2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once +#include "drm_pointer.h" +#include "drm_logging.h" + +#include +#include +#include + +#include + +namespace KWin +{ + +class DrmObject; +class DrmPropertyList; + +class DrmProperty +{ +public: + explicit DrmProperty(DrmObject *obj, const QByteArray &name, const QList &enumNames = {}); + + const QByteArray &name() const; + DrmObject *drmObject() const; + + uint32_t propId() const; + bool isImmutable() const; + bool isBitmask() const; + bool hasAllEnums() const; + uint64_t value() const; + drmModePropertyBlobRes *immutableBlob() const; + uint64_t minValue() const; + uint64_t maxValue() const; + bool isValid() const; + + /** + * Prints a warning and returns false if @p value is unacceptable for the property + */ + void checkValueInRange(uint64_t value) const; + + void update(DrmPropertyList &propertyList); + bool setPropertyLegacy(uint64_t value); + + QList possibleEnumValues() const; + +protected: + DrmObject *const m_obj; + const QByteArray m_propName; + const QList m_enumNames; + + uint32_t m_propId = 0; + // the last known value from the kernel + uint64_t m_current = 0; + DrmUniquePtr m_immutableBlob; + + uint64_t m_minValue = -1; + uint64_t m_maxValue = -1; + + QMap m_enumToPropertyMap; + QMap m_propertyToEnumMap; + QList m_enumValues; + uint32_t m_flags; +}; + +template +class DrmEnumProperty : public DrmProperty +{ +public: + DrmEnumProperty(DrmObject *obj, const QByteArray &name, const QList &enumNames) + : DrmProperty(obj, name, enumNames) + { + } + + Enum enumValue() const + { + return enumForValue(value()); + } + + bool hasEnum(Enum value) const + { + const uint64_t integerValue = static_cast(value); + if (isBitmask()) { + for (uint64_t mask = 1; integerValue >= mask && mask != 0; mask <<= 1) { + if ((integerValue & mask) && !m_enumToPropertyMap.contains(mask)) { + return false; + } + } + return true; + } else { + return m_enumToPropertyMap.contains(integerValue); + } + } + + bool hasEnumForValue(uint64_t value) const + { + return m_propertyToEnumMap.contains(value); + } + + Enum enumForValue(uint64_t value) const + { + if (isBitmask()) { + uint64_t ret = 0; + for (uint64_t mask = 1; value >= mask && mask != 0; mask <<= 1) { + if (value & mask) { + ret |= m_propertyToEnumMap[mask]; + } + } + return static_cast(ret); + } else { + return static_cast(m_propertyToEnumMap[value]); + } + } + + uint64_t valueForEnum(Enum enumValue) const + { + const uint64_t integer = static_cast(enumValue); + if (isBitmask()) { + uint64_t set = 0; + for (uint64_t mask = 1; integer >= mask && mask != 0; mask <<= 1) { + if (integer & mask) { + set |= m_enumToPropertyMap[mask]; + } + } + return set; + } else { + return m_enumToPropertyMap[integer]; + } + } + + bool setEnumLegacy(Enum value) + { + if (hasEnum(value)) { + return setPropertyLegacy(valueForEnum(value)); + } else { + return false; + } + } +}; +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_qpainter_backend.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_qpainter_backend.cpp new file mode 100644 index 0000000000..9be2d21bd9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_qpainter_backend.cpp @@ -0,0 +1,66 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_qpainter_backend.h" +#include "drm_backend.h" +#include "drm_gpu.h" +#include "drm_output.h" +#include "drm_pipeline.h" +#include "drm_qpainter_layer.h" +#include "drm_virtual_output.h" + +#include + +namespace KWin +{ + +DrmQPainterBackend::DrmQPainterBackend(DrmBackend *backend) + : QPainterBackend() + , m_backend(backend) +{ + m_backend->setRenderBackend(this); + m_backend->createLayers(); +} + +DrmQPainterBackend::~DrmQPainterBackend() +{ + m_backend->releaseBuffers(); + m_backend->setRenderBackend(nullptr); +} + +DrmDevice *DrmQPainterBackend::drmDevice() const +{ + return m_backend->primaryGpu()->drmDevice(); +} + +QList DrmQPainterBackend::compatibleOutputLayers(BackendOutput *output) +{ + if (auto virtualOutput = qobject_cast(output)) { + return {virtualOutput->primaryLayer()}; + } else { + return static_cast(output)->pipeline()->gpu()->compatibleOutputLayers(output); + } +} + +std::unique_ptr DrmQPainterBackend::createDrmPlaneLayer(DrmPlane *plane) +{ + return std::make_unique(plane); +} + +std::unique_ptr DrmQPainterBackend::createDrmPlaneLayer(DrmGpu *gpu, DrmPlane::TypeIndex type) +{ + return std::make_unique(type); +} + +std::unique_ptr DrmQPainterBackend::createLayer(DrmVirtualOutput *output) +{ + return std::make_unique(output); +} +} + +#include "moc_drm_qpainter_backend.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_qpainter_backend.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_qpainter_backend.h new file mode 100644 index 0000000000..99939ae49a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_qpainter_backend.h @@ -0,0 +1,40 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "drm_render_backend.h" +#include "qpainter/qpainterbackend.h" + +#include +#include + +namespace KWin +{ + +class DrmBackend; +class DrmAbstractOutput; +class DrmQPainterLayer; +class DrmPipeline; + +class DrmQPainterBackend : public QPainterBackend, public DrmRenderBackend +{ + Q_OBJECT +public: + DrmQPainterBackend(DrmBackend *backend); + ~DrmQPainterBackend(); + + DrmDevice *drmDevice() const override; + QList compatibleOutputLayers(BackendOutput *output) override; + std::unique_ptr createDrmPlaneLayer(DrmPlane *plane) override; + std::unique_ptr createDrmPlaneLayer(DrmGpu *gpu, DrmPlane::TypeIndex type) override; + std::unique_ptr createLayer(DrmVirtualOutput *output) override; + +private: + DrmBackend *m_backend; +}; +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_qpainter_layer.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_qpainter_layer.cpp new file mode 100644 index 0000000000..4870f555d9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_qpainter_layer.cpp @@ -0,0 +1,141 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_qpainter_layer.h" +#include "core/graphicsbufferview.h" +#include "drm_buffer.h" +#include "drm_gpu.h" +#include "drm_logging.h" +#include "drm_output.h" +#include "drm_pipeline.h" +#include "drm_virtual_output.h" +#include "qpainter/qpainterswapchain.h" + +#include +#include + +namespace KWin +{ + +DrmQPainterLayer::DrmQPainterLayer(DrmPlane *plane) + : DrmPipelineLayer(plane) +{ +} + +DrmQPainterLayer::DrmQPainterLayer(DrmPlane::TypeIndex type) + : DrmPipelineLayer(type) +{ +} + +std::optional DrmQPainterLayer::doBeginFrame() +{ + if (!doesSwapchainFit()) { + m_swapchain = std::make_shared(scanoutDevice()->allocator(), targetRect().size(), supportedDrmFormats().contains(DRM_FORMAT_ARGB8888) ? DRM_FORMAT_ARGB8888 : DRM_FORMAT_XRGB8888); + m_damageJournal = DamageJournal(); + } + + m_currentBuffer = m_swapchain->acquire(); + if (!m_currentBuffer) { + return std::nullopt; + } + + m_renderTime = std::make_unique(); + const Region repaint = m_damageJournal.accumulate(m_currentBuffer->age(), Region::infinite()); + return OutputLayerBeginFrameInfo{ + .renderTarget = RenderTarget(m_currentBuffer->view()->image()), + .repaint = repaint, + }; +} + +bool DrmQPainterLayer::doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) +{ + m_renderTime->end(); + if (frame) { + frame->addRenderTimeQuery(std::move(m_renderTime)); + } + m_currentFramebuffer = gpu()->importBuffer(m_currentBuffer->buffer(), FileDescriptor{}); + m_damageJournal.add(damagedDeviceRegion); + m_swapchain->release(m_currentBuffer); + if (!m_currentFramebuffer) { + qCWarning(KWIN_DRM, "Failed to create dumb framebuffer: %s", strerror(errno)); + } + return m_currentFramebuffer != nullptr; +} + +bool DrmQPainterLayer::preparePresentationTest() +{ + if (!doesSwapchainFit()) { + m_swapchain = std::make_shared(scanoutDevice()->allocator(), targetRect().size(), supportedDrmFormats().contains(DRM_FORMAT_ARGB8888) ? DRM_FORMAT_ARGB8888 : DRM_FORMAT_XRGB8888); + m_currentBuffer = m_swapchain->acquire(); + if (m_currentBuffer) { + m_currentFramebuffer = gpu()->importBuffer(m_currentBuffer->buffer(), FileDescriptor{}); + m_swapchain->release(m_currentBuffer); + if (!m_currentFramebuffer) { + qCWarning(KWIN_DRM, "Failed to create dumb framebuffer: %s", strerror(errno)); + } + } else { + m_currentFramebuffer.reset(); + } + } + return m_currentFramebuffer != nullptr; +} + +bool DrmQPainterLayer::doesSwapchainFit() const +{ + return m_swapchain && m_swapchain->size() == targetRect().size(); +} + +std::shared_ptr DrmQPainterLayer::currentBuffer() const +{ + return m_currentFramebuffer; +} + +void DrmQPainterLayer::releaseBuffers() +{ + m_swapchain.reset(); +} + +DrmVirtualQPainterLayer::DrmVirtualQPainterLayer(DrmVirtualOutput *output) + : DrmOutputLayer(output, OutputLayerType::Primary) +{ +} + +std::optional DrmVirtualQPainterLayer::doBeginFrame() +{ + if (m_image.isNull() || m_image.size() != m_output->modeSize()) { + m_image = QImage(m_output->modeSize(), QImage::Format_RGB32); + } + m_renderTime = std::make_unique(); + return OutputLayerBeginFrameInfo{ + .renderTarget = RenderTarget(&m_image), + .repaint = Region(), + }; +} + +bool DrmVirtualQPainterLayer::doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) +{ + m_renderTime->end(); + frame->addRenderTimeQuery(std::move(m_renderTime)); + return true; +} + +void DrmVirtualQPainterLayer::releaseBuffers() +{ +} + +DrmDevice *DrmVirtualQPainterLayer::scanoutDevice() const +{ + // TODO make this use GraphicsBuffers too? + return nullptr; +} + +QHash> DrmVirtualQPainterLayer::supportedDrmFormats() const +{ + return {{DRM_FORMAT_ARGB8888, QList{DRM_FORMAT_MOD_LINEAR}}}; +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_qpainter_layer.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_qpainter_layer.h new file mode 100644 index 0000000000..f520d53c59 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_qpainter_layer.h @@ -0,0 +1,64 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "core/renderbackend.h" +#include "drm_layer.h" +#include "utils/damagejournal.h" + +#include + +namespace KWin +{ + +class QPainterSwapchain; +class QPainterSwapchainSlot; +class DrmPipeline; +class DrmVirtualOutput; +class DrmQPainterBackend; +class DrmFramebuffer; + +class DrmQPainterLayer : public DrmPipelineLayer +{ +public: + explicit DrmQPainterLayer(DrmPlane *plane); + explicit DrmQPainterLayer(DrmPlane::TypeIndex type); + + std::optional doBeginFrame() override; + bool doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) override; + bool preparePresentationTest() override; + std::shared_ptr currentBuffer() const override; + void releaseBuffers() override; + +private: + bool doesSwapchainFit() const; + + std::shared_ptr m_swapchain; + std::shared_ptr m_currentBuffer; + std::shared_ptr m_currentFramebuffer; + DamageJournal m_damageJournal; + std::unique_ptr m_renderTime; +}; + +class DrmVirtualQPainterLayer : public DrmOutputLayer +{ +public: + explicit DrmVirtualQPainterLayer(DrmVirtualOutput *output); + + std::optional doBeginFrame() override; + bool doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) override; + + void releaseBuffers() override; + DrmDevice *scanoutDevice() const override; + QHash> supportedDrmFormats() const override; + +private: + QImage m_image; + std::unique_ptr m_renderTime; +}; +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_render_backend.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_render_backend.h new file mode 100644 index 0000000000..750f166221 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_render_backend.h @@ -0,0 +1,33 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "drm_plane.h" + +#include + +namespace KWin +{ + +class DrmPipelineLayer; +class DrmVirtualOutput; +class DrmPipeline; +class DrmOutputLayer; +class DrmOverlayLayer; + +class DrmRenderBackend +{ +public: + virtual ~DrmRenderBackend() = default; + + virtual std::unique_ptr createDrmPlaneLayer(DrmPlane *plane) = 0; + virtual std::unique_ptr createDrmPlaneLayer(DrmGpu *gpu, DrmPlane::TypeIndex type) = 0; + virtual std::unique_ptr createLayer(DrmVirtualOutput *output) = 0; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_virtual_egl_layer.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_virtual_egl_layer.cpp new file mode 100644 index 0000000000..7d7cf4e99b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_virtual_egl_layer.cpp @@ -0,0 +1,161 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_virtual_egl_layer.h" +#include "drm_egl_backend.h" +#include "drm_gpu.h" +#include "drm_logging.h" +#include "drm_virtual_output.h" +#include "opengl/eglnativefence.h" +#include "opengl/eglswapchain.h" +#include "opengl/glrendertimequery.h" +#include "scene/surfaceitem_wayland.h" +#include "wayland/surface.h" + +#include +#include +#include +#include + +namespace KWin +{ + +VirtualEglGbmLayer::VirtualEglGbmLayer(EglGbmBackend *eglBackend, DrmVirtualOutput *output) + : DrmOutputLayer(output, OutputLayerType::Primary) + , m_eglBackend(eglBackend) +{ +} + +VirtualEglGbmLayer::~VirtualEglGbmLayer() +{ + releaseBuffers(); +} + +std::optional VirtualEglGbmLayer::doBeginFrame() +{ + // gbm surface + if (doesGbmSwapchainFit(m_gbmSwapchain.get())) { + m_oldGbmSwapchain.reset(); + m_oldDamageJournal.clear(); + } else { + if (doesGbmSwapchainFit(m_oldGbmSwapchain.get())) { + m_gbmSwapchain = m_oldGbmSwapchain; + m_damageJournal = m_oldDamageJournal; + } else { + if (const auto swapchain = createGbmSwapchain()) { + m_oldGbmSwapchain = m_gbmSwapchain; + m_oldDamageJournal = m_damageJournal; + m_gbmSwapchain = swapchain; + m_damageJournal = DamageJournal(); + } else { + return std::nullopt; + } + } + } + + if (!m_eglBackend->openglContext()->makeCurrent()) { + return std::nullopt; + } + + auto slot = m_gbmSwapchain->acquire(); + if (!slot) { + return std::nullopt; + } + + m_currentSlot = slot; + + m_query = std::make_unique(m_eglBackend->openglContextRef()); + m_query->begin(); + + const Region repair = m_damageJournal.accumulate(slot->age(), Region::infinite()); + return OutputLayerBeginFrameInfo{ + .renderTarget = RenderTarget(slot->framebuffer()), + .repaint = repair, + }; +} + +bool VirtualEglGbmLayer::doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) +{ + m_query->end(); + frame->addRenderTimeQuery(std::move(m_query)); + glFlush(); + m_damageJournal.add(damagedDeviceRegion); + + EGLNativeFence releaseFence{m_eglBackend->eglDisplayObject()}; + m_gbmSwapchain->release(m_currentSlot, releaseFence.takeFileDescriptor()); + return true; +} + +std::shared_ptr VirtualEglGbmLayer::createGbmSwapchain() const +{ + static bool modifiersEnvSet = false; + static const bool modifiersEnv = qEnvironmentVariableIntValue("KWIN_DRM_USE_MODIFIERS", &modifiersEnvSet) != 0; + const bool allowModifiers = !modifiersEnvSet || modifiersEnv; + + const auto tranches = m_eglBackend->tranches(); + for (const auto &tranche : tranches) { + for (auto it = tranche.formatTable.constBegin(); it != tranche.formatTable.constEnd(); it++) { + const auto size = m_output->modeSize(); + const auto format = it.key(); + const auto modifiers = it.value(); + + if (allowModifiers && !modifiers.isEmpty()) { + if (auto swapchain = EglSwapchain::create(m_eglBackend->gpu()->drmDevice()->allocator(), m_eglBackend->openglContext(), size, format, modifiers)) { + return swapchain; + } + } + + static const QList implicitModifier{DRM_FORMAT_MOD_INVALID}; + if (auto swapchain = EglSwapchain::create(m_eglBackend->gpu()->drmDevice()->allocator(), m_eglBackend->openglContext(), size, format, implicitModifier)) { + return swapchain; + } + } + } + qCWarning(KWIN_DRM) << "couldn't create a gbm swapchain for a virtual output!"; + return nullptr; +} + +bool VirtualEglGbmLayer::doesGbmSwapchainFit(EglSwapchain *swapchain) const +{ + return swapchain && swapchain->size() == m_output->modeSize(); +} + +bool VirtualEglGbmLayer::importScanoutBuffer(GraphicsBuffer *buffer, const std::shared_ptr &frame) +{ + static bool valid; + static const bool directScanoutDisabled = qEnvironmentVariableIntValue("KWIN_DRM_NO_DIRECT_SCANOUT", &valid) == 1 && valid; + if (directScanoutDisabled) { + return false; + } + + if (sourceRect() != targetRect() || targetRect().topLeft() != QPointF(0, 0) || targetRect().size() != m_output->modeSize() || targetRect().size() != buffer->size() || offloadTransform() != OutputTransform::Kind::Normal) { + return false; + } + m_scanoutBuffer = buffer; + return true; +} + +void VirtualEglGbmLayer::releaseBuffers() +{ + m_eglBackend->openglContext()->makeCurrent(); + m_gbmSwapchain.reset(); + m_oldGbmSwapchain.reset(); + m_currentSlot.reset(); + m_scanoutBuffer.reset(); +} + +DrmDevice *VirtualEglGbmLayer::scanoutDevice() const +{ + return m_eglBackend->drmDevice(); +} + +QHash> VirtualEglGbmLayer::supportedDrmFormats() const +{ + return m_eglBackend->supportedFormats(); +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_virtual_egl_layer.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_virtual_egl_layer.h new file mode 100644 index 0000000000..3e6a34caf4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_virtual_egl_layer.h @@ -0,0 +1,58 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "core/graphicsbuffer.h" +#include "drm_layer.h" +#include "utils/damagejournal.h" + +#include +#include +#include +#include + +namespace KWin +{ + +class EglSwapchain; +class EglSwapchainSlot; +class GLTexture; +class EglGbmBackend; +class DrmVirtualOutput; +class GLRenderTimeQuery; +class SurfaceInterface; + +class VirtualEglGbmLayer : public DrmOutputLayer +{ +public: + VirtualEglGbmLayer(EglGbmBackend *eglBackend, DrmVirtualOutput *output); + ~VirtualEglGbmLayer() override; + + std::optional doBeginFrame() override; + bool doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) override; + void releaseBuffers() override; + DrmDevice *scanoutDevice() const override; + QHash> supportedDrmFormats() const override; + +private: + bool importScanoutBuffer(GraphicsBuffer *buffer, const std::shared_ptr &frame) override; + std::shared_ptr createGbmSwapchain() const; + bool doesGbmSwapchainFit(EglSwapchain *swapchain) const; + + GraphicsBufferRef m_scanoutBuffer; + DamageJournal m_damageJournal; + DamageJournal m_oldDamageJournal; + std::shared_ptr m_gbmSwapchain; + std::shared_ptr m_oldGbmSwapchain; + std::shared_ptr m_currentSlot; + std::unique_ptr m_query; + + EglGbmBackend *const m_eglBackend; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_virtual_output.cpp b/local/recipes/kde/kwin/source/src/backends/drm/drm_virtual_output.cpp new file mode 100644 index 0000000000..4f963c1bb1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_virtual_output.cpp @@ -0,0 +1,147 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Roman Gilg + SPDX-FileCopyrightText: 2021 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drm_virtual_output.h" + +#include "core/outputconfiguration.h" +#include "core/renderbackend.h" +#include "drm_backend.h" +#include "drm_gpu.h" +#include "drm_layer.h" +#include "drm_render_backend.h" +#include "utils/softwarevsyncmonitor.h" + +namespace KWin +{ + +DrmVirtualOutput::DrmVirtualOutput(DrmBackend *backend, const QString &name, const QString &description, const QSize &size, qreal scale) + : m_backend(backend) + , m_vsyncMonitor(SoftwareVsyncMonitor::create()) +{ + connect(m_vsyncMonitor.get(), &VsyncMonitor::vblankOccurred, this, &DrmVirtualOutput::vblank); + + auto mode = std::make_shared(size, 60000, OutputMode::Flag::Preferred); + m_renderLoop->setRefreshRate(mode->refreshRate()); + + setInformation(Information{ + .name = QStringLiteral("Virtual-") + name, + .model = description, + .physicalSize = size, + .capabilities = Capability::CustomModes, + }); + + setState(State{ + .scale = scale, + .modes = {mode}, + .currentMode = mode, + }); + + recreateSurface(); +} + +DrmVirtualOutput::~DrmVirtualOutput() +{ +} + +bool DrmVirtualOutput::testPresentation(const std::shared_ptr &frame) +{ + return true; +} + +bool DrmVirtualOutput::present(const QList &layersToUpdate, const std::shared_ptr &frame) +{ + m_frame = frame; + m_vsyncMonitor->arm(); + return true; +} + +void DrmVirtualOutput::vblank(std::chrono::nanoseconds timestamp) +{ + if (m_frame) { + m_frame->presented(timestamp, PresentationMode::VSync); + m_frame.reset(); + } +} + +DrmOutputLayer *DrmVirtualOutput::primaryLayer() const +{ + return m_layer.get(); +} + +void DrmVirtualOutput::recreateSurface() +{ + m_layer = m_backend->renderBackend()->createLayer(this); +} + +void DrmVirtualOutput::applyChanges(const OutputConfiguration &config) +{ + auto props = config.constChangeSet(this); + if (!props) { + return; + } + Q_EMIT aboutToChange(props.get()); + + State next = m_state; + next.enabled = props->enabled.value_or(m_state.enabled); + next.transform = props->transform.value_or(m_state.transform); + next.position = props->pos.value_or(m_state.position); + next.scale = props->scale.value_or(m_state.scale); + next.scaleSetting = props->scaleSetting.value_or(m_state.scaleSetting); + next.desiredModeSize = props->desiredModeSize.value_or(m_state.desiredModeSize); + next.desiredModeRefreshRate = props->desiredModeRefreshRate.value_or(m_state.desiredModeRefreshRate); + next.desiredModeFlags = props->desiredModeFlags.value_or(m_state.desiredModeFlags); + next.currentMode = props->mode.value_or(m_state.currentMode).lock(); + if (!next.currentMode) { + next.currentMode = next.modes.front(); + } + next.uuid = props->uuid.value_or(m_state.uuid); + next.replicationSource = props->replicationSource.value_or(m_state.replicationSource); + next.priority = props->priority.value_or(m_state.priority); + next.deviceOffset = props->deviceOffset.value_or(m_state.deviceOffset); + if (props->customModes.has_value()) { + next.customModes = *props->customModes; + + QList> newModes; + for (const auto &mode : next.modes) { + if (mode->flags() & OutputMode::Flag::Custom) { + continue; + } + newModes.push_back(mode); + } + for (const auto &custom : next.customModes) { + newModes.push_back(std::make_shared(custom.size, custom.refreshRate, custom.flags | OutputMode::Flag::Custom)); + } + next.modes = newModes; + + if (!next.currentMode) { + next.currentMode = next.modes.front(); + } else if (!next.modes.contains(next.currentMode)) { + const auto it = std::ranges::find_if(next.modes, [&next](const auto &mode) { + return mode->size() == next.currentMode->size() + && mode->refreshRate() == next.currentMode->refreshRate() + && mode->flags() == next.currentMode->flags(); + }); + if (it != next.modes.end()) { + next.currentMode = *it; + } else { + next.modes.push_front(next.currentMode); + next.currentMode->setRemoved(); + } + } + } + + setState(next); + m_renderLoop->setRefreshRate(next.currentMode->refreshRate()); + m_vsyncMonitor->setRefreshRate(next.currentMode->refreshRate()); + + Q_EMIT changed(); +} +} + +#include "moc_drm_virtual_output.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/drm/drm_virtual_output.h b/local/recipes/kde/kwin/source/src/backends/drm/drm_virtual_output.h new file mode 100644 index 0000000000..c4d37f4daa --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/drm_virtual_output.h @@ -0,0 +1,48 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Roman Gilg + SPDX-FileCopyrightText: 2021 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "drm_abstract_output.h" + +#include + +namespace KWin +{ + +class SoftwareVsyncMonitor; +class VirtualBackend; +class DrmPipelineLayer; + +class DrmVirtualOutput : public DrmAbstractOutput +{ + Q_OBJECT + +public: + explicit DrmVirtualOutput(DrmBackend *backend, const QString &name, const QString &description, const QSize &size, qreal scale); + ~DrmVirtualOutput() override; + + bool testPresentation(const std::shared_ptr &frame) override; + bool present(const QList &layersToUpdate, const std::shared_ptr &frame) override; + void applyChanges(const OutputConfiguration &config) override; + + DrmOutputLayer *primaryLayer() const; + void recreateSurface(); + +private: + void vblank(std::chrono::nanoseconds timestamp); + + DrmBackend *const m_backend; + std::shared_ptr m_layer; + std::shared_ptr m_frame; + + std::unique_ptr m_vsyncMonitor; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/backends/drm/overview.md b/local/recipes/kde/kwin/source/src/backends/drm/overview.md new file mode 100644 index 0000000000..83211b7566 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/drm/overview.md @@ -0,0 +1,51 @@ + +Documentation on the drm/kms API is sparse and split up in a few places and files, mostly in the Linux kernel code. +This file is meant to provide an overview for drm/kms and gbm to provide a starting point for understanding how the APIs and the drm backend work. + +# drm/kms + +In the drm API there is a few objects that we care about: + +- `connector`s. A drm connector represents either an actual physical connector out of your graphics card or, in the case of DisplayPort MultiStream, a virtual one +- `crtc`s. A drm crtc represents hardware that can drive a connector uniquely - be it physical or virtual. Crtcs can also drive multiple connectors by cloning, aka showing the same stuff on all of them. The number of crtcs on your GPU or display device is a hard limit of how many display you can drive with it +- `plane`s. A drm plane represents scanout hardware that can be used for hardware composition, so cropping, scaling and rotation. There is multiple types of planes: + + * `primary`: it can be assumed that without the primary plane the output won't work. The kernel will always expose one per crtc + * `cursor`: cursor planes are what they sound like, they allow to use special hardware built for cursors. They have special restrictions like size (DRM_CAP_CURSOR_WIDTH, DRM_CAP_CURSOR_HEIGHT), scaling (on AMD gpus its scaling must be the same as with the primary scale) and even position on some very funky hardware (Apple stuff). Cursor planes are always optional + * `overlay`: overlay planes are what they sound like as well, they allow to use special hardware built for overlays (or underlays). The restrictions on this are of arbitrary complexity, you can never just assume they work. Overlay planes are also always optional +- `framebuffer`s, fb for short. These represent some sort of buffer that we'd like to show up on the screen and have to be created and destroyed by us +- `encoder`s. Can effectively be ignored, they were exposed in the API more or less by mistake and are just there for backwards compatibility + +All drm objects have properties with a name and a value. Depending on the type this value can have a different meaning; some are a bitfield, some are just integer values and some are for arbitrary data blobs. Properties can be read-only (immutable) and are only informational, some are needed for functionality. + +There's two APIs for drm, legacy and atomic modesetting (AMS). + +Legacy only exposes connectors and crtcs, and only some of their properties. You first enable a connector and set a mode with `drmModeSetCrtc` and then push new frames with `drmModePageFlip`. `drmModePageFlip` has two flags we care about: + +- `DRM_MODE_PAGE_FLIP_EVENT` tells the kernel to generate a page flip event for the crtc, which tells us when the new framebuffer has actually been set / when the old one is not needed anymore +- `DRM_MODE_PAGE_FLIP_ASYNC` tells the kernel that it should immediately apply the new framebuffer without waiting. This may cause tearing + +For dynamic power management (dpms) you set the dpms property with `drmModeObjectSetProperty` and the kernel will handle the rest behind the scenes, or fail the request. Same story with `VRR_ENABLED`, `overscan` and similar. + +With atomic modesetting all objects and properties are exposed. AMS works very differently from legacy: it has one generic function `drmModeAtomicCommit` that is used for pretty much everything. How this function works is that you first fill a `drmModeAtomicReq` with the properties you want to set, then you call `drmModeAtomicCommit` with some combination of flags. These flags decide on what the function actually does: + +- `DRM_MODE_ATOMIC_TEST_ONLY` only tests whether or not the configuration would work but is guaranteed to not change anything +- `DRM_MODE_PAGE_FLIP_EVENT` tells the kernel that it should generate a page flip event for all crtcs that we change in the commit +- `DRM_MODE_ATOMIC_NONBLOCK` tells the kernel to make this function not blocking; it should not wait until things are actually applied before returning +- `DRM_MODE_ATOMIC_ALLOW_MODESET` tells the kernel that it is allowed to make our changes happen with a modeset, that is an event that can cause the display(s) to flicker or black out for a moment +- `DRM_MODE_PAGE_FLIP_ASYNC` is currently *not* supported. All requests with this flag set fail + +Some upstream documentation can be found at https://www.kernel.org/doc/html/latest/gpu/drm-kms.html, https://01.org/linuxgraphics/gfx-docs/drm/drm-kms-properties.html and in the files at https://github.com/torvalds/linux/tree/master/drivers/gpu/drm. + +For a lot of documentation on properties and capabilities of devices there's also https://drmdb.emersion.fr/ + +# gbm + +The generic buffer manager API allows us to allocate buffers in graphics memory with a few properties. It's a relatively straight forward API: + +- `gbm_bo` is a gbm buffer. It can be manually created and destroyed +- `gbm_surface` is a gbm surface, which allows us to create an egl surface that's using gbm buffers. With it we can render in egl and then create framebuffers from the things rendered in egl and present them on the display +- the `GBM_FORMAT_*` defines are just copies of the `DRM_FORMAT_*` defines in drm_fourcc.h and describe a buffer format. For example `DRM_FORMAT_XRGB8888` describes a buffer with 8 bits of red, 8 bits of green, 8 bits of blue and 8 bits of unused alpha (that's what the `X` stands for). Do not use the `GBM_BO_FORMAT_*` enum, it can cause problems! In general, ignore the buffer formats from the gbm header and instead use what drm_fourcc.h provides +- modifiers describe the actual memory layout that needs to be assumed for accessing the buffer. Older drivers like `radeon` don't support modifiers at all, on the other end of the spectrum the NVidia driver requires them. When we don't use functions that have us explicitly provide modifiers that's called an "implicit modifier" - that means the driver automatically picks a modifier for the use case. With implicit modifiers we have no guarantees about multi-gpu compatibility by default, instead the `GBM_BO_USE_LINEAR` usage flag has to be set when creating the buffer to enforce a linear format that all drivers can access without messing up the image + +For gbm most of the upstream documentation is contained in https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/gbm/main/gbm.c diff --git a/local/recipes/kde/kwin/source/src/backends/fakeinput/CMakeLists.txt b/local/recipes/kde/kwin/source/src/backends/fakeinput/CMakeLists.txt new file mode 100644 index 0000000000..dc558146e5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/fakeinput/CMakeLists.txt @@ -0,0 +1,4 @@ +target_sources(kwin PRIVATE + fakeinputbackend.cpp + fakeinputdevice.cpp +) diff --git a/local/recipes/kde/kwin/source/src/backends/fakeinput/fakeinputbackend.cpp b/local/recipes/kde/kwin/source/src/backends/fakeinput/fakeinputbackend.cpp new file mode 100644 index 0000000000..03883f743a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/fakeinput/fakeinputbackend.cpp @@ -0,0 +1,377 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "fakeinputbackend.h" +#include "fakeinputdevice.h" +#include "keyboard_input.h" +#include "wayland/display.h" +#include "xkb.h" + +#include "wayland/qwayland-server-fake-input.h" + +#include + +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(KWIN_LIBINPUT) +Q_LOGGING_CATEGORY(KWIN_FAKEINPUT, "kwin_fakeinput", QtWarningMsg) + +namespace KWin +{ + +static const quint32 s_version = 6; + +class FakeInputBackendPrivate : public QtWaylandServer::org_kde_kwin_fake_input +{ +public: + FakeInputBackendPrivate(FakeInputBackend *q, Display *display); + + using FakeInputDeviceMap = std::map>; + + FakeInputDevice *findDevice(Resource *resource); + void removeDeviceByResource(Resource *resource); + void removeDeviceByIterator(FakeInputDeviceMap::iterator it); + static void sendKey(FakeInputDevice *device, uint32_t keyCode, KeyboardKeyState state); + + FakeInputBackend *q; + Display *display; + FakeInputDeviceMap devices; + +protected: + void org_kde_kwin_fake_input_bind_resource(Resource *resource) override; + void org_kde_kwin_fake_input_destroy_resource(Resource *resource) override; + void org_kde_kwin_fake_input_authenticate(Resource *resource, const QString &application, const QString &reason) override; + void org_kde_kwin_fake_input_pointer_motion(Resource *resource, wl_fixed_t delta_x, wl_fixed_t delta_y) override; + void org_kde_kwin_fake_input_button(Resource *resource, uint32_t button, uint32_t state) override; + void org_kde_kwin_fake_input_axis(Resource *resource, uint32_t axis, wl_fixed_t value) override; + void org_kde_kwin_fake_input_touch_down(Resource *resource, uint32_t id, wl_fixed_t x, wl_fixed_t y) override; + void org_kde_kwin_fake_input_touch_motion(Resource *resource, uint32_t id, wl_fixed_t x, wl_fixed_t y) override; + void org_kde_kwin_fake_input_touch_up(Resource *resource, uint32_t id) override; + void org_kde_kwin_fake_input_touch_cancel(Resource *resource) override; + void org_kde_kwin_fake_input_touch_frame(Resource *resource) override; + void org_kde_kwin_fake_input_pointer_motion_absolute(Resource *resource, wl_fixed_t x, wl_fixed_t y) override; + void org_kde_kwin_fake_input_keyboard_key(Resource *resource, uint32_t button, uint32_t state) override; + void org_kde_kwin_fake_input_keyboard_keysym(Resource *resource, uint32_t keysym, uint32_t state) override; + void org_kde_kwin_fake_input_destroy(Resource *resource) override; +}; + +static std::chrono::microseconds currentTime() +{ + return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()); +} + +FakeInputBackendPrivate::FakeInputBackendPrivate(FakeInputBackend *q, Display *display) + : q(q) + , display(display) +{ +} + +void FakeInputBackendPrivate::org_kde_kwin_fake_input_bind_resource(Resource *resource) +{ + auto device = new FakeInputDevice(q); + devices[resource] = std::unique_ptr(device); + Q_EMIT q->deviceAdded(device); +} + +void FakeInputBackendPrivate::org_kde_kwin_fake_input_destroy(Resource *resource) +{ + wl_resource_destroy(resource->handle); +} + +void FakeInputBackendPrivate::org_kde_kwin_fake_input_destroy_resource(Resource *resource) +{ + removeDeviceByResource(resource); +} + +FakeInputDevice *FakeInputBackendPrivate::findDevice(Resource *resource) +{ + return devices[resource].get(); +} + +void FakeInputBackendPrivate::removeDeviceByResource(Resource *resource) +{ + if (const auto it = devices.find(resource); it != devices.end()) { + removeDeviceByIterator(it); + } +} + +void FakeInputBackendPrivate::removeDeviceByIterator(FakeInputDeviceMap::iterator it) +{ + const auto device = std::move(it->second); + for (const auto button : device->pressedButtons) { + Q_EMIT device->pointerButtonChanged(button, PointerButtonState::Released, currentTime(), device.get()); + } + for (const auto key : device->pressedKeys) { + Q_EMIT device->keyChanged(key, KeyboardKeyState::Released, currentTime(), device.get()); + } + if (!device->activeTouches.empty()) { + Q_EMIT device->touchCanceled(device.get()); + } + devices.erase(it); + Q_EMIT q->deviceRemoved(device.get()); +} + +void FakeInputBackendPrivate::org_kde_kwin_fake_input_authenticate(Resource *resource, const QString &application, const QString &reason) +{ + FakeInputDevice *device = findDevice(resource); + if (device) { + // TODO: make secure + device->setAuthenticated(true); + } +} + +void FakeInputBackendPrivate::org_kde_kwin_fake_input_pointer_motion(Resource *resource, wl_fixed_t delta_x, wl_fixed_t delta_y) +{ + FakeInputDevice *device = findDevice(resource); + if (!device->isAuthenticated()) { + return; + } + const QPointF delta(wl_fixed_to_double(delta_x), wl_fixed_to_double(delta_y)); + + Q_EMIT device->pointerMotion(delta, delta, currentTime(), device); + Q_EMIT device->pointerFrame(device); +} + +void FakeInputBackendPrivate::org_kde_kwin_fake_input_button(Resource *resource, uint32_t button, uint32_t state) +{ + FakeInputDevice *device = findDevice(resource); + if (!device->isAuthenticated()) { + return; + } + + PointerButtonState nativeState; + switch (state) { + case WL_POINTER_BUTTON_STATE_PRESSED: + nativeState = PointerButtonState::Pressed; + if (device->pressedButtons.contains(button)) { + return; + } + device->pressedButtons.insert(button); + break; + case WL_POINTER_BUTTON_STATE_RELEASED: + nativeState = PointerButtonState::Released; + if (!device->pressedButtons.remove(button)) { + return; + } + break; + default: + return; + } + + Q_EMIT device->pointerButtonChanged(button, nativeState, currentTime(), device); + Q_EMIT device->pointerFrame(device); +} + +void FakeInputBackendPrivate::org_kde_kwin_fake_input_axis(Resource *resource, uint32_t axis, wl_fixed_t value) +{ + FakeInputDevice *device = findDevice(resource); + if (!device->isAuthenticated()) { + return; + } + + PointerAxis nativeAxis; + switch (axis) { + case WL_POINTER_AXIS_HORIZONTAL_SCROLL: + nativeAxis = PointerAxis::Horizontal; + break; + + case WL_POINTER_AXIS_VERTICAL_SCROLL: + nativeAxis = PointerAxis::Vertical; + break; + + default: + return; + } + + Q_EMIT device->pointerAxisChanged(nativeAxis, wl_fixed_to_double(value), 0, PointerAxisSource::Unknown, false, currentTime(), device); + Q_EMIT device->pointerFrame(device); +} + +void FakeInputBackendPrivate::org_kde_kwin_fake_input_touch_down(Resource *resource, uint32_t id, wl_fixed_t x, wl_fixed_t y) +{ + FakeInputDevice *device = findDevice(resource); + if (!device->isAuthenticated()) { + return; + } + if (device->activeTouches.contains(id)) { + return; + } + device->activeTouches.insert(id); + Q_EMIT device->touchDown(id, QPointF(wl_fixed_to_double(x), wl_fixed_to_double(y)), currentTime(), device); +} + +void FakeInputBackendPrivate::org_kde_kwin_fake_input_touch_motion(Resource *resource, uint32_t id, wl_fixed_t x, wl_fixed_t y) +{ + FakeInputDevice *device = findDevice(resource); + if (!device->isAuthenticated()) { + return; + } + if (!device->activeTouches.contains(id)) { + return; + } + Q_EMIT device->touchMotion(id, QPointF(wl_fixed_to_double(x), wl_fixed_to_double(y)), currentTime(), device); +} + +void FakeInputBackendPrivate::org_kde_kwin_fake_input_touch_up(Resource *resource, uint32_t id) +{ + FakeInputDevice *device = findDevice(resource); + if (!device->isAuthenticated()) { + return; + } + if (device->activeTouches.remove(id)) { + Q_EMIT device->touchUp(id, currentTime(), device); + } +} + +void FakeInputBackendPrivate::org_kde_kwin_fake_input_touch_cancel(Resource *resource) +{ + FakeInputDevice *device = findDevice(resource); + if (!device->isAuthenticated()) { + return; + } + device->activeTouches.clear(); + Q_EMIT device->touchCanceled(device); +} + +void FakeInputBackendPrivate::org_kde_kwin_fake_input_touch_frame(Resource *resource) +{ + FakeInputDevice *device = findDevice(resource); + if (!device->isAuthenticated()) { + return; + } + Q_EMIT device->touchFrame(device); +} + +void FakeInputBackendPrivate::org_kde_kwin_fake_input_pointer_motion_absolute(Resource *resource, wl_fixed_t x, wl_fixed_t y) +{ + FakeInputDevice *device = findDevice(resource); + if (!device->isAuthenticated()) { + return; + } + + Q_EMIT device->pointerMotionAbsolute(QPointF(wl_fixed_to_double(x), wl_fixed_to_double(y)), currentTime(), device); + Q_EMIT device->pointerFrame(device); +} + +void FakeInputBackendPrivate::org_kde_kwin_fake_input_keyboard_key(Resource *resource, uint32_t key, uint32_t state) +{ + FakeInputDevice *device = findDevice(resource); + if (!device->isAuthenticated()) { + return; + } + KeyboardKeyState nativeState; + switch (state) { + case WL_KEYBOARD_KEY_STATE_PRESSED: + nativeState = KeyboardKeyState::Pressed; + break; + + case WL_KEYBOARD_KEY_STATE_RELEASED: + nativeState = KeyboardKeyState::Released; + break; + + default: + return; + } + sendKey(device, key, nativeState); +} + +void FakeInputBackendPrivate::org_kde_kwin_fake_input_keyboard_keysym(Resource *resource, uint32_t keySym, uint32_t state) +{ + FakeInputDevice *device = findDevice(resource); + if (!device->isAuthenticated()) { + return; + } + + KeyboardKeyState nativeState; + switch (state) { + case WL_KEYBOARD_KEY_STATE_PRESSED: + nativeState = KeyboardKeyState::Pressed; + break; + case WL_KEYBOARD_KEY_STATE_RELEASED: + nativeState = KeyboardKeyState::Released; + break; + default: + return; + } + + std::optional keyCode = input()->keyboard()->xkb()->keycodeFromKeysym(keySym); + if (keyCode) { + // grab the current modifier state, cache it, send our key with our own modifiers at a known state, then reset back + xkb_state *state = input()->keyboard()->xkb()->state(); + xkb_mod_mask_t formerDepressed = xkb_state_serialize_mods(state, XKB_STATE_MODS_DEPRESSED); + xkb_mod_mask_t formerLatched = xkb_state_serialize_mods(state, XKB_STATE_MODS_LATCHED); + xkb_mod_mask_t formerLocked = xkb_state_serialize_mods(state, XKB_STATE_MODS_LOCKED); + xkb_layout_index_t formerLayout = xkb_state_serialize_layout(state, XKB_STATE_LAYOUT_EFFECTIVE); + + input()->keyboard()->xkb()->updateModifiers(keyCode->modifiers, 0, 0, 0); + sendKey(device, keyCode->keyCode, nativeState); + + input()->keyboard()->xkb()->updateModifiers(formerDepressed, formerLatched, formerLocked, formerLayout); + return; + } + + // otherwise create a new keymap and send that one key + // clients don't seem to like having a keymap change whilst a key is pressed + // for now send a fake release with every press and ignore other releases. We can make the keymap resetting more lazy if it's an issue IRL + if (nativeState == KeyboardKeyState::Pressed) { + static const uint unmappedKeyCode = 247; + bool keymapUpdated = input()->keyboard()->xkb()->updateToKeymapForKeySym(unmappedKeyCode, keySym); + if (!keymapUpdated) { + return; + } + sendKey(device, unmappedKeyCode, KeyboardKeyState::Pressed); + + for (quint32 key : std::as_const(device->pressedKeys)) { + sendKey(device, key, KeyboardKeyState::Released); + } + // reset keyboard back + input()->keyboard()->xkb()->reconfigure(); + } +} + +void FakeInputBackendPrivate::sendKey(FakeInputDevice *device, uint32_t keyCode, KeyboardKeyState state) +{ + switch (state) { + case KeyboardKeyState::Pressed: + if (device->pressedKeys.contains(keyCode)) { + return; + } + device->pressedKeys.insert(keyCode); + break; + + case KeyboardKeyState::Released: + if (!device->pressedKeys.remove(keyCode)) { + return; + } + break; + default: + return; + } + + Q_EMIT device->keyChanged(keyCode, state, currentTime(), device); +} + +FakeInputBackend::FakeInputBackend(Display *display) + : d(std::make_unique(this, display)) +{ +} + +FakeInputBackend::~FakeInputBackend() +{ + while (!d->devices.empty()) { + d->removeDeviceByIterator(d->devices.begin()); + } +} + +void FakeInputBackend::initialize() +{ + d->init(*d->display, s_version); +} + +} // namespace KWin + +#include "moc_fakeinputbackend.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/fakeinput/fakeinputbackend.h b/local/recipes/kde/kwin/source/src/backends/fakeinput/fakeinputbackend.h new file mode 100644 index 0000000000..b734515a1f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/fakeinput/fakeinputbackend.h @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/inputbackend.h" + +#include +#include + +namespace KWin +{ + +class Display; +class FakeInputBackendPrivate; +class FakeInputDevice; + +class FakeInputBackend : public InputBackend +{ + Q_OBJECT + +public: + explicit FakeInputBackend(Display *display); + ~FakeInputBackend(); + + void initialize() override; + +private: + std::unique_ptr d; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/backends/fakeinput/fakeinputdevice.cpp b/local/recipes/kde/kwin/source/src/backends/fakeinput/fakeinputdevice.cpp new file mode 100644 index 0000000000..6eb00cec62 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/fakeinput/fakeinputdevice.cpp @@ -0,0 +1,85 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "fakeinputdevice.h" + +namespace KWin +{ +static int s_lastDeviceId = 0; + +FakeInputDevice::FakeInputDevice(QObject *parent) + : InputDevice(parent) + , m_name(QStringLiteral("Fake Input Device %1").arg(++s_lastDeviceId)) +{ +} + +bool FakeInputDevice::isAuthenticated() const +{ + return m_authenticated; +} + +void FakeInputDevice::setAuthenticated(bool authenticated) +{ + m_authenticated = authenticated; +} + +QString FakeInputDevice::name() const +{ + return m_name; +} + +bool FakeInputDevice::isEnabled() const +{ + return true; +} + +void FakeInputDevice::setEnabled(bool enabled) +{ +} + +bool FakeInputDevice::isKeyboard() const +{ + return true; +} + +bool FakeInputDevice::isPointer() const +{ + return true; +} + +bool FakeInputDevice::isTouchpad() const +{ + return false; +} + +bool FakeInputDevice::isTouch() const +{ + return true; +} + +bool FakeInputDevice::isTabletTool() const +{ + return false; +} + +bool FakeInputDevice::isTabletPad() const +{ + return false; +} + +bool FakeInputDevice::isTabletModeSwitch() const +{ + return false; +} + +bool FakeInputDevice::isLidSwitch() const +{ + return false; +} + +} // namespace KWin + +#include "moc_fakeinputdevice.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/fakeinput/fakeinputdevice.h b/local/recipes/kde/kwin/source/src/backends/fakeinput/fakeinputdevice.h new file mode 100644 index 0000000000..e1dee070f4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/fakeinput/fakeinputdevice.h @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/inputdevice.h" + +#include + +namespace KWin +{ + +class KWIN_EXPORT FakeInputDevice : public InputDevice +{ + Q_OBJECT + +public: + explicit FakeInputDevice(QObject *parent = nullptr); + + QString name() const override; + + bool isEnabled() const override; + void setEnabled(bool enabled) override; + + bool isKeyboard() const override; + bool isPointer() const override; + bool isTouchpad() const override; + bool isTouch() const override; + bool isTabletTool() const override; + bool isTabletPad() const override; + bool isTabletModeSwitch() const override; + bool isLidSwitch() const override; + + void setAuthenticated(bool authenticated); + bool isAuthenticated() const; + + QSet pressedButtons; + QSet pressedKeys; + QSet activeTouches; + +private: + QString m_name; + bool m_authenticated = false; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/backends/libinput/CMakeLists.txt b/local/recipes/kde/kwin/source/src/backends/libinput/CMakeLists.txt new file mode 100644 index 0000000000..1a19d1cb23 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/libinput/CMakeLists.txt @@ -0,0 +1,25 @@ +qt_generate_dbus_interface(device.h org.kde.kwin.InputDevice.xml OPTIONS -A) + +add_custom_target( + KWinInputDBusInterfaces + ALL + DEPENDS + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.kwin.InputDevice.xml +) + +install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.kwin.InputDevice.xml + DESTINATION + ${KDE_INSTALL_DBUSINTERFACEDIR} +) + +target_sources(kwin PRIVATE + connection.cpp + context.cpp + device.cpp + events.cpp + libinput_logging.cpp + libinputbackend.cpp +) +target_link_libraries(kwin PRIVATE Libinput::Libinput) diff --git a/local/recipes/kde/kwin/source/src/backends/libinput/connection.cpp b/local/recipes/kde/kwin/source/src/backends/libinput/connection.cpp new file mode 100644 index 0000000000..7ede0351f2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/libinput/connection.cpp @@ -0,0 +1,772 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "connection.h" +#include "context.h" +#include "core/backendoutput.h" +#include "device.h" +#include "events.h" + +// TODO: Make it compile also in testing environment +#ifndef KWIN_BUILD_TESTING +#include "core/output.h" +#include "core/outputbackend.h" +#include "main.h" +#include "window.h" +#include "workspace.h" +#endif + +#include "core/session.h" +#include "input_event.h" +#include "libinput_logging.h" +#include "utils/realtime.h" +#include "utils/udev.h" + +#include +#include +#include + +#include +#include +#include + +namespace KWin +{ +namespace LibInput +{ + +class ConnectionAdaptor : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.KWin.InputDeviceManager") + Q_PROPERTY(QStringList devicesSysNames READ devicesSysNames CONSTANT) + +private: + Connection *m_con; + +public: + ConnectionAdaptor(Connection *con) + : QObject(con) + , m_con(con) + { + connect(con, &Connection::deviceAdded, this, [this](LibInput::Device *inputDevice) { + Q_EMIT deviceAdded(inputDevice->sysName()); + }); + connect(con, &Connection::deviceRemoved, this, [this](LibInput::Device *inputDevice) { + Q_EMIT deviceRemoved(inputDevice->sysName()); + }); + + QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/kde/KWin/InputDevice"), + QStringLiteral("org.kde.KWin.InputDeviceManager"), + this, + QDBusConnection::ExportAllProperties | QDBusConnection::ExportAllSignals | QDBusConnection::ExportScriptableContents); + } + + ~ConnectionAdaptor() override + { + QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/org/kde/KWin/InputDeviceManager")); + } + + QStringList devicesSysNames() + { + return m_con->devicesSysNames(); + } + + Q_SCRIPTABLE QStringList ListPointers() const + { + return m_con->ListPointers(); + } + Q_SCRIPTABLE QStringList ListKeyboards() const + { + return m_con->ListKeyboards(); + } + Q_SCRIPTABLE QStringList ListTouch() const + { + return m_con->ListTouch(); + } + +Q_SIGNALS: + void deviceAdded(QString sysName); + void deviceRemoved(QString sysName); +}; + +Connection *Connection::create(Session *session) +{ + std::unique_ptr udev = std::make_unique(); + if (!udev->isValid()) { + qCWarning(KWIN_LIBINPUT) << "Failed to initialize udev"; + return nullptr; + } + std::unique_ptr context = std::make_unique(session, std::move(udev)); + if (!context->isValid()) { + qCWarning(KWIN_LIBINPUT) << "Failed to create context from udev"; + return nullptr; + } + if (!context->initialize()) { + qCWarning(KWIN_LIBINPUT) << "Failed to initialize context"; + return nullptr; + } + return new Connection(std::move(context)); +} + +Connection::Connection(std::unique_ptr &&input) + : m_notifier(nullptr) + , m_connectionAdaptor(std::make_unique(this)) + , m_input(std::move(input)) +{ + Q_ASSERT(m_input); + // need to connect to KGlobalSettings as the mouse KCM does not emit a dedicated signal + QDBusConnection::sessionBus().connect(QString(), QStringLiteral("/KGlobalSettings"), QStringLiteral("org.kde.KGlobalSettings"), + QStringLiteral("notifyChange"), this, SLOT(slotKGlobalSettingsNotifyChange(int, int))); +} + +Connection::~Connection() +{ + m_eventQueue.clear(); + qDeleteAll(m_devices); + qDeleteAll(m_tools); +} + +void Connection::setup() +{ + QMetaObject::invokeMethod(this, &Connection::doSetup, Qt::QueuedConnection); +} + +void Connection::doSetup() +{ + Q_ASSERT(!m_notifier); + + gainRealTime(); + + m_notifier = std::make_unique(m_input->fileDescriptor(), QSocketNotifier::Read); + connect(m_notifier.get(), &QSocketNotifier::activated, this, &Connection::handleEvent); + + connect(m_input->session(), &Session::activeChanged, this, [this](bool active) { + if (active) { + if (!m_input->isSuspended()) { + return; + } + m_input->resume(); + } else { + deactivate(); + } + }); + handleEvent(); +} + +void Connection::deactivate() +{ + if (m_input->isSuspended()) { + return; + } + m_input->suspend(); + handleEvent(); +} + +void Connection::handleEvent() +{ + QMutexLocker locker(&m_mutex); + const bool wasEmpty = m_eventQueue.empty(); + do { + m_input->dispatch(); + std::unique_ptr event = m_input->event(); + if (!event) { + break; + } + m_eventQueue.push_back(std::move(event)); + } while (true); + if (wasEmpty && !m_eventQueue.empty()) { + Q_EMIT eventsRead(); + } +} + +#ifndef KWIN_BUILD_TESTING +QPointF devicePointToGlobalPosition(const QPointF &devicePos, const BackendOutput *output) +{ + QPointF pos = devicePos; + // TODO: Do we need to handle the flipped cases differently? + switch (output->transform().kind()) { + case OutputTransform::Normal: + case OutputTransform::FlipX: + break; + case OutputTransform::Rotate90: + case OutputTransform::FlipX90: + pos = QPointF(output->modeSize().height() - devicePos.y(), devicePos.x()); + break; + case OutputTransform::Rotate180: + case OutputTransform::FlipX180: + pos = QPointF(output->modeSize().width() - devicePos.x(), + output->modeSize().height() - devicePos.y()); + break; + case OutputTransform::Rotate270: + case OutputTransform::FlipX270: + pos = QPointF(devicePos.y(), output->modeSize().width() - devicePos.x()); + break; + default: + Q_UNREACHABLE(); + } + pos -= output->deviceOffset(); + pos = pos / output->scale(); + + auto logicalOutput = workspace()->findOutput(output); + if (!logicalOutput) { + qWarning() << "Could not find logical output for " << output; + return {}; + } + const QRectF geo = logicalOutput->geometryF(); + pos += geo.topLeft(); + return QPointF(std::clamp(pos.x(), geo.x(), geo.x() + geo.width() - 1), + std::clamp(pos.y(), geo.y(), geo.y() + geo.height() - 1)); +} +#endif + +static QPointF tabletToolPosition(TabletToolEvent *event) +{ +#ifndef KWIN_BUILD_TESTING + if (event->device()->isMapToWorkspace()) { + return workspace()->geometry().topLeft() + event->transformedPosition(workspace()->geometry().size()); + } else { + BackendOutput *backendOutput = event->device()->output(); + if (backendOutput) { + return devicePointToGlobalPosition(event->transformedPosition(backendOutput->modeSize()), backendOutput); + } + BackendOutput *output = workspace()->activeOutput()->backendOutput(); + return devicePointToGlobalPosition(event->transformedPosition(output->modeSize()), output); + } +#else + return QPointF(); +#endif +} + +TabletTool *Connection::getOrCreateTool(libinput_tablet_tool *handle) +{ + for (TabletTool *tool : std::as_const(m_tools)) { + if (tool->handle() == handle) { + return tool; + } + } + + auto tool = new TabletTool(handle); + tool->moveToThread(thread()); + m_tools.append(tool); + + return tool; +} + +void Connection::processEvents() +{ + QMutexLocker locker(&m_mutex); + while (m_eventQueue.size() != 0) { + std::unique_ptr event = std::move(m_eventQueue.front()); + m_eventQueue.pop_front(); + switch (event->type()) { + case LIBINPUT_EVENT_DEVICE_ADDED: { + auto device = new Device(event->nativeDevice()); + device->moveToThread(thread()); + m_devices << device; + + applyDeviceConfig(device); + applyScreenToDevice(device); + + connect(device, &Device::outputNameChanged, this, [this, device] { + // If the output name changes from something to empty we need to + // re-run the assignment heuristic so that an output is assigned + if (device->outputName().isEmpty()) { + applyScreenToDevice(device); + } + }); + + Q_EMIT deviceAdded(device); + break; + } + case LIBINPUT_EVENT_DEVICE_REMOVED: { + auto it = std::ranges::find_if(std::as_const(m_devices), [&event](Device *d) { + return event->device() == d; + }); + if (it == m_devices.cend()) { + // we don't know this device + break; + } + auto device = *it; + m_devices.erase(it); + Q_EMIT deviceRemoved(device); + device->deleteLater(); + break; + } + case LIBINPUT_EVENT_KEYBOARD_KEY: { + KeyEvent *ke = static_cast(event.get()); + const int seatKeyCount = libinput_event_keyboard_get_seat_key_count(*ke); + const int keyState = libinput_event_keyboard_get_key_state(*ke); + if ((keyState == LIBINPUT_KEY_STATE_PRESSED && seatKeyCount != 1) || + (keyState == LIBINPUT_KEY_STATE_RELEASED && seatKeyCount != 0)) { + break; + } + Q_EMIT ke->device()->keyChanged(ke->key(), ke->state(), ke->time(), ke->device()); + break; + } + case LIBINPUT_EVENT_POINTER_SCROLL_WHEEL: { + const PointerEvent *pointerEvent = static_cast(event.get()); + const auto axes = pointerEvent->axis(); + for (const PointerAxis &axis : axes) { + Q_EMIT pointerEvent->device()->pointerAxisChanged(axis, + pointerEvent->scrollValue(axis), + pointerEvent->scrollValueV120(axis), + PointerAxisSource::Wheel, + pointerEvent->device()->isNaturalScroll(), + pointerEvent->time(), + pointerEvent->device()); + } + Q_EMIT pointerEvent->device()->pointerFrame(pointerEvent->device()); + break; + } + case LIBINPUT_EVENT_POINTER_SCROLL_FINGER: { + const PointerEvent *pointerEvent = static_cast(event.get()); + const auto axes = pointerEvent->axis(); + for (const PointerAxis &axis : axes) { + Q_EMIT pointerEvent->device()->pointerAxisChanged(axis, + pointerEvent->scrollValue(axis), + 0, + PointerAxisSource::Finger, + pointerEvent->device()->isNaturalScroll(), + pointerEvent->time(), + pointerEvent->device()); + } + Q_EMIT pointerEvent->device()->pointerFrame(pointerEvent->device()); + break; + } + case LIBINPUT_EVENT_POINTER_SCROLL_CONTINUOUS: { + const PointerEvent *pointerEvent = static_cast(event.get()); + const auto axes = pointerEvent->axis(); + for (const PointerAxis &axis : axes) { + Q_EMIT pointerEvent->device()->pointerAxisChanged(axis, + pointerEvent->scrollValue(axis), + 0, + PointerAxisSource::Continuous, + pointerEvent->device()->isNaturalScroll(), + pointerEvent->time(), + pointerEvent->device()); + } + Q_EMIT pointerEvent->device()->pointerFrame(pointerEvent->device()); + break; + } + case LIBINPUT_EVENT_POINTER_BUTTON: { + PointerEvent *pe = static_cast(event.get()); + const int seatButtonCount = libinput_event_pointer_get_seat_button_count(*pe); + const int buttonState = libinput_event_pointer_get_button_state(*pe); + if ((buttonState == LIBINPUT_BUTTON_STATE_PRESSED && seatButtonCount != 1) || + (buttonState == LIBINPUT_BUTTON_STATE_RELEASED && seatButtonCount != 0)) { + break; + } + Q_EMIT pe->device()->pointerButtonChanged(pe->button(), pe->buttonState(), pe->time(), pe->device()); + Q_EMIT pe->device()->pointerFrame(pe->device()); + break; + } + case LIBINPUT_EVENT_POINTER_MOTION: { + PointerEvent *pe = static_cast(event.get()); + auto delta = pe->delta(); + auto deltaNonAccel = pe->deltaUnaccelerated(); + auto latestTime = pe->time(); + auto it = m_eventQueue.begin(); + while (it != m_eventQueue.end()) { + if ((*it)->type() == LIBINPUT_EVENT_POINTER_MOTION) { + std::unique_ptr p{static_cast(it->release())}; + delta += p->delta(); + deltaNonAccel += p->deltaUnaccelerated(); + latestTime = p->time(); + it = m_eventQueue.erase(it); + } else { + break; + } + } + Q_EMIT pe->device()->pointerMotion(delta, deltaNonAccel, latestTime, pe->device()); + Q_EMIT pe->device()->pointerFrame(pe->device()); + break; + } + case LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE: { + PointerEvent *pe = static_cast(event.get()); + if (workspace()) { + Q_EMIT pe->device()->pointerMotionAbsolute(pe->absolutePos(workspace()->geometry().size()), pe->time(), pe->device()); + Q_EMIT pe->device()->pointerFrame(pe->device()); + } + break; + } + case LIBINPUT_EVENT_TOUCH_DOWN: { +#ifndef KWIN_BUILD_TESTING + TouchEvent *te = static_cast(event.get()); + const auto *output = te->device()->output(); + if (!output) { + qCWarning(KWIN_LIBINPUT) << "Touch down received for device with no output assigned"; + break; + } + const QPointF globalPos = devicePointToGlobalPosition(te->absolutePos(output->modeSize()), output); + Q_EMIT te->device()->touchDown(te->id(), globalPos, te->time(), te->device()); + break; +#endif + } + case LIBINPUT_EVENT_TOUCH_UP: { + TouchEvent *te = static_cast(event.get()); + const auto *output = te->device()->output(); + if (!output) { + break; + } + Q_EMIT te->device()->touchUp(te->id(), te->time(), te->device()); + break; + } + case LIBINPUT_EVENT_TOUCH_MOTION: { +#ifndef KWIN_BUILD_TESTING + TouchEvent *te = static_cast(event.get()); + const auto *output = te->device()->output(); + if (!output) { + break; + } + const QPointF globalPos = devicePointToGlobalPosition(te->absolutePos(output->modeSize()), output); + Q_EMIT te->device()->touchMotion(te->id(), globalPos, te->time(), te->device()); + break; +#endif + } + case LIBINPUT_EVENT_TOUCH_CANCEL: { + Q_EMIT event->device()->touchCanceled(event->device()); + break; + } + case LIBINPUT_EVENT_TOUCH_FRAME: { + Q_EMIT event->device()->touchFrame(event->device()); + break; + } + case LIBINPUT_EVENT_GESTURE_PINCH_BEGIN: { + PinchGestureEvent *pe = static_cast(event.get()); + Q_EMIT pe->device()->pinchGestureBegin(pe->fingerCount(), pe->time(), pe->device()); + break; + } + case LIBINPUT_EVENT_GESTURE_PINCH_UPDATE: { + PinchGestureEvent *pe = static_cast(event.get()); + Q_EMIT pe->device()->pinchGestureUpdate(pe->scale(), pe->angleDelta(), pe->delta(), pe->time(), pe->device()); + break; + } + case LIBINPUT_EVENT_GESTURE_PINCH_END: { + PinchGestureEvent *pe = static_cast(event.get()); + if (pe->isCancelled()) { + Q_EMIT pe->device()->pinchGestureCancelled(pe->time(), pe->device()); + } else { + Q_EMIT pe->device()->pinchGestureEnd(pe->time(), pe->device()); + } + break; + } + case LIBINPUT_EVENT_GESTURE_SWIPE_BEGIN: { + SwipeGestureEvent *se = static_cast(event.get()); + Q_EMIT se->device()->swipeGestureBegin(se->fingerCount(), se->time(), se->device()); + break; + } + case LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE: { + SwipeGestureEvent *se = static_cast(event.get()); + Q_EMIT se->device()->swipeGestureUpdate(se->delta(), se->time(), se->device()); + break; + } + case LIBINPUT_EVENT_GESTURE_SWIPE_END: { + SwipeGestureEvent *se = static_cast(event.get()); + if (se->isCancelled()) { + Q_EMIT se->device()->swipeGestureCancelled(se->time(), se->device()); + } else { + Q_EMIT se->device()->swipeGestureEnd(se->time(), se->device()); + } + break; + } + case LIBINPUT_EVENT_GESTURE_HOLD_BEGIN: { + HoldGestureEvent *he = static_cast(event.get()); + Q_EMIT he->device()->holdGestureBegin(he->fingerCount(), he->time(), he->device()); + break; + } + case LIBINPUT_EVENT_GESTURE_HOLD_END: { + HoldGestureEvent *he = static_cast(event.get()); + if (he->isCancelled()) { + Q_EMIT he->device()->holdGestureCancelled(he->time(), he->device()); + } else { + Q_EMIT he->device()->holdGestureEnd(he->time(), he->device()); + } + break; + } + case LIBINPUT_EVENT_SWITCH_TOGGLE: { + SwitchEvent *se = static_cast(event.get()); + Q_EMIT se->device()->switchToggle(se->state(), se->time(), se->device()); + break; + } + case LIBINPUT_EVENT_TABLET_TOOL_AXIS: { + auto *tte = static_cast(event.get()); + if (libinput_tablet_tool_config_pressure_range_is_available(tte->tool())) { + tte->device()->setSupportsPressureRange(true); + libinput_tablet_tool_config_pressure_range_set(tte->tool(), tte->device()->pressureRangeMin(), tte->device()->pressureRangeMax()); + } + + if (event->device()->tabletToolIsRelative()) { + Q_EMIT event->device()->tabletToolAxisEventRelative(tte->delta(), + tte->device()->pressureCurve().valueForProgress(tte->pressure()), + tte->xTilt(), + tte->yTilt(), + tte->rotation(), + tte->distance(), + tte->isTipDown(), + tte->sliderPosition(), + getOrCreateTool(tte->tool()), + tte->time(), + tte->device()); + } else { + Q_EMIT event->device()->tabletToolAxisEvent(tabletToolPosition(tte), + tte->device()->pressureCurve().valueForProgress(tte->pressure()), + tte->xTilt(), + tte->yTilt(), + tte->rotation(), + tte->distance(), + tte->isTipDown(), + tte->sliderPosition(), + getOrCreateTool(tte->tool()), + tte->time(), + tte->device()); + } + break; + } + case LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY: { + auto *tte = static_cast(event.get()); + if (libinput_tablet_tool_config_pressure_range_is_available(tte->tool())) { + tte->device()->setSupportsPressureRange(true); + libinput_tablet_tool_config_pressure_range_set(tte->tool(), tte->device()->pressureRangeMin(), tte->device()->pressureRangeMax()); + } + Q_EMIT event->device()->tabletToolProximityEvent(tabletToolPosition(tte), + tte->xTilt(), + tte->yTilt(), + tte->rotation(), + tte->distance(), + tte->isNearby(), + tte->sliderPosition(), + getOrCreateTool(tte->tool()), + tte->time(), + tte->device()); + break; + } + case LIBINPUT_EVENT_TABLET_TOOL_TIP: { + auto *tte = static_cast(event.get()); + if (libinput_tablet_tool_config_pressure_range_is_available(tte->tool())) { + tte->device()->setSupportsPressureRange(true); + libinput_tablet_tool_config_pressure_range_set(tte->tool(), tte->device()->pressureRangeMin(), tte->device()->pressureRangeMax()); + } + Q_EMIT event->device()->tabletToolTipEvent(tabletToolPosition(tte), + tte->device()->pressureCurve().valueForProgress(tte->pressure()), + tte->xTilt(), + tte->yTilt(), + tte->rotation(), + tte->distance(), + tte->isTipDown(), + tte->sliderPosition(), + getOrCreateTool(tte->tool()), + tte->time(), + tte->device()); + break; + } + case LIBINPUT_EVENT_TABLET_TOOL_BUTTON: { + auto *tabletEvent = static_cast(event.get()); + Q_EMIT event->device()->tabletToolButtonEvent(tabletEvent->buttonId(), + tabletEvent->isButtonPressed(), + getOrCreateTool(tabletEvent->tool()), tabletEvent->time(), tabletEvent->device()); + break; + } + case LIBINPUT_EVENT_TABLET_PAD_BUTTON: { + auto *tabletEvent = static_cast(event.get()); + Q_EMIT event->device()->tabletPadButtonEvent(tabletEvent->buttonId(), + tabletEvent->isButtonPressed(), + tabletEvent->group(), + tabletEvent->mode(), + tabletEvent->isModeSwitch(), + tabletEvent->time(), tabletEvent->device()); + break; + } + case LIBINPUT_EVENT_TABLET_PAD_RING: { + auto *tabletEvent = static_cast(event.get()); + Q_EMIT event->device()->tabletPadRingEvent(tabletEvent->number(), + tabletEvent->position(), + tabletEvent->source() == LIBINPUT_TABLET_PAD_RING_SOURCE_FINGER, + tabletEvent->group(), + tabletEvent->mode(), + tabletEvent->time(), tabletEvent->device()); + break; + } + case LIBINPUT_EVENT_TABLET_PAD_STRIP: { + auto *tabletEvent = static_cast(event.get()); + Q_EMIT event->device()->tabletPadStripEvent(tabletEvent->number(), + tabletEvent->position(), + tabletEvent->source() == LIBINPUT_TABLET_PAD_STRIP_SOURCE_FINGER, + tabletEvent->group(), + tabletEvent->mode(), + tabletEvent->time(), tabletEvent->device()); + break; + } + case LIBINPUT_EVENT_TABLET_PAD_DIAL: { + auto *tabletEvent = static_cast(event.get()); + Q_EMIT event->device()->tabletPadDialEvent(tabletEvent->number(), tabletEvent->delta(), tabletEvent->group(), tabletEvent->time(), tabletEvent->device()); + break; + } + default: + // nothing + break; + } + } +} + +void Connection::updateScreens() +{ + QMutexLocker locker(&m_mutex); + for (auto device : std::as_const(m_devices)) { + applyScreenToDevice(device); + } +} + +void Connection::applyScreenToDevice(Device *device) +{ +#ifndef KWIN_BUILD_TESTING + QMutexLocker locker(&m_mutex); + if (!device->isTouch() && !device->isTabletTool()) { + return; + } + + BackendOutput *deviceOutput = nullptr; + const QList outputs = kwinApp()->outputBackend()->outputs(); + + // let's try to find a screen for it + if (!device->outputUuid().isEmpty()) { + // use the UUID if possible, which is more stable than the output name + const auto it = std::ranges::find_if(outputs, [device](BackendOutput *output) { + return output->uuid() == device->outputUuid(); + }); + deviceOutput = it == outputs.end() ? nullptr : *it; + } + if (!deviceOutput && !device->outputName().isEmpty()) { + // we have an output name, try to find a screen with matching name + for (BackendOutput *output : outputs) { + if (output->name() == device->outputName()) { + deviceOutput = output; + break; + } + } + } + if (!deviceOutput && device->isTouch()) { + // do we have an internal screen? + BackendOutput *internalOutput = nullptr; + for (BackendOutput *output : outputs) { + if (output->isInternal()) { + internalOutput = output; + break; + } + } + auto testScreenMatches = [device](const BackendOutput *output) { + const auto &size = device->size(); + const auto &screenSize = output->physicalSize(); + return std::round(size.width()) == std::round(screenSize.width()) + && std::round(size.height()) == std::round(screenSize.height()); + }; + if (internalOutput && testScreenMatches(internalOutput)) { + deviceOutput = internalOutput; + } + // let's compare all screens for size + for (BackendOutput *output : outputs) { + if (testScreenMatches(output)) { + deviceOutput = output; + break; + } + } + if (!deviceOutput) { + // still not found + if (internalOutput) { + // we have an internal id, so let's use that + deviceOutput = internalOutput; + } else { + // just take first screen, we have no clue + deviceOutput = outputs.front(); + } + } + } + + device->setOutput(deviceOutput); + + // TODO: this is currently non-functional even on DRM. Needs orientation() override there. + device->setOrientation(Qt::PrimaryOrientation); +#endif +} + +void Connection::applyDeviceConfig(Device *device) +{ + KConfigGroup defaults = m_config->group(QStringLiteral("Libinput")).group(QStringLiteral("Defaults")); + if (defaults.isValid()) { + if (device->isAlphaNumericKeyboard() && defaults.hasGroup(QStringLiteral("Keyboard"))) { + defaults = defaults.group(QStringLiteral("Keyboard")); + } else if (device->isTouchpad() && defaults.hasGroup(QStringLiteral("Touchpad"))) { + // A Touchpad is a Pointer, so we need to check for it before Pointer. + defaults = defaults.group(QStringLiteral("Touchpad")); + } else if (device->isPointer() && defaults.hasGroup(QStringLiteral("Pointer"))) { + defaults = defaults.group(QStringLiteral("Pointer")); + } + + device->setDefaultConfig(defaults); + } + + // pass configuration to Device + device->setConfig(m_config->group(QStringLiteral("Libinput")).group(QString::number(device->vendor())).group(QString::number(device->product())).group(device->name())); + device->loadConfiguration(); +} + +void Connection::slotKGlobalSettingsNotifyChange(int type, int arg) +{ + if (type == 3 /**SettingsChanged**/ && arg == 0 /** SETTINGS_MOUSE */) { + m_config->reparseConfiguration(); + for (auto it = m_devices.constBegin(), end = m_devices.constEnd(); it != end; ++it) { + if ((*it)->isPointer()) { + applyDeviceConfig(*it); + } + } + } +} + +QList Connection::devices() const +{ + return m_devices; +} + +QStringList Connection::devicesSysNames() const +{ + QStringList sl; + for (Device *d : std::as_const(m_devices)) { + sl.append(d->sysName()); + } + return sl; +} + +QStringList Connection::ListPointers() const +{ + return m_devices + | std::views::filter(&Device::isPointer) + | std::views::transform(&Device::sysName) + | std::ranges::to(); +} + +QStringList Connection::ListKeyboards() const +{ + return m_devices + | std::views::filter(&Device::isKeyboard) + | std::views::transform(&Device::sysName) + | std::ranges::to(); +} + +QStringList Connection::ListTouch() const +{ + return m_devices + | std::views::filter(&Device::isTouch) + | std::views::transform(&Device::sysName) + | std::ranges::to(); +} +} +} + +#include "connection.moc" + +#include "moc_connection.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/libinput/connection.h b/local/recipes/kde/kwin/source/src/backends/libinput/connection.h new file mode 100644 index 0000000000..0eccd47358 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/libinput/connection.h @@ -0,0 +1,98 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "effect/globals.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +class QSocketNotifier; +class QThread; + +struct libinput_tablet_tool; + +namespace KWin +{ + +class Session; +class Udev; + +namespace LibInput +{ + +class Event; +class Device; +class Context; +class ConnectionAdaptor; +class TabletTool; + +class KWIN_EXPORT Connection : public QObject +{ + Q_OBJECT + +public: + ~Connection() override; + + void setInputConfig(const KSharedConfigPtr &config) + { + m_config = config; + } + + void setup(); + void updateScreens(); + void deactivate(); + void processEvents(); + + QList devices() const; + QStringList devicesSysNames() const; + + QStringList ListPointers() const; + QStringList ListKeyboards() const; + QStringList ListTouch() const; + + static Connection *create(Session *session); + +Q_SIGNALS: + void deviceAdded(KWin::LibInput::Device *); + void deviceRemoved(KWin::LibInput::Device *); + + void eventsRead(); + +private Q_SLOTS: + void slotKGlobalSettingsNotifyChange(int type, int arg); + +private: + Connection(std::unique_ptr &&input); + void handleEvent(); + void applyDeviceConfig(Device *device); + void applyScreenToDevice(Device *device); + void doSetup(); + TabletTool *getOrCreateTool(libinput_tablet_tool *tool); + + std::unique_ptr m_notifier; + QRecursiveMutex m_mutex; + std::deque> m_eventQueue; + QList m_devices; + QList m_tools; + KSharedConfigPtr m_config; + std::unique_ptr m_connectionAdaptor; + std::unique_ptr m_input; + std::unique_ptr m_udev; +}; + +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/libinput/context.cpp b/local/recipes/kde/kwin/source/src/backends/libinput/context.cpp new file mode 100644 index 0000000000..ae0cf02e55 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/libinput/context.cpp @@ -0,0 +1,196 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "context.h" +#include "events.h" +#include "libinput_logging.h" + +#include "core/session.h" +#include "utils/udev.h" + +#include +#include + +#include + +namespace KWin +{ +namespace LibInput +{ + +static void libinputLogHandler(libinput *libinput, libinput_log_priority priority, const char *format, va_list args) +{ + char buf[1024]; + if (std::vsnprintf(buf, 1023, format, args) <= 0) { + return; + } + switch (priority) { + case LIBINPUT_LOG_PRIORITY_DEBUG: + qCDebug(KWIN_LIBINPUT) << "Libinput:" << buf; + break; + case LIBINPUT_LOG_PRIORITY_INFO: + qCInfo(KWIN_LIBINPUT) << "Libinput:" << buf; + break; + case LIBINPUT_LOG_PRIORITY_ERROR: + default: + qCCritical(KWIN_LIBINPUT) << "Libinput:" << buf; + break; + } +} + +Context::Context(Session *session, std::unique_ptr &&udev) + : m_session(session) + , m_libinput(libinput_udev_create_context(&Context::s_interface, this, *udev.get())) + , m_suspended(false) + , m_udev(std::move(udev)) +{ + libinput_log_set_priority(m_libinput, LIBINPUT_LOG_PRIORITY_DEBUG); + libinput_log_set_handler(m_libinput, &libinputLogHandler); +} + +Context::~Context() +{ + if (m_libinput) { + libinput_unref(m_libinput); + } +} + +bool Context::initialize() +{ + if (!isValid()) { + return false; + } + return libinput_udev_assign_seat(m_libinput, m_session->seat().toUtf8().constData()) == 0; +} + +Session *Context::session() const +{ + return m_session; +} + +int Context::fileDescriptor() +{ + if (!isValid()) { + return -1; + } + return libinput_get_fd(m_libinput); +} + +void Context::dispatch() +{ + libinput_dispatch(m_libinput); +} + +const struct libinput_interface Context::s_interface = { + Context::openRestrictedCallback, + Context::closeRestrictedCallBack, +}; + +int Context::openRestrictedCallback(const char *path, int flags, void *user_data) +{ + return ((Context *)user_data)->openRestricted(path, flags); +} + +void Context::closeRestrictedCallBack(int fd, void *user_data) +{ + ((Context *)user_data)->closeRestricted(fd); +} + +int Context::openRestricted(const char *path, int flags) +{ + const QString filepath = QString::fromLatin1(path); + int fd; + + // Ask for control over everything but sys devices, which do not need it and it wouldn't be accepted by backends like logind anyway. + if (!filepath.startsWith(QLatin1String("/sys/"))) { + fd = m_session->openRestricted(filepath).value_or(-1); + if (fd < 0) { + // failed + return fd; + } + + // adjust flags - based on Weston (logind-util.c) + int fl = fcntl(fd, F_GETFL); + auto errorHandling = [fd, this]() { + closeRestricted(fd); + }; + if (fl < 0) { + errorHandling(); + return -1; + } + + if (flags & O_NONBLOCK) { + fl |= O_NONBLOCK; + } + + if (fcntl(fd, F_SETFL, fl) < 0) { + errorHandling(); + return -1; + } + + fl = fcntl(fd, F_GETFD); + if (fl < 0) { + errorHandling(); + return -1; + } + + if (!(flags & O_CLOEXEC)) { + fl &= ~FD_CLOEXEC; + } + + if (fcntl(fd, F_SETFD, fl) < 0) { + errorHandling(); + return -1; + } + } else { + fd = open(path, flags); + if (fd < 0) { + // failed + return fd; + } + m_nonRestrictedFds.push_back(FileDescriptor(fd)); + } + + return fd; +} + +void Context::closeRestricted(int fd) +{ + // Close it if it's an unrestricted fd, otherwise we need to inform the session to close it. + if (std::erase_if(m_nonRestrictedFds, [fd](const auto &otherFd) { + return otherFd.get() == fd; + }) == 0) { + m_session->closeRestricted(fd); + } +} + +std::unique_ptr Context::event() +{ + return Event::create(libinput_get_event(m_libinput)); +} + +void Context::suspend() +{ + if (m_suspended) { + return; + } + libinput_suspend(m_libinput); + m_suspended = true; +} + +void Context::resume() +{ + if (!m_suspended) { + return; + } + libinput_resume(m_libinput); + m_suspended = false; +} + +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/libinput/context.h b/local/recipes/kde/kwin/source/src/backends/libinput/context.h new file mode 100644 index 0000000000..b5aafea7a8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/libinput/context.h @@ -0,0 +1,79 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include +#include + +#include "utils/filedescriptor.h" + +namespace KWin +{ + +class Session; +class Udev; + +namespace LibInput +{ + +class Event; + +class Context +{ +public: + Context(Session *session, std::unique_ptr &&udev); + ~Context(); + bool initialize(); + bool isValid() const + { + return m_libinput != nullptr; + } + bool isSuspended() const + { + return m_suspended; + } + + Session *session() const; + int fileDescriptor(); + void dispatch(); + void suspend(); + void resume(); + + operator libinput *() + { + return m_libinput; + } + operator libinput *() const + { + return m_libinput; + } + + /** + * Gets the next event, if there is no new event @c nullptr is returned + */ + std::unique_ptr event(); + + static int openRestrictedCallback(const char *path, int flags, void *user_data); + static void closeRestrictedCallBack(int fd, void *user_data); + static const struct libinput_interface s_interface; + +private: + int openRestricted(const char *path, int flags); + void closeRestricted(int fd); + + Session *m_session; + struct libinput *m_libinput; + bool m_suspended; + std::unique_ptr m_udev; + std::vector m_nonRestrictedFds; +}; + +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/libinput/device.cpp b/local/recipes/kde/kwin/source/src/backends/libinput/device.cpp new file mode 100644 index 0000000000..a1586c0842 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/libinput/device.cpp @@ -0,0 +1,1207 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "device.h" + +#include "config-kwin.h" + +#include "core/output.h" +#include "core/outputbackend.h" +#include "libinput_logging.h" +#include "main.h" +#include "mousebuttons.h" +#include "pointer_input.h" +#ifndef KWIN_BUILD_TESTING +#include "workspace.h" +#endif + +#include +#include +#include +#include + +#include + +#ifndef KWIN_BUILD_TESTING +#include "workspace.h" +#endif + +namespace KWin +{ +namespace LibInput +{ +static const QRectF s_identityRect = QRectF(0, 0, 1, 1); + +TabletTool::TabletTool(libinput_tablet_tool *handle) + : m_handle(libinput_tablet_tool_ref(handle)) +{ +} + +TabletTool::~TabletTool() +{ + libinput_tablet_tool_unref(m_handle); +} + +libinput_tablet_tool *TabletTool::handle() const +{ + return m_handle; +} + +quint64 TabletTool::serialId() const +{ + return libinput_tablet_tool_get_serial(m_handle); +} + +quint64 TabletTool::uniqueId() const +{ + return libinput_tablet_tool_get_tool_id(m_handle); +} + +TabletTool::Type TabletTool::type() const +{ + switch (libinput_tablet_tool_get_type(m_handle)) { + case LIBINPUT_TABLET_TOOL_TYPE_PEN: + return Type::Pen; + case LIBINPUT_TABLET_TOOL_TYPE_ERASER: + return Type::Eraser; + case LIBINPUT_TABLET_TOOL_TYPE_BRUSH: + return Type::Brush; + case LIBINPUT_TABLET_TOOL_TYPE_PENCIL: + return Type::Pencil; + case LIBINPUT_TABLET_TOOL_TYPE_AIRBRUSH: + return Type::Airbrush; + case LIBINPUT_TABLET_TOOL_TYPE_MOUSE: + return Type::Mouse; + case LIBINPUT_TABLET_TOOL_TYPE_LENS: + return Type::Lens; + case LIBINPUT_TABLET_TOOL_TYPE_TOTEM: + return Type::Totem; + default: + return Type(); + } +} + +QList TabletTool::capabilities() const +{ + QList capabilities; + if (libinput_tablet_tool_has_pressure(m_handle)) { + capabilities << Capability::Pressure; + } + if (libinput_tablet_tool_has_distance(m_handle)) { + capabilities << Capability::Distance; + } + if (libinput_tablet_tool_has_rotation(m_handle)) { + capabilities << Capability::Rotation; + } + if (libinput_tablet_tool_has_tilt(m_handle)) { + capabilities << Capability::Tilt; + } + if (libinput_tablet_tool_has_slider(m_handle)) { + capabilities << Capability::Slider; + } + if (libinput_tablet_tool_has_wheel(m_handle)) { + capabilities << Capability::Wheel; + } + return capabilities; +} + +static bool checkAlphaNumericKeyboard(libinput_device *device) +{ + for (uint i = KEY_1; i <= KEY_0; i++) { + if (libinput_device_keyboard_has_key(device, i) == 0) { + return false; + } + } + for (uint i = KEY_Q; i <= KEY_P; i++) { + if (libinput_device_keyboard_has_key(device, i) == 0) { + return false; + } + } + for (uint i = KEY_A; i <= KEY_L; i++) { + if (libinput_device_keyboard_has_key(device, i) == 0) { + return false; + } + } + for (uint i = KEY_Z; i <= KEY_M; i++) { + if (libinput_device_keyboard_has_key(device, i) == 0) { + return false; + } + } + return true; +} + +enum class ConfigKey { + Enabled, + DisableEventsOnExternalMouse, + LeftHanded, + DisableWhileTyping, + PointerAcceleration, + PointerAccelerationProfile, + TapToClick, + LmrTapButtonMap, + TapAndDrag, + TapDragLock, + MiddleButtonEmulation, + NaturalScroll, + ScrollMethod, + ScrollButton, + ClickMethod, + ScrollFactor, + Orientation, + Calibration, + OutputName, + OutputArea, + MapToWorkspace, + TabletToolPressureCurve, + TabletToolPressureRangeMin, + TabletToolPressureRangeMax, + InputArea, + TabletToolRelativeMode, + Rotation, + OutputUuid, +}; + +struct ConfigDataBase +{ + ConfigDataBase(const QByteArray &_key) + : key(_key) + { + } + virtual ~ConfigDataBase() = default; + + QByteArray key; + virtual void read(Device * /*device*/, const KConfigGroup & /*values*/) const = 0; +}; + +template +struct ConfigData : public ConfigDataBase +{ + using SetterFunction = std::function; + using DefaultValueFunction = std::function; + + explicit ConfigData(const QByteArray &_key, const SetterFunction &_setter, const DefaultValueFunction &_defaultValue) + : ConfigDataBase(_key) + , setterFunction(_setter) + , defaultValueFunction(_defaultValue) + { + } + + void read(Device *device, const KConfigGroup &values) const override + { + if (!setterFunction || !defaultValueFunction) { + return; + } + + setterFunction(device, values.readEntry(key.constData(), defaultValueFunction(device))); + } + + SetterFunction setterFunction; + DefaultValueFunction defaultValueFunction; +}; + +// Template specializations for some specific config types that can't be handled +// through plain readEntry. +// +// This uses tagged types to avoid specialising the general type since we +// directly call the getters/setters. + +using DeviceOrientation = Qt::ScreenOrientation; + +template<> +struct ConfigData : public ConfigDataBase +{ + explicit ConfigData() + : ConfigDataBase(QByteArrayLiteral("Orientation")) + { + } + + void read(Device *device, const KConfigGroup &values) const override + { + int defaultValue = device->defaultOrientation(); + device->setOrientation(static_cast(values.readEntry(key.constData(), defaultValue))); + } +}; + +using CalibrationMatrix = QMatrix4x4; + +template<> +struct ConfigData : public ConfigDataBase +{ + explicit ConfigData() + : ConfigDataBase(QByteArrayLiteral("CalibrationMatrix")) + { + } + + void read(Device *device, const KConfigGroup &values) const override + { + device->setCalibrationMatrix(values.readEntry(key.constData(), device->defaultCalibrationMatrix())); + } +}; + +static const QMap> s_configData{ + {ConfigKey::Enabled, std::make_shared>(QByteArrayLiteral("Enabled"), &Device::setEnabled, &Device::isEnabledByDefault)}, + {ConfigKey::DisableEventsOnExternalMouse, std::make_shared>(QByteArrayLiteral("DisableEventsOnExternalMouse"), &Device::setDisableEventsOnExternalMouse, &Device::disableEventsOnExternalMouseEnabledByDefault)}, + {ConfigKey::LeftHanded, std::make_shared>(QByteArrayLiteral("LeftHanded"), &Device::setLeftHanded, &Device::leftHandedEnabledByDefault)}, + {ConfigKey::DisableWhileTyping, std::make_shared>(QByteArrayLiteral("DisableWhileTyping"), &Device::setDisableWhileTyping, &Device::disableWhileTypingEnabledByDefault)}, + {ConfigKey::PointerAcceleration, std::make_shared>(QByteArrayLiteral("PointerAcceleration"), &Device::setPointerAccelerationFromString, &Device::defaultPointerAccelerationToString)}, + {ConfigKey::PointerAccelerationProfile, std::make_shared>(QByteArrayLiteral("PointerAccelerationProfile"), &Device::setPointerAccelerationProfileFromInt, &Device::defaultPointerAccelerationProfileToInt)}, + {ConfigKey::TapToClick, std::make_shared>(QByteArrayLiteral("TapToClick"), &Device::setTapToClick, &Device::tapToClickEnabledByDefault)}, + {ConfigKey::TapAndDrag, std::make_shared>(QByteArrayLiteral("TapAndDrag"), &Device::setTapAndDrag, &Device::tapAndDragEnabledByDefault)}, + {ConfigKey::TapDragLock, std::make_shared>(QByteArrayLiteral("TapDragLock"), &Device::setTapDragLock, &Device::tapDragLockEnabledByDefault)}, + {ConfigKey::MiddleButtonEmulation, std::make_shared>(QByteArrayLiteral("MiddleButtonEmulation"), &Device::setMiddleEmulation, &Device::middleEmulationEnabledByDefault)}, + {ConfigKey::LmrTapButtonMap, std::make_shared>(QByteArrayLiteral("LmrTapButtonMap"), &Device::setLmrTapButtonMap, &Device::lmrTapButtonMapEnabledByDefault)}, + {ConfigKey::NaturalScroll, std::make_shared>(QByteArrayLiteral("NaturalScroll"), &Device::setNaturalScroll, &Device::naturalScrollEnabledByDefault)}, + {ConfigKey::ScrollMethod, std::make_shared>(QByteArrayLiteral("ScrollMethod"), &Device::activateScrollMethodFromInt, &Device::defaultScrollMethodToInt)}, + {ConfigKey::ScrollButton, std::make_shared>(QByteArrayLiteral("ScrollButton"), &Device::setScrollButton, &Device::defaultScrollButton)}, + {ConfigKey::ClickMethod, std::make_shared>(QByteArrayLiteral("ClickMethod"), &Device::setClickMethodFromInt, &Device::defaultClickMethodToInt)}, + {ConfigKey::ScrollFactor, std::make_shared>(QByteArrayLiteral("ScrollFactor"), &Device::setScrollFactor, &Device::scrollFactorDefault)}, + {ConfigKey::Orientation, std::make_shared>()}, + {ConfigKey::Calibration, std::make_shared>()}, + {ConfigKey::TabletToolPressureCurve, std::make_shared>(QByteArrayLiteral("TabletToolPressureCurve"), &Device::setPressureCurve, &Device::defaultPressureCurve)}, + {ConfigKey::OutputUuid, std::make_shared>(QByteArrayLiteral("OutputUuid"), &Device::setOutputUuid, &Device::defaultOutputUuid)}, + {ConfigKey::OutputName, std::make_shared>(QByteArrayLiteral("OutputName"), &Device::setConfigOutputName, &Device::defaultOutputName)}, + {ConfigKey::OutputArea, std::make_shared>(QByteArrayLiteral("OutputArea"), &Device::setOutputArea, &Device::defaultOutputArea)}, + {ConfigKey::MapToWorkspace, std::make_shared>(QByteArrayLiteral("MapToWorkspace"), &Device::setMapToWorkspace, &Device::defaultMapToWorkspace)}, + {ConfigKey::TabletToolPressureRangeMin, std::make_shared>(QByteArrayLiteral("TabletToolPressureRangeMin"), &Device::setPressureRangeMin, &Device::defaultPressureRangeMin)}, + {ConfigKey::TabletToolPressureRangeMax, std::make_shared>(QByteArrayLiteral("TabletToolPressureRangeMax"), &Device::setPressureRangeMax, &Device::defaultPressureRangeMax)}, + {ConfigKey::InputArea, std::make_shared>(QByteArrayLiteral("InputArea"), &Device::setInputArea, &Device::defaultInputArea)}, + {ConfigKey::TabletToolRelativeMode, std::make_shared>(QByteArrayLiteral("TabletToolRelativeMode"), &Device::setTabletToolRelative, &Device::defaultTabletToolIsRelative)}, + {ConfigKey::Rotation, std::make_shared>(QByteArrayLiteral("Rotation"), &Device::setRotation, &Device::defaultRotation)}, +}; + +namespace +{ +QMatrix4x4 getMatrix(libinput_device *device, std::function getter) +{ + float matrix[6]; + if (!getter(device, matrix)) { + return {}; + } + return QMatrix4x4{ + matrix[0], matrix[1], matrix[2], 0.0f, + matrix[3], matrix[4], matrix[5], 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f}; +} + +bool setOrientedCalibrationMatrix(libinput_device *device, QMatrix4x4 matrix, Qt::ScreenOrientation orientation) +{ + // 90 deg cw + static const QMatrix4x4 portraitMatrix{ + 0.0f, -1.0f, 1.0f, 0.0f, + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f}; + // 180 deg cw + static const QMatrix4x4 invertedLandscapeMatrix{ + -1.0f, 0.0f, 1.0f, 0.0f, + 0.0f, -1.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f}; + // 270 deg cw + static const QMatrix4x4 invertedPortraitMatrix{ + 0.0f, 1.0f, 0.0f, 0.0f, + -1.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f}; + + switch (orientation) { + case Qt::PortraitOrientation: + matrix *= portraitMatrix; + break; + case Qt::InvertedLandscapeOrientation: + matrix *= invertedLandscapeMatrix; + break; + case Qt::InvertedPortraitOrientation: + matrix *= invertedPortraitMatrix; + break; + case Qt::PrimaryOrientation: + case Qt::LandscapeOrientation: + default: + break; + } + + float data[6]{matrix(0, 0), matrix(0, 1), matrix(0, 2), matrix(1, 0), matrix(1, 1), matrix(1, 2)}; + return libinput_device_config_calibration_set_matrix(device, data) == LIBINPUT_CONFIG_STATUS_SUCCESS; +} +} + +Device::Device(libinput_device *device, QObject *parent) + : InputDevice(parent) + , m_device(device) + , m_keyboard(libinput_device_has_capability(m_device, LIBINPUT_DEVICE_CAP_KEYBOARD)) + , m_pointer(libinput_device_has_capability(m_device, LIBINPUT_DEVICE_CAP_POINTER)) + , m_touch(libinput_device_has_capability(m_device, LIBINPUT_DEVICE_CAP_TOUCH)) + , m_tabletTool(libinput_device_has_capability(m_device, LIBINPUT_DEVICE_CAP_TABLET_TOOL)) + , m_tabletPad(libinput_device_has_capability(m_device, LIBINPUT_DEVICE_CAP_TABLET_PAD)) + , m_supportsGesture(libinput_device_has_capability(m_device, LIBINPUT_DEVICE_CAP_GESTURE)) + , m_switch(libinput_device_has_capability(m_device, LIBINPUT_DEVICE_CAP_SWITCH)) + , m_lidSwitch(m_switch ? libinput_device_switch_has_switch(m_device, LIBINPUT_SWITCH_LID) : false) + , m_tabletSwitch(m_switch ? libinput_device_switch_has_switch(m_device, LIBINPUT_SWITCH_TABLET_MODE) : false) + , m_touchpad(m_pointer && udev_device_get_property_value(libinput_device_get_udev_device(m_device), "ID_INPUT_TOUCHPAD")) + , m_name(QString::fromLocal8Bit(libinput_device_get_name(m_device))) + , m_sysName(QString::fromLocal8Bit(libinput_device_get_sysname(m_device))) + , m_sysPath(QString::fromLocal8Bit(udev_device_get_syspath(libinput_device_get_udev_device(m_device)))) + , m_outputName(QString::fromLocal8Bit(libinput_device_get_output_name(m_device))) + , m_product(libinput_device_get_id_product(m_device)) + , m_vendor(libinput_device_get_id_vendor(m_device)) + , m_busType(libinput_device_get_id_bustype(m_device)) + , m_tapFingerCount(libinput_device_config_tap_get_finger_count(m_device)) + , m_defaultTapButtonMap(libinput_device_config_tap_get_default_button_map(m_device)) + , m_tapButtonMap(libinput_device_config_tap_get_button_map(m_device)) + , m_tapToClickEnabledByDefault(true) + , m_tapToClick(libinput_device_config_tap_get_enabled(m_device)) + , m_tapAndDragEnabledByDefault(true) + , m_tapAndDrag(libinput_device_config_tap_get_drag_enabled(m_device)) + , m_tapDragLockEnabledByDefault(libinput_device_config_tap_get_default_drag_lock_enabled(m_device)) + , m_tapDragLock(libinput_device_config_tap_get_drag_lock_enabled(m_device)) + , m_supportsDisableWhileTyping(libinput_device_config_dwt_is_available(m_device)) + , m_supportsPointerAcceleration(libinput_device_config_accel_is_available(m_device)) + , m_supportsLeftHanded(libinput_device_config_left_handed_is_available(m_device)) + , m_supportsCalibrationMatrix(libinput_device_config_calibration_has_matrix(m_device)) + , m_supportsDisableEvents(libinput_device_config_send_events_get_modes(m_device) & LIBINPUT_CONFIG_SEND_EVENTS_DISABLED) + , m_supportsDisableEventsOnExternalMouse(libinput_device_config_send_events_get_modes(m_device) & LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE) + , m_supportsMiddleEmulation(libinput_device_config_middle_emulation_is_available(m_device)) + , m_supportsNaturalScroll(libinput_device_config_scroll_has_natural_scroll(m_device)) + , m_supportedScrollMethods(libinput_device_config_scroll_get_methods(m_device)) + , m_leftHandedEnabledByDefault(libinput_device_config_left_handed_get_default(m_device)) + , m_middleEmulationEnabledByDefault(libinput_device_config_middle_emulation_get_default_enabled(m_device) == LIBINPUT_CONFIG_MIDDLE_EMULATION_ENABLED) + , m_naturalScrollEnabledByDefault(libinput_device_config_scroll_get_default_natural_scroll_enabled(m_device)) + , m_defaultScrollMethod(libinput_device_config_scroll_get_default_method(m_device)) + , m_defaultScrollButton(libinput_device_config_scroll_get_default_button(m_device)) + , m_disableWhileTypingEnabledByDefault(libinput_device_config_dwt_get_default_enabled(m_device)) + , m_disableWhileTyping(m_supportsDisableWhileTyping ? libinput_device_config_dwt_get_enabled(m_device) == LIBINPUT_CONFIG_DWT_ENABLED : false) + , m_middleEmulation(libinput_device_config_middle_emulation_get_enabled(m_device) == LIBINPUT_CONFIG_MIDDLE_EMULATION_ENABLED) + , m_leftHanded(m_supportsLeftHanded ? libinput_device_config_left_handed_get(m_device) : false) + , m_naturalScroll(m_supportsNaturalScroll ? libinput_device_config_scroll_get_natural_scroll_enabled(m_device) : false) + , m_scrollMethod(libinput_device_config_scroll_get_method(m_device)) + , m_scrollButton(libinput_device_config_scroll_get_button(m_device)) + , m_defaultPointerAcceleration(libinput_device_config_accel_get_default_speed(m_device)) + , m_pointerAcceleration(libinput_device_config_accel_get_speed(m_device)) + , m_scrollFactor(1.0) + , m_supportedPointerAccelerationProfiles(libinput_device_config_accel_get_profiles(m_device)) + , m_defaultPointerAccelerationProfile(libinput_device_config_accel_get_default_profile(m_device)) + , m_pointerAccelerationProfile(libinput_device_config_accel_get_profile(m_device)) + , m_enabled(m_supportsDisableEvents ? (libinput_device_config_send_events_get_mode(m_device) & LIBINPUT_CONFIG_SEND_EVENTS_DISABLED) == 0 : true) + , m_disableEventsOnExternalMouseEnabledByDefault(m_supportsDisableEventsOnExternalMouse && (libinput_device_config_send_events_get_default_mode(m_device) & LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE)) + , m_disableEventsOnExternalMouse(m_supportsDisableEventsOnExternalMouse && (libinput_device_config_send_events_get_mode(m_device) & LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE)) + , m_config() + , m_defaultCalibrationMatrix(getMatrix(m_device, &libinput_device_config_calibration_get_default_matrix)) + , m_calibrationMatrix(getMatrix(m_device, &libinput_device_config_calibration_get_matrix)) + , m_pressureCurve(deserializePressureCurve(defaultPressureCurve())) + , m_supportedClickMethods(libinput_device_config_click_get_methods(m_device)) + , m_defaultClickMethod(libinput_device_config_click_get_default_method(m_device)) + , m_clickMethod(libinput_device_config_click_get_method(m_device)) + , m_outputArea(s_identityRect) + , m_supportsPressureRange(false) + , m_pressureRangeMin(0.0) + , m_pressureRangeMax(1.0) + , m_defaultPressureRangeMin(0.0) + , m_defaultPressureRangeMax(1.0) + , m_inputArea(s_identityRect) +{ + libinput_device_ref(m_device); + libinput_device_set_user_data(m_device, this); + + qreal width = 0; + qreal height = 0; + if (libinput_device_get_size(m_device, &width, &height) == 0) { + m_size = QSizeF(width, height); + } + if (m_pointer) { + // 0x120 is the first joystick Button + for (int button = BTN_LEFT; button < 0x120; ++button) { + if (libinput_device_pointer_has_button(m_device, button)) { + m_supportedButtons |= buttonToQtMouseButton(button); + } + } + } + + if (m_keyboard) { + m_alphaNumericKeyboard = checkAlphaNumericKeyboard(m_device); + } + + if (m_supportsCalibrationMatrix && m_calibrationMatrix != m_defaultCalibrationMatrix) { + float matrix[]{m_defaultCalibrationMatrix(0, 0), + m_defaultCalibrationMatrix(0, 1), + m_defaultCalibrationMatrix(0, 2), + m_defaultCalibrationMatrix(1, 0), + m_defaultCalibrationMatrix(1, 1), + m_defaultCalibrationMatrix(1, 2)}; + libinput_device_config_calibration_set_matrix(m_device, matrix); + m_calibrationMatrix = m_defaultCalibrationMatrix; + } + + if (supportsInputArea() && m_inputArea != defaultInputArea()) { + const libinput_config_area_rectangle rect{ + .x1 = m_inputArea.topLeft().x(), + .y1 = m_inputArea.topLeft().y(), + .x2 = m_inputArea.bottomRight().x(), + .y2 = m_inputArea.bottomRight().y(), + }; + libinput_device_config_area_set_rectangle(m_device, &rect); + } + + libinput_device_group *group = libinput_device_get_device_group(device); + m_deviceGroupId = QCryptographicHash::hash(QString::asprintf("%p", group).toLatin1(), QCryptographicHash::Sha1).toBase64(); + + const int numGroups = libinput_device_tablet_pad_get_num_mode_groups(m_device); + m_currentModes.reserve(numGroups); + + for (int groupIndex = 0; groupIndex < numGroups; ++groupIndex) { + const auto modeGroup = libinput_device_tablet_pad_get_mode_group(m_device, groupIndex); + m_currentModes.push_back(libinput_tablet_pad_mode_group_get_mode(modeGroup)); + } + + connect(this, &Device::tabletPadButtonEvent, this, [this](uint, bool, quint32 group, quint32 mode, bool isModeSwitch, std::chrono::microseconds, InputDevice *) { + Q_ASSERT(group < m_currentModes.length()); + m_currentModes[group] = mode; + Q_EMIT currentModesChanged(); + }); + + const auto udevDevice = libinput_device_get_udev_device(m_device); + if (udevDevice != nullptr) { + if (const auto devPath = udev_device_get_devpath(udevDevice)) { + // In UDev, all virtual uinput devices have a devpath start with /devices/virtual + m_isVirtual = strstr(devPath, "/devices/virtual/") != nullptr; + } + udev_device_unref(udevDevice); + } + + QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/kde/KWin/InputDevice/") + m_sysName, + QStringLiteral("org.kde.KWin.InputDevice"), + this, + QDBusConnection::ExportAllProperties); +} + +Device::~Device() +{ + QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/org/kde/KWin/InputDevice/") + m_sysName); + libinput_device_set_user_data(m_device, nullptr); + libinput_device_unref(m_device); +} + +Device *Device::get(libinput_device *native) +{ + return static_cast(libinput_device_get_user_data(native)); +} + +template +void Device::writeEntry(const ConfigKey &key, const T &value) +{ + if (!m_config.isValid()) { + return; + } + if (m_loading) { + return; + } + auto it = s_configData.find(key); + Q_ASSERT(it != s_configData.end()); + m_config.writeEntry(it.value()->key.constData(), value); + m_config.sync(); +} + +void Device::loadConfiguration() +{ + if (!m_config.isValid() && !m_defaultConfig.isValid()) { + return; + } + + m_loading = true; + for (auto it = s_configData.begin(), end = s_configData.end(); it != end; ++it) { + (*it)->read(this, m_config); + }; + + m_loading = false; +} + +void Device::setPointerAcceleration(qreal acceleration) +{ + if (!m_supportsPointerAcceleration) { + return; + } + acceleration = std::clamp(acceleration, -1.0, 1.0); + if (libinput_device_config_accel_set_speed(m_device, acceleration) == LIBINPUT_CONFIG_STATUS_SUCCESS) { + if (m_pointerAcceleration != acceleration) { + m_pointerAcceleration = acceleration; + Q_EMIT pointerAccelerationChanged(); + writeEntry(ConfigKey::PointerAcceleration, QString::number(acceleration, 'f', 3)); + } + } +} + +void Device::setScrollButton(quint32 button) +{ + if (!(m_supportedScrollMethods & LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN)) { + return; + } + if (libinput_device_config_scroll_set_button(m_device, button) == LIBINPUT_CONFIG_STATUS_SUCCESS) { + if (m_scrollButton != button) { + m_scrollButton = button; + writeEntry(ConfigKey::ScrollButton, m_scrollButton); + Q_EMIT scrollButtonChanged(); + } + } +} + +void Device::setPointerAccelerationProfile(bool set, enum libinput_config_accel_profile profile) +{ + if (!(m_supportedPointerAccelerationProfiles & profile)) { + return; + } + if (!set) { + profile = (profile == LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT) ? LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE : LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT; + if (!(m_supportedPointerAccelerationProfiles & profile)) { + return; + } + } + + if (libinput_device_config_accel_set_profile(m_device, profile) == LIBINPUT_CONFIG_STATUS_SUCCESS) { + if (m_pointerAccelerationProfile != profile) { + m_pointerAccelerationProfile = profile; + Q_EMIT pointerAccelerationProfileChanged(); + writeEntry(ConfigKey::PointerAccelerationProfile, (quint32)profile); + } + } +} + +void Device::setClickMethod(bool set, enum libinput_config_click_method method) +{ + if (!(m_supportedClickMethods & method)) { + return; + } + if (!set) { + method = (method == LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS) ? LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER : LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS; + if (!(m_supportedClickMethods & method)) { + return; + } + } + + if (libinput_device_config_click_set_method(m_device, method) == LIBINPUT_CONFIG_STATUS_SUCCESS) { + if (m_clickMethod != method) { + m_clickMethod = method; + Q_EMIT clickMethodChanged(); + writeEntry(ConfigKey::ClickMethod, (quint32)method); + } + } +} + +void Device::setScrollMethod(bool set, enum libinput_config_scroll_method method) +{ + if (!(m_supportedScrollMethods & method)) { + return; + } + + bool isCurrent = m_scrollMethod == method; + if (!set) { + if (isCurrent) { + method = LIBINPUT_CONFIG_SCROLL_NO_SCROLL; + isCurrent = false; + } else { + return; + } + } + + if (libinput_device_config_scroll_set_method(m_device, method) == LIBINPUT_CONFIG_STATUS_SUCCESS) { + if (!isCurrent) { + m_scrollMethod = method; + Q_EMIT scrollMethodChanged(); + writeEntry(ConfigKey::ScrollMethod, (quint32)method); + } + } +} + +void Device::setLmrTapButtonMap(bool set) +{ + enum libinput_config_tap_button_map map = set ? LIBINPUT_CONFIG_TAP_MAP_LMR : LIBINPUT_CONFIG_TAP_MAP_LRM; + + if (m_tapFingerCount < 2) { + return; + } + if (!set) { + map = LIBINPUT_CONFIG_TAP_MAP_LRM; + } + + if (libinput_device_config_tap_set_button_map(m_device, map) == LIBINPUT_CONFIG_STATUS_SUCCESS) { + if (m_tapButtonMap != map) { + m_tapButtonMap = map; + writeEntry(ConfigKey::LmrTapButtonMap, set); + Q_EMIT tapButtonMapChanged(); + } + } +} + +void *Device::group() const +{ + return libinput_device_get_device_group(m_device); +} + +int Device::tabletPadButtonCount() const +{ + return libinput_device_tablet_pad_get_num_buttons(m_device); +} + +int Device::tabletPadDialCount() const +{ + return libinput_device_tablet_pad_get_num_dials(m_device); +} + +int Device::tabletPadRingCount() const +{ + return libinput_device_tablet_pad_get_num_rings(m_device); +} + +int Device::tabletPadStripCount() const +{ + return libinput_device_tablet_pad_get_num_strips(m_device); +} + +QList Device::modeGroups() const +{ + QList result; + + int numGroups = libinput_device_tablet_pad_get_num_mode_groups(m_device); + + for (int groupIndex = 0; groupIndex < numGroups; ++groupIndex) { + libinput_tablet_pad_mode_group *group = libinput_device_tablet_pad_get_mode_group(m_device, groupIndex); + int modeCount = libinput_tablet_pad_mode_group_get_num_modes(group); + + QList buttons; + int totalButtons = libinput_device_tablet_pad_get_num_buttons(m_device); + for (int buttonIndex = 0; buttonIndex < totalButtons; ++buttonIndex) { + if (libinput_tablet_pad_mode_group_has_button(group, buttonIndex)) { + buttons << buttonIndex; + } + } + + QList rings; + int totalRings = libinput_device_tablet_pad_get_num_rings(m_device); + for (int ringIndex = 0; ringIndex < totalRings; ++ringIndex) { + if (libinput_tablet_pad_mode_group_has_ring(group, ringIndex)) { + rings << ringIndex; + } + } + + QList strips; + int totalStrips = libinput_device_tablet_pad_get_num_strips(m_device); + for (int stripIndex = 0; stripIndex < totalStrips; ++stripIndex) { + if (libinput_tablet_pad_mode_group_has_strip(group, stripIndex)) { + strips << stripIndex; + } + } + + QList dials; + int totalDials = libinput_device_tablet_pad_get_num_dials(m_device); + for (int dialIndex = 0; dialIndex < totalDials; ++dialIndex) { + if (libinput_tablet_pad_mode_group_has_dial(group, dialIndex)) { + dials << dialIndex; + } + } + + result << InputDeviceTabletPadModeGroup{ + .modeCount = modeCount, + .buttons = buttons, + .rings = rings, + .strips = strips, + .dials = dials, + }; + } + return result; +} + +#define CONFIG(method, condition, function, variable, key) \ + void Device::method(bool set) \ + { \ + if (condition) { \ + return; \ + } \ + if (libinput_device_config_##function(m_device, set) == LIBINPUT_CONFIG_STATUS_SUCCESS) { \ + if (m_##variable != set) { \ + m_##variable = set; \ + writeEntry(ConfigKey::key, m_##variable); \ + Q_EMIT variable##Changed(); \ + } \ + } \ + } + +CONFIG(setLeftHanded, !m_supportsLeftHanded, left_handed_set, leftHanded, LeftHanded) +CONFIG(setNaturalScroll, !m_supportsNaturalScroll, scroll_set_natural_scroll_enabled, naturalScroll, NaturalScroll) + +#undef CONFIG + +#define CONFIG(method, condition, function, enum, variable, key) \ + void Device::method(bool set) \ + { \ + if (condition) { \ + return; \ + } \ + if (libinput_device_config_##function(m_device, set ? LIBINPUT_CONFIG_##enum##_ENABLED : LIBINPUT_CONFIG_##enum##_DISABLED) == LIBINPUT_CONFIG_STATUS_SUCCESS) { \ + if (m_##variable != set) { \ + m_##variable = set; \ + writeEntry(ConfigKey::key, m_##variable); \ + Q_EMIT variable##Changed(); \ + } \ + } \ + } + +CONFIG(setDisableWhileTyping, !m_supportsDisableWhileTyping, dwt_set_enabled, DWT, disableWhileTyping, DisableWhileTyping) +CONFIG(setTapToClick, m_tapFingerCount == 0, tap_set_enabled, TAP, tapToClick, TapToClick) +CONFIG(setTapAndDrag, false, tap_set_drag_enabled, DRAG, tapAndDrag, TapAndDrag) +CONFIG(setTapDragLock, false, tap_set_drag_lock_enabled, DRAG_LOCK, tapDragLock, TapDragLock) +CONFIG(setMiddleEmulation, m_supportsMiddleEmulation == false, middle_emulation_set_enabled, MIDDLE_EMULATION, middleEmulation, MiddleButtonEmulation) + +#undef CONFIG + +void Device::setEnabled(bool set) +{ + if (!m_supportsDisableEvents) { + return; + } + const auto enabledMode = (m_supportsDisableEventsOnExternalMouse && m_disableEventsOnExternalMouse) ? LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE : LIBINPUT_CONFIG_SEND_EVENTS_ENABLED; + const auto mode = set ? enabledMode : LIBINPUT_CONFIG_SEND_EVENTS_DISABLED; + + if (libinput_device_config_send_events_set_mode(m_device, mode) == LIBINPUT_CONFIG_STATUS_SUCCESS) { + if (m_enabled != set) { + m_enabled = set; + writeEntry(ConfigKey::Enabled, m_enabled); + Q_EMIT enabledChanged(); + } + } +} + +void Device::setDisableEventsOnExternalMouse(bool set) +{ + if (!m_supportsDisableEventsOnExternalMouse) { + return; + } + const auto enabledMode = set ? LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE : LIBINPUT_CONFIG_SEND_EVENTS_ENABLED; + + if (!m_enabled || libinput_device_config_send_events_set_mode(m_device, enabledMode) == LIBINPUT_CONFIG_STATUS_SUCCESS) { + if (m_disableEventsOnExternalMouse != set) { + m_disableEventsOnExternalMouse = set; + writeEntry(ConfigKey::DisableEventsOnExternalMouse, m_disableEventsOnExternalMouse); + Q_EMIT disableEventsOnExternalMouseChanged(); + } + } +} + +void Device::setScrollFactor(qreal factor) +{ + if (m_scrollFactor != factor) { + m_scrollFactor = factor; + writeEntry(ConfigKey::ScrollFactor, m_scrollFactor); + Q_EMIT scrollFactorChanged(); + } +} + +void Device::setCalibrationMatrix(const QString &value) +{ + const auto matrix = deserializeMatrix(value); + if (!m_supportsCalibrationMatrix || m_calibrationMatrix == matrix) { + return; + } + + if (setOrientedCalibrationMatrix(m_device, matrix, m_orientation)) { + QList list; + list.reserve(16); + for (uchar row = 0; row < 4; ++row) { + for (uchar col = 0; col < 4; ++col) { + list << matrix(row, col); + } + } + writeEntry(ConfigKey::Calibration, list); + m_calibrationMatrix = matrix; + Q_EMIT calibrationMatrixChanged(); + } +} + +QString Device::defaultPressureCurve() const +{ + QEasingCurve curve(QEasingCurve::Type::BezierSpline); + curve.addCubicBezierSegment(QPointF{0.0f, 0.0f}, QPointF{1.0f, 1.0f}, QPointF{1.0f, 1.0f}); + return serializePressureCurve(curve); +} + +QEasingCurve Device::pressureCurve() const +{ + return m_pressureCurve; +} + +QString Device::serializedPressureCurve() const +{ + return serializePressureCurve(m_pressureCurve); +} + +void Device::setPressureCurve(const QString &curve) +{ + const auto easingCurve = deserializePressureCurve(curve); + if (m_pressureCurve != easingCurve) { + writeEntry(ConfigKey::TabletToolPressureCurve, curve); + m_pressureCurve = easingCurve; + Q_EMIT pressureCurveChanged(); + } +} + +QString Device::serializePressureCurve(const QEasingCurve &curve) +{ + // We only care about the first two points. toCubicSpline adds the end point as the third, but to us that's always (1,1). + const auto points = curve.toCubicSpline().first(2); + QString serializedString; + for (const QPointF &pair : points) { + serializedString += QString::number(pair.x()); + serializedString += ','; + serializedString += QString::number(pair.y()); + serializedString += ';'; + } + + return serializedString; +} + +QEasingCurve Device::deserializePressureCurve(const QString &curve) +{ + const QStringList data = curve.split(';'); + + QList points; + for (const QString &pair : data) { + if (pair.indexOf(',') > -1) { + points.append({pair.section(',', 0, 0).toDouble(), + pair.section(',', 1, 1).toDouble()}); + } + } + + auto easingCurve = QEasingCurve(QEasingCurve::Type::BezierSpline); + + // We only support 2 points + if (points.size() >= 2) { + easingCurve.addCubicBezierSegment(points.at(0), points.at(1), QPointF{1.0f, 1.0f}); + } + return easingCurve; +} + +void Device::setOrientation(Qt::ScreenOrientation orientation) +{ + if (!m_supportsCalibrationMatrix || m_orientation == orientation) { + return; + } + + if (setOrientedCalibrationMatrix(m_device, m_calibrationMatrix, orientation)) { + writeEntry(ConfigKey::Orientation, static_cast(orientation)); + m_orientation = orientation; + Q_EMIT orientationChanged(); + } +} + +void Device::setOutputName(const QString &name) +{ +#ifndef KWIN_BUILD_TESTING + if (name == m_outputName) { + return; + } + m_outputName = name; + const auto outputs = kwinApp()->outputBackend()->outputs(); + const auto it = std::ranges::find_if(outputs, [&name](BackendOutput *output) { + return output->name() == name && workspace()->findOutput(output) != nullptr; + }); + if (it == outputs.end()) { + setOutput(nullptr); + m_outputUuid.clear(); + } else { + auto *output = *it; + setOutput(output); + m_outputUuid = output->uuid(); + writeEntry(ConfigKey::OutputUuid, m_outputUuid); + } + Q_EMIT outputNameChanged(); +#endif +} + +void Device::setConfigOutputName(const QString &name) +{ +#ifndef KWIN_BUILD_TESTING + if (name == m_outputName) { + return; + } + if (m_outputUuid.isEmpty()) { + setOutputName(name); + } +#endif +} + +void Device::setOutputUuid(const QString &uuid) +{ +#ifndef KWIN_BUILD_TESTING + if (uuid == m_outputUuid) { + return; + } + m_outputUuid = uuid; + const auto outputs = kwinApp()->outputBackend()->outputs(); + const auto it = std::ranges::find_if(outputs, [&uuid](BackendOutput *output) { + return output->uuid() == uuid && workspace()->findOutput(output) != nullptr; + }); + if (it == outputs.end()) { + setOutput(nullptr); + m_outputName.clear(); + } else { + auto *output = *it; + setOutput(output); + m_outputName = output->name(); + } + Q_EMIT outputNameChanged(); + writeEntry(ConfigKey::OutputUuid, uuid); +#endif +} + +BackendOutput *Device::output() const +{ + return m_output; +} + +void Device::setOutput(BackendOutput *output) +{ + m_output = output; +} + +static libinput_led toLibinputLEDS(LEDs leds) +{ + quint32 libinputLeds = 0; + if (leds.testFlag(LED::NumLock)) { + libinputLeds = libinputLeds | LIBINPUT_LED_NUM_LOCK; + } + if (leds.testFlag(LED::CapsLock)) { + libinputLeds = libinputLeds | LIBINPUT_LED_CAPS_LOCK; + } + if (leds.testFlag(LED::ScrollLock)) { + libinputLeds = libinputLeds | LIBINPUT_LED_SCROLL_LOCK; + } + if (leds.testFlag(LED::Compose)) { + libinputLeds = libinputLeds | LIBINPUT_LED_COMPOSE; + } + if (leds.testFlag(LED::Kana)) { + libinputLeds = libinputLeds | LIBINPUT_LED_KANA; + } + return libinput_led(libinputLeds); +} + +LEDs Device::leds() const +{ + return m_leds; +} + +void Device::setLeds(LEDs leds) +{ + if (m_leds != leds) { + m_leds = leds; + libinput_device_led_update(m_device, toLibinputLEDS(m_leds)); + } +} + +bool Device::supportsOutputArea() const +{ + return m_tabletTool; +} + +QRectF Device::defaultOutputArea() const +{ + return s_identityRect; +} + +QRectF Device::outputArea() const +{ + return m_outputArea; +} + +void Device::setOutputArea(const QRectF &outputArea) +{ + if (m_outputArea != outputArea) { + m_outputArea = outputArea; + writeEntry(ConfigKey::OutputArea, m_outputArea); + Q_EMIT outputAreaChanged(); + } +} + +void Device::setMapToWorkspace(bool mapToWorkspace) +{ + if (m_mapToWorkspace != mapToWorkspace) { + m_mapToWorkspace = mapToWorkspace; + writeEntry(ConfigKey::MapToWorkspace, m_mapToWorkspace); + Q_EMIT mapToWorkspaceChanged(); + } +} + +bool Device::supportsPressureRange() const +{ + return m_supportsPressureRange; +} + +void Device::setSupportsPressureRange(const bool supported) +{ + if (m_supportsPressureRange != supported) { + m_supportsPressureRange = supported; + Q_EMIT supportsPressureRangeChanged(); + } +} + +double Device::pressureRangeMin() const +{ + return m_pressureRangeMin; +} + +void Device::setPressureRangeMin(const double value) +{ + if (m_pressureRangeMin != value) { + m_pressureRangeMin = value; + writeEntry(ConfigKey::TabletToolPressureRangeMin, m_pressureRangeMin); + Q_EMIT pressureRangeMinChanged(); + } +} + +double Device::pressureRangeMax() const +{ + return m_pressureRangeMax; +} + +void Device::setPressureRangeMax(const double value) +{ + if (m_pressureRangeMax != value) { + m_pressureRangeMax = value; + writeEntry(ConfigKey::TabletToolPressureRangeMax, m_pressureRangeMax); + Q_EMIT pressureRangeMaxChanged(); + } +} + +double Device::defaultPressureRangeMin() const +{ + return m_defaultPressureRangeMin; +} + +double Device::defaultPressureRangeMax() const +{ + return m_defaultPressureRangeMax; +} + +bool Device::supportsInputArea() const +{ + return libinput_device_config_area_has_rectangle(m_device); +} + +QRectF Device::inputArea() const +{ + return m_inputArea; +} + +void Device::setInputArea(const QRectF &inputArea) +{ + if (m_inputArea != inputArea) { + m_inputArea = inputArea; + + const libinput_config_area_rectangle rect{ + .x1 = m_inputArea.topLeft().x(), + .y1 = m_inputArea.topLeft().y(), + .x2 = m_inputArea.bottomRight().x(), + .y2 = m_inputArea.bottomRight().y(), + }; + libinput_device_config_area_set_rectangle(m_device, &rect); + + writeEntry(ConfigKey::InputArea, m_inputArea); + Q_EMIT inputAreaChanged(); + } +} + +QRectF Device::defaultInputArea() const +{ + return s_identityRect; +} + +QString Device::serializeMatrix(const QMatrix4x4 &matrix) +{ + QString result; + for (int i = 0; i < 16; i++) { + result.append(QString::number(matrix.constData()[i])); + if (i != 15) { + result.append(QLatin1Char(',')); + } + } + return result; +} + +QMatrix4x4 Device::deserializeMatrix(const QString &matrix) +{ + const auto items = QStringView(matrix).split(QLatin1Char(',')); + if (items.size() == 16) { + QList data; + data.reserve(16); + std::ranges::transform(std::as_const(items), std::back_inserter(data), [](const QStringView &item) { + return item.toFloat(); + }); + + return QMatrix4x4{data.constData()}; + } + + return QMatrix4x4{}; +} + +void Device::setTabletToolRelative(bool relative) +{ + if (relative == m_tabletToolIsRelative) { + return; + } + + m_tabletToolIsRelative = relative; + writeEntry(ConfigKey::TabletToolRelativeMode, m_tabletToolIsRelative); + Q_EMIT tabletToolRelativeChanged(); +} + +QList Device::numModes() const +{ + const int numGroups = libinput_device_tablet_pad_get_num_mode_groups(m_device); + + QList numModes; + numModes.reserve(numGroups); + + for (int groupIndex = 0; groupIndex < numGroups; ++groupIndex) { + numModes.push_back(libinput_tablet_pad_mode_group_get_num_modes(libinput_device_tablet_pad_get_mode_group(m_device, groupIndex))); + } + return numModes; +} + +QList Device::currentModes() const +{ + return m_currentModes; +} + +bool Device::supportsRotation() const +{ + return libinput_device_config_rotation_is_available(m_device); +} + +uint32_t Device::rotation() const +{ + return libinput_device_config_rotation_get_angle(m_device); +} + +void Device::setRotation(uint32_t degrees_cw) +{ + if (rotation() != degrees_cw && libinput_device_config_rotation_set_angle(m_device, degrees_cw) == LIBINPUT_CONFIG_STATUS_SUCCESS) { + writeEntry(ConfigKey::Rotation, degrees_cw); + Q_EMIT rotationChanged(); + } +} + +uint32_t Device::defaultRotation() const +{ + return libinput_device_config_rotation_get_default_angle(m_device); +} + +bool Device::isVirtual() const +{ + return m_isVirtual; +} +} +} + +#include "moc_device.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/libinput/device.h b/local/recipes/kde/kwin/source/src/backends/libinput/device.h new file mode 100644 index 0000000000..fd2a95edf3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/libinput/device.h @@ -0,0 +1,931 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "core/backendoutput.h" +#include "core/inputdevice.h" + +#include + +#include +#include + +#include +#include +#include +#include +#include + +struct libinput_device; + +namespace KWin +{ +class LogicalOutput; + +namespace LibInput +{ +enum class ConfigKey; + +class TabletTool : public InputDeviceTabletTool +{ + Q_OBJECT + +public: + explicit TabletTool(libinput_tablet_tool *handle); + ~TabletTool() override; + + libinput_tablet_tool *handle() const; + + quint64 serialId() const override; + quint64 uniqueId() const override; + + Type type() const override; + QList capabilities() const override; + +private: + libinput_tablet_tool *const m_handle; +}; + +class KWIN_EXPORT Device : public InputDevice +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.KWin.InputDevice") + // + // general + Q_PROPERTY(bool keyboard READ isKeyboard CONSTANT) + Q_PROPERTY(bool alphaNumericKeyboard READ isAlphaNumericKeyboard CONSTANT) + Q_PROPERTY(bool pointer READ isPointer CONSTANT) + Q_PROPERTY(bool touchpad READ isTouchpad CONSTANT) + Q_PROPERTY(bool touch READ isTouch CONSTANT) + Q_PROPERTY(bool tabletTool READ isTabletTool CONSTANT) + Q_PROPERTY(bool tabletPad READ isTabletPad CONSTANT) + Q_PROPERTY(bool gestureSupport READ supportsGesture CONSTANT) + Q_PROPERTY(QString name READ name CONSTANT) + Q_PROPERTY(QString sysName READ sysName CONSTANT) + Q_PROPERTY(QString outputName READ outputName WRITE setOutputName NOTIFY outputNameChanged) + Q_PROPERTY(QSizeF size READ size CONSTANT) + Q_PROPERTY(quint32 product READ product CONSTANT) + Q_PROPERTY(quint32 vendor READ vendor CONSTANT) + Q_PROPERTY(bool supportsDisableEvents READ supportsDisableEvents CONSTANT) + Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged) + Q_PROPERTY(bool enabledByDefault READ isEnabledByDefault CONSTANT) + // + // advanced + Q_PROPERTY(int supportedButtons READ supportedButtons CONSTANT) + Q_PROPERTY(bool supportsCalibrationMatrix READ supportsCalibrationMatrix CONSTANT) + Q_PROPERTY(QString defaultCalibrationMatrix READ defaultCalibrationMatrix CONSTANT) + Q_PROPERTY(QString calibrationMatrix READ serializedCalibrationMatrix WRITE setCalibrationMatrix NOTIFY calibrationMatrixChanged) + Q_PROPERTY(Qt::ScreenOrientation orientation READ orientation WRITE setOrientation NOTIFY orientationChanged) + Q_PROPERTY(int orientationDBus READ orientation WRITE setOrientationDBus NOTIFY orientationChanged) + + Q_PROPERTY(bool supportsLeftHanded READ supportsLeftHanded CONSTANT) + Q_PROPERTY(bool leftHandedEnabledByDefault READ leftHandedEnabledByDefault CONSTANT) + Q_PROPERTY(bool leftHanded READ isLeftHanded WRITE setLeftHanded NOTIFY leftHandedChanged) + + Q_PROPERTY(bool supportsDisableEventsOnExternalMouse READ supportsDisableEventsOnExternalMouse CONSTANT) + Q_PROPERTY(bool disableEventsOnExternalMouseEnabledByDefault READ disableEventsOnExternalMouseEnabledByDefault CONSTANT) + Q_PROPERTY(bool disableEventsOnExternalMouse READ isDisableEventsOnExternalMouse WRITE setDisableEventsOnExternalMouse NOTIFY disableEventsOnExternalMouseChanged) + + Q_PROPERTY(bool supportsDisableWhileTyping READ supportsDisableWhileTyping CONSTANT) + Q_PROPERTY(bool disableWhileTypingEnabledByDefault READ disableWhileTypingEnabledByDefault CONSTANT) + Q_PROPERTY(bool disableWhileTyping READ isDisableWhileTyping WRITE setDisableWhileTyping NOTIFY disableWhileTypingChanged) + // + // acceleration speed and profile + Q_PROPERTY(bool supportsPointerAcceleration READ supportsPointerAcceleration CONSTANT) + Q_PROPERTY(qreal defaultPointerAcceleration READ defaultPointerAcceleration CONSTANT) + Q_PROPERTY(qreal pointerAcceleration READ pointerAcceleration WRITE setPointerAcceleration NOTIFY pointerAccelerationChanged) + + Q_PROPERTY(bool supportsPointerAccelerationProfileFlat READ supportsPointerAccelerationProfileFlat CONSTANT) + Q_PROPERTY(bool defaultPointerAccelerationProfileFlat READ defaultPointerAccelerationProfileFlat CONSTANT) + Q_PROPERTY(bool pointerAccelerationProfileFlat READ pointerAccelerationProfileFlat WRITE setPointerAccelerationProfileFlat NOTIFY pointerAccelerationProfileChanged) + + Q_PROPERTY(bool supportsPointerAccelerationProfileAdaptive READ supportsPointerAccelerationProfileAdaptive CONSTANT) + Q_PROPERTY(bool defaultPointerAccelerationProfileAdaptive READ defaultPointerAccelerationProfileAdaptive CONSTANT) + Q_PROPERTY(bool pointerAccelerationProfileAdaptive READ pointerAccelerationProfileAdaptive WRITE setPointerAccelerationProfileAdaptive NOTIFY pointerAccelerationProfileChanged) + // + // tapping + Q_PROPERTY(int tapFingerCount READ tapFingerCount CONSTANT) + Q_PROPERTY(bool tapToClickEnabledByDefault READ tapToClickEnabledByDefault CONSTANT) + Q_PROPERTY(bool tapToClick READ isTapToClick WRITE setTapToClick NOTIFY tapToClickChanged) + + Q_PROPERTY(bool supportsLmrTapButtonMap READ supportsLmrTapButtonMap CONSTANT) + Q_PROPERTY(bool lmrTapButtonMapEnabledByDefault READ lmrTapButtonMapEnabledByDefault CONSTANT) + Q_PROPERTY(bool lmrTapButtonMap READ lmrTapButtonMap WRITE setLmrTapButtonMap NOTIFY tapButtonMapChanged) + + Q_PROPERTY(bool tapAndDragEnabledByDefault READ tapAndDragEnabledByDefault CONSTANT) + Q_PROPERTY(bool tapAndDrag READ isTapAndDrag WRITE setTapAndDrag NOTIFY tapAndDragChanged) + Q_PROPERTY(bool tapDragLockEnabledByDefault READ tapDragLockEnabledByDefault CONSTANT) + Q_PROPERTY(bool tapDragLock READ isTapDragLock WRITE setTapDragLock NOTIFY tapDragLockChanged) + + Q_PROPERTY(bool supportsMiddleEmulation READ supportsMiddleEmulation CONSTANT) + Q_PROPERTY(bool middleEmulationEnabledByDefault READ middleEmulationEnabledByDefault CONSTANT) + Q_PROPERTY(bool middleEmulation READ isMiddleEmulation WRITE setMiddleEmulation NOTIFY middleEmulationChanged) + // + // scrolling + Q_PROPERTY(bool supportsNaturalScroll READ supportsNaturalScroll CONSTANT) + Q_PROPERTY(bool naturalScrollEnabledByDefault READ naturalScrollEnabledByDefault CONSTANT) + Q_PROPERTY(bool naturalScroll READ isNaturalScroll WRITE setNaturalScroll NOTIFY naturalScrollChanged) + + Q_PROPERTY(bool supportsScrollTwoFinger READ supportsScrollTwoFinger CONSTANT) + Q_PROPERTY(bool scrollTwoFingerEnabledByDefault READ scrollTwoFingerEnabledByDefault CONSTANT) + Q_PROPERTY(bool scrollTwoFinger READ isScrollTwoFinger WRITE setScrollTwoFinger NOTIFY scrollMethodChanged) + + Q_PROPERTY(bool supportsScrollEdge READ supportsScrollEdge CONSTANT) + Q_PROPERTY(bool scrollEdgeEnabledByDefault READ scrollEdgeEnabledByDefault CONSTANT) + Q_PROPERTY(bool scrollEdge READ isScrollEdge WRITE setScrollEdge NOTIFY scrollMethodChanged) + + Q_PROPERTY(bool supportsScrollOnButtonDown READ supportsScrollOnButtonDown CONSTANT) + Q_PROPERTY(bool scrollOnButtonDownEnabledByDefault READ scrollOnButtonDownEnabledByDefault CONSTANT) + Q_PROPERTY(quint32 defaultScrollButton READ defaultScrollButton CONSTANT) + Q_PROPERTY(bool scrollOnButtonDown READ isScrollOnButtonDown WRITE setScrollOnButtonDown NOTIFY scrollMethodChanged) + Q_PROPERTY(quint32 scrollButton READ scrollButton WRITE setScrollButton NOTIFY scrollButtonChanged) + + Q_PROPERTY(qreal scrollFactor READ scrollFactor WRITE setScrollFactor NOTIFY scrollFactorChanged) + // + // switches + Q_PROPERTY(bool switchDevice READ isSwitch CONSTANT) + Q_PROPERTY(bool lidSwitch READ isLidSwitch CONSTANT) + Q_PROPERTY(bool tabletModeSwitch READ isTabletModeSwitch CONSTANT) + + // Click Methods + Q_PROPERTY(bool supportsClickMethodAreas READ supportsClickMethodAreas CONSTANT) + Q_PROPERTY(bool defaultClickMethodAreas READ defaultClickMethodAreas CONSTANT) + Q_PROPERTY(bool clickMethodAreas READ isClickMethodAreas WRITE setClickMethodAreas NOTIFY clickMethodChanged) + + Q_PROPERTY(bool supportsClickMethodClickfinger READ supportsClickMethodClickfinger CONSTANT) + Q_PROPERTY(bool defaultClickMethodClickfinger READ defaultClickMethodClickfinger CONSTANT) + Q_PROPERTY(bool clickMethodClickfinger READ isClickMethodClickfinger WRITE setClickMethodClickfinger NOTIFY clickMethodChanged) + + Q_PROPERTY(bool supportsOutputArea READ supportsOutputArea CONSTANT) + Q_PROPERTY(QRectF defaultOutputArea READ defaultOutputArea CONSTANT) + Q_PROPERTY(QRectF outputArea READ outputArea WRITE setOutputArea NOTIFY outputAreaChanged) + Q_PROPERTY(bool defaultMapToWorkspace READ defaultMapToWorkspace CONSTANT) + Q_PROPERTY(bool mapToWorkspace READ isMapToWorkspace WRITE setMapToWorkspace NOTIFY mapToWorkspaceChanged) + Q_PROPERTY(QString deviceGroupId READ deviceGroupId CONSTANT) + Q_PROPERTY(QString defaultPressureCurve READ defaultPressureCurve CONSTANT) + Q_PROPERTY(QString pressureCurve READ serializedPressureCurve WRITE setPressureCurve NOTIFY pressureCurveChanged) + Q_PROPERTY(quint32 tabletPadButtonCount READ tabletPadButtonCount CONSTANT) + Q_PROPERTY(quint32 tabletPadDialCount READ tabletPadDialCount CONSTANT) + Q_PROPERTY(quint32 tabletPadRingCount READ tabletPadRingCount CONSTANT) + Q_PROPERTY(quint32 tabletPadStripCount READ tabletPadStripCount CONSTANT) + Q_PROPERTY(bool supportsInputArea READ supportsInputArea CONSTANT) + Q_PROPERTY(QRectF defaultInputArea READ defaultInputArea CONSTANT) + Q_PROPERTY(QRectF inputArea READ inputArea WRITE setInputArea NOTIFY inputAreaChanged) + Q_PROPERTY(QList numModes READ numModes CONSTANT) + Q_PROPERTY(QList currentModes READ currentModes NOTIFY currentModesChanged) + + Q_PROPERTY(bool supportsPressureRange READ supportsPressureRange NOTIFY supportsPressureRangeChanged) + Q_PROPERTY(double pressureRangeMin READ pressureRangeMin WRITE setPressureRangeMin NOTIFY pressureRangeMinChanged) + Q_PROPERTY(double pressureRangeMax READ pressureRangeMax WRITE setPressureRangeMax NOTIFY pressureRangeMaxChanged) + Q_PROPERTY(double defaultPressureRangeMin READ defaultPressureRangeMin CONSTANT) + Q_PROPERTY(double defaultPressureRangeMax READ defaultPressureRangeMax CONSTANT) + + Q_PROPERTY(bool tabletToolIsRelative READ tabletToolIsRelative WRITE setTabletToolRelative NOTIFY tabletToolRelativeChanged) + Q_PROPERTY(bool supportsRotation READ supportsRotation CONSTANT) + /// rotation angle, as 0 to 360 degrees + Q_PROPERTY(uint32_t rotation READ rotation WRITE setRotation NOTIFY rotationChanged) + Q_PROPERTY(uint32_t defaultRotation READ defaultRotation CONSTANT) + Q_PROPERTY(bool isVirtual READ isVirtual CONSTANT) + +public: + explicit Device(libinput_device *device, QObject *parent = nullptr); + ~Device() override; + + bool isKeyboard() const override + { + return m_keyboard; + } + /** + * Note that this has a lot of false positives + */ + bool isAlphaNumericKeyboard() const + { + return m_alphaNumericKeyboard; + } + bool isPointer() const override + { + return m_pointer; + } + bool isTouchpad() const override + { + return m_touchpad && + // ignore all combined devices. E.g. a touchpad on a keyboard we don't want to toggle + // as that would result in the keyboard going off as well + !(m_keyboard || m_touch || m_tabletPad || m_tabletTool); + } + bool isTouch() const override + { + return m_touch; + } + bool isTabletTool() const override + { + return m_tabletTool; + } + bool isTabletPad() const override + { + return m_tabletPad; + } + bool supportsGesture() const + { + return m_supportsGesture; + } + QString name() const override + { + return m_name; + } + QString sysName() const + { + return m_sysName; + } + QString sysPath() const override + { + return m_sysPath; + } + QString outputName() const override + { + return m_outputName; + } + QString outputUuid() const + { + return m_outputUuid; + } + QSizeF size() const + { + return m_size; + } + quint32 product() const override + { + return m_product; + } + quint32 vendor() const override + { + return m_vendor; + } + quint32 busType() const override + { + return m_busType; + } + void *group() const override; + Qt::MouseButtons supportedButtons() const + { + return m_supportedButtons; + } + int tapFingerCount() const + { + return m_tapFingerCount; + } + bool tapToClickEnabledByDefault() const + { + return defaultValue("TapToClick", m_tapToClickEnabledByDefault); + } + bool isTapToClick() const + { + return m_tapToClick; + } + /** + * Set the Device to tap to click if @p set is @c true. + */ + void setTapToClick(bool set); + bool tapAndDragEnabledByDefault() const + { + return defaultValue("TapAndDrag", m_tapAndDragEnabledByDefault); + } + bool isTapAndDrag() const + { + return m_tapAndDrag; + } + void setTapAndDrag(bool set); + bool tapDragLockEnabledByDefault() const + { + return defaultValue("TapDragLock", m_tapDragLockEnabledByDefault); + } + bool isTapDragLock() const + { + return m_tapDragLock; + } + void setTapDragLock(bool set); + bool supportsDisableWhileTyping() const + { + return m_supportsDisableWhileTyping; + } + bool disableWhileTypingEnabledByDefault() const + { + return defaultValue("DisableWhileTyping", m_disableWhileTypingEnabledByDefault); + } + bool supportsPointerAcceleration() const + { + return m_supportsPointerAcceleration; + } + bool supportsLeftHanded() const + { + return m_supportsLeftHanded; + } + bool supportsCalibrationMatrix() const + { + return m_supportsCalibrationMatrix; + } + bool supportsDisableEvents() const + { + return m_supportsDisableEvents; + } + bool supportsDisableEventsOnExternalMouse() const + { + return m_supportsDisableEventsOnExternalMouse; + } + bool supportsMiddleEmulation() const + { + return m_supportsMiddleEmulation; + } + bool supportsNaturalScroll() const + { + return m_supportsNaturalScroll; + } + bool supportsScrollTwoFinger() const + { + return (m_supportedScrollMethods & LIBINPUT_CONFIG_SCROLL_2FG); + } + bool supportsScrollEdge() const + { + return (m_supportedScrollMethods & LIBINPUT_CONFIG_SCROLL_EDGE); + } + bool supportsScrollOnButtonDown() const + { + return (m_supportedScrollMethods & LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN); + } + bool leftHandedEnabledByDefault() const + { + return defaultValue("LeftHanded", m_leftHandedEnabledByDefault); + } + bool middleEmulationEnabledByDefault() const + { + return defaultValue("MiddleButtonEmulation", m_middleEmulationEnabledByDefault); + } + bool naturalScrollEnabledByDefault() const + { + return defaultValue("NaturalScroll", m_naturalScrollEnabledByDefault); + } + enum libinput_config_scroll_method defaultScrollMethod() const + { + quint32 defaultScrollMethod = defaultValue("ScrollMethod", static_cast(m_defaultScrollMethod)); + return static_cast(defaultScrollMethod); + } + quint32 defaultScrollMethodToInt() const + { + return static_cast(defaultScrollMethod()); + } + bool scrollTwoFingerEnabledByDefault() const + { + return defaultScrollMethod() == LIBINPUT_CONFIG_SCROLL_2FG; + } + bool scrollEdgeEnabledByDefault() const + { + return defaultScrollMethod() == LIBINPUT_CONFIG_SCROLL_EDGE; + } + bool scrollOnButtonDownEnabledByDefault() const + { + return defaultScrollMethod() == LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN; + } + bool supportsLmrTapButtonMap() const + { + return m_tapFingerCount > 1; + } + bool lmrTapButtonMapEnabledByDefault() const + { + quint32 lmrButtonMap = defaultValue("LmrTapButtonMap", static_cast(m_defaultTapButtonMap)); + return lmrButtonMap == LIBINPUT_CONFIG_TAP_MAP_LMR; + } + + void setLmrTapButtonMap(bool set); + bool lmrTapButtonMap() const + { + return m_tapButtonMap & LIBINPUT_CONFIG_TAP_MAP_LMR; + } + + quint32 defaultScrollButton() const + { + return m_defaultScrollButton; + } + bool isMiddleEmulation() const + { + return m_middleEmulation; + } + void setMiddleEmulation(bool set); + bool isNaturalScroll() const + { + return m_naturalScroll; + } + void setNaturalScroll(bool set); + void setScrollMethod(bool set, enum libinput_config_scroll_method method); + bool isScrollTwoFinger() const + { + return m_scrollMethod & LIBINPUT_CONFIG_SCROLL_2FG; + } + void setScrollTwoFinger(bool set) + { + setScrollMethod(set, LIBINPUT_CONFIG_SCROLL_2FG); + } + bool isScrollEdge() const + { + return m_scrollMethod & LIBINPUT_CONFIG_SCROLL_EDGE; + } + void setScrollEdge(bool set) + { + setScrollMethod(set, LIBINPUT_CONFIG_SCROLL_EDGE); + } + bool isScrollOnButtonDown() const + { + return m_scrollMethod & LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN; + } + void setScrollOnButtonDown(bool set) + { + setScrollMethod(set, LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN); + } + void activateScrollMethodFromInt(quint32 method) + { + setScrollMethod(true, (libinput_config_scroll_method)method); + } + quint32 scrollButton() const + { + return m_scrollButton; + } + void setScrollButton(quint32 button); + + qreal scrollFactorDefault() const + { + return defaultValue("ScrollFactor", 1.0); + } + qreal scrollFactor() const + { + return m_scrollFactor; + } + void setScrollFactor(qreal factor); + + void setDisableWhileTyping(bool set); + bool isDisableWhileTyping() const + { + return m_disableWhileTyping; + } + bool isLeftHanded() const + { + return m_leftHanded; + } + /** + * Sets the Device to left handed mode if @p set is @c true. + * If @p set is @c false the device is set to right handed mode + */ + void setLeftHanded(bool set); + + QString defaultCalibrationMatrix() const + { + auto list = defaultValue("CalibrationMatrix", QList{}); + if (list.size() == 16) { + return serializeMatrix(QMatrix4x4{list.constData()}); + } + + return serializeMatrix(m_defaultCalibrationMatrix); + } + QMatrix4x4 calibrationMatrix() const + { + return m_calibrationMatrix; + } + void setCalibrationMatrix(const QString &value); + QString serializedCalibrationMatrix() const + { + return serializeMatrix(m_calibrationMatrix); + } + + static QString serializeMatrix(const QMatrix4x4 &matrix); + static QMatrix4x4 deserializeMatrix(const QString &matrix); + + QString defaultPressureCurve() const; + QEasingCurve pressureCurve() const; + QString serializedPressureCurve() const; + void setPressureCurve(const QString &curve); + + static QString serializePressureCurve(const QEasingCurve &curve); + static QEasingCurve deserializePressureCurve(const QString &curve); + + Qt::ScreenOrientation defaultOrientation() const + { + quint32 orientation = defaultValue("Orientation", static_cast(Qt::PrimaryOrientation)); + return static_cast(orientation); + } + Qt::ScreenOrientation orientation() const + { + return m_orientation; + } + void setOrientation(Qt::ScreenOrientation orientation); + void setOrientationDBus(int orientation) + { + setOrientation(Qt::ScreenOrientation(orientation)); + } + + qreal defaultPointerAcceleration() const + { + return m_defaultPointerAcceleration; + } + qreal pointerAcceleration() const + { + return m_pointerAcceleration; + } + /** + * @param acceleration mapped to range [-1,1] with -1 being the slowest, 1 being the fastest supported acceleration. + */ + void setPointerAcceleration(qreal acceleration); + void setPointerAccelerationFromString(const QString &acceleration) + { + setPointerAcceleration(acceleration.toDouble()); + } + QString defaultPointerAccelerationToString() const + { + return QString::number(m_pointerAcceleration, 'f', 3); + } + bool supportsPointerAccelerationProfileFlat() const + { + return (m_supportedPointerAccelerationProfiles & LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT); + } + bool supportsPointerAccelerationProfileAdaptive() const + { + return (m_supportedPointerAccelerationProfiles & LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE); + } + bool defaultPointerAccelerationProfileFlat() const + { + return (m_defaultPointerAccelerationProfile & LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT); + } + bool defaultPointerAccelerationProfileAdaptive() const + { + return (m_defaultPointerAccelerationProfile & LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE); + } + bool pointerAccelerationProfileFlat() const + { + return (m_pointerAccelerationProfile & LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT); + } + bool pointerAccelerationProfileAdaptive() const + { + return (m_pointerAccelerationProfile & LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE); + } + void setPointerAccelerationProfile(bool set, enum libinput_config_accel_profile profile); + void setPointerAccelerationProfileFlat(bool set) + { + setPointerAccelerationProfile(set, LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT); + } + void setPointerAccelerationProfileAdaptive(bool set) + { + setPointerAccelerationProfile(set, LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE); + } + void setPointerAccelerationProfileFromInt(quint32 profile) + { + setPointerAccelerationProfile(true, (libinput_config_accel_profile)profile); + } + quint32 defaultPointerAccelerationProfileToInt() const + { + return defaultValue("PointerAccelerationProfile", static_cast(m_defaultPointerAccelerationProfile)); + } + bool supportsClickMethodAreas() const + { + return (m_supportedClickMethods & LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS); + } + bool defaultClickMethodAreas() const + { + return (defaultClickMethod() == LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS); + } + bool isClickMethodAreas() const + { + return (m_clickMethod == LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS); + } + bool supportsClickMethodClickfinger() const + { + return (m_supportedClickMethods & LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER); + } + bool defaultClickMethodClickfinger() const + { + return (defaultClickMethod() == LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER); + } + bool isClickMethodClickfinger() const + { + return (m_clickMethod == LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER); + } + void setClickMethod(bool set, enum libinput_config_click_method method); + void setClickMethodAreas(bool set) + { + setClickMethod(set, LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS); + } + void setClickMethodClickfinger(bool set) + { + setClickMethod(set, LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER); + } + void setClickMethodFromInt(quint32 method) + { + setClickMethod(true, (libinput_config_click_method)method); + } + libinput_config_click_method defaultClickMethod() const + { + return static_cast(defaultClickMethodToInt()); + } + quint32 defaultClickMethodToInt() const + { + return defaultValue("ClickMethod", static_cast(m_defaultClickMethod)); + } + + bool isEnabled() const override + { + return m_enabled; + } + void setEnabled(bool enabled) override; + + bool isEnabledByDefault() const + { + return defaultValue("Enabled", true); + } + bool disableEventsOnExternalMouseEnabledByDefault() const + { + return defaultValue("DisableEventsOnExternalMouse", m_disableEventsOnExternalMouseEnabledByDefault); + } + bool isDisableEventsOnExternalMouse() const + { + return m_disableEventsOnExternalMouse; + } + void setDisableEventsOnExternalMouse(bool set); + + libinput_device *device() const + { + return m_device; + } + + /** + * Sets the @p config to load the Device configuration from and to store each + * successful Device configuration. + */ + void setConfig(const KConfigGroup &config) + { + m_config = config; + } + + void setDefaultConfig(const KConfigGroup &config) + { + m_defaultConfig = config; + } + + /** + * Sets the output name, and if a matching output is found, + * also the UUID of that output + */ + void setOutputName(const QString &name) override; + /** + * Only sets the output name, for config loading purposes + */ + void setConfigOutputName(const QString &name); + QString defaultOutputName() const + { + return {}; + } + + void setOutputUuid(const QString &uuid); + QString defaultOutputUuid() const + { + return QString(); + } + + /** + * Loads the configuration and applies it to the Device + */ + void loadConfiguration(); + + bool isSwitch() const + { + return m_switch; + } + + bool isLidSwitch() const override + { + return m_lidSwitch; + } + + bool isTabletModeSwitch() const override + { + return m_tabletSwitch; + } + + int tabletPadButtonCount() const override; + int tabletPadDialCount() const override; + int tabletPadRingCount() const override; + int tabletPadStripCount() const override; + QList modeGroups() const override; + + BackendOutput *output() const; + void setOutput(KWin::BackendOutput *output); + + LEDs leds() const override; + void setLeds(LEDs leds) override; + + QRectF defaultOutputArea() const; + bool supportsOutputArea() const; + QRectF outputArea() const; + void setOutputArea(const QRectF &outputArea); + + bool defaultMapToWorkspace() const + { + return defaultValue("MapToWorkspace", false); + } + + bool isMapToWorkspace() const + { + return m_mapToWorkspace; + } + + void setMapToWorkspace(bool mapToWorkspace); + + QString deviceGroupId() const + { + return m_deviceGroupId; + } + + bool supportsPressureRange() const; + void setSupportsPressureRange(bool supported); + double pressureRangeMin() const; + void setPressureRangeMin(double value); + double pressureRangeMax() const; + void setPressureRangeMax(double value); + double defaultPressureRangeMin() const; + double defaultPressureRangeMax() const; + + bool supportsInputArea() const; + QRectF inputArea() const; + void setInputArea(const QRectF &inputArea); + QRectF defaultInputArea() const; + + bool tabletToolIsRelative() const override + { + return m_tabletToolIsRelative; + } + + void setTabletToolRelative(bool relative); + + bool defaultTabletToolIsRelative() const + { + return defaultValue("TabletToolRelativeMode", false); + } + + QList numModes() const; + QList currentModes() const; + + bool supportsRotation() const; + uint32_t rotation() const; + void setRotation(uint32_t degrees_cw); + uint32_t defaultRotation() const; + bool isVirtual() const; + + /** + * Gets the Device for @p native. @c null if there is no Device for @p native. + */ + static Device *get(libinput_device *native); + +Q_SIGNALS: + void tapButtonMapChanged(); + void calibrationMatrixChanged(); + void orientationChanged(); + void outputNameChanged(); + void leftHandedChanged(); + void disableWhileTypingChanged(); + void pointerAccelerationChanged(); + void pointerAccelerationProfileChanged(); + void enabledChanged(); + void disableEventsOnExternalMouseChanged(); + void tapToClickChanged(); + void tapAndDragChanged(); + void tapDragLockChanged(); + void middleEmulationChanged(); + void naturalScrollChanged(); + void scrollMethodChanged(); + void scrollButtonChanged(); + void scrollFactorChanged(); + void clickMethodChanged(); + void outputAreaChanged(); + void mapToWorkspaceChanged(); + void pressureCurveChanged(); + void supportsPressureRangeChanged(); + void pressureRangeMinChanged(); + void pressureRangeMaxChanged(); + void inputAreaChanged(); + void tabletToolRelativeChanged(); + void rotationChanged(); + void currentModesChanged(); + +private: + template + void writeEntry(const ConfigKey &key, const T &value); + + template + T defaultValue(const char *key, const T &fallback) const + { + if (m_defaultConfig.isValid() && m_defaultConfig.hasKey(key)) { + return m_defaultConfig.readEntry(key, fallback); + } + + return fallback; + } + + libinput_device *m_device; + bool m_keyboard; + bool m_alphaNumericKeyboard = false; + bool m_pointer; + bool m_touch; + bool m_tabletTool; + bool m_tabletPad; + bool m_supportsGesture; + bool m_switch = false; + bool m_lidSwitch = false; + bool m_tabletSwitch = false; + bool m_touchpad = false; + QString m_name; + QString m_sysName; + QString m_sysPath; + QString m_outputName; + QString m_outputUuid; + QSizeF m_size; + quint32 m_product; + quint32 m_vendor; + quint32 m_busType; + Qt::MouseButtons m_supportedButtons = Qt::NoButton; + int m_tapFingerCount; + enum libinput_config_tap_button_map m_defaultTapButtonMap; + enum libinput_config_tap_button_map m_tapButtonMap; + bool m_tapToClickEnabledByDefault; + bool m_tapToClick; + bool m_tapAndDragEnabledByDefault; + bool m_tapAndDrag; + bool m_tapDragLockEnabledByDefault; + bool m_tapDragLock; + bool m_supportsDisableWhileTyping; + bool m_supportsPointerAcceleration; + bool m_supportsLeftHanded; + bool m_supportsCalibrationMatrix; + bool m_supportsDisableEvents; + bool m_supportsDisableEventsOnExternalMouse; + bool m_supportsMiddleEmulation; + bool m_supportsNaturalScroll; + quint32 m_supportedScrollMethods; + bool m_supportsScrollEdge; + bool m_supportsScrollOnButtonDown; + bool m_leftHandedEnabledByDefault; + bool m_middleEmulationEnabledByDefault; + bool m_naturalScrollEnabledByDefault; + enum libinput_config_scroll_method m_defaultScrollMethod; + quint32 m_defaultScrollButton; + bool m_disableWhileTypingEnabledByDefault; + bool m_disableWhileTyping; + bool m_middleEmulation; + bool m_leftHanded; + bool m_naturalScroll; + enum libinput_config_scroll_method m_scrollMethod; + quint32 m_scrollButton; + qreal m_defaultPointerAcceleration; + qreal m_pointerAcceleration; + qreal m_scrollFactor; + quint32 m_supportedPointerAccelerationProfiles; + enum libinput_config_accel_profile m_defaultPointerAccelerationProfile; + enum libinput_config_accel_profile m_pointerAccelerationProfile; + bool m_enabled; + bool m_disableEventsOnExternalMouseEnabledByDefault; + bool m_disableEventsOnExternalMouse; + + KConfigGroup m_config; + KConfigGroup m_defaultConfig; + bool m_loading = false; + + QPointer m_output; + Qt::ScreenOrientation m_orientation = Qt::PrimaryOrientation; + QMatrix4x4 m_defaultCalibrationMatrix; + QMatrix4x4 m_calibrationMatrix; + QEasingCurve m_pressureCurve; + quint32 m_supportedClickMethods; + enum libinput_config_click_method m_defaultClickMethod; + enum libinput_config_click_method m_clickMethod; + + LEDs m_leds; + QRectF m_outputArea; + bool m_mapToWorkspace = false; + QString m_deviceGroupId; + + bool m_supportsPressureRange; + double m_pressureRangeMin; + double m_pressureRangeMax; + double m_defaultPressureRangeMin; + double m_defaultPressureRangeMax; + + QRectF m_inputArea; + bool m_tabletToolIsRelative = false; + QList m_currentModes; + bool m_isVirtual = false; +}; + +} +} + +Q_DECLARE_METATYPE(KWin::LibInput::Device *) diff --git a/local/recipes/kde/kwin/source/src/backends/libinput/events.cpp b/local/recipes/kde/kwin/source/src/backends/libinput/events.cpp new file mode 100644 index 0000000000..60f0cb91c9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/libinput/events.cpp @@ -0,0 +1,382 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "events.h" +#include "device.h" + +#include + +namespace KWin +{ +namespace LibInput +{ + +std::unique_ptr Event::create(libinput_event *event) +{ + if (!event) { + return nullptr; + } + const auto t = libinput_event_get_type(event); + // TODO: add touch events + // TODO: add device notify events + switch (t) { + case LIBINPUT_EVENT_KEYBOARD_KEY: + return std::make_unique(event); + case LIBINPUT_EVENT_POINTER_SCROLL_WHEEL: + case LIBINPUT_EVENT_POINTER_SCROLL_FINGER: + case LIBINPUT_EVENT_POINTER_SCROLL_CONTINUOUS: + case LIBINPUT_EVENT_POINTER_BUTTON: + case LIBINPUT_EVENT_POINTER_MOTION: + case LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE: + return std::make_unique(event, t); + case LIBINPUT_EVENT_TOUCH_DOWN: + case LIBINPUT_EVENT_TOUCH_UP: + case LIBINPUT_EVENT_TOUCH_MOTION: + case LIBINPUT_EVENT_TOUCH_CANCEL: + case LIBINPUT_EVENT_TOUCH_FRAME: + return std::make_unique(event, t); + case LIBINPUT_EVENT_GESTURE_SWIPE_BEGIN: + case LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE: + case LIBINPUT_EVENT_GESTURE_SWIPE_END: + return std::make_unique(event, t); + case LIBINPUT_EVENT_GESTURE_PINCH_BEGIN: + case LIBINPUT_EVENT_GESTURE_PINCH_UPDATE: + case LIBINPUT_EVENT_GESTURE_PINCH_END: + return std::make_unique(event, t); + case LIBINPUT_EVENT_GESTURE_HOLD_BEGIN: + case LIBINPUT_EVENT_GESTURE_HOLD_END: + return std::make_unique(event, t); + case LIBINPUT_EVENT_TABLET_TOOL_AXIS: + case LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY: + case LIBINPUT_EVENT_TABLET_TOOL_TIP: + return std::make_unique(event, t); + case LIBINPUT_EVENT_TABLET_TOOL_BUTTON: + return std::make_unique(event, t); + case LIBINPUT_EVENT_TABLET_PAD_RING: + return std::make_unique(event, t); + case LIBINPUT_EVENT_TABLET_PAD_STRIP: + return std::make_unique(event, t); + case LIBINPUT_EVENT_TABLET_PAD_BUTTON: + return std::make_unique(event, t); + case LIBINPUT_EVENT_SWITCH_TOGGLE: + return std::make_unique(event, t); + case LIBINPUT_EVENT_TABLET_PAD_DIAL: + return std::make_unique(event, t); + default: + return std::unique_ptr{new Event(event, t)}; + } +} + +Event::Event(libinput_event *event, libinput_event_type type) + : m_event(event) + , m_type(type) + , m_device(nullptr) +{ +} + +Event::~Event() +{ + libinput_event_destroy(m_event); +} + +Device *Event::device() const +{ + if (!m_device) { + m_device = Device::get(libinput_event_get_device(m_event)); + } + return m_device; +} + +libinput_device *Event::nativeDevice() const +{ + if (m_device) { + return m_device->device(); + } + return libinput_event_get_device(m_event); +} + +KeyEvent::KeyEvent(libinput_event *event) + : Event(event, LIBINPUT_EVENT_KEYBOARD_KEY) + , m_keyboardEvent(libinput_event_get_keyboard_event(event)) +{ +} + +KeyEvent::~KeyEvent() = default; + +uint32_t KeyEvent::key() const +{ + return libinput_event_keyboard_get_key(m_keyboardEvent); +} + +KeyboardKeyState KeyEvent::state() const +{ + switch (libinput_event_keyboard_get_key_state(m_keyboardEvent)) { + case LIBINPUT_KEY_STATE_PRESSED: + return KeyboardKeyState::Pressed; + case LIBINPUT_KEY_STATE_RELEASED: + return KeyboardKeyState::Released; + default: + Q_UNREACHABLE(); + } +} + +std::chrono::microseconds KeyEvent::time() const +{ + return std::chrono::microseconds(libinput_event_keyboard_get_time_usec(m_keyboardEvent)); +} + +PointerEvent::PointerEvent(libinput_event *event, libinput_event_type type) + : Event(event, type) + , m_pointerEvent(libinput_event_get_pointer_event(event)) +{ +} + +PointerEvent::~PointerEvent() = default; + +QPointF PointerEvent::absolutePos() const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE); + return QPointF(libinput_event_pointer_get_absolute_x(m_pointerEvent), + libinput_event_pointer_get_absolute_y(m_pointerEvent)); +} + +QPointF PointerEvent::absolutePos(const QSize &size) const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE); + return QPointF(libinput_event_pointer_get_absolute_x_transformed(m_pointerEvent, size.width()), + libinput_event_pointer_get_absolute_y_transformed(m_pointerEvent, size.height())); +} + +QPointF PointerEvent::delta() const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_POINTER_MOTION); + return QPointF(libinput_event_pointer_get_dx(m_pointerEvent), libinput_event_pointer_get_dy(m_pointerEvent)); +} + +QPointF PointerEvent::deltaUnaccelerated() const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_POINTER_MOTION); + return QPointF(libinput_event_pointer_get_dx_unaccelerated(m_pointerEvent), libinput_event_pointer_get_dy_unaccelerated(m_pointerEvent)); +} + +std::chrono::microseconds PointerEvent::time() const +{ + return std::chrono::microseconds(libinput_event_pointer_get_time_usec(m_pointerEvent)); +} + +uint32_t PointerEvent::button() const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_POINTER_BUTTON); + return libinput_event_pointer_get_button(m_pointerEvent); +} + +PointerButtonState PointerEvent::buttonState() const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_POINTER_BUTTON); + switch (libinput_event_pointer_get_button_state(m_pointerEvent)) { + case LIBINPUT_BUTTON_STATE_PRESSED: + return PointerButtonState::Pressed; + case LIBINPUT_BUTTON_STATE_RELEASED: + return PointerButtonState::Released; + default: + Q_UNREACHABLE(); + } +} + +QList PointerEvent::axis() const +{ + QList a; + if (libinput_event_pointer_has_axis(m_pointerEvent, LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL)) { + a << PointerAxis::Horizontal; + } + if (libinput_event_pointer_has_axis(m_pointerEvent, LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL)) { + a << PointerAxis::Vertical; + } + return a; +} + +qreal PointerEvent::scrollValue(PointerAxis axis) const +{ + const libinput_pointer_axis a = axis == PointerAxis::Horizontal + ? LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL + : LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL; + return libinput_event_pointer_get_scroll_value(m_pointerEvent, a) * device()->scrollFactor(); +} + +qint32 PointerEvent::scrollValueV120(PointerAxis axis) const +{ + const libinput_pointer_axis a = (axis == PointerAxis::Horizontal) + ? LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL + : LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL; + return libinput_event_pointer_get_scroll_value_v120(m_pointerEvent, a) * device()->scrollFactor(); +} + +TouchEvent::TouchEvent(libinput_event *event, libinput_event_type type) + : Event(event, type) + , m_touchEvent(libinput_event_get_touch_event(event)) +{ +} + +TouchEvent::~TouchEvent() = default; + +std::chrono::microseconds TouchEvent::time() const +{ + return std::chrono::microseconds(libinput_event_touch_get_time_usec(m_touchEvent)); +} + +QPointF TouchEvent::absolutePos() const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_TOUCH_DOWN || type() == LIBINPUT_EVENT_TOUCH_MOTION); + return QPointF(libinput_event_touch_get_x(m_touchEvent), + libinput_event_touch_get_y(m_touchEvent)); +} + +QPointF TouchEvent::absolutePos(const QSize &size) const +{ + Q_ASSERT(type() == LIBINPUT_EVENT_TOUCH_DOWN || type() == LIBINPUT_EVENT_TOUCH_MOTION); + return QPointF(libinput_event_touch_get_x_transformed(m_touchEvent, size.width()), + libinput_event_touch_get_y_transformed(m_touchEvent, size.height())); +} + +qint32 TouchEvent::id() const +{ + Q_ASSERT(type() != LIBINPUT_EVENT_TOUCH_FRAME); + + return libinput_event_touch_get_seat_slot(m_touchEvent); +} + +GestureEvent::GestureEvent(libinput_event *event, libinput_event_type type) + : Event(event, type) + , m_gestureEvent(libinput_event_get_gesture_event(event)) +{ +} + +GestureEvent::~GestureEvent() = default; + +std::chrono::microseconds GestureEvent::time() const +{ + return std::chrono::microseconds(libinput_event_gesture_get_time_usec(m_gestureEvent)); +} + +int GestureEvent::fingerCount() const +{ + return libinput_event_gesture_get_finger_count(m_gestureEvent); +} + +QPointF GestureEvent::delta() const +{ + return QPointF(libinput_event_gesture_get_dx_unaccelerated(m_gestureEvent), + libinput_event_gesture_get_dy_unaccelerated(m_gestureEvent)); +} + +bool GestureEvent::isCancelled() const +{ + return libinput_event_gesture_get_cancelled(m_gestureEvent) != 0; +} + +PinchGestureEvent::PinchGestureEvent(libinput_event *event, libinput_event_type type) + : GestureEvent(event, type) +{ +} + +PinchGestureEvent::~PinchGestureEvent() = default; + +qreal PinchGestureEvent::scale() const +{ + return libinput_event_gesture_get_scale(m_gestureEvent); +} + +qreal PinchGestureEvent::angleDelta() const +{ + return libinput_event_gesture_get_angle_delta(m_gestureEvent); +} + +SwipeGestureEvent::SwipeGestureEvent(libinput_event *event, libinput_event_type type) + : GestureEvent(event, type) +{ +} + +SwipeGestureEvent::~SwipeGestureEvent() = default; + +HoldGestureEvent::HoldGestureEvent(libinput_event *event, libinput_event_type type) + : GestureEvent(event, type) +{ +} + +HoldGestureEvent::~HoldGestureEvent() = default; + +SwitchEvent::SwitchEvent(libinput_event *event, libinput_event_type type) + : Event(event, type) + , m_switchEvent(libinput_event_get_switch_event(event)) +{ +} + +SwitchEvent::~SwitchEvent() = default; + +SwitchState SwitchEvent::state() const +{ + switch (libinput_event_switch_get_switch_state(m_switchEvent)) { + case LIBINPUT_SWITCH_STATE_OFF: + return SwitchState::Off; + case LIBINPUT_SWITCH_STATE_ON: + return SwitchState::On; + default: + Q_UNREACHABLE(); + } + return SwitchState::Off; +} + +std::chrono::microseconds SwitchEvent::time() const +{ + return std::chrono::microseconds(libinput_event_switch_get_time_usec(m_switchEvent)); +} + +TabletToolEvent::TabletToolEvent(libinput_event *event, libinput_event_type type) + : Event(event, type) + , m_tabletToolEvent(libinput_event_get_tablet_tool_event(event)) +{ +} + +QPointF TabletToolEvent::transformedPosition(const QSize &size) const +{ + const QRectF outputArea = device()->outputArea(); + return {(size.width() - 1) * outputArea.x() + libinput_event_tablet_tool_get_x_transformed(m_tabletToolEvent, (size.width() - 1) * outputArea.width()), + (size.height() - 1) * outputArea.y() + libinput_event_tablet_tool_get_y_transformed(m_tabletToolEvent, (size.height() - 1) * outputArea.height())}; +} + +TabletToolButtonEvent::TabletToolButtonEvent(libinput_event *event, libinput_event_type type) + : Event(event, type) + , m_tabletToolEvent(libinput_event_get_tablet_tool_event(event)) +{ +} + +TabletPadButtonEvent::TabletPadButtonEvent(libinput_event *event, libinput_event_type type) + : Event(event, type) + , m_tabletPadEvent(libinput_event_get_tablet_pad_event(event)) +{ +} + +TabletPadStripEvent::TabletPadStripEvent(libinput_event *event, libinput_event_type type) + : Event(event, type) + , m_tabletPadEvent(libinput_event_get_tablet_pad_event(event)) +{ +} + +TabletPadRingEvent::TabletPadRingEvent(libinput_event *event, libinput_event_type type) + : Event(event, type) + , m_tabletPadEvent(libinput_event_get_tablet_pad_event(event)) +{ +} + +TabletPadDialEvent::TabletPadDialEvent(libinput_event *event, libinput_event_type type) + : Event(event, type) + , m_tabletPadEvent(libinput_event_get_tablet_pad_event(event)) +{ +} +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/libinput/events.h b/local/recipes/kde/kwin/source/src/backends/libinput/events.h new file mode 100644 index 0000000000..e9bc7b220f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/libinput/events.h @@ -0,0 +1,483 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "input.h" + +#include + +namespace KWin +{ +namespace LibInput +{ + +class Device; + +class Event +{ +public: + virtual ~Event(); + + libinput_event_type type() const; + Device *device() const; + libinput_device *nativeDevice() const; + + operator libinput_event *() + { + return m_event; + } + operator libinput_event *() const + { + return m_event; + } + + static std::unique_ptr create(libinput_event *event); + +protected: + Event(libinput_event *event, libinput_event_type type); + +private: + libinput_event *m_event; + libinput_event_type m_type; + mutable Device *m_device; +}; + +class KeyEvent : public Event +{ +public: + KeyEvent(libinput_event *event); + ~KeyEvent() override; + + uint32_t key() const; + KeyboardKeyState state() const; + std::chrono::microseconds time() const; + + operator libinput_event_keyboard *() + { + return m_keyboardEvent; + } + operator libinput_event_keyboard *() const + { + return m_keyboardEvent; + } + +private: + libinput_event_keyboard *m_keyboardEvent; +}; + +class PointerEvent : public Event +{ +public: + PointerEvent(libinput_event *event, libinput_event_type type); + ~PointerEvent() override; + + QPointF absolutePos() const; + QPointF absolutePos(const QSize &size) const; + QPointF delta() const; + QPointF deltaUnaccelerated() const; + uint32_t button() const; + PointerButtonState buttonState() const; + std::chrono::microseconds time() const; + QList axis() const; + qreal scrollValue(PointerAxis a) const; + qint32 scrollValueV120(PointerAxis axis) const; + + operator libinput_event_pointer *() + { + return m_pointerEvent; + } + operator libinput_event_pointer *() const + { + return m_pointerEvent; + } + +private: + libinput_event_pointer *m_pointerEvent; +}; + +class TouchEvent : public Event +{ +public: + TouchEvent(libinput_event *event, libinput_event_type type); + ~TouchEvent() override; + + std::chrono::microseconds time() const; + QPointF absolutePos() const; + QPointF absolutePos(const QSize &size) const; + qint32 id() const; + + operator libinput_event_touch *() + { + return m_touchEvent; + } + operator libinput_event_touch *() const + { + return m_touchEvent; + } + +private: + libinput_event_touch *m_touchEvent; +}; + +class GestureEvent : public Event +{ +public: + ~GestureEvent() override; + + std::chrono::microseconds time() const; + int fingerCount() const; + + QPointF delta() const; + + bool isCancelled() const; + + operator libinput_event_gesture *() + { + return m_gestureEvent; + } + operator libinput_event_gesture *() const + { + return m_gestureEvent; + } + +protected: + GestureEvent(libinput_event *event, libinput_event_type type); + libinput_event_gesture *m_gestureEvent; +}; + +class PinchGestureEvent : public GestureEvent +{ +public: + PinchGestureEvent(libinput_event *event, libinput_event_type type); + ~PinchGestureEvent() override; + + qreal scale() const; + qreal angleDelta() const; +}; + +class SwipeGestureEvent : public GestureEvent +{ +public: + SwipeGestureEvent(libinput_event *event, libinput_event_type type); + ~SwipeGestureEvent() override; +}; + +class HoldGestureEvent : public GestureEvent +{ +public: + HoldGestureEvent(libinput_event *event, libinput_event_type type); + ~HoldGestureEvent() override; +}; + +class SwitchEvent : public Event +{ +public: + SwitchEvent(libinput_event *event, libinput_event_type type); + ~SwitchEvent() override; + + SwitchState state() const; + + std::chrono::microseconds time() const; + +private: + libinput_event_switch *m_switchEvent; +}; + +class TabletToolEvent : public Event +{ +public: + TabletToolEvent(libinput_event *event, libinput_event_type type); + + std::chrono::microseconds time() const + { + return std::chrono::microseconds(libinput_event_tablet_tool_get_time_usec(m_tabletToolEvent)); + } + bool xHasChanged() const + { + return libinput_event_tablet_tool_x_has_changed(m_tabletToolEvent); + } + bool yHasChanged() const + { + return libinput_event_tablet_tool_y_has_changed(m_tabletToolEvent); + } + bool pressureHasChanged() const + { + return libinput_event_tablet_tool_pressure_has_changed(m_tabletToolEvent); + } + bool distanceHasChanged() const + { + return libinput_event_tablet_tool_distance_has_changed(m_tabletToolEvent); + } + bool tiltXHasChanged() const + { + return libinput_event_tablet_tool_tilt_x_has_changed(m_tabletToolEvent); + } + bool tiltYHasChanged() const + { + return libinput_event_tablet_tool_tilt_y_has_changed(m_tabletToolEvent); + } + bool rotationHasChanged() const + { + return libinput_event_tablet_tool_rotation_has_changed(m_tabletToolEvent); + } + bool sliderHasChanged() const + { + return libinput_event_tablet_tool_slider_has_changed(m_tabletToolEvent); + } + + // uncomment when depending on libinput 1.14 or when implementing totems + // bool sizeMajorHasChanged() const { return + // libinput_event_tablet_tool_size_major_has_changed(m_tabletToolEvent); } bool + // sizeMinorHasChanged() const { return + // libinput_event_tablet_tool_size_minor_has_changed(m_tabletToolEvent); } + bool wheelHasChanged() const + { + return libinput_event_tablet_tool_wheel_has_changed(m_tabletToolEvent); + } + QPointF position() const + { + return {libinput_event_tablet_tool_get_x(m_tabletToolEvent), + libinput_event_tablet_tool_get_y(m_tabletToolEvent)}; + } + QPointF delta() const + { + return {libinput_event_tablet_tool_get_dx(m_tabletToolEvent), + libinput_event_tablet_tool_get_dy(m_tabletToolEvent)}; + } + qreal pressure() const + { + return libinput_event_tablet_tool_get_pressure(m_tabletToolEvent); + } + qreal distance() const + { + return libinput_event_tablet_tool_get_distance(m_tabletToolEvent); + } + qreal xTilt() const + { + return libinput_event_tablet_tool_get_tilt_x(m_tabletToolEvent); + } + qreal yTilt() const + { + return libinput_event_tablet_tool_get_tilt_y(m_tabletToolEvent); + } + qreal rotation() const + { + return libinput_event_tablet_tool_get_rotation(m_tabletToolEvent); + } + qreal sliderPosition() const + { + return libinput_event_tablet_tool_get_slider_position(m_tabletToolEvent); + } + // Uncomment when depending on libinput 1.14 or when implementing totems + // qreal sizeMajor() const { return + // libinput_event_tablet_tool_get_size_major(m_tabletToolEvent); } + // qreal sizeMinor() const { + // return libinput_event_tablet_tool_get_size_minor(m_tabletToolEvent); } + qreal wheelDelta() const + { + return libinput_event_tablet_tool_get_wheel_delta(m_tabletToolEvent); + } + int wheelDeltaDiscrete() const + { + return libinput_event_tablet_tool_get_wheel_delta_discrete(m_tabletToolEvent); + } + + bool isTipDown() const + { + const auto state = libinput_event_tablet_tool_get_tip_state(m_tabletToolEvent); + return state == LIBINPUT_TABLET_TOOL_TIP_DOWN; + } + bool isNearby() const + { + const auto state = libinput_event_tablet_tool_get_proximity_state(m_tabletToolEvent); + return state == LIBINPUT_TABLET_TOOL_PROXIMITY_STATE_IN; + } + + QPointF transformedPosition(const QSize &size) const; + + struct libinput_tablet_tool *tool() + { + return libinput_event_tablet_tool_get_tool(m_tabletToolEvent); + } + +private: + libinput_event_tablet_tool *m_tabletToolEvent; +}; + +class TabletToolButtonEvent : public Event +{ +public: + TabletToolButtonEvent(libinput_event *event, libinput_event_type type); + + uint buttonId() const + { + return libinput_event_tablet_tool_get_button(m_tabletToolEvent); + } + + bool isButtonPressed() const + { + const auto state = libinput_event_tablet_tool_get_button_state(m_tabletToolEvent); + return state == LIBINPUT_BUTTON_STATE_PRESSED; + } + + struct libinput_tablet_tool *tool() + { + return libinput_event_tablet_tool_get_tool(m_tabletToolEvent); + } + + std::chrono::microseconds time() const + { + return std::chrono::microseconds(libinput_event_tablet_tool_get_time_usec(m_tabletToolEvent)); + } + +private: + libinput_event_tablet_tool *m_tabletToolEvent; +}; + +class TabletPadRingEvent : public Event +{ +public: + TabletPadRingEvent(libinput_event *event, libinput_event_type type); + + qreal position() const + { + return libinput_event_tablet_pad_get_ring_position(m_tabletPadEvent); + } + int number() const + { + return libinput_event_tablet_pad_get_ring_number(m_tabletPadEvent); + } + libinput_tablet_pad_ring_axis_source source() const + { + return libinput_event_tablet_pad_get_ring_source(m_tabletPadEvent); + } + int group() const + { + auto mode_group = libinput_event_tablet_pad_get_mode_group(m_tabletPadEvent); + return libinput_tablet_pad_mode_group_get_index(mode_group); + } + uint mode() const + { + return libinput_event_tablet_pad_get_mode(m_tabletPadEvent); + } + std::chrono::microseconds time() const + { + return std::chrono::microseconds(libinput_event_tablet_pad_get_time_usec(m_tabletPadEvent)); + } + +private: + libinput_event_tablet_pad *m_tabletPadEvent; +}; + +class TabletPadDialEvent : public Event +{ +public: + TabletPadDialEvent(libinput_event *event, libinput_event_type type); + + double delta() const + { + return libinput_event_tablet_pad_get_dial_delta_v120(m_tabletPadEvent); + } + int number() const + { + return libinput_event_tablet_pad_get_dial_number(m_tabletPadEvent); + } + int group() const + { + auto mode_group = libinput_event_tablet_pad_get_mode_group(m_tabletPadEvent); + return libinput_tablet_pad_mode_group_get_index(mode_group); + } + std::chrono::microseconds time() const + { + return std::chrono::microseconds(libinput_event_tablet_pad_get_time_usec(m_tabletPadEvent)); + } + +private: + libinput_event_tablet_pad *m_tabletPadEvent; +}; + +class TabletPadStripEvent : public Event +{ +public: + TabletPadStripEvent(libinput_event *event, libinput_event_type type); + + qreal position() const + { + return libinput_event_tablet_pad_get_strip_position(m_tabletPadEvent); + } + int number() const + { + return libinput_event_tablet_pad_get_strip_number(m_tabletPadEvent); + } + libinput_tablet_pad_strip_axis_source source() const + { + return libinput_event_tablet_pad_get_strip_source(m_tabletPadEvent); + } + int group() const + { + auto mode_group = libinput_event_tablet_pad_get_mode_group(m_tabletPadEvent); + return libinput_tablet_pad_mode_group_get_index(mode_group); + } + uint mode() const + { + return libinput_event_tablet_pad_get_mode(m_tabletPadEvent); + } + std::chrono::microseconds time() const + { + return std::chrono::microseconds(libinput_event_tablet_pad_get_time_usec(m_tabletPadEvent)); + } + +private: + libinput_event_tablet_pad *m_tabletPadEvent; +}; + +class TabletPadButtonEvent : public Event +{ +public: + TabletPadButtonEvent(libinput_event *event, libinput_event_type type); + + uint buttonId() const + { + return libinput_event_tablet_pad_get_button_number(m_tabletPadEvent); + } + bool isButtonPressed() const + { + const auto state = libinput_event_tablet_pad_get_button_state(m_tabletPadEvent); + return state == LIBINPUT_BUTTON_STATE_PRESSED; + } + uint group() const + { + auto mode_group = libinput_event_tablet_pad_get_mode_group(m_tabletPadEvent); + return libinput_tablet_pad_mode_group_get_index(mode_group); + } + uint mode() const + { + return libinput_event_tablet_pad_get_mode(m_tabletPadEvent); + } + bool isModeSwitch() const + { + auto mode_group = libinput_event_tablet_pad_get_mode_group(m_tabletPadEvent); + return libinput_tablet_pad_mode_group_button_is_toggle(mode_group, buttonId()); + } + std::chrono::microseconds time() const + { + return std::chrono::microseconds(libinput_event_tablet_pad_get_time_usec(m_tabletPadEvent)); + } + +private: + libinput_event_tablet_pad *m_tabletPadEvent; +}; + +inline libinput_event_type Event::type() const +{ + return m_type; +} + +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/libinput/libinput_logging.cpp b/local/recipes/kde/kwin/source/src/backends/libinput/libinput_logging.cpp new file mode 100644 index 0000000000..fd660c97fa --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/libinput/libinput_logging.cpp @@ -0,0 +1,10 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "libinput_logging.h" +Q_LOGGING_CATEGORY(KWIN_LIBINPUT, "kwin_libinput", QtWarningMsg) diff --git a/local/recipes/kde/kwin/source/src/backends/libinput/libinput_logging.h b/local/recipes/kde/kwin/source/src/backends/libinput/libinput_logging.h new file mode 100644 index 0000000000..52c08ce3de --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/libinput/libinput_logging.h @@ -0,0 +1,12 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include +#include +Q_DECLARE_LOGGING_CATEGORY(KWIN_LIBINPUT) diff --git a/local/recipes/kde/kwin/source/src/backends/libinput/libinputbackend.cpp b/local/recipes/kde/kwin/source/src/backends/libinput/libinputbackend.cpp new file mode 100644 index 0000000000..ff984ecb58 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/libinput/libinputbackend.cpp @@ -0,0 +1,62 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "libinputbackend.h" +#include "connection.h" +#include "device.h" + +namespace KWin +{ + +LibinputBackend::LibinputBackend(Session *session, QObject *parent) + : InputBackend(parent) +{ + m_thread.setObjectName(QStringLiteral("libinput-connection")); + m_thread.start(); + + m_connection = LibInput::Connection::create(session); + m_connection->moveToThread(&m_thread); + + connect( + m_connection, &LibInput::Connection::eventsRead, this, [this]() { + m_connection->processEvents(); + }, + Qt::QueuedConnection); + + connect(m_connection, &LibInput::Connection::deviceAdded, + this, &InputBackend::deviceAdded); + connect(m_connection, &LibInput::Connection::deviceRemoved, + this, &InputBackend::deviceRemoved); +} + +LibinputBackend::~LibinputBackend() +{ + // Notify the main thread about removed input devices. The deviceRemoved() signal cannot + // be emitted from the connection thread otherwise it will be queued, which we don't want. + const auto devices = m_connection->devices(); + for (const auto device : devices) { + Q_EMIT deviceRemoved(device); + } + + m_connection->deleteLater(); + m_thread.quit(); + m_thread.wait(); +} + +void LibinputBackend::initialize() +{ + m_connection->setInputConfig(config()); + m_connection->setup(); +} + +void LibinputBackend::updateScreens() +{ + m_connection->updateScreens(); +} + +} // namespace KWin + +#include "moc_libinputbackend.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/libinput/libinputbackend.h b/local/recipes/kde/kwin/source/src/backends/libinput/libinputbackend.h new file mode 100644 index 0000000000..5f00d2542d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/libinput/libinputbackend.h @@ -0,0 +1,39 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/inputbackend.h" + +#include + +namespace KWin +{ + +class Session; + +namespace LibInput +{ +class Connection; +} + +class KWIN_EXPORT LibinputBackend : public InputBackend +{ + Q_OBJECT + +public: + explicit LibinputBackend(Session *session, QObject *parent = nullptr); + ~LibinputBackend() override; + + void initialize() override; + void updateScreens() override; + +private: + QThread m_thread; + LibInput::Connection *m_connection = nullptr; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/backends/virtual/CMakeLists.txt b/local/recipes/kde/kwin/source/src/backends/virtual/CMakeLists.txt new file mode 100644 index 0000000000..9cf02b2c9a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/virtual/CMakeLists.txt @@ -0,0 +1,7 @@ +target_sources(kwin PRIVATE + virtual_backend.cpp + virtual_egl_backend.cpp + virtual_logging.cpp + virtual_output.cpp + virtual_qpainter_backend.cpp +) diff --git a/local/recipes/kde/kwin/source/src/backends/virtual/virtual_backend.cpp b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_backend.cpp new file mode 100644 index 0000000000..93f8a00cf0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_backend.cpp @@ -0,0 +1,190 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "virtual_backend.h" + +#include "core/drmdevice.h" +#include "virtual_egl_backend.h" +#include "virtual_output.h" +#include "virtual_qpainter_backend.h" + +#include +#include +#include +#include + +namespace KWin +{ + +static std::unique_ptr findRenderDevice() +{ +#if !HAVE_LIBDRM_FAUX +#if defined(Q_OS_LINUX) + // Workaround for libdrm being unaware of faux bus. + if (qEnvironmentVariableIsSet("CI")) { + return DrmDevice::open(QStringLiteral("/dev/dri/card1")); + } +#endif +#endif + + const int deviceCount = drmGetDevices2(0, nullptr, 0); + if (deviceCount <= 0) { + return nullptr; + } + + QList devices(deviceCount); + if (drmGetDevices2(0, devices.data(), devices.size()) < 0) { + return nullptr; + } + auto deviceCleanup = qScopeGuard([&devices]() { + drmFreeDevices(devices.data(), devices.size()); + }); + + for (drmDevice *device : std::as_const(devices)) { + // If it's a vgem device, prefer the primary node because gbm will attempt to allocate + // dumb buffers and they can be allocated only on the primary node. + int nodeType = DRM_NODE_RENDER; + if (device->bustype == DRM_BUS_PLATFORM) { + if (strcmp(device->businfo.platform->fullname, "vgem") == 0) { + nodeType = DRM_NODE_PRIMARY; + } + } +#if HAVE_LIBDRM_FAUX + if (device->bustype == DRM_BUS_FAUX) { + if (strcmp(device->businfo.faux->name, "vgem") == 0) { + nodeType = DRM_NODE_PRIMARY; + } + } +#endif + + if (device->available_nodes & (1 << nodeType)) { + if (auto ret = DrmDevice::open(device->nodes[nodeType])) { + return ret; + } + } + } + + return nullptr; +} + +VirtualBackend::VirtualBackend(QObject *parent) + : OutputBackend(parent) + , m_drmDevice(findRenderDevice()) +{ +} + +VirtualBackend::~VirtualBackend() +{ +} + +bool VirtualBackend::initialize() +{ + return true; +} + +QList VirtualBackend::supportedCompositors() const +{ + QList compositingTypes; + if (m_drmDevice) { + compositingTypes.append(OpenGLCompositing); + } + compositingTypes.append(QPainterCompositing); + return compositingTypes; +} + +DrmDevice *VirtualBackend::drmDevice() const +{ + return m_drmDevice.get(); +} + +std::unique_ptr VirtualBackend::createQPainterBackend() +{ + return std::make_unique(this); +} + +std::unique_ptr VirtualBackend::createOpenGLBackend() +{ + return std::make_unique(this); +} + +BackendOutput *VirtualBackend::createVirtualOutput(const QString &name, const QString &description, const QSize &size, qreal scale) +{ + return addOutput(OutputInfo{ + .geometry = Rect(QPoint(), size), + .scale = scale, + .connectorName = QStringLiteral("Virtual-") + name, + }); +} + +void VirtualBackend::removeVirtualOutput(BackendOutput *output) +{ + if (auto virtualOutput = qobject_cast(output)) { + removeOutput(virtualOutput); + } +} + +QList VirtualBackend::outputs() const +{ + return m_outputs | std::ranges::to>(); +} + +VirtualOutput *VirtualBackend::createOutput(const OutputInfo &info) +{ + VirtualOutput *output = new VirtualOutput(this, info.internal, info.physicalSizeInMM, info.panelOrientation, info.edid, info.edidIdentifierOverride, info.connectorName, info.mstPath); + output->init(info.geometry.topLeft(), info.geometry.size() * info.scale, info.scale, info.modes); + m_outputs.append(output); + Q_EMIT outputAdded(output); + return output; +} + +BackendOutput *VirtualBackend::addOutput(const OutputInfo &info) +{ + VirtualOutput *output = createOutput(info); + Q_EMIT outputsQueried(); + return output; +} + +void VirtualBackend::removeOutput(VirtualOutput *output) +{ + if (m_outputs.removeOne(output)) { + Q_EMIT outputRemoved(output); + Q_EMIT outputsQueried(); + output->unref(); + } +} + +void VirtualBackend::setVirtualOutputs(const QList &infos) +{ + const QList removed = m_outputs; + + for (const auto &info : infos) { + createOutput(info); + } + + for (VirtualOutput *output : removed) { + m_outputs.removeOne(output); + Q_EMIT outputRemoved(output); + output->unref(); + } + + Q_EMIT outputsQueried(); +} + +void VirtualBackend::setEglDisplay(std::unique_ptr &&display) +{ + m_display = std::move(display); +} + +EglDisplay *VirtualBackend::sceneEglDisplayObject() const +{ + return m_display.get(); +} + +} // namespace KWin + +#include "moc_virtual_backend.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/virtual/virtual_backend.h b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_backend.h new file mode 100644 index 0000000000..ca9b5268eb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_backend.h @@ -0,0 +1,74 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "core/output.h" +#include "core/outputbackend.h" +#include "utils/filedescriptor.h" + +namespace KWin +{ +class VirtualBackend; +class VirtualOutput; +class DrmDevice; + +class KWIN_EXPORT VirtualBackend : public OutputBackend +{ + Q_OBJECT + +public: + VirtualBackend(QObject *parent = nullptr); + ~VirtualBackend() override; + + bool initialize() override; + + std::unique_ptr createQPainterBackend() override; + std::unique_ptr createOpenGLBackend() override; + + BackendOutput *createVirtualOutput(const QString &name, const QString &description, const QSize &size, qreal scale) override; + void removeVirtualOutput(BackendOutput *output) override; + + struct OutputInfo + { + Rect geometry; + double scale = 1; + bool internal = false; + QSize physicalSizeInMM; + QList> modes; + OutputTransform panelOrientation = OutputTransform::Kind::Normal; + QByteArray edid; + std::optional edidIdentifierOverride; + std::optional connectorName; + std::optional mstPath; + }; + BackendOutput *addOutput(const OutputInfo &info); + void setVirtualOutputs(const QList &infos); + + QList outputs() const override; + + QList supportedCompositors() const override; + + void setEglDisplay(std::unique_ptr &&display); + EglDisplay *sceneEglDisplayObject() const override; + + DrmDevice *drmDevice() const; + +Q_SIGNALS: + void virtualOutputsSet(bool countChanged); + +private: + VirtualOutput *createOutput(const OutputInfo &info); + void removeOutput(VirtualOutput *output); + + QList m_outputs; + std::unique_ptr m_drmDevice; + std::unique_ptr m_display; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/backends/virtual/virtual_egl_backend.cpp b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_egl_backend.cpp new file mode 100644 index 0000000000..40c4288e8d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_egl_backend.cpp @@ -0,0 +1,186 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "virtual_egl_backend.h" +#include "core/drmdevice.h" +#include "core/gbmgraphicsbufferallocator.h" +#include "opengl/eglswapchain.h" +#include "opengl/glrendertimequery.h" +#include "opengl/glutils.h" +#include "utils/softwarevsyncmonitor.h" +#include "virtual_backend.h" +#include "virtual_logging.h" +#include "virtual_output.h" + +#include + +namespace KWin +{ + +static const bool s_bufferAgeEnabled = qEnvironmentVariable("KWIN_USE_BUFFER_AGE") != QStringLiteral("0"); + +VirtualEglLayer::VirtualEglLayer(BackendOutput *output, VirtualEglBackend *backend) + : OutputLayer(output, OutputLayerType::Primary) + , m_backend(backend) +{ +} + +VirtualEglLayer::~VirtualEglLayer() +{ + m_backend->openglContext()->makeCurrent(); +} + +std::optional VirtualEglLayer::doBeginFrame() +{ + m_backend->openglContext()->makeCurrent(); + + const QSize nativeSize = m_output->modeSize(); + if (!m_swapchain || m_swapchain->size() != nativeSize) { + m_swapchain = EglSwapchain::create(m_backend->drmDevice()->allocator(), m_backend->openglContext(), nativeSize, DRM_FORMAT_XRGB8888, m_backend->supportedFormats()[DRM_FORMAT_XRGB8888]); + if (!m_swapchain) { + return std::nullopt; + } + } + + m_current = m_swapchain->acquire(); + if (!m_current) { + return std::nullopt; + } + + m_query = std::make_unique(m_backend->openglContextRef()); + m_query->begin(); + + return OutputLayerBeginFrameInfo{ + .renderTarget = RenderTarget(m_current->framebuffer()), + .repaint = s_bufferAgeEnabled ? m_damageJournal.accumulate(m_current->age(), Region::infinite()) : Region::infinite(), + }; +} + +bool VirtualEglLayer::doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) +{ + m_query->end(); + frame->addRenderTimeQuery(std::move(m_query)); + glFlush(); // flush pending rendering commands. + m_swapchain->release(m_current, FileDescriptor{}); + m_damageJournal.add(damagedDeviceRegion); + return true; +} + +DrmDevice *VirtualEglLayer::scanoutDevice() const +{ + return m_backend->drmDevice(); +} + +QHash> VirtualEglLayer::supportedDrmFormats() const +{ + return m_backend->supportedFormats(); +} + +void VirtualEglLayer::releaseBuffers() +{ + m_current.reset(); + m_swapchain.reset(); +} + +GLTexture *VirtualEglLayer::texture() const +{ + return m_current ? m_current->texture().get() : nullptr; +} + +VirtualEglBackend::VirtualEglBackend(VirtualBackend *b) + : m_backend(b) +{ +} + +VirtualEglBackend::~VirtualEglBackend() +{ + const auto outputs = m_backend->outputs(); + for (BackendOutput *output : outputs) { + static_cast(output)->setOutputLayer(nullptr); + } + cleanup(); +} + +VirtualBackend *VirtualEglBackend::backend() const +{ + return m_backend; +} + +DrmDevice *VirtualEglBackend::drmDevice() const +{ + return m_backend->drmDevice(); +} + +bool VirtualEglBackend::initializeEgl() +{ + initClientExtensions(); + + if (!m_backend->sceneEglDisplayObject()) { + for (const QByteArray &extension : {QByteArrayLiteral("EGL_EXT_platform_base"), QByteArrayLiteral("EGL_KHR_platform_gbm")}) { + if (!hasClientExtension(extension)) { + qCWarning(KWIN_VIRTUAL) << extension << "client extension is not supported by the platform"; + return false; + } + } + + m_backend->setEglDisplay(EglDisplay::create(eglGetPlatformDisplayEXT(EGL_PLATFORM_GBM_KHR, m_backend->drmDevice()->gbmDevice(), nullptr))); + } + + auto display = m_backend->sceneEglDisplayObject(); + if (!display) { + return false; + } + setEglDisplay(display); + return true; +} + +void VirtualEglBackend::init() +{ + if (!initializeEgl()) { + setFailed("Could not initialize egl"); + return; + } + if (!initRenderingContext()) { + setFailed("Could not initialize rendering context"); + return; + } + + if (checkGLError("Init")) { + setFailed("Error during init of EglGbmBackend"); + return; + } + + initWayland(); + + const auto outputs = m_backend->outputs(); + for (BackendOutput *output : outputs) { + addOutput(output); + } + + connect(m_backend, &VirtualBackend::outputAdded, this, &VirtualEglBackend::addOutput); +} + +bool VirtualEglBackend::initRenderingContext() +{ + return createContext(EGL_NO_CONFIG_KHR) && openglContext()->makeCurrent(); +} + +void VirtualEglBackend::addOutput(BackendOutput *output) +{ + openglContext()->makeCurrent(); + static_cast(output)->setOutputLayer(std::make_unique(output, this)); +} + +QList VirtualEglBackend::compatibleOutputLayers(BackendOutput *output) +{ + return {static_cast(output)->outputLayer()}; +} + +} // namespace + +#include "moc_virtual_egl_backend.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/virtual/virtual_egl_backend.h b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_egl_backend.h new file mode 100644 index 0000000000..d7c2b2773b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_egl_backend.h @@ -0,0 +1,79 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "core/outputlayer.h" +#include "opengl/eglbackend.h" +#include "utils/damagejournal.h" + +#include +#include + +namespace KWin +{ + +class EglSwapchainSlot; +class EglSwapchain; +class GraphicsBufferAllocator; +class VirtualBackend; +class GLFramebuffer; +class GLTexture; +class VirtualEglBackend; +class GLRenderTimeQuery; + +class KWIN_EXPORT VirtualEglLayer : public OutputLayer +{ +public: + VirtualEglLayer(BackendOutput *output, VirtualEglBackend *backend); + ~VirtualEglLayer() override; + + std::optional doBeginFrame() override; + bool doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) override; + + DrmDevice *scanoutDevice() const override; + QHash> supportedDrmFormats() const override; + void releaseBuffers() override; + + GLTexture *texture() const; + +private: + VirtualEglBackend *const m_backend; + std::shared_ptr m_swapchain; + std::shared_ptr m_current; + std::unique_ptr m_query; + DamageJournal m_damageJournal; +}; + +/** + * @brief OpenGL Backend using Egl on a GBM surface. + */ +class VirtualEglBackend : public EglBackend +{ + Q_OBJECT + +public: + VirtualEglBackend(VirtualBackend *b); + ~VirtualEglBackend() override; + + QList compatibleOutputLayers(BackendOutput *output) override; + void init() override; + + VirtualBackend *backend() const; + DrmDevice *drmDevice() const override; + +private: + bool initializeEgl(); + bool initRenderingContext(); + + void addOutput(BackendOutput *output); + + VirtualBackend *m_backend; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/backends/virtual/virtual_logging.cpp b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_logging.cpp new file mode 100644 index 0000000000..ec8a13798c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_logging.cpp @@ -0,0 +1,9 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "virtual_logging.h" + +Q_LOGGING_CATEGORY(KWIN_VIRTUAL, "kwin_platform_virtual", QtWarningMsg) diff --git a/local/recipes/kde/kwin/source/src/backends/virtual/virtual_logging.h b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_logging.h new file mode 100644 index 0000000000..24b44b5afd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_logging.h @@ -0,0 +1,11 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +Q_DECLARE_LOGGING_CATEGORY(KWIN_VIRTUAL) diff --git a/local/recipes/kde/kwin/source/src/backends/virtual/virtual_output.cpp b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_output.cpp new file mode 100644 index 0000000000..0066088706 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_output.cpp @@ -0,0 +1,171 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "virtual_output.h" +#include "virtual_backend.h" + +#include "compositor.h" +#include "core/outputconfiguration.h" +#include "core/outputlayer.h" +#include "core/renderbackend.h" +#include "core/renderloop.h" +#include "utils/softwarevsyncmonitor.h" + +namespace KWin +{ + +VirtualOutput::VirtualOutput(VirtualBackend *parent, bool internal, const QSize &physicalSizeInMM, OutputTransform panelOrientation, const QByteArray &edid, std::optional edidIdentifierOverride, const std::optional &connectorName, const std::optional &mstPath) + : BackendOutput() + , m_backend(parent) + , m_renderLoop(std::make_unique(this)) + , m_vsyncMonitor(SoftwareVsyncMonitor::create()) +{ + connect(m_vsyncMonitor.get(), &VsyncMonitor::vblankOccurred, this, &VirtualOutput::vblank); + + static int identifier = -1; + m_identifier = ++identifier; + setInformation(Information{ + .name = connectorName.value_or(QStringLiteral("Virtual-%1").arg(identifier)), + .physicalSize = physicalSizeInMM, + .edid = Edid{edid, edidIdentifierOverride}, + .capabilities = Capability::CustomModes, + .panelOrientation = panelOrientation, + .internal = internal, + .mstPath = mstPath.value_or(QByteArray()), + }); +} + +VirtualOutput::~VirtualOutput() +{ +} + +RenderLoop *VirtualOutput::renderLoop() const +{ + return m_renderLoop.get(); +} + +bool VirtualOutput::testPresentation(const std::shared_ptr &frame) +{ + return true; +} + +bool VirtualOutput::present(const QList &layersToUpdate, const std::shared_ptr &frame) +{ + m_frame = frame; + m_vsyncMonitor->arm(); + return true; +} + +void VirtualOutput::init(const QPoint &logicalPosition, const QSize &pixelSize, qreal scale, const QList> &modes) +{ + QList> modeList; + for (const auto &mode : modes) { + const auto &[size, refresh, flags] = mode; + modeList.push_back(std::make_shared(size, refresh, flags)); + } + if (modeList.empty()) { + modeList.push_back(std::make_shared(pixelSize, 60000, OutputMode::Flag::Preferred)); + } + + m_renderLoop->setRefreshRate(modeList.front()->refreshRate()); + m_vsyncMonitor->setRefreshRate(modeList.front()->refreshRate()); + setState(State{ + .position = logicalPosition, + .scale = scale, + .modes = modeList, + .currentMode = modeList.front(), + .scaleSetting = scale, + }); +} + +void VirtualOutput::applyChanges(const OutputConfiguration &config) +{ + auto props = config.constChangeSet(this); + if (!props) { + return; + } + Q_EMIT aboutToChange(props.get()); + + State next = m_state; + next.enabled = props->enabled.value_or(m_state.enabled); + next.transform = props->transform.value_or(m_state.transform); + next.position = props->pos.value_or(m_state.position); + next.scale = props->scale.value_or(m_state.scale); + next.scaleSetting = props->scaleSetting.value_or(m_state.scaleSetting); + next.desiredModeSize = props->desiredModeSize.value_or(m_state.desiredModeSize); + next.desiredModeRefreshRate = props->desiredModeRefreshRate.value_or(m_state.desiredModeRefreshRate); + next.desiredModeFlags = props->desiredModeFlags.value_or(m_state.desiredModeFlags); + next.currentMode = props->mode.value_or(m_state.currentMode).lock(); + if (!next.currentMode) { + next.currentMode = next.modes.front(); + } + next.uuid = props->uuid.value_or(m_state.uuid); + next.replicationSource = props->replicationSource.value_or(m_state.replicationSource); + next.priority = props->priority.value_or(m_state.priority); + next.deviceOffset = props->deviceOffset.value_or(m_state.deviceOffset); + if (props->customModes.has_value()) { + next.customModes = *props->customModes; + + QList> newModes; + for (const auto &mode : next.modes) { + if (mode->flags() & OutputMode::Flag::Custom) { + continue; + } + newModes.push_back(mode); + } + for (const auto &custom : next.customModes) { + newModes.push_back(std::make_shared(custom.size, custom.refreshRate, custom.flags | OutputMode::Flag::Custom)); + } + next.modes = newModes; + + if (!next.currentMode) { + next.currentMode = next.modes.front(); + } else if (!next.modes.contains(next.currentMode)) { + const auto it = std::ranges::find_if(next.modes, [&next](const auto &mode) { + return mode->size() == next.currentMode->size() + && mode->refreshRate() == next.currentMode->refreshRate() + && mode->flags() == next.currentMode->flags(); + }); + if (it != next.modes.end()) { + next.currentMode = *it; + } else { + next.modes.push_front(next.currentMode); + next.currentMode->setRemoved(); + } + } + } + next.dpmsMode = props->dpmsMode.value_or(next.dpmsMode); + next.uuid = props->uuid.value_or(next.uuid); + + setState(next); + m_renderLoop->setRefreshRate(next.currentMode->refreshRate()); + m_vsyncMonitor->setRefreshRate(next.currentMode->refreshRate()); + + Q_EMIT changed(); +} + +void VirtualOutput::vblank(std::chrono::nanoseconds timestamp) +{ + if (m_frame) { + m_frame->presented(timestamp, PresentationMode::VSync); + m_frame.reset(); + } +} + +void VirtualOutput::setOutputLayer(std::unique_ptr &&layer) +{ + m_layer = std::move(layer); +} + +OutputLayer *VirtualOutput::outputLayer() const +{ + return m_layer.get(); +} +} + +#include "moc_virtual_output.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/virtual/virtual_output.h b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_output.h new file mode 100644 index 0000000000..a274222f12 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_output.h @@ -0,0 +1,56 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "core/backendoutput.h" + +#include + +namespace KWin +{ + +class SoftwareVsyncMonitor; +class VirtualBackend; +class OutputFrame; + +class VirtualOutput : public BackendOutput +{ + Q_OBJECT + +public: + explicit VirtualOutput(VirtualBackend *parent, bool internal, const QSize &physicalSizeInMM, OutputTransform panelOrientation, const QByteArray &edid, std::optional edidIdentifierOverride, const std::optional &connectorName, const std::optional &mstPath); + ~VirtualOutput() override; + + RenderLoop *renderLoop() const override; + bool testPresentation(const std::shared_ptr &frame) override; + bool present(const QList &layersToUpdate, const std::shared_ptr &frame) override; + + void init(const QPoint &logicalPosition, const QSize &pixelSize, qreal scale, const QList> &modes); + + void applyChanges(const OutputConfiguration &config) override; + + void setOutputLayer(std::unique_ptr &&layer); + OutputLayer *outputLayer() const; + +private: + void vblank(std::chrono::nanoseconds timestamp); + + friend class VirtualBackend; + + std::unique_ptr m_layer; + VirtualBackend *m_backend; + std::unique_ptr m_renderLoop; + std::unique_ptr m_vsyncMonitor; + int m_gammaSize = 200; + bool m_gammaResult = true; + int m_identifier; + std::shared_ptr m_frame; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/backends/virtual/virtual_qpainter_backend.cpp b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_qpainter_backend.cpp new file mode 100644 index 0000000000..d940fb3518 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_qpainter_backend.cpp @@ -0,0 +1,116 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "virtual_qpainter_backend.h" +#include "core/drmdevice.h" +#include "core/graphicsbufferview.h" +#include "core/shmgraphicsbufferallocator.h" +#include "qpainter/qpainterswapchain.h" +#include "utils/softwarevsyncmonitor.h" +#include "virtual_backend.h" +#include "virtual_output.h" + +#include + +namespace KWin +{ + +VirtualQPainterLayer::VirtualQPainterLayer(BackendOutput *output, VirtualQPainterBackend *backend) + : OutputLayer(output, OutputLayerType::Primary) + , m_backend(backend) +{ +} + +VirtualQPainterLayer::~VirtualQPainterLayer() +{ +} + +std::optional VirtualQPainterLayer::doBeginFrame() +{ + const QSize nativeSize(m_output->modeSize()); + if (!m_swapchain || m_swapchain->size() != nativeSize) { + m_swapchain = std::make_unique(m_backend->graphicsBufferAllocator(), nativeSize, DRM_FORMAT_XRGB8888); + } + + m_current = m_swapchain->acquire(); + if (!m_current) { + return std::nullopt; + } + + m_renderTime = std::make_unique(); + return OutputLayerBeginFrameInfo{ + .renderTarget = RenderTarget(m_current->view()->image()), + .repaint = Region::infinite(), + }; +} + +bool VirtualQPainterLayer::doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) +{ + m_renderTime->end(); + frame->addRenderTimeQuery(std::move(m_renderTime)); + return true; +} + +QImage *VirtualQPainterLayer::image() +{ + return m_current->view()->image(); +} + +DrmDevice *VirtualQPainterLayer::scanoutDevice() const +{ + return m_backend->drmDevice(); +} + +QHash> VirtualQPainterLayer::supportedDrmFormats() const +{ + return {{DRM_FORMAT_ARGB8888, {DRM_FORMAT_MOD_LINEAR}}}; +} + +void VirtualQPainterLayer::releaseBuffers() +{ + m_current.reset(); + m_swapchain.reset(); +} + +VirtualQPainterBackend::VirtualQPainterBackend(VirtualBackend *backend) + : m_backend(backend) + , m_allocator(std::make_unique()) +{ + connect(backend, &VirtualBackend::outputAdded, this, &VirtualQPainterBackend::addOutput); + + const auto outputs = backend->outputs(); + for (BackendOutput *output : outputs) { + addOutput(output); + } +} + +VirtualQPainterBackend::~VirtualQPainterBackend() +{ + const auto outputs = m_backend->outputs(); + for (BackendOutput *output : outputs) { + static_cast(output)->setOutputLayer(nullptr); + } +} + +void VirtualQPainterBackend::addOutput(BackendOutput *output) +{ + static_cast(output)->setOutputLayer(std::make_unique(output, this)); +} + +GraphicsBufferAllocator *VirtualQPainterBackend::graphicsBufferAllocator() const +{ + return m_allocator.get(); +} + +QList VirtualQPainterBackend::compatibleOutputLayers(BackendOutput *output) +{ + return {static_cast(output)->outputLayer()}; +} +} + +#include "moc_virtual_qpainter_backend.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/virtual/virtual_qpainter_backend.h b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_qpainter_backend.h new file mode 100644 index 0000000000..8d4abcf98e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/virtual/virtual_qpainter_backend.h @@ -0,0 +1,68 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "core/outputlayer.h" +#include "core/renderbackend.h" +#include "qpainter/qpainterbackend.h" + +#include +#include +#include +#include +#include + +namespace KWin +{ + +class GraphicsBufferAllocator; +class QPainterSwapchainSlot; +class QPainterSwapchain; +class VirtualBackend; +class VirtualQPainterBackend; + +class VirtualQPainterLayer : public OutputLayer +{ +public: + VirtualQPainterLayer(BackendOutput *output, VirtualQPainterBackend *backend); + ~VirtualQPainterLayer() override; + + std::optional doBeginFrame() override; + bool doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) override; + QImage *image(); + DrmDevice *scanoutDevice() const override; + QHash> supportedDrmFormats() const override; + void releaseBuffers() override; + +private: + VirtualQPainterBackend *const m_backend; + std::unique_ptr m_swapchain; + std::shared_ptr m_current; + std::unique_ptr m_renderTime; +}; + +class VirtualQPainterBackend : public QPainterBackend +{ + Q_OBJECT +public: + VirtualQPainterBackend(VirtualBackend *backend); + ~VirtualQPainterBackend() override; + + GraphicsBufferAllocator *graphicsBufferAllocator() const; + + QList compatibleOutputLayers(BackendOutput *output) override; + +private: + void addOutput(BackendOutput *output); + + VirtualBackend *const m_backend; + std::unique_ptr m_allocator; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/backends/wayland/CMakeLists.txt b/local/recipes/kde/kwin/source/src/backends/wayland/CMakeLists.txt new file mode 100644 index 0000000000..437abf8b07 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/wayland/CMakeLists.txt @@ -0,0 +1,12 @@ +target_sources(kwin PRIVATE + color_manager.cpp + wayland_backend.cpp + wayland_display.cpp + wayland_egl_backend.cpp + wayland_layer.cpp + wayland_logging.cpp + wayland_output.cpp + wayland_qpainter_backend.cpp +) + +target_link_libraries(kwin PRIVATE Plasma::KWaylandClient Wayland::Client gbm::gbm) diff --git a/local/recipes/kde/kwin/source/src/backends/wayland/wayland_backend.cpp b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_backend.cpp new file mode 100644 index 0000000000..557156e92e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_backend.cpp @@ -0,0 +1,730 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "wayland_backend.h" +#include "compositor.h" +#include "core/drmdevice.h" +#include "input.h" +#include "wayland-client/linuxdmabuf.h" +#include "wayland_display.h" +#include "wayland_egl_backend.h" +#include "wayland_logging.h" +#include "wayland_output.h" +#include "wayland_qpainter_backend.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "wayland-linux-dmabuf-unstable-v1-client-protocol.h" + +namespace KWin +{ +namespace Wayland +{ + +using namespace KWayland::Client; + +inline static QPointF sizeToPoint(const QSizeF &size) +{ + return QPointF(size.width(), size.height()); +} + +WaylandInputDevice::WaylandInputDevice(KWayland::Client::Keyboard *keyboard, WaylandSeat *seat) + : m_seat(seat) + , m_keyboard(keyboard) +{ + connect(keyboard, &Keyboard::left, this, [this](quint32 time) { + for (quint32 key : std::as_const(m_pressedKeys)) { + Q_EMIT keyChanged(key, KeyboardKeyState::Released, std::chrono::milliseconds(time), this); + } + m_pressedKeys.clear(); + }); + connect(keyboard, &Keyboard::keyChanged, this, [this](quint32 key, Keyboard::KeyState nativeState, quint32 time) { + KeyboardKeyState state; + switch (nativeState) { + case Keyboard::KeyState::Pressed: + if (key == KEY_RIGHTCTRL) { + m_seat->backend()->togglePointerLock(); + } + state = KeyboardKeyState::Pressed; + m_pressedKeys.insert(key); + break; + case Keyboard::KeyState::Released: + m_pressedKeys.remove(key); + state = KeyboardKeyState::Released; + break; + default: + Q_UNREACHABLE(); + } + Q_EMIT keyChanged(key, state, std::chrono::milliseconds(time), this); + }); +} + +WaylandInputDevice::WaylandInputDevice(KWayland::Client::Pointer *pointer, WaylandSeat *seat) + : m_seat(seat) + , m_pointer(pointer) +{ + connect(pointer, &Pointer::entered, this, [this](quint32 serial, const QPointF &relativeToSurface) { + WaylandOutput *output = m_seat->backend()->findOutput(m_pointer->enteredSurface()); + Q_ASSERT(output); + output->cursor()->setPointer(m_pointer.get()); + }); + connect(pointer, &Pointer::left, this, [this]() { + // wl_pointer.leave carries the wl_surface, but KWayland::Client::Pointer::left does not. + const auto outputs = m_seat->backend()->outputs(); + for (BackendOutput *output : outputs) { + WaylandOutput *waylandOutput = static_cast(output); + if (waylandOutput->cursor()->pointer()) { + waylandOutput->cursor()->setPointer(nullptr); + } + } + }); + connect(pointer, &Pointer::motion, this, [this](const QPointF &relativeToSurface, quint32 time) { + WaylandOutput *output = m_seat->backend()->findOutput(m_pointer->enteredSurface()); + Q_ASSERT(output); + const auto subsurface = m_seat->backend()->findSubSurface(m_pointer->enteredSurface()); + const QPointF absolutePos = output->position() + relativeToSurface + + (subsurface ? subsurface->position() : QPoint()); + Q_EMIT pointerMotionAbsolute(absolutePos, std::chrono::milliseconds(time), this); + }); + connect(pointer, &Pointer::buttonStateChanged, this, [this](quint32 serial, quint32 time, quint32 button, Pointer::ButtonState nativeState) { + PointerButtonState state; + switch (nativeState) { + case Pointer::ButtonState::Pressed: + state = PointerButtonState::Pressed; + break; + case Pointer::ButtonState::Released: + state = PointerButtonState::Released; + break; + default: + Q_UNREACHABLE(); + } + Q_EMIT pointerButtonChanged(button, state, std::chrono::milliseconds(time), this); + }); + // TODO: Send discreteDelta and source as well. + connect(pointer, &Pointer::axisChanged, this, [this](quint32 time, Pointer::Axis nativeAxis, qreal delta) { + PointerAxis axis; + switch (nativeAxis) { + case Pointer::Axis::Horizontal: + axis = PointerAxis::Horizontal; + break; + case Pointer::Axis::Vertical: + axis = PointerAxis::Vertical; + break; + default: + Q_UNREACHABLE(); + } + Q_EMIT pointerAxisChanged(axis, delta, 0, PointerAxisSource::Unknown, false, std::chrono::milliseconds(time), this); + }); + + connect(pointer, &Pointer::frame, this, [this]() { + Q_EMIT pointerFrame(this); + }); + + KWayland::Client::PointerGestures *pointerGestures = m_seat->backend()->display()->pointerGestures(); + if (pointerGestures) { + m_pinchGesture.reset(pointerGestures->createPinchGesture(m_pointer.get(), this)); + connect(m_pinchGesture.get(), &PointerPinchGesture::started, this, [this](quint32 serial, quint32 time) { + Q_EMIT pinchGestureBegin(m_pinchGesture->fingerCount(), std::chrono::milliseconds(time), this); + }); + connect(m_pinchGesture.get(), &PointerPinchGesture::updated, this, [this](const QSizeF &delta, qreal scale, qreal rotation, quint32 time) { + Q_EMIT pinchGestureUpdate(scale, rotation, sizeToPoint(delta), std::chrono::milliseconds(time), this); + }); + connect(m_pinchGesture.get(), &PointerPinchGesture::ended, this, [this](quint32 serial, quint32 time) { + Q_EMIT pinchGestureEnd(std::chrono::milliseconds(time), this); + }); + connect(m_pinchGesture.get(), &PointerPinchGesture::cancelled, this, [this](quint32 serial, quint32 time) { + Q_EMIT pinchGestureCancelled(std::chrono::milliseconds(time), this); + }); + + m_swipeGesture.reset(pointerGestures->createSwipeGesture(m_pointer.get(), this)); + connect(m_swipeGesture.get(), &PointerSwipeGesture::started, this, [this](quint32 serial, quint32 time) { + Q_EMIT swipeGestureBegin(m_swipeGesture->fingerCount(), std::chrono::milliseconds(time), this); + }); + connect(m_swipeGesture.get(), &PointerSwipeGesture::updated, this, [this](const QSizeF &delta, quint32 time) { + Q_EMIT swipeGestureUpdate(sizeToPoint(delta), std::chrono::milliseconds(time), this); + }); + connect(m_swipeGesture.get(), &PointerSwipeGesture::ended, this, [this](quint32 serial, quint32 time) { + Q_EMIT swipeGestureEnd(std::chrono::milliseconds(time), this); + }); + connect(m_swipeGesture.get(), &PointerSwipeGesture::cancelled, this, [this](quint32 serial, quint32 time) { + Q_EMIT swipeGestureCancelled(std::chrono::milliseconds(time), this); + }); + } +} + +WaylandInputDevice::WaylandInputDevice(KWayland::Client::RelativePointer *relativePointer, WaylandSeat *seat) + : m_seat(seat) + , m_relativePointer(relativePointer) +{ + connect(relativePointer, &RelativePointer::relativeMotion, this, [this](const QSizeF &delta, const QSizeF &deltaNonAccelerated, quint64 timestamp) { + Q_EMIT pointerMotion(sizeToPoint(delta), sizeToPoint(deltaNonAccelerated), std::chrono::microseconds(timestamp), this); + }); +} + +WaylandInputDevice::WaylandInputDevice(KWayland::Client::Touch *touch, WaylandSeat *seat) + : m_seat(seat) + , m_touch(touch) +{ + connect(touch, &Touch::sequenceCanceled, this, [this]() { + Q_EMIT touchCanceled(this); + }); + connect(touch, &Touch::frameEnded, this, [this]() { + Q_EMIT touchFrame(this); + }); + connect(touch, &Touch::sequenceStarted, this, [this](TouchPoint *tp) { + auto o = m_seat->backend()->findOutput(tp->surface()); + Q_ASSERT(o); + const QPointF position = o->position() + tp->position(); + Q_EMIT touchDown(tp->id(), position, std::chrono::milliseconds(tp->time()), this); + }); + connect(touch, &Touch::pointAdded, this, [this](TouchPoint *tp) { + auto o = m_seat->backend()->findOutput(tp->surface()); + Q_ASSERT(o); + const QPointF position = o->position() + tp->position(); + Q_EMIT touchDown(tp->id(), position, std::chrono::milliseconds(tp->time()), this); + }); + connect(touch, &Touch::pointRemoved, this, [this](TouchPoint *tp) { + Q_EMIT touchUp(tp->id(), std::chrono::milliseconds(tp->time()), this); + }); + connect(touch, &Touch::pointMoved, this, [this](TouchPoint *tp) { + auto o = m_seat->backend()->findOutput(tp->surface()); + Q_ASSERT(o); + const QPointF position = o->position() + tp->position(); + Q_EMIT touchMotion(tp->id(), position, std::chrono::milliseconds(tp->time()), this); + }); +} + +WaylandInputDevice::~WaylandInputDevice() +{ +} + +QString WaylandInputDevice::name() const +{ + return QString(); +} + +bool WaylandInputDevice::isEnabled() const +{ + return true; +} + +void WaylandInputDevice::setEnabled(bool enabled) +{ +} + +bool WaylandInputDevice::isKeyboard() const +{ + return m_keyboard != nullptr; +} + +bool WaylandInputDevice::isPointer() const +{ + return m_pointer || m_relativePointer; +} + +bool WaylandInputDevice::isTouchpad() const +{ + return false; +} + +bool WaylandInputDevice::isTouch() const +{ + return m_touch != nullptr; +} + +bool WaylandInputDevice::isTabletTool() const +{ + return false; +} + +bool WaylandInputDevice::isTabletPad() const +{ + return false; +} + +bool WaylandInputDevice::isTabletModeSwitch() const +{ + return false; +} + +bool WaylandInputDevice::isLidSwitch() const +{ + return false; +} + +KWayland::Client::Pointer *WaylandInputDevice::nativePointer() const +{ + return m_pointer.get(); +} + +WaylandInputBackend::WaylandInputBackend(WaylandBackend *backend, QObject *parent) + : InputBackend(parent) + , m_backend(backend) +{ +} + +void WaylandInputBackend::initialize() +{ + WaylandSeat *seat = m_backend->seat(); + if (seat->relativePointerDevice()) { + Q_EMIT deviceAdded(seat->relativePointerDevice()); + } + if (seat->pointerDevice()) { + Q_EMIT deviceAdded(seat->pointerDevice()); + } + if (seat->keyboardDevice()) { + Q_EMIT deviceAdded(seat->keyboardDevice()); + } + if (seat->touchDevice()) { + Q_EMIT deviceAdded(seat->touchDevice()); + } + + connect(seat, &WaylandSeat::deviceAdded, this, &InputBackend::deviceAdded); + connect(seat, &WaylandSeat::deviceRemoved, this, &InputBackend::deviceRemoved); +} + +WaylandSeat::WaylandSeat(KWayland::Client::Seat *nativeSeat, WaylandBackend *backend) + : QObject(nullptr) + , m_seat(nativeSeat) + , m_backend(backend) +{ + auto updateKeyboardDevice = [this](){ + if (m_seat->hasKeyboard()) { + createKeyboardDevice(); + } else { + destroyKeyboardDevice(); + } + }; + + updateKeyboardDevice(); + connect(m_seat, &Seat::hasKeyboardChanged, this, updateKeyboardDevice); + + auto updatePointerDevice = [this]() { + if (m_seat->hasPointer()) { + createPointerDevice(); + } else { + destroyPointerDevice(); + } + }; + + updatePointerDevice(); + connect(m_seat, &Seat::hasPointerChanged, this, updatePointerDevice); + + auto updateTouchDevice = [this]() { + if (m_seat->hasTouch()) { + createTouchDevice(); + } else { + destroyTouchDevice(); + } + }; + + updateTouchDevice(); + connect(m_seat, &Seat::hasTouchChanged, this, updateTouchDevice); +} + +WaylandSeat::~WaylandSeat() +{ + destroyPointerDevice(); + destroyKeyboardDevice(); + destroyTouchDevice(); +} + +void WaylandSeat::createPointerDevice() +{ + m_pointerDevice = std::make_unique(m_seat->createPointer(), this); + Q_EMIT deviceAdded(m_pointerDevice.get()); +} + +void WaylandSeat::destroyPointerDevice() +{ + if (m_pointerDevice) { + Q_EMIT deviceRemoved(m_pointerDevice.get()); + destroyRelativePointer(); + m_pointerDevice.reset(); + } +} + +void WaylandSeat::createRelativePointer() +{ + KWayland::Client::RelativePointerManager *manager = m_backend->display()->relativePointerManager(); + if (manager) { + m_relativePointerDevice = std::make_unique(manager->createRelativePointer(m_pointerDevice->nativePointer()), this); + Q_EMIT deviceAdded(m_relativePointerDevice.get()); + } +} + +void WaylandSeat::destroyRelativePointer() +{ + if (m_relativePointerDevice) { + Q_EMIT deviceRemoved(m_relativePointerDevice.get()); + m_relativePointerDevice.reset(); + } +} + +void WaylandSeat::createKeyboardDevice() +{ + m_keyboardDevice = std::make_unique(m_seat->createKeyboard(), this); + Q_EMIT deviceAdded(m_keyboardDevice.get()); +} + +void WaylandSeat::destroyKeyboardDevice() +{ + if (m_keyboardDevice) { + Q_EMIT deviceRemoved(m_keyboardDevice.get()); + m_keyboardDevice.reset(); + } +} + +void WaylandSeat::createTouchDevice() +{ + m_touchDevice = std::make_unique(m_seat->createTouch(), this); + Q_EMIT deviceAdded(m_touchDevice.get()); +} + +void WaylandSeat::destroyTouchDevice() +{ + if (m_touchDevice) { + Q_EMIT deviceRemoved(m_touchDevice.get()); + m_touchDevice.reset(); + } +} + +WaylandBackend::WaylandBackend(const WaylandBackendOptions &options, QObject *parent) + : OutputBackend(parent) + , m_options(options) +{ +} + +WaylandBackend::~WaylandBackend() +{ + m_eglDisplay.reset(); + destroyOutputs(); + + m_buffers.clear(); + + m_seat.reset(); + m_display.reset(); + qCDebug(KWIN_WAYLAND_BACKEND) << "Destroyed Wayland display"; +} + +bool WaylandBackend::initialize() +{ + m_display = std::make_unique(); + if (!m_display->initialize(m_options.socketName)) { + return false; + } + + if (WaylandClient::LinuxDmabufV1 *dmabuf = m_display->linuxDmabuf()) { + m_drmDevice = DrmDevice::open(dmabuf->mainDevice()); + if (!m_drmDevice) { + qCWarning(KWIN_WAYLAND_BACKEND) << "Failed to open drm render node" << dmabuf->mainDevice(); + } + } + + createOutputs(); + + m_seat = std::make_unique(m_display->seat(), this); + + QAbstractEventDispatcher *dispatcher = QAbstractEventDispatcher::instance(); + QObject::connect(dispatcher, &QAbstractEventDispatcher::aboutToBlock, m_display.get(), &WaylandDisplay::flush); + QObject::connect(dispatcher, &QAbstractEventDispatcher::awake, m_display.get(), &WaylandDisplay::flush); + + connect(this, &WaylandBackend::pointerLockChanged, this, [this](bool locked) { + if (locked) { + m_seat->createRelativePointer(); + } else { + m_seat->destroyRelativePointer(); + } + }); + + return true; +} + +void WaylandBackend::createOutputs() +{ + // we need to multiply the initial window size with the scale in order to + // create an output window of this size in the end + const QSize pixelSize = m_options.outputSize * m_options.outputScale; + for (int i = 0; i < m_options.outputCount; i++) { + WaylandOutput *output = createOutput(QStringLiteral("WL-%1").arg(i), pixelSize, m_options.outputScale, m_options.fullscreen); + m_outputs << output; + Q_EMIT outputAdded(output); + } + + Q_EMIT outputsQueried(); +} + +WaylandOutput *WaylandBackend::createOutput(const QString &name, const QSize &size, qreal scale, bool fullscreen) +{ + WaylandOutput *waylandOutput = new WaylandOutput(name, this); + waylandOutput->init(size, scale, fullscreen); + + // Wait until the output window is configured by the host compositor. + while (!waylandOutput->isReady()) { + wl_display_roundtrip(m_display->nativeDisplay()); + } + + return waylandOutput; +} + +void WaylandBackend::destroyOutputs() +{ + while (!m_outputs.isEmpty()) { + WaylandOutput *output = m_outputs.takeLast(); + Q_EMIT outputRemoved(output); + delete output; + } +} + +std::unique_ptr WaylandBackend::createInputBackend() +{ + return std::make_unique(this); +} + +std::unique_ptr WaylandBackend::createOpenGLBackend() +{ + return std::make_unique(this); +} + +std::unique_ptr WaylandBackend::createQPainterBackend() +{ + return std::make_unique(this); +} + +WaylandOutput *WaylandBackend::findOutput(KWayland::Client::Surface *nativeSurface) const +{ + for (WaylandOutput *output : m_outputs) { + const auto layers = Compositor::self()->backend()->compatibleOutputLayers(output); + const bool isALayer = std::ranges::any_of(layers, [nativeSurface](OutputLayer *layer) { + if (layer->type() == OutputLayerType::CursorOnly) { + return false; + } + return static_cast(layer)->surface() == nativeSurface; + }); + if (isALayer) { + return output; + } + if (output->surface() == nativeSurface) { + return output; + } + } + return nullptr; +} + +KWayland::Client::SubSurface *WaylandBackend::findSubSurface(KWayland::Client::Surface *nativeSurface) const +{ + for (WaylandOutput *output : m_outputs) { + const auto layers = Compositor::self()->backend()->compatibleOutputLayers(output); + const auto it = std::ranges::find_if(layers, [nativeSurface](OutputLayer *layer) { + // cursor-only layers are a different class + // and can't be a subsurface + if (layer->type() == OutputLayerType::CursorOnly) { + return false; + } + return static_cast(layer)->surface() == nativeSurface; + }); + if (it != layers.end()) { + return static_cast(*it)->subSurface(); + } + if (output->surface() == nativeSurface) { + return nullptr; + } + } + return nullptr; +} + +bool WaylandBackend::supportsPointerLock() +{ + return m_display->pointerConstraints() && m_display->relativePointerManager(); +} + +void WaylandBackend::togglePointerLock() +{ + if (!supportsPointerLock()) { + return; + } + if (!m_seat) { + return; + } + auto pointer = m_seat->pointerDevice()->nativePointer(); + if (!pointer) { + return; + } + if (m_outputs.isEmpty()) { + return; + } + + for (auto output : std::as_const(m_outputs)) { + output->lockPointer(m_seat->pointerDevice()->nativePointer(), !m_pointerLockRequested); + } + m_pointerLockRequested = !m_pointerLockRequested; +} + +QList WaylandBackend::supportedCompositors() const +{ + QList ret; + if (m_display->linuxDmabuf() && m_drmDevice) { + ret.append(OpenGLCompositing); + } + ret.append(QPainterCompositing); + return ret; +} + +QList WaylandBackend::outputs() const +{ + return m_outputs | std::ranges::to>(); +} + +BackendOutput *WaylandBackend::createVirtualOutput(const QString &name, const QString &description, const QSize &size, double scale) +{ + return createOutput(name, size * scale, scale, false); +} + +void WaylandBackend::removeVirtualOutput(BackendOutput *output) +{ + WaylandOutput *waylandOutput = dynamic_cast(output); + if (waylandOutput && m_outputs.removeAll(waylandOutput)) { + Q_EMIT outputRemoved(waylandOutput); + Q_EMIT outputsQueried(); + waylandOutput->unref(); + } +} + +static wl_buffer *importShmBuffer(WaylandDisplay *display, const ShmAttributes *attributes) +{ + wl_shm_format format; + switch (attributes->format) { + case DRM_FORMAT_ARGB8888: + format = WL_SHM_FORMAT_ARGB8888; + break; + case DRM_FORMAT_XRGB8888: + format = WL_SHM_FORMAT_XRGB8888; + break; + default: + format = static_cast(attributes->format); + break; + } + + wl_shm_pool *pool = wl_shm_create_pool(display->shm(), attributes->fd.get(), attributes->size.height() * attributes->stride); + wl_buffer *buffer = wl_shm_pool_create_buffer(pool, + attributes->offset, + attributes->size.width(), + attributes->size.height(), + attributes->stride, + format); + wl_shm_pool_destroy(pool); + + return buffer; +} + +wl_buffer *WaylandBackend::importBuffer(GraphicsBuffer *graphicsBuffer) +{ + auto &buffer = m_buffers[graphicsBuffer]; + if (!buffer) { + wl_buffer *handle = nullptr; + if (const DmaBufAttributes *attributes = graphicsBuffer->dmabufAttributes()) { + handle = m_display->linuxDmabuf()->importBuffer(graphicsBuffer); + } else if (const ShmAttributes *attributes = graphicsBuffer->shmAttributes()) { + handle = importShmBuffer(m_display.get(), attributes); + } else { + qCWarning(KWIN_WAYLAND_BACKEND) << graphicsBuffer << "has unknown type"; + return nullptr; + } + + buffer = std::make_unique(handle, graphicsBuffer); + connect(buffer.get(), &WaylandBuffer::defunct, this, [this, graphicsBuffer]() { + m_buffers.erase(graphicsBuffer); + }); + + static const wl_buffer_listener listener = { + .release = [](void *userData, wl_buffer *buffer) { + WaylandBuffer *slot = static_cast(userData); + slot->unlock(); + }, + }; + wl_buffer_add_listener(handle, &listener, buffer.get()); + } + + buffer->lock(); + return buffer->handle(); +} + +void WaylandBackend::setEglDisplay(std::unique_ptr &&display) +{ + m_eglDisplay = std::move(display); +} + +EglDisplay *WaylandBackend::sceneEglDisplayObject() const +{ + return m_eglDisplay.get(); +} + +DrmDevice *WaylandBackend::drmDevice() const +{ + return m_drmDevice.get(); +} + +WaylandBuffer::WaylandBuffer(wl_buffer *handle, GraphicsBuffer *graphicsBuffer) + : m_graphicsBuffer(graphicsBuffer) + , m_handle(handle) +{ + connect(graphicsBuffer, &GraphicsBuffer::destroyed, this, &WaylandBuffer::defunct); +} + +WaylandBuffer::~WaylandBuffer() +{ + m_graphicsBuffer->disconnect(this); + if (m_locked) { + m_graphicsBuffer->unref(); + } + wl_buffer_destroy(m_handle); +} + +wl_buffer *WaylandBuffer::handle() const +{ + return m_handle; +} + +void WaylandBuffer::lock() +{ + if (!m_locked) { + m_locked = true; + m_graphicsBuffer->ref(); + } +} + +void WaylandBuffer::unlock() +{ + if (m_locked) { + m_locked = false; + m_graphicsBuffer->unref(); + } +} +} + +} // KWin + +#include "moc_wayland_backend.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/wayland/wayland_backend.h b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_backend.h new file mode 100644 index 0000000000..dd6f1cd70a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_backend.h @@ -0,0 +1,272 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "config-kwin.h" + +// KWin +#include "core/inputbackend.h" +#include "core/inputdevice.h" +#include "core/outputbackend.h" +#include "effect/globals.h" +#include "utils/filedescriptor.h" +// Qt +#include +#include +#include +#include +#include + +struct wl_buffer; +struct wl_display; + +namespace KWayland +{ +namespace Client +{ +class Keyboard; +class Pointer; +class PointerSwipeGesture; +class PointerPinchGesture; +class RelativePointer; +class Seat; +class Surface; +class Touch; +class SubSurface; +} +} + +namespace KWin +{ +class GraphicsBuffer; +class DrmDevice; + +namespace Wayland +{ + +class WaylandBackend; +class WaylandSeat; +class WaylandOutput; +class WaylandEglBackend; +class WaylandDisplay; + +class WaylandInputDevice : public InputDevice +{ + Q_OBJECT + +public: + WaylandInputDevice(KWayland::Client::Touch *touch, WaylandSeat *seat); + WaylandInputDevice(KWayland::Client::Keyboard *keyboard, WaylandSeat *seat); + WaylandInputDevice(KWayland::Client::RelativePointer *relativePointer, WaylandSeat *seat); + WaylandInputDevice(KWayland::Client::Pointer *pointer, WaylandSeat *seat); + ~WaylandInputDevice() override; + + QString name() const override; + + bool isEnabled() const override; + void setEnabled(bool enabled) override; + + bool isKeyboard() const override; + bool isPointer() const override; + bool isTouchpad() const override; + bool isTouch() const override; + bool isTabletTool() const override; + bool isTabletPad() const override; + bool isTabletModeSwitch() const override; + bool isLidSwitch() const override; + + KWayland::Client::Pointer *nativePointer() const; + +private: + WaylandSeat *const m_seat; + + std::unique_ptr m_keyboard; + std::unique_ptr m_touch; + std::unique_ptr m_relativePointer; + std::unique_ptr m_pointer; + std::unique_ptr m_pinchGesture; + std::unique_ptr m_swipeGesture; + + QSet m_pressedKeys; +}; + +class WaylandInputBackend : public InputBackend +{ + Q_OBJECT + +public: + explicit WaylandInputBackend(WaylandBackend *backend, QObject *parent = nullptr); + + void initialize() override; + +private: + WaylandBackend *m_backend; +}; + +class WaylandSeat : public QObject +{ + Q_OBJECT +public: + WaylandSeat(KWayland::Client::Seat *nativeSeat, WaylandBackend *backend); + ~WaylandSeat() override; + + WaylandBackend *backend() const + { + return m_backend; + } + + WaylandInputDevice *pointerDevice() const + { + return m_pointerDevice.get(); + } + WaylandInputDevice *relativePointerDevice() const + { + return m_relativePointerDevice.get(); + } + WaylandInputDevice *keyboardDevice() const + { + return m_keyboardDevice.get(); + } + WaylandInputDevice *touchDevice() const + { + return m_touchDevice.get(); + } + + void createRelativePointer(); + void destroyRelativePointer(); + +Q_SIGNALS: + void deviceAdded(WaylandInputDevice *device); + void deviceRemoved(WaylandInputDevice *device); + +private: + void createPointerDevice(); + void destroyPointerDevice(); + void createKeyboardDevice(); + void destroyKeyboardDevice(); + void createTouchDevice(); + void destroyTouchDevice(); + + KWayland::Client::Seat *m_seat; + WaylandBackend *m_backend; + + std::unique_ptr m_pointerDevice; + std::unique_ptr m_relativePointerDevice; + std::unique_ptr m_keyboardDevice; + std::unique_ptr m_touchDevice; +}; + +class WaylandBuffer : public QObject +{ + Q_OBJECT + +public: + WaylandBuffer(wl_buffer *handle, GraphicsBuffer *graphicsBuffer); + ~WaylandBuffer() override; + + wl_buffer *handle() const; + + void lock(); + void unlock(); + +Q_SIGNALS: + void defunct(); + +private: + GraphicsBuffer *m_graphicsBuffer; + wl_buffer *m_handle; + bool m_locked = false; +}; + +struct WaylandBackendOptions +{ + QString socketName; + int outputCount = 1; + qreal outputScale = 1; + QSize outputSize = QSize(1024, 768); + bool fullscreen = false; +}; + +/** + * @brief Class encapsulating all Wayland data structures needed by the Egl backend. + * + * It creates the connection to the Wayland Compositor, sets up the registry and creates + * the Wayland output surfaces and its shell mappings. + */ +class KWIN_EXPORT WaylandBackend : public OutputBackend +{ + Q_OBJECT + +public: + explicit WaylandBackend(const WaylandBackendOptions &options, QObject *parent = nullptr); + ~WaylandBackend() override; + bool initialize() override; + + std::unique_ptr createInputBackend() override; + std::unique_ptr createOpenGLBackend() override; + std::unique_ptr createQPainterBackend() override; + + WaylandDisplay *display() const + { + return m_display.get(); + } + WaylandSeat *seat() const + { + return m_seat.get(); + } + + bool supportsPointerLock(); + void togglePointerLock(); + + QList supportedCompositors() const override; + + WaylandOutput *findOutput(KWayland::Client::Surface *nativeSurface) const; + KWayland::Client::SubSurface *findSubSurface(KWayland::Client::Surface *nativeSurface) const; + QList outputs() const override; + QList waylandOutputs() const + { + return m_outputs; + } + + BackendOutput *createVirtualOutput(const QString &name, const QString &description, const QSize &size, double scale) override; + void removeVirtualOutput(BackendOutput *output) override; + + wl_buffer *importBuffer(GraphicsBuffer *graphicsBuffer); + + DrmDevice *drmDevice() const; + + void setEglBackend(WaylandEglBackend *eglBackend) + { + m_eglBackend = eglBackend; + } + void setEglDisplay(std::unique_ptr &&display); + EglDisplay *sceneEglDisplayObject() const override; + +Q_SIGNALS: + void pointerLockChanged(bool locked); + +private: + void createOutputs(); + void destroyOutputs(); + WaylandOutput *createOutput(const QString &name, const QSize &size, qreal scale, bool fullscreen); + + WaylandBackendOptions m_options; + std::unique_ptr m_display; + std::unique_ptr m_seat; + WaylandEglBackend *m_eglBackend = nullptr; + QList m_outputs; + bool m_pointerLockRequested = false; + std::unique_ptr m_drmDevice; + std::unique_ptr m_eglDisplay; + std::map> m_buffers; +}; + +} // namespace Wayland +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/backends/wayland/wayland_display.cpp b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_display.cpp new file mode 100644 index 0000000000..5eed420729 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_display.cpp @@ -0,0 +1,446 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "wayland_display.h" +#include "utils/memorymap.h" +#include "wayland-client/linuxdmabuf.h" +#include "wayland-client/viewporter.h" +#include "wayland_logging.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "color_manager.h" + +// Generated in src/wayland. +#include "wayland-fractional-scale-v1-client-protocol.h" +#include "wayland-keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h" +#include "wayland-linux-dmabuf-unstable-v1-client-protocol.h" +#include "wayland-pointer-constraints-unstable-v1-client-protocol.h" +#include "wayland-pointer-gestures-unstable-v1-server-protocol.h" +#include "wayland-presentation-time-client-protocol.h" +#include "wayland-relative-pointer-unstable-v1-client-protocol.h" +#include "wayland-single-pixel-buffer-v1-client-protocol.h" +#include "wayland-tearing-control-v1-client-protocol.h" +#include "wayland-viewporter-client-protocol.h" +#include "wayland-xdg-decoration-unstable-v1-client-protocol.h" +#include "wayland-xdg-shell-client-protocol.h" +#include "wayland-xdg-toplevel-icon-v1-client-protocol.h" + +namespace KWin +{ +namespace Wayland +{ + +class WaylandEventThread : public QThread +{ + Q_OBJECT + +public: + WaylandEventThread(wl_display *display) + : m_display(display) + , m_fd(wl_display_get_fd(display)) + , m_quitPipe{-1, -1} + , m_reading(true) + , m_quitting(false) + { + if (pipe2(m_quitPipe, O_CLOEXEC) == -1) { + qCWarning(KWIN_WAYLAND_BACKEND) << "Failed to create quite pipe in WaylandEventThread"; + } + } + + ~WaylandEventThread() override + { + if (m_quitPipe[0] != -1) { + close(m_quitPipe[0]); + close(m_quitPipe[1]); + } + } + + void dispatch() + { + while (true) { + if (wl_display_dispatch_pending(m_display) < 0) { + qFatal("Wayland connection broke"); + } + + wl_display_flush(m_display); + + if (m_reading.loadAcquire()) { + break; + } + + if (wl_display_prepare_read(m_display) == 0) { + QMutexLocker lock(&m_mutex); + m_reading.storeRelease(true); + m_cond.wakeOne(); + break; + } + } + } + + void stop() + { + if (m_quitPipe[1] != -1) { + write(m_quitPipe[1], "\0", 1); + } + + m_mutex.lock(); + m_quitting = true; + m_cond.wakeOne(); + m_mutex.unlock(); + + wait(); + } + +Q_SIGNALS: + void available(); + +protected: + void run() override + { + while (true) { + m_reading.storeRelease(false); + + Q_EMIT available(); + + m_mutex.lock(); + while (!m_reading.loadRelaxed() && !m_quitting) { + m_cond.wait(&m_mutex); + } + m_mutex.unlock(); + + if (m_quitting) { + break; + } + + pollfd fds[2] = { { m_fd, POLLIN, 0 }, { m_quitPipe[0], POLLIN, 0 } }; + poll(fds, 2, -1); + + if (fds[1].revents & POLLIN) { + wl_display_cancel_read(m_display); + break; + } + + if (fds[0].revents & POLLIN) { + wl_display_read_events(m_display); + } else { + wl_display_cancel_read(m_display); + } + } + } + +private: + wl_display *const m_display; + int m_fd; + int m_quitPipe[2]; + QAtomicInteger m_reading; + QMutex m_mutex; + QWaitCondition m_cond; + bool m_quitting; +}; + +WaylandDisplay::WaylandDisplay() +{ +} + +WaylandDisplay::~WaylandDisplay() +{ + m_eventThread->stop(); + m_eventThread.reset(); + + m_compositor.reset(); + m_subCompositor.reset(); + m_pointerConstraints.reset(); + m_pointerGestures.reset(); + m_relativePointerManager.reset(); + m_seat.reset(); + m_xdgDecorationManager.reset(); + m_xdgShell.reset(); + m_linuxDmabuf.reset(); + m_colorManager.reset(); + m_viewporter.reset(); + + if (m_shm) { + wl_shm_destroy(m_shm); + } + if (m_presentationTime) { + wp_presentation_destroy(m_presentationTime); + } + if (m_tearingControl) { + wp_tearing_control_manager_v1_destroy(m_tearingControl); + } + if (m_fractionalScaleV1) { + wp_fractional_scale_manager_v1_destroy(m_fractionalScaleV1); + } + if (m_singlePixelManager) { + wp_single_pixel_buffer_manager_v1_destroy(m_singlePixelManager); + } + if (m_toplevelIconManager) { + xdg_toplevel_icon_manager_v1_destroy(m_toplevelIconManager); + } + if (m_keyboardShortcutsInhibitManager) { + zwp_keyboard_shortcuts_inhibit_manager_v1_destroy(m_keyboardShortcutsInhibitManager); + } + if (m_registry) { + wl_registry_destroy(m_registry); + } + if (m_display) { + wl_display_disconnect(m_display); + } +} + +void WaylandDisplay::flush() +{ + m_eventThread->dispatch(); +} + +bool WaylandDisplay::initialize(const QString &socketName) +{ + m_display = wl_display_connect(socketName.toUtf8()); + if (!m_display) { + return false; + } + + m_eventThread = std::make_unique(m_display); + connect(m_eventThread.get(), &WaylandEventThread::available, this, &WaylandDisplay::flush, Qt::QueuedConnection); + m_eventThread->start(); + + static wl_registry_listener registryListener { + .global = registry_global, + .global_remove = registry_global_remove, + }; + m_registry = wl_display_get_registry(m_display); + wl_registry_add_listener(m_registry, ®istryListener, this); + wl_display_roundtrip(m_display); + wl_display_roundtrip(m_display); // get dmabuf formats + + if (!m_compositor) { + qCWarning(KWIN_WAYLAND_BACKEND, "wl_compositor isn't supported by the host compositor"); + return false; + } + if (!m_subCompositor) { + qCWarning(KWIN_WAYLAND_BACKEND, "wl_subcompositor isn't supported by the host compositor"); + return false; + } + if (!m_xdgShell) { + qCWarning(KWIN_WAYLAND_BACKEND, "xdg_shell isn't supported by the host compositor"); + return false; + } + if (!m_singlePixelManager) { + qCWarning(KWIN_WAYLAND_BACKEND, "wp_single_pixel_buffer_manager_v1 isn't supported by the host compositor"); + return false; + } + if (!m_viewporter) { + qCWarning(KWIN_WAYLAND_BACKEND, "wp_viewporter isn't supported by the host compositor"); + return false; + } + if (!m_seat) { + qCWarning(KWIN_WAYLAND_BACKEND, "wl_seat isn't supported by the host compositor"); + return false; + } + if (!m_pointerConstraints) { + qCWarning(KWIN_WAYLAND_BACKEND, "zwp_pointer_constraints_v1 isn't supported by the host compositor"); + return false; + } + if (!m_presentationTime) { + qCWarning(KWIN_WAYLAND_BACKEND, "wp_presentation_time isn't supported by the host compositor"); + return false; + } + if (!m_toplevelIconManager) { + qCWarning(KWIN_WAYLAND_BACKEND, "xdg_toplevel_icon_manager_v1 isn't supported by the host compositor"); + // Not fatal, can live without it. + } + if (!m_keyboardShortcutsInhibitManager) { + qCWarning(KWIN_WAYLAND_BACKEND, "zwp_keyboard_shortcuts_inhibit_manager_v1 isn't supported by the host compositor"); + // Not fatal, can live without it. + } + return true; +} + +wl_display *WaylandDisplay::nativeDisplay() const +{ + return m_display; +} + +KWayland::Client::Compositor *WaylandDisplay::compositor() const +{ + return m_compositor.get(); +} + +KWayland::Client::SubCompositor *WaylandDisplay::subCompositor() const +{ + return m_subCompositor.get(); +} + +KWayland::Client::PointerConstraints *WaylandDisplay::pointerConstraints() const +{ + return m_pointerConstraints.get(); +} + +KWayland::Client::PointerGestures *WaylandDisplay::pointerGestures() const +{ + return m_pointerGestures.get(); +} + +KWayland::Client::RelativePointerManager *WaylandDisplay::relativePointerManager() const +{ + return m_relativePointerManager.get(); +} + +wl_shm *WaylandDisplay::shm() const +{ + return m_shm; +} + +KWayland::Client::Seat *WaylandDisplay::seat() const +{ + return m_seat.get(); +} + +KWayland::Client::XdgShell *WaylandDisplay::xdgShell() const +{ + return m_xdgShell.get(); +} + +KWayland::Client::XdgDecorationManager *WaylandDisplay::xdgDecorationManager() const +{ + return m_xdgDecorationManager.get(); +} + +WaylandClient::LinuxDmabufV1 *WaylandDisplay::linuxDmabuf() const +{ + return m_linuxDmabuf.get(); +} + +wp_presentation *WaylandDisplay::presentationTime() const +{ + return m_presentationTime; +} + +wp_tearing_control_manager_v1 *WaylandDisplay::tearingControl() const +{ + return m_tearingControl; +} + +WaylandClient::Viewporter *WaylandDisplay::viewporter() const +{ + return m_viewporter.get(); +} + +ColorManager *WaylandDisplay::colorManager() const +{ + return m_colorManager.get(); +} + +wp_fractional_scale_manager_v1 *WaylandDisplay::fractionalScale() const +{ + return m_fractionalScaleV1; +} + +wp_single_pixel_buffer_manager_v1 *WaylandDisplay::singlePixelManager() const +{ + return m_singlePixelManager; +} + +xdg_toplevel_icon_manager_v1 *WaylandDisplay::toplevelIconManager() const +{ + return m_toplevelIconManager; +} + +zwp_keyboard_shortcuts_inhibit_manager_v1 *WaylandDisplay::keyboardShortcutsInhibitManager() const +{ + return m_keyboardShortcutsInhibitManager; +} + +void WaylandDisplay::registry_global(void *data, wl_registry *registry, uint32_t name, const char *interface, uint32_t version) +{ + WaylandDisplay *display = static_cast(data); + + if (strcmp(interface, wl_compositor_interface.name) == 0) { + if (version < 4) { + qFatal("wl_compositor version 4 or later is required"); + } + display->m_compositor = std::make_unique(); + display->m_compositor->setup(static_cast(wl_registry_bind(registry, name, &wl_compositor_interface, std::min(version, 4u)))); + } else if (strcmp(interface, wl_shm_interface.name) == 0) { + display->m_shm = static_cast(wl_registry_bind(registry, name, &wl_shm_interface, std::min(version, 1u))); + } else if (strcmp(interface, wl_seat_interface.name) == 0) { + display->m_seat = std::make_unique(); + display->m_seat->setup(static_cast(wl_registry_bind(registry, name, &wl_seat_interface, std::min(version, 5u)))); + } else if (strcmp(interface, xdg_wm_base_interface.name) == 0) { + display->m_xdgShell = std::make_unique(); + display->m_xdgShell->setup(static_cast(wl_registry_bind(registry, name, &xdg_wm_base_interface, std::min(version, 1u)))); + } else if (strcmp(interface, zwp_pointer_constraints_v1_interface.name) == 0) { + display->m_pointerConstraints = std::make_unique(); + display->m_pointerConstraints->setup(static_cast(wl_registry_bind(registry, name, &zwp_pointer_constraints_v1_interface, std::min(version, 1u)))); + } else if (strcmp(interface, zwp_pointer_gestures_v1_interface.name) == 0) { + display->m_pointerGestures = std::make_unique(); + display->m_pointerGestures->setup(static_cast(wl_registry_bind(registry, name, &zwp_pointer_gestures_v1_interface, std::min(version, 1u)))); + } else if (strcmp(interface, zwp_relative_pointer_manager_v1_interface.name) == 0) { + display->m_relativePointerManager = std::make_unique(); + display->m_relativePointerManager->setup(static_cast(wl_registry_bind(registry, name, &zwp_relative_pointer_manager_v1_interface, std::min(version, 1u)))); + } else if (strcmp(interface, zxdg_decoration_manager_v1_interface.name) == 0) { + display->m_xdgDecorationManager = std::make_unique(); + display->m_xdgDecorationManager->setup(static_cast(wl_registry_bind(registry, name, &zxdg_decoration_manager_v1_interface, std::min(version, 1u)))); + } else if (strcmp(interface, zwp_linux_dmabuf_v1_interface.name) == 0) { + if (version < 4) { + qWarning("zwp_linux_dmabuf_v1 v4 or newer is needed"); + return; + } + display->m_linuxDmabuf = std::make_unique(registry, name, std::min(version, 4u)); + } else if (strcmp(interface, wp_presentation_interface.name) == 0) { + display->m_presentationTime = reinterpret_cast(wl_registry_bind(registry, name, &wp_presentation_interface, std::min(version, 2u))); + } else if (strcmp(interface, wp_tearing_control_manager_v1_interface.name) == 0) { + display->m_tearingControl = reinterpret_cast(wl_registry_bind(registry, name, &wp_tearing_control_manager_v1_interface, 1)); + } else if (strcmp(interface, wp_color_manager_v1_interface.name) == 0) { + const auto global = reinterpret_cast(wl_registry_bind(registry, name, &wp_color_manager_v1_interface, 1)); + display->m_colorManager = std::make_unique(global); + } else if (strcmp(interface, wp_fractional_scale_manager_v1_interface.name) == 0) { + display->m_fractionalScaleV1 = reinterpret_cast(wl_registry_bind(registry, name, &wp_fractional_scale_manager_v1_interface, 1)); + } else if (strcmp(interface, wp_viewporter_interface.name) == 0) { + display->m_viewporter = std::make_unique(registry, name, 1u); + } else if (strcmp(interface, wl_subcompositor_interface.name) == 0) { + display->m_subCompositor = std::make_unique(); + display->m_subCompositor->setup(static_cast(wl_registry_bind(registry, name, &wl_subcompositor_interface, 1))); + } else if (strcmp(interface, wp_single_pixel_buffer_manager_v1_interface.name) == 0) { + display->m_singlePixelManager = reinterpret_cast(wl_registry_bind(registry, name, &wp_single_pixel_buffer_manager_v1_interface, 1)); + } else if (strcmp(interface, xdg_toplevel_icon_manager_v1_interface.name) == 0) { + display->m_toplevelIconManager = reinterpret_cast(wl_registry_bind(registry, name, &xdg_toplevel_icon_manager_v1_interface, 1)); + } else if (strcmp(interface, zwp_keyboard_shortcuts_inhibit_manager_v1_interface.name) == 0) { + display->m_keyboardShortcutsInhibitManager = reinterpret_cast(wl_registry_bind(registry, name, &zwp_keyboard_shortcuts_inhibit_manager_v1_interface, 1)); + } +} + +void WaylandDisplay::registry_global_remove(void *data, wl_registry *registry, uint32_t name) +{ +} + +} +} + +#include "wayland_display.moc" + +#include "moc_wayland_display.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/wayland/wayland_display.h b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_display.h new file mode 100644 index 0000000000..e41da18c72 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_display.h @@ -0,0 +1,116 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include + +struct wl_display; +struct wl_registry; +struct wl_shm; +struct zwp_linux_dmabuf_v1; +struct wp_presentation; +struct wp_tearing_control_manager_v1; +struct wp_fractional_scale_manager_v1; +struct wp_single_pixel_buffer_manager_v1; +struct xdg_toplevel_icon_manager_v1; +struct zwp_keyboard_shortcuts_inhibit_manager_v1; + +namespace KWayland +{ +namespace Client +{ +class Compositor; +class PointerConstraints; +class PointerGestures; +class RelativePointerManager; +class Seat; +class XdgDecorationManager; +class XdgShell; +class SubCompositor; +} +} + +namespace KWin::WaylandClient +{ +class LinuxDmabufV1; +class Viewporter; +} + +namespace KWin +{ +namespace Wayland +{ + +class WaylandEventThread; +class WaylandLinuxDmabufFeedbackV1; +class ColorManager; + +class WaylandDisplay : public QObject +{ + Q_OBJECT + +public: + WaylandDisplay(); + ~WaylandDisplay() override; + + bool initialize(const QString &socketName); + + wl_display *nativeDisplay() const; + KWayland::Client::Compositor *compositor() const; + KWayland::Client::PointerConstraints *pointerConstraints() const; + KWayland::Client::PointerGestures *pointerGestures() const; + KWayland::Client::RelativePointerManager *relativePointerManager() const; + KWayland::Client::Seat *seat() const; + KWayland::Client::XdgDecorationManager *xdgDecorationManager() const; + KWayland::Client::SubCompositor *subCompositor() const; + wl_shm *shm() const; + KWayland::Client::XdgShell *xdgShell() const; + WaylandClient::LinuxDmabufV1 *linuxDmabuf() const; + wp_presentation *presentationTime() const; + wp_tearing_control_manager_v1 *tearingControl() const; + ColorManager *colorManager() const; + wp_fractional_scale_manager_v1 *fractionalScale() const; + WaylandClient::Viewporter *viewporter() const; + wp_single_pixel_buffer_manager_v1 *singlePixelManager() const; + xdg_toplevel_icon_manager_v1 *toplevelIconManager() const; + zwp_keyboard_shortcuts_inhibit_manager_v1 *keyboardShortcutsInhibitManager() const; + +public Q_SLOTS: + void flush(); + +private: + static void registry_global(void *data, wl_registry *registry, uint32_t name, const char *interface, uint32_t version); + static void registry_global_remove(void *data, wl_registry *registry, uint32_t name); + + wl_display *m_display = nullptr; + wl_registry *m_registry = nullptr; + wl_shm *m_shm = nullptr; + wp_presentation *m_presentationTime = nullptr; + wp_tearing_control_manager_v1 *m_tearingControl = nullptr; + wp_fractional_scale_manager_v1 *m_fractionalScaleV1 = nullptr; + std::unique_ptr m_viewporter; + wp_single_pixel_buffer_manager_v1 *m_singlePixelManager = nullptr; + xdg_toplevel_icon_manager_v1 *m_toplevelIconManager = nullptr; + zwp_keyboard_shortcuts_inhibit_manager_v1 *m_keyboardShortcutsInhibitManager = nullptr; + std::unique_ptr m_colorManager; + std::unique_ptr m_eventThread; + std::unique_ptr m_linuxDmabuf; + std::unique_ptr m_compositor; + std::unique_ptr m_subCompositor; + std::unique_ptr m_pointerConstraints; + std::unique_ptr m_pointerGestures; + std::unique_ptr m_relativePointerManager; + std::unique_ptr m_seat; + std::unique_ptr m_xdgDecorationManager; + std::unique_ptr m_xdgShell; +}; + +} +} diff --git a/local/recipes/kde/kwin/source/src/backends/wayland/wayland_egl_backend.cpp b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_egl_backend.cpp new file mode 100644 index 0000000000..66ae1b2e05 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_egl_backend.cpp @@ -0,0 +1,342 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "wayland_egl_backend.h" +#include "core/drmdevice.h" +#include "core/gbmgraphicsbufferallocator.h" +#include "opengl/eglswapchain.h" +#include "opengl/glrendertimequery.h" +#include "opengl/glutils.h" +#include "scene/surfaceitem_wayland.h" +#include "wayland-client/linuxdmabuf.h" +#include "wayland/surface.h" +#include "wayland_backend.h" +#include "wayland_display.h" +#include "wayland_logging.h" +#include "wayland_output.h" + +#include +#include + +#include +#include +#include +#include + +namespace KWin +{ +namespace Wayland +{ + +static const bool bufferAgeEnabled = qEnvironmentVariable("KWIN_USE_BUFFER_AGE") != QStringLiteral("0"); + +WaylandEglLayer::WaylandEglLayer(WaylandOutput *output, WaylandEglBackend *backend, OutputLayerType type, int zpos) + : WaylandLayer(output, type, zpos) + , m_backend(backend) +{ +} + +WaylandEglLayer::~WaylandEglLayer() +{ +} + +GLFramebuffer *WaylandEglLayer::fbo() const +{ + return m_buffer->framebuffer(); +} + +std::optional WaylandEglLayer::doBeginFrame() +{ + if (!m_backend->openglContext()->makeCurrent()) { + qCCritical(KWIN_WAYLAND_BACKEND) << "Make Context Current failed"; + return std::nullopt; + } + + if (m_color != m_previousColor) { + // need to force a full repaint + m_damageJournal.clear(); + } + + const QSize nativeSize = targetRect().size(); + if (!m_swapchain || m_swapchain->size() != nativeSize) { + const QHash> formatTable = m_backend->backend()->display()->linuxDmabuf()->formats(); + const auto suitableFormats = filterAndSortFormats(formatTable, m_requiredAlphaBits, m_output->colorPowerTradeoff()); + for (const auto &candidate : suitableFormats) { + auto it = formatTable.constFind(candidate.drmFormat); + if (it == formatTable.constEnd()) { + continue; + } + m_swapchain = EglSwapchain::create(m_backend->drmDevice()->allocator(), m_backend->openglContext(), nativeSize, it.key(), it.value()); + if (m_swapchain) { + break; + } + } + if (!m_swapchain) { + qCWarning(KWIN_WAYLAND_BACKEND) << "Could not find a suitable render format"; + return std::nullopt; + } + } + + m_buffer = m_swapchain->acquire(); + if (!m_buffer) { + return std::nullopt; + } + + const Region repair = bufferAgeEnabled ? m_damageJournal.accumulate(m_buffer->age(), Region::infinite()) : Region::infinite(); + m_query = std::make_unique(m_backend->openglContextRef()); + m_query->begin(); + return OutputLayerBeginFrameInfo{ + .renderTarget = RenderTarget(m_buffer->framebuffer(), m_color), + .repaint = repair, + }; +} + +bool WaylandEglLayer::doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) +{ + m_query->end(); + frame->addRenderTimeQuery(std::move(m_query)); + // Flush rendering commands to the dmabuf. + glFlush(); + EGLNativeFence releaseFence{m_backend->eglDisplayObject()}; + + setBuffer(m_backend->backend()->importBuffer(m_buffer->buffer()), damagedDeviceRegion); + m_swapchain->release(m_buffer, releaseFence.takeFileDescriptor()); + + m_damageJournal.add(damagedDeviceRegion); + return true; +} + +bool WaylandEglLayer::importScanoutBuffer(GraphicsBuffer *buffer, const std::shared_ptr &frame) +{ + if (!test()) { + return false; + } + auto presentationBuffer = m_backend->backend()->importBuffer(buffer); + if (!presentationBuffer) { + return false; + } + setBuffer(presentationBuffer, Region::infinite()); + return true; +} + +DrmDevice *WaylandEglLayer::scanoutDevice() const +{ + return m_backend->drmDevice(); +} + +QHash> WaylandEglLayer::supportedDrmFormats() const +{ + return m_backend->backend()->display()->linuxDmabuf()->formats(); +} + +void WaylandEglLayer::releaseBuffers() +{ + m_buffer.reset(); + m_swapchain.reset(); +} + +WaylandEglCursorLayer::WaylandEglCursorLayer(WaylandOutput *output, WaylandEglBackend *backend) + : OutputLayer(output, OutputLayerType::CursorOnly, 255, 255, 255) + , m_backend(backend) +{ +} + +WaylandEglCursorLayer::~WaylandEglCursorLayer() +{ + m_backend->openglContext()->makeCurrent(); +} + +std::optional WaylandEglCursorLayer::doBeginFrame() +{ + if (!m_backend->openglContext()->makeCurrent()) { + qCCritical(KWIN_WAYLAND_BACKEND) << "Make Context Current failed"; + return std::nullopt; + } + + const auto bufferSize = targetRect().size(); + if (!m_swapchain || m_swapchain->size() != bufferSize) { + const QHash> formatTable = m_backend->backend()->display()->linuxDmabuf()->formats(); + const auto suitableFormats = filterAndSortFormats(formatTable, m_requiredAlphaBits, m_output->colorPowerTradeoff()); + for (const auto &candidate : suitableFormats) { + auto it = formatTable.constFind(candidate.drmFormat); + if (it == formatTable.constEnd()) { + continue; + } + m_swapchain = EglSwapchain::create(m_backend->drmDevice()->allocator(), m_backend->openglContext(), bufferSize, it.key(), it.value()); + if (m_swapchain) { + break; + } + } + if (!m_swapchain) { + return std::nullopt; + } + } + + m_buffer = m_swapchain->acquire(); + if (!m_buffer) { + return std::nullopt; + } + + m_query = std::make_unique(m_backend->openglContextRef()); + m_query->begin(); + return OutputLayerBeginFrameInfo{ + .renderTarget = RenderTarget(m_buffer->framebuffer()), + .repaint = Region::infinite(), + }; +} + +bool WaylandEglCursorLayer::doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) +{ + m_query->end(); + if (frame) { + frame->addRenderTimeQuery(std::move(m_query)); + } + // Flush rendering commands to the dmabuf. + glFlush(); + + wl_buffer *buffer = m_backend->backend()->importBuffer(m_buffer->buffer()); + Q_ASSERT(buffer); + + static_cast(m_output.get())->cursor()->update(buffer, m_buffer->buffer()->size() / m_output->scale(), (hotspot() / m_output->scale()).toPoint()); + + EGLNativeFence releaseFence{m_backend->eglDisplayObject()}; + m_swapchain->release(m_buffer, releaseFence.takeFileDescriptor()); + return true; +} + +DrmDevice *WaylandEglCursorLayer::scanoutDevice() const +{ + return m_backend->drmDevice(); +} + +QHash> WaylandEglCursorLayer::supportedDrmFormats() const +{ + return m_backend->supportedFormats(); +} + +void WaylandEglCursorLayer::releaseBuffers() +{ + m_buffer.reset(); + m_swapchain.reset(); +} + +WaylandEglBackend::WaylandEglBackend(WaylandBackend *b) + : m_backend(b) +{ + connect(m_backend, &WaylandBackend::outputAdded, this, &WaylandEglBackend::createOutputLayers); + + b->setEglBackend(this); +} + +WaylandEglBackend::~WaylandEglBackend() +{ + cleanup(); +} + +WaylandBackend *WaylandEglBackend::backend() const +{ + return m_backend; +} + +DrmDevice *WaylandEglBackend::drmDevice() const +{ + return m_backend->drmDevice(); +} + +void WaylandEglBackend::cleanupSurfaces() +{ + const auto outputs = m_backend->outputs(); + for (BackendOutput *output : outputs) { + static_cast(output)->setOutputLayers({}); + } +} + +void WaylandEglBackend::createOutputLayers(BackendOutput *output) +{ + const auto waylandOutput = static_cast(output); + std::vector> layers; + auto primary = std::make_unique(waylandOutput, this, OutputLayerType::Primary, 0); + primary->subSurface()->placeAbove(waylandOutput->surface()); + layers.push_back(std::move(primary)); + for (int z = 1; z < 5; z++) { + auto layer = std::make_unique(waylandOutput, this, OutputLayerType::GenericLayer, z); + layer->subSurface()->placeAbove(static_cast(layers.back().get())->surface()); + layers.push_back(std::move(layer)); + } + layers.push_back(std::make_unique(waylandOutput, this)); + waylandOutput->setOutputLayers(std::move(layers)); +} + +bool WaylandEglBackend::initializeEgl() +{ + initClientExtensions(); + + if (!m_backend->sceneEglDisplayObject()) { + for (const QByteArray &extension : {QByteArrayLiteral("EGL_EXT_platform_base"), QByteArrayLiteral("EGL_KHR_platform_gbm")}) { + if (!hasClientExtension(extension)) { + qCWarning(KWIN_WAYLAND_BACKEND) << extension << "client extension is not supported by the platform"; + return false; + } + } + + m_backend->setEglDisplay(EglDisplay::create(eglGetPlatformDisplayEXT(EGL_PLATFORM_GBM_KHR, m_backend->drmDevice()->gbmDevice(), nullptr))); + } + + const auto display = m_backend->sceneEglDisplayObject(); + if (!display) { + return false; + } + setEglDisplay(display); + return true; +} + +void WaylandEglBackend::init() +{ + if (!initializeEgl()) { + setFailed("Could not initialize egl"); + return; + } + if (!initRenderingContext()) { + setFailed("Could not initialize rendering context"); + return; + } + + initWayland(); +} + +bool WaylandEglBackend::initRenderingContext() +{ + if (!createContext(EGL_NO_CONFIG_KHR)) { + return false; + } + + auto waylandOutputs = m_backend->waylandOutputs(); + + // we only allow to start with at least one output + if (waylandOutputs.isEmpty()) { + return false; + } + + for (auto *out : waylandOutputs) { + createOutputLayers(out); + } + + return openglContext()->makeCurrent(); +} + +QList WaylandEglBackend::compatibleOutputLayers(BackendOutput *output) +{ + return static_cast(output)->outputLayers(); +} + +} +} + +#include "moc_wayland_egl_backend.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/wayland/wayland_egl_backend.h b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_egl_backend.h new file mode 100644 index 0000000000..18fd69fff4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_egl_backend.h @@ -0,0 +1,115 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "opengl/eglbackend.h" +#include "opengl/eglnativefence.h" +#include "utils/damagejournal.h" +#include "wayland_layer.h" + +#include + +struct wl_buffer; + +namespace KWin +{ +class EglSwapchainSlot; +class EglSwapchain; +class GLFramebuffer; +class GraphicsBufferAllocator; +class GLRenderTimeQuery; + +namespace Wayland +{ +class WaylandBackend; +class WaylandOutput; +class WaylandEglBackend; + +class WaylandEglLayer : public WaylandLayer +{ +public: + WaylandEglLayer(WaylandOutput *output, WaylandEglBackend *backend, OutputLayerType type, int zpos); + ~WaylandEglLayer() override; + + GLFramebuffer *fbo() const; + std::optional doBeginFrame() override; + bool doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) override; + bool importScanoutBuffer(GraphicsBuffer *buffer, const std::shared_ptr &frame) override; + DrmDevice *scanoutDevice() const override; + QHash> supportedDrmFormats() const override; + void releaseBuffers() override; + +private: + DamageJournal m_damageJournal; + std::shared_ptr m_swapchain; + std::shared_ptr m_buffer; + std::unique_ptr m_query; + WaylandEglBackend *const m_backend; + + friend class WaylandEglBackend; +}; + +class WaylandEglCursorLayer : public OutputLayer +{ + Q_OBJECT + +public: + WaylandEglCursorLayer(WaylandOutput *output, WaylandEglBackend *backend); + ~WaylandEglCursorLayer() override; + + std::optional doBeginFrame() override; + bool doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) override; + DrmDevice *scanoutDevice() const override; + QHash> supportedDrmFormats() const override; + void releaseBuffers() override; + +private: + WaylandEglBackend *m_backend; + std::shared_ptr m_swapchain; + std::shared_ptr m_buffer; + std::unique_ptr m_query; +}; + +/** + * @brief OpenGL Backend using Egl on a Wayland surface. + * + * This Backend is the basis for a session compositor running on top of a Wayland system compositor. + * It creates a Surface as large as the screen and maps it as a fullscreen shell surface on the + * system compositor. The OpenGL context is created on the Wayland surface, so for rendering X11 is + * not involved. + * + * Also in repainting the backend is currently still rather limited. Only supported mode is fullscreen + * repaints, which is obviously not optimal. Best solution is probably to go for buffer_age extension + * and make it the only available solution next to fullscreen repaints. + */ +class WaylandEglBackend : public EglBackend +{ + Q_OBJECT +public: + WaylandEglBackend(WaylandBackend *b); + ~WaylandEglBackend() override; + + WaylandBackend *backend() const; + DrmDevice *drmDevice() const override; + + void init() override; + QList compatibleOutputLayers(BackendOutput *output) override; + +private: + bool initializeEgl(); + bool initRenderingContext(); + void createOutputLayers(BackendOutput *output); + void cleanupSurfaces() override; + + WaylandBackend *m_backend; +}; + +} // namespace Wayland +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/backends/wayland/wayland_logging.cpp b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_logging.cpp new file mode 100644 index 0000000000..79446f6cfd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_logging.cpp @@ -0,0 +1,10 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "wayland_logging.h" +Q_LOGGING_CATEGORY(KWIN_WAYLAND_BACKEND, "kwin_wayland_backend", QtWarningMsg) diff --git a/local/recipes/kde/kwin/source/src/backends/wayland/wayland_logging.h b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_logging.h new file mode 100644 index 0000000000..c9c6c875c3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_logging.h @@ -0,0 +1,14 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(KWIN_WAYLAND_BACKEND) diff --git a/local/recipes/kde/kwin/source/src/backends/wayland/wayland_output.cpp b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_output.cpp new file mode 100644 index 0000000000..e435266c4f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_output.cpp @@ -0,0 +1,566 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "wayland_output.h" +#include "color_manager.h" +#include "compositor.h" +#include "core/outputconfiguration.h" +#include "core/outputlayer.h" +#include "core/renderbackend.h" +#include "core/renderloop_p.h" +#include "wayland-client/viewporter.h" +#include "wayland_backend.h" +#include "wayland_display.h" +#include "wayland_layer.h" + +#include +#include +#include +#include +#include +#include + +#include "wayland-fractional-scale-v1-client-protocol.h" +#include "wayland-keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h" +#include "wayland-presentation-time-client-protocol.h" +#include "wayland-single-pixel-buffer-v1-client-protocol.h" +#include "wayland-tearing-control-v1-client-protocol.h" +#include "wayland-xdg-toplevel-icon-v1-client-protocol.h" +#include "workspace.h" + +#include + +#include + +#include +#include + +namespace KWin +{ +namespace Wayland +{ + +using namespace KWayland::Client; + +WaylandCursor::WaylandCursor(WaylandBackend *backend) + : m_surface(backend->display()->compositor()->createSurface()) + , m_viewport(backend->display()->viewporter()->createViewport(*m_surface)) +{ +} + +WaylandCursor::~WaylandCursor() +{ +} + +KWayland::Client::Pointer *WaylandCursor::pointer() const +{ + return m_pointer; +} + +void WaylandCursor::setPointer(KWayland::Client::Pointer *pointer) +{ + if (m_pointer == pointer) { + return; + } + m_pointer = pointer; + if (m_pointer) { + m_pointer->setCursor(m_surface.get(), m_hotspot); + } +} + +void WaylandCursor::setEnabled(bool enable) +{ + if (m_enabled != enable) { + m_enabled = enable; + sync(); + } +} + +void WaylandCursor::update(wl_buffer *buffer, const QSize &logicalSize, const QPoint &hotspot) +{ + if (m_buffer != buffer || m_size != logicalSize || m_hotspot != hotspot) { + m_buffer = buffer; + m_size = logicalSize; + m_hotspot = hotspot; + + sync(); + } +} + +void WaylandCursor::sync() +{ + if (!m_enabled) { + m_surface->attachBuffer(KWayland::Client::Buffer::Ptr()); + m_surface->commit(KWayland::Client::Surface::CommitFlag::None); + } else { + m_viewport->setDestination(m_size); + m_surface->attachBuffer(m_buffer); + m_surface->damageBuffer(QRect(0, 0, INT32_MAX, INT32_MAX)); + m_surface->commit(KWayland::Client::Surface::CommitFlag::None); + } + + if (m_pointer) { + m_pointer->setCursor(m_surface.get(), m_hotspot); + } +} + +void WaylandOutput::handleFractionalScaleChanged(void *data, struct wp_fractional_scale_v1 *wp_fractional_scale_v1, uint32_t scale120) +{ + reinterpret_cast(data)->m_pendingScale = scale120 / 120.0; +} + +const wp_fractional_scale_v1_listener WaylandOutput::s_fractionalScaleListener{ + .preferred_scale = &WaylandOutput::handleFractionalScaleChanged, +}; + +WaylandOutput::WaylandOutput(const QString &name, WaylandBackend *backend) + : BackendOutput() + , m_renderLoop(std::make_unique(this)) + , m_surface(backend->display()->compositor()->createSurface()) + , m_xdgShellSurface(backend->display()->xdgShell()->createSurface(m_surface.get())) + , m_backend(backend) + , m_cursor(std::make_unique(backend)) +{ + m_renderLoop->setMaxPendingFrameCount(2); + if (KWayland::Client::XdgDecorationManager *manager = m_backend->display()->xdgDecorationManager()) { + m_xdgDecoration.reset(manager->getToplevelDecoration(m_xdgShellSurface.get())); + m_xdgDecoration->setMode(KWayland::Client::XdgDecoration::Mode::ServerSide); + } + Capabilities caps = Capability::Dpms; + if (backend->display()->tearingControl()) { + caps |= Capability::Tearing; + } + if (auto manager = backend->display()->colorManager()) { + const bool supportsMinFeatures = manager->supportsFeature(WP_COLOR_MANAGER_V1_FEATURE_PARAMETRIC) + && manager->supportsFeature(WP_COLOR_MANAGER_V1_FEATURE_SET_PRIMARIES) + && manager->supportsFeature(WP_COLOR_MANAGER_V1_FEATURE_SET_LUMINANCES) + && manager->supportsTransferFunction(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA22); + if (supportsMinFeatures) { + m_colorSurfaceFeedback = std::make_unique(wp_color_manager_v1_get_surface_feedback(manager->object(), *m_surface)); + connect(m_colorSurfaceFeedback.get(), &ColorSurfaceFeedback::preferredColorChanged, this, &WaylandOutput::updateColor); + } + } + if (auto manager = backend->display()->fractionalScale()) { + m_fractionalScale = wp_fractional_scale_manager_v1_get_fractional_scale(manager, *m_surface); + wp_fractional_scale_v1_add_listener(m_fractionalScale, &s_fractionalScaleListener, this); + } + m_viewport = backend->display()->viewporter()->createViewport(*m_surface); + setInformation(Information{ + .name = name, + .model = name, + .capabilities = caps, + }); + + m_configureThrottleTimer.setSingleShot(true); + connect(&m_configureThrottleTimer, &QTimer::timeout, this, [this]() { + applyConfigure(m_pendingConfigureSize, m_pendingConfigureSerial); + }); + + updateWindowTitle(); + if (auto toplevelIconManager = backend->display()->toplevelIconManager()) { + auto toplevelIcon = xdg_toplevel_icon_manager_v1_create_icon(toplevelIconManager); + xdg_toplevel_icon_v1_set_name(toplevelIcon, "kwin"); + xdg_toplevel_icon_manager_v1_set_icon(toplevelIconManager, *m_xdgShellSurface, toplevelIcon); + xdg_toplevel_icon_v1_destroy(toplevelIcon); + } + + connect(m_xdgShellSurface.get(), &XdgShellSurface::configureRequested, this, &WaylandOutput::handleConfigure); + connect(m_xdgShellSurface.get(), &XdgShellSurface::closeRequested, qApp, &QCoreApplication::quit); + connect(this, &WaylandOutput::enabledChanged, this, &WaylandOutput::updateWindowTitle); + connect(this, &WaylandOutput::dpmsModeChanged, this, &WaylandOutput::updateWindowTitle); +} + +WaylandOutput::~WaylandOutput() +{ + m_frames.clear(); + if (m_shortcutInhibition) { + zwp_keyboard_shortcuts_inhibitor_v1_destroy(m_shortcutInhibition); + } + m_viewport.reset(); + m_xdgDecoration.reset(); + m_xdgShellSurface.reset(); + m_surface.reset(); +} + +void WaylandOutput::updateColor() +{ + const auto &preferred = m_colorSurfaceFeedback->preferredColor(); + const auto tf = TransferFunction(TransferFunction::gamma22, preferred->transferFunction().minLuminance, preferred->transferFunction().maxLuminance); + State next = m_state; + next.colorDescription = std::make_shared(ColorDescription{ + preferred->containerColorimetry(), + tf, + preferred->referenceLuminance(), + preferred->minLuminance(), + preferred->maxAverageLuminance(), + preferred->maxHdrLuminance(), + }); + next.originalColorDescription = next.colorDescription; + next.blendingColor = next.colorDescription; + // we don't actually know this, but we have to assume *something* + next.layerBlendingColor = next.colorDescription; + setState(next); +} + +static void handleDiscarded(void *data, + struct wp_presentation_feedback *wp_presentation_feedback) +{ + reinterpret_cast(data)->frameDiscarded(); +} + +static void handlePresented(void *data, + struct wp_presentation_feedback *wp_presentation_feedback, + uint32_t tv_sec_hi, + uint32_t tv_sec_lo, + uint32_t tv_nsec, + uint32_t refresh, + uint32_t seq_hi, + uint32_t seq_lo, + uint32_t flags) +{ + const auto timestamp = std::chrono::seconds((uint64_t(tv_sec_hi) << 32) | tv_sec_lo) + std::chrono::nanoseconds(tv_nsec); + uint32_t refreshRate = 60'000; + if (refresh != 0) { + refreshRate = 1'000'000'000'000 / refresh; + } + reinterpret_cast(data)->framePresented(timestamp, refreshRate); +} + +static void handleSyncOutput(void *data, struct wp_presentation_feedback *, struct wl_output *) +{ + // intentionally ignored +} + +static constexpr struct wp_presentation_feedback_listener s_presentationListener{ + .sync_output = handleSyncOutput, + .presented = handlePresented, + .discarded = handleDiscarded, +}; + +void WaylandOutput::handleFrame(void *data, wl_callback *callback, uint32_t time) +{ + auto output = reinterpret_cast(data); + auto it = std::ranges::find_if(output->m_frames, [callback](const auto &frame) { + return frame.frameCallback == callback; + }); + if (it != output->m_frames.end()) { + // don't use the "time" argument for this, as it's in an unspecified base. + it->frameCallbackTime = std::chrono::steady_clock::now(); + } +} + +const wl_callback_listener WaylandOutput::s_frameCallbackListener{ + .done = &WaylandOutput::handleFrame, +}; + +bool WaylandOutput::testPresentation(const std::shared_ptr &frame) +{ + auto cursorLayers = Compositor::self()->backend()->compatibleOutputLayers(this) | std::views::filter([](OutputLayer *layer) { + return layer->type() == OutputLayerType::CursorOnly; + }); + if (m_hasPointerLock && std::ranges::any_of(cursorLayers, &OutputLayer::isEnabled)) { + return false; + } + return true; +} + +WaylandOutput::FrameData::FrameData(const std::shared_ptr &frame, struct wp_presentation_feedback *presentationFeedback, struct wl_callback *frameCallback) + : outputFrame(frame) + , presentationFeedback(presentationFeedback) + , frameCallback(frameCallback) +{ +} + +WaylandOutput::FrameData::FrameData(FrameData &&move) + : outputFrame(std::move(move.outputFrame)) + , presentationFeedback(std::exchange(move.presentationFeedback, nullptr)) + , frameCallback(std::exchange(move.frameCallback, nullptr)) + , frameCallbackTime(std::exchange(move.frameCallbackTime, std::nullopt)) +{ +} + +WaylandOutput::FrameData::~FrameData() +{ + if (presentationFeedback) { + wp_presentation_feedback_destroy(presentationFeedback); + } + if (frameCallback) { + wl_callback_destroy(frameCallback); + } +} + +bool WaylandOutput::present(const QList &layersToUpdate, const std::shared_ptr &frame) +{ + auto cursorLayers = layersToUpdate | std::views::filter([](OutputLayer *layer) { + return layer->type() == OutputLayerType::CursorOnly; + }); + if (!cursorLayers.empty()) { + if (m_hasPointerLock && cursorLayers.front()->isEnabled()) { + return false; + } + m_cursor->setEnabled(cursorLayers.front()->isEnabled()); + // TODO also move the actual cursor image update here too... + } + if (!m_mapped) { + // we only ever want a black background + auto buffer = wp_single_pixel_buffer_manager_v1_create_u32_rgba_buffer(m_backend->display()->singlePixelManager(), 0, 0, 0, 0xFFFFFFFF); + m_surface->attachBuffer(buffer); + m_mapped = true; + } + m_viewport->setDestination(QSize(std::round(modeSize().width() / scale()), std::round(modeSize().height() / scale()))); + m_surface->setScale(1); + // commit the subsurfaces before the main surface + for (OutputLayer *layer : layersToUpdate) { + // TODO maybe also make the cursor a WaylandLayer? + if (layer->type() != OutputLayerType::CursorOnly) { + static_cast(layer)->commit(frame->presentationMode()); + } + } + if (m_backend->display()->tearingControl()) { + m_renderLoop->setPresentationMode(frame->presentationMode()); + } + FrameData frameData{ + frame, + wp_presentation_feedback(m_backend->display()->presentationTime(), *m_surface), + wl_surface_frame(*m_surface), + }; + wp_presentation_feedback_add_listener(frameData.presentationFeedback, &s_presentationListener, this); + wl_callback_add_listener(frameData.frameCallback, &s_frameCallbackListener, this); + m_surface->commit(KWayland::Client::Surface::CommitFlag::None); + m_frames.push_back(std::move(frameData)); + return true; +} + +void WaylandOutput::frameDiscarded() +{ + m_frames.pop_front(); +} + +void WaylandOutput::framePresented(std::chrono::nanoseconds timestamp, uint32_t refreshRate) +{ + if (refreshRate != this->refreshRate()) { + m_refreshRate = refreshRate; + const auto mode = std::make_shared(pixelSize(), m_refreshRate); + State next = m_state; + next.modes = {mode}; + next.currentMode = mode; + setState(next); + m_renderLoop->setRefreshRate(m_refreshRate); + } + const auto &frame = m_frames.front(); + if (auto t = frame.frameCallbackTime) { + // NOTE that the frame callback gets signaled *after* the host compositor + // is done compositing the frame on the CPU side, not before! + // This is the best estimate we currently have for the commit deadline, but + // it should be replaced with something more accurate when possible. + const auto difference = timestamp - t->time_since_epoch(); + m_renderLoop->setPresentationSafetyMargin(difference + std::chrono::milliseconds(1)); + } + frame.outputFrame->presented(timestamp, PresentationMode::VSync); + m_frames.pop_front(); +} + +void WaylandOutput::applyChanges(const OutputConfiguration &config) +{ + const auto props = config.constChangeSet(this); + if (!props) { + return; + } + State next = m_state; + next.enabled = props->enabled.value_or(m_state.enabled); + next.transform = props->transform.value_or(m_state.transform); + next.position = props->pos.value_or(m_state.position); + // intentionally ignored, as it would get overwritten + // with the fractional scale protocol anyways + // next.scale = props->scale.value_or(m_state.scale); + next.desiredModeSize = props->desiredModeSize.value_or(m_state.desiredModeSize); + next.desiredModeRefreshRate = props->desiredModeRefreshRate.value_or(m_state.desiredModeRefreshRate); + next.desiredModeFlags = props->desiredModeFlags.value_or(m_state.desiredModeFlags); + next.uuid = props->uuid.value_or(m_state.uuid); + next.replicationSource = props->replicationSource.value_or(m_state.replicationSource); + next.dpmsMode = props->dpmsMode.value_or(m_state.dpmsMode); + if (next.dpmsMode != m_state.dpmsMode) { + if (next.dpmsMode == DpmsMode::On) { + m_renderLoop->uninhibit(); + } else { + m_renderLoop->inhibit(); + } + } + next.priority = props->priority.value_or(m_state.priority); + setState(next); +} + +bool WaylandOutput::isReady() const +{ + return m_ready; +} + +KWayland::Client::Surface *WaylandOutput::surface() const +{ + return m_surface.get(); +} + +WaylandCursor *WaylandOutput::cursor() const +{ + return m_cursor.get(); +} + +WaylandBackend *WaylandOutput::backend() const +{ + return m_backend; +} + +RenderLoop *WaylandOutput::renderLoop() const +{ + return m_renderLoop.get(); +} + +bool WaylandOutput::presentAsync(OutputLayer *layer, std::optional allowedVrrDelay) +{ + // the host compositor moves the cursor, there's nothing to do + return layer->type() == OutputLayerType::CursorOnly; +} + +void WaylandOutput::init(const QSize &pixelSize, qreal scale, bool fullscreen) +{ + m_renderLoop->setRefreshRate(m_refreshRate); + + auto mode = std::make_shared(pixelSize, m_refreshRate); + + State initialState; + initialState.modes = {mode}; + initialState.currentMode = mode; + initialState.scale = scale; + setState(initialState); + + m_xdgShellSurface->setFullscreen(fullscreen); + m_surface->commit(KWayland::Client::Surface::CommitFlag::None); +} + +void WaylandOutput::handleConfigure(const QSize &size, XdgShellSurface::States states, quint32 serial) +{ + if (!m_ready) { + m_ready = true; + + applyConfigure(size, serial); + } else { + // LogicalOutput resizing is a resource intensive task, so the configure events are throttled. + m_pendingConfigureSerial = serial; + m_pendingConfigureSize = size; + + if (!m_configureThrottleTimer.isActive()) { + m_configureThrottleTimer.start(1000000 / m_state.currentMode->refreshRate()); + } + } +} + +void WaylandOutput::applyConfigure(const QSize &size, quint32 serial) +{ + m_xdgShellSurface->ackConfigure(serial); + if (!size.isEmpty()) { + auto mode = std::make_shared(size * m_pendingScale, m_refreshRate); + + State next = m_state; + next.modes = {mode}; + next.currentMode = mode; + next.scale = m_pendingScale; + setState(next); + + Q_EMIT m_backend->outputsQueried(); + } +} + +void WaylandOutput::updateWindowTitle() +{ + QString grab; + if (m_hasPointerLock) { + grab = i18n("Press right control to ungrab pointer"); + } else if (m_backend->display()->pointerConstraints()) { + grab = i18n("Press right control key to grab pointer"); + } + + QString title = i18nc("Title of nested KWin Wayland with Wayland socket identifier as argument", + "KDE Wayland Compositor %1", name()); + + if (!isEnabled()) { + title += i18n("- Output disabled"); + } else if (dpmsMode() != DpmsMode::On) { + title += i18n("- Output dimmed"); + } else if (!grab.isEmpty()) { + title += QStringLiteral(" — ") + grab; + } + m_xdgShellSurface->setTitle(title); +} + +void WaylandOutput::lockPointer(Pointer *pointer, bool lock) +{ + if (!lock) { + const bool surfaceWasLocked = m_pointerLock && m_hasPointerLock; + m_pointerLock.reset(); + m_hasPointerLock = false; + if (surfaceWasLocked) { + inhibitShortcuts(false); + updateWindowTitle(); + Q_EMIT m_backend->pointerLockChanged(false); + } + return; + } + + Q_ASSERT(!m_pointerLock); + m_pointerLock.reset(m_backend->display()->pointerConstraints()->lockPointer(surface(), pointer, nullptr, PointerConstraints::LifeTime::OneShot)); + if (!m_pointerLock->isValid()) { + m_pointerLock.reset(); + return; + } + connect(m_pointerLock.get(), &LockedPointer::locked, this, [this]() { + m_hasPointerLock = true; + inhibitShortcuts(true); + updateWindowTitle(); + Q_EMIT m_backend->pointerLockChanged(true); + }); + connect(m_pointerLock.get(), &LockedPointer::unlocked, this, [this]() { + m_pointerLock.reset(); + inhibitShortcuts(false); + m_hasPointerLock = false; + updateWindowTitle(); + Q_EMIT m_backend->pointerLockChanged(false); + }); +} + +void WaylandOutput::inhibitShortcuts(bool inhibit) +{ + if (!inhibit) { + if (m_shortcutInhibition) { + zwp_keyboard_shortcuts_inhibitor_v1_destroy(m_shortcutInhibition); + m_shortcutInhibition = nullptr; + } + return; + } + + auto *inhibitionManager = m_backend->display()->keyboardShortcutsInhibitManager(); + if (!inhibitionManager) { + return; + } + + Q_ASSERT(!m_shortcutInhibition); + m_shortcutInhibition = zwp_keyboard_shortcuts_inhibit_manager_v1_inhibit_shortcuts(inhibitionManager, *m_surface, *(m_backend->display()->seat())); +} + +void WaylandOutput::setOutputLayers(std::vector> &&layers) +{ + m_layers = std::move(layers); +} + +QList WaylandOutput::outputLayers() const +{ + return m_layers | std::views::transform(&std::unique_ptr::get) | std::ranges::to(); +} +} +} + +#include "moc_wayland_output.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/wayland/wayland_output.h b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_output.h new file mode 100644 index 0000000000..7c30ce382b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_output.h @@ -0,0 +1,154 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "core/backendoutput.h" + +#include +#include +#include +#include +#include + +namespace KWayland +{ +namespace Client +{ +class Surface; +class Pointer; +class LockedPointer; +class XdgDecoration; +} +} + +struct wl_buffer; +struct wp_presentation_feedback; +struct wp_tearing_control_v1; +struct wp_color_management_surface_v1; +struct wp_fractional_scale_v1; +struct wp_fractional_scale_v1_listener; +struct zwp_keyboard_shortcuts_inhibitor_v1; +struct wl_callback; +struct wl_callback_listener; + +namespace KWin +{ +class OutputFrame; +namespace WaylandClient +{ +class Viewport; +} + +namespace Wayland +{ +class WaylandBackend; +class ColorSurfaceFeedback; + +class WaylandCursor +{ +public: + explicit WaylandCursor(WaylandBackend *backend); + ~WaylandCursor(); + + KWayland::Client::Pointer *pointer() const; + void setPointer(KWayland::Client::Pointer *pointer); + + void setEnabled(bool enable); + void update(wl_buffer *buffer, const QSize &logicalSize, const QPoint &hotspot); + +private: + void sync(); + + KWayland::Client::Pointer *m_pointer = nullptr; + std::unique_ptr m_surface; + wl_buffer *m_buffer = nullptr; + std::unique_ptr m_viewport; + QPoint m_hotspot; + QSize m_size; + bool m_enabled = true; +}; + +class WaylandOutput : public BackendOutput +{ + Q_OBJECT +public: + WaylandOutput(const QString &name, WaylandBackend *backend); + ~WaylandOutput() override; + + RenderLoop *renderLoop() const override; + bool presentAsync(OutputLayer *layer, std::optional allowedVrrDelay) override; + + void init(const QSize &pixelSize, qreal scale, bool fullscreen); + + bool isReady() const; + KWayland::Client::Surface *surface() const; + WaylandCursor *cursor() const; + WaylandBackend *backend() const; + + void lockPointer(KWayland::Client::Pointer *pointer, bool lock); + + bool testPresentation(const std::shared_ptr &frame) override; + bool present(const QList &layersToUpdate, const std::shared_ptr &frame) override; + + void frameDiscarded(); + void framePresented(std::chrono::nanoseconds timestamp, uint32_t refreshRate); + + void applyChanges(const OutputConfiguration &config) override; + + void setOutputLayers(std::vector> &&layers); + QList outputLayers() const; + +private: + void handleConfigure(const QSize &size, KWayland::Client::XdgShellSurface::States states, quint32 serial); + void updateWindowTitle(); + void applyConfigure(const QSize &size, quint32 serial); + void updateColor(); + void inhibitShortcuts(bool inhibit); + + static const wp_fractional_scale_v1_listener s_fractionalScaleListener; + static void handleFractionalScaleChanged(void *data, struct wp_fractional_scale_v1 *wp_fractional_scale_v1, uint32_t scale120); + static const wl_callback_listener s_frameCallbackListener; + static void handleFrame(void *data, wl_callback *callback, uint32_t time); + + std::vector> m_layers; + std::unique_ptr m_renderLoop; + std::unique_ptr m_surface; + std::unique_ptr m_xdgShellSurface; + std::unique_ptr m_pointerLock; + std::unique_ptr m_xdgDecoration; + WaylandBackend *const m_backend; + std::unique_ptr m_cursor; + bool m_hasPointerLock = false; + bool m_ready = false; + bool m_mapped = false; + struct FrameData + { + explicit FrameData(const std::shared_ptr &frame, struct wp_presentation_feedback *presentationFeedback, struct wl_callback *frameCallback); + FrameData(FrameData &&move); + ~FrameData(); + + std::shared_ptr outputFrame; + wp_presentation_feedback *presentationFeedback; + wl_callback *frameCallback; + std::optional frameCallbackTime; + }; + std::deque m_frames; + quint32 m_pendingConfigureSerial = 0; + QSize m_pendingConfigureSize; + QTimer m_configureThrottleTimer; + std::unique_ptr m_colorSurfaceFeedback; + wp_fractional_scale_v1 *m_fractionalScale = nullptr; + std::unique_ptr m_viewport; + zwp_keyboard_shortcuts_inhibitor_v1 *m_shortcutInhibition = nullptr; + uint32_t m_refreshRate = 60'000; + qreal m_pendingScale = 1.0; +}; + +} // namespace Wayland +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/backends/wayland/wayland_qpainter_backend.cpp b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_qpainter_backend.cpp new file mode 100644 index 0000000000..b433dad5c9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_qpainter_backend.cpp @@ -0,0 +1,190 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + SPDX-FileCopyrightText: 2013, 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "wayland_qpainter_backend.h" +#include "core/graphicsbufferview.h" +#include "core/shmgraphicsbufferallocator.h" +#include "qpainter/qpainterswapchain.h" +#include "wayland_backend.h" +#include "wayland_output.h" + +#include + +#include +#include +#include + +namespace KWin +{ +namespace Wayland +{ + +WaylandQPainterPrimaryLayer::WaylandQPainterPrimaryLayer(WaylandOutput *output, WaylandQPainterBackend *backend) + : WaylandLayer(output, OutputLayerType::Primary, 0) + , m_waylandOutput(output) + , m_backend(backend) +{ +} + +WaylandQPainterPrimaryLayer::~WaylandQPainterPrimaryLayer() +{ +} + +Region WaylandQPainterPrimaryLayer::accumulateDamage(int bufferAge) const +{ + return m_damageJournal.accumulate(bufferAge, Region::infinite()); +} + +std::optional WaylandQPainterPrimaryLayer::doBeginFrame() +{ + const QSize nativeSize = targetRect().size(); + if (!m_swapchain || m_swapchain->size() != nativeSize) { + m_swapchain = std::make_unique(m_backend->graphicsBufferAllocator(), nativeSize, DRM_FORMAT_XRGB8888); + } + + m_back = m_swapchain->acquire(); + if (!m_back) { + return std::nullopt; + } + + m_renderTime = std::make_unique(); + return OutputLayerBeginFrameInfo{ + .renderTarget = RenderTarget(m_back->view()->image(), m_color), + .repaint = accumulateDamage(m_back->age()), + }; +} + +bool WaylandQPainterPrimaryLayer::doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) +{ + m_renderTime->end(); + frame->addRenderTimeQuery(std::move(m_renderTime)); + m_damageJournal.add(damagedDeviceRegion); + setBuffer(m_waylandOutput->backend()->importBuffer(m_back->buffer()), damagedDeviceRegion); + m_swapchain->release(m_back); + return true; +} + +DrmDevice *WaylandQPainterPrimaryLayer::scanoutDevice() const +{ + return m_backend->drmDevice(); +} + +QHash> WaylandQPainterPrimaryLayer::supportedDrmFormats() const +{ + return {{DRM_FORMAT_ARGB8888, {DRM_FORMAT_MOD_LINEAR}}}; +} + +void WaylandQPainterPrimaryLayer::releaseBuffers() +{ + m_back.reset(); + m_swapchain.reset(); +} + +WaylandQPainterCursorLayer::WaylandQPainterCursorLayer(WaylandOutput *output, WaylandQPainterBackend *backend) + : OutputLayer(output, OutputLayerType::CursorOnly) + , m_backend(backend) +{ +} + +WaylandQPainterCursorLayer::~WaylandQPainterCursorLayer() +{ +} + +std::optional WaylandQPainterCursorLayer::doBeginFrame() +{ + const auto bufferSize = targetRect().size(); + if (!m_swapchain || m_swapchain->size() != bufferSize) { + m_swapchain = std::make_unique(m_backend->graphicsBufferAllocator(), bufferSize, DRM_FORMAT_ARGB8888); + } + + m_back = m_swapchain->acquire(); + if (!m_back) { + return std::nullopt; + } + + m_renderTime = std::make_unique(); + return OutputLayerBeginFrameInfo{ + .renderTarget = RenderTarget(m_back->view()->image()), + .repaint = Region::infinite(), + }; +} + +bool WaylandQPainterCursorLayer::doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) +{ + m_renderTime->end(); + if (frame) { + frame->addRenderTimeQuery(std::move(m_renderTime)); + } + wl_buffer *buffer = static_cast(m_output.get())->backend()->importBuffer(m_back->buffer()); + Q_ASSERT(buffer); + + static_cast(m_output.get())->cursor()->update(buffer, m_back->buffer()->size() / m_output->scale(), hotspot().toPoint()); + m_swapchain->release(m_back); + return true; +} + +DrmDevice *WaylandQPainterCursorLayer::scanoutDevice() const +{ + return m_backend->drmDevice(); +} + +QHash> WaylandQPainterCursorLayer::supportedDrmFormats() const +{ + return {{DRM_FORMAT_ARGB8888, {DRM_FORMAT_MOD_LINEAR}}}; +} + +void WaylandQPainterCursorLayer::releaseBuffers() +{ + m_back.reset(); + m_swapchain.reset(); +} + +WaylandQPainterBackend::WaylandQPainterBackend(Wayland::WaylandBackend *b) + : QPainterBackend() + , m_backend(b) + , m_allocator(std::make_unique()) +{ + const auto waylandOutputs = m_backend->waylandOutputs(); + for (auto *output : waylandOutputs) { + createOutput(output); + } + connect(m_backend, &WaylandBackend::outputAdded, this, &WaylandQPainterBackend::createOutput); +} + +WaylandQPainterBackend::~WaylandQPainterBackend() +{ + const auto waylandOutputs = m_backend->waylandOutputs(); + for (auto *output : waylandOutputs) { + output->setOutputLayers({}); + } +} + +void WaylandQPainterBackend::createOutput(BackendOutput *output) +{ + const auto waylandOutput = static_cast(output); + std::vector> layers; + layers.push_back(std::make_unique(waylandOutput, this)); + layers.push_back(std::make_unique(waylandOutput, this)); + waylandOutput->setOutputLayers(std::move(layers)); +} + +GraphicsBufferAllocator *WaylandQPainterBackend::graphicsBufferAllocator() const +{ + return m_allocator.get(); +} + +QList WaylandQPainterBackend::compatibleOutputLayers(BackendOutput *output) +{ + return static_cast(output)->outputLayers(); +} + +} +} + +#include "moc_wayland_qpainter_backend.cpp" diff --git a/local/recipes/kde/kwin/source/src/backends/wayland/wayland_qpainter_backend.h b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_qpainter_backend.h new file mode 100644 index 0000000000..7433b58630 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/wayland/wayland_qpainter_backend.h @@ -0,0 +1,99 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + SPDX-FileCopyrightText: 2013, 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "qpainter/qpainterbackend.h" +#include "utils/damagejournal.h" +#include "wayland_layer.h" + +#include +#include +#include + +namespace KWin +{ +class BackendOutput; +class GraphicsBufferAllocator; +class QPainterSwapchainSlot; +class QPainterSwapchain; + +namespace Wayland +{ +class WaylandBackend; +class WaylandDisplay; +class WaylandOutput; +class WaylandQPainterBackend; + +class WaylandQPainterPrimaryLayer : public WaylandLayer +{ +public: + WaylandQPainterPrimaryLayer(WaylandOutput *output, WaylandQPainterBackend *backend); + ~WaylandQPainterPrimaryLayer() override; + + std::optional doBeginFrame() override; + bool doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) override; + DrmDevice *scanoutDevice() const override; + QHash> supportedDrmFormats() const override; + void releaseBuffers() override; + + Region accumulateDamage(int bufferAge) const; + +private: + WaylandOutput *m_waylandOutput; + WaylandQPainterBackend *m_backend; + DamageJournal m_damageJournal; + + std::unique_ptr m_swapchain; + std::shared_ptr m_back; + std::unique_ptr m_renderTime; + + friend class WaylandQPainterBackend; +}; + +class WaylandQPainterCursorLayer : public OutputLayer +{ + Q_OBJECT + +public: + WaylandQPainterCursorLayer(WaylandOutput *output, WaylandQPainterBackend *backend); + ~WaylandQPainterCursorLayer() override; + + std::optional doBeginFrame() override; + bool doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) override; + DrmDevice *scanoutDevice() const override; + QHash> supportedDrmFormats() const override; + void releaseBuffers() override; + +private: + WaylandQPainterBackend *m_backend; + std::unique_ptr m_swapchain; + std::shared_ptr m_back; + std::unique_ptr m_renderTime; +}; + +class WaylandQPainterBackend : public QPainterBackend +{ + Q_OBJECT +public: + explicit WaylandQPainterBackend(WaylandBackend *b); + ~WaylandQPainterBackend() override; + + GraphicsBufferAllocator *graphicsBufferAllocator() const; + QList compatibleOutputLayers(BackendOutput *output) override; + +private: + void createOutput(BackendOutput *waylandOutput); + + WaylandBackend *m_backend; + std::unique_ptr m_allocator; +}; + +} // namespace Wayland +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/backends/x11/CMakeLists.txt b/local/recipes/kde/kwin/source/src/backends/x11/CMakeLists.txt new file mode 100644 index 0000000000..4dca1d274a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/backends/x11/CMakeLists.txt @@ -0,0 +1,17 @@ +target_sources(kwin PRIVATE + x11_windowed_backend.cpp + x11_windowed_egl_backend.cpp + x11_windowed_logging.cpp + x11_windowed_output.cpp + x11_windowed_qpainter_backend.cpp +) + +target_link_libraries(kwin +PRIVATE + XCB::XCB + XCB::DRI3 + XCB::PRESENT +) +if (TARGET XCB::XINPUT) + target_link_libraries(kwin PRIVATE XCB::XINPUT) +endif() diff --git a/local/recipes/kde/kwin/source/src/client_machine.cpp b/local/recipes/kde/kwin/source/src/client_machine.cpp new file mode 100644 index 0000000000..e75b659d08 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/client_machine.cpp @@ -0,0 +1,243 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +// own +#include "client_machine.h" +#include "main.h" +#include "utils/common.h" + +#if KWIN_BUILD_X11 +#include "effect/xcb.h" +#endif + +// KF5 +#if KWIN_BUILD_X11 +#include +#endif +// Qt +#include +#include +// system +#include +#include +#include +#include + +namespace KWin +{ + +static QString getHostName() +{ +#ifdef HOST_NAME_MAX + char hostnamebuf[HOST_NAME_MAX]; +#else + char hostnamebuf[256]; +#endif + if (gethostname(hostnamebuf, sizeof hostnamebuf) >= 0) { + hostnamebuf[sizeof(hostnamebuf) - 1] = 0; + return QString::fromLocal8Bit(hostnamebuf); + } + return QString(); +} + +GetAddrInfo::GetAddrInfo(const QString &hostName, QObject *parent) + : QObject(parent) + , m_resolving(false) + , m_resolved(false) + , m_ownResolved(false) + , m_hostName(hostName) + , m_addressHints(std::make_unique()) + , m_address(nullptr) + , m_ownAddress(nullptr) + , m_watcher(std::make_unique>()) + , m_ownAddressWatcher(std::make_unique>()) +{ + // watcher will be deleted together with the GetAddrInfo once the future + // got canceled or finished + connect(m_watcher.get(), &QFutureWatcher::canceled, this, &GetAddrInfo::deleteLater); + connect(m_watcher.get(), &QFutureWatcher::finished, this, &GetAddrInfo::slotResolved); + connect(m_ownAddressWatcher.get(), &QFutureWatcher::canceled, this, &GetAddrInfo::deleteLater); + connect(m_ownAddressWatcher.get(), &QFutureWatcher::finished, this, &GetAddrInfo::slotOwnAddressResolved); +} + +GetAddrInfo::~GetAddrInfo() +{ + if (m_watcher && m_watcher->isRunning()) { + m_watcher->cancel(); + m_watcher->waitForFinished(); + } + if (m_ownAddressWatcher && m_ownAddressWatcher->isRunning()) { + m_ownAddressWatcher->cancel(); + m_ownAddressWatcher->waitForFinished(); + } + if (m_address) { + freeaddrinfo(m_address); + } + if (m_ownAddress) { + freeaddrinfo(m_ownAddress); + } +} + +void GetAddrInfo::resolve() +{ + if (m_resolving) { + return; + } + m_resolving = true; + *m_addressHints = {}; + m_addressHints->ai_family = PF_UNSPEC; + m_addressHints->ai_socktype = SOCK_STREAM; + m_addressHints->ai_flags |= AI_CANONNAME; + + m_watcher->setFuture(QtConcurrent::run([this]() { + return getaddrinfo(m_hostName.toLocal8Bit().constData(), nullptr, m_addressHints.get(), &m_address); + })); + m_ownAddressWatcher->setFuture(QtConcurrent::run([this] { + // needs to be performed in a lambda as getHostName() returns a temporary value which would + // get destroyed in the main thread before the getaddrinfo thread is able to read it + return getaddrinfo(getHostName().toLocal8Bit().constData(), nullptr, m_addressHints.get(), &m_ownAddress); + })); +} + +void GetAddrInfo::slotResolved() +{ + if (resolved(m_watcher.get())) { + m_resolved = true; + compare(); + } +} + +void GetAddrInfo::slotOwnAddressResolved() +{ + if (resolved(m_ownAddressWatcher.get())) { + m_ownResolved = true; + compare(); + } +} + +bool GetAddrInfo::resolved(QFutureWatcher *watcher) +{ + if (!watcher->isFinished()) { + return false; + } + if (watcher->result() != 0) { + qCDebug(KWIN_CORE) << "getaddrinfo failed with error:" << gai_strerror(watcher->result()); + // call failed; + deleteLater(); + return false; + } + return true; +} + +void GetAddrInfo::compare() +{ + if (!m_resolved || !m_ownResolved) { + return; + } + addrinfo *address = m_address; + while (address) { + if (address->ai_canonname && m_hostName == QByteArray(address->ai_canonname).toLower()) { + addrinfo *ownAddress = m_ownAddress; + bool localFound = false; + while (ownAddress) { + if (ownAddress->ai_canonname && QByteArray(ownAddress->ai_canonname).toLower() == m_hostName) { + localFound = true; + break; + } + ownAddress = ownAddress->ai_next; + } + if (localFound) { + Q_EMIT local(); + break; + } + } + address = address->ai_next; + } + deleteLater(); +} + +ClientMachine::ClientMachine(QObject *parent) + : QObject(parent) + , m_localhost(false) + , m_resolved(false) + , m_resolving(false) +{ +} + +ClientMachine::~ClientMachine() +{ +} + +#if KWIN_BUILD_X11 +void ClientMachine::resolve(xcb_window_t window, xcb_window_t clientLeader) +{ + if (m_resolved) { + return; + } + QString name = NETWinInfo(connection(), window, rootWindow(), NET::Properties(), NET::WM2ClientMachine).clientMachine(); + if (name.isEmpty() && clientLeader && clientLeader != window) { + name = NETWinInfo(connection(), clientLeader, rootWindow(), NET::Properties(), NET::WM2ClientMachine).clientMachine(); + } + if (name.isEmpty()) { + name = localhost(); + } + if (name == localhost()) { + setLocal(); + } + m_hostName = name; + checkForLocalhost(); + m_resolved = true; +} +#endif + +void ClientMachine::checkForLocalhost() +{ + if (isLocal()) { + // nothing to do + return; + } + QString host = getHostName(); + + if (!host.isEmpty()) { + host = host.toLower(); + const QString lowerHostName(m_hostName.toLower()); + if (host == lowerHostName) { + setLocal(); + return; + } + if (int index = host.indexOf('.'); index != -1) { + if (QStringView(host).left(index) == lowerHostName) { + setLocal(); + return; + } + } else { + m_resolving = true; + // check using information from get addr info + // GetAddrInfo gets automatically destroyed once it finished or not + GetAddrInfo *info = new GetAddrInfo(lowerHostName, this); + connect(info, &GetAddrInfo::local, this, &ClientMachine::setLocal); + connect(info, &GetAddrInfo::destroyed, this, &ClientMachine::resolveFinished); + info->resolve(); + } + } +} + +void ClientMachine::setLocal() +{ + m_localhost = true; + Q_EMIT localhostChanged(); +} + +void ClientMachine::resolveFinished() +{ + m_resolving = false; +} + +} // namespace + +#include "moc_client_machine.cpp" diff --git a/local/recipes/kde/kwin/source/src/client_machine.h b/local/recipes/kde/kwin/source/src/client_machine.h new file mode 100644 index 0000000000..a91dd7152d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/client_machine.h @@ -0,0 +1,107 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "config-kwin.h" + +#include +#include +#if KWIN_BUILD_X11 +#include +#endif + +// forward declaration +struct addrinfo; +template +class QFutureWatcher; + +namespace KWin +{ + +class GetAddrInfo : public QObject +{ + Q_OBJECT +public: + explicit GetAddrInfo(const QString &hostName, QObject *parent = nullptr); + ~GetAddrInfo() override; + + void resolve(); + +Q_SIGNALS: + void local(); + +private Q_SLOTS: + void slotResolved(); + void slotOwnAddressResolved(); + +private: + void compare(); + bool resolved(QFutureWatcher *watcher); + bool m_resolving; + bool m_resolved; + bool m_ownResolved; + QString m_hostName; + std::unique_ptr m_addressHints; + addrinfo *m_address; + addrinfo *m_ownAddress; + std::unique_ptr> m_watcher; + std::unique_ptr> m_ownAddressWatcher; +}; + +class ClientMachine : public QObject +{ + Q_OBJECT +public: + explicit ClientMachine(QObject *parent = nullptr); + ~ClientMachine() override; + +#if KWIN_BUILD_X11 + void resolve(xcb_window_t window, xcb_window_t clientLeader); +#endif + const QString &hostName() const; + bool isLocal() const; + static QString localhost(); + bool isResolving() const; + +Q_SIGNALS: + void localhostChanged(); + +private Q_SLOTS: + void setLocal(); + void resolveFinished(); + +private: + void checkForLocalhost(); + QString m_hostName; + bool m_localhost; + bool m_resolved; + bool m_resolving; +}; + +inline bool ClientMachine::isLocal() const +{ + return m_localhost; +} + +inline const QString &ClientMachine::hostName() const +{ + return m_hostName; +} + +inline QString ClientMachine::localhost() +{ + return QStringLiteral("localhost"); +} + +inline bool ClientMachine::isResolving() const +{ + return m_resolving; +} + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/compositor.cpp b/local/recipes/kde/kwin/source/src/compositor.cpp new file mode 100644 index 0000000000..1643031490 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/compositor.cpp @@ -0,0 +1,1042 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "compositor.h" + +#include "config-kwin.h" + +#include "core/backendoutput.h" +#include "core/brightnessdevice.h" +#include "core/drmdevice.h" +#include "core/graphicsbufferview.h" +#include "core/outputbackend.h" +#include "core/outputlayer.h" +#include "core/renderbackend.h" +#include "core/renderloop.h" +#include "cursor.h" +#include "cursorsource.h" +#include "dbusinterface.h" +#include "effect/effecthandler.h" +#include "ftrace.h" +#include "opengl/eglbackend.h" +#include "opengl/glplatform.h" +#include "qpainter/qpainterbackend.h" +#include "renderloopdrivenqanimationdriver.h" +#include "scene/cursoritem.h" +#include "scene/itemrenderer_opengl.h" +#include "scene/itemrenderer_qpainter.h" +#include "scene/surfaceitem.h" +#include "scene/surfaceitem_wayland.h" +#include "scene/workspacescene.h" +#include "utils/common.h" +#include "utils/envvar.h" +#include "wayland/surface.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include "utils/drm_format_helper.h" + +#include +#if KWIN_BUILD_NOTIFICATIONS +#include +#include +#endif + +#include +#include +#include + +namespace KWin +{ + +Compositor *Compositor::create(QObject *parent) +{ + Q_ASSERT(!s_compositor); + auto *compositor = new Compositor(parent); + s_compositor = compositor; + return compositor; +} + +Compositor *Compositor::s_compositor = nullptr; +Compositor *Compositor::self() +{ + return s_compositor; +} + +Compositor::Compositor(QObject *workspace) + : QObject(workspace) + , m_allowOverlaysEnv(environmentVariableBoolValue("KWIN_USE_OVERLAYS")) + , m_renderLoopDrivenAnimationDriver(new RenderLoopDrivenQAnimationDriver(this)) +{ + // register DBus + new CompositorDBusInterface(this); + + m_renderLoopDrivenAnimationDriver->install(); + connect(m_renderLoopDrivenAnimationDriver, &RenderLoopDrivenQAnimationDriver::started, this, [this]() { + // foreach output, schedule repaint on render loop + for (const auto &it : m_primaryViews) { + RenderLoop *loop = it.first; + loop->scheduleRepaint(); + } + }); + + FTraceLogger::create(); +} + +Compositor::~Compositor() +{ + Q_EMIT aboutToDestroy(); + stop(); // this can't be called in the destructor of Compositor + s_compositor = nullptr; +} + +BackendOutput *Compositor::findOutput(RenderLoop *loop) const +{ + const auto outputs = kwinApp()->outputBackend()->outputs(); + for (BackendOutput *output : outputs) { + if (output->renderLoop() == loop) { + return output; + } + } + return nullptr; +} + +void Compositor::reinitialize() +{ + // Restart compositing + stop(); + start(); +} + +void Compositor::handleFrameRequested(RenderLoop *renderLoop) +{ + composite(renderLoop); +} + +bool Compositor::isActive() +{ + return m_state == State::On; +} + +static QVariantHash collectCrashInformation(const EglBackend *backend) +{ + const GLPlatform *glPlatform = backend->openglContext()->glPlatform(); + + QVariantHash gpuInformation; + gpuInformation[QStringLiteral("api_type")] = QStringLiteral("OpenGL"); + gpuInformation[QStringLiteral("name")] = QString::fromUtf8(glPlatform->glRendererString()); + if (const auto pciInfo = backend->drmDevice()->pciDeviceInfo()) { + gpuInformation[QStringLiteral("id")] = QString::number(pciInfo->device_id, 16); + gpuInformation[QStringLiteral("vendor_id")] = QString::number(pciInfo->vendor_id, 16); + } + if (glPlatform->driverVersion().isValid()) { + gpuInformation[QStringLiteral("version")] = glPlatform->driverVersion().toString(); + } + + return gpuInformation; +} + +bool Compositor::attemptOpenGLCompositing() +{ + std::unique_ptr backend = kwinApp()->outputBackend()->createOpenGLBackend(); + if (!backend) { + return false; + } + if (!backend->isFailed()) { + backend->init(); + } + if (backend->isFailed()) { + return false; + } + + KCrash::setGPUData(collectCrashInformation(backend.get())); + + const QByteArray forceEnv = qgetenv("KWIN_COMPOSE"); + if (!forceEnv.isEmpty()) { + if (qstrcmp(forceEnv, "O2") == 0 || qstrcmp(forceEnv, "O2ES") == 0) { + qCDebug(KWIN_CORE) << "OpenGL 2 compositing enforced by environment variable"; + } else { + // OpenGL 2 disabled by environment variable + return false; + } + } else { + if (backend->openglContext()->glPlatform()->recommendedCompositor() < OpenGLCompositing) { + qCDebug(KWIN_CORE) << "Driver does not recommend OpenGL compositing"; + return false; + } + } + + // We only support the OpenGL 2+ shader API, not GL_ARB_shader_objects + if (!backend->openglContext()->hasVersion(Version(2, 0))) { + qCDebug(KWIN_CORE) << "OpenGL 2.0 is not supported"; + return false; + } + m_backend = std::move(backend); + qCDebug(KWIN_CORE) << "OpenGL compositing has been successfully initialized"; + return true; +} + +bool Compositor::attemptQPainterCompositing() +{ + std::unique_ptr backend(kwinApp()->outputBackend()->createQPainterBackend()); + if (!backend || backend->isFailed()) { + return false; + } + m_backend = std::move(backend); + qCDebug(KWIN_CORE) << "QPainter compositing has been successfully initialized"; + return true; +} + +void Compositor::createRenderer() +{ + // If compositing has been restarted, try to use the last used compositing type. + const QList availableCompositors = kwinApp()->outputBackend()->supportedCompositors(); + QList candidateCompositors; + + if (m_selectedCompositor != NoCompositing) { + candidateCompositors.append(m_selectedCompositor); + } else { + candidateCompositors = availableCompositors; + + const auto userConfigIt = std::find(candidateCompositors.begin(), candidateCompositors.end(), options->compositingMode()); + if (userConfigIt != candidateCompositors.end()) { + candidateCompositors.erase(userConfigIt); + candidateCompositors.prepend(options->compositingMode()); + } else { + qCWarning(KWIN_CORE) << "Configured compositor not supported by Platform. Falling back to defaults"; + } + } + + for (auto type : std::as_const(candidateCompositors)) { + bool stop = false; + switch (type) { + case OpenGLCompositing: + qCDebug(KWIN_CORE) << "Attempting to load the OpenGL scene"; + stop = attemptOpenGLCompositing(); + break; + case QPainterCompositing: + qCDebug(KWIN_CORE) << "Attempting to load the QPainter scene"; + stop = attemptQPainterCompositing(); + break; + case NoCompositing: + qCDebug(KWIN_CORE) << "Starting without compositing..."; + stop = true; + break; + } + + if (stop) { + break; + } else if (qEnvironmentVariableIsSet("KWIN_COMPOSE")) { + qCCritical(KWIN_CORE) << "Could not fulfill the requested compositing mode in KWIN_COMPOSE:" << type << ". Exiting."; + qApp->quit(); + } + } +} + +void Compositor::createScene() +{ + if (const auto eglBackend = qobject_cast(m_backend.get())) { + m_scene = std::make_unique(std::make_unique(eglBackend->eglDisplayObject())); + } else { + m_scene = std::make_unique(std::make_unique()); + } + Q_EMIT sceneCreated(); +} + +void Compositor::start() +{ + if (kwinApp()->isTerminating()) { + return; + } + if (m_state != State::Off) { + return; + } + + Q_EMIT aboutToToggleCompositing(); + m_state = State::Starting; + + if (!m_backend) { + createRenderer(); + } + + if (!m_backend) { + m_state = State::Off; + + qCCritical(KWIN_CORE) << "The used windowing system requires compositing"; + qCCritical(KWIN_CORE) << "We are going to quit KWin now as it is broken"; + qApp->quit(); + return; + } + + if (m_selectedCompositor == NoCompositing) { + m_selectedCompositor = m_backend->compositingType(); + + switch (m_selectedCompositor) { + case NoCompositing: + break; + case OpenGLCompositing: + QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL); + break; + case QPainterCompositing: + QQuickWindow::setGraphicsApi(QSGRendererInterface::Software); + break; + } + } + + createScene(); + + handleOutputsChanged(); + connect(workspace(), &Workspace::outputsChanged, this, &Compositor::handleOutputsChanged); + connect(kwinApp()->outputBackend(), &OutputBackend::outputRemoved, this, &Compositor::removeOutput); + + m_state = State::On; + + const auto windows = workspace()->windows(); + for (Window *window : windows) { + window->setupCompositing(); + } + + // Sets also the 'effects' pointer. + new EffectsHandler(this, m_scene.get()); + + Q_EMIT compositingToggled(true); +} + +void Compositor::stop() +{ + if (m_state == State::Off || m_state == State::Stopping) { + return; + } + m_state = State::Stopping; + Q_EMIT aboutToToggleCompositing(); + + // Some effects might need access to effect windows when they are about to + // be destroyed, for example to unreference deleted windows, so we have to + // make sure that effect windows outlive effects. + delete effects; + effects = nullptr; + + if (Workspace::self()) { + const auto windows = workspace()->windows(); + for (Window *window : windows) { + window->finishCompositing(); + } + disconnect(workspace(), &Workspace::outputsChanged, this, &Compositor::handleOutputsChanged); + disconnect(kwinApp()->outputBackend(), &OutputBackend::outputRemoved, this, &Compositor::removeOutput); + } + + if (m_backend->compositingType() == OpenGLCompositing) { + // some layers need a context current for destruction + static_cast(m_backend.get())->openglContext()->makeCurrent(); + } + + const auto loops = m_primaryViews | std::views::transform([](const auto &pair) { + return pair.first; + }) | std::ranges::to(); + for (RenderLoop *loop : loops) { + removeOutput(findOutput(loop)); + } + + m_scene.reset(); + m_backend.reset(); + + m_state = State::Off; + Q_EMIT compositingToggled(false); +} + +static bool isTearingRequested(const Item *item) +{ + if (item->presentationHint() == PresentationModeHint::Async) { + return true; + } + + const auto childItems = item->childItems(); + return std::ranges::any_of(childItems, [](const Item *childItem) { + return isTearingRequested(childItem); + }); +} + +static Rect mapGlobalLogicalToOutputDeviceCoordinates(const RectF &logicalGeometry, LogicalOutput *logicalOutput, BackendOutput *backendOutput) +{ + const Rect localDevice = logicalGeometry.scaled(backendOutput->scale()).rounded(); + const QPoint scaledOutputPos = (logicalOutput->geometryF().topLeft() * backendOutput->scale()).toPoint(); + return backendOutput->transform().map(localDevice.translated(backendOutput->deviceOffset() - scaledOutputPos), backendOutput->pixelSize()); +} + +static Rect mapItemToOutputDeviceCoordinates(Item *item, RenderView *view, LogicalOutput *logicalOutput, BackendOutput *backendOutput) +{ + const Rect scaledItemRect = item->mapToView(item->rect(), view).scaled(view->scale()).rounded(); + const QPoint scaledOutputPos = (logicalOutput->geometryF().topLeft() * view->scale()).toPoint(); + return backendOutput->transform().map(scaledItemRect.translated(backendOutput->deviceOffset() - scaledOutputPos), backendOutput->pixelSize()); +} + +static bool prepareDirectScanout(RenderView *view, LogicalOutput *logicalOutput, BackendOutput *backendOutput, const std::shared_ptr &frame) +{ + if (!view->isVisible()) { + return false; + } + const auto layer = view->layer(); + const auto scanoutCandidates = view->scanoutCandidates(1); + if (scanoutCandidates.isEmpty()) { + layer->setScanoutCandidate(nullptr); + return false; + } + SurfaceItem *candidate = scanoutCandidates.front(); + SurfaceItemWayland *wayland = qobject_cast(candidate); + if (!wayland || !wayland->surface()) { + return false; + } + const auto buffer = wayland->surface()->buffer(); + if (!buffer) { + return false; + } + const auto attrs = buffer->dmabufAttributes(); + if (!attrs) { + return false; + } + const bool tearing = frame->presentationMode() == PresentationMode::Async || frame->presentationMode() == PresentationMode::AdaptiveAsync; + const auto formats = tearing ? layer->supportedAsyncDrmFormats() : layer->supportedDrmFormats(); + if (auto it = formats.find(attrs->format); it == formats.end() || !it->contains(attrs->modifier)) { + layer->setScanoutCandidate(candidate); + candidate->setScanoutHint(layer->scanoutDevice(), formats); + return false; + } + layer->setTargetRect(mapItemToOutputDeviceCoordinates(candidate, view, logicalOutput, backendOutput)); + layer->setEnabled(true); + layer->setSourceRect(candidate->bufferSourceBox()); + layer->setBufferTransform(candidate->bufferTransform()); + layer->setOffloadTransform(candidate->bufferTransform().combine(backendOutput->transform().inverted())); + layer->setColor(candidate->colorDescription(), candidate->renderingIntent(), ColorPipeline::create(candidate->colorDescription(), backendOutput->layerBlendingColor(), candidate->renderingIntent())); + const bool ret = layer->importScanoutBuffer(candidate->buffer(), frame); + if (ret) { + candidate->resetDamage(); + // ensure the pixmap is updated when direct scanout ends + candidate->destroyTexture(); + } + return ret; +} + +static bool prepareRendering(RenderView *view, LogicalOutput *logicalOutput, BackendOutput *backendOutput, uint32_t requiredAlphaBits) +{ + if (!view->isVisible()) { + return false; + } + auto nativeRect = mapGlobalLogicalToOutputDeviceCoordinates(view->viewport(), logicalOutput, backendOutput); + // we need to render black bars for mirroring, + // so add the relevant area to the source and target rect + const QSize renderOffset = backendOutput->transform().map(QSize(view->renderOffset().x(), view->renderOffset().y())); + nativeRect.adjust(-renderOffset.width(), -renderOffset.height(), renderOffset.width(), renderOffset.height()); + if ((nativeRect & Rect(QPoint(), backendOutput->modeSize())).isEmpty()) { + return false; + } + + const auto layer = view->layer(); + const double reference = backendOutput->colorDescription()->referenceLuminance(); + const double maxOutputLuminance = backendOutput->colorDescription()->maxHdrLuminance().value_or(reference); + const double usedMaxLuminance = std::min(view->desiredHdrHeadroom() * reference, maxOutputLuminance); + layer->setSourceRect(Rect(QPoint(0, 0), nativeRect.size())); + layer->setTargetRect(nativeRect); + layer->setHotspot(backendOutput->transform().map(view->hotspot() * view->scale(), nativeRect.size())); + layer->setEnabled(true); + layer->setOffloadTransform(OutputTransform::Normal); + layer->setBufferTransform(backendOutput->transform()); + layer->setColor(backendOutput->layerBlendingColor()->withHdrMetadata(reference, usedMaxLuminance), RenderingIntent::AbsoluteColorimetricNoAdaptation, ColorPipeline{}); + layer->setRequiredAlphaBits(requiredAlphaBits); + return layer->preparePresentationTest(); +} + +static bool renderLayer(RenderView *view, LogicalOutput *logicalOutput, BackendOutput *backendOutput, const std::shared_ptr &frame, const Region &surfaceDamage) +{ + auto beginInfo = view->layer()->beginFrame(); + if (!beginInfo) { + return false; + } + auto &[renderTarget, repaint] = beginInfo.value(); + const Region bufferDamage = surfaceDamage.united(repaint).intersected(renderTarget.transformedRect()); + view->paint(renderTarget, view->renderOffset(), bufferDamage); + return view->layer()->endFrame(bufferDamage, surfaceDamage, frame.get()); +} + +static OutputLayer *findLayer(std::span layers, OutputLayerType type, std::optional minZPos) +{ + const auto it = std::ranges::find_if(layers, [type, minZPos](OutputLayer *layer) { + if (minZPos.has_value() && layer->maxZpos() < *minZPos) { + return false; + } + return layer->type() == type; + }); + return it == layers.end() ? nullptr : *it; +} + +/** + * items and layers need to be sorted top to bottom + */ +static std::unordered_map assignOverlays(RenderView *sceneView, std::span underlays, std::span overlays, std::span layers) +{ + if (layers.empty() || (underlays.empty() && overlays.empty())) { + return {}; + } + // TODO also allow assigning the primary view to a different plane + const int primaryZpos = sceneView->layer()->zpos(); + auto layerIt = layers.begin(); + int zpos = (*layerIt)->maxZpos(); + std::unordered_map ret; + auto overlaysIt = overlays.begin(); + for (; overlaysIt != overlays.end();) { + Item *item = *overlaysIt; + const bool compositingAllowed = qobject_cast(item) != nullptr; + const RectF sceneRect = item->mapToView(compositingAllowed ? item->boundingRect() : item->rect(), sceneView); + if (sceneRect.contains(sceneView->viewport())) { + // leave fullscreen direct scanout to the primary plane + overlaysIt++; + continue; + } + if (layerIt == layers.end()) { + return {}; + } + OutputLayer *layer = *layerIt; + const int nextZpos = std::min(zpos, layer->maxZpos()); + if (layer->minZpos() > nextZpos) { + layerIt++; + continue; + } + if (nextZpos < primaryZpos) { + // can't use this + return {}; + } + if (layer->type() == OutputLayerType::CursorOnly && qobject_cast(item) == nullptr) { + layerIt++; + continue; + } + const auto recommendedSizes = layer->recommendedSizes(); + if (!recommendedSizes.isEmpty()) { + // it's likely that sizes other than the recommended ones won't work + const Rect deviceRect = sceneRect.translated(-sceneView->viewport().topLeft()).scaled(sceneView->scale()).rounded(); + const bool hasFittingSize = std::ranges::any_of(recommendedSizes, [compositingAllowed, deviceRect](const QSize &size) { + if (compositingAllowed) { + return deviceRect.size().width() <= size.width() + && deviceRect.size().height() <= size.height(); + } else { + return deviceRect.size() == size; + } + }); + if (!hasFittingSize) { + layerIt++; + continue; + } + } + layer->setZpos(nextZpos); + ret[item] = layer; + overlaysIt++; + layerIt++; + zpos = nextZpos - 1; + } + if (overlaysIt != overlays.end()) { + // not all items were assigned, we need to composite + return {}; + } + if (layerIt == layers.end()) { + if (underlays.empty()) { + return ret; + } else { + return {}; + } + } + zpos = std::min(primaryZpos - 1, (*layerIt)->maxZpos()); + auto underlaysIt = underlays.begin(); + for (; underlaysIt != underlays.end();) { + Item *item = *underlaysIt; + const bool compositingAllowed = qobject_cast(item) != nullptr; + const RectF sceneRect = item->mapToView(compositingAllowed ? item->boundingRect() : item->rect(), sceneView); + if (sceneRect.contains(sceneView->viewport())) { + // leave fullscreen direct scanout to the primary plane + underlaysIt++; + continue; + } + if (layerIt == layers.end()) { + return {}; + } + OutputLayer *layer = *layerIt; + const int nextZpos = std::min(zpos, layer->maxZpos()); + if (layer->minZpos() > nextZpos) { + layerIt++; + continue; + } + if (layer->type() == OutputLayerType::CursorOnly && qobject_cast(item) == nullptr) { + layerIt++; + continue; + } + const auto recommendedSizes = layer->recommendedSizes(); + if (!recommendedSizes.isEmpty()) { + // it's likely that sizes other than the recommended ones won't work + const Rect deviceRect = sceneRect.translated(-sceneView->viewport().topLeft()).scaled(sceneView->scale()).rounded(); + const bool hasFittingSize = std::ranges::any_of(recommendedSizes, [compositingAllowed, deviceRect](const QSize &size) { + if (compositingAllowed) { + return deviceRect.size().width() <= size.width() + && deviceRect.size().height() <= size.height(); + } else { + return deviceRect.size() == size; + } + }); + if (!hasFittingSize) { + layerIt++; + continue; + } + } + layer->setZpos(nextZpos); + ret[item] = layer; + underlaysIt++; + layerIt++; + zpos = nextZpos - 1; + } + if (underlaysIt != underlays.end()) { + // not all items were assigned, we need to composite + return {}; + } + return ret; +} + +void Compositor::composite(RenderLoop *renderLoop) +{ + if (m_backend->checkGraphicsReset()) { + qCDebug(KWIN_CORE) << "Graphics reset occurred"; +#if KWIN_BUILD_NOTIFICATIONS + KNotification::event(QStringLiteral("graphicsreset"), i18n("Desktop effects were restarted due to a graphics reset")); +#endif + reinitialize(); + return; + } + + BackendOutput *output = findOutput(renderLoop); + LogicalOutput *logicalOutput = workspace()->findOutput(output); + const auto primaryView = m_primaryViews[renderLoop].get(); + fTraceDuration("Paint (", output->name(), ")"); + + // This must come first. + renderLoop->prepareNewFrame(); + + if (m_renderLoopDrivenAnimationDriver->isRunning()) { + m_renderLoopDrivenAnimationDriver->advanceToNextFrame(renderLoop->nextPresentationTimestamp()); + } + + auto totalTimeQuery = std::make_unique(); + auto frame = std::make_shared(renderLoop, std::chrono::nanoseconds(1'000'000'000'000 / output->refreshRate())); + std::optional desiredArtificalHdrHeadroom; + + // brightness animations should be skipped when + // - the output is new, and we didn't have the output configuration applied yet + // - there's not enough steps to do a smooth animation + // - the brightness device is external, most of them do an animation on their own + if (!output->currentBrightness().has_value() + || (!output->highDynamicRange() && output->brightnessDevice() && !output->isInternal()) + || (!output->highDynamicRange() && output->brightnessDevice() && output->brightnessDevice()->brightnessSteps() < 5)) { + frame->setBrightness(output->brightnessSetting()); + } else { + // animate much slower for automatic brightness + double changePerSecond = 3; + if (output->lastBrightnessAdjustmentReason() == BackendOutput::BrightnessReason::AutomaticBrightness) { + if (output->brightnessSetting() < output->currentBrightness()) { + // brightness should be reduced slowly, or it'll be annoying + changePerSecond = 0.1; + } else { + // but increased more quickly, so that you can still read your screen + changePerSecond = 0.5; + } + } + const double maxChangePerFrame = changePerSecond * 1'000.0 / renderLoop->refreshRate(); + // brightness perception is non-linear, gamma 2.2 encoding *roughly* represents that + const double current = std::pow(*output->currentBrightness(), 1.0 / 2.2); + frame->setBrightness(std::pow(std::clamp(std::pow(output->brightnessSetting(), 1.0 / 2.2), current - maxChangePerFrame, current + maxChangePerFrame), 2.2)); + } + // always animate the dimming factor + const double maxDimChange = 0.5 * 1'000.0 / renderLoop->refreshRate(); + // undim more quickly to not get into the way of using the system again after it's been idle. + const double maxUndimChange = 6 * maxDimChange; + frame->setDimmingFactor(std::clamp(output->dimming(), output->currentDimming() - maxDimChange, output->currentDimming() + maxUndimChange)); + + Window *const activeWindow = workspace()->activeWindow(); + SurfaceItem *const activeFullscreenItem = activeWindow && activeWindow->isFullScreen() && activeWindow->frameGeometry().intersects(primaryView->viewport()) ? activeWindow->surfaceItem() : nullptr; + frame->setContentType(activeWindow && activeFullscreenItem ? activeFullscreenItem->contentType() : ContentType::None); + + const bool wantsAdaptiveSync = activeWindow && activeWindow->frameGeometry().intersects(primaryView->viewport()) && activeWindow->wantsAdaptiveSync(); + const bool vrr = (output->capabilities() & BackendOutput::Capability::Vrr) && (output->vrrPolicy() == VrrPolicy::Always || (output->vrrPolicy() == VrrPolicy::Automatic && wantsAdaptiveSync)); + const bool tearing = (output->capabilities() & BackendOutput::Capability::Tearing) && options->allowTearing() && activeFullscreenItem && activeWindow->wantsTearing(isTearingRequested(activeFullscreenItem)); + if (vrr) { + frame->setPresentationMode(tearing ? PresentationMode::AdaptiveAsync : PresentationMode::AdaptiveSync); + } else { + frame->setPresentationMode(tearing ? PresentationMode::Async : PresentationMode::VSync); + } + + // collect all the layers we may use + struct LayerData + { + RenderView *view; + bool directScanout = false; + bool directScanoutOnly = false; + bool highPriority = false; + Region surfaceDamage; + uint32_t requiredAlphaBits; + }; + QList layers; + + primaryView->prePaint(); + layers.push_back(LayerData{ + .view = primaryView, + .directScanout = false, + .directScanoutOnly = false, + .highPriority = false, + .surfaceDamage = Region{}, + .requiredAlphaBits = 0, + }); + + // slowly adjust the artificial HDR headroom for the next frame. Note that + // - this has to happen (right) after prePaint, so that the scene's stacking order is valid + // - this is only done for internal displays, because external displays usually apply slow animations to brightness changes + if (!output->highDynamicRange() && output->brightnessDevice() && output->currentBrightness() && output->isInternal()) { + const auto desiredHdrHeadroom = output->edrPolicy() == BackendOutput::EdrPolicy::Always ? primaryView->desiredHdrHeadroom() : 1.0; + // the higher this is, the more likely the user is to notice the change in backlight brightness + // at the same time, if it's too low, it takes ages until the user sees the HDR effect + constexpr double changePerSecond = 0.5; + desiredArtificalHdrHeadroom = std::clamp(desiredHdrHeadroom, 1.0, output->maxPossibleArtificialHdrHeadroom()); + const double changePerFrame = changePerSecond * double(frame->refreshDuration().count()) / 1'000'000'000; + const double newHeadroom = std::clamp(*desiredArtificalHdrHeadroom, output->artificialHdrHeadroom() - changePerFrame, output->artificialHdrHeadroom() + changePerFrame); + frame->setArtificialHdrHeadroom(newHeadroom); + } else { + frame->setArtificialHdrHeadroom(1); + } + + QList unusedOutputLayers = m_backend->compatibleOutputLayers(output); + // the primary output layer is currently always used for the main content + unusedOutputLayers.removeOne(primaryView->layer()); + + const bool overlaysAllowed = m_allowOverlaysEnv.value_or(!output->overlayLayersLikelyBroken() && PROJECT_VERSION_PATCH >= 80); + QList specialLayers = unusedOutputLayers | std::views::filter([this, renderLoop, overlaysAllowed](OutputLayer *layer) { + return layer->type() != OutputLayerType::Primary + && (!m_brokenCursors.contains(renderLoop) || layer->type() != OutputLayerType::CursorOnly) + && (overlaysAllowed || layer->type() != OutputLayerType::GenericLayer); + }) | std::ranges::to(); + std::ranges::sort(specialLayers, [](OutputLayer *left, OutputLayer *right) { + return left->maxZpos() > right->maxZpos(); + }); + const size_t maxOverlayCount = std::ranges::count_if(specialLayers, [primaryView](OutputLayer *layer) { + return layer->maxZpos() > primaryView->layer()->zpos(); + }); + const size_t maxUnderlayCount = std::ranges::count_if(specialLayers, [primaryView](OutputLayer *layer) { + return layer->minZpos() < primaryView->layer()->zpos(); + }); + const auto [overlayCandidates, underlayCandidates] = m_scene->overlayCandidates(specialLayers.size(), maxOverlayCount, maxUnderlayCount); + auto overlayAssignments = assignOverlays(primaryView, underlayCandidates, overlayCandidates, specialLayers); + if (overlayAssignments.empty()) { + // the cursor is important, so try again without other over/underlays + const auto cursorOnly = overlayCandidates | std::views::filter([](Item *item) { + return qobject_cast(item) != nullptr; + }) | std::ranges::to(); + overlayAssignments = assignOverlays(primaryView, {}, cursorOnly, specialLayers); + } + for (const auto &[item, layer] : overlayAssignments) { + const bool isCursor = qobject_cast(item) != nullptr; + auto &view = m_overlayViews[output->renderLoop()][layer]; + if (!view || view->item() != item) { + if (isCursor) { + // special handling for the cursor + view = std::make_unique(primaryView, item, logicalOutput, output, layer); + connect(layer, &OutputLayer::repaintScheduled, view.get(), [logicalOutput, output, cursorView = view.get()]() { + // this just deals with moving the plane asynchronously, for improved latency. + // enabling, disabling and updating the cursor image still happen in composite() + const auto outputLayer = cursorView->layer(); + if (!outputLayer->isEnabled() + || !outputLayer->deviceRepaints().isEmpty() + || !cursorView->isVisible() + || cursorView->needsRepaint()) { + // composite() handles this + return; + } + std::optional maxVrrCursorDelay; + if (output->renderLoop()->activeWindowControlsVrrRefreshRate()) { + const auto effectiveMinRate = output->minVrrRefreshRateHz().transform([](uint32_t value) { + // this is intentionally using a tiny bit higher refresh rate than the minimum + // so that slight differences in timing don't drop us below the minimum + return value + 2; + }).value_or(30); + maxVrrCursorDelay = std::chrono::nanoseconds(1'000'000'000) / std::max(effectiveMinRate, 30u); + } + outputLayer->setTargetRect(mapGlobalLogicalToOutputDeviceCoordinates(cursorView->viewport(), logicalOutput, output)); + outputLayer->setEnabled(true); + if (output->presentAsync(outputLayer, maxVrrCursorDelay)) { + // prevent composite() from also pushing an update with the cursor layer + // to avoid adding cursor updates that are synchronized with primary layer updates + outputLayer->resetRepaints(); + } + }); + } else { + view = std::make_unique(primaryView, item, logicalOutput, output, layer); + } + } + view->prePaint(); + layers.push_back(LayerData{ + .view = view.get(), + .directScanout = !isCursor, + .directScanoutOnly = !isCursor, + .highPriority = isCursor, + .surfaceDamage = Region(), + .requiredAlphaBits = isCursor ? 8u : 0u, + }); + unusedOutputLayers.removeOne(layer); + if (layer->zpos() < primaryView->layer()->zpos()) { + view->setUnderlay(true); + // require more alpha bits on the primary plane, + // otherwise shadows from windows on top of the + // underlay will look terrible + // TODO also make sure we still use more than 8 color bits when possible? + layers.front().requiredAlphaBits = 8; + } else { + view->setUnderlay(false); + } + } + + QList toUpdate; + + // disable entirely unused output layers + for (OutputLayer *layer : unusedOutputLayers) { + m_overlayViews[renderLoop].erase(layer); + layer->setEnabled(false); + // TODO only add the layer to `toUpdate` when necessary + toUpdate.push_back(layer); + } + + // update all of them for the ideal configuration + for (auto &layer : layers) { + if (prepareDirectScanout(layer.view, logicalOutput, output, frame)) { + layer.directScanout = true; + } else if (!layer.directScanoutOnly && prepareRendering(layer.view, logicalOutput, output, layer.requiredAlphaBits)) { + layer.directScanout = false; + } else { + layer.view->layer()->setEnabled(false); + layer.view->layer()->scheduleRepaint(nullptr); + } + } + + // test and downgrade the configuration until the test is successful + bool result = output->testPresentation(frame); + if (!result) { + bool primaryFailure = false; + auto &primary = layers.front(); + if (primary.directScanout) { + if (prepareRendering(primary.view, logicalOutput, output, primary.requiredAlphaBits)) { + primary.directScanout = false; + result = output->testPresentation(frame); + } else { + primaryFailure = true; + // this should be very rare, but could happen with GPU resets + qCWarning(KWIN_CORE, "Preparing the primary layer failed!"); + } + } + if (!result && !primaryFailure) { + // disable all low priority layers, and if that isn't enough + // the high priority layers as well + for (bool priority : {false, true}) { + auto toDisable = layers | std::views::filter([priority](const LayerData &layer) { + return layer.view->layer()->isEnabled() + && layer.highPriority == priority + && layer.view->layer()->type() != OutputLayerType::Primary; + }); + if (!toDisable.empty()) { + for (const auto &layer : toDisable) { + layer.view->layer()->setEnabled(false); + layer.view->layer()->scheduleRepaint(nullptr); + } + result = output->testPresentation(frame); + if (result) { + break; + } + } + } + } + } + + // now actually render the layers that need rendering + if (result) { + // before rendering, enable and disable all the views that need it, + // which may add repaints to other layers + for (auto &layer : layers) { + layer.view->setExclusive(layer.view->layer()->isEnabled()); + } + + // Note that effects may schedule repaints while rendering + renderLoop->newFramePrepared(); + + for (auto &layer : layers) { + if (!layer.view->layer()->needsRepaint()) { + continue; + } + toUpdate.push_back(layer.view->layer()); + layer.surfaceDamage |= layer.view->collectDamage(); + layer.surfaceDamage |= layer.view->layer()->deviceRepaints(); + layer.view->layer()->resetRepaints(); + if (layer.view->layer()->isEnabled() && !layer.directScanout) { + result &= renderLayer(layer.view, logicalOutput, output, frame, layer.surfaceDamage); + if (!result) { + qCWarning(KWIN_CORE, "Rendering a layer failed!"); + break; + } + } + } + } else { + renderLoop->newFramePrepared(); + } + + // NOTE that this does not count the time spent in BackendOutput::present, + // but the drm backend, where that's necessary, tracks that time itself + totalTimeQuery->end(); + frame->addRenderTimeQuery(std::move(totalTimeQuery)); + if (result && !output->present(toUpdate, frame)) { + // legacy modesetting can't do (useful) presentation tests + // and even with atomic modesetting, drivers are buggy and atomic tests + // sometimes have false positives + result = false; + // first, remove all non-primary layers we attempted direct scanout with + auto toDisable = layers | std::views::filter([](const LayerData &layer) { + return layer.view->layer()->type() != OutputLayerType::Primary + && layer.view->layer()->isEnabled() + && layer.directScanout; + }); + auto &primary = layers.front(); + if (primary.directScanout || !toDisable.empty()) { + for (const auto &layer : toDisable) { + layer.view->layer()->setEnabled(false); + layer.view->setExclusive(false); + } + // re-render without direct scanout + if (prepareRendering(primary.view, logicalOutput, output, primary.requiredAlphaBits) + && renderLayer(primary.view, logicalOutput, output, frame, primary.surfaceDamage)) { + result = output->present(toUpdate, frame); + } else { + qCWarning(KWIN_CORE, "Rendering the primary layer failed!"); + } + } + + if (!result && layers.size() == 2 && layers[1].view->layer()->isEnabled()) { + // presentation failed even without direct scanout. + // try again even without the cursor layer + layers[1].view->layer()->setEnabled(false); + layers[1].view->setExclusive(false); + if (prepareRendering(primary.view, logicalOutput, output, primary.requiredAlphaBits) + && renderLayer(primary.view, logicalOutput, output, frame, Region::infinite())) { + result = output->present(toUpdate, frame); + if (result) { + // disabling the cursor layer helped... so disable it permanently, + // to prevent constantly attempting to render the hardware cursor again + // this should only ever happen with legacy modesetting, where + // presentation can't be tested + qCWarning(KWIN_CORE, "Disabling hardware cursor because of presentation failure"); + m_brokenCursors.insert(renderLoop); + } + } else { + qCWarning(KWIN_CORE, "Rendering the primary layer failed!"); + } + } + } + + m_scene->frame(primaryView, frame.get()); + for (auto &layer : layers) { + layer.view->postPaint(); + } + + // the layers have to stay valid until after postPaint, so this needs to happen after it + if (!result) { + qCWarning(KWIN_CORE, "Failed to find a working output layer configuration! Enabled layers:"); + for (const auto &layer : layers) { + if (!layer.view->layer()->isEnabled()) { + continue; + } + qCWarning(KWIN_CORE) << "src" << layer.view->layer()->sourceRect() << "-> dst" << layer.view->layer()->targetRect(); + } + output->repairPresentation(); + } + + const bool forceRepaintForBrightness = (frame->brightness() && std::abs(*frame->brightness() - output->brightnessSetting()) > 0.001) + || (desiredArtificalHdrHeadroom && frame->artificialHdrHeadroom() && std::abs(*frame->artificialHdrHeadroom() - *desiredArtificalHdrHeadroom) > 0.001); + const bool forceRepaintForDimming = frame->dimmingFactor() && std::abs(*frame->dimmingFactor() - output->currentDimming()) > 0.001; + + const bool forceRepaintForOffscreenAnimations = m_renderLoopDrivenAnimationDriver->isRunning(); + + if (forceRepaintForBrightness || forceRepaintForOffscreenAnimations || forceRepaintForDimming) { + // we're currently running an animation to change the brightness + renderLoop->scheduleRepaint(); + } +} + +void Compositor::handleOutputsChanged() +{ + for (auto &[loop, layer] : m_primaryViews) { + disconnect(loop, &RenderLoop::frameRequested, this, &Compositor::handleFrameRequested); + } + m_overlayViews.clear(); + m_primaryViews.clear(); + const auto outputs = kwinApp()->outputBackend()->outputs(); + for (BackendOutput *output : outputs) { + if (LogicalOutput *logicalOutput = workspace()->findOutput(output)) { + addOutput(logicalOutput, output); + } + } +} + +void Compositor::addOutput(LogicalOutput *logicalOutput, BackendOutput *backendOutput) +{ + if (backendOutput->isPlaceholder()) { + return; + } + assignOutputLayers(logicalOutput, backendOutput); + connect(backendOutput->renderLoop(), &RenderLoop::frameRequested, this, &Compositor::handleFrameRequested); +} + +void Compositor::removeOutput(BackendOutput *output) +{ + if (output->isPlaceholder()) { + return; + } + disconnect(output->renderLoop(), &RenderLoop::frameRequested, this, &Compositor::handleFrameRequested); + m_overlayViews.erase(output->renderLoop()); + m_primaryViews.erase(output->renderLoop()); + m_brokenCursors.erase(output->renderLoop()); +} + +void Compositor::assignOutputLayers(LogicalOutput *logicalOutput, BackendOutput *backendOutput) +{ + const auto layers = m_backend->compatibleOutputLayers(backendOutput); + const auto primaryLayer = findLayer(layers, OutputLayerType::Primary, std::nullopt); + Q_ASSERT(primaryLayer); + auto &sceneView = m_primaryViews[backendOutput->renderLoop()]; + if (sceneView) { + sceneView->setLayer(primaryLayer); + } else { + sceneView = std::make_unique(m_scene.get(), logicalOutput, backendOutput, primaryLayer); + sceneView->setScale(backendOutput->scale()); + sceneView->setRenderOffset(backendOutput->deviceOffset()); + const auto updateViewport = [view = sceneView.get(), logicalOutput, backendOutput]() { + // this matches how the renderer snaps elements to the pixel grid + const Rect scaled = logicalOutput->geometryF().scaled(backendOutput->scale()).rounded(); + view->setViewport(scaled.scaled(1.0 / backendOutput->scale())); + }; + updateViewport(); + connect(logicalOutput, &LogicalOutput::geometryChanged, sceneView.get(), updateViewport); + connect(backendOutput, &BackendOutput::scaleChanged, sceneView.get(), [view = sceneView.get(), backendOutput]() { + view->setScale(backendOutput->scale()); + }); + connect(backendOutput, &BackendOutput::deviceOffsetChanged, sceneView.get(), [view = sceneView.get(), backendOutput]() { + view->setRenderOffset(backendOutput->deviceOffset()); + }); + } + // will be re-assigned in the next composite() pass + m_overlayViews.erase(backendOutput->renderLoop()); +} + +} // namespace KWin + +#include "moc_compositor.cpp" diff --git a/local/recipes/kde/kwin/source/src/compositor.h b/local/recipes/kde/kwin/source/src/compositor.h new file mode 100644 index 0000000000..ddfb71f522 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/compositor.h @@ -0,0 +1,120 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Arthur Arlt + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "effect/globals.h" +#include "kwin_export.h" + +#include +#include + +#include + +namespace KWin +{ + +class ColorDescription; +class GLTexture; +class LogicalOutput; +class BackendOutput; +class RenderBackend; +class OutputLayer; +class RenderLoop; +class RenderTarget; +class WorkspaceScene; +class Window; +class OutputFrame; +class SceneView; +class ItemView; +class RenderLoopDrivenQAnimationDriver; + +class KWIN_EXPORT Compositor : public QObject +{ + Q_OBJECT +public: + static Compositor *create(QObject *parent = nullptr); + + enum class State { + On = 0, + Off, + Starting, + Stopping + }; + + ~Compositor() override; + static Compositor *self(); + + void start(); + void stop(); + + /** + * Re-initializes the Compositor completely. + * Connected to the D-Bus signal org.kde.KWin /KWin reinitCompositing + */ + void reinitialize(); + + /** + * Whether the Compositor is active. That is a Scene is present and the Compositor is + * not shutting down itself. + */ + bool isActive(); + + WorkspaceScene *scene() const + { + return m_scene.get(); + } + RenderBackend *backend() const + { + return m_backend.get(); + } + + void createRenderer(); + +Q_SIGNALS: + void compositingToggled(bool active); + void aboutToDestroy(); + void aboutToToggleCompositing(); + void sceneCreated(); + +protected: + explicit Compositor(QObject *parent = nullptr); + + static Compositor *s_compositor; + +protected Q_SLOTS: + void composite(RenderLoop *renderLoop); + +private Q_SLOTS: + void handleFrameRequested(RenderLoop *renderLoop); + +protected: + BackendOutput *findOutput(RenderLoop *loop) const; + + void createScene(); + bool attemptOpenGLCompositing(); + bool attemptQPainterCompositing(); + void handleOutputsChanged(); + void addOutput(LogicalOutput *logicalOutput, BackendOutput *backendOutput); + void removeOutput(BackendOutput *output); + void assignOutputLayers(LogicalOutput *logicalOutput, BackendOutput *backendOutput); + + CompositingType m_selectedCompositor = NoCompositing; + + State m_state = State::Off; + std::unique_ptr m_scene; + std::unique_ptr m_backend; + std::unordered_map> m_primaryViews; + std::unordered_map>> m_overlayViews; + std::unordered_set m_brokenCursors; + std::optional m_allowOverlaysEnv; + RenderLoopDrivenQAnimationDriver *m_renderLoopDrivenAnimationDriver; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/config-kwin.h.cmake b/local/recipes/kde/kwin/source/src/config-kwin.h.cmake new file mode 100644 index 0000000000..6c652f188a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/config-kwin.h.cmake @@ -0,0 +1,33 @@ +#pragma once +#include + +#define KWIN_PLUGIN_VERSION_STRING "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}" +#define PROJECT_VERSION_PATCH ${PROJECT_VERSION_PATCH} + +#cmakedefine01 KWIN_BUILD_DECORATIONS +#cmakedefine01 KWIN_BUILD_KCMS +#cmakedefine01 KWIN_BUILD_NOTIFICATIONS +#cmakedefine01 KWIN_BUILD_SCREENLOCKER +#cmakedefine01 KWIN_BUILD_TABBOX +#cmakedefine01 KWIN_BUILD_ACTIVITIES +#cmakedefine01 KWIN_BUILD_GLOBALSHORTCUTS +#cmakedefine01 KWIN_BUILD_X11 +#cmakedefine01 KWIN_BUILD_QACCESSIBILITYCLIENT +constexpr QLatin1String KWIN_CONFIG("kwinrc"); +constexpr QLatin1String KWIN_VERSION_STRING("${PROJECT_VERSION}"); +constexpr QLatin1String XCB_VERSION_STRING("${XCB_VERSION}"); +constexpr QLatin1String KWIN_KILLER_BIN("${KWIN_KILLER_BIN}"); +constexpr QLatin1String LIBEXEC_DIR("${CMAKE_INSTALL_FULL_LIBEXECDIR}"); +#cmakedefine01 HAVE_X11_XCB +#cmakedefine01 HAVE_X11_XINPUT +#cmakedefine01 HAVE_GBM_BO_GET_FD_FOR_PLANE +#cmakedefine01 HAVE_GBM_BO_CREATE_WITH_MODIFIERS2 +#cmakedefine01 HAVE_MEMFD +#cmakedefine01 HAVE_SCHED_RESET_ON_FORK +#cmakedefine01 HAVE_XKBCOMMON_NO_SECURE_GETENV +#cmakedefine01 HAVE_XWAYLAND_ENABLE_EI_PORTAL +#cmakedefine01 HAVE_DL_LIBRARY +#cmakedefine01 HAVE_LIBDRM_FAUX + +constexpr QLatin1String XWAYLAND_SESSION_SCRIPTS("${XWAYLAND_SESSION_SCRIPTS}"); +#cmakedefine01 HAVE_WAYLAND_PROTOCOLS_147 diff --git a/local/recipes/kde/kwin/source/src/core/brightnessdevice.h b/local/recipes/kde/kwin/source/src/core/brightnessdevice.h new file mode 100644 index 0000000000..71a566634d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/brightnessdevice.h @@ -0,0 +1,33 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2024 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include + +namespace KWin +{ + +class LogicalOutput; + +class BrightnessDevice +{ +public: + virtual ~BrightnessDevice() = default; + + virtual void setBrightness(double brightness) = 0; + + virtual std::optional observedBrightness() const = 0; + virtual bool isInternal() const = 0; + virtual QByteArray edidBeginning() const = 0; + virtual bool usesDdcCi() const = 0; + virtual int brightnessSteps() const = 0; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/core/colorlut3d.cpp b/local/recipes/kde/kwin/source/src/core/colorlut3d.cpp new file mode 100644 index 0000000000..b6f9473023 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/colorlut3d.cpp @@ -0,0 +1,50 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "colorlut3d.h" +#include "colortransformation.h" + +#include + +namespace KWin +{ + +ColorLUT3D::ColorLUT3D(std::unique_ptr &&transformation, size_t xSize, size_t ySize, size_t zSize) + : m_transformation(std::move(transformation)) + , m_xSize(xSize) + , m_ySize(ySize) + , m_zSize(zSize) +{ +} + +size_t ColorLUT3D::xSize() const +{ + return m_xSize; +} + +size_t ColorLUT3D::ySize() const +{ + return m_ySize; +} + +size_t ColorLUT3D::zSize() const +{ + return m_zSize; +} + +QVector3D ColorLUT3D::sample(const QVector3D &rgb) +{ + return m_transformation->transform(rgb); +} + +QVector3D ColorLUT3D::sample(size_t x, size_t y, size_t z) +{ + return m_transformation->transform(QVector3D(x / double(m_xSize - 1), y / double(m_ySize - 1), z / double(m_zSize - 1))); +} + +} diff --git a/local/recipes/kde/kwin/source/src/core/colorlut3d.h b/local/recipes/kde/kwin/source/src/core/colorlut3d.h new file mode 100644 index 0000000000..17772be66c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/colorlut3d.h @@ -0,0 +1,42 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include + +#include "kwin_export.h" + +class QVector3D; + +namespace KWin +{ + +class ColorTransformation; + +class KWIN_EXPORT ColorLUT3D +{ +public: + ColorLUT3D(std::unique_ptr &&transformation, size_t xSize, size_t ySize, size_t zSize); + + size_t xSize() const; + size_t ySize() const; + size_t zSize() const; + + QVector3D sample(const QVector3D &rgb); + QVector3D sample(size_t x, size_t y, size_t z); + +private: + const std::unique_ptr m_transformation; + const size_t m_xSize; + const size_t m_ySize; + const size_t m_zSize; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/core/colorpipeline.cpp b/local/recipes/kde/kwin/source/src/core/colorpipeline.cpp new file mode 100644 index 0000000000..20f9df28fe --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/colorpipeline.cpp @@ -0,0 +1,579 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2024 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "colorpipeline.h" +#include "iccprofile.h" + +#include + +namespace KWin +{ + +ValueRange ValueRange::operator*(double mult) const +{ + return ValueRange{ + .min = min * mult, + .max = max * mult, + }; +} + +static bool s_disableTonemapping = qEnvironmentVariableIntValue("KWIN_DISABLE_TONEMAPPING") == 1; + +static ValueRange getValueRange(std::initializer_list &&vectors) +{ + auto it = vectors.begin(); + ValueRange ret{ + .min = std::min({it->x(), it->y(), it->z()}), + .max = std::max({it->x(), it->y(), it->z()}), + }; + for (it = it + 1; it != vectors.end(); it++) { + ret.min = std::min({ret.min, it->x(), it->y(), it->z()}); + ret.max = std::max({ret.max, it->x(), it->y(), it->z()}); + } + return ret; +} + +ColorPipeline ColorPipeline::create(const std::shared_ptr &from, const std::shared_ptr &to, RenderingIntent intent, InputType inputType) +{ + // to figure out the most extreme RGB values that could be used, we check the extreme values + // of the mastering display colorimetry - black, red, green, blue and white + // NOTE that the mastering display white point may differ from the container white point! + const double minLum = from->minLuminance(); + const double maxLum = from->maxHdrLuminance().value_or(from->referenceLuminance()); + const auto mdBlackPoint = from->masteringColorimetry().white() * minLum; + const auto &fromXYZ = from->containerColorimetry().fromXYZ(); + const QVector3D mdBlack = fromXYZ * mdBlackPoint.asVector(); + const QVector3D mdRed = fromXYZ * (from->masteringColorimetry().red() * (maxLum - minLum) + mdBlackPoint).asVector(); + const QVector3D mdGreen = fromXYZ * (from->masteringColorimetry().green() * (maxLum - minLum) + mdBlackPoint).asVector(); + const QVector3D mdBlue = fromXYZ * (from->masteringColorimetry().blue() * (maxLum - minLum) + mdBlackPoint).asVector(); + const QVector3D mdWhite = fromXYZ * (from->masteringColorimetry().white() * maxLum).asVector(); + + const auto range1 = getValueRange({mdBlack, mdRed, mdGreen, mdBlue, mdWhite}); + const double maxOutputLuminance = to->maxHdrLuminance().value_or(to->referenceLuminance()); + const auto rgbInputSpace = from->transferFunction().type == TransferFunction::linear ? ColorspaceType::LinearRGB : ColorspaceType::NonLinearRGB; + ColorPipeline ret(ValueRange{ + .min = from->transferFunction().nitsToEncoded(range1.min), + .max = from->transferFunction().nitsToEncoded(range1.max), + }, + rgbInputSpace); + ret.addTransferFunction(from->transferFunction(), ColorspaceType::LinearRGB); + + const QMatrix4x4 toOther = from->toOther(*to, intent); + const QVector3D black = toOther.map(mdBlack); + const QVector3D red = toOther.map(mdRed); + const QVector3D green = toOther.map(mdGreen); + const QVector3D blue = toOther.map(mdBlue); + const QVector3D white = toOther.map(mdWhite); + + ret.addMatrix(toOther, getValueRange({black, red, green, blue, white}), ColorspaceType::LinearRGB); + + // NOTE that this is different from currentOutputRange().max, as the range + // also takes the gamut into account. For tone mapping, we don't care about + // the gamut though, only the luminance of white is relevant + const double maxWhiteLuminance = std::max({white.x(), white.y(), white.z()}); + if (!s_disableTonemapping && maxWhiteLuminance > maxOutputLuminance * 1.01 && intent == RenderingIntent::Perceptual) { + ret.addTonemapper(to->containerColorimetry(), to->referenceLuminance(), maxWhiteLuminance, maxOutputLuminance); + // if values outside of [0; 1] are possible, we must clamp + } else if (inputType == InputType::FloatingPoint || from->range() == EncodingRange::Limited || maxLum < from->transferFunction().maxLuminance) { + ret.addClamp(ValueRange{ + .min = to->minLuminance(), + .max = to->maxHdrLuminance().value_or(to->referenceLuminance()), + }); + } + + ret.addInverseTransferFunction(to->transferFunction(), to->transferFunction().type == TransferFunction::linear ? ColorspaceType::LinearRGB : ColorspaceType::NonLinearRGB); + return ret; +} + +ColorPipeline::ColorPipeline() + : inputRange(ValueRange{ + .min = 0, + .max = 1, + }) + , inputSpace(ColorspaceType::AnyNonRGB) +{ +} + +ColorPipeline::ColorPipeline(const ValueRange &inputRange, ColorspaceType inputSpace) + : inputRange(inputRange) + , inputSpace(inputSpace) +{ +} + +const ValueRange &ColorPipeline::currentOutputRange() const +{ + return ops.empty() ? inputRange : ops.back().output; +} + +ColorspaceType ColorPipeline::currentOutputSpace() const +{ + return ops.empty() ? inputSpace : ops.back().outputSpace; +} + +void ColorPipeline::addMultiplier(double factor) +{ + addMultiplier(QVector3D(factor, factor, factor)); +} + +void ColorPipeline::addMultiplier(const QVector3D &factors) +{ + if (factors == QVector3D(1, 1, 1)) { + return; + } + const ValueRange output{ + .min = currentOutputRange().min * std::min(factors.x(), std::min(factors.y(), factors.z())), + .max = currentOutputRange().max * std::max(factors.x(), std::max(factors.y(), factors.z())), + }; + if (!ops.empty()) { + auto *lastOp = &ops.back().operation; + if (const auto mat = std::get_if(lastOp)) { + QMatrix4x4 newMat; + newMat.scale(factors); + newMat *= mat->mat; + ops.erase(ops.end() - 1); + addMatrix(newMat, output, currentOutputSpace()); + return; + } else if (const auto mult = std::get_if(lastOp)) { + mult->factors *= factors; + if ((mult->factors - QVector3D(1, 1, 1)).lengthSquared() < s_maxResolution * s_maxResolution) { + ops.erase(ops.end() - 1); + } else { + ops.back().output = output; + } + return; + } else if (std::abs(factors.x() - factors.y()) < s_maxResolution && std::abs(factors.x() - factors.z()) < s_maxResolution) { + if (const auto tf = std::get_if(lastOp); tf && tf->tf.hasLinearMinLuminance()) { + tf->tf.minLuminance *= factors.x(); + tf->tf.maxLuminance *= factors.x(); + ops.back().output = output; + return; + } else if (const auto tf = std::get_if(lastOp); tf && tf->tf.hasLinearMinLuminance()) { + tf->tf.minLuminance /= factors.x(); + tf->tf.maxLuminance /= factors.x(); + ops.back().output = output; + return; + } + } + } + ops.push_back(ColorOp{ + .input = currentOutputRange(), + .inputSpace = currentOutputSpace(), + .operation = ColorMultiplier(factors), + .output = output, + .outputSpace = currentOutputSpace(), + }); +} + +void ColorPipeline::addTransferFunction(TransferFunction tf, ColorspaceType outputType) +{ + if (!ops.empty()) { + if (const auto invTf = std::get_if(&ops.back().operation)) { + if (invTf->tf == tf) { + ops.erase(ops.end() - 1); + return; + } + } + } + if (tf.type == TransferFunction::linear) { + QMatrix4x4 mat; + mat.translate(tf.minLuminance, tf.minLuminance, tf.minLuminance); + mat.scale(tf.maxLuminance - tf.minLuminance); + addMatrix(mat, ValueRange{ + .min = (mat * QVector3D(currentOutputRange().min, 0, 0)).x(), + .max = (mat * QVector3D(currentOutputRange().max, 0, 0)).x(), + }, + outputType); + } else { + ops.push_back(ColorOp{ + .input = currentOutputRange(), + .inputSpace = currentOutputSpace(), + .operation = ColorTransferFunction(tf), + .output = ValueRange{ + .min = tf.encodedToNits(currentOutputRange().min), + .max = tf.encodedToNits(currentOutputRange().max), + }, + .outputSpace = outputType, + }); + } +} + +void ColorPipeline::addInverseTransferFunction(TransferFunction tf, ColorspaceType outputType) +{ + if (!ops.empty()) { + if (const auto otherTf = std::get_if(&ops.back().operation)) { + if (otherTf->tf == tf) { + ops.erase(ops.end() - 1); + return; + } + } + } + if (tf.type == TransferFunction::linear) { + QMatrix4x4 mat; + mat.scale(1.0 / (tf.maxLuminance - tf.minLuminance)); + mat.translate(-tf.minLuminance, -tf.minLuminance, -tf.minLuminance); + addMatrix(mat, ValueRange{ + .min = (mat * QVector3D(currentOutputRange().min, 0, 0)).x(), + .max = (mat * QVector3D(currentOutputRange().max, 0, 0)).x(), + }, + outputType); + } else { + ops.push_back(ColorOp{ + .input = currentOutputRange(), + .inputSpace = currentOutputSpace(), + .operation = InverseColorTransferFunction(tf), + .output = ValueRange{ + .min = tf.nitsToEncoded(currentOutputRange().min), + .max = tf.nitsToEncoded(currentOutputRange().max), + }, + .outputSpace = outputType, + }); + } +} + +bool isFuzzyIdentity(const QMatrix4x4 &mat) +{ + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + const float targetValue = i == j ? 1 : 0; + if (std::abs(mat(i, j) - targetValue) > ColorPipeline::s_maxResolution) { + return false; + } + } + } + return true; +} + +static bool isFuzzyScalingOnly(const QMatrix4x4 &mat) +{ + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + if (i == j) { + continue; + } + if (std::abs(mat(i, j)) > ColorPipeline::s_maxResolution) { + return false; + } + } + } + return true; +} + +void ColorPipeline::addMatrix(const QMatrix4x4 &mat, const ValueRange &output, ColorspaceType outputType) +{ + if (isFuzzyIdentity(mat)) { + return; + } + if (!ops.empty()) { + auto *lastOp = &ops.back().operation; + if (const auto otherMat = std::get_if(lastOp)) { + const auto newMat = mat * otherMat->mat; + ops.erase(ops.end() - 1); + addMatrix(newMat, output, outputType); + return; + } else if (const auto mult = std::get_if(lastOp)) { + QMatrix4x4 scaled = mat; + scaled.scale(mult->factors); + ops.erase(ops.end() - 1); + addMatrix(scaled, output, outputType); + return; + } else if (std::abs(mat(0, 3)) >= s_maxResolution + && std::abs(mat(0, 3) - mat(1, 3)) < s_maxResolution + && std::abs(mat(1, 3) - mat(2, 3)) < s_maxResolution) { + // there's a color-neutral offset, + // which can be folded into transfer functions + bool success = false; + QMatrix4x4 inverse = mat.inverted(&success); + if (success) { + if (const auto tf = std::get_if(lastOp); tf && tf->tf.hasLinearMinLuminance()) { + tf->tf.minLuminance -= inverse(0, 3); + tf->tf.maxLuminance -= inverse(0, 3); + ops.back().output.min -= inverse(0, 3); + ops.back().output.max -= inverse(0, 3); + QMatrix4x4 newMat = mat; + newMat(0, 3) = 0; + newMat(1, 3) = 0; + newMat(2, 3) = 0; + addMatrix(newMat, output, outputType); + return; + } else if (const auto invTf = std::get_if(lastOp); invTf && invTf->tf.hasLinearMinLuminance()) { + invTf->tf.minLuminance += inverse(0, 3); + invTf->tf.maxLuminance += inverse(0, 3); + ops.back().input.min += inverse(0, 3); + ops.back().input.max += inverse(0, 3); + QMatrix4x4 newMat = mat; + newMat(0, 3) = 0; + newMat(1, 3) = 0; + newMat(2, 3) = 0; + addMatrix(newMat, output, outputType); + return; + } + } + } + } + if (isFuzzyScalingOnly(mat)) { + // pure scaling, this can be simplified + addMultiplier(QVector3D(mat(0, 0), mat(1, 1), mat(2, 2))); + return; + } + ops.push_back(ColorOp{ + .input = currentOutputRange(), + .inputSpace = currentOutputSpace(), + .operation = ColorMatrix(mat), + .output = output, + .outputSpace = outputType, + }); +} + +static const QMatrix4x4 s_toICtCp = QMatrix4x4( + 2048.0 / 4096.0, 2048.0 / 4096.0, 0.0, 0.0, + 6610.0 / 4096.0, -13613.0 / 4096.0, 7003.0 / 4096.0, 0.0, + 17933.0 / 4096.0, -17390.0 / 4096.0, -543.0 / 4096.0, 0.0, + 0.0, 0.0, 0.0, 1.0); +static const QMatrix4x4 s_fromICtCp = s_toICtCp.inverted(); + +void ColorPipeline::addTonemapper(const Colorimetry &containerColorimetry, double referenceLuminance, double maxInputLuminance, double maxOutputLuminance) +{ + // convert from rgb to ICtCp + Q_ASSERT(currentOutputSpace() == ColorspaceType::LinearRGB); + addMatrix(containerColorimetry.toLMS(), currentOutputRange(), currentOutputSpace()); + const TransferFunction PQ(TransferFunction::PerceptualQuantizer, 0, 10'000); + addInverseTransferFunction(PQ, ColorspaceType::AnyNonRGB); + addMatrix(s_toICtCp, currentOutputRange(), ColorspaceType::ICtCp); + // apply the tone mapping to the intensity component + ops.push_back(ColorOp{ + .input = currentOutputRange(), + .operation = ColorTonemapper(referenceLuminance, maxInputLuminance, maxOutputLuminance), + .output = ValueRange{ + .min = PQ.nitsToEncoded(currentOutputRange().min), + .max = PQ.nitsToEncoded(maxOutputLuminance), + }, + }); + // convert back to rgb + addMatrix(s_fromICtCp, currentOutputRange(), ColorspaceType::AnyNonRGB); + addTransferFunction(PQ, ColorspaceType::AnyNonRGB); + addMatrix(containerColorimetry.fromLMS(), currentOutputRange(), ColorspaceType::LinearRGB); + addClamp(currentOutputRange()); +} + +void ColorPipeline::add1DLUT(const std::shared_ptr &transform, ColorspaceType outputType) +{ + const auto min = transform->transform(QVector3D(currentOutputRange().min, currentOutputRange().min, currentOutputRange().min)); + const auto max = transform->transform(QVector3D(currentOutputRange().max, currentOutputRange().max, currentOutputRange().max)); + ops.push_back(ColorOp{ + .input = currentOutputRange(), + .inputSpace = currentOutputSpace(), + .operation = transform, + .output = ValueRange{ + .min = std::min({min.x(), min.y(), min.z()}), + .max = std::max({max.x(), max.y(), max.z()}), + }, + .outputSpace = outputType, + }); +} + +void ColorPipeline::addClamp(const ValueRange &range) +{ + ops.push_back(ColorOp{ + .input = currentOutputRange(), + .inputSpace = currentOutputSpace(), + .operation = ColorClamp(range), + .output = ValueRange{ + .min = std::clamp(range.min, currentOutputRange().min, currentOutputRange().max), + .max = std::clamp(range.max, currentOutputRange().min, currentOutputRange().max), + }, + .outputSpace = currentOutputSpace(), + }); +} + +bool ColorPipeline::isIdentity() const +{ + return ops.empty(); +} + +void ColorPipeline::add(const ColorOp &op) +{ + if (const auto mat = std::get_if(&op.operation)) { + addMatrix(mat->mat, op.output, op.outputSpace); + } else if (const auto mult = std::get_if(&op.operation)) { + addMultiplier(mult->factors); + } else if (const auto tf = std::get_if(&op.operation)) { + addTransferFunction(tf->tf, op.outputSpace); + } else if (const auto tf = std::get_if(&op.operation)) { + addInverseTransferFunction(tf->tf, op.outputSpace); + } else { + ops.push_back(op); + } +} + +void ColorPipeline::add(const ColorPipeline &pipeline) +{ + for (const auto &op : pipeline.ops) { + add(op); + } +} + +ColorPipeline ColorPipeline::merged(const ColorPipeline &onTop) const +{ + ColorPipeline ret{inputRange, inputSpace}; + ret.ops = ops; + for (const auto &op : onTop.ops) { + ret.add(op); + } + return ret; +} + +QVector3D ColorPipeline::evaluate(const QVector3D &input) const +{ + QVector3D ret = input; + for (const auto &op : ops) { + ret = op.apply(ret); + } + return ret; +} + +bool ColorPipeline::operator==(const ColorPipeline &other) const +{ + // NOTE that this can't just use the default compiler-generated + // comparison, as identity transformations may have different + // input ranges, which we want to ignore here! + return ops == other.ops; +} + +QVector3D ColorOp::apply(const QVector3D input) const +{ + return applyOperation(operation, input); +} + +QVector3D ColorOp::applyOperation(const ColorOp::Operation &operation, const QVector3D &input) +{ + if (const auto mat = std::get_if(&operation)) { + return mat->mat * input; + } else if (const auto mult = std::get_if(&operation)) { + return mult->factors * input; + } else if (const auto tf = std::get_if(&operation)) { + return tf->tf.encodedToNits(input); + } else if (const auto tf = std::get_if(&operation)) { + return tf->tf.nitsToEncoded(input); + } else if (const auto tonemap = std::get_if(&operation)) { + return QVector3D(tonemap->map(input.x()), input.y(), input.z()); + } else if (const auto transform1D = std::get_if>(&operation)) { + return (*transform1D)->transform(input); + } else if (const auto transform3D = std::get_if>(&operation)) { + return (*transform3D)->sample(input); + } else if (auto clamp = std::get_if(&operation)) { + return QVector3D{ + std::clamp(input.x(), clamp->m_minValue, clamp->m_maxValue), + std::clamp(input.y(), clamp->m_minValue, clamp->m_maxValue), + std::clamp(input.z(), clamp->m_minValue, clamp->m_maxValue), + }; + } else { + Q_UNREACHABLE(); + } +} + +ColorTransferFunction::ColorTransferFunction(TransferFunction tf) + : tf(tf) +{ +} + +InverseColorTransferFunction::InverseColorTransferFunction(TransferFunction tf) + : tf(tf) +{ +} + +ColorMatrix::ColorMatrix(const QMatrix4x4 &mat) + : mat(mat) +{ +} + +ColorMultiplier::ColorMultiplier(const QVector3D &factors) + : factors(factors) +{ +} + +ColorMultiplier::ColorMultiplier(double factor) + : factors(factor, factor, factor) +{ +} + +ColorTonemapper::ColorTonemapper(double referenceLuminance, double maxInputLuminance, double maxOutputLuminance) + : m_referenceLuminance(referenceLuminance) + , m_inputRange(maxInputLuminance / referenceLuminance) + , m_outputRange(maxOutputLuminance / referenceLuminance) + // derived from + // outputRange = inputRange * (1 + inputRange * v) / (1 + inputRange) + // => outputRange * (1 + inputRange) = inputRange + inputRange ^ 2 * v + // => v = (outputRange * (1 + inputRange) - inputRange) / inputRange ^ 2 + , m_v((m_outputRange * (1 + m_inputRange) - m_inputRange) / std::pow(m_inputRange, 2)) +{ +} + +double ColorTonemapper::map(double pqEncodedLuminance) const +{ + const double luminance = TransferFunction(TransferFunction::PerceptualQuantizer).encodedToNits(pqEncodedLuminance); + + double relativeLuminance = std::max(luminance / m_referenceLuminance, 0.0); + // This is a modified Reinhart curve. It ensures that + // f(0) = 0 + // f(x) <= x + // f(inputRange) = outputRange + // with inputRange -> infinity, f(1) = 0.5 (=at most reduces reference luminance by half) + relativeLuminance = relativeLuminance * (1 + relativeLuminance * m_v) / (1.0 + relativeLuminance); + return TransferFunction(TransferFunction::PerceptualQuantizer).nitsToEncoded(relativeLuminance * m_referenceLuminance); +} + +ColorClamp::ColorClamp(ValueRange range) + : m_minValue(range.min) + , m_maxValue(range.max) +{ +} + +ColorClamp::ColorClamp(double minValue, double maxValue) + : m_minValue(minValue) + , m_maxValue(maxValue) +{ +} +} + +QDebug operator<<(QDebug debug, const KWin::ColorOp &op) +{ + if (auto tf = std::get_if(&op.operation)) { + debug << tf->tf; + } else if (auto tf = std::get_if(&op.operation)) { + debug << "inverse" << tf->tf; + } else if (auto mat = std::get_if(&op.operation)) { + debug << mat->mat; + } else if (auto mult = std::get_if(&op.operation)) { + debug << mult->factors; + } else if (auto tonemap = std::get_if(&op.operation)) { + debug << "tonemapper(" << tonemap->m_inputRange << tonemap->m_outputRange << ")"; + } else if (std::holds_alternative>(op.operation)) { + debug << "lut1d"; + } else if (std::holds_alternative>(op.operation)) { + debug << "lut3d"; + } else if (auto clip = std::get_if(&op.operation)) { + debug << "clamp(" << clip->m_minValue << clip->m_maxValue << ")"; + } + return debug; +} + +QDebug operator<<(QDebug debug, const KWin::ColorPipeline &pipeline) +{ + debug << "ColorPipeline("; + for (const auto &op : pipeline.ops) { + debug << op; + } + debug << ")"; + return debug; +} + +QDebug operator<<(QDebug debug, const KWin::ValueRange &range) +{ + debug << "[" << range.min << "," << range.max << "]"; + return debug; +} diff --git a/local/recipes/kde/kwin/source/src/core/colorpipeline.h b/local/recipes/kde/kwin/source/src/core/colorpipeline.h new file mode 100644 index 0000000000..30e9c13104 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/colorpipeline.h @@ -0,0 +1,167 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2024 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "colorlut3d.h" +#include "colorspace.h" +#include "colortransformation.h" +#include "kwin_export.h" + +namespace KWin +{ + +enum class ColorspaceType { + LinearRGB = 0, + NonLinearRGB, + ICtCp, + AnyNonRGB, +}; + +class IccProfile; + +class KWIN_EXPORT ValueRange +{ +public: + double min = 0; + double max = 1; + + bool operator==(const ValueRange &) const = default; + ValueRange operator*(double mult) const; +}; + +class KWIN_EXPORT ColorTransferFunction +{ +public: + explicit ColorTransferFunction(TransferFunction tf); + + bool operator==(const ColorTransferFunction &) const = default; + + TransferFunction tf; +}; + +class KWIN_EXPORT InverseColorTransferFunction +{ +public: + explicit InverseColorTransferFunction(TransferFunction tf); + + bool operator==(const InverseColorTransferFunction &) const = default; + + TransferFunction tf; +}; + +class KWIN_EXPORT ColorMatrix +{ +public: + explicit ColorMatrix(const QMatrix4x4 &mat); + + bool operator==(const ColorMatrix &) const = default; + + QMatrix4x4 mat; +}; + +class KWIN_EXPORT ColorMultiplier +{ +public: + explicit ColorMultiplier(double factor); + explicit ColorMultiplier(const QVector3D &factors); + + bool operator==(const ColorMultiplier &) const = default; + + QVector3D factors; +}; + +class KWIN_EXPORT ColorTonemapper +{ +public: + explicit ColorTonemapper(double referenceLuminance, double maxInputLuminance, double maxOutputLuminance); + + double map(double pqEncodedLuminance) const; + bool operator==(const ColorTonemapper &) const = default; + + double m_referenceLuminance; + double m_inputRange; + double m_outputRange; + double m_v; +}; + +class KWIN_EXPORT ColorClamp +{ +public: + explicit ColorClamp(ValueRange range); + explicit ColorClamp(double minValue, double maxValue); + + bool operator==(const ColorClamp &) const = default; + + double m_minValue; + double m_maxValue; +}; + +class KWIN_EXPORT ColorOp +{ +public: + using Operation = std::variant, std::shared_ptr, ColorClamp>; + ValueRange input; + ColorspaceType inputSpace = ColorspaceType::AnyNonRGB; + Operation operation; + ValueRange output; + ColorspaceType outputSpace = ColorspaceType::AnyNonRGB; + + bool operator==(const ColorOp &) const = default; + QVector3D apply(const QVector3D input) const; + static QVector3D applyOperation(const ColorOp::Operation &operation, const QVector3D &input); +}; + +class KWIN_EXPORT ColorPipeline +{ +public: + /** + * matrix calculations with floating point numbers can result in very small errors + * this value is the minimum difference we actually care about; everything below + * can and should be optimized out + */ + static constexpr float s_maxResolution = 0.00001; + + explicit ColorPipeline(); + explicit ColorPipeline(const ValueRange &inputRange, ColorspaceType inputType); + + enum class InputType { + FixedPoint, + FloatingPoint, + }; + static ColorPipeline create(const std::shared_ptr &from, const std::shared_ptr &to, RenderingIntent intent, InputType inputType = InputType::FixedPoint); + + ColorPipeline merged(const ColorPipeline &onTop) const; + + bool isIdentity() const; + bool operator==(const ColorPipeline &other) const; + const ValueRange ¤tOutputRange() const; + ColorspaceType currentOutputSpace() const; + QVector3D evaluate(const QVector3D &input) const; + + void addMultiplier(double factor); + void addMultiplier(const QVector3D &factors); + void addTransferFunction(TransferFunction tf, ColorspaceType outputType); + void addInverseTransferFunction(TransferFunction tf, ColorspaceType outputType); + void addMatrix(const QMatrix4x4 &mat, const ValueRange &output, ColorspaceType outputType); + void addTonemapper(const Colorimetry &containerColorimetry, double referenceLuminance, double maxInputLuminance, double maxOutputLuminance); + void add(const ColorOp &op); + void add(const ColorPipeline &pipeline); + void add1DLUT(const std::shared_ptr &transform, ColorspaceType outputType); + void addClamp(const ValueRange &range); + + ValueRange inputRange; + ColorspaceType inputSpace; + std::vector ops; +}; + +KWIN_EXPORT bool isFuzzyIdentity(const QMatrix4x4 &mat); +} + +KWIN_EXPORT QDebug operator<<(QDebug debug, const KWin::ColorOp &op); +KWIN_EXPORT QDebug operator<<(QDebug debug, const KWin::ColorPipeline &pipeline); +KWIN_EXPORT QDebug operator<<(QDebug debug, const KWin::ValueRange &value); diff --git a/local/recipes/kde/kwin/source/src/core/colorpipelinestage.cpp b/local/recipes/kde/kwin/source/src/core/colorpipelinestage.cpp new file mode 100644 index 0000000000..79a8c0ad62 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/colorpipelinestage.cpp @@ -0,0 +1,48 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "colorpipelinestage.h" + +#include + +#include "utils/common.h" + +namespace KWin +{ + +ColorPipelineStage::ColorPipelineStage(cmsStage *stage) + : m_stage(stage) +{ +} + +ColorPipelineStage::~ColorPipelineStage() +{ + if (m_stage) { + cmsStageFree(m_stage); + } +} + +std::unique_ptr ColorPipelineStage::dup() const +{ + if (m_stage) { + auto dup = cmsStageDup(m_stage); + if (dup) { + return std::make_unique(dup); + } else { + qCWarning(KWIN_CORE) << "Failed to duplicate cmsStage!"; + } + } + return nullptr; +} + +cmsStage *ColorPipelineStage::stage() const +{ + return m_stage; +} + +} diff --git a/local/recipes/kde/kwin/source/src/core/colorpipelinestage.h b/local/recipes/kde/kwin/source/src/core/colorpipelinestage.h new file mode 100644 index 0000000000..50cd3edc48 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/colorpipelinestage.h @@ -0,0 +1,33 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "kwin_export.h" + +#include + +typedef struct _cmsStage_struct cmsStage; + +namespace KWin +{ + +class KWIN_EXPORT ColorPipelineStage +{ +public: + ColorPipelineStage(cmsStage *stage); + ~ColorPipelineStage(); + + std::unique_ptr dup() const; + cmsStage *stage() const; + +private: + cmsStage *const m_stage; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/core/colorspace.cpp b/local/recipes/kde/kwin/source/src/core/colorspace.cpp new file mode 100644 index 0000000000..234bc188f3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/colorspace.cpp @@ -0,0 +1,953 @@ +/* + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "colorspace.h" +#include "colorpipeline.h" + +#include + +namespace KWin +{ + +static QMatrix4x4 matrixFromColumns(const QVector3D &first, const QVector3D &second, const QVector3D &third) +{ + QMatrix4x4 ret; + ret(0, 0) = first.x(); + ret(1, 0) = first.y(); + ret(2, 0) = first.z(); + ret(0, 1) = second.x(); + ret(1, 1) = second.y(); + ret(2, 1) = second.z(); + ret(0, 2) = third.x(); + ret(1, 2) = third.y(); + ret(2, 2) = third.z(); + return ret; +} + +XYZ xy::toXYZ() const +{ + if (y == 0) { + return XYZ{0, 0, 0}; + } + return XYZ{ + .X = x / y, + .Y = 1.0, + .Z = (1 - x - y) / y, + }; +} + +QVector2D xy::asVector() const +{ + return QVector2D(x, y); +} + +bool xy::operator==(const xy &other) const +{ + return qFuzzyCompare(x, other.x) + && qFuzzyCompare(y, other.y); +} + +XYZ xyY::toXYZ() const +{ + if (y == 0) { + return XYZ{0, 0, 0}; + } + return XYZ{ + .X = Y * x / y, + .Y = Y, + .Z = Y * (1 - x - y) / y, + }; +} + +bool xyY::operator==(const xyY &other) const +{ + return qFuzzyCompare(x, other.x) + && qFuzzyCompare(y, other.y) + && qFuzzyCompare(Y, other.Y); +} + +xyY XYZ::toxyY() const +{ + const double sum = X + Y + Z; + if (qFuzzyIsNull(sum)) { + // this is nonsense, but at least won't crash + return xyY{ + .x = 0, + .y = 0, + .Y = 1, + }; + } + return xyY{ + .x = X / sum, + .y = Y / sum, + .Y = Y, + }; +} + +xy XYZ::toxy() const +{ + const double sum = X + Y + Z; + if (qFuzzyIsNull(sum)) { + // this is nonsense, but at least won't crash + return xy{ + .x = 0, + .y = 0, + }; + } + return xy{ + .x = X / sum, + .y = Y / sum, + }; +} + +XYZ XYZ::operator*(double factor) const +{ + return XYZ{ + .X = X * factor, + .Y = Y * factor, + .Z = Z * factor, + }; +} + +XYZ XYZ::operator/(double divisor) const +{ + return XYZ{ + .X = X / divisor, + .Y = Y / divisor, + .Z = Z / divisor, + }; +} + +XYZ XYZ::operator+(const XYZ &other) const +{ + return XYZ{ + .X = X + other.X, + .Y = Y + other.Y, + .Z = Z + other.Z, + }; +} + +QVector3D XYZ::asVector() const +{ + return QVector3D(X, Y, Z); +} + +XYZ XYZ::fromVector(const QVector3D &vector) +{ + return XYZ{ + .X = vector.x(), + .Y = vector.y(), + .Z = vector.z(), + }; +} + +bool XYZ::operator==(const XYZ &other) const +{ + return qFuzzyCompare(X, other.X) + && qFuzzyCompare(Y, other.Y) + && qFuzzyCompare(Z, other.Z); +} + +QMatrix4x4 Colorimetry::chromaticAdaptationMatrix(XYZ sourceWhitepoint, XYZ destinationWhitepoint) +{ + static const QMatrix4x4 bradford = []() { + QMatrix4x4 ret; + ret(0, 0) = 0.8951; + ret(0, 1) = 0.2664; + ret(0, 2) = -0.1614; + ret(1, 0) = -0.7502; + ret(1, 1) = 1.7135; + ret(1, 2) = 0.0367; + ret(2, 0) = 0.0389; + ret(2, 1) = -0.0685; + ret(2, 2) = 1.0296; + return ret; + }(); + static const QMatrix4x4 inverseBradford = []() { + QMatrix4x4 ret; + ret(0, 0) = 0.9869929; + ret(0, 1) = -0.1470543; + ret(0, 2) = 0.1599627; + ret(1, 0) = 0.4323053; + ret(1, 1) = 0.5183603; + ret(1, 2) = 0.0492912; + ret(2, 0) = -0.0085287; + ret(2, 1) = 0.0400428; + ret(2, 2) = 0.9684867; + return ret; + }(); + if (sourceWhitepoint == destinationWhitepoint) { + return QMatrix4x4{}; + } + const QVector3D factors = (bradford.map(destinationWhitepoint.asVector())) / (bradford.map(sourceWhitepoint.asVector())); + QMatrix4x4 adaptation{}; + adaptation(0, 0) = factors.x(); + adaptation(1, 1) = factors.y(); + adaptation(2, 2) = factors.z(); + return inverseBradford * adaptation * bradford; +} + +QMatrix4x4 Colorimetry::calculateToXYZMatrix(XYZ red, XYZ green, XYZ blue, XYZ white) +{ + const QVector3D r = red.asVector(); + const QVector3D g = green.asVector(); + const QVector3D b = blue.asVector(); + const auto component_scale = (matrixFromColumns(r, g, b)).inverted().map(white.asVector()); + return matrixFromColumns(r * component_scale.x(), g * component_scale.y(), b * component_scale.z()); +} + +Colorimetry Colorimetry::interpolateGamutTo(const Colorimetry &one, double factor) const +{ + return Colorimetry{ + m_red * (1 - factor) + one.red() * factor, + m_green * (1 - factor) + one.green() * factor, + m_blue * (1 - factor) + one.blue() * factor, + m_white, // whitepoint should stay the same + }; +} + +static double triangleArea(QVector2D p1, QVector2D p2, QVector2D p3) +{ + return std::abs(0.5 * (p1.x() * (p2.y() - p3.y()) + p2.x() * (p3.y() - p1.y()) + p3.x() * (p1.y() - p2.y()))); +} + +bool Colorimetry::isValid(xy red, xy green, xy blue, xy white) +{ + // this is more of a heuristic than a hard rule + // but if the gamut is too small, it's not really usable + const double gamutArea = triangleArea(red.asVector(), green.asVector(), blue.asVector()); + if (gamutArea < 0.02) { + return false; + } + // if the white point is inside the gamut triangle, + // the three triangles made up between the primaries and the whitepoint + // must have the same area as the gamut triangle + const double area1 = triangleArea(white.asVector(), green.asVector(), blue.asVector()); + const double area2 = triangleArea(red.asVector(), white.asVector(), blue.asVector()); + const double area3 = triangleArea(red.asVector(), green.asVector(), white.asVector()); + if (std::abs(area1 + area2 + area3 - gamutArea) > 0.001) { + // this would cause terrible glitches + return false; + } + return true; +} + +bool Colorimetry::isReal(xy red, xy green, xy blue, xy white) +{ + if (!isValid(red, green, blue, white)) { + return false; + } + // outside of XYZ definitely can't be shown on a display + // TODO maybe calculate if all values are within the human-visible gamut too? + if (red.x < 0 || red.x > 1 + || red.y < 0 || red.y > 1 + || green.x < 0 || green.x > 1 + || green.y < 0 || green.y > 1 + || blue.x < 0 || blue.x > 1 + || blue.y < 0 || blue.y > 1 + || white.x < 0 || white.x > 1 + || white.y < 0 || white.y > 1) { + return false; + } + return true; +} + +Colorimetry::Colorimetry(XYZ red, XYZ green, XYZ blue, XYZ white) + : m_red(red) + , m_green(green) + , m_blue(blue) + , m_white(white) + , m_toXYZ(calculateToXYZMatrix(red, green, blue, white)) + , m_fromXYZ(m_toXYZ.inverted()) +{ +} + +Colorimetry::Colorimetry(xyY red, xyY green, xyY blue, xyY white) + : Colorimetry(red.toXYZ(), green.toXYZ(), blue.toXYZ(), white.toXYZ()) +{ +} + +Colorimetry::Colorimetry(xy red, xy green, xy blue, xy white) + : m_white(xyY(white.x, white.y, 1.0).toXYZ()) +{ + const auto brightness = (matrixFromColumns( + xyY(red.x, red.y, 1.0).toXYZ().asVector(), + xyY(green.x, green.y, 1.0).toXYZ().asVector(), + xyY(blue.x, blue.y, 1.0).toXYZ().asVector())) + .inverted() + .map( + xyY(white.x, white.y, 1.0).toXYZ().asVector()); + m_red = xyY(red.x, red.y, brightness.x()).toXYZ(); + m_green = xyY(green.x, green.y, brightness.y()).toXYZ(); + m_blue = xyY(blue.x, blue.y, brightness.z()).toXYZ(); + m_toXYZ = calculateToXYZMatrix(m_red, m_green, m_blue, m_white); + m_fromXYZ = m_toXYZ.inverted(); +} + +const QMatrix4x4 &Colorimetry::toXYZ() const +{ + return m_toXYZ; +} + +const QMatrix4x4 &Colorimetry::fromXYZ() const +{ + return m_fromXYZ; +} + +// converts from XYZ to LMS suitable for ICtCp +static const QMatrix4x4 s_xyzToDolbyLMS = []() { + QMatrix4x4 ret; + ret(0, 0) = 0.3593; + ret(0, 1) = 0.6976; + ret(0, 2) = -0.0359; + ret(1, 0) = -0.1921; + ret(1, 1) = 1.1005; + ret(1, 2) = 0.0754; + ret(2, 0) = 0.0071; + ret(2, 1) = 0.0748; + ret(2, 2) = 0.8433; + return ret; +}(); +static const QMatrix4x4 s_inverseDolbyLMS = s_xyzToDolbyLMS.inverted(); + +QMatrix4x4 Colorimetry::toLMS() const +{ + return s_xyzToDolbyLMS * m_toXYZ; +} + +QMatrix4x4 Colorimetry::fromLMS() const +{ + return m_fromXYZ * s_inverseDolbyLMS; +} + +Colorimetry Colorimetry::adaptedTo(xyY newWhitepoint) const +{ + const auto mat = chromaticAdaptationMatrix(this->white(), newWhitepoint.toXYZ()); + return Colorimetry{ + XYZ::fromVector(mat.map(red().asVector())), + XYZ::fromVector(mat.map(green().asVector())), + XYZ::fromVector(mat.map(blue().asVector())), + newWhitepoint.toXYZ(), + }; +} + +Colorimetry Colorimetry::withWhitepoint(xyY newWhitePoint) const +{ + newWhitePoint.Y = 1; + return Colorimetry{ + m_red, + m_green, + m_blue, + newWhitePoint.toXYZ(), + }; +} + +QMatrix4x4 Colorimetry::relativeColorimetricTo(const Colorimetry &other) const +{ + return other.fromXYZ() * chromaticAdaptationMatrix(white(), other.white()) * toXYZ(); +} + +QMatrix4x4 Colorimetry::absoluteColorimetricTo(const Colorimetry &other) const +{ + return other.fromXYZ() * toXYZ(); +} + +bool Colorimetry::operator==(const Colorimetry &other) const +{ + return red() == other.red() && green() == other.green() && blue() == other.blue() && white() == other.white(); +} + +const XYZ &Colorimetry::red() const +{ + return m_red; +} + +const XYZ &Colorimetry::green() const +{ + return m_green; +} + +const XYZ &Colorimetry::blue() const +{ + return m_blue; +} + +const XYZ &Colorimetry::white() const +{ + return m_white; +} + +const Colorimetry Colorimetry::BT709 = Colorimetry{ + xy{0.64, 0.33}, + xy{0.30, 0.60}, + xy{0.15, 0.06}, + xy{0.3127, 0.3290}, +}; +const Colorimetry Colorimetry::PAL_M = Colorimetry{ + xy{0.67, 0.33}, + xy{0.21, 0.71}, + xy{0.14, 0.08}, + xy{0.310, 0.316}, +}; +const Colorimetry Colorimetry::PAL = Colorimetry{ + xy{0.640, 0.330}, + xy{0.290, 0.600}, + xy{0.150, 0.060}, + xy{0.3127, 0.3290}, +}; +const Colorimetry Colorimetry::NTSC = Colorimetry{ + xy{0.630, 0.340}, + xy{0.310, 0.595}, + xy{0.155, 0.070}, + xy{0.3127, 0.3290}, +}; +const Colorimetry Colorimetry::GenericFilm = Colorimetry{ + xy{0.681, 0.319}, + xy{0.243, 0.692}, + xy{0.145, 0.049}, + xy{0.310, 0.316}, +}; +const Colorimetry Colorimetry::BT2020 = Colorimetry{ + xy{0.708, 0.292}, + xy{0.170, 0.797}, + xy{0.131, 0.046}, + xy{0.3127, 0.3290}, +}; +const Colorimetry Colorimetry::CIEXYZ = Colorimetry{ + XYZ{1.0, 0.0, 0.0}, + XYZ{0.0, 1.0, 0.0}, + XYZ{0.0, 0.0, 1.0}, + xy{1.0 / 3.0, 1.0 / 3.0}.toXYZ(), +}; +const Colorimetry Colorimetry::DCIP3 = Colorimetry{ + xy{0.680, 0.320}, + xy{0.265, 0.690}, + xy{0.150, 0.060}, + xy{0.314, 0.351}, +}; +const Colorimetry Colorimetry::DisplayP3 = Colorimetry{ + xy{0.680, 0.320}, + xy{0.265, 0.690}, + xy{0.150, 0.060}, + xy{0.3127, 0.3290}, +}; +const Colorimetry Colorimetry::AdobeRGB = Colorimetry{ + xy{0.6400, 0.3300}, + xy{0.2100, 0.7100}, + xy{0.1500, 0.0600}, + xy{0.3127, 0.3290}, +}; + +const std::shared_ptr ColorDescription::sRGB = std::make_shared(Colorimetry::BT709, TransferFunction(TransferFunction::gamma22)); +const std::shared_ptr ColorDescription::BT2020PQ = std::make_shared(Colorimetry::BT2020, TransferFunction(TransferFunction::PerceptualQuantizer)); + +ColorDescription::ColorDescription(const Colorimetry &containerColorimetry, TransferFunction tf, + double referenceLuminance, double minLuminance, std::optional maxAverageLuminance, std::optional maxHdrLuminance, + YUVMatrixCoefficients yuvCoefficients, EncodingRange range) + : ColorDescription(containerColorimetry, tf, referenceLuminance, minLuminance, maxAverageLuminance, maxHdrLuminance, containerColorimetry, Colorimetry::BT709, yuvCoefficients, range) +{ +} + +ColorDescription::ColorDescription(const Colorimetry &containerColorimetry, TransferFunction tf, + double referenceLuminance, double minLuminance, std::optional maxAverageLuminance, std::optional maxHdrLuminance, + const Colorimetry &masteringColorimetry, const Colorimetry &sdrColorimetry, + YUVMatrixCoefficients yuvCoefficients, EncodingRange range) + : m_containerColorimetry(containerColorimetry) + , m_masteringColorimetry(masteringColorimetry) + , m_transferFunction(tf) + , m_sdrColorimetry(sdrColorimetry) + , m_referenceLuminance(referenceLuminance) + , m_minLuminance(minLuminance) + , m_maxAverageLuminance(maxAverageLuminance) + , m_maxHdrLuminance(maxHdrLuminance) + , m_yuvCoefficients(yuvCoefficients) + , m_range(range) +{ +} + +ColorDescription::ColorDescription(const Colorimetry &containerColorimetry, TransferFunction tf, YUVMatrixCoefficients yuvCoefficients, EncodingRange range) + : ColorDescription(containerColorimetry, tf, TransferFunction::defaultReferenceLuminanceFor(tf.type), tf.minLuminance, tf.maxLuminance, tf.maxLuminance, yuvCoefficients, range) +{ +} + +const Colorimetry &ColorDescription::containerColorimetry() const +{ + return m_containerColorimetry; +} + +const Colorimetry &ColorDescription::masteringColorimetry() const +{ + return m_masteringColorimetry; +} + +const Colorimetry &ColorDescription::sdrColorimetry() const +{ + return m_sdrColorimetry; +} + +TransferFunction ColorDescription::transferFunction() const +{ + return m_transferFunction; +} + +double ColorDescription::referenceLuminance() const +{ + return m_referenceLuminance; +} + +double ColorDescription::minLuminance() const +{ + return m_minLuminance; +} + +std::optional ColorDescription::maxAverageLuminance() const +{ + return m_maxAverageLuminance; +} + +std::optional ColorDescription::maxHdrLuminance() const +{ + return m_maxHdrLuminance; +} + +YUVMatrixCoefficients ColorDescription::yuvCoefficients() const +{ + return m_yuvCoefficients; +} + +EncodingRange ColorDescription::range() const +{ + return m_range; +} + +/** + * @returns a matrix that converts colors in the specified YCbCr (full range: Y[0; 1] and CbCr[-0.5; 0.5]) to RGB ([0; 1]) + */ +static QMatrix4x4 calculateYuvToRgbMatrix(double kr, double kg, double kb, EncodingRange range) +{ + const QMatrix4x4 conversion( + 1, 0, 2 - 2 * kr, 0.0, + 1, -kb / kg * (2 - 2 * kb), -kr / kg * (2 - 2 * kr), 0.0, + 1, 2 - 2 * kb, 0, 0.0, + 0.0, 0.0, 0.0, 1.0); + if (range == EncodingRange::Limited) { + QMatrix4x4 limitedToFullRangeYCbCr; + limitedToFullRangeYCbCr.scale(255.0 / 219.0, 255.0 / 224.0, 255.0 / 224.0); + limitedToFullRangeYCbCr.translate(-16.0 / 255.0, -0.5, -0.5); + return conversion * limitedToFullRangeYCbCr; + } else { + QMatrix4x4 chromaConversion; + chromaConversion.translate(0, -0.5, -0.5); + return conversion * chromaConversion; + } +} + +static const QMatrix4x4 s_limitedRangeBT601 = calculateYuvToRgbMatrix(0.299, 0.587, 0.114, EncodingRange::Limited); +static const QMatrix4x4 s_fullRangeBT601 = calculateYuvToRgbMatrix(0.299, 0.587, 0.114, EncodingRange::Full); +static const QMatrix4x4 s_limitedRangeBT709 = calculateYuvToRgbMatrix(0.2126, 0.7152, 0.0722, EncodingRange::Limited); +static const QMatrix4x4 s_fullRangeBT709 = calculateYuvToRgbMatrix(0.2126, 0.7152, 0.0722, EncodingRange::Full); +static const QMatrix4x4 s_limitedRangeBT2020 = calculateYuvToRgbMatrix(0.2627, 0.6780, 0.0593, EncodingRange::Limited); +static const QMatrix4x4 s_fullRangeBT2020 = calculateYuvToRgbMatrix(0.2627, 0.6780, 0.0593, EncodingRange::Full); + +QMatrix4x4 ColorDescription::yuvMatrix() const +{ + switch (m_yuvCoefficients) { + case YUVMatrixCoefficients::Identity: + Q_ASSERT(m_range == EncodingRange::Full); + return QMatrix4x4(); + case YUVMatrixCoefficients::BT601: + if (m_range == EncodingRange::Limited) { + return s_limitedRangeBT601; + } else { + return s_fullRangeBT601; + } + case YUVMatrixCoefficients::BT709: + if (m_range == EncodingRange::Limited) { + return s_limitedRangeBT709; + } else { + return s_fullRangeBT709; + } + case YUVMatrixCoefficients::BT2020: + if (m_range == EncodingRange::Limited) { + return s_limitedRangeBT2020; + } else { + return s_fullRangeBT2020; + } + } + Q_UNREACHABLE(); +} + +QMatrix4x4 ColorDescription::toOther(const ColorDescription &other, RenderingIntent intent) const +{ + QMatrix4x4 luminanceBefore; + QMatrix4x4 luminanceAfter; + if (intent == RenderingIntent::Perceptual || intent == RenderingIntent::RelativeColorimetricWithBPC) { + // add black point compensation: black and reference white from the source color space + // should both be mapped to black and reference white in the destination color space + + const double effectiveMin = std::max(minLuminance(), m_transferFunction.minLuminance); + const double otherEffectiveMin = std::max(other.minLuminance(), other.m_transferFunction.minLuminance); + + // before color conversions, map [src min, src ref] to [0, 1] + luminanceBefore.scale(1.0 / (referenceLuminance() - minLuminance())); + luminanceBefore.translate(-effectiveMin, -effectiveMin, -effectiveMin); + // afterwards, map [0, 1] again to [dst min, dst ref] + luminanceAfter.translate(otherEffectiveMin, otherEffectiveMin, otherEffectiveMin); + luminanceAfter.scale(other.referenceLuminance() - other.minLuminance()); + } else { + // map only the reference luminance + luminanceBefore.scale(other.referenceLuminance() / referenceLuminance()); + } + switch (intent) { + case RenderingIntent::Perceptual: { + const Colorimetry &srcContainer = containerColorimetry() == Colorimetry::BT709 ? other.sdrColorimetry() : containerColorimetry(); + return luminanceAfter * other.containerColorimetry().fromXYZ() * Colorimetry::chromaticAdaptationMatrix(srcContainer.white(), other.containerColorimetry().white()) * srcContainer.toXYZ() * luminanceBefore; + } + case RenderingIntent::RelativeColorimetric: { + return luminanceAfter * other.containerColorimetry().fromXYZ() * Colorimetry::chromaticAdaptationMatrix(containerColorimetry().white(), other.containerColorimetry().white()) * containerColorimetry().toXYZ() * luminanceBefore; + } + case RenderingIntent::RelativeColorimetricWithBPC: { + return luminanceAfter * other.containerColorimetry().fromXYZ() * Colorimetry::chromaticAdaptationMatrix(containerColorimetry().white(), other.containerColorimetry().white()) * containerColorimetry().toXYZ() * luminanceBefore; + } + case RenderingIntent::AbsoluteColorimetricNoAdaptation: { + return luminanceAfter * other.containerColorimetry().fromXYZ() * containerColorimetry().toXYZ() * luminanceBefore; + } + } + Q_UNREACHABLE(); +} + +QVector3D ColorDescription::mapTo(QVector3D rgb, const ColorDescription &dst, RenderingIntent intent) const +{ + rgb = m_transferFunction.encodedToNits(rgb); + rgb = toOther(dst, intent) * rgb; + return dst.transferFunction().nitsToEncoded(rgb); +} + +std::shared_ptr ColorDescription::withTransferFunction(const TransferFunction &func) const +{ + return std::make_shared(m_containerColorimetry, func, m_referenceLuminance, m_minLuminance, m_maxAverageLuminance, m_maxHdrLuminance, m_masteringColorimetry, m_sdrColorimetry); +} + +std::shared_ptr ColorDescription::withWhitepoint(xyY newWhitePoint) const +{ + return std::make_shared(ColorDescription{ + m_containerColorimetry.withWhitepoint(newWhitePoint), + m_transferFunction, + m_referenceLuminance, + m_minLuminance, + m_maxAverageLuminance, + m_maxHdrLuminance, + m_masteringColorimetry.withWhitepoint(newWhitePoint), + m_sdrColorimetry, + }); +} + +std::shared_ptr ColorDescription::dimmed(double brightnessFactor) const +{ + return std::make_shared(ColorDescription{ + m_containerColorimetry, + m_transferFunction, + m_referenceLuminance * brightnessFactor, + m_minLuminance, + m_maxAverageLuminance.transform([&](double value) { + return value * brightnessFactor; + }), + m_maxHdrLuminance.transform([&](double value) { + return value * brightnessFactor; + }), + m_masteringColorimetry, + m_sdrColorimetry, + }); +} + +std::shared_ptr ColorDescription::withReference(double referenceLuminance) const +{ + return std::make_shared(ColorDescription{ + m_containerColorimetry, + m_transferFunction, + referenceLuminance, + m_minLuminance, + m_maxAverageLuminance, + m_maxHdrLuminance, + m_masteringColorimetry, + m_sdrColorimetry, + }); +} + +std::shared_ptr ColorDescription::withHdrMetadata(double maxAverageLuminance, double maxLuminance) const +{ + return std::make_shared(ColorDescription{ + m_containerColorimetry, + m_transferFunction, + m_referenceLuminance, + m_minLuminance, + maxAverageLuminance, + maxLuminance, + m_masteringColorimetry, + m_sdrColorimetry, + }); +} + +std::shared_ptr ColorDescription::withYuvCoefficients(YUVMatrixCoefficients coefficient, EncodingRange range) const +{ + return std::make_shared(ColorDescription{ + m_containerColorimetry, + m_transferFunction, + m_referenceLuminance, + m_minLuminance, + m_maxAverageLuminance, + m_maxHdrLuminance, + m_masteringColorimetry, + m_sdrColorimetry, + coefficient, + range, + }); +} + +double TransferFunction::defaultMinLuminanceFor(Type type) +{ + switch (type) { + case Type::sRGB: + case Type::gamma22: + case Type::linear: + return 0.2; + case Type::BT1886: + return 0.01; + case Type::PerceptualQuantizer: + return 0.005; + } + Q_UNREACHABLE(); +} + +double TransferFunction::defaultMaxLuminanceFor(Type type) +{ + switch (type) { + case Type::sRGB: + case Type::gamma22: + case Type::linear: + return 80; + case Type::BT1886: + return 100; + case Type::PerceptualQuantizer: + return 10'000; + } + Q_UNREACHABLE(); +} + +double TransferFunction::defaultReferenceLuminanceFor(Type type) +{ + switch (type) { + case Type::PerceptualQuantizer: + return 203; + case Type::linear: + case Type::sRGB: + case Type::gamma22: + return 80; + case Type::BT1886: + return 100; + } + Q_UNREACHABLE(); +} + +bool TransferFunction::operator==(const TransferFunction &other) const +{ + // allow for a greater error with large max. luminance, as floating point errors get larger there + // and the effect of errors is smaller too + return type == other.type + && std::abs(other.minLuminance - minLuminance) < ColorPipeline::s_maxResolution + && std::abs(other.maxLuminance - maxLuminance) < ColorPipeline::s_maxResolution * maxLuminance; +} + +TransferFunction::TransferFunction(Type tf) + : TransferFunction(tf, defaultMinLuminanceFor(tf), defaultMaxLuminanceFor(tf)) +{ +} + +TransferFunction::TransferFunction(Type tf, double minLuminance, double maxLuminance) + : type(tf) + , minLuminance(minLuminance) + , maxLuminance(maxLuminance) +{ +} + +double TransferFunction::encodedToNits(double encoded) const +{ + switch (type) { + case TransferFunction::sRGB: { + if (encoded < 0.04045) { + return std::max(encoded / 12.92, 0.0) * (maxLuminance - minLuminance) + minLuminance; + } else { + return std::clamp(std::pow((encoded + 0.055) / 1.055, 12.0 / 5.0), 0.0, 1.0) * (maxLuminance - minLuminance) + minLuminance; + } + } + case TransferFunction::gamma22: + return std::pow(encoded, 2.2) * (maxLuminance - minLuminance) + minLuminance; + case TransferFunction::linear: + return encoded * (maxLuminance - minLuminance) + minLuminance; + case TransferFunction::PerceptualQuantizer: { + const double c1 = 0.8359375; + const double c2 = 18.8515625; + const double c3 = 18.6875; + const double m1_inv = 1.0 / 0.1593017578125; + const double m2_inv = 1.0 / 78.84375; + const double powed = std::pow(encoded, m2_inv); + const double num = std::max(powed - c1, 0.0); + const double den = c2 - c3 * powed; + return std::pow(num / den, m1_inv) * (maxLuminance - minLuminance) + minLuminance; + } + case TransferFunction::BT1886: { + constexpr double gamma = 2.4; + const double minLumPow = std::pow(minLuminance, 1.0 / gamma); + const double tmp = std::pow(maxLuminance, 1.0 / gamma) - minLumPow; + const double alpha = std::pow(tmp, gamma); + const double beta = minLumPow / tmp; + return alpha * std::pow(std::max(encoded + beta, 0.0), gamma); + } + } + Q_UNREACHABLE(); +} + +QVector3D TransferFunction::encodedToNits(const QVector3D &encoded) const +{ + return QVector3D(encodedToNits(encoded.x()), encodedToNits(encoded.y()), encodedToNits(encoded.z())); +} + +QVector4D TransferFunction::encodedToNits(const QVector4D &encoded) const +{ + return QVector4D(encodedToNits(encoded.x()), encodedToNits(encoded.y()), encodedToNits(encoded.z()), encoded.w()); +} + +double TransferFunction::nitsToEncoded(double nits) const +{ + const double normalized = (nits - minLuminance) / (maxLuminance - minLuminance); + switch (type) { + case TransferFunction::sRGB: { + if (normalized < 0.0031308) { + return std::max(normalized / 12.92, 0.0); + } else { + return std::clamp(std::pow(normalized, 5.0 / 12.0) * 1.055 - 0.055, 0.0, 1.0); + } + } + case TransferFunction::gamma22: + return std::pow(std::clamp(normalized, 0.0, 1.0), 1.0 / 2.2); + case TransferFunction::linear: + return normalized; + case TransferFunction::PerceptualQuantizer: { + const double c1 = 0.8359375; + const double c2 = 18.8515625; + const double c3 = 18.6875; + const double m1 = 0.1593017578125; + const double m2 = 78.84375; + const double powed = std::pow(std::clamp(normalized, 0.0, 1.0), m1); + const double num = c1 + c2 * powed; + const double denum = 1 + c3 * powed; + return std::pow(num / denum, m2); + } + case TransferFunction::BT1886: { + constexpr double gamma = 2.4; + const double minLumPow = std::pow(minLuminance, 1.0 / gamma); + const double tmp = std::pow(maxLuminance, 1.0 / gamma) - minLumPow; + const double alpha = std::pow(tmp, gamma); + const double beta = minLumPow / tmp; + return std::pow(nits / alpha, 1.0 / gamma) - beta; + } + } + Q_UNREACHABLE(); +} + +QVector3D TransferFunction::nitsToEncoded(const QVector3D &nits) const +{ + return QVector3D(nitsToEncoded(nits.x()), nitsToEncoded(nits.y()), nitsToEncoded(nits.z())); +} + +QVector4D TransferFunction::nitsToEncoded(const QVector4D &nits) const +{ + return QVector4D(nitsToEncoded(nits.x()), nitsToEncoded(nits.y()), nitsToEncoded(nits.z()), nits.w()); +} + +bool TransferFunction::isRelative() const +{ + switch (type) { + case TransferFunction::gamma22: + case TransferFunction::sRGB: + case TransferFunction::BT1886: + return true; + case TransferFunction::linear: + case TransferFunction::PerceptualQuantizer: + return false; + } + Q_UNREACHABLE(); +} + +bool TransferFunction::hasLinearMinLuminance() const +{ + // With BT1886, min luminance is part of an electrical offset + // and causes non-linear changes to the curve + return type != TransferFunction::BT1886; +} + +TransferFunction TransferFunction::relativeScaledTo(double referenceLuminance) const +{ + if (isRelative()) { + return TransferFunction(type, minLuminance * referenceLuminance / maxLuminance, referenceLuminance); + } else { + return *this; + } +} + +double TransferFunction::bt1886A() const +{ + return std::pow(std::pow(maxLuminance, 1.0 / 2.4) - std::pow(minLuminance, 1.0 / 2.4), 2.4); +} + +double TransferFunction::bt1886B() const +{ + const double powBlack = std::pow(minLuminance, 1.0 / 2.4); + const double powWhite = std::pow(maxLuminance, 1.0 / 2.4); + return powBlack / (powWhite - powBlack); +} +} + +QDebug operator<<(QDebug debug, const KWin::TransferFunction &tf) +{ + QDebugStateSaver state(debug); + debug.nospace() << "TransferFunction(" << tf.type << ", [" << tf.minLuminance << "," << tf.maxLuminance << "] )"; + return debug; +} + +QDebug operator<<(QDebug debug, const KWin::XYZ &xyz) +{ + QDebugStateSaver state(debug); + debug.nospace() << "XYZ(" << xyz.X << ", " << xyz.Y << ", " << xyz.Z << ")"; + return debug; +} + +QDebug operator<<(QDebug debug, const KWin::xyY &xyY) +{ + QDebugStateSaver state(debug); + debug.nospace() << "xyY(" << xyY.x << ", " << xyY.y << ", " << xyY.Y << ")"; + return debug; +} + +QDebug operator<<(QDebug debug, const KWin::xy &xy) +{ + QDebugStateSaver state(debug); + debug.nospace() << "xy(" << xy.x << ", " << xy.y << ")"; + return debug; +} + +QDebug operator<<(QDebug debug, const KWin::Colorimetry &color) +{ + QDebugStateSaver state(debug); + debug.nospace() << "Colorimetry(" << color.red() << ", " << color.green() << ", " << color.blue() << ", " << color.white() << ")"; + return debug; +} + +QDebug operator<<(QDebug debug, const KWin::ColorDescription &color) +{ + QDebugStateSaver state(debug); + debug << "ColorDescription(" << color.containerColorimetry() << ", " << color.transferFunction() << ", ref" << color.referenceLuminance() << ", min" << color.minLuminance() << ", max. avg" << color.maxAverageLuminance() << ", max" << color.maxHdrLuminance() << ")"; + return debug; +} diff --git a/local/recipes/kde/kwin/source/src/core/colorspace.h b/local/recipes/kde/kwin/source/src/core/colorspace.h new file mode 100644 index 0000000000..280bf674fa --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/colorspace.h @@ -0,0 +1,310 @@ +/* + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include + +#include +#include + +#include "kwin_export.h" + +namespace KWin +{ + +/** + * rendering intents describe how colors should be mapped between different color spaces + */ +enum class RenderingIntent { + /* "vendor specific", preserves the overall color appearance */ + Perceptual, + /* "vendor specific", maps saturated colors to be saturated in the target color space too */ + // TODO Saturation, + /* colorimetric mapping between color spaces, with whitepoint adaptation */ + RelativeColorimetric, + /* colorimetric mapping between color spaces, without whitepoint adaptation */ + AbsoluteColorimetricNoAdaptation, + /* colorimetric mapping between color spaces, with whitepoint adaptation and black point compensation */ + RelativeColorimetricWithBPC, +}; + +struct XYZ; +/** + * xyY, with Y unspecified + */ +struct KWIN_EXPORT xy +{ + double x; + double y; + + XYZ toXYZ() const; + QVector2D asVector() const; + bool operator==(const xy &other) const; +}; +struct KWIN_EXPORT xyY +{ + double x; + double y; + double Y; + + XYZ toXYZ() const; + bool operator==(const xyY &other) const; +}; +struct KWIN_EXPORT XYZ +{ + double X; + double Y; + double Z; + + xyY toxyY() const; + xy toxy() const; + QVector3D asVector() const; + XYZ operator*(double factor) const; + XYZ operator/(double factor) const; + XYZ operator+(const XYZ &other) const; + bool operator==(const XYZ &other) const; + + static XYZ fromVector(const QVector3D &vector); +}; + +/** + * Describes the definition of colors in a color space. + * Red, green and blue define the chromaticities ("absolute colors") of the red, green and blue LEDs on a display in xy coordinates + * White defines the the chromaticity of the reference white in xy coordinates + */ +class KWIN_EXPORT Colorimetry +{ +public: + static const Colorimetry BT709; + static const Colorimetry PAL_M; + static const Colorimetry PAL; + static const Colorimetry NTSC; + static const Colorimetry GenericFilm; + static const Colorimetry BT2020; + static const Colorimetry CIEXYZ; + static const Colorimetry DCIP3; + static const Colorimetry DisplayP3; + static const Colorimetry AdobeRGB; + + /** + * @returns a matrix adapting XYZ values from the source whitepoint to the destination whitepoint with the Bradford transform + */ + static QMatrix4x4 chromaticAdaptationMatrix(XYZ sourceWhitepoint, XYZ destinationWhitepoint); + + static QMatrix4x4 calculateToXYZMatrix(XYZ red, XYZ green, XYZ blue, XYZ white); + + /** + * checks if the colorimetry is sane and won't cause crashes or glitches + */ + static bool isValid(xy red, xy green, xy blue, xy white); + /** + * checks if the colorimetry could be from a real display + */ + static bool isReal(xy red, xy green, xy blue, xy white); + + explicit Colorimetry(XYZ red, XYZ green, XYZ blue, XYZ white); + explicit Colorimetry(xyY red, xyY green, xyY blue, xyY white); + explicit Colorimetry(xy red, xy green, xy blue, xy white); + + /** + * @returns a matrix that transforms from the linear RGB representation of colors in this colorimetry to the XYZ representation + */ + const QMatrix4x4 &toXYZ() const; + /** + * @returns a matrix that transforms from the XYZ representation to the linear RGB representation of colors in this colorimetry + */ + const QMatrix4x4 &fromXYZ() const; + QMatrix4x4 toLMS() const; + QMatrix4x4 fromLMS() const; + + bool operator==(const Colorimetry &other) const; + /** + * @returns this colorimetry, adapted to the new whitepoint using the Bradford transform + */ + Colorimetry adaptedTo(xyY newWhitepoint) const; + /** + * replaces the current whitepoint with the new one + * this does not do whitepoint adaptation! + */ + Colorimetry withWhitepoint(xyY newWhitePoint) const; + /** + * interpolates the primaries depending on the passed factor. The whitepoint stays unchanged + */ + Colorimetry interpolateGamutTo(const Colorimetry &one, double factor) const; + + QMatrix4x4 relativeColorimetricTo(const Colorimetry &other) const; + QMatrix4x4 absoluteColorimetricTo(const Colorimetry &other) const; + + const XYZ &red() const; + const XYZ &green() const; + const XYZ &blue() const; + const XYZ &white() const; + +private: + XYZ m_red; + XYZ m_green; + XYZ m_blue; + XYZ m_white; + QMatrix4x4 m_toXYZ; + QMatrix4x4 m_fromXYZ; +}; + +/** + * Describes an EOTF - how encoded values are converted to light + */ +class KWIN_EXPORT TransferFunction +{ +public: + enum Type { + sRGB = 0, + linear = 1, + PerceptualQuantizer = 2, + gamma22 = 3, + BT1886 = 4, + }; + explicit TransferFunction(Type tf); + explicit TransferFunction(Type tf, double minLuminance, double maxLuminance); + + bool operator==(const TransferFunction &) const; + + bool hasLinearMinLuminance() const; + bool isRelative() const; + TransferFunction relativeScaledTo(double referenceLuminance) const; + double encodedToNits(double encoded) const; + double nitsToEncoded(double nits) const; + QVector3D encodedToNits(const QVector3D &encoded) const; + QVector3D nitsToEncoded(const QVector3D &nits) const; + QVector4D encodedToNits(const QVector4D &encoded) const; + QVector4D nitsToEncoded(const QVector4D &nits) const; + + double bt1886A() const; + double bt1886B() const; + + Type type; + /** + * the luminance at encoded value zero + */ + double minLuminance; + /** + * the luminance at encoded value 1 + */ + double maxLuminance; + + static double defaultMinLuminanceFor(Type type); + static double defaultMaxLuminanceFor(Type type); + static double defaultReferenceLuminanceFor(Type type); +}; + +enum class YUVMatrixCoefficients { + Identity, + BT601, + BT709, + BT2020, +}; + +enum class EncodingRange { + Limited, + Full, +}; + +/** + * Describes the meaning of encoded color values, with additional metadata for how to convert between different encodings + * Note that not all properties of this description are relevant in all contexts + */ +class KWIN_EXPORT ColorDescription +{ +public: + /** + * @param containerColorimetry the container colorimety of this description + * @param tf the transfer function of this description + * @param referenceLuminance the brightness of SDR content + * @param minLuminance the minimum brightness of HDR content + * @param maxAverageLuminance the maximum brightness of HDR content, if the whole screen is white + * @param maxHdrLuminance the maximum brightness of HDR content, for a small part of the screen only + * @param sdrColorimetry + */ + explicit ColorDescription(const Colorimetry &containerColorimetry, TransferFunction tf, double referenceLuminance, double minLuminance, std::optional maxAverageLuminance, std::optional maxHdrLuminance, YUVMatrixCoefficients yuvCoefficients = YUVMatrixCoefficients::Identity, EncodingRange range = EncodingRange::Full); + explicit ColorDescription(const Colorimetry &containerColorimetry, TransferFunction tf, double referenceLuminance, double minLuminance, std::optional maxAverageLuminance, std::optional maxHdrLuminance, const Colorimetry &masteringColorimetry, const Colorimetry &sdrColorimetry, YUVMatrixCoefficients yuvCoefficients = YUVMatrixCoefficients::Identity, EncodingRange range = EncodingRange::Full); + explicit ColorDescription(const Colorimetry &containerColorimetry, TransferFunction tf, YUVMatrixCoefficients yuvCoefficients = YUVMatrixCoefficients::Identity, EncodingRange range = EncodingRange::Full); + + /** + * The primaries and whitepoint that colors are encoded for. This is used to convert between different colorspaces. + * In most cases this will be the rec.709 primaries for SDR, or rec.2020 for HDR + */ + const Colorimetry &containerColorimetry() const; + /** + * The mastering colorimetry contains all colors that the image actually (may) contain, which can be used to improve the conversion to a different color description. + * In most cases this will be smaller than the container colorimetry; for example a screen with an HDR mode but only rec.709 colors would have a container colorimetry of rec.2020 and a mastering colorimetry of rec.709. + * In some cases however it can be bigger than the container colorimetry, like with scRGB. It has the container colorimetry of sRGB, but a mastering colorimetry that can be bigger (like rec.2020 for example) + */ + const Colorimetry &masteringColorimetry() const; + const Colorimetry &sdrColorimetry() const; + TransferFunction transferFunction() const; + double referenceLuminance() const; + double minLuminance() const; + std::optional maxAverageLuminance() const; + std::optional maxHdrLuminance() const; + YUVMatrixCoefficients yuvCoefficients() const; + EncodingRange range() const; + + /** + * @returns the matrix that converts from this ColorDescription's encoding to full range RGB + * TODO move this to ColorPipeline, to deal with ICtCp + */ + QMatrix4x4 yuvMatrix() const; + + bool operator==(const ColorDescription &other) const = default; + + std::shared_ptr withTransferFunction(const TransferFunction &func) const; + /** + * replaces the current whitepoint with the new one + * this does not do whitepoint adaptation! + */ + std::shared_ptr withWhitepoint(xyY newWhitePoint) const; + std::shared_ptr dimmed(double brightnessFactor) const; + std::shared_ptr withReference(double referenceLuminance) const; + std::shared_ptr withHdrMetadata(double maxAverageLuminance, double maxLuminance) const; + std::shared_ptr withYuvCoefficients(YUVMatrixCoefficients coefficient, EncodingRange range) const; + + /** + * @returns a matrix that transforms from linear RGB in this color description to linear RGB in the other one + */ + QMatrix4x4 toOther(const ColorDescription &other, RenderingIntent intent) const; + QVector3D mapTo(QVector3D rgb, const ColorDescription &other, RenderingIntent intent) const; + + /** + * This color description describes display-referred sRGB, with a gamma22 transfer function + */ + static const std::shared_ptr sRGB; + static const std::shared_ptr BT2020PQ; + +private: + Colorimetry m_containerColorimetry; + Colorimetry m_masteringColorimetry; + TransferFunction m_transferFunction; + Colorimetry m_sdrColorimetry; + double m_referenceLuminance; + double m_minLuminance; + std::optional m_maxAverageLuminance; + std::optional m_maxHdrLuminance; + YUVMatrixCoefficients m_yuvCoefficients = YUVMatrixCoefficients::Identity; + EncodingRange m_range = EncodingRange::Full; +}; +} + +inline bool operator==(const std::shared_ptr &left, const std::shared_ptr &right) +{ + if (!left || !right) { + return left.get() == right.get(); + } + return left.get() == right.get() || *left == *right; +} + +KWIN_EXPORT QDebug operator<<(QDebug debug, const KWin::TransferFunction &tf); +KWIN_EXPORT QDebug operator<<(QDebug debug, const KWin::XYZ &xyz); +KWIN_EXPORT QDebug operator<<(QDebug debug, const KWin::xyY &xyY); +KWIN_EXPORT QDebug operator<<(QDebug debug, const KWin::xy &xy); +KWIN_EXPORT QDebug operator<<(QDebug debug, const KWin::Colorimetry &color); +KWIN_EXPORT QDebug operator<<(QDebug debug, const KWin::ColorDescription &color); diff --git a/local/recipes/kde/kwin/source/src/core/colortransformation.cpp b/local/recipes/kde/kwin/source/src/core/colortransformation.cpp new file mode 100644 index 0000000000..03a688f400 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/colortransformation.cpp @@ -0,0 +1,112 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "colortransformation.h" +#include "colorpipelinestage.h" + +#include + +#include "utils/common.h" + +namespace KWin +{ + +ColorTransformation::ColorTransformation(std::vector> &&stages) + : m_pipeline(cmsPipelineAlloc(nullptr, 3, 3)) + , m_stages(std::move(stages)) +{ + if (!m_pipeline) { + qCWarning(KWIN_CORE) << "Failed to allocate cmsPipeline!"; + m_valid = false; + return; + } + for (auto &stage : m_stages) { + if (!cmsPipelineInsertStage(m_pipeline, cmsAT_END, stage->stage())) { + qCWarning(KWIN_CORE) << "Failed to insert cmsPipeline stage!"; + m_valid = false; + return; + } + } +} + +ColorTransformation::~ColorTransformation() +{ + if (m_pipeline) { + cmsStage *last = nullptr; + do { + cmsPipelineUnlinkStage(m_pipeline, cmsAT_END, &last); + } while (last); + cmsPipelineFree(m_pipeline); + } +} + +void ColorTransformation::append(ColorTransformation *transformation) +{ + for (auto &stage : transformation->m_stages) { + auto dup = stage->dup(); + if (!cmsPipelineInsertStage(m_pipeline, cmsAT_END, dup->stage())) { + qCWarning(KWIN_CORE) << "Failed to insert cmsPipeline stage!"; + m_valid = false; + return; + } + m_stages.push_back(std::move(dup)); + } +} + +bool ColorTransformation::valid() const +{ + return m_valid; +} + +std::tuple ColorTransformation::transform(uint16_t r, uint16_t g, uint16_t b) const +{ + const uint16_t in[3] = {r, g, b}; + uint16_t out[3] = {0, 0, 0}; + cmsPipelineEval16(in, out, m_pipeline); + return {out[0], out[1], out[2]}; +} + +QVector3D ColorTransformation::transform(QVector3D in) const +{ + QVector3D ret; + cmsPipelineEvalFloat(&in[0], &ret[0], m_pipeline); + return ret; +} + +std::unique_ptr ColorTransformation::createScalingTransform(const QVector3D &scale) +{ + std::array curveParams = {1.0, scale.x(), 0.0}; + auto r = cmsBuildParametricToneCurve(nullptr, 2, curveParams.data()); + curveParams = {1.0, scale.y(), 0.0}; + auto g = cmsBuildParametricToneCurve(nullptr, 2, curveParams.data()); + curveParams = {1.0, scale.z(), 0.0}; + auto b = cmsBuildParametricToneCurve(nullptr, 2, curveParams.data()); + const auto guard = qScopeGuard([r, g, b]() { + cmsFreeToneCurve(r); + cmsFreeToneCurve(g); + cmsFreeToneCurve(b); + }); + if (!r || !g || !b) { + qCWarning(KWIN_CORE) << "Failed to build tone curves"; + return nullptr; + } + const std::array curves = {r, g, b}; + const auto stage = cmsStageAllocToneCurves(nullptr, 3, curves.data()); + if (!stage) { + qCWarning(KWIN_CORE) << "Failed to allocate tone curves"; + return nullptr; + } + std::vector> stages; + stages.push_back(std::make_unique(stage)); + auto transform = std::make_unique(std::move(stages)); + if (!transform->valid()) { + return nullptr; + } + return transform; +} +} diff --git a/local/recipes/kde/kwin/source/src/core/colortransformation.h b/local/recipes/kde/kwin/source/src/core/colortransformation.h new file mode 100644 index 0000000000..f63a6f60f1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/colortransformation.h @@ -0,0 +1,47 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include +#include +#include + +#include "kwin_export.h" + +typedef struct _cmsPipeline_struct cmsPipeline; +class QVector3D; + +namespace KWin +{ + +class ColorPipelineStage; + +class KWIN_EXPORT ColorTransformation +{ +public: + ColorTransformation(std::vector> &&stages); + ~ColorTransformation(); + + void append(ColorTransformation *transformation); + + bool valid() const; + + std::tuple transform(uint16_t r, uint16_t g, uint16_t b) const; + QVector3D transform(QVector3D in) const; + + static std::unique_ptr createScalingTransform(const QVector3D &scale); + +private: + cmsPipeline *const m_pipeline; + std::vector> m_stages; + bool m_valid = true; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/core/drmdevice.cpp b/local/recipes/kde/kwin/source/src/core/drmdevice.cpp new file mode 100644 index 0000000000..236d9c37a7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/drmdevice.cpp @@ -0,0 +1,119 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2024 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "drmdevice.h" + +#include "gbmgraphicsbufferallocator.h" +#include "utils/common.h" + +#include +#include +#include +#include + +namespace KWin +{ + +DrmDevice::DrmDevice(const QString &path, dev_t id, FileDescriptor &&fd, gbm_device *gbmDevice) + : m_path(path) + , m_id(id) + , m_fd(std::move(fd)) + , m_gbmDevice(gbmDevice) + , m_allocator(std::make_unique(gbmDevice)) +{ + uint64_t value = 0; + m_supportsSyncObjTimelines = drmGetCap(m_fd.get(), DRM_CAP_SYNCOBJ_TIMELINE, &value) == 0 && value != 0; +} + +DrmDevice::~DrmDevice() +{ + gbm_device_destroy(m_gbmDevice); +} + +QString DrmDevice::path() const +{ + return m_path; +} + +dev_t DrmDevice::deviceId() const +{ + return m_id; +} + +gbm_device *DrmDevice::gbmDevice() const +{ + return m_gbmDevice; +} + +GraphicsBufferAllocator *DrmDevice::allocator() const +{ + return m_allocator.get(); +} + +int DrmDevice::fileDescriptor() const +{ + return m_fd.get(); +} + +bool DrmDevice::supportsSyncObjTimelines() const +{ + return m_supportsSyncObjTimelines; +} + +std::optional DrmDevice::pciDeviceInfo() const +{ + drmDevice *nativeDevice = nullptr; + auto nativeDeviceCleanup = qScopeGuard([&nativeDevice]() { + drmFreeDevice(&nativeDevice); + }); + + if (drmGetDeviceFromDevId(deviceId(), 0, &nativeDevice) != 0) { + return std::nullopt; + } + + if (nativeDevice->bustype != DRM_BUS_PCI) { + return std::nullopt; + } + + return *nativeDevice->deviceinfo.pci; +} + +std::unique_ptr DrmDevice::open(const QString &path) +{ + return openWithAuthentication(path, -1); +} + +std::unique_ptr DrmDevice::openWithAuthentication(const QString &path, int authenticatedFd) +{ + FileDescriptor fd(::open(path.toLocal8Bit(), O_RDWR | O_CLOEXEC)); + if (!fd.isValid()) { + qCWarning(KWIN_CORE) << "Failed to open drm node:" << path; + return nullptr; + } + struct stat buf; + if (fstat(fd.get(), &buf) == -1) { + qCWarning(KWIN_CORE) << "Failed to fstat drm fd" << path; + return nullptr; + } + if (authenticatedFd != -1) { + drm_magic_t magic; + if (drmGetMagic(fd.get(), &magic) < 0) { + qCDebug(KWIN_CORE) << "Failed to get the drm magic token for" << path; + } + if (drmAuthMagic(authenticatedFd, magic) < 0) { + qCWarning(KWIN_CORE) << "Failed to authenticate the drm magic token. path:" << path << "error:" << strerror(errno); + } + } + gbm_device *device = gbm_create_device(fd.get()); + if (!device) { + qCWarning(KWIN_CORE) << "Failed to create gbm device for" << path; + return nullptr; + } + return std::unique_ptr(new DrmDevice(path, buf.st_rdev, std::move(fd), device)); +} +} diff --git a/local/recipes/kde/kwin/source/src/core/drmdevice.h b/local/recipes/kde/kwin/source/src/core/drmdevice.h new file mode 100644 index 0000000000..adfa4dc6fb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/drmdevice.h @@ -0,0 +1,52 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2024 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "graphicsbufferallocator.h" +#include "kwin_export.h" +#include "utils/filedescriptor.h" + +#include + +#include +#include + +struct gbm_device; + +namespace KWin +{ + +class KWIN_EXPORT DrmDevice +{ +public: + ~DrmDevice(); + + QString path() const; + dev_t deviceId() const; + gbm_device *gbmDevice() const; + GraphicsBufferAllocator *allocator() const; + int fileDescriptor() const; + bool supportsSyncObjTimelines() const; + std::optional pciDeviceInfo() const; + + static std::unique_ptr open(const QString &path); + static std::unique_ptr openWithAuthentication(const QString &path, int authenticatedFd); + +private: + explicit DrmDevice(const QString &path, dev_t id, FileDescriptor &&fd, gbm_device *gbmDevice); + + const QString m_path; + const dev_t m_id; + const FileDescriptor m_fd; + gbm_device *const m_gbmDevice; + const std::unique_ptr m_allocator; + bool m_supportsSyncObjTimelines; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/core/gbmgraphicsbufferallocator.cpp b/local/recipes/kde/kwin/source/src/core/gbmgraphicsbufferallocator.cpp new file mode 100644 index 0000000000..98cf3369f8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/gbmgraphicsbufferallocator.cpp @@ -0,0 +1,339 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "core/gbmgraphicsbufferallocator.h" + +#include "config-kwin.h" + +#include "core/graphicsbuffer.h" +#include "utils/common.h" + +#include +#include +#include +#include +#include + +namespace KWin +{ + +static inline std::optional dmaBufAttributesForBo(gbm_bo *bo) +{ + DmaBufAttributes attributes; + attributes.planeCount = gbm_bo_get_plane_count(bo); + attributes.width = gbm_bo_get_width(bo); + attributes.height = gbm_bo_get_height(bo); + attributes.format = gbm_bo_get_format(bo); + attributes.modifier = gbm_bo_get_modifier(bo); + +#if HAVE_GBM_BO_GET_FD_FOR_PLANE + for (int i = 0; i < attributes.planeCount; ++i) { + attributes.fd[i] = FileDescriptor{gbm_bo_get_fd_for_plane(bo, i)}; + if (!attributes.fd[i].isValid()) { + qWarning() << "gbm_bo_get_fd_for_plane() failed:" << strerror(errno); + return std::nullopt; + } + attributes.offset[i] = gbm_bo_get_offset(bo, i); + attributes.pitch[i] = gbm_bo_get_stride_for_plane(bo, i); + } +#else + if (attributes.planeCount > 1) { + return attributes; + } + + attributes.fd[0] = FileDescriptor{gbm_bo_get_fd(bo)}; + if (!attributes.fd[0].isValid()) { + qWarning() << "gbm_bo_get_fd() failed:" << strerror(errno); + return std::nullopt; + } + attributes.offset[0] = gbm_bo_get_offset(bo, 0); + attributes.pitch[0] = gbm_bo_get_stride_for_plane(bo, 0); +#endif + + return attributes; +} + +class GbmGraphicsBuffer : public GraphicsBuffer +{ + Q_OBJECT + +public: + GbmGraphicsBuffer(DmaBufAttributes attributes, gbm_bo *handle); + ~GbmGraphicsBuffer() override; + + Map map(MapFlags flags) override; + void unmap() override; + + QSize size() const override; + bool hasAlphaChannel() const override; + const DmaBufAttributes *dmabufAttributes() const override; + +private: + gbm_bo *m_bo; + void *m_mapPtr = nullptr; + void *m_mapData = nullptr; + // the stride of the buffer mapping can be different from the stride of the buffer itself + uint32_t m_mapStride = 0; + DmaBufAttributes m_dmabufAttributes; + QSize m_size; + bool m_hasAlphaChannel; +}; + +class DumbGraphicsBuffer : public GraphicsBuffer +{ + Q_OBJECT + +public: + DumbGraphicsBuffer(int drmFd, uint32_t handle, DmaBufAttributes attributes); + ~DumbGraphicsBuffer() override; + + Map map(MapFlags flags) override; + void unmap() override; + + QSize size() const override; + bool hasAlphaChannel() const override; + const DmaBufAttributes *dmabufAttributes() const override; + +private: + int m_drmFd; + uint32_t m_handle; + void *m_data = nullptr; + size_t m_size = 0; + DmaBufAttributes m_dmabufAttributes; + bool m_hasAlphaChannel; +}; + +GbmGraphicsBufferAllocator::GbmGraphicsBufferAllocator(gbm_device *device) + : m_gbmDevice(device) +{ +} + +GbmGraphicsBufferAllocator::~GbmGraphicsBufferAllocator() +{ +} + +static GraphicsBuffer *allocateDumb(gbm_device *device, const GraphicsBufferOptions &options) +{ + if (!options.modifiers.isEmpty()) { + return nullptr; + } + + drm_mode_create_dumb createArgs{ + .height = uint32_t(options.size.height()), + .width = uint32_t(options.size.width()), + .bpp = 32, + }; + if (drmIoctl(gbm_device_get_fd(device), DRM_IOCTL_MODE_CREATE_DUMB, &createArgs) != 0) { + qCWarning(KWIN_CORE) << "DRM_IOCTL_MODE_CREATE_DUMB failed:" << strerror(errno); + return nullptr; + } + + int primeFd; + if (drmPrimeHandleToFD(gbm_device_get_fd(device), createArgs.handle, DRM_CLOEXEC, &primeFd) != 0) { + qCWarning(KWIN_CORE) << "drmPrimeHandleToFD() failed:" << strerror(errno); + drm_mode_destroy_dumb destroyArgs{ + .handle = createArgs.handle, + }; + drmIoctl(gbm_device_get_fd(device), DRM_IOCTL_MODE_DESTROY_DUMB, &destroyArgs); + return nullptr; + } + + return new DumbGraphicsBuffer(gbm_device_get_fd(device), createArgs.handle, DmaBufAttributes{ + .planeCount = 1, + .width = options.size.width(), + .height = options.size.height(), + .format = options.format, + .modifier = DRM_FORMAT_MOD_LINEAR, + .fd = {FileDescriptor(primeFd), FileDescriptor{}, FileDescriptor{}, FileDescriptor{}}, + .offset = {0, 0, 0, 0}, + .pitch = {createArgs.pitch, 0, 0, 0}, + }); +} + +static GraphicsBuffer *allocateDmaBuf(gbm_device *device, const GraphicsBufferOptions &options) +{ + if (!options.modifiers.isEmpty() && !(options.modifiers.size() == 1 && options.modifiers.first() == DRM_FORMAT_MOD_INVALID)) { + gbm_bo *bo = gbm_bo_create_with_modifiers(device, + options.size.width(), + options.size.height(), + options.format, + options.modifiers.constData(), + options.modifiers.size()); + if (bo) { + std::optional attributes = dmaBufAttributesForBo(bo); + if (!attributes.has_value()) { + gbm_bo_destroy(bo); + return nullptr; + } + return new GbmGraphicsBuffer(std::move(attributes.value()), bo); + } + } + + uint32_t flags = GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING; + if (options.modifiers.size() == 1 && options.modifiers.first() == DRM_FORMAT_MOD_LINEAR) { + flags |= GBM_BO_USE_LINEAR; + } else if (!options.modifiers.isEmpty() && !options.modifiers.contains(DRM_FORMAT_MOD_INVALID)) { + return nullptr; + } + + gbm_bo *bo = gbm_bo_create(device, + options.size.width(), + options.size.height(), + options.format, + flags); + if (bo) { + std::optional attributes = dmaBufAttributesForBo(bo); + if (!attributes.has_value()) { + gbm_bo_destroy(bo); + return nullptr; + } + if (flags & GBM_BO_USE_LINEAR) { + attributes->modifier = DRM_FORMAT_MOD_LINEAR; + } else { + attributes->modifier = DRM_FORMAT_MOD_INVALID; + } + return new GbmGraphicsBuffer(std::move(attributes.value()), bo); + } + + return nullptr; +} + +GraphicsBuffer *GbmGraphicsBufferAllocator::allocate(const GraphicsBufferOptions &options) +{ + if (options.software) { + return allocateDumb(m_gbmDevice, options); + } + + return allocateDmaBuf(m_gbmDevice, options); +} + +GbmGraphicsBuffer::GbmGraphicsBuffer(DmaBufAttributes attributes, gbm_bo *handle) + : m_bo(handle) + , m_dmabufAttributes(std::move(attributes)) + , m_size(m_dmabufAttributes.width, m_dmabufAttributes.height) + , m_hasAlphaChannel(alphaChannelFromDrmFormat(m_dmabufAttributes.format)) +{ +} + +GbmGraphicsBuffer::~GbmGraphicsBuffer() +{ + unmap(); + gbm_bo_destroy(m_bo); +} + +QSize GbmGraphicsBuffer::size() const +{ + return m_size; +} + +bool GbmGraphicsBuffer::hasAlphaChannel() const +{ + return m_hasAlphaChannel; +} + +const DmaBufAttributes *GbmGraphicsBuffer::dmabufAttributes() const +{ + return &m_dmabufAttributes; +} + +GraphicsBuffer::Map GbmGraphicsBuffer::map(MapFlags flags) +{ + if (!m_mapPtr) { + uint32_t access = 0; + if (flags & MapFlag::Read) { + access |= GBM_BO_TRANSFER_READ; + } + if (flags & MapFlag::Write) { + access |= GBM_BO_TRANSFER_WRITE; + } + m_mapPtr = gbm_bo_map(m_bo, 0, 0, m_dmabufAttributes.width, m_dmabufAttributes.height, access, &m_mapStride, &m_mapData); + } + return Map{ + .data = m_mapPtr, + .stride = m_mapStride, + }; +} + +void GbmGraphicsBuffer::unmap() +{ + if (m_mapPtr) { + gbm_bo_unmap(m_bo, m_mapData); + m_mapPtr = nullptr; + m_mapData = nullptr; + } +} + +DumbGraphicsBuffer::DumbGraphicsBuffer(int drmFd, uint32_t handle, DmaBufAttributes attributes) + : m_drmFd(drmFd) + , m_handle(handle) + , m_size(attributes.pitch[0] * attributes.height) + , m_dmabufAttributes(std::move(attributes)) + , m_hasAlphaChannel(alphaChannelFromDrmFormat(m_dmabufAttributes.format)) +{ +} + +DumbGraphicsBuffer::~DumbGraphicsBuffer() +{ + unmap(); + + drm_mode_destroy_dumb destroyArgs{ + .handle = m_handle, + }; + drmIoctl(m_drmFd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroyArgs); +} + +QSize DumbGraphicsBuffer::size() const +{ + return QSize(m_dmabufAttributes.width, m_dmabufAttributes.height); +} + +bool DumbGraphicsBuffer::hasAlphaChannel() const +{ + return m_hasAlphaChannel; +} + +const DmaBufAttributes *DumbGraphicsBuffer::dmabufAttributes() const +{ + return &m_dmabufAttributes; +} + +GraphicsBuffer::Map DumbGraphicsBuffer::map(MapFlags flags) +{ + if (!m_data) { + drm_mode_map_dumb mapArgs{ + .handle = m_handle, + }; + if (drmIoctl(m_drmFd, DRM_IOCTL_MODE_MAP_DUMB, &mapArgs) != 0) { + qCWarning(KWIN_CORE) << "DRM_IOCTL_MODE_MAP_DUMB failed:" << strerror(errno); + return {}; + } + + void *address = mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_drmFd, mapArgs.offset); + if (address == MAP_FAILED) { + qCWarning(KWIN_CORE) << "mmap() failed:" << strerror(errno); + return {}; + } + + m_data = address; + } + return Map{ + .data = m_data, + .stride = m_dmabufAttributes.pitch[0], + }; +} + +void DumbGraphicsBuffer::unmap() +{ + if (m_data) { + munmap(m_data, m_size); + m_data = nullptr; + } +} + +} // namespace KWin + +#include "gbmgraphicsbufferallocator.moc" +#include "moc_gbmgraphicsbufferallocator.cpp" diff --git a/local/recipes/kde/kwin/source/src/core/gbmgraphicsbufferallocator.h b/local/recipes/kde/kwin/source/src/core/gbmgraphicsbufferallocator.h new file mode 100644 index 0000000000..2c8f5e47fa --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/gbmgraphicsbufferallocator.h @@ -0,0 +1,28 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/graphicsbufferallocator.h" + +struct gbm_device; + +namespace KWin +{ + +class KWIN_EXPORT GbmGraphicsBufferAllocator : public GraphicsBufferAllocator +{ +public: + explicit GbmGraphicsBufferAllocator(gbm_device *device); + ~GbmGraphicsBufferAllocator() override; + + GraphicsBuffer *allocate(const GraphicsBufferOptions &options) override; + +private: + gbm_device *m_gbmDevice; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/graphicsbuffer.cpp b/local/recipes/kde/kwin/source/src/core/graphicsbuffer.cpp new file mode 100644 index 0000000000..ea3a149f2d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/graphicsbuffer.cpp @@ -0,0 +1,104 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "core/graphicsbuffer.h" +#include "utils/drm_format_helper.h" + +#include + +#include + +namespace KWin +{ + +GraphicsBuffer::GraphicsBuffer(QObject *parent) + : QObject(parent) +{ +} + +GraphicsBuffer::~GraphicsBuffer() +{ + Q_ASSERT(m_dropped); +} + +bool GraphicsBuffer::isReferenced() const +{ + return m_refCount > 0; +} + +bool GraphicsBuffer::isDropped() const +{ + return m_dropped; +} + +void GraphicsBuffer::ref() +{ + Q_ASSERT(QCoreApplication::instance()->thread() == thread()); + ++m_refCount; +} + +void GraphicsBuffer::unref() +{ + Q_ASSERT(QCoreApplication::instance()->thread() == thread()); + Q_ASSERT(m_refCount > 0); + --m_refCount; + if (!m_refCount) { + if (m_dropped) { + delete this; + } else { + m_releasePoints.clear(); + Q_EMIT released(); + } + } +} + +void GraphicsBuffer::drop() +{ + m_dropped = true; + + if (!m_refCount) { + delete this; + } +} + +GraphicsBuffer::Map GraphicsBuffer::map(MapFlags flags) +{ + return {}; +} + +void GraphicsBuffer::unmap() +{ +} + +const DmaBufAttributes *GraphicsBuffer::dmabufAttributes() const +{ + return nullptr; +} + +const ShmAttributes *GraphicsBuffer::shmAttributes() const +{ + return nullptr; +} + +const SinglePixelAttributes *GraphicsBuffer::singlePixelAttributes() const +{ + return nullptr; +} + +void GraphicsBuffer::addReleasePoint(const std::shared_ptr &releasePoint) +{ + m_releasePoints.push_back(releasePoint); +} + +bool GraphicsBuffer::alphaChannelFromDrmFormat(uint32_t format) +{ + const auto info = FormatInfo::get(format); + return info && info->alphaBits > 0; +} + +} // namespace KWin + +#include "moc_graphicsbuffer.cpp" diff --git a/local/recipes/kde/kwin/source/src/core/graphicsbuffer.h b/local/recipes/kde/kwin/source/src/core/graphicsbuffer.h new file mode 100644 index 0000000000..80d325b378 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/graphicsbuffer.h @@ -0,0 +1,219 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "kwin_export.h" +#include "utils/filedescriptor.h" + +#include +#include +#include + +namespace KWin +{ + +class SyncReleasePoint; + +struct DmaBufAttributes +{ + int planeCount = 0; + int width = 0; + int height = 0; + uint32_t format = 0; + uint64_t modifier = 0; + + std::array fd; + std::array offset{0, 0, 0, 0}; + std::array pitch{0, 0, 0, 0}; +}; + +struct ShmAttributes +{ + FileDescriptor fd; + int stride; + off_t offset; + QSize size; + uint32_t format; +}; + +struct SinglePixelAttributes +{ + uint32_t red; + uint32_t green; + uint32_t blue; + uint32_t alpha; +}; + +/** + * The GraphicsBuffer class represents a chunk of memory containing graphics data. + * + * A graphics buffer can be referenced. In which case, it won't be destroyed until all + * references are dropped. You can use the isDropped() function to check whether the + * buffer has been marked as destroyed. + */ +class KWIN_EXPORT GraphicsBuffer : public QObject +{ + Q_OBJECT + +public: + explicit GraphicsBuffer(QObject *parent = nullptr); + ~GraphicsBuffer() override; + + bool isReferenced() const; + bool isDropped() const; + + void ref(); + void unref(); + void drop(); + + enum MapFlag { + Read = 0x1, + Write = 0x2, + }; + Q_DECLARE_FLAGS(MapFlags, MapFlag) + + struct Map + { + void *data = nullptr; + uint32_t stride = 0; + }; + virtual Map map(MapFlags flags); + virtual void unmap(); + + virtual QSize size() const = 0; + virtual bool hasAlphaChannel() const = 0; + + virtual const DmaBufAttributes *dmabufAttributes() const; + virtual const ShmAttributes *shmAttributes() const; + virtual const SinglePixelAttributes *singlePixelAttributes() const; + + /** + * the added release point will be referenced as long as this buffer is referenced + */ + void addReleasePoint(const std::shared_ptr &releasePoint); + + static bool alphaChannelFromDrmFormat(uint32_t format); + +Q_SIGNALS: + void released(); + +protected: + int m_refCount = 0; + bool m_dropped = false; + std::vector> m_releasePoints; +}; + +/** + * The GraphicsBufferRef type holds a reference to a GraphicsBuffer. While the reference + * exists, the graphics buffer cannot be destroyed and the client cannot modify it. + */ +class GraphicsBufferRef +{ +public: + GraphicsBufferRef() + : m_buffer(nullptr) + { + } + + GraphicsBufferRef(GraphicsBuffer *buffer) + : m_buffer(buffer) + { + if (m_buffer) { + m_buffer->ref(); + } + } + + GraphicsBufferRef(const GraphicsBufferRef &other) + : m_buffer(other.m_buffer) + { + if (m_buffer) { + m_buffer->ref(); + } + } + + GraphicsBufferRef(GraphicsBufferRef &&other) + : m_buffer(std::exchange(other.m_buffer, nullptr)) + { + } + + ~GraphicsBufferRef() + { + if (m_buffer) { + m_buffer->unref(); + } + } + + void reset() + { + if (m_buffer) { + m_buffer->unref(); + m_buffer = nullptr; + } + } + + GraphicsBufferRef &operator=(const GraphicsBufferRef &other) + { + if (other.m_buffer) { + other.m_buffer->ref(); + } + if (m_buffer) { + m_buffer->unref(); + } + m_buffer = other.m_buffer; + return *this; + } + + GraphicsBufferRef &operator=(GraphicsBufferRef &&other) + { + if (m_buffer) { + m_buffer->unref(); + } + m_buffer = std::exchange(other.m_buffer, nullptr); + return *this; + } + + GraphicsBufferRef &operator=(GraphicsBuffer *buffer) + { + if (m_buffer != buffer) { + if (m_buffer) { + m_buffer->unref(); + } + if (buffer) { + buffer->ref(); + } + m_buffer = buffer; + } + return *this; + } + + inline GraphicsBuffer *buffer() const + { + return m_buffer; + } + + inline GraphicsBuffer *operator*() const + { + return m_buffer; + } + + inline GraphicsBuffer *operator->() const + { + return m_buffer; + } + + inline operator bool() const + { + return m_buffer; + } + +private: + GraphicsBuffer *m_buffer; +}; + +} // namespace KWin + +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::GraphicsBuffer::MapFlags) diff --git a/local/recipes/kde/kwin/source/src/core/graphicsbufferallocator.cpp b/local/recipes/kde/kwin/source/src/core/graphicsbufferallocator.cpp new file mode 100644 index 0000000000..b0e36f079a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/graphicsbufferallocator.cpp @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "core/graphicsbufferallocator.h" + +namespace KWin +{ + +GraphicsBufferAllocator::GraphicsBufferAllocator() +{ +} + +GraphicsBufferAllocator::~GraphicsBufferAllocator() +{ +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/graphicsbufferallocator.h b/local/recipes/kde/kwin/source/src/core/graphicsbufferallocator.h new file mode 100644 index 0000000000..e66c0a23a4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/graphicsbufferallocator.h @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "kwin_export.h" + +#include +#include + +namespace KWin +{ + +class GraphicsBuffer; + +/** + * The GraphicsBufferOptions describes the properties of an allocated graphics buffer. + */ +struct GraphicsBufferOptions +{ + /// The size of the buffer, in device pixels. + QSize size; + + /// The pixel format of the buffer, see DRM_FORMAT_*. + uint32_t format; + + /// An optional list of modifiers, see DRM_FORMAT_MOD_*. + QList modifiers; + + /// Whether the graphics buffer should be suitable for software rendering. + bool software = false; +}; + +class KWIN_EXPORT GraphicsBufferAllocator +{ +public: + GraphicsBufferAllocator(); + virtual ~GraphicsBufferAllocator(); + + virtual GraphicsBuffer *allocate(const GraphicsBufferOptions &options) = 0; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/graphicsbufferview.cpp b/local/recipes/kde/kwin/source/src/core/graphicsbufferview.cpp new file mode 100644 index 0000000000..1aa45bff7e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/graphicsbufferview.cpp @@ -0,0 +1,107 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "core/graphicsbufferview.h" +#include "core/graphicsbuffer.h" +#include "utils/common.h" +#include "utils/drm_format_helper.h" + +#include + +namespace KWin +{ + +static QImage::Format drmFormatToQImageFormat(uint32_t drmFormat) +{ + switch (drmFormat) { +#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN + case DRM_FORMAT_ABGR16161616: + return QImage::Format_RGBA64_Premultiplied; + case DRM_FORMAT_XBGR16161616: + return QImage::Format_RGBX64; + case DRM_FORMAT_ARGB2101010: + return QImage::Format_A2RGB30_Premultiplied; + case DRM_FORMAT_XRGB2101010: + return QImage::Format_RGB30; + case DRM_FORMAT_ABGR2101010: + return QImage::Format_A2BGR30_Premultiplied; + case DRM_FORMAT_XBGR2101010: + return QImage::Format_BGR30; +#endif + case DRM_FORMAT_ARGB8888: + return QImage::Format_ARGB32_Premultiplied; + case DRM_FORMAT_XRGB8888: + // it's up to the calling code to do conversions + case DRM_FORMAT_XYUV8888: + return QImage::Format_RGB32; + case DRM_FORMAT_BGR888: + return QImage::Format_RGB888; + case DRM_FORMAT_RGB888: + return QImage::Format_BGR888; + default: + return QImage::Format_Invalid; + } +} + +GraphicsBufferView::GraphicsBufferView(GraphicsBuffer *buffer, GraphicsBuffer::MapFlags accessFlags) + : m_buffer(buffer) +{ + int width; + int height; + int format; + + if (auto dmabuf = buffer->dmabufAttributes()) { + if (dmabuf->planeCount != 1) { + return; + } + width = dmabuf->width; + height = dmabuf->height; + format = dmabuf->format; + } else if (auto shm = buffer->shmAttributes()) { + width = shm->size.width(); + height = shm->size.height(); + format = shm->format; + } else if (buffer->singlePixelAttributes()) { + width = 1; + height = 1; + format = DRM_FORMAT_ARGB8888; + } else { + qCWarning(KWIN_CORE) << "Cannot create a graphics buffer view for unknown buffer type" << buffer; + return; + } + + const auto [data, stride] = buffer->map(accessFlags); + if (data) { + m_image = QImage(static_cast(data), width, height, stride, drmFormatToQImageFormat(format)); + if (Q_UNLIKELY(m_image.isNull())) { + qCWarning(KWIN_CORE) << "Cannot create a graphics buffer view" << buffer << FormatInfo::drmFormatName(format) << drmFormatToQImageFormat(format); + } + } +} + +GraphicsBufferView::~GraphicsBufferView() +{ + if (!m_image.isNull()) { + m_buffer->unmap(); + } +} + +bool GraphicsBufferView::isNull() const +{ + return m_image.isNull(); +} + +QImage *GraphicsBufferView::image() +{ + return &m_image; +} + +const QImage *GraphicsBufferView::image() const +{ + return &m_image; +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/graphicsbufferview.h b/local/recipes/kde/kwin/source/src/core/graphicsbufferview.h new file mode 100644 index 0000000000..c47bdebdd0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/graphicsbufferview.h @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/graphicsbuffer.h" + +#include + +namespace KWin +{ + +class KWIN_EXPORT GraphicsBufferView +{ +public: + explicit GraphicsBufferView(GraphicsBuffer *buffer, GraphicsBuffer::MapFlags accessFlags = GraphicsBuffer::Read); + ~GraphicsBufferView(); + + bool isNull() const; + QImage *image(); + const QImage *image() const; + +private: + GraphicsBuffer *m_buffer; + QImage m_image; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/iccprofile.cpp b/local/recipes/kde/kwin/source/src/core/iccprofile.cpp new file mode 100644 index 0000000000..4490e7d283 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/iccprofile.cpp @@ -0,0 +1,581 @@ +/* + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "iccprofile.h" +#include "colorlut3d.h" +#include "colorpipelinestage.h" +#include "colortransformation.h" +#include "utils/common.h" + +#include +#include +#include +#include +#include + +namespace KWin +{ + +static const Colorimetry CIEXYZD50 = Colorimetry{ + XYZ{1.0, 0.0, 0.0}, + XYZ{0.0, 1.0, 0.0}, + XYZ{0.0, 0.0, 1.0}, + XYZ(0.9642, 1.0, 0.8249), +}; + +const ColorDescription IccProfile::s_connectionSpace = ColorDescription(CIEXYZD50, TransferFunction(TransferFunction::linear, 0, 1), 1, 0, 1, 1); + +IccProfile::IccProfile(cmsHPROFILE handle, const Colorimetry &colorimetry, + std::optional &&bToA0Tag, std::optional &&bToA1Tag, + const std::shared_ptr &inverseEOTF, + const QMatrix4x4 &xyzMatrix, const std::shared_ptr &vcgt, + std::optional relativeBlackPoint, std::optional maxFALL, std::optional maxCLL) + : m_handle(handle) + , m_colorimetry(colorimetry) + , m_bToA0Tag(std::move(bToA0Tag)) + , m_bToA1Tag(std::move(bToA1Tag)) + , m_inverseEOTF(inverseEOTF) + , m_xyzMatrix(xyzMatrix) + , m_vcgt(vcgt) + , m_relativeBlackPoint(relativeBlackPoint) + , m_maxFALL(maxFALL) + , m_maxCLL(maxCLL) +{ +} + +IccProfile::~IccProfile() +{ + cmsCloseProfile(m_handle); +} + +std::optional IccProfile::relativeBlackPoint() const +{ + return m_relativeBlackPoint; +} + +std::optional IccProfile::maxFALL() const +{ + return m_maxFALL; +} + +std::optional IccProfile::maxCLL() const +{ + return m_maxCLL; +} + +const Colorimetry &IccProfile::colorimetry() const +{ + return m_colorimetry; +} + +std::shared_ptr IccProfile::inverseTransferFunction() const +{ + return m_inverseEOTF; +} + +std::shared_ptr IccProfile::vcgt() const +{ + return m_vcgt; +} + +const QMatrix4x4 &IccProfile::mhc2Matrix() const +{ + return m_xyzMatrix; +} + +const ColorPipeline *IccProfile::BToATag(RenderingIntent intent) const +{ + switch (intent) { + case RenderingIntent::Perceptual: + return m_bToA0Tag ? &*m_bToA0Tag : nullptr; + case RenderingIntent::RelativeColorimetric: + // these two are different from relative colorimetric + // but that has to be handled before the tag is applied + case RenderingIntent::RelativeColorimetricWithBPC: + case RenderingIntent::AbsoluteColorimetricNoAdaptation: + return m_bToA1Tag ? &*m_bToA1Tag : nullptr; + } + Q_UNREACHABLE(); +} + +static std::vector readTagRaw(cmsHPROFILE profile, cmsTagSignature tag) +{ + const auto numBytes = cmsReadRawTag(profile, tag, nullptr, 0); + std::vector data(numBytes); + cmsReadRawTag(profile, tag, data.data(), numBytes); + return data; +} + +template +static T read(std::span data, size_t index) +{ + // ICC profile data is big-endian + T ret; + for (size_t i = 0; i < sizeof(T); i++) { + *(reinterpret_cast(&ret) + i) = data[index + sizeof(T) - i - 1]; + } + return ret; +} + +static float readS15Fixed16(std::span data, size_t index) +{ + return read(data, index) / 65536.0; +} + +static std::optional> parseBToACLUTSize(std::span data) +{ + const uint32_t tagType = read(data, 0); + const bool isLutTag = tagType == cmsSigLut8Type || tagType == cmsSigLut16Type; + if (isLutTag) { + const uint8_t size = data[10]; + return std::make_tuple(size, size, size); + } else { + const uint32_t clutOffset = read(data, 24); + if (data.size() < clutOffset + 19) { + qCWarning(KWIN_CORE, "CLut offset points to invalid position %u", clutOffset); + return std::nullopt; + } + return std::make_tuple(data[clutOffset + 0], data[clutOffset + 1], data[clutOffset + 2]); + } +} + +static std::optional parseMatrix(std::span data, bool hasOffset) +{ + const size_t matrixSize = hasOffset ? 12 : 9; + std::vector floats; + floats.reserve(matrixSize); + for (size_t i = 0; i < matrixSize; i++) { + floats.push_back(readS15Fixed16(data, i * 4)); + } + QMatrix4x4 ret; + ret(0, 0) = floats[0]; + ret(0, 1) = floats[1]; + ret(0, 2) = floats[2]; + ret(1, 0) = floats[3]; + ret(1, 1) = floats[4]; + ret(1, 2) = floats[5]; + ret(2, 0) = floats[6]; + ret(2, 1) = floats[7]; + ret(2, 2) = floats[8]; + if (hasOffset) { + ret(0, 3) = floats[9]; + ret(1, 3) = floats[10]; + ret(2, 3) = floats[11]; + } + return ret; +} + +static std::optional parseBToATag(cmsHPROFILE profile, cmsTagSignature tag) +{ + cmsPipeline *bToAPipeline = static_cast(cmsReadTag(profile, tag)); + if (!bToAPipeline) { + return std::nullopt; + } + ColorPipeline ret; + // ICC profiles assume you're working in their encoding of XYZ + // this multiplier converts from our [0, 1] encoding to the ICC one + ret.addMultiplier(65536.0 / (2 * 65535.0)); + auto data = readTagRaw(profile, tag); + const uint32_t tagType = read(data, 0); + switch (tagType) { + case cmsSigLut8Type: + case cmsSigLut16Type: + if (data.size() < 48) { + qCWarning(KWIN_CORE) << "ICC profile tag is too small" << data.size(); + return std::nullopt; + } + break; + case cmsSigLutBtoAType: + if (data.size() < 32) { + qCWarning(KWIN_CORE) << "ICC profile tag is too small" << data.size(); + return std::nullopt; + } + break; + default: + qCWarning(KWIN_CORE).nospace() << "unknown lut type " << (char)data[0] << (char)data[1] << (char)data[2] << (char)data[3]; + return std::nullopt; + } + for (auto stage = cmsPipelineGetPtrToFirstStage(bToAPipeline); stage != nullptr; stage = cmsStageNext(stage)) { + switch (const cmsStageSignature stageType = cmsStageType(stage)) { + case cmsStageSignature::cmsSigCurveSetElemType: { + // TODO read the actual functions and apply them in the shader instead + // of using LUTs for more accuracy + std::vector> stages; + stages.push_back(std::make_unique(cmsStageDup(stage))); + auto transformation = std::make_shared(std::move(stages)); + ret.add(ColorOp{ + .input = ValueRange(), + .operation = transformation, + .output = ValueRange(), + }); + } break; + case cmsStageSignature::cmsSigMatrixElemType: { + const bool isLutTag = tagType == cmsSigLut8Type || tagType == cmsSigLut16Type; + const uint32_t matrixOffset = isLutTag ? 12 : read(data, 16); + const uint32_t matrixSize = isLutTag ? 9 : 12; + if (data.size() < matrixOffset + matrixSize * 4) { + qCWarning(KWIN_CORE, "matrix offset points to invalid position %u", matrixOffset); + return std::nullopt; + } + const auto mat = parseMatrix(std::span(data).subspan(matrixOffset), !isLutTag); + if (!mat) { + return std::nullopt; + } + ret.add(ColorOp{ + .input = ValueRange{}, + .operation = ColorMatrix(*mat), + .output = ValueRange{}, + }); + }; break; + case cmsStageSignature::cmsSigCLutElemType: { + const auto size = parseBToACLUTSize(data); + if (!size) { + return std::nullopt; + } + const auto [x, y, z] = *size; + std::vector> stages; + stages.push_back(std::make_unique(cmsStageDup(stage))); + ret.add(ColorOp{ + .input = ValueRange{}, + .operation = std::make_shared(std::make_unique(std::move(stages)), x, y, z), + .output = ValueRange{}, + }); + } break; + default: + qCWarning(KWIN_CORE, "unknown stage type %u", stageType); + return std::nullopt; + } + } + return ret; +} + +static cmsTagSignature stringToSignature(std::string_view str) +{ + return cmsTagSignature((uint32_t(str[3]) << 0) | (uint32_t(str[2]) << 8) | (uint32_t(str[1]) << 16) | (uint32_t(str[0]) << 24)); +} + +struct MHC2 +{ + double minLuminance; + double maxLuminance; + QMatrix4x4 xyzMatrix; + std::vector red; + std::vector green; + std::vector blue; +}; +std::optional parseMhc2Tag(cmsHPROFILE profile) +{ + // see https://learn.microsoft.com/en-us/windows/win32/wcs/display-calibration-mhc + // for documentation of the tag + // NOTE that cmsTagSignature::cmsSigMHC2Tag isn't used here, because that requires + // newer LCMS + const cmsTagSignature tagSignature = stringToSignature("MHC2"); + if (!cmsIsTag(profile, tagSignature)) { + return std::nullopt; + } + auto data = readTagRaw(profile, tagSignature); + if (data.size() < 36) { + qCWarning(KWIN_CORE, "MHC2 tag smaller than expected"); + return std::nullopt; + } + MHC2 ret; + ret.minLuminance = readS15Fixed16(data, 12); + ret.maxLuminance = readS15Fixed16(data, 16); + const uint32_t lutSize = read(data, 8); + const uint32_t matrixOffset = read(data, 20); + const uint32_t redOffset = read(data, 24); + const uint32_t greenOffset = read(data, 28); + const uint32_t blueOffset = read(data, 32); + if (matrixOffset != 0) { + if (data.size() < matrixOffset + 48) { + qCWarning(KWIN_CORE, "Parsing MHC2 tag failed"); + return std::nullopt; + } + // NOTE That this technically also has an offset. + // Windows ignores it though, so it's best to ignore it too. + for (int row = 0; row < 3; ++row) { + for (int column = 0; column < 3; ++column) { + ret.xyzMatrix(row, column) = readS15Fixed16(data, matrixOffset + (row * 4 + column) * 4); + } + } + } + if (lutSize > 0) { + const uint32_t lutHeaderSize = 8; + const uint32_t lutDataSize = lutHeaderSize + lutSize * 4; + if (data.size() < std::max({redOffset, greenOffset, blueOffset}) + lutDataSize) { + qCWarning(KWIN_CORE, "Parsing MHC2 tag failed"); + return std::nullopt; + } + for (uint32_t i = 0; i < lutSize; i++) { + ret.red.push_back(readS15Fixed16(data, redOffset + lutHeaderSize + i * 4)); + ret.green.push_back(readS15Fixed16(data, greenOffset + lutHeaderSize + i * 4)); + ret.blue.push_back(readS15Fixed16(data, blueOffset + lutHeaderSize + i * 4)); + } + } + return ret; +} + +static constexpr size_t trcSize = 4096; + +static uint16_t findToneCurveInput(cmsToneCurve *curve, uint16_t desiredOutput, uint16_t begin, uint16_t end) +{ + if (begin == end || begin + 1 == end) { + return begin; + } + const uint16_t middle = std::midpoint(begin, end); + const uint16_t v = cmsEvalToneCurve16(curve, middle); + if (desiredOutput == v) { + return middle; + } else if (desiredOutput < v) { + return findToneCurveInput(curve, desiredOutput, begin, middle); + } else { + return findToneCurveInput(curve, desiredOutput, middle, end); + } +} + +/** + * NOTE this is different from cmsReverseToneCurve, which does not handle + * 100% segments at the end in the way we require + */ +static std::optional> inverseToneCurve(cmsToneCurve *curve) +{ + if (!cmsIsToneCurveMonotonic(curve) || cmsIsToneCurveDescending(curve)) { + return std::nullopt; + } + constexpr uint16_t max = std::numeric_limits::max(); + + uint16_t peakInput = max; + const uint16_t peakValue = cmsEvalToneCurve16(curve, max); + if (!peakValue) { + return std::nullopt; + } + // some TRCs have a segment at 100% at the end, + // find the first non-100% value + for (int32_t i = trcSize - 2; i > 0; i--) { + const uint16_t in = std::clamp(std::round(max * (i / double(trcSize - 1))), 0, max); + const uint16_t out = cmsEvalToneCurve16(curve, in); + if (out < peakValue) { + break; + } else { + peakInput = in; + } + } + + // now calculate the curve, but ignore everything above peakInput + std::array ret; + for (uint16_t i = 0; i < trcSize; i++) { + const uint16_t desiredOut = std::clamp(std::round(max * (i / double(trcSize - 1))), 0, max); + const uint16_t input = findToneCurveInput(curve, desiredOut, 0, peakInput); + ret[i] = input; + } + return ret; +} + +static constexpr XYZ D50{ + .X = 0.9642, + .Y = 1.0, + .Z = 0.8249, +}; + +std::expected, QString> IccProfile::load(const QString &path) +{ + if (path.isEmpty()) { + return nullptr; + } + cmsHPROFILE handle = cmsOpenProfileFromFile(path.toUtf8(), "r"); + if (!handle) { + if (QFileInfo::exists(path)) { + return std::unexpected(i18n("Failed to open ICC profile \"%1\"", path)); + } else { + return std::unexpected(i18n("ICC profile \"%1\" doesn't exist", path)); + } + } + if (cmsGetDeviceClass(handle) != cmsSigDisplayClass) { + return std::unexpected(i18n("ICC profile \"%1\" is not usable for displays", path)); + } + if (cmsGetPCS(handle) != cmsColorSpaceSignature::cmsSigXYZData) { + return std::unexpected(i18n("ICC profile \"%1\" has unsupported connection space, only XYZ is supported", path)); + } + if (cmsGetColorSpace(handle) != cmsColorSpaceSignature::cmsSigRgbData) { + return std::unexpected(i18n("ICC profile \"%1\" is broken, input/output color space isn't RGB", path)); + } + + std::shared_ptr vcgt; + cmsToneCurve **vcgtTag = static_cast(cmsReadTag(handle, cmsSigVcgtTag)); + if (vcgtTag && vcgtTag[0]) { + std::vector> stages; + stages.push_back(std::make_unique(cmsStageAllocToneCurves(nullptr, 3, vcgtTag))); + vcgt = std::make_shared(std::move(stages)); + } + + const cmsCIEXYZ *whitepoint = static_cast(cmsReadTag(handle, cmsSigMediaWhitePointTag)); + if (!whitepoint) { + return std::unexpected(i18n("ICC profile \"%1\" is broken, it has no whitepoint", path)); + } + if (whitepoint->Y == 0) { + return std::unexpected(i18n("ICC profile \"%1\" is broken, its whitepoint is invalid", path)); + } + + XYZ red; + XYZ green; + XYZ blue; + XYZ white = XYZ{whitepoint->X, whitepoint->Y, whitepoint->Z}; + std::optional chromaticAdaptationMatrix; + if (cmsIsTag(handle, cmsSigChromaticAdaptationTag)) { + // the chromatic adaptation tag is a 3x3 matrix that converts from the actual whitepoint to D50 + const auto data = readTagRaw(handle, cmsSigChromaticAdaptationTag); + const auto mat = parseMatrix(std::span(data).subspan(8), false); + if (!mat) { + return std::unexpected(i18n("ICC profile \"%1\" is broken, parsing chromatic adaptation matrix failed", path)); + } + bool invertable = false; + chromaticAdaptationMatrix = mat->inverted(&invertable); + if (!invertable) { + return std::unexpected(i18n("ICC profile \"%1\" is broken, inverting chromatic adaptation matrix failed", path)); + } + white = XYZ::fromVector(*chromaticAdaptationMatrix * D50.asVector()); + } + if (cmsCIExyYTRIPLE *chrmTag = static_cast(cmsReadTag(handle, cmsSigChromaticityTag))) { + red = xyY{chrmTag->Red.x, chrmTag->Red.y, chrmTag->Red.Y}.toXYZ(); + green = xyY{chrmTag->Green.x, chrmTag->Green.y, chrmTag->Green.Y}.toXYZ(); + blue = xyY{chrmTag->Blue.x, chrmTag->Blue.y, chrmTag->Blue.Y}.toXYZ(); + } else { + const cmsCIEXYZ *r = static_cast(cmsReadTag(handle, cmsSigRedColorantTag)); + const cmsCIEXYZ *g = static_cast(cmsReadTag(handle, cmsSigGreenColorantTag)); + const cmsCIEXYZ *b = static_cast(cmsReadTag(handle, cmsSigBlueColorantTag)); + if (!r || !g || !b) { + return std::unexpected(i18n("ICC profile \"%1\" is broken, it has no primaries", path)); + } + if (chromaticAdaptationMatrix) { + red = XYZ::fromVector(*chromaticAdaptationMatrix * QVector3D(r->X, r->Y, r->Z)); + green = XYZ::fromVector(*chromaticAdaptationMatrix * QVector3D(g->X, g->Y, g->Z)); + blue = XYZ::fromVector(*chromaticAdaptationMatrix * QVector3D(b->X, b->Y, b->Z)); + } else { + // if the chromatic adaptation tag isn't available, fall back to using the media whitepoint instead + cmsCIEXYZ adaptedR{}; + cmsCIEXYZ adaptedG{}; + cmsCIEXYZ adaptedB{}; + bool success = cmsAdaptToIlluminant(&adaptedR, cmsD50_XYZ(), whitepoint, r); + success &= cmsAdaptToIlluminant(&adaptedG, cmsD50_XYZ(), whitepoint, g); + success &= cmsAdaptToIlluminant(&adaptedB, cmsD50_XYZ(), whitepoint, b); + if (!success) { + return std::unexpected(i18n("ICC profile \"%1\" is broken, couldn't calculate its primaries", path)); + } + red = XYZ(adaptedR.X, adaptedR.Y, adaptedR.Z); + green = XYZ(adaptedG.X, adaptedG.Y, adaptedG.Z); + blue = XYZ(adaptedB.X, adaptedB.Y, adaptedB.Z); + } + } + + if (red.Y == 0 || green.Y == 0 || blue.Y == 0 || white.Y == 0) { + return std::unexpected(i18n("ICC profile \"%1\" is broken, its primaries are invalid", path)); + } + + std::optional maxFALL; + if (cmsCIEXYZ *luminance = static_cast(cmsReadTag(handle, cmsSigLuminanceTag))) { + // for some reason, lcms exposes the luminance as a XYZ triple... + // only Y is non-zero, and it's the brightness in nits + maxFALL = luminance->Y; + } + std::optional relativeBlackPoint; + cmsCIEXYZ blackPoint; + if (cmsDetectDestinationBlackPoint(&blackPoint, handle, INTENT_RELATIVE_COLORIMETRIC, 0)) { + relativeBlackPoint = blackPoint.Y; + } + + if (cmsIsTag(handle, cmsSigBToD1Tag) && !cmsIsTag(handle, cmsSigBToA1Tag) && !cmsIsTag(handle, cmsSigBToA0Tag)) { + return std::unexpected(i18n("ICC profile \"%1\" with only BToD tags isn't supported", path)); + } + std::optional bToA0; + std::optional bToA1; + if (cmsIsTag(handle, cmsSigBToA0Tag)) { + bToA0 = parseBToATag(handle, cmsSigBToA0Tag); + } + if (cmsIsTag(handle, cmsSigBToA1Tag)) { + bToA1 = parseBToATag(handle, cmsSigBToA1Tag); + } + std::array toneCurves; + if (bToA0 || bToA1) { + // the TRC tags are often nonsense when the BToA tag exists, so this estimates the + // inverse transfer function by doing a grayscale transform on the BToA tag instead + const QMatrix4x4 toXYZD50 = Colorimetry::chromaticAdaptationMatrix(white, D50) * Colorimetry(red, green, blue, white).toXYZ(); + ColorPipeline pipeline; + pipeline.addMatrix(toXYZD50, ValueRange{}, ColorspaceType::AnyNonRGB); + pipeline.add(bToA1 ? *bToA1 : *bToA0); + std::array red; + std::array green; + std::array blue; + for (size_t i = 0; i < trcSize; i++) { + const float relativeI = i / float(trcSize - 1); + const QVector3D result = pipeline.evaluate(QVector3D{relativeI, relativeI, relativeI}); + red[i] = result.x(); + green[i] = result.y(); + blue[i] = result.z(); + } + toneCurves = { + cmsBuildTabulatedToneCurveFloat(nullptr, trcSize, red.data()), + cmsBuildTabulatedToneCurveFloat(nullptr, trcSize, green.data()), + cmsBuildTabulatedToneCurveFloat(nullptr, trcSize, blue.data()), + }; + } else { + cmsToneCurve *r = static_cast(cmsReadTag(handle, cmsSigRedTRCTag)); + cmsToneCurve *g = static_cast(cmsReadTag(handle, cmsSigGreenTRCTag)); + cmsToneCurve *b = static_cast(cmsReadTag(handle, cmsSigBlueTRCTag)); + if (!r || !g || !b) { + return std::unexpected(i18n("Color profile is missing TRC tags")); + } + + const auto redCurve = inverseToneCurve(r); + const auto greenCurve = inverseToneCurve(g); + const auto blueCurve = inverseToneCurve(b); + if (!redCurve || !greenCurve || !blueCurve) { + return std::unexpected(i18n("Couldn't invert tone curves of ICC profile \"%1\"", qPrintable(path))); + } + + toneCurves = { + cmsBuildTabulatedToneCurve16(nullptr, redCurve->size(), redCurve->data()), + cmsBuildTabulatedToneCurve16(nullptr, greenCurve->size(), greenCurve->data()), + cmsBuildTabulatedToneCurve16(nullptr, blueCurve->size(), blueCurve->data()), + }; + } + std::vector> stages; + stages.push_back(std::make_unique(cmsStageAllocToneCurves(nullptr, toneCurves.size(), toneCurves.data()))); + for (auto toneCurve : toneCurves) { + cmsFreeToneCurve(toneCurve); + } + const auto inverseEOTF = std::make_shared(std::move(stages)); + + QMatrix4x4 xyzMatrix; + std::optional maxCLL = maxFALL; + if (auto mhc2 = parseMhc2Tag(handle)) { + if (mhc2->maxLuminance != 0) { + maxCLL = mhc2->maxLuminance; + } + if (mhc2->minLuminance != 0 && maxFALL.has_value()) { + relativeBlackPoint = mhc2->minLuminance / *maxFALL; + } + if (!mhc2->red.empty()) { + std::array toneCurves = { + cmsBuildTabulatedToneCurveFloat(nullptr, mhc2->red.size(), mhc2->red.data()), + cmsBuildTabulatedToneCurveFloat(nullptr, mhc2->green.size(), mhc2->green.data()), + cmsBuildTabulatedToneCurveFloat(nullptr, mhc2->blue.size(), mhc2->blue.data()), + }; + std::vector> stages; + stages.push_back(std::make_unique(cmsStageAllocToneCurves(nullptr, toneCurves.size(), toneCurves.data()))); + vcgt = std::make_shared(std::move(stages)); + for (auto toneCurve : toneCurves) { + cmsFreeToneCurve(toneCurve); + } + } + // NOTE that this matrix is usually used for bad hacks Windows needs + // like "sRGB clamping", which are completely unnecessary with KWin. + // Maybe warn the user about this in KScreen? + xyzMatrix = mhc2->xyzMatrix; + } + + return std::make_unique(handle, Colorimetry(red, green, blue, white), std::move(bToA0), std::move(bToA1), inverseEOTF, xyzMatrix, vcgt, relativeBlackPoint, maxFALL, maxCLL); +} + +} diff --git a/local/recipes/kde/kwin/source/src/core/iccprofile.h b/local/recipes/kde/kwin/source/src/core/iccprofile.h new file mode 100644 index 0000000000..9e5114bbb3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/iccprofile.h @@ -0,0 +1,76 @@ +/* + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "core/colorpipeline.h" +#include "core/colorspace.h" +#include "kwin_export.h" + +#include +#include +#include +#include +#include + +typedef void *cmsHPROFILE; + +namespace KWin +{ + +class ColorTransformation; +class ColorLUT3D; + +class KWIN_EXPORT IccProfile +{ +public: + explicit IccProfile(cmsHPROFILE handle, const Colorimetry &colorimetry, std::optional &&bToA0Tag, std::optional &&bToA1Tag, const std::shared_ptr &inverseEOTF, const QMatrix4x4 &xyzMatrix, const std::shared_ptr &vcgt, std::optional relativeBlackPoint, std::optional maxFALL, std::optional maxCLL); + ~IccProfile(); + + /** + * the BToA tag describes a transformation from XYZ with D50 whitepoint + * to the display color space. May be nullptr! + */ + const ColorPipeline *BToATag(RenderingIntent intent) const; + /** + * NOTE that this inverse transfer function is an estimation + * and not necessarily exact! + */ + std::shared_ptr inverseTransferFunction() const; + /** + * The VCGT is a non-standard tag that needs to be applied before + * pixels are sent to the display. May be nullptr! + */ + std::shared_ptr vcgt() const; + /** + * This comes from the non-standard 'MHC2' tag Microsoft uses. + * It needs to be applied in XYZ space before scanout + */ + const QMatrix4x4 &mhc2Matrix() const; + const Colorimetry &colorimetry() const; + /** + * relative to maxFALL, not maxCLL + */ + std::optional relativeBlackPoint() const; + std::optional maxFALL() const; + std::optional maxCLL() const; + + static std::expected, QString> load(const QString &path); + static const ColorDescription s_connectionSpace; + +private: + cmsHPROFILE const m_handle; + const Colorimetry m_colorimetry; + const std::optional m_bToA0Tag; + const std::optional m_bToA1Tag; + const std::shared_ptr m_inverseEOTF; + const QMatrix4x4 m_xyzMatrix; + const std::shared_ptr m_vcgt; + const std::optional m_relativeBlackPoint; + const std::optional m_maxFALL; + const std::optional m_maxCLL; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/core/inputbackend.cpp b/local/recipes/kde/kwin/source/src/core/inputbackend.cpp new file mode 100644 index 0000000000..5a89baf4c6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/inputbackend.cpp @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "inputbackend.h" + +namespace KWin +{ + +InputBackend::InputBackend(QObject *parent) + : QObject(parent) +{ +} + +KSharedConfigPtr InputBackend::config() const +{ + return m_config; +} + +void InputBackend::setConfig(KSharedConfigPtr config) +{ + m_config = config; +} + +} // namespace KWin + +#include "moc_inputbackend.cpp" diff --git a/local/recipes/kde/kwin/source/src/core/inputbackend.h b/local/recipes/kde/kwin/source/src/core/inputbackend.h new file mode 100644 index 0000000000..0dd0169a8b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/inputbackend.h @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "kwin_export.h" + +#include + +#include + +namespace KWin +{ + +class InputDevice; + +class KWIN_EXPORT InputBackend : public QObject +{ + Q_OBJECT + +public: + explicit InputBackend(QObject *parent = nullptr); + + KSharedConfigPtr config() const; + void setConfig(KSharedConfigPtr config); + + virtual void initialize() + { + } + + virtual void updateScreens() + { + } + +Q_SIGNALS: + void deviceAdded(InputDevice *device); + void deviceRemoved(InputDevice *device); + +private: + KSharedConfigPtr m_config; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/inputdevice.cpp b/local/recipes/kde/kwin/source/src/core/inputdevice.cpp new file mode 100644 index 0000000000..cc81d1ea6d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/inputdevice.cpp @@ -0,0 +1,97 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "inputdevice.h" + +namespace KWin +{ + +InputDeviceTabletTool::InputDeviceTabletTool(QObject *parent) + : QObject(parent) +{ +} + +InputDevice::InputDevice(QObject *parent) + : QObject(parent) +{ +} + +QString InputDevice::sysPath() const +{ + return QString(); +} + +quint32 InputDevice::vendor() const +{ + return 0; +} + +quint32 InputDevice::product() const +{ + return 0; +} + +quint32 InputDevice::busType() const +{ + return 0; +} + +void *InputDevice::group() const +{ + return nullptr; +} + +LEDs InputDevice::leds() const +{ + return LEDs(); +} + +void InputDevice::setLeds(LEDs leds) +{ +} + +QString InputDevice::outputName() const +{ + return {}; +} + +void InputDevice::setOutputName(const QString &outputName) +{ +} + +int InputDevice::tabletPadButtonCount() const +{ + return 0; +} + +int InputDevice::tabletPadDialCount() const +{ + return 0; +} + +int InputDevice::tabletPadRingCount() const +{ + return 0; +} + +int InputDevice::tabletPadStripCount() const +{ + return 0; +} + +QList InputDevice::modeGroups() const +{ + return {}; +} + +bool InputDevice::tabletToolIsRelative() const +{ + return false; +} + +} // namespace KWin + +#include "moc_inputdevice.cpp" diff --git a/local/recipes/kde/kwin/source/src/core/inputdevice.h b/local/recipes/kde/kwin/source/src/core/inputdevice.h new file mode 100644 index 0000000000..ead6bf7cc9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/inputdevice.h @@ -0,0 +1,172 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Gräßlin + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/globals.h" +#include "kwin_export.h" + +#include + +namespace KWin +{ + +enum class PointerButtonState { + Released, + Pressed, +}; + +enum class PointerAxis { + Vertical, + Horizontal, +}; + +enum class PointerAxisSource { + Unknown, + Wheel, + Finger, + Continuous, + WheelTilt, +}; + +enum class KeyboardKeyState { + Released, + Pressed, + Repeated, +}; + +enum class SwitchState { + Off, + On, +}; + +class KWIN_EXPORT InputDeviceTabletTool : public QObject +{ + Q_OBJECT + +public: + enum Type { + Pen, + Eraser, + Brush, + Pencil, + Airbrush, + Finger, + Mouse, + Lens, + Totem, + }; + + enum Capability { + Tilt, + Pressure, + Distance, + Rotation, + Slider, + Wheel, + }; + + explicit InputDeviceTabletTool(QObject *parent = nullptr); + + virtual quint64 serialId() const = 0; + virtual quint64 uniqueId() const = 0; + + virtual Type type() const = 0; + virtual QList capabilities() const = 0; +}; + +struct InputDeviceTabletPadModeGroup +{ + int modeCount = 0; + QList buttons; + QList rings; + QList strips; + QList dials; +}; + +/** + * The InputDevice class represents an input device, e.g. a mouse, or a keyboard, etc. + */ +class KWIN_EXPORT InputDevice : public QObject +{ + Q_OBJECT + +public: + explicit InputDevice(QObject *parent = nullptr); + + virtual QString sysPath() const; + virtual QString name() const = 0; + virtual quint32 vendor() const; + virtual quint32 product() const; + virtual quint32 busType() const; + + virtual void *group() const; + + virtual bool isEnabled() const = 0; + virtual void setEnabled(bool enabled) = 0; + + virtual LEDs leds() const; + virtual void setLeds(LEDs leds); + + virtual bool isKeyboard() const = 0; + virtual bool isPointer() const = 0; + virtual bool isTouchpad() const = 0; + virtual bool isTouch() const = 0; + virtual bool isTabletTool() const = 0; + virtual bool isTabletPad() const = 0; + virtual bool isTabletModeSwitch() const = 0; + virtual bool isLidSwitch() const = 0; + + virtual QString outputName() const; + virtual void setOutputName(const QString &outputName); + + virtual int tabletPadButtonCount() const; + virtual int tabletPadDialCount() const; + virtual int tabletPadRingCount() const; + virtual int tabletPadStripCount() const; + + virtual QList modeGroups() const; + + virtual bool tabletToolIsRelative() const; + +Q_SIGNALS: + void keyChanged(quint32 key, KeyboardKeyState, std::chrono::microseconds time, InputDevice *device); + void pointerButtonChanged(quint32 button, PointerButtonState state, std::chrono::microseconds time, InputDevice *device); + void pointerMotionAbsolute(const QPointF &position, std::chrono::microseconds time, InputDevice *device); + void pointerMotion(const QPointF &delta, const QPointF &deltaNonAccelerated, std::chrono::microseconds time, InputDevice *device); + void pointerAxisChanged(PointerAxis axis, qreal delta, qint32 deltaV120, PointerAxisSource source, bool inverted, std::chrono::microseconds time, InputDevice *device); + void pointerFrame(InputDevice *device); + void touchFrame(InputDevice *device); + void touchCanceled(InputDevice *device); + void touchDown(qint32 id, const QPointF &absolutePos, std::chrono::microseconds time, InputDevice *device); + void touchUp(qint32 id, std::chrono::microseconds time, InputDevice *device); + void touchMotion(qint32 id, const QPointF &absolutePos, std::chrono::microseconds time, InputDevice *device); + void swipeGestureBegin(int fingerCount, std::chrono::microseconds time, InputDevice *device); + void swipeGestureUpdate(const QPointF &delta, std::chrono::microseconds time, InputDevice *device); + void swipeGestureEnd(std::chrono::microseconds time, InputDevice *device); + void swipeGestureCancelled(std::chrono::microseconds time, InputDevice *device); + void pinchGestureBegin(int fingerCount, std::chrono::microseconds time, InputDevice *device); + void pinchGestureUpdate(qreal scale, qreal angleDelta, const QPointF &delta, std::chrono::microseconds time, InputDevice *device); + void pinchGestureEnd(std::chrono::microseconds time, InputDevice *device); + void pinchGestureCancelled(std::chrono::microseconds time, InputDevice *device); + void holdGestureBegin(int fingerCount, std::chrono::microseconds time, InputDevice *device); + void holdGestureEnd(std::chrono::microseconds time, InputDevice *device); + void holdGestureCancelled(std::chrono::microseconds time, InputDevice *device); + void switchToggle(SwitchState state, std::chrono::microseconds time, InputDevice *device); + void tabletToolAxisEvent(const QPointF &pos, qreal pressure, qreal xTilt, qreal yTilt, qreal rotation, qreal distance, bool tipDown, qreal sliderPosition, InputDeviceTabletTool *tool, std::chrono::microseconds time, InputDevice *device); + void tabletToolAxisEventRelative(const QPointF &delta, + qreal pressure, qreal xTilt, qreal yTilt, qreal rotation, qreal distance, bool tipDown, qreal sliderPosition, InputDeviceTabletTool *tool, std::chrono::microseconds time, InputDevice *device); + void tabletToolProximityEvent(const QPointF &pos, qreal xTilt, qreal yTilt, qreal rotation, qreal distance, bool tipNear, qreal sliderPosition, InputDeviceTabletTool *tool, std::chrono::microseconds time, InputDevice *device); + void tabletToolTipEvent(const QPointF &pos, qreal pressure, qreal xTilt, qreal yTilt, qreal rotation, qreal distance, bool tipDown, qreal sliderPosition, InputDeviceTabletTool *tool, std::chrono::microseconds time, InputDevice *device); + void tabletToolButtonEvent(uint button, bool isPressed, InputDeviceTabletTool *tool, std::chrono::microseconds time, InputDevice *device); + void tabletPadButtonEvent(uint button, bool isPressed, quint32 group, quint32 mode, bool isModeSwitch, std::chrono::microseconds time, InputDevice *device); + void tabletPadStripEvent(int number, qreal position, bool isFinger, quint32 group, quint32 mode, std::chrono::microseconds time, InputDevice *device); + void tabletPadRingEvent(int number, qreal position, bool isFinger, quint32 group, quint32 mode, std::chrono::microseconds time, InputDevice *device); + void tabletPadDialEvent(int number, double delta, quint32 group, std::chrono::microseconds time, InputDevice *device); +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/output.cpp b/local/recipes/kde/kwin/source/src/core/output.cpp new file mode 100644 index 0000000000..e7e0c98214 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/output.cpp @@ -0,0 +1,500 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "output.h" +#include "brightnessdevice.h" +#include "iccprofile.h" +#include "outputconfiguration.h" + +#include +#include +#include + +#include + +namespace KWin +{ + +QDebug operator<<(QDebug debug, const LogicalOutput *output) +{ + QDebugStateSaver saver(debug); + debug.nospace(); + if (output) { + debug << output->metaObject()->className() << '(' << static_cast(output); + debug << ", name=" << output->name(); + debug << ", geometry=" << output->geometry(); + debug << ", scale=" << output->scale(); + if (debug.verbosity() > 2) { + debug << ", manufacturer=" << output->backendOutput()->manufacturer(); + debug << ", model=" << output->backendOutput()->model(); + debug << ", serialNumber=" << output->backendOutput()->serialNumber(); + } + debug << ')'; + } else { + debug << "LogicalOutput(0x0)"; + } + return debug; +} + +OutputMode::OutputMode(const QSize &size, uint32_t refreshRate, Flags flags) + : m_size(size) + , m_refreshRate(refreshRate) + , m_flags(flags) +{ +} + +QSize OutputMode::size() const +{ + return m_size; +} + +uint32_t OutputMode::refreshRate() const +{ + return m_refreshRate; +} + +OutputMode::Flags OutputMode::flags() const +{ + return m_flags; +} + +bool OutputMode::isRemoved() const +{ + return m_removed; +} + +void OutputMode::setRemoved() +{ + m_removed = true; +} + +OutputTransform::Kind OutputTransform::kind() const +{ + return m_kind; +} + +OutputTransform OutputTransform::inverted() const +{ + switch (m_kind) { + case Kind::Normal: + return Kind::Normal; + case Kind::Rotate90: + return Kind::Rotate270; + case Kind::Rotate180: + return Kind::Rotate180; + case Kind::Rotate270: + return Kind::Rotate90; + case Kind::FlipX: + case Kind::FlipX90: + case Kind::FlipX180: + case Kind::FlipX270: + return m_kind; // inverse transform of a flip transform is itself + } + + Q_UNREACHABLE(); +} + +RectF OutputTransform::map(const RectF &rect, const QSizeF &bounds) const +{ + switch (m_kind) { + case Kind::Normal: + return rect; + case Kind::Rotate90: + return RectF(QPointF(rect.top(), bounds.width() - rect.right()), + QPointF(rect.bottom(), bounds.width() - rect.left())); + case Kind::Rotate180: + return RectF(QPointF(bounds.width() - rect.right(), bounds.height() - rect.bottom()), + QPointF(bounds.width() - rect.left(), bounds.height() - rect.top())); + case Kind::Rotate270: + return RectF(QPointF(bounds.height() - rect.bottom(), rect.left()), + QPointF(bounds.height() - rect.top(), rect.right())); + case Kind::FlipX: + return RectF(QPointF(bounds.width() - rect.right(), rect.top()), + QPointF(bounds.width() - rect.left(), rect.bottom())); + case Kind::FlipX90: + return RectF(QPointF(rect.top(), rect.left()), + QPointF(rect.bottom(), rect.right())); + case Kind::FlipX180: + return RectF(QPointF(rect.left(), bounds.height() - rect.bottom()), + QPointF(rect.right(), bounds.height() - rect.top())); + case Kind::FlipX270: + return RectF(QPointF(bounds.height() - rect.bottom(), bounds.width() - rect.right()), + QPointF(bounds.height() - rect.top(), bounds.width() - rect.left())); + default: + Q_UNREACHABLE(); + } +} + +Rect OutputTransform::map(const Rect &rect, const QSize &bounds) const +{ + switch (m_kind) { + case Kind::Normal: + return rect; + case Kind::Rotate90: + return Rect(QPoint(rect.top(), bounds.width() - rect.right()), + QPoint(rect.bottom(), bounds.width() - rect.left())); + case Kind::Rotate180: + return Rect(QPoint(bounds.width() - rect.right(), bounds.height() - rect.bottom()), + QPoint(bounds.width() - rect.left(), bounds.height() - rect.top())); + case Kind::Rotate270: + return Rect(QPoint(bounds.height() - rect.bottom(), rect.left()), + QPoint(bounds.height() - rect.top(), rect.right())); + case Kind::FlipX: + return Rect(QPoint(bounds.width() - rect.right(), rect.top()), + QPoint(bounds.width() - rect.left(), rect.bottom())); + case Kind::FlipX90: + return Rect(QPoint(rect.top(), rect.left()), + QPoint(rect.bottom(), rect.right())); + case Kind::FlipX180: + return Rect(QPoint(rect.left(), bounds.height() - rect.bottom()), + QPoint(rect.right(), bounds.height() - rect.top())); + case Kind::FlipX270: + return Rect(QPoint(bounds.height() - rect.bottom(), bounds.width() - rect.right()), + QPoint(bounds.height() - rect.top(), bounds.width() - rect.left())); + default: + Q_UNREACHABLE(); + } +} + +QPointF OutputTransform::map(const QPointF &point, const QSizeF &bounds) const +{ + switch (m_kind) { + case Kind::Normal: + return point; + case Kind::Rotate90: + return QPointF(point.y(), + bounds.width() - point.x()); + case Kind::Rotate180: + return QPointF(bounds.width() - point.x(), + bounds.height() - point.y()); + case Kind::Rotate270: + return QPointF(bounds.height() - point.y(), + point.x()); + case Kind::FlipX: + return QPointF(bounds.width() - point.x(), + point.y()); + case Kind::FlipX90: + return QPointF(point.y(), + point.x()); + case Kind::FlipX180: + return QPointF(point.x(), + bounds.height() - point.y()); + case Kind::FlipX270: + return QPointF(bounds.height() - point.y(), + bounds.width() - point.x()); + default: + Q_UNREACHABLE(); + } +} + +QPoint OutputTransform::map(const QPoint &point, const QSize &bounds) const +{ + switch (m_kind) { + case Kind::Normal: + return point; + case Kind::Rotate90: + return QPoint(point.y(), + bounds.width() - point.x()); + case Kind::Rotate180: + return QPoint(bounds.width() - point.x(), + bounds.height() - point.y()); + case Kind::Rotate270: + return QPoint(bounds.height() - point.y(), + point.x()); + case Kind::FlipX: + return QPoint(bounds.width() - point.x(), + point.y()); + case Kind::FlipX90: + return QPoint(point.y(), + point.x()); + case Kind::FlipX180: + return QPoint(point.x(), + bounds.height() - point.y()); + case Kind::FlipX270: + return QPoint(bounds.height() - point.y(), + bounds.width() - point.x()); + default: + Q_UNREACHABLE(); + } +} + +QSizeF OutputTransform::map(const QSizeF &size) const +{ + switch (m_kind) { + case Kind::Normal: + case Kind::Rotate180: + case Kind::FlipX: + case Kind::FlipX180: + return size; + default: + return size.transposed(); + } +} + +QSize OutputTransform::map(const QSize &size) const +{ + switch (m_kind) { + case Kind::Normal: + case Kind::Rotate180: + case Kind::FlipX: + case Kind::FlipX180: + return size; + default: + return size.transposed(); + } +} + +OutputTransform OutputTransform::combine(OutputTransform other) const +{ + // Combining a rotate-N or flip-N (mirror-x | rotate-N) transform with a rotate-M + // transform involves only adding rotation angles: + // rotate-N | rotate-M => rotate-(N + M) + // flip-N | rotate-M => mirror-x | rotate-N | rotate-M + // => mirror-x | rotate-(N + M) + // => flip-(N + M) + // + // rotate-N | mirror-x is the same as mirror-x | rotate-(360 - N). This can be used + // to derive the resulting transform if the other transform flips the x axis + // rotate-N | flip-M => rotate-N | mirror-x | rotate-M + // => mirror-x | rotate-(360 - N + M) + // => flip-(M - N) + // flip-N | flip-M => mirror-x | rotate-N | mirror-x | rotate-M + // => mirror-x | mirror-x | rotate-(360 - N + M) + // => rotate-(360 - N + M) + // => rotate-(M - N) + // + // The remaining code here relies on the bit pattern of transform enums, i.e. the + // lower two bits specify the rotation, the third bit indicates mirroring along the x axis. + + const int flip = (m_kind ^ other.m_kind) & 0x4; + int rotate; + if (other.m_kind & 0x4) { + rotate = (other.m_kind - m_kind) & 0x3; + } else { + rotate = (m_kind + other.m_kind) & 0x3; + } + return OutputTransform(Kind(flip | rotate)); +} + +QMatrix4x4 OutputTransform::toMatrix() const +{ + QMatrix4x4 matrix; + switch (m_kind) { + case Kind::Normal: + break; + case Kind::Rotate90: + matrix.rotate(-90, 0, 0, 1); + break; + case Kind::Rotate180: + matrix.rotate(-180, 0, 0, 1); + break; + case Kind::Rotate270: + matrix.rotate(-270, 0, 0, 1); + break; + case Kind::FlipX: + matrix.scale(-1, 1); + break; + case Kind::FlipX90: + matrix.rotate(-90, 0, 0, 1); + matrix.scale(-1, 1); + break; + case Kind::FlipX180: + matrix.rotate(-180, 0, 0, 1); + matrix.scale(-1, 1); + break; + case Kind::FlipX270: + matrix.rotate(-270, 0, 0, 1); + matrix.scale(-1, 1); + break; + default: + Q_UNREACHABLE(); + } + return matrix; +} + +Region OutputTransform::map(const Region ®ion, const QSize &bounds) const +{ + Region ret; + for (const Rect &rect : region.rects()) { + ret |= map(rect, bounds); + } + return ret; +} + +LogicalOutput::LogicalOutput(BackendOutput *backendOutput) + : m_backendOutput(backendOutput) +{ + QJSEngine::setObjectOwnership(this, QJSEngine::CppOwnership); + connect(backendOutput, &BackendOutput::positionChanged, this, &LogicalOutput::geometryChanged); + connect(backendOutput, &BackendOutput::currentModeChanged, this, &LogicalOutput::geometryChanged); + connect(backendOutput, &BackendOutput::transformChanged, this, &LogicalOutput::geometryChanged); + connect(backendOutput, &BackendOutput::scaleChanged, this, &LogicalOutput::geometryChanged); + connect(backendOutput, &BackendOutput::scaleChanged, this, &LogicalOutput::scaleChanged); + // TODO dpms being kind of on the backend output and kind of here isn't great + connect(backendOutput, &BackendOutput::aboutToChange, this, &LogicalOutput::aboutToChange); + connect(backendOutput, &BackendOutput::changed, this, &LogicalOutput::changed); + connect(backendOutput, &BackendOutput::blendingColorChanged, this, &LogicalOutput::blendingColorChanged); + connect(backendOutput, &BackendOutput::transformChanged, this, &LogicalOutput::transformChanged); + connect(backendOutput, &BackendOutput::currentModeChanged, this, &LogicalOutput::currentModeChanged); +} + +LogicalOutput::~LogicalOutput() +{ +} + +void LogicalOutput::ref() +{ + m_refCount++; +} + +void LogicalOutput::unref() +{ + Q_ASSERT(m_refCount > 0); + m_refCount--; + if (m_refCount == 0) { + delete this; + } +} + +Rect LogicalOutput::mapFromGlobal(const Rect &rect) const +{ + return rect.translated(-geometry().topLeft()); +} + +RectF LogicalOutput::mapFromGlobal(const RectF &rect) const +{ + return rect.translated(-geometry().topLeft()); +} + +RectF LogicalOutput::mapToGlobal(const RectF &rect) const +{ + return rect.translated(geometry().topLeft()); +} + +Region LogicalOutput::mapToGlobal(const Region ®ion) const +{ + return region.translated(geometry().topLeft()); +} + +QPointF LogicalOutput::mapToGlobal(const QPointF &pos) const +{ + return pos + geometry().topLeft(); +} + +QPointF LogicalOutput::mapFromGlobal(const QPointF &pos) const +{ + return pos - geometry().topLeft(); +} + +qreal LogicalOutput::scale() const +{ + return m_backendOutput->scale(); +} + +Rect LogicalOutput::geometry() const +{ + return Rect(m_backendOutput->position(), m_backendOutput->pixelSize() / scale()); +} + +RectF LogicalOutput::geometryF() const +{ + return RectF(m_backendOutput->position(), QSizeF(m_backendOutput->pixelSize()) / scale()); +} + +QSize LogicalOutput::modeSize() const +{ + return m_backendOutput->modeSize(); +} + +QSize LogicalOutput::pixelSize() const +{ + return orientateSize(modeSize()); +} + +QSize LogicalOutput::orientateSize(const QSize &size) const +{ + switch (transform().kind()) { + case OutputTransform::Rotate90: + case OutputTransform::Rotate270: + case OutputTransform::FlipX90: + case OutputTransform::FlipX270: + return size.transposed(); + default: + return size; + } +} + +BackendOutput *LogicalOutput::backendOutput() const +{ + return m_backendOutput; +} + +QString LogicalOutput::name() const +{ + return m_backendOutput->name(); +} + +QString LogicalOutput::description() const +{ + return m_backendOutput->description(); +} + +QString LogicalOutput::manufacturer() const +{ + return m_backendOutput->manufacturer(); +} + +QString LogicalOutput::model() const +{ + return m_backendOutput->model(); +} + +QString LogicalOutput::serialNumber() const +{ + return m_backendOutput->serialNumber(); +} + +QString LogicalOutput::uuid() const +{ + return m_backendOutput->uuid(); +} + +bool LogicalOutput::isPlaceholder() const +{ + return m_backendOutput->isPlaceholder(); +} + +QSize LogicalOutput::physicalSize() const +{ + return m_backendOutput->physicalSize(); +} + +const std::shared_ptr &LogicalOutput::blendingColor() const +{ + return m_backendOutput->blendingColor(); +} + +OutputTransform LogicalOutput::transform() const +{ + return m_backendOutput->transform(); +} + +bool LogicalOutput::isInternal() const +{ + return m_backendOutput->isInternal(); +} + +uint32_t LogicalOutput::refreshRate() const +{ + return m_backendOutput->refreshRate(); +} + +} // namespace KWin + +#include "moc_output.cpp" diff --git a/local/recipes/kde/kwin/source/src/core/output.h b/local/recipes/kde/kwin/source/src/core/output.h new file mode 100644 index 0000000000..f833f063c2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/output.h @@ -0,0 +1,304 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Roman Gilg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include + +#include "core/rect.h" +#include "core/region.h" +#include "renderloop.h" +#include "utils/edid.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +class BackendOutput; +class OutputChangeSet; + +/** + * The OutputTransform type is used to describe the transform applied to the output content. + */ +class KWIN_EXPORT OutputTransform +{ +public: + enum Kind { + Normal = 0, // no rotation + Rotate90 = 1, // rotate 90 degrees counterclockwise + Rotate180 = 2, // rotate 180 degrees counterclockwise + Rotate270 = 3, // rotate 270 degrees counterclockwise + FlipX = 4, // mirror horizontally + FlipX90 = 5, // mirror horizontally, then rotate 90 degrees counterclockwise + FlipX180 = 6, // mirror horizontally, then rotate 180 degrees counterclockwise + FlipX270 = 7, // mirror horizontally, then rotate 270 degrees counterclockwise + FlipY = FlipX180, // mirror vertically + FlipY90 = FlipX270, // mirror vertically, then rotate 90 degrees counterclockwise + FlipY180 = FlipX, // mirror vertically, then rotate 180 degrees counterclockwise + FlipY270 = FlipX90, // mirror vertically, then rotate 270 degrees counterclockwise + }; + + OutputTransform() = default; + OutputTransform(Kind kind) + : m_kind(kind) + { + } + + bool operator<=>(const OutputTransform &other) const = default; + + /** + * Returns the transform kind. + */ + Kind kind() const; + + /** + * Returns the inverse transform. The inverse transform can be used for mapping between + * surface and buffer coordinate systems. + */ + OutputTransform inverted() const; + + /** + * Applies the output transform to the given @a size. + */ + QSizeF map(const QSizeF &size) const; + QSize map(const QSize &size) const; + + /** + * Applies the output transform to the given @a rect within a buffer with dimensions @a bounds. + */ + RectF map(const RectF &rect, const QSizeF &bounds) const; + Rect map(const Rect &rect, const QSize &bounds) const; + + /** + * Applies the output transform to the given @a point. + */ + QPointF map(const QPointF &point, const QSizeF &bounds) const; + QPoint map(const QPoint &point, const QSize &bounds) const; + + /** + * Applies the output transform to the given @a region + */ + Region map(const Region ®ion, const QSize &bounds) const; + + /** + * Returns an output transform that is equivalent to applying this transform and @a other + * transform sequentially. + */ + OutputTransform combine(OutputTransform other) const; + + /** + * Returns the matrix corresponding to this output transform. + */ + QMatrix4x4 toMatrix() const; + +private: + Kind m_kind = Kind::Normal; +}; + +class KWIN_EXPORT OutputMode +{ +public: + enum class Flag : uint { + Preferred = 0x1, + Generated = 0x2, + Custom = 0x8, + ReducedBlanking = 0x10, + }; + Q_DECLARE_FLAGS(Flags, Flag) + + OutputMode(const QSize &size, uint32_t refreshRate, Flags flags = {}); + virtual ~OutputMode() = default; + + QSize size() const; + uint32_t refreshRate() const; + Flags flags() const; + + bool isRemoved() const; + void setRemoved(); + +private: + const QSize m_size; + const uint32_t m_refreshRate; + const Flags m_flags; + bool m_removed = false; +}; + +struct CustomModeDefinition +{ + QSize size; + uint32_t refreshRate; + OutputMode::Flags flags; +}; + +/** + * Generic output representation for window management purposes + */ +class KWIN_EXPORT LogicalOutput : public QObject +{ + Q_OBJECT + Q_PROPERTY(KWin::Rect geometry READ geometry NOTIFY geometryChanged) + Q_PROPERTY(qreal devicePixelRatio READ scale NOTIFY scaleChanged) + Q_PROPERTY(QString name READ name CONSTANT) + Q_PROPERTY(QString manufacturer READ manufacturer CONSTANT) + Q_PROPERTY(QString model READ model CONSTANT) + Q_PROPERTY(QString serialNumber READ serialNumber CONSTANT) + +public: + explicit LogicalOutput(BackendOutput *backendOutput); + ~LogicalOutput() override; + + void ref(); + void unref(); + + /** + * Maps the specified @a rect from the global coordinate system to the output-local coords. + */ + Rect mapFromGlobal(const Rect &rect) const; + + /** + * Maps the specified @a rect from the global coordinate system to the output-local coords. + */ + RectF mapFromGlobal(const RectF &rect) const; + + /** + * Maps a @a rect in this output coordinates to the global coordinate system. + */ + RectF mapToGlobal(const RectF &rect) const; + + /** + * Maps a @a region in this output coordinates to the global coordinate system. + */ + Region mapToGlobal(const Region ®ion) const; + + Q_INVOKABLE QPointF mapToGlobal(const QPointF &pos) const; + Q_INVOKABLE QPointF mapFromGlobal(const QPointF &pos) const; + + /** + * Returns a short identifiable name of this output. + */ + QString name() const; + QString description() const; + QString manufacturer() const; + QString model() const; + QString serialNumber() const; + QString uuid() const; + + /** + * Returns geometry of this output in device independent pixels. + */ + Rect geometry() const; + + /** + * Returns geometry of this output in device independent pixels, without rounding + */ + RectF geometryF() const; + + /** + * Equivalent to `Rect(QPoint(0, 0), geometry().size())` + */ + Rect rect() const; + + /** + * Equivalent to `RectF(QPointF(0, 0), geometryF().size())` + */ + RectF rectF() const; + + uint32_t refreshRate() const; + + /** + * Returns whether this output is connected through an internal connector, + * e.g. LVDS, or eDP. + */ + bool isInternal() const; + + /** + * Returns the ratio between physical pixels and logical pixels. + */ + qreal scale() const; + + /** + * Returns the non-rotated physical size of this output, in millimeters. + */ + QSize physicalSize() const; + + /** Returns the resolution of the output. */ + QSize pixelSize() const; + QSize modeSize() const; + OutputTransform transform() const; + QSize orientateSize(const QSize &size) const; + + bool isPlaceholder() const; + + /** + * The color space in which the scene is blended + */ + const std::shared_ptr &blendingColor() const; + + BackendOutput *backendOutput() const; + +Q_SIGNALS: + /** + * This signal is emitted when the geometry of this output has changed. + */ + void geometryChanged(); + /** + * This signal is emitted when the device pixel ratio of the output has changed. + */ + void scaleChanged(); + + /** + * Notifies that the output is about to change configuration based on a + * user interaction. + * + * Be it because it gets a transformation or moved around. + * + * Only to be used for effects + */ + void aboutToChange(OutputChangeSet *changeSet); + + /** + * Notifies that the output changed based on a user interaction. + * + * Be it because it gets a transformation or moved around. + * + * Only to be used for effects + */ + void changed(); + + void blendingColorChanged(); + void transformChanged(); + /** + * This signal is emitted when either modeSize or refreshRate change + */ + void currentModeChanged(); + +protected: + BackendOutput *const m_backendOutput; + int m_refCount = 1; +}; + +inline Rect LogicalOutput::rect() const +{ + return Rect(QPoint(0, 0), geometry().size()); +} + +inline RectF LogicalOutput::rectF() const +{ + return RectF(QPointF(0, 0), geometryF().size()); +} + +KWIN_EXPORT QDebug operator<<(QDebug debug, const LogicalOutput *output); + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/outputbackend.cpp b/local/recipes/kde/kwin/source/src/core/outputbackend.cpp new file mode 100644 index 0000000000..b0f33963cf --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/outputbackend.cpp @@ -0,0 +1,112 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "outputbackend.h" + +#include "inputbackend.h" +#include "opengl/eglbackend.h" +#include "opengl/egldisplay.h" +#include "output.h" +#include "outputconfiguration.h" +#include "qpainter/qpainterbackend.h" + +namespace KWin +{ + +OutputBackend::OutputBackend(QObject *parent) + : QObject(parent) +{ +} + +OutputBackend::~OutputBackend() +{ +} + +std::unique_ptr OutputBackend::createInputBackend() +{ + return nullptr; +} + +std::unique_ptr OutputBackend::createOpenGLBackend() +{ + return nullptr; +} + +std::unique_ptr OutputBackend::createQPainterBackend() +{ + return nullptr; +} + +OutputConfigurationError OutputBackend::applyOutputChanges(const OutputConfiguration &config) +{ + const auto availableOutputs = outputs(); + QList toBeEnabledOutputs; + QList toBeDisabledOutputs; + for (BackendOutput *output : availableOutputs) { + if (const auto changeset = config.constChangeSet(output)) { + if (changeset->enabled.value_or(output->isEnabled())) { + toBeEnabledOutputs << output; + } else { + toBeDisabledOutputs << output; + } + } + } + for (BackendOutput *output : toBeEnabledOutputs) { + output->applyChanges(config); + } + for (BackendOutput *output : toBeDisabledOutputs) { + output->applyChanges(config); + } + return OutputConfigurationError::None; +} + +BackendOutput *OutputBackend::findOutput(const QString &name) const +{ + const auto candidates = outputs(); + for (BackendOutput *candidate : candidates) { + if (candidate->name() == name) { + return candidate; + } + } + return nullptr; +} + +BackendOutput *OutputBackend::createVirtualOutput(const QString &name, const QString &description, const QSize &size, double scale) +{ + return nullptr; +} + +void OutputBackend::removeVirtualOutput(BackendOutput *output) +{ + Q_ASSERT(!output); +} + +QString OutputBackend::supportInformation() const +{ + return QStringLiteral("Name: %1\n").arg(metaObject()->className()); +} + +::EGLContext OutputBackend::sceneEglGlobalShareContext() const +{ + return m_globalShareContext; +} + +void OutputBackend::setSceneEglGlobalShareContext(::EGLContext context) +{ + m_globalShareContext = context; +} + +Session *OutputBackend::session() const +{ + return nullptr; +} + +} // namespace KWin + +#include "moc_outputbackend.cpp" diff --git a/local/recipes/kde/kwin/source/src/core/outputbackend.h b/local/recipes/kde/kwin/source/src/core/outputbackend.h new file mode 100644 index 0000000000..f6a353a37d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/outputbackend.h @@ -0,0 +1,106 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/globals.h" +#include +#include + +#include + +#include +#include + +namespace KWin +{ + +class LogicalOutput; +class InputBackend; +class EglBackend; +class QPainterBackend; +class OutputConfiguration; +class EglDisplay; +class Session; +class BackendOutput; + +class KWIN_EXPORT OutputBackend : public QObject +{ + Q_OBJECT +public: + ~OutputBackend() override; + + virtual bool initialize() = 0; + virtual std::unique_ptr createInputBackend(); + virtual std::unique_ptr createOpenGLBackend(); + virtual std::unique_ptr createQPainterBackend(); + + virtual EglDisplay *sceneEglDisplayObject() const = 0; + /** + * Returns the compositor-wide shared EGL context. This function may return EGL_NO_CONTEXT + * if the underlying rendering backend does not use EGL. + * + * Note that the returned context should never be made current. Instead, create a context + * that shares with this one and make the new context current. + */ + ::EGLContext sceneEglGlobalShareContext() const; + /** + * Sets the global share context to @a context. This function is intended to be called only + * by rendering backends. + */ + void setSceneEglGlobalShareContext(::EGLContext context); + + /** + * The CompositingTypes supported by the Platform. + * The first item should be the most preferred one. + * @since 5.11 + */ + virtual QList supportedCompositors() const = 0; + + virtual QList outputs() const = 0; + BackendOutput *findOutput(const QString &name) const; + + /** + * A string of information to include in kwin debug output + * It should not be translated. + * + * The base implementation prints the name. + * @since 5.12 + */ + virtual QString supportInformation() const; + + virtual BackendOutput *createVirtualOutput(const QString &name, const QString &description, const QSize &size, qreal scale); + virtual void removeVirtualOutput(BackendOutput *output); + + /** + * Applies the output changes. Default implementation only sets values common between platforms + */ + virtual OutputConfigurationError applyOutputChanges(const OutputConfiguration &config); + + virtual Session *session() const; + +Q_SIGNALS: + void outputsQueried(); + /** + * This signal is emitted when an output has been connected. The @a output is not ready + * for compositing yet. + */ + void outputAdded(BackendOutput *output); + /** + * This signal is emitted when an output has been disconnected. + */ + void outputRemoved(BackendOutput *output); + +protected: + explicit OutputBackend(QObject *parent = nullptr); + + ::EGLContext m_globalShareContext = EGL_NO_CONTEXT; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/outputconfiguration.cpp b/local/recipes/kde/kwin/source/src/core/outputconfiguration.cpp new file mode 100644 index 0000000000..1948908ef3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/outputconfiguration.cpp @@ -0,0 +1,27 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2021 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "outputconfiguration.h" + +namespace KWin +{ + +std::shared_ptr OutputConfiguration::changeSet(BackendOutput *output) +{ + auto &ret = m_properties[output]; + if (!ret) { + ret = std::make_shared(); + } + return ret; +} + +std::shared_ptr OutputConfiguration::constChangeSet(BackendOutput *output) const +{ + return m_properties[output]; +} +} diff --git a/local/recipes/kde/kwin/source/src/core/outputconfiguration.h b/local/recipes/kde/kwin/source/src/core/outputconfiguration.h new file mode 100644 index 0000000000..edd289b61b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/outputconfiguration.h @@ -0,0 +1,99 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2021 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "kwin_export.h" + +#include "backendoutput.h" + +#include +#include + +namespace KWin +{ + +class IccProfile; +class BrightnessDevice; + +class KWIN_EXPORT OutputChangeSet +{ +public: + std::optional> mode; + std::optional desiredModeSize; + std::optional desiredModeRefreshRate; + std::optional> desiredModeFlags; + std::optional enabled; + std::optional pos; + std::optional scale; + std::optional scaleSetting; + std::optional transform; + std::optional manualTransform; + std::optional overscan; + std::optional rgbRange; + std::optional vrrPolicy; + std::optional highDynamicRange; + std::optional referenceLuminance; + std::optional wideColorGamut; + std::optional autoRotationPolicy; + std::optional iccProfilePath; + std::optional> iccProfile; + std::optional> maxPeakBrightnessOverride; + std::optional> maxAverageBrightnessOverride; + std::optional> minBrightnessOverride; + std::optional sdrGamutWideness; + std::optional colorProfileSource; + std::optional brightness; + // setting "brightness" may trigger animations; + // setting the current brightness doesn't + std::optional currentHardwareBrightness; + std::optional allowSdrSoftwareBrightness; + std::optional colorPowerTradeoff; + std::optional dimming; + std::optional brightnessDevice; + std::optional uuid; + std::optional replicationSource; + std::optional detectedDdcCi; + std::optional allowDdcCi; + std::optional maxBitsPerColor; + std::optional edrPolicy; + std::optional sharpness; + std::optional dpmsMode; + std::optional priority; + std::optional> customModes; + std::optional deviceOffset; + std::optional automaticBrightness; + std::optional autoBrightnessCurve; + std::optional brightnessReason; +}; + +class KWIN_EXPORT OutputConfiguration +{ +public: + std::shared_ptr changeSet(BackendOutput *output); + std::shared_ptr constChangeSet(BackendOutput *output) const; + + enum class Source { + /** + * The output configuration is provided by the user, for example after changing + * display settings in system settings. + */ + User, + /** + * The output configuration is provided by the system/compositor, for example + * when automatically adjusting the brightness of a monitor, etc. + */ + System, + }; + Source source = Source::System; + +private: + QMap> m_properties; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/core/outputlayer.cpp b/local/recipes/kde/kwin/source/src/core/outputlayer.cpp new file mode 100644 index 0000000000..efd1821191 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/outputlayer.cpp @@ -0,0 +1,284 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "outputlayer.h" +#include "scene/surfaceitem.h" +#include "scene/surfaceitem_wayland.h" +#include "wayland/surface.h" + +namespace KWin +{ + +OutputLayer::OutputLayer(BackendOutput *output, OutputLayerType type) + : m_type(type) + , m_output(output) + , m_renderLoop(output ? output->renderLoop() : nullptr) + , m_zpos(type == OutputLayerType::Primary ? 0 : 1) + , m_minZpos(m_zpos) + , m_maxZpos(m_zpos) +{ +} + +OutputLayer::OutputLayer(BackendOutput *output, OutputLayerType type, int zpos, int minZpos, int maxZpos) + : m_type(type) + , m_output(output) + , m_renderLoop(output ? output->renderLoop() : nullptr) + , m_zpos(zpos) + , m_minZpos(minZpos) + , m_maxZpos(maxZpos) +{ +} + +OutputLayerType OutputLayer::type() const +{ + return m_type; +} + +void OutputLayer::setRenderLoop(RenderLoop *loop) +{ + m_renderLoop = loop; +} + +void OutputLayer::setOutput(BackendOutput *output) +{ + m_output = output; + if (output) { + m_renderLoop = output->renderLoop(); + addDeviceRepaint(Region::infinite()); + } else { + m_renderLoop = nullptr; + } +} + +QPointF OutputLayer::hotspot() const +{ + return m_hotspot; +} + +void OutputLayer::setHotspot(const QPointF &hotspot) +{ + m_hotspot = hotspot; +} + +QList OutputLayer::recommendedSizes() const +{ + return {}; +} + +Region OutputLayer::deviceRepaints() const +{ + return m_repaints; +} + +void OutputLayer::scheduleRepaint(Item *item) +{ + if (!m_output) { + return; + } + m_repaintScheduled = true; + if (m_renderLoop) { + m_renderLoop->scheduleRepaint(item, this); + } + Q_EMIT repaintScheduled(); +} + +void OutputLayer::addDeviceRepaint(const Region ®ion) +{ + if (region.isEmpty() || !m_output) { + return; + } + m_repaints += region; + if (m_renderLoop) { + m_renderLoop->scheduleRepaint(nullptr, this); + } + Q_EMIT repaintScheduled(); +} + +void OutputLayer::resetRepaints() +{ + m_repaintScheduled = false; + m_repaints = Region(); +} + +bool OutputLayer::needsRepaint() const +{ + return m_repaintScheduled || !m_repaints.isEmpty(); +} + +bool OutputLayer::importScanoutBuffer(GraphicsBuffer *buffer, const std::shared_ptr &frame) +{ + return false; +} + +std::optional OutputLayer::beginFrame() +{ + return doBeginFrame(); +} + +bool OutputLayer::endFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) +{ + return doEndFrame(renderedDeviceRegion, damagedDeviceRegion, frame); +} + +void OutputLayer::setScanoutCandidate(SurfaceItem *item) +{ + if (m_scanoutCandidate && item != m_scanoutCandidate) { + m_scanoutCandidate->setScanoutHint(nullptr, {}); + } + m_scanoutCandidate = item; +} + +void OutputLayer::setEnabled(bool enable) +{ + m_enabled = enable; + if (!enable) { + releaseBuffers(); + } +} + +bool OutputLayer::isEnabled() const +{ + return m_enabled; +} + +RectF OutputLayer::sourceRect() const +{ + return m_sourceRect; +} + +void OutputLayer::setSourceRect(const RectF &rect) +{ + m_sourceRect = rect; +} + +OutputTransform OutputLayer::offloadTransform() const +{ + return m_offloadTransform; +} + +OutputTransform OutputLayer::bufferTransform() const +{ + return m_bufferTransform; +} + +Rect OutputLayer::targetRect() const +{ + return m_targetRect; +} + +void OutputLayer::setTargetRect(const Rect &rect) +{ + m_targetRect = rect; +} + +QHash> OutputLayer::supportedAsyncDrmFormats() const +{ + return supportedDrmFormats(); +} + +void OutputLayer::setOffloadTransform(const OutputTransform &transform) +{ + m_offloadTransform = transform; +} + +void OutputLayer::setBufferTransform(const OutputTransform &transform) +{ + m_bufferTransform = transform; +} + +const ColorPipeline &OutputLayer::colorPipeline() const +{ + return m_colorPipeline; +} + +const std::shared_ptr &OutputLayer::colorDescription() const +{ + return m_color; +} + +RenderingIntent OutputLayer::renderIntent() const +{ + return m_renderingIntent; +} + +void OutputLayer::setColor(const std::shared_ptr &color, RenderingIntent intent, const ColorPipeline &pipeline) +{ + m_color = color; + m_renderingIntent = intent; + m_colorPipeline = pipeline; +} + +bool OutputLayer::preparePresentationTest() +{ + return true; +} + +void OutputLayer::setRequiredAlphaBits(uint32_t bits) +{ + m_requiredAlphaBits = bits; +} + +void OutputLayer::setZpos(int zpos) +{ + Q_ASSERT(zpos >= m_minZpos); + Q_ASSERT(zpos <= m_maxZpos); + m_zpos = zpos; +} + +int OutputLayer::zpos() const +{ + return m_zpos; +} + +int OutputLayer::minZpos() const +{ + return m_minZpos; +} + +int OutputLayer::maxZpos() const +{ + return m_maxZpos; +} + +QList OutputLayer::filterAndSortFormats(const QHash> &formats, uint32_t requiredAlphaBits, BackendOutput::ColorPowerTradeoff tradeoff) +{ + QList ret; + for (auto it = formats.begin(); it != formats.end(); it++) { + const auto info = FormatInfo::get(it.key()); + if (!info) { + continue; + } + if (info->alphaBits < requiredAlphaBits) { + continue; + } + if (info->bitsPerColor < 8) { + continue; + } + ret.push_back(*info); + } + std::ranges::sort(ret, [tradeoff](const FormatInfo &before, const FormatInfo &after) { + if (tradeoff == BackendOutput::ColorPowerTradeoff::PreferAccuracy && before.bitsPerColor != after.bitsPerColor) { + return before.bitsPerColor > after.bitsPerColor; + } + if (before.floatingPoint != after.floatingPoint) { + return !before.floatingPoint; + } + const bool beforeHasAlpha = before.alphaBits != 0; + const bool afterHasAlpha = after.alphaBits != 0; + if (beforeHasAlpha != afterHasAlpha) { + return beforeHasAlpha; + } + if (before.bitsPerPixel != after.bitsPerPixel) { + return before.bitsPerPixel < after.bitsPerPixel; + } + return before.bitsPerColor > after.bitsPerColor; + }); + return ret; +} + +} // namespace KWin + +#include "moc_outputlayer.cpp" diff --git a/local/recipes/kde/kwin/source/src/core/outputlayer.h b/local/recipes/kde/kwin/source/src/core/outputlayer.h new file mode 100644 index 0000000000..f7efa40a84 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/outputlayer.h @@ -0,0 +1,183 @@ + +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/backendoutput.h" +#include "core/colorpipeline.h" +#include "core/rendertarget.h" +#include "kwin_export.h" +#include "utils/drm_format_helper.h" + +#include +#include + +#include +#include + +namespace KWin +{ + +class SurfaceItem; +class DrmDevice; +class GraphicsBuffer; +class OutputFrame; +class GLTexture; + +struct OutputLayerBeginFrameInfo +{ + RenderTarget renderTarget; + Region repaint; +}; + +enum class OutputLayerType { + /** + * Required for driving an output + */ + Primary, + /** + * Can only be used for cursor, or cursor-attached items, + * as the layer may be moved asynchronously by a different process + * (like the host compositor in a nested session) + */ + CursorOnly, + /** + * Should be preferred to normal overlays when possible, as they're + * often more efficient (but often come with size restrictions) + */ + EfficientOverlay, + /** + * Generic over- or underlay + */ + GenericLayer, +}; + +class KWIN_EXPORT OutputLayer : public QObject +{ + Q_OBJECT +public: + explicit OutputLayer(BackendOutput *output, OutputLayerType type); + explicit OutputLayer(BackendOutput *output, OutputLayerType type, int zpos, int minZpos, int maxZpos); + + OutputLayerType type() const; + + void setRenderLoop(RenderLoop *loop); + void setOutput(BackendOutput *output); + + QPointF hotspot() const; + void setHotspot(const QPointF &hotspot); + + /** + * For some layers it can be beneficial to use specific sizes only. + * This returns those specific sizes, if present + */ + virtual QList recommendedSizes() const; + + Region deviceRepaints() const; + void resetRepaints(); + void scheduleRepaint(Item *item); + void addDeviceRepaint(const Region ®ion); + bool needsRepaint() const; + + /** + * Enables or disables this layer. Note that disabling the primary layer will cause problems + */ + void setEnabled(bool enable); + bool isEnabled() const; + + /** + * If the output backend needs to test presentations, + * the layer should override this function to allocate buffers for the test + */ + virtual bool preparePresentationTest(); + + std::optional beginFrame(); + bool endFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame); + + /** + * Tries to import the newest buffer of the surface for direct scanout and does some early checks + * for whether or not direct scanout *could* be successful + * A presentation request on the output must however be used afterwards to find out if it's actually successful! + */ + virtual bool importScanoutBuffer(GraphicsBuffer *buffer, const std::shared_ptr &frame); + + void setScanoutCandidate(SurfaceItem *item); + + virtual DrmDevice *scanoutDevice() const = 0; + virtual QHash> supportedDrmFormats() const = 0; + virtual QHash> supportedAsyncDrmFormats() const; + + /** + * Returns the source rect this output layer should sample from, in buffer local coordinates + */ + RectF sourceRect() const; + void setSourceRect(const RectF &rect); + /** + * Returns the target rect this output layer should be shown at, in device coordinates + */ + Rect targetRect() const; + void setTargetRect(const Rect &rect); + /** + * Returns the transform this layer will apply to content passed to it + */ + OutputTransform offloadTransform() const; + void setOffloadTransform(const OutputTransform &transform); + /** + * Returns the transform a buffer passed into this layer already has + */ + OutputTransform bufferTransform() const; + void setBufferTransform(const OutputTransform &transform); + + const ColorPipeline &colorPipeline() const; + const std::shared_ptr &colorDescription() const; + RenderingIntent renderIntent() const; + void setColor(const std::shared_ptr &color, RenderingIntent intent, const ColorPipeline &pipeline); + + /** + * Set the required bits for compositing on this plane. Direct scanout is not affected. + */ + void setRequiredAlphaBits(uint32_t bits); + + void setZpos(int zpos); + int zpos() const; + int minZpos() const; + int maxZpos() const; + + static QList filterAndSortFormats(const QHash> &formats, uint32_t requiredAlphaBits, BackendOutput::ColorPowerTradeoff tradeoff); + + virtual void releaseBuffers() = 0; + +Q_SIGNALS: + void repaintScheduled(); + +protected: + virtual std::optional doBeginFrame() = 0; + virtual bool doEndFrame(const Region &renderedDeviceRegion, const Region &damagedDeviceRegion, OutputFrame *frame) = 0; + + const OutputLayerType m_type; + Region m_repaints; + QPointF m_hotspot; + RectF m_sourceRect; + Rect m_targetRect; + qreal m_scale = 1.0; + bool m_enabled = false; + OutputTransform m_offloadTransform = OutputTransform::Kind::Normal; + OutputTransform m_bufferTransform = OutputTransform::Kind::Normal; + ColorPipeline m_colorPipeline; + std::shared_ptr m_color = ColorDescription::sRGB; + RenderingIntent m_renderingIntent = RenderingIntent::Perceptual; + QPointer m_scanoutCandidate; + QPointer m_output; + uint32_t m_requiredAlphaBits = 0; + bool m_repaintScheduled = false; + RenderLoop *m_renderLoop = nullptr; + int m_zpos = 0; + int m_minZpos = 0; + int m_maxZpos = 0; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/pixelgrid.h b/local/recipes/kde/kwin/source/src/core/pixelgrid.h new file mode 100644 index 0000000000..26824c719e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/pixelgrid.h @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "kwin_export.h" + +#include +#include + +namespace KWin +{ + +KWIN_EXPORT inline QPoint snapToPixelGrid(const QPointF &point) +{ + return QPoint(std::round(point.x()), std::round(point.y())); +} + +KWIN_EXPORT inline QPointF snapToPixelGridF(const QPointF &point) +{ + return QPointF(std::round(point.x()), std::round(point.y())); +} + +KWIN_EXPORT inline QRect snapToPixelGrid(const QRectF &rect) +{ + const QPoint topLeft = snapToPixelGrid(rect.topLeft()); + const QPoint bottomRight = snapToPixelGrid(rect.bottomRight()); + return QRect(topLeft.x(), topLeft.y(), bottomRight.x() - topLeft.x(), bottomRight.y() - topLeft.y()); +} + +KWIN_EXPORT inline QRectF snapToPixelGridF(const QRectF &rect) +{ + return QRectF(snapToPixelGridF(rect.topLeft()), snapToPixelGridF(rect.bottomRight())); +} + +KWIN_EXPORT constexpr double snapToPixels(double logicalValue, double scale) +{ + return std::round(logicalValue * scale) / scale; +} + +KWIN_EXPORT constexpr QPointF snapToPixels(const QPointF &logicalValue, double scale) +{ + return QPointF(snapToPixels(logicalValue.x(), scale), snapToPixels(logicalValue.y(), scale)); +} + +KWIN_EXPORT constexpr QSizeF snapToPixels(const QSizeF &logicalValue, double scale) +{ + return QSizeF(snapToPixels(logicalValue.width(), scale), snapToPixels(logicalValue.height(), scale)); +} + +KWIN_EXPORT constexpr QRectF snapToPixels(const QRectF &logicalValue, double scale) +{ + const QPointF topLeft = snapToPixels(logicalValue.topLeft(), scale); + const QPointF bottomRight = snapToPixels(logicalValue.bottomRight(), scale); + return QRectF(topLeft, bottomRight); +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/renderbackend.cpp b/local/recipes/kde/kwin/source/src/core/renderbackend.cpp new file mode 100644 index 0000000000..f04ba499f7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/renderbackend.cpp @@ -0,0 +1,193 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "renderbackend.h" +#include "renderloop_p.h" +#include "syncobjtimeline.h" + +#include +#include +#include +#include + +namespace KWin +{ + +RenderTimeSpan RenderTimeSpan::operator|(const RenderTimeSpan &other) const +{ + return RenderTimeSpan{ + .start = std::min(start, other.start), + .end = std::max(end, other.end), + }; +} + +CpuRenderTimeQuery::CpuRenderTimeQuery() + : m_start(std::chrono::steady_clock::now()) +{ +} + +void CpuRenderTimeQuery::end() +{ + m_end = std::chrono::steady_clock::now(); +} + +std::optional CpuRenderTimeQuery::query() +{ + Q_ASSERT(m_end); + return RenderTimeSpan{ + .start = m_start, + .end = *m_end, + }; +} + +OutputFrame::OutputFrame(RenderLoop *loop, std::chrono::nanoseconds refreshDuration) + : m_loop(loop) + , m_refreshDuration(refreshDuration) + , m_targetPageflipTime(loop->nextPresentationTimestamp()) + , m_predictedRenderTime(loop->predictedRenderTime()) +{ +} + +OutputFrame::~OutputFrame() +{ + Q_ASSERT(QThread::currentThread() == QCoreApplication::instance()->thread()); + if (!m_presented && m_loop) { + RenderLoopPrivate::get(m_loop)->notifyFrameDropped(); + } +} + +void OutputFrame::addFeedback(std::shared_ptr &&feedback) +{ + m_feedbacks.push_back(std::move(feedback)); +} + +std::optional OutputFrame::queryRenderTime() const +{ + if (m_renderTimeQueries.empty()) { + return RenderTimeSpan{}; + } + const auto first = m_renderTimeQueries.front()->query(); + if (!first) { + return std::nullopt; + } + RenderTimeSpan ret = *first; + for (const auto &query : m_renderTimeQueries | std::views::drop(1)) { + const auto opt = query->query(); + if (!opt) { + return std::nullopt; + } + ret = ret | *opt; + } + return ret; +} + +void OutputFrame::presented(std::chrono::nanoseconds timestamp, PresentationMode mode) +{ + Q_ASSERT(!m_presented); + m_presented = true; + + const auto renderTime = queryRenderTime(); + if (m_loop) { + RenderLoopPrivate::get(m_loop)->notifyFrameCompleted(timestamp, renderTime, mode, this); + } + for (const auto &feedback : m_feedbacks) { + feedback->presented(m_refreshDuration, timestamp, mode); + } +} + +void OutputFrame::setContentType(ContentType type) +{ + m_contentType = type; +} + +std::optional OutputFrame::contentType() const +{ + return m_contentType; +} + +void OutputFrame::setPresentationMode(PresentationMode mode) +{ + m_presentationMode = mode; +} + +PresentationMode OutputFrame::presentationMode() const +{ + return m_presentationMode; +} + +void OutputFrame::addRenderTimeQuery(std::unique_ptr &&query) +{ + m_renderTimeQueries.push_back(std::move(query)); +} + +std::chrono::steady_clock::time_point OutputFrame::targetPageflipTime() const +{ + return m_targetPageflipTime; +} + +std::chrono::nanoseconds OutputFrame::refreshDuration() const +{ + return m_refreshDuration; +} + +std::chrono::nanoseconds OutputFrame::predictedRenderTime() const +{ + return m_predictedRenderTime; +} + +std::optional OutputFrame::brightness() const +{ + return m_brightness; +} + +void OutputFrame::setBrightness(double brightness) +{ + m_brightness = brightness; +} + +std::optional OutputFrame::dimmingFactor() const +{ + return m_dimmingFactor; +} + +void OutputFrame::setDimmingFactor(double factor) +{ + m_dimmingFactor = factor; +} + +std::optional OutputFrame::artificialHdrHeadroom() const +{ + return m_artificialHdrHeadroom; +} + +void OutputFrame::setArtificialHdrHeadroom(double edr) +{ + m_artificialHdrHeadroom = edr; +} + +bool RenderBackend::checkGraphicsReset() +{ + return false; +} + +DrmDevice *RenderBackend::drmDevice() const +{ + return nullptr; +} + +bool RenderBackend::testImportBuffer(GraphicsBuffer *buffer) +{ + return false; +} + +QHash> RenderBackend::supportedFormats() const +{ + return QHash>{{DRM_FORMAT_XRGB8888, QList{DRM_FORMAT_MOD_LINEAR}}}; +} + +} // namespace KWin + +#include "moc_renderbackend.cpp" diff --git a/local/recipes/kde/kwin/source/src/core/renderbackend.h b/local/recipes/kde/kwin/source/src/core/renderbackend.h new file mode 100644 index 0000000000..d730ca91d0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/renderbackend.h @@ -0,0 +1,139 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/rendertarget.h" +#include "effect/globals.h" +#include "utils/filedescriptor.h" + +#include +#include +#include + +namespace KWin +{ + +class GraphicsBuffer; +class LogicalOutput; +class OutputLayer; +class PresentationFeedback; +class RenderLoop; +class DrmDevice; +class SyncTimeline; + +class PresentationFeedback +{ +public: + explicit PresentationFeedback() = default; + PresentationFeedback(const PresentationFeedback ©) = delete; + PresentationFeedback(PresentationFeedback &&move) = default; + virtual ~PresentationFeedback() = default; + + virtual void presented(std::chrono::nanoseconds refreshCycleDuration, std::chrono::nanoseconds timestamp, PresentationMode mode) = 0; +}; + +struct RenderTimeSpan +{ + std::chrono::steady_clock::time_point start = std::chrono::steady_clock::time_point{std::chrono::nanoseconds::zero()}; + std::chrono::steady_clock::time_point end = std::chrono::steady_clock::time_point{std::chrono::nanoseconds::zero()}; + + RenderTimeSpan operator|(const RenderTimeSpan &other) const; +}; + +class KWIN_EXPORT RenderTimeQuery +{ +public: + virtual ~RenderTimeQuery() = default; + virtual std::optional query() = 0; +}; + +class KWIN_EXPORT CpuRenderTimeQuery : public RenderTimeQuery +{ +public: + /** + * marks the start of the query + */ + explicit CpuRenderTimeQuery(); + + void end(); + + std::optional query() override; + +private: + const std::chrono::steady_clock::time_point m_start; + std::optional m_end; +}; + +class KWIN_EXPORT OutputFrame +{ +public: + explicit OutputFrame(RenderLoop *loop, std::chrono::nanoseconds refreshDuration); + ~OutputFrame(); + + void presented(std::chrono::nanoseconds timestamp, PresentationMode mode); + + void addFeedback(std::shared_ptr &&feedback); + + void setContentType(ContentType type); + std::optional contentType() const; + + void setPresentationMode(PresentationMode mode); + PresentationMode presentationMode() const; + + void addRenderTimeQuery(std::unique_ptr &&query); + + std::chrono::steady_clock::time_point targetPageflipTime() const; + std::chrono::nanoseconds refreshDuration() const; + std::chrono::nanoseconds predictedRenderTime() const; + + std::optional brightness() const; + void setBrightness(double brightness); + + std::optional dimmingFactor() const; + void setDimmingFactor(double factor); + + std::optional artificialHdrHeadroom() const; + void setArtificialHdrHeadroom(double edr); + +private: + std::optional queryRenderTime() const; + + const QPointer m_loop; + const std::chrono::nanoseconds m_refreshDuration; + const std::chrono::steady_clock::time_point m_targetPageflipTime; + const std::chrono::nanoseconds m_predictedRenderTime; + std::vector> m_feedbacks; + std::optional m_contentType; + PresentationMode m_presentationMode = PresentationMode::VSync; + std::vector> m_renderTimeQueries; + bool m_presented = false; + std::optional m_brightness; + std::optional m_dimmingFactor; + std::optional m_artificialHdrHeadroom; +}; + +/** + * The RenderBackend class is the base class for all rendering backends. + */ +class KWIN_EXPORT RenderBackend : public QObject +{ + Q_OBJECT + +public: + virtual CompositingType compositingType() const = 0; + + virtual bool checkGraphicsReset(); + + virtual QList compatibleOutputLayers(BackendOutput *output) = 0; + + virtual DrmDevice *drmDevice() const; + + virtual bool testImportBuffer(GraphicsBuffer *buffer); + virtual QHash> supportedFormats() const; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/renderjournal.cpp b/local/recipes/kde/kwin/source/src/core/renderjournal.cpp new file mode 100644 index 0000000000..39b075fcce --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/renderjournal.cpp @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "renderjournal.h" + +#include +#include + +using namespace std::chrono_literals; + +namespace KWin +{ + +RenderJournal::RenderJournal() +{ +} + +static std::chrono::nanoseconds mix(std::chrono::nanoseconds duration1, std::chrono::nanoseconds duration2, double ratio) +{ + return std::chrono::nanoseconds(int64_t(std::round(duration1.count() * ratio + duration2.count() * (1 - ratio)))); +} + +void RenderJournal::add(std::chrono::nanoseconds renderTime, std::chrono::nanoseconds presentationTimestamp) +{ + const auto timeDifference = m_lastAdd ? presentationTimestamp - *m_lastAdd : 10s; + m_lastAdd = presentationTimestamp; + + static constexpr std::chrono::nanoseconds varianceTimeConstant = 6s; + const double varianceRatio = std::clamp(timeDifference.count() / double(varianceTimeConstant.count()), 0.001, 0.1); + const auto renderTimeDiff = std::max(renderTime - m_result, 0ns); + m_variance = std::max(mix(renderTimeDiff, m_variance, varianceRatio), renderTimeDiff); + + static constexpr std::chrono::nanoseconds timeConstant = 500ms; + const double ratio = std::clamp(timeDifference.count() / double(timeConstant.count()), 0.01, 1.0); + m_result = mix(renderTime, m_result, ratio); +} + +std::chrono::nanoseconds RenderJournal::result() const +{ + return m_result + m_variance * 2; +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/renderjournal.h b/local/recipes/kde/kwin/source/src/core/renderjournal.h new file mode 100644 index 0000000000..764516ad42 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/renderjournal.h @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once +#include "kwin_export.h" + +#include +#include + +namespace KWin +{ + +/** + * The RenderJournal class measures how long it takes to render frames and estimates how + * long it will take to render the next frame. + */ +class KWIN_EXPORT RenderJournal +{ +public: + explicit RenderJournal(); + + void add(std::chrono::nanoseconds renderTime, std::chrono::nanoseconds presentationTimestamp); + + std::chrono::nanoseconds result() const; + +private: + std::chrono::nanoseconds m_result{0}; + std::chrono::nanoseconds m_variance{0}; + std::optional m_lastAdd; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/renderloop.cpp b/local/recipes/kde/kwin/source/src/core/renderloop.cpp new file mode 100644 index 0000000000..f7c5a9f7f8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/renderloop.cpp @@ -0,0 +1,322 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "renderloop.h" +#include "backendoutput.h" +#include "options.h" +#include "renderloop_p.h" +#include "scene/surfaceitem.h" +#include "utils/common.h" +#include "window.h" +#include "workspace.h" + +#include + +using namespace std::chrono_literals; + +namespace KWin +{ + +RenderLoopPrivate *RenderLoopPrivate::get(RenderLoop *loop) +{ + return loop->d.get(); +} + +static const bool s_printDebugInfo = qEnvironmentVariableIntValue("KWIN_LOG_PERFORMANCE_DATA") != 0; + +RenderLoopPrivate::RenderLoopPrivate(RenderLoop *q, BackendOutput *output) + : q(q) + , output(output) +{ +} + +void RenderLoopPrivate::scheduleNextRepaint() +{ + if (kwinApp()->isTerminating() || compositeTimer.isActive() || preparingNewFrame) { + return; + } + scheduleRepaint(nextPresentationTimestamp); +} + +void RenderLoopPrivate::scheduleRepaint(std::chrono::nanoseconds lastTargetTimestamp) +{ + pendingReschedule = false; + const std::chrono::nanoseconds vblankInterval(1'000'000'000'000ull / refreshRate); + const std::chrono::nanoseconds currentTime(std::chrono::steady_clock::now().time_since_epoch()); + + // Estimate when it's a good time to perform the next compositing cycle. + // the 1ms on top of the safety margin is required for timer and scheduler inaccuracies + std::chrono::nanoseconds expectedCompositingTime = std::min(renderJournal.result() + safetyMargin + 1ms, 2 * vblankInterval); + + if (presentationMode == PresentationMode::VSync) { + // normal presentation: pageflips only happen at vblank + const uint64_t pageflipsSince = std::max((currentTime - lastPresentationTimestamp) / vblankInterval, 0); + if (pageflipsSince > 100) { + // if it's been a while since the last frame, the GPU is likely in a low power state and render time will be increased + // -> take that into account and start compositing very early + expectedCompositingTime = std::max(vblankInterval - 1us, expectedCompositingTime); + } + const uint64_t pageflipsSinceLastToTarget = std::max(std::round((lastTargetTimestamp - lastPresentationTimestamp).count() / double(vblankInterval.count())), 0); + uint64_t pageflipsInAdvance = std::min(expectedCompositingTime / vblankInterval + 1, maxPendingFrameCount); + + // switching from double to triple buffering causes a frame drop + // -> apply some amount of hysteresis to avoid switching back and forth constantly + if (pageflipsInAdvance > 1) { + // immediately switch to triple buffering when needed + wasTripleBuffering = true; + doubleBufferingCounter = 0; + } else if (wasTripleBuffering) { + // but wait a bit before switching back to double buffering + if (doubleBufferingCounter >= 10) { + wasTripleBuffering = false; + } else if (expectedCompositingTime >= vblankInterval * 0.95) { + // also don't switch back if render times are just barely enough for double buffering + pageflipsInAdvance = 2; + doubleBufferingCounter = 0; + expectedCompositingTime = vblankInterval; + } else { + doubleBufferingCounter++; + pageflipsInAdvance = 2; + expectedCompositingTime = vblankInterval; + } + } + + if (compositeTimer.isActive()) { + // we already scheduled this frame, but we got a new timestamp + // which might require starting to composite earlier than we planned + // It's important here that we do not change the targeted vblank interval, + // otherwise with a pessimistic compositing time estimation we might + // unnecessarily drop frames + const uint32_t intervalsSinceLastTimestamp = std::max(std::round((nextPresentationTimestamp - lastPresentationTimestamp).count() / double(vblankInterval.count())), 0); + nextPresentationTimestamp = lastPresentationTimestamp + intervalsSinceLastTimestamp * vblankInterval; + } else { + nextPresentationTimestamp = lastPresentationTimestamp + std::max(pageflipsSince + pageflipsInAdvance, pageflipsSinceLastToTarget + 1) * vblankInterval; + } + } else { + wasTripleBuffering = false; + doubleBufferingCounter = 0; + if (presentationMode == PresentationMode::Async || presentationMode == PresentationMode::AdaptiveAsync) { + // tearing: pageflips happen ASAP + nextPresentationTimestamp = currentTime; + } else { + // adaptive sync: pageflips happen after one vblank interval + // TODO read minimum refresh rate from the EDID and take it into account here + nextPresentationTimestamp = std::max(currentTime, lastPresentationTimestamp + vblankInterval); + } + } + + const std::chrono::nanoseconds nextRenderTimestamp = nextPresentationTimestamp - expectedCompositingTime; + compositeTimer.start(std::max(0ms, std::chrono::duration_cast(nextRenderTimestamp - currentTime)), Qt::PreciseTimer, q); +} + +void RenderLoopPrivate::delayScheduleRepaint() +{ + pendingReschedule = true; +} + +void RenderLoopPrivate::notifyFrameDropped() +{ + Q_ASSERT(pendingFrameCount > 0); + pendingFrameCount--; + + if (!inhibitCount && pendingReschedule) { + scheduleNextRepaint(); + } +} + +void RenderLoopPrivate::notifyFrameCompleted(std::chrono::nanoseconds timestamp, std::optional renderTime, PresentationMode mode, OutputFrame *frame) +{ + if (output && s_printDebugInfo && !m_debugOutput) { + m_debugOutput = std::fstream(qPrintable("kwin perf statistics " + output->name() + ".csv"), std::ios::out); + *m_debugOutput << "target pageflip timestamp,pageflip timestamp,render start,render end,safety margin,refresh duration,vrr,tearing,predicted render time\n"; + } + if (m_debugOutput) { + auto times = renderTime.value_or(RenderTimeSpan{}); + const bool vrr = mode == PresentationMode::AdaptiveSync || mode == PresentationMode::AdaptiveAsync; + const bool tearing = mode == PresentationMode::Async || mode == PresentationMode::AdaptiveAsync; + *m_debugOutput << frame->targetPageflipTime().time_since_epoch().count() << "," << timestamp.count() << "," << times.start.time_since_epoch().count() << "," << times.end.time_since_epoch().count() + << "," << safetyMargin.count() << "," << frame->refreshDuration().count() << "," << (vrr ? 1 : 0) << "," << (tearing ? 1 : 0) << "," << frame->predictedRenderTime().count() << "\n"; + } + + Q_ASSERT(pendingFrameCount > 0); + pendingFrameCount--; + + notifyVblank(timestamp); + + if (renderTime) { + renderJournal.add(renderTime->end - renderTime->start, timestamp); + } + if (compositeTimer.isActive()) { + // reschedule to match the new timestamp and render time + scheduleRepaint(lastPresentationTimestamp); + } + if (!inhibitCount && pendingReschedule) { + scheduleNextRepaint(); + } + + Q_EMIT q->framePresented(q, timestamp, mode); +} + +void RenderLoopPrivate::notifyVblank(std::chrono::nanoseconds timestamp) +{ + if (lastPresentationTimestamp <= timestamp) { + lastPresentationTimestamp = timestamp; + } else { + qCDebug(KWIN_CORE, + "Got invalid presentation timestamp: %lld (current %lld)", + static_cast(timestamp.count()), + static_cast(lastPresentationTimestamp.count())); + lastPresentationTimestamp = std::chrono::steady_clock::now().time_since_epoch(); + } +} + +void RenderLoop::timerEvent(QTimerEvent *event) +{ + if (event->timerId() == d->compositeTimer.timerId()) { + d->compositeTimer.stop(); + d->dispatch(); + } else if (event->timerId() == d->delayedVrrTimer.timerId()) { + d->delayedVrrTimer.stop(); + scheduleRepaint(nullptr, nullptr); + } else { + QObject::timerEvent(event); + } +} + +void RenderLoopPrivate::dispatch() +{ + Q_EMIT q->frameRequested(q); +} + +RenderLoop::RenderLoop(BackendOutput *output) + : d(std::make_unique(this, output)) +{ +} + +RenderLoop::~RenderLoop() +{ +} + +void RenderLoop::inhibit() +{ + d->inhibitCount++; + + if (d->inhibitCount == 1) { + d->compositeTimer.stop(); + } +} + +void RenderLoop::uninhibit() +{ + Q_ASSERT(d->inhibitCount > 0); + d->inhibitCount--; + + if (d->inhibitCount == 0) { + d->scheduleNextRepaint(); + } +} + +void RenderLoop::prepareNewFrame() +{ + d->pendingFrameCount++; + d->preparingNewFrame = true; +} + +void RenderLoop::newFramePrepared() +{ + d->preparingNewFrame = false; +} + +int RenderLoop::refreshRate() const +{ + return d->refreshRate; +} + +void RenderLoop::setRefreshRate(int refreshRate) +{ + if (d->refreshRate == refreshRate) { + return; + } + d->refreshRate = refreshRate; + Q_EMIT refreshRateChanged(); + + if (d->compositeTimer.isActive()) { + d->scheduleRepaint(d->lastPresentationTimestamp); + } +} + +void RenderLoop::setPresentationSafetyMargin(std::chrono::nanoseconds safetyMargin) +{ + d->safetyMargin = safetyMargin; +} + +void RenderLoop::scheduleRepaint(Item *item, OutputLayer *outputLayer) +{ + const bool vrr = d->presentationMode == PresentationMode::AdaptiveSync || d->presentationMode == PresentationMode::AdaptiveAsync; + const bool tearing = d->presentationMode == PresentationMode::Async || d->presentationMode == PresentationMode::AdaptiveAsync; + if ((vrr || tearing) && workspace() && workspace()->activeWindow() && d->output) { + SurfaceItem *const surfaceItem = workspace()->activeWindow()->surfaceItem(); + if ((item || outputLayer) && activeWindowControlsVrrRefreshRate() && item != surfaceItem && !surfaceItem->isAncestorOf(item)) { + constexpr std::chrono::milliseconds s_delayVrrTimer = 1'000ms / 30; + d->delayedVrrTimer.start(s_delayVrrTimer, Qt::PreciseTimer, this); + return; + } + } + d->delayedVrrTimer.stop(); + const int effectiveMaxPendingFrameCount = (vrr || tearing) ? 1 : d->maxPendingFrameCount; + if (d->pendingFrameCount < effectiveMaxPendingFrameCount && !d->inhibitCount) { + d->scheduleNextRepaint(); + } else { + d->delayScheduleRepaint(); + } +} + +bool RenderLoop::activeWindowControlsVrrRefreshRate() const +{ + Window *const activeWindow = workspace()->activeWindow(); + LogicalOutput *logical = workspace()->findOutput(d->output); + if (!logical) { + return false; + } + return activeWindow + && activeWindow->frameGeometry().intersects(logical->geometryF()) + && activeWindow->surfaceItem() + && activeWindow->surfaceItem()->recursiveFrameTimeEstimation().transform([](const auto t) { + return t <= std::chrono::nanoseconds(1'000'000'000) / 30; + }).value_or(false); +} + +std::chrono::nanoseconds RenderLoop::lastPresentationTimestamp() const +{ + return d->lastPresentationTimestamp; +} + +std::chrono::nanoseconds RenderLoop::nextPresentationTimestamp() const +{ + return d->nextPresentationTimestamp; +} + +void RenderLoop::setPresentationMode(PresentationMode mode) +{ + if (mode != d->presentationMode) { + qCDebug(KWIN_CORE) << "Changed presentation mode to" << mode; + } + d->presentationMode = mode; +} + +void RenderLoop::setMaxPendingFrameCount(uint32_t maxCount) +{ + d->maxPendingFrameCount = maxCount; +} + +std::chrono::nanoseconds RenderLoop::predictedRenderTime() const +{ + return d->renderJournal.result(); +} + +} // namespace KWin + +#include "moc_renderloop.cpp" diff --git a/local/recipes/kde/kwin/source/src/core/renderloop.h b/local/recipes/kde/kwin/source/src/core/renderloop.h new file mode 100644 index 0000000000..ad9b33971d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/renderloop.h @@ -0,0 +1,132 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/globals.h" + +#include + +namespace KWin +{ + +class RenderLoopPrivate; +class SurfaceItem; +class Item; +class BackendOutput; +class OutputLayer; + +/** + * The RenderLoop class represents the compositing scheduler on a particular output. + * + * The RenderLoop class drives the compositing. The frameRequested() signal is emitted + * when the loop wants a new frame to be rendered. The frameCompleted() signal is + * emitted when a previously rendered frame has been presented on the screen. In case + * you want the compositor to repaint the scene, call the scheduleRepaint() function. + */ +class KWIN_EXPORT RenderLoop : public QObject +{ + Q_OBJECT + +public: + explicit RenderLoop(BackendOutput *output); + ~RenderLoop() override; + + /** + * Pauses the render loop. While the render loop is inhibited, scheduleRepaint() + * requests are queued. + * + * Once the render loop is uninhibited, the pending schedule requests are going to + * be re-applied. + */ + void inhibit(); + + /** + * Uninhibits the render loop. + */ + void uninhibit(); + + /** + * This function must be called before the Compositor prepares a new frame. + * Note that this inhibits scheduleRepaint requests, without re-applying the + * missed requests afterwards + */ + void prepareNewFrame(); + + /** + * This function must be called after the Compositor, and uninhibits the renderloop + */ + void newFramePrepared(); + + /** + * Returns the refresh rate at which the output is being updated, in millihertz. + */ + int refreshRate() const; + + /** + * Sets the refresh rate of this RenderLoop to @a refreshRate, in millihertz. + */ + void setRefreshRate(int refreshRate); + + void setPresentationSafetyMargin(std::chrono::nanoseconds safetyMargin); + + /** + * Schedules a compositing cycle at the next available moment. + */ + void scheduleRepaint(Item *item = nullptr, OutputLayer *outputLayer = nullptr); + + /** + * Returns the timestamp of the last frame that has been presented on the screen. + * The returned timestamp is sourced from the monotonic clock. + */ + std::chrono::nanoseconds lastPresentationTimestamp() const; + + /** + * If a repaint has been scheduled, this function returns the expected time when + * the next frame will be presented on the screen. The returned timestamp is sourced + * from the monotonic clock. + */ + std::chrono::nanoseconds nextPresentationTimestamp() const; + + void setPresentationMode(PresentationMode mode); + + void setMaxPendingFrameCount(uint32_t maxCount); + + /** + * Returns the expected time how long it is going to take to render the next frame. + */ + std::chrono::nanoseconds predictedRenderTime() const; + + // TODO integrate cursor updates into the render loop / frame scheduling somehow? + // and then remove this again + bool activeWindowControlsVrrRefreshRate() const; + + void timerEvent(QTimerEvent *event) override; + +Q_SIGNALS: + /** + * This signal is emitted when the refresh rate of this RenderLoop has changed. + */ + void refreshRateChanged(); + /** + * This signal is emitted when a frame has been actually presented on the screen. + * @a timestamp indicates the time when it took place. + */ + void framePresented(RenderLoop *loop, std::chrono::nanoseconds timestamp, PresentationMode mode); + + /** + * This signal is emitted when the render loop wants a new frame to be composited. + * + * The Compositor should make a connection to this signal using Qt::DirectConnection. + */ + void frameRequested(RenderLoop *loop); + +private: + std::unique_ptr d; + friend class RenderLoopPrivate; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/renderloop_p.h b/local/recipes/kde/kwin/source/src/core/renderloop_p.h new file mode 100644 index 0000000000..4402bb4bd5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/renderloop_p.h @@ -0,0 +1,62 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "renderbackend.h" +#include "renderjournal.h" +#include "renderloop.h" + +#include + +#include +#include + +namespace KWin +{ + +class SurfaceItem; +class OutputFrame; + +class KWIN_EXPORT RenderLoopPrivate +{ +public: + static RenderLoopPrivate *get(RenderLoop *loop); + explicit RenderLoopPrivate(RenderLoop *q, BackendOutput *output); + + void dispatch(); + + void delayScheduleRepaint(); + void scheduleNextRepaint(); + void scheduleRepaint(std::chrono::nanoseconds lastTargetTimestamp); + + void notifyFrameDropped(); + void notifyFrameCompleted(std::chrono::nanoseconds timestamp, std::optional renderTime, PresentationMode mode, OutputFrame *frame); + void notifyVblank(std::chrono::nanoseconds timestamp); + + RenderLoop *const q; + BackendOutput *const output; + std::optional m_debugOutput; + std::chrono::nanoseconds lastPresentationTimestamp = std::chrono::nanoseconds::zero(); + std::chrono::nanoseconds nextPresentationTimestamp = std::chrono::nanoseconds::zero(); + bool wasTripleBuffering = false; + int doubleBufferingCounter = 0; + QBasicTimer compositeTimer; + RenderJournal renderJournal; + int refreshRate = 60000; + int pendingFrameCount = 0; + bool preparingNewFrame = false; + int inhibitCount = 0; + bool pendingReschedule = false; + std::chrono::nanoseconds safetyMargin{0}; + + PresentationMode presentationMode = PresentationMode::VSync; + int maxPendingFrameCount = 1; + + QBasicTimer delayedVrrTimer; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/rendertarget.cpp b/local/recipes/kde/kwin/source/src/core/rendertarget.cpp new file mode 100644 index 0000000000..3897f72119 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/rendertarget.cpp @@ -0,0 +1,72 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "core/rendertarget.h" +#include "opengl/glutils.h" + +namespace KWin +{ + +RenderTarget::RenderTarget(GLFramebuffer *fbo, const std::shared_ptr &colorDescription) + : m_framebuffer(fbo) + , m_transform(fbo->colorAttachment() ? fbo->colorAttachment()->contentTransform() : OutputTransform()) + , m_colorDescription(colorDescription) +{ +} + +RenderTarget::RenderTarget(QImage *image, const std::shared_ptr &colorDescription) + : m_image(image) + , m_colorDescription(colorDescription) +{ +} + +QSize RenderTarget::transformedSize() const +{ + return m_transform.map(size()); +} + +Rect RenderTarget::transformedRect() const +{ + return Rect(QPoint(0, 0), transformedSize()); +} + +QSize RenderTarget::size() const +{ + if (m_framebuffer) { + return m_framebuffer->size(); + } else if (m_image) { + return m_image->size(); + } else { + Q_UNREACHABLE(); + } +} + +OutputTransform RenderTarget::transform() const +{ + return m_transform; +} + +GLFramebuffer *RenderTarget::framebuffer() const +{ + return m_framebuffer; +} + +GLTexture *RenderTarget::texture() const +{ + return m_framebuffer->colorAttachment(); +} + +QImage *RenderTarget::image() const +{ + return m_image; +} + +const std::shared_ptr &RenderTarget::colorDescription() const +{ + return m_colorDescription; +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/rendertarget.h b/local/recipes/kde/kwin/source/src/core/rendertarget.h new file mode 100644 index 0000000000..1cfc701f7f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/rendertarget.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/colorspace.h" +#include "core/output.h" + +#include + +namespace KWin +{ + +class GLFramebuffer; +class GLTexture; + +class KWIN_EXPORT RenderTarget +{ +public: + explicit RenderTarget(GLFramebuffer *fbo, const std::shared_ptr &colorDescription = ColorDescription::sRGB); + explicit RenderTarget(QImage *image, const std::shared_ptr &colorDescription = ColorDescription::sRGB); + + QSize transformedSize() const; + Rect transformedRect() const; + + QSize size() const; + OutputTransform transform() const; + const std::shared_ptr &colorDescription() const; + + QImage *image() const; + GLFramebuffer *framebuffer() const; + GLTexture *texture() const; + +private: + QImage *m_image = nullptr; + GLFramebuffer *m_framebuffer = nullptr; + const OutputTransform m_transform; + const std::shared_ptr m_colorDescription; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/renderviewport.cpp b/local/recipes/kde/kwin/source/src/core/renderviewport.cpp new file mode 100644 index 0000000000..496bb1a906 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/renderviewport.cpp @@ -0,0 +1,211 @@ +/* + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "core/renderviewport.h" +#include "core/pixelgrid.h" +#include "core/rendertarget.h" + +namespace KWin +{ + +static QMatrix4x4 createProjectionMatrix(const RenderTarget &renderTarget, const QRect &rect, const QPoint &renderOffset) +{ + QMatrix4x4 ret; + + ret.scale(1, -1); // flip the y axis back + ret *= renderTarget.transform().toMatrix(); + // TODO change the "offset" to a device-local viewport rect? + ret.scale((renderTarget.transformedSize().width() - 2 * renderOffset.x()) / double(renderTarget.transformedSize().width()), + (renderTarget.transformedSize().height() - 2 * renderOffset.y()) / double(renderTarget.transformedSize().height())); + ret.scale(1, -1); // undo ortho() flipping the y axis + + ret.ortho(rect); + return ret; +} + +RenderViewport::RenderViewport(const RectF &renderRect, double scale, const RenderTarget &renderTarget, const QPoint &renderOffset) + : m_transform(renderTarget.transform()) + , m_transformBounds(m_transform.map(renderTarget.size())) + , m_renderRect(renderRect) + , m_scaledRenderRect(renderRect.scaled(scale).rounded()) + , m_renderOffset(renderOffset) + , m_projectionMatrix(createProjectionMatrix(renderTarget, m_scaledRenderRect, renderOffset)) + , m_scale(scale) +{ +} + +QMatrix4x4 RenderViewport::projectionMatrix() const +{ + return m_projectionMatrix; +} + +RectF RenderViewport::renderRect() const +{ + return m_renderRect; +} + +Rect RenderViewport::scaledRenderRect() const +{ + return m_scaledRenderRect; +} + +double RenderViewport::scale() const +{ + return m_scale; +} + +OutputTransform RenderViewport::transform() const +{ + return m_transform; +} + +QPoint RenderViewport::renderOffset() const +{ + return m_renderOffset; +} + +Rect RenderViewport::deviceRect() const +{ + return Rect(m_renderOffset, deviceSize()); +} + +QSize RenderViewport::deviceSize() const +{ + return m_scaledRenderRect.size(); +} + +RectF RenderViewport::mapToDeviceCoordinates(const RectF &logicalGeometry) const +{ + return logicalGeometry.translated(-m_renderRect.topLeft()).scaled(m_scale).translated(m_renderOffset); +} + +Rect RenderViewport::mapToDeviceCoordinatesAligned(const RectF &logicalGeometry) const +{ + return mapToDeviceCoordinates(logicalGeometry).roundedOut(); +} + +Rect RenderViewport::mapToDeviceCoordinatesAligned(const Rect &logicalGeometry) const +{ + return RectF(logicalGeometry).translated(-m_renderRect.topLeft()).scaled(m_scale).roundedOut().translated(m_renderOffset); +} + +Region RenderViewport::mapToDeviceCoordinatesAligned(const Region &logicalGeometry) const +{ + Region ret; + for (const Rect &logicalRect : logicalGeometry.rects()) { + ret |= mapToDeviceCoordinatesAligned(logicalRect); + } + return ret; +} + +RectF RenderViewport::mapFromDeviceCoordinates(const RectF &deviceGeometry) const +{ + return deviceGeometry.translated(-m_renderOffset).scaled(1.0 / m_scale).translated(m_renderRect.topLeft()); +} + +Rect RenderViewport::mapFromDeviceCoordinatesAligned(const Rect &deviceGeometry) const +{ + return deviceGeometry.translated(-m_renderOffset).scaled(1.0 / m_scale).translated(m_renderRect.topLeft()).toAlignedRect(); +} + +Rect RenderViewport::mapFromDeviceCoordinatesContained(const Rect &deviceGeometry) const +{ + return deviceGeometry + .translated(-m_renderOffset) + .scaled(1.0 / m_scale) + .translated(m_renderRect.topLeft()) + .roundedIn(); +} + +Region RenderViewport::mapFromDeviceCoordinatesAligned(const Region &deviceGeometry) const +{ + Region ret; + for (const Rect &deviceRect : deviceGeometry.rects()) { + ret |= mapFromDeviceCoordinatesAligned(deviceRect); + } + return ret; +} + +Region RenderViewport::mapFromDeviceCoordinatesContained(const Region &deviceGeometry) const +{ + Region ret; + for (const Rect &deviceRect : deviceGeometry.rects()) { + ret |= mapFromDeviceCoordinatesContained(deviceRect); + } + return ret; +} + +RectF RenderViewport::mapToRenderTarget(const RectF &logicalGeometry) const +{ + const RectF deviceGeometry = logicalGeometry + .scaled(m_scale) + .translated(-m_scaledRenderRect.topLeft() + m_renderOffset); + return m_transform.map(deviceGeometry, m_transformBounds); +} + +Rect RenderViewport::mapToRenderTarget(const Rect &logicalGeometry) const +{ + const Rect deviceGeometry = logicalGeometry + .scaled(m_scale) + .rounded() + .translated(-m_scaledRenderRect.topLeft() + m_renderOffset); + return m_transform.map(deviceGeometry, m_transformBounds); +} + +QPoint RenderViewport::mapToRenderTarget(const QPoint &logicalGeometry) const +{ + const QPoint devicePoint = snapToPixelGrid(QPointF(logicalGeometry) * m_scale) - m_scaledRenderRect.topLeft() + m_renderOffset; + return m_transform.map(devicePoint, m_transformBounds); +} + +QPointF RenderViewport::mapToRenderTarget(const QPointF &logicalGeometry) const +{ + const QPointF devicePoint = logicalGeometry * m_scale - m_scaledRenderRect.topLeft() + m_renderOffset; + return m_transform.map(devicePoint, m_transformBounds); +} + +Region RenderViewport::mapToRenderTarget(const Region &logicalGeometry) const +{ + Region ret; + for (const auto &rect : logicalGeometry.rects()) { + ret += mapToRenderTarget(rect); + } + return ret; +} + +RectF RenderViewport::mapToRenderTargetTexture(const RectF &logicalGeometry) const +{ + return logicalGeometry + .scaled(m_scale) + .translated(-m_scaledRenderRect.topLeft() + m_renderOffset); +} + +Rect RenderViewport::mapToRenderTargetTexture(const Rect &logicalGeometry) const +{ + return logicalGeometry + .scaled(m_scale) + .rounded() + .translated(-m_scaledRenderRect.topLeft() + m_renderOffset); +} + +QPoint RenderViewport::mapToRenderTargetTexture(const QPoint &logicalGeometry) const +{ + return snapToPixelGrid(QPointF(logicalGeometry) * m_scale) - m_scaledRenderRect.topLeft() + m_renderOffset; +} + +QPointF RenderViewport::mapToRenderTargetTexture(const QPointF &logicalGeometry) const +{ + return logicalGeometry * m_scale - m_scaledRenderRect.topLeft() + m_renderOffset; +} + +Region RenderViewport::mapToRenderTargetTexture(const Region &logicalGeometry) const +{ + Region ret; + for (const auto &rect : logicalGeometry.rects()) { + ret += mapToRenderTargetTexture(rect); + } + return ret; +} +} diff --git a/local/recipes/kde/kwin/source/src/core/renderviewport.h b/local/recipes/kde/kwin/source/src/core/renderviewport.h new file mode 100644 index 0000000000..ff9a001dab --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/renderviewport.h @@ -0,0 +1,70 @@ +/* + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/output.h" +#include "core/region.h" + +#include + +namespace KWin +{ + +class RenderTarget; + +class KWIN_EXPORT RenderViewport +{ +public: + explicit RenderViewport(const RectF &renderRect, double scale, const RenderTarget &renderTarget, const QPoint &renderOffset); + + QMatrix4x4 projectionMatrix() const; + RectF renderRect() const; + Rect scaledRenderRect() const; + double scale() const; + OutputTransform transform() const; + QPoint renderOffset() const; + + /** + * @returns Rect(renderOffset(), deviceSize()) + */ + Rect deviceRect() const; + QSize deviceSize() const; + + RectF mapToDeviceCoordinates(const RectF &logicalGeometry) const; + Rect mapToDeviceCoordinatesAligned(const RectF &logicalGeometry) const; + Rect mapToDeviceCoordinatesAligned(const Rect &logicalGeometry) const; + Region mapToDeviceCoordinatesAligned(const Region &logicalGeometry) const; + + RectF mapFromDeviceCoordinates(const RectF &deviceGeometry) const; + Rect mapFromDeviceCoordinatesAligned(const Rect &deviceGeometry) const; + Rect mapFromDeviceCoordinatesContained(const Rect &deviceGeometry) const; + Region mapFromDeviceCoordinatesAligned(const Region &deviceGeometry) const; + Region mapFromDeviceCoordinatesContained(const Region &deviceGeometry) const; + + RectF mapToRenderTarget(const RectF &logicalGeometry) const; + Rect mapToRenderTarget(const Rect &logicalGeometry) const; + QPoint mapToRenderTarget(const QPoint &logicalGeometry) const; + QPointF mapToRenderTarget(const QPointF &logicalGeometry) const; + Region mapToRenderTarget(const Region &logicalGeometry) const; + + RectF mapToRenderTargetTexture(const RectF &logicalGeometry) const; + Rect mapToRenderTargetTexture(const Rect &logicalGeometry) const; + QPoint mapToRenderTargetTexture(const QPoint &logicalGeometry) const; + QPointF mapToRenderTargetTexture(const QPointF &logicalGeometry) const; + Region mapToRenderTargetTexture(const Region &logicalGeometry) const; + +private: + const OutputTransform m_transform; + const QSize m_transformBounds; + const RectF m_renderRect; + const Rect m_scaledRenderRect; + const QPoint m_renderOffset; + const QMatrix4x4 m_projectionMatrix; + const double m_scale; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/session.cpp b/local/recipes/kde/kwin/source/src/core/session.cpp new file mode 100644 index 0000000000..8038b5e24f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/session.cpp @@ -0,0 +1,57 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "session.h" +#include "session_consolekit.h" +#include "session_logind.h" +#include "session_noop.h" + +namespace KWin +{ + +static const struct +{ + Session::Type type; + std::function()> createFunc; +} s_availableSessions[] = { + {Session::Type::Logind, &LogindSession::create}, + {Session::Type::ConsoleKit, &ConsoleKitSession::create}, + {Session::Type::Noop, &NoopSession::create}, +}; + +std::unique_ptr Session::create() +{ + for (const auto &sessionInfo : s_availableSessions) { + std::unique_ptr session = sessionInfo.createFunc(); + if (session) { + return session; + } + } + return nullptr; +} + +std::unique_ptr Session::create(Type type) +{ + for (const auto &sessionInfo : s_availableSessions) { + if (sessionInfo.type == type) { + return sessionInfo.createFunc(); + } + } + return nullptr; +} + +Session::Error Session::errorFromErrno() +{ + if (errno == EBUSY) { + return Error::EBusy; + } else { + return Error::Other; + } +} + +} // namespace KWin + +#include "moc_session.cpp" diff --git a/local/recipes/kde/kwin/source/src/core/session.h b/local/recipes/kde/kwin/source/src/core/session.h new file mode 100644 index 0000000000..76722fecdd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/session.h @@ -0,0 +1,133 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/globals.h" +#include "utils/filedescriptor.h" + +#include +#include + +#include +#include +#include + +namespace KWin +{ + +/** + * The Session class represents the session controlled by the compositor. + * + * The Session class provides information about the virtual terminal where the compositor + * is running and a way to open files that require special privileges, e.g. DRM devices or + * input devices. + */ +class KWIN_EXPORT Session : public QObject +{ + Q_OBJECT + +public: + /** + * This enum type is used to specify the type of the session. + */ + enum class Type { + Noop, + ConsoleKit, + Logind, + }; + + /** + * This enum type is used to specify optional capabilities of the session. + */ + enum class Capability : uint { + SwitchTerminal = 0x1, + }; + Q_DECLARE_FLAGS(Capabilities, Capability) + + static std::unique_ptr create(); + static std::unique_ptr create(Type type); + + /** + * Returns @c true if the session is active; otherwise returns @c false. + */ + virtual bool isActive() const = 0; + + /** + * Returns the capabilities supported by the session. + */ + virtual Capabilities capabilities() const = 0; + + /** + * Returns the seat name for the Session. + */ + virtual QString seat() const = 0; + + /** + * Returns the terminal controlled by the Session. + */ + virtual uint terminal() const = 0; + + enum class Error { + EBusy, + Other, + }; + + /** + * Opens the file with the specified @a fileName + */ + virtual std::expected openRestricted(const QString &fileName) = 0; + + /** + * Closes a file that has been opened using the openRestricted() function. + */ + virtual void closeRestricted(int fileDescriptor) = 0; + + /** + * Switches to the specified virtual @a terminal. This function does nothing if the + * Capability::SwitchTerminal capability is unsupported. + */ + virtual void switchTo(uint terminal) = 0; + + virtual FileDescriptor delaySleep(const QString &reason) = 0; + +Q_SIGNALS: + /** + * This signal is emitted when the session is resuming from suspend. + */ + void awoke(); + /** + * This signal is emitted before the session goes to suspend + */ + void aboutToSleep(); + /** + * This signal is emitted when the active state of the session has changed. + */ + void activeChanged(bool active); + + /** + * This signal is emitted when the specified device can be used again. + */ + void deviceResumed(dev_t deviceId); + + /** + * This signal is emitted when the given device cannot be used by the compositor + * anymore. For example, this normally occurs when switching between VTs. + * + * Note that when this signal is emitted for a DRM device, master permissions can + * be already revoked. + */ + void devicePaused(dev_t deviceId); + +protected: + explicit Session() = default; + + static Error errorFromErrno(); +}; + +} // namespace KWin + +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::Session::Capabilities) diff --git a/local/recipes/kde/kwin/source/src/core/session_consolekit.cpp b/local/recipes/kde/kwin/source/src/core/session_consolekit.cpp new file mode 100644 index 0000000000..e75ed46ac5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/session_consolekit.cpp @@ -0,0 +1,363 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "session_consolekit.h" +#include "utils/common.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#if __has_include() +#include +#endif + +// Note that ConsoleKit's session api is not fully compatible with logind's session api. + +struct DBusConsoleKitSeat +{ + QString id; + QDBusObjectPath path; +}; + +QDBusArgument &operator<<(QDBusArgument &argument, const DBusConsoleKitSeat &seat) +{ + argument.beginStructure(); + argument << seat.id << seat.path; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, DBusConsoleKitSeat &seat) +{ + argument.beginStructure(); + argument >> seat.id >> seat.path; + argument.endStructure(); + return argument; +} + +Q_DECLARE_METATYPE(DBusConsoleKitSeat) + +namespace KWin +{ + +static const QString s_serviceName = QStringLiteral("org.freedesktop.ConsoleKit"); +static const QString s_propertiesInterface = QStringLiteral("org.freedesktop.DBus.Properties"); +static const QString s_sessionInterface = QStringLiteral("org.freedesktop.ConsoleKit.Session"); +static const QString s_seatInterface = QStringLiteral("org.freedesktop.ConsoleKit.Seat"); +static const QString s_managerInterface = QStringLiteral("org.freedesktop.ConsoleKit.Manager"); +static const QString s_managerPath = QStringLiteral("/org/freedesktop/ConsoleKit/Manager"); + +static QString findProcessSessionPath() +{ + QDBusMessage message = QDBusMessage::createMethodCall(s_serviceName, s_managerPath, + s_managerInterface, + QStringLiteral("GetSessionByPID")); + message.setArguments({uint32_t(QCoreApplication::applicationPid())}); + + const QDBusMessage reply = QDBusConnection::systemBus().call(message); + if (reply.type() == QDBusMessage::ErrorMessage) { + return QString(); + } + + return reply.arguments().constFirst().value().path(); +} + +static bool takeControl(const QString &sessionPath) +{ + QDBusMessage message = QDBusMessage::createMethodCall(s_serviceName, sessionPath, + s_sessionInterface, + QStringLiteral("TakeControl")); + message.setArguments({false}); + + const QDBusMessage reply = QDBusConnection::systemBus().call(message); + + return reply.type() != QDBusMessage::ErrorMessage; +} + +static void releaseControl(const QString &sessionPath) +{ + const QDBusMessage message = QDBusMessage::createMethodCall(s_serviceName, sessionPath, + s_sessionInterface, + QStringLiteral("ReleaseControl")); + + QDBusConnection::systemBus().asyncCall(message); +} + +std::unique_ptr ConsoleKitSession::create() +{ + if (!QDBusConnection::systemBus().interface()->isServiceRegistered(s_serviceName)) { + return nullptr; + } + + const QString sessionPath = findProcessSessionPath(); + if (sessionPath.isEmpty()) { + qCWarning(KWIN_CORE) << "Could not determine the active graphical session"; + return nullptr; + } + + if (!takeControl(sessionPath)) { + qCWarning(KWIN_CORE, "Failed to take control of %s session. Maybe another compositor is running?", + qPrintable(sessionPath)); + return nullptr; + } + + std::unique_ptr session{new ConsoleKitSession(sessionPath)}; + if (session->initialize()) { + return session; + } else { + return nullptr; + } +} + +bool ConsoleKitSession::isActive() const +{ + return m_isActive; +} + +ConsoleKitSession::Capabilities ConsoleKitSession::capabilities() const +{ + return Capability::SwitchTerminal; +} + +QString ConsoleKitSession::seat() const +{ + return m_seatId; +} + +uint ConsoleKitSession::terminal() const +{ + return m_terminal; +} + +std::expected ConsoleKitSession::openRestricted(const QString &fileName) +{ + struct stat st; + if (stat(fileName.toUtf8(), &st) < 0) { + return std::unexpected(errorFromErrno()); + } + + QDBusMessage message = QDBusMessage::createMethodCall(s_serviceName, m_sessionPath, + s_sessionInterface, + QStringLiteral("TakeDevice")); + // major() and minor() macros return ints on FreeBSD instead of uints. + message.setArguments({uint(major(st.st_rdev)), uint(minor(st.st_rdev))}); + + const QDBusMessage reply = QDBusConnection::systemBus().call(message); + if (reply.type() == QDBusMessage::ErrorMessage) { + qCDebug(KWIN_CORE, "Failed to open %s device (%s)", + qPrintable(fileName), qPrintable(reply.errorMessage())); + if (reply.errorName() == "System.Error.EBUSY") { + return std::unexpected(Error::EBusy); + } else { + return std::unexpected(Error::Other); + } + } + + const QDBusUnixFileDescriptor descriptor = reply.arguments().constFirst().value(); + if (!descriptor.isValid()) { + return std::unexpected(Error::Other); + } + + const int ret = fcntl(descriptor.fileDescriptor(), F_DUPFD_CLOEXEC, 0); + if (ret == -1) { + return std::unexpected(errorFromErrno()); + } else { + return ret; + } +} + +void ConsoleKitSession::closeRestricted(int fileDescriptor) +{ + struct stat st; + if (fstat(fileDescriptor, &st) < 0) { + close(fileDescriptor); + return; + } + + QDBusMessage message = QDBusMessage::createMethodCall(s_serviceName, m_sessionPath, + s_sessionInterface, + QStringLiteral("ReleaseDevice")); + // major() and minor() macros return ints on FreeBSD instead of uints. + message.setArguments({uint(major(st.st_rdev)), uint(minor(st.st_rdev))}); + + QDBusConnection::systemBus().asyncCall(message); + + close(fileDescriptor); +} + +void ConsoleKitSession::switchTo(uint terminal) +{ + QDBusMessage message = QDBusMessage::createMethodCall(s_serviceName, m_seatPath, + s_seatInterface, + QStringLiteral("SwitchTo")); + message.setArguments({terminal}); + + QDBusConnection::systemBus().asyncCall(message); +} + +FileDescriptor ConsoleKitSession::delaySleep(const QString &reason) +{ + QDBusMessage message = QDBusMessage::createMethodCall(s_serviceName, s_managerPath, + s_managerInterface, + QStringLiteral("Inhibit")); + message.setArguments({QStringLiteral("sleep"), QStringLiteral("compositor"), reason, QStringLiteral("delay")}); + + const QDBusMessage reply = QDBusConnection::systemBus().call(message); + if (reply.type() == QDBusMessage::ErrorMessage) { + qCWarning(KWIN_CORE, "Failed to delay sleep: %s", qPrintable(reply.errorMessage())); + return FileDescriptor{}; + } + const QDBusUnixFileDescriptor descriptor = reply.arguments().constFirst().value(); + return FileDescriptor{fcntl(descriptor.fileDescriptor(), F_DUPFD_CLOEXEC, 0)}; +} + +ConsoleKitSession::ConsoleKitSession(const QString &sessionPath) + : m_sessionPath(sessionPath) +{ + qDBusRegisterMetaType(); +} + +ConsoleKitSession::~ConsoleKitSession() +{ + releaseControl(m_sessionPath); +} + +bool ConsoleKitSession::initialize() +{ + QDBusMessage activeMessage = QDBusMessage::createMethodCall(s_serviceName, m_sessionPath, + s_propertiesInterface, + QStringLiteral("Get")); + activeMessage.setArguments({s_sessionInterface, QStringLiteral("active")}); + + QDBusMessage seatMessage = QDBusMessage::createMethodCall(s_serviceName, m_sessionPath, + s_propertiesInterface, + QStringLiteral("Get")); + seatMessage.setArguments({s_sessionInterface, QStringLiteral("Seat")}); + + QDBusMessage terminalMessage = QDBusMessage::createMethodCall(s_serviceName, m_sessionPath, + s_propertiesInterface, + QStringLiteral("Get")); + terminalMessage.setArguments({s_sessionInterface, QStringLiteral("VTNr")}); + + QDBusPendingReply activeReply = + QDBusConnection::systemBus().asyncCall(activeMessage); + QDBusPendingReply terminalReply = + QDBusConnection::systemBus().asyncCall(terminalMessage); + QDBusPendingReply seatReply = + QDBusConnection::systemBus().asyncCall(seatMessage); + + // We must wait until all replies have been received because the drm backend needs a + // valid seat name to properly select gpu devices, this also simplifies startup code. + activeReply.waitForFinished(); + terminalReply.waitForFinished(); + seatReply.waitForFinished(); + + if (activeReply.isError()) { + qCWarning(KWIN_CORE) << "Failed to query active session property:" << activeReply.error(); + return false; + } + if (terminalReply.isError()) { + qCWarning(KWIN_CORE) << "Failed to query VTNr session property:" << terminalReply.error(); + return false; + } + if (seatReply.isError()) { + qCWarning(KWIN_CORE) << "Failed to query Seat session property:" << seatReply.error(); + return false; + } + + m_isActive = activeReply.value().toBool(); + m_terminal = terminalReply.value().toUInt(); + + const DBusConsoleKitSeat seat = qdbus_cast(seatReply.value().value()); + m_seatId = seat.id; + m_seatPath = seat.path.path(); + + QDBusConnection::systemBus().connect(s_serviceName, s_managerPath, s_managerInterface, + QStringLiteral("PrepareForSleep"), + this, + SLOT(handlePrepareForSleep(bool))); + + QDBusConnection::systemBus().connect(s_serviceName, m_sessionPath, s_sessionInterface, + QStringLiteral("PauseDevice"), + this, + SLOT(handlePauseDevice(uint, uint, QString))); + + QDBusConnection::systemBus().connect(s_serviceName, m_sessionPath, s_sessionInterface, + QStringLiteral("ResumeDevice"), + this, + SLOT(handleResumeDevice(uint, uint, QDBusUnixFileDescriptor))); + + QDBusConnection::systemBus().connect(s_serviceName, m_sessionPath, s_propertiesInterface, + QStringLiteral("PropertiesChanged"), + this, + SLOT(handlePropertiesChanged(QString, QVariantMap))); + + return true; +} + +void ConsoleKitSession::updateActive(bool active) +{ + if (m_isActive != active) { + m_isActive = active; + Q_EMIT activeChanged(active); + } +} + +void ConsoleKitSession::handlePauseDevice(uint major, uint minor, const QString &type) +{ + Q_EMIT devicePaused(makedev(major, minor)); + + if (type == QLatin1String("pause")) { + QDBusMessage message = QDBusMessage::createMethodCall(s_serviceName, m_sessionPath, + s_sessionInterface, + QStringLiteral("PauseDeviceComplete")); + message.setArguments({major, minor}); + + QDBusConnection::systemBus().asyncCall(message); + } +} + +void ConsoleKitSession::handleResumeDevice(uint major, uint minor, QDBusUnixFileDescriptor fileDescriptor) +{ + // We don't care about the file descriptor as the libinput backend will re-open input devices + // and the drm file descriptors remain valid after pausing gpus. + + Q_EMIT deviceResumed(makedev(major, minor)); +} + +void ConsoleKitSession::handlePropertiesChanged(const QString &interfaceName, const QVariantMap &properties) +{ + if (interfaceName == s_sessionInterface) { + const QVariant active = properties.value(QStringLiteral("active")); + if (active.isValid()) { + updateActive(active.toBool()); + } + } +} + +void ConsoleKitSession::handlePrepareForSleep(bool sleep) +{ + if (sleep) { + Q_EMIT aboutToSleep(); + } else { + Q_EMIT awoke(); + } +} + +} // namespace KWin + +#include "moc_session_consolekit.cpp" diff --git a/local/recipes/kde/kwin/source/src/core/session_consolekit.h b/local/recipes/kde/kwin/source/src/core/session_consolekit.h new file mode 100644 index 0000000000..44b8f69c64 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/session_consolekit.h @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "session.h" + +#include + +namespace KWin +{ + +class ConsoleKitSession : public Session +{ + Q_OBJECT + +public: + static std::unique_ptr create(); + ~ConsoleKitSession() override; + + bool isActive() const override; + Capabilities capabilities() const override; + QString seat() const override; + uint terminal() const override; + std::expected openRestricted(const QString &fileName) override; + void closeRestricted(int fileDescriptor) override; + void switchTo(uint terminal) override; + FileDescriptor delaySleep(const QString &reason) override; + +private Q_SLOTS: + void handleResumeDevice(uint major, uint minor, QDBusUnixFileDescriptor fileDescriptor); + void handlePauseDevice(uint major, uint minor, const QString &type); + void handlePropertiesChanged(const QString &interfaceName, const QVariantMap &properties); + void handlePrepareForSleep(bool sleep); + +private: + explicit ConsoleKitSession(const QString &sessionPath); + + bool initialize(); + void updateActive(bool active); + + QString m_sessionPath; + QString m_seatId; + QString m_seatPath; + uint m_terminal = 0; + bool m_isActive = false; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/session_logind.cpp b/local/recipes/kde/kwin/source/src/core/session_logind.cpp new file mode 100644 index 0000000000..dfadf0ec48 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/session_logind.cpp @@ -0,0 +1,361 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "session_logind.h" +#include "utils/common.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#if __has_include() +#include +#endif + +struct DBusLogindSeat +{ + QString id; + QDBusObjectPath path; +}; + +QDBusArgument &operator<<(QDBusArgument &argument, const DBusLogindSeat &seat) +{ + argument.beginStructure(); + argument << seat.id << seat.path; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, DBusLogindSeat &seat) +{ + argument.beginStructure(); + argument >> seat.id >> seat.path; + argument.endStructure(); + return argument; +} + +Q_DECLARE_METATYPE(DBusLogindSeat) + +namespace KWin +{ + +static const QString s_serviceName = QStringLiteral("org.freedesktop.login1"); +static const QString s_propertiesInterface = QStringLiteral("org.freedesktop.DBus.Properties"); +static const QString s_sessionInterface = QStringLiteral("org.freedesktop.login1.Session"); +static const QString s_seatInterface = QStringLiteral("org.freedesktop.login1.Seat"); +static const QString s_managerInterface = QStringLiteral("org.freedesktop.login1.Manager"); +static const QString s_managerPath = QStringLiteral("/org/freedesktop/login1"); + +static QString findProcessSessionPath() +{ + const QString sessionId = qEnvironmentVariable("XDG_SESSION_ID", QStringLiteral("auto")); + QDBusMessage message = QDBusMessage::createMethodCall(s_serviceName, s_managerPath, + s_managerInterface, + QStringLiteral("GetSession")); + message.setArguments({sessionId}); + const QDBusMessage reply = QDBusConnection::systemBus().call(message); + if (reply.type() == QDBusMessage::ErrorMessage) { + return QString(); + } + + return reply.arguments().constFirst().value().path(); +} + +static bool takeControl(const QString &sessionPath) +{ + QDBusMessage message = QDBusMessage::createMethodCall(s_serviceName, sessionPath, + s_sessionInterface, + QStringLiteral("TakeControl")); + message.setArguments({false}); + + const QDBusMessage reply = QDBusConnection::systemBus().call(message); + + return reply.type() != QDBusMessage::ErrorMessage; +} + +static void releaseControl(const QString &sessionPath) +{ + const QDBusMessage message = QDBusMessage::createMethodCall(s_serviceName, sessionPath, + s_sessionInterface, + QStringLiteral("ReleaseControl")); + + QDBusConnection::systemBus().asyncCall(message); +} + +std::unique_ptr LogindSession::create() +{ + if (!QDBusConnection::systemBus().interface()->isServiceRegistered(s_serviceName)) { + return nullptr; + } + + const QString sessionPath = findProcessSessionPath(); + if (sessionPath.isEmpty()) { + qCWarning(KWIN_CORE) << "Could not determine the active graphical session"; + return nullptr; + } + + if (!takeControl(sessionPath)) { + qCWarning(KWIN_CORE, "Failed to take control of %s session. Maybe another compositor is running?", + qPrintable(sessionPath)); + return nullptr; + } + + std::unique_ptr session{new LogindSession(sessionPath)}; + if (session->initialize()) { + return session; + } else { + return nullptr; + } +} + +bool LogindSession::isActive() const +{ + return m_isActive; +} + +LogindSession::Capabilities LogindSession::capabilities() const +{ + return Capability::SwitchTerminal; +} + +QString LogindSession::seat() const +{ + return m_seatId; +} + +uint LogindSession::terminal() const +{ + return m_terminal; +} + +std::expected LogindSession::openRestricted(const QString &fileName) +{ + struct stat st; + if (stat(fileName.toUtf8(), &st) < 0) { + return std::unexpected(errorFromErrno()); + } + + QDBusMessage message = QDBusMessage::createMethodCall(s_serviceName, m_sessionPath, + s_sessionInterface, + QStringLiteral("TakeDevice")); + // major() and minor() macros return ints on FreeBSD instead of uints. + message.setArguments({uint(major(st.st_rdev)), uint(minor(st.st_rdev))}); + + const QDBusMessage reply = QDBusConnection::systemBus().call(message); + if (reply.type() == QDBusMessage::ErrorMessage) { + qCWarning(KWIN_CORE, "Failed to open %s device (%s)", + qPrintable(fileName), qPrintable(reply.errorMessage())); + if (reply.errorName() == "System.Error.EBUSY") { + return std::unexpected(Error::EBusy); + } else { + return std::unexpected(Error::Other); + } + } + + const QDBusUnixFileDescriptor descriptor = reply.arguments().constFirst().value(); + if (!descriptor.isValid()) { + return std::unexpected(Error::Other); + } + + const int ret = fcntl(descriptor.fileDescriptor(), F_DUPFD_CLOEXEC, 0); + if (ret == -1) { + return std::unexpected(errorFromErrno()); + } else { + return ret; + } +} + +void LogindSession::closeRestricted(int fileDescriptor) +{ + struct stat st; + if (fstat(fileDescriptor, &st) < 0) { + close(fileDescriptor); + return; + } + + QDBusMessage message = QDBusMessage::createMethodCall(s_serviceName, m_sessionPath, + s_sessionInterface, + QStringLiteral("ReleaseDevice")); + // major() and minor() macros return ints on FreeBSD instead of uints. + message.setArguments({uint(major(st.st_rdev)), uint(minor(st.st_rdev))}); + + QDBusConnection::systemBus().asyncCall(message); + + close(fileDescriptor); +} + +void LogindSession::switchTo(uint terminal) +{ + QDBusMessage message = QDBusMessage::createMethodCall(s_serviceName, m_seatPath, + s_seatInterface, + QStringLiteral("SwitchTo")); + message.setArguments({terminal}); + + QDBusConnection::systemBus().asyncCall(message); +} + +FileDescriptor LogindSession::delaySleep(const QString &reason) +{ + QDBusMessage message = QDBusMessage::createMethodCall(s_serviceName, s_managerPath, + s_managerInterface, + QStringLiteral("Inhibit")); + message.setArguments({QStringLiteral("sleep"), QStringLiteral("compositor"), reason, QStringLiteral("delay")}); + + const QDBusMessage reply = QDBusConnection::systemBus().call(message); + if (reply.type() == QDBusMessage::ErrorMessage) { + qCWarning(KWIN_CORE, "Failed to delay sleep: %s", qPrintable(reply.errorMessage())); + return FileDescriptor{}; + } + const QDBusUnixFileDescriptor descriptor = reply.arguments().constFirst().value(); + return FileDescriptor(fcntl(descriptor.fileDescriptor(), F_DUPFD_CLOEXEC, 0)); +} + +LogindSession::LogindSession(const QString &sessionPath) + : m_sessionPath(sessionPath) +{ + qDBusRegisterMetaType(); +} + +LogindSession::~LogindSession() +{ + releaseControl(m_sessionPath); +} + +bool LogindSession::initialize() +{ + QDBusMessage activeMessage = QDBusMessage::createMethodCall(s_serviceName, m_sessionPath, + s_propertiesInterface, + QStringLiteral("Get")); + activeMessage.setArguments({s_sessionInterface, QStringLiteral("Active")}); + + QDBusMessage seatMessage = QDBusMessage::createMethodCall(s_serviceName, m_sessionPath, + s_propertiesInterface, + QStringLiteral("Get")); + seatMessage.setArguments({s_sessionInterface, QStringLiteral("Seat")}); + + QDBusMessage terminalMessage = QDBusMessage::createMethodCall(s_serviceName, m_sessionPath, + s_propertiesInterface, + QStringLiteral("Get")); + terminalMessage.setArguments({s_sessionInterface, QStringLiteral("VTNr")}); + + QDBusPendingReply activeReply = + QDBusConnection::systemBus().asyncCall(activeMessage); + QDBusPendingReply terminalReply = + QDBusConnection::systemBus().asyncCall(terminalMessage); + QDBusPendingReply seatReply = + QDBusConnection::systemBus().asyncCall(seatMessage); + + // We must wait until all replies have been received because the drm backend needs a + // valid seat name to properly select gpu devices, this also simplifies startup code. + activeReply.waitForFinished(); + terminalReply.waitForFinished(); + seatReply.waitForFinished(); + + if (activeReply.isError()) { + qCWarning(KWIN_CORE) << "Failed to query Active session property:" << activeReply.error(); + return false; + } + if (terminalReply.isError()) { + qCWarning(KWIN_CORE) << "Failed to query VTNr session property:" << terminalReply.error(); + return false; + } + if (seatReply.isError()) { + qCWarning(KWIN_CORE) << "Failed to query Seat session property:" << seatReply.error(); + return false; + } + + m_isActive = activeReply.value().toBool(); + m_terminal = terminalReply.value().toUInt(); + + const DBusLogindSeat seat = qdbus_cast(seatReply.value().value()); + m_seatId = seat.id; + m_seatPath = seat.path.path(); + + QDBusConnection::systemBus().connect(s_serviceName, s_managerPath, s_managerInterface, + QStringLiteral("PrepareForSleep"), + this, + SLOT(handlePrepareForSleep(bool))); + + QDBusConnection::systemBus().connect(s_serviceName, m_sessionPath, s_sessionInterface, + QStringLiteral("PauseDevice"), + this, + SLOT(handlePauseDevice(uint, uint, QString))); + + QDBusConnection::systemBus().connect(s_serviceName, m_sessionPath, s_sessionInterface, + QStringLiteral("ResumeDevice"), + this, + SLOT(handleResumeDevice(uint, uint, QDBusUnixFileDescriptor))); + + QDBusConnection::systemBus().connect(s_serviceName, m_sessionPath, s_propertiesInterface, + QStringLiteral("PropertiesChanged"), + this, + SLOT(handlePropertiesChanged(QString, QVariantMap))); + + return true; +} + +void LogindSession::updateActive(bool active) +{ + if (m_isActive != active) { + m_isActive = active; + Q_EMIT activeChanged(active); + } +} + +void LogindSession::handlePauseDevice(uint major, uint minor, const QString &type) +{ + Q_EMIT devicePaused(makedev(major, minor)); + + if (type == QLatin1String("pause")) { + QDBusMessage message = QDBusMessage::createMethodCall(s_serviceName, m_sessionPath, + s_sessionInterface, + QStringLiteral("PauseDeviceComplete")); + message.setArguments({major, minor}); + + QDBusConnection::systemBus().asyncCall(message); + } +} + +void LogindSession::handleResumeDevice(uint major, uint minor, QDBusUnixFileDescriptor fileDescriptor) +{ + // We don't care about the file descriptor as the libinput backend will re-open input devices + // and the drm file descriptors remain valid after pausing gpus. + + Q_EMIT deviceResumed(makedev(major, minor)); +} + +void LogindSession::handlePropertiesChanged(const QString &interfaceName, const QVariantMap &properties) +{ + if (interfaceName == s_sessionInterface) { + const QVariant active = properties.value(QStringLiteral("Active")); + if (active.isValid()) { + updateActive(active.toBool()); + } + } +} + +void LogindSession::handlePrepareForSleep(bool sleep) +{ + if (sleep) { + Q_EMIT aboutToSleep(); + } else { + Q_EMIT awoke(); + } +} + +} // namespace KWin + +#include "moc_session_logind.cpp" diff --git a/local/recipes/kde/kwin/source/src/core/session_logind.h b/local/recipes/kde/kwin/source/src/core/session_logind.h new file mode 100644 index 0000000000..a65dffecc4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/session_logind.h @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "session.h" + +#include + +namespace KWin +{ + +class LogindSession : public Session +{ + Q_OBJECT + +public: + static std::unique_ptr create(); + ~LogindSession() override; + + bool isActive() const override; + Capabilities capabilities() const override; + QString seat() const override; + uint terminal() const override; + std::expected openRestricted(const QString &fileName) override; + void closeRestricted(int fileDescriptor) override; + void switchTo(uint terminal) override; + FileDescriptor delaySleep(const QString &reason) override; + +private Q_SLOTS: + void handleResumeDevice(uint major, uint minor, QDBusUnixFileDescriptor fileDescriptor); + void handlePauseDevice(uint major, uint minor, const QString &type); + void handlePropertiesChanged(const QString &interfaceName, const QVariantMap &properties); + void handlePrepareForSleep(bool sleep); + +private: + explicit LogindSession(const QString &sessionPath); + + bool initialize(); + void updateActive(bool active); + + QString m_sessionPath; + QString m_seatId; + QString m_seatPath; + uint m_terminal = 0; + bool m_isActive = false; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/session_noop.cpp b/local/recipes/kde/kwin/source/src/core/session_noop.cpp new file mode 100644 index 0000000000..11e137964d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/session_noop.cpp @@ -0,0 +1,74 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "session_noop.h" + +#include +#include + +namespace KWin +{ + +std::unique_ptr NoopSession::create() +{ + return std::unique_ptr{new NoopSession()}; +} + +NoopSession::~NoopSession() +{ +} + +bool NoopSession::isActive() const +{ + return true; +} + +NoopSession::Capabilities NoopSession::capabilities() const +{ + return Capabilities(); +} + +QString NoopSession::seat() const +{ + return QStringLiteral("seat0"); +} + +uint NoopSession::terminal() const +{ + return 0; +} + +std::expected NoopSession::openRestricted(const QString &fileName) +{ + const int fd = ::open(fileName.toUtf8().data(), O_RDWR | O_CLOEXEC); + if (fd != -1) { + return fd; + } else { + if (errno == EBUSY) { + return std::unexpected(Error::EBusy); + } else { + return std::unexpected(Error::Other); + } + } +} + +void NoopSession::closeRestricted(int fileDescriptor) +{ + ::close(fileDescriptor); +} + +void NoopSession::switchTo(uint terminal) +{ +} + +FileDescriptor NoopSession::delaySleep(const QString &reason) +{ + return FileDescriptor{}; +} + +} // namespace KWin + +#include "moc_session_noop.cpp" diff --git a/local/recipes/kde/kwin/source/src/core/session_noop.h b/local/recipes/kde/kwin/source/src/core/session_noop.h new file mode 100644 index 0000000000..6a063704ba --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/session_noop.h @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "session.h" + +namespace KWin +{ + +class NoopSession : public Session +{ + Q_OBJECT + +public: + static std::unique_ptr create(); + ~NoopSession() override; + + bool isActive() const override; + Capabilities capabilities() const override; + QString seat() const override; + uint terminal() const override; + std::expected openRestricted(const QString &fileName) override; + void closeRestricted(int fileDescriptor) override; + void switchTo(uint terminal) override; + FileDescriptor delaySleep(const QString &reason) override; + +private: + explicit NoopSession() = default; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/shmgraphicsbufferallocator.cpp b/local/recipes/kde/kwin/source/src/core/shmgraphicsbufferallocator.cpp new file mode 100644 index 0000000000..17529e2676 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/shmgraphicsbufferallocator.cpp @@ -0,0 +1,148 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "core/shmgraphicsbufferallocator.h" + +#include "config-kwin.h" + +#include "core/graphicsbuffer.h" +#include "utils/memorymap.h" + +#include +#include +#include +#include + +namespace KWin +{ + +class ShmGraphicsBuffer : public GraphicsBuffer +{ + Q_OBJECT + +public: + ShmGraphicsBuffer(ShmAttributes &&attributes, MemoryMap &&memoryMap); + + Map map(MapFlags flags) override; + void unmap() override; + + QSize size() const override; + bool hasAlphaChannel() const override; + const ShmAttributes *shmAttributes() const override; + +private: + ShmAttributes m_attributes; + MemoryMap m_memoryMap; + bool m_hasAlphaChannel; +}; + +ShmGraphicsBuffer::ShmGraphicsBuffer(ShmAttributes &&attributes, MemoryMap &&memoryMap) + : m_attributes(std::move(attributes)) + , m_memoryMap(std::move(memoryMap)) + , m_hasAlphaChannel(alphaChannelFromDrmFormat(attributes.format)) +{ +} + +GraphicsBuffer::Map ShmGraphicsBuffer::map(MapFlags flags) +{ + if (m_memoryMap.isValid()) { + return Map{ + .data = m_memoryMap.data(), + .stride = uint32_t(m_attributes.stride), + }; + } else { + return Map{}; + } +} + +void ShmGraphicsBuffer::unmap() +{ +} + +QSize ShmGraphicsBuffer::size() const +{ + return m_attributes.size; +} + +bool ShmGraphicsBuffer::hasAlphaChannel() const +{ + return m_hasAlphaChannel; +} + +const ShmAttributes *ShmGraphicsBuffer::shmAttributes() const +{ + return &m_attributes; +} + +GraphicsBuffer *ShmGraphicsBufferAllocator::allocate(const GraphicsBufferOptions &options) +{ + if (!options.software) { + return nullptr; + } + if (!options.modifiers.isEmpty() && !options.modifiers.contains(DRM_FORMAT_MOD_LINEAR)) { + return nullptr; + } + + switch (options.format) { + case DRM_FORMAT_ARGB8888: + case DRM_FORMAT_XRGB8888: + break; + default: + return nullptr; + } + + const int stride = options.size.width() * 4; + const int bufferSize = options.size.height() * stride; + +#if HAVE_MEMFD + FileDescriptor fd = FileDescriptor(memfd_create("shm", MFD_CLOEXEC | MFD_ALLOW_SEALING)); + if (!fd.isValid()) { + return nullptr; + } + + if (ftruncate(fd.get(), bufferSize) < 0) { + return nullptr; + } + + fcntl(fd.get(), F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_SEAL); +#else + char templateName[] = "/tmp/kwin-shm-XXXXXX"; + FileDescriptor fd{mkstemp(templateName)}; + if (!fd.isValid()) { + return nullptr; + } + + unlink(templateName); + int flags = fcntl(fd.get(), F_GETFD); + if (flags == -1 || fcntl(fd.get(), F_SETFD, flags | FD_CLOEXEC) == -1) { + return nullptr; + } + + if (ftruncate(fd.get(), bufferSize) < 0) { + return nullptr; + } +#endif + + ShmAttributes attributes{ + .fd = std::move(fd), + .stride = stride, + .offset = 0, + .size = options.size, + .format = options.format, + }; + + MemoryMap memoryMap(attributes.stride * attributes.size.height(), PROT_READ | PROT_WRITE, MAP_SHARED, attributes.fd.get(), attributes.offset); + if (!memoryMap.isValid()) { + return nullptr; + } + + return new ShmGraphicsBuffer(std::move(attributes), std::move(memoryMap)); +} + +} // namespace KWin + +#include "moc_shmgraphicsbufferallocator.cpp" +#include "shmgraphicsbufferallocator.moc" diff --git a/local/recipes/kde/kwin/source/src/core/shmgraphicsbufferallocator.h b/local/recipes/kde/kwin/source/src/core/shmgraphicsbufferallocator.h new file mode 100644 index 0000000000..f69336195b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/shmgraphicsbufferallocator.h @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/graphicsbufferallocator.h" + +namespace KWin +{ + +class KWIN_EXPORT ShmGraphicsBufferAllocator : public GraphicsBufferAllocator +{ +public: + GraphicsBuffer *allocate(const GraphicsBufferOptions &options) override; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/core/syncobjtimeline.cpp b/local/recipes/kde/kwin/source/src/core/syncobjtimeline.cpp new file mode 100644 index 0000000000..cb8296a854 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/syncobjtimeline.cpp @@ -0,0 +1,160 @@ +/* + SPDX-FileCopyrightText: 2024 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "syncobjtimeline.h" + +#include +#include +#include +#include + +#if defined(Q_OS_LINUX) +#include +#else +struct sync_merge_data +{ + char name[32]; + __s32 fd2; + __s32 fence; + __u32 flags; + __u32 pad; +}; +#define SYNC_IOC_MAGIC '>' +#define SYNC_IOC_MERGE _IOWR(SYNC_IOC_MAGIC, 3, struct sync_merge_data) +#endif + +namespace KWin +{ + +SyncReleasePoint::SyncReleasePoint(const std::shared_ptr &timeline, uint64_t timelinePoint) + : m_timeline(timeline) + , m_timelinePoint(timelinePoint) +{ +} + +SyncReleasePoint::~SyncReleasePoint() +{ + if (m_releaseFence.isValid()) { + m_timeline->moveInto(m_timelinePoint, m_releaseFence); + } else { + m_timeline->signal(m_timelinePoint); + } +} + +static FileDescriptor mergeSyncFds(const FileDescriptor &fd1, const FileDescriptor &fd2) +{ + struct sync_merge_data data + { + .name = "merged release fence", + .fd2 = fd2.get(), + .fence = -1, + }; + int err = -1; + do { + err = ioctl(fd1.get(), SYNC_IOC_MERGE, &data); + } while (err == -1 && (errno == EINTR || errno == EAGAIN)); + if (err < 0) { + return FileDescriptor{}; + } else { + return FileDescriptor(data.fence); + } +} + +void SyncReleasePoint::addReleaseFence(const FileDescriptor &fd) +{ + if (m_releaseFence.isValid()) { + m_releaseFence = mergeSyncFds(m_releaseFence, fd); + } else { + m_releaseFence = fd.duplicate(); + } +} + +SyncTimeline *SyncReleasePoint::timeline() const +{ + return m_timeline.get(); +} + +uint64_t SyncReleasePoint::timelinePoint() const +{ + return m_timelinePoint; +} + +SyncTimeline::SyncTimeline(int drmFd, uint32_t handle) + : m_drmFd(drmFd) + , m_handle(handle) +{ +} + +SyncTimeline::SyncTimeline(int drmFd) + : m_drmFd(drmFd) +{ + drmSyncobjCreate(m_drmFd, 0, &m_handle); +} + +const FileDescriptor &SyncTimeline::fileDescriptor() +{ + if (!m_fileDescriptor.isValid()) { + int fd = -1; + drmSyncobjHandleToFD(m_drmFd, m_handle, &fd); + m_fileDescriptor = FileDescriptor(fd); + } + + return m_fileDescriptor; +} + +SyncTimeline::~SyncTimeline() +{ + drmSyncobjDestroy(m_drmFd, m_handle); +} + +FileDescriptor SyncTimeline::eventFd(uint64_t timelinePoint) const +{ + FileDescriptor ret{eventfd(0, EFD_CLOEXEC)}; + if (!ret.isValid()) { + return {}; + } + if (drmSyncobjEventfd(m_drmFd, m_handle, timelinePoint, ret.get(), 0) != 0) { + return {}; + } + return ret; +} + +void SyncTimeline::signal(uint64_t timelinePoint) +{ + drmSyncobjTimelineSignal(m_drmFd, &m_handle, &timelinePoint, 1); +} + +void SyncTimeline::moveInto(uint64_t timelinePoint, const FileDescriptor &fd) +{ + uint32_t tempHandle = 0; + drmSyncobjCreate(m_drmFd, 0, &tempHandle); + drmSyncobjImportSyncFile(m_drmFd, tempHandle, fd.get()); + drmSyncobjTransfer(m_drmFd, m_handle, timelinePoint, tempHandle, 0, 0); + drmSyncobjDestroy(m_drmFd, tempHandle); +} + +FileDescriptor SyncTimeline::exportSyncFile(uint64_t timelinePoint) +{ + uint32_t tempHandle = 0; + int syncFileFd = -1; + drmSyncobjCreate(m_drmFd, 0, &tempHandle); + drmSyncobjTransfer(m_drmFd, tempHandle, 0, m_handle, timelinePoint, 0); + drmSyncobjExportSyncFile(m_drmFd, tempHandle, &syncFileFd); + drmSyncobjDestroy(m_drmFd, tempHandle); + return FileDescriptor(syncFileFd); +} + +bool SyncTimeline::isMaterialized(uint64_t timelinePoint) +{ + return (drmSyncobjTimelineWait(m_drmFd, + &m_handle, + &timelinePoint, + 1, + 0, + DRM_SYNCOBJ_WAIT_FLAGS_WAIT_AVAILABLE, + nullptr) + == 0); +} +} diff --git a/local/recipes/kde/kwin/source/src/core/syncobjtimeline.h b/local/recipes/kde/kwin/source/src/core/syncobjtimeline.h new file mode 100644 index 0000000000..17bc8265bd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/core/syncobjtimeline.h @@ -0,0 +1,65 @@ +/* + SPDX-FileCopyrightText: 2024 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "kwin_export.h" +#include "utils/filedescriptor.h" + +#include +#include + +namespace KWin +{ + +class SyncTimeline; + +/** + * A helper to signal the release point when it goes out of scope + */ +class KWIN_EXPORT SyncReleasePoint +{ +public: + explicit SyncReleasePoint(const std::shared_ptr &timeline, uint64_t timelinePoint); + ~SyncReleasePoint(); + + SyncTimeline *timeline() const; + uint64_t timelinePoint() const; + + /** + * Adds the fence of a graphics job that this release point should wait for + * before the timeline point is signaled + */ + void addReleaseFence(const FileDescriptor &fd); + +private: + const std::shared_ptr m_timeline; + const uint64_t m_timelinePoint; + FileDescriptor m_releaseFence; +}; + +class KWIN_EXPORT SyncTimeline +{ +public: + explicit SyncTimeline(int drmFd, uint32_t handle); + explicit SyncTimeline(int drmFd); + ~SyncTimeline(); + + /** + * @returns an event fd that gets signalled when the timeline point gets signalled + */ + FileDescriptor eventFd(uint64_t timelinePoint) const; + + const FileDescriptor &fileDescriptor(); + void signal(uint64_t timelinePoint); + void moveInto(uint64_t timelinePoint, const FileDescriptor &fd); + FileDescriptor exportSyncFile(uint64_t timelinePoint); + bool isMaterialized(uint64_t timelinePoint); + +private: + const int32_t m_drmFd; + uint32_t m_handle = 0; + FileDescriptor m_fileDescriptor; +}; +} diff --git a/local/recipes/kde/kwin/source/src/cursor.cpp b/local/recipes/kde/kwin/source/src/cursor.cpp new file mode 100644 index 0000000000..fafabbe6a3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/cursor.cpp @@ -0,0 +1,614 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "cursor.h" +// kwin +#include "core/output.h" +#include "cursorsource.h" +#include "main.h" + +// KDE +#include +#include +// Qt +#include + +namespace KWin +{ +Cursors *Cursors::s_self = nullptr; +Cursors *Cursors::self() +{ + if (!s_self) { + s_self = new Cursors; + } + return s_self; +} + +Cursors::Cursors() + : m_mouse(std::make_unique()) +{ + addCursor(m_mouse.get()); + setCurrentCursor(m_mouse.get()); +} + +void Cursors::addCursor(Cursor *cursor) +{ + Q_ASSERT(!m_cursors.contains(cursor)); + m_cursors += cursor; + + connect(cursor, &Cursor::destroyed, this, [this, cursor]() { + m_cursors.removeOne(cursor); + if (m_currentCursor == cursor) { + if (m_cursors.isEmpty()) { + m_currentCursor = nullptr; + } else { + setCurrentCursor(m_cursors.constFirst()); + } + } + }); + + connect(cursor, &Cursor::posChanged, this, [this, cursor](const QPointF &pos) { + setCurrentCursor(cursor); + Q_EMIT positionChanged(cursor, pos); + }); +} + +void Cursors::hideCursor() +{ + m_cursorHideCounter++; + if (m_cursorHideCounter == 1) { + Q_EMIT hiddenChanged(); + } +} + +void Cursors::showCursor() +{ + m_cursorHideCounter--; + if (m_cursorHideCounter == 0) { + Q_EMIT hiddenChanged(); + } +} + +bool Cursors::isCursorHidden() const +{ + return m_cursorHideCounter > 0; +} + +void Cursors::setCurrentCursor(Cursor *cursor) +{ + if (m_currentCursor == cursor) { + return; + } + + Q_ASSERT(m_cursors.contains(cursor) || !cursor); + + if (m_currentCursor) { + disconnect(m_currentCursor, &Cursor::cursorChanged, this, &Cursors::emitCurrentCursorChanged); + } + m_currentCursor = cursor; + connect(m_currentCursor, &Cursor::cursorChanged, this, &Cursors::emitCurrentCursorChanged); + + Q_EMIT currentCursorChanged(m_currentCursor); +} + +void Cursors::emitCurrentCursorChanged() +{ + Q_EMIT currentCursorChanged(m_currentCursor); +} + +Cursor::Cursor() + : m_themeName(defaultThemeName()) + , m_themeSize(defaultThemeSize()) +{ + loadThemeSettings(); + QDBusConnection::sessionBus().connect(QString(), QStringLiteral("/KGlobalSettings"), QStringLiteral("org.kde.KGlobalSettings"), + QStringLiteral("notifyChange"), this, SLOT(slotKGlobalSettingsNotifyChange(int, int))); +} + +void Cursor::loadThemeSettings() +{ + QString themeName = QString::fromUtf8(qgetenv("XCURSOR_THEME")); + bool ok = false; + // XCURSOR_SIZE might not be set (e.g. by startkde) + const uint themeSize = qEnvironmentVariableIntValue("XCURSOR_SIZE", &ok); + if (!themeName.isEmpty() && ok) { + updateTheme(themeName, themeSize); + return; + } + // didn't get from environment variables, read from config file + loadThemeFromKConfig(); +} + +void Cursor::loadThemeFromKConfig() +{ + KConfigGroup mousecfg(kwinApp()->inputConfig(), QStringLiteral("Mouse")); + const QString themeName = mousecfg.readEntry("cursorTheme", defaultThemeName()); + const uint themeSize = mousecfg.readEntry("cursorSize", defaultThemeSize()); + updateTheme(themeName, themeSize); +} + +void Cursor::updateTheme(const QString &name, int size) +{ + if (m_themeName != name || m_themeSize != size) { + m_themeName = name; + m_themeSize = size; + Q_EMIT themeChanged(); + } +} + +void Cursor::slotKGlobalSettingsNotifyChange(int type, int arg) +{ + if (type == 5 /*CursorChanged*/) { + kwinApp()->inputConfig()->reparseConfiguration(); + loadThemeFromKConfig(); + } +} + +bool Cursor::isOnOutput(LogicalOutput *output) const +{ + if (Cursors::self()->isCursorHidden()) { + return false; + } + return geometry().intersects(output->geometry()); +} + +QPointF Cursor::hotspot() const +{ + if (Q_UNLIKELY(!m_source)) { + return QPointF(); + } + return m_source->hotspot(); +} + +RectF Cursor::geometry() const +{ + return rect().translated(m_pos - hotspot()); +} + +RectF Cursor::rect() const +{ + if (Q_UNLIKELY(!m_source)) { + return RectF(); + } else { + return RectF(QPointF(0, 0), m_source->size()); + } +} + +QPointF Cursor::pos() +{ + return m_pos; +} + +void Cursor::setPos(const QPointF &pos) +{ + if (m_pos == pos) { + return; + } + m_pos = pos; + Q_EMIT posChanged(m_pos); +} + +QString Cursor::defaultThemeName() +{ + return QStringLiteral("default"); +} + +int Cursor::defaultThemeSize() +{ + return 24; +} + +QString Cursor::fallbackThemeName() +{ + return QStringLiteral("breeze_cursors"); +} + +QList CursorShape::alternatives(const QByteArray &name) +{ + static const QHash> alternatives = { + { + QByteArrayLiteral("crosshair"), + { + QByteArrayLiteral("cross"), + QByteArrayLiteral("diamond-cross"), + QByteArrayLiteral("cross-reverse"), + }, + }, + { + QByteArrayLiteral("default"), + { + QByteArrayLiteral("left_ptr"), + QByteArrayLiteral("arrow"), + QByteArrayLiteral("dnd-none"), + QByteArrayLiteral("op_left_arrow"), + }, + }, + { + QByteArrayLiteral("up-arrow"), + { + QByteArrayLiteral("up_arrow"), + QByteArrayLiteral("sb_up_arrow"), + QByteArrayLiteral("center_ptr"), + QByteArrayLiteral("centre_ptr"), + }, + }, + { + QByteArrayLiteral("wait"), + { + QByteArrayLiteral("watch"), + QByteArrayLiteral("progress"), + }, + }, + { + QByteArrayLiteral("text"), + { + QByteArrayLiteral("ibeam"), + QByteArrayLiteral("xterm"), + }, + }, + { + QByteArrayLiteral("all-scroll"), + { + QByteArrayLiteral("size_all"), + QByteArrayLiteral("fleur"), + }, + }, + { + QByteArrayLiteral("pointer"), + { + QByteArrayLiteral("pointing_hand"), + QByteArrayLiteral("hand2"), + QByteArrayLiteral("hand"), + QByteArrayLiteral("hand1"), + QByteArrayLiteral("e29285e634086352946a0e7090d73106"), + QByteArrayLiteral("9d800788f1b08800ae810202380a0822"), + }, + }, + { + QByteArrayLiteral("ns-resize"), + { + QByteArrayLiteral("size_ver"), + QByteArrayLiteral("00008160000006810000408080010102"), + QByteArrayLiteral("sb_v_double_arrow"), + QByteArrayLiteral("v_double_arrow"), + QByteArrayLiteral("n-resize"), + QByteArrayLiteral("s-resize"), + QByteArrayLiteral("col-resize"), + QByteArrayLiteral("top_side"), + QByteArrayLiteral("bottom_side"), + QByteArrayLiteral("base_arrow_up"), + QByteArrayLiteral("base_arrow_down"), + QByteArrayLiteral("based_arrow_down"), + QByteArrayLiteral("based_arrow_up"), + }, + }, + { + QByteArrayLiteral("ew-resize"), + { + QByteArrayLiteral("size_hor"), + QByteArrayLiteral("028006030e0e7ebffc7f7070c0600140"), + QByteArrayLiteral("sb_h_double_arrow"), + QByteArrayLiteral("h_double_arrow"), + QByteArrayLiteral("e-resize"), + QByteArrayLiteral("w-resize"), + QByteArrayLiteral("row-resize"), + QByteArrayLiteral("right_side"), + QByteArrayLiteral("left_side"), + }, + }, + { + QByteArrayLiteral("nesw-resize"), + { + QByteArrayLiteral("size_bdiag"), + QByteArrayLiteral("fcf1c3c7cd4491d801f1e1c78f100000"), + QByteArrayLiteral("fd_double_arrow"), + QByteArrayLiteral("bottom_left_corner"), + QByteArrayLiteral("top_right_corner"), + }, + }, + { + QByteArrayLiteral("nwse-resize"), + { + QByteArrayLiteral("size_fdiag"), + QByteArrayLiteral("c7088f0f3e6c8088236ef8e1e3e70000"), + QByteArrayLiteral("bd_double_arrow"), + QByteArrayLiteral("bottom_right_corner"), + QByteArrayLiteral("top_left_corner"), + }, + }, + { + QByteArrayLiteral("help"), + { + QByteArrayLiteral("whats_this"), + QByteArrayLiteral("d9ce0ab605698f320427677b458ad60b"), + QByteArrayLiteral("left_ptr_help"), + QByteArrayLiteral("question_arrow"), + QByteArrayLiteral("dnd-ask"), + QByteArrayLiteral("5c6cd98b3f3ebcb1f9c7f1c204630408"), + }, + }, + { + QByteArrayLiteral("col-resize"), + { + QByteArrayLiteral("split_h"), + QByteArrayLiteral("14fef782d02440884392942c11205230"), + QByteArrayLiteral("size_hor"), + }, + }, + { + QByteArrayLiteral("row-resize"), + { + QByteArrayLiteral("split_v"), + QByteArrayLiteral("2870a09082c103050810ffdffffe0204"), + QByteArrayLiteral("size_ver"), + }, + }, + { + QByteArrayLiteral("not-allowed"), + { + QByteArrayLiteral("forbidden"), + QByteArrayLiteral("03b6e0fcb3499374a867c041f52298f0"), + QByteArrayLiteral("circle"), + QByteArrayLiteral("dnd-no-drop"), + }, + }, + { + QByteArrayLiteral("progress"), + { + QByteArrayLiteral("left_ptr_watch"), + QByteArrayLiteral("3ecb610c1bf2410f44200f48c40d3599"), + QByteArrayLiteral("00000000000000020006000e7e9ffc3f"), + QByteArrayLiteral("08e8e1c95fe2fc01f976f1e063a24ccd"), + }, + }, + { + QByteArrayLiteral("grab"), + { + QByteArrayLiteral("openhand"), + QByteArrayLiteral("9141b49c8149039304290b508d208c40"), + QByteArrayLiteral("all_scroll"), + QByteArrayLiteral("all-scroll"), + }, + }, + { + QByteArrayLiteral("grabbing"), + { + QByteArrayLiteral("closedhand"), + QByteArrayLiteral("05e88622050804100c20044008402080"), + QByteArrayLiteral("4498f0e0c1937ffe01fd06f973665830"), + QByteArrayLiteral("9081237383d90e509aa00f00170e968f"), + QByteArrayLiteral("fcf21c00b30f7e3f83fe0dfd12e71cff"), + }, + }, + { + QByteArrayLiteral("alias"), + { + QByteArrayLiteral("link"), + QByteArrayLiteral("dnd-link"), + QByteArrayLiteral("3085a0e285430894940527032f8b26df"), + QByteArrayLiteral("640fb0e74195791501fd1ed57b41487f"), + QByteArrayLiteral("a2a266d0498c3104214a47bd64ab0fc8"), + }, + }, + { + QByteArrayLiteral("copy"), + { + QByteArrayLiteral("dnd-copy"), + QByteArrayLiteral("1081e37283d90000800003c07f3ef6bf"), + QByteArrayLiteral("6407b0e94181790501fd1e167b474872"), + QByteArrayLiteral("b66166c04f8c3109214a4fbd64a50fc8"), + }, + }, + { + QByteArrayLiteral("move"), + { + QByteArrayLiteral("dnd-move"), + }, + }, + { + QByteArrayLiteral("sw-resize"), + { + QByteArrayLiteral("size_bdiag"), + QByteArrayLiteral("fcf1c3c7cd4491d801f1e1c78f100000"), + QByteArrayLiteral("fd_double_arrow"), + QByteArrayLiteral("bottom_left_corner"), + }, + }, + { + QByteArrayLiteral("se-resize"), + { + QByteArrayLiteral("size_fdiag"), + QByteArrayLiteral("c7088f0f3e6c8088236ef8e1e3e70000"), + QByteArrayLiteral("bd_double_arrow"), + QByteArrayLiteral("bottom_right_corner"), + }, + }, + { + QByteArrayLiteral("ne-resize"), + { + QByteArrayLiteral("size_bdiag"), + QByteArrayLiteral("fcf1c3c7cd4491d801f1e1c78f100000"), + QByteArrayLiteral("fd_double_arrow"), + QByteArrayLiteral("top_right_corner"), + }, + }, + { + QByteArrayLiteral("nw-resize"), + { + QByteArrayLiteral("size_fdiag"), + QByteArrayLiteral("c7088f0f3e6c8088236ef8e1e3e70000"), + QByteArrayLiteral("bd_double_arrow"), + QByteArrayLiteral("top_left_corner"), + }, + }, + { + QByteArrayLiteral("n-resize"), + { + QByteArrayLiteral("size_ver"), + QByteArrayLiteral("00008160000006810000408080010102"), + QByteArrayLiteral("sb_v_double_arrow"), + QByteArrayLiteral("v_double_arrow"), + QByteArrayLiteral("col-resize"), + QByteArrayLiteral("top_side"), + }, + }, + { + QByteArrayLiteral("e-resize"), + { + QByteArrayLiteral("size_hor"), + QByteArrayLiteral("028006030e0e7ebffc7f7070c0600140"), + QByteArrayLiteral("sb_h_double_arrow"), + QByteArrayLiteral("h_double_arrow"), + QByteArrayLiteral("row-resize"), + QByteArrayLiteral("left_side"), + }, + }, + { + QByteArrayLiteral("s-resize"), + { + QByteArrayLiteral("size_ver"), + QByteArrayLiteral("00008160000006810000408080010102"), + QByteArrayLiteral("sb_v_double_arrow"), + QByteArrayLiteral("v_double_arrow"), + QByteArrayLiteral("col-resize"), + QByteArrayLiteral("bottom_side"), + }, + }, + { + QByteArrayLiteral("w-resize"), + { + QByteArrayLiteral("size_hor"), + QByteArrayLiteral("028006030e0e7ebffc7f7070c0600140"), + QByteArrayLiteral("sb_h_double_arrow"), + QByteArrayLiteral("h_double_arrow"), + QByteArrayLiteral("right_side"), + }, + }, + { + QByteArrayLiteral("dnd-ask"), + { + QByteArrayLiteral("copy"), + }, + }, + { + QByteArrayLiteral("all-resize"), + { + QByteArrayLiteral("move"), + }, + }, + }; + + auto it = alternatives.find(name); + if (it == alternatives.end()) { + return QList(); + } + + QList result = it.value(); + for (int i = 0; i < result.size(); ++i) { + if (auto it = alternatives.find(result[i]); it != alternatives.end()) { + for (const QByteArray &alternative : *it) { + if (!result.contains(alternative)) { + result.append(alternative); + } + } + } + } + + return result; +} + +QByteArray CursorShape::name() const +{ + switch (m_shape) { + case Qt::ArrowCursor: + return QByteArrayLiteral("default"); + case Qt::UpArrowCursor: + return QByteArrayLiteral("up-arrow"); + case Qt::CrossCursor: + return QByteArrayLiteral("crosshair"); + case Qt::WaitCursor: + return QByteArrayLiteral("wait"); + case Qt::IBeamCursor: + return QByteArrayLiteral("text"); + case Qt::SizeVerCursor: + return QByteArrayLiteral("ns-resize"); + case Qt::SizeHorCursor: + return QByteArrayLiteral("ew-resize"); + case Qt::SizeBDiagCursor: + return QByteArrayLiteral("nesw-resize"); + case Qt::SizeFDiagCursor: + return QByteArrayLiteral("nwse-resize"); + case Qt::SizeAllCursor: + return QByteArrayLiteral("all-scroll"); + case Qt::SplitVCursor: + return QByteArrayLiteral("row-resize"); + case Qt::SplitHCursor: + return QByteArrayLiteral("col-resize"); + case Qt::PointingHandCursor: + return QByteArrayLiteral("pointer"); + case Qt::ForbiddenCursor: + return QByteArrayLiteral("not-allowed"); + case Qt::OpenHandCursor: + return QByteArrayLiteral("grab"); + case Qt::ClosedHandCursor: + return QByteArrayLiteral("grabbing"); + case Qt::WhatsThisCursor: + return QByteArrayLiteral("help"); + case Qt::BusyCursor: + return QByteArrayLiteral("progress"); + case Qt::DragMoveCursor: + return QByteArrayLiteral("move"); + case Qt::DragCopyCursor: + return QByteArrayLiteral("copy"); + case Qt::DragLinkCursor: + return QByteArrayLiteral("alias"); + case KWin::ExtendedCursor::SizeNorthEast: + return QByteArrayLiteral("ne-resize"); + case KWin::ExtendedCursor::SizeNorth: + return QByteArrayLiteral("n-resize"); + case KWin::ExtendedCursor::SizeNorthWest: + return QByteArrayLiteral("nw-resize"); + case KWin::ExtendedCursor::SizeEast: + return QByteArrayLiteral("e-resize"); + case KWin::ExtendedCursor::SizeWest: + return QByteArrayLiteral("w-resize"); + case KWin::ExtendedCursor::SizeSouthEast: + return QByteArrayLiteral("se-resize"); + case KWin::ExtendedCursor::SizeSouth: + return QByteArrayLiteral("s-resize"); + case KWin::ExtendedCursor::SizeSouthWest: + return QByteArrayLiteral("sw-resize"); + default: + return QByteArray(); + } +} + +CursorSource *Cursor::source() const +{ + return m_source; +} + +void Cursor::setSource(CursorSource *source) +{ + if (m_source == source) { + return; + } + if (m_source) { + disconnect(m_source, &CursorSource::changed, this, &Cursor::cursorChanged); + } + m_source = source; + if (m_source) { + connect(m_source, &CursorSource::changed, this, &Cursor::cursorChanged); + } + Q_EMIT cursorChanged(); +} + +} // namespace + +#include "moc_cursor.cpp" diff --git a/local/recipes/kde/kwin/source/src/cursor.h b/local/recipes/kde/kwin/source/src/cursor.h new file mode 100644 index 0000000000..59d0811506 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/cursor.h @@ -0,0 +1,198 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "core/rect.h" + +// Qt +#include +#include +#include + +namespace KWin +{ + +class CursorSource; +class LogicalOutput; + +namespace ExtendedCursor +{ +/** + * Extension of Qt::CursorShape with values not currently present there + */ +enum Shape { + SizeNorthWest = 0x100 + 0, + SizeNorth = 0x100 + 1, + SizeNorthEast = 0x100 + 2, + SizeEast = 0x100 + 3, + SizeWest = 0x100 + 4, + SizeSouthEast = 0x100 + 5, + SizeSouth = 0x100 + 6, + SizeSouthWest = 0x100 + 7 +}; +} + +/** + * @brief Wrapper round Qt::CursorShape with extensions enums into a single entity + */ +class KWIN_EXPORT CursorShape +{ +public: + CursorShape() = default; + CursorShape(Qt::CursorShape qtShape) + { + m_shape = qtShape; + } + CursorShape(KWin::ExtendedCursor::Shape kwinShape) + { + m_shape = kwinShape; + } + bool operator==(const CursorShape &o) const + { + return m_shape == o.m_shape; + } + operator int() const + { + return m_shape; + } + /** + * @brief The name of a cursor shape in the theme. + */ + QByteArray name() const; + + /** + * Returns the list of alternative shape names for a shape with the specified @a name. + */ + static QList alternatives(const QByteArray &name); + +private: + int m_shape = Qt::ArrowCursor; +}; + +/** + * The Cursor type represents a pointer or a tablet cursor on the screen. + */ +class KWIN_EXPORT Cursor : public QObject +{ + Q_OBJECT + +public: + Cursor(); + + /** + * @brief The name of the currently used Cursor theme. + * + * @return const QString& + */ + const QString &themeName() const; + /** + * @brief The size of the currently used Cursor theme. + * + * @return int + */ + int themeSize() const; + /** + * Returns the default Xcursor theme name. + */ + static QString defaultThemeName(); + /** + * Returns the default Xcursor theme size. + */ + static int defaultThemeSize(); + /** + * Returns the fallback Xcursor theme name. + */ + static QString fallbackThemeName(); + + QPointF pos(); + void setPos(const QPointF &pos); + + QPointF hotspot() const; + RectF geometry() const; + RectF rect() const; + + CursorSource *source() const; + void setSource(CursorSource *source); + + /** + * Returns @c true if the cursor is visible on the given output; otherwise returns @c false. + */ + bool isOnOutput(LogicalOutput *output) const; + +Q_SIGNALS: + void posChanged(const QPointF &pos); + void cursorChanged(); + void themeChanged(); + +private Q_SLOTS: + void loadThemeSettings(); + void slotKGlobalSettingsNotifyChange(int type, int arg); + +private: + void updateTheme(const QString &name, int size); + void loadThemeFromKConfig(); + CursorSource *m_source = nullptr; + QPointF m_pos; + QString m_themeName; + int m_themeSize; +}; + +class KWIN_EXPORT Cursors : public QObject +{ + Q_OBJECT +public: + Cursors(); + + Cursor *mouse() const + { + return m_mouse.get(); + } + + void addCursor(Cursor *cursor); + + ///@returns the last cursor that moved + Cursor *currentCursor() const + { + return m_currentCursor; + } + + void hideCursor(); + void showCursor(); + bool isCursorHidden() const; + + static Cursors *self(); + +Q_SIGNALS: + void currentCursorChanged(Cursor *cursor); + void hiddenChanged(); + void positionChanged(Cursor *cursor, const QPointF &position); + +private: + void emitCurrentCursorChanged(); + void setCurrentCursor(Cursor *cursor); + + static Cursors *s_self; + std::unique_ptr m_mouse; + Cursor *m_currentCursor = nullptr; + QList m_cursors; + int m_cursorHideCounter = 0; +}; + +inline const QString &Cursor::themeName() const +{ + return m_themeName; +} + +inline int Cursor::themeSize() const +{ + return m_themeSize; +} +} + +Q_DECLARE_METATYPE(KWin::CursorShape) diff --git a/local/recipes/kde/kwin/source/src/cursorsource.cpp b/local/recipes/kde/kwin/source/src/cursorsource.cpp new file mode 100644 index 0000000000..0075775fb7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/cursorsource.cpp @@ -0,0 +1,195 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "cursorsource.h" +#include "cursor.h" +#include "wayland/surface.h" + +namespace KWin +{ + +CursorSource::CursorSource(QObject *parent) + : QObject(parent) +{ +} + +bool CursorSource::isBlank() const +{ + return m_size.isEmpty(); +} + +QSizeF CursorSource::size() const +{ + return m_size; +} + +QPointF CursorSource::hotspot() const +{ + return m_hotspot; +} + +void CursorSource::frame(std::chrono::milliseconds timestamp) +{ +} + +ShapeCursorSource::ShapeCursorSource(QObject *parent) + : CursorSource(parent) +{ + m_delayTimer.setSingleShot(true); + connect(&m_delayTimer, &QTimer::timeout, this, &ShapeCursorSource::selectNextSprite); +} + +QImage ShapeCursorSource::image() const +{ + return m_image; +} + +QByteArray ShapeCursorSource::shape() const +{ + return m_shape; +} + +void ShapeCursorSource::setShape(const QByteArray &shape) +{ + if (m_shape != shape) { + m_shape = shape; + refresh(); + } +} + +void ShapeCursorSource::setShape(Qt::CursorShape shape) +{ + setShape(CursorShape(shape).name()); +} + +CursorTheme ShapeCursorSource::theme() const +{ + return m_theme; +} + +void ShapeCursorSource::setTheme(const CursorTheme &theme) +{ + if (m_theme != theme) { + m_theme = theme; + refresh(); + } +} + +void ShapeCursorSource::refresh() +{ + m_currentSprite = -1; + m_delayTimer.stop(); + + m_sprites = m_theme.shape(m_shape); + if (m_sprites.isEmpty()) { + const auto alternativeNames = CursorShape::alternatives(m_shape); + for (const QByteArray &alternativeName : alternativeNames) { + m_sprites = m_theme.shape(alternativeName); + if (!m_sprites.isEmpty()) { + break; + } + } + } + + if (!m_sprites.isEmpty()) { + selectSprite(0); + } +} + +void ShapeCursorSource::selectNextSprite() +{ + selectSprite((m_currentSprite + 1) % m_sprites.size()); +} + +void ShapeCursorSource::selectSprite(int index) +{ + if (m_currentSprite == index) { + return; + } + const CursorSprite &sprite = m_sprites[index]; + m_currentSprite = index; + m_image = sprite.data(); + m_size = QSizeF(m_image.size()) / m_image.devicePixelRatio(); + m_hotspot = sprite.hotspot(); + if (sprite.delay().count() && m_sprites.size() > 1) { + m_delayTimer.start(sprite.delay()); + } + Q_EMIT changed(); +} + +SurfaceCursorSource::SurfaceCursorSource(QObject *parent) + : CursorSource(parent) +{ +} + +SurfaceInterface *SurfaceCursorSource::surface() const +{ + return m_surface; +} + +void SurfaceCursorSource::frame(std::chrono::milliseconds timestamp) +{ + if (m_surface) { + m_surface->traverseTree([×tamp](SurfaceInterface *surface) { + surface->frameRendered(timestamp.count()); + // FIXME also handle presentation feedback! + surface->clearFifoBarrier(); + }); + } +} + +void SurfaceCursorSource::refresh() +{ + m_size = m_surface->size(); + m_hotspot -= m_surface->offset(); + Q_EMIT changed(); +} + +void SurfaceCursorSource::reset() +{ + m_size = QSizeF(0, 0); + m_hotspot = QPointF(0, 0); + m_surface = nullptr; + Q_EMIT changed(); +} + +void SurfaceCursorSource::update(SurfaceInterface *surface, const QPointF &hotspot) +{ + bool dirty = false; + + if (m_hotspot != hotspot) { + dirty = true; + m_hotspot = hotspot; + } + + if (m_surface != surface) { + dirty = true; + + if (m_surface) { + disconnect(m_surface, &SurfaceInterface::committed, this, &SurfaceCursorSource::refresh); + disconnect(m_surface, &SurfaceInterface::destroyed, this, &SurfaceCursorSource::reset); + } + + m_surface = surface; + + if (m_surface) { + m_size = surface->size(); + + connect(m_surface, &SurfaceInterface::committed, this, &SurfaceCursorSource::refresh); + connect(m_surface, &SurfaceInterface::destroyed, this, &SurfaceCursorSource::reset); + } else { + m_size = QSizeF(0, 0); + } + } + + if (dirty) { + Q_EMIT changed(); + } +} + +} // namespace KWin + +#include "moc_cursorsource.cpp" diff --git a/local/recipes/kde/kwin/source/src/cursorsource.h b/local/recipes/kde/kwin/source/src/cursorsource.h new file mode 100644 index 0000000000..fcb88375ae --- /dev/null +++ b/local/recipes/kde/kwin/source/src/cursorsource.h @@ -0,0 +1,101 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "utils/cursortheme.h" + +#include +#include +#include +#include + +namespace KWin +{ + +class SurfaceInterface; + +/** + * The CursorSource class represents the contents of the Cursor. + */ +class KWIN_EXPORT CursorSource : public QObject +{ + Q_OBJECT + +public: + explicit CursorSource(QObject *parent = nullptr); + + bool isBlank() const; + QSizeF size() const; + QPointF hotspot() const; + + virtual void frame(std::chrono::milliseconds timestamp); + +Q_SIGNALS: + void changed(); + +protected: + QSizeF m_size = QSizeF(0, 0); + QPointF m_hotspot; +}; + +/** + * The ShapeCursorSource class represents the contents of a shape in the cursor theme. + */ +class KWIN_EXPORT ShapeCursorSource : public CursorSource +{ + Q_OBJECT + +public: + explicit ShapeCursorSource(QObject *parent = nullptr); + + QImage image() const; + + QByteArray shape() const; + void setShape(const QByteArray &shape); + void setShape(Qt::CursorShape shape); + + CursorTheme theme() const; + void setTheme(const CursorTheme &theme); + +private: + void refresh(); + void selectNextSprite(); + void selectSprite(int index); + + CursorTheme m_theme; + QByteArray m_shape; + QList m_sprites; + QTimer m_delayTimer; + QImage m_image; + int m_currentSprite = -1; +}; + +/** + * The SurfaceCursorSource class repsents the contents of a cursor backed by a wl_surface. + */ +class KWIN_EXPORT SurfaceCursorSource : public CursorSource +{ + Q_OBJECT + +public: + explicit SurfaceCursorSource(QObject *parent = nullptr); + + SurfaceInterface *surface() const; + + void frame(std::chrono::milliseconds timestamp) override; + +public Q_SLOTS: + void update(SurfaceInterface *surface, const QPointF &hotspot); + +private: + void refresh(); + void reset(); + + SurfaceInterface *m_surface = nullptr; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/dbusinterface.cpp b/local/recipes/kde/kwin/source/src/dbusinterface.cpp new file mode 100644 index 0000000000..3468da5ba2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/dbusinterface.cpp @@ -0,0 +1,469 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +// own +#include "dbusinterface.h" +#include "compositingadaptor.h" +#include "pluginsadaptor.h" +#include "virtualdesktopmanageradaptor.h" + +// kwin +#include "compositor.h" +#include "core/output.h" +#include "core/renderbackend.h" +#include "debug_console.h" +#include "kwinadaptor.h" +#include "main.h" +#include "placement.h" +#include "pluginmanager.h" +#include "virtualdesktops.h" +#include "window.h" +#include "workspace.h" +#if KWIN_BUILD_ACTIVITIES +#include "activities.h" +#endif + +// Qt +#include +#include + +namespace KWin +{ + +DBusInterface::DBusInterface(QObject *parent) + : QObject(parent) + , m_serviceName(QStringLiteral("org.kde.KWin")) +{ + (void)new KWinAdaptor(this); + + QDBusConnection dbus = QDBusConnection::sessionBus(); + dbus.registerObject(QStringLiteral("/KWin"), this); + dbus.registerService(m_serviceName); + dbus.connect(QString(), QStringLiteral("/KWin"), QStringLiteral("org.kde.KWin"), QStringLiteral("reloadConfig"), + Workspace::self(), SLOT(slotReloadConfig())); + + connect(Workspace::self(), &Workspace::showingDesktopChanged, this, &DBusInterface::onShowingDesktopChanged); +} + +DBusInterface::~DBusInterface() +{ + QDBusConnection::sessionBus().unregisterService(m_serviceName); +} + +bool DBusInterface::showingDesktop() const +{ + return workspace()->showingDesktop(); +} + +void DBusInterface::reconfigure() +{ + Workspace::self()->reconfigure(); +} + +void DBusInterface::killWindow() +{ + Workspace::self()->slotKillWindow(); +} + +QString DBusInterface::supportInformation() +{ + return Workspace::self()->supportInformation(); +} + +QString DBusInterface::activeOutputName() +{ + return Workspace::self()->activeOutput()->name(); +} + +int DBusInterface::currentDesktop() +{ + return VirtualDesktopManager::self()->current(); +} + +bool DBusInterface::setCurrentDesktop(int desktop) +{ + return VirtualDesktopManager::self()->setCurrent(desktop); +} + +void DBusInterface::nextDesktop() +{ + VirtualDesktopManager::self()->moveTo(VirtualDesktopManager::Direction::Next); +} + +void DBusInterface::previousDesktop() +{ + VirtualDesktopManager::self()->moveTo(VirtualDesktopManager::Direction::Previous); +} + +void DBusInterface::showDebugConsole() +{ + DebugConsole *console = new DebugConsole; + console->show(); +} + +void DBusInterface::replace() +{ + QCoreApplication::exit(133); +} + +namespace +{ +QVariantMap clientToVariantMap(const Window *c) +{ + return + { + {QStringLiteral("resourceClass"), c->resourceClass()}, + {QStringLiteral("resourceName"), c->resourceName()}, + {QStringLiteral("desktopFile"), c->desktopFileName()}, + {QStringLiteral("role"), c->windowRole()}, + {QStringLiteral("caption"), c->captionNormal()}, + {QStringLiteral("clientMachine"), c->wmClientMachine(true)}, + {QStringLiteral("localhost"), c->isLocalhost()}, + {QStringLiteral("type"), int(c->windowType())}, + {QStringLiteral("x"), c->x()}, + {QStringLiteral("y"), c->y()}, + {QStringLiteral("width"), c->width()}, + {QStringLiteral("height"), c->height()}, + {QStringLiteral("desktops"), c->desktopIds()}, + {QStringLiteral("minimized"), c->isMinimized()}, + {QStringLiteral("fullscreen"), c->isFullScreen()}, + {QStringLiteral("keepAbove"), c->keepAbove()}, + {QStringLiteral("keepBelow"), c->keepBelow()}, + {QStringLiteral("noBorder"), c->noBorder()}, + {QStringLiteral("skipTaskbar"), c->skipTaskbar()}, + {QStringLiteral("skipPager"), c->skipPager()}, + {QStringLiteral("skipSwitcher"), c->skipSwitcher()}, + {QStringLiteral("maximizeHorizontal"), c->maximizeMode() & MaximizeHorizontal}, + {QStringLiteral("maximizeVertical"), c->maximizeMode() & MaximizeVertical}, + {QStringLiteral("uuid"), c->internalId().toString()}, +#if KWIN_BUILD_ACTIVITIES + {QStringLiteral("activities"), c->activities()}, +#endif + {QStringLiteral("layer"), c->layer()}, + }; +} +} + +QVariantMap DBusInterface::queryWindowInfo() +{ + m_replyQueryWindowInfo = message(); + setDelayedReply(true); + kwinApp()->startInteractiveWindowSelection( + [this](Window *t) { + if (!t) { + QDBusConnection::sessionBus().send(m_replyQueryWindowInfo.createErrorReply( + QStringLiteral("org.kde.KWin.Error.UserCancel"), + QStringLiteral("User cancelled the query"))); + return; + } + if (t->isClient()) { + QDBusConnection::sessionBus().send(m_replyQueryWindowInfo.createReply(clientToVariantMap(t))); + } else { + QDBusConnection::sessionBus().send(m_replyQueryWindowInfo.createErrorReply( + QStringLiteral("org.kde.KWin.Error.InvalidWindow"), + QStringLiteral("Tried to query information about an unmanaged window"))); + } + }); + return QVariantMap{}; +} + +QVariantMap DBusInterface::getWindowInfo(const QString &uuid) +{ + const auto window = workspace()->findWindow(QUuid::fromString(uuid)); + if (window) { + return clientToVariantMap(window); + } else { + return {}; + } +} + +void DBusInterface::showDesktop(bool show) +{ + workspace()->setShowingDesktop(show, true); + + auto m = message(); + if (m.service().isEmpty()) { + return; + } + + // Keep track of whatever D-Bus client asked to show the desktop. If + // they disappear from the bus, cancel the show desktop state so we do + // not end up in a state where we are stuck showing the desktop. + static QPointer watcher; + + if (show) { + if (watcher) { + // If we get a second call to `showDesktop(true)`, drop the previous + // watcher and watch the new client. That way, we simply always + // track the last state. + watcher->deleteLater(); + } + + watcher = new QDBusServiceWatcher(m.service(), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForUnregistration, this); + connect(watcher, &QDBusServiceWatcher::serviceUnregistered, []() { + workspace()->setShowingDesktop(false, true); + watcher->deleteLater(); + }); + } else if (watcher) { + // Someone cancelled showing the desktop, so there's no more need to + // watch to cancel the show desktop state. + watcher->deleteLater(); + } +} + +void DBusInterface::onShowingDesktopChanged(bool show, bool /*animated*/) +{ + Q_EMIT showingDesktopChanged(show); +} + +CompositorDBusInterface::CompositorDBusInterface(Compositor *parent) + : QObject(parent) + , m_compositor(parent) +{ + connect(m_compositor, &Compositor::compositingToggled, this, &CompositorDBusInterface::compositingToggled); + new CompositingAdaptor(this); + QDBusConnection dbus = QDBusConnection::sessionBus(); + dbus.registerObject(QStringLiteral("/Compositor"), this); + dbus.connect(QString(), QStringLiteral("/Compositor"), QStringLiteral("org.kde.kwin.Compositing"), + QStringLiteral("reinit"), this, SLOT(reinitialize())); +} + +QString CompositorDBusInterface::compositingType() const +{ + switch (m_compositor->backend()->compositingType()) { + case OpenGLCompositing: + if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES) { + return QStringLiteral("gles"); + } else { + return QStringLiteral("gl2"); + } + case QPainterCompositing: + return QStringLiteral("qpainter"); + case NoCompositing: + default: + return QStringLiteral("none"); + } +} + +bool CompositorDBusInterface::isActive() const +{ + return m_compositor->isActive(); +} + +bool CompositorDBusInterface::isCompositingPossible() const +{ + return true; +} + +QString CompositorDBusInterface::compositingNotPossibleReason() const +{ + return QString(); +} + +bool CompositorDBusInterface::isOpenGLBroken() const +{ + return false; +} + +bool CompositorDBusInterface::platformRequiresCompositing() const +{ + return true; +} + +void CompositorDBusInterface::reinitialize() +{ + m_compositor->reinitialize(); +} + +QStringList CompositorDBusInterface::supportedOpenGLPlatformInterfaces() const +{ + return {QStringLiteral("egl")}; +} + +VirtualDesktopManagerDBusInterface::VirtualDesktopManagerDBusInterface(VirtualDesktopManager *parent) + : QObject(parent) + , m_manager(parent) +{ + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + new VirtualDesktopManagerAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/VirtualDesktopManager"), + QStringLiteral("org.kde.KWin.VirtualDesktopManager"), + this); + + connect(m_manager, &VirtualDesktopManager::currentChanged, this, [this]() { + Q_EMIT currentChanged(m_manager->currentDesktop()->id()); + }); + + connect(m_manager, &VirtualDesktopManager::countChanged, this, [this](uint previousCount, uint newCount) { + Q_EMIT countChanged(newCount); + Q_EMIT desktopsChanged(desktops()); + }); + + connect(m_manager, &VirtualDesktopManager::navigationWrappingAroundChanged, this, [this]() { + Q_EMIT navigationWrappingAroundChanged(isNavigationWrappingAround()); + }); + + connect(m_manager, &VirtualDesktopManager::rowsChanged, this, &VirtualDesktopManagerDBusInterface::rowsChanged); + + const QList allDesks = m_manager->desktops(); + for (auto *vd : allDesks) { + connect(vd, &VirtualDesktop::x11DesktopNumberChanged, this, [this, vd]() { + DBusDesktopDataStruct data{.position = vd->x11DesktopNumber() - 1, .id = vd->id(), .name = vd->name()}; + Q_EMIT desktopDataChanged(vd->id(), data); + Q_EMIT desktopsChanged(desktops()); + }); + connect(vd, &VirtualDesktop::nameChanged, this, [this, vd]() { + DBusDesktopDataStruct data{.position = vd->x11DesktopNumber() - 1, .id = vd->id(), .name = vd->name()}; + Q_EMIT desktopDataChanged(vd->id(), data); + Q_EMIT desktopsChanged(desktops()); + }); + } + connect(m_manager, &VirtualDesktopManager::desktopAdded, this, [this](VirtualDesktop *vd) { + connect(vd, &VirtualDesktop::x11DesktopNumberChanged, this, [this, vd]() { + DBusDesktopDataStruct data{.position = vd->x11DesktopNumber() - 1, .id = vd->id(), .name = vd->name()}; + Q_EMIT desktopDataChanged(vd->id(), data); + Q_EMIT desktopsChanged(desktops()); + }); + connect(vd, &VirtualDesktop::nameChanged, this, [this, vd]() { + DBusDesktopDataStruct data{.position = vd->x11DesktopNumber() - 1, .id = vd->id(), .name = vd->name()}; + Q_EMIT desktopDataChanged(vd->id(), data); + Q_EMIT desktopsChanged(desktops()); + }); + DBusDesktopDataStruct data{.position = vd->x11DesktopNumber() - 1, .id = vd->id(), .name = vd->name()}; + Q_EMIT desktopCreated(vd->id(), data); + Q_EMIT desktopsChanged(desktops()); + }); + connect(m_manager, &VirtualDesktopManager::desktopRemoved, this, [this](VirtualDesktop *vd) { + Q_EMIT desktopRemoved(vd->id()); + Q_EMIT desktopsChanged(desktops()); + }); +} + +uint VirtualDesktopManagerDBusInterface::count() const +{ + return m_manager->count(); +} + +void VirtualDesktopManagerDBusInterface::setRows(uint rows) +{ + if (static_cast(m_manager->grid().height()) == rows) { + return; + } + + m_manager->setRows(rows); + m_manager->save(); +} + +uint VirtualDesktopManagerDBusInterface::rows() const +{ + return m_manager->rows(); +} + +void VirtualDesktopManagerDBusInterface::setCurrent(const QString &id) +{ + if (m_manager->currentDesktop()->id() == id) { + return; + } + + auto *vd = m_manager->desktopForId(id); + if (vd) { + m_manager->setCurrent(vd); + } +} + +QString VirtualDesktopManagerDBusInterface::current() const +{ + return m_manager->currentDesktop()->id(); +} + +void VirtualDesktopManagerDBusInterface::setNavigationWrappingAround(bool wraps) +{ + if (m_manager->isNavigationWrappingAround() == wraps) { + return; + } + + m_manager->setNavigationWrappingAround(wraps); +} + +bool VirtualDesktopManagerDBusInterface::isNavigationWrappingAround() const +{ + return m_manager->isNavigationWrappingAround(); +} + +DBusDesktopDataVector VirtualDesktopManagerDBusInterface::desktops() const +{ + const auto desks = m_manager->desktops(); + DBusDesktopDataVector desktopVect; + desktopVect.reserve(m_manager->count()); + + std::transform(desks.constBegin(), desks.constEnd(), + std::back_inserter(desktopVect), + [](const VirtualDesktop *vd) { + return DBusDesktopDataStruct{.position = vd->x11DesktopNumber() - 1, .id = vd->id(), .name = vd->name()}; + }); + + return desktopVect; +} + +void VirtualDesktopManagerDBusInterface::createDesktop(uint position, const QString &name) +{ + m_manager->createVirtualDesktop(position, name); +} + +void VirtualDesktopManagerDBusInterface::setDesktopName(const QString &id, const QString &name) +{ + VirtualDesktop *vd = m_manager->desktopForId(id); + if (!vd) { + return; + } + + vd->setName(name); +} + +void VirtualDesktopManagerDBusInterface::removeDesktop(const QString &id) +{ + m_manager->removeVirtualDesktop(id); +} + +PluginManagerDBusInterface::PluginManagerDBusInterface(PluginManager *manager) + : QObject(manager) + , m_manager(manager) +{ + new PluginsAdaptor(this); + + QDBusConnection::sessionBus().registerObject(QStringLiteral("/Plugins"), + QStringLiteral("org.kde.KWin.Plugins"), + this); +} + +QStringList PluginManagerDBusInterface::loadedPlugins() const +{ + return m_manager->loadedPlugins(); +} + +QStringList PluginManagerDBusInterface::availablePlugins() const +{ + return m_manager->availablePlugins(); +} + +bool PluginManagerDBusInterface::LoadPlugin(const QString &name) +{ + return m_manager->loadPlugin(name); +} + +void PluginManagerDBusInterface::UnloadPlugin(const QString &name) +{ + m_manager->unloadPlugin(name); +} + +} // namespace + +#include "moc_dbusinterface.cpp" diff --git a/local/recipes/kde/kwin/source/src/dbusinterface.h b/local/recipes/kde/kwin/source/src/dbusinterface.h new file mode 100644 index 0000000000..b128e2270b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/dbusinterface.h @@ -0,0 +1,270 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +#include "virtualdesktopsdbustypes.h" + +namespace KWin +{ + +class Compositor; +class PluginManager; +class VirtualDesktopManager; + +/** + * @brief This class is a wrapper for the org.kde.KWin D-Bus interface. + * + * The main purpose of this class is to be exported on the D-Bus as object /KWin. + * It is a pure wrapper to provide the deprecated D-Bus methods which have been + * removed from Workspace which used to implement the complete D-Bus interface. + * + * Nowadays the D-Bus interfaces are distributed, parts of it are exported on + * /Compositor, parts on /Effects and parts on /KWin. The implementation in this + * class just delegates the method calls to the actual implementation in one of the + * three singletons. + * + * @author Martin Gräßlin + */ +class DBusInterface : public QObject, protected QDBusContext +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.KWin") +public: + explicit DBusInterface(QObject *parent); + ~DBusInterface() override; + +public: // PROPERTIES + Q_PROPERTY(bool showingDesktop READ showingDesktop NOTIFY showingDesktopChanged) + bool showingDesktop() const; + +public Q_SLOTS: // METHODS + int currentDesktop(); + Q_NOREPLY void killWindow(); + void nextDesktop(); + void previousDesktop(); + Q_NOREPLY void reconfigure(); + bool setCurrentDesktop(int desktop); + QString supportInformation(); + QString activeOutputName(); + Q_NOREPLY void showDebugConsole(); + + /** + * Instructs kwin_wayland to restart itself. + * + * This acts as an implementation detail of: kwin_wayland --replace + */ + Q_NOREPLY void replace(); + + /** + * Allows the user to pick a window and get info on it. + * + * When called the user's mouse cursor will become a targeting reticule. + * On clicking a window with the target a map will be returned + * with various information about the picked window, such as: + * height, width, minimized, fullscreen, etc. + */ + QVariantMap queryWindowInfo(); + + /** + * Returns a map with information about the window. + * + * The map includes entries such as position, size, status, and more. + * + * @param uuid is a QUuid from Window::internalId(). + */ + QVariantMap getWindowInfo(const QString &uuid); + + Q_NOREPLY void showDesktop(bool show); + +Q_SIGNALS: + void showingDesktopChanged(bool showing); + +private Q_SLOTS: + void onShowingDesktopChanged(bool show, bool /*animated*/); + +private: + QString m_serviceName; + QDBusMessage m_replyQueryWindowInfo; +}; + +class CompositorDBusInterface : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.Compositing") + + /** + * @brief Whether the Compositor is active. That is a Scene is present and the Compositor is + * not shutting down itself. + */ + Q_PROPERTY(bool active READ isActive) + + /** + * @brief Whether compositing is possible. Mostly means whether the required X extensions + * are available. + */ + Q_PROPERTY(bool compositingPossible READ isCompositingPossible) + + /** + * @brief The reason why compositing is not possible. Empty String if compositing is possible. + */ + Q_PROPERTY(QString compositingNotPossibleReason READ compositingNotPossibleReason) + + /** + * @brief Whether OpenGL has failed badly in the past (crash) and is considered as broken. + */ + Q_PROPERTY(bool openGLIsBroken READ isOpenGLBroken) + + /** + * The type of the currently used Scene: + * @li @c none No Compositing + * @li @c gl1 OpenGL 1 + * @li @c gl2 OpenGL 2 + * @li @c gles OpenGL ES 2 + */ + Q_PROPERTY(QString compositingType READ compositingType) + + /** + * @brief All currently supported OpenGLPlatformInterfaces. + * + * Possible values: + * @li egl + * + * Values depend on operation mode and compile time options. + */ + Q_PROPERTY(QStringList supportedOpenGLPlatformInterfaces READ supportedOpenGLPlatformInterfaces) + + Q_PROPERTY(bool platformRequiresCompositing READ platformRequiresCompositing) +public: + explicit CompositorDBusInterface(Compositor *parent); + ~CompositorDBusInterface() override = default; + + bool isActive() const; + bool isCompositingPossible() const; + QString compositingNotPossibleReason() const; + bool isOpenGLBroken() const; + QString compositingType() const; + QStringList supportedOpenGLPlatformInterfaces() const; + bool platformRequiresCompositing() const; + +public Q_SLOTS: + /** + * @brief Used by Compositing KCM after settings change. + * + * On signal Compositor reloads settings and restarts. + */ + void reinitialize(); + +Q_SIGNALS: + void compositingToggled(bool active); + +private: + Compositor *m_compositor; +}; + +// TODO: disable all of this in case of kiosk? + +class VirtualDesktopManagerDBusInterface : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.KWin.VirtualDesktopManager") + + /** + * The number of virtual desktops currently available. + * The ids of the virtual desktops are in the range [1, VirtualDesktopManager::maximum()]. + */ + Q_PROPERTY(uint count READ count NOTIFY countChanged) + + /** + * The number of rows the virtual desktops will be laid out in + */ + Q_PROPERTY(uint rows READ rows WRITE setRows NOTIFY rowsChanged) + + /** + * The id of the virtual desktop which is currently in use. + */ + Q_PROPERTY(QString current READ current WRITE setCurrent NOTIFY currentChanged) + + /** + * Whether navigation in the desktop layout wraps around at the borders. + */ + Q_PROPERTY(bool navigationWrappingAround READ isNavigationWrappingAround WRITE setNavigationWrappingAround NOTIFY navigationWrappingAroundChanged) + + /** + * list of key/value pairs which every one of them is representing a desktop + */ + Q_PROPERTY(KWin::DBusDesktopDataVector desktops READ desktops NOTIFY desktopsChanged); + +public: + VirtualDesktopManagerDBusInterface(VirtualDesktopManager *parent); + ~VirtualDesktopManagerDBusInterface() override = default; + + uint count() const; + + void setRows(uint rows); + uint rows() const; + + void setCurrent(const QString &id); + QString current() const; + + void setNavigationWrappingAround(bool wraps); + bool isNavigationWrappingAround() const; + + KWin::DBusDesktopDataVector desktops() const; + +Q_SIGNALS: + void countChanged(uint count); + void rowsChanged(uint rows); + void currentChanged(const QString &id); + void navigationWrappingAroundChanged(bool wraps); + void desktopsChanged(KWin::DBusDesktopDataVector); + void desktopDataChanged(const QString &id, KWin::DBusDesktopDataStruct); + void desktopCreated(const QString &id, KWin::DBusDesktopDataStruct); + void desktopRemoved(const QString &id); + +public Q_SLOTS: + /** + * Create a desktop with a new name at a given position + * note: the position starts from 1 + */ + void createDesktop(uint position, const QString &name); + void setDesktopName(const QString &id, const QString &name); + void removeDesktop(const QString &id); + +private: + VirtualDesktopManager *m_manager; +}; + +class PluginManagerDBusInterface : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.KWin.Plugins") + + Q_PROPERTY(QStringList LoadedPlugins READ loadedPlugins) + Q_PROPERTY(QStringList AvailablePlugins READ availablePlugins) + +public: + explicit PluginManagerDBusInterface(PluginManager *manager); + + QStringList loadedPlugins() const; + QStringList availablePlugins() const; + +public Q_SLOTS: + bool LoadPlugin(const QString &name); + void UnloadPlugin(const QString &name); + +private: + PluginManager *m_manager; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/debug_console.cpp b/local/recipes/kde/kwin/source/src/debug_console.cpp new file mode 100644 index 0000000000..428d3baf27 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/debug_console.cpp @@ -0,0 +1,1865 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "debug_console.h" +#include "compositor.h" +#include "core/inputdevice.h" +#include "effect/effecthandler.h" +#include "input_event.h" +#include "internalwindow.h" +#include "keyboard_input.h" +#include "main.h" +#include "opengl/eglbackend.h" +#include "opengl/glplatform.h" +#include "opengl/glutils.h" +#include "scene/workspacescene.h" +#include "tiles/customtile.h" +#include "tiles/tile.h" +#include "utils/filedescriptor.h" +#include "utils/pipe.h" +#include "virtualdesktops.h" +#include "wayland/abstract_data_source.h" +#include "wayland/clientconnection.h" +#include "wayland/datacontrolsource_v1.h" +#include "wayland/datasource.h" +#include "wayland/display.h" +#include "wayland/primaryselectionsource_v1.h" +#include "wayland/seat.h" +#include "wayland/surface.h" +#include "wayland_server.h" +#include "waylandwindow.h" +#include "workspace.h" +#include "xkb.h" +#include +#if KWIN_BUILD_X11 +#include "x11window.h" +#endif + +#include "ui_debug_console.h" + +// frameworks +#include +// Qt +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +// xkb +#include + +#include +#include +#include + +namespace KWin +{ + +static QString tableHeaderRow(const QString &title) +{ + return QStringLiteral("%1").arg(title); +} + +template +static QString tableRow(const QString &title, const T &argument) +{ + return QStringLiteral("%1%2").arg(title).arg(argument); +} + +static QString timestampRow(std::chrono::microseconds timestamp) +{ + return tableRow(i18n("Timestamp"), std::chrono::duration_cast(timestamp).count()); +} + +static QString timestampRowUsec(std::chrono::microseconds timestamp) +{ + return tableRow(i18n("Timestamp (µsec)"), timestamp.count()); +} + +static QString buttonToString(Qt::MouseButton button) +{ + switch (button) { + case Qt::LeftButton: + return i18nc("A mouse button", "Left"); + case Qt::RightButton: + return i18nc("A mouse button", "Right"); + case Qt::MiddleButton: + return i18nc("A mouse button", "Middle"); + case Qt::BackButton: + return i18nc("A mouse button", "Back"); + case Qt::ForwardButton: + return i18nc("A mouse button", "Forward"); + case Qt::TaskButton: + return i18nc("A mouse button", "Task"); + case Qt::ExtraButton4: + return i18nc("A mouse button", "Extra Button 4"); + case Qt::ExtraButton5: + return i18nc("A mouse button", "Extra Button 5"); + case Qt::ExtraButton6: + return i18nc("A mouse button", "Extra Button 6"); + case Qt::ExtraButton7: + return i18nc("A mouse button", "Extra Button 7"); + case Qt::ExtraButton8: + return i18nc("A mouse button", "Extra Button 8"); + case Qt::ExtraButton9: + return i18nc("A mouse button", "Extra Button 9"); + case Qt::ExtraButton10: + return i18nc("A mouse button", "Extra Button 10"); + case Qt::ExtraButton11: + return i18nc("A mouse button", "Extra Button 11"); + case Qt::ExtraButton12: + return i18nc("A mouse button", "Extra Button 12"); + case Qt::ExtraButton13: + return i18nc("A mouse button", "Extra Button 13"); + case Qt::ExtraButton14: + return i18nc("A mouse button", "Extra Button 14"); + case Qt::ExtraButton15: + return i18nc("A mouse button", "Extra Button 15"); + case Qt::ExtraButton16: + return i18nc("A mouse button", "Extra Button 16"); + case Qt::ExtraButton17: + return i18nc("A mouse button", "Extra Button 17"); + case Qt::ExtraButton18: + return i18nc("A mouse button", "Extra Button 18"); + case Qt::ExtraButton19: + return i18nc("A mouse button", "Extra Button 19"); + case Qt::ExtraButton20: + return i18nc("A mouse button", "Extra Button 20"); + case Qt::ExtraButton21: + return i18nc("A mouse button", "Extra Button 21"); + case Qt::ExtraButton22: + return i18nc("A mouse button", "Extra Button 22"); + case Qt::ExtraButton23: + return i18nc("A mouse button", "Extra Button 23"); + case Qt::ExtraButton24: + return i18nc("A mouse button", "Extra Button 24"); + default: + return QString(); + } +} + +static QString deviceRow(InputDevice *device) +{ + if (!device) { + return tableRow(i18n("Input Device"), i18nc("The input device of the event is not known", "Unknown")); + } + return tableRow(i18n("Input Device"), QStringLiteral("%1 (%2)").arg(device->name(), device->sysPath())); +} + +static QString buttonsToString(Qt::MouseButtons buttons) +{ + QString ret; + for (uint i = 1; i < Qt::ExtraButton24; i = i << 1) { + if (buttons & i) { + ret.append(buttonToString(Qt::MouseButton(uint(buttons) & i))); + ret.append(QStringLiteral(" ")); + } + }; + return ret.trimmed(); +} + +static const QString s_hr = QStringLiteral("
"); +static const QString s_tableStart = QStringLiteral(""); +static const QString s_tableEnd = QStringLiteral("
"); + +DebugConsoleFilter::DebugConsoleFilter(QTextEdit *textEdit) + : InputEventSpy() + , m_textEdit(textEdit) +{ +} + +DebugConsoleFilter::~DebugConsoleFilter() = default; + +void DebugConsoleFilter::pointerMotion(PointerMotionEvent *event) +{ + QString text = s_hr; + const QString timestamp = timestampRow(event->timestamp); + + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A mouse pointer motion event", "Pointer Motion"))); + text.append(deviceRow(event->device)); + text.append(timestamp); + text.append(timestampRowUsec(event->timestamp)); + if (!event->delta.isNull()) { + text.append(tableRow(i18nc("The relative mouse movement", "Delta"), + QStringLiteral("%1/%2").arg(event->delta.x()).arg(event->delta.y()))); + } + if (!event->deltaUnaccelerated.isNull()) { + text.append(tableRow(i18nc("The relative mouse movement", "Delta (not accelerated)"), + QStringLiteral("%1/%2").arg(event->deltaUnaccelerated.x()).arg(event->deltaUnaccelerated.y()))); + } + text.append(tableRow(i18nc("The global mouse pointer position", "Global Position"), QStringLiteral("%1/%2").arg(event->position.x()).arg(event->position.y()))); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::pointerButton(PointerButtonEvent *event) +{ + QString text = s_hr; + const QString timestamp = timestampRow(event->timestamp); + + text.append(s_tableStart); + if (event->state == PointerButtonState::Pressed) { + text.append(tableHeaderRow(i18nc("A mouse pointer button press event", "Pointer Button Press"))); + text.append(deviceRow(event->device)); + text.append(timestamp); + text.append(tableRow(i18nc("A button in a mouse press/release event", "Button"), buttonToString(event->button))); + text.append(tableRow(i18nc("A button in a mouse press/release event", "Native Button code"), event->nativeButton)); + text.append(tableRow(i18nc("All currently pressed buttons in a mouse press/release event", "Pressed Buttons"), buttonsToString(event->buttons))); + } else { + text.append(tableHeaderRow(i18nc("A mouse pointer button release event", "Pointer Button Release"))); + text.append(deviceRow(event->device)); + text.append(timestamp); + text.append(tableRow(i18nc("A button in a mouse press/release event", "Button"), buttonToString(event->button))); + text.append(tableRow(i18nc("A button in a mouse press/release event", "Native Button code"), event->nativeButton)); + text.append(tableRow(i18nc("All currently pressed buttons in a mouse press/release event", "Pressed Buttons"), buttonsToString(event->buttons))); + } + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::pointerAxis(PointerAxisEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A mouse pointer axis (wheel) event", "Pointer Axis"))); + text.append(deviceRow(event->device)); + text.append(timestampRow(event->timestamp)); + text.append(tableRow(i18nc("The orientation of a pointer axis event", "Orientation"), + event->orientation == Qt::Horizontal ? i18nc("An orientation of a pointer axis event", "Horizontal") + : i18nc("An orientation of a pointer axis event", "Vertical"))); + text.append(tableRow(i18nc("The angle delta of a pointer axis event", "Delta"), event->delta)); + text.append(tableRow(i18nc("The normalized V120 angle delta of a pointer axis event. V120 is a technical term and shouldn't be changed.", "Delta (V120)"), event->deltaV120)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::keyboardKey(KeyboardKeyEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + + switch (event->state) { + case KeyboardKeyState::Repeated: + case KeyboardKeyState::Pressed: + text.append(tableHeaderRow(i18nc("A key press event", "Key Press"))); + break; + case KeyboardKeyState::Released: + text.append(tableHeaderRow(i18nc("A key release event", "Key Release"))); + break; + default: + break; + } + text.append(deviceRow(event->device)); + auto modifiersToString = [event] { + QString ret; + if (event->modifiers.testFlag(Qt::ShiftModifier)) { + ret.append(i18nc("A keyboard modifier", "Shift")); + ret.append(QStringLiteral(" ")); + } + if (event->modifiers.testFlag(Qt::ControlModifier)) { + ret.append(i18nc("A keyboard modifier", "Control")); + ret.append(QStringLiteral(" ")); + } + if (event->modifiers.testFlag(Qt::AltModifier)) { + ret.append(i18nc("A keyboard modifier", "Alt")); + ret.append(QStringLiteral(" ")); + } + if (event->modifiers.testFlag(Qt::MetaModifier)) { + ret.append(i18nc("A keyboard modifier", "Meta")); + ret.append(QStringLiteral(" ")); + } + if (event->modifiers.testFlag(Qt::KeypadModifier)) { + ret.append(i18nc("A keyboard modifier", "Keypad")); + ret.append(QStringLiteral(" ")); + } + if (event->modifiers.testFlag(Qt::GroupSwitchModifier)) { + ret.append(i18nc("A keyboard modifier", "Group-switch")); + ret.append(QStringLiteral(" ")); + } + return ret; + }; + text.append(timestampRow(event->timestamp)); + text.append(tableRow(i18nc("Whether the event is an automatic key repeat", "Repeat"), event->state == KeyboardKeyState::Repeated)); + + const auto keyMetaObject = Qt::qt_getEnumMetaObject(Qt::Key()); + const auto enumerator = keyMetaObject->enumerator(keyMetaObject->indexOfEnumerator("Key")); + text.append(tableRow(i18nc("The code reported by the kernel", "Keycode"), event->nativeScanCode)); + text.append(tableRow(i18nc("Key according to Qt", "Qt::Key code"), enumerator.valueToKey(event->key))); + text.append(tableRow(i18nc("The translated code to an Xkb symbol", "Xkb symbol"), event->nativeVirtualKey)); + text.append(tableRow(i18nc("The translated code interpreted as text", "Utf8"), event->text)); + text.append(tableRow(i18nc("The currently active modifiers", "Modifiers"), modifiersToString())); + + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::touchDown(TouchDownEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A touch down event", "Touch down"))); + text.append(timestampRow(event->time)); + text.append(tableRow(i18nc("The id of the touch point in the touch event", "Point identifier"), event->id)); + text.append(tableRow(i18nc("The global position of the touch point", "Global position"), + QStringLiteral("%1/%2").arg(event->pos.x()).arg(event->pos.y()))); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::touchMotion(TouchMotionEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A touch motion event", "Touch Motion"))); + text.append(timestampRow(event->time)); + text.append(tableRow(i18nc("The id of the touch point in the touch event", "Point identifier"), event->id)); + text.append(tableRow(i18nc("The global position of the touch point", "Global position"), + QStringLiteral("%1/%2").arg(event->pos.x()).arg(event->pos.y()))); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::touchUp(TouchUpEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A touch up event", "Touch Up"))); + text.append(timestampRow(event->time)); + text.append(tableRow(i18nc("The id of the touch point in the touch event", "Point identifier"), event->id)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::pinchGestureBegin(PointerPinchGestureBeginEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A pinch gesture is started", "Pinch start"))); + text.append(timestampRow(event->time)); + text.append(tableRow(i18nc("Number of fingers in this pinch gesture", "Finger count"), event->fingerCount)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::pinchGestureUpdate(PointerPinchGestureUpdateEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A pinch gesture is updated", "Pinch update"))); + text.append(timestampRow(event->time)); + text.append(tableRow(i18nc("Current scale in pinch gesture", "Scale"), event->scale)); + text.append(tableRow(i18nc("Current angle in pinch gesture", "Angle delta"), event->angleDelta)); + text.append(tableRow(i18nc("Current delta in pinch gesture", "Delta x"), event->delta.x())); + text.append(tableRow(i18nc("Current delta in pinch gesture", "Delta y"), event->delta.y())); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::pinchGestureEnd(PointerPinchGestureEndEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A pinch gesture ended", "Pinch end"))); + text.append(timestampRow(event->time)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::pinchGestureCancelled(PointerPinchGestureCancelEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A pinch gesture got cancelled", "Pinch cancelled"))); + text.append(timestampRow(event->time)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::swipeGestureBegin(PointerSwipeGestureBeginEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A swipe gesture is started", "Swipe start"))); + text.append(timestampRow(event->time)); + text.append(tableRow(i18nc("Number of fingers in this swipe gesture", "Finger count"), event->fingerCount)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::swipeGestureUpdate(PointerSwipeGestureUpdateEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A swipe gesture is updated", "Swipe update"))); + text.append(timestampRow(event->time)); + text.append(tableRow(i18nc("Current delta in swipe gesture", "Delta x"), event->delta.x())); + text.append(tableRow(i18nc("Current delta in swipe gesture", "Delta y"), event->delta.y())); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::swipeGestureEnd(PointerSwipeGestureEndEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A swipe gesture ended", "Swipe end"))); + text.append(timestampRow(event->time)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::swipeGestureCancelled(PointerSwipeGestureCancelEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A swipe gesture got cancelled", "Swipe cancelled"))); + text.append(timestampRow(event->time)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::holdGestureBegin(PointerHoldGestureBeginEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A hold gesture is started", "Hold start"))); + text.append(timestampRow(event->time)); + text.append(tableRow(i18nc("Number of fingers in this hold gesture", "Finger count"), event->fingerCount)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::holdGestureEnd(PointerHoldGestureEndEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A hold gesture ended", "Hold end"))); + text.append(timestampRow(event->time)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::holdGestureCancelled(PointerHoldGestureCancelEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A hold gesture got cancelled", "Hold cancelled"))); + text.append(timestampRow(event->time)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::switchEvent(SwitchEvent *event) +{ + QString text = s_hr; + text.append(s_tableStart); + text.append(tableHeaderRow(i18nc("A hardware switch (e.g. notebook lid) got toggled", "Switch toggled"))); + text.append(timestampRow(event->timestamp)); + text.append(timestampRowUsec(event->timestamp)); + text.append(deviceRow(event->device)); + QString switchName; + if (event->device->isLidSwitch()) { + switchName = i18nc("Name of a hardware switch", "Notebook lid"); + } else if (event->device->isTabletModeSwitch()) { + switchName = i18nc("Name of a hardware switch", "Tablet mode"); + } + text.append(tableRow(i18nc("A hardware switch", "Switch"), switchName)); + QString switchState; + switch (event->state) { + case SwitchState::Off: + switchState = i18nc("The hardware switch got turned off", "Off"); + break; + case SwitchState::On: + switchState = i18nc("The hardware switch got turned on", "On"); + break; + default: + Q_UNREACHABLE(); + } + text.append(tableRow(i18nc("State of a hardware switch (on/off)", "State"), switchState)); + text.append(s_tableEnd); + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::tabletToolProximityEvent(TabletToolProximityEvent *event) +{ + QString text = s_hr + s_tableStart + tableHeaderRow(i18n("Tablet Tool Proximity")) + + timestampRow(event->timestamp) + + timestampRowUsec(event->timestamp) + + deviceRow(event->device) + + tableRow(i18n("Proximity"), event->type == TabletToolProximityEvent::EnterProximity ? i18n("In") : i18n("Out")) + + tableRow(i18n("Position"), + QStringLiteral("%1,%2").arg(QString::number(event->position.x()), QString::number(event->position.y()))) + + tableRow(i18n("Tilt"), + QStringLiteral("%1,%2").arg(event->xTilt).arg(event->yTilt)) + + tableRow(i18n("Rotation"), QString::number(event->rotation)) + + tableRow(i18n("Distance"), QString::number(event->distance)) + + s_tableEnd; + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::tabletToolAxisEvent(TabletToolAxisEvent *event) +{ + QString text = s_hr + s_tableStart + tableHeaderRow(i18n("Tablet Tool Axis")) + + timestampRow(event->timestamp) + + timestampRowUsec(event->timestamp) + + deviceRow(event->device) + + tableRow(i18n("Position"), + QStringLiteral("%1,%2").arg(QString::number(event->position.x()), QString::number(event->position.y()))) + + tableRow(i18n("Tilt"), + QStringLiteral("%1,%2").arg(event->xTilt).arg(event->yTilt)) + + tableRow(i18n("Rotation"), QString::number(event->rotation)) + + tableRow(i18n("Pressure"), QString::number(event->pressure)) + + tableRow(i18n("Distance"), QString::number(event->distance)) + + s_tableEnd; + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::tabletToolTipEvent(TabletToolTipEvent *event) +{ + QString text = s_hr + s_tableStart + tableHeaderRow(i18n("Tablet Tool Tip")) + + timestampRow(event->timestamp) + + timestampRowUsec(event->timestamp) + + deviceRow(event->device) + + tableRow(i18n("Tip"), event->type == TabletToolTipEvent::Press ? i18n("Down") : i18n("Up")) + + tableRow(i18n("Position"), + QStringLiteral("%1,%2").arg(QString::number(event->position.x()), QString::number(event->position.y()))) + + tableRow(i18n("Tilt"), + QStringLiteral("%1,%2").arg(event->xTilt).arg(event->yTilt)) + + tableRow(i18n("Rotation"), QString::number(event->rotation)) + + tableRow(i18n("Pressure"), QString::number(event->pressure)) + + tableRow(i18n("Slider Position"), QString::number(event->sliderPosition)) + + s_tableEnd; + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::tabletToolButtonEvent(TabletToolButtonEvent *event) +{ + QString text = s_hr + s_tableStart + tableHeaderRow(i18n("Tablet Tool Button")) + + deviceRow(event->device) + + tableRow(i18n("Button"), event->button) + + tableRow(i18n("Pressed"), event->pressed) + + tableRow(i18n("Tablet"), event->device->name()) + + timestampRow(event->time) + + s_tableEnd; + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::tabletPadButtonEvent(TabletPadButtonEvent *event) +{ + QString text = s_hr + s_tableStart + + tableHeaderRow(i18n("Tablet Pad Button")) + + deviceRow(event->device) + + tableRow(i18n("Button"), event->button) + + tableRow(i18n("Pressed"), event->pressed) + + tableRow(i18n("Group"), event->group) + + tableRow(i18n("Mode"), event->mode) + + tableRow(i18n("Is Mode Switch"), event->isModeSwitch) + + tableRow(i18n("Tablet"), event->device->name()) + + timestampRow(event->time) + + s_tableEnd; + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::tabletPadStripEvent(TabletPadStripEvent *event) +{ + QString text = s_hr + s_tableStart + tableHeaderRow(i18n("Tablet Pad Strip")) + + deviceRow(event->device) + + tableRow(i18n("Number"), event->number) + + tableRow(i18n("Position"), event->position) + + tableRow(i18n("isFinger"), event->isFinger) + + tableRow(i18n("Group"), event->group) + + tableRow(i18n("Mode"), event->mode) + + tableRow(i18n("Tablet"), event->device->name()) + + timestampRow(event->time) + + s_tableEnd; + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::tabletPadRingEvent(TabletPadRingEvent *event) +{ + QString text = s_hr + s_tableStart + tableHeaderRow(i18n("Tablet Pad Ring")) + + deviceRow(event->device) + + tableRow(i18n("Number"), event->number) + + tableRow(i18n("Position"), event->position) + + tableRow(i18n("isFinger"), event->isFinger) + + tableRow(i18n("Group"), event->group) + + tableRow(i18n("Mode"), event->mode) + + tableRow(i18n("Tablet"), event->device->name()) + + timestampRow(event->time) + + s_tableEnd; + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +void DebugConsoleFilter::tabletPadDialEvent(TabletPadDialEvent *event) +{ + QString text = s_hr + s_tableStart + tableHeaderRow(i18n("Tablet Pad Dial")) + + deviceRow(event->device) + + tableRow(i18n("Number"), event->number) + + tableRow(i18n("Delta"), event->delta) + + tableRow(i18n("Tablet"), event->device->name()) + + timestampRow(event->time) + + s_tableEnd; + + m_textEdit->insertHtml(text); + m_textEdit->ensureCursorVisible(); +} + +static QString sourceString(const AbstractDataSource *const source) +{ + if (!source) { + return QString(); + } + + if (source->client()) { + const QString executable = ClientConnection::get(source->client())->executablePath(); + + if (auto dataSource = qobject_cast(source)) { + return QStringLiteral("wl_data_source@%1 of %2").arg(wl_resource_get_id(dataSource->resource())).arg(executable); + } else if (qobject_cast(source)) { + return QStringLiteral("zwp_primary_selection_source_v1 of %1").arg(executable); + } else if (qobject_cast(source)) { + return QStringLiteral("data control by %1").arg(executable); + } + + return QStringLiteral("unknown source of").arg(executable); + } + + return QStringLiteral("%1(0x%2)").arg(source->metaObject()->className()).arg(qulonglong(source), 0, 16); +} + +DebugConsole::DebugConsole() + : QWidget() + , m_ui(new Ui::DebugConsole) +{ + setAttribute(Qt::WA_ShowWithoutActivating); + m_ui->setupUi(this); + + auto windowsModel = new DebugConsoleModel(this); + QSortFilterProxyModel *proxyWindowsModel = new QSortFilterProxyModel(this); + proxyWindowsModel->setSourceModel(windowsModel); + m_ui->windowsView->setModel(proxyWindowsModel); + m_ui->windowsView->sortByColumn(0, Qt::AscendingOrder); + m_ui->windowsView->header()->setSortIndicatorShown(true); + m_ui->windowsView->setItemDelegate(new DebugConsoleDelegate(this)); + + m_ui->clipboardContent->setModel(new DataSourceModel(this)); + m_ui->primaryContent->setModel(new DataSourceModel(this)); + m_ui->inputDevicesView->setModel(new InputDeviceModel(this)); + m_ui->inputDevicesView->setItemDelegate(new DebugConsoleDelegate(this)); + m_ui->tabWidget->setTabIcon(0, QIcon::fromTheme(QStringLiteral("view-list-tree"))); + + m_ui->tabWidget->addTab(new DebugConsoleEffectsTab(), i18nc("@label", "Effects")); + + connect(m_ui->tabWidget, &QTabWidget::currentChanged, this, [this](int index) { + // delay creation of input event filter until the tab is selected + if (index == m_ui->tabWidget->indexOf(m_ui->input) && !m_inputFilter) { + m_inputFilter = std::make_unique(m_ui->inputTextEdit); + input()->installInputEventSpy(m_inputFilter.get()); + } + if (index == m_ui->tabWidget->indexOf(m_ui->keyboard)) { + updateKeyboardTab(); + connect(input(), &InputRedirection::keyStateChanged, this, &DebugConsole::updateKeyboardTab); + } + if (index == m_ui->tabWidget->indexOf(m_ui->clipboard)) { + static_cast(m_ui->clipboardContent->model())->setSource(waylandServer()->seat()->selection()); + m_ui->clipboardSource->setText(sourceString(waylandServer()->seat()->selection())); + connect(waylandServer()->seat(), &SeatInterface::selectionChanged, this, [this](AbstractDataSource *source) { + static_cast(m_ui->clipboardContent->model())->setSource(source); + m_ui->clipboardSource->setText(sourceString(source)); + }); + static_cast(m_ui->primaryContent->model())->setSource(waylandServer()->seat()->primarySelection()); + m_ui->primarySource->setText(sourceString(waylandServer()->seat()->primarySelection())); + connect(waylandServer()->seat(), &SeatInterface::primarySelectionChanged, this, [this](AbstractDataSource *source) { + static_cast(m_ui->primaryContent->model())->setSource(source); + m_ui->primarySource->setText(sourceString(source)); + }); + } + }); + + initGLTab(); +} + +DebugConsole::~DebugConsole() = default; + +void DebugConsole::initGLTab() +{ + if (!effects || !effects->isOpenGLCompositing()) { + m_ui->noOpenGLLabel->setVisible(true); + m_ui->glInfoScrollArea->setVisible(false); + return; + } + const auto gl = Compositor::self()->scene()->openglContext()->glPlatform(); + m_ui->noOpenGLLabel->setVisible(false); + m_ui->glInfoScrollArea->setVisible(true); + m_ui->glVendorStringLabel->setText(QString::fromLocal8Bit(gl->glVendorString())); + m_ui->glRendererStringLabel->setText(QString::fromLocal8Bit(gl->glRendererString())); + m_ui->glVersionStringLabel->setText(QString::fromLocal8Bit(gl->glVersionString())); + m_ui->glslVersionStringLabel->setText(QString::fromLocal8Bit(gl->glShadingLanguageVersionString())); + m_ui->glDriverLabel->setText(GLPlatform::driverToString(gl->driver())); + m_ui->glGPULabel->setText(GLPlatform::chipClassToString(gl->chipClass())); + m_ui->glVersionLabel->setText(gl->glVersion().toString()); + m_ui->glslLabel->setText(gl->glslVersion().toString()); + + auto extensionsString = [](const auto &extensions) { + QString text = QStringLiteral("
    "); + for (auto extension : extensions) { + text.append(QStringLiteral("
  • %1
  • ").arg(QString::fromLocal8Bit(extension))); + } + text.append(QStringLiteral("
")); + return text; + }; + + const EglBackend *backend = static_cast(Compositor::self()->backend()); + m_ui->platformExtensionsLabel->setText(extensionsString(backend->extensions())); + m_ui->openGLExtensionsLabel->setText(extensionsString(backend->openglContext()->openglExtensions())); +} + +template +QString keymapComponentToString(xkb_keymap *map, const T &count, std::function f) +{ + QString text = QStringLiteral("
    "); + for (T i = 0; i < count; i++) { + text.append(QStringLiteral("
  • %1
  • ").arg(QString::fromLocal8Bit(f(map, i)))); + } + text.append(QStringLiteral("
")); + return text; +} + +template +QString stateActiveComponents(xkb_state *state, const T &count, std::function f, std::function name) +{ + QString text = QStringLiteral("
    "); + xkb_keymap *map = xkb_state_get_keymap(state); + for (T i = 0; i < count; i++) { + if (f(state, i) == 1) { + text.append(QStringLiteral("
  • %1
  • ").arg(QString::fromLocal8Bit(name(map, i)))); + } + } + text.append(QStringLiteral("
")); + return text; +} + +void DebugConsole::updateKeyboardTab() +{ + auto xkb = input()->keyboard()->xkb(); + xkb_keymap *map = xkb->keymap(); + xkb_state *state = xkb->state(); + m_ui->layoutsLabel->setText(keymapComponentToString(map, xkb_keymap_num_layouts(map), &xkb_keymap_layout_get_name)); + m_ui->currentLayoutLabel->setText(xkb_keymap_layout_get_name(map, xkb->currentLayout())); + m_ui->modifiersLabel->setText(keymapComponentToString(map, xkb_keymap_num_mods(map), &xkb_keymap_mod_get_name)); + m_ui->ledsLabel->setText(keymapComponentToString(map, xkb_keymap_num_leds(map), &xkb_keymap_led_get_name)); + m_ui->activeLedsLabel->setText(stateActiveComponents(state, xkb_keymap_num_leds(map), &xkb_state_led_index_is_active, &xkb_keymap_led_get_name)); + + using namespace std::placeholders; + auto modActive = std::bind(xkb_state_mod_index_is_active, _1, _2, XKB_STATE_MODS_EFFECTIVE); + m_ui->activeModifiersLabel->setText(stateActiveComponents(state, xkb_keymap_num_mods(map), modActive, &xkb_keymap_mod_get_name)); +} + +void DebugConsole::showEvent(QShowEvent *event) +{ + QWidget::showEvent(event); + + // delay the connection to the show event as in ctor the windowHandle returns null + connect(windowHandle(), &QWindow::visibleChanged, this, [this](bool visible) { + if (visible) { + // ignore + return; + } + deleteLater(); + }); +} + +DebugConsoleDelegate::DebugConsoleDelegate(QObject *parent) + : QStyledItemDelegate(parent) +{ +} + +DebugConsoleDelegate::~DebugConsoleDelegate() = default; + +QString DebugConsoleDelegate::displayText(const QVariant &value, const QLocale &locale) const +{ + switch (value.userType()) { + case QMetaType::QPoint: { + const QPoint p = value.toPoint(); + return QStringLiteral("%1,%2").arg(p.x()).arg(p.y()); + } + case QMetaType::QPointF: { + const QPointF p = value.toPointF(); + return QStringLiteral("%1,%2").arg(p.x()).arg(p.y()); + } + case QMetaType::QSize: { + const QSize s = value.toSize(); + return QStringLiteral("%1x%2").arg(s.width()).arg(s.height()); + } + case QMetaType::QSizeF: { + const QSizeF s = value.toSizeF(); + return QStringLiteral("%1x%2").arg(s.width()).arg(s.height()); + } + case QMetaType::QRect: { + const QRect r = value.toRect(); + return QStringLiteral("%1,%2 %3x%4").arg(r.x()).arg(r.y()).arg(r.width()).arg(r.height()); + } + case QMetaType::QRectF: { + const QRectF r = value.toRectF(); + return QStringLiteral("%1,%2 %3x%4").arg(r.x()).arg(r.y()).arg(r.width()).arg(r.height()); + } + case QMetaType::QIcon: { + const QIcon icon = value.value(); + if (!icon.isNull()) { + const auto sizes = icon.availableSizes(); + + QStringList sizesStringList; + sizesStringList.reserve(sizes.size()); + for (const auto &size : sizes) { + sizesStringList.append(QString::number(size.width())); + } + return QStringLiteral("%1 (%2)").arg(icon.name(), displayText(sizesStringList, locale)); + } else { + return QStringLiteral("null"); + } + } + default: + if (value.userType() == qMetaTypeId()) { + return value.toStringList().join(QLatin1String(", ")); + } + if (value.userType() == qMetaTypeId()) { + if (auto s = value.value()) { + return QStringLiteral("KWin::SurfaceInterface(0x%1)").arg(qulonglong(s), 0, 16); + } else { + return QStringLiteral("nullptr"); + } + } + if (value.userType() == qMetaTypeId()) { + if (auto w = value.value()) { + return w->caption() + QLatin1Char(' ') + QString::fromUtf8(w->metaObject()->className()); + } else { + return QStringLiteral("nullptr"); + } + } + if (value.userType() == qMetaTypeId()) { + if (auto output = value.value()) { + return QStringLiteral("%1 (%2@%3x)").arg(output->name(), displayText(QVariant::fromValue(output->geometry()), locale), QString::number(output->scale())); + } else { + return QStringLiteral("nullptr"); + } + } + if (value.userType() == qMetaTypeId()) { + if (auto tile = value.value()) { + const QString tileGeometry = displayText(QVariant::fromValue(tile->absoluteGeometry()), locale); + + if (auto customTile = qobject_cast(tile)) { + const QString direction = QMetaEnum::fromType().valueToKey(static_cast(customTile->layoutDirection())); + return QStringLiteral("Custom (%1: %2)").arg(direction, tileGeometry); + } else { + const QString quickTileMode = QMetaEnum::fromType().valueToKey(tile->quickTileMode()); + return QStringLiteral("%1 (%2)").arg(quickTileMode, tileGeometry); + } + } else { + return QStringLiteral("nullptr"); + } + } + if (value.userType() == qMetaTypeId()) { + if (auto desktop = value.value()) { + return desktop->name(); + } else { + return QStringLiteral("nullptr"); + } + } + if (value.userType() == qMetaTypeId>()) { + const auto desktops = value.value>(); + + QStringList result; + result.reserve(desktops.size()); + for (auto *desktop : desktops) { + result.append(displayText(QVariant::fromValue(desktop), locale)); + } + return displayText(result, locale); + } + if (value.userType() == qMetaTypeId()) { + const auto buttons = value.value(); + if (buttons == Qt::NoButton) { + return i18n("No Mouse Buttons"); + } + QStringList list; + if (buttons.testFlag(Qt::LeftButton)) { + list << i18nc("Mouse Button", "left"); + } + if (buttons.testFlag(Qt::RightButton)) { + list << i18nc("Mouse Button", "right"); + } + if (buttons.testFlag(Qt::MiddleButton)) { + list << i18nc("Mouse Button", "middle"); + } + if (buttons.testFlag(Qt::BackButton)) { + list << i18nc("Mouse Button", "back"); + } + if (buttons.testFlag(Qt::ForwardButton)) { + list << i18nc("Mouse Button", "forward"); + } + if (buttons.testFlag(Qt::ExtraButton1)) { + list << i18nc("Mouse Button", "extra 1"); + } + if (buttons.testFlag(Qt::ExtraButton2)) { + list << i18nc("Mouse Button", "extra 2"); + } + if (buttons.testFlag(Qt::ExtraButton3)) { + list << i18nc("Mouse Button", "extra 3"); + } + if (buttons.testFlag(Qt::ExtraButton4)) { + list << i18nc("Mouse Button", "extra 4"); + } + if (buttons.testFlag(Qt::ExtraButton5)) { + list << i18nc("Mouse Button", "extra 5"); + } + if (buttons.testFlag(Qt::ExtraButton6)) { + list << i18nc("Mouse Button", "extra 6"); + } + if (buttons.testFlag(Qt::ExtraButton7)) { + list << i18nc("Mouse Button", "extra 7"); + } + if (buttons.testFlag(Qt::ExtraButton8)) { + list << i18nc("Mouse Button", "extra 8"); + } + if (buttons.testFlag(Qt::ExtraButton9)) { + list << i18nc("Mouse Button", "extra 9"); + } + if (buttons.testFlag(Qt::ExtraButton10)) { + list << i18nc("Mouse Button", "extra 10"); + } + if (buttons.testFlag(Qt::ExtraButton11)) { + list << i18nc("Mouse Button", "extra 11"); + } + if (buttons.testFlag(Qt::ExtraButton12)) { + list << i18nc("Mouse Button", "extra 12"); + } + if (buttons.testFlag(Qt::ExtraButton13)) { + list << i18nc("Mouse Button", "extra 13"); + } + if (buttons.testFlag(Qt::ExtraButton14)) { + list << i18nc("Mouse Button", "extra 14"); + } + if (buttons.testFlag(Qt::ExtraButton15)) { + list << i18nc("Mouse Button", "extra 15"); + } + if (buttons.testFlag(Qt::ExtraButton16)) { + list << i18nc("Mouse Button", "extra 16"); + } + if (buttons.testFlag(Qt::ExtraButton17)) { + list << i18nc("Mouse Button", "extra 17"); + } + if (buttons.testFlag(Qt::ExtraButton18)) { + list << i18nc("Mouse Button", "extra 18"); + } + if (buttons.testFlag(Qt::ExtraButton19)) { + list << i18nc("Mouse Button", "extra 19"); + } + if (buttons.testFlag(Qt::ExtraButton20)) { + list << i18nc("Mouse Button", "extra 20"); + } + if (buttons.testFlag(Qt::ExtraButton21)) { + list << i18nc("Mouse Button", "extra 21"); + } + if (buttons.testFlag(Qt::ExtraButton22)) { + list << i18nc("Mouse Button", "extra 22"); + } + if (buttons.testFlag(Qt::ExtraButton23)) { + list << i18nc("Mouse Button", "extra 23"); + } + if (buttons.testFlag(Qt::ExtraButton24)) { + list << i18nc("Mouse Button", "extra 24"); + } + if (buttons.testFlag(Qt::TaskButton)) { + list << i18nc("Mouse Button", "task"); + } + return list.join(QStringLiteral(", ")); + } + if (value.userType() == qMetaTypeId()) { + const Rect r = value.value(); + return QStringLiteral("%1,%2 %3x%4").arg(r.x()).arg(r.y()).arg(r.width()).arg(r.height()); + } + if (value.userType() == qMetaTypeId()) { + const RectF r = value.value(); + return QStringLiteral("%1,%2 %3x%4").arg(r.x()).arg(r.y()).arg(r.width()).arg(r.height()); + } + break; + } + return QStyledItemDelegate::displayText(value, locale); +} + +static const int s_x11WindowId = 1; +static const int s_x11UnmanagedId = 2; +static const int s_waylandWindowId = 3; +static const int s_workspaceInternalId = 4; +static const quint32 s_propertyBitMask = 0xFFFF0000; +static const quint32 s_windowBitMask = 0x0000FFFF; +static const quint32 s_idDistance = 10000; + +template +void DebugConsoleModel::add(int parentRow, QList &windows, T *window) +{ + beginInsertRows(index(parentRow, 0, QModelIndex()), windows.count(), windows.count()); + windows.append(window); + endInsertRows(); +} + +template +void DebugConsoleModel::remove(int parentRow, QList &windows, T *window) +{ + const int remove = windows.indexOf(window); + if (remove == -1) { + return; + } + beginRemoveRows(index(parentRow, 0, QModelIndex()), remove, remove); + windows.removeAt(remove); + endRemoveRows(); +} + +DebugConsoleModel::DebugConsoleModel(QObject *parent) + : QAbstractItemModel(parent) +{ + const auto windows = workspace()->windows(); + for (auto window : windows) { + handleWindowAdded(window); + } + connect(workspace(), &Workspace::windowAdded, this, &DebugConsoleModel::handleWindowAdded); + connect(workspace(), &Workspace::windowRemoved, this, &DebugConsoleModel::handleWindowRemoved); +} + +void DebugConsoleModel::handleWindowAdded(Window *window) +{ +#if KWIN_BUILD_X11 + if (auto x11 = qobject_cast(window)) { + if (x11->isUnmanaged()) { + add(s_x11UnmanagedId - 1, m_unmanageds, x11); + } else { + add(s_x11WindowId - 1, m_x11Windows, x11); + } + return; + } +#endif + + if (auto wayland = qobject_cast(window)) { + add(s_waylandWindowId - 1, m_waylandWindows, wayland); + return; + } + + if (auto internal = qobject_cast(window)) { + add(s_workspaceInternalId - 1, m_internalWindows, internal); + return; + } +} + +void DebugConsoleModel::handleWindowRemoved(Window *window) +{ +#if KWIN_BUILD_X11 + if (auto x11 = qobject_cast(window)) { + if (x11->isUnmanaged()) { + remove(s_x11UnmanagedId - 1, m_unmanageds, x11); + } else { + remove(s_x11WindowId - 1, m_x11Windows, x11); + } + return; + } +#endif + + if (auto wayland = qobject_cast(window)) { + remove(s_waylandWindowId - 1, m_waylandWindows, wayland); + return; + } + + if (auto internal = qobject_cast(window)) { + remove(s_workspaceInternalId - 1, m_internalWindows, internal); + return; + } +} + +DebugConsoleModel::~DebugConsoleModel() = default; + +int DebugConsoleModel::columnCount(const QModelIndex &parent) const +{ + return 2; +} + +int DebugConsoleModel::topLevelRowCount() const +{ + return 4; +} + +template +int DebugConsoleModel::propertyCount(const QModelIndex &parent, T *(DebugConsoleModel::*filter)(const QModelIndex &) const) const +{ + if (T *t = (this->*filter)(parent)) { + return t->metaObject()->propertyCount(); + } + return 0; +} + +int DebugConsoleModel::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return topLevelRowCount(); + } + + switch (parent.internalId()) { + case s_x11WindowId: + return m_x11Windows.count(); + case s_x11UnmanagedId: + return m_unmanageds.count(); + case s_waylandWindowId: + return m_waylandWindows.count(); + case s_workspaceInternalId: + return m_internalWindows.count(); + default: + break; + } + + if (parent.internalId() & s_propertyBitMask) { + // properties do not have children + return 0; + } + + if (parent.internalId() < s_idDistance * (s_x11WindowId + 1)) { +#if KWIN_BUILD_X11 + return propertyCount(parent, &DebugConsoleModel::x11Window); +#else + return 0; +#endif + } else if (parent.internalId() < s_idDistance * (s_x11UnmanagedId + 1)) { +#if KWIN_BUILD_X11 + return propertyCount(parent, &DebugConsoleModel::unmanaged); +#else + return 0; +#endif + } else if (parent.internalId() < s_idDistance * (s_waylandWindowId + 1)) { + return propertyCount(parent, &DebugConsoleModel::waylandWindow); + } else if (parent.internalId() < s_idDistance * (s_workspaceInternalId + 1)) { + return propertyCount(parent, &DebugConsoleModel::internalWindow); + } + + return 0; +} + +template +QModelIndex DebugConsoleModel::indexForWindow(int row, int column, const QList &windows, int id) const +{ + if (column != 0) { + return QModelIndex(); + } + if (row >= windows.count()) { + return QModelIndex(); + } + return createIndex(row, column, s_idDistance * id + row); +} + +template +QModelIndex DebugConsoleModel::indexForProperty(int row, int column, const QModelIndex &parent, T *(DebugConsoleModel::*filter)(const QModelIndex &) const) const +{ + if (T *t = (this->*filter)(parent)) { + if (row >= t->metaObject()->propertyCount()) { + return QModelIndex(); + } + return createIndex(row, column, quint32(row + 1) << 16 | parent.internalId()); + } + return QModelIndex(); +} + +QModelIndex DebugConsoleModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!parent.isValid()) { + // index for a top level item + if (column != 0 || row >= topLevelRowCount()) { + return QModelIndex(); + } + return createIndex(row, column, row + 1); + } + if (column >= 2) { + // max of 2 columns + return QModelIndex(); + } + // index for a window (second level) + switch (parent.internalId()) { + case s_x11WindowId: + return indexForWindow(row, column, m_x11Windows, s_x11WindowId); + case s_x11UnmanagedId: + return indexForWindow(row, column, m_unmanageds, s_x11UnmanagedId); + case s_waylandWindowId: + return indexForWindow(row, column, m_waylandWindows, s_waylandWindowId); + case s_workspaceInternalId: + return indexForWindow(row, column, m_internalWindows, s_workspaceInternalId); + default: + break; + } + + // index for a property (third level) + if (parent.internalId() < s_idDistance * (s_x11WindowId + 1)) { +#if KWIN_BUILD_X11 + return indexForProperty(row, column, parent, &DebugConsoleModel::x11Window); +#else + return {}; +#endif + } else if (parent.internalId() < s_idDistance * (s_x11UnmanagedId + 1)) { +#if KWIN_BUILD_X11 + return indexForProperty(row, column, parent, &DebugConsoleModel::unmanaged); +#else + return {}; +#endif + } else if (parent.internalId() < s_idDistance * (s_waylandWindowId + 1)) { + return indexForProperty(row, column, parent, &DebugConsoleModel::waylandWindow); + } else if (parent.internalId() < s_idDistance * (s_workspaceInternalId + 1)) { + return indexForProperty(row, column, parent, &DebugConsoleModel::internalWindow); + } + + return QModelIndex(); +} + +QModelIndex DebugConsoleModel::parent(const QModelIndex &child) const +{ + if (child.internalId() <= s_workspaceInternalId) { + return QModelIndex(); + } + if (child.internalId() & s_propertyBitMask) { + // a property + const quint32 parentId = child.internalId() & s_windowBitMask; + if (parentId < s_idDistance * (s_x11WindowId + 1)) { + return createIndex(parentId - (s_idDistance * s_x11WindowId), 0, parentId); + } else if (parentId < s_idDistance * (s_x11UnmanagedId + 1)) { + return createIndex(parentId - (s_idDistance * s_x11UnmanagedId), 0, parentId); + } else if (parentId < s_idDistance * (s_waylandWindowId + 1)) { + return createIndex(parentId - (s_idDistance * s_waylandWindowId), 0, parentId); + } else if (parentId < s_idDistance * (s_workspaceInternalId + 1)) { + return createIndex(parentId - (s_idDistance * s_workspaceInternalId), 0, parentId); + } + return QModelIndex(); + } + if (child.internalId() < s_idDistance * (s_x11WindowId + 1)) { + return createIndex(s_x11WindowId - 1, 0, s_x11WindowId); + } else if (child.internalId() < s_idDistance * (s_x11UnmanagedId + 1)) { + return createIndex(s_x11UnmanagedId - 1, 0, s_x11UnmanagedId); + } else if (child.internalId() < s_idDistance * (s_waylandWindowId + 1)) { + return createIndex(s_waylandWindowId - 1, 0, s_waylandWindowId); + } else if (child.internalId() < s_idDistance * (s_workspaceInternalId + 1)) { + return createIndex(s_workspaceInternalId - 1, 0, s_workspaceInternalId); + } + return QModelIndex(); +} + +QVariant DebugConsoleModel::propertyData(KWin::Window *window, const QModelIndex &index, int role) const +{ + const auto property = window->metaObject()->property(index.row()); + if (role == Qt::DisplayRole) { + if (index.column() == 0) { + return property.name(); + } else { + const QVariant value = property.read(window); + if (qstrcmp(property.name(), "windowType") == 0) { + switch (value.toInt()) { + case NET::Normal: + return QStringLiteral("NET::Normal"); + case NET::Desktop: + return QStringLiteral("NET::Desktop"); + case NET::Dock: + return QStringLiteral("NET::Dock"); + case NET::Toolbar: + return QStringLiteral("NET::Toolbar"); + case NET::Menu: + return QStringLiteral("NET::Menu"); + case NET::Dialog: + return QStringLiteral("NET::Dialog"); + case NET::Override: + return QStringLiteral("NET::Override"); + case NET::TopMenu: + return QStringLiteral("NET::TopMenu"); + case NET::Utility: + return QStringLiteral("NET::Utility"); + case NET::Splash: + return QStringLiteral("NET::Splash"); + case NET::DropdownMenu: + return QStringLiteral("NET::DropdownMenu"); + case NET::PopupMenu: + return QStringLiteral("NET::PopupMenu"); + case NET::Tooltip: + return QStringLiteral("NET::Tooltip"); + case NET::Notification: + return QStringLiteral("NET::Notification"); + case NET::ComboBox: + return QStringLiteral("NET::ComboBox"); + case NET::DNDIcon: + return QStringLiteral("NET::DNDIcon"); + case NET::OnScreenDisplay: + return QStringLiteral("NET::OnScreenDisplay"); + case NET::CriticalNotification: + return QStringLiteral("NET::CriticalNotification"); + case NET::AppletPopup: + return QStringLiteral("NET::AppletPopup"); + case NET::Unknown: + default: + return QStringLiteral("NET::Unknown"); + } + } else if (qstrcmp(property.name(), "layer") == 0) { + return QMetaEnum::fromType().valueToKey(value.value()); + } + return value; + } + } else if (role == Qt::DecorationRole) { + if (index.column() == 1) { + const QVariant value = property.read(window); + if (value.userType() == qMetaTypeId()) { + return value; + } else if (value.userType() == qMetaTypeId()) { + if (auto window = value.value()) { + return window->icon(); + } + } else if (qstrcmp(property.name(), "colorScheme") == 0) { + const QPalette palette = window->palette(); + + // Draw a little color scheme preview, + // inspired by KColorSchemeManagerPrivate::createPreview. + QPixmap pixmap(16, 16); + pixmap.fill(Qt::black); + QPainter painter(&pixmap); + constexpr int itemSize = 16 / 2 - 1; + painter.fillRect(1, 1, itemSize, itemSize, palette.window().color()); + painter.fillRect(1 + itemSize, 1, itemSize, itemSize, palette.button().color()); + painter.fillRect(1, 1 + itemSize, itemSize, itemSize, palette.base().color()); + painter.fillRect(1 + itemSize, 1 + itemSize, itemSize, itemSize, palette.highlight().color()); + return pixmap; + } + } + } + return QVariant(); +} + +template +QVariant DebugConsoleModel::windowData(const QModelIndex &index, int role, const QList windows, const std::function &toString) const +{ + if (index.row() >= windows.count()) { + return QVariant(); + } + auto c = windows.at(index.row()); + if (role == Qt::DisplayRole) { + return toString(c); + } else if (role == Qt::DecorationRole) { + return c->icon(); + } + return QVariant(); +} + +QVariant DebugConsoleModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + if (!index.parent().isValid()) { + // one of the top levels + if (index.column() != 0 || role != Qt::DisplayRole) { + return QVariant(); + } + switch (index.internalId()) { + case s_x11WindowId: + return i18n("X11 Windows"); + case s_x11UnmanagedId: + return i18n("X11 Unmanaged Windows"); + case s_waylandWindowId: + return i18n("Wayland Windows"); + case s_workspaceInternalId: + return i18n("Internal Windows"); + default: + return QVariant(); + } + } + if (index.internalId() & s_propertyBitMask) { + if (index.column() >= 2) { + return QVariant(); + } + if (Window *w = waylandWindow(index)) { + return propertyData(w, index, role); + } else if (InternalWindow *w = internalWindow(index)) { + return propertyData(w, index, role); +#if KWIN_BUILD_X11 + } else if (X11Window *w = x11Window(index)) { + return propertyData(w, index, role); + } else if (X11Window *u = unmanaged(index)) { + return propertyData(u, index, role); +#endif + } + } else { + if (index.column() != 0) { + return QVariant(); + } + + auto generic = [](Window *c) -> QString { + return c->caption() + QLatin1Char(' ') + QString::fromUtf8(c->metaObject()->className()); + }; + switch (index.parent().internalId()) { + case s_x11WindowId: +#if KWIN_BUILD_X11 + return windowData(index, role, m_x11Windows, [](X11Window *c) -> QString { + return QStringLiteral("0x%1: %2").arg(c->window(), 0, 16).arg(c->caption()); + }); +#endif + break; + case s_x11UnmanagedId: { +#if KWIN_BUILD_X11 + if (index.row() >= m_unmanageds.count()) { + return QVariant(); + } + auto u = m_unmanageds.at(index.row()); + if (role == Qt::DisplayRole) { + return QStringLiteral("0x%1").arg(u->window(), 0, 16); + } +#endif + break; + } + case s_waylandWindowId: + return windowData(index, role, m_waylandWindows, generic); + case s_workspaceInternalId: + return windowData(index, role, m_internalWindows, generic); + default: + break; + } + } + + return QVariant(); +} + +template +static T *windowForIndex(const QModelIndex &index, const QList &windows, int id) +{ + const qint32 row = (index.internalId() & s_windowBitMask) - (s_idDistance * id); + if (row < 0 || row >= windows.count()) { + return nullptr; + } + return windows.at(row); +} + +WaylandWindow *DebugConsoleModel::waylandWindow(const QModelIndex &index) const +{ + return windowForIndex(index, m_waylandWindows, s_waylandWindowId); +} + +InternalWindow *DebugConsoleModel::internalWindow(const QModelIndex &index) const +{ + return windowForIndex(index, m_internalWindows, s_workspaceInternalId); +} + +X11Window *DebugConsoleModel::x11Window(const QModelIndex &index) const +{ + return windowForIndex(index, m_x11Windows, s_x11WindowId); +} + +X11Window *DebugConsoleModel::unmanaged(const QModelIndex &index) const +{ + return windowForIndex(index, m_unmanageds, s_x11UnmanagedId); +} + +InputDeviceModel::InputDeviceModel(QObject *parent) + : QAbstractItemModel(parent) + , m_devices(input()->devices()) +{ + for (auto it = m_devices.constBegin(); it != m_devices.constEnd(); ++it) { + setupDeviceConnections(*it); + } + + connect(input(), &InputRedirection::deviceAdded, this, [this](InputDevice *d) { + beginInsertRows(QModelIndex(), m_devices.count(), m_devices.count()); + m_devices << d; + setupDeviceConnections(d); + endInsertRows(); + }); + connect(input(), &InputRedirection::deviceRemoved, this, [this](InputDevice *d) { + const int index = m_devices.indexOf(d); + if (index == -1) { + return; + } + beginRemoveRows(QModelIndex(), index, index); + m_devices.removeAt(index); + endRemoveRows(); + }); +} + +InputDeviceModel::~InputDeviceModel() = default; + +int InputDeviceModel::columnCount(const QModelIndex &parent) const +{ + return 2; +} + +QVariant InputDeviceModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + if (!index.parent().isValid() && index.column() == 0) { + if (index.row() >= m_devices.count()) { + return QVariant(); + } + if (role == Qt::DisplayRole) { + return m_devices.at(index.row())->name(); + } + } + if (index.parent().isValid()) { + if (role == Qt::DisplayRole) { + const auto device = m_devices.at(index.parent().row()); + const auto property = device->metaObject()->property(index.row()); + if (index.column() == 0) { + return property.name(); + } else if (index.column() == 1) { + return device->property(property.name()); + } + } + } + return QVariant(); +} + +QModelIndex InputDeviceModel::index(int row, int column, const QModelIndex &parent) const +{ + if (column >= 2) { + return QModelIndex(); + } + if (parent.isValid()) { + if (parent.internalId() & s_propertyBitMask) { + return QModelIndex(); + } + if (row >= m_devices.at(parent.row())->metaObject()->propertyCount()) { + return QModelIndex(); + } + return createIndex(row, column, quint32(row + 1) << 16 | parent.internalId()); + } + if (row >= m_devices.count()) { + return QModelIndex(); + } + return createIndex(row, column, row + 1); +} + +int InputDeviceModel::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return m_devices.count(); + } + if (parent.internalId() & s_propertyBitMask) { + return 0; + } + + return m_devices.at(parent.row())->metaObject()->propertyCount(); +} + +QModelIndex InputDeviceModel::parent(const QModelIndex &child) const +{ + if (child.internalId() & s_propertyBitMask) { + const quintptr parentId = child.internalId() & s_windowBitMask; + return createIndex(parentId - 1, 0, parentId); + } + return QModelIndex(); +} + +void InputDeviceModel::slotPropertyChanged() +{ + const auto device = static_cast(sender()); + + for (int i = 0; i < device->metaObject()->propertyCount(); ++i) { + const QMetaProperty metaProperty = device->metaObject()->property(i); + if (metaProperty.notifySignalIndex() == senderSignalIndex()) { + const QModelIndex parent = index(m_devices.indexOf(device), 0, QModelIndex()); + const QModelIndex child = index(i, 1, parent); + Q_EMIT dataChanged(child, child, QList{Qt::DisplayRole}); + } + } +} + +void InputDeviceModel::setupDeviceConnections(InputDevice *device) +{ + QMetaMethod handler = metaObject()->method(metaObject()->indexOfMethod("slotPropertyChanged()")); + for (int i = 0; i < device->metaObject()->propertyCount(); ++i) { + const QMetaProperty metaProperty = device->metaObject()->property(i); + if (metaProperty.hasNotifySignal()) { + connect(device, metaProperty.notifySignal(), this, handler); + } + } +} + +QModelIndex DataSourceModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!m_source || parent.isValid() || column >= 2 || row >= m_source->mimeTypes().size()) { + return QModelIndex(); + } + return createIndex(row, column, nullptr); +} + +QModelIndex DataSourceModel::parent(const QModelIndex &child) const +{ + return QModelIndex(); +} + +int DataSourceModel::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return m_source ? m_source->mimeTypes().count() : 0; + } + return 0; +} + +QVariant DataSourceModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole || orientation != Qt::Horizontal || section >= 2) { + return QVariant(); + } + return section == 0 ? QStringLiteral("Mime type") : QStringLiteral("Content"); +} + +QVariant DataSourceModel::data(const QModelIndex &index, int role) const +{ + if (!checkIndex(index, CheckIndexOption::ParentIsInvalid | CheckIndexOption::IndexIsValid)) { + return QVariant(); + } + const QString mimeType = m_source->mimeTypes().at(index.row()); + ; + if (index.column() == 0 && role == Qt::DisplayRole) { + return mimeType; + } else if (index.column() == 1 && index.row() < m_data.count()) { + const QByteArray &data = m_data.at(index.row()); + if (mimeType.contains(QLatin1String("image"))) { + if (role == Qt::DecorationRole) { + return QImage::fromData(data); + } + } else if (role == Qt::DisplayRole) { + return data; + } + } + return QVariant(); +} + +static QByteArray readData(int fd) +{ + pollfd pfd; + pfd.fd = fd; + pfd.events = POLLIN; + FileDescriptor closeFd{fd}; + QByteArray data; + while (true) { + const int ready = poll(&pfd, 1, 1000); + if (ready < 0) { + if (errno != EINTR) { + return QByteArrayLiteral("poll() failed: ") + strerror(errno); + } + } else if (ready == 0) { + return QByteArrayLiteral("timeout reading from pipe"); + } else { + char buf[4096]; + int n = read(fd, buf, sizeof buf); + + if (n < 0) { + return QByteArrayLiteral("read failed: ") + strerror(errno); + } else if (n == 0) { + return data; + } else if (n > 0) { + data.append(buf, n); + } + } + } +} + +void DataSourceModel::setSource(AbstractDataSource *source) +{ + beginResetModel(); + m_source = source; + m_data.clear(); + if (source) { + const QStringList mimeTypes = m_source->mimeTypes(); + m_data.resize(mimeTypes.size()); + for (auto type = mimeTypes.begin(); type != mimeTypes.end(); ++type) { + std::optional pipe = Pipe::create(O_CLOEXEC); + if (!pipe) { + continue; + } + source->requestData(*type, std::move(pipe->writeEndpoint)); + QFuture data = QtConcurrent::run(readData, pipe->readEndpoint.take()); + auto watcher = new QFutureWatcher(this); + watcher->setFuture(data); + const int index = type - mimeTypes.begin(); + connect(watcher, &QFutureWatcher::finished, this, [this, watcher, index, source = QPointer(source)] { + watcher->deleteLater(); + if (source && source == m_source) { + m_data[index] = watcher->result(); + Q_EMIT dataChanged(this->index(index, 1), this->index(index, 1), {Qt::DecorationRole | Qt::DisplayRole}); + } + }); + } + } + endResetModel(); +} + +DebugConsoleEffectItem::DebugConsoleEffectItem(const QString &name, bool loaded, QWidget *parent) + : QWidget(parent) + , m_name(name) + , m_loaded(loaded) +{ + QHBoxLayout *layout = new QHBoxLayout(this); + + m_label = new QLabel(name, this); + layout->addWidget(m_label); + + m_toggleButton = new QPushButton(this); + layout->addWidget(m_toggleButton); + + updateToggleButton(); + + connect(m_toggleButton, &QPushButton::clicked, this, [this]() { + if (m_loaded) { + m_loaded = false; + effects->unloadEffect(m_name); + } else { + m_loaded = effects->loadEffect(m_name); + } + updateToggleButton(); + }); +} + +void DebugConsoleEffectItem::updateToggleButton() +{ + QFont font = m_label->font(); + if (m_loaded) { + m_toggleButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::MediaPlaybackPause)); + m_toggleButton->setText(i18nc("@action:button unload an effect", "Unload")); + font.setBold(true); + } else { + m_toggleButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::MediaPlaybackStart)); + m_toggleButton->setText(i18nc("@action:button load an effect", "Load")); + font.setBold(false); + } + m_label->setFont(font); +} + +DebugConsoleEffectsTab::DebugConsoleEffectsTab(QWidget *parent) + : QListWidget(parent) +{ + if (!effects) { + return; + } + + QStringList availableEffects = effects->listOfEffects(); + const QStringList loadedEffects = effects->loadedEffects(); + + // Remove any duplicates with the same name + availableEffects.removeDuplicates(); + + // Show these debugging effects at the top of the list, sort the rest so they can be easily found + const QStringList priorityEffects = {QStringLiteral("showcompositing"), QStringLiteral("showfps"), QStringLiteral("showpaint")}; + std::sort(availableEffects.begin(), availableEffects.end(), [&priorityEffects](const QString &a, const QString &b) { + const int indexA = priorityEffects.indexOf(a); + const int indexB = priorityEffects.indexOf(b); + + if (indexA != -1 && indexB != -1) { + return a < b; + } else if (indexA != -1) { + return true; + } else if (indexB != -1) { + return false; + } + + return a < b; + }); + + // Determine the index of the last priority effect so we can insert a separator beneath + int lastPriorityIndex = -1; + for (int i = 0; i < availableEffects.count(); ++i) { + if (priorityEffects.contains(availableEffects[i])) { + lastPriorityIndex = i; + } else { + break; + } + } + + for (int i = 0; i < availableEffects.count(); ++i) { + const QString &effectName = availableEffects[i]; + + QListWidgetItem *item = new QListWidgetItem(this); + DebugConsoleEffectItem *effectItem = new DebugConsoleEffectItem(effectName, loadedEffects.contains(effectName)); + + addItem(item); + setItemWidget(item, effectItem); + item->setSizeHint(effectItem->sizeHint()); + + if (i == lastPriorityIndex) { + QListWidgetItem *separatorItem = new QListWidgetItem(this); + separatorItem->setFlags(Qt::NoItemFlags); + + QFrame *separator = new QFrame(); + separator->setFrameShape(QFrame::HLine); + + addItem(separatorItem); + setItemWidget(separatorItem, separator); + } + } +} + +} // namespace KWin + +#include "moc_debug_console.cpp" diff --git a/local/recipes/kde/kwin/source/src/debug_console.h b/local/recipes/kde/kwin/source/src/debug_console.h new file mode 100644 index 0000000000..e225e92986 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/debug_console.h @@ -0,0 +1,225 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "config-kwin.h" + +#include "input.h" +#include "input_event_spy.h" +#include + +#include +#include +#include +#include + +#include +#include + +class QLabel; +class QPushButton; +class QTextEdit; + +namespace Ui +{ +class DebugConsole; +} + +namespace KWin +{ + +class AbstractDataSource; +class Window; +class X11Window; +class InternalWindow; +class DebugConsoleFilter; +class WaylandWindow; + +class KWIN_EXPORT DebugConsoleModel : public QAbstractItemModel +{ + Q_OBJECT +public: + explicit DebugConsoleModel(QObject *parent = nullptr); + ~DebugConsoleModel() override; + + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + QModelIndex index(int row, int column, const QModelIndex &parent) const override; + int rowCount(const QModelIndex &parent) const override; + QModelIndex parent(const QModelIndex &child) const override; + +private Q_SLOTS: + void handleWindowAdded(Window *window); + void handleWindowRemoved(Window *window); + +private: + template + QModelIndex indexForWindow(int row, int column, const QList &windows, int id) const; + template + QModelIndex indexForProperty(int row, int column, const QModelIndex &parent, T *(DebugConsoleModel::*filter)(const QModelIndex &) const) const; + template + int propertyCount(const QModelIndex &parent, T *(DebugConsoleModel::*filter)(const QModelIndex &) const) const; + QVariant propertyData(Window *object, const QModelIndex &index, int role) const; + template + QVariant windowData(const QModelIndex &index, int role, const QList windows, const std::function &toString) const; + template + void add(int parentRow, QList &windows, T *window); + template + void remove(int parentRow, QList &windows, T *window); + WaylandWindow *waylandWindow(const QModelIndex &index) const; + InternalWindow *internalWindow(const QModelIndex &index) const; + X11Window *x11Window(const QModelIndex &index) const; + X11Window *unmanaged(const QModelIndex &index) const; + int topLevelRowCount() const; + + QList m_waylandWindows; + QList m_internalWindows; + QList m_x11Windows; + QList m_unmanageds; +}; + +class DebugConsoleDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + explicit DebugConsoleDelegate(QObject *parent = nullptr); + ~DebugConsoleDelegate() override; + + QString displayText(const QVariant &value, const QLocale &locale) const override; +}; + +class KWIN_EXPORT DebugConsole : public QWidget +{ + Q_OBJECT +public: + DebugConsole(); + ~DebugConsole() override; + +protected: + void showEvent(QShowEvent *event) override; + +private: + void initGLTab(); + void updateKeyboardTab(); + + std::unique_ptr m_ui; + std::unique_ptr m_inputFilter; +}; + +class DebugConsoleFilter : public InputEventSpy +{ +public: + explicit DebugConsoleFilter(QTextEdit *textEdit); + ~DebugConsoleFilter() override; + + void pointerMotion(PointerMotionEvent *event) override; + void pointerButton(PointerButtonEvent *event) override; + void pointerAxis(PointerAxisEvent *event) override; + void keyboardKey(KeyboardKeyEvent *event) override; + void touchDown(TouchDownEvent *event) override; + void touchMotion(TouchMotionEvent *event) override; + void touchUp(TouchUpEvent *event) override; + + void pinchGestureBegin(PointerPinchGestureBeginEvent *event) override; + void pinchGestureUpdate(PointerPinchGestureUpdateEvent *event) override; + void pinchGestureEnd(PointerPinchGestureEndEvent *event) override; + void pinchGestureCancelled(PointerPinchGestureCancelEvent *event) override; + + void swipeGestureBegin(PointerSwipeGestureBeginEvent *event) override; + void swipeGestureUpdate(PointerSwipeGestureUpdateEvent *event) override; + void swipeGestureEnd(PointerSwipeGestureEndEvent *event) override; + void swipeGestureCancelled(PointerSwipeGestureCancelEvent *event) override; + + void holdGestureBegin(PointerHoldGestureBeginEvent *event) override; + void holdGestureEnd(PointerHoldGestureEndEvent *event) override; + void holdGestureCancelled(PointerHoldGestureCancelEvent *event) override; + + void switchEvent(SwitchEvent *event) override; + + void tabletToolProximityEvent(TabletToolProximityEvent *event) override; + void tabletToolAxisEvent(TabletToolAxisEvent *event) override; + void tabletToolTipEvent(TabletToolTipEvent *event) override; + void tabletToolButtonEvent(TabletToolButtonEvent *event) override; + void tabletPadButtonEvent(TabletPadButtonEvent *event) override; + void tabletPadStripEvent(TabletPadStripEvent *event) override; + void tabletPadRingEvent(TabletPadRingEvent *event) override; + void tabletPadDialEvent(TabletPadDialEvent *event) override; + +private: + QTextEdit *m_textEdit; +}; + +class InputDeviceModel : public QAbstractItemModel +{ + Q_OBJECT +public: + explicit InputDeviceModel(QObject *parent = nullptr); + ~InputDeviceModel() override; + + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + QModelIndex index(int row, int column, const QModelIndex &parent) const override; + int rowCount(const QModelIndex &parent) const override; + QModelIndex parent(const QModelIndex &child) const override; + +private Q_SLOTS: + void slotPropertyChanged(); + +private: + void setupDeviceConnections(InputDevice *device); + QList m_devices; +}; + +class DataSourceModel : public QAbstractItemModel +{ +public: + using QAbstractItemModel::QAbstractItemModel; + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &child) const override; + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override + { + return parent.isValid() ? 0 : 2; + } + QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + void setSource(AbstractDataSource *source); + +private: + AbstractDataSource *m_source = nullptr; + QList m_data; +}; + +class DebugConsoleEffectItem : public QWidget +{ + Q_OBJECT + +public: + explicit DebugConsoleEffectItem(const QString &name, bool loaded, QWidget *parent = nullptr); + +private: + void updateToggleButton(); + + QString m_name; + QLabel *m_label; + QPushButton *m_toggleButton; + bool m_loaded = false; +}; + +class DebugConsoleEffectsTab : public QListWidget +{ + Q_OBJECT + +public: + explicit DebugConsoleEffectsTab(QWidget *parent = nullptr); +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/debug_console.ui b/local/recipes/kde/kwin/source/src/debug_console.ui new file mode 100644 index 0000000000..941d3385d5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/debug_console.ui @@ -0,0 +1,497 @@ + + + DebugConsole + + + + 0 + 0 + 600 + 600 + + + + Debug Console + + + + + + + + + 0 + + + false + + + + Windows + + + + + + 250 + + + + + + + + Input Events + + + + + + false + + + true + + + + + + + + Input Devices + + + + + + + + + + OpenGL + + + + + + No OpenGL compositor running + + + + + + + QFrame::Plain + + + 0 + + + true + + + + + 0 + 0 + 564 + 471 + + + + + + + OpenGL (ES) driver information + + + + + + Vendor: + + + + + + + Renderer: + + + + + + + Version: + + + + + + + Shading Language Version: + + + + + + + Driver: + + + + + + + GPU class: + + + + + + + OpenGL Version: + + + + + + + GLSL Version: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Platform Extensions + + + + + + + + + + + + + + + + OpenGL (ES) Extensions + + + + + + + + + + + + + + + + + + + + + Keyboard + + + + + + QFrame::Plain + + + 0 + + + true + + + + + 0 + 0 + 564 + 495 + + + + + + + Keymap Layouts + + + + + + + + + + + + + Qt::Horizontal + + + + + + + + + Current Layout: + + + + + + + + + + + + + + + + + + + Modifiers + + + + + + + + + + + + + + + + Active Modifiers + + + + + + + + + + + + + + + + LEDs + + + + + + + + + + + + + + + + Active LEDs + + + + + + + + + + + + + + + + + + + + + Clipboard + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Clipboard + + + + + + + + 0 + 0 + + + + + + + + + + + + + true + + + false + + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Primary Selection + + + + + + + + + + + + + + + + true + + + false + + + + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/decorations/decoratedwindow.cpp b/local/recipes/kde/kwin/source/src/decorations/decoratedwindow.cpp new file mode 100644 index 0000000000..74f799daa8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/decorations/decoratedwindow.cpp @@ -0,0 +1,358 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "decoratedwindow.h" +#include "cursor.h" +#include "decorationbridge.h" +#include "decorationpalette.h" +#include "tiles/tile.h" +#include "window.h" +#include "workspace.h" + +#include +#include + +#include +#include +#include +#include + +namespace KWin +{ +namespace Decoration +{ + +DecoratedWindowImpl::DecoratedWindowImpl(Window *window, KDecoration3::DecoratedWindow *decoratedClient, KDecoration3::Decoration *decoration) + : QObject() + , DecoratedWindowPrivateV4(decoratedClient, decoration) + , m_window(window) + , m_clientSize(window->clientSize()) +{ + window->setDecoratedWindow(this); + connect(window, &Window::activeChanged, this, [decoratedClient, window]() { + Q_EMIT decoratedClient->activeChanged(window->isActive()); + }); + connect(window, &Window::clientGeometryChanged, this, [decoratedClient, this]() { + if (m_window->clientSize() == m_clientSize) { + return; + } + const auto oldSize = m_clientSize; + m_clientSize = m_window->clientSize(); + if (oldSize.width() != m_clientSize.width()) { + Q_EMIT decoratedClient->widthChanged(m_clientSize.width()); + } + if (oldSize.height() != m_clientSize.height()) { + Q_EMIT decoratedClient->heightChanged(m_clientSize.height()); + } + Q_EMIT decoratedClient->sizeChanged(m_clientSize); + }); + connect(window, &Window::desktopsChanged, this, [decoratedClient, window]() { + Q_EMIT decoratedClient->onAllDesktopsChanged(window->isOnAllDesktops()); + }); + connect(window, &Window::captionChanged, this, [decoratedClient, window]() { + Q_EMIT decoratedClient->captionChanged(window->caption()); + }); + connect(window, &Window::iconChanged, this, [decoratedClient, window]() { + Q_EMIT decoratedClient->iconChanged(window->icon()); + }); + connect(window, &Window::keepAboveChanged, decoratedClient, &KDecoration3::DecoratedWindow::keepAboveChanged); + connect(window, &Window::keepBelowChanged, decoratedClient, &KDecoration3::DecoratedWindow::keepBelowChanged); + connect(window, &Window::excludeFromCaptureChanged, this, [decoratedClient, window]() { + Q_EMIT decoratedClient->excludeFromCaptureChanged(window->excludeFromCapture()); + }); + connect(window, &Window::requestedTileChanged, decoratedClient, [this, decoratedClient]() { + Q_EMIT decoratedClient->adjacentScreenEdgesChanged(adjacentScreenEdges()); + }); + connect(window, &Window::closeableChanged, decoratedClient, &KDecoration3::DecoratedWindow::closeableChanged); + connect(window, &Window::minimizeableChanged, decoratedClient, &KDecoration3::DecoratedWindow::minimizeableChanged); + connect(window, &Window::maximizeableChanged, decoratedClient, &KDecoration3::DecoratedWindow::maximizeableChanged); + + connect(window, &Window::paletteChanged, decoratedClient, &KDecoration3::DecoratedWindow::paletteChanged); + + connect(window, &Window::hasApplicationMenuChanged, decoratedClient, &KDecoration3::DecoratedWindow::hasApplicationMenuChanged); + connect(window, &Window::applicationMenuActiveChanged, decoratedClient, &KDecoration3::DecoratedWindow::applicationMenuActiveChanged); + + connect(window, &Window::targetScaleChanged, decoratedClient, &KDecoration3::DecoratedWindow::scaleChanged); + connect(window, &Window::nextTargetScaleChanged, decoratedClient, &KDecoration3::DecoratedWindow::nextScaleChanged); + connect(window, &Window::applicationMenuChanged, decoratedClient, &KDecoration3::DecoratedWindow::applicationMenuChanged); + + m_toolTipWakeUp.setSingleShot(true); + connect(&m_toolTipWakeUp, &QTimer::timeout, this, [this]() { + int fallAsleepDelay = QApplication::style()->styleHint(QStyle::SH_ToolTip_FallAsleepDelay); + this->m_toolTipFallAsleep.setRemainingTime(fallAsleepDelay); + + QToolTip::showText(Cursors::self()->currentCursor()->pos().toPoint(), this->m_toolTipText); + m_toolTipShowing = true; + }); +} + +DecoratedWindowImpl::~DecoratedWindowImpl() +{ + if (m_toolTipShowing) { + requestHideToolTip(); + } +} + +bool DecoratedWindowImpl::isShadeable() const +{ + return false; +} + +#define DELEGATE(type, name, clientName) \ + type DecoratedWindowImpl::name() const \ + { \ + return m_window->clientName(); \ + } + +#define DELEGATE2(type, name) DELEGATE(type, name, name) + +DELEGATE2(QString, caption) +DELEGATE2(bool, isActive) +DELEGATE2(bool, isCloseable) +DELEGATE(bool, isMaximizeable, isMaximizable) +DELEGATE(bool, isMinimizeable, isMinimizable) +DELEGATE2(bool, isModal) +DELEGATE(bool, isMoveable, isMovable) +DELEGATE(bool, isResizeable, isResizable) +DELEGATE2(bool, providesContextHelp) +DELEGATE2(bool, isOnAllDesktops) +DELEGATE2(QPalette, palette) +DELEGATE2(QIcon, icon) + +#undef DELEGATE2 +#undef DELEGATE + +bool DecoratedWindowImpl::isShaded() const +{ + return false; +} + +#define DELEGATE(type, name, clientName) \ + type DecoratedWindowImpl::name() const \ + { \ + return m_window->clientName(); \ + } + +DELEGATE(bool, isKeepAbove, keepAbove) +DELEGATE(bool, isKeepBelow, keepBelow) +DELEGATE(bool, isExcludedFromCapture, excludeFromCapture) + +#undef DELEGATE + +void DecoratedWindowImpl::requestToggleShade() +{ +} + +#define DELEGATE(name, op) \ + void DecoratedWindowImpl::name() \ + { \ + if (m_window->isDeleted()) { \ + return; \ + } \ + Workspace::self()->performWindowOperation(m_window, Options::op); \ + } + +DELEGATE(requestToggleOnAllDesktops, OnAllDesktopsOp) +DELEGATE(requestToggleKeepAbove, KeepAboveOp) +DELEGATE(requestToggleKeepBelow, KeepBelowOp) +DELEGATE(requestToggleExcludeFromCapture, ExcludeFromCaptureOp) + +#undef DELEGATE + +#define DELEGATE(name, clientName) \ + void DecoratedWindowImpl::name() \ + { \ + if (m_window->isDeleted()) { \ + return; \ + } \ + m_window->clientName(); \ + } + +DELEGATE(requestContextHelp, showContextHelp) + +#undef DELEGATE + +void DecoratedWindowImpl::requestMinimize() +{ + m_window->setMinimized(true); +} + +void DecoratedWindowImpl::requestClose() +{ + if (m_window->isDeleted()) { + return; + } + QMetaObject::invokeMethod(m_window, &Window::closeWindow, Qt::QueuedConnection); +} + +QColor DecoratedWindowImpl::color(KDecoration3::ColorGroup group, KDecoration3::ColorRole role) const +{ + auto dp = m_window->decorationPalette(); + if (dp) { + return dp->color(group, role); + } + + return QColor(); +} + +void DecoratedWindowImpl::requestShowToolTip(const QString &text) +{ + if (m_window->isDeleted()) { + return; + } + if (!workspace()->decorationBridge()->showToolTips()) { + return; + } + + m_toolTipText = text; + + int wakeUpDelay = QApplication::style()->styleHint(QStyle::SH_ToolTip_WakeUpDelay); + m_toolTipWakeUp.start(m_toolTipFallAsleep.hasExpired() ? wakeUpDelay : 20); +} + +void DecoratedWindowImpl::requestHideToolTip() +{ + m_toolTipWakeUp.stop(); + QToolTip::hideText(); + m_toolTipShowing = false; +} + +void DecoratedWindowImpl::requestShowWindowMenu(const QRect &rect) +{ + if (m_window->isDeleted()) { + return; + } + Workspace::self()->showWindowMenu(QRectF(m_window->pos() + rect.topLeft(), m_window->pos() + rect.bottomRight()).toRect(), m_window); +} + +void DecoratedWindowImpl::requestShowApplicationMenu(const QRect &rect, int actionId) +{ + if (m_window->isDeleted()) { + return; + } + Workspace::self()->showApplicationMenu(rect, m_window, actionId); +} + +void DecoratedWindowImpl::showApplicationMenu(int actionId) +{ + if (m_window->isDeleted()) { + return; + } + decoration()->showApplicationMenu(actionId); +} + +void DecoratedWindowImpl::requestToggleMaximization(Qt::MouseButtons buttons) +{ + if (m_window->isDeleted()) { + return; + } + auto operation = options->operationMaxButtonClick(buttons); + QMetaObject::invokeMethod( + this, [this, operation] { + delayedRequestToggleMaximization(operation); + }, Qt::QueuedConnection); +} + +void DecoratedWindowImpl::delayedRequestToggleMaximization(Options::WindowOperation operation) +{ + if (m_window->isDeleted()) { + return; + } + Workspace::self()->performWindowOperation(m_window, operation); +} + +qreal DecoratedWindowImpl::width() const +{ + return m_clientSize.width(); +} + +qreal DecoratedWindowImpl::height() const +{ + return m_clientSize.height(); +} + +QSizeF DecoratedWindowImpl::size() const +{ + return m_clientSize; +} + +bool DecoratedWindowImpl::isMaximizedVertically() const +{ + return m_window->requestedMaximizeMode() & MaximizeVertical; +} + +bool DecoratedWindowImpl::isMaximized() const +{ + return isMaximizedHorizontally() && isMaximizedVertically(); +} + +bool DecoratedWindowImpl::isMaximizedHorizontally() const +{ + return m_window->requestedMaximizeMode() & MaximizeHorizontal; +} + +Qt::Edges DecoratedWindowImpl::adjacentScreenEdges() const +{ + if (Tile *tile = m_window->requestedTile()) { + return tile->anchors(); + } + return Qt::Edges(); +} + +bool DecoratedWindowImpl::hasApplicationMenu() const +{ + return m_window->hasApplicationMenu(); +} + +bool DecoratedWindowImpl::isApplicationMenuActive() const +{ + return m_window->applicationMenuActive(); +} + +QString DecoratedWindowImpl::windowClass() const +{ + return m_window->resourceName() + QLatin1Char(' ') + m_window->resourceClass(); +} + +qreal DecoratedWindowImpl::scale() const +{ + return m_window->targetScale(); +} + +qreal DecoratedWindowImpl::nextScale() const +{ + return m_window->nextTargetScale(); +} + +QString DecoratedWindowImpl::applicationMenuServiceName() const +{ + return m_window->applicationMenuServiceName(); +} + +QString DecoratedWindowImpl::applicationMenuObjectPath() const +{ + return m_window->applicationMenuObjectPath(); +} + +void DecoratedWindowImpl::popup(const KDecoration3::Positioner &positioner, QMenu *menu) +{ + const QRectF anchorRect = positioner.anchorRect().translated(m_window->pos()); + const QPointF position = qGuiApp->layoutDirection() == Qt::RightToLeft + ? QPointF(anchorRect.right() - menu->width(), anchorRect.bottom()) + : QPointF(anchorRect.left(), anchorRect.bottom()); + + if (menu->isVisible()) { + menu->move(position.toPoint()); + } else { + menu->popup(position.toPoint()); + } +} +} +} + +#include "moc_decoratedwindow.cpp" diff --git a/local/recipes/kde/kwin/source/src/decorations/decoratedwindow.h b/local/recipes/kde/kwin/source/src/decorations/decoratedwindow.h new file mode 100644 index 0000000000..f5c4a7042f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/decorations/decoratedwindow.h @@ -0,0 +1,108 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "options.h" + +#include + +#include +#include +#include + +namespace KWin +{ + +class Window; + +namespace Decoration +{ + +class DecoratedWindowImpl : public QObject, public KDecoration3::DecoratedWindowPrivateV4 +{ + Q_OBJECT +public: + explicit DecoratedWindowImpl(Window *window, KDecoration3::DecoratedWindow *decoratedClient, KDecoration3::Decoration *decoration); + ~DecoratedWindowImpl() override; + QString caption() const override; + qreal height() const override; + QIcon icon() const override; + bool isActive() const override; + bool isCloseable() const override; + bool isKeepAbove() const override; + bool isKeepBelow() const override; + bool isExcludedFromCapture() const override; + bool isMaximizeable() const override; + bool isMaximized() const override; + bool isMaximizedHorizontally() const override; + bool isMaximizedVertically() const override; + bool isMinimizeable() const override; + bool isModal() const override; + bool isMoveable() const override; + bool isOnAllDesktops() const override; + bool isResizeable() const override; + bool isShadeable() const override; + bool isShaded() const override; + QPalette palette() const override; + QColor color(KDecoration3::ColorGroup group, KDecoration3::ColorRole role) const override; + bool providesContextHelp() const override; + QSizeF size() const override; + qreal width() const override; + QString windowClass() const override; + qreal scale() const override; + qreal nextScale() const override; + QString applicationMenuServiceName() const override; + QString applicationMenuObjectPath() const override; + + Qt::Edges adjacentScreenEdges() const override; + + bool hasApplicationMenu() const override; + bool isApplicationMenuActive() const override; + + void requestShowToolTip(const QString &text) override; + void requestHideToolTip() override; + void requestClose() override; + void requestContextHelp() override; + void requestToggleMaximization(Qt::MouseButtons buttons) override; + void requestMinimize() override; + void requestShowWindowMenu(const QRect &rect) override; + void requestShowApplicationMenu(const QRect &rect, int actionId) override; + void requestToggleKeepAbove() override; + void requestToggleKeepBelow() override; + void requestToggleExcludeFromCapture() override; + void requestToggleOnAllDesktops() override; + void requestToggleShade() override; + + void showApplicationMenu(int actionId) override; + + void popup(const KDecoration3::Positioner &positioner, QMenu *menu) override; + + Window *window() + { + return m_window; + } + KDecoration3::DecoratedWindow *decoratedWindow() + { + return KDecoration3::DecoratedWindowPrivate::window(); + } + +private Q_SLOTS: + void delayedRequestToggleMaximization(Options::WindowOperation operation); + +private: + Window *m_window; + QSizeF m_clientSize; + + QString m_toolTipText; + QTimer m_toolTipWakeUp; + QDeadlineTimer m_toolTipFallAsleep; + bool m_toolTipShowing = false; +}; + +} +} diff --git a/local/recipes/kde/kwin/source/src/decorations/decorationbridge.cpp b/local/recipes/kde/kwin/source/src/decorations/decorationbridge.cpp new file mode 100644 index 0000000000..24a8b6b9b0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/decorations/decorationbridge.cpp @@ -0,0 +1,292 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "decorationbridge.h" + +#include "decoratedwindow.h" +#include "decorations_logging.h" +#include "settings.h" +// KWin core +#include "wayland/server_decoration.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +// KDecoration +#include +#include +#include + +// Frameworks +#include +#include + +// Qt +#include +#include + +namespace KWin +{ +namespace Decoration +{ + +static const QString s_pluginName = QStringLiteral("org.kde.kdecoration3"); +static const QString s_configKeyName = QStringLiteral("org.kde.kdecoration2"); +static const QString s_defaultPlugin = QStringLiteral("org.kde.breeze"); +static const QString s_fallbackPlugin = QStringLiteral("org.kde.kwin.aurorae"); + +static void migrateAuroraeTheme() +{ + const QString themeName = kwinApp()->config()->group(s_configKeyName).readEntry("theme"); + if (!themeName.startsWith(QLatin1String("__aurorae__svg__"))) { + return; + } + + const QString pluginName = kwinApp()->config()->group(s_configKeyName).readEntry("library"); + if (pluginName != QLatin1String("org.kde.kwin.aurorae")) { + return; + } + + kwinApp()->config()->group(s_configKeyName).writeEntry("library", "org.kde.kwin.aurorae.v2"); +} + +DecorationBridge::DecorationBridge() + : m_factory(nullptr) + , m_showToolTips(false) + , m_settings() + , m_noPlugin(false) +{ + migrateAuroraeTheme(); + readDecorationOptions(); +} + +QString DecorationBridge::readPlugin() +{ + return kwinApp()->config()->group(s_configKeyName).readEntry("library", s_defaultPlugin); +} + +static bool readNoPlugin() +{ + return kwinApp()->config()->group(s_configKeyName).readEntry("NoPlugin", false); +} + +QString DecorationBridge::readTheme() const +{ + return kwinApp()->config()->group(s_configKeyName).readEntry("theme", m_defaultTheme); +} + +void DecorationBridge::readDecorationOptions() +{ + m_showToolTips = kwinApp()->config()->group(s_configKeyName).readEntry("ShowToolTips", true); +} + +bool DecorationBridge::hasPlugin() +{ + const DecorationBridge *bridge = workspace()->decorationBridge(); + if (!bridge) { + return false; + } + return !bridge->m_noPlugin && bridge->m_factory; +} + +void DecorationBridge::init() +{ + m_noPlugin = readNoPlugin(); + if (m_noPlugin) { + waylandServer()->decorationManager()->setDefaultMode(ServerSideDecorationManagerInterface::Mode::None); + return; + } + m_settings = std::make_shared(this); + + const QString pluginId = readPlugin(); + if (!initPlugin(pluginId)) { + if (s_defaultPlugin != pluginId) { + initPlugin(s_defaultPlugin); + } + if (!m_factory) { + if (s_fallbackPlugin != pluginId) { + initPlugin(s_fallbackPlugin); + } + } + } + + waylandServer()->decorationManager()->setDefaultMode(m_factory ? ServerSideDecorationManagerInterface::Mode::Server : ServerSideDecorationManagerInterface::Mode::None); +} + +bool DecorationBridge::initPlugin(const QString &pluginId) +{ + const KPluginMetaData metaData = KPluginMetaData::findPluginById(s_pluginName, pluginId); + if (!metaData.isValid()) { + qCWarning(KWIN_DECORATIONS) << "Could not locate decoration plugin" << pluginId; + return false; + } + qCDebug(KWIN_DECORATIONS) << "Trying to load decoration plugin: " << metaData.fileName(); + if (auto factoryResult = KPluginFactory::loadFactory(metaData)) { + m_factory.reset(factoryResult.plugin); + m_plugin = pluginId; + loadMetaData(metaData.rawData()); + return true; + } else { + qCWarning(KWIN_DECORATIONS) << "Error loading plugin:" << factoryResult.errorText; + return false; + } +} + +static void recreateDecorations() +{ + Workspace::self()->forEachWindow([](Window *window) { + window->invalidateDecoration(); + }); +} + +void DecorationBridge::reconfigure() +{ + readDecorationOptions(); + + if (m_noPlugin != readNoPlugin()) { + m_noPlugin = !m_noPlugin; + // no plugin setting changed + if (m_noPlugin) { + // decorations disabled now + m_plugin = QString(); + m_factory.reset(); + m_settings.reset(); + } else { + // decorations enabled now + init(); + } + recreateDecorations(); + return; + } + + const QString newPlugin = readPlugin(); + if (newPlugin != m_plugin) { + if (initPlugin(newPlugin)) { + recreateDecorations(); + // TODO: unload and destroy old plugin + } + } else { + // same plugin, but theme might have changed + const QString oldTheme = m_theme; + m_theme = readTheme(); + if (m_theme != oldTheme) { + recreateDecorations(); + } + } +} + +void DecorationBridge::loadMetaData(const QJsonObject &object) +{ + // reset all settings + m_recommendedBorderSize = QString(); + m_theme = QString(); + m_defaultTheme = QString(); + + // load the settings + const QJsonValue decoSettings = object.value(s_pluginName); + if (decoSettings.isUndefined()) { + // no settings + return; + } + const QVariantMap decoSettingsMap = decoSettings.toObject().toVariantMap(); + auto recBorderSizeIt = decoSettingsMap.find(QStringLiteral("recommendedBorderSize")); + if (recBorderSizeIt != decoSettingsMap.end()) { + m_recommendedBorderSize = recBorderSizeIt.value().toString(); + } + findTheme(decoSettingsMap); + + Q_EMIT metaDataLoaded(); +} + +void DecorationBridge::findTheme(const QVariantMap &map) +{ + auto it = map.find(QStringLiteral("themes")); + if (it == map.end()) { + return; + } + if (!it.value().toBool()) { + return; + } + it = map.find(QStringLiteral("defaultTheme")); + m_defaultTheme = it != map.end() ? it.value().toString() : QString(); + m_theme = readTheme(); +} + +std::unique_ptr DecorationBridge::createClient(KDecoration3::DecoratedWindow *client, KDecoration3::Decoration *decoration) +{ + return std::unique_ptr(new DecoratedWindowImpl(static_cast(decoration->parent()), client, decoration)); +} + +std::unique_ptr DecorationBridge::settings(KDecoration3::DecorationSettings *parent) +{ + return std::unique_ptr(new SettingsImpl(parent)); +} + +KDecoration3::Decoration *DecorationBridge::createDecoration(Window *window) +{ + if (m_noPlugin) { + return nullptr; + } + if (!m_factory) { + return nullptr; + } + QVariantMap args({{QStringLiteral("bridge"), QVariant::fromValue(this)}}); + + if (!m_theme.isEmpty()) { + args.insert(QStringLiteral("theme"), m_theme); + } + auto deco = m_factory->create(window, QVariantList{args}); + deco->setSettings(m_settings); + deco->create(); + deco->init(); + return deco; +} + +static QString settingsProperty(const QVariant &variant) +{ + if (QLatin1String(variant.typeName()) == QLatin1String("KDecoration3::BorderSize")) { + return QString::number(variant.toInt()); + } else if (QLatin1String(variant.typeName()) == QLatin1String("QList")) { + const auto &b = variant.value>(); + QString buffer; + for (auto it = b.begin(); it != b.end(); ++it) { + if (it != b.begin()) { + buffer.append(QStringLiteral(", ")); + } + buffer.append(QString::number(int(*it))); + } + return buffer; + } + return variant.toString(); +} + +QString DecorationBridge::supportInformation() const +{ + QString b; + if (m_noPlugin) { + b.append(QStringLiteral("Decorations are disabled")); + } else { + b.append(QStringLiteral("Plugin: %1\n").arg(m_plugin)); + b.append(QStringLiteral("Theme: %1\n").arg(m_theme)); + b.append(QStringLiteral("Plugin recommends border size: %1\n").arg(m_recommendedBorderSize.isNull() ? "No" : m_recommendedBorderSize)); + const QMetaObject *metaOptions = m_settings->metaObject(); + for (int i = 0; i < metaOptions->propertyCount(); ++i) { + const QMetaProperty property = metaOptions->property(i); + if (QLatin1String(property.name()) == QLatin1String("objectName")) { + continue; + } + b.append(QStringLiteral("%1: %2\n").arg(property.name(), settingsProperty(m_settings->property(property.name())))); + } + } + return b; +} + +} // Decoration +} // KWin + +#include "moc_decorationbridge.cpp" diff --git a/local/recipes/kde/kwin/source/src/decorations/decorationbridge.h b/local/recipes/kde/kwin/source/src/decorations/decorationbridge.h new file mode 100644 index 0000000000..e06a131134 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/decorations/decorationbridge.h @@ -0,0 +1,82 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "kwin_export.h" +#include +#include + +class KPluginFactory; +namespace KDecoration3 +{ +class DecorationSettings; +} + +namespace KWin +{ + +class Window; + +namespace Decoration +{ + +class KWIN_EXPORT DecorationBridge : public KDecoration3::DecorationBridge +{ + Q_OBJECT +public: + explicit DecorationBridge(); + + static bool hasPlugin(); + + void init(); + KDecoration3::Decoration *createDecoration(Window *window); + + std::unique_ptr createClient(KDecoration3::DecoratedWindow *client, KDecoration3::Decoration *decoration) override; + std::unique_ptr settings(KDecoration3::DecorationSettings *parent) override; + + QString recommendedBorderSize() const + { + return m_recommendedBorderSize; + } + + bool showToolTips() const + { + return m_showToolTips; + } + + void reconfigure(); + + const std::shared_ptr &settings() const + { + return m_settings; + } + + QString supportInformation() const; + +Q_SIGNALS: + void metaDataLoaded(); + +private: + QString readPlugin(); + void loadMetaData(const QJsonObject &object); + void findTheme(const QVariantMap &map); + bool initPlugin(const QString &pluginId); + QString readTheme() const; + void readDecorationOptions(); + std::unique_ptr m_factory; + bool m_showToolTips; + QString m_recommendedBorderSize; + QString m_plugin; + QString m_defaultTheme; + QString m_theme; + std::shared_ptr m_settings; + bool m_noPlugin; +}; +} // Decoration +} // KWin diff --git a/local/recipes/kde/kwin/source/src/decorations/decorationpalette.cpp b/local/recipes/kde/kwin/source/src/decorations/decorationpalette.cpp new file mode 100644 index 0000000000..f4c68a3dde --- /dev/null +++ b/local/recipes/kde/kwin/source/src/decorations/decorationpalette.cpp @@ -0,0 +1,163 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2014 Hugo Pereira Da Costa + SPDX-FileCopyrightText: 2015 Mika Allan Rauhala + SPDX-FileCopyrightText: 2020 Carson Black + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "decorationpalette.h" + +#include + +#include +#include + +namespace KWin +{ +namespace Decoration +{ + +DecorationPalette::DecorationPalette(const QString &colorScheme) + : m_colorScheme(colorScheme != QStringLiteral("kdeglobals") ? colorScheme : QString()) +{ + if (m_colorScheme.isEmpty()) { + m_colorSchemeConfig = KSharedConfig::openConfig(m_colorScheme, KConfig::FullConfig); + } else { + m_colorSchemeConfig = KSharedConfig::openConfig(m_colorScheme, KConfig::SimpleConfig); + } + m_watcher = KConfigWatcher::create(m_colorSchemeConfig); + + connect(m_watcher.data(), &KConfigWatcher::configChanged, this, &DecorationPalette::update); + + update(); +} + +bool DecorationPalette::isValid() const +{ + return true; +} + +QColor DecorationPalette::color(KDecoration3::ColorGroup group, KDecoration3::ColorRole role) const +{ + using KDecoration3::ColorGroup; + using KDecoration3::ColorRole; + + if (m_legacyColors.has_value()) { + switch (role) { + case ColorRole::Frame: + switch (group) { + case ColorGroup::Active: + return m_legacyColors->activeFrameColor; + case ColorGroup::Inactive: + return m_legacyColors->inactiveFrameColor; + default: + return QColor(); + } + case ColorRole::TitleBar: + switch (group) { + case ColorGroup::Active: + return m_legacyColors->activeTitleBarColor; + case ColorGroup::Inactive: + return m_legacyColors->inactiveTitleBarColor; + default: + return QColor(); + } + case ColorRole::Foreground: + switch (group) { + case ColorGroup::Active: + return m_legacyColors->activeForegroundColor; + case ColorGroup::Inactive: + return m_legacyColors->inactiveForegroundColor; + case ColorGroup::Warning: + return m_legacyColors->warningForegroundColor; + default: + return QColor(); + } + default: + return QColor(); + } + } + + switch (role) { + case ColorRole::Frame: + switch (group) { + case ColorGroup::Active: + return m_colors.active.background().color(); + case ColorGroup::Inactive: + return m_colors.inactive.background().color(); + default: + return QColor(); + } + case ColorRole::TitleBar: + switch (group) { + case ColorGroup::Active: + return m_colors.active.background().color(); + case ColorGroup::Inactive: + return m_colors.inactive.background().color(); + default: + return QColor(); + } + case ColorRole::Foreground: + switch (group) { + case ColorGroup::Active: + return m_colors.active.foreground().color(); + case ColorGroup::Inactive: + return m_colors.inactive.foreground().color(); + case ColorGroup::Warning: + return m_colors.inactive.foreground(KColorScheme::ForegroundRole::NegativeText).color(); + default: + return QColor(); + } + default: + return QColor(); + } +} + +QPalette DecorationPalette::palette() const +{ + return m_palette; +} + +void DecorationPalette::update() +{ + m_colorSchemeConfig->sync(); + m_palette = KColorScheme::createApplicationPalette(m_colorSchemeConfig); + + if (KColorScheme::isColorSetSupported(m_colorSchemeConfig, KColorScheme::Header)) { + m_colors.active = KColorScheme(QPalette::Normal, KColorScheme::Header, m_colorSchemeConfig); + m_colors.inactive = KColorScheme(QPalette::Inactive, KColorScheme::Header, m_colorSchemeConfig); + m_legacyColors.reset(); + } else { + KConfigGroup wmConfig(m_colorSchemeConfig, QStringLiteral("WM")); + + if (!wmConfig.exists()) { + m_colors.active = KColorScheme(QPalette::Normal, KColorScheme::Window, m_colorSchemeConfig); + m_colors.inactive = KColorScheme(QPalette::Inactive, KColorScheme::Window, m_colorSchemeConfig); + m_legacyColors.reset(); + return; + } + + m_legacyColors = LegacyColors{}; + m_legacyColors->activeFrameColor = wmConfig.readEntry("frame", m_palette.color(QPalette::Active, QPalette::Window)); + m_legacyColors->inactiveFrameColor = wmConfig.readEntry("inactiveFrame", m_legacyColors->activeFrameColor); + m_legacyColors->activeTitleBarColor = wmConfig.readEntry("activeBackground", m_palette.color(QPalette::Active, QPalette::Highlight)); + m_legacyColors->inactiveTitleBarColor = wmConfig.readEntry("inactiveBackground", m_legacyColors->inactiveTitleBarColor); + m_legacyColors->activeForegroundColor = wmConfig.readEntry("activeForeground", m_palette.color(QPalette::Active, QPalette::HighlightedText)); + m_legacyColors->inactiveForegroundColor = wmConfig.readEntry("inactiveForeground", m_legacyColors->activeForegroundColor.darker()); + + KConfigGroup windowColorsConfig(m_colorSchemeConfig, QStringLiteral("Colors:Window")); + m_legacyColors->warningForegroundColor = windowColorsConfig.readEntry("ForegroundNegative", QColor(237, 21, 2)); + } + + Q_EMIT changed(); +} + +} +} + +#include "moc_decorationpalette.cpp" diff --git a/local/recipes/kde/kwin/source/src/decorations/decorationpalette.h b/local/recipes/kde/kwin/source/src/decorations/decorationpalette.h new file mode 100644 index 0000000000..f815647936 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/decorations/decorationpalette.h @@ -0,0 +1,74 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2014 Hugo Pereira Da Costa + SPDX-FileCopyrightText: 2015 Mika Allan Rauhala + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +namespace KWin +{ +namespace Decoration +{ + +class DecorationPalette : public QObject +{ + Q_OBJECT +public: + DecorationPalette(const QString &colorScheme); + + bool isValid() const; + + QColor color(KDecoration3::ColorGroup group, KDecoration3::ColorRole role) const; + QPalette palette() const; + +Q_SIGNALS: + void changed(); + +private: + void update(); + + QString m_colorScheme; + KConfigWatcher::Ptr m_watcher; + + struct LegacyColors + { + QColor activeTitleBarColor; + QColor inactiveTitleBarColor; + + QColor activeFrameColor; + QColor inactiveFrameColor; + + QColor activeForegroundColor; + QColor inactiveForegroundColor; + QColor warningForegroundColor; + }; + + struct ModernColors + { + KColorScheme active; + KColorScheme inactive; + }; + + KSharedConfig::Ptr m_colorSchemeConfig; + QPalette m_palette; + ModernColors m_colors; + std::optional m_legacyColors; +}; + +} +} diff --git a/local/recipes/kde/kwin/source/src/decorations/decorations_logging.cpp b/local/recipes/kde/kwin/source/src/decorations/decorations_logging.cpp new file mode 100644 index 0000000000..9f49bcda75 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/decorations/decorations_logging.cpp @@ -0,0 +1,10 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "decorations_logging.h" +Q_LOGGING_CATEGORY(KWIN_DECORATIONS, "kwin_decorations", QtWarningMsg) diff --git a/local/recipes/kde/kwin/source/src/decorations/decorations_logging.h b/local/recipes/kde/kwin/source/src/decorations/decorations_logging.h new file mode 100644 index 0000000000..cae90c91cb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/decorations/decorations_logging.h @@ -0,0 +1,12 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include +#include +Q_DECLARE_LOGGING_CATEGORY(KWIN_DECORATIONS) diff --git a/local/recipes/kde/kwin/source/src/decorations/settings.cpp b/local/recipes/kde/kwin/source/src/decorations/settings.cpp new file mode 100644 index 0000000000..75388366e6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/decorations/settings.cpp @@ -0,0 +1,186 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "settings.h" +#include "appmenu.h" +#include "decorationbridge.h" +#include "virtualdesktops.h" +#include "workspace.h" + +#include +#include + +#include + +namespace KWin +{ +namespace Decoration +{ +SettingsImpl::SettingsImpl(KDecoration3::DecorationSettings *parent) + : QObject() + , DecorationSettingsPrivateV2(parent) + , m_borderSize(KDecoration3::BorderSize::Normal) +{ + readSettings(); + + connect(VirtualDesktopManager::self(), &VirtualDesktopManager::countChanged, this, [parent](uint previous, uint current) { + if (previous != 1 && current != 1) { + return; + } + Q_EMIT parent->onAllDesktopsAvailableChanged(current > 1); + }); + connect(Workspace::self(), &Workspace::configChanged, this, &SettingsImpl::readSettings); + connect(Workspace::self()->decorationBridge(), &DecorationBridge::metaDataLoaded, this, &SettingsImpl::readSettings); +} + +SettingsImpl::~SettingsImpl() = default; + +bool SettingsImpl::isAlphaChannelSupported() const +{ + return true; +} + +bool SettingsImpl::isOnAllDesktopsAvailable() const +{ + return VirtualDesktopManager::self()->count() > 1; +} + +bool SettingsImpl::isCloseOnDoubleClickOnMenu() const +{ + return m_closeDoubleClickMenu; +} + +bool SettingsImpl::isAlwaysShowExcludeFromCapture() const +{ + return m_alwaysShowExcludeFromCapture; +} + +static QHash s_buttonNames; +static void initButtons() +{ + if (!s_buttonNames.isEmpty()) { + return; + } + s_buttonNames[KDecoration3::DecorationButtonType::Menu] = QChar('M'); + s_buttonNames[KDecoration3::DecorationButtonType::ApplicationMenu] = QChar('N'); + s_buttonNames[KDecoration3::DecorationButtonType::OnAllDesktops] = QChar('S'); + s_buttonNames[KDecoration3::DecorationButtonType::ContextHelp] = QChar('H'); + s_buttonNames[KDecoration3::DecorationButtonType::Minimize] = QChar('I'); + s_buttonNames[KDecoration3::DecorationButtonType::Maximize] = QChar('A'); + s_buttonNames[KDecoration3::DecorationButtonType::Close] = QChar('X'); + s_buttonNames[KDecoration3::DecorationButtonType::KeepAbove] = QChar('F'); + s_buttonNames[KDecoration3::DecorationButtonType::KeepBelow] = QChar('B'); + s_buttonNames[KDecoration3::DecorationButtonType::ExcludeFromCapture] = QChar('E'); + s_buttonNames[KDecoration3::DecorationButtonType::Spacer] = QChar('_'); +} + +static QString buttonsToString(const QList &buttons) +{ + auto buttonToString = [](KDecoration3::DecorationButtonType button) -> QChar { + const auto it = s_buttonNames.constFind(button); + if (it != s_buttonNames.constEnd()) { + return it.value(); + } + return QChar(); + }; + QString ret; + for (auto button : buttons) { + ret.append(buttonToString(button)); + } + return ret; +} + +QList SettingsImpl::readDecorationButtons(const KConfigGroup &config, + const char *key, + const QList &defaultValue) const +{ + initButtons(); + auto buttonsFromString = [](const QString &buttons) -> QList { + QList ret; + for (auto it = buttons.begin(); it != buttons.end(); ++it) { + for (auto it2 = s_buttonNames.constBegin(); it2 != s_buttonNames.constEnd(); ++it2) { + if (it2.value() == (*it)) { + ret << it2.key(); + } + } + } + return ret; + }; + return buttonsFromString(config.readEntry(key, buttonsToString(defaultValue))); +} + +static KDecoration3::BorderSize stringToSize(const QString &name) +{ + static const QMap s_sizes = QMap({{QStringLiteral("None"), KDecoration3::BorderSize::None}, + {QStringLiteral("NoSides"), KDecoration3::BorderSize::NoSides}, + {QStringLiteral("Tiny"), KDecoration3::BorderSize::Tiny}, + {QStringLiteral("Normal"), KDecoration3::BorderSize::Normal}, + {QStringLiteral("Large"), KDecoration3::BorderSize::Large}, + {QStringLiteral("VeryLarge"), KDecoration3::BorderSize::VeryLarge}, + {QStringLiteral("Huge"), KDecoration3::BorderSize::Huge}, + {QStringLiteral("VeryHuge"), KDecoration3::BorderSize::VeryHuge}, + {QStringLiteral("Oversized"), KDecoration3::BorderSize::Oversized}}); + auto it = s_sizes.constFind(name); + if (it == s_sizes.constEnd()) { + // non sense values are interpreted just like normal + return KDecoration3::BorderSize::Normal; + } + return it.value(); +} + +void SettingsImpl::readSettings() +{ + KConfigGroup config = kwinApp()->config()->group(QStringLiteral("org.kde.kdecoration2")); + const auto &left = readDecorationButtons(config, "ButtonsOnLeft", QList({KDecoration3::DecorationButtonType::Menu, KDecoration3::DecorationButtonType::OnAllDesktops, KDecoration3::DecorationButtonType::ExcludeFromCapture})); + if (left != m_leftButtons) { + m_leftButtons = left; + Q_EMIT decorationSettings()->decorationButtonsLeftChanged(m_leftButtons); + } + const auto &right = readDecorationButtons(config, "ButtonsOnRight", QList({KDecoration3::DecorationButtonType::ContextHelp, KDecoration3::DecorationButtonType::Minimize, KDecoration3::DecorationButtonType::Maximize, KDecoration3::DecorationButtonType::Close})); + if (right != m_rightButtons) { + m_rightButtons = right; + Q_EMIT decorationSettings()->decorationButtonsRightChanged(m_rightButtons); + } + Workspace::self()->applicationMenu()->setViewEnabled(left.contains(KDecoration3::DecorationButtonType::ApplicationMenu) || right.contains(KDecoration3::DecorationButtonType::ApplicationMenu)); + const bool close = config.readEntry("CloseOnDoubleClickOnMenu", false); + if (close != m_closeDoubleClickMenu) { + m_closeDoubleClickMenu = close; + Q_EMIT decorationSettings()->closeOnDoubleClickOnMenuChanged(m_closeDoubleClickMenu); + } + + const bool alwaysShowExcludeFromCapture = config.readEntry("AlwaysShowExcludeFromCapture", false); + if (alwaysShowExcludeFromCapture != m_alwaysShowExcludeFromCapture) { + m_alwaysShowExcludeFromCapture = alwaysShowExcludeFromCapture; + Q_EMIT decorationSettings()->alwaysShowExcludeFromCaptureChanged(m_alwaysShowExcludeFromCapture); + } + + m_autoBorderSize = config.readEntry("BorderSizeAuto", true); + + auto size = stringToSize(config.readEntry("BorderSize", QStringLiteral("Normal"))); + if (m_autoBorderSize) { + /* Falls back to Normal border size, if the plugin does not provide a valid recommendation. */ + size = stringToSize(Workspace::self()->decorationBridge()->recommendedBorderSize()); + } + if (size != m_borderSize) { + m_borderSize = size; + Q_EMIT decorationSettings()->borderSizeChanged(m_borderSize); + } + const QFont font = QFontDatabase::systemFont(QFontDatabase::TitleFont); + if (font != m_font) { + m_font = font; + Q_EMIT decorationSettings()->fontChanged(m_font); + } + + Q_EMIT decorationSettings()->reconfigured(); +} + +} +} + +#include "moc_settings.cpp" diff --git a/local/recipes/kde/kwin/source/src/decorations/settings.h b/local/recipes/kde/kwin/source/src/decorations/settings.h new file mode 100644 index 0000000000..527eae9c54 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/decorations/settings.h @@ -0,0 +1,63 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include + +#include + +class KConfigGroup; + +namespace KWin +{ +namespace Decoration +{ + +class SettingsImpl : public QObject, public KDecoration3::DecorationSettingsPrivateV2 +{ + Q_OBJECT +public: + explicit SettingsImpl(KDecoration3::DecorationSettings *parent); + ~SettingsImpl() override; + bool isAlphaChannelSupported() const override; + bool isOnAllDesktopsAvailable() const override; + bool isCloseOnDoubleClickOnMenu() const override; + bool isAlwaysShowExcludeFromCapture() const override; + KDecoration3::BorderSize borderSize() const override + { + return m_borderSize; + } + QList decorationButtonsLeft() const override + { + return m_leftButtons; + } + QList decorationButtonsRight() const override + { + return m_rightButtons; + } + QFont font() const override + { + return m_font; + } + +private: + void readSettings(); + QList readDecorationButtons(const KConfigGroup &config, + const char *key, + const QList &defaultValue) const; + QList m_leftButtons; + QList m_rightButtons; + KDecoration3::BorderSize m_borderSize; + bool m_autoBorderSize = true; + bool m_closeDoubleClickMenu = false; + bool m_alwaysShowExcludeFromCapture = false; + QFont m_font; +}; +} // Decoration +} // KWin diff --git a/local/recipes/kde/kwin/source/src/dpmsinputeventfilter.cpp b/local/recipes/kde/kwin/source/src/dpmsinputeventfilter.cpp new file mode 100644 index 0000000000..c88d643db3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/dpmsinputeventfilter.cpp @@ -0,0 +1,202 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "dpmsinputeventfilter.h" +#include "core/backendoutput.h" +#include "core/outputbackend.h" +#include "input_event.h" +#include "main.h" +#include "utils/keys.h" +#include "utils/proximitysensor.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include + +namespace KWin +{ + +DpmsInputEventFilter::DpmsInputEventFilter() + : InputEventFilter(InputFilterOrder::Dpms) +{ + KSharedConfig::Ptr kwinSettings = kwinApp()->config(); + m_enableDoubleTap = kwinSettings->group(QStringLiteral("Wayland")).readEntry("DoubleTapWakeup", true); + + if (m_enableDoubleTap) { + m_sensor = std::make_unique(); + m_sensor->setEnabled(true); + + updateProximitySensor(); + connect(m_sensor.get(), &ProximitySensor::readingReceived, this, &DpmsInputEventFilter::updateProximitySensor); + } +} + +DpmsInputEventFilter::~DpmsInputEventFilter() +{ +} + +void DpmsInputEventFilter::updateProximitySensor() +{ + if (m_sensor->reading()) { + m_proximityClose = *m_sensor->reading(); + } +} + +bool DpmsInputEventFilter::pointerMotion(PointerMotionEvent *event) +{ + if (!event->warp) { + // The intention is to wake the screen on user interactions + // warp events aren't user interactions, so ignore them. + notify(); + } + return true; +} + +bool DpmsInputEventFilter::pointerButton(PointerButtonEvent *event) +{ + notify(); + return true; +} + +bool DpmsInputEventFilter::pointerAxis(PointerAxisEvent *event) +{ + notify(); + return true; +} + +bool DpmsInputEventFilter::keyboardKey(KeyboardKeyEvent *event) +{ + if (isMediaKey(event->key)) { + // don't wake up the screens for media or volume keys + return false; + } + + // Wakeup key is sent by either ACPI driver or other drivers when + // system is resumed from sleep but that is not necessarily wakeup intended + // to do full-scale dpms on event. Let system wake-up without display, only + // wake system up if we get actual keyboard key. + if (event->key == Qt::Key::Key_WakeUp) { + return false; + } + + if (event->state == KeyboardKeyState::Pressed) { + notify(); + } else if (event->state == KeyboardKeyState::Released) { + return false; + } + return true; +} + +bool DpmsInputEventFilter::touchDown(TouchDownEvent *event) +{ + if (m_enableDoubleTap) { + if (m_touchPoints.isEmpty()) { + if (!m_doubleTapTimer.isValid()) { + // this is the first tap + m_doubleTapTimer.start(); + } else { + if (m_doubleTapTimer.elapsed() < qApp->doubleClickInterval()) { + m_secondTap = true; + } else { + // took too long. Let's consider it a new click + m_doubleTapTimer.restart(); + } + } + } else { + // not a double tap + m_doubleTapTimer.invalidate(); + m_secondTap = false; + } + m_touchPoints << event->id; + } + return true; +} + +bool DpmsInputEventFilter::touchUp(TouchUpEvent *event) +{ + if (m_enableDoubleTap) { + m_touchPoints.removeAll(event->id); + if (m_touchPoints.isEmpty() && m_doubleTapTimer.isValid() && m_secondTap) { + // if device in pocket, do not wake device up + if (m_doubleTapTimer.elapsed() < qApp->doubleClickInterval() && !m_proximityClose) { + notify(); + } + m_doubleTapTimer.invalidate(); + m_secondTap = false; + } + } + return true; +} + +bool DpmsInputEventFilter::touchMotion(TouchMotionEvent *event) +{ + // ignore the event + return true; +} + +bool DpmsInputEventFilter::tabletToolProximityEvent(TabletToolProximityEvent *event) +{ + return true; +} + +bool DpmsInputEventFilter::tabletToolAxisEvent(TabletToolAxisEvent *event) +{ + return true; +} + +bool DpmsInputEventFilter::tabletToolTipEvent(TabletToolTipEvent *event) +{ + if (event->type == TabletToolTipEvent::Press) { + // Only wake when the tool is actually pressed down not just hovered over the tablet + notify(); + } + return true; +} + +bool DpmsInputEventFilter::tabletToolButtonEvent(TabletToolButtonEvent *event) +{ + if (event->pressed) { + notify(); + } + return true; +} + +bool DpmsInputEventFilter::tabletPadButtonEvent(TabletPadButtonEvent *event) +{ + if (event->pressed) { + notify(); + } + return true; +} + +bool DpmsInputEventFilter::tabletPadStripEvent(TabletPadStripEvent *event) +{ + notify(); + return true; +} + +bool DpmsInputEventFilter::tabletPadRingEvent(TabletPadRingEvent *event) +{ + notify(); + return true; +} + +bool DpmsInputEventFilter::tabletPadDialEvent(TabletPadDialEvent *event) +{ + notify(); + return true; +} + +void DpmsInputEventFilter::notify() +{ + workspace()->requestDpmsState(Workspace::DpmsState::On); +} + +} +#include "moc_dpmsinputeventfilter.cpp" diff --git a/local/recipes/kde/kwin/source/src/dpmsinputeventfilter.h b/local/recipes/kde/kwin/source/src/dpmsinputeventfilter.h new file mode 100644 index 0000000000..369e998547 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/dpmsinputeventfilter.h @@ -0,0 +1,60 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "input.h" + +#include +#include + +#include + +namespace KWin +{ + +class DrmBackend; +class ProximitySensor; + +class KWIN_EXPORT DpmsInputEventFilter : public QObject, public InputEventFilter +{ + Q_OBJECT +public: + DpmsInputEventFilter(); + ~DpmsInputEventFilter() override; + + bool pointerMotion(PointerMotionEvent *event) override; + bool pointerButton(PointerButtonEvent *event) override; + bool pointerAxis(PointerAxisEvent *event) override; + bool keyboardKey(KeyboardKeyEvent *event) override; + bool touchDown(TouchDownEvent *event) override; + bool touchMotion(TouchMotionEvent *event) override; + bool touchUp(TouchUpEvent *event) override; + bool tabletToolProximityEvent(TabletToolProximityEvent *event) override; + bool tabletToolAxisEvent(TabletToolAxisEvent *event) override; + bool tabletToolTipEvent(TabletToolTipEvent *event) override; + bool tabletToolButtonEvent(TabletToolButtonEvent *event) override; + bool tabletPadButtonEvent(TabletPadButtonEvent *event) override; + bool tabletPadStripEvent(TabletPadStripEvent *event) override; + bool tabletPadRingEvent(TabletPadRingEvent *event) override; + bool tabletPadDialEvent(TabletPadDialEvent *event) override; + +private Q_SLOTS: + void updateProximitySensor(); + +private: + void notify(); + QElapsedTimer m_doubleTapTimer; + QList m_touchPoints; + std::unique_ptr m_sensor; + + bool m_secondTap = false; + bool m_enableDoubleTap = false; + bool m_proximityClose = false; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/effect/anidata.cpp b/local/recipes/kde/kwin/source/src/effect/anidata.cpp new file mode 100644 index 0000000000..c8e34a543b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/anidata.cpp @@ -0,0 +1,110 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Thomas Lübking + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "effect/anidata_p.h" +#include "effect/effecthandler.h" + +namespace KWin +{ + +QDebug operator<<(QDebug dbg, const KWin::AniData &a) +{ + dbg.nospace() << a.debugInfo(); + return dbg.space(); +} + +FullScreenEffectLock::FullScreenEffectLock(Effect *effect) +{ + effects->setActiveFullScreenEffect(effect); +} + +FullScreenEffectLock::~FullScreenEffectLock() +{ + effects->setActiveFullScreenEffect(nullptr); +} + +AniData::AniData() + : attribute(AnimationEffect::Opacity) + , customCurve(0) // Linear + , meta(0) + , frozenTime(-1) + , startTime(0) + , waitAtSource(false) + , keepAlive(true) +{ +} + +AniData::AniData(AnimationEffect::Attribute a, int meta_, const FPx2 &to_, + int delay, const FPx2 &from_, bool waitAtSource_, + const std::shared_ptr &fullScreenEffectLock, bool keepAlive, + GLShader *shader) + : attribute(a) + , from(from_) + , to(to_) + , meta(meta_) + , frozenTime(-1) + , startTime(AnimationEffect::clock() + delay) + , fullScreenEffectLock(fullScreenEffectLock) + , waitAtSource(waitAtSource_) + , keepAlive(keepAlive) + , shader(shader) +{ +} + +bool AniData::isActive() const +{ + if (!timeLine.done()) { + return true; + } + + if (timeLine.direction() == TimeLine::Backward) { + return !(terminationFlags & AnimationEffect::TerminateAtSource); + } + + return !(terminationFlags & AnimationEffect::TerminateAtTarget); +} + +static QString attributeString(KWin::AnimationEffect::Attribute attribute) +{ + switch (attribute) { + case KWin::AnimationEffect::Opacity: + return QStringLiteral("Opacity"); + case KWin::AnimationEffect::Brightness: + return QStringLiteral("Brightness"); + case KWin::AnimationEffect::Saturation: + return QStringLiteral("Saturation"); + case KWin::AnimationEffect::Scale: + return QStringLiteral("Scale"); + case KWin::AnimationEffect::Translation: + return QStringLiteral("Translation"); + case KWin::AnimationEffect::Rotation: + return QStringLiteral("Rotation"); + case KWin::AnimationEffect::Position: + return QStringLiteral("Position"); + case KWin::AnimationEffect::Size: + return QStringLiteral("Size"); + case KWin::AnimationEffect::Clip: + return QStringLiteral("Clip"); + default: + return QStringLiteral(" "); + } +} + +QString AniData::debugInfo() const +{ + return (QLatin1String("Animation: ") + attributeString(attribute) + + QLatin1String("\n From: ") + from.toString() + + QLatin1String("\n To: ") + to.toString() + + QLatin1String("\n Started: ") + QString::number(AnimationEffect::clock() - startTime) + QLatin1String("ms ago\n") + + QLatin1String(" Duration: ") + QString::number(timeLine.duration().count()) + QLatin1String("ms\n") + + QLatin1String(" Passed: ") + QString::number(timeLine.elapsed().count()) + QLatin1String("ms\n")); +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/effect/anidata_p.h b/local/recipes/kde/kwin/source/src/effect/anidata_p.h new file mode 100644 index 0000000000..206cb7feb4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/anidata_p.h @@ -0,0 +1,73 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Thomas Lübking + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/animationeffect.h" +#include "effect/effectwindow.h" +#include "effect/timeline.h" +#include "scene/item.h" + +#include + +namespace KWin +{ + +/** + * Wraps effects->setActiveFullScreenEffect for the duration of it's lifespan + */ +class FullScreenEffectLock +{ +public: + FullScreenEffectLock(Effect *effect); + ~FullScreenEffectLock(); + +private: + Q_DISABLE_COPY(FullScreenEffectLock) +}; + +class KWIN_EXPORT AniData +{ +public: + AniData(); + AniData(AnimationEffect::Attribute a, int meta, const FPx2 &to, + int delay, const FPx2 &from, bool waitAtSource, + const std::shared_ptr &lock = nullptr, + bool keepAlive = true, GLShader *shader = nullptr); + + bool isActive() const; + + inline bool isOneDimensional() const + { + return from[0] == from[1] && to[0] == to[1]; + } + + quint64 id{0}; + QString debugInfo() const; + AnimationEffect::Attribute attribute; + int customCurve; + FPx2 from, to; + TimeLine timeLine; + uint meta; + qint64 frozenTime; + qint64 startTime; + std::shared_ptr fullScreenEffectLock; + bool waitAtSource; + bool keepAlive; + EffectWindowDeletedRef deletedRef; + EffectWindowVisibleRef visibleRef; + AnimationEffect::TerminationFlags terminationFlags; + GLShader *shader{nullptr}; + ItemEffect itemEffect; +}; + +} // namespace + +QDebug operator<<(QDebug dbg, const KWin::AniData &a); diff --git a/local/recipes/kde/kwin/source/src/effect/animationeffect.cpp b/local/recipes/kde/kwin/source/src/effect/animationeffect.cpp new file mode 100644 index 0000000000..f01aac8a74 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/animationeffect.cpp @@ -0,0 +1,993 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Thomas Lübking + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "effect/animationeffect.h" +#include "core/renderviewport.h" +#include "effect/anidata_p.h" +#include "effect/effecthandler.h" +#include "opengl/glshader.h" +#include "opengl/glshadermanager.h" +#include "scene/windowitem.h" + +#include +#include +#include +#include + +namespace KWin +{ + +QDebug operator<<(QDebug dbg, const KWin::FPx2 &fpx2) +{ + dbg.nospace() << fpx2[0] << "," << fpx2[1] << QString(fpx2.isValid() ? QStringLiteral(" (valid)") : QStringLiteral(" (invalid)")); + return dbg.space(); +} + +QElapsedTimer AnimationEffect::s_clock; + +class AnimationEffectPrivate +{ +public: + AnimationEffectPrivate() + { + m_animationsTouched = m_isInitialized = false; + m_justEndedAnimation = 0; + } + AnimationEffect::AniMap m_animations; + static quint64 m_animCounter; + quint64 m_justEndedAnimation; // protect against cancel + std::weak_ptr m_fullScreenEffectLock; + bool m_needSceneRepaint, m_animationsTouched, m_isInitialized; +}; + +quint64 AnimationEffectPrivate::m_animCounter = 0; + +AnimationEffect::AnimationEffect() + : CrossFadeEffect() + , d(std::make_unique()) +{ + if (!s_clock.isValid()) { + s_clock.start(); + } + /* this is the same as the QTimer::singleShot(0, SLOT(init())) kludge + * deferring the init and esp. the connection to the windowClosed slot */ + QMetaObject::invokeMethod(this, &AnimationEffect::init, Qt::QueuedConnection); +} + +AnimationEffect::~AnimationEffect() +{ + if (d->m_isInitialized) { + disconnect(effects, &EffectsHandler::windowDeleted, this, &AnimationEffect::_windowDeleted); + } + d->m_animations.clear(); +} + +void AnimationEffect::init() +{ + if (d->m_isInitialized) { + return; // not more than once, please + } + d->m_isInitialized = true; + /* by connecting the signal from a slot AFTER the inheriting class constructor had the chance to + * connect it we can provide auto-referencing of animated and closed windows, since at the time + * our slot will be called, the slot of the subclass has been (SIGNAL/SLOT connections are FIFO) + * and has pot. started an animation so we have the window in our hash :) */ + connect(effects, &EffectsHandler::windowClosed, this, &AnimationEffect::_windowClosed); + connect(effects, &EffectsHandler::windowDeleted, this, &AnimationEffect::_windowDeleted); +} + +bool AnimationEffect::isActive() const +{ + return !d->m_animations.empty(); +} + +#define RELATIVE_XY(_FIELD_) const bool relative[2] = {static_cast(metaData(Relative##_FIELD_##X, meta)), \ + static_cast(metaData(Relative##_FIELD_##Y, meta))} + +void AnimationEffect::validate(Attribute a, uint &meta, FPx2 *from, FPx2 *to, const EffectWindow *w) const +{ + if (a < NonFloatBase) { + if (a == Scale) { + QRectF area = effects->clientArea(ScreenArea, w); + if (from && from->isValid()) { + RELATIVE_XY(Source); + from->set(relative[0] ? (*from)[0] * area.width() / w->width() : (*from)[0], + relative[1] ? (*from)[1] * area.height() / w->height() : (*from)[1]); + } + if (to && to->isValid()) { + RELATIVE_XY(Target); + to->set(relative[0] ? (*to)[0] * area.width() / w->width() : (*to)[0], + relative[1] ? (*to)[1] * area.height() / w->height() : (*to)[1]); + } + } else if (a == Rotation) { + if (from && !from->isValid()) { + setMetaData(SourceAnchor, metaData(TargetAnchor, meta), meta); + from->set(0.0, 0.0); + } + if (to && !to->isValid()) { + setMetaData(TargetAnchor, metaData(SourceAnchor, meta), meta); + to->set(0.0, 0.0); + } + } + if (from && !from->isValid()) { + from->set(1.0, 1.0); + } + if (to && !to->isValid()) { + to->set(1.0, 1.0); + } + + } else if (a == Position) { + QRectF area = effects->clientArea(ScreenArea, w); + QPointF pt = w->frameGeometry().bottomRight(); // cannot be < 0 ;-) + if (from) { + if (from->isValid()) { + RELATIVE_XY(Source); + from->set(relative[0] ? area.x() + (*from)[0] * area.width() : (*from)[0], + relative[1] ? area.y() + (*from)[1] * area.height() : (*from)[1]); + } else { + from->set(pt.x(), pt.y()); + setMetaData(SourceAnchor, AnimationEffect::Bottom | AnimationEffect::Right, meta); + } + } + + if (to) { + if (to->isValid()) { + RELATIVE_XY(Target); + to->set(relative[0] ? area.x() + (*to)[0] * area.width() : (*to)[0], + relative[1] ? area.y() + (*to)[1] * area.height() : (*to)[1]); + } else { + to->set(pt.x(), pt.y()); + setMetaData(TargetAnchor, AnimationEffect::Bottom | AnimationEffect::Right, meta); + } + } + + } else if (a == Size) { + QRectF area = effects->clientArea(ScreenArea, w); + if (from) { + if (from->isValid()) { + RELATIVE_XY(Source); + from->set(relative[0] ? (*from)[0] * area.width() : (*from)[0], + relative[1] ? (*from)[1] * area.height() : (*from)[1]); + } else { + from->set(w->width(), w->height()); + } + } + + if (to) { + if (to->isValid()) { + RELATIVE_XY(Target); + to->set(relative[0] ? (*to)[0] * area.width() : (*to)[0], + relative[1] ? (*to)[1] * area.height() : (*to)[1]); + } else { + to->set(w->width(), w->height()); + } + } + + } else if (a == Translation) { + QRect area = w->rect().toRect(); + if (from) { + if (from->isValid()) { + RELATIVE_XY(Source); + from->set(relative[0] ? (*from)[0] * area.width() : (*from)[0], + relative[1] ? (*from)[1] * area.height() : (*from)[1]); + } else { + from->set(0.0, 0.0); + } + } + + if (to) { + if (to->isValid()) { + RELATIVE_XY(Target); + to->set(relative[0] ? (*to)[0] * area.width() : (*to)[0], + relative[1] ? (*to)[1] * area.height() : (*to)[1]); + } else { + to->set(0.0, 0.0); + } + } + + } else if (a == Clip) { + if (from && !from->isValid()) { + from->set(1.0, 1.0); + setMetaData(SourceAnchor, metaData(TargetAnchor, meta), meta); + } + if (to && !to->isValid()) { + to->set(1.0, 1.0); + setMetaData(TargetAnchor, metaData(SourceAnchor, meta), meta); + } + + } else if (a == CrossFadePrevious) { + if (from && !from->isValid()) { + from->set(0.0); + } + if (to && !to->isValid()) { + to->set(1.0); + } + } +} + +quint64 AnimationEffect::p_animate(EffectWindow *w, Attribute a, uint meta, int ms, FPx2 to, const QEasingCurve &curve, int delay, FPx2 from, bool keepAtTarget, bool fullScreenEffect, bool keepAlive, GLShader *shader) +{ + const bool waitAtSource = from.isValid(); + validate(a, meta, &from, &to, w); + + if (!d->m_isInitialized) { + init(); // needs to ensure the window gets removed if deleted in the same event cycle + } + AniMap::iterator it = d->m_animations.find(w); + if (it == d->m_animations.end()) { + connect(w, &EffectWindow::windowExpandedGeometryChanged, + this, &AnimationEffect::_windowExpandedGeometryChanged); + it = d->m_animations.emplace(std::make_pair(w, std::pair, QRect>{})).first; + } + auto &[animations, rect] = it->second; + + std::shared_ptr fullscreen; + if (fullScreenEffect) { + fullscreen = d->m_fullScreenEffectLock.lock(); + if (!fullscreen) { + fullscreen = std::make_shared(this); + d->m_fullScreenEffectLock = fullscreen; + } + } + + if (a == CrossFadePrevious) { + CrossFadeEffect::redirect(w); + } + + animations.push_back(AniData( + a, // Attribute + meta, // Metadata + to, // Target + delay, // Delay + from, // Source + waitAtSource, // Whether the animation should be kept at source + fullscreen, // Full screen effect lock + keepAlive, // Keep alive flag + shader)); + + const quint64 ret_id = ++d->m_animCounter; + AniData &animation = animations.back(); + animation.id = ret_id; + + animation.visibleRef = EffectWindowVisibleRef(w, EffectWindow::PAINT_DISABLED_BY_MINIMIZE | EffectWindow::PAINT_DISABLED_BY_DESKTOP | EffectWindow::PAINT_DISABLED); + animation.timeLine.setDirection(TimeLine::Forward); + animation.timeLine.setDuration(std::chrono::milliseconds(ms)); + animation.timeLine.setEasingCurve(curve); + animation.timeLine.setSourceRedirectMode(TimeLine::RedirectMode::Strict); + animation.timeLine.setTargetRedirectMode(TimeLine::RedirectMode::Relaxed); + animation.itemEffect = ItemEffect(w->windowItem()); + + animation.terminationFlags = TerminateAtSource; + if (!keepAtTarget) { + animation.terminationFlags |= TerminateAtTarget; + } + + rect = QRect(); + + d->m_animationsTouched = true; + + if (delay > 0) { + QTimer::singleShot(delay, this, &AnimationEffect::triggerRepaint); + const QSize &s = effects->virtualScreenSize(); + if (waitAtSource) { + w->addLayerRepaint(0, 0, s.width(), s.height()); + } + } else { + triggerRepaint(); + } + if (shader) { + CrossFadeEffect::redirect(w); + } + return ret_id; +} + +bool AnimationEffect::retarget(quint64 animationId, FPx2 newTarget, int newRemainingTime) +{ + if (animationId == d->m_justEndedAnimation) { + return false; // this is just ending, do not try to retarget it + } + for (auto &[window, pair] : d->m_animations) { + auto &[animations, rect] = pair; + const auto anim = std::ranges::find_if(animations, [animationId](const auto &anim) { + return anim.id == animationId; + }); + if (anim != animations.end()) { + anim->from.set(interpolated(*anim, 0), interpolated(*anim, 1)); + validate(anim->attribute, anim->meta, nullptr, &newTarget, window); + anim->to.set(newTarget[0], newTarget[1]); + + anim->timeLine.setDirection(TimeLine::Forward); + anim->timeLine.setDuration(std::chrono::milliseconds(newRemainingTime)); + anim->timeLine.reset(); + + if (anim->attribute == CrossFadePrevious) { + CrossFadeEffect::redirect(window); + } + + triggerRepaint(); + return true; + } + } + return false; +} + +bool AnimationEffect::freezeInTime(quint64 animationId, qint64 frozenTime) +{ + if (animationId == d->m_justEndedAnimation) { + return false; // this is just ending, do not try to retarget it + } + for (auto &[window, pair] : d->m_animations) { + auto &[animations, rect] = pair; + const auto anim = std::ranges::find_if(animations, [animationId](const auto &anim) { + return anim.id == animationId; + }); + if (anim != animations.end()) { + if (frozenTime >= 0) { + anim->timeLine.setElapsed(std::chrono::milliseconds(frozenTime)); + } + anim->frozenTime = frozenTime; + return true; + } + } + return false; +} + +bool AnimationEffect::redirect(quint64 animationId, Direction direction, TerminationFlags terminationFlags) +{ + if (animationId == d->m_justEndedAnimation) { + return false; + } + for (auto &[window, pair] : d->m_animations) { + auto &[animations, rect] = pair; + const auto anim = std::ranges::find_if(animations, [animationId](const auto &anim) { + return anim.id == animationId; + }); + if (anim != animations.end()) { + switch (direction) { + case Backward: + anim->timeLine.setDirection(TimeLine::Backward); + break; + case Forward: + anim->timeLine.setDirection(TimeLine::Forward); + break; + } + anim->terminationFlags = terminationFlags & ~TerminateAtTarget; + return true; + } + } + return false; +} + +bool AnimationEffect::complete(quint64 animationId) +{ + if (animationId == d->m_justEndedAnimation) { + return false; + } + for (auto &[window, pair] : d->m_animations) { + auto &[animations, rect] = pair; + const auto anim = std::ranges::find_if(animations, [animationId](const auto &anim) { + return anim.id == animationId; + }); + if (anim != animations.end()) { + anim->timeLine.setElapsed(anim->timeLine.duration()); + unredirect(window); + return true; + } + } + return false; +} + +bool AnimationEffect::cancel(quint64 animationId) +{ + if (animationId == d->m_justEndedAnimation) { + return true; // this is just ending, do not try to cancel it but fake success + } + for (auto &[window, pair] : d->m_animations) { + auto &[animations, rect] = pair; + const auto anim = std::ranges::find_if(animations, [animationId](const auto &anim) { + return anim.id == animationId; + }); + if (anim != animations.end()) { + EffectWindowDeletedRef ref = std::move(anim->deletedRef); // delete window once we're done updating m_animations + if (std::ranges::none_of(animations, [animationId](const auto &anim) { + return anim.id != animationId && (anim.shader || anim.attribute == AnimationEffect::CrossFadePrevious); + })) { + unredirect(window); + } + animations.erase(anim); + if (animations.empty()) { // no other animations on the window, release it. + disconnect(window, &EffectWindow::windowExpandedGeometryChanged, + this, &AnimationEffect::_windowExpandedGeometryChanged); + d->m_animations.erase(window); + } + d->m_animationsTouched = true; // could be called from animationEnded + return true; + } + } + return false; +} + +void AnimationEffect::animationEnded(EffectWindow *w, Attribute a, uint meta) +{ +} + +void AnimationEffect::genericAnimation(EffectWindow *w, WindowPaintData &data, float progress, uint meta) +{ +} + +static qreal xCoord(const RectF &r, int flag) +{ + if (flag & AnimationEffect::Left) { + return r.x(); + } else if (flag & AnimationEffect::Right) { + return r.right(); + } else { + return r.x() + r.width() / 2; + } +} + +static qreal yCoord(const RectF &r, int flag) +{ + if (flag & AnimationEffect::Top) { + return r.y(); + } else if (flag & AnimationEffect::Bottom) { + return r.bottom(); + } else { + return r.y() + r.height() / 2; + } +} + +Rect AnimationEffect::clipRect(const Rect &geo, const AniData &anim) const +{ + Rect clip = geo; + FPx2 ratio = anim.from + progress(anim) * (anim.to - anim.from); + if (anim.from[0] < 1.0 || anim.to[0] < 1.0) { + clip.setWidth(clip.width() * ratio[0]); + } + if (anim.from[1] < 1.0 || anim.to[1] < 1.0) { + clip.setHeight(clip.height() * ratio[1]); + } + const Rect center = geo.adjusted(clip.width() / 2, clip.height() / 2, + -(clip.width() + 1) / 2, -(clip.height() + 1) / 2); + const qreal x[2] = {xCoord(center, metaData(SourceAnchor, anim.meta)), + xCoord(center, metaData(TargetAnchor, anim.meta))}; + const qreal y[2] = {yCoord(center, metaData(SourceAnchor, anim.meta)), + yCoord(center, metaData(TargetAnchor, anim.meta))}; + const QPoint d(x[0] + ratio[0] * (x[1] - x[0]), y[0] + ratio[1] * (y[1] - y[0])); + clip.moveTopLeft(QPoint(d.x() - clip.width() / 2, d.y() - clip.height() / 2)); + return clip; +} + +void AnimationEffect::prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) +{ + auto entry = d->m_animations.find(w); + if (entry != d->m_animations.end()) { + auto &[window, pair] = *entry; + auto &[list, rect] = pair; + for (auto &anim : list) { + if (anim.startTime > clock() && !anim.waitAtSource) { + continue; + } + + if (anim.frozenTime < 0) { + anim.timeLine.advance(presentTime); + } + + if (anim.attribute == Opacity || anim.attribute == CrossFadePrevious) { + data.setTranslucent(); + } else if (!(anim.attribute == Brightness || anim.attribute == Saturation)) { + data.setTransformed(); + } + } + } + effects->prePaintWindow(view, w, data, presentTime); +} + +static inline float geometryCompensation(int flags, float v) +{ + if (flags & (AnimationEffect::Left | AnimationEffect::Top)) { + return 0.0; // no compensation required + } + if (flags & (AnimationEffect::Right | AnimationEffect::Bottom)) { + return 1.0 - v; // full compensation + } + return 0.5 * (1.0 - v); // half compensation +} + +void AnimationEffect::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) +{ + auto it = d->m_animations.find(w); + if (it == d->m_animations.end()) { + effects->paintWindow(renderTarget, viewport, w, mask, deviceRegion, data); + return; + } + Region effectiveDeviceRegion = deviceRegion; + auto &[window, pair] = *it; + auto &[list, rect] = pair; + for (auto &anim : list) { + if (anim.startTime > clock() && !anim.waitAtSource) { + continue; + } + + switch (anim.attribute) { + case Opacity: + data.multiplyOpacity(interpolated(anim)); + break; + case Brightness: + data.multiplyBrightness(interpolated(anim)); + break; + case Saturation: + data.multiplySaturation(interpolated(anim)); + break; + case Scale: { + const QSizeF sz = w->frameGeometry().size(); + float f1(1.0), f2(0.0); + if (anim.from[0] >= 0.0 && anim.to[0] >= 0.0) { // scale x + f1 = interpolated(anim, 0); + f2 = geometryCompensation(anim.meta & AnimationEffect::Horizontal, f1); + data.translate(f2 * sz.width()); + data.setXScale(data.xScale() * f1); + } + if (anim.from[1] >= 0.0 && anim.to[1] >= 0.0) { // scale y + if (!anim.isOneDimensional()) { + f1 = interpolated(anim, 1); + f2 = geometryCompensation(anim.meta & AnimationEffect::Vertical, f1); + } else if (((anim.meta & AnimationEffect::Vertical) >> 1) != (anim.meta & AnimationEffect::Horizontal)) { + f2 = geometryCompensation(anim.meta & AnimationEffect::Vertical, f1); + } + data.translate(0.0, f2 * sz.height()); + data.setYScale(data.yScale() * f1); + } + break; + } + case Clip: + effectiveDeviceRegion &= viewport.mapToDeviceCoordinatesAligned(clipRect(w->expandedGeometry().toAlignedRect(), anim)); + break; + case Translation: + data += QPointF(interpolated(anim, 0), interpolated(anim, 1)); + break; + case Size: { + FPx2 dest = anim.from + progress(anim) * (anim.to - anim.from); + const QSizeF sz = w->frameGeometry().size(); + float f; + if (anim.from[0] >= 0.0 && anim.to[0] >= 0.0) { // resize x + f = dest[0] / sz.width(); + data.translate(geometryCompensation(anim.meta & AnimationEffect::Horizontal, f) * sz.width()); + data.setXScale(data.xScale() * f); + } + if (anim.from[1] >= 0.0 && anim.to[1] >= 0.0) { // resize y + f = dest[1] / sz.height(); + data.translate(0.0, geometryCompensation(anim.meta & AnimationEffect::Vertical, f) * sz.height()); + data.setYScale(data.yScale() * f); + } + break; + } + case Position: { + const QRectF geo = w->frameGeometry(); + const float prgrs = progress(anim); + if (anim.from[0] >= 0.0 && anim.to[0] >= 0.0) { + float dest = interpolated(anim, 0); + const qreal x[2] = {xCoord(geo, metaData(SourceAnchor, anim.meta)), + xCoord(geo, metaData(TargetAnchor, anim.meta))}; + data.translate(dest - (x[0] + prgrs * (x[1] - x[0]))); + } + if (anim.from[1] >= 0.0 && anim.to[1] >= 0.0) { + float dest = interpolated(anim, 1); + const qreal y[2] = {yCoord(geo, metaData(SourceAnchor, anim.meta)), + yCoord(geo, metaData(TargetAnchor, anim.meta))}; + data.translate(0.0, dest - (y[0] + prgrs * (y[1] - y[0]))); + } + break; + } + case Rotation: { + data.setRotationAxis((Qt::Axis)metaData(Axis, anim.meta)); + const float prgrs = progress(anim); + data.setRotationAngle(anim.from[0] + prgrs * (anim.to[0] - anim.from[0])); + + const QRect geo = w->rect().toRect(); + const uint sAnchor = metaData(SourceAnchor, anim.meta), + tAnchor = metaData(TargetAnchor, anim.meta); + QPointF pt(xCoord(geo, sAnchor), yCoord(geo, sAnchor)); + + if (tAnchor != sAnchor) { + QPointF pt2(xCoord(geo, tAnchor), yCoord(geo, tAnchor)); + pt += static_cast(prgrs) * (pt2 - pt); + } + data.setRotationOrigin(QVector3D(pt)); + break; + } + case Generic: + genericAnimation(w, data, progress(anim), anim.meta); + break; + case CrossFadePrevious: + data.setCrossFadeProgress(progress(anim)); + break; + case Shader: + if (anim.shader && anim.shader->isValid()) { + ShaderBinder binder{anim.shader}; + anim.shader->setUniform("animationProgress", progress(anim)); + setShader(w, anim.shader); + } + break; + case ShaderUniform: + if (anim.shader && anim.shader->isValid()) { + ShaderBinder binder{anim.shader}; + anim.shader->setUniform("animationProgress", progress(anim)); + anim.shader->setUniform(anim.meta, interpolated(anim)); + setShader(w, anim.shader); + } + break; + default: + break; + } + } + effects->paintWindow(renderTarget, viewport, w, mask, effectiveDeviceRegion, data); +} + +void AnimationEffect::postPaintScreen() +{ + d->m_animationsTouched = false; + bool damageDirty = false; + std::vector zombies; + + for (auto entry = d->m_animations.begin(); entry != d->m_animations.end();) { + bool invalidateLayerRect = false; + size_t animCounter = 0; + EffectWindow *const window = entry->first; + for (auto anim = entry->second.first.begin(); anim != entry->second.first.end();) { + if (anim->isActive() || (anim->startTime > clock() && !anim->waitAtSource)) { + ++anim; + ++animCounter; + continue; + } + d->m_justEndedAnimation = anim->id; + if (std::ranges::none_of(entry->second.first, [anim](const auto &other) { + return anim->id != other.id && (other.shader || other.attribute == AnimationEffect::CrossFadePrevious); + })) { + unredirect(window); + } + animationEnded(window, anim->attribute, anim->meta); + d->m_justEndedAnimation = 0; + // NOTICE animationEnded is an external call and might have called "::animate" + // as a result our iterators could now point random junk on the heap + // so we've to restore the former states, ie. find our window list and animation + if (d->m_animationsTouched) { + d->m_animationsTouched = false; + entry = std::ranges::find_if(d->m_animations, [window](const auto &pair) { + return pair.first == window; + }); + Q_ASSERT(entry != d->m_animations.end()); // usercode should not delete animations from animationEnded (not even possible atm.) + Q_ASSERT(animCounter < entry->second.first.size()); + anim = entry->second.first.begin() + animCounter; + } + // If it's a closed window, keep it alive for a little bit longer until we're done + // updating m_animations. Otherwise our windowDeleted slot can access m_animations + // while we still modify it. + if (!anim->deletedRef.isNull()) { + zombies.emplace_back(std::move(anim->deletedRef)); + } + anim = entry->second.first.erase(anim); + invalidateLayerRect = damageDirty = true; + } + if (entry->second.first.empty()) { + disconnect(window, &EffectWindow::windowExpandedGeometryChanged, + this, &AnimationEffect::_windowExpandedGeometryChanged); + effects->addRepaint(entry->second.second); + entry = d->m_animations.erase(entry); + } else { + if (invalidateLayerRect) { + entry->second.second = QRect(); // invalidate + } + ++entry; + } + } + + if (damageDirty) { + updateLayerRepaints(); + } + if (d->m_needSceneRepaint) { + effects->addRepaintFull(); + } else { + for (const auto &[window, pair] : d->m_animations) { + const auto &[data, rect] = pair; + for (const auto &anim : data) { + if (anim.startTime > clock()) { + continue; + } + if (!anim.timeLine.done()) { + window->addLayerRepaint(rect); + break; + } + } + } + } + + effects->postPaintScreen(); +} + +float AnimationEffect::interpolated(const AniData &a, int i) const +{ + return a.from[i] + a.timeLine.value() * (a.to[i] - a.from[i]); +} + +float AnimationEffect::progress(const AniData &a) const +{ + return a.startTime < clock() ? a.timeLine.value() : 0.0; +} + +// TODO - get this out of the header - the functionpointer usage of QEasingCurve somehow sucks ;-) +// qreal AnimationEffect::qecGaussian(qreal progress) // exp(-5*(2*x-1)^2) +// { +// progress = 2*progress - 1; +// progress *= -5*progress; +// return qExp(progress); +// } + +int AnimationEffect::metaData(MetaType type, uint meta) +{ + switch (type) { + case SourceAnchor: + return ((meta >> 5) & 0x1f); + case TargetAnchor: + return (meta & 0x1f); + case RelativeSourceX: + case RelativeSourceY: + case RelativeTargetX: + case RelativeTargetY: { + const int shift = 10 + type - RelativeSourceX; + return ((meta >> shift) & 1); + } + case Axis: + return ((meta >> 10) & 3); + default: + return 0; + } +} + +void AnimationEffect::setMetaData(MetaType type, uint value, uint &meta) +{ + switch (type) { + case SourceAnchor: + meta &= ~(0x1f << 5); + meta |= ((value & 0x1f) << 5); + break; + case TargetAnchor: + meta &= ~(0x1f); + meta |= (value & 0x1f); + break; + case RelativeSourceX: + case RelativeSourceY: + case RelativeTargetX: + case RelativeTargetY: { + const int shift = 10 + type - RelativeSourceX; + if (value) { + meta |= (1 << shift); + } else { + meta &= ~(1 << shift); + } + break; + } + case Axis: + meta &= ~(3 << 10); + meta |= ((value & 3) << 10); + break; + default: + break; + } +} + +void AnimationEffect::triggerRepaint() +{ + for (auto &[window, pair] : d->m_animations) { + pair.second = QRect(); + } + updateLayerRepaints(); + if (d->m_needSceneRepaint) { + effects->addRepaintFull(); + } else { + for (const auto &[window, pair] : d->m_animations) { + window->addLayerRepaint(pair.second); + } + } +} + +static float fixOvershoot(float f, const AniData &d, short int dir, float s = 1.1) +{ + switch (d.timeLine.easingCurve().type()) { + case QEasingCurve::InOutElastic: + case QEasingCurve::InOutBack: + return f * s; + case QEasingCurve::InElastic: + case QEasingCurve::OutInElastic: + case QEasingCurve::OutBack: + return (dir & 2) ? f * s : f; + case QEasingCurve::OutElastic: + case QEasingCurve::InBack: + return (dir & 1) ? f * s : f; + default: + return f; + } +} + +void AnimationEffect::updateLayerRepaints() +{ + d->m_needSceneRepaint = false; + for (auto &[window, pair] : d->m_animations) { + auto &[data, rect] = pair; + if (!rect.isNull()) { + continue; + } + float f[2] = {1.0, 1.0}; + float t[2] = {0.0, 0.0}; + bool createRegion = false; + QList rects; + for (auto &anim : data) { + if (anim.startTime > clock()) { + continue; + } + switch (anim.attribute) { + case Opacity: + case Brightness: + case Saturation: + case CrossFadePrevious: + case Shader: + case ShaderUniform: + createRegion = true; + break; + case Rotation: + createRegion = false; + rect = QRect(QPoint(0, 0), effects->virtualScreenSize()); + break; // sic! no need to do anything else + case Generic: + d->m_needSceneRepaint = true; // we don't know whether this will change visual stacking order + return; // sic! no need to do anything else + case Translation: + case Position: { + createRegion = true; + QRect r(window->frameGeometry().toRect()); + int x[2] = {0, 0}; + int y[2] = {0, 0}; + if (anim.attribute == Translation) { + x[0] = anim.from[0]; + x[1] = anim.to[0]; + y[0] = anim.from[1]; + y[1] = anim.to[1]; + } else { + if (anim.from[0] >= 0.0 && anim.to[0] >= 0.0) { + x[0] = anim.from[0] - xCoord(r, metaData(SourceAnchor, anim.meta)); + x[1] = anim.to[0] - xCoord(r, metaData(TargetAnchor, anim.meta)); + } + if (anim.from[1] >= 0.0 && anim.to[1] >= 0.0) { + y[0] = anim.from[1] - yCoord(r, metaData(SourceAnchor, anim.meta)); + y[1] = anim.to[1] - yCoord(r, metaData(TargetAnchor, anim.meta)); + } + } + r = window->expandedGeometry().toRect(); + rects.push_back(r.translated(x[0], y[0])); + rects.push_back(r.translated(x[1], y[1])); + break; + } + case Clip: + createRegion = true; + break; + case Size: + case Scale: { + createRegion = true; + const QSize sz = window->frameGeometry().size().toSize(); + float fx = std::max(fixOvershoot(anim.from[0], anim, 1), fixOvershoot(anim.to[0], anim, 2)); + // float fx = std::max(interpolated(*anim,0), anim.to[0]); + if (fx >= 0.0) { + if (anim.attribute == Size) { + fx /= sz.width(); + } + f[0] *= fx; + t[0] += geometryCompensation(anim.meta & AnimationEffect::Horizontal, fx) * sz.width(); + } + // float fy = std::max(interpolated(*anim,1), anim.to[1]); + float fy = std::max(fixOvershoot(anim.from[1], anim, 1), fixOvershoot(anim.to[1], anim, 2)); + if (fy >= 0.0) { + if (anim.attribute == Size) { + fy /= sz.height(); + } + if (!anim.isOneDimensional()) { + f[1] *= fy; + t[1] += geometryCompensation(anim.meta & AnimationEffect::Vertical, fy) * sz.height(); + } else if (((anim.meta & AnimationEffect::Vertical) >> 1) != (anim.meta & AnimationEffect::Horizontal)) { + f[1] *= fx; + t[1] += geometryCompensation(anim.meta & AnimationEffect::Vertical, fx) * sz.height(); + } + } + break; + } + } + } + if (createRegion) { + const QRect geo = window->expandedGeometry().toRect(); + if (rects.empty()) { + rects.push_back(geo); + } + for (auto &r : rects) { // transform + r.setSize(QSize(std::round(r.width() * f[0]), std::round(r.height() * f[1]))); + r.translate(t[0], t[1]); // "const_cast" - don't do that at home, kids ;-) + } + rect = rects.at(0); + if (rects.count() > 1) { + for (const auto &r : rects | std::views::drop(1)) { // unite + rect |= r; + } + const int dx = 110 * (rect.width() - geo.width()) / 100 + 1 - rect.width() + geo.width(); + const int dy = 110 * (rect.height() - geo.height()) / 100 + 1 - rect.height() + geo.height(); + rect.adjust(-dx, -dy, dx, dy); // fix pot. overshoot + } + } + } +} + +void AnimationEffect::_windowExpandedGeometryChanged(KWin::EffectWindow *w) +{ + const auto entry = d->m_animations.find(w); + if (entry != d->m_animations.end()) { + auto &[data, rect] = entry->second; + rect = QRect(); + updateLayerRepaints(); + if (!rect.isNull()) { // actually got updated, ie. is in use - ensure it gets a repaint + w->addLayerRepaint(rect); + } + } +} + +void AnimationEffect::_windowClosed(EffectWindow *w) +{ + auto it = d->m_animations.find(w); + if (it == d->m_animations.end()) { + return; + } + auto &[animations, rect] = it->second; + for (auto &animation : animations) { + if (animation.keepAlive) { + animation.deletedRef = EffectWindowDeletedRef(w); + } + } +} + +void AnimationEffect::_windowDeleted(EffectWindow *w) +{ + d->m_animations.erase(w); +} + +QString AnimationEffect::debug(const QString ¶meter) const +{ + QString dbg; + if (d->m_animations.empty()) { + dbg = QStringLiteral("No window is animated"); + } else { + for (const auto &[window, pair] : d->m_animations) { + const auto &[data, rect] = pair; + QString caption = window->isDeleted() ? QStringLiteral("[Deleted]") : window->caption(); + if (caption.isEmpty()) { + caption = QStringLiteral("[Untitled]"); + } + dbg += QLatin1String("Animating window: ") + caption + QLatin1Char('\n'); + for (const auto &anim : data) { + dbg += anim.debugInfo(); + } + } + } + return dbg; +} + +const AnimationEffect::AniMap &AnimationEffect::state() const +{ + return d->m_animations; +} + +} // namespace KWin + +#include "moc_animationeffect.cpp" diff --git a/local/recipes/kde/kwin/source/src/effect/animationeffect.h b/local/recipes/kde/kwin/source/src/effect/animationeffect.h new file mode 100644 index 0000000000..61dc6eaba4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/animationeffect.h @@ -0,0 +1,532 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Thomas Lübking + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "kwin_export.h" + +#include "effect/offscreeneffect.h" +#include +#include +#include + +namespace KWin +{ + +class KWIN_EXPORT FPx2 +{ +public: + FPx2() + { + f[0] = f[1] = 0.0; + valid = false; + } + explicit FPx2(float v) + { + f[0] = f[1] = v; + valid = true; + } + FPx2(float v1, float v2) + { + f[0] = v1; + f[1] = v2; + valid = true; + } + FPx2(const FPx2 &other) + { + f[0] = other.f[0]; + f[1] = other.f[1]; + valid = other.valid; + } + explicit FPx2(const QPoint &other) + { + f[0] = other.x(); + f[1] = other.y(); + valid = true; + } + explicit FPx2(const QPointF &other) + { + f[0] = other.x(); + f[1] = other.y(); + valid = true; + } + explicit FPx2(const QSize &other) + { + f[0] = other.width(); + f[1] = other.height(); + valid = true; + } + explicit FPx2(const QSizeF &other) + { + f[0] = other.width(); + f[1] = other.height(); + valid = true; + } + inline void invalidate() + { + valid = false; + } + inline bool isValid() const + { + return valid; + } + inline float operator[](int n) const + { + return f[n]; + } + inline QString toString() const + { + QString ret; + if (valid) { + ret = QString::number(f[0]) + QLatin1Char(',') + QString::number(f[1]); + } else { + ret = QString(); + } + return ret; + } + + inline FPx2 &operator=(const FPx2 &other) + { + f[0] = other.f[0]; + f[1] = other.f[1]; + valid = other.valid; + return *this; + } + inline FPx2 &operator+=(const FPx2 &other) + { + f[0] += other[0]; + f[1] += other[1]; + return *this; + } + inline FPx2 &operator-=(const FPx2 &other) + { + f[0] -= other[0]; + f[1] -= other[1]; + return *this; + } + inline FPx2 &operator*=(float fl) + { + f[0] *= fl; + f[1] *= fl; + return *this; + } + inline FPx2 &operator/=(float fl) + { + f[0] /= fl; + f[1] /= fl; + return *this; + } + + friend inline bool operator==(const FPx2 &f1, const FPx2 &f2) + { + return f1[0] == f2[0] && f1[1] == f2[1]; + } + friend inline bool operator!=(const FPx2 &f1, const FPx2 &f2) + { + return f1[0] != f2[0] || f1[1] != f2[1]; + } + friend inline const FPx2 operator+(const FPx2 &f1, const FPx2 &f2) + { + return FPx2(f1[0] + f2[0], f1[1] + f2[1]); + } + friend inline const FPx2 operator-(const FPx2 &f1, const FPx2 &f2) + { + return FPx2(f1[0] - f2[0], f1[1] - f2[1]); + } + friend inline const FPx2 operator*(const FPx2 &f, float fl) + { + return FPx2(f[0] * fl, f[1] * fl); + } + friend inline const FPx2 operator*(float fl, const FPx2 &f) + { + return FPx2(f[0] * fl, f[1] * fl); + } + friend inline const FPx2 operator-(const FPx2 &f) + { + return FPx2(-f[0], -f[1]); + } + friend inline const FPx2 operator/(const FPx2 &f, float fl) + { + return FPx2(f[0] / fl, f[1] / fl); + } + + inline void set(float v) + { + f[0] = v; + valid = true; + } + inline void set(float v1, float v2) + { + f[0] = v1; + f[1] = v2; + valid = true; + } + +private: + float f[2]; + bool valid; +}; + +class AniData; +class AnimationEffectPrivate; + +/** + * Base class for animation effects. + * + * AnimationEffect serves as a base class for animation effects. It makes easier + * implementing animated transitions, without having to worry about low-level + * specific stuff, e.g. referencing and unreferencing deleted windows, scheduling + * repaints for the next frame, etc. + * + * Each animation animates one specific attribute, e.g. size, position, scale, etc. + * You can provide your own implementation of the Generic attribute if none of the + * standard attributes(e.g. size, position, etc) satisfy your requirements. + * + * @since 4.8 + */ +class KWIN_EXPORT AnimationEffect : public CrossFadeEffect +{ + Q_OBJECT + +public: + enum Anchor { Left = 1 << 0, + Top = 1 << 1, + Right = 1 << 2, + Bottom = 1 << 3, + Horizontal = Left | Right, + Vertical = Top | Bottom, + Mouse = 1 << 4 }; + Q_ENUM(Anchor) + + enum Attribute { + Opacity = 0, + Brightness, + Saturation, + Scale, + Rotation, + Position, + Size, + Translation, + Clip, + Generic, + CrossFadePrevious, + /** + * Performs an animation with a provided shader. + * The float uniform @c animationProgress is set to the current progress of the animation. + **/ + Shader, + /** + * Like Shader, but additionally allows to animate a float uniform passed to the shader. + * The uniform location must be provided as metadata. + **/ + ShaderUniform, + NonFloatBase = Position + }; + Q_ENUM(Attribute) + + enum MetaType { SourceAnchor, + TargetAnchor, + RelativeSourceX, + RelativeSourceY, + RelativeTargetX, + RelativeTargetY, + Axis }; + Q_ENUM(MetaType) + + /** + * This enum type is used to specify the direction of the animation. + * + * @since 5.15 + */ + enum Direction { + Forward, ///< The animation goes from source to target. + Backward ///< The animation goes from target to source. + }; + Q_ENUM(Direction) + + /** + * This enum type is used to specify when the animation should be terminated. + * + * @since 5.15 + */ + enum TerminationFlag { + /** + * Don't terminate the animation when it reaches source or target position. + */ + DontTerminate = 0x00, + /** + * Terminate the animation when it reaches the source position. An animation + * can reach the source position if its direction was changed to go backward + * (from target to source). + */ + TerminateAtSource = 0x01, + /** + * Terminate the animation when it reaches the target position. If this flag + * is not set, then the animation will be persistent. + */ + TerminateAtTarget = 0x02 + }; + Q_DECLARE_FLAGS(TerminationFlags, TerminationFlag) + Q_FLAG(TerminationFlags) + + /** + * Constructs AnimationEffect. + * + * Whenever you intend to connect to the EffectsHandler::windowClosed() signal, + * do so when reimplementing the constructor. Do not add private slots named + * _windowClosed or _windowDeleted! The AnimationEffect connects them right after + * the construction. + * + * If you shadow the _windowDeleted slot (it doesn't matter that it's a private + * slot), this will lead to segfaults. + * + * If you shadow _windowClosed or connect your slot to EffectsHandler::windowClosed() + * after _windowClosed was connected, animations for closing windows will fail. + */ + AnimationEffect(); + ~AnimationEffect() override; + + bool isActive() const override; + + /** + * Gets stored metadata. + * + * Metadata can be used to store some extra information, for example rotation axis, + * etc. The first 24 bits are reserved for the AnimationEffect class, you can use + * the last 8 bits for custom hints. In case when you transform a Generic attribute, + * all 32 bits are yours and you can use them as you want and read them in your + * genericAnimation() implementation. + * + * @param type The type of the metadata. + * @param meta Where the metadata is stored. + * @returns Stored metadata. + * @since 4.8 + */ + static int metaData(MetaType type, uint meta); + + /** + * Sets metadata. + * + * @param type The type of the metadata. + * @param value The data to be stored. + * @param meta Where the metadata will be stored. + * @since 4.8 + */ + static void setMetaData(MetaType type, uint value, uint &meta); + + // Reimplemented from KWin::Effect. + QString debug(const QString ¶meter) const override; + void prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) override; + void paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) override; + void postPaintScreen() override; + + /** + * Gaussian (bumper) animation curve for QEasingCurve. + * + * @since 4.8 + */ + static qreal qecGaussian(qreal progress) + { + progress = 2 * progress - 1; + progress *= -5 * progress; + return qExp(progress); + } + + /** + * @since 4.8 + */ + static inline qint64 clock() + { + return s_clock.elapsed(); + } + +protected: + /** + * Starts an animated transition of any supported attribute. + * + * @param w The animated window. + * @param a The animated attribute. + * @param meta Basically a wildcard to carry various extra information, e.g. + * the anchor, relativity or rotation axis. You will probably use it when + * performing Generic animations. + * @param ms How long the transition will last. + * @param to The target value. FPx2 is an agnostic two component float type + * (like QPointF or QSizeF, but without requiring to be either and supporting + * an invalid state). + * @param curve How the animation progresses, e.g. Linear progresses constantly + * while Exponential start slow and becomes very fast in the end. + * @param delay When the animation will start compared to "now" (the window will + * remain at the "from" position until then). + * @param from The starting value, the default is invalid, ie. the attribute for + * the window is not transformed in the beginning. + * @param fullScreen Sets this effect as the active full screen effect for the + * duration of the animation. + * @param keepAlive Whether closed windows should be kept alive during animation. + * @param shader Optional shader to use to render the window. + * @returns An ID that you can use to cancel a running animation. + * @since 4.8 + */ + quint64 animate(EffectWindow *w, Attribute a, uint meta, int ms, const FPx2 &to, const QEasingCurve &curve = QEasingCurve(), int delay = 0, const FPx2 &from = FPx2(), bool fullScreen = false, bool keepAlive = true, GLShader *shader = nullptr) + { + return p_animate(w, a, meta, ms, to, curve, delay, from, false, fullScreen, keepAlive, shader); + } + + /** + * Starts a persistent animated transition of any supported attribute. + * + * This method is equal to animate() with one important difference: + * the target value for the attribute is kept until you call cancel(). + * + * @param w The animated window. + * @param a The animated attribute. + * @param meta Basically a wildcard to carry various extra information, e.g. + * the anchor, relativity or rotation axis. You will probably use it when + * performing Generic animations. + * @param ms How long the transition will last. + * @param to The target value. FPx2 is an agnostic two component float type + * (like QPointF or QSizeF, but without requiring to be either and supporting + * an invalid state). + * @param curve How the animation progresses, e.g. Linear progresses constantly + * while Exponential start slow and becomes very fast in the end. + * @param delay When the animation will start compared to "now" (the window will + * remain at the "from" position until then). + * @param from The starting value, the default is invalid, ie. the attribute for + * the window is not transformed in the beginning. + * @param fullScreen Sets this effect as the active full screen effect for the + * duration of the animation. + * @param keepAlive Whether closed windows should be kept alive during animation. + * @param shader Optional shader to use to render the window. + * @returns An ID that you need to use to cancel this manipulation. + * @since 4.11 + */ + quint64 set(EffectWindow *w, Attribute a, uint meta, int ms, const FPx2 &to, const QEasingCurve &curve = QEasingCurve(), int delay = 0, const FPx2 &from = FPx2(), bool fullScreen = false, bool keepAlive = true, GLShader *shader = nullptr) + { + return p_animate(w, a, meta, ms, to, curve, delay, from, true, fullScreen, keepAlive, shader); + } + + /** + * Changes the target (but not type or curve) of a running animation. + * + * Please use cancel() to cancel an animation rather than altering it. + * + * @param animationId The id of the animation to be retargetted. + * @param newTarget The new target. + * @param newRemainingTime The new duration of the transition. By default (-1), + * the remaining time remains unchanged. + * @returns @c true if the animation was retargetted successfully, @c false otherwise. + * @note You can NOT retarget an animation that just has just ended! + * @since 5.6 + */ + bool retarget(quint64 animationId, FPx2 newTarget, int newRemainingTime = -1); + + bool freezeInTime(quint64 animationId, qint64 frozenTime); + + /** + * Changes the direction of the animation. + * + * @param animationId The id of the animation. + * @param direction The new direction of the animation. + * @param terminationFlags Whether the animation should be terminated when it + * reaches the source position after its direction was changed to go backward. + * Currently, TerminationFlag::TerminateAtTarget has no effect. + * @returns @c true if the direction of the animation was changed successfully, + * otherwise @c false. + * @since 5.15 + */ + bool redirect(quint64 animationId, + Direction direction, + TerminationFlags terminationFlags = TerminateAtSource); + + /** + * Fast-forwards the animation to the target position. + * + * @param animationId The id of the animation. + * @returns @c true if the animation was fast-forwarded successfully, otherwise + * @c false. + * @since 5.15 + */ + bool complete(quint64 animationId); + + /** + * Called whenever an animation ends. + * + * You can reimplement this method to keep a constant transformation for the window + * (i.e. keep it at some opacity or position) or to start another animation. + * + * @param w The animated window. + * @param a The animated attribute. + * @param meta Originally supplied metadata to animate() or set(). + * @since 4.8 + */ + virtual void animationEnded(EffectWindow *w, Attribute a, uint meta); + + /** + * Cancels a running animation. + * + * @param animationId The id of the animation. + * @returns @c true if the animation was found (and canceled), @c false otherwise. + * @note There is NO animated reset of the original value. You'll have to provide + * that with a second animation. + * @note This will eventually release a Deleted window as well. + * @note If you intend to run another animation on the (Deleted) window, you have + * to do that before cancelling the old animation (to keep the window around). + * @since 4.11 + */ + bool cancel(quint64 animationId); + + /** + * Called whenever animation that transforms Generic attribute needs to be painted. + * + * You should reimplement this method if you transform Generic attribute. @p meta + * can be used to support more than one additional animations. + * + * @param w The animated window. + * @param data The paint data. + * @param progress Current progress value. + * @param meta The metadata. + * @since 4.8 + */ + virtual void genericAnimation(EffectWindow *w, WindowPaintData &data, float progress, uint meta); + + /** + * @internal + */ + typedef std::unordered_map, QRect>> AniMap; + + /** + * @internal + */ + const AniMap &state() const; + +private: + quint64 p_animate(EffectWindow *w, Attribute a, uint meta, int ms, FPx2 to, const QEasingCurve &curve, int delay, FPx2 from, bool keepAtTarget, bool fullScreenEffect, bool keepAlive, GLShader *shader); + Rect clipRect(const Rect &windowRect, const AniData &) const; + float interpolated(const AniData &, int i = 0) const; + float progress(const AniData &) const; + void updateLayerRepaints(); + void validate(Attribute a, uint &meta, FPx2 *from, FPx2 *to, const EffectWindow *w) const; + +private Q_SLOTS: + void init(); + void triggerRepaint(); + void _windowClosed(KWin::EffectWindow *w); + void _windowDeleted(KWin::EffectWindow *w); + void _windowExpandedGeometryChanged(KWin::EffectWindow *w); + +private: + static QElapsedTimer s_clock; + const std::unique_ptr d; + friend class AnimationEffectPrivate; +}; + +} // namespace + +Q_DECLARE_METATYPE(KWin::FPx2) +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::AnimationEffect::TerminationFlags) diff --git a/local/recipes/kde/kwin/source/src/effect/effect.cpp b/local/recipes/kde/kwin/source/src/effect/effect.cpp new file mode 100644 index 0000000000..5560074f48 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/effect.cpp @@ -0,0 +1,548 @@ +/* + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "effect/effect.h" +#include "effect/effecthandler.h" + +#include + +#include +#include + +namespace KWin +{ + +void WindowPrePaintData::setTranslucent() +{ + mask |= Effect::PAINT_WINDOW_TRANSLUCENT; + mask &= ~Effect::PAINT_WINDOW_OPAQUE; + deviceOpaque = Region(); // cannot clip, will be transparent +} + +void WindowPrePaintData::setTransformed() +{ + mask |= Effect::PAINT_WINDOW_TRANSFORMED; +} + +class PaintDataPrivate +{ +public: + PaintDataPrivate() + : scale(1., 1., 1.) + , rotationAxis(0, 0, 1.) + , rotationAngle(0.) + { + } + QVector3D scale; + QVector3D translation; + + QVector3D rotationAxis; + QVector3D rotationOrigin; + qreal rotationAngle; +}; + +PaintData::PaintData() + : d(std::make_unique()) +{ +} + +PaintData::~PaintData() = default; + +qreal PaintData::xScale() const +{ + return d->scale.x(); +} + +qreal PaintData::yScale() const +{ + return d->scale.y(); +} + +qreal PaintData::zScale() const +{ + return d->scale.z(); +} + +void PaintData::setScale(const QVector2D &scale) +{ + d->scale.setX(scale.x()); + d->scale.setY(scale.y()); +} + +void PaintData::setScale(const QVector3D &scale) +{ + d->scale = scale; +} +void PaintData::setXScale(qreal scale) +{ + d->scale.setX(scale); +} + +void PaintData::setYScale(qreal scale) +{ + d->scale.setY(scale); +} + +void PaintData::setZScale(qreal scale) +{ + d->scale.setZ(scale); +} + +const QVector3D &PaintData::scale() const +{ + return d->scale; +} + +void PaintData::setXTranslation(qreal translate) +{ + d->translation.setX(translate); +} + +void PaintData::setYTranslation(qreal translate) +{ + d->translation.setY(translate); +} + +void PaintData::setZTranslation(qreal translate) +{ + d->translation.setZ(translate); +} + +void PaintData::translate(qreal x, qreal y, qreal z) +{ + translate(QVector3D(x, y, z)); +} + +void PaintData::translate(const QVector3D &t) +{ + d->translation += t; +} + +qreal PaintData::xTranslation() const +{ + return d->translation.x(); +} + +qreal PaintData::yTranslation() const +{ + return d->translation.y(); +} + +qreal PaintData::zTranslation() const +{ + return d->translation.z(); +} + +const QVector3D &PaintData::translation() const +{ + return d->translation; +} + +qreal PaintData::rotationAngle() const +{ + return d->rotationAngle; +} + +QVector3D PaintData::rotationAxis() const +{ + return d->rotationAxis; +} + +QVector3D PaintData::rotationOrigin() const +{ + return d->rotationOrigin; +} + +void PaintData::setRotationAngle(qreal angle) +{ + d->rotationAngle = angle; +} + +void PaintData::setRotationAxis(Qt::Axis axis) +{ + switch (axis) { + case Qt::XAxis: + setRotationAxis(QVector3D(1, 0, 0)); + break; + case Qt::YAxis: + setRotationAxis(QVector3D(0, 1, 0)); + break; + case Qt::ZAxis: + setRotationAxis(QVector3D(0, 0, 1)); + break; + } +} + +void PaintData::setRotationAxis(const QVector3D &axis) +{ + d->rotationAxis = axis; +} + +void PaintData::setRotationOrigin(const QVector3D &origin) +{ + d->rotationOrigin = origin; +} + +QMatrix4x4 PaintData::toMatrix(qreal deviceScale) const +{ + QMatrix4x4 ret; + if (d->translation != QVector3D(0, 0, 0)) { + ret.translate(d->translation * deviceScale); + } + if (d->scale != QVector3D(1, 1, 1)) { + ret.scale(d->scale); + } + + if (d->rotationAngle != 0) { + ret.translate(d->rotationOrigin * deviceScale); + ret.rotate(d->rotationAngle, d->rotationAxis); + ret.translate(-d->rotationOrigin * deviceScale); + } + + return ret; +} + +class WindowPaintDataPrivate +{ +public: + qreal opacity; + qreal saturation; + qreal brightness; + qreal crossFadeProgress; +}; + +WindowPaintData::WindowPaintData() + : PaintData() + , d(std::make_unique()) +{ + setOpacity(1.0); + setSaturation(1.0); + setBrightness(1.0); + setCrossFadeProgress(0.0); +} + +WindowPaintData::WindowPaintData(const WindowPaintData &other) + : PaintData() + , d(std::make_unique()) +{ + setXScale(other.xScale()); + setYScale(other.yScale()); + setZScale(other.zScale()); + translate(other.translation()); + setRotationOrigin(other.rotationOrigin()); + setRotationAxis(other.rotationAxis()); + setRotationAngle(other.rotationAngle()); + setOpacity(other.opacity()); + setSaturation(other.saturation()); + setBrightness(other.brightness()); + setCrossFadeProgress(other.crossFadeProgress()); +} + +WindowPaintData::~WindowPaintData() = default; + +qreal WindowPaintData::opacity() const +{ + return d->opacity; +} + +qreal WindowPaintData::saturation() const +{ + return d->saturation; +} + +qreal WindowPaintData::brightness() const +{ + return d->brightness; +} + +void WindowPaintData::setOpacity(qreal opacity) +{ + d->opacity = opacity; +} + +void WindowPaintData::setSaturation(qreal saturation) const +{ + d->saturation = saturation; +} + +void WindowPaintData::setBrightness(qreal brightness) +{ + d->brightness = brightness; +} + +qreal WindowPaintData::crossFadeProgress() const +{ + return d->crossFadeProgress; +} + +void WindowPaintData::setCrossFadeProgress(qreal factor) +{ + d->crossFadeProgress = std::clamp(factor, 0.0, 1.0); +} + +qreal WindowPaintData::multiplyOpacity(qreal factor) +{ + d->opacity *= factor; + return d->opacity; +} + +qreal WindowPaintData::multiplySaturation(qreal factor) +{ + d->saturation *= factor; + return d->saturation; +} + +qreal WindowPaintData::multiplyBrightness(qreal factor) +{ + d->brightness *= factor; + return d->brightness; +} + +WindowPaintData &WindowPaintData::operator*=(qreal scale) +{ + this->setXScale(this->xScale() * scale); + this->setYScale(this->yScale() * scale); + this->setZScale(this->zScale() * scale); + return *this; +} + +WindowPaintData &WindowPaintData::operator*=(const QVector2D &scale) +{ + this->setXScale(this->xScale() * scale.x()); + this->setYScale(this->yScale() * scale.y()); + return *this; +} + +WindowPaintData &WindowPaintData::operator*=(const QVector3D &scale) +{ + this->setXScale(this->xScale() * scale.x()); + this->setYScale(this->yScale() * scale.y()); + this->setZScale(this->zScale() * scale.z()); + return *this; +} + +WindowPaintData &WindowPaintData::operator+=(const QPointF &translation) +{ + return this->operator+=(QVector3D(translation)); +} + +WindowPaintData &WindowPaintData::operator+=(const QPoint &translation) +{ + return this->operator+=(QVector3D(translation)); +} + +WindowPaintData &WindowPaintData::operator+=(const QVector2D &translation) +{ + return this->operator+=(QVector3D(translation)); +} + +WindowPaintData &WindowPaintData::operator+=(const QVector3D &translation) +{ + translate(translation); + return *this; +} + +Effect::Effect(QObject *parent) + : QObject(parent) +{ +} + +Effect::~Effect() +{ +} + +void Effect::reconfigure(ReconfigureFlags) +{ +} + +void Effect::windowInputMouseEvent(QEvent *) +{ +} + +void Effect::grabbedKeyboardEvent(QKeyEvent *) +{ +} + +bool Effect::borderActivated(ElectricBorder) +{ + return false; +} + +void Effect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + effects->prePaintScreen(data, presentTime); +} + +void Effect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) +{ + effects->paintScreen(renderTarget, viewport, mask, deviceRegion, screen); +} + +void Effect::postPaintScreen() +{ + effects->postPaintScreen(); +} + +void Effect::prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) +{ + effects->prePaintWindow(view, w, data, presentTime); +} + +void Effect::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) +{ + effects->paintWindow(renderTarget, viewport, w, mask, deviceRegion, data); +} + +bool Effect::provides(Feature) +{ + return false; +} + +bool Effect::isActive() const +{ + return true; +} + +QString Effect::debug(const QString &) const +{ + return QString(); +} + +void Effect::drawWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) +{ + effects->drawWindow(renderTarget, viewport, w, mask, deviceRegion, data); +} + +void Effect::setPositionTransformations(WindowPaintData &data, Rect &logicalRegion, EffectWindow *w, + const Rect &r, Qt::AspectRatioMode aspect) +{ + QSizeF size = w->size(); + size.scale(r.size(), aspect); + data.setXScale(size.width() / double(w->width())); + data.setYScale(size.height() / double(w->height())); + int width = int(w->width() * data.xScale()); + int height = int(w->height() * data.yScale()); + int x = r.x() + (r.width() - width) / 2; + int y = r.y() + (r.height() - height) / 2; + logicalRegion = Rect(x, y, width, height); + data.setXTranslation(x - w->x()); + data.setYTranslation(y - w->y()); +} + +QPointF Effect::cursorPos() +{ + return effects->cursorPos(); +} + +double Effect::animationTime(const KConfigGroup &cfg, const QString &key, std::chrono::milliseconds defaultTime) +{ + int time = cfg.readEntry(key, 0); + return time != 0 ? time : std::max(defaultTime.count() * effects->animationTimeFactor(), 1.); +} + +double Effect::animationTime(std::chrono::milliseconds defaultTime) +{ + // at least 1ms, otherwise 0ms times can break some things + return std::max(defaultTime.count() * effects->animationTimeFactor(), 1.); +} + +int Effect::requestedEffectChainPosition() const +{ + return 0; +} + +bool Effect::touchDown(qint32 id, const QPointF &pos, std::chrono::microseconds time) +{ + return false; +} + +bool Effect::touchMotion(qint32 id, const QPointF &pos, std::chrono::microseconds time) +{ + return false; +} + +bool Effect::touchUp(qint32 id, std::chrono::microseconds time) +{ + return false; +} + +void Effect::touchCancel() +{ +} + +bool Effect::perform(Feature feature, const QVariantList &arguments) +{ + return false; +} + +bool Effect::tabletToolProximity(TabletToolProximityEvent *event) +{ + return false; +} + +bool Effect::tabletToolAxis(TabletToolAxisEvent *event) +{ + return false; +} + +bool Effect::tabletToolTip(TabletToolTipEvent *event) +{ + return false; +} + +bool Effect::tabletToolButtonEvent(uint button, bool pressed, quint64 toolId) +{ + return false; +} + +bool Effect::tabletPadButtonEvent(uint button, bool pressed, void *device) +{ + return false; +} + +bool Effect::tabletPadStripEvent(int number, qreal position, bool isFinger, void *device) +{ + return false; +} + +bool Effect::tabletPadRingEvent(int number, qreal position, bool isFinger, void *device) +{ + return false; +} + +bool Effect::tabletPadDialEvent(int number, double delta, void *device) +{ + return false; +} + +bool Effect::blocksDirectScanout() const +{ + return true; +} + +EffectPluginFactory::EffectPluginFactory() +{ +} + +EffectPluginFactory::~EffectPluginFactory() +{ +} + +bool EffectPluginFactory::enabledByDefault() const +{ + return true; +} + +bool EffectPluginFactory::isSupported() const +{ + return true; +} + +} // namespace KWin + +#include "moc_effect.cpp" diff --git a/local/recipes/kde/kwin/source/src/effect/effect.h b/local/recipes/kde/kwin/source/src/effect/effect.h new file mode 100644 index 0000000000..e0ce680e4b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/effect.h @@ -0,0 +1,1009 @@ +/* + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/region.h" +#include "effect/globals.h" + +#include +#include + +class QKeyEvent; + +namespace KWin +{ + +class EffectWindow; +class LogicalOutput; +class PaintDataPrivate; +class RenderTarget; +class RenderViewport; +struct TabletToolProximityEvent; +struct TabletToolTipEvent; +struct TabletToolAxisEvent; +class WindowPaintDataPrivate; +class RenderView; + +/** @defgroup kwineffects KWin effects library + * KWin effects library contains necessary classes for creating new KWin + * compositing effects. + * + * @section creating Creating new effects + * This example will demonstrate the basics of creating an effect. We'll use + * CoolEffect as the class name, cooleffect as internal name and + * "Cool Effect" as user-visible name of the effect. + * + * This example doesn't demonstrate how to write the effect's code. For that, + * see the documentation of the Effect class. + * + * @subsection creating-class CoolEffect class + * First you need to create CoolEffect class which has to be a subclass of + * @ref KWin::Effect. In that class you can reimplement various virtual + * methods to control how and where the windows are drawn. + * + * @subsection creating-macro KWIN_EFFECT_FACTORY macro + * This library provides a specialized KPluginFactory subclass and macros to + * create a sub class. This subclass of KPluginFactory has to be used, otherwise + * KWin won't load the plugin. Use the @ref KWIN_EFFECT_FACTORY macro to create the + * plugin factory. This macro will take the embedded json metadata filename as the second argument. + * + * @subsection creating-buildsystem Buildsystem + * To build the effect, you can use the kcoreaddons_add_plugin cmake macro which + * takes care of creating the library and installing it. + * The first parameter is the name of the library, this is the same as the id of the plugin. + * If our effect's source is in cooleffect.cpp, we'd use following: + * @code + * kcoreaddons_add_plugin(cooleffect SOURCES cooleffect.cpp INSTALL_NAMESPACE "kwin/effects/plugins") + * @endcode + * + * @subsection creating-json-metadata Effect's .json file for embedded metadata + * The format follows the one of the @see KPluginMetaData class. + * + * Example cooleffect.json file: + * @code +{ + "KPlugin": { + "Authors": [ + { + "Email": "my@email.here", + "Name": "My Name" + } + ], + "Category": "Misc", + "Description": "The coolest effect you've ever seen", + "Icon": "preferences-system-windows-effect-cooleffect", + "Name": "Cool Effect" + } +} + * @endcode + * + * @section accessing Accessing windows and workspace + * Effects can gain access to the properties of windows and workspace via + * EffectWindow and EffectsHandler classes. + * + * There is one global EffectsHandler object which you can access using the + * @ref effects pointer. + * For each window, there is an EffectWindow object which can be used to read + * window properties such as position and also to change them. + * + * For more information about this, see the documentation of the corresponding + * classes. + * + * @{ + */ + +#define KWIN_EFFECT_API_MAKE_VERSION(major, minor) ((major) << 8 | (minor)) +#define KWIN_EFFECT_API_VERSION_MAJOR 0 +#define KWIN_EFFECT_API_VERSION_MINOR 237 +#define KWIN_EFFECT_API_VERSION KWIN_EFFECT_API_MAKE_VERSION( \ + KWIN_EFFECT_API_VERSION_MAJOR, KWIN_EFFECT_API_VERSION_MINOR) + +class KWIN_EXPORT PaintData +{ +public: + virtual ~PaintData(); + /** + * @returns scale factor in X direction. + * @since 4.10 + */ + qreal xScale() const; + /** + * @returns scale factor in Y direction. + * @since 4.10 + */ + qreal yScale() const; + /** + * @returns scale factor in Z direction. + * @since 4.10 + */ + qreal zScale() const; + /** + * Sets the scale factor in X direction to @p scale + * @param scale The scale factor in X direction + * @since 4.10 + */ + void setXScale(qreal scale); + /** + * Sets the scale factor in Y direction to @p scale + * @param scale The scale factor in Y direction + * @since 4.10 + */ + void setYScale(qreal scale); + /** + * Sets the scale factor in Z direction to @p scale + * @param scale The scale factor in Z direction + * @since 4.10 + */ + void setZScale(qreal scale); + /** + * Sets the scale factor in X and Y direction. + * @param scale The scale factor for X and Y direction + * @since 4.10 + */ + void setScale(const QVector2D &scale); + /** + * Sets the scale factor in X, Y and Z direction + * @param scale The scale factor for X, Y and Z direction + * @since 4.10 + */ + void setScale(const QVector3D &scale); + const QVector3D &scale() const; + const QVector3D &translation() const; + /** + * @returns the translation in X direction. + * @since 4.10 + */ + qreal xTranslation() const; + /** + * @returns the translation in Y direction. + * @since 4.10 + */ + qreal yTranslation() const; + /** + * @returns the translation in Z direction. + * @since 4.10 + */ + qreal zTranslation() const; + /** + * Sets the translation in X direction to @p translate. + * @since 4.10 + */ + void setXTranslation(qreal translate); + /** + * Sets the translation in Y direction to @p translate. + * @since 4.10 + */ + void setYTranslation(qreal translate); + /** + * Sets the translation in Z direction to @p translate. + * @since 4.10 + */ + void setZTranslation(qreal translate); + /** + * Performs a translation by adding the values component wise. + * @param x Translation in X direction + * @param y Translation in Y direction + * @param z Translation in Z direction + * @since 4.10 + */ + void translate(qreal x, qreal y = 0.0, qreal z = 0.0); + /** + * Performs a translation by adding the values component wise. + * Overloaded method for convenience. + * @param translate The translation + * @since 4.10 + */ + void translate(const QVector3D &translate); + + /** + * Sets the rotation angle. + * @param angle The new rotation angle. + * @since 4.10 + * @see rotationAngle() + */ + void setRotationAngle(qreal angle); + /** + * Returns the rotation angle. + * Initially 0.0. + * @returns The current rotation angle. + * @since 4.10 + * @see setRotationAngle + */ + qreal rotationAngle() const; + /** + * Sets the rotation origin. + * @param origin The new rotation origin. + * @since 4.10 + * @see rotationOrigin() + */ + void setRotationOrigin(const QVector3D &origin); + /** + * Returns the rotation origin. That is the point in space which is fixed during the rotation. + * Initially this is 0/0/0. + * @returns The rotation's origin + * @since 4.10 + * @see setRotationOrigin() + */ + QVector3D rotationOrigin() const; + /** + * Sets the rotation axis. + * Set a component to 1.0 to rotate around this axis and to 0.0 to disable rotation around the + * axis. + * @param axis A vector holding information on which axis to rotate + * @since 4.10 + * @see rotationAxis() + */ + void setRotationAxis(const QVector3D &axis); + /** + * Sets the rotation axis. + * Overloaded method for convenience. + * @param axis The axis around which should be rotated. + * @since 4.10 + * @see rotationAxis() + */ + void setRotationAxis(Qt::Axis axis); + /** + * The current rotation axis. + * By default the rotation is (0/0/1) which means a rotation around the z axis. + * @returns The current rotation axis. + * @since 4.10 + * @see setRotationAxis + */ + QVector3D rotationAxis() const; + + /** + * Returns the corresponding transform matrix. + * + * The transform matrix is converted to device coordinates using the + * supplied deviceScale. + */ + QMatrix4x4 toMatrix(qreal deviceScale) const; + +protected: + PaintData(); + PaintData(const PaintData &other); + +private: + const std::unique_ptr d; +}; + +class KWIN_EXPORT WindowPrePaintData +{ +public: + int mask; + /** + * Region that will be painted, in device coordinates. + */ + Region devicePaint; + /** + * Region indicating the opaque content. It can be used to avoid painting + * windows occluded by the opaque region. + */ + Region deviceOpaque; + /** + * Simple helper that sets data to say the window will be painted as non-opaque. + * Takes also care of changing the regions. + */ + void setTranslucent(); + /** + * Helper to mark that this window will be transformed + */ + void setTransformed(); +}; + +class KWIN_EXPORT WindowPaintData : public PaintData +{ +public: + WindowPaintData(); + WindowPaintData(const WindowPaintData &other); + ~WindowPaintData() override; + /** + * Scales the window by @p scale factor. + * Multiplies all three components by the given factor. + * @since 4.10 + */ + WindowPaintData &operator*=(qreal scale); + /** + * Scales the window by @p scale factor. + * Performs a component wise multiplication on x and y components. + * @since 4.10 + */ + WindowPaintData &operator*=(const QVector2D &scale); + /** + * Scales the window by @p scale factor. + * Performs a component wise multiplication. + * @since 4.10 + */ + WindowPaintData &operator*=(const QVector3D &scale); + /** + * Translates the window by the given @p translation and returns a reference to the ScreenPaintData. + * @since 4.10 + */ + WindowPaintData &operator+=(const QPointF &translation); + /** + * Translates the window by the given @p translation and returns a reference to the ScreenPaintData. + * Overloaded method for convenience. + * @since 4.10 + */ + WindowPaintData &operator+=(const QPoint &translation); + /** + * Translates the window by the given @p translation and returns a reference to the ScreenPaintData. + * Overloaded method for convenience. + * @since 4.10 + */ + WindowPaintData &operator+=(const QVector2D &translation); + /** + * Translates the window by the given @p translation and returns a reference to the ScreenPaintData. + * Overloaded method for convenience. + * @since 4.10 + */ + WindowPaintData &operator+=(const QVector3D &translation); + /** + * Window opacity, in range 0 = transparent to 1 = fully opaque + * @see setOpacity + * @since 4.10 + */ + qreal opacity() const; + /** + * Sets the window opacity to the new @p opacity. + * If you want to modify the existing opacity level consider using multiplyOpacity. + * @param opacity The new opacity level + * @since 4.10 + */ + void setOpacity(qreal opacity); + /** + * Multiplies the current opacity with the @p factor. + * @param factor Factor with which the opacity should be multiplied + * @return New opacity level + * @since 4.10 + */ + qreal multiplyOpacity(qreal factor); + /** + * Saturation of the window, in range [0; 1] + * 1 means that the window is unchanged, 0 means that it's completely + * unsaturated (greyscale). 0.5 would make the colors less intense, + * but not completely grey + * Use EffectsHandler::saturationSupported() to find out whether saturation + * is supported by the system, otherwise this value has no effect. + * @return The current saturation + * @see setSaturation() + * @since 4.10 + */ + qreal saturation() const; + /** + * Sets the window saturation level to @p saturation. + * If you want to modify the existing saturation level consider using multiplySaturation. + * @param saturation The new saturation level + * @since 4.10 + */ + void setSaturation(qreal saturation) const; + /** + * Multiplies the current saturation with @p factor. + * @param factor with which the saturation should be multiplied + * @return New saturation level + * @since 4.10 + */ + qreal multiplySaturation(qreal factor); + /** + * Brightness of the window, in range [0; 1] + * 1 means that the window is unchanged, 0 means that it's completely + * black. 0.5 would make it 50% darker than usual + */ + qreal brightness() const; + /** + * Sets the window brightness level to @p brightness. + * If you want to modify the existing brightness level consider using multiplyBrightness. + * @param brightness The new brightness level + */ + void setBrightness(qreal brightness); + /** + * Multiplies the current brightness level with @p factor. + * @param factor with which the brightness should be multiplied. + * @return New brightness level + * @since 4.10 + */ + qreal multiplyBrightness(qreal factor); + /** + * @brief Sets the cross fading @p factor to fade over with previously sized window. + * If @c 1.0 only the current window is used, if @c 0.0 only the previous window is used. + * + * By default only the current window is used. This factor can only make any visual difference + * if the previous window get referenced. + * + * @param factor The cross fade factor between @c 0.0 (previous window) and @c 1.0 (current window) + * @see crossFadeProgress + */ + void setCrossFadeProgress(qreal factor); + /** + * @see setCrossFadeProgress + */ + qreal crossFadeProgress() const; + +private: + const std::unique_ptr d; +}; + +class KWIN_EXPORT ScreenPrePaintData +{ +public: + int mask; + Region paint; + LogicalOutput *screen = nullptr; + RenderView *view = nullptr; +}; + +/** + * @short Base class for all KWin effects + * + * This is the base class for all effects. By reimplementing virtual methods + * of this class, you can customize how the windows are painted. + * + * The virtual methods are used for painting and need to be implemented for + * custom painting. + * + * In order to react to state changes (e.g. a window gets closed) the effect + * should provide slots for the signals emitted by the EffectsHandler. + * + * @section Chaining + * Most methods of this class are called in chain style. This means that when + * effects A and B area active then first e.g. A::paintWindow() is called and + * then from within that method B::paintWindow() is called (although + * indirectly). To achieve this, you need to make sure to call corresponding + * method in EffectsHandler class from each such method (using @ref effects + * pointer): + * @code + * void MyEffect::postPaintScreen() + * { + * // Do your own processing here + * ... + * // Call corresponding EffectsHandler method + * effects->postPaintScreen(); + * } + * @endcode + * + * @section Effectsptr Effects pointer + * @ref effects pointer points to the global EffectsHandler object that you can + * use to interact with the windows. + * + * @section painting Painting stages + * Painting of windows is done in three stages: + * @li First, the prepaint pass.
+ * Here you can specify how the windows will be painted, e.g. that they will + * be translucent and transformed. + * @li Second, the paint pass.
+ * Here the actual painting takes place. You can change attributes such as + * opacity of windows as well as apply transformations to them. You can also + * paint something onto the screen yourself. + * @li Finally, the postpaint pass.
+ * Here you can mark windows, part of windows or even the entire screen for + * repainting to create animations. + * + * For each stage there are *Screen() and *Window() methods. The window method + * is called for every window while the screen method is usually called just + * once. + * + * @section OpenGL + * Effects can use OpenGL if EffectsHandler::isOpenGLCompositing() returns @c true. + * The OpenGL context may not always be current when code inside the effect is + * executed. The framework ensures that the OpenGL context is current when the Effect + * gets created, destroyed or reconfigured and during the painting stages. All virtual + * methods which have the OpenGL context current are documented. + * + * If OpenGL code is going to be executed outside the painting stages, e.g. in reaction + * to a global shortcut, it is the task of the Effect to make the OpenGL context current: + * @code + * effects->makeOpenGLContextCurrent(); + * @endcode + * + * There is in general no need to call the matching doneCurrent method. + */ +class KWIN_EXPORT Effect : public QObject +{ + Q_OBJECT +public: + /** Flags controlling how painting is done. */ + // TODO: is that ok here? + enum { + /** + * Window (or at least part of it) will be painted opaque. + */ + PAINT_WINDOW_OPAQUE = 1 << 0, + /** + * Window (or at least part of it) will be painted translucent. + */ + PAINT_WINDOW_TRANSLUCENT = 1 << 1, + /** + * Window will be painted with transformed geometry. + */ + PAINT_WINDOW_TRANSFORMED = 1 << 2, + /** + * Paint only a region of the screen (can be optimized, cannot + * be used together with TRANSFORMED flags). + */ + PAINT_SCREEN_REGION = 1 << 3, + /** + * The whole screen will be painted with transformed geometry. + * Forces the entire screen to be painted. + */ + PAINT_SCREEN_TRANSFORMED = 1 << 4, + /** + * At least one window will be painted with transformed geometry. + * Forces the entire screen to be painted. + */ + PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS = 1 << 5, + /** + * Clear whole background as the very first step, without optimizing it + */ + PAINT_SCREEN_BACKGROUND_FIRST = 1 << 6, + }; + + enum Feature { + Nothing = 0, + ScreenInversion, + Blur, + Contrast, + HighlightWindows, + SystemBell, + }; + + /** + * Constructs new Effect object. + * + * In OpenGL based compositing, the frameworks ensures that the context is current + * when the Effect is constructed. + */ + Effect(QObject *parent = nullptr); + /** + * Destructs the Effect object. + * + * In OpenGL based compositing, the frameworks ensures that the context is current + * when the Effect is destroyed. + */ + ~Effect() override; + + /** + * Flags describing which parts of configuration have changed. + */ + enum ReconfigureFlag { + ReconfigureAll = 1 << 0 /// Everything needs to be reconfigured. + }; + Q_DECLARE_FLAGS(ReconfigureFlags, ReconfigureFlag) + + /** + * Called when configuration changes (either the effect's or KWin's global). + * + * In OpenGL based compositing, the frameworks ensures that the context is current + * when the Effect is reconfigured. If this method is called from within the Effect it is + * required to ensure that the context is current if the implementation does OpenGL calls. + */ + virtual void reconfigure(ReconfigureFlags flags); + + /** + * Called before starting to paint the screen. + * In this method you can: + * @li set whether the windows or the entire screen will be transformed + * @li change the region of the screen that will be painted + * @li do various housekeeping tasks such as initing your effect's variables + for the upcoming paint pass or updating animation's progress + * + * @a presentTime specifies the expected monotonic time when the rendered frame + * will be displayed on the screen. + */ + virtual void prePaintScreen(ScreenPrePaintData &data, + std::chrono::milliseconds presentTime); + /** + * In this method you can: + * @li paint something on top of the windows (by painting after calling + * effects->paintScreen()) + * @li paint multiple desktops and/or multiple copies of the same desktop + * by calling effects->paintScreen() multiple times + * + * In OpenGL based compositing, the frameworks ensures that the context is current + * when this method is invoked. + */ + virtual void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen); + /** + * Called after all the painting has been finished. + * In this method you can: + * @li schedule next repaint in case of animations + * You shouldn't paint anything here. + * + * In OpenGL based compositing, the frameworks ensures that the context is current + * when this method is invoked. + */ + virtual void postPaintScreen(); + + /** + * Called for every window before the actual paint pass + * In this method you can: + * @li enable or disable painting of the window (e.g. enable painting of minimized window) + * @li set window to be painted with translucency + * @li set window to be transformed + * @li request the window to be divided into multiple parts + * + * In OpenGL based compositing, the frameworks ensures that the context is current + * when this method is invoked. + * + * @a presentTime specifies the expected monotonic time when the rendered frame + * will be displayed on the screen. + */ + virtual void prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, + std::chrono::milliseconds presentTime); + /** + * This is the main method for painting windows. + * In this method you can: + * @li do various transformations + * @li change opacity of the window + * @li change brightness and/or saturation, if it's supported + * + * In OpenGL based compositing, the frameworks ensures that the context is current + * when this method is invoked. + */ + virtual void paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data); + + /** + * Called on Transparent resizes. + * return true if your effect substitutes questioned feature + */ + virtual bool provides(Feature); + + /** + * Performs the @p feature with the @p arguments. + * + * This allows to have specific protocols between KWin core and an Effect. + * + * The method is supposed to return @c true if it performed the features, + * @c false otherwise. + * + * The default implementation returns @c false. + * @since 5.8 + */ + virtual bool perform(Feature feature, const QVariantList &arguments); + + /** + * Can be called to draw multiple copies (e.g. thumbnails) of a window. + * You can change window's opacity/brightness/etc here, but you can't + * do any transformations. + * + * In OpenGL based compositing, the frameworks ensures that the context is current + * when this method is invoked. + */ + virtual void drawWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data); + + virtual void windowInputMouseEvent(QEvent *e); + virtual void grabbedKeyboardEvent(QKeyEvent *e); + + /** + * Overwrite this method to indicate whether your effect will be doing something in + * the next frame to be rendered. If the method returns @c false the effect will be + * excluded from the chained methods in the next rendered frame. + * + * This method is called always directly before the paint loop begins. So it is totally + * fine to e.g. react on a window event, issue a repaint to trigger an animation and + * change a flag to indicate that this method returns @c true. + * + * As the method is called each frame, you should not perform complex calculations. + * Best use just a boolean flag. + * + * The default implementation of this method returns @c true. + * @since 4.8 + */ + virtual bool isActive() const; + + /** + * Reimplement this method to provide online debugging. + * This could be as trivial as printing specific detail information about the effect state + * but could also be used to move the effect in and out of a special debug modes, clear bogus + * data, etc. + * Notice that the functions is const by intent! Whenever you alter the state of the object + * due to random user input, you should do so with greatest care, hence const_cast<> your + * object - signalling "let me alone, i know what i'm doing" + * @param parameter A freeform string user input for your effect to interpret. + * @since 4.11 + */ + virtual QString debug(const QString ¶meter) const; + + /** + * Reimplement this method to indicate where in the Effect chain the Effect should be placed. + * + * A low number indicates early chain position, thus before other Effects got called, a high + * number indicates a late position. The returned number should be in the interval [0, 100]. + * The default value is 0. + * + * In KWin4 this information was provided in the Effect's desktop file as property + * X-KDE-Ordering. In the case of Scripted Effects this property is still used. + * + * @since 5.0 + */ + virtual int requestedEffectChainPosition() const; + + /** + * A touch point was pressed. + * + * If the effect wants to exclusively use the touch event it should return @c true. + * If @c false is returned the touch event is passed to further effects. + * + * In general an Effect should only return @c true if it is the exclusive effect getting + * input events. E.g. has grabbed mouse events. + * + * Default implementation returns @c false. + * + * @param id The unique id of the touch point + * @param pos The position of the touch point in global coordinates + * @param time Timestamp + * + * @see touchMotion + * @see touchUp + * @since 5.8 + */ + virtual bool touchDown(qint32 id, const QPointF &pos, std::chrono::microseconds time); + /** + * A touch point moved. + * + * If the effect wants to exclusively use the touch event it should return @c true. + * If @c false is returned the touch event is passed to further effects. + * + * In general an Effect should only return @c true if it is the exclusive effect getting + * input events. E.g. has grabbed mouse events. + * + * Default implementation returns @c false. + * + * @param id The unique id of the touch point + * @param pos The position of the touch point in global coordinates + * @param time Timestamp + * + * @see touchDown + * @see touchUp + * @since 5.8 + */ + virtual bool touchMotion(qint32 id, const QPointF &pos, std::chrono::microseconds time); + /** + * A touch point was released. + * + * If the effect wants to exclusively use the touch event it should return @c true. + * If @c false is returned the touch event is passed to further effects. + * + * In general an Effect should only return @c true if it is the exclusive effect getting + * input events. E.g. has grabbed mouse events. + * + * Default implementation returns @c false. + * + * @param id The unique id of the touch point + * @param time Timestamp + * + * @see touchDown + * @see touchMotion + * @since 5.8 + */ + virtual bool touchUp(qint32 id, std::chrono::microseconds time); + /** + * All touch points were canceled + * @since 6.3 + */ + virtual void touchCancel(); + + /** + * There has been a proximity tablet tool event. + */ + virtual bool tabletToolProximity(TabletToolProximityEvent *event); + + /** + * There has been an axis tablet tool event. + */ + virtual bool tabletToolAxis(TabletToolAxisEvent *event); + + /** + * There has been a tip tablet tool event. + */ + virtual bool tabletToolTip(TabletToolTipEvent *event); + + /** + * There has been an event from a button on a drawing tablet tool + * + * @param button which button + * @param pressed true if pressed, false when released + * @param toolId the identifier of the tool id + * + * @since 5.25 + */ + virtual bool tabletToolButtonEvent(uint button, bool pressed, quint64 toolId); + + /** + * There has been an event from a button on a drawing tablet pad + * + * @param button which button + * @param pressed true if pressed, false when released + * @param device the identifier of the tool id + * + * @since 5.25 + */ + virtual bool tabletPadButtonEvent(uint button, bool pressed, void *device); + + /** + * There has been an event from a input strip on a drawing tablet pad + * + * @param number which strip + * @param position the value within the strip that was selected + * @param isFinger if it was activated with a finger + * @param device the identifier of the tool id + * + * @since 5.25 + */ + virtual bool tabletPadStripEvent(int number, qreal position, bool isFinger, void *device); + + /** + * There has been an event from a input ring on a drawing tablet pad + * + * @param number which ring + * @param position the value within the ring that was selected + * @param isFinger if it was activated with a finger + * @param device the identifier of the tool id + * + * @since 5.25 + */ + virtual bool tabletPadRingEvent(int number, qreal position, bool isFinger, void *device); + + /** + * There has been an event from a input dial on a drawing tablet pad + * + * @param number which dial + * @param delta the delta value + * @param device the identifier of the tool id + * + * @since 6.4 + */ + virtual bool tabletPadDialEvent(int number, double delta, void *device); + + static QPointF cursorPos(); + + /** + * Read animation time from the configuration and possibly adjust using animationTimeFactor(). + * The configuration value in the effect should also have special value 'default' (set using + * QSpinBox::setSpecialValueText()) with the value 0. This special value is adjusted + * using the global animation speed, otherwise the exact time configured is returned. + * @param cfg configuration group to read value from + * @param key configuration key to read value from + * @param defaultTime default animation time in milliseconds + */ + // return type is intentionally double so that one can divide using it without losing data + static double animationTime(const KConfigGroup &cfg, const QString &key, std::chrono::milliseconds defaultTime); + /** + * @overload Use this variant if the animation time is hardcoded and not configurable + * in the effect itself. + */ + static double animationTime(std::chrono::milliseconds defaultTime); + /** + * @overload Use this variant if animation time is provided through a KConfigXT generated class + * having a property called "duration". + */ + template + int animationTime(std::chrono::milliseconds defaultDuration); + /** + * Linearly interpolates between @p x and @p y. + * + * Returns @p x when @p a = 0; returns @p y when @p a = 1. + */ + static double interpolate(double x, double y, double a) + { + return x * (1 - a) + y * a; + } + /** Helper to set WindowPaintData and Region to necessary transformations so that + * a following drawWindow() would put the window at the requested geometry (useful for thumbnails) + */ + static void setPositionTransformations(WindowPaintData &data, Rect &logicalRegion, EffectWindow *w, + const Rect &r, Qt::AspectRatioMode aspect); + + /** + * overwrite this method to return false if your effect does not need to be drawn over opaque fullscreen windows + */ + virtual bool blocksDirectScanout() const; + +public Q_SLOTS: + virtual bool borderActivated(ElectricBorder border); +}; + +template +int Effect::animationTime(std::chrono::milliseconds defaultDuration) +{ + return animationTime(T::duration() != 0 ? std::chrono::milliseconds(T::duration()) : defaultDuration); +} + +/** + * Prefer the KWIN_EFFECT_FACTORY macros. + */ +class KWIN_EXPORT EffectPluginFactory : public KPluginFactory +{ + Q_OBJECT +public: + EffectPluginFactory(); + ~EffectPluginFactory() override; + /** + * Returns whether the Effect is supported. + * + * An Effect can implement this method to determine at runtime whether the Effect is supported. + * + * If the current compositing backend is not supported it should return @c false. + * + * This method is optional, by default @c true is returned. + */ + virtual bool isSupported() const; + /** + * Returns whether the Effect should get enabled by default. + * + * This function provides a way for an effect to override the default at runtime, + * e.g. based on the capabilities of the hardware. + * + * This method is optional; the effect doesn't have to provide it. + * + * Note that this function is only called if the supported() function returns true, + * and if X-KDE-PluginInfo-EnabledByDefault is set to true in the .desktop file. + * + * This method is optional, by default @c true is returned. + */ + virtual bool enabledByDefault() const; + /** + * This method returns the created Effect. + */ + virtual KWin::Effect *createEffect() const = 0; +}; + +#define EffectPluginFactory_iid "org.kde.kwin.EffectPluginFactory" KWIN_PLUGIN_VERSION_STRING +#define KWIN_PLUGIN_FACTORY_NAME KPLUGINFACTORY_PLUGIN_CLASS_INTERNAL_NAME + +/** + * Defines an EffectPluginFactory sub class with customized isSupported and enabledByDefault methods. + * + * If the Effect to be created does not need the isSupported or enabledByDefault methods prefer + * the simplified KWIN_EFFECT_FACTORY, KWIN_EFFECT_FACTORY_SUPPORTED or KWIN_EFFECT_FACTORY_ENABLED + * macros which create an EffectPluginFactory with a usable default value. + * + * This API is not providing binary compatibility and thus the effect plugin must be compiled against + * the same kwineffects library version as KWin. + * + * @param factoryName The name to be used for the EffectPluginFactory + * @param className The class name of the Effect sub class which is to be created by the factory + * @param jsonFile Name of the json file to be compiled into the plugin as metadata + * @param supported Source code to go into the isSupported() method, must return a boolean + * @param enabled Source code to go into the enabledByDefault() method, must return a boolean + */ +#define KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED(className, jsonFile, supported, enabled) \ + class KWIN_PLUGIN_FACTORY_NAME : public KWin::EffectPluginFactory \ + { \ + Q_OBJECT \ + Q_PLUGIN_METADATA(IID EffectPluginFactory_iid FILE jsonFile) \ + Q_INTERFACES(KPluginFactory) \ + public: \ + explicit KWIN_PLUGIN_FACTORY_NAME() \ + { \ + } \ + ~KWIN_PLUGIN_FACTORY_NAME() \ + { \ + } \ + bool isSupported() const override \ + { \ + supported \ + } \ + bool enabledByDefault() const override{ \ + enabled} KWin::Effect *createEffect() const override \ + { \ + return new className(); \ + } \ + }; + +#define KWIN_EFFECT_FACTORY_ENABLED(className, jsonFile, enabled) \ + KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED(className, jsonFile, return true;, enabled) + +#define KWIN_EFFECT_FACTORY_SUPPORTED(className, jsonFile, supported) \ + KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED(className, jsonFile, supported, return true;) + +#define KWIN_EFFECT_FACTORY(className, jsonFile) \ + KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED(className, jsonFile, return true;, return true;) + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/effect/effectframe.cpp b/local/recipes/kde/kwin/source/src/effect/effectframe.cpp new file mode 100644 index 0000000000..237e563a20 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/effectframe.cpp @@ -0,0 +1,362 @@ +/* + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "effect/effectframe.h" +#include "effect/effecthandler.h" + +#include +#include + +namespace KWin +{ + +EffectFrameQuickScene::EffectFrameQuickScene(EffectFrameStyle style, bool staticSize, QPoint position, Qt::Alignment alignment) + : m_style(style) + , m_static(staticSize) + , m_point(position) + , m_alignment(alignment) +{ + + QString name; + switch (style) { + case EffectFrameNone: + name = QStringLiteral("none"); + break; + case EffectFrameUnstyled: + name = QStringLiteral("unstyled"); + break; + case EffectFrameStyled: + name = QStringLiteral("styled"); + break; + } + + const QString defaultPath = QStringLiteral("kwin-wayland/frames/plasma/frame_%1.qml").arg(name); + // TODO read from kwinApp()->config() "QmlPath" like Outline/OnScreenNotification + // *if* someone really needs this to be configurable. + const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, defaultPath); + + setSource(QUrl::fromLocalFile(path), QVariantMap{{QStringLiteral("effectFrame"), QVariant::fromValue(this)}}); + + if (rootItem()) { + connect(rootItem(), &QQuickItem::implicitWidthChanged, this, &EffectFrameQuickScene::reposition); + connect(rootItem(), &QQuickItem::implicitHeightChanged, this, &EffectFrameQuickScene::reposition); + } +} + +EffectFrameQuickScene::~EffectFrameQuickScene() = default; + +EffectFrameStyle EffectFrameQuickScene::style() const +{ + return m_style; +} + +bool EffectFrameQuickScene::isStatic() const +{ + return m_static; +} + +QFont EffectFrameQuickScene::font() const +{ + return m_font; +} + +void EffectFrameQuickScene::setFont(const QFont &font) +{ + if (m_font == font) { + return; + } + + m_font = font; + Q_EMIT fontChanged(font); + reposition(); +} + +QIcon EffectFrameQuickScene::icon() const +{ + return m_icon; +} + +void EffectFrameQuickScene::setIcon(const QIcon &icon) +{ + m_icon = icon; + Q_EMIT iconChanged(icon); + reposition(); +} + +QSize EffectFrameQuickScene::iconSize() const +{ + return m_iconSize; +} + +void EffectFrameQuickScene::setIconSize(const QSize &iconSize) +{ + if (m_iconSize == iconSize) { + return; + } + + m_iconSize = iconSize; + Q_EMIT iconSizeChanged(iconSize); + reposition(); +} + +QString EffectFrameQuickScene::text() const +{ + return m_text; +} + +void EffectFrameQuickScene::setText(const QString &text) +{ + if (m_text == text) { + return; + } + + m_text = text; + Q_EMIT textChanged(text); + reposition(); +} + +qreal EffectFrameQuickScene::frameOpacity() const +{ + return m_frameOpacity; +} + +void EffectFrameQuickScene::setFrameOpacity(qreal frameOpacity) +{ + if (m_frameOpacity != frameOpacity) { + m_frameOpacity = frameOpacity; + Q_EMIT frameOpacityChanged(frameOpacity); + } +} + +bool EffectFrameQuickScene::crossFadeEnabled() const +{ + return m_crossFadeEnabled; +} + +void EffectFrameQuickScene::setCrossFadeEnabled(bool enabled) +{ + if (m_crossFadeEnabled != enabled) { + m_crossFadeEnabled = enabled; + Q_EMIT crossFadeEnabledChanged(enabled); + } +} + +qreal EffectFrameQuickScene::crossFadeProgress() const +{ + return m_crossFadeProgress; +} + +void EffectFrameQuickScene::setCrossFadeProgress(qreal progress) +{ + if (m_crossFadeProgress != progress) { + m_crossFadeProgress = progress; + Q_EMIT crossFadeProgressChanged(progress); + } +} + +Qt::Alignment EffectFrameQuickScene::alignment() const +{ + return m_alignment; +} + +void EffectFrameQuickScene::setAlignment(Qt::Alignment alignment) +{ + if (m_alignment == alignment) { + return; + } + + m_alignment = alignment; + reposition(); +} + +QPoint EffectFrameQuickScene::position() const +{ + return m_point; +} + +void EffectFrameQuickScene::setPosition(const QPoint &point) +{ + if (m_point == point) { + return; + } + + m_point = point; + reposition(); +} + +void EffectFrameQuickScene::reposition() +{ + if (!rootItem() || m_point.x() < 0 || m_point.y() < 0) { + return; + } + + QSizeF size; + if (m_static) { + size = rootItem()->size(); + } else { + size = QSizeF(rootItem()->implicitWidth(), rootItem()->implicitHeight()); + } + + QRect geometry(QPoint(), size.toSize()); + + if (m_alignment & Qt::AlignLeft) + geometry.moveLeft(m_point.x()); + else if (m_alignment & Qt::AlignRight) + geometry.moveLeft(m_point.x() - geometry.width()); + else + geometry.moveLeft(m_point.x() - geometry.width() / 2); + if (m_alignment & Qt::AlignTop) + geometry.moveTop(m_point.y()); + else if (m_alignment & Qt::AlignBottom) + geometry.moveTop(m_point.y() - geometry.height()); + else + geometry.moveTop(m_point.y() - geometry.height() / 2); + + if (geometry == this->geometry()) { + return; + } + + setGeometry(geometry); +} + +EffectFrame::EffectFrame(EffectFrameStyle style, bool staticSize, QPoint position, Qt::Alignment alignment) + : m_view(new EffectFrameQuickScene(style, staticSize, position, alignment)) +{ + connect(m_view, &OffscreenQuickScene::repaintNeeded, this, [this] { + effects->addRepaint(geometry()); + }); + connect(m_view, &OffscreenQuickScene::geometryChanged, this, [](const QRect &oldGeometry, const QRect &newGeometry) { + effects->addRepaint(oldGeometry); + effects->addRepaint(newGeometry); + }); +} + +EffectFrame::~EffectFrame() +{ + // Effects often destroy their cached TextFrames in pre/postPaintScreen. + // Destroying an OffscreenQuickView changes GL context, which we + // must not do during effect rendering. + // Delay destruction of the view until after the rendering. + m_view->deleteLater(); +} + +Qt::Alignment EffectFrame::alignment() const +{ + return m_view->alignment(); +} + +void EffectFrame::setAlignment(Qt::Alignment alignment) +{ + m_view->setAlignment(alignment); +} + +QFont EffectFrame::font() const +{ + return m_view->font(); +} + +void EffectFrame::setFont(const QFont &font) +{ + m_view->setFont(font); +} + +void EffectFrame::free() +{ + m_view->hide(); +} + +QRect EffectFrame::geometry() const +{ + return m_view->geometry(); +} + +void EffectFrame::setGeometry(const QRect &geometry, bool force) +{ + m_view->setGeometry(geometry); +} + +QIcon EffectFrame::icon() const +{ + return m_view->icon(); +} + +void EffectFrame::setIcon(const QIcon &icon) +{ + m_view->setIcon(icon); + + if (m_view->iconSize().isEmpty() && !icon.availableSizes().isEmpty()) { // Set a size if we don't already have one + setIconSize(icon.availableSizes().constFirst()); + } +} + +QSize EffectFrame::iconSize() const +{ + return m_view->iconSize(); +} + +void EffectFrame::setIconSize(const QSize &size) +{ + m_view->setIconSize(size); +} + +void EffectFrame::setPosition(const QPoint &point) +{ + m_view->setPosition(point); +} + +void EffectFrame::render(const RenderTarget &renderTarget, const RenderViewport &viewport, const Region &deviceRegion, double opacity, double frameOpacity) +{ + if (!m_view->rootItem()) { + return; + } + + m_view->show(); + + m_view->setOpacity(opacity); + m_view->setFrameOpacity(frameOpacity); + + effects->renderOffscreenQuickView(renderTarget, viewport, m_view); +} + +QString EffectFrame::text() const +{ + return m_view->text(); +} + +void EffectFrame::setText(const QString &text) +{ + m_view->setText(text); +} + +EffectFrameStyle EffectFrame::style() const +{ + return m_view->style(); +} + +bool EffectFrame::isCrossFade() const +{ + return m_view->crossFadeEnabled(); +} + +void EffectFrame::enableCrossFade(bool enable) +{ + m_view->setCrossFadeEnabled(enable); +} + +qreal EffectFrame::crossFadeProgress() const +{ + return m_view->crossFadeProgress(); +} + +void EffectFrame::setCrossFadeProgress(qreal progress) +{ + m_view->setCrossFadeProgress(progress); +} + +} // namespace KWin + +#include "moc_effectframe.cpp" diff --git a/local/recipes/kde/kwin/source/src/effect/effectframe.h b/local/recipes/kde/kwin/source/src/effect/effectframe.h new file mode 100644 index 0000000000..3c2619a249 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/effectframe.h @@ -0,0 +1,203 @@ +/* + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/region.h" +#include "effect/globals.h" +#include "effect/offscreenquickview.h" + +#include +#include + +namespace KWin +{ + +class RenderTarget; +class RenderViewport; + +/** + * Style types used by @ref EffectFrame. + * @since 4.6 + */ +enum EffectFrameStyle { + EffectFrameNone, ///< Displays no frame around the contents. + EffectFrameUnstyled, ///< Displays a basic box around the contents. + EffectFrameStyled ///< Displays a Plasma-styled frame around the contents. +}; + +class EffectFrameQuickScene : public OffscreenQuickScene +{ + Q_OBJECT + + Q_PROPERTY(QFont font READ font NOTIFY fontChanged) + Q_PROPERTY(QIcon icon READ icon NOTIFY iconChanged) + Q_PROPERTY(QSize iconSize READ iconSize NOTIFY iconSizeChanged) + Q_PROPERTY(QString text READ text NOTIFY textChanged) + Q_PROPERTY(qreal frameOpacity READ frameOpacity NOTIFY frameOpacityChanged) + Q_PROPERTY(bool crossFadeEnabled READ crossFadeEnabled NOTIFY crossFadeEnabledChanged) + Q_PROPERTY(qreal crossFadeProgress READ crossFadeProgress NOTIFY crossFadeProgressChanged) + +public: + EffectFrameQuickScene(EffectFrameStyle style, bool staticSize, QPoint position, Qt::Alignment alignment); + ~EffectFrameQuickScene() override; + + EffectFrameStyle style() const; + bool isStatic() const; + + QFont font() const; + void setFont(const QFont &font); + Q_SIGNAL void fontChanged(const QFont &font); + + QIcon icon() const; + void setIcon(const QIcon &icon); + Q_SIGNAL void iconChanged(const QIcon &icon); + + QSize iconSize() const; + void setIconSize(const QSize &iconSize); + Q_SIGNAL void iconSizeChanged(const QSize &iconSize); + + QString text() const; + void setText(const QString &text); + Q_SIGNAL void textChanged(const QString &text); + + qreal frameOpacity() const; + void setFrameOpacity(qreal frameOpacity); + Q_SIGNAL void frameOpacityChanged(qreal frameOpacity); + + bool crossFadeEnabled() const; + void setCrossFadeEnabled(bool enabled); + Q_SIGNAL void crossFadeEnabledChanged(bool enabled); + + qreal crossFadeProgress() const; + void setCrossFadeProgress(qreal progress); + Q_SIGNAL void crossFadeProgressChanged(qreal progress); + + Qt::Alignment alignment() const; + void setAlignment(Qt::Alignment alignment); + + QPoint position() const; + void setPosition(const QPoint &point); + +private: + void reposition(); + + EffectFrameStyle m_style; + + // Position + bool m_static; + QPoint m_point; + Qt::Alignment m_alignment; + + // Contents + QFont m_font; + QIcon m_icon; + QSize m_iconSize; + QString m_text; + qreal m_frameOpacity = 0.0; + bool m_crossFadeEnabled = false; + qreal m_crossFadeProgress = 0.0; +}; + +/** + * @short Helper class for displaying text and icons in frames. + * + * Paints text and/or and icon with an optional frame around them. The + * available frames includes one that follows the default Plasma theme and + * another that doesn't. + * It is recommended to use this class whenever displaying text. + */ +class KWIN_EXPORT EffectFrame : public QObject +{ + Q_OBJECT + +public: + explicit EffectFrame(EffectFrameStyle style, bool staticSize = true, QPoint position = QPoint(-1, -1), + Qt::Alignment alignment = Qt::AlignCenter); + ~EffectFrame(); + + /** + * Delete any existing textures to free up graphics memory. They will + * be automatically recreated the next time they are required. + */ + void free(); + + /** + * Render the frame. + */ + void render(const RenderTarget &renderTarget, const RenderViewport &viewport, const Region &deviceRegion = Region::infinite(), double opacity = 1.0, double frameOpacity = 1.0); + + void setPosition(const QPoint &point); + /** + * Set the text alignment for static frames and the position alignment + * for non-static. + */ + void setAlignment(Qt::Alignment alignment); + Qt::Alignment alignment() const; + void setGeometry(const QRect &geometry, bool force = false); + QRect geometry() const; + + void setText(const QString &text); + QString text() const; + void setFont(const QFont &font); + QFont font() const; + /** + * Set the icon that will appear on the left-hand size of the frame. + */ + void setIcon(const QIcon &icon); + QIcon icon() const; + void setIconSize(const QSize &size); + QSize iconSize() const; + + /** + * @returns The style of this EffectFrame. + */ + EffectFrameStyle style() const; + + /** + * If @p enable is @c true cross fading between icons and text is enabled + * By default disabled. Use setCrossFadeProgress to cross fade. + * Cross Fading is currently only available if OpenGL is used. + * @param enable @c true enables cross fading, @c false disables it again + * @see isCrossFade + * @see setCrossFadeProgress + * @since 4.6 + */ + void enableCrossFade(bool enable); + /** + * @returns @c true if cross fading is enabled, @c false otherwise + * @see enableCrossFade + * @since 4.6 + */ + bool isCrossFade() const; + /** + * Sets the current progress for cross fading the last used icon/text + * with current icon/text to @p progress. + * A value of 0.0 means completely old icon/text, a value of 1.0 means + * completely current icon/text. + * Default value is 1.0. You have to enable cross fade before using it. + * Cross Fading is currently only available if OpenGL is used. + * @see enableCrossFade + * @see isCrossFade + * @see crossFadeProgress + * @since 4.6 + */ + void setCrossFadeProgress(qreal progress); + /** + * @returns The current progress for cross fading + * @see setCrossFadeProgress + * @see enableCrossFade + * @see isCrossFade + * @since 4.6 + */ + qreal crossFadeProgress() const; + +private: + EffectFrameQuickScene *m_view; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/effect/effecthandler.cpp b/local/recipes/kde/kwin/source/src/effect/effecthandler.cpp new file mode 100644 index 0000000000..6f6142a820 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/effecthandler.cpp @@ -0,0 +1,1703 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "effect/effecthandler.h" + +#include "config-kwin.h" + +#include "compositor.h" +#include "core/inputdevice.h" +#include "core/output.h" +#include "core/renderbackend.h" +#include "core/rendertarget.h" +#include "core/renderviewport.h" +#include "decorations/decorationbridge.h" +#include "effect/effectloader.h" +#include "effect/offscreenquickview.h" +#include "effectsadaptor.h" +#include "input.h" +#include "input_event.h" +#include "inputmethod.h" +#include "inputpanelv1window.h" +#include "keyboard_input.h" +#include "opengl/eglcontext.h" +#include "opengl/glshader.h" +#include "opengl/glshadermanager.h" +#include "opengl/gltexture.h" +#include "osd.h" +#include "pointer_input.h" +#include "scene/itemrenderer.h" +#include "scene/windowitem.h" +#include "scene/workspacescene.h" +#include "screenedge.h" +#include "scripting/scripting.h" +#include "sm.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "window_property_notify_x11_filter.h" +#include "workspace.h" +#if KWIN_BUILD_X11 +#include "x11window.h" +#endif +#if KWIN_BUILD_ACTIVITIES +#include "activities.h" +#endif +#if KWIN_BUILD_TABBOX +#include "tabbox/tabbox.h" +#endif +#if KWIN_BUILD_SCREENLOCKER +#include +#endif + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ +#if KWIN_BUILD_X11 +static QByteArray readWindowProperty(xcb_window_t win, xcb_atom_t atom, xcb_atom_t type, int format) +{ + if (win == XCB_WINDOW_NONE) { + return QByteArray(); + } + uint32_t len = 32768; + for (;;) { + Xcb::Property prop(false, win, atom, XCB_ATOM_ANY, 0, len); + if (prop.isNull()) { + // get property failed + return QByteArray(); + } + if (prop->bytes_after > 0) { + len *= 2; + continue; + } + return prop.toByteArray(format, type).value_or(QByteArray()); + } +} + +static xcb_atom_t registerSupportProperty(const QByteArray &propertyName) +{ + auto c = kwinApp()->x11Connection(); + if (!c) { + return XCB_ATOM_NONE; + } + // get the atom for the propertyName + UniqueCPtr atomReply(xcb_intern_atom_reply(c, + xcb_intern_atom_unchecked(c, false, propertyName.size(), propertyName.constData()), + nullptr)); + if (!atomReply) { + return XCB_ATOM_NONE; + } + // announce property on root window + unsigned char dummy = 0; + xcb_change_property(c, XCB_PROP_MODE_REPLACE, kwinApp()->x11RootWindow(), atomReply->atom, atomReply->atom, 8, 1, &dummy); + // TODO: add to _NET_SUPPORTED + return atomReply->atom; +} + +static void unregisterSupportProperty(xcb_atom_t atom) +{ + auto c = kwinApp()->x11Connection(); + if (!c) { + return; + } + xcb_delete_property(c, kwinApp()->x11RootWindow(), atom); +} +#endif + +//**************************************** +// EffectsHandler +//**************************************** + +EffectsHandler::EffectsHandler(Compositor *compositor, WorkspaceScene *scene) + : keyboard_grab_effect(nullptr) + , fullscreen_effect(nullptr) + , compositing_type(compositor->backend()->compositingType()) + , m_compositor(compositor) + , m_scene(scene) + , m_effectLoader(new EffectLoader(this)) +{ + if (compositing_type == NoCompositing) { + return; + } + KWin::effects = this; + + qRegisterMetaType>(); + qRegisterMetaType(); + connect(m_effectLoader, &AbstractEffectLoader::effectLoaded, this, [this](Effect *effect, const QString &name) { + effect_order.insert(effect->requestedEffectChainPosition(), EffectPair(name, effect)); + loaded_effects << EffectPair(name, effect); + effectsChanged(); + }); + m_effectLoader->setConfig(kwinApp()->config()); + + m_configWatcher = KConfigWatcher::create(kwinApp()->config()); + connect(m_configWatcher.get(), &KConfigWatcher::configChanged, this, &EffectsHandler::configChanged); + + new EffectsAdaptor(this); + QDBusConnection dbus = QDBusConnection::sessionBus(); + dbus.registerObject(QStringLiteral("/Effects"), this); + + connect(options, &Options::animationSpeedChanged, this, &EffectsHandler::reconfigureEffects); + + Workspace *ws = Workspace::self(); + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + connect(ws, &Workspace::showingDesktopChanged, this, [this](bool showing, bool animated) { + if (animated) { + Q_EMIT showingDesktopChanged(showing); + } + }); + connect(ws, &Workspace::currentDesktopChanged, this, [this](VirtualDesktop *old, Window *window) { + VirtualDesktop *newDesktop = VirtualDesktopManager::self()->currentDesktop(); + Q_EMIT desktopChanged(old, newDesktop, window ? window->effectWindow() : nullptr); + }); + connect(ws, &Workspace::currentDesktopChanging, this, [this](VirtualDesktop *currentDesktop, QPointF offset, KWin::Window *window) { + Q_EMIT desktopChanging(currentDesktop, offset, window ? window->effectWindow() : nullptr); + }); + connect(ws, &Workspace::currentDesktopChangingCancelled, this, [this]() { + Q_EMIT desktopChangingCancelled(); + }); + connect(ws, &Workspace::windowAdded, this, [this](Window *window) { + setupWindowConnections(window); + Q_EMIT windowAdded(window->effectWindow()); + }); + connect(ws, &Workspace::windowActivated, this, [this](Window *window) { + Q_EMIT windowActivated(window ? window->effectWindow() : nullptr); + }); + connect(ws, &Workspace::deletedRemoved, this, [this](KWin::Window *d) { + Q_EMIT windowDeleted(d->effectWindow()); + }); + connect(ws->sessionManager(), &SessionManager::stateChanged, this, &KWin::EffectsHandler::sessionStateChanged); + connect(vds, &VirtualDesktopManager::layoutChanged, this, [this](int width, int height) { + Q_EMIT desktopGridSizeChanged(QSize(width, height)); + Q_EMIT desktopGridWidthChanged(width); + Q_EMIT desktopGridHeightChanged(height); + }); + connect(vds, &VirtualDesktopManager::desktopAdded, this, &EffectsHandler::desktopAdded); + connect(vds, &VirtualDesktopManager::desktopRemoved, this, &EffectsHandler::desktopRemoved); + connect(vds, &VirtualDesktopManager::desktopMoved, this, &EffectsHandler::desktopMoved); + connect(ws, &Workspace::geometryChanged, this, &EffectsHandler::virtualScreenSizeChanged); + connect(ws, &Workspace::geometryChanged, this, &EffectsHandler::virtualScreenGeometryChanged); +#if KWIN_BUILD_ACTIVITIES + if (Activities *activities = Workspace::self()->activities()) { + connect(activities, &Activities::added, this, &EffectsHandler::activityAdded); + connect(activities, &Activities::removed, this, &EffectsHandler::activityRemoved); + connect(activities, &Activities::currentChanged, this, &EffectsHandler::currentActivityChanged); + connect(activities, &Activities::currentAboutToChange, this, &EffectsHandler::currentActivityAboutToChange); + } +#endif + connect(ws, &Workspace::stackingOrderChanged, this, &EffectsHandler::stackingOrderChanged); +#if KWIN_BUILD_TABBOX + TabBox::TabBox *tabBox = workspace()->tabbox(); + connect(tabBox, &TabBox::TabBox::tabBoxAdded, this, &EffectsHandler::tabBoxAdded); + connect(tabBox, &TabBox::TabBox::tabBoxUpdated, this, &EffectsHandler::tabBoxUpdated); + connect(tabBox, &TabBox::TabBox::tabBoxClosed, this, &EffectsHandler::tabBoxClosed); + connect(tabBox, &TabBox::TabBox::tabBoxKeyEvent, this, &EffectsHandler::tabBoxKeyEvent); +#endif + connect(workspace()->screenEdges(), &ScreenEdges::approaching, this, &EffectsHandler::screenEdgeApproaching); +#if KWIN_BUILD_SCREENLOCKER + connect(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged, this, [this] { + const bool locked = ScreenLocker::KSldApp::self()->lockState() == ScreenLocker::KSldApp::Locked; + Q_EMIT screenLockingChanged(locked); + }); + + connect(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::aboutToLock, this, &EffectsHandler::screenAboutToLock); +#endif + + m_cursor.position = input()->globalPointer(); + m_cursor.buttons = input()->qtButtonStates(); + m_cursor.modifiers = input()->keyboardModifiers(); + + connect(input(), &InputRedirection::globalPointerChanged, this, [this]() { + const QPointF oldPos = m_cursor.position; + m_cursor.position = input()->globalPointer(); + + Q_EMIT mouseChanged(m_cursor.position, oldPos, + m_cursor.buttons, m_cursor.buttons, + m_cursor.modifiers, m_cursor.modifiers); + }); + + connect(input(), &InputRedirection::pointerButtonStateChanged, this, [this]() { + const Qt::MouseButtons oldButtons = m_cursor.buttons; + m_cursor.buttons = input()->qtButtonStates(); + + Q_EMIT mouseChanged(m_cursor.position, m_cursor.position, + m_cursor.buttons, oldButtons, + m_cursor.modifiers, m_cursor.modifiers); + }); + + connect(input(), &InputRedirection::keyboardModifiersChanged, this, [this]() { + const Qt::KeyboardModifiers oldModifiers = m_cursor.modifiers; + m_cursor.modifiers = input()->keyboardModifiers(); + + Q_EMIT mouseChanged(m_cursor.position, m_cursor.position, + m_cursor.buttons, m_cursor.buttons, + m_cursor.modifiers, oldModifiers); + }); + +#if KWIN_BUILD_X11 + connect(kwinApp(), &Application::x11ConnectionChanged, this, [this]() { + registered_atoms.clear(); + for (auto it = m_propertiesForEffects.keyBegin(); it != m_propertiesForEffects.keyEnd(); it++) { + const auto atom = registerSupportProperty(*it); + if (atom == XCB_ATOM_NONE) { + continue; + } + m_managedProperties.insert(*it, atom); + registerPropertyType(atom, true); + } + if (kwinApp()->x11Connection()) { + m_x11WindowPropertyNotify = std::make_unique(this); + } else { + m_x11WindowPropertyNotify.reset(); + } + Q_EMIT xcbConnectionChanged(); + }); + + if (kwinApp()->x11Connection()) { + m_x11WindowPropertyNotify = std::make_unique(this); + } +#endif + + // connect all clients + for (Window *window : ws->windows()) { + setupWindowConnections(window); + } + + connect(ws, &Workspace::outputAdded, this, &EffectsHandler::screenAdded); + connect(ws, &Workspace::outputRemoved, this, &EffectsHandler::screenRemoved); + + if (auto inputMethod = kwinApp()->inputMethod()) { + connect(inputMethod, &InputMethod::panelChanged, this, &EffectsHandler::inputPanelChanged); + } + + connect(Cursors::self()->mouse(), &Cursor::cursorChanged, this, &EffectsHandler::cursorShapeChanged); + + connect(scene, &WorkspaceScene::viewRemoved, this, &EffectsHandler::viewRemoved); + + reconfigure(); +} + +EffectsHandler::~EffectsHandler() +{ + unloadAllEffects(); + KWin::effects = nullptr; +} + +#if KWIN_BUILD_X11 +xcb_window_t EffectsHandler::x11RootWindow() const +{ + return kwinApp()->x11RootWindow(); +} + +xcb_connection_t *EffectsHandler::xcbConnection() const +{ + return kwinApp()->x11Connection(); +} +#endif + +CompositingType EffectsHandler::compositingType() const +{ + return compositing_type; +} + +bool EffectsHandler::isOpenGLCompositing() const +{ + return compositing_type & OpenGLCompositing; +} + +EglContext *EffectsHandler::openglContext() const +{ + return m_scene->openglContext(); +} + +void EffectsHandler::unloadAllEffects() +{ + m_activeEffects.clear(); + effect_order.clear(); + m_effectLoader->clear(); + + const auto loaded = std::move(loaded_effects); + for (const EffectPair &pair : loaded) { + destroyEffect(pair.second); + } + + effectsChanged(); +} + +void EffectsHandler::setupWindowConnections(Window *window) +{ + connect(window, &Window::closed, this, [this, window]() { + if (window->effectWindow()) { + Q_EMIT windowClosed(window->effectWindow()); + } + }); +} + +void EffectsHandler::reconfigure() +{ + m_effectLoader->queryAndLoadAll(); +} + +// the idea is that effects call this function again which calls the next one +void EffectsHandler::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + if (m_currentPaintScreenIterator != m_activeEffects.constEnd()) { + (*m_currentPaintScreenIterator++)->prePaintScreen(data, presentTime); + --m_currentPaintScreenIterator; + } + // no special final code +} + +void EffectsHandler::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) +{ + if (m_currentPaintScreenIterator != m_activeEffects.constEnd()) { + (*m_currentPaintScreenIterator++)->paintScreen(renderTarget, viewport, mask, deviceRegion, screen); + --m_currentPaintScreenIterator; + } else { + m_scene->finalPaintScreen(renderTarget, viewport, mask, deviceRegion, screen); + } +} + +void EffectsHandler::postPaintScreen() +{ + if (m_currentPaintScreenIterator != m_activeEffects.constEnd()) { + (*m_currentPaintScreenIterator++)->postPaintScreen(); + --m_currentPaintScreenIterator; + } + // no special final code +} + +void EffectsHandler::prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) +{ + if (m_currentPaintWindowIterator != m_activeEffects.constEnd()) { + (*m_currentPaintWindowIterator++)->prePaintWindow(view, w, data, presentTime); + --m_currentPaintWindowIterator; + } + // no special final code +} + +void EffectsHandler::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) +{ + if (m_currentPaintWindowIterator != m_activeEffects.constEnd()) { + (*m_currentPaintWindowIterator++)->paintWindow(renderTarget, viewport, w, mask, deviceRegion, data); + --m_currentPaintWindowIterator; + } else { + m_scene->finalPaintWindow(renderTarget, viewport, w, mask, deviceRegion, data); + } +} + +Effect *EffectsHandler::provides(Effect::Feature ef) +{ + for (int i = 0; i < loaded_effects.size(); ++i) { + if (loaded_effects.at(i).second->provides(ef)) { + return loaded_effects.at(i).second; + } + } + return nullptr; +} + +void EffectsHandler::drawWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) +{ + if (m_currentDrawWindowIterator != m_activeEffects.constEnd()) { + (*m_currentDrawWindowIterator++)->drawWindow(renderTarget, viewport, w, mask, deviceRegion, data); + --m_currentDrawWindowIterator; + } else { + m_scene->finalDrawWindow(renderTarget, viewport, w, mask, deviceRegion, data); + } +} + +void EffectsHandler::renderWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) +{ + m_scene->finalDrawWindow(renderTarget, viewport, w, mask, deviceRegion, data); +} + +bool EffectsHandler::hasDecorationShadows() const +{ + return false; +} + +bool EffectsHandler::decorationsHaveAlpha() const +{ + return true; +} + +// start another painting pass +void EffectsHandler::startPaint() +{ + m_activeEffects.clear(); + m_activeEffects.reserve(loaded_effects.count()); + for (QList::const_iterator it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if (it->second->isActive()) { + m_activeEffects << it->second; + } + } + m_currentDrawWindowIterator = m_activeEffects.constBegin(); + m_currentPaintWindowIterator = m_activeEffects.constBegin(); + m_currentPaintScreenIterator = m_activeEffects.constBegin(); +} + +void EffectsHandler::setActiveFullScreenEffect(Effect *e) +{ + if (fullscreen_effect == e) { + return; + } + const bool activeChanged = (e == nullptr || fullscreen_effect == nullptr); + fullscreen_effect = e; + Q_EMIT activeFullScreenEffectChanged(); + if (activeChanged) { + Q_EMIT hasActiveFullScreenEffectChanged(); + + setShowingDesktop(false); + workspace()->screenEdges()->checkBlocking(); + } +} + +Effect *EffectsHandler::activeFullScreenEffect() const +{ + return fullscreen_effect; +} + +bool EffectsHandler::hasActiveFullScreenEffect() const +{ + return fullscreen_effect; +} + +bool EffectsHandler::isColorPickerActive() const +{ + return isEffectActive(QStringLiteral("colorpicker")); +} + +bool EffectsHandler::grabKeyboard(Effect *effect) +{ + if (keyboard_grab_effect != nullptr) { + return false; + } + keyboard_grab_effect = effect; + return true; +} + +void EffectsHandler::ungrabKeyboard() +{ + Q_ASSERT(keyboard_grab_effect != nullptr); + keyboard_grab_effect = nullptr; + input()->keyboard()->update(); +} + +void EffectsHandler::grabbedKeyboardEvent(QKeyEvent *e) +{ + if (keyboard_grab_effect != nullptr) { + keyboard_grab_effect->grabbedKeyboardEvent(e); + } +} + +void EffectsHandler::startMouseInterception(Effect *effect, Qt::CursorShape shape) +{ + if (m_grabbedMouseEffects.contains(effect)) { + return; + } + m_grabbedMouseEffects.append(effect); + if (m_grabbedMouseEffects.size() != 1) { + return; + } + + input()->pointer()->setEffectsOverrideCursor(shape); + + // We want to allow global shortcuts to be triggered when moving a + // window so it is possible to pick up a window and then move it to a + // different desktop by using the global shortcut to switch desktop. + // However, that means that some other things can also be triggered. If + // an effect that fill the screen gets triggered that way, we end up in a + // weird state where the move will restart after the effect closes. So to + // avoid that, abort move/resize if a full screen effect starts. + if (workspace()->moveResizeWindow()) { + workspace()->moveResizeWindow()->endInteractiveMoveResize(); + } +} + +void EffectsHandler::stopMouseInterception(Effect *effect) +{ + if (!m_grabbedMouseEffects.contains(effect)) { + return; + } + m_grabbedMouseEffects.removeAll(effect); + if (m_grabbedMouseEffects.isEmpty()) { + input()->pointer()->removeEffectsOverrideCursor(); + } +} + +bool EffectsHandler::isMouseInterception() const +{ + return m_grabbedMouseEffects.count() > 0; +} + +bool EffectsHandler::touchDown(qint32 id, const QPointF &pos, std::chrono::microseconds time) +{ + // TODO: reverse call order? + for (auto it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if (it->second->touchDown(id, pos, time)) { + return true; + } + } + return false; +} + +bool EffectsHandler::touchMotion(qint32 id, const QPointF &pos, std::chrono::microseconds time) +{ + // TODO: reverse call order? + for (auto it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if (it->second->touchMotion(id, pos, time)) { + return true; + } + } + return false; +} + +bool EffectsHandler::touchUp(qint32 id, std::chrono::microseconds time) +{ + // TODO: reverse call order? + for (auto it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if (it->second->touchUp(id, time)) { + return true; + } + } + return false; +} + +void EffectsHandler::touchCancel() +{ + for (const auto &[name, effect] : std::as_const(loaded_effects)) { + effect->touchCancel(); + } +} + +bool EffectsHandler::tabletToolProximityEvent(TabletToolProximityEvent *event) +{ + // TODO: reverse call order? + for (auto it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if (it->second->tabletToolProximity(event)) { + return true; + } + } + return false; +} + +bool EffectsHandler::tabletToolAxisEvent(TabletToolAxisEvent *event) +{ + // TODO: reverse call order? + for (auto it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if (it->second->tabletToolAxis(event)) { + return true; + } + } + return false; +} + +bool EffectsHandler::tabletToolTipEvent(TabletToolTipEvent *event) +{ + // TODO: reverse call order? + for (auto it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if (it->second->tabletToolTip(event)) { + return true; + } + } + return false; +} + +bool EffectsHandler::tabletToolButtonEvent(uint button, bool pressed, InputDeviceTabletTool *tool, std::chrono::microseconds time) +{ + // TODO: reverse call order? + for (auto it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if (it->second->tabletToolButtonEvent(button, pressed, tool->uniqueId())) { + return true; + } + } + return false; +} + +bool EffectsHandler::tabletPadButtonEvent(uint button, bool pressed, std::chrono::microseconds time, InputDevice *device) +{ + // TODO: reverse call order? + for (auto it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if (it->second->tabletPadButtonEvent(button, pressed, device)) { + return true; + } + } + return false; +} + +bool EffectsHandler::tabletPadStripEvent(int number, qreal position, bool isFinger, std::chrono::microseconds time, InputDevice *device) +{ + // TODO: reverse call order? + for (auto it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if (it->second->tabletPadStripEvent(number, position, isFinger, device)) { + return true; + } + } + return false; +} + +bool EffectsHandler::tabletPadRingEvent(int number, qreal position, bool isFinger, std::chrono::microseconds time, InputDevice *device) +{ + // TODO: reverse call order? + for (auto it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if (it->second->tabletPadRingEvent(number, position, isFinger, device)) { + return true; + } + } + return false; +} + +bool EffectsHandler::tabletPadDialEvent(int number, double delta, std::chrono::microseconds time, InputDevice *device) +{ + // TODO: reverse call order? + for (auto it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if (it->second->tabletPadDialEvent(number, delta, device)) { + return true; + } + } + return false; +} + +void EffectsHandler::registerPointerShortcut(Qt::KeyboardModifiers modifiers, Qt::MouseButton pointerButtons, QAction *action) +{ + input()->registerPointerShortcut(modifiers, pointerButtons, action); +} + +void EffectsHandler::registerAxisShortcut(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis, QAction *action) +{ + input()->registerAxisShortcut(modifiers, axis, action); +} + +void EffectsHandler::registerTouchpadSwipeShortcut(SwipeDirection dir, uint fingerCount, QAction *onUp, std::function progressCallback) +{ + input()->registerTouchpadSwipeShortcut(dir, fingerCount, onUp, progressCallback); +} + +void EffectsHandler::registerTouchpadPinchShortcut(PinchDirection dir, uint fingerCount, QAction *onUp, std::function progressCallback) +{ + input()->registerTouchpadPinchShortcut(dir, fingerCount, onUp, progressCallback); +} + +void EffectsHandler::registerTouchscreenSwipeShortcut(SwipeDirection direction, uint fingerCount, QAction *action, std::function progressCallback) +{ + input()->registerTouchscreenSwipeShortcut(direction, fingerCount, action, progressCallback); +} + +bool EffectsHandler::hasKeyboardGrab() const +{ + return keyboard_grab_effect != nullptr; +} + +#if KWIN_BUILD_X11 +void EffectsHandler::registerPropertyType(long atom, bool reg) +{ + if (reg) { + ++registered_atoms[atom]; // initialized to 0 if not present yet + } else { + if (--registered_atoms[atom] == 0) { + registered_atoms.remove(atom); + } + } +} + +xcb_atom_t EffectsHandler::announceSupportProperty(const QByteArray &propertyName, Effect *effect) +{ + PropertyEffectMap::iterator it = m_propertiesForEffects.find(propertyName); + if (it != m_propertiesForEffects.end()) { + // property has already been registered for an effect + // just append Effect and return the atom stored in m_managedProperties + if (!it.value().contains(effect)) { + it.value().append(effect); + } + return m_managedProperties.value(propertyName, XCB_ATOM_NONE); + } + m_propertiesForEffects.insert(propertyName, QList() << effect); + const auto atom = registerSupportProperty(propertyName); + if (atom == XCB_ATOM_NONE) { + return atom; + } + m_managedProperties.insert(propertyName, atom); + registerPropertyType(atom, true); + return atom; +} + +void EffectsHandler::removeSupportProperty(const QByteArray &propertyName, Effect *effect) +{ + PropertyEffectMap::iterator it = m_propertiesForEffects.find(propertyName); + if (it == m_propertiesForEffects.end()) { + // property is not registered - nothing to do + return; + } + if (!it.value().contains(effect)) { + // property is not registered for given effect - nothing to do + return; + } + it.value().removeAll(effect); + if (!it.value().isEmpty()) { + // property still registered for another effect - nothing further to do + return; + } + const xcb_atom_t atom = m_managedProperties.take(propertyName); + registerPropertyType(atom, false); + m_propertiesForEffects.remove(propertyName); + unregisterSupportProperty(atom); +} +#endif + +QByteArray EffectsHandler::readRootProperty(long atom, long type, int format) const +{ +#if KWIN_BUILD_X11 + if (!kwinApp()->x11Connection()) { + return QByteArray(); + } + return readWindowProperty(kwinApp()->x11RootWindow(), atom, type, format); +#else + return {}; +#endif +} + +void EffectsHandler::activateWindow(EffectWindow *effectWindow) +{ + auto window = effectWindow->window(); + if (window->isClient()) { + Workspace::self()->activateWindow(window, true); + } +} + +EffectWindow *EffectsHandler::activeWindow() const +{ + return Workspace::self()->activeWindow() ? Workspace::self()->activeWindow()->effectWindow() : nullptr; +} + +void EffectsHandler::moveWindow(EffectWindow *w, const QPoint &pos, bool snap, double snapAdjust) +{ + auto window = w->window(); + if (!window->isClient() || !window->isMovable()) { + return; + } + + if (snap) { + window->move(Workspace::self()->adjustWindowPosition(window, pos, true, snapAdjust)); + } else { + window->move(pos); + } +} + +void EffectsHandler::windowToDesktops(EffectWindow *w, const QList &desktops) +{ + auto window = w->window(); + if (!window->isClient() || window->isDesktop() || window->isDock()) { + return; + } + window->setDesktops(desktops); +} + +void EffectsHandler::windowToScreen(EffectWindow *w, LogicalOutput *screen) +{ + auto window = w->window(); + if (window->isClient() && !window->isDesktop() && !window->isDock()) { + window->sendToOutput(screen); + } +} + +void EffectsHandler::setShowingDesktop(bool showing) +{ + Workspace::self()->setShowingDesktop(showing); +} + +QString EffectsHandler::currentActivity() const +{ +#if KWIN_BUILD_ACTIVITIES + if (!Workspace::self()->activities()) { + return QString(); + } + return Workspace::self()->activities()->current(); +#else + return QString(); +#endif +} + +VirtualDesktop *EffectsHandler::currentDesktop() const +{ + return VirtualDesktopManager::self()->currentDesktop(); +} + +QList EffectsHandler::desktops() const +{ + return VirtualDesktopManager::self()->desktops(); +} + +void EffectsHandler::setCurrentDesktop(VirtualDesktop *desktop) +{ + VirtualDesktopManager::self()->setCurrent(desktop); +} + +QSize EffectsHandler::desktopGridSize() const +{ + return VirtualDesktopManager::self()->grid().size(); +} + +int EffectsHandler::desktopGridWidth() const +{ + return desktopGridSize().width(); +} + +int EffectsHandler::desktopGridHeight() const +{ + return desktopGridSize().height(); +} + +int EffectsHandler::workspaceWidth() const +{ + return desktopGridWidth() * Workspace::self()->geometry().width(); +} + +int EffectsHandler::workspaceHeight() const +{ + return desktopGridHeight() * Workspace::self()->geometry().height(); +} + +VirtualDesktop *EffectsHandler::desktopAtCoords(QPoint coords) const +{ + return VirtualDesktopManager::self()->grid().at(coords); +} + +QPoint EffectsHandler::desktopGridCoords(VirtualDesktop *desktop) const +{ + return VirtualDesktopManager::self()->grid().gridCoords(desktop); +} + +QPoint EffectsHandler::desktopCoords(VirtualDesktop *desktop) const +{ + QPoint coords = VirtualDesktopManager::self()->grid().gridCoords(desktop); + if (coords.x() == -1) { + return QPoint(-1, -1); + } + const QSize displaySize = Workspace::self()->geometry().size(); + return QPoint(coords.x() * displaySize.width(), coords.y() * displaySize.height()); +} + +VirtualDesktop *EffectsHandler::desktopAbove(VirtualDesktop *desktop, bool wrap) const +{ + return VirtualDesktopManager::self()->inDirection(desktop, VirtualDesktopManager::Direction::Up, wrap); +} + +VirtualDesktop *EffectsHandler::desktopToRight(VirtualDesktop *desktop, bool wrap) const +{ + return VirtualDesktopManager::self()->inDirection(desktop, VirtualDesktopManager::Direction::Right, wrap); +} + +VirtualDesktop *EffectsHandler::desktopBelow(VirtualDesktop *desktop, bool wrap) const +{ + return VirtualDesktopManager::self()->inDirection(desktop, VirtualDesktopManager::Direction::Down, wrap); +} + +VirtualDesktop *EffectsHandler::desktopToLeft(VirtualDesktop *desktop, bool wrap) const +{ + return VirtualDesktopManager::self()->inDirection(desktop, VirtualDesktopManager::Direction::Left, wrap); +} + +QString EffectsHandler::desktopName(VirtualDesktop *desktop) const +{ + return desktop->name(); +} + +bool EffectsHandler::optionRollOverDesktops() const +{ + return options->isRollOverDesktops(); +} + +double EffectsHandler::animationTimeFactor() const +{ + return options->animationTimeFactor(); +} + +EffectWindow *EffectsHandler::findWindow(WId id) const +{ +#if KWIN_BUILD_X11 + if (X11Window *w = Workspace::self()->findClient(id)) { + return w->effectWindow(); + } + if (X11Window *w = Workspace::self()->findUnmanaged(id)) { + return w->effectWindow(); + } +#endif + return nullptr; +} +EffectWindow *EffectsHandler::findWindow(SurfaceInterface *surf) const +{ + if (Window *w = waylandServer()->findWindow(surf)) { + return w->effectWindow(); + } + return nullptr; +} + +EffectWindow *EffectsHandler::findWindow(QWindow *w) const +{ + if (Window *window = workspace()->findInternal(w)) { + return window->effectWindow(); + } + return nullptr; +} + +EffectWindow *EffectsHandler::findWindow(const QUuid &id) const +{ + if (Window *window = workspace()->findWindow(id)) { + return window->effectWindow(); + } + return nullptr; +} + +QList EffectsHandler::stackingOrder() const +{ + QList list = workspace()->stackingOrder(); + QList ret; + for (Window *t : list) { + if (EffectWindow *w = t->effectWindow()) { + ret.append(w); + } + } + return ret; +} + +void EffectsHandler::setElevatedWindow(KWin::EffectWindow *w, bool set) +{ + WindowItem *item = w->windowItem(); + + if (set) { + item->elevate(); + } else { + item->deelevate(); + } +} + +void EffectsHandler::setTabBoxWindow(EffectWindow *w) +{ +#if KWIN_BUILD_TABBOX + auto window = w->window(); + if (window->isClient()) { + workspace()->tabbox()->setCurrentClient(window); + } +#endif +} + +QList EffectsHandler::currentTabBoxWindowList() const +{ +#if KWIN_BUILD_TABBOX + const auto clients = workspace()->tabbox()->currentClientList(); + QList ret; + ret.reserve(clients.size()); + std::transform(std::cbegin(clients), std::cend(clients), + std::back_inserter(ret), + [](auto client) { + return client->effectWindow(); + }); + return ret; +#else + return QList(); +#endif +} + +void EffectsHandler::refTabBox() +{ +#if KWIN_BUILD_TABBOX + workspace()->tabbox()->reference(); +#endif +} + +void EffectsHandler::unrefTabBox() +{ +#if KWIN_BUILD_TABBOX + workspace()->tabbox()->unreference(); +#endif +} + +void EffectsHandler::closeTabBox() +{ +#if KWIN_BUILD_TABBOX + workspace()->tabbox()->close(); +#endif +} + +EffectWindow *EffectsHandler::currentTabBoxWindow() const +{ +#if KWIN_BUILD_TABBOX + if (auto c = workspace()->tabbox()->currentClient()) { + return c->effectWindow(); + } +#endif + return nullptr; +} + +void EffectsHandler::addRepaintFull() +{ + m_compositor->scene()->addRepaintFull(); +} + +void EffectsHandler::addRepaint(const QRect &logicalRegion) +{ + m_compositor->scene()->addLogicalRepaint(Rect(logicalRegion)); +} + +void EffectsHandler::addRepaint(const QRectF &logicalRegion) +{ + m_compositor->scene()->addLogicalRepaint(Rect(logicalRegion.toAlignedRect())); +} + +void EffectsHandler::addRepaint(const Rect &logicalRegion) +{ + m_compositor->scene()->addLogicalRepaint(logicalRegion); +} + +void EffectsHandler::addRepaint(const RectF &logicalRegion) +{ + m_compositor->scene()->addLogicalRepaint(logicalRegion.toAlignedRect()); +} + +void EffectsHandler::addRepaint(const Region &logicalRegion) +{ + m_compositor->scene()->addLogicalRepaint(logicalRegion); +} + +void EffectsHandler::addRepaint(int x, int y, int w, int h) +{ + m_compositor->scene()->addLogicalRepaint(x, y, w, h); +} + +LogicalOutput *EffectsHandler::activeScreen() const +{ + return workspace()->activeOutput(); +} + +QRectF EffectsHandler::clientArea(clientAreaOption opt, const LogicalOutput *screen, const VirtualDesktop *desktop) const +{ + return Workspace::self()->clientArea(opt, screen, desktop); +} + +QRectF EffectsHandler::clientArea(clientAreaOption opt, const EffectWindow *effectWindow) const +{ + const Window *window = effectWindow->window(); + return Workspace::self()->clientArea(opt, window); +} + +QRectF EffectsHandler::clientArea(clientAreaOption opt, const QPoint &p, const VirtualDesktop *desktop) const +{ + const LogicalOutput *output = Workspace::self()->outputAt(p); + return Workspace::self()->clientArea(opt, output, desktop); +} + +QRect EffectsHandler::virtualScreenGeometry() const +{ + return Workspace::self()->geometry(); +} + +QSize EffectsHandler::virtualScreenSize() const +{ + return Workspace::self()->geometry().size(); +} + +void EffectsHandler::defineCursor(Qt::CursorShape shape) +{ + input()->pointer()->setEffectsOverrideCursor(shape); +} + +bool EffectsHandler::checkInputWindowEvent(QMouseEvent *e) +{ + if (m_grabbedMouseEffects.isEmpty()) { + return false; + } + for (Effect *effect : std::as_const(m_grabbedMouseEffects)) { + effect->windowInputMouseEvent(e); + } + return true; +} + +bool EffectsHandler::checkInputWindowEvent(QWheelEvent *e) +{ + if (m_grabbedMouseEffects.isEmpty()) { + return false; + } + for (Effect *effect : std::as_const(m_grabbedMouseEffects)) { + effect->windowInputMouseEvent(e); + } + return true; +} + +QPointF EffectsHandler::cursorPos() const +{ + return Cursors::self()->mouse()->pos(); +} + +void EffectsHandler::reserveElectricBorder(ElectricBorder border, Effect *effect) +{ + workspace()->screenEdges()->reserve(border, effect, "borderActivated"); +} + +void EffectsHandler::unreserveElectricBorder(ElectricBorder border, Effect *effect) +{ + workspace()->screenEdges()->unreserve(border, effect); +} + +void EffectsHandler::registerTouchBorder(ElectricBorder border, QAction *action) +{ + workspace()->screenEdges()->reserveTouch(border, action); +} + +void EffectsHandler::registerRealtimeTouchBorder(ElectricBorder border, QAction *action, EffectsHandler::TouchBorderCallback progressCallback) +{ + workspace()->screenEdges()->reserveTouch(border, action, progressCallback); +} + +void EffectsHandler::unregisterTouchBorder(ElectricBorder border, QAction *action) +{ + workspace()->screenEdges()->unreserveTouch(border, action); +} + +QPainter *EffectsHandler::scenePainter() +{ + return m_scene->renderer()->painter(); +} + +void EffectsHandler::toggleEffect(const QString &name) +{ + if (isEffectLoaded(name)) { + unloadEffect(name); + } else { + loadEffect(name); + } +} + +QStringList EffectsHandler::loadedEffects() const +{ + QStringList listModules; + listModules.reserve(loaded_effects.count()); + std::transform(loaded_effects.constBegin(), loaded_effects.constEnd(), + std::back_inserter(listModules), + [](const EffectPair &pair) { + return pair.first; + }); + return listModules; +} + +QStringList EffectsHandler::listOfEffects() const +{ + return m_effectLoader->listOfKnownEffects(); +} + +bool EffectsHandler::loadEffect(const QString &name) +{ + makeOpenGLContextCurrent(); + m_compositor->scene()->addRepaintFull(); + + return m_effectLoader->loadEffect(name); +} + +void EffectsHandler::unloadEffect(const QString &name) +{ + auto it = std::find_if(effect_order.begin(), effect_order.end(), + [name](EffectPair &pair) { + return pair.first == name; + }); + if (it == effect_order.end()) { + qCDebug(KWIN_CORE) << "EffectsHandler::unloadEffect : Effect not loaded :" << name; + return; + } + + qCDebug(KWIN_CORE) << "EffectsHandler::unloadEffect : Unloading Effect :" << name; + destroyEffect((*it).second); + effect_order.erase(it); + effectsChanged(); + + m_compositor->scene()->addRepaintFull(); +} + +void EffectsHandler::destroyEffect(Effect *effect) +{ + makeOpenGLContextCurrent(); + + if (fullscreen_effect == effect) { + setActiveFullScreenEffect(nullptr); + } + + if (keyboard_grab_effect == effect) { + ungrabKeyboard(); + } + + stopMouseInterception(effect); + +#if KWIN_BUILD_X11 + const QList properties = m_propertiesForEffects.keys(); + for (const QByteArray &property : properties) { + removeSupportProperty(property, effect); + } +#endif + + delete effect; +} + +void EffectsHandler::reconfigureEffects() +{ + makeOpenGLContextCurrent(); + for (const EffectPair &pair : loaded_effects) { + pair.second->reconfigure(Effect::ReconfigureAll); + } +} + +void EffectsHandler::reconfigureEffect(const QString &name) +{ + for (QList::const_iterator it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if ((*it).first == name) { + kwinApp()->config()->reparseConfiguration(); + makeOpenGLContextCurrent(); + (*it).second->reconfigure(Effect::ReconfigureAll); + return; + } + } +} + +bool EffectsHandler::isEffectLoaded(const QString &name) const +{ + auto it = std::find_if(loaded_effects.constBegin(), loaded_effects.constEnd(), + [&name](const EffectPair &pair) { + return pair.first == name; + }); + return it != loaded_effects.constEnd(); +} + +bool EffectsHandler::isEffectSupported(const QString &name) +{ + // If the effect is loaded, it is obviously supported. + if (isEffectLoaded(name)) { + return true; + } + + // next checks might require a context + makeOpenGLContextCurrent(); + + return m_effectLoader->isEffectSupported(name); +} + +QList EffectsHandler::areEffectsSupported(const QStringList &names) +{ + QList retList; + retList.reserve(names.count()); + std::transform(names.constBegin(), names.constEnd(), + std::back_inserter(retList), + [this](const QString &name) { + return isEffectSupported(name); + }); + return retList; +} + +void EffectsHandler::reloadEffect(Effect *effect) +{ + QString effectName; + for (QList::const_iterator it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if ((*it).second == effect) { + effectName = (*it).first; + break; + } + } + if (!effectName.isNull()) { + unloadEffect(effectName); + m_effectLoader->loadEffect(effectName); + } +} + +void EffectsHandler::effectsChanged() +{ + loaded_effects.clear(); + m_activeEffects.clear(); // it's possible to have a reconfigure and a quad rebuild between two paint cycles - bug #308201 + + loaded_effects.reserve(effect_order.count()); + std::copy(effect_order.constBegin(), effect_order.constEnd(), + std::back_inserter(loaded_effects)); + + m_activeEffects.reserve(loaded_effects.count()); + + m_currentPaintScreenIterator = m_activeEffects.constBegin(); + m_currentPaintWindowIterator = m_activeEffects.constBegin(); + m_currentDrawWindowIterator = m_activeEffects.constBegin(); +} + +QStringList EffectsHandler::activeEffects() const +{ + QStringList ret; + for (QList::const_iterator it = loaded_effects.constBegin(), + end = loaded_effects.constEnd(); + it != end; ++it) { + if (it->second->isActive()) { + ret << it->first; + } + } + return ret; +} + +bool EffectsHandler::isEffectActive(const QString &pluginId) const +{ + auto it = std::find_if(loaded_effects.cbegin(), loaded_effects.cend(), [&pluginId](const EffectPair &p) { + return p.first == pluginId; + }); + if (it == loaded_effects.cend()) { + return false; + } + return it->second->isActive(); +} + +bool EffectsHandler::blocksDirectScanout() const +{ + return std::any_of(m_activeEffects.constBegin(), m_activeEffects.constEnd(), [](const Effect *effect) { + return effect->blocksDirectScanout(); + }); +} + +Display *EffectsHandler::waylandDisplay() const +{ + return waylandServer()->display(); +} + +QVariant EffectsHandler::kwinOption(KWinOption kwopt) +{ + switch (kwopt) { + case CloseButtonCorner: { + // TODO: this could become per window and be derived from the actual position in the deco + const auto settings = Workspace::self()->decorationBridge()->settings(); + return settings && settings->decorationButtonsLeft().contains(KDecoration3::DecorationButtonType::Close) ? Qt::TopLeftCorner : Qt::TopRightCorner; + } + case SwitchDesktopOnScreenEdge: + return workspace()->screenEdges()->isDesktopSwitching(); + case SwitchDesktopOnScreenEdgeMovingWindows: + return workspace()->screenEdges()->isDesktopSwitchingMovingClients(); + default: + return QVariant(); // an invalid one + } +} + +QString EffectsHandler::supportInformation(const QString &name) const +{ + auto it = std::find_if(loaded_effects.constBegin(), loaded_effects.constEnd(), + [name](const EffectPair &pair) { + return pair.first == name; + }); + if (it == loaded_effects.constEnd()) { + return QString(); + } + + QString support((*it).first + QLatin1String(":\n")); + const QMetaObject *metaOptions = (*it).second->metaObject(); + for (int i = 0; i < metaOptions->propertyCount(); ++i) { + const QMetaProperty property = metaOptions->property(i); + if (qstrcmp(property.name(), "objectName") == 0) { + continue; + } + support += QString::fromUtf8(property.name()) + QLatin1String(": ") + (*it).second->property(property.name()).toString() + QLatin1Char('\n'); + } + + return support; +} + +bool EffectsHandler::isScreenLocked() const +{ +#if KWIN_BUILD_SCREENLOCKER + return ScreenLocker::KSldApp::self()->lockState() == ScreenLocker::KSldApp::Locked; +#else + return false; +#endif +} + +QString EffectsHandler::debug(const QString &name, const QString ¶meter) const +{ + QString internalName = name.toLower(); + for (QList::const_iterator it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { + if ((*it).first == internalName) { + return it->second->debug(parameter); + } + } + return QString(); +} + +bool EffectsHandler::makeOpenGLContextCurrent() +{ + if (!isOpenGLCompositing()) { + return false; + } + return m_scene->openglContext()->makeCurrent(); +} + +void EffectsHandler::doneOpenGLContextCurrent() +{ + if (isOpenGLCompositing()) { + m_scene->openglContext()->doneCurrent(); + } +} + +bool EffectsHandler::animationsSupported() const +{ + static const QByteArray forceEnvVar = qgetenv("KWIN_EFFECTS_FORCE_ANIMATIONS"); + if (!forceEnvVar.isEmpty()) { + static const int forceValue = forceEnvVar.toInt(); + return forceValue == 1; + } + return m_scene->animationsSupported(); +} + +void EffectsHandler::highlightWindows(const QList &windows) +{ + Effect *e = provides(Effect::HighlightWindows); + if (!e) { + return; + } + e->perform(Effect::HighlightWindows, QVariantList{QVariant::fromValue(windows)}); +} + +PlatformCursorImage EffectsHandler::cursorImage() const +{ + return kwinApp()->cursorImage(); +} + +void EffectsHandler::hideCursor() +{ + Cursors::self()->hideCursor(); +} + +void EffectsHandler::showCursor() +{ + Cursors::self()->showCursor(); +} + +void EffectsHandler::startInteractiveWindowSelection(std::function callback) +{ + kwinApp()->startInteractiveWindowSelection([callback](KWin::Window *window) { + if (window && window->effectWindow()) { + callback(window->effectWindow()); + } else { + callback(nullptr); + } + }); +} + +void EffectsHandler::startInteractivePositionSelection(std::function callback) +{ + kwinApp()->startInteractivePositionSelection(callback); +} + +void EffectsHandler::showOnScreenMessage(const QString &message, const QString &iconName) +{ + OSD::show(message, iconName); +} + +void EffectsHandler::hideOnScreenMessage(OnScreenMessageHideFlags flags) +{ + OSD::HideFlags osdFlags; + if (flags.testFlag(OnScreenMessageHideFlag::SkipsCloseAnimation)) { + osdFlags |= OSD::HideFlag::SkipCloseAnimation; + } + OSD::hide(osdFlags); +} + +KSharedConfigPtr EffectsHandler::config() const +{ + return kwinApp()->config(); +} + +KSharedConfigPtr EffectsHandler::inputConfig() const +{ + return kwinApp()->inputConfig(); +} + +Effect *EffectsHandler::findEffect(const QString &name) const +{ + auto it = std::find_if(loaded_effects.constBegin(), loaded_effects.constEnd(), [name](const EffectPair &pair) { + return pair.first == name; + }); + if (it == loaded_effects.constEnd()) { + return nullptr; + } + return (*it).second; +} + +void EffectsHandler::renderOffscreenQuickView(const RenderTarget &renderTarget, const RenderViewport &viewport, OffscreenQuickView *w) const +{ + if (!w->isVisible()) { + return; + } + if (compositingType() == OpenGLCompositing) { + GLTexture *t = w->bufferAsTexture(); + if (!t) { + return; + } + + ShaderTraits traits = ShaderTrait::MapTexture | ShaderTrait::TransformColorspace; + const qreal a = w->opacity(); + if (a != 1.0) { + traits |= ShaderTrait::Modulate; + } + + GLShader *shader = ShaderManager::instance()->pushShader(traits); + const QRectF rect = scaledRect(w->geometry(), viewport.scale()); + + QMatrix4x4 mvp(viewport.projectionMatrix()); + mvp.translate(rect.x(), rect.y()); + shader->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, mvp); + + if (a != 1.0) { + shader->setUniform(GLShader::Vec4Uniform::ModulationConstant, QVector4D(a, a, a, a)); + } + shader->setColorspaceUniforms(ColorDescription::sRGB, renderTarget.colorDescription(), RenderingIntent::Perceptual); + + const bool alphaBlending = w->hasAlphaChannel() || (a != 1.0); + if (alphaBlending) { + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + } + + t->render(rect.size()); + + if (alphaBlending) { + glDisable(GL_BLEND); + } + + ShaderManager::instance()->popShader(); + } else if (compositingType() == QPainterCompositing) { + QPainter *painter = effects->scenePainter(); + const QImage buffer = w->bufferAsImage(); + if (buffer.isNull()) { + return; + } + painter->save(); + painter->setOpacity(w->opacity()); + painter->drawImage(w->geometry(), buffer); + painter->restore(); + } +} + +SessionState EffectsHandler::sessionState() const +{ + return Workspace::self()->sessionManager()->state(); +} + +QList EffectsHandler::screens() const +{ + return Workspace::self()->outputs(); +} + +LogicalOutput *EffectsHandler::screenAt(const QPoint &point) const +{ + return Workspace::self()->outputAt(point); +} + +LogicalOutput *EffectsHandler::findScreen(const QString &name) const +{ + const auto outputs = Workspace::self()->outputs(); + for (LogicalOutput *screen : outputs) { + if (screen->name() == name) { + return screen; + } + } + return nullptr; +} + +LogicalOutput *EffectsHandler::findScreen(int screenId) const +{ + return Workspace::self()->outputs().value(screenId); +} + +bool EffectsHandler::isCursorHidden() const +{ + return Cursors::self()->isCursorHidden(); +} + +KWin::EffectWindow *EffectsHandler::inputPanel() const +{ + if (!kwinApp()->inputMethod() || !kwinApp()->inputMethod()->isEnabled()) { + return nullptr; + } + + auto panel = kwinApp()->inputMethod()->panel(); + if (panel) { + return panel->effectWindow(); + } + return nullptr; +} + +bool EffectsHandler::isInputPanelOverlay() const +{ + if (!kwinApp()->inputMethod() || !kwinApp()->inputMethod()->isEnabled()) { + return true; + } + + auto panel = kwinApp()->inputMethod()->panel(); + if (panel) { + return panel->mode() == InputPanelV1Window::Mode::Overlay; + } + return true; +} + +QQmlEngine *EffectsHandler::qmlEngine() const +{ + return Scripting::self()->qmlEngine(); +} + +void EffectsHandler::configChanged(const KConfigGroup &group, const QByteArrayList &names) +{ + if (group.name() != QLatin1String("Plugins")) { + return; + } + + QStringList toLoad; + QStringList toUnload; + + for (const QByteArray &key : names) { + if (!key.endsWith("Enabled")) { + continue; + } + const QString effectName = QString::fromUtf8(key).replace(QStringLiteral("Enabled"), QString()); + auto md = m_effectLoader->findEffect(effectName); + + if (md.isValid()) { + const auto result = m_effectLoader->readConfig(effectName, md.isEnabledByDefault()); + + if (result.testFlag(LoadEffectFlag::Load)) { + toLoad << effectName; + } else { + toUnload << effectName; + } + } + } + + // Unload effects first, it's need to ensure that switching between mutually exclusive + // effects works as expected, for example so global shortcuts are handed over, etc. + for (const QString &effect : std::as_const(toUnload)) { + unloadEffect(effect); + } + + for (const QString &effect : std::as_const(toLoad)) { + loadEffect(effect); + } +} + +EffectsHandler *effects = nullptr; + +} // namespace + +#include "moc_effecthandler.cpp" +#include "moc_globals.cpp" diff --git a/local/recipes/kde/kwin/source/src/effect/effecthandler.h b/local/recipes/kde/kwin/source/src/effect/effecthandler.h new file mode 100644 index 0000000000..c7d9fec61b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/effecthandler.h @@ -0,0 +1,1130 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "config-kwin.h" + +#include "effect/effect.h" +#include "effect/effectwindow.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include + +#if KWIN_BUILD_X11 +#include +#endif + +class KConfigGroup; +class QFont; +class QKeyEvent; +class QMatrix4x4; +class QMouseEvent; +class QWheelEvent; +class QAction; +class QQmlEngine; + +/** + * Logging category to be used inside the KWin effects. + * Do not use in this library. + */ +Q_DECLARE_LOGGING_CATEGORY(KWINEFFECTS) + +namespace KDecoration3 +{ +class Decoration; +} + +namespace KWin +{ + +class SurfaceInterface; +class Display; +class PaintDataPrivate; +class WindowPaintDataPrivate; + +class Compositor; +class EffectLoader; +class EffectWindow; +class EffectWindowGroup; +class OffscreenQuickView; +class Group; +class LogicalOutput; +class Effect; +struct TabletToolProximityEvent; +struct TabletToolAxisEvent; +struct TabletToolTipEvent; +class Window; +class WindowItem; +class WindowPropertyNotifyX11Filter; +class WorkspaceScene; +class VirtualDesktop; +class EglContext; +class InputDevice; +class InputDeviceTabletTool; + +typedef QPair EffectPair; + +/** + * EffectWindow::setData() and EffectWindow::data() global roles. + * All values between 0 and 999 are reserved for global roles. + */ +enum DataRole { + // Grab roles are used to force all other animations to ignore the window. + // The value of the data is set to the Effect's `this` value. + WindowAddedGrabRole = 1, + WindowClosedGrabRole, + WindowMinimizedGrabRole, + WindowUnminimizedGrabRole, + WindowForceBlurRole, ///< For fullscreen effects to enforce blurring of windows, + WindowForceBackgroundContrastRole, ///< For fullscreen effects to enforce the background contrast, +}; + +/** + * @short Manager class that handles all the effects. + * + * This class creates Effect objects and calls it's appropriate methods. + * + * Effect objects can call methods of this class to interact with the + * workspace, e.g. to activate or move a specific window, change current + * desktop or create a special input window to receive mouse and keyboard + * events. + */ +class KWIN_EXPORT EffectsHandler : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.Effects") + + Q_PROPERTY(QStringList activeEffects READ activeEffects) + Q_PROPERTY(QStringList loadedEffects READ loadedEffects) + Q_PROPERTY(QStringList listOfEffects READ listOfEffects) + + Q_PROPERTY(KWin::VirtualDesktop *currentDesktop READ currentDesktop WRITE setCurrentDesktop NOTIFY desktopChanged) + Q_PROPERTY(QString currentActivity READ currentActivity NOTIFY currentActivityChanged) + Q_PROPERTY(KWin::EffectWindow *activeWindow READ activeWindow WRITE activateWindow NOTIFY windowActivated) + Q_PROPERTY(QSize desktopGridSize READ desktopGridSize NOTIFY desktopGridSizeChanged) + Q_PROPERTY(int desktopGridWidth READ desktopGridWidth NOTIFY desktopGridWidthChanged) + Q_PROPERTY(int desktopGridHeight READ desktopGridHeight NOTIFY desktopGridHeightChanged) + Q_PROPERTY(int workspaceWidth READ workspaceWidth) + Q_PROPERTY(int workspaceHeight READ workspaceHeight) + Q_PROPERTY(QList desktops READ desktops) + Q_PROPERTY(bool optionRollOverDesktops READ optionRollOverDesktops) + Q_PROPERTY(KWin::LogicalOutput *activeScreen READ activeScreen) + /** + * Factor by which animation speed in the effect should be modified (multiplied). + * If configurable in the effect itself, the option should have also 'default' + * animation speed. The actual value should be determined using animationTime(). + * Note: The factor can be also 0, so make sure your code can cope with 0ms time + * if used manually. + */ + Q_PROPERTY(qreal animationTimeFactor READ animationTimeFactor) + Q_PROPERTY(QList stackingOrder READ stackingOrder) + /** + * Whether window decorations use the alpha channel. + */ + Q_PROPERTY(bool decorationsHaveAlpha READ decorationsHaveAlpha) + Q_PROPERTY(CompositingType compositingType READ compositingType CONSTANT) + Q_PROPERTY(QPointF cursorPos READ cursorPos) + Q_PROPERTY(QSize virtualScreenSize READ virtualScreenSize NOTIFY virtualScreenSizeChanged) + Q_PROPERTY(QRect virtualScreenGeometry READ virtualScreenGeometry NOTIFY virtualScreenGeometryChanged) + Q_PROPERTY(bool hasActiveFullScreenEffect READ hasActiveFullScreenEffect NOTIFY hasActiveFullScreenEffectChanged) + Q_PROPERTY(bool colorPickerActive READ isColorPickerActive NOTIFY colorPickerActiveChanged) + + /** + * The status of the session i.e if the user is logging out + * @since 5.18 + */ + Q_PROPERTY(KWin::SessionState sessionState READ sessionState NOTIFY sessionStateChanged) + + Q_PROPERTY(KWin::EffectWindow *inputPanel READ inputPanel NOTIFY inputPanelChanged) + + friend class Effect; + +public: + using TouchBorderCallback = std::function; + + EffectsHandler(Compositor *compositor, WorkspaceScene *scene); + ~EffectsHandler() override; + + // internal (used by kwin core or compositing code) + void startPaint(); + + // for use by effects + void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime); + void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen); + void postPaintScreen(); + void prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime); + void paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data); + void drawWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data); + void renderWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data); + QVariant kwinOption(KWinOption kwopt); + /** + * Sets the cursor while the mouse is intercepted. + * @see startMouseInterception + * @since 4.11 + */ + void defineCursor(Qt::CursorShape shape); + QPointF cursorPos() const; + bool grabKeyboard(Effect *effect); + void ungrabKeyboard(); + /** + * Ensures that all mouse events are sent to the @p effect. + * No window will get the mouse events. Only fullscreen effects providing a custom user interface should + * be using this method. The input events are delivered to Effect::windowInputMouseEvent. + * + * @note This method does not perform an X11 mouse grab. On X11 a fullscreen input window is raised above + * all other windows, but no grab is performed. + * + * @param effect The effect + * @param shape Sets the cursor to be used while the mouse is intercepted + * @see stopMouseInterception + * @see Effect::windowInputMouseEvent + * @since 4.11 + */ + void startMouseInterception(Effect *effect, Qt::CursorShape shape); + /** + * Releases the hold mouse interception for @p effect + * @see startMouseInterception + * @since 4.11 + */ + void stopMouseInterception(Effect *effect); + bool isMouseInterception() const; + + bool checkInputWindowEvent(QMouseEvent *e); + bool checkInputWindowEvent(QWheelEvent *e); + + void grabbedKeyboardEvent(QKeyEvent *e); + bool hasKeyboardGrab() const; + + /** + * @brief Registers a global pointer shortcut with the provided @p action. + * + * @param modifiers The keyboard modifiers which need to be holded + * @param pointerButtons The pointer buttons which need to be pressed + * @param action The action which gets triggered when the shortcut matches + */ + void registerPointerShortcut(Qt::KeyboardModifiers modifiers, Qt::MouseButton pointerButtons, QAction *action); + /** + * @brief Registers a global axis shortcut with the provided @p action. + * + * @param modifiers The keyboard modifiers which need to be holded + * @param axis The direction in which the axis needs to be moved + * @param action The action which gets triggered when the shortcut matches + */ + void registerAxisShortcut(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis, QAction *action); + + /** + * @brief Registers a global touchpad swipe gesture shortcut with the provided @p action. + * + * @param direction The direction for the swipe + * @param action The action which gets triggered when the gesture triggers + * @since 5.10 + */ + void registerTouchpadSwipeShortcut(SwipeDirection dir, uint fingerCount, QAction *onUp, std::function progressCallback = {}); + + void registerTouchpadPinchShortcut(PinchDirection dir, uint fingerCount, QAction *onUp, std::function progressCallback = {}); + + /** + * @brief Registers a global touchscreen swipe gesture shortcut with the provided @p action. + * + * @param direction The direction for the swipe + * @param action The action which gets triggered when the gesture triggers + * @since 5.25 + */ + void registerTouchscreenSwipeShortcut(SwipeDirection direction, uint fingerCount, QAction *action, std::function progressCallback); + + void reserveElectricBorder(ElectricBorder border, Effect *effect); + void unreserveElectricBorder(ElectricBorder border, Effect *effect); + + /** + * Registers the given @p action for the given @p border to be activated through + * a touch swipe gesture. + * + * If the @p border gets triggered through a touch swipe gesture the QAction::triggered + * signal gets invoked. + * + * To unregister the touch screen action either delete the @p action or + * invoke unregisterTouchBorder. + * + * @see unregisterTouchBorder + * @since 5.10 + */ + void registerTouchBorder(ElectricBorder border, QAction *action); + + /** + * Registers the given @p action for the given @p border to be activated through + * a touch swipe gesture. + * + * If the @p border gets triggered through a touch swipe gesture the QAction::triggered + * signal gets invoked. + * + * progressCallback will be dynamically called each time the touch position is updated + * to show the effect "partially" activated + * + * To unregister the touch screen action either delete the @p action or + * invoke unregisterTouchBorder. + * + * @see unregisterTouchBorder + * @since 5.25 + */ + void registerRealtimeTouchBorder(ElectricBorder border, QAction *action, TouchBorderCallback progressCallback); + + /** + * Unregisters the given @p action for the given touch @p border. + * + * @see registerTouchBorder + * @since 5.10 + */ + void unregisterTouchBorder(ElectricBorder border, QAction *action); + + // functions that allow controlling windows/desktop + void activateWindow(KWin::EffectWindow *c); + KWin::EffectWindow *activeWindow() const; + Q_SCRIPTABLE void moveWindow(KWin::EffectWindow *w, const QPoint &pos, bool snap = false, double snapAdjust = 1.0); + + /** + * Moves a window to the given desktops + * On X11, the window will end up on the last window in the list + * Setting this to an empty list will set the window on all desktops + */ + Q_SCRIPTABLE void windowToDesktops(KWin::EffectWindow *w, const QList &desktops); + + Q_SCRIPTABLE void windowToScreen(KWin::EffectWindow *w, LogicalOutput *screen); + void setShowingDesktop(bool showing); + + // Activities + /** + * @returns The ID of the current activity. + */ + QString currentActivity() const; + // Desktops + /** + * @returns The current desktop. + */ + VirtualDesktop *currentDesktop() const; + /** + * @returns Total number of desktops currently in existence. + */ + QList desktops() const; + /** + * Set the current desktop to @a desktop. + */ + void setCurrentDesktop(KWin::VirtualDesktop *desktop); + /** + * @returns The size of desktop layout in grid units. + */ + QSize desktopGridSize() const; + /** + * @returns The width of desktop layout in grid units. + */ + int desktopGridWidth() const; + /** + * @returns The height of desktop layout in grid units. + */ + int desktopGridHeight() const; + /** + * @returns The width of desktop layout in pixels. + */ + int workspaceWidth() const; + /** + * @returns The height of desktop layout in pixels. + */ + int workspaceHeight() const; + /** + * @returns The desktop at the point @a coords or 0 if no desktop exists at that + * point. @a coords is to be in grid units. + */ + VirtualDesktop *desktopAtCoords(QPoint coords) const; + /** + * @returns The coords of the specified @a desktop in grid units. + */ + QPoint desktopGridCoords(VirtualDesktop *desktop) const; + /** + * @returns The coords of the top-left corner of @a desktop in pixels. + */ + QPoint desktopCoords(VirtualDesktop *desktop) const; + /** + * @returns The desktop above the given @a desktop. Wraps around to the bottom of + * the layout if @a wrap is set. If @a id is not set use the current one. + */ + Q_SCRIPTABLE KWin::VirtualDesktop *desktopAbove(KWin::VirtualDesktop *desktop = nullptr, bool wrap = true) const; + /** + * @returns The desktop to the right of the given @a desktop. Wraps around to the + * left of the layout if @a wrap is set. If @a id is not set use the current one. + */ + Q_SCRIPTABLE KWin::VirtualDesktop *desktopToRight(KWin::VirtualDesktop *desktop = nullptr, bool wrap = true) const; + /** + * @returns The desktop below the given @a desktop. Wraps around to the top of the + * layout if @a wrap is set. If @a id is not set use the current one. + */ + Q_SCRIPTABLE KWin::VirtualDesktop *desktopBelow(KWin::VirtualDesktop *desktop = nullptr, bool wrap = true) const; + /** + * @returns The desktop to the left of the given @a desktop. Wraps around to the + * right of the layout if @a wrap is set. If @a id is not set use the current one. + */ + Q_SCRIPTABLE KWin::VirtualDesktop *desktopToLeft(KWin::VirtualDesktop *desktop = nullptr, bool wrap = true) const; + Q_SCRIPTABLE QString desktopName(KWin::VirtualDesktop *desktop) const; + bool optionRollOverDesktops() const; + + LogicalOutput *activeScreen() const; // Xinerama + QRectF clientArea(clientAreaOption, const LogicalOutput *screen, const VirtualDesktop *desktop) const; + QRectF clientArea(clientAreaOption, const EffectWindow *c) const; + QRectF clientArea(clientAreaOption, const QPoint &p, const VirtualDesktop *desktop) const; + + /** + * The bounding size of all screens combined. Overlapping areas + * are not counted multiple times. + * + * @see virtualScreenGeometry() + * @see virtualScreenSizeChanged() + * @since 5.0 + */ + QSize virtualScreenSize() const; + /** + * The bounding geometry of all outputs combined. Always starts at (0,0) and has + * virtualScreenSize as it's size. + * + * @see virtualScreenSize() + * @see virtualScreenGeometryChanged() + * @since 5.0 + */ + QRect virtualScreenGeometry() const; + /** + * Factor by which animation speed in the effect should be modified (multiplied). + * If configurable in the effect itself, the option should have also 'default' + * animation speed. The actual value should be determined using animationTime(). + * Note: The factor can be also 0, so make sure your code can cope with 0ms time + * if used manually. + */ + double animationTimeFactor() const; + + Q_SCRIPTABLE KWin::EffectWindow *findWindow(WId id) const; + Q_SCRIPTABLE KWin::EffectWindow *findWindow(SurfaceInterface *surf) const; + /** + * Finds the EffectWindow for the internal window @p w. + * If there is no such window @c null is returned. + * + * On Wayland this returns the internal window. On X11 it returns an Unmanaged with the + * window id matching that of the provided window @p w. + * + * @since 5.16 + */ + Q_SCRIPTABLE KWin::EffectWindow *findWindow(QWindow *w) const; + /** + * Finds the EffectWindow for the Window with KWin internal @p id. + * If there is no such window @c null is returned. + * + * @since 5.16 + */ + Q_SCRIPTABLE KWin::EffectWindow *findWindow(const QUuid &id) const; + QList stackingOrder() const; + // window will be temporarily painted as if being at the top of the stack + Q_SCRIPTABLE void setElevatedWindow(KWin::EffectWindow *w, bool set); + + void setTabBoxWindow(EffectWindow *); + QList currentTabBoxWindowList() const; + void refTabBox(); + void unrefTabBox(); + void closeTabBox(); + EffectWindow *currentTabBoxWindow() const; + + void setActiveFullScreenEffect(Effect *e); + Effect *activeFullScreenEffect() const; + + /** + * Schedules the entire workspace to be repainted next time. + * If you call it during painting (including prepaint) then it does not + * affect the current painting. + */ + Q_SCRIPTABLE void addRepaintFull(); + // TODO Plasma 7: rename these to "addLogicalRepaint" + Q_SCRIPTABLE void addRepaint(const QRectF &logicalRegion); + Q_SCRIPTABLE void addRepaint(const QRect &logicalRegion); + Q_SCRIPTABLE void addRepaint(const RectF &logicalRegion); + Q_SCRIPTABLE void addRepaint(const Rect &logicalRegion); + Q_SCRIPTABLE void addRepaint(const Region &logicalRegion); + Q_SCRIPTABLE void addRepaint(int x, int y, int w, int h); + + CompositingType compositingType() const; + /** + * @brief Whether the Compositor is OpenGL based (either GL 1 or 2). + * + * @return bool @c true in case of OpenGL based Compositor, @c false otherwise + */ + bool isOpenGLCompositing() const; + EglContext *openglContext() const; + /** + * @brief Provides access to the QPainter which is rendering to the back buffer. + * + * Only relevant for CompositingType QPainterCompositing. For all other compositing types + * @c null is returned. + * + * @return QPainter* The Scene's QPainter or @c null. + */ + QPainter *scenePainter(); + void reconfigure(); + + QByteArray readRootProperty(long atom, long type, int format) const; + +#if KWIN_BUILD_X11 + /** + * @brief Announces support for the feature with the given name. If no other Effect + * has announced support for this feature yet, an X11 property will be installed on + * the root window. + * + * The Effect will be notified for events through the signal propertyNotify(). + * + * To remove the support again use removeSupportProperty. When an Effect is + * destroyed it is automatically taken care of removing the support. It is not + * required to call removeSupportProperty in the Effect's cleanup handling. + * + * @param propertyName The name of the property to announce support for + * @param effect The effect which announces support + * @return xcb_atom_t The created X11 atom + * @see removeSupportProperty + * @since 4.11 + */ + xcb_atom_t announceSupportProperty(const QByteArray &propertyName, Effect *effect); + /** + * @brief Removes support for the feature with the given name. If there is no other Effect left + * which has announced support for the given property, the property will be removed from the + * root window. + * + * In case the Effect had not registered support, calling this function does not change anything. + * + * @param propertyName The name of the property to remove support for + * @param effect The effect which had registered the property. + * @see announceSupportProperty + * @since 4.11 + */ + void removeSupportProperty(const QByteArray &propertyName, Effect *effect); +#endif + + /** + * Returns @a true if the active window decoration has shadow API hooks. + */ + bool hasDecorationShadows() const; + + /** + * Returns @a true if the window decorations use the alpha channel, and @a false otherwise. + * @since 4.5 + */ + bool decorationsHaveAlpha() const; + + /** + * Allows an effect to trigger a reload of itself. + * This can be used by an effect which needs to be reloaded when screen geometry changes. + * It is possible that the effect cannot be loaded again as it's supported method does no longer + * hold. + * @param effect The effect to reload + * @since 4.8 + */ + void reloadEffect(Effect *effect); + Effect *provides(Effect::Feature ef); + Effect *findEffect(const QString &name) const; + QStringList loadedEffects() const; + QStringList listOfEffects() const; + void unloadAllEffects(); + QStringList activeEffects() const; + bool isEffectActive(const QString &pluginId) const; + + /** + * Whether the screen is currently considered as locked. + * Note for technical reasons this is not always possible to detect. The screen will only + * be considered as locked if the screen locking process implements the + * org.freedesktop.ScreenSaver interface. + * + * @returns @c true if the screen is currently locked, @c false otherwise + * @see screenLockingChanged + * @since 4.11 + */ + bool isScreenLocked() const; + + /** + * @brief Makes the OpenGL compositing context current. + * + * If the compositing backend is not using OpenGL, this method returns @c false. + * + * @return bool @c true if the context became current, @c false otherwise. + */ + bool makeOpenGLContextCurrent(); + /** + * @brief Makes a null OpenGL context current resulting in no context + * being current. + * + * If the compositing backend is not OpenGL based, this method is a noop. + * + * There is normally no reason for an Effect to call this method. + */ + void doneOpenGLContextCurrent(); + +#if KWIN_BUILD_X11 + xcb_connection_t *xcbConnection() const; + xcb_window_t x11RootWindow() const; +#endif + + /** + * Interface to the Wayland display: this is relevant only + * on Wayland, on X11 it will be nullptr + * @since 5.5 + */ + Display *waylandDisplay() const; + + /** + * Whether animations are supported by the Scene. + * If this method returns @c false Effects are supposed to not + * animate transitions. + * + * @returns Whether the Scene can drive animations + * @since 5.8 + */ + bool animationsSupported() const; + + /** + * The current cursor image of the Platform. + * @see cursorPos + * @since 5.9 + */ + PlatformCursorImage cursorImage() const; + + /** + * The cursor image should be hidden. + * @see showCursor + * @since 5.9 + */ + void hideCursor(); + + /** + * The cursor image should be shown again after having been hidden. + * @see hideCursor + * @since 5.9 + */ + void showCursor(); + + /** + * @returns Whether or not the cursor is currently hidden + */ + bool isCursorHidden() const; + + /** + * Starts an interactive window selection process. + * + * Once the user selected a window the @p callback is invoked with the selected EffectWindow as + * argument. In case the user cancels the interactive window selection or selecting a window is currently + * not possible (e.g. screen locked) the @p callback is invoked with a @c nullptr argument. + * + * During the interactive window selection the cursor is turned into a crosshair cursor. + * + * @param callback The function to invoke once the interactive window selection ends + * @since 5.9 + */ + void startInteractiveWindowSelection(std::function callback); + + /** + * Starts an interactive position selection process. + * + * Once the user selected a position on the screen the @p callback is invoked with + * the selected point as argument. In case the user cancels the interactive position selection + * or selecting a position is currently not possible (e.g. screen locked) the @p callback + * is invoked with a point at @c -1 as x and y argument. + * + * During the interactive window selection the cursor is turned into a crosshair cursor. + * + * @param callback The function to invoke once the interactive position selection ends + * @since 5.9 + */ + void startInteractivePositionSelection(std::function callback); + + /** + * Shows an on-screen-message. To hide it again use hideOnScreenMessage. + * + * @param message The message to show + * @param iconName The optional themed icon name + * @see hideOnScreenMessage + * @since 5.9 + */ + void showOnScreenMessage(const QString &message, const QString &iconName = QString()); + + /** + * Flags for how to hide a shown on-screen-message + * @see hideOnScreenMessage + * @since 5.9 + */ + enum class OnScreenMessageHideFlag { + /** + * The on-screen-message should skip the close window animation. + * @see EffectWindow::skipsCloseAnimation + */ + SkipsCloseAnimation = 1 + }; + Q_DECLARE_FLAGS(OnScreenMessageHideFlags, OnScreenMessageHideFlag) + /** + * Hides a previously shown on-screen-message again. + * @param flags The flags for how to hide the message + * @see showOnScreenMessage + * @since 5.9 + */ + void hideOnScreenMessage(OnScreenMessageHideFlags flags = OnScreenMessageHideFlags()); + + /* + * @returns The configuration used by the EffectsHandler. + * @since 5.10 + */ + KSharedConfigPtr config() const; + + /** + * @returns The global input configuration (kcminputrc) + * @since 5.10 + */ + KSharedConfigPtr inputConfig() const; + + /** + * Returns true if activeFullScreenEffect is set + */ + bool hasActiveFullScreenEffect() const; + + /** + * Returns true if color picker effect is currently picking colors. + */ + bool isColorPickerActive() const; + + /** + * Render the supplied OffscreenQuickView onto the scene + * It can be called at any point during the scene rendering + * @since 5.18 + */ + void renderOffscreenQuickView(const RenderTarget &renderTarget, const RenderViewport &viewport, OffscreenQuickView *effectQuickView) const; + + /** + * The status of the session i.e if the user is logging out + * @since 5.18 + */ + SessionState sessionState() const; + + /** + * Returns the list of all the screens connected to the system. + */ + QList screens() const; + LogicalOutput *screenAt(const QPoint &point) const; + LogicalOutput *findScreen(const QString &name) const; + LogicalOutput *findScreen(int screenId) const; + + KWin::EffectWindow *inputPanel() const; + bool isInputPanelOverlay() const; + + QQmlEngine *qmlEngine() const; + + /** + * @returns whether or not any effect is currently active where KWin should not use direct scanout + */ + bool blocksDirectScanout() const; + + WorkspaceScene *scene() const + { + return m_scene; + } + + bool touchDown(qint32 id, const QPointF &pos, std::chrono::microseconds time); + bool touchMotion(qint32 id, const QPointF &pos, std::chrono::microseconds time); + bool touchUp(qint32 id, std::chrono::microseconds time); + void touchCancel(); + + bool tabletToolProximityEvent(KWin::TabletToolProximityEvent *event); + bool tabletToolAxisEvent(KWin::TabletToolAxisEvent *event); + bool tabletToolTipEvent(KWin::TabletToolTipEvent *event); + bool tabletToolButtonEvent(uint button, bool pressed, InputDeviceTabletTool *tool, std::chrono::microseconds time); + bool tabletPadButtonEvent(uint button, bool pressed, std::chrono::microseconds time, InputDevice *device); + bool tabletPadStripEvent(int number, qreal position, bool isFinger, std::chrono::microseconds time, InputDevice *device); + bool tabletPadRingEvent(int number, qreal position, bool isFinger, std::chrono::microseconds time, InputDevice *device); + bool tabletPadDialEvent(int number, double delta, std::chrono::microseconds time, InputDevice *device); + + void highlightWindows(const QList &windows); + +#if KWIN_BUILD_X11 + bool isPropertyTypeRegistered(xcb_atom_t atom) const + { + return registered_atoms.contains(atom); + } +#endif + +Q_SIGNALS: + /** + * This signal is emitted whenever a new @a screen is added to the system. + */ + void screenAdded(KWin::LogicalOutput *screen); + /** + * This signal is emitted whenever a @a screen is removed from the system. + */ + void screenRemoved(KWin::LogicalOutput *screen); + /** + * Signal emitted when the current desktop changed. + * @param oldDesktop The previously current desktop + * @param newDesktop The new current desktop + * @param with The window which is taken over to the new desktop, can be NULL + * @since 4.9 + */ + void desktopChanged(KWin::VirtualDesktop *oldDesktop, KWin::VirtualDesktop *newDesktop, KWin::EffectWindow *with); + + /** + * Signal emitted while desktop is changing for animation. + * @param currentDesktop The current desktop until otherwise. + * @param offset The current desktop offset. + * offset.x() = .6 means 60% of the way to the desktop to the right. + * Positive Values means Up and Right. + */ + void desktopChanging(KWin::VirtualDesktop *currentDesktop, QPointF offset, KWin::EffectWindow *with); + void desktopChangingCancelled(); + void desktopAdded(KWin::VirtualDesktop *desktop); + void desktopRemoved(KWin::VirtualDesktop *desktop); + void desktopMoved(KWin::VirtualDesktop *desktop, int position); + + /** + * Emitted when the virtual desktop grid layout changes + * @param size new size + * @since 5.25 + */ + void desktopGridSizeChanged(const QSize &size); + /** + * Emitted when the virtual desktop grid layout changes + * @param width new width + * @since 5.25 + */ + void desktopGridWidthChanged(int width); + /** + * Emitted when the virtual desktop grid layout changes + * @param height new height + * @since 5.25 + */ + void desktopGridHeightChanged(int height); + /** + * Signal emitted when the desktop showing ("dashboard") state changed + * The desktop is risen to the keepAbove layer, you may want to elevate + * windows or such. + * @since 5.3 + */ + void showingDesktopChanged(bool); + /** + * Signal emitted when a new window has been added to the Workspace. + * @param w The added window + * @since 4.7 + */ + void windowAdded(KWin::EffectWindow *w); + /** + * Signal emitted when a window is being removed from the Workspace. + * An effect which wants to animate the window closing should connect + * to this signal and reference the window by using + * refWindow + * @param w The window which is being closed + * @since 4.7 + */ + void windowClosed(KWin::EffectWindow *w); + /** + * Signal emitted when a window gets activated. + * @param w The new active window, or @c NULL if there is no active window. + * @since 4.7 + */ + void windowActivated(KWin::EffectWindow *w); + /** + * Signal emitted when a window is deleted. + * This means that a closed window is not referenced any more. + * An effect bookkeeping the closed windows should connect to this + * signal to clean up the internal references. + * @param w The window which is going to be deleted. + * @see EffectWindow::refWindow + * @see EffectWindow::unrefWindow + * @see windowClosed + * @since 4.7 + */ + void windowDeleted(KWin::EffectWindow *w); + /** + * Signal emitted when a tabbox is added. + * An effect who wants to replace the tabbox with itself should use refTabBox. + * @param mode The TabBoxMode. + * @see refTabBox + * @see tabBoxClosed + * @see tabBoxUpdated + * @see tabBoxKeyEvent + * @since 4.7 + */ + void tabBoxAdded(int mode); + /** + * Signal emitted when the TabBox was closed by KWin core. + * An effect which referenced the TabBox should use unrefTabBox to unref again. + * @see unrefTabBox + * @see tabBoxAdded + * @since 4.7 + */ + void tabBoxClosed(); + /** + * Signal emitted when the selected TabBox window changed or the TabBox List changed. + * An effect should only respond to this signal if it referenced the TabBox with refTabBox. + * @see refTabBox + * @see currentTabBoxWindowList + * @see currentTabBoxDesktopList + * @see currentTabBoxWindow + * @see currentTabBoxDesktop + * @since 4.7 + */ + void tabBoxUpdated(); + /** + * Signal emitted when a key event, which is not handled by TabBox directly, happens while + * TabBox is active. An effect might use the key event to e.g. change the selected window. + * An effect should only respond to this signal if it referenced the TabBox with refTabBox. + * @param event The key event not handled by TabBox directly + * @see refTabBox + * @since 4.7 + */ + void tabBoxKeyEvent(QKeyEvent *event); + /** + * Signal emitted when mouse changed. + * If an effect needs to get updated mouse positions, it needs to first call startMousePolling. + * For a fullscreen effect it is better to use an input window and react on windowInputMouseEvent. + * @param pos The new mouse position + * @param oldpos The previously mouse position + * @param buttons The pressed mouse buttons + * @param oldbuttons The previously pressed mouse buttons + * @param modifiers Pressed keyboard modifiers + * @param oldmodifiers Previously pressed keyboard modifiers. + * @see startMousePolling + * @since 4.7 + */ + void mouseChanged(const QPointF &pos, const QPointF &oldpos, + Qt::MouseButtons buttons, Qt::MouseButtons oldbuttons, + Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers); + /** + * Signal emitted when the cursor shape changed. + * You'll likely want to query the current cursor as reaction: xcb_xfixes_get_cursor_image_unchecked + * Connection to this signal is tracked, so if you don't need it anymore, disconnect from it to stop cursor event filtering + */ + void cursorShapeChanged(); + /** + * Receives events registered for using registerPropertyType. + * Use readProperty() to get the property data. + * Note that the property may be already set on the window, so doing the same + * processing from windowAdded() (e.g. simply calling propertyNotify() from it) + * is usually needed. + * @param w The window whose property changed, is @c null if it is a root window property + * @param atom The property + * @since 4.7 + */ + void propertyNotify(KWin::EffectWindow *w, long atom); + + /** + * emitted before the current activity actually changes + * @since 6.3 + */ + void currentActivityAboutToChange(); + /** + * This signal is emitted when the global + * activity is changed + * @param id id of the new current activity + * @since 4.9 + */ + void currentActivityChanged(const QString &id); + /** + * This signal is emitted when a new activity is added + * @param id id of the new activity + * @since 4.9 + */ + void activityAdded(const QString &id); + /** + * This signal is emitted when the activity + * is removed + * @param id id of the removed activity + * @since 4.9 + */ + void activityRemoved(const QString &id); + /** + * This signal is emitted when the screen got locked or unlocked. + * @param locked @c true if the screen is now locked, @c false if it is now unlocked + * @since 4.11 + */ + void screenLockingChanged(bool locked); + + /** + * This signal is emitted just before the screen locker tries to grab keys and lock the screen + * Effects should release any grabs immediately + * @since 5.17 + */ + void screenAboutToLock(); + + /** + * This signal is emitted whenever the stacking order is changed, i.e. a window is raised + * or lowered + * @since 4.10 + */ + void stackingOrderChanged(); + /** + * This signal is emitted when the user starts to approach the @p border with the mouse. + * The @p factor describes how far away the mouse is in a relative mean. The values are in + * [0.0, 1.0] with 0.0 being emitted when first entered and on leaving. The value 1.0 means that + * the @p border is reached with the mouse. So the values are well suited for animations. + * The signal is always emitted when the mouse cursor position changes. + * @param border The screen edge which is being approached + * @param factor Value in range [0.0,1.0] to describe how close the mouse is to the border + * @param geometry The geometry of the edge which is being approached + * @since 4.11 + */ + void screenEdgeApproaching(ElectricBorder border, qreal factor, const QRect &geometry); + /** + * Emitted whenever the virtualScreenSize changes. + * @see virtualScreenSize() + * @since 5.0 + */ + void virtualScreenSizeChanged(); + /** + * Emitted whenever the virtualScreenGeometry changes. + * @see virtualScreenGeometry() + * @since 5.0 + */ + void virtualScreenGeometryChanged(); + + /** + * This signal gets emitted when the data on EffectWindow @p w for @p role changed. + * + * An Effect can connect to this signal to read the new value and react on it. + * E.g. an Effect which does not operate on windows grabbed by another Effect wants + * to cancel the already scheduled animation if another Effect adds a grab. + * + * @param w The EffectWindow for which the data changed + * @param role The data role which changed + * @see EffectWindow::setData + * @see EffectWindow::data + * @since 5.8.4 + */ + void windowDataChanged(KWin::EffectWindow *w, int role); + +#if KWIN_BUILD_X11 + /** + * The xcb connection changed, either a new xcbConnection got created or the existing one + * got destroyed. + * Effects can use this to refetch the properties they want to set. + * + * When the xcbConnection changes also the x11RootWindow becomes invalid. + * @see xcbConnection + * @see x11RootWindow + * @since 5.11 + */ + void xcbConnectionChanged(); +#endif + + /** + * This signal is emitted when active fullscreen effect changed. + * + * @see activeFullScreenEffect + * @see setActiveFullScreenEffect + * @since 5.14 + */ + void activeFullScreenEffectChanged(); + + /** + * This signal is emitted when active fullscreen effect changed to being + * set or unset + * + * @see activeFullScreenEffect + * @see setActiveFullScreenEffect + * @since 5.15 + */ + void hasActiveFullScreenEffectChanged(); + + void colorPickerActiveChanged(); + + /** + * This signal is emitted when the session state was changed + * @since 5.18 + */ + void sessionStateChanged(); + + void startupAdded(const QString &id, const QIcon &icon); + void startupChanged(const QString &id, const QIcon &icon); + void startupRemoved(const QString &id); + + void inputPanelChanged(); + + void viewRemoved(RenderView *view); + +public Q_SLOTS: + // slots for D-Bus interface + Q_SCRIPTABLE void reconfigureEffect(const QString &name); + Q_SCRIPTABLE bool loadEffect(const QString &name); + Q_SCRIPTABLE void toggleEffect(const QString &name); + Q_SCRIPTABLE void unloadEffect(const QString &name); + Q_SCRIPTABLE bool isEffectLoaded(const QString &name) const; + Q_SCRIPTABLE bool isEffectSupported(const QString &name); + Q_SCRIPTABLE QList areEffectsSupported(const QStringList &names); + Q_SCRIPTABLE QString supportInformation(const QString &name) const; + Q_SCRIPTABLE QString debug(const QString &name, const QString ¶meter = QString()) const; + +protected: + void effectsChanged(); + void setupWindowConnections(KWin::Window *window); + + void registerPropertyType(long atom, bool reg); + void destroyEffect(Effect *effect); + void reconfigureEffects(); + void configChanged(const KConfigGroup &group, const QByteArrayList &names); + + typedef QList EffectsList; + typedef EffectsList::const_iterator EffectsIterator; + + struct + { + QPointF position; + Qt::MouseButtons buttons; + Qt::KeyboardModifiers modifiers; + } m_cursor; + + Effect *keyboard_grab_effect; + Effect *fullscreen_effect; + QMultiMap effect_order; +#if KWIN_BUILD_X11 + QHash registered_atoms; +#endif + QList loaded_effects; + CompositingType compositing_type; + EffectsList m_activeEffects; + EffectsIterator m_currentDrawWindowIterator; + EffectsIterator m_currentPaintWindowIterator; + EffectsIterator m_currentPaintScreenIterator; + typedef QHash> PropertyEffectMap; +#if KWIN_BUILD_X11 + PropertyEffectMap m_propertiesForEffects; +#endif + QHash m_managedProperties; + Compositor *m_compositor; + WorkspaceScene *m_scene; + QList m_grabbedMouseEffects; + EffectLoader *m_effectLoader; + std::unique_ptr m_x11WindowPropertyNotify; + KConfigWatcher::Ptr m_configWatcher; +}; + +/** + * Pointer to the global EffectsHandler object. + */ +extern KWIN_EXPORT EffectsHandler *effects; + +} // namespace + +/** @} */ diff --git a/local/recipes/kde/kwin/source/src/effect/effectloader.cpp b/local/recipes/kde/kwin/source/src/effect/effectloader.cpp new file mode 100644 index 0000000000..d40ee952ea --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/effectloader.cpp @@ -0,0 +1,500 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +// own +#include "effect/effectloader.h" +// config +#include "config-kwin.h" +// KWin +#include "effect/effect.h" +#include "effect/effecthandler.h" +#include "plugin.h" +#include "scripting/scriptedeffect.h" +#include "scripting/scriptedquicksceneeffect.h" +#include "scripting/scripting.h" +#include "utils/common.h" +// KDE +#include +#include +#include +// Qt +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +AbstractEffectLoader::AbstractEffectLoader(QObject *parent) + : QObject(parent) +{ +} + +AbstractEffectLoader::~AbstractEffectLoader() +{ +} + +void AbstractEffectLoader::setConfig(KSharedConfig::Ptr config) +{ + m_config = config; +} + +LoadEffectFlags AbstractEffectLoader::readConfig(const QString &effectName, bool defaultValue) const +{ + Q_ASSERT(m_config); + KConfigGroup plugins(m_config, QStringLiteral("Plugins")); + + const QString key = effectName + QStringLiteral("Enabled"); + + // do we have a key for the effect? + if (plugins.hasKey(key)) { + // we have a key in the config, so read the enabled state + const bool load = plugins.readEntry(key, defaultValue); + return load ? LoadEffectFlags(LoadEffectFlag::Load) : LoadEffectFlags(); + } + // we don't have a key, so we just use the enabled by default value + if (defaultValue) { + return LoadEffectFlag::Load | LoadEffectFlag::CheckDefaultFunction; + } + return LoadEffectFlags(); +} + +static const QString s_serviceType = QStringLiteral("KWin/Effect"); + +ScriptedEffectLoader::ScriptedEffectLoader(QObject *parent) + : AbstractEffectLoader(parent) + , m_queue(new EffectLoadQueue(this)) +{ +} + +ScriptedEffectLoader::~ScriptedEffectLoader() +{ +} + +bool ScriptedEffectLoader::hasEffect(const QString &name) const +{ + return findEffect(name).isValid(); +} + +bool ScriptedEffectLoader::isEffectSupported(const QString &name) const +{ + // scripted effects are in general supported + if (!ScriptedEffect::supported()) { + return false; + } + return hasEffect(name); +} + +QStringList ScriptedEffectLoader::listOfKnownEffects() const +{ + const auto effects = findAllEffects(); + QStringList result; + for (const auto &service : effects) { + result << service.pluginId(); + } + return result; +} + +bool ScriptedEffectLoader::loadEffect(const QString &name) +{ + auto effect = findEffect(name); + if (!effect.isValid()) { + return false; + } + return loadEffect(effect, LoadEffectFlag::Load); +} + +bool ScriptedEffectLoader::loadEffect(const KPluginMetaData &effect, LoadEffectFlags flags) +{ + const QString name = effect.pluginId(); + if (!flags.testFlag(LoadEffectFlag::Load)) { + qCDebug(KWIN_CORE) << "Loading flags disable effect: " << name; + return false; + } + + if (m_loadedEffects.contains(name)) { + qCDebug(KWIN_CORE) << name << "already loaded"; + return false; + } + + const QString api = effect.value(QStringLiteral("X-Plasma-API")); + if (api == QLatin1String("javascript")) { + return loadJavascriptEffect(effect); + } else if (api == QLatin1String("declarativescript")) { + return loadDeclarativeEffect(effect); + } else { + qCWarning(KWIN_CORE, "Failed to load %s effect: invalid X-Plasma-API field: %s. " + "Available options are javascript, and declarativescript", qPrintable(name), qPrintable(api)); + } + + return false; +} + +bool ScriptedEffectLoader::loadJavascriptEffect(const KPluginMetaData &effect) +{ + const QString name = effect.pluginId(); + if (!ScriptedEffect::supported()) { + qCDebug(KWIN_CORE) << "Effect is not supported: " << name; + return false; + } + + ScriptedEffect *e = ScriptedEffect::create(effect); + if (!e) { + qCDebug(KWIN_CORE) << "Could not initialize scripted effect: " << name; + return false; + } + connect(e, &ScriptedEffect::destroyed, this, [this, name]() { + m_loadedEffects.removeAll(name); + }); + + qCDebug(KWIN_CORE) << "Successfully loaded scripted effect: " << name; + Q_EMIT effectLoaded(e, name); + m_loadedEffects << name; + return true; +} + +bool ScriptedEffectLoader::loadDeclarativeEffect(const KPluginMetaData &metadata) +{ + const QString name = metadata.pluginId(); + QString scriptFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kwin-wayland/effects/") + name + QLatin1String("/contents/ui/main.qml")); + if (scriptFile.isNull()) { + scriptFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kwin/effects/") + name + QLatin1String("/contents/ui/main.qml")); + if (scriptFile.isNull()) { + qCWarning(KWIN_CORE) << "Could not locate the effect script"; + return false; + } + } + + QQmlEngine *engine = Scripting::self()->qmlEngine(); + QQmlComponent component(engine); + component.loadUrl(QUrl::fromLocalFile(scriptFile)); + if (component.isError()) { + qCWarning(KWIN_CORE).nospace() << "Failed to load " << scriptFile << ": " << component.errors(); + return false; + } + + QObject *object = component.beginCreate(engine->rootContext()); + auto effect = qobject_cast(object); + if (!effect) { + qCDebug(KWIN_CORE) << "Could not initialize scripted effect: " << name; + delete object; + return false; + } + effect->setMetaData(metadata); + component.completeCreate(); + + connect(effect, &Effect::destroyed, this, [this, name]() { + m_loadedEffects.removeAll(name); + }); + + qCDebug(KWIN_CORE) << "Successfully loaded scripted effect: " << name; + Q_EMIT effectLoaded(effect, name); + m_loadedEffects << name; + return true; +} + +void ScriptedEffectLoader::queryAndLoadAll() +{ + if (m_queryConnection) { + return; + } + // perform querying for the services in a thread + QFutureWatcher> *watcher = new QFutureWatcher>(this); + m_queryConnection = connect( + watcher, &QFutureWatcher>::finished, this, [this, watcher]() { + const auto effects = watcher->result(); + for (const auto &effect : effects) { + const LoadEffectFlags flags = readConfig(effect.pluginId(), effect.isEnabledByDefault()); + if (flags.testFlag(LoadEffectFlag::Load)) { + m_queue->enqueue(qMakePair(effect, flags)); + } + } + watcher->deleteLater(); + m_queryConnection = QMetaObject::Connection(); + }, + Qt::QueuedConnection); + watcher->setFuture(QtConcurrent::run(&ScriptedEffectLoader::findAllEffects, this)); +} + +QList ScriptedEffectLoader::findAllEffects() const +{ + return KPackage::PackageLoader::self()->listPackages(s_serviceType, QStringLiteral("kwin-wayland/effects")) + + KPackage::PackageLoader::self()->listPackages(s_serviceType, QStringLiteral("kwin/effects")); +} + +KPluginMetaData ScriptedEffectLoader::findEffect(const QString &name) const +{ + auto plugins = KPackage::PackageLoader::self()->findPackages(s_serviceType, QStringLiteral("kwin-wayland/effects"), [name](const KPluginMetaData &metadata) { + return metadata.pluginId().compare(name, Qt::CaseInsensitive) == 0; + }); + if (!plugins.isEmpty()) { + return plugins.first(); + } + plugins = KPackage::PackageLoader::self()->findPackages(s_serviceType, QStringLiteral("kwin/effects"), [name](const KPluginMetaData &metadata) { + return metadata.pluginId().compare(name, Qt::CaseInsensitive) == 0; + }); + if (!plugins.isEmpty()) { + return plugins.first(); + } + return KPluginMetaData(); +} + +void ScriptedEffectLoader::clear() +{ + disconnect(m_queryConnection); + m_queryConnection = QMetaObject::Connection(); + m_queue->clear(); +} + +PluginEffectLoader::PluginEffectLoader(QObject *parent) + : AbstractEffectLoader(parent) + , m_pluginSubDirectory(QStringLiteral("kwin/effects/plugins")) +{ +} + +PluginEffectLoader::~PluginEffectLoader() +{ +} + +bool PluginEffectLoader::hasEffect(const QString &name) const +{ + const auto info = findEffect(name); + return info.isValid(); +} + +KPluginMetaData PluginEffectLoader::findEffect(const QString &name) const +{ + const auto plugins = KPluginMetaData::findPlugins(m_pluginSubDirectory, + [name](const KPluginMetaData &data) { + return data.pluginId().compare(name, Qt::CaseInsensitive) == 0; + }); + if (plugins.isEmpty()) { + return KPluginMetaData(); + } + return plugins.first(); +} + +bool PluginEffectLoader::isEffectSupported(const QString &name) const +{ + if (EffectPluginFactory *effectFactory = factory(findEffect(name))) { + return effectFactory->isSupported(); + } + return false; +} + +EffectPluginFactory *PluginEffectLoader::factory(const KPluginMetaData &info) const +{ + if (!info.isValid()) { + return nullptr; + } + QString error; + KPluginFactory *factory; + if (info.isStaticPlugin()) { + // in case of static plugins we don't need to worry about the versions, because + // they are shipped as part of the kwin executables + const auto result = KPluginFactory::loadFactory(info); + factory = result.plugin; + error = result.errorText; + } else { + QPluginLoader loader(info.fileName()); + if (loader.metaData().value("IID").toString() != QLatin1String(EffectPluginFactory_iid)) { + qCDebug(KWIN_CORE) << info.pluginId() << " has not matching plugin version, expected " << EffectPluginFactory_iid << "got " + << loader.metaData().value("IID"); + return nullptr; + } + factory = qobject_cast(loader.instance()); + if (!factory) { + error = loader.errorString(); + } + } + if (!factory) { + qCWarning(KWIN_CORE).nospace() << "Did not get KPluginFactory for " << info.pluginId() << ':' << error; + return nullptr; + } + return dynamic_cast(factory); +} + +QStringList PluginEffectLoader::listOfKnownEffects() const +{ + const auto plugins = findAllEffects(); + QStringList result; + for (const auto &plugin : plugins) { + result << plugin.pluginId(); + } + qCDebug(KWIN_CORE) << result; + return result; +} + +bool PluginEffectLoader::loadEffect(const QString &name) +{ + const auto info = findEffect(name); + return loadEffect(info, LoadEffectFlag::Load); +} + +bool PluginEffectLoader::loadEffect(const KPluginMetaData &info, LoadEffectFlags flags) +{ + if (!info.isValid()) { + qCDebug(KWIN_CORE) << "Plugin info is not valid"; + return false; + } + const QString name = info.pluginId(); + if (!flags.testFlag(LoadEffectFlag::Load)) { + qCDebug(KWIN_CORE) << "Loading flags disable effect: " << name; + return false; + } + if (m_loadedEffects.contains(name)) { + qCDebug(KWIN_CORE) << name << " already loaded"; + return false; + } + EffectPluginFactory *effectFactory = factory(info); + if (!effectFactory) { + qCDebug(KWIN_CORE) << "Couldn't get an EffectPluginFactory for: " << name; + return false; + } + + effects->makeOpenGLContextCurrent(); // TODO: remove it + if (!effectFactory->isSupported()) { + qCDebug(KWIN_CORE) << "Effect is not supported: " << name; + return false; + } + + if (flags.testFlag(LoadEffectFlag::CheckDefaultFunction)) { + if (!effectFactory->enabledByDefault()) { + qCDebug(KWIN_CORE) << "Enabled by default function disables effect: " << name; + return false; + } + } + + // ok, now we can try to create the Effect + Effect *e = effectFactory->createEffect(); + if (!e) { + qCDebug(KWIN_CORE) << "Failed to create effect: " << name; + return false; + } + // insert in our loaded effects + m_loadedEffects << name; + connect(e, &Effect::destroyed, this, [this, name]() { + m_loadedEffects.removeAll(name); + }); + qCDebug(KWIN_CORE) << "Successfully loaded plugin effect: " << name; + Q_EMIT effectLoaded(e, name); + return true; +} + +void PluginEffectLoader::queryAndLoadAll() +{ + const auto effects = findAllEffects(); + for (const auto &effect : effects) { + const LoadEffectFlags flags = readConfig(effect.pluginId(), effect.isEnabledByDefault()); + if (flags.testFlag(LoadEffectFlag::Load)) { + loadEffect(effect, flags); + } + } +} + +QList PluginEffectLoader::findAllEffects() const +{ + return KPluginMetaData::findPlugins(m_pluginSubDirectory); +} + +void PluginEffectLoader::setPluginSubDirectory(const QString &directory) +{ + m_pluginSubDirectory = directory; +} + +void PluginEffectLoader::clear() +{ +} + +EffectLoader::EffectLoader(QObject *parent) + : AbstractEffectLoader(parent) +{ + m_loaders << new ScriptedEffectLoader(this) + << new PluginEffectLoader(this); + for (auto it = m_loaders.constBegin(); it != m_loaders.constEnd(); ++it) { + connect(*it, &AbstractEffectLoader::effectLoaded, this, &AbstractEffectLoader::effectLoaded); + } +} + +EffectLoader::~EffectLoader() +{ +} + +bool EffectLoader::hasEffect(const QString &name) const +{ + return std::any_of(m_loaders.cbegin(), m_loaders.cend(), [&name](const auto &loader) { + return loader->hasEffect(name); + }); +} + +bool EffectLoader::isEffectSupported(const QString &name) const +{ + return std::any_of(m_loaders.cbegin(), m_loaders.cend(), [&name](const auto &loader) { + return loader->isEffectSupported(name); + }); +} + +QStringList EffectLoader::listOfKnownEffects() const +{ + QStringList result; + for (auto it = m_loaders.constBegin(); it != m_loaders.constEnd(); ++it) { + result << (*it)->listOfKnownEffects(); + } + return result; +} + +bool EffectLoader::loadEffect(const QString &name) +{ + for (auto it = m_loaders.constBegin(); it != m_loaders.constEnd(); ++it) { + if ((*it)->loadEffect(name)) { + return true; + } + } + return false; +} + +void EffectLoader::queryAndLoadAll() +{ + for (auto it = m_loaders.constBegin(); it != m_loaders.constEnd(); ++it) { + (*it)->queryAndLoadAll(); + } +} + +void EffectLoader::setConfig(KSharedConfig::Ptr config) +{ + AbstractEffectLoader::setConfig(config); + for (auto it = m_loaders.constBegin(); it != m_loaders.constEnd(); ++it) { + (*it)->setConfig(config); + } +} + +void EffectLoader::clear() +{ + for (auto it = m_loaders.constBegin(); it != m_loaders.constEnd(); ++it) { + (*it)->clear(); + } +} + +KPluginMetaData EffectLoader::findEffect(const QString &name) const +{ + for (const auto loader : m_loaders) { + if (auto result = loader->findEffect(name); result.isValid()) { + return result; + } + } + return {}; +} + +} // namespace KWin + +#include "moc_effectloader.cpp" diff --git a/local/recipes/kde/kwin/source/src/effect/effectloader.h b/local/recipes/kde/kwin/source/src/effect/effectloader.h new file mode 100644 index 0000000000..47cf5f6aab --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/effectloader.h @@ -0,0 +1,355 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include +// KDE +#include +#include +// Qt +#include +#include +#include +#include +#include +#include + +namespace KWin +{ +class Effect; +class EffectPluginFactory; + +/** + * @brief Flags defining how a Loader should load an Effect. + * + * These Flags are only used internally when querying the configuration on whether + * an Effect should be loaded. + * + * @see AbstractEffectLoader::readConfig() + */ +enum class LoadEffectFlag { + Load = 1 << 0, ///< Effect should be loaded + CheckDefaultFunction = 1 << 2 ///< The Check Default Function needs to be invoked if the Effect provides it +}; +Q_DECLARE_FLAGS(LoadEffectFlags, LoadEffectFlag) + +/** + * @brief Interface to describe how an effect loader has to function. + * + * The AbstractEffectLoader specifies the methods a concrete loader has to implement and how + * those methods are expected to perform. Also it provides an interface to the outside world + * (that is EffectsHandler). + * + * The abstraction is used because there are multiple types of Effects which need to be loaded: + * @li Built-In Effects + * @li Scripted Effects + * @li Binary Plugin Effects + * + * Serving all of them with one Effect Loader is rather complex given that different stores need + * to be queried at the same time. Thus the idea is to have one implementation per type and one + * implementation which makes use of all of them and combines the loading. + */ +class KWIN_EXPORT AbstractEffectLoader : public QObject +{ + Q_OBJECT +public: + ~AbstractEffectLoader() override; + + /** + * @brief The KSharedConfig this EffectLoader should operate on. + * + * Important: a valid KSharedConfig must be provided before trying to load any effects! + * + * @param config + * @internal + */ + virtual void setConfig(KSharedConfig::Ptr config); + + /** + * @brief Whether this Effect Loader can load the Effect with the given @p name. + * + * The Effect Loader determines whether it knows or can find an Effect called @p name, + * and thus whether it can attempt to load the Effect. + * + * @param name The name of the Effect to look for. + * @return bool @c true if the Effect Loader knows this effect, false otherwise + */ + virtual bool hasEffect(const QString &name) const = 0; + + /** + * @brief All the Effects this loader knows of. + * + * The implementation should re-query its store whenever this method is invoked. + * It's possible that the store of effects changed (e.g. a new one got installed) + * + * @return QStringList The internal names of the known Effects + */ + virtual QStringList listOfKnownEffects() const = 0; + + /** + * @brief Synchronous loading of the Effect with the given @p name. + * + * Loads the Effect without checking any configuration value or any enabled by default + * function provided by the Effect. + * + * The loader is expected to apply the following checks: + * If the Effect is already loaded, the Effect should not get loaded again. Thus the loader + * is expected to track which Effects it has loaded, and which of those have been destroyed. + * The loader should check whether the Effect is supported. If the Effect indicates it is + * not supported, it should not get loaded. + * + * If the Effect loaded successfully the signal effectLoaded(KWin::Effect*,const QString&) + * must be emitted. Otherwise the user of the loader is not able to get the loaded Effect. + * It's not returning the Effect as queryAndLoadAll() is working async and thus the users + * of the loader are expected to be prepared for async loading. + * + * @param name The internal name of the Effect which should be loaded + * @return bool @c true if the effect could be loaded, @c false in error case + * @see queryAndLoadAll() + * @see effectLoaded(KWin::Effect*,const QString&) + */ + virtual bool loadEffect(const QString &name) = 0; + + /** + * @brief The Effect Loader should query its store for all available effects and try to load them. + * + * The Effect Loader is supposed to perform this operation in a highly async way. If there is + * IO which needs to be performed this should be done in a background thread and a queue should + * be used to load the effects. The loader should make sure to not load more than one Effect + * in one event cycle. Loading the Effect has to be performed in the Compositor thread and + * thus blocks the Compositor. Therefore after loading one Effect all events should get + * processed first, so that the Compositor can perform a painting pass if needed. To simplify + * this operation one can use the EffectLoadQueue. This requires to add another loadEffect + * method with the custom loader specific type to refer to an Effect and LoadEffectFlags. + * + * The LoadEffectFlags have to be determined by querying the configuration with readConfig(). + * If the Load flag is set the loading can proceed and all the checks from + * loadEffect(const QString &) have to be applied. + * In addition if the CheckDefaultFunction flag is set and the Effect provides such a method, + * it should be queried to determine whether the Effect is enabled by default. If such a method + * returns @c false the Effect should not get loaded. If the Effect does not provide a way to + * query whether it's enabled by default at runtime the flag can get ignored. + * + * If the Effect loaded successfully the signal effectLoaded(KWin::Effect*,const QString&) + * must be emitted. + * + * @see loadEffect(const QString &) + * @see effectLoaded(KWin::Effect*,const QString&) + */ + virtual void queryAndLoadAll() = 0; + + /** + * @brief Whether the Effect with the given @p name is supported by the compositing backend. + * + * @param name The name of the Effect to check. + * @return bool @c true if it is supported, @c false otherwise + */ + virtual bool isEffectSupported(const QString &name) const = 0; + + /** + * @brief Clears the load queue, that is all scheduled Effects are discarded from loading. + */ + virtual void clear() = 0; + + /** + * @brief Finds the effect with a given @p name. + * + * If not found an invalid KPluginMetaData object is returned. + */ + virtual KPluginMetaData findEffect(const QString &name) const = 0; + + /** + * @brief Checks the configuration for the Effect identified by @p effectName. + * + * For each Effect there could be a key called "Enabled". If there is such a key + * the returned flags will contain Load in case it's @c true. If the key does not exist the + * @p defaultValue determines whether the Effect should be loaded. A value of @c true means + * that Load | CheckDefaultFunction is returned, in case of @c false no Load flags are returned. + * + * @param effectName The name of the Effect to look for in the configuration + * @param defaultValue Whether the Effect is enabled by default or not. + * @returns Flags indicating whether the Effect should be loaded and how it should be loaded + */ + LoadEffectFlags readConfig(const QString &effectName, bool defaultValue) const; + +Q_SIGNALS: + /** + * @brief The loader emits this signal when it successfully loaded an effect. + * + * @param effect The created Effect + * @param name The internal name of the loaded Effect + * @return void + */ + void effectLoaded(KWin::Effect *effect, const QString &name); + +protected: + explicit AbstractEffectLoader(QObject *parent = nullptr); + +private: + KSharedConfig::Ptr m_config; +}; + +template +class EffectLoadQueue; +/** + * @brief Helper class to queue the loading of Effects. + * + * Loading an Effect has to be done in the compositor thread and thus the Compositor is blocked + * while the Effect loads. To not block the compositor for several frames the loading of all + * Effects need to be queued. By invoking the slot dequeue() through a QueuedConnection the queue + * can ensure that events are processed between the loading of two Effects and thus the compositor + * doesn't block. + * + * As it needs to be a slot, the queue must subclass QObject, but it also needs to be templated as + * the information to load an Effect is specific to the Effect Loader. Thus there is the + * AbstractEffectLoadQueue providing the slots as pure virtual functions and the templated + * EffectLoadQueue inheriting from AbstractEffectLoadQueue. + * + * The queue operates like a normal queue providing enqueue and a scheduleDequeue instead of dequeue. + * + */ +class AbstractEffectLoadQueue : public QObject +{ + Q_OBJECT +public: + explicit AbstractEffectLoadQueue(QObject *parent = nullptr) + : QObject(parent) + { + } +protected Q_SLOTS: + virtual void dequeue() = 0; + +private: + template + friend class EffectLoadQueue; +}; + +template +class EffectLoadQueue : public AbstractEffectLoadQueue +{ +public: + explicit EffectLoadQueue(Loader *parent) + : AbstractEffectLoadQueue(parent) + , m_effectLoader(parent) + , m_dequeueScheduled(false) + { + } + void enqueue(const QPair value) + { + m_queue.enqueue(value); + scheduleDequeue(); + } + void clear() + { + m_queue.clear(); + m_dequeueScheduled = false; + } + +protected: + void dequeue() override + { + if (m_queue.isEmpty()) { + return; + } + m_dequeueScheduled = false; + const auto pair = m_queue.dequeue(); + m_effectLoader->loadEffect(pair.first, pair.second); + scheduleDequeue(); + } + +private: + void scheduleDequeue() + { + if (m_queue.isEmpty() || m_dequeueScheduled) { + return; + } + m_dequeueScheduled = true; + QMetaObject::invokeMethod(this, &AbstractEffectLoadQueue::dequeue, Qt::QueuedConnection); + } + Loader *m_effectLoader; + bool m_dequeueScheduled; + QQueue> m_queue; +}; + +/** + * @brief Can load scripted Effects + */ +class KWIN_EXPORT ScriptedEffectLoader : public AbstractEffectLoader +{ + Q_OBJECT +public: + explicit ScriptedEffectLoader(QObject *parent = nullptr); + ~ScriptedEffectLoader() override; + + bool hasEffect(const QString &name) const override; + bool isEffectSupported(const QString &name) const override; + QStringList listOfKnownEffects() const override; + + void clear() override; + void queryAndLoadAll() override; + bool loadEffect(const QString &name) override; + KPluginMetaData findEffect(const QString &name) const override; + bool loadEffect(const KPluginMetaData &effect, LoadEffectFlags flags); + +private: + QList findAllEffects() const; + bool loadJavascriptEffect(const KPluginMetaData &effect); + bool loadDeclarativeEffect(const KPluginMetaData &effect); + + QStringList m_loadedEffects; + EffectLoadQueue *m_queue; + QMetaObject::Connection m_queryConnection; +}; + +class PluginEffectLoader : public AbstractEffectLoader +{ + Q_OBJECT +public: + explicit PluginEffectLoader(QObject *parent = nullptr); + ~PluginEffectLoader() override; + + bool hasEffect(const QString &name) const override; + bool isEffectSupported(const QString &name) const override; + QStringList listOfKnownEffects() const override; + + void clear() override; + void queryAndLoadAll() override; + bool loadEffect(const QString &name) override; + bool loadEffect(const KPluginMetaData &info, LoadEffectFlags flags); + KPluginMetaData findEffect(const QString &name) const override; + + void setPluginSubDirectory(const QString &directory); + +private: + QList findAllEffects() const; + EffectPluginFactory *factory(const KPluginMetaData &info) const; + QStringList m_loadedEffects; + QString m_pluginSubDirectory; +}; + +class KWIN_EXPORT EffectLoader : public AbstractEffectLoader +{ + Q_OBJECT +public: + explicit EffectLoader(QObject *parent = nullptr); + ~EffectLoader() override; + bool hasEffect(const QString &name) const override; + bool isEffectSupported(const QString &name) const override; + QStringList listOfKnownEffects() const override; + bool loadEffect(const QString &name) override; + void queryAndLoadAll() override; + void setConfig(KSharedConfig::Ptr config) override; + void clear() override; + KPluginMetaData findEffect(const QString &name) const override; + +private: + QList m_loaders; +}; + +} +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::LoadEffectFlags) diff --git a/local/recipes/kde/kwin/source/src/effect/effecttogglablestate.cpp b/local/recipes/kde/kwin/source/src/effect/effecttogglablestate.cpp new file mode 100644 index 0000000000..01c02ecab7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/effecttogglablestate.cpp @@ -0,0 +1,263 @@ +/* + SPDX-FileCopyrightText: 2023 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "effect/effecttogglablestate.h" +#include "effect/effecthandler.h" + +namespace KWin +{ + +EffectTogglableState::EffectTogglableState(Effect *effect) + : QObject(effect) + , m_deactivateAction(std::make_unique()) + , m_activateAction(std::make_unique()) + , m_toggleAction(std::make_unique()) +{ + connect(m_activateAction.get(), &QAction::triggered, this, [this]() { + if (m_status == Status::Activating) { + if (m_partialActivationFactor > 0.5) { + activate(); + Q_EMIT activated(); + } else { + deactivate(); + Q_EMIT deactivated(); + } + } + }); + connect(m_deactivateAction.get(), &QAction::triggered, this, [this]() { + if (m_status == Status::Deactivating) { + if (m_partialActivationFactor < 0.5) { + deactivate(); + Q_EMIT deactivated(); + } else { + activate(); + Q_EMIT activated(); + } + } + }); + connect(m_toggleAction.get(), &QAction::triggered, this, &EffectTogglableState::toggle); +} + +void EffectTogglableState::activate() +{ + setInProgress(false); + setPartialActivationFactor(1.0); + setStatus(Status::Active); +} + +void EffectTogglableState::setPartialActivationFactor(qreal factor) +{ + if (m_partialActivationFactor != factor) { + m_partialActivationFactor = factor; + Q_EMIT partialActivationFactorChanged(); + } +} + +void EffectTogglableState::deactivate() +{ + setInProgress(false); + setPartialActivationFactor(0.0); + setStatus(Status::Inactive); +} + +void EffectTogglableState::stop() +{ + setInProgress(false); + setPartialActivationFactor(0.0); + setStatus(Status::Stopped); +} + +bool EffectTogglableState::inProgress() const +{ + return m_inProgress; +} + +void EffectTogglableState::setInProgress(bool gesture) +{ + if (m_inProgress != gesture) { + m_inProgress = gesture; + Q_EMIT inProgressChanged(); + } +} + +void EffectTogglableState::setStatus(Status status) +{ + if (m_status != status) { + m_status = status; + Q_EMIT statusChanged(status); + } +} + +void EffectTogglableState::partialActivate(qreal factor) +{ + if (effects->isScreenLocked()) { + return; + } + + setStatus(Status::Activating); + setInProgress(true); + setPartialActivationFactor(factor); +} + +void EffectTogglableState::partialDeactivate(qreal factor) +{ + setStatus(Status::Deactivating); + setInProgress(true); + setPartialActivationFactor(1.0 - factor); +} + +void EffectTogglableState::toggle() +{ + if (m_status != Status::Active) { + activate(); + Q_EMIT activated(); + } else { + deactivate(); + Q_EMIT deactivated(); + } +} + +void EffectTogglableState::setProgress(qreal progress) +{ + if (m_status == Status::Stopped) { + return; + } + if (!effects->hasActiveFullScreenEffect() || effects->activeFullScreenEffect() == parent()) { + switch (m_status) { + case Status::Inactive: + case Status::Activating: + partialActivate(progress); + break; + default: + break; + } + } +} + +void EffectTogglableState::setRegress(qreal regress) +{ + if (m_status == Status::Stopped) { + return; + } + if (!effects->hasActiveFullScreenEffect() || effects->activeFullScreenEffect() == parent()) { + switch (m_status) { + case Status::Active: + case Status::Deactivating: + partialDeactivate(regress); + break; + default: + break; + } + } +} + +EffectTogglableGesture::EffectTogglableGesture(EffectTogglableState *state) + : QObject(state) + , m_state(state) +{ +} + +static PinchDirection opposite(PinchDirection direction) +{ + switch (direction) { + case PinchDirection::Contracting: + return PinchDirection::Expanding; + case PinchDirection::Expanding: + return PinchDirection::Contracting; + } + return PinchDirection::Expanding; +} + +static SwipeDirection opposite(SwipeDirection direction) +{ + switch (direction) { + case SwipeDirection::Invalid: + return SwipeDirection::Invalid; + case SwipeDirection::Down: + return SwipeDirection::Up; + case SwipeDirection::Up: + return SwipeDirection::Down; + case SwipeDirection::Left: + return SwipeDirection::Right; + case SwipeDirection::Right: + return SwipeDirection::Left; + } + return SwipeDirection::Invalid; +} + +std::function EffectTogglableState::progressCallback() +{ + return [this](qreal progress) { + setProgress(progress); + }; +} + +std::function EffectTogglableState::regressCallback() +{ + return [this](qreal progress) { + setRegress(progress); + }; +} + +void EffectTogglableGesture::addTouchpadPinchGesture(PinchDirection direction, uint fingerCount) +{ + effects->registerTouchpadPinchShortcut(direction, fingerCount, m_state->activateAction(), m_state->progressCallback()); + effects->registerTouchpadPinchShortcut(opposite(direction), fingerCount, m_state->deactivateAction(), m_state->regressCallback()); +} + +void EffectTogglableGesture::addTouchpadSwipeGesture(SwipeDirection direction, uint fingerCount) +{ + effects->registerTouchpadSwipeShortcut(direction, fingerCount, m_state->activateAction(), m_state->progressCallback()); + effects->registerTouchpadSwipeShortcut(opposite(direction), fingerCount, m_state->deactivateAction(), m_state->regressCallback()); +} + +void EffectTogglableGesture::addTouchscreenSwipeGesture(SwipeDirection direction, uint fingerCount) +{ + effects->registerTouchscreenSwipeShortcut(direction, fingerCount, m_state->activateAction(), m_state->progressCallback()); + effects->registerTouchscreenSwipeShortcut(opposite(direction), fingerCount, m_state->deactivateAction(), m_state->regressCallback()); +} + +EffectTogglableTouchBorder::EffectTogglableTouchBorder(EffectTogglableState *state) + : QObject(state) + , m_state(state) +{ +} + +EffectTogglableTouchBorder::~EffectTogglableTouchBorder() +{ + for (const ElectricBorder &border : std::as_const(m_touchBorderActivate)) { + effects->unregisterTouchBorder(border, m_state->activateAction()); + } +} + +void EffectTogglableTouchBorder::setBorders(const QList &touchActivateBorders) +{ + for (const ElectricBorder &border : std::as_const(m_touchBorderActivate)) { + effects->unregisterTouchBorder(border, m_state->activateAction()); + } + m_touchBorderActivate.clear(); + + for (const int &border : touchActivateBorders) { + m_touchBorderActivate.append(ElectricBorder(border)); + effects->registerRealtimeTouchBorder(ElectricBorder(border), m_state->activateAction(), [this](ElectricBorder border, const QPointF &deltaProgress, const LogicalOutput *screen) { + if (m_state->status() == EffectTogglableState::Status::Active) { + return; + } + const int maxDelta = 500; // Arbitrary logical pixels value seems to behave better than scaledScreenSize + qreal progress = 0; + if (border == ElectricTop || border == ElectricBottom) { + progress = std::min(1.0, std::abs(deltaProgress.y()) / maxDelta); + } else { + progress = std::min(1.0, std::abs(deltaProgress.x()) / maxDelta); + } + m_state->setProgress(progress); + }); + } +} + +} + +#include "moc_effecttogglablestate.cpp" diff --git a/local/recipes/kde/kwin/source/src/effect/effecttogglablestate.h b/local/recipes/kde/kwin/source/src/effect/effecttogglablestate.h new file mode 100644 index 0000000000..6e2c505d14 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/effecttogglablestate.h @@ -0,0 +1,140 @@ +/* + SPDX-FileCopyrightText: 2023 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/globals.h" + +#include +#include + +namespace KWin +{ + +class Effect; + +/** + * It's common to have effects that get activated and deactivated. + * This class helps us simplify this process, especially in the cases where we want activation to happen + * progressively, like through a touch our touchpad events. + */ +class KWIN_EXPORT EffectTogglableState : public QObject +{ + Q_OBJECT +public: + enum class Status { + Inactive, + Activating, + Deactivating, + Active, + Stopped + }; + Q_ENUM(Status) + + /** Constructs the object, passes the effect as the parent. */ + EffectTogglableState(Effect *parent); + + bool inProgress() const; + void setInProgress(bool gesture); + + qreal partialActivationFactor() const + { + return m_partialActivationFactor; + } + void setPartialActivationFactor(qreal factor); + + QAction *activateAction() const + { + return m_activateAction.get(); + } + QAction *deactivateAction() const + { + return m_deactivateAction.get(); + } + QAction *toggleAction() const + { + return m_toggleAction.get(); + } + + void activate(); + void deactivate(); + void toggle(); + void stop(); + void setStatus(Status status); + Status status() const + { + return m_status; + } + +Q_SIGNALS: + void inProgressChanged(); + void partialActivationFactorChanged(); + void activated(); + void deactivated(); + void statusChanged(Status status); + +protected: + std::function progressCallback(); + std::function regressCallback(); + void setProgress(qreal progress); + + /// regress being the progress when on an active state + void setRegress(qreal regress); + +private: + void partialActivate(qreal factor); + void partialDeactivate(qreal factor); + + std::unique_ptr m_deactivateAction; + std::unique_ptr m_activateAction; + std::unique_ptr m_toggleAction; + Status m_status = Status::Inactive; + bool m_inProgress = false; + qreal m_partialActivationFactor = 0; + + friend class EffectTogglableGesture; + friend class EffectTogglableTouchBorder; +}; + +class KWIN_EXPORT EffectTogglableGesture : public QObject +{ +public: + /** + * Allows specifying which gestures toggle the state. + * + * The gesture will activate it and once enabled the opposite will disable it back. + * + * @param state the state we care about. This state will become the parent object and will take care to clean it up. + */ + EffectTogglableGesture(EffectTogglableState *state); + + void addTouchpadPinchGesture(PinchDirection dir, uint fingerCount); + void addTouchpadSwipeGesture(SwipeDirection dir, uint fingerCount); + void addTouchscreenSwipeGesture(SwipeDirection direction, uint fingerCount); + +private: + EffectTogglableState *const m_state; +}; + +class KWIN_EXPORT EffectTogglableTouchBorder : public QObject +{ +public: + /** + * Allows specifying which boarders get to toggle the state. + * + * @param state the state we care about. This state will become the parent object and will take care to clean it up. + */ + EffectTogglableTouchBorder(EffectTogglableState *state); + ~EffectTogglableTouchBorder(); + + void setBorders(const QList &borders); + +private: + QList m_touchBorderActivate; + EffectTogglableState *const m_state; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/effect/effectwindow.cpp b/local/recipes/kde/kwin/source/src/effect/effectwindow.cpp new file mode 100644 index 0000000000..824fb44d9e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/effectwindow.cpp @@ -0,0 +1,529 @@ +/* + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "effect/effectwindow.h" +#include "core/output.h" +#include "effect/effecthandler.h" +#include "internalwindow.h" +#include "scene/windowitem.h" +#include "virtualdesktops.h" +#include "waylandwindow.h" + +#if KWIN_BUILD_X11 +#include "group.h" +#include "x11window.h" +#endif + +#include + +namespace KWin +{ + +class Q_DECL_HIDDEN EffectWindow::Private +{ +public: + Private(EffectWindow *q, WindowItem *windowItem); + + EffectWindow *q; + Window *m_window; + WindowItem *m_windowItem; // This one is used only during paint pass. + QHash dataMap; + bool managed = false; + bool m_waylandWindow; + bool m_x11Window; +}; + +EffectWindow::Private::Private(EffectWindow *q, WindowItem *windowItem) + : q(q) + , m_window(windowItem->window()) + , m_windowItem(windowItem) +{ +} + +EffectWindow::EffectWindow(WindowItem *windowItem) + : d(new Private(this, windowItem)) +{ + QJSEngine::setObjectOwnership(this, QJSEngine::CppOwnership); + + // Deleted windows are not managed. So, when windowClosed signal is + // emitted, effects can't distinguish managed windows from unmanaged + // windows(e.g. combo box popups, popup menus, etc). Save value of the + // managed property during construction of EffectWindow. At that time, + // parent can be Client, XdgShellClient, or Unmanaged. So, later on, when + // an instance of Deleted becomes parent of the EffectWindow, effects + // can still figure out whether it is/was a managed window. + d->managed = d->m_window->isClient(); + + d->m_waylandWindow = qobject_cast(d->m_window) != nullptr; +#if KWIN_BUILD_X11 + d->m_x11Window = qobject_cast(d->m_window) != nullptr; +#else + d->m_x11Window = false; +#endif + + connect(d->m_window, &Window::hiddenChanged, this, [this]() { + Q_EMIT windowHiddenChanged(this); + }); + connect(d->m_window, &Window::maximizedChanged, this, [this]() { + const MaximizeMode mode = d->m_window->maximizeMode(); + Q_EMIT windowMaximizedStateChanged(this, mode & MaximizeHorizontal, mode & MaximizeVertical); + }); + connect(d->m_window, &Window::maximizedAboutToChange, this, [this](MaximizeMode m) { + Q_EMIT windowMaximizedStateAboutToChange(this, m & MaximizeHorizontal, m & MaximizeVertical); + }); + connect(d->m_window, &Window::frameGeometryAboutToChange, this, [this]() { + Q_EMIT windowFrameGeometryAboutToChange(this); + }); + connect(d->m_window, &Window::interactiveMoveResizeStarted, this, [this]() { + Q_EMIT windowStartUserMovedResized(this); + }); + connect(d->m_window, &Window::interactiveMoveResizeStepped, this, [this](const QRectF &geometry) { + Q_EMIT windowStepUserMovedResized(this, geometry); + }); + connect(d->m_window, &Window::interactiveMoveResizeFinished, this, [this]() { + Q_EMIT windowFinishUserMovedResized(this); + }); + connect(d->m_window, &Window::opacityChanged, this, [this](Window *window, qreal oldOpacity) { + Q_EMIT windowOpacityChanged(this, oldOpacity, window->opacity()); + }); + connect(d->m_window, &Window::minimizedChanged, this, [this]() { + Q_EMIT minimizedChanged(this); + }); + connect(d->m_window, &Window::modalChanged, this, [this]() { + Q_EMIT windowModalityChanged(this); + }); + connect(d->m_window, &Window::frameGeometryChanged, this, [this](const QRectF &oldGeometry) { + Q_EMIT windowFrameGeometryChanged(this, oldGeometry); + }); + connect(d->m_window, &Window::damaged, this, [this]() { + Q_EMIT windowDamaged(this); + }); + connect(d->m_window, &Window::unresponsiveChanged, this, [this](bool unresponsive) { + Q_EMIT windowUnresponsiveChanged(this, unresponsive); + }); + connect(d->m_window, &Window::keepAboveChanged, this, [this]() { + Q_EMIT windowKeepAboveChanged(this); + }); + connect(d->m_window, &Window::keepBelowChanged, this, [this]() { + Q_EMIT windowKeepBelowChanged(this); + }); + connect(d->m_window, &Window::fullScreenChanged, this, [this]() { + Q_EMIT windowFullScreenChanged(this); + }); + connect(d->m_window, &Window::visibleGeometryChanged, this, [this]() { + Q_EMIT windowExpandedGeometryChanged(this); + }); + connect(d->m_window, &Window::decorationChanged, this, [this]() { + Q_EMIT windowDecorationChanged(this); + }); + connect(d->m_window, &Window::desktopsChanged, this, [this]() { + Q_EMIT windowDesktopsChanged(this); + }); +} + +EffectWindow::~EffectWindow() +{ +} + +Window *EffectWindow::window() const +{ + return d->m_window; +} + +WindowItem *EffectWindow::windowItem() const +{ + return d->m_windowItem; +} + +bool EffectWindow::isOnActivity(const QString &activity) const +{ + const QStringList _activities = activities(); + return _activities.isEmpty() || _activities.contains(activity); +} + +bool EffectWindow::isOnAllActivities() const +{ + return activities().isEmpty(); +} + +void EffectWindow::setMinimized(bool min) +{ + if (min) { + minimize(); + } else { + unminimize(); + } +} + +bool EffectWindow::isOnCurrentActivity() const +{ + return isOnActivity(effects->currentActivity()); +} + +bool EffectWindow::isOnCurrentDesktop() const +{ + return isOnDesktop(effects->currentDesktop()); +} + +bool EffectWindow::isOnDesktop(VirtualDesktop *desktop) const +{ + const QList ds = desktops(); + return ds.isEmpty() || ds.contains(desktop); +} + +bool EffectWindow::isOnAllDesktops() const +{ + return desktops().isEmpty(); +} + +bool EffectWindow::hasDecoration() const +{ + return d->m_window->decoration(); +} + +bool EffectWindow::isVisible() const +{ + return !isMinimized() + && isOnCurrentDesktop() + && isOnCurrentActivity(); +} + +void EffectWindow::refVisible(const EffectWindowVisibleRef *holder) +{ + d->m_windowItem->refVisible(holder->reason()); +} + +void EffectWindow::unrefVisible(const EffectWindowVisibleRef *holder) +{ + d->m_windowItem->unrefVisible(holder->reason()); +} + +void EffectWindow::addRepaint(const Rect &r) +{ + d->m_windowItem->scheduleRepaint(Region(r)); +} + +void EffectWindow::addRepaintFull() +{ + d->m_windowItem->scheduleRepaint(d->m_windowItem->boundingRect()); +} + +void EffectWindow::addLayerRepaint(const Rect &r) +{ + d->m_windowItem->scheduleRepaint(d->m_windowItem->mapFromScene(r)); +} + +const EffectWindowGroup *EffectWindow::group() const +{ +#if KWIN_BUILD_X11 + if (Group *group = d->m_window->group()) { + return group->effectGroup(); + } +#endif + return nullptr; +} + +void EffectWindow::refWindow() +{ + d->m_window->ref(); +} + +void EffectWindow::unrefWindow() +{ + d->m_window->unref(); +} + +LogicalOutput *EffectWindow::screen() const +{ + return d->m_window->output(); +} + +#define WINDOW_HELPER(rettype, prototype, toplevelPrototype) \ + rettype EffectWindow::prototype() const \ + { \ + return d->m_window->toplevelPrototype(); \ + } + +WINDOW_HELPER(double, opacity, opacity) +WINDOW_HELPER(qreal, x, x) +WINDOW_HELPER(qreal, y, y) +WINDOW_HELPER(qreal, width, width) +WINDOW_HELPER(qreal, height, height) +WINDOW_HELPER(QPointF, pos, pos) +WINDOW_HELPER(QSizeF, size, size) +WINDOW_HELPER(QRectF, frameGeometry, frameGeometry) +WINDOW_HELPER(QRectF, bufferGeometry, bufferGeometry) +WINDOW_HELPER(QRectF, clientGeometry, clientGeometry) +WINDOW_HELPER(QRectF, expandedGeometry, visibleGeometry) +WINDOW_HELPER(QRectF, rect, rect) +WINDOW_HELPER(bool, isDesktop, isDesktop) +WINDOW_HELPER(bool, isDock, isDock) +WINDOW_HELPER(bool, isToolbar, isToolbar) +WINDOW_HELPER(bool, isMenu, isMenu) +WINDOW_HELPER(bool, isNormalWindow, isNormalWindow) +WINDOW_HELPER(bool, isDialog, isDialog) +WINDOW_HELPER(bool, isSplash, isSplash) +WINDOW_HELPER(bool, isUtility, isUtility) +WINDOW_HELPER(bool, isDropdownMenu, isDropdownMenu) +WINDOW_HELPER(bool, isPopupMenu, isPopupMenu) +WINDOW_HELPER(bool, isTooltip, isTooltip) +WINDOW_HELPER(bool, isNotification, isNotification) +WINDOW_HELPER(bool, isCriticalNotification, isCriticalNotification) +WINDOW_HELPER(bool, isAppletPopup, isAppletPopup) +WINDOW_HELPER(bool, isOnScreenDisplay, isOnScreenDisplay) +WINDOW_HELPER(bool, isComboBox, isComboBox) +WINDOW_HELPER(bool, isDNDIcon, isDNDIcon) +WINDOW_HELPER(bool, isDeleted, isDeleted) +WINDOW_HELPER(QString, windowRole, windowRole) +WINDOW_HELPER(QString, tag, tag) +WINDOW_HELPER(QStringList, activities, activities) +WINDOW_HELPER(bool, skipsCloseAnimation, skipsCloseAnimation) +WINDOW_HELPER(SurfaceInterface *, surface, surface) +WINDOW_HELPER(bool, isPopupWindow, isPopupWindow) +WINDOW_HELPER(bool, isOutline, isOutline) +WINDOW_HELPER(bool, isLockScreen, isLockScreen) +WINDOW_HELPER(pid_t, pid, pid) +WINDOW_HELPER(QUuid, internalId, internalId) +WINDOW_HELPER(bool, isMinimized, isMinimized) +WINDOW_HELPER(bool, isHidden, isHidden) +WINDOW_HELPER(bool, isHiddenByShowDesktop, isHiddenByShowDesktop) +WINDOW_HELPER(bool, isModal, isModal) +WINDOW_HELPER(bool, isFullScreen, isFullScreen) +WINDOW_HELPER(bool, keepAbove, keepAbove) +WINDOW_HELPER(bool, keepBelow, keepBelow) +WINDOW_HELPER(QString, caption, caption) +WINDOW_HELPER(bool, isMovable, isMovable) +WINDOW_HELPER(bool, isMovableAcrossScreens, isMovableAcrossScreens) +WINDOW_HELPER(bool, isUserMove, isInteractiveMove) +WINDOW_HELPER(bool, isUserResize, isInteractiveResize) +WINDOW_HELPER(QRectF, iconGeometry, iconGeometry) +WINDOW_HELPER(bool, isSpecialWindow, isSpecialWindow) +WINDOW_HELPER(bool, acceptsFocus, wantsInput) +WINDOW_HELPER(QIcon, icon, icon) +WINDOW_HELPER(bool, isSkipSwitcher, skipSwitcher) +WINDOW_HELPER(bool, decorationHasAlpha, decorationHasAlpha) +WINDOW_HELPER(bool, isUnresponsive, unresponsive) +WINDOW_HELPER(QList, desktops, desktops) +WINDOW_HELPER(bool, isInputMethod, isInputMethod) + +#undef WINDOW_HELPER + +qlonglong EffectWindow::windowId() const +{ +#if KWIN_BUILD_X11 + if (X11Window *x11Window = qobject_cast(d->m_window)) { + return x11Window->window(); + } +#endif + return 0; +} + +QString EffectWindow::windowClass() const +{ + return d->m_window->resourceName() + QLatin1Char(' ') + d->m_window->resourceClass(); +} + +QRectF EffectWindow::contentsRect() const +{ + return d->m_window->clientGeometry().translated(-d->m_window->frameGeometry().topLeft()); +} + +WindowType EffectWindow::windowType() const +{ + return d->m_window->windowType(); +} + +QSizeF EffectWindow::basicUnit() const +{ +#if KWIN_BUILD_X11 + if (auto window = qobject_cast(d->m_window)) { + return window->basicUnit(); + } +#endif + return QSize(1, 1); +} + +KDecoration3::Decoration *EffectWindow::decoration() const +{ + return d->m_window->decoration(); +} + +QByteArray EffectWindow::readProperty(long atom, long type, int format) const +{ +#if KWIN_BUILD_X11 + auto x11Window = qobject_cast(d->m_window); + if (!x11Window) { + return QByteArray(); + } + if (!kwinApp()->x11Connection()) { + return QByteArray(); + } + uint32_t len = 32768; + for (;;) { + Xcb::Property prop(false, x11Window->window(), atom, XCB_ATOM_ANY, 0, len); + if (prop.isNull()) { + // get property failed + return QByteArray(); + } + if (prop->bytes_after > 0) { + len *= 2; + continue; + } + return prop.toByteArray(format, type).value_or(QByteArray()); + } +#endif + return {}; +} + +void EffectWindow::deleteProperty(long int atom) const +{ +#if KWIN_BUILD_X11 + auto x11Window = qobject_cast(d->m_window); + if (!x11Window) { + return; + } + if (!kwinApp()->x11Connection()) { + return; + } + xcb_delete_property(kwinApp()->x11Connection(), x11Window->window(), atom); +#endif +} + +EffectWindow *EffectWindow::findModal() +{ + Window *modal = d->m_window->findModal(); + if (modal) { + return modal->effectWindow(); + } + + return nullptr; +} + +EffectWindow *EffectWindow::transientFor() +{ + Window *transientFor = d->m_window->transientFor(); + if (transientFor) { + return transientFor->effectWindow(); + } + + return nullptr; +} + +QWindow *EffectWindow::internalWindow() const +{ + if (auto window = qobject_cast(d->m_window)) { + return window->handle(); + } + return nullptr; +} + +template +QList getMainWindows(T *c) +{ + const auto mainwindows = c->mainWindows(); + QList ret; + ret.reserve(mainwindows.size()); + std::transform(std::cbegin(mainwindows), std::cend(mainwindows), + std::back_inserter(ret), + [](auto window) { + return window->effectWindow(); + }); + return ret; +} + +QList EffectWindow::mainWindows() const +{ + return getMainWindows(d->m_window); +} + +void EffectWindow::setData(int role, const QVariant &data) +{ + if (!data.isNull()) { + d->dataMap[role] = data; + } else { + d->dataMap.remove(role); + } + Q_EMIT effects->windowDataChanged(this, role); +} + +QVariant EffectWindow::data(int role) const +{ + return d->dataMap.value(role); +} + +void EffectWindow::elevate(bool elevate) +{ + effects->setElevatedWindow(this, elevate); +} + +void EffectWindow::minimize() +{ + if (d->m_window->isClient()) { + d->m_window->setMinimized(true); + } +} + +void EffectWindow::unminimize() +{ + if (d->m_window->isClient()) { + d->m_window->setMinimized(false); + } +} + +void EffectWindow::closeWindow() +{ + if (d->m_window->isClient()) { + d->m_window->closeWindow(); + } +} + +bool EffectWindow::isManaged() const +{ + return d->managed; +} + +bool EffectWindow::isWaylandClient() const +{ + return d->m_waylandWindow; +} + +bool EffectWindow::isX11Client() const +{ + return d->m_x11Window; +} + +//**************************************** +// EffectWindowGroup +//**************************************** + +EffectWindowGroup::EffectWindowGroup(Group *group) + : m_group(group) +{ +} + +EffectWindowGroup::~EffectWindowGroup() +{ +} + +QList EffectWindowGroup::members() const +{ + QList ret; +#if KWIN_BUILD_X11 + const auto memberList = m_group->members(); + ret.reserve(memberList.size()); + std::transform(std::cbegin(memberList), std::cend(memberList), std::back_inserter(ret), [](auto window) { + return window->effectWindow(); + }); +#endif + return ret; +} + +} // namespace KWin + +#include "moc_effectwindow.cpp" diff --git a/local/recipes/kde/kwin/source/src/effect/effectwindow.h b/local/recipes/kde/kwin/source/src/effect/effectwindow.h new file mode 100644 index 0000000000..015b0f9798 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/effectwindow.h @@ -0,0 +1,991 @@ +/* + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/output.h" +#include "kwin_export.h" + +#include "globals.h" + +#include +#include + +class QWindow; + +namespace KDecoration3 +{ +class Decoration; +} + +namespace KWin +{ + +class EffectWindowGroup; +class EffectWindowVisibleRef; +class Group; +class LogicalOutput; +class SurfaceInterface; +class VirtualDesktop; +class Window; +class WindowItem; + +/** + * @short Representation of a window used by/for Effect classes. + * + * The purpose is to hide internal data and also to serve as a single + * representation for the case when Client/Unmanaged becomes Deleted. + */ +class KWIN_EXPORT EffectWindow : public QObject +{ + Q_OBJECT + Q_PROPERTY(QRectF geometry READ frameGeometry) + Q_PROPERTY(QRectF expandedGeometry READ expandedGeometry) + Q_PROPERTY(qreal height READ height) + Q_PROPERTY(qreal opacity READ opacity) + Q_PROPERTY(QPointF pos READ pos) + Q_PROPERTY(KWin::LogicalOutput *screen READ screen) + Q_PROPERTY(QSizeF size READ size) + Q_PROPERTY(qreal width READ width) + Q_PROPERTY(qreal x READ x) + Q_PROPERTY(qreal y READ y) + Q_PROPERTY(QList desktops READ desktops) + Q_PROPERTY(bool onAllDesktops READ isOnAllDesktops) + Q_PROPERTY(bool onCurrentDesktop READ isOnCurrentDesktop) + Q_PROPERTY(QRectF rect READ rect) + Q_PROPERTY(QString windowClass READ windowClass) + Q_PROPERTY(QString windowRole READ windowRole) + /** + * Returns whether the window is a desktop background window (the one with wallpaper). + * See _NET_WM_WINDOW_TYPE_DESKTOP at https://specifications.freedesktop.org/wm-spec . + */ + Q_PROPERTY(bool desktopWindow READ isDesktop) + /** + * Returns whether the window is a dock (i.e. a panel). + * See _NET_WM_WINDOW_TYPE_DOCK at https://specifications.freedesktop.org/wm-spec . + */ + Q_PROPERTY(bool dock READ isDock) + /** + * Returns whether the window is a standalone (detached) toolbar window. + * See _NET_WM_WINDOW_TYPE_TOOLBAR at https://specifications.freedesktop.org/wm-spec . + */ + Q_PROPERTY(bool toolbar READ isToolbar) + /** + * Returns whether the window is a torn-off menu. + * See _NET_WM_WINDOW_TYPE_MENU at https://specifications.freedesktop.org/wm-spec . + */ + Q_PROPERTY(bool menu READ isMenu) + /** + * Returns whether the window is a "normal" window, i.e. an application or any other window + * for which none of the specialized window types fit. + * See _NET_WM_WINDOW_TYPE_NORMAL at https://specifications.freedesktop.org/wm-spec . + */ + Q_PROPERTY(bool normalWindow READ isNormalWindow) + /** + * Returns whether the window is a dialog window. + * See _NET_WM_WINDOW_TYPE_DIALOG at https://specifications.freedesktop.org/wm-spec . + */ + Q_PROPERTY(bool dialog READ isDialog) + /** + * Returns whether the window is a splashscreen. Note that many (especially older) applications + * do not support marking their splash windows with this type. + * See _NET_WM_WINDOW_TYPE_SPLASH at https://specifications.freedesktop.org/wm-spec . + */ + Q_PROPERTY(bool splash READ isSplash) + /** + * Returns whether the window is a utility window, such as a tool window. + * See _NET_WM_WINDOW_TYPE_UTILITY at https://specifications.freedesktop.org/wm-spec . + */ + Q_PROPERTY(bool utility READ isUtility) + /** + * Returns whether the window is a dropdown menu (i.e. a popup directly or indirectly open + * from the application's menubar). + * See _NET_WM_WINDOW_TYPE_DROPDOWN_MENU at https://specifications.freedesktop.org/wm-spec . + */ + Q_PROPERTY(bool dropdownMenu READ isDropdownMenu) + /** + * Returns whether the window is a popup menu (that is not a torn-off or dropdown menu). + * See _NET_WM_WINDOW_TYPE_POPUP_MENU at https://specifications.freedesktop.org/wm-spec . + */ + Q_PROPERTY(bool popupMenu READ isPopupMenu) + /** + * Returns whether the window is a tooltip. + * See _NET_WM_WINDOW_TYPE_TOOLTIP at https://specifications.freedesktop.org/wm-spec . + */ + Q_PROPERTY(bool tooltip READ isTooltip) + /** + * Returns whether the window is a window with a notification. + * See _NET_WM_WINDOW_TYPE_NOTIFICATION at https://specifications.freedesktop.org/wm-spec . + */ + Q_PROPERTY(bool notification READ isNotification) + /** + * Returns whether the window is a window with a critical notification. + * using the non-standard _KDE_NET_WM_WINDOW_TYPE_CRITICAL_NOTIFICATION + */ + Q_PROPERTY(bool criticalNotification READ isCriticalNotification) + /** + * Returns whether the window is an on screen display window + * using the non-standard _KDE_NET_WM_WINDOW_TYPE_ON_SCREEN_DISPLAY + */ + Q_PROPERTY(bool onScreenDisplay READ isOnScreenDisplay) + /** + * Returns whether the window is a combobox popup. + * See _NET_WM_WINDOW_TYPE_COMBO at https://specifications.freedesktop.org/wm-spec . + */ + Q_PROPERTY(bool comboBox READ isComboBox) + /** + * Returns whether the window is a Drag&Drop icon. + * See _NET_WM_WINDOW_TYPE_DND at https://specifications.freedesktop.org/wm-spec . + */ + Q_PROPERTY(bool dndIcon READ isDNDIcon) + /** + * Returns the NETWM window type. + * See https://specifications.freedesktop.org/wm-spec . + */ + Q_PROPERTY(int windowType READ windowTypeInt) + /** + * Whether this EffectWindow is managed by KWin (it has control over its placement and other + * aspects, as opposed to override-redirect windows that are entirely handled by the application). + */ + Q_PROPERTY(bool managed READ isManaged) + /** + * Whether this EffectWindow represents an already deleted window and only kept for the compositor for animations. + */ + Q_PROPERTY(bool deleted READ isDeleted) + /** + * The Caption of the window. Read from WM_NAME property together with a suffix for hostname and shortcut. + */ + Q_PROPERTY(QString caption READ caption) + /** + * Whether the window is set to be kept above other windows. + */ + Q_PROPERTY(bool keepAbove READ keepAbove) + /** + * Whether the window is set to be kept below other windows. + */ + Q_PROPERTY(bool keepBelow READ keepBelow) + /** + * Whether the window is minimized. + */ + Q_PROPERTY(bool minimized READ isMinimized WRITE setMinimized) + /** + * Whether the window represents a modal window. + */ + Q_PROPERTY(bool modal READ isModal) + /** + * Whether the window is moveable. Even if it is not moveable, it might be possible to move + * it to another screen. + * @see moveableAcrossScreens + */ + Q_PROPERTY(bool moveable READ isMovable) + /** + * Whether the window can be moved to another screen. + * @see moveable + */ + Q_PROPERTY(bool moveableAcrossScreens READ isMovableAcrossScreens) + /** + * By how much the window wishes to grow/shrink at least. Usually QSize(1,1). + * MAY BE DISOBEYED BY THE WM! It's only for information, do NOT rely on it at all. + */ + Q_PROPERTY(QSizeF basicUnit READ basicUnit) + /** + * Whether the window is currently being moved by the user. + */ + Q_PROPERTY(bool move READ isUserMove) + /** + * Whether the window is currently being resized by the user. + */ + Q_PROPERTY(bool resize READ isUserResize) + /** + * The optional geometry representing the minimized Client in e.g a taskbar. + * See _NET_WM_ICON_GEOMETRY at https://specifications.freedesktop.org/wm-spec . + */ + Q_PROPERTY(QRectF iconGeometry READ iconGeometry) + /** + * Returns whether the window is any of special windows types (desktop, dock, splash, ...), + * i.e. window types that usually don't have a window frame and the user does not use window + * management (moving, raising,...) on them. + */ + Q_PROPERTY(bool specialWindow READ isSpecialWindow) + Q_PROPERTY(QIcon icon READ icon) + /** + * Whether the window should be excluded from window switching effects. + */ + Q_PROPERTY(bool skipSwitcher READ isSkipSwitcher) + /** + * Geometry of the actual window contents inside the whole (including decorations) window. + */ + Q_PROPERTY(QRectF contentsRect READ contentsRect) + Q_PROPERTY(bool hasDecoration READ hasDecoration) + Q_PROPERTY(QStringList activities READ activities) + Q_PROPERTY(bool onCurrentActivity READ isOnCurrentActivity) + Q_PROPERTY(bool onAllActivities READ isOnAllActivities) + /** + * Whether the decoration currently uses an alpha channel. + * @since 4.10 + */ + Q_PROPERTY(bool decorationHasAlpha READ decorationHasAlpha) + /** + * Whether the window is currently visible to the user, that is: + *
    + *
  • Not minimized
  • + *
  • On current desktop
  • + *
  • On current activity
  • + *
+ * @since 4.11 + */ + Q_PROPERTY(bool visible READ isVisible) + /** + * Whether the window does not want to be animated on window close. + * In case this property is @c true it is not useful to start an animation on window close. + * The window will not be visible, but the animation hooks are executed. + * @since 5.0 + */ + Q_PROPERTY(bool skipsCloseAnimation READ skipsCloseAnimation) + + /** + * Whether the window is fullscreen. + * @since 5.6 + */ + Q_PROPERTY(bool fullScreen READ isFullScreen) + + /** + * Whether this client is unresponsive. + * + * When an application failed to react on a ping request in time, it is + * considered unresponsive. This usually indicates that the application froze or crashed. + * + * @since 5.10 + */ + Q_PROPERTY(bool unresponsive READ isUnresponsive) + + /** + * Whether this is a Wayland client. + * @since 5.15 + */ + Q_PROPERTY(bool waylandClient READ isWaylandClient CONSTANT) + + /** + * Whether this is an X11 client. + * @since 5.15 + */ + Q_PROPERTY(bool x11Client READ isX11Client CONSTANT) + + /** + * Whether the window is a popup. + * + * A popup is a window that can be used to implement tooltips, combo box popups, + * popup menus and other similar user interface concepts. + * + * @since 5.15 + */ + Q_PROPERTY(bool popupWindow READ isPopupWindow CONSTANT) + + /** + * KWin internal window. Specific to Wayland platform. + * + * If the EffectWindow does not reference an internal window, this property is @c null. + * @since 5.16 + */ + Q_PROPERTY(QWindow *internalWindow READ internalWindow CONSTANT) + + /** + * Whether this EffectWindow represents the outline. + * + * When compositing is turned on, the outline is an actual window. + * + * @since 5.16 + */ + Q_PROPERTY(bool outline READ isOutline CONSTANT) + + /** + * The PID of the application this window belongs to. + * + * @since 5.18 + */ + Q_PROPERTY(pid_t pid READ pid CONSTANT) + + /** + * Whether this EffectWindow represents the screenlocker greeter. + * + * @since 5.22 + */ + Q_PROPERTY(bool lockScreen READ isLockScreen CONSTANT) + + /** + * Whether the window is an applet popup. + * + * An applet popup is created by an applet to show its + * fullRepresentation. + * + * @since 6.3 + */ + Q_PROPERTY(bool appletPopup READ isAppletPopup CONSTANT) + + /** + * Whether this EffectWindow is hidden because the show desktop mode is active. + */ + Q_PROPERTY(bool hiddenByShowDesktop READ isHiddenByShowDesktop) + + /** + * A client-provided tag of the window. + * Not necessarily unique, but can be used to identify similar windows + * across application restarts + */ + Q_PROPERTY(QString tag READ tag) + +public: + /** Flags explaining why painting should be disabled */ + enum { + /** Window will not be painted */ + PAINT_DISABLED = 1 << 0, + /** Window will not be painted because of which desktop it's on */ + PAINT_DISABLED_BY_DESKTOP = 1 << 1, + /** Window will not be painted because it is minimized */ + PAINT_DISABLED_BY_MINIMIZE = 1 << 2, + /** Window will not be painted because it's not on the current activity */ + PAINT_DISABLED_BY_ACTIVITY = 1 << 3, + }; + + explicit EffectWindow(WindowItem *windowItem); + ~EffectWindow() override; + + Q_SCRIPTABLE void addRepaint(const Rect &r); + Q_SCRIPTABLE void addRepaint(int x, int y, int w, int h); + Q_SCRIPTABLE void addRepaintFull(); + Q_SCRIPTABLE void addLayerRepaint(const Rect &r); + Q_SCRIPTABLE void addLayerRepaint(int x, int y, int w, int h); + + void refWindow(); + void unrefWindow(); + + bool isDeleted() const; + bool isHidden() const; + bool isHiddenByShowDesktop() const; + + bool isMinimized() const; + double opacity() const; + + bool isOnCurrentActivity() const; + Q_SCRIPTABLE bool isOnActivity(const QString &id) const; + bool isOnAllActivities() const; + QStringList activities() const; + + Q_SCRIPTABLE bool isOnDesktop(KWin::VirtualDesktop *desktop) const; + bool isOnCurrentDesktop() const; + bool isOnAllDesktops() const; + /** + * All the desktops by number that the window is in. On X11 this list will always have + * a length of 1, on Wayland can be any subset. + * If the list is empty it means the window is on all desktops + */ + QList desktops() const; + + qreal x() const; + qreal y() const; + qreal width() const; + qreal height() const; + /** + * By how much the window wishes to grow/shrink at least. Usually QSize(1,1). + * MAY BE DISOBEYED BY THE WM! It's only for information, do NOT rely on it at all. + */ + QSizeF basicUnit() const; + /** + * Returns the geometry of the window excluding server-side and client-side + * drop-shadows. + * + * @since 5.18 + */ + QRectF frameGeometry() const; + /** + * Returns the geometry of the pixmap or buffer attached to this window. + * + * For X11 clients, this method returns server-side geometry of the Window. + * + * For Wayland clients, this method returns rectangle that the main surface + * occupies on the screen, in global screen coordinates. + * + * @since 5.18 + */ + QRectF bufferGeometry() const; + QRectF clientGeometry() const; + /** + * Geometry of the window including decoration and potentially shadows. + * May be different from geometry() if the window has a shadow. + * @since 4.9 + */ + QRectF expandedGeometry() const; + LogicalOutput *screen() const; + QPointF pos() const; + QSizeF size() const; + QRectF rect() const; + bool isMovable() const; + bool isMovableAcrossScreens() const; + bool isUserMove() const; + bool isUserResize() const; + QRectF iconGeometry() const; + + /** + * Geometry of the actual window contents inside the whole (including decorations) window. + */ + QRectF contentsRect() const; + bool hasDecoration() const; + bool decorationHasAlpha() const; + /** + * Returns the decoration + * @since 5.25 + */ + KDecoration3::Decoration *decoration() const; + QByteArray readProperty(long atom, long type, int format) const; + void deleteProperty(long atom) const; + + QString caption() const; + QIcon icon() const; + QString windowClass() const; + QString windowRole() const; + QString tag() const; + const EffectWindowGroup *group() const; + + /** + * Returns whether the window is a desktop background window (the one with wallpaper). + * See _NET_WM_WINDOW_TYPE_DESKTOP at https://specifications.freedesktop.org/wm-spec . + */ + bool isDesktop() const; + /** + * Returns whether the window is a dock (i.e. a panel). + * See _NET_WM_WINDOW_TYPE_DOCK at https://specifications.freedesktop.org/wm-spec . + */ + bool isDock() const; + /** + * Returns whether the window is a standalone (detached) toolbar window. + * See _NET_WM_WINDOW_TYPE_TOOLBAR at https://specifications.freedesktop.org/wm-spec . + */ + bool isToolbar() const; + /** + * Returns whether the window is a torn-off menu. + * See _NET_WM_WINDOW_TYPE_MENU at https://specifications.freedesktop.org/wm-spec . + */ + bool isMenu() const; + /** + * Returns whether the window is a "normal" window, i.e. an application or any other window + * for which none of the specialized window types fit. + * See _NET_WM_WINDOW_TYPE_NORMAL at https://specifications.freedesktop.org/wm-spec . + */ + bool isNormalWindow() const; // normal as in 'NET::Normal or NET::Unknown non-transient' + /** + * Returns whether the window is any of special windows types (desktop, dock, splash, ...), + * i.e. window types that usually don't have a window frame and the user does not use window + * management (moving, raising,...) on them. + */ + bool isSpecialWindow() const; + /** + * Returns whether the window is a dialog window. + * See _NET_WM_WINDOW_TYPE_DIALOG at https://specifications.freedesktop.org/wm-spec . + */ + bool isDialog() const; + /** + * Returns whether the window is a splashscreen. Note that many (especially older) applications + * do not support marking their splash windows with this type. + * See _NET_WM_WINDOW_TYPE_SPLASH at https://specifications.freedesktop.org/wm-spec . + */ + bool isSplash() const; + /** + * Returns whether the window is a utility window, such as a tool window. + * See _NET_WM_WINDOW_TYPE_UTILITY at https://specifications.freedesktop.org/wm-spec . + */ + bool isUtility() const; + /** + * Returns whether the window is a dropdown menu (i.e. a popup directly or indirectly open + * from the application's menubar). + * See _NET_WM_WINDOW_TYPE_DROPDOWN_MENU at https://specifications.freedesktop.org/wm-spec . + */ + bool isDropdownMenu() const; + /** + * Returns whether the window is a popup menu (that is not a torn-off or dropdown menu). + * See _NET_WM_WINDOW_TYPE_POPUP_MENU at https://specifications.freedesktop.org/wm-spec . + */ + bool isPopupMenu() const; // a context popup, not dropdown, not torn-off + /** + * Returns whether the window is a tooltip. + * See _NET_WM_WINDOW_TYPE_TOOLTIP at https://specifications.freedesktop.org/wm-spec . + */ + bool isTooltip() const; + /** + * Returns whether the window is a window with a notification. + * See _NET_WM_WINDOW_TYPE_NOTIFICATION at https://specifications.freedesktop.org/wm-spec . + */ + bool isNotification() const; + /** + * Returns whether the window is a window with a critical notification. + * using the non-standard _KDE_NET_WM_WINDOW_TYPE_CRITICAL_NOTIFICATION + */ + bool isCriticalNotification() const; + /** + * Returns whether the window is a window used for applet popups. + */ + bool isAppletPopup() const; + /** + * Returns whether the window is an on screen display window + * using the non-standard _KDE_NET_WM_WINDOW_TYPE_ON_SCREEN_DISPLAY + */ + bool isOnScreenDisplay() const; + /** + * Returns whether the window is a combobox popup. + * See _NET_WM_WINDOW_TYPE_COMBO at https://specifications.freedesktop.org/wm-spec . + */ + bool isComboBox() const; + /** + * Returns whether the window is a Drag&Drop icon. + * See _NET_WM_WINDOW_TYPE_DND at https://specifications.freedesktop.org/wm-spec . + */ + bool isDNDIcon() const; + /** + * Returns the NETWM window type. + * See https://specifications.freedesktop.org/wm-spec . + */ + WindowType windowType() const; + int windowTypeInt() const + { + return int(windowType()); + } + /** + * Returns whether the window is managed by KWin (it has control over its placement and other + * aspects, as opposed to override-redirect windows that are entirely handled by the application). + */ + bool isManaged() const; // whether it's managed or override-redirect + /** + * Returns whether or not the window can accept keyboard focus. + */ + bool acceptsFocus() const; + /** + * Returns whether or not the window is kept above all other windows. + */ + bool keepAbove() const; + /** + * Returns whether the window is kept below all other windows. + */ + bool keepBelow() const; + + bool isModal() const; + Q_SCRIPTABLE KWin::EffectWindow *findModal(); + Q_SCRIPTABLE KWin::EffectWindow *transientFor(); + Q_SCRIPTABLE QList mainWindows() const; + + /** + * Returns whether the window should be excluded from window switching effects. + * @since 4.5 + */ + bool isSkipSwitcher() const; + + void setMinimized(bool minimize); + void minimize(); + void unminimize(); + Q_SCRIPTABLE void closeWindow(); + + /** + * @since 4.11 + */ + bool isVisible() const; + + /** + * @since 5.0 + */ + bool skipsCloseAnimation() const; + + /** + * @since 5.5 + */ + SurfaceInterface *surface() const; + + /** + * @since 5.6 + */ + bool isFullScreen() const; + + /** + * @since 5.10 + */ + bool isUnresponsive() const; + + /** + * @since 5.15 + */ + bool isWaylandClient() const; + + /** + * @since 5.15 + */ + bool isX11Client() const; + + /** + * @since 5.15 + */ + bool isPopupWindow() const; + + /** + * @since 5.16 + */ + QWindow *internalWindow() const; + + /** + * @since 5.16 + */ + bool isOutline() const; + + /** + * @since 5.22 + */ + bool isLockScreen() const; + + /** + * @since 5.18 + */ + pid_t pid() const; + + /** + * @since 5.21 + */ + qlonglong windowId() const; + /** + * Returns the internal id of the window that uniquely identifies it. The main difference + * between internalId() and windowId() is that the latter one works as expected only on X11, + * while the former is unique regardless of the window system. + * + * Note that the internaId() has special meaning only to kwin. + * @since 5.24 + */ + QUuid internalId() const; + + /** + * @since 6.0 + */ + bool isInputMethod() const; + + /** + * Can be used to by effects to store arbitrary data in the EffectWindow. + * + * Invoking this method will emit the signal EffectsHandler::windowDataChanged. + * @see EffectsHandler::windowDataChanged + */ + Q_SCRIPTABLE void setData(int role, const QVariant &data); + Q_SCRIPTABLE QVariant data(int role) const; + + Window *window() const; + WindowItem *windowItem() const; + void elevate(bool elevate); + +Q_SIGNALS: + /** + * Signal emitted when a user begins a window move or resize operation. + * To figure out whether the user resizes or moves the window use + * isUserMove or isUserResize. + * Whenever the geometry is updated the signal @ref windowStepUserMovedResized + * is emitted with the current geometry. + * The move/resize operation ends with the signal @ref windowFinishUserMovedResized. + * Only one window can be moved/resized by the user at the same time! + * @param w The window which is being moved/resized + * @see windowStepUserMovedResized + * @see windowFinishUserMovedResized + * @see EffectWindow::isUserMove + * @see EffectWindow::isUserResize + */ + void windowStartUserMovedResized(KWin::EffectWindow *w); + /** + * Signal emitted during a move/resize operation when the user changed the geometry. + * Please note: KWin supports two operation modes. In one mode all changes are applied + * instantly. This means the window's geometry matches the passed in @p geometry. In the + * other mode the geometry is changed after the user ended the move/resize mode. + * The @p geometry differs from the window's geometry. Also the window's pixmap still has + * the same size as before. Depending what the effect wants to do it would be recommended + * to scale/translate the window. + * @param w The window which is being moved/resized + * @param geometry The geometry of the window in the current move/resize step. + * @see windowStartUserMovedResized + * @see windowFinishUserMovedResized + * @see EffectWindow::isUserMove + * @see EffectWindow::isUserResize + */ + void windowStepUserMovedResized(KWin::EffectWindow *w, const QRectF &geometry); + /** + * Signal emitted when the user finishes move/resize of window @p w. + * @param w The window which has been moved/resized + * @see windowStartUserMovedResized + * @see windowFinishUserMovedResized + */ + void windowFinishUserMovedResized(KWin::EffectWindow *w); + + /** + * Signal emitted when the maximized state of the window @p w changed. + * A window can be in one of four states: + * @li restored: both @p horizontal and @p vertical are @c false + * @li horizontally maximized: @p horizontal is @c true and @p vertical is @c false + * @li vertically maximized: @p horizontal is @c false and @p vertical is @c true + * @li completely maximized: both @p horizontal and @p vertical are @c true + * @param w The window whose maximized state changed + * @param horizontal If @c true maximized horizontally + * @param vertical If @c true maximized vertically + */ + void windowMaximizedStateChanged(KWin::EffectWindow *w, bool horizontal, bool vertical); + + /** + * Signal emitted when the maximized state of the window @p w is about to change, + * but before windowMaximizedStateChanged is emitted or any geometry change. + * Useful for OffscreenEffect to grab a window image before any actual change happens + * + * A window can be in one of four states: + * @li restored: both @p horizontal and @p vertical are @c false + * @li horizontally maximized: @p horizontal is @c true and @p vertical is @c false + * @li vertically maximized: @p horizontal is @c false and @p vertical is @c true + * @li completely maximized: both @p horizontal and @p vertical are @c true + * @param w The window whose maximized state changed + * @param horizontal If @c true maximized horizontally + * @param vertical If @c true maximized vertically + */ + void windowMaximizedStateAboutToChange(KWin::EffectWindow *w, bool horizontal, bool vertical); + + /** + * This signal is emitted when the frame geometry of a window changed. + * @param window The window whose geometry changed + * @param oldGeometry The previous geometry + */ + void windowFrameGeometryChanged(KWin::EffectWindow *window, const QRectF &oldGeometry); + + /** + * This signal is emitted when the frame geometry is about to change, the new one is not known yet. + * Useful for OffscreenEffect to grab a window image before any actual change happens. + * + * @param window The window whose geometry is about to change + */ + void windowFrameGeometryAboutToChange(KWin::EffectWindow *window); + + /** + * Signal emitted when the windows opacity is changed. + * @param w The window whose opacity level is changed. + * @param oldOpacity The previous opacity level + * @param newOpacity The new opacity level + */ + void windowOpacityChanged(KWin::EffectWindow *w, qreal oldOpacity, qreal newOpacity); + /** + * Signal emitted when a window is minimized or unminimized. + * @param w The window whose minimized state has changed + */ + void minimizedChanged(KWin::EffectWindow *w); + /** + * Signal emitted when a window either becomes modal (ie. blocking for its main client) or looses that state. + * @param w The window which was unminimized + */ + void windowModalityChanged(KWin::EffectWindow *w); + /** + * Signal emitted when a window either became unresponsive (eg. app froze or crashed) + * or respoonsive + * @param w The window that became (un)responsive + * @param unresponsive Whether the window is responsive or unresponsive + */ + void windowUnresponsiveChanged(KWin::EffectWindow *w, bool unresponsive); + /** + * Signal emitted when an area of a window is scheduled for repainting. + * Use this signal in an effect if another area needs to be synced as well. + * @param w The window which is scheduled for repainting + */ + void windowDamaged(KWin::EffectWindow *w); + + /** + * This signal is emitted when the keep above state of @p w was changed. + * + * @param w The window whose keep above state was changed. + */ + void windowKeepAboveChanged(KWin::EffectWindow *w); + + /** + * This signal is emitted when the keep below state of @p was changed. + * + * @param w The window whose the keep below state was changed. + */ + void windowKeepBelowChanged(KWin::EffectWindow *w); + + /** + * This signal is emitted when the full screen state of @p w was changed. + * + * @param w The window whose the full screen state was changed. + */ + void windowFullScreenChanged(KWin::EffectWindow *w); + + /** + * This signal is emitted when decoration of @p was changed. + * + * @param w The window for which decoration changed + */ + void windowDecorationChanged(KWin::EffectWindow *window); + + /** + * This signal is emitted when the visible geometry of a window changed. + */ + void windowExpandedGeometryChanged(KWin::EffectWindow *window); + + /** + * This signal is emitted when a window enters or leaves a virtual desktop. + */ + void windowDesktopsChanged(KWin::EffectWindow *window); + + /** + * This signal is emitted when a window is hidden or shown. + */ + void windowHiddenChanged(KWin::EffectWindow *window); + +protected: + friend EffectWindowVisibleRef; + void refVisible(const EffectWindowVisibleRef *holder); + void unrefVisible(const EffectWindowVisibleRef *holder); + +private: + class Private; + std::unique_ptr d; +}; + +/** + * The EffectWindowDeletedRef provides a convenient way to prevent deleting a closed + * window until an effect has finished animating it. + */ +class KWIN_EXPORT EffectWindowDeletedRef +{ +public: + EffectWindowDeletedRef() + : m_window(nullptr) + { + } + + explicit EffectWindowDeletedRef(EffectWindow *window) + : m_window(window) + { + m_window->refWindow(); + } + + EffectWindowDeletedRef(const EffectWindowDeletedRef &other) + : m_window(other.m_window) + { + if (m_window) { + m_window->refWindow(); + } + } + + ~EffectWindowDeletedRef() + { + if (m_window) { + m_window->unrefWindow(); + } + } + + EffectWindowDeletedRef &operator=(const EffectWindowDeletedRef &other) + { + if (other.m_window) { + other.m_window->refWindow(); + } + if (m_window) { + m_window->unrefWindow(); + } + m_window = other.m_window; + return *this; + } + + bool isNull() const + { + return m_window == nullptr; + } + +private: + EffectWindow *m_window; +}; + +/** + * The EffectWindowVisibleRef provides a convenient way to force the visible status of a + * window until an effect is finished animating it. + */ +class KWIN_EXPORT EffectWindowVisibleRef +{ +public: + EffectWindowVisibleRef() + : m_window(nullptr) + , m_reason(0) + { + } + + explicit EffectWindowVisibleRef(EffectWindow *window, int reason) + : m_window(window) + , m_reason(reason) + { + m_window->refVisible(this); + } + + EffectWindowVisibleRef(const EffectWindowVisibleRef &other) + : m_window(other.m_window) + , m_reason(other.m_reason) + { + if (m_window) { + m_window->refVisible(this); + } + } + + ~EffectWindowVisibleRef() + { + if (m_window) { + m_window->unrefVisible(this); + } + } + + int reason() const + { + return m_reason; + } + + EffectWindowVisibleRef &operator=(const EffectWindowVisibleRef &other) + { + if (other.m_window) { + other.m_window->refVisible(&other); + } + if (m_window) { + m_window->unrefVisible(this); + } + m_window = other.m_window; + m_reason = other.m_reason; + return *this; + } + + bool isNull() const + { + return m_window == nullptr; + } + +private: + EffectWindow *m_window; + int m_reason; +}; + +class KWIN_EXPORT EffectWindowGroup +{ +public: + explicit EffectWindowGroup(Group *group); + virtual ~EffectWindowGroup(); + + QList members() const; + +private: + Group *m_group; +}; + +inline void EffectWindow::addRepaint(int x, int y, int w, int h) +{ + addRepaint(Rect(x, y, w, h)); +} + +inline void EffectWindow::addLayerRepaint(int x, int y, int w, int h) +{ + addLayerRepaint(Rect(x, y, w, h)); +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/effect/globals.h b/local/recipes/kde/kwin/source/src/effect/globals.h new file mode 100644 index 0000000000..70e077cd69 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/globals.h @@ -0,0 +1,449 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "config-kwin.h" + +#include +#include +#include +#include +#include + +#include + +namespace KWin +{ +KWIN_EXPORT Q_NAMESPACE + + enum CompositingType { + NoCompositing = 0, + /** + * Used as a flag whether OpenGL based compositing is used. + * The flag is or-ed to the enum values of the specific OpenGL types. + * The actual Compositors use the or @c OpenGLCompositing + * flags. If you need to know whether OpenGL is used, either and the flag or + * use EffectsHandler::isOpenGLCompositing(). + */ + OpenGLCompositing = 1, + QPainterCompositing = 1 << 2, + }; + +enum clientAreaOption { + PlacementArea, // geometry where a window will be initially placed after being mapped + MovementArea, // ??? window movement snapping area? ignore struts + MaximizeArea, // geometry to which a window will be maximized + MaximizeFullArea, // like MaximizeArea, but ignore struts - used e.g. for topmenu + FullScreenArea, // area for fullscreen windows + // these below don't depend on xinerama settings + WorkArea, // whole workarea (all screens together) + FullArea, // whole area (all screens together), ignore struts + ScreenArea, // one whole screen, ignore struts +}; + +/** + * Maximize mode. These values specify how a window is maximized. + * + * @note these values are written to session files, don't change the order + */ +enum MaximizeMode { + MaximizeRestore = 0, ///< The window is not maximized in any direction. + MaximizeVertical = 1, ///< The window is maximized vertically. + MaximizeHorizontal = 2, ///< The window is maximized horizontally. + /// Equal to @p MaximizeVertical | @p MaximizeHorizontal + MaximizeFull = MaximizeVertical | MaximizeHorizontal, +}; +Q_ENUM_NS(MaximizeMode) + +inline MaximizeMode operator^(MaximizeMode m1, MaximizeMode m2) +{ + return MaximizeMode(int(m1) ^ int(m2)); +} + +enum ElectricBorder { + ElectricTop, + ElectricTopRight, + ElectricRight, + ElectricBottomRight, + ElectricBottom, + ElectricBottomLeft, + ElectricLeft, + ElectricTopLeft, + ELECTRIC_COUNT, + ElectricNone, +}; +Q_ENUM_NS(ElectricBorder) + +// TODO: Hardcoding is bad, need to add some way of registering global actions to these. +// When designing the new system we must keep in mind that we have conditional actions +// such as "only when moving windows" desktop switching that the current global action +// system doesn't support. +enum ElectricBorderAction { + ElectricActionNone, // No special action, not set, desktop switch or an effect + ElectricActionShowDesktop, // Show desktop or restore + ElectricActionLockScreen, // Lock screen + ElectricActionKRunner, // Open KRunner + ElectricActionActivityManager, // Activity Manager + ElectricActionApplicationLauncher, // Application Launcher + ELECTRIC_ACTION_COUNT, +}; + +enum TabBoxMode { + TabBoxWindowsMode, // Primary window switching mode + TabBoxWindowsAlternativeMode, // Secondary window switching mode + TabBoxCurrentAppWindowsMode, // Same as primary window switching mode but only for windows of current application + TabBoxCurrentAppWindowsAlternativeMode, // Same as secondary switching mode but only for windows of current application +}; + +enum KWinOption { + CloseButtonCorner, + SwitchDesktopOnScreenEdge, + SwitchDesktopOnScreenEdgeMovingWindows, +}; + +/** + * @brief The direction in which a pointer axis is moved. + */ +enum PointerAxisDirection { + PointerAxisUp, + PointerAxisDown, + PointerAxisLeft, + PointerAxisRight, +}; + +/** + * @brief Directions for swipe gestures + * @since 5.10 + */ +enum class SwipeDirection { + Invalid, + Down, + Left, + Up, + Right, +}; + +enum class PinchDirection { + Expanding, + Contracting +}; + +/** + * Represents the state of the session running outside kwin + * Under Plasma this is managed by ksmserver + */ +enum class SessionState { + Normal, + Saving, + Quitting, +}; +Q_ENUM_NS(SessionState) + +enum class LED { + NumLock = 1 << 0, + CapsLock = 1 << 1, + ScrollLock = 1 << 2, + Compose = 1 << 3, + Kana = 1 << 4, +}; +Q_DECLARE_FLAGS(LEDs, LED) +Q_FLAG_NS(LEDs) + +enum Layer { + UnknownLayer = -1, + FirstLayer = 0, + DesktopLayer = FirstLayer, + BelowLayer, + NormalLayer, + AboveLayer, + NotificationLayer, // layer for windows of type notification + ActiveLayer, // active fullscreen, or active dialog + PopupLayer, // tooltips, sub- and context menus + CriticalNotificationLayer, // layer for notifications that should be shown even on top of fullscreen + OnScreenDisplayLayer, // layer for On Screen Display windows such as volume feedback + OverlayLayer, + NumLayers, // number of layers, must be last +}; +Q_ENUM_NS(Layer) + +// TODO: could this be in Tile itself? +enum class QuickTileFlag { + None = 0, + Left = 1 << 0, + Right = 1 << 1, + Top = 1 << 2, + Bottom = 1 << 3, + Custom = 1 << 4, + Horizontal = Left | Right, + Vertical = Top | Bottom, +}; +Q_ENUM_NS(QuickTileFlag) +Q_DECLARE_FLAGS(QuickTileMode, QuickTileFlag) + +inline QuickTileMode operator~(QuickTileFlag flag) +{ + return QuickTileMode(~int(flag)); +} + +/** + * Short wrapper for a cursor image provided by the Platform. + * @since 5.9 + */ +class PlatformCursorImage +{ +public: + explicit PlatformCursorImage() + : m_image() + , m_hotSpot() + { + } + explicit PlatformCursorImage(const QImage &image, const QPointF &hotSpot) + : m_image(image) + , m_hotSpot(hotSpot) + { + } + virtual ~PlatformCursorImage() = default; + + bool isNull() const + { + return m_image.isNull(); + } + QImage image() const + { + return m_image; + } + QPointF hotSpot() const + { + return m_hotSpot; + } + +private: + QImage m_image; + QPointF m_hotSpot; +}; + +/** + * Scale a rect by a scalar. + */ +KWIN_EXPORT inline QRectF scaledRect(const QRectF &rect, qreal scale) +{ + return QRectF{rect.x() * scale, rect.y() * scale, rect.width() * scale, rect.height() * scale}; +} + +/** + * Round a vector to nearest integer. + */ +KWIN_EXPORT inline QVector2D roundVector(const QVector2D &input) +{ + return QVector2D(std::round(input.x()), std::round(input.y())); +} + +/** + * Convert a QPointF to a QPoint by flooring instead of rounding. + * + * By default, QPointF::toPoint() rounds which can cause problems in certain + * cases. + */ +KWIN_EXPORT inline QPoint flooredPoint(const QPointF &point) +{ + return QPoint(std::floor(point.x()), std::floor(point.y())); +} + +enum class PresentationMode { + VSync, + AdaptiveSync, + Async, + AdaptiveAsync, +}; +Q_ENUM_NS(PresentationMode); + +enum class ContentType { + None = 0, + Photo = 1, + Video = 2, + Game = 3, +}; +Q_ENUM_NS(ContentType); + +enum class VrrPolicy { + Never = 0, + Always = 1, + Automatic = 2, +}; +Q_ENUM_NS(VrrPolicy); + +enum class PresentationModeHint { + VSync, + Async +}; +Q_ENUM_NS(PresentationModeHint); + +// For now, keep in sync with NETWM::WindowType from KWindowSystem +enum class WindowType { + /** + * intermediate value, do not use + */ + Undefined = -2, + /** + * indicates that the window did not define a window type. + */ + Unknown = -1, + /** + * indicates that this is a normal, top-level window + */ + Normal = 0, + /** + * indicates a desktop feature. This can include a single window + * containing desktop icons with the same dimensions as the screen, allowing + * the desktop environment to have full control of the desktop, without the + * need for proxying root window clicks. + */ + Desktop = 1, + /** + * indicates a dock or panel feature + */ + Dock = 2, + /** + * indicates a toolbar window + */ + Toolbar = 3, + /** + * indicates a pinnable (torn-off) menu window + */ + Menu = 4, + /** + * indicates that this is a dialog window + */ + Dialog = 5, + // cannot deprecate to compiler: used both by clients & manager, later needs to keep supporting it for now + // KF6: remove + /** + * @deprecated has unclear meaning and is KDE-only + */ + Override = 6, // NON STANDARD + /** + * indicates a toplevel menu (AKA macmenu). This is a KDE extension to the + * _NET_WM_WINDOW_TYPE mechanism. + */ + TopMenu = 7, // NON STANDARD + /** + * indicates a utility window + */ + Utility = 8, + /** + * indicates that this window is a splash screen window. + */ + Splash = 9, + /** + * indicates a dropdown menu (from a menubar typically) + */ + DropdownMenu = 10, + /** + * indicates a popup menu (a context menu typically) + */ + PopupMenu = 11, + /** + * indicates a tooltip window + */ + Tooltip = 12, + /** + * indicates a notification window + */ + Notification = 13, + /** + * indicates that the window is a list for a combobox + */ + ComboBox = 14, + /** + * indicates a window that represents the dragged object during DND operation + */ + DNDIcon = 15, + /** + * indicates an On Screen Display window (such as volume feedback) + */ + OnScreenDisplay = 16, // NON STANDARD + /** + * indicates a critical notification (such as battery is running out) + */ + CriticalNotification = 17, // NON STANDARD + /** + * indicates that this window is an applet. + */ + AppletPopup = 18, // NON STANDARD +}; +Q_ENUM_NS(WindowType); + +/** + * Values for WindowType when they should be OR'ed together, e.g. + * for the properties argument of the NETRootInfo constructor. + * @see WindowTypes + */ +enum WindowTypeMask { + NormalMask = 1u << 0, ///< @see Normal + DesktopMask = 1u << 1, ///< @see Desktop + DockMask = 1u << 2, ///< @see Dock + ToolbarMask = 1u << 3, ///< @see Toolbar + MenuMask = 1u << 4, ///< @see Menu + DialogMask = 1u << 5, ///< @see Dialog + OverrideMask = 1u << 6, ///< @see Override + TopMenuMask = 1u << 7, ///< @see TopMenu + UtilityMask = 1u << 8, ///< @see Utility + SplashMask = 1u << 9, ///< @see Splash + DropdownMenuMask = 1u << 10, ///< @see DropdownMenu + PopupMenuMask = 1u << 11, ///< @see PopupMenu + TooltipMask = 1u << 12, ///< @see Tooltip + NotificationMask = 1u << 13, ///< @see Notification + ComboBoxMask = 1u << 14, ///< @see ComboBox + DNDIconMask = 1u << 15, ///< @see DNDIcon + OnScreenDisplayMask = 1u << 16, ///< NON STANDARD @see OnScreenDisplay @since 5.6 + CriticalNotificationMask = 1u << 17, ///< NON STANDARD @see CriticalNotification @since 5.58 + AppletPopupMask = 1u << 18, ///< NON STANDARD @see AppletPopup + AllTypesMask = 0U - 1, ///< All window types. +}; +Q_DECLARE_FLAGS(WindowTypes, WindowTypeMask) + +enum class OutputConfigurationError { + None, + Unknown, + TooManyEnabledOutputs, + Timeout, +}; + +} // namespace + +Q_DECLARE_METATYPE(std::chrono::nanoseconds) + +#define KWIN_SINGLETON_VARIABLE(ClassName, variableName) \ +public: \ + static ClassName *create(QObject *parent = nullptr); \ + static ClassName *self() \ + { \ + return variableName; \ + } \ + \ +protected: \ + explicit ClassName(QObject *parent = nullptr); \ + \ +private: \ + static ClassName *variableName; + +#define KWIN_SINGLETON(ClassName) KWIN_SINGLETON_VARIABLE(ClassName, s_self) + +#define KWIN_SINGLETON_FACTORY_VARIABLE_FACTORED(ClassName, FactoredClassName, variableName) \ + ClassName *ClassName::variableName = nullptr; \ + ClassName *ClassName::create(QObject *parent) \ + { \ + Q_ASSERT(!variableName); \ + variableName = new FactoredClassName(parent); \ + return variableName; \ + } +#define KWIN_SINGLETON_FACTORY_VARIABLE(ClassName, variableName) KWIN_SINGLETON_FACTORY_VARIABLE_FACTORED(ClassName, ClassName, variableName) +#define KWIN_SINGLETON_FACTORY_FACTORED(ClassName, FactoredClassName) KWIN_SINGLETON_FACTORY_VARIABLE_FACTORED(ClassName, FactoredClassName, s_self) +#define KWIN_SINGLETON_FACTORY(ClassName) KWIN_SINGLETON_FACTORY_VARIABLE(ClassName, s_self) diff --git a/local/recipes/kde/kwin/source/src/effect/logging.cpp b/local/recipes/kde/kwin/source/src/effect/logging.cpp new file mode 100644 index 0000000000..a9f1ae8f36 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/logging.cpp @@ -0,0 +1,10 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "logging_p.h" +Q_LOGGING_CATEGORY(LIBKWINEFFECTS, "effect", QtWarningMsg) diff --git a/local/recipes/kde/kwin/source/src/effect/logging_p.h b/local/recipes/kde/kwin/source/src/effect/logging_p.h new file mode 100644 index 0000000000..8c94346352 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/logging_p.h @@ -0,0 +1,14 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(LIBKWINEFFECTS) diff --git a/local/recipes/kde/kwin/source/src/effect/offscreeneffect.cpp b/local/recipes/kde/kwin/source/src/effect/offscreeneffect.cpp new file mode 100644 index 0000000000..83883da2a9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/offscreeneffect.cpp @@ -0,0 +1,438 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "effect/offscreeneffect.h" +#include "core/output.h" +#include "core/pixelgrid.h" +#include "core/rendertarget.h" +#include "core/renderviewport.h" +#include "effect/effecthandler.h" +#include "opengl/eglcontext.h" +#include "opengl/gltexture.h" +#include "opengl/glutils.h" +#include "scene/windowitem.h" + +namespace KWin +{ + +struct OffscreenData +{ +public: + virtual ~OffscreenData(); + void setDirty(); + void setShader(GLShader *newShader); + void setVertexSnappingMode(RenderGeometry::VertexSnappingMode mode); + + void paint(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *window, const Region &deviceRegion, + const WindowPaintData &data, const WindowQuadList &quads); + + void maybeRender(EffectWindow *window); + + std::unique_ptr m_texture; + std::unique_ptr m_fbo; + bool m_isDirty = true; + GLShader *m_shader = nullptr; + RenderGeometry::VertexSnappingMode m_vertexSnappingMode = RenderGeometry::VertexSnappingMode::Round; + QMetaObject::Connection m_windowDamagedConnection; + ItemEffect m_windowEffect; +}; + +class OffscreenEffectPrivate +{ +public: + std::map> windows; + QMetaObject::Connection windowDeletedConnection; + RenderGeometry::VertexSnappingMode vertexSnappingMode = RenderGeometry::VertexSnappingMode::Round; +}; + +OffscreenEffect::OffscreenEffect(QObject *parent) + : Effect(parent) + , d(std::make_unique()) +{ +} + +OffscreenEffect::~OffscreenEffect() = default; + +bool OffscreenEffect::supported() +{ + return effects->isOpenGLCompositing(); +} + +void OffscreenEffect::redirect(EffectWindow *window) +{ + std::unique_ptr &offscreenData = d->windows[window]; + if (offscreenData) { + return; + } + offscreenData = std::make_unique(); + offscreenData->setVertexSnappingMode(d->vertexSnappingMode); + offscreenData->m_windowEffect = ItemEffect(window->windowItem()); + offscreenData->m_windowDamagedConnection = + connect(window, &EffectWindow::windowDamaged, this, &OffscreenEffect::handleWindowDamaged); + + if (d->windows.size() == 1) { + setupConnections(); + } +} + +void OffscreenEffect::unredirect(EffectWindow *window) +{ + auto it = d->windows.find(window); + if (it == d->windows.end()) { + return; + } + + if (!EglContext::currentContext()) { + effects->openglContext()->makeCurrent(); + } + + d->windows.erase(it); + if (d->windows.empty()) { + destroyConnections(); + } +} + +void OffscreenEffect::setShader(EffectWindow *window, GLShader *shader) +{ + if (const auto it = d->windows.find(window); it != d->windows.end()) { + it->second->setShader(shader); + } +} + +void OffscreenEffect::apply(EffectWindow *window, int mask, WindowPaintData &data, WindowQuadList &quads) +{ +} + +void OffscreenData::maybeRender(EffectWindow *window) +{ + const qreal scale = window->screen()->scale(); + const QRectF logicalGeometry = snapToPixels(window->expandedGeometry(), scale); + const QSize textureSize = (logicalGeometry.size() * scale).toSize(); + + if (textureSize.isEmpty()) { + m_fbo.reset(); + m_texture.reset(); + return; + } + if (!m_texture || m_texture->size() != textureSize) { + m_texture = GLTexture::allocate(GL_RGBA8, textureSize); + if (!m_texture) { + return; + } + m_texture->setFilter(GL_LINEAR); + m_texture->setWrapMode(GL_CLAMP_TO_EDGE); + m_fbo = std::make_unique(m_texture.get()); + m_isDirty = true; + } + + if (m_isDirty) { + RenderTarget renderTarget(m_fbo.get()); + RenderViewport viewport(logicalGeometry, scale, renderTarget, QPoint()); + GLFramebuffer::pushFramebuffer(m_fbo.get()); + glClearColor(0.0, 0.0, 0.0, 0.0); + glClear(GL_COLOR_BUFFER_BIT); + + WindowPaintData data; + data.setOpacity(1.0); + + const int mask = Effect::PAINT_WINDOW_TRANSFORMED | Effect::PAINT_WINDOW_TRANSLUCENT; + effects->drawWindow(renderTarget, viewport, window, mask, Region::infinite(), data); + + GLFramebuffer::popFramebuffer(); + m_isDirty = false; + } +} + +OffscreenData::~OffscreenData() +{ + QObject::disconnect(m_windowDamagedConnection); +} + +void OffscreenData::setDirty() +{ + m_isDirty = true; +} + +void OffscreenData::setShader(GLShader *newShader) +{ + m_shader = newShader; +} + +void OffscreenData::setVertexSnappingMode(RenderGeometry::VertexSnappingMode mode) +{ + m_vertexSnappingMode = mode; +} + +void OffscreenData::paint(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *window, const Region &deviceRegion, + const WindowPaintData &data, const WindowQuadList &quads) +{ + if (!m_texture) { + return; + } + GLShader *shader = m_shader ? m_shader : ShaderManager::instance()->shader(ShaderTrait::MapTexture | ShaderTrait::Modulate | ShaderTrait::AdjustSaturation | ShaderTrait::TransformColorspace); + ShaderBinder binder(shader); + + const double scale = viewport.scale(); + + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + vbo->setAttribLayout(std::span(GLVertexBuffer::GLVertex2DLayout), sizeof(GLVertex2D)); + + RenderGeometry geometry; + geometry.setVertexSnappingMode(m_vertexSnappingMode); + for (auto &quad : quads) { + geometry.appendWindowQuad(quad, scale); + } + geometry.postProcessTextureCoordinates(m_texture->matrix(NormalizedCoordinates)); + + const auto map = vbo->map(geometry.size()); + if (!map) { + return; + } + geometry.copy(*map); + vbo->unmap(); + + vbo->bindArrays(); + + const qreal rgb = data.brightness() * data.opacity(); + const qreal a = data.opacity(); + + QMatrix4x4 mvp = viewport.projectionMatrix(); + mvp.translate(std::round(window->x() * scale), std::round(window->y() * scale)); + + const auto toXYZ = renderTarget.colorDescription()->containerColorimetry().toXYZ(); + shader->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, mvp * data.toMatrix(scale)); + shader->setUniform(GLShader::Vec4Uniform::ModulationConstant, QVector4D(rgb, rgb, rgb, a)); + shader->setUniform(GLShader::FloatUniform::Saturation, data.saturation()); + shader->setUniform(GLShader::Vec3Uniform::PrimaryBrightness, QVector3D(toXYZ(1, 0), toXYZ(1, 1), toXYZ(1, 2))); + shader->setUniform(GLShader::IntUniform::TextureWidth, m_texture->width()); + shader->setUniform(GLShader::IntUniform::TextureHeight, m_texture->height()); + shader->setColorspaceUniforms(ColorDescription::sRGB, renderTarget.colorDescription(), RenderingIntent::Perceptual); + + const bool clipping = deviceRegion != Region::infinite(); + const Region clipRegion = clipping ? viewport.transform().map(deviceRegion, renderTarget.transformedSize()) : Region::infinite(); + + if (clipping) { + glEnable(GL_SCISSOR_TEST); + } + + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + m_texture->bind(); + vbo->draw(clipRegion, GL_TRIANGLES, 0, geometry.count(), clipping); + m_texture->unbind(); + + glDisable(GL_BLEND); + if (clipping) { + glDisable(GL_SCISSOR_TEST); + } + vbo->unbindArrays(); +} + +void OffscreenEffect::drawWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *window, int mask, const Region &deviceRegion, WindowPaintData &data) +{ + const auto it = d->windows.find(window); + if (it == d->windows.end()) { + effects->drawWindow(renderTarget, viewport, window, mask, deviceRegion, data); + return; + } + OffscreenData *offscreenData = it->second.get(); + + const QRectF expandedGeometry = snapToPixels(window->expandedGeometry(), viewport.scale()); + const QRectF frameGeometry = snapToPixels(window->frameGeometry(), viewport.scale()); + + QRectF visibleRect = expandedGeometry; + visibleRect.moveTopLeft(expandedGeometry.topLeft() - frameGeometry.topLeft()); + WindowQuad quad; + quad[0] = WindowVertex(visibleRect.topLeft(), QPointF(0, 0)); + quad[1] = WindowVertex(visibleRect.topRight(), QPointF(1, 0)); + quad[2] = WindowVertex(visibleRect.bottomRight(), QPointF(1, 1)); + quad[3] = WindowVertex(visibleRect.bottomLeft(), QPointF(0, 1)); + + WindowQuadList quads; + quads.append(quad); + apply(window, mask, data, quads); + + offscreenData->maybeRender(window); + offscreenData->paint(renderTarget, viewport, window, deviceRegion, data, quads); +} + +void OffscreenEffect::handleWindowDamaged(EffectWindow *window) +{ + if (const auto it = d->windows.find(window); it != d->windows.end()) { + it->second->setDirty(); + } +} + +void OffscreenEffect::handleWindowDeleted(EffectWindow *window) +{ + unredirect(window); +} + +void OffscreenEffect::setupConnections() +{ + d->windowDeletedConnection = + connect(effects, &EffectsHandler::windowDeleted, this, &OffscreenEffect::handleWindowDeleted); +} + +void OffscreenEffect::destroyConnections() +{ + disconnect(d->windowDeletedConnection); + + d->windowDeletedConnection = {}; +} + +void OffscreenEffect::setVertexSnappingMode(RenderGeometry::VertexSnappingMode mode) +{ + d->vertexSnappingMode = mode; + for (auto &window : std::as_const(d->windows)) { + window.second->setVertexSnappingMode(mode); + } +} + +bool OffscreenEffect::blocksDirectScanout() const +{ + return false; +} + +class CrossFadeWindowData : public OffscreenData +{ +public: + QRectF frameGeometryAtCapture; +}; + +class CrossFadeEffectPrivate +{ +public: + std::map> windows; + qreal progress; +}; + +CrossFadeEffect::CrossFadeEffect(QObject *parent) + : Effect(parent) + , d(std::make_unique()) +{ +} + +CrossFadeEffect::~CrossFadeEffect() = default; + +void CrossFadeEffect::drawWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *window, int mask, const Region &deviceRegion, WindowPaintData &data) +{ + const auto it = d->windows.find(window); + + // paint the new window (if applicable) underneath + if (data.crossFadeProgress() > 0 || it == d->windows.end()) { + Effect::drawWindow(renderTarget, viewport, window, mask, deviceRegion, data); + } + + if (it == d->windows.end()) { + return; + } + CrossFadeWindowData *offscreenData = it->second.get(); + + // paint old snapshot on top + WindowPaintData previousWindowData = data; + previousWindowData.setOpacity((1.0 - data.crossFadeProgress()) * data.opacity()); + + const QRectF expandedGeometry = snapToPixels(window->expandedGeometry(), viewport.scale()); + const QRectF frameGeometry = snapToPixels(window->frameGeometry(), viewport.scale()); + + // This is for the case of *non* live effect, when the window buffer we saved has a different size + // compared to the size the window has now. The "old" window will be rendered scaled to the current + // window geometry, but everything will be scaled, also the shadow if there is any, making the window + // frame not line up anymore with window->frameGeometry() + // to fix that, we consider how much the shadow will have scaled, and use that as margins to the + // current frame geometry. this causes the scaled window to visually line up perfectly with frameGeometry, + // having the scaled shadow all outside of it. + const qreal widthRatio = offscreenData->frameGeometryAtCapture.width() / frameGeometry.width(); + const qreal heightRatio = offscreenData->frameGeometryAtCapture.height() / frameGeometry.height(); + + const QMarginsF margins( + (expandedGeometry.x() - frameGeometry.x()) / widthRatio, + (expandedGeometry.y() - frameGeometry.y()) / heightRatio, + (frameGeometry.right() - expandedGeometry.right()) / widthRatio, + (frameGeometry.bottom() - expandedGeometry.bottom()) / heightRatio); + + QRectF visibleRect = QRectF(QPointF(0, 0), frameGeometry.size()) - margins; + + WindowQuad quad; + quad[0] = WindowVertex(visibleRect.topLeft(), QPointF(0, 0)); + quad[1] = WindowVertex(visibleRect.topRight(), QPointF(1, 0)); + quad[2] = WindowVertex(visibleRect.bottomRight(), QPointF(1, 1)); + quad[3] = WindowVertex(visibleRect.bottomLeft(), QPointF(0, 1)); + + WindowQuadList quads; + quads.append(quad); + offscreenData->paint(renderTarget, viewport, window, deviceRegion, previousWindowData, quads); +} + +void CrossFadeEffect::redirect(EffectWindow *window) +{ + if (d->windows.empty()) { + connect(effects, &EffectsHandler::windowDeleted, this, &CrossFadeEffect::handleWindowDeleted); + } + + std::unique_ptr &offscreenData = d->windows[window]; + if (offscreenData) { + return; + } + offscreenData = std::make_unique(); + offscreenData->m_windowEffect = ItemEffect(window->windowItem()); + + // Avoid including blur and contrast effects. During a normal painting cycle they + // won't be included, but since we call effects->drawWindow() outside usual compositing + // cycle, we have to prevent backdrop effects kicking in. + const QVariant blurRole = window->data(WindowForceBlurRole); + window->setData(WindowForceBlurRole, QVariant()); + const QVariant contrastRole = window->data(WindowForceBackgroundContrastRole); + window->setData(WindowForceBackgroundContrastRole, QVariant()); + + effects->makeOpenGLContextCurrent(); + offscreenData->maybeRender(window); + offscreenData->frameGeometryAtCapture = window->frameGeometry(); + + window->setData(WindowForceBlurRole, blurRole); + window->setData(WindowForceBackgroundContrastRole, contrastRole); +} + +void CrossFadeEffect::unredirect(EffectWindow *window) +{ + auto it = d->windows.find(window); + if (it == d->windows.end()) { + return; + } + + if (!EglContext::currentContext()) { + effects->openglContext()->makeCurrent(); + } + + d->windows.erase(it); + if (d->windows.empty()) { + disconnect(effects, &EffectsHandler::windowDeleted, this, &CrossFadeEffect::handleWindowDeleted); + } +} + +void CrossFadeEffect::handleWindowDeleted(EffectWindow *window) +{ + unredirect(window); +} + +void CrossFadeEffect::setShader(EffectWindow *window, GLShader *shader) +{ + if (const auto it = d->windows.find(window); it != d->windows.end()) { + it->second->setShader(shader); + } +} + +bool CrossFadeEffect::blocksDirectScanout() const +{ + return false; +} + +} // namespace KWin + +#include "moc_offscreeneffect.cpp" diff --git a/local/recipes/kde/kwin/source/src/effect/offscreeneffect.h b/local/recipes/kde/kwin/source/src/effect/offscreeneffect.h new file mode 100644 index 0000000000..9704caa214 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/offscreeneffect.h @@ -0,0 +1,135 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/effect.h" +#include "scene/itemgeometry.h" + +namespace KWin +{ + +class GLShader; +class OffscreenEffectPrivate; +class CrossFadeEffectPrivate; +class ShaderEffectPrivate; + +/** + * The OffscreenEffect class is the base class for effects that paint deformed windows. + * + * Under the hood, the OffscreenEffect will paint the window into an offscreen texture + * and the offscreen texture will be transformed afterwards. + * + * The redirect() function must be called when the effect wants to transform a window. + * Once the effect is no longer interested in the window, the unredirect() function + * must be called. + * + * If a window is redirected into offscreen texture, the deform() function will be + * called to transform the offscreen texture. + */ +class KWIN_EXPORT OffscreenEffect : public Effect +{ + Q_OBJECT + +public: + explicit OffscreenEffect(QObject *parent = nullptr); + ~OffscreenEffect() override; + + static bool supported(); + +protected: + void drawWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *window, int mask, const Region &deviceRegion, WindowPaintData &data) override; + + /** + * This function must be called when the effect wants to animate the specified + * @a window. + */ + void redirect(EffectWindow *window); + /** + * This function must be called when the effect is done animating the specified + * @a window. The window will be automatically unredirected if it's deleted. + */ + void unredirect(EffectWindow *window); + + /** + * Override this function to transform the window. + */ + virtual void apply(EffectWindow *window, int mask, WindowPaintData &data, WindowQuadList &quads); + + /** + * Allows to specify a @p shader to draw the redirected texture for @p window. + * Can only be called once the window is redirected. + **/ + void setShader(EffectWindow *window, GLShader *shader); + + /** + * Set what mode to use to snap the vertices of this effect. + * + * @see RenderGeometry::VertexSnappingMode + */ + void setVertexSnappingMode(RenderGeometry::VertexSnappingMode mode); + + bool blocksDirectScanout() const override; + +private Q_SLOTS: + void handleWindowDamaged(EffectWindow *window); + void handleWindowDeleted(EffectWindow *window); + +private: + void setupConnections(); + void destroyConnections(); + + std::unique_ptr d; +}; + +/** + * The CrossFadeEffect class is the base class for effects that paints crossfades + * + * Windows are snapshotted at the time we want to start crossfading from. Hereafter we draw both the new contents + * and the old pixmap at the ratio defined by WindowPaintData::crossFadeProgress + * + * Subclasses are responsible for driving the animation and calling unredirect after animation completes. + * + * If window geometry changes shape after this point our "old" pixmap is resized to fit approximately matching + * frame geometry + */ +class KWIN_EXPORT CrossFadeEffect : public Effect +{ + Q_OBJECT +public: + explicit CrossFadeEffect(QObject *parent = nullptr); + ~CrossFadeEffect() override; + + void drawWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *window, int mask, const Region &deviceRegion, WindowPaintData &data) override; + + /** + * This function must be called when the effect wants to animate the specified + * @a window. + */ + void redirect(EffectWindow *window); + /** + * This function must be called when the effect is done animating the specified + * @a window. The window will be automatically unredirected if it's deleted. + */ + void unredirect(EffectWindow *window); + + /** + * Allows to specify a @p shader to draw the redirected texture for @p window. + * Can only be called once the window is redirected. + * @since 5.25 + **/ + void setShader(EffectWindow *window, GLShader *shader); + + bool blocksDirectScanout() const override; + + static bool supported(); + +private: + void handleWindowDeleted(EffectWindow *window); + std::unique_ptr d; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/effect/offscreenquickview.cpp b/local/recipes/kde/kwin/source/src/effect/offscreenquickview.cpp new file mode 100644 index 0000000000..fd3b64ee97 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/offscreenquickview.cpp @@ -0,0 +1,673 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "effect/offscreenquickview.h" +#include "effect/effecthandler.h" + +#include "logging_p.h" +#include "opengl/eglcontext.h" +#include "opengl/glutils.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include // for QMutableEventPoint + +namespace KWin +{ + +class Q_DECL_HIDDEN OffscreenQuickView::Private +{ +public: + std::unique_ptr m_view; + std::unique_ptr m_renderControl; + std::unique_ptr m_offscreenSurface; + std::unique_ptr m_glcontext; + std::unique_ptr m_fbo; + + std::unique_ptr m_repaintTimer; + QImage m_image; + std::unique_ptr m_textureExport; + // if we should capture a QImage after rendering into our BO. + // Used for either software QtQuick rendering and nonGL kwin rendering + bool m_useBlit = false; + bool m_visible = true; + bool m_hasAlphaChannel = true; + bool m_automaticRepaint = true; + + std::optional m_explicitDpr; + + QList touchPoints; + QSet acceptedTouchPoints; + QPointingDevice *touchDevice; + + ulong lastMousePressTime = 0; + Qt::MouseButton lastMousePressButton = Qt::NoButton; + + void releaseResources(); + + void updateTouchState(Qt::TouchPointState state, qint32 id, const QPointF &pos); +}; + +class Q_DECL_HIDDEN OffscreenQuickScene::Private +{ +public: + Private(OffscreenQuickScene *q) + : q(q) + { + } + + void createItem(const QVariantMap &initialProperties); + + OffscreenQuickScene *q; + std::unique_ptr qmlComponent; + std::unique_ptr quickItem; +}; + +OffscreenQuickView::OffscreenQuickView(ExportMode exportMode, bool alpha) + : d(new OffscreenQuickView::Private) +{ + d->m_renderControl = std::make_unique(); + + d->m_view = std::make_unique(d->m_renderControl.get()); + Q_ASSERT(d->m_view->setProperty("_KWIN_WINDOW_IS_OFFSCREEN", true) || true); + d->m_view->setFlags(Qt::FramelessWindowHint); + d->m_view->setColor(Qt::transparent); + + d->m_hasAlphaChannel = alpha; + if (exportMode == ExportMode::Image) { + d->m_useBlit = true; + } + + const bool usingGl = d->m_view->rendererInterface()->graphicsApi() == QSGRendererInterface::OpenGL; + + if (!usingGl) { + qCDebug(LIBKWINEFFECTS) << "QtQuick Software rendering mode detected"; + d->m_useBlit = true; + // explicitly do not call QQuickRenderControl::initialize, see Qt docs + } else { + QSurfaceFormat format; + format.setOption(QSurfaceFormat::ResetNotification); + format.setDepthBufferSize(16); + format.setStencilBufferSize(8); + if (alpha) { + format.setAlphaBufferSize(8); + } + + d->m_view->setFormat(format); + + auto shareContext = QOpenGLContext::globalShareContext(); + d->m_glcontext = std::make_unique(); + d->m_glcontext->setShareContext(shareContext); + d->m_glcontext->setFormat(format); + d->m_glcontext->create(); + + // and the offscreen surface + d->m_offscreenSurface = std::make_unique(); + d->m_offscreenSurface->setFormat(d->m_glcontext->format()); + d->m_offscreenSurface->create(); + + d->m_glcontext->makeCurrent(d->m_offscreenSurface.get()); + d->m_view->setGraphicsDevice(QQuickGraphicsDevice::fromOpenGLContext(d->m_glcontext.get())); + d->m_renderControl->initialize(); + d->m_glcontext->doneCurrent(); + + // On Wayland, contexts are implicitly shared and QOpenGLContext::globalShareContext() is null. + if (shareContext && !d->m_glcontext->shareContext()) { + qCDebug(LIBKWINEFFECTS) << "Failed to create a shared context, falling back to raster rendering"; + // still render via GL, but blit for presentation + d->m_useBlit = true; + } + } + + auto updateSize = [this]() { + contentItem()->setSize(d->m_view->size()); + }; + updateSize(); + connect(d->m_view.get(), &QWindow::widthChanged, this, updateSize); + connect(d->m_view.get(), &QWindow::heightChanged, this, updateSize); + + d->m_repaintTimer = std::make_unique(); + d->m_repaintTimer->setSingleShot(true); + d->m_repaintTimer->setInterval(10); + + connect(d->m_repaintTimer.get(), &QTimer::timeout, this, &OffscreenQuickView::update); + connect(d->m_renderControl.get(), &QQuickRenderControl::renderRequested, this, &OffscreenQuickView::handleRenderRequested); + connect(d->m_renderControl.get(), &QQuickRenderControl::sceneChanged, this, &OffscreenQuickView::handleSceneChanged); + + d->touchDevice = new QPointingDevice(QStringLiteral("ForwardingTouchDevice"), {}, QInputDevice::DeviceType::TouchScreen, QPointingDevice::PointerType::Finger, QInputDevice::Capability::Position, 10, {}); +} + +OffscreenQuickView::~OffscreenQuickView() +{ + disconnect(d->m_renderControl.get(), &QQuickRenderControl::renderRequested, this, &OffscreenQuickView::handleRenderRequested); + disconnect(d->m_renderControl.get(), &QQuickRenderControl::sceneChanged, this, &OffscreenQuickView::handleSceneChanged); + + if (d->m_glcontext) { + // close the view whilst we have an active GL context + d->m_glcontext->makeCurrent(d->m_offscreenSurface.get()); + } + + d->m_view.reset(); + d->m_renderControl.reset(); +} + +bool OffscreenQuickView::automaticRepaint() const +{ + return d->m_automaticRepaint; +} + +void OffscreenQuickView::setAutomaticRepaint(bool set) +{ + if (d->m_automaticRepaint != set) { + d->m_automaticRepaint = set; + + // If there's an in-flight update, disable it. + if (!d->m_automaticRepaint) { + d->m_repaintTimer->stop(); + } + } +} + +void OffscreenQuickView::setDevicePixelRatio(qreal dpr) +{ + d->m_explicitDpr = dpr; +} + +void OffscreenQuickView::handleSceneChanged() +{ + if (d->m_automaticRepaint) { + d->m_repaintTimer->start(); + } + Q_EMIT sceneChanged(); +} + +void OffscreenQuickView::handleRenderRequested() +{ + if (d->m_automaticRepaint) { + d->m_repaintTimer->start(); + } + Q_EMIT renderRequested(); +} + +void OffscreenQuickView::update() +{ + if (!d->m_visible) { + return; + } + if (d->m_view->size().isEmpty()) { + return; + } + + bool usingGl = d->m_glcontext != nullptr; + EglContext *previousContext = EglContext::currentContext(); + + if (usingGl) { + if (!d->m_glcontext->makeCurrent(d->m_offscreenSurface.get())) { + // probably a context loss event, kwin is about to reset all the effects anyway + return; + } + + qreal dpr = d->m_view->screen() ? d->m_view->screen()->devicePixelRatio() : 1.0; + if (d->m_explicitDpr.has_value()) { + dpr = d->m_explicitDpr.value(); + } + + const QSize nativeSize = d->m_view->size() * dpr; + if (!d->m_fbo || d->m_fbo->size() != nativeSize) { + d->m_textureExport.reset(nullptr); + + QOpenGLFramebufferObjectFormat fboFormat; + fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil); + fboFormat.setInternalTextureFormat(GL_RGBA8); + + d->m_fbo = std::make_unique(nativeSize, fboFormat); + if (!d->m_fbo->isValid()) { + d->m_fbo.reset(); + d->m_glcontext->doneCurrent(); + qCWarning(LIBKWINEFFECTS, "Creating FBO for OffscreenQuickView failed!"); + return; + } + } + + QQuickRenderTarget renderTarget = QQuickRenderTarget::fromOpenGLTexture(d->m_fbo->texture(), d->m_fbo->size()); + renderTarget.setDevicePixelRatio(dpr); + d->m_view->setRenderTarget(renderTarget); + } + + d->m_renderControl->polishItems(); + if (usingGl) { + d->m_renderControl->beginFrame(); + } + d->m_renderControl->sync(); + d->m_renderControl->render(); + if (usingGl) { + d->m_renderControl->endFrame(); + } + + if (usingGl) { + QQuickOpenGLUtils::resetOpenGLState(); + } + + if (d->m_useBlit) { + if (usingGl) { + d->m_image = d->m_fbo->toImage(); + d->m_image.setDevicePixelRatio(d->m_view->effectiveDevicePixelRatio()); + } else { + d->m_image = d->m_view->grabWindow(); + } + } + + if (usingGl) { + QOpenGLFramebufferObject::bindDefault(); + d->m_glcontext->doneCurrent(); + if (previousContext) { + previousContext->makeCurrent(); + } + } + Q_EMIT repaintNeeded(); +} + +void OffscreenQuickView::forwardMouseEvent(QEvent *e) +{ + if (!d->m_visible) { + return; + } + switch (e->type()) { + case QEvent::MouseMove: + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: { + QMouseEvent *me = static_cast(e); + const QPoint widgetPos = d->m_view->mapFromGlobal(me->pos()); + QMouseEvent cloneEvent(me->type(), widgetPos, me->pos(), me->button(), me->buttons(), me->modifiers()); + cloneEvent.setAccepted(false); + QCoreApplication::sendEvent(d->m_view.get(), &cloneEvent); + e->setAccepted(cloneEvent.isAccepted()); + + if (e->type() == QEvent::MouseButtonPress) { + const ulong doubleClickInterval = static_cast(QGuiApplication::styleHints()->mouseDoubleClickInterval()); + const bool doubleClick = (me->timestamp() - d->lastMousePressTime < doubleClickInterval) && me->button() == d->lastMousePressButton; + d->lastMousePressTime = me->timestamp(); + d->lastMousePressButton = me->button(); + if (doubleClick) { + d->lastMousePressButton = Qt::NoButton; + QMouseEvent doubleClickEvent(QEvent::MouseButtonDblClick, me->position(), me->globalPosition(), me->button(), me->buttons(), me->modifiers()); + QCoreApplication::sendEvent(d->m_view.get(), &doubleClickEvent); + } + } + + return; + } + case QEvent::HoverEnter: + case QEvent::HoverLeave: + case QEvent::HoverMove: { + QHoverEvent *he = static_cast(e); + const QPointF widgetPos = d->m_view->mapFromGlobal(he->position()); + const QPointF oldWidgetPos = d->m_view->mapFromGlobal(he->oldPos()); + QHoverEvent cloneEvent(he->type(), widgetPos, oldWidgetPos, he->modifiers()); + cloneEvent.setAccepted(false); + QCoreApplication::sendEvent(d->m_view.get(), &cloneEvent); + e->setAccepted(cloneEvent.isAccepted()); + return; + } + case QEvent::Wheel: { + QWheelEvent *we = static_cast(e); + const QPointF widgetPos = d->m_view->mapFromGlobal(we->position().toPoint()); + QWheelEvent cloneEvent(widgetPos, we->globalPosition(), we->pixelDelta(), we->angleDelta(), we->buttons(), + we->modifiers(), we->phase(), we->inverted()); + cloneEvent.setAccepted(false); + QCoreApplication::sendEvent(d->m_view.get(), &cloneEvent); + e->setAccepted(cloneEvent.isAccepted()); + return; + } + default: + return; + } +} + +void OffscreenQuickView::forwardKeyEvent(QKeyEvent *keyEvent) +{ + if (!d->m_visible) { + return; + } + QCoreApplication::sendEvent(d->m_view.get(), keyEvent); +} + +bool OffscreenQuickView::forwardTouchDown(qint32 id, const QPointF &pos, std::chrono::microseconds time) +{ + d->updateTouchState(Qt::TouchPointPressed, id, pos); + + QTouchEvent event(QEvent::TouchBegin, d->touchDevice, Qt::NoModifier, d->touchPoints); + event.setTimestamp(std::chrono::duration_cast(time).count()); + event.setAccepted(false); + QCoreApplication::sendEvent(d->m_view.get(), &event); + + const bool ret = event.isAccepted(); + if (ret) { + d->acceptedTouchPoints.insert(id); + } + return ret; +} + +bool OffscreenQuickView::forwardTouchMotion(qint32 id, const QPointF &pos, std::chrono::microseconds time) +{ + d->updateTouchState(Qt::TouchPointMoved, id, pos); + + if (!d->acceptedTouchPoints.contains(id)) { + return false; + } + + QTouchEvent event(QEvent::TouchUpdate, d->touchDevice, Qt::NoModifier, d->touchPoints); + event.setTimestamp(std::chrono::duration_cast(time).count()); + event.setAccepted(false); + QCoreApplication::sendEvent(d->m_view.get(), &event); + + return event.isAccepted(); +} + +bool OffscreenQuickView::forwardTouchUp(qint32 id, std::chrono::microseconds time) +{ + d->updateTouchState(Qt::TouchPointReleased, id, QPointF{}); + + if (!d->acceptedTouchPoints.contains(id)) { + return false; + } + + QTouchEvent event(QEvent::TouchEnd, d->touchDevice, Qt::NoModifier, d->touchPoints); + event.setTimestamp(std::chrono::duration_cast(time).count()); + event.setAccepted(false); + QCoreApplication::sendEvent(d->m_view.get(), &event); + + d->acceptedTouchPoints.remove(id); + + return event.isAccepted(); +} + +void OffscreenQuickView::forwardTouchCancel() +{ + d->acceptedTouchPoints.clear(); + d->touchPoints.clear(); + QTouchEvent event(QEvent::TouchCancel, d->touchDevice, Qt::NoModifier, d->touchPoints); + event.setTimestamp(std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); + event.setAccepted(false); + QCoreApplication::sendEvent(d->m_view.get(), &event); +} + +QRect OffscreenQuickView::geometry() const +{ + return d->m_view->geometry(); +} + +void OffscreenQuickView::setOpacity(qreal opacity) +{ + d->m_view->setOpacity(opacity); +} + +qreal OffscreenQuickView::opacity() const +{ + return d->m_view->opacity(); +} + +bool OffscreenQuickView::hasAlphaChannel() const +{ + return d->m_hasAlphaChannel; +} + +QQuickItem *OffscreenQuickView::contentItem() const +{ + return d->m_view->contentItem(); +} + +QQuickWindow *OffscreenQuickView::window() const +{ + return d->m_view.get(); +} + +void OffscreenQuickView::setVisible(bool visible) +{ + if (d->m_visible == visible) { + return; + } + d->m_visible = visible; + + if (visible) { + Q_EMIT d->m_renderControl->renderRequested(); + } else { + // deferred to not change GL context + QTimer::singleShot(0, this, [this]() { + d->releaseResources(); + }); + } +} + +bool OffscreenQuickView::isVisible() const +{ + return d->m_visible; +} + +void OffscreenQuickView::show() +{ + setVisible(true); +} + +void OffscreenQuickView::hide() +{ + setVisible(false); +} + +GLTexture *OffscreenQuickView::bufferAsTexture() +{ + if (d->m_useBlit) { + d->m_textureExport = GLTexture::upload(d->m_image); + if (!d->m_textureExport) { + qCWarning(LIBKWINEFFECTS, "Uploading texture for OffscreenQuickView failed!"); + } else { + d->m_textureExport->setFilter(GL_LINEAR); + } + } else { + if (!d->m_fbo) { + qCWarning(LIBKWINEFFECTS, "OffscreenQuickView has no fbo!"); + return nullptr; + } + if (!d->m_textureExport) { + d->m_textureExport = GLTexture::createNonOwningWrapper(d->m_fbo->texture(), d->m_fbo->format().internalTextureFormat(), d->m_fbo->size()); + if (d->m_textureExport) { + d->m_textureExport->setFilter(GL_LINEAR); + } + } + } + return d->m_textureExport.get(); +} + +QImage OffscreenQuickView::bufferAsImage() const +{ + return d->m_image; +} + +QSize OffscreenQuickView::size() const +{ + return d->m_view->geometry().size(); +} + +void OffscreenQuickView::setGeometry(const QRect &rect) +{ + const QRect oldGeometry = d->m_view->geometry(); + d->m_view->setGeometry(rect); + // QWindow::setGeometry() won't sync output if there's no platform window. + d->m_view->setScreen(QGuiApplication::screenAt(rect.center())); + Q_EMIT geometryChanged(oldGeometry, rect); +} + +void OffscreenQuickView::Private::releaseResources() +{ + if (m_glcontext) { + m_glcontext->makeCurrent(m_offscreenSurface.get()); + m_view->releaseResources(); + m_glcontext->doneCurrent(); + } else { + m_view->releaseResources(); + } +} + +void OffscreenQuickView::Private::updateTouchState(Qt::TouchPointState state, qint32 id, const QPointF &pos) +{ + // Remove the points that were previously in a released state, since they + // are no longer relevant. Additionally, reset the state of all remaining + // points to Stationary so we only have one touch point with a different + // state. + touchPoints.erase(std::remove_if(touchPoints.begin(), touchPoints.end(), [](QTouchEvent::TouchPoint &point) { + if (point.state() == QEventPoint::Released) { + return true; + } + QMutableEventPoint::setState(point, QEventPoint::Stationary); + return false; + }), + touchPoints.end()); + + // QtQuick Pointer Handlers incorrectly consider a touch point with ID 0 + // to be an invalid touch point. This has been fixed in Qt 6 but could not + // be fixed for Qt 5. Instead, we offset kwin's internal IDs with this + // offset to trick QtQuick into treating them as valid points. + static const qint32 idOffset = 111; + + // Find the touch point that has changed. This is separate from the above + // loop because removing the released touch points invalidates iterators. + auto changed = std::find_if(touchPoints.begin(), touchPoints.end(), [id](const QTouchEvent::TouchPoint &point) { + return point.id() == id + idOffset; + }); + + switch (state) { + case Qt::TouchPointPressed: { + if (changed != touchPoints.end()) { + return; + } + + QTouchEvent::TouchPoint point; + QMutableEventPoint::setState(point, QEventPoint::Pressed); + QMutableEventPoint::setId(point, id + idOffset); + QMutableEventPoint::setGlobalPosition(point, pos); + QMutableEventPoint::setScenePosition(point, m_view->mapFromGlobal(pos.toPoint())); + QMutableEventPoint::setPosition(point, m_view->mapFromGlobal(pos.toPoint())); + + touchPoints.append(point); + } break; + case Qt::TouchPointMoved: { + if (changed == touchPoints.end()) { + return; + } + + auto &point = *changed; + QMutableEventPoint::setGlobalLastPosition(point, point.globalPosition()); + QMutableEventPoint::setState(point, QEventPoint::Updated); + QMutableEventPoint::setScenePosition(point, m_view->mapFromGlobal(pos.toPoint())); + QMutableEventPoint::setPosition(point, m_view->mapFromGlobal(pos.toPoint())); + QMutableEventPoint::setGlobalPosition(point, pos); + } break; + case Qt::TouchPointReleased: { + if (changed == touchPoints.end()) { + return; + } + + auto &point = *changed; + QMutableEventPoint::setGlobalLastPosition(point, point.globalPosition()); + QMutableEventPoint::setState(point, QEventPoint::Released); + } break; + default: + break; + } +} + +OffscreenQuickScene::OffscreenQuickScene(OffscreenQuickView::ExportMode exportMode, bool alpha) + : OffscreenQuickView(exportMode, alpha) + , d(new OffscreenQuickScene::Private(this)) +{ +} + +OffscreenQuickScene::~OffscreenQuickScene() = default; + +void OffscreenQuickScene::setSource(const QUrl &source) +{ + setSource(source, QVariantMap()); +} + +void OffscreenQuickScene::setSource(const QUrl &source, const QVariantMap &initialProperties) +{ + if (!d->qmlComponent) { + d->qmlComponent = std::make_unique(effects->qmlEngine()); + } + + d->qmlComponent->loadUrl(source); + if (d->qmlComponent->isError()) { + qCWarning(LIBKWINEFFECTS).nospace() << "Failed to load effect quick view " << source << ": " << d->qmlComponent->errors(); + d->qmlComponent.reset(); + return; + } + d->createItem(initialProperties); +} + +void OffscreenQuickScene::loadFromModule(const QString &uri, const QString &typeName, const QVariantMap &initialProperties) +{ + if (!d->qmlComponent) { + d->qmlComponent = std::make_unique(effects->qmlEngine()); + } + + d->qmlComponent->loadFromModule(uri, typeName); + if (d->qmlComponent->isError()) { + qCWarning(LIBKWINEFFECTS).nospace() << "Failed to load effect quick view " << (uri + u'.' + typeName) << ": " << d->qmlComponent->errors(); + d->qmlComponent.reset(); + return; + } + d->createItem(initialProperties); +} + +QQuickItem *OffscreenQuickScene::rootItem() const +{ + return d->quickItem.get(); +} + +void OffscreenQuickScene::Private::createItem(const QVariantMap &initialProperties) +{ + quickItem.reset(); + + std::unique_ptr qmlObject(qmlComponent->createWithInitialProperties(initialProperties)); + QQuickItem *item = qobject_cast(qmlObject.get()); + if (!item) { + qCWarning(LIBKWINEFFECTS) << "Root object of effect quick view" << qmlComponent->url() << "is not a QQuickItem"; + return; + } + + qmlObject.release(); + quickItem.reset(item); + + item->setParentItem(q->contentItem()); + + auto updateSize = [item, this]() { + item->setSize(q->contentItem()->size()); + }; + updateSize(); + connect(q->contentItem(), &QQuickItem::widthChanged, item, updateSize); + connect(q->contentItem(), &QQuickItem::heightChanged, item, updateSize); +} + +} // namespace KWin + +#include "moc_offscreenquickview.cpp" diff --git a/local/recipes/kde/kwin/source/src/effect/offscreenquickview.h b/local/recipes/kde/kwin/source/src/effect/offscreenquickview.h new file mode 100644 index 0000000000..78263f8018 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/offscreenquickview.h @@ -0,0 +1,179 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "kwin_export.h" + +#include +#include +#include + +#include + +class QKeyEvent; +class QMouseEvent; + +class QMouseEvent; +class QKeyEvent; + +class QQmlContext; +class QQuickItem; +class QQuickWindow; + +namespace KWin +{ +class GLTexture; + +class OffscreenQuickView; + +/** + * @brief The KwinQuickView class provides a convenient API for exporting + * QtQuick scenes as buffers that can be composited in any other fashion. + * + * Contents can be fetched as a GL Texture or as a QImage + * If data is to be fetched as an image, it should be specified upfront as + * blitting is performed when we update our FBO to keep kwin's render loop + * as fast as possible. + */ +class KWIN_EXPORT OffscreenQuickView : public QObject +{ + Q_OBJECT + +public: + enum class ExportMode { + /** The contents will be available as a texture in the shared contexts. Image will be blank */ + Texture, + /** The contents will be blit during the update into a QImage buffer. */ + Image + }; + + /** + * Construct a new KWinQuickView explicitly stating an export mode. \a alpha indicates + * whether the view is translucent or not. + */ + explicit OffscreenQuickView(ExportMode exportMode = ExportMode::Texture, bool alpha = true); + + /** + * Note that this may change the current GL Context + */ + ~OffscreenQuickView(); + + QSize size() const; + + /** + * The geometry of the current view + * This may be out of sync with the current buffer size if an update is pending + */ + void setGeometry(const QRect &rect); + QRect geometry() const; + + void setOpacity(qreal opacity); + qreal opacity() const; + bool hasAlphaChannel() const; + + /** + * Render the current scene graph into the FBO. + * This is typically done automatically when the scene changes + * albeit deferred by a timer + * + * It can be manually invoked to update the contents immediately. + * Note this will change the GL context + */ + void update(); + + /** The invisible root item of the window */ + QQuickItem *contentItem() const; + QQuickWindow *window() const; + + /** + * @brief Marks the window as visible/invisible + * This can be used to release resources used by the window + * The default is true. + */ + void setVisible(bool visible); + bool isVisible() const; + + void show(); + void hide(); + + bool automaticRepaint() const; + void setAutomaticRepaint(bool set); + + void setDevicePixelRatio(qreal dpr); + + /** + * Returns the current output of the scene graph + * @note The render context must valid at the time of calling + */ + GLTexture *bufferAsTexture(); + + /** + * Returns the current output of the scene graph + */ + QImage bufferAsImage() const; + + /** + * Inject any mouse event into the QQuickWindow. + * Local coordinates are transformed + * If it is handled the event will be accepted + */ + void forwardMouseEvent(QEvent *mouseEvent); + /** + * Inject a key event into the window. + * If it is handled the event will be accepted + */ + void forwardKeyEvent(QKeyEvent *keyEvent); + + bool forwardTouchDown(qint32 id, const QPointF &pos, std::chrono::microseconds time); + bool forwardTouchMotion(qint32 id, const QPointF &pos, std::chrono::microseconds time); + bool forwardTouchUp(qint32 id, std::chrono::microseconds time); + void forwardTouchCancel(); + +Q_SIGNALS: + /** + * The frame buffer has changed, contents need re-rendering on screen + */ + void repaintNeeded(); + void geometryChanged(const QRect &oldGeometry, const QRect &newGeometry); + void renderRequested(); + void sceneChanged(); + +private: + void handleRenderRequested(); + void handleSceneChanged(); + + class Private; + std::unique_ptr d; +}; + +/** + * The KWinQuickScene class extends KWinQuickView + * adding QML support. This will represent a context + * powered by an engine + */ +class KWIN_EXPORT OffscreenQuickScene : public OffscreenQuickView +{ +public: + explicit OffscreenQuickScene(ExportMode exportMode = ExportMode::Texture, bool alpha = true); + ~OffscreenQuickScene(); + + /** top level item in the given source*/ + QQuickItem *rootItem() const; + + void setSource(const QUrl &source); + void setSource(const QUrl &source, const QVariantMap &initialProperties); + void loadFromModule(const QString &uri, const QString &typeName, const QVariantMap &initialProperties); + +private: + class Private; + std::unique_ptr d; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/effect/quickeffect.cpp b/local/recipes/kde/kwin/source/src/effect/quickeffect.cpp new file mode 100644 index 0000000000..b8e771f2c1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/quickeffect.cpp @@ -0,0 +1,643 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "effect/quickeffect.h" +#include "core/output.h" +#include "effect/effecthandler.h" + +#include "logging_p.h" + +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +static QHash s_views; + +class QuickSceneViewIncubator : public QQmlIncubator +{ +public: + QuickSceneViewIncubator(QuickSceneEffect *effect, LogicalOutput *screen, const std::function &statusChangedCallback) + : QQmlIncubator(QQmlIncubator::Asynchronous) + , m_effect(effect) + , m_screen(screen) + , m_statusChangedCallback(statusChangedCallback) + { + } + + std::unique_ptr result() + { + return std::move(m_view); + } + + void setInitialState(QObject *object) override + { + m_view = std::make_unique(m_effect, m_screen); + m_view->setAutomaticRepaint(false); + m_view->setRootItem(qobject_cast(object)); + } + + void statusChanged(QQmlIncubator::Status status) override + { + m_statusChangedCallback(this); + } + +private: + QuickSceneEffect *m_effect; + LogicalOutput *m_screen; + std::function m_statusChangedCallback; + std::unique_ptr m_view; +}; + +class QuickSceneEffectPrivate +{ +public: + static QuickSceneEffectPrivate *get(QuickSceneEffect *effect) + { + return effect->d.get(); + } + bool isItemOnScreen(QQuickItem *item, LogicalOutput *screen) const; + + QPointer delegate; + QUrl source; + struct + { + QString uri; + QString typeName; + } loadInfo; + std::map> contexts; + std::map> incubators; + std::map> views; + QPointer mouseImplicitGrab; + bool running = false; +}; + +bool QuickSceneEffectPrivate::isItemOnScreen(QQuickItem *item, LogicalOutput *screen) const +{ + if (!item || !screen) { + return false; + } + + const auto it = views.find(screen); + return it != views.end() && item->window() == it->second->window(); +} + +QuickSceneView::QuickSceneView(QuickSceneEffect *effect, LogicalOutput *screen) + : OffscreenQuickView(ExportMode::Texture, false) + , m_effect(effect) + , m_screen(screen) +{ + setGeometry(screen->geometry()); + connect(screen, &LogicalOutput::geometryChanged, this, [this, screen]() { + setGeometry(screen->geometry()); + }); + + s_views.insert(window(), this); +} + +QuickSceneView::~QuickSceneView() +{ + s_views.remove(window()); +} + +QQuickItem *QuickSceneView::rootItem() const +{ + return m_rootItem.get(); +} + +void QuickSceneView::setRootItem(QQuickItem *item) +{ + Q_ASSERT_X(item, "setRootItem", "root item cannot be null"); + m_rootItem.reset(item); + m_rootItem->setParentItem(contentItem()); + + auto updateSize = [this]() { + m_rootItem->setSize(contentItem()->size()); + }; + updateSize(); + connect(contentItem(), &QQuickItem::widthChanged, m_rootItem.get(), updateSize); + connect(contentItem(), &QQuickItem::heightChanged, m_rootItem.get(), updateSize); +} + +QuickSceneEffect *QuickSceneView::effect() const +{ + return m_effect; +} + +LogicalOutput *QuickSceneView::screen() const +{ + return m_screen; +} + +bool QuickSceneView::isDirty() const +{ + return m_dirty; +} + +void QuickSceneView::markDirty() +{ + m_dirty = true; +} + +void QuickSceneView::resetDirty() +{ + m_dirty = false; +} + +void QuickSceneView::scheduleRepaint() +{ + markDirty(); + effects->addRepaint(geometry()); +} + +QuickSceneView *QuickSceneView::findView(QQuickItem *item) +{ + return s_views.value(item->window()); +} + +QuickSceneView *QuickSceneView::qmlAttachedProperties(QObject *object) +{ + QQuickItem *item = qobject_cast(object); + if (item) { + if (QuickSceneView *view = findView(item)) { + return view; + } + } + qCWarning(LIBKWINEFFECTS) << "Could not find SceneView for" << object; + return nullptr; +} + +QuickSceneEffect::QuickSceneEffect(QObject *parent) + : Effect(parent) + , d(new QuickSceneEffectPrivate) +{ +} + +QuickSceneEffect::~QuickSceneEffect() +{ +} + +bool QuickSceneEffect::supported() +{ + return effects->compositingType() == OpenGLCompositing; +} + +void QuickSceneEffect::checkItemDraggedOutOfScreen(QQuickItem *item) +{ + const QRectF globalGeom = QRectF(item->mapToGlobal(QPointF(0, 0)), QSizeF(item->width(), item->height())); + QList screens; + + for (const auto &[screen, view] : d->views) { + if (!d->isItemOnScreen(item, screen) && screen->geometry().intersects(globalGeom.toRect())) { + screens << screen; + } + } + + Q_EMIT itemDraggedOutOfScreen(item, screens); +} + +void QuickSceneEffect::checkItemDroppedOutOfScreen(const QPointF &globalPos, QQuickItem *item) +{ + const auto it = std::find_if(d->views.begin(), d->views.end(), [this, globalPos, item](const auto &view) { + LogicalOutput *screen = view.first; + return !d->isItemOnScreen(item, screen) && screen->geometry().contains(globalPos.toPoint()); + }); + if (it != d->views.end()) { + Q_EMIT itemDroppedOutOfScreen(globalPos, item, it->first); + } +} + +bool QuickSceneEffect::eventFilter(QObject *watched, QEvent *event) +{ + if (event->type() == QEvent::CursorChange) { + if (const QWindow *window = qobject_cast(watched)) { + effects->defineCursor(window->cursor().shape()); + } + } + return false; +} + +bool QuickSceneEffect::isRunning() const +{ + return d->running; +} + +void QuickSceneEffect::setRunning(bool running) +{ + if (d->running != running) { + if (running) { + startInternal(); + } else { + stopInternal(); + } + } +} + +QUrl QuickSceneEffect::source() const +{ + return d->source; +} + +void QuickSceneEffect::setSource(const QUrl &url) +{ + if (isRunning()) { + qWarning() << "Cannot change QuickSceneEffect.source while running"; + return; + } + if (d->source != url) { + d->source = url; + d->delegate.clear(); + d->loadInfo = {}; + } +} + +QQmlComponent *QuickSceneEffect::delegate() const +{ + return d->delegate.get(); +} + +void QuickSceneEffect::loadFromModule(const QString &uri, const QString &typeName) +{ + if (isRunning()) { + qWarning() << "Cannot call QuickSceneEffect::loadFromModule while running"; + return; + } + if (d->loadInfo.uri != uri && d->loadInfo.typeName != typeName) { + d->delegate.clear(); + d->source = QUrl(); + d->loadInfo.uri = uri; + d->loadInfo.typeName = typeName; + } +} + +void QuickSceneEffect::setDelegate(QQmlComponent *delegate) +{ + if (d->delegate.get() != delegate) { + d->source = QUrl(); + d->loadInfo = {}; + d->delegate = delegate; + if (isRunning()) { + auto reloadViews = [this]() { + if (!isRunning()) { + return; + } + const auto screens = effects->screens(); + for (LogicalOutput *screen : screens) { + removeScreen(screen); + addScreen(screen); + } + }; + QMetaObject::invokeMethod(this, reloadViews, Qt::QueuedConnection); + } + Q_EMIT delegateChanged(); + } +} + +QuickSceneView *QuickSceneEffect::viewForScreen(LogicalOutput *screen) const +{ + const auto it = d->views.find(screen); + return it == d->views.end() ? nullptr : it->second.get(); +} + +QuickSceneView *QuickSceneEffect::viewAt(const QPoint &pos) const +{ + const auto it = std::find_if(d->views.begin(), d->views.end(), [pos](const auto &view) { + return view.second->geometry().contains(pos); + }); + return it == d->views.end() ? nullptr : it->second.get(); +} + +QuickSceneView *QuickSceneEffect::activeView() const +{ + auto it = std::find_if(d->views.begin(), d->views.end(), [](const auto &view) { + return view.second->window()->activeFocusItem(); + }); + if (it == d->views.end()) { + it = d->views.find(effects->activeScreen()); + } + return it == d->views.end() ? nullptr : it->second.get(); +} + +KWin::QuickSceneView *QuickSceneEffect::getView(Qt::Edge edge) +{ + auto screenView = activeView(); + + QuickSceneView *candidate = nullptr; + + for (const auto &[screen, view] : d->views) { + switch (edge) { + case Qt::LeftEdge: + if (view->geometry().left() < screenView->geometry().left()) { + // Look for the nearest view from the current + if (!candidate || view->geometry().left() > candidate->geometry().left() || (view->geometry().left() == candidate->geometry().left() && view->geometry().top() > candidate->geometry().top())) { + candidate = view.get(); + } + } + break; + case Qt::TopEdge: + if (view->geometry().top() < screenView->geometry().top()) { + if (!candidate || view->geometry().top() > candidate->geometry().top() || (view->geometry().top() == candidate->geometry().top() && view->geometry().left() > candidate->geometry().left())) { + candidate = view.get(); + } + } + break; + case Qt::RightEdge: + if (view->geometry().right() > screenView->geometry().right()) { + if (!candidate || view->geometry().right() < candidate->geometry().right() || (view->geometry().right() == candidate->geometry().right() && view->geometry().top() > candidate->geometry().top())) { + candidate = view.get(); + } + } + break; + case Qt::BottomEdge: + if (view->geometry().bottom() > screenView->geometry().bottom()) { + if (!candidate || view->geometry().bottom() < candidate->geometry().bottom() || (view->geometry().bottom() == candidate->geometry().bottom() && view->geometry().left() > candidate->geometry().left())) { + candidate = view.get(); + } + } + break; + } + } + + return candidate; +} + +void QuickSceneEffect::activateView(QuickSceneView *view) +{ + if (!view) { + return; + } + + auto *av = activeView(); + // Already properly active? + if (view == av && av->window()->activeFocusItem()) { + return; + } + + for (const auto &[screen, otherView] : d->views) { + if (otherView.get() == view && !view->window()->activeFocusItem()) { + QWindowSystemInterface::handleFocusWindowChanged(view->window()); + } + } + + Q_EMIT activeViewChanged(view); +} + +void QuickSceneEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + data.mask |= PAINT_SCREEN_TRANSFORMED; +} + +void QuickSceneEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) +{ + const auto it = d->views.find(screen); + if (it != d->views.end()) { + const auto &screenView = it->second; + if (screenView->isDirty()) { + screenView->resetDirty(); + screenView->update(); + } + effects->renderOffscreenQuickView(renderTarget, viewport, screenView.get()); + } +} + +bool QuickSceneEffect::isActive() const +{ + return !d->views.empty() && !effects->isScreenLocked(); +} + +QVariantMap QuickSceneEffect::initialProperties(LogicalOutput *screen) +{ + return QVariantMap(); +} + +void QuickSceneEffect::handleScreenAdded(LogicalOutput *screen) +{ + addScreen(screen); +} + +void QuickSceneEffect::handleScreenRemoved(LogicalOutput *screen) +{ + removeScreen(screen); +} + +void QuickSceneEffect::addScreen(LogicalOutput *screen) +{ + auto properties = initialProperties(screen); + properties["width"] = screen->geometry().width(); + properties["height"] = screen->geometry().height(); + + auto incubator = new QuickSceneViewIncubator(this, screen, [this, screen](QuickSceneViewIncubator *incubator) { + if (incubator->isReady()) { + auto view = incubator->result(); + if (view->contentItem()) { + view->contentItem()->setFocus(false); + } + connect(view.get(), &QuickSceneView::renderRequested, view.get(), &QuickSceneView::scheduleRepaint); + connect(view.get(), &QuickSceneView::sceneChanged, view.get(), &QuickSceneView::scheduleRepaint); + view->scheduleRepaint(); + // view is returned via invokables elsewhere + QJSEngine::setObjectOwnership(view.get(), QJSEngine::CppOwnership); + d->views[screen] = std::move(view); + } else if (incubator->isError()) { + qCWarning(LIBKWINEFFECTS) << "Could not create a view for QML file" << d->delegate->url(); + qCWarning(LIBKWINEFFECTS) << incubator->errors(); + } + }); + incubator->setInitialProperties(properties); + + QQmlContext *parentContext; + if (QQmlContext *context = d->delegate->creationContext()) { + parentContext = context; + } else if (QQmlContext *context = qmlContext(this)) { + parentContext = context; + } else { + parentContext = d->delegate->engine()->rootContext(); + } + QQmlContext *context = new QQmlContext(parentContext); + + d->contexts[screen].reset(context); + d->incubators[screen].reset(incubator); + d->delegate->create(*incubator, context); +} + +void QuickSceneEffect::removeScreen(LogicalOutput *screen) +{ + d->views.erase(screen); + d->incubators.erase(screen); + d->contexts.erase(screen); +} + +void QuickSceneEffect::startInternal() +{ + if (effects->activeFullScreenEffect()) { + return; + } + + if (!d->delegate) { + if (Q_UNLIKELY(d->source.isEmpty() && d->loadInfo.uri.isEmpty())) { + qWarning() << "QuickSceneEffect.source is empty. Did you forget to call setSource() or loadFromModule()?"; + return; + } + + d->delegate = new QQmlComponent(effects->qmlEngine(), this); + + if (!d->source.isEmpty()) { + d->delegate->loadUrl(d->source); + if (d->delegate->isError()) { + qWarning().nospace() << "Failed to load " << d->source << ": " << d->delegate->errors(); + d->delegate.clear(); + return; + } + } else { + d->delegate->loadFromModule(d->loadInfo.uri, d->loadInfo.typeName); + if (d->delegate->isError()) { + qWarning().nospace() << "Failed to load " << (d->loadInfo.uri + u'.' + d->loadInfo.typeName) << d->delegate->errors(); + d->delegate.clear(); + return; + } + } + + Q_EMIT delegateChanged(); + } + + if (!d->delegate->isReady()) { + return; + } + + if (!effects->grabKeyboard(this)) { + return; + } + + effects->startMouseInterception(this, Qt::ArrowCursor); + + effects->setActiveFullScreenEffect(this); + d->running = true; + + // Install an event filter to monitor cursor shape changes. + qApp->installEventFilter(this); + + const QList screens = effects->screens(); + for (LogicalOutput *screen : screens) { + addScreen(screen); + } + + // Ensure one view has an active focus item + activateView(activeView()); + + connect(effects, &EffectsHandler::screenAdded, this, &QuickSceneEffect::handleScreenAdded); + connect(effects, &EffectsHandler::screenRemoved, this, &QuickSceneEffect::handleScreenRemoved); +} + +void QuickSceneEffect::stopInternal() +{ + disconnect(effects, &EffectsHandler::screenAdded, this, &QuickSceneEffect::handleScreenAdded); + disconnect(effects, &EffectsHandler::screenRemoved, this, &QuickSceneEffect::handleScreenRemoved); + + d->incubators.clear(); + d->views.clear(); + d->contexts.clear(); + d->running = false; + qApp->removeEventFilter(this); + effects->ungrabKeyboard(); + effects->stopMouseInterception(this); + effects->setActiveFullScreenEffect(nullptr); + effects->addRepaintFull(); +} + +void QuickSceneEffect::windowInputMouseEvent(QEvent *event) +{ + Qt::MouseButtons buttons; + QPoint globalPosition; + if (QMouseEvent *mouseEvent = dynamic_cast(event)) { + buttons = mouseEvent->buttons(); + globalPosition = mouseEvent->globalPosition().toPoint(); + } else if (QWheelEvent *wheelEvent = dynamic_cast(event)) { + buttons = wheelEvent->buttons(); + globalPosition = wheelEvent->globalPosition().toPoint(); + } else { + return; + } + + if (buttons) { + if (!d->mouseImplicitGrab) { + d->mouseImplicitGrab = viewAt(globalPosition); + } + } + + QuickSceneView *target = d->mouseImplicitGrab; + if (!target) { + target = viewAt(globalPosition); + } + + if (!buttons) { + d->mouseImplicitGrab = nullptr; + } + + if (target) { + if (buttons) { + activateView(target); + } + target->forwardMouseEvent(event); + } +} + +void QuickSceneEffect::grabbedKeyboardEvent(QKeyEvent *keyEvent) +{ + auto *screenView = activeView(); + + if (screenView) { + // ActiveView may not have an activeFocusItem yet + activateView(screenView); + screenView->forwardKeyEvent(keyEvent); + } +} + +bool QuickSceneEffect::touchDown(qint32 id, const QPointF &pos, std::chrono::microseconds time) +{ + for (const auto &[screen, screenView] : d->views) { + if (screenView->geometry().contains(pos.toPoint())) { + activateView(screenView.get()); + return screenView->forwardTouchDown(id, pos, time); + } + } + return false; +} + +bool QuickSceneEffect::touchMotion(qint32 id, const QPointF &pos, std::chrono::microseconds time) +{ + for (const auto &[screen, screenView] : d->views) { + if (screenView->geometry().contains(pos.toPoint())) { + return screenView->forwardTouchMotion(id, pos, time); + } + } + return false; +} + +bool QuickSceneEffect::touchUp(qint32 id, std::chrono::microseconds time) +{ + for (const auto &[screen, screenView] : d->views) { + if (screenView->forwardTouchUp(id, time)) { + return true; + } + } + return false; +} + +void QuickSceneEffect::touchCancel() +{ + for (const auto &[screen, screenView] : d->views) { + screenView->forwardTouchCancel(); + } +} + +} // namespace KWin + +#include "moc_quickeffect.cpp" diff --git a/local/recipes/kde/kwin/source/src/effect/quickeffect.h b/local/recipes/kde/kwin/source/src/effect/quickeffect.h new file mode 100644 index 0000000000..0a6e976bf6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/quickeffect.h @@ -0,0 +1,200 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/output.h" +#include "effect/effect.h" +#include "effect/offscreenquickview.h" + +#include + +namespace KWin +{ + +class QuickSceneEffect; +class QuickSceneEffectPrivate; + +/** + * The QuickSceneView represents a QtQuick scene view on a particular screen. + * + * The root QML object must be a QQuickItem or a subclass of QQuickItem, other + * cases are unsupported. + * + * @see QuickSceneEffect, OffscreenQuickView + */ +class KWIN_EXPORT QuickSceneView : public OffscreenQuickView +{ + Q_OBJECT + Q_PROPERTY(QuickSceneEffect *effect READ effect CONSTANT) + Q_PROPERTY(LogicalOutput *screen READ screen CONSTANT) + Q_PROPERTY(QQuickItem *rootItem READ rootItem CONSTANT) + +public: + explicit QuickSceneView(QuickSceneEffect *effect, LogicalOutput *screen); + ~QuickSceneView() override; + + QuickSceneEffect *effect() const; + LogicalOutput *screen() const; + + QQuickItem *rootItem() const; + void setRootItem(QQuickItem *item); + + bool isDirty() const; + void markDirty(); + void resetDirty(); + + static QuickSceneView *findView(QQuickItem *item); + static QuickSceneView *qmlAttachedProperties(QObject *object); + +public Q_SLOTS: + void scheduleRepaint(); + +private: + QuickSceneEffect *m_effect; + LogicalOutput *m_screen; + std::unique_ptr m_rootItem; + bool m_dirty = false; +}; + +/** + * The QuickSceneEffect class provides a convenient way to write fullscreen + * QtQuick-based effects. + * + * QuickSceneView objects are managed internally. + * + * The QuickSceneEffect takes care of forwarding input events to QuickSceneView and + * rendering. You can override relevant hooks from the Effect class to customize input + * handling or rendering, although it's highly recommended that you avoid doing that. + * + * @see QuickSceneView + */ +class KWIN_EXPORT QuickSceneEffect : public Effect +{ + Q_OBJECT + Q_PROPERTY(QuickSceneView *activeView READ activeView NOTIFY activeViewChanged) + Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) + +public: + explicit QuickSceneEffect(QObject *parent = nullptr); + ~QuickSceneEffect() override; + + /** + * Returns @c true if the effect is running; otherwise returns @c false. + */ + bool isRunning() const; + + /** + * Starts or stops the effect depending on @a running. + */ + void setRunning(bool running); + + QuickSceneView *activeView() const; + + /** + * Returns the scene view on the specified screen + */ + Q_INVOKABLE QuickSceneView *viewForScreen(LogicalOutput *screen) const; + + /** + * Returns the view at the specified @a pos in the global screen coordinates. + */ + Q_INVOKABLE QuickSceneView *viewAt(const QPoint &pos) const; + + /** + * Get a view at the given direction from the active view + * Returns null if no other views exist in the given direction + */ + Q_INVOKABLE KWin::QuickSceneView *getView(Qt::Edge edge); + + /** + * Sets the given @a view as active. It will get a focusin event and all the other views will be set as inactive + */ + Q_INVOKABLE void activateView(QuickSceneView *view); + + /** + * The delegate provides a template defining the contents of each instantiated screen view. + */ + QQmlComponent *delegate() const; + void setDelegate(QQmlComponent *delegate); + + /** + * Returns the source URL. + */ + QUrl source() const; + + /** + * Sets the source url to @a url. Note that the QML component will be loaded the next + * time the effect is started. + * + * While the effect is running, the source url cannot be changed. + * + * In order to provide your custom initial properties, you need to override + * the initialProperties() function. + */ + void setSource(const QUrl &url); + + /** + * Use the QML component identified by @a uri and @a typename. Note that the QML component will + * be loaded the next time the effect is started. + * + * Cannot be called while the effect is running. + * + * In order to provide your custom initial properties, you need to override + * the initialProperties() function. + */ + void loadFromModule(const QString &uri, const QString &typeName); + + bool eventFilter(QObject *watched, QEvent *event) override; + + void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override; + void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &logicalRegion, LogicalOutput *screen) override; + bool isActive() const override; + + void windowInputMouseEvent(QEvent *event) override; + void grabbedKeyboardEvent(QKeyEvent *keyEvent) override; + + bool touchDown(qint32 id, const QPointF &pos, std::chrono::microseconds time) override; + bool touchMotion(qint32 id, const QPointF &pos, std::chrono::microseconds time) override; + bool touchUp(qint32 id, std::chrono::microseconds time) override; + void touchCancel() override; + + static bool supported(); + + Q_INVOKABLE void checkItemDraggedOutOfScreen(QQuickItem *item); + Q_INVOKABLE void checkItemDroppedOutOfScreen(const QPointF &globalPos, QQuickItem *item); + +Q_SIGNALS: + void itemDraggedOutOfScreen(QQuickItem *item, QList screens); + void itemDroppedOutOfScreen(const QPointF &globalPos, QQuickItem *item, LogicalOutput *screen); + void activeViewChanged(KWin::QuickSceneView *view); + void delegateChanged(); + +protected: + /** + * Reimplement this function to provide your initial properties for the scene view + * on the specified @a screen. + * + * @see QQmlComponent::createWithInitialProperties() + */ + virtual QVariantMap initialProperties(LogicalOutput *screen); + +private: + void handleScreenAdded(LogicalOutput *screen); + void handleScreenRemoved(LogicalOutput *screen); + + void addScreen(LogicalOutput *screen); + void removeScreen(LogicalOutput *screen); + void startInternal(); + void stopInternal(); + + std::unique_ptr d; + friend class QuickSceneEffectPrivate; +}; + +} // namespace KWin + +QML_DECLARE_TYPEINFO(KWin::QuickSceneView, QML_HAS_ATTACHED_PROPERTIES) diff --git a/local/recipes/kde/kwin/source/src/effect/timeline.cpp b/local/recipes/kde/kwin/source/src/effect/timeline.cpp new file mode 100644 index 0000000000..3d377dc5c7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/timeline.cpp @@ -0,0 +1,209 @@ +/* + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "effect/timeline.h" + +namespace KWin +{ + +class Q_DECL_HIDDEN TimeLine::Data : public QSharedData +{ +public: + std::chrono::milliseconds duration; + Direction direction; + QEasingCurve easingCurve; + + std::chrono::milliseconds elapsed = std::chrono::milliseconds::zero(); + std::optional lastTimestamp = std::nullopt; + bool done = false; + RedirectMode sourceRedirectMode = RedirectMode::Relaxed; + RedirectMode targetRedirectMode = RedirectMode::Strict; +}; + +TimeLine::TimeLine(std::chrono::milliseconds duration, Direction direction) + : d(new Data) +{ + Q_ASSERT(duration > std::chrono::milliseconds::zero()); + d->duration = duration; + d->direction = direction; +} + +TimeLine::TimeLine(const TimeLine &other) + : d(other.d) +{ +} + +TimeLine::~TimeLine() = default; + +qreal TimeLine::progress() const +{ + return static_cast(d->elapsed.count()) / d->duration.count(); +} + +qreal TimeLine::value() const +{ + const qreal t = progress(); + return d->easingCurve.valueForProgress( + d->direction == Backward ? 1.0 - t : t); +} + +void TimeLine::advance(std::chrono::milliseconds timestamp) +{ + if (d->done) { + return; + } + + std::chrono::milliseconds delta = std::chrono::milliseconds::zero(); + if (d->lastTimestamp.has_value()) { + delta = timestamp - d->lastTimestamp.value(); + } + + Q_ASSERT(delta >= std::chrono::milliseconds::zero()); + d->lastTimestamp = timestamp; + + d->elapsed += delta; + if (d->elapsed >= d->duration) { + d->elapsed = d->duration; + d->done = true; + d->lastTimestamp = std::nullopt; + } +} + +std::chrono::milliseconds TimeLine::elapsed() const +{ + return d->elapsed; +} + +void TimeLine::setElapsed(std::chrono::milliseconds elapsed) +{ + Q_ASSERT(elapsed >= std::chrono::milliseconds::zero()); + if (elapsed == d->elapsed) { + return; + } + + reset(); + + d->elapsed = elapsed; + + if (d->elapsed >= d->duration) { + d->elapsed = d->duration; + d->done = true; + d->lastTimestamp = std::nullopt; + } +} + +std::chrono::milliseconds TimeLine::duration() const +{ + return d->duration; +} + +void TimeLine::setDuration(std::chrono::milliseconds duration) +{ + Q_ASSERT(duration > std::chrono::milliseconds::zero()); + if (duration == d->duration) { + return; + } + d->elapsed = std::chrono::milliseconds(qRound(progress() * duration.count())); + d->duration = duration; + if (d->elapsed == d->duration) { + d->done = true; + d->lastTimestamp = std::nullopt; + } +} + +TimeLine::Direction TimeLine::direction() const +{ + return d->direction; +} + +void TimeLine::setDirection(TimeLine::Direction direction) +{ + if (d->direction == direction) { + return; + } + + d->direction = direction; + + if (d->elapsed > std::chrono::milliseconds::zero() + || d->sourceRedirectMode == RedirectMode::Strict) { + d->elapsed = d->duration - d->elapsed; + } + + if (d->done && d->targetRedirectMode == RedirectMode::Relaxed) { + d->done = false; + } + + if (d->elapsed >= d->duration) { + d->done = true; + d->lastTimestamp = std::nullopt; + } +} + +void TimeLine::toggleDirection() +{ + setDirection(d->direction == Forward ? Backward : Forward); +} + +QEasingCurve TimeLine::easingCurve() const +{ + return d->easingCurve; +} + +void TimeLine::setEasingCurve(const QEasingCurve &easingCurve) +{ + d->easingCurve = easingCurve; +} + +void TimeLine::setEasingCurve(QEasingCurve::Type type) +{ + d->easingCurve.setType(type); +} + +bool TimeLine::running() const +{ + return d->elapsed != std::chrono::milliseconds::zero() + && d->elapsed != d->duration; +} + +bool TimeLine::done() const +{ + return d->done; +} + +void TimeLine::reset() +{ + d->lastTimestamp = std::nullopt; + d->elapsed = std::chrono::milliseconds::zero(); + d->done = false; +} + +TimeLine::RedirectMode TimeLine::sourceRedirectMode() const +{ + return d->sourceRedirectMode; +} + +void TimeLine::setSourceRedirectMode(RedirectMode mode) +{ + d->sourceRedirectMode = mode; +} + +TimeLine::RedirectMode TimeLine::targetRedirectMode() const +{ + return d->targetRedirectMode; +} + +void TimeLine::setTargetRedirectMode(RedirectMode mode) +{ + d->targetRedirectMode = mode; +} + +TimeLine &TimeLine::operator=(const TimeLine &other) +{ + d = other.d; + return *this; +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/effect/timeline.h b/local/recipes/kde/kwin/source/src/effect/timeline.h new file mode 100644 index 0000000000..3cef95e35a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/timeline.h @@ -0,0 +1,270 @@ +/* + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "kwin_export.h" + +#include +#include + +#include + +namespace KWin +{ + +/** + * The TimeLine class is a helper for controlling animations. + */ +class KWIN_EXPORT TimeLine +{ +public: + /** + * Direction of the timeline. + * + * When the direction of the timeline is Forward, the progress + * value will go from 0.0 to 1.0. + * + * When the direction of the timeline is Backward, the progress + * value will go from 1.0 to 0.0. + */ + enum Direction { + Forward, + Backward + }; + + /** + * Constructs a new instance of TimeLine. + * + * @param duration Duration of the timeline, in milliseconds + * @param direction Direction of the timeline + * @since 5.14 + */ + explicit TimeLine(std::chrono::milliseconds duration = std::chrono::milliseconds(1000), + Direction direction = Forward); + TimeLine(const TimeLine &other); + ~TimeLine(); + + /** + * Returns the current value of the timeline. + * + * @since 5.14 + */ + qreal value() const; + + /** + * Advances the timeline to the specified @a timestamp. + */ + void advance(std::chrono::milliseconds timestamp); + + /** + * Returns the number of elapsed milliseconds. + * + * @see setElapsed + * @since 5.14 + */ + std::chrono::milliseconds elapsed() const; + + /** + * Sets the number of elapsed milliseconds. + * + * This method overwrites previous value of elapsed milliseconds. + * If the new value of elapsed milliseconds is greater or equal + * to duration of the timeline, the timeline will be finished, i.e. + * proceeding TimeLine::done method calls will return @c true. + * Please don't use it. Instead, use TimeLine::update. + * + * @note The new number of elapsed milliseconds should be a non-negative + * number, i.e. it should be greater or equal to 0. + * + * @param elapsed The new number of elapsed milliseconds + * @see elapsed + * @since 5.14 + */ + void setElapsed(std::chrono::milliseconds elapsed); + + /** + * Returns the duration of the timeline. + * + * @returns Duration of the timeline, in milliseconds + * @see setDuration + * @since 5.14 + */ + std::chrono::milliseconds duration() const; + + /** + * Sets the duration of the timeline. + * + * In addition to setting new value of duration, the timeline will + * try to retarget the number of elapsed milliseconds to match + * as close as possible old progress value. If the new duration + * is much smaller than old duration, there is a big chance that + * the timeline will be finished after setting new duration. + * + * @note The new duration should be a positive number, i.e. it + * should be greater or equal to 1. + * + * @param duration The new duration of the timeline, in milliseconds + * @see duration + * @since 5.14 + */ + void setDuration(std::chrono::milliseconds duration); + + /** + * Returns the direction of the timeline. + * + * @returns Direction of the timeline(TimeLine::Forward or TimeLine::Backward) + * @see setDirection + * @see toggleDirection + * @since 5.14 + */ + Direction direction() const; + + /** + * Sets the direction of the timeline. + * + * @param direction The new direction of the timeline + * @see direction + * @see toggleDirection + * @since 5.14 + */ + void setDirection(Direction direction); + + /** + * Toggles the direction of the timeline. + * + * If the direction of the timeline was TimeLine::Forward, it becomes + * TimeLine::Backward, and vice verca. + * + * @see direction + * @see setDirection + * @since 5.14 + */ + void toggleDirection(); + + /** + * Returns the easing curve of the timeline. + * + * @see setEasingCurve + * @since 5.14 + */ + QEasingCurve easingCurve() const; + + /** + * Sets new easing curve. + * + * @param easingCurve An easing curve to be set + * @see easingCurve + * @since 5.14 + */ + void setEasingCurve(const QEasingCurve &easingCurve); + + /** + * Sets new easing curve by providing its type. + * + * @param type Type of the easing curve(e.g. QEasingCurve::InCubic, etc) + * @see easingCurve + * @since 5.14 + */ + void setEasingCurve(QEasingCurve::Type type); + + /** + * Returns whether the timeline is currently in progress. + * + * @see done + * @since 5.14 + */ + bool running() const; + + /** + * Returns whether the timeline is finished. + * + * @see reset + * @since 5.14 + */ + bool done() const; + + /** + * Resets the timeline to initial state. + * + * @since 5.14 + */ + void reset(); + + enum class RedirectMode { + Strict, + Relaxed + }; + + /** + * Returns the redirect mode for the source position. + * + * The redirect mode controls behavior of the timeline when its direction is + * changed at the source position, e.g. what should we do when the timeline + * initially goes forward and we change its direction to go backward. + * + * In the strict mode, the timeline will stop. + * + * In the relaxed mode, the timeline will go in the new direction. For example, + * if the timeline goes forward(from 0 to 1), then with the new direction it + * will go backward(from 1 to 0). + * + * The default is RedirectMode::Relaxed. + * + * @see targetRedirectMode + * @since 5.15 + */ + RedirectMode sourceRedirectMode() const; + + /** + * Sets the redirect mode for the source position. + * + * @param mode The new mode. + * @since 5.15 + */ + void setSourceRedirectMode(RedirectMode mode); + + /** + * Returns the redirect mode for the target position. + * + * The redirect mode controls behavior of the timeline when its direction is + * changed at the target position. + * + * In the strict mode, subsequent update calls won't have any effect on the + * current value of the timeline. + * + * In the relaxed mode, the timeline will go in the new direction. + * + * The default is RedirectMode::Strict. + * + * @see sourceRedirectMode + * @since 5.15 + */ + RedirectMode targetRedirectMode() const; + + /** + * Sets the redirect mode for the target position. + * + * @param mode The new mode. + * @since 5.15 + */ + void setTargetRedirectMode(RedirectMode mode); + + TimeLine &operator=(const TimeLine &other); + + /** + * @returns a value between 0 and 1 defining the progress of the timeline + * + * @since 5.23 + */ + qreal progress() const; + +private: + class Data; + QSharedDataPointer d; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/effect/xcb.h b/local/recipes/kde/kwin/source/src/effect/xcb.h new file mode 100644 index 0000000000..ee0271f897 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/effect/xcb.h @@ -0,0 +1,55 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once +#include "config-kwin.h" + +#if !KWIN_BUILD_X11 +#error Do not include on non-X11 builds +#endif + +#include +#include +#include + +namespace KWin +{ + +inline KWIN_EXPORT xcb_connection_t *connection() +{ + return reinterpret_cast(qApp->property("x11Connection").value()); +} + +inline KWIN_EXPORT xcb_window_t rootWindow() +{ + return qApp->property("x11RootWindow").value(); +} + +void KWIN_EXPORT grabXServer(); +void KWIN_EXPORT ungrabXServer(); + +/** + * Small helper class which performs grabXServer in the ctor and + * ungrabXServer in the dtor. Use this class to ensure that grab and + * ungrab are matched. + */ +class XServerGrabber +{ +public: + XServerGrabber() + { + grabXServer(); + } + ~XServerGrabber() + { + ungrabXServer(); + } +}; + +} diff --git a/local/recipes/kde/kwin/source/src/events.cpp b/local/recipes/kde/kwin/source/src/events.cpp new file mode 100644 index 0000000000..459e1991d5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/events.cpp @@ -0,0 +1,747 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* + + This file contains things relevant to handling incoming events. + +*/ + +#include "atoms.h" +#include "cursor.h" +#include "focuschain.h" +#include "group.h" +#include "input.h" +#include "netinfo.h" +#include "pointer_input.h" +#include "touch_input.h" +#include "useractions.h" +#include "utils/envvar.h" +#include "utils/xcbutils.h" +#include "wayland/xwaylandshell_v1.h" +#include "wayland_server.h" +#include "workspace.h" +#include "x11window.h" + +#include + +namespace KWin +{ + +// **************************************** +// Workspace +// **************************************** + +static xcb_window_t findEventWindow(xcb_generic_event_t *event) +{ + const uint8_t eventType = event->response_type & ~0x80; + switch (eventType) { + case XCB_KEY_PRESS: + case XCB_KEY_RELEASE: + return reinterpret_cast(event)->event; + case XCB_BUTTON_PRESS: + case XCB_BUTTON_RELEASE: + return reinterpret_cast(event)->event; + case XCB_MOTION_NOTIFY: + return reinterpret_cast(event)->event; + case XCB_ENTER_NOTIFY: + case XCB_LEAVE_NOTIFY: + return reinterpret_cast(event)->event; + case XCB_FOCUS_IN: + case XCB_FOCUS_OUT: + return reinterpret_cast(event)->event; + case XCB_EXPOSE: + return reinterpret_cast(event)->window; + case XCB_GRAPHICS_EXPOSURE: + return reinterpret_cast(event)->drawable; + case XCB_NO_EXPOSURE: + return reinterpret_cast(event)->drawable; + case XCB_VISIBILITY_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_CREATE_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_DESTROY_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_UNMAP_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_MAP_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_MAP_REQUEST: + return reinterpret_cast(event)->window; + case XCB_REPARENT_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_CONFIGURE_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_CONFIGURE_REQUEST: + return reinterpret_cast(event)->window; + case XCB_GRAVITY_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_RESIZE_REQUEST: + return reinterpret_cast(event)->window; + case XCB_CIRCULATE_NOTIFY: + case XCB_CIRCULATE_REQUEST: + return reinterpret_cast(event)->window; + case XCB_PROPERTY_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_COLORMAP_NOTIFY: + return reinterpret_cast(event)->window; + case XCB_CLIENT_MESSAGE: + return reinterpret_cast(event)->window; + default: + // extension handling + if (eventType == Xcb::Extensions::self()->shapeNotifyEvent()) { + return reinterpret_cast(event)->affected_window; + } + return XCB_WINDOW_NONE; + } +} + +static Qt::MouseButton x11ToQtMouseButton(int button) +{ + if (button == XCB_BUTTON_INDEX_1) { + return Qt::LeftButton; + } + if (button == XCB_BUTTON_INDEX_2) { + return Qt::MiddleButton; + } + if (button == XCB_BUTTON_INDEX_3) { + return Qt::RightButton; + } + if (button == XCB_BUTTON_INDEX_4) { + return Qt::XButton1; + } + if (button == XCB_BUTTON_INDEX_5) { + return Qt::XButton2; + } + return Qt::NoButton; +} + +/** + * Handles workspace specific XCB event + */ +bool Workspace::workspaceEvent(xcb_generic_event_t *e) +{ + const uint8_t eventType = e->response_type & ~0x80; + + const xcb_window_t eventWindow = findEventWindow(e); + if (eventWindow != XCB_WINDOW_NONE) { + if (X11Window *window = findClient(eventWindow)) { + if (window->windowEvent(e)) { + return true; + } + } else if (X11Window *window = findUnmanaged(eventWindow)) { + if (window->windowEvent(e)) { + return true; + } + } + } + + switch (eventType) { + case XCB_CREATE_NOTIFY: { + const auto *event = reinterpret_cast(e); + if (event->parent == kwinApp()->x11RootWindow() && !event->override_redirect) { + // see comments for allowWindowActivation() + const xcb_timestamp_t t = kwinApp()->x11Time(); + xcb_change_property(kwinApp()->x11Connection(), XCB_PROP_MODE_REPLACE, event->window, atoms->kde_net_wm_user_creation_time, XCB_ATOM_CARDINAL, 32, 1, &t); + } + break; + } + case XCB_UNMAP_NOTIFY: { + const auto *event = reinterpret_cast(e); + return (event->event != event->window); // hide wm typical event from Qt + } + case XCB_MAP_REQUEST: { + const auto *event = reinterpret_cast(e); + if (!createX11Window(event->window, false)) { + xcb_map_window(kwinApp()->x11Connection(), event->window); + const uint32_t values[] = {XCB_STACK_MODE_ABOVE}; + xcb_configure_window(kwinApp()->x11Connection(), event->window, XCB_CONFIG_WINDOW_STACK_MODE, values); + } + return true; + } + case XCB_MAP_NOTIFY: { + const auto *event = reinterpret_cast(e); + if (event->override_redirect && event->event == kwinApp()->x11RootWindow()) { + X11Window *window = findUnmanaged(event->window); + if (window == nullptr) { + window = createUnmanaged(event->window); + } + if (window) { + // if hasScheduledRelease is true, it means a unamp and map sequence has occurred. + // since release is scheduled after map notify, this old Unmanaged will get released + // before KWIN has chance to remanage it again. so release it right now. + if (window->hasScheduledRelease()) { + window->releaseWindow(); + window = createUnmanaged(event->window); + } + if (window) { + return window->windowEvent(e); + } + } + } + return (event->event != event->window); // hide wm typical event from Qt + } + + case XCB_CONFIGURE_REQUEST: { + const auto *event = reinterpret_cast(e); + if (event->parent == kwinApp()->x11RootWindow()) { + uint32_t values[5] = {0, 0, 0, 0, 0}; + const uint32_t value_mask = event->value_mask + & (XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT | XCB_CONFIG_WINDOW_BORDER_WIDTH); + int i = 0; + if (value_mask & XCB_CONFIG_WINDOW_X) { + values[i++] = event->x; + } + if (value_mask & XCB_CONFIG_WINDOW_Y) { + values[i++] = event->y; + } + if (value_mask & XCB_CONFIG_WINDOW_WIDTH) { + values[i++] = event->width; + } + if (value_mask & XCB_CONFIG_WINDOW_HEIGHT) { + values[i++] = event->height; + } + if (value_mask & XCB_CONFIG_WINDOW_BORDER_WIDTH) { + values[i++] = event->border_width; + } + xcb_configure_window(kwinApp()->x11Connection(), event->window, value_mask, values); + return true; + } + break; + } + case XCB_CONFIGURE_NOTIFY: { + const auto configureNotifyEvent = reinterpret_cast(e); + if (configureNotifyEvent->override_redirect && configureNotifyEvent->event == kwinApp()->x11RootWindow()) { + if (updateXStackingOrder()) { + updateStackingOrder(); + } + } + break; + } + case XCB_FOCUS_IN: { + const auto *event = reinterpret_cast(e); + if (event->event == kwinApp()->x11RootWindow() + && (event->detail == XCB_NOTIFY_DETAIL_NONE || event->detail == XCB_NOTIFY_DETAIL_POINTER_ROOT || event->detail == XCB_NOTIFY_DETAIL_INFERIOR)) { + Xcb::CurrentInput currentInput; + if (!currentInput.isNull()) { + // it seems we can "loose" focus reversions when the closing window hold a grab + // => catch the typical pattern (though we don't want the focus on the root anyway) #348935 + const bool lostFocusPointerToRoot = currentInput->focus == kwinApp()->x11RootWindow() && event->detail == XCB_NOTIFY_DETAIL_INFERIOR; + if (currentInput->focus == XCB_WINDOW_NONE || currentInput->focus == XCB_INPUT_FOCUS_POINTER_ROOT || lostFocusPointerToRoot) { + if (Window *window = activeWindow()) { + requestFocus(window, true); + } else { + activateNextWindow(nullptr); + } + } + } + } + } + // fall through + case XCB_FOCUS_OUT: + return true; // always eat these, they would tell Qt that KWin is the active app + default: + break; + } + return false; +} + +// **************************************** +// Client +// **************************************** + +/** + * General handler for XEvents concerning the client window + */ +bool X11Window::windowEvent(xcb_generic_event_t *e) +{ + if (isUnmanaged()) { + NET::Properties dirtyProperties; + NET::Properties2 dirtyProperties2; + info->event(e, &dirtyProperties, &dirtyProperties2); // pass through the NET stuff + if (dirtyProperties2 & NET::WM2Opacity) { + setOpacity(info->opacityF()); + } + if (dirtyProperties2 & NET::WM2OpaqueRegion) { + getWmOpaqueRegion(); + } + if (dirtyProperties2.testFlag(NET::WM2WindowRole)) { + Q_EMIT windowRoleChanged(); + } + if (dirtyProperties2.testFlag(NET::WM2WindowClass)) { + getResourceClass(); + } + const uint8_t eventType = e->response_type & ~0x80; + switch (eventType) { + case XCB_DESTROY_NOTIFY: + destroyWindow(); + break; + case XCB_UNMAP_NOTIFY: { + // using 1 msec to not just move it at the end of the event loop but add an very short + // timespan to cover cases like unmap() followed by destroy(). The only other way to + // ensure that the window is not destroyed when we do the release handling is to grab + // the XServer which we do not want to do for an Unmanaged. The timespan of 1 msec is + // short enough to not cause problems in the close window animations. + // It's of course still possible that we miss the destroy in which case non-fatal + // X errors are reported to the event loop and logged by Qt. + m_releaseTimer.start(1); + break; + } + case XCB_CONFIGURE_NOTIFY: + configureNotifyEvent(reinterpret_cast(e)); + break; + case XCB_PROPERTY_NOTIFY: + propertyNotifyEvent(reinterpret_cast(e)); + break; + case XCB_CLIENT_MESSAGE: + clientMessageEvent(reinterpret_cast(e)); + break; + default: { + if (eventType == Xcb::Extensions::self()->shapeNotifyEvent()) { + shapeNotifyEvent(reinterpret_cast(e)); + } + break; + } + } + return false; // don't eat events, even our own unmanaged widgets are tracked + } + + NET::Properties dirtyProperties; + NET::Properties2 dirtyProperties2; + info->event(e, &dirtyProperties, &dirtyProperties2); // pass through the NET stuff + + if ((dirtyProperties & NET::WMName) != 0) { + fetchName(); + } + if ((dirtyProperties & NET::WMIconName) != 0) { + fetchIconicName(); + } + if ((dirtyProperties & NET::WMIcon) != 0) { + getIcons(); + } + if ((dirtyProperties2 & NET::WM2UserTime) != 0) { + updateUserTime(info->userTime()); + } + if ((dirtyProperties2 & NET::WM2StartupId) != 0) { + startupIdChanged(); + } + if (dirtyProperties2 & NET::WM2Opacity) { + setOpacity(info->opacityF()); + } + if (dirtyProperties2.testFlag(NET::WM2WindowRole)) { + Q_EMIT windowRoleChanged(); + } + if (dirtyProperties2.testFlag(NET::WM2WindowClass)) { + getResourceClass(); + } + if (dirtyProperties2.testFlag(NET::WM2GroupLeader)) { + checkGroup(); + updateAllowedActions(); // Group affects isMinimizable() + } + if (dirtyProperties2.testFlag(NET::WM2Urgency)) { + updateUrgency(); + } + if (dirtyProperties2 & NET::WM2OpaqueRegion) { + getWmOpaqueRegion(); + } + if (dirtyProperties2 & NET::WM2DesktopFileName) { + setDesktopFileName(QString::fromUtf8(info->desktopFileName())); + } + if (dirtyProperties2 & NET::WM2GTKFrameExtents) { + setClientFrameExtents(info->gtkFrameExtents()); + } + + const uint8_t eventType = e->response_type & ~0x80; + switch (eventType) { + case XCB_UNMAP_NOTIFY: + unmapNotifyEvent(reinterpret_cast(e)); + break; + case XCB_DESTROY_NOTIFY: + destroyNotifyEvent(reinterpret_cast(e)); + break; + case XCB_MAP_REQUEST: + mapRequestEvent(reinterpret_cast(e)); + break; + case XCB_CONFIGURE_REQUEST: + configureRequestEvent(reinterpret_cast(e)); + break; + case XCB_PROPERTY_NOTIFY: + propertyNotifyEvent(reinterpret_cast(e)); + break; + case XCB_FOCUS_IN: + focusInEvent(e); + break; + case XCB_FOCUS_OUT: + focusOutEvent(reinterpret_cast(e)); + break; + case XCB_REPARENT_NOTIFY: + break; + case XCB_CLIENT_MESSAGE: + clientMessageEvent(reinterpret_cast(e)); + break; + default: + if (eventType == Xcb::Extensions::self()->shapeNotifyEvent()) { + shapeNotifyEvent(reinterpret_cast(e)); + } + break; + } + return true; // eat all events +} + +/** + * Handles map requests of the client window + */ +void X11Window::mapRequestEvent(xcb_map_request_event_t *e) +{ + // also copied in clientMessage() + if (isMinimized()) { + setMinimized(false); + } + if (!isOnCurrentDesktop()) { + if (allowWindowActivation()) { + workspace()->activateWindow(this); + } else { + demandAttention(); + } + } +} + +/** + * Handles unmap notify events of the client window + */ +void X11Window::unmapNotifyEvent(xcb_unmap_notify_event_t *e) +{ + if (m_inflightUnmaps == 0) { + releaseWindow(); + } else { + m_inflightUnmaps--; + } +} + +void X11Window::destroyNotifyEvent(xcb_destroy_notify_event_t *e) +{ + destroyWindow(); +} + +/** + * Handles client messages for the client window + */ +void X11Window::clientMessageEvent(xcb_client_message_event_t *e) +{ + if (e->type == atoms->wl_surface_serial) { + m_surfaceSerial = (uint64_t(e->data.data32[1]) << 32) | e->data.data32[0]; + if (XwaylandSurfaceV1Interface *xwaylandSurface = waylandServer()->xwaylandShell()->findSurface(m_surfaceSerial)) { + associate(xwaylandSurface); + } + } + + if (e->type == atoms->wm_change_state) { + if (e->data.data32[0] == XCB_ICCCM_WM_STATE_ICONIC) { + setMinimized(true); + } + return; + } +} + +void X11Window::configureNotifyEvent(xcb_configure_notify_event_t *e) +{ + RectF newgeom(Xcb::fromXNative(e->x), Xcb::fromXNative(e->y), Xcb::fromXNative(e->width), Xcb::fromXNative(e->height)); + if (newgeom != m_frameGeometry) { + Q_EMIT frameGeometryAboutToChange(); + + RectF old = m_frameGeometry; + m_clientGeometry = newgeom; + m_frameGeometry = newgeom; + m_bufferGeometry = newgeom; + checkOutput(); + updateShapeRegion(); + Q_EMIT bufferGeometryChanged(old); + Q_EMIT clientGeometryChanged(old); + Q_EMIT frameGeometryChanged(old); + } +} + +/** + * Handles configure requests of the client window + */ +void X11Window::configureRequestEvent(xcb_configure_request_event_t *e) +{ + if (isInteractiveResize() || isInteractiveMove()) { + return; // we have better things to do right now + } + + if (m_fullscreenMode == FullScreenNormal) { // refuse resizing of fullscreen windows + // but allow resizing fullscreen hacks in order to let them cancel fullscreen mode + sendSyntheticConfigureNotify(); + return; + } + if (isSplash()) { // no manipulations with splashscreens either + sendSyntheticConfigureNotify(); + return; + } + + if (e->value_mask & XCB_CONFIG_WINDOW_BORDER_WIDTH) { + // first, get rid of a window border + m_client.setBorderWidth(0); + } + + if (e->value_mask & (XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | XCB_CONFIG_WINDOW_HEIGHT | XCB_CONFIG_WINDOW_WIDTH)) { + configureRequest(e->value_mask, Xcb::fromXNative(e->x), + Xcb::fromXNative(e->y), Xcb::fromXNative(e->width), Xcb::fromXNative(e->height), 0, false); + } + if (e->value_mask & XCB_CONFIG_WINDOW_STACK_MODE) { + restackWindow(e->sibling, e->stack_mode, NET::FromApplication, userTime()); + } + + // Sending a synthetic configure notify always is fine, even in cases where + // the ICCCM doesn't require this - it can be though of as 'the WM decided to move + // the window later'. The window should not cause that many configure request, + // so this should not have any significant impact. With user moving/resizing + // the it should be optimized though (see also X11Window::setGeometry()/resize()/move()). + sendSyntheticConfigureNotify(); + + // SELI TODO accept configure requests for isDesktop windows (because kdesktop + // may get XRANDR resize event before kwin), but check it's still at the bottom? +} + +/** + * Handles property changes of the client window + */ +void X11Window::propertyNotifyEvent(xcb_property_notify_event_t *e) +{ + switch (e->atom) { + case XCB_ATOM_WM_NORMAL_HINTS: + getWmNormalHints(); + break; + case XCB_ATOM_WM_NAME: + fetchName(); + break; + case XCB_ATOM_WM_ICON_NAME: + fetchIconicName(); + break; + case XCB_ATOM_WM_TRANSIENT_FOR: + readTransient(); + break; + case XCB_ATOM_WM_HINTS: + getIcons(); // because KWin::icon() uses WMHints as fallback + break; + default: + if (e->atom == atoms->motif_wm_hints) { + getMotifHints(); + } else if (e->atom == atoms->net_wm_sync_request_counter) { + getSyncCounter(); + } else if (e->atom == atoms->activities) { + checkActivities(); + } else if (e->atom == atoms->kde_color_sheme) { + updateColorScheme(); + } else if (e->atom == atoms->kde_net_wm_appmenu_service_name) { + checkApplicationMenuServiceName(); + } else if (e->atom == atoms->kde_net_wm_appmenu_object_path) { + checkApplicationMenuObjectPath(); + } else if (e->atom == atoms->wm_client_leader) { + getWmClientLeader(); + } else if (e->atom == atoms->kde_net_wm_shadow) { + updateShadow(); + } else if (e->atom == atoms->kde_skip_close_animation) { + getSkipCloseAnimation(); + } else if (e->atom == atoms->xwayland_xrandr_emulation) { + configure(Xcb::toXNative(m_bufferGeometry)); + } + break; + } +} + +void X11Window::focusInEvent(xcb_generic_event_t *event) +{ + const auto focusEvent = reinterpret_cast(event); + + if (focusEvent->mode == XCB_NOTIFY_MODE_GRAB || focusEvent->mode == XCB_NOTIFY_MODE_UNGRAB) { + return; // we don't care + } + if (focusEvent->detail == XCB_NOTIFY_DETAIL_POINTER) { + return; // we don't care + } + if (!isShown() || !isOnCurrentDesktop()) { // we unmapped it, but it got focus meanwhile -> + return; // activateNextWindow() already transferred focus elsewhere + } + workspace()->forEachClient([](X11Window *window) { + window->cancelFocusOutTimer(); + }); + + // Note that xcb_focus_in_event_t::sequence is a uint16_t serial. + const UInt32Serial serial = event->full_sequence; + + // This is a FocusIn event from an XSetInputFocus() request that was issued before ours, it will + // be superseded later, so don't bother updating the active window. + if (serial < workspace()->x11FocusSerial()) { + return; + } + + // This is a FocusIn event in response to the XSetInputFocus() getting called by us or somebody + // else tried to focus their window around the same time, in which case consult with our focus + // stealing prevention policies. + if (serial == workspace()->x11FocusSerial()) { + if (isActive()) { + return; + } + } + + // Somebody focused their window but they opted in only to the WM_TAKE_FOCUS protocol or somebody + // tried to steal input focus. If we've attempted to focus that window, then just update the + // focus serial, and that's it, otherwise check if this FocusIn event is fine according to our + // focus stealing prevention policies. + if (serial > workspace()->x11FocusSerial()) { + if (isActive()) { + workspace()->setX11FocusSerial(serial); + return; + } + } + + if (allowWindowActivation(-1U, true)) { + workspace()->setX11FocusSerial(serial); + workspace()->setActiveWindow(this); + } else { + if (workspace()->restoreFocus()) { + demandAttention(); + } else { + qCWarning(KWIN_CORE, "Failed to restore focus. Activating 0x%x", window()); + workspace()->setX11FocusSerial(serial); + workspace()->setActiveWindow(this); + } + } +} + +static const bool s_enableFocusOut = environmentVariableBoolValue("KWIN_ENABLE_FOCUS_OUT").value_or(false); + +void X11Window::focusOutEvent(xcb_focus_out_event_t *e) +{ + if (!s_enableFocusOut) { + // Focus out events cause problems with some applications that do + // questionable things with override redirect windows. + // As they shouldn't be necessary with clients that behave sensibly + // (accept focus when given), ignore these events entirely + return; + } + if (e->mode == XCB_NOTIFY_MODE_GRAB || e->mode == XCB_NOTIFY_MODE_UNGRAB) { + return; // we don't care + } + if (e->detail != XCB_NOTIFY_DETAIL_NONLINEAR + && e->detail != XCB_NOTIFY_DETAIL_NONLINEAR_VIRTUAL) { + // SELI check all this + return; // hack for motif apps like netscape + } + + // When a window loses focus, FocusOut events are usually immediately + // followed by FocusIn events for another window that gains the focus + // (unless the focus goes to another screen, or to the nofocus widget). + // Without this check, the former focused window would have to be + // deactivated, and after that, the new one would be activated, with + // a short time when there would be no active window. This can cause + // flicker sometimes, e.g. when a fullscreen is shown, and focus is transferred + // from it to its transient, the fullscreen would be kept in the Active layer + // at the beginning and at the end, but not in the middle, when the active + // window would be temporarily none (see X11Window::belongToLayer() ). + // Therefore the setActive(false) call is moved to the end of the current + // event queue. If there is a matching FocusIn event in the current queue + // this will be processed before the setActive(false) call and the activation + // of the window which gained FocusIn will automatically deactivate the + // previously active window. + if (!m_focusOutTimer) { + m_focusOutTimer = new QTimer(this); + m_focusOutTimer->setSingleShot(true); + m_focusOutTimer->setInterval(0); + connect(m_focusOutTimer, &QTimer::timeout, this, [this]() { + if (workspace()->activeWindow() == this) { + workspace()->setActiveWindow(nullptr); + } + }); + } + m_focusOutTimer->start(); +} + +void X11Window::shapeNotifyEvent(xcb_shape_notify_event_t *e) +{ + if (e->affected_window != window()) { + return; + } + + switch (e->shape_kind) { + case XCB_SHAPE_SK_BOUNDING: + case XCB_SHAPE_SK_CLIP: + updateShapeRegion(); + break; + case XCB_SHAPE_SK_INPUT: + break; + } +} + +// performs _NET_WM_MOVERESIZE +void X11Window::NETMoveResize(qreal x_root, qreal y_root, NET::Direction direction, xcb_button_t button) +{ + if (isInteractiveMoveResize() && direction == NET::MoveResizeCancel) { + finishInteractiveMoveResize(true); + setInteractiveMoveResizePointerButtonDown(false); + updateCursor(); + } else if (direction == NET::Move || (direction >= NET::TopLeft && direction <= NET::Left)) { + if (!button) { + if (!input()->qtButtonStates() && !input()->touch()->touchPointCount()) { + return; + } + } else { + if (!(input()->qtButtonStates() & x11ToQtMouseButton(button)) && !input()->touch()->touchPointCount()) { + return; + } + } + + if (direction == NET::Move) { + // move cursor to the provided position to prevent the window jumping there on first movement + // the expectation is that the cursor is already at the provided position, + // thus it's more a safety measurement + input()->pointer()->warp(QPointF(x_root, y_root)); + performMousePressCommand(Options::MouseMove, QPointF(x_root, y_root)); + } else { + static const Gravity convert[] = { + Gravity::TopLeft, + Gravity::Top, + Gravity::TopRight, + Gravity::Right, + Gravity::BottomRight, + Gravity::Bottom, + Gravity::BottomLeft, + Gravity::Left}; + if (!isResizable()) { + return; + } + if (isInteractiveMoveResize()) { + finishInteractiveMoveResize(false); + } + setInteractiveMoveResizePointerButtonDown(true); + setInteractiveMoveResizeAnchor(QPointF(x_root, y_root)); + setInteractiveMoveResizeModifiers(Qt::KeyboardModifiers()); + setInteractiveMoveOffset(QPointF(qreal(x_root - x()) / width(), qreal(y_root - y()) / height())); // map from global + setUnrestrictedInteractiveMoveResize(false); + setInteractiveMoveResizeGravity(convert[direction]); + if (!startInteractiveMoveResize()) { + setInteractiveMoveResizePointerButtonDown(false); + } + updateCursor(); + } + } else if (direction == NET::KeyboardMove) { + // ignore mouse coordinates given in the message, mouse position is used by the moving algorithm + input()->pointer()->warp(frameGeometry().center()); + performMousePressCommand(Options::MouseUnrestrictedMove, frameGeometry().center()); + } else if (direction == NET::KeyboardSize) { + // ignore mouse coordinates given in the message, mouse position is used by the resizing algorithm + input()->pointer()->warp(frameGeometry().bottomRight()); + performMousePressCommand(Options::MouseUnrestrictedResize, frameGeometry().bottomRight()); + } +} + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/focuschain.cpp b/local/recipes/kde/kwin/source/src/focuschain.cpp new file mode 100644 index 0000000000..857e3a053c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/focuschain.cpp @@ -0,0 +1,298 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "focuschain.h" +#include "window.h" +#include "workspace.h" + +namespace KWin +{ + +void FocusChain::remove(Window *window) +{ + for (auto it = m_desktopFocusChains.begin(); + it != m_desktopFocusChains.end(); + ++it) { + it.value().removeAll(window); + } + m_mostRecentlyUsed.removeAll(window); +} + +void FocusChain::addDesktop(VirtualDesktop *desktop) +{ + m_desktopFocusChains.insert(desktop, Chain()); +} + +void FocusChain::removeDesktop(VirtualDesktop *desktop) +{ + if (m_currentDesktop == desktop) { + m_currentDesktop = nullptr; + } + m_desktopFocusChains.remove(desktop); +} + +Window *FocusChain::getForActivation(VirtualDesktop *desktop) const +{ + return getForActivation(desktop, workspace()->activeOutput()); +} + +Window *FocusChain::getForActivation(VirtualDesktop *desktop, LogicalOutput *output) const +{ + auto it = m_desktopFocusChains.constFind(desktop); + if (it == m_desktopFocusChains.constEnd()) { + return nullptr; + } + const auto &chain = it.value(); + for (int i = chain.size() - 1; i >= 0; --i) { + auto tmp = chain.at(i); + // TODO: move the check into Window + if (tmp->isShown() && tmp->isOnCurrentActivity() + && (!m_separateScreenFocus || tmp->output() == output)) { + return tmp; + } + } + return nullptr; +} + +void FocusChain::update(Window *window, FocusChain::Change change) +{ + if (!window->wantsTabFocus()) { + // Doesn't want tab focus, remove + remove(window); + return; + } + + if (window->isOnAllDesktops()) { + // Now on all desktops, add it to focus chains it is not already in + for (auto it = m_desktopFocusChains.begin(); + it != m_desktopFocusChains.end(); + ++it) { + auto &chain = it.value(); + // Making first/last works only on current desktop, don't affect all desktops + if (it.key() == m_currentDesktop + && (change == MakeFirst || change == MakeLast)) { + if (change == MakeFirst) { + makeFirstInChain(window, chain); + } else { + makeLastInChain(window, chain); + } + } else { + insertWindowIntoChain(window, chain); + } + } + } else { + // Now only on desktop, remove it anywhere else + for (auto it = m_desktopFocusChains.begin(); + it != m_desktopFocusChains.end(); + ++it) { + auto &chain = it.value(); + if (window->isOnDesktop(it.key())) { + updateWindowInChain(window, change, chain); + } else { + chain.removeAll(window); + } + } + } + + // add for most recently used chain + updateWindowInChain(window, change, m_mostRecentlyUsed); +} + +void FocusChain::updateWindowInChain(Window *window, FocusChain::Change change, Chain &chain) +{ + if (change == MakeFirst) { + makeFirstInChain(window, chain); + } else if (change == MakeLast) { + makeLastInChain(window, chain); + } else { + insertWindowIntoChain(window, chain); + } +} + +void FocusChain::insertWindowIntoChain(Window *window, Chain &chain) +{ + if (window->isDeleted()) { + return; + } + if (chain.contains(window)) { + return; + } + if (m_activeWindow && m_activeWindow != window && !chain.empty() && chain.last() == m_activeWindow) { + // Add it after the active window + chain.insert(chain.size() - 1, window); + } else { + // Otherwise add as the first one + chain.append(window); + } +} + +void FocusChain::moveAfterWindow(Window *window, Window *reference) +{ + if (window->isDeleted()) { + return; + } + if (!window->wantsTabFocus()) { + return; + } + if (reference == window) { + return; + } + + for (auto it = m_desktopFocusChains.begin(); + it != m_desktopFocusChains.end(); + ++it) { + if (!window->isOnDesktop(it.key())) { + continue; + } + moveAfterWindowInChain(window, reference, it.value()); + } + moveAfterWindowInChain(window, reference, m_mostRecentlyUsed); +} + +void FocusChain::moveBeforeWindow(Window *window, Window *reference) +{ + if (window->isDeleted()) { + return; + } + if (!window->wantsTabFocus()) { + return; + } + if (reference == window) { + return; + } + + for (auto it = m_desktopFocusChains.begin(); + it != m_desktopFocusChains.end(); + ++it) { + if (!window->isOnDesktop(it.key())) { + continue; + } + moveBeforeWindowInChain(window, reference, it.value()); + } + moveBeforeWindowInChain(window, reference, m_mostRecentlyUsed); +} + +void FocusChain::moveAfterWindowInChain(Window *window, Window *reference, Chain &chain) +{ + if (window->isDeleted()) { + return; + } + if (!chain.contains(reference)) { + return; + } + if (Window::belongToSameApplication(reference, window)) { + chain.removeAll(window); + chain.insert(chain.indexOf(reference), window); + } else { + chain.removeAll(window); + for (int i = 0; i < chain.size(); ++i) { + if (Window::belongToSameApplication(reference, chain.at(i))) { + chain.insert(i, window); + break; + } + } + } +} + +void FocusChain::moveBeforeWindowInChain(Window *window, Window *reference, Chain &chain) +{ + if (window->isDeleted()) { + return; + } + if (!chain.contains(reference)) { + return; + } + if (Window::belongToSameApplication(reference, window)) { + chain.removeAll(window); + chain.insert(chain.indexOf(reference) + 1, window); + } else { + chain.removeAll(window); + for (int i = chain.size() - 1; i >= 0; --i) { + if (Window::belongToSameApplication(reference, chain.at(i))) { + chain.insert(i + 1, window); + break; + } + } + } +} + +Window *FocusChain::firstMostRecentlyUsed() const +{ + if (m_mostRecentlyUsed.isEmpty()) { + return nullptr; + } + return m_mostRecentlyUsed.first(); +} + +Window *FocusChain::nextMostRecentlyUsed(Window *reference) const +{ + if (m_mostRecentlyUsed.isEmpty()) { + return nullptr; + } + const int index = m_mostRecentlyUsed.indexOf(reference); + if (index == -1) { + return m_mostRecentlyUsed.first(); + } + if (index == 0) { + return m_mostRecentlyUsed.last(); + } + return m_mostRecentlyUsed.at(index - 1); +} + +// copied from activation.cpp +bool FocusChain::isUsableFocusCandidate(Window *c, Window *prev) const +{ + return c != prev && c->isShown() && c->isOnCurrentDesktop() && c->isOnCurrentActivity() && (!m_separateScreenFocus || c->isOnOutput(prev ? prev->output() : workspace()->activeOutput())); +} + +Window *FocusChain::nextForDesktop(Window *reference, VirtualDesktop *desktop) const +{ + auto it = m_desktopFocusChains.constFind(desktop); + if (it == m_desktopFocusChains.constEnd()) { + return nullptr; + } + const auto &chain = it.value(); + for (int i = chain.size() - 1; i >= 0; --i) { + auto window = chain.at(i); + if (isUsableFocusCandidate(window, reference)) { + return window; + } + } + return nullptr; +} + +void FocusChain::makeFirstInChain(Window *window, Chain &chain) +{ + if (window->isDeleted()) { + return; + } + chain.removeAll(window); + chain.append(window); +} + +void FocusChain::makeLastInChain(Window *window, Chain &chain) +{ + if (window->isDeleted()) { + return; + } + chain.removeAll(window); + chain.prepend(window); +} + +bool FocusChain::contains(Window *window, VirtualDesktop *desktop) const +{ + auto it = m_desktopFocusChains.constFind(desktop); + if (it == m_desktopFocusChains.constEnd()) { + return false; + } + return it.value().contains(window); +} + +} // namespace + +#include "moc_focuschain.cpp" diff --git a/local/recipes/kde/kwin/source/src/focuschain.h b/local/recipes/kde/kwin/source/src/focuschain.h new file mode 100644 index 0000000000..c988555ed6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/focuschain.h @@ -0,0 +1,238 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +// KWin +#include "effect/globals.h" +// Qt +#include +#include + +namespace KWin +{ +// forward declarations +class Window; +class LogicalOutput; +class VirtualDesktop; + +/** + * @brief Singleton class to handle the various focus chains. + * + * A focus chain is a list of Windows containing information on which Window should be activated. + * + * Internally this FocusChain holds multiple independent chains. There is one chain of most recently + * used Windows which is primarily used by TabBox to build up the list of Windows for navigation. + * The chains are organized as a normal QList of Windows with the most recently used Window being the + * last item of the list, that is a LIFO like structure. + * + * In addition there is one chain for each virtual desktop which is used to determine which Window + * should get activated when the user switches to another virtual desktop. + * + * Furthermore this class contains various helper methods for the two different kind of chains. + */ +class FocusChain : public QObject +{ + Q_OBJECT +public: + enum Change { + MakeFirst, + MakeLast, + Update, + MakeFirstMinimized = MakeFirst + }; + explicit FocusChain() = default; + + /** + * @brief Updates the position of the @p window according to the requested @p change in the + * focus chain. + * + * This method affects both the most recently used focus chain and the per virtual desktop focus + * chain. + * + * In case the window does no longer want to get focus, it is removed from all chains. In case + * the window is on all virtual desktops it is ensured that it is present in each of the virtual + * desktops focus chain. In case it's on exactly one virtual desktop it is ensured that it is only + * in the focus chain for that virtual desktop. + * + * Depending on @p change the Window is inserted at different positions in the focus chain. In case + * of @c MakeFirst it is moved to the first position of the chain, in case of + * @c MakeLast it is moved to the last position of the chain. In all other cases it + * depends on whether the @p window is the currently active Window. If it is the active Window it + * becomes the first Window in the chain, otherwise it is inserted at the second position that is + * directly after the currently active Window. + * + * @param window The Window which should be moved inside the chains. + * @param change Where to move the Window + * @return void + */ + void update(Window *window, Change change); + /** + * @brief Moves @p window behind the @p reference Window in all focus chains. + * + * @param window The Window to move in the chains + * @param reference The Window behind which the @p window should be moved + * @return void + */ + void moveAfterWindow(Window *window, Window *reference); + /** + * @brief Moves @p window in front of the @p reference Window in all focus chains. + * + * @param window The Window to move in the chains + * @param reference The Window in front of which the @p window should be moved + * @return void + */ + void moveBeforeWindow(Window *window, Window *reference); + /** + * @brief Finds the best Window to become the new active Window in the focus chain for the given + * virtual @p desktop. + * + * In case that separate screen focus is used only Windows on the current screen are considered. + * If no Window for activation is found @c null is returned. + * + * @param desktop The virtual desktop to look for a Window for activation + * @return :X11Window *The Window which could be activated or @c null if there is none. + */ + Window *getForActivation(VirtualDesktop *desktop) const; + /** + * @brief Finds the best Window to become the new active Window in the focus chain for the given + * virtual @p desktop on the given @p screen. + * + * This method makes only sense to use if separate screen focus is used. If separate screen focus + * is disabled the @p screen is ignored. + * If no Window for activation is found @c null is returned. + * + * @param desktop The virtual desktop to look for a Window for activation + * @param output The screen to constrain the search on with separate screen focus + * @return :X11Window *The Window which could be activated or @c null if there is none. + */ + Window *getForActivation(VirtualDesktop *desktop, LogicalOutput *output) const; + + /** + * @brief Checks whether the most recently used focus chain contains the given @p window. + * + * Does not consider the per-desktop focus chains. + * @param window The Window to look for. + * @return bool @c true if the most recently used focus chain contains @p window, @c false otherwise. + */ + bool contains(Window *window) const; + /** + * @brief Checks whether the focus chain for the given @p desktop contains the given @p window. + * + * Does not consider the most recently used focus chain. + * + * @param window The Window to look for. + * @param desktop The virtual desktop whose focus chain should be used + * @return bool @c true if the focus chain for @p desktop contains @p window, @c false otherwise. + */ + bool contains(Window *window, VirtualDesktop *desktop) const; + /** + * @brief Queries the most recently used focus chain for the next Window after the given + * @p reference Window. + * + * The navigation wraps around the borders of the chain. That is if the @p reference Window is + * the last item of the focus chain, the first Window will be returned. + * + * If the @p reference Window cannot be found in the focus chain, the first element of the focus + * chain is returned. + * + * @param reference The start point in the focus chain to search + * @return :X11Window *The relatively next Window in the most recently used chain. + */ + Window *nextMostRecentlyUsed(Window *reference) const; + /** + * @brief Queries the focus chain for @p desktop for the next Window in relation to the given + * @p reference Window. + * + * The method finds the first usable Window which is not the @p reference Window. If no Window + * can be found @c null is returned + * + * @param reference The reference Window which should not be returned + * @param desktop The virtual desktop whose focus chain should be used + * @return :X11Window *The next usable Window or @c null if none can be found. + */ + Window *nextForDesktop(Window *reference, VirtualDesktop *desktop) const; + /** + * @brief Returns the first Window in the most recently used focus chain. First Window in this + * case means really the first Window in the chain and not the most recently used Window. + * + * @return :X11Window *The first Window in the most recently used chain. + */ + Window *firstMostRecentlyUsed() const; + + bool isUsableFocusCandidate(Window *window, Window *prev) const; + +public Q_SLOTS: + /** + * @brief Removes @p window from all focus chains. + * + * @param window The Window to remove from all focus chains. + * @return void + */ + void remove(KWin::Window *window); + void setSeparateScreenFocus(bool enabled); + void setActiveWindow(KWin::Window *window); + void setCurrentDesktop(VirtualDesktop *desktop); + void addDesktop(VirtualDesktop *desktop); + void removeDesktop(VirtualDesktop *desktop); + +private: + using Chain = QList; + /** + * @brief Makes @p window the first Window in the given focus @p chain. + * + * This means the existing position of @p window is dropped and @p window is appended to the + * @p chain which makes it the first item. + * + * @param window The Window to become the first in @p chain + * @param chain The focus chain to operate on + * @return void + */ + void makeFirstInChain(Window *window, Chain &chain); + /** + * @brief Makes @p window the last Window in the given focus @p chain. + * + * This means the existing position of @p window is dropped and @p window is prepended to the + * @p chain which makes it the last item. + * + * @param window The Window to become the last in @p chain + * @param chain The focus chain to operate on + * @return void + */ + void makeLastInChain(Window *window, Chain &chain); + void moveAfterWindowInChain(Window *window, Window *reference, Chain &chain); + void moveBeforeWindowInChain(Window *window, Window *reference, Chain &chain); + void updateWindowInChain(Window *window, Change change, Chain &chain); + void insertWindowIntoChain(Window *window, Chain &chain); + Chain m_mostRecentlyUsed; + QHash m_desktopFocusChains; + bool m_separateScreenFocus = false; + Window *m_activeWindow = nullptr; + VirtualDesktop *m_currentDesktop = nullptr; +}; + +inline bool FocusChain::contains(Window *window) const +{ + return m_mostRecentlyUsed.contains(window); +} + +inline void FocusChain::setSeparateScreenFocus(bool enabled) +{ + m_separateScreenFocus = enabled; +} + +inline void FocusChain::setActiveWindow(Window *window) +{ + m_activeWindow = window; +} + +inline void FocusChain::setCurrentDesktop(VirtualDesktop *desktop) +{ + m_currentDesktop = desktop; +} + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/ftrace.cpp b/local/recipes/kde/kwin/source/src/ftrace.cpp new file mode 100644 index 0000000000..20076e44f1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/ftrace.cpp @@ -0,0 +1,123 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2021 David Edmundson + SPDX-FileCopyrightText: 2020 Roman Gilg + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "ftrace.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ +KWIN_SINGLETON_FACTORY(KWin::FTraceLogger) + +FTraceLogger::FTraceLogger(QObject *parent) + : QObject(parent) +{ + if (qEnvironmentVariableIsSet("KWIN_PERF_FTRACE")) { + setEnabled(true); + } else { + QDBusConnection::sessionBus().registerObject(QStringLiteral("/FTrace"), this, QDBusConnection::ExportScriptableContents); + } +} + +bool FTraceLogger::isEnabled() const +{ + return m_file.isOpen(); +} + +void FTraceLogger::setEnabled(bool enabled) +{ + QMutexLocker lock(&m_mutex); + if (enabled == isEnabled()) { + return; + } + + if (enabled) { + open(); + } else { + m_file.close(); + } + Q_EMIT enabledChanged(); +} + +bool FTraceLogger::open() +{ + const QString path = filePath(); + if (path.isEmpty()) { + return false; + } + + m_file.setFileName(path); + if (!m_file.open(QIODevice::WriteOnly)) { + qWarning() << "No access to trace marker file at:" << path; + } + return true; +} + +QString FTraceLogger::filePath() +{ + if (qEnvironmentVariableIsSet("KWIN_PERF_FTRACE_FILE")) { + return qgetenv("KWIN_PERF_FTRACE_FILE"); + } + + QFile mountsFile("/proc/mounts"); + if (!mountsFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + qWarning() << "No access to mounts file. Can not determine trace marker file location."; + return QString(); + } + + auto lineInfo = [](const QString &line) { + const int start = line.indexOf(' ') + 1; + const int end = line.indexOf(' ', start); + const QString dirPath(line.mid(start, end - start)); + if (dirPath.isEmpty() || !QFileInfo::exists(dirPath)) { + return QFileInfo(); + } + return QFileInfo(QDir(dirPath), QStringLiteral("trace_marker")); + }; + QFileInfo markerFileInfo; + QTextStream mountsIn(&mountsFile); + QString mountsLine = mountsIn.readLine(); + + while (!mountsLine.isNull()) { + if (mountsLine.startsWith("tracefs")) { + const auto info = lineInfo(mountsLine); + if (info.exists()) { + markerFileInfo = info; + break; + } + } + if (mountsLine.startsWith("debugfs")) { + markerFileInfo = lineInfo(mountsLine); + } + mountsLine = mountsIn.readLine(); + } + mountsFile.close(); + if (!markerFileInfo.exists()) { + qWarning() << "Could not determine trace marker file location from mounts."; + return QString(); + } + + return markerFileInfo.absoluteFilePath(); +} + +FTraceDuration::~FTraceDuration() +{ + FTraceLogger::self()->trace(m_message, " end_ctx=", m_context); +} + +} + +#include "moc_ftrace.cpp" diff --git a/local/recipes/kde/kwin/source/src/ftrace.h b/local/recipes/kde/kwin/source/src/ftrace.h new file mode 100644 index 0000000000..3147a5e333 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/ftrace.h @@ -0,0 +1,107 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2021 David Edmundson + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "effect/globals.h" + +#include +#include +#include +#include +#include + +namespace KWin +{ +/** + * FTraceLogger is a singleton utility for writing log messages using ftrace + * + * Usage: Either: + * Set the KWIN_PERF_FTRACE environment variable before starting the application + * Calling on DBus /FTrace org.kde.kwin.FTrace.setEnabled true + * After having created the ftrace mount + */ +class KWIN_EXPORT FTraceLogger : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.FTrace"); + Q_PROPERTY(bool isEnabled READ isEnabled NOTIFY enabledChanged) + +public: + /** + * Enabled through DBus and logging has started + */ + bool isEnabled() const; + + /** + * Main log function + * Takes any number of arguments that can be written into QTextStream + */ + template + void trace(Args... args) + { + Q_ASSERT(isEnabled()); + QMutexLocker lock(&m_mutex); + if (!m_file.isOpen()) { + return; + } + QTextStream stream(&m_file); + (stream << ... << args) << Qt::endl; + } + +Q_SIGNALS: + void enabledChanged(); + +public Q_SLOTS: + Q_SCRIPTABLE void setEnabled(bool enabled); + +private: + static QString filePath(); + bool open(); + QFile m_file; + QMutex m_mutex; + KWIN_SINGLETON(FTraceLogger) +}; + +class KWIN_EXPORT FTraceDuration +{ +public: + template + FTraceDuration(Args... args) + { + static QAtomicInteger s_context = 0; + QTextStream stream(&m_message); + (stream << ... << args); + stream.flush(); + m_context = ++s_context; + FTraceLogger::self()->trace(m_message, " begin_ctx=", m_context); + } + + ~FTraceDuration(); + +private: + QByteArray m_message; + quint32 m_context; +}; + +} // namespace KWin + +/** + * Optimised macro, arguments are only copied if tracing is enabled + */ +#define fTrace(...) \ + if (KWin::FTraceLogger::self()->isEnabled()) \ + KWin::FTraceLogger::self()->trace(__VA_ARGS__); + +/** + * Will insert two markers into the log. Once when called, and the second at the end of the relevant block + * In GPUVis this will appear as a timed block with begin_ctx and end_ctx markers + */ +#define fTraceDuration(...) \ + std::unique_ptr _duration(KWin::FTraceLogger::self()->isEnabled() ? new KWin::FTraceDuration(__VA_ARGS__) : nullptr); diff --git a/local/recipes/kde/kwin/source/src/gestures.cpp b/local/recipes/kde/kwin/source/src/gestures.cpp new file mode 100644 index 0000000000..c5aecd9e4e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/gestures.cpp @@ -0,0 +1,373 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "gestures.h" + +#include + +#include +#include + +namespace KWin +{ + +Gesture::Gesture() +{ +} + +Gesture::~Gesture() = default; + +SwipeGesture::SwipeGesture(uint32_t fingerCount) + : m_fingerCount(fingerCount) +{ +} + +SwipeGesture::~SwipeGesture() = default; + +qreal SwipeGesture::deltaToProgress(const QPointF &delta) const +{ + switch (m_direction) { + case SwipeDirection::Up: + case SwipeDirection::Down: + return std::min(std::abs(delta.y()) / s_minimumDelta, 1.0); + case SwipeDirection::Left: + case SwipeDirection::Right: + return std::min(std::abs(delta.x()) / s_minimumDelta, 1.0); + default: + Q_UNREACHABLE(); + } +} + +bool SwipeGesture::minimumDeltaReached(const QPointF &delta) const +{ + return deltaToProgress(delta) >= 1.0; +} + +PinchGesture::PinchGesture(uint32_t fingerCount) + : m_fingerCount(fingerCount) +{ +} + +PinchGesture::~PinchGesture() = default; + +qreal PinchGesture::scaleDeltaToProgress(const qreal &scaleDelta) const +{ + return std::abs(scaleDelta - 1) / s_minimumScaleDelta; +} + +bool PinchGesture::minimumScaleDeltaReached(const qreal &scaleDelta) const +{ + return scaleDeltaToProgress(scaleDelta) >= 1.0; +} + +GestureRecognizer::GestureRecognizer(QObject *parent) + : QObject(parent) +{ +} + +GestureRecognizer::~GestureRecognizer() = default; + +void GestureRecognizer::registerSwipeGesture(KWin::SwipeGesture *gesture) +{ + Q_ASSERT(!m_swipeGestures.contains(gesture)); + auto connection = connect(gesture, &QObject::destroyed, this, std::bind(&GestureRecognizer::unregisterSwipeGesture, this, gesture)); + m_destroyConnections.insert(gesture, connection); + m_swipeGestures << gesture; +} + +void GestureRecognizer::unregisterSwipeGesture(KWin::SwipeGesture *gesture) +{ + auto it = m_destroyConnections.find(gesture); + if (it != m_destroyConnections.end()) { + disconnect(it.value()); + m_destroyConnections.erase(it); + } + m_swipeGestures.removeAll(gesture); + if (m_activeSwipeGestures.removeOne(gesture)) { + Q_EMIT gesture->cancelled(); + } +} + +void GestureRecognizer::registerPinchGesture(KWin::PinchGesture *gesture) +{ + Q_ASSERT(!m_pinchGestures.contains(gesture)); + auto connection = connect(gesture, &QObject::destroyed, this, std::bind(&GestureRecognizer::unregisterPinchGesture, this, gesture)); + m_destroyConnections.insert(gesture, connection); + m_pinchGestures << gesture; +} + +void GestureRecognizer::unregisterPinchGesture(KWin::PinchGesture *gesture) +{ + auto it = m_destroyConnections.find(gesture); + if (it != m_destroyConnections.end()) { + disconnect(it.value()); + m_destroyConnections.erase(it); + } + m_pinchGestures.removeAll(gesture); + if (m_activePinchGestures.removeOne(gesture)) { + Q_EMIT gesture->cancelled(); + } +} + +int GestureRecognizer::startSwipeGesture(uint fingerCount, const QPointF &startPos) +{ + m_currentFingerCount = fingerCount; + if (!m_activeSwipeGestures.isEmpty() || !m_activePinchGestures.isEmpty()) { + return 0; + } + int count = 0; + for (SwipeGesture *gesture : std::as_const(m_swipeGestures)) { + if (gesture->fingerCount() != fingerCount) { + continue; + } + + // Only add gestures who's direction aligns with current swipe axis + switch (gesture->direction()) { + case SwipeDirection::Up: + case SwipeDirection::Down: + if (m_currentSwipeAxis == Axis::Horizontal) { + continue; + } + break; + case SwipeDirection::Left: + case SwipeDirection::Right: + if (m_currentSwipeAxis == Axis::Vertical) { + continue; + } + break; + case SwipeDirection::Invalid: + Q_UNREACHABLE(); + } + + m_activeSwipeGestures << gesture; + count++; + Q_EMIT gesture->started(); + } + return count; +} + +void GestureRecognizer::updateSwipeGesture(const QPointF &delta) +{ + m_currentDelta += delta; + + SwipeDirection direction; // Overall direction + Axis swipeAxis; + + // Pick an axis for gestures so horizontal ones don't change to vertical ones without lifting fingers + if (m_currentSwipeAxis == Axis::None) { + if (std::abs(m_currentDelta.x()) >= std::abs(m_currentDelta.y())) { + swipeAxis = Axis::Horizontal; + direction = m_currentDelta.x() < 0 ? SwipeDirection::Left : SwipeDirection::Right; + } else { + swipeAxis = Axis::Vertical; + direction = m_currentDelta.y() < 0 ? SwipeDirection::Up : SwipeDirection::Down; + } + if (std::abs(m_currentDelta.x()) >= 5 || std::abs(m_currentDelta.y()) >= 5) { + // only lock in a direction if the delta is big enough + // to prevent accidentally choosing the wrong direction + m_currentSwipeAxis = swipeAxis; + } + } else { + swipeAxis = m_currentSwipeAxis; + } + + // Find the current swipe direction + switch (swipeAxis) { + case Axis::Vertical: + direction = m_currentDelta.y() < 0 ? SwipeDirection::Up : SwipeDirection::Down; + break; + case Axis::Horizontal: + direction = m_currentDelta.x() < 0 ? SwipeDirection::Left : SwipeDirection::Right; + break; + default: + Q_UNREACHABLE(); + } + + // Eliminate wrong gestures (takes two iterations) + for (int i = 0; i < 2; i++) { + + if (m_activeSwipeGestures.isEmpty()) { + startSwipeGesture(m_currentFingerCount); + } + + for (auto it = m_activeSwipeGestures.begin(); it != m_activeSwipeGestures.end();) { + auto g = static_cast(*it); + + if (g->direction() != direction) { + Q_EMIT g->cancelled(); + it = m_activeSwipeGestures.erase(it); + continue; + } + + it++; + } + } + + // Send progress update + for (SwipeGesture *g : std::as_const(m_activeSwipeGestures)) { + Q_EMIT g->progress(g->deltaToProgress(m_currentDelta)); + Q_EMIT g->deltaProgress(m_currentDelta); + } +} + +void GestureRecognizer::cancelActiveGestures() +{ + for (auto g : std::as_const(m_activeSwipeGestures)) { + Q_EMIT g->cancelled(); + } + for (auto g : std::as_const(m_activePinchGestures)) { + Q_EMIT g->cancelled(); + } + m_activeSwipeGestures.clear(); + m_activePinchGestures.clear(); + m_currentScale = 0; + m_currentDelta = QPointF(0, 0); + m_currentSwipeAxis = Axis::None; +} + +void GestureRecognizer::cancelSwipeGesture() +{ + cancelActiveGestures(); + m_currentFingerCount = 0; + m_currentDelta = QPointF(0, 0); + m_currentSwipeAxis = Axis::None; +} + +void GestureRecognizer::endSwipeGesture() +{ + const QPointF delta = m_currentDelta; + for (auto g : std::as_const(m_activeSwipeGestures)) { + if (static_cast(g)->minimumDeltaReached(delta)) { + Q_EMIT g->triggered(); + } else { + Q_EMIT g->cancelled(); + } + } + m_activeSwipeGestures.clear(); + m_currentFingerCount = 0; + m_currentDelta = QPointF(0, 0); + m_currentSwipeAxis = Axis::None; +} + +int GestureRecognizer::startPinchGesture(uint fingerCount) +{ + m_currentFingerCount = fingerCount; + int count = 0; + if (!m_activeSwipeGestures.isEmpty() || !m_activePinchGestures.isEmpty()) { + return 0; + } + for (PinchGesture *gesture : std::as_const(m_pinchGestures)) { + if (gesture->fingerCount() != fingerCount) { + continue; + } + + // direction doesn't matter yet + m_activePinchGestures << gesture; + count++; + Q_EMIT gesture->started(); + } + return count; +} + +void GestureRecognizer::updatePinchGesture(qreal scale, qreal angleDelta, const QPointF &posDelta) +{ + m_currentScale = scale; + + // Determine the direction of the swipe + PinchDirection direction; + if (scale < 1) { + direction = PinchDirection::Contracting; + } else { + direction = PinchDirection::Expanding; + } + + // Eliminate wrong gestures (takes two iterations) + for (int i = 0; i < 2; i++) { + if (m_activePinchGestures.isEmpty()) { + startPinchGesture(m_currentFingerCount); + } + + for (auto it = m_activePinchGestures.begin(); it != m_activePinchGestures.end();) { + auto g = static_cast(*it); + + if (g->direction() != direction) { + Q_EMIT g->cancelled(); + it = m_activePinchGestures.erase(it); + continue; + } + it++; + } + } + + for (PinchGesture *g : std::as_const(m_activePinchGestures)) { + Q_EMIT g->progress(g->scaleDeltaToProgress(scale)); + } +} + +void GestureRecognizer::cancelPinchGesture() +{ + cancelActiveGestures(); + m_currentScale = 1; + m_currentFingerCount = 0; + m_currentSwipeAxis = Axis::None; +} + +void GestureRecognizer::endPinchGesture() // because fingers up +{ + for (auto g : std::as_const(m_activePinchGestures)) { + if (g->minimumScaleDeltaReached(m_currentScale)) { + Q_EMIT g->triggered(); + } else { + Q_EMIT g->cancelled(); + } + } + m_activeSwipeGestures.clear(); + m_activePinchGestures.clear(); + m_currentScale = 1; + m_currentFingerCount = 0; + m_currentSwipeAxis = Axis::None; +} + +uint32_t SwipeGesture::fingerCount() const +{ + return m_fingerCount; +} + +SwipeDirection SwipeGesture::direction() const +{ + return m_direction; +} + +void SwipeGesture::setDirection(SwipeDirection direction) +{ + m_direction = direction; +} + +uint32_t PinchGesture::fingerCount() const +{ + return m_fingerCount; +} + +PinchDirection PinchGesture::direction() const +{ + return m_direction; +} + +void PinchGesture::setDirection(PinchDirection direction) +{ + m_direction = direction; +} + +int GestureRecognizer::startSwipeGesture(uint fingerCount) +{ + return startSwipeGesture(fingerCount, QPointF()); +} + +} + +#include "moc_gestures.cpp" diff --git a/local/recipes/kde/kwin/source/src/gestures.h b/local/recipes/kde/kwin/source/src/gestures.h new file mode 100644 index 0000000000..e6c5e100d4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/gestures.h @@ -0,0 +1,157 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "effect/globals.h" +#include + +#include +#include +#include +#include + +namespace KWin +{ + +class Gesture : public QObject +{ + Q_OBJECT +public: + ~Gesture() override; + +protected: + explicit Gesture(); + +Q_SIGNALS: + /** + * Matching of a gesture started and this Gesture might match. + * On further evaluation either the signal @ref triggered or + * @ref cancelled will get emitted. + */ + void started(); + /** + * Gesture matching ended and this Gesture matched. + */ + void triggered(); + /** + * This Gesture no longer matches. + */ + void cancelled(); +}; + +class SwipeGesture : public Gesture +{ + Q_OBJECT +public: + static constexpr double s_minimumDelta = 200; + + explicit SwipeGesture(uint32_t fingerCount); + ~SwipeGesture() override; + + uint32_t fingerCount() const; + + SwipeDirection direction() const; + void setDirection(SwipeDirection direction); + + qreal deltaToProgress(const QPointF &delta) const; + bool minimumDeltaReached(const QPointF &delta) const; + +Q_SIGNALS: + /** + * The progress of the gesture if a minimumDelta is set. + * The progress is reported in [0.0,1.0] + */ + void progress(qreal); + + /** + * The progress in actual pixel distance traveled by the fingers + */ + void deltaProgress(const QPointF &delta); + +private: + const uint32_t m_fingerCount; + SwipeDirection m_direction = SwipeDirection::Down; +}; + +class PinchGesture : public Gesture +{ + Q_OBJECT +public: + /** + * Every time the scale of the gesture changes by this much, the callback changes by 1. + * This is the amount of change for 1 unit of change, like switch by 1 desktop. + */ + static constexpr double s_minimumScaleDelta = 0.2; + + explicit PinchGesture(uint32_t fingerCount); + ~PinchGesture() override; + + uint32_t fingerCount() const; + + PinchDirection direction() const; + void setDirection(PinchDirection direction); + + qreal scaleDeltaToProgress(const qreal &scaleDelta) const; + bool minimumScaleDeltaReached(const qreal &scaleDelta) const; + +Q_SIGNALS: + /** + * The progress of the gesture if a minimumDelta is set. + * The progress is reported in [0.0,1.0] + */ + void progress(qreal); + +private: + const uint32_t m_fingerCount; + PinchDirection m_direction = PinchDirection::Expanding; +}; + +class KWIN_EXPORT GestureRecognizer : public QObject +{ + Q_OBJECT +public: + GestureRecognizer(QObject *parent = nullptr); + ~GestureRecognizer() override; + + void registerSwipeGesture(SwipeGesture *gesture); + void unregisterSwipeGesture(SwipeGesture *gesture); + void registerPinchGesture(PinchGesture *gesture); + void unregisterPinchGesture(PinchGesture *gesture); + + int startSwipeGesture(uint fingerCount); + void updateSwipeGesture(const QPointF &delta); + void cancelSwipeGesture(); + void endSwipeGesture(); + + int startPinchGesture(uint fingerCount); + void updatePinchGesture(qreal scale, qreal angleDelta, const QPointF &posDelta); + void cancelPinchGesture(); + void endPinchGesture(); + +private: + void cancelActiveGestures(); + enum class Axis { + Horizontal, + Vertical, + None, + }; + int startSwipeGesture(uint fingerCount, const QPointF &startPos); + QList m_swipeGestures; + QList m_pinchGestures; + QList m_activeSwipeGestures; + QList m_activePinchGestures; + QMap m_destroyConnections; + + QPointF m_currentDelta = QPointF(0, 0); + qreal m_currentScale = 1; // For Pinch Gesture recognition + uint m_currentFingerCount = 0; + Axis m_currentSwipeAxis = Axis::None; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/globalshortcuts.cpp b/local/recipes/kde/kwin/source/src/globalshortcuts.cpp new file mode 100644 index 0000000000..7c71f9dfbd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/globalshortcuts.cpp @@ -0,0 +1,345 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +// own +#include "globalshortcuts.h" +// config +#include "config-kwin.h" +// kwin +#include "effect/globals.h" +#include "gestures.h" +#include "main.h" +#include "utils/common.h" +// KDE +#if KWIN_BUILD_GLOBALSHORTCUTS +#include +#include +#endif +// Qt +#include +// system +#include +#include + +namespace KWin +{ +GlobalShortcut::GlobalShortcut(Shortcut &&sc, QAction *action) + : m_shortcut(sc) + , m_action(action) +{ + if (auto swipeGesture = std::get_if(&sc)) { + m_swipeGesture = std::make_unique(swipeGesture->fingerCount); + m_swipeGesture->setDirection(swipeGesture->direction); + QObject::connect(m_swipeGesture.get(), &SwipeGesture::triggered, m_action, &QAction::trigger, Qt::QueuedConnection); + QObject::connect(m_swipeGesture.get(), &SwipeGesture::cancelled, m_action, &QAction::trigger, Qt::QueuedConnection); + if (swipeGesture->progressCallback) { + QObject::connect(m_swipeGesture.get(), &SwipeGesture::progress, swipeGesture->progressCallback); + } + } else if (auto pinchGesture = std::get_if(&sc)) { + m_pinchGesture = std::make_unique(pinchGesture->fingerCount); + m_pinchGesture->setDirection(pinchGesture->direction); + QObject::connect(m_pinchGesture.get(), &PinchGesture::triggered, m_action, &QAction::trigger, Qt::QueuedConnection); + QObject::connect(m_pinchGesture.get(), &PinchGesture::cancelled, m_action, &QAction::trigger, Qt::QueuedConnection); + if (pinchGesture->scaleCallback) { + QObject::connect(m_pinchGesture.get(), &PinchGesture::progress, pinchGesture->scaleCallback); + } + } +} + +GlobalShortcut::~GlobalShortcut() +{ +} + +QAction *GlobalShortcut::action() const +{ + return m_action; +} + +void GlobalShortcut::invoke() const +{ + QMetaObject::invokeMethod(m_action, &QAction::trigger, Qt::QueuedConnection); +} + +const Shortcut &GlobalShortcut::shortcut() const +{ + return m_shortcut; +} + +SwipeGesture *GlobalShortcut::swipeGesture() const +{ + return m_swipeGesture.get(); +} + +PinchGesture *GlobalShortcut::pinchGesture() const +{ + return m_pinchGesture.get(); +} + +GlobalShortcutsManager::GlobalShortcutsManager(QObject *parent) + : QObject(parent) + , m_touchpadGestureRecognizer(new GestureRecognizer(this)) + , m_touchscreenGestureRecognizer(new GestureRecognizer(this)) +{ +} + +GlobalShortcutsManager::~GlobalShortcutsManager() +{ +} + +void GlobalShortcutsManager::init() +{ +#if KWIN_BUILD_GLOBALSHORTCUTS + qputenv("KGLOBALACCELD_PLATFORM", QByteArrayLiteral("org.kde.kwin")); + m_kglobalAccel = std::make_unique(); + if (!m_kglobalAccel->init()) { + qCDebug(KWIN_CORE) << "Init of kglobalaccel failed"; + m_kglobalAccel.reset(); + } else { + m_kglobalAccelInterface = m_kglobalAccel->interface(); + qCDebug(KWIN_CORE) << "KGlobalAcceld inited"; + } +#endif +} + +void GlobalShortcutsManager::objectDeleted(QObject *object) +{ + auto it = m_shortcuts.begin(); + while (it != m_shortcuts.end()) { + if (it->action() == object) { + it = m_shortcuts.erase(it); + } else { + ++it; + } + } +} + +bool GlobalShortcutsManager::add(GlobalShortcut sc, DeviceType device) +{ + const auto &recognizer = device == DeviceType::Touchpad ? m_touchpadGestureRecognizer : m_touchscreenGestureRecognizer; + if (std::holds_alternative(sc.shortcut())) { + recognizer->registerSwipeGesture(sc.swipeGesture()); + } else if (std::holds_alternative(sc.shortcut())) { + recognizer->registerPinchGesture(sc.pinchGesture()); + } + connect(sc.action(), &QAction::destroyed, this, &GlobalShortcutsManager::objectDeleted); + m_shortcuts.push_back(std::move(sc)); + return true; +} + +void GlobalShortcutsManager::registerPointerShortcut(QAction *action, Qt::KeyboardModifiers modifiers, Qt::MouseButtons pointerButtons) +{ + add(GlobalShortcut(PointerButtonShortcut{modifiers, pointerButtons}, action)); +} + +void GlobalShortcutsManager::registerAxisShortcut(QAction *action, Qt::KeyboardModifiers modifiers, PointerAxisDirection axis) +{ + add(GlobalShortcut(PointerAxisShortcut{modifiers, axis}, action)); +} + +void GlobalShortcutsManager::registerTouchpadSwipe(SwipeDirection direction, uint32_t fingerCount, QAction *action, std::function progressCallback) +{ + add(GlobalShortcut(RealtimeFeedbackSwipeShortcut{DeviceType::Touchpad, direction, progressCallback, fingerCount}, action), DeviceType::Touchpad); +} + +void GlobalShortcutsManager::registerTouchpadSwipe(SwipeGesture *swipeGesture) +{ + m_touchpadGestureRecognizer->registerSwipeGesture(swipeGesture); +} + +void GlobalShortcutsManager::registerTouchpadPinch(PinchDirection direction, uint32_t fingerCount, QAction *action, std::function progressCallback) +{ + add(GlobalShortcut(RealtimeFeedbackPinchShortcut{direction, progressCallback, fingerCount}, action), DeviceType::Touchpad); +} + +void GlobalShortcutsManager::registerTouchpadPinch(PinchGesture *pinchGesture) +{ + m_touchpadGestureRecognizer->registerPinchGesture(pinchGesture); +} + +void GlobalShortcutsManager::registerTouchscreenSwipe(SwipeDirection direction, uint32_t fingerCount, QAction *action, std::function progressCallback) +{ + add(GlobalShortcut(RealtimeFeedbackSwipeShortcut{DeviceType::Touchscreen, direction, progressCallback, fingerCount}, action), DeviceType::Touchscreen); +} + +void GlobalShortcutsManager::registerTouchscreenSwipe(SwipeGesture *swipeGesture) +{ + m_touchscreenGestureRecognizer->registerSwipeGesture(swipeGesture); +} + +void GlobalShortcutsManager::forceRegisterTouchscreenSwipe(SwipeDirection direction, uint32_t fingerCount, QAction *action, std::function progressCallback) +{ + GlobalShortcut shortcut{RealtimeFeedbackSwipeShortcut{DeviceType::Touchscreen, direction, progressCallback, fingerCount}, action}; + const auto it = std::find_if(m_shortcuts.begin(), m_shortcuts.end(), [&shortcut](const auto &s) { + return shortcut.shortcut() == s.shortcut(); + }); + if (it != m_shortcuts.end()) { + m_shortcuts.erase(it); + } + m_touchscreenGestureRecognizer->registerSwipeGesture(shortcut.swipeGesture()); + connect(shortcut.action(), &QAction::destroyed, this, &GlobalShortcutsManager::objectDeleted); + m_shortcuts.push_back(std::move(shortcut)); +} + +bool GlobalShortcutsManager::processKey(Qt::KeyboardModifiers mods, int keyQt, KeyboardKeyState state) +{ +#if KWIN_BUILD_GLOBALSHORTCUTS + if (m_kglobalAccelInterface) { + auto check = [this](Qt::KeyboardModifiers mods, int keyQt, KeyboardKeyState keyState) { + bool retVal = false; + QMetaObject::invokeMethod(m_kglobalAccelInterface, + "checkKeyPressed", + Qt::DirectConnection, + Q_RETURN_ARG(bool, retVal), + Q_ARG(int, int(mods) | keyQt), + Q_ARG(KeyboardKeyState, keyState)); + return retVal; + }; + if (check(mods, keyQt, state)) { + return true; + } else if (keyQt == Qt::Key_Backtab) { + // KGlobalAccel on X11 has some workaround for Backtab + // see kglobalaccel/src/runtime/plugins/xcb/kglobalccel_x11.cpp method x11KeyPress + // Apparently KKeySequenceWidget captures Shift+Tab instead of Backtab + // thus if the key is backtab we should adjust to add shift again and use tab + // in addition KWin registers the shortcut incorrectly as Alt+Shift+Backtab + // this should be changed to either Alt+Backtab or Alt+Shift+Tab to match KKeySequenceWidget + // trying the variants + if (check(mods | Qt::ShiftModifier, keyQt, state)) { + return true; + + } + if (check(mods | Qt::ShiftModifier, Qt::Key_Tab, state)) { + return true; + } + } + } +#endif + return false; +} + +template +GlobalShortcut *match(QList &shortcuts, Args... args) +{ + for (auto &sc : shortcuts) { + if (std::holds_alternative(sc.shortcut())) { + if (std::get(sc.shortcut()) == ShortcutKind{args...}) { + return ≻ + } + } + } + return nullptr; +} + +// TODO(C++20): use ranges for a nicer way of filtering by shortcut type +bool GlobalShortcutsManager::processPointerPressed(Qt::KeyboardModifiers mods, Qt::MouseButtons pointerButtons) +{ +#if KWIN_BUILD_GLOBALSHORTCUTS + // currently only used to better support modifier only shortcuts + // modifier-only shortcuts are not triggered if a pointer button is pressed + if (m_kglobalAccelInterface) { + QMetaObject::invokeMethod(m_kglobalAccelInterface, + "checkPointerPressed", + Qt::DirectConnection, + Q_ARG(Qt::MouseButtons, pointerButtons)); + } +#endif + GlobalShortcut *shortcut = match(m_shortcuts, mods, pointerButtons); + if (shortcut) { + shortcut->invoke(); + } + return shortcut != nullptr; +} + +bool GlobalShortcutsManager::processAxis(Qt::KeyboardModifiers mods, PointerAxisDirection axis, qreal delta) +{ +#if KWIN_BUILD_GLOBALSHORTCUTS + // currently only used to better support modifier only shortcuts + // modifier-only shortcuts are not triggered if a pointer axis is used + if (m_kglobalAccelInterface) { + QMetaObject::invokeMethod(m_kglobalAccelInterface, + "checkAxisTriggered", + Qt::DirectConnection, + Q_ARG(int, axis)); + } +#endif + GlobalShortcut *shortcut = match(m_shortcuts, mods, axis); + if (shortcut && std::abs(delta) >= 1.0f) { + shortcut->invoke(); + } + return shortcut != nullptr; +} + +void GlobalShortcutsManager::processSwipeStart(DeviceType device, uint fingerCount) +{ + if (device == DeviceType::Touchpad) { + m_touchpadGestureRecognizer->startSwipeGesture(fingerCount); + } else { + m_touchscreenGestureRecognizer->startSwipeGesture(fingerCount); + } +} + +void GlobalShortcutsManager::processSwipeUpdate(DeviceType device, const QPointF &delta) +{ + if (device == DeviceType::Touchpad) { + m_touchpadGestureRecognizer->updateSwipeGesture(delta); + } else { + m_touchscreenGestureRecognizer->updateSwipeGesture(delta); + } +} + +void GlobalShortcutsManager::processSwipeCancel(DeviceType device) +{ + if (device == DeviceType::Touchpad) { + m_touchpadGestureRecognizer->cancelSwipeGesture(); + } else { + m_touchscreenGestureRecognizer->cancelSwipeGesture(); + } +} + +void GlobalShortcutsManager::processSwipeEnd(DeviceType device) +{ + if (device == DeviceType::Touchpad) { + m_touchpadGestureRecognizer->endSwipeGesture(); + } else { + m_touchscreenGestureRecognizer->endSwipeGesture(); + } + // TODO: cancel on Wayland Seat if one triggered +} + +void GlobalShortcutsManager::processPinchStart(uint fingerCount) +{ + m_touchpadGestureRecognizer->startPinchGesture(fingerCount); +} + +void GlobalShortcutsManager::processPinchUpdate(qreal scale, qreal angleDelta, const QPointF &delta) +{ + m_touchpadGestureRecognizer->updatePinchGesture(scale, angleDelta, delta); +} + +void GlobalShortcutsManager::processPinchCancel() +{ + m_touchpadGestureRecognizer->cancelPinchGesture(); +} + +void GlobalShortcutsManager::processPinchEnd() +{ + m_touchpadGestureRecognizer->endPinchGesture(); +} + +void GlobalShortcutsManager::cancelModiferOnlySequence() +{ +#if KWIN_BUILD_GLOBALSHORTCUTS + if (m_kglobalAccelInterface) { + QMetaObject::invokeMethod(m_kglobalAccelInterface, "cancelModiferOnlySequence"); + } +#endif +} + +} // namespace + +#include "moc_globalshortcuts.cpp" diff --git a/local/recipes/kde/kwin/source/src/globalshortcuts.h b/local/recipes/kde/kwin/source/src/globalshortcuts.h new file mode 100644 index 0000000000..27722bb09f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/globalshortcuts.h @@ -0,0 +1,203 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +// KWin +#include "effect/globals.h" +// Qt +#include "core/inputdevice.h" + +#include + +#include + +class QAction; +#if KWIN_BUILD_GLOBALSHORTCUTS +class KGlobalAccelD; +class KGlobalAccelInterface; +#endif +namespace KWin +{ +class GlobalShortcut; +class SwipeGesture; +class PinchGesture; +class GestureRecognizer; + +enum class DeviceType { + Touchpad, + Touchscreen +}; + +/** + * @brief Manager for the global shortcut system inside KWin. + * + * This class is responsible for holding all the global shortcuts and to process a key press event. + * That is trigger a shortcut if there is a match. + * + * For internal shortcut handling (those which are delivered inside KWin) QActions are used and + * triggered if the shortcut matches. For external shortcut handling a DBus interface is used. + */ +class GlobalShortcutsManager : public QObject +{ + Q_OBJECT +public: + explicit GlobalShortcutsManager(QObject *parent = nullptr); + ~GlobalShortcutsManager() override; + void init(); + + /** + * @brief Registers an internal global pointer shortcut + * + * @param action The action to trigger if the shortcut is pressed + * @param modifiers The modifiers which need to be hold to trigger the action + * @param pointerButtons The pointer button which needs to be pressed + */ + void registerPointerShortcut(QAction *action, Qt::KeyboardModifiers modifiers, Qt::MouseButtons pointerButtons); + /** + * @brief Registers an internal global axis shortcut + * + * @param action The action to trigger if the shortcut is triggered + * @param modifiers The modifiers which need to be hold to trigger the action + * @param axis The pointer axis + */ + void registerAxisShortcut(QAction *action, Qt::KeyboardModifiers modifiers, PointerAxisDirection axis); + + void registerTouchpadSwipe(SwipeDirection direction, uint32_t fingerCount, QAction *action, std::function progressCallback = {}); + void registerTouchpadSwipe(SwipeGesture *swipeGesture); + void registerTouchpadPinch(PinchDirection direction, uint32_t fingerCount, QAction *action, std::function progressCallback = {}); + void registerTouchpadPinch(PinchGesture *pinchGesture); + void registerTouchscreenSwipe(SwipeDirection direction, uint32_t fingerCount, QAction *action, std::function progressCallback = {}); + void registerTouchscreenSwipe(SwipeGesture *swipeGesture); + void forceRegisterTouchscreenSwipe(SwipeDirection direction, uint32_t fingerCount, QAction *action, std::function progressCallback = {}); + + /** + * @brief Processes a key event to decide whether a shortcut needs to be triggered. + * + * If a shortcut triggered this method returns @c true to indicate to the caller that the event + * should not be further processed. If there is no shortcut which triggered for the key, then + * @c false is returned. + * + * @param modifiers The current hold modifiers + * @param keyQt The Qt::Key which got pressed + * @return @c true if a shortcut triggered, @c false otherwise + */ + bool processKey(Qt::KeyboardModifiers modifiers, int keyQt, KeyboardKeyState state); + bool processPointerPressed(Qt::KeyboardModifiers modifiers, Qt::MouseButtons pointerButtons); + /** + * @brief Processes a pointer axis event to decide whether a shortcut needs to be triggered. + * + * If a shortcut triggered this method returns @c true to indicate to the caller that the event + * should not be further processed. If there is no shortcut which triggered for the key, then + * @c false is returned. + * + * @param modifiers The current hold modifiers + * @param axis The axis direction which has triggered this event + * @return @c true if a shortcut triggered, @c false otherwise + */ + bool processAxis(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis, qreal delta); + + void processSwipeStart(DeviceType device, uint fingerCount); + void processSwipeUpdate(DeviceType device, const QPointF &delta); + void processSwipeCancel(DeviceType device); + void processSwipeEnd(DeviceType device); + + void processPinchStart(uint fingerCount); + void processPinchUpdate(qreal scale, qreal angleDelta, const QPointF &delta); + void processPinchCancel(); + void processPinchEnd(); + + void cancelModiferOnlySequence(); + +private: + void objectDeleted(QObject *object); + bool add(GlobalShortcut sc, DeviceType device = DeviceType::Touchpad); + + QList m_shortcuts; + +#if KWIN_BUILD_GLOBALSHORTCUTS + std::unique_ptr m_kglobalAccel; + KGlobalAccelInterface *m_kglobalAccelInterface = nullptr; +#endif + std::unique_ptr m_touchpadGestureRecognizer; + std::unique_ptr m_touchscreenGestureRecognizer; +}; + +struct KeyboardShortcut +{ + QKeySequence sequence; + bool operator==(const KeyboardShortcut &rhs) const + { + return sequence == rhs.sequence; + } +}; +struct PointerButtonShortcut +{ + Qt::KeyboardModifiers pointerModifiers; + Qt::MouseButtons pointerButtons; + bool operator==(const PointerButtonShortcut &rhs) const + { + return pointerModifiers == rhs.pointerModifiers && pointerButtons == rhs.pointerButtons; + } +}; +struct PointerAxisShortcut +{ + Qt::KeyboardModifiers axisModifiers; + PointerAxisDirection axisDirection; + bool operator==(const PointerAxisShortcut &rhs) const + { + return axisModifiers == rhs.axisModifiers && axisDirection == rhs.axisDirection; + } +}; +struct RealtimeFeedbackSwipeShortcut +{ + DeviceType device; + SwipeDirection direction; + std::function progressCallback; + uint fingerCount; + + template + bool operator==(const T &rhs) const + { + return direction == rhs.direction && fingerCount == rhs.fingerCount && device == rhs.device; + } +}; +struct RealtimeFeedbackPinchShortcut +{ + PinchDirection direction; + std::function scaleCallback; + uint fingerCount; + + template + bool operator==(const T &rhs) const + { + return direction == rhs.direction && fingerCount == rhs.fingerCount; + } +}; + +using Shortcut = std::variant; + +class GlobalShortcut +{ +public: + GlobalShortcut(Shortcut &&shortcut, QAction *action); + ~GlobalShortcut(); + + void invoke() const; + QAction *action() const; + const Shortcut &shortcut() const; + SwipeGesture *swipeGesture() const; + PinchGesture *pinchGesture() const; + +private: + std::shared_ptr m_swipeGesture; + std::shared_ptr m_pinchGesture; + Shortcut m_shortcut = {}; + QAction *m_action = nullptr; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/group.cpp b/local/recipes/kde/kwin/source/src/group.cpp new file mode 100644 index 0000000000..cd61444aad --- /dev/null +++ b/local/recipes/kde/kwin/source/src/group.cpp @@ -0,0 +1,147 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "group.h" +#include "effect/effecthandler.h" +#include "workspace.h" +#include "x11window.h" + +#include +#include +#include + +namespace KWin +{ + +//******************************************** +// Group +//******************************************** + +Group::Group(xcb_window_t leader_P) + : leader_client(nullptr) + , leader_wid(leader_P) + , leader_info(nullptr) + , user_time(-1U) + , refcount(0) +{ + if (leader_P != XCB_WINDOW_NONE) { + leader_client = workspace()->findClient(leader_P); + leader_info = std::make_unique(kwinApp()->x11Connection(), leader_P, kwinApp()->x11RootWindow(), + NET::Properties(), NET::WM2StartupId); + } + effect_group = std::make_unique(this); + workspace()->addGroup(this); +} + +Group::~Group() = default; + +QIcon Group::icon() const +{ + if (leader_client != nullptr) { + return leader_client->icon(); + } else if (leader_wid != XCB_WINDOW_NONE) { + QIcon ic; + NETWinInfo info(kwinApp()->x11Connection(), leader_wid, kwinApp()->x11RootWindow(), NET::WMIcon, NET::WM2IconPixmap); + auto readIcon = [&ic, &info, this](int size, bool scale = true) { + const QPixmap pix = KX11Extras::icon(leader_wid, size, size, scale, KX11Extras::NETWM | KX11Extras::WMHints, &info); + if (!pix.isNull()) { + ic.addPixmap(pix); + } + }; + readIcon(16); + readIcon(32); + readIcon(48, false); + readIcon(64, false); + readIcon(128, false); + return ic; + } + return QIcon(); +} + +void Group::addMember(X11Window *member_P) +{ + _members.append(member_P); + // qDebug() << "GROUPADD:" << this << ":" << member_P; + // qDebug() << kBacktrace(); +} + +void Group::removeMember(X11Window *member_P) +{ + // qDebug() << "GROUPREMOVE:" << this << ":" << member_P; + // qDebug() << kBacktrace(); + Q_ASSERT(_members.contains(member_P)); + _members.removeAll(member_P); + // there are cases when automatic deleting of groups must be delayed, + // e.g. when removing a member and doing some operation on the possibly + // other members of the group (which would be however deleted already + // if there were no other members) + if (refcount == 0 && _members.isEmpty()) { + workspace()->removeGroup(this); + delete this; + } +} + +void Group::ref() +{ + ++refcount; +} + +void Group::deref() +{ + if (--refcount == 0 && _members.isEmpty()) { + workspace()->removeGroup(this); + delete this; + } +} + +void Group::gotLeader(X11Window *leader_P) +{ + Q_ASSERT(leader_P->window() == leader_wid); + leader_client = leader_P; +} + +void Group::lostLeader() +{ + Q_ASSERT(!_members.contains(leader_client)); + leader_client = nullptr; + if (_members.isEmpty()) { + workspace()->removeGroup(this); + delete this; + } +} + +void Group::startupIdChanged() +{ + KStartupInfoId asn_id; + KStartupInfoData asn_data; + bool asn_valid = workspace()->checkStartupNotification(leader_wid, asn_id, asn_data); + if (!asn_valid) { + return; + } + if (asn_id.timestamp() != 0 && user_time != -1U + && NET::timestampCompare(asn_id.timestamp(), user_time) > 0) { + user_time = asn_id.timestamp(); + } +} + +void Group::updateUserTime(xcb_timestamp_t time) +{ + // copy of X11Window::updateUserTime + if (time == XCB_CURRENT_TIME) { + time = kwinApp()->x11Time(); + } + if (time != -1U + && (user_time == XCB_CURRENT_TIME + || NET::timestampCompare(time, user_time) > 0)) { // time > user_time + user_time = time; + } +} + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/group.h b/local/recipes/kde/kwin/source/src/group.h new file mode 100644 index 0000000000..55efba4ce9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/group.h @@ -0,0 +1,88 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#if !KWIN_BUILD_X11 +#error Do not include on non-X11 builds +#endif + +#include "utils/common.h" +#include + +namespace KWin +{ + +class EffectWindowGroup; +class X11Window; + +class Group +{ +public: + Group(xcb_window_t leader); + ~Group(); + xcb_window_t leader() const; + const X11Window *leaderClient() const; + X11Window *leaderClient(); + const QList &members() const; + QIcon icon() const; + void addMember(X11Window *member); + void removeMember(X11Window *member); + void gotLeader(X11Window *leader); + void lostLeader(); + void updateUserTime(xcb_timestamp_t time); + xcb_timestamp_t userTime() const; + void ref(); + void deref(); + EffectWindowGroup *effectGroup(); + +private: + void startupIdChanged(); + QList _members; + X11Window *leader_client; + xcb_window_t leader_wid; + std::unique_ptr leader_info; + xcb_timestamp_t user_time; + int refcount; + std::unique_ptr effect_group; +}; + +inline xcb_window_t Group::leader() const +{ + return leader_wid; +} + +inline const X11Window *Group::leaderClient() const +{ + return leader_client; +} + +inline X11Window *Group::leaderClient() +{ + return leader_client; +} + +inline const QList &Group::members() const +{ + return _members; +} + +inline xcb_timestamp_t Group::userTime() const +{ + return user_time; +} + +inline EffectWindowGroup *Group::effectGroup() +{ + return effect_group.get(); +} + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/helpers/CMakeLists.txt b/local/recipes/kde/kwin/source/src/helpers/CMakeLists.txt new file mode 100644 index 0000000000..3a570186af --- /dev/null +++ b/local/recipes/kde/kwin/source/src/helpers/CMakeLists.txt @@ -0,0 +1,3 @@ +add_subdirectory(killer) +add_subdirectory(wayland_wrapper) +add_subdirectory(kwindowprop) diff --git a/local/recipes/kde/kwin/source/src/helpers/killer/CMakeLists.txt b/local/recipes/kde/kwin/source/src/helpers/killer/CMakeLists.txt new file mode 100644 index 0000000000..7eaacbb5d2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/helpers/killer/CMakeLists.txt @@ -0,0 +1,27 @@ +########### next target ############### + +set(kwin_killer_helper_SRCS killer.cpp) + +add_executable(kwin_killer_helper ${kwin_killer_helper_SRCS}) + +target_link_libraries(kwin_killer_helper + KF6::AuthCore + KF6::GuiAddons + KF6::I18n + KF6::Service + KF6::WidgetsAddons + KF6::WindowSystem + Qt::GuiPrivate + Qt::Widgets +) + +ecm_qt_declare_logging_category(kwin_killer_helper + HEADER debug.h + IDENTIFIER KWIN_KILLER + CATEGORY_NAME org.kde.kwin.killer +) + +install(TARGETS kwin_killer_helper DESTINATION ${KDE_INSTALL_LIBEXECDIR}) + +configure_file(org.kde.kwin.killer.desktop.in ${CMAKE_CURRENT_BINARY_DIR}/org.kde.kwin.killer.desktop @ONLY) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.kwin.killer.desktop DESTINATION ${KDE_INSTALL_APPDIR}) diff --git a/local/recipes/kde/kwin/source/src/helpers/killer/killer.cpp b/local/recipes/kde/kwin/source/src/helpers/killer/killer.cpp new file mode 100644 index 0000000000..4f73e1ef11 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/helpers/killer/killer.cpp @@ -0,0 +1,280 @@ +/* + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2023 Kai Uwe Broulik + SPDX-FileCopyrightText: 2024 Harald Sitter + + SPDX-License-Identifier: MIT + +*/ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include + +#include "debug.h" + +using namespace std::chrono_literals; + +namespace +{ +#if defined(Q_OS_LINUX) +std::optional exeOf(pid_t pid) +{ + const QFileInfo info(QStringLiteral("/proc/%1/exe").arg(QString::number(pid))); + auto baseName = QFileInfo(info.canonicalFilePath()).baseName(); // not const to allow move return + if (baseName.isEmpty()) { + qCWarning(KWIN_KILLER) << "Failed to resolve exe of pid" << pid; + return std::nullopt; + } + return baseName; +} + +std::optional bootId() +{ + QFile file(QStringLiteral("/proc/sys/kernel/random/boot_id")); + if (!file.open(QFile::ReadOnly)) { + qCWarning(KWIN_KILLER) << "Failed to read /proc/sys/kernel/random/boot_id" << file.errorString(); + return std::nullopt; + } + return QString::fromUtf8(file.readAll().simplified().replace('-', QByteArrayView())); +} + +void writeApplicationNotResponding(pid_t pid) +{ + const QString dirPath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/drkonqi/application-not-responding/"); + QDir dir(dirPath); + if (!dir.exists()) { + if (!dir.mkpath(dirPath)) { + qCWarning(KWIN_KILLER) << "Failed to create ApplicationNotResponding path" << dirPath; + return; + } + } + // $exe.$bootid.$pid.$time_at_time_of_crash.ini + const auto optionalExe = exeOf(pid); + if (!optionalExe) { + return; + } + const auto optionalBootId = bootId(); + if (!optionalBootId) { + return; + } + const QString anrPath = dirPath + QStringLiteral("/%1.%2.%3.%4.json").arg(optionalExe.value(), optionalBootId.value(), QString::number(pid), QString::number(QDateTime::currentMSecsSinceEpoch())); + QFile file(anrPath); + if (!file.open(QFile::NewOnly)) { + qCWarning(KWIN_KILLER) << "Failed to create ApplicationNotResponding file" << anrPath << file.error() << file.errorString(); + return; + } + // No content for now, simply close it once created +} +#endif // Q_OS_LINUX + +bool hasPidAborted(pid_t pid) +{ + constexpr auto timeout = 4000ms; + constexpr auto interval = 250ms; + constexpr auto iterations = timeout / interval; + for (int i = 0; i < iterations; i++) { + int status = 0; + if (waitpid(pid, &status, WNOHANG | WNOWAIT) == pid) { + if (WIFSIGNALED(status) || WIFEXITED(status)) { + // PID terminated by signal … or exited, but that should not really happen as a result of ABRT + return true; + } + } + QThread::sleep(interval); + } + return false; +} + +} // namespace + +int main(int argc, char *argv[]) +{ + KLocalizedString::setApplicationDomain(QByteArrayLiteral("kwin")); + QApplication app(argc, argv); + QApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("tools-report-bug"))); + QCoreApplication::setApplicationName(QStringLiteral("kwin_killer_helper")); + QCoreApplication::setOrganizationDomain(QStringLiteral("kde.org")); + QApplication::setApplicationDisplayName(i18n("Window Manager")); + QCoreApplication::setApplicationVersion(QStringLiteral("1.0")); + QApplication::setDesktopFileName(QStringLiteral("org.kde.kwin.killer")); + + QCommandLineOption pidOption(QStringLiteral("pid"), + i18n("PID of the application to terminate"), i18n("pid")); + QCommandLineOption hostNameOption(QStringLiteral("hostname"), + i18n("Hostname on which the application is running"), i18n("hostname")); + QCommandLineOption windowNameOption(QStringLiteral("windowname"), + i18n("Caption of the window to be terminated"), i18n("caption")); + QCommandLineOption applicationNameOption(QStringLiteral("applicationname"), + i18n("Name of the application to be terminated"), i18n("name")); + QCommandLineOption widOption(QStringLiteral("wid"), + i18n("ID of resource belonging to the application"), i18n("id")); + QCommandLineOption timestampOption(QStringLiteral("timestamp"), + i18n("Time of user action causing termination"), i18n("time")); + QCommandLineParser parser; + parser.setApplicationDescription(i18n("KWin helper utility")); + parser.addHelpOption(); + parser.addVersionOption(); + + parser.addOption(pidOption); + parser.addOption(hostNameOption); + parser.addOption(windowNameOption); + parser.addOption(applicationNameOption); + parser.addOption(widOption); + parser.addOption(timestampOption); + + parser.process(app); + + const bool isX11 = app.platformName() == QLatin1String("xcb"); + + QString hostname = parser.value(hostNameOption); + bool pid_ok = false; + pid_t pid = parser.value(pidOption).toULong(&pid_ok); + QString caption = parser.value(windowNameOption); + QString appname = parser.value(applicationNameOption); + QString windowHandle = parser.value(widOption); + + bool time_ok = false; + xcb_timestamp_t timestamp = parser.value(timestampOption).toULong(&time_ok); + + if (!pid_ok || pid == 0 || windowHandle.isEmpty() + || (isX11 && (!time_ok || timestamp == XCB_CURRENT_TIME)) + || hostname.isEmpty() || caption.isEmpty() || appname.isEmpty()) { + fprintf(stdout, "%s\n", qPrintable(i18n("This helper utility is not supposed to be called directly."))); + parser.showHelp(1); + } + bool isLocal = hostname == QLatin1StringView("localhost"); + + const auto service = KService::serviceByDesktopName(appname); + if (service) { + appname = service->name(); + QApplication::setApplicationDisplayName(appname); + } + + // Drop redundant application name, cf. QXcbWindow::setWindowTitle. + const QString titleSeparator = QString::fromUtf8(" \xe2\x80\x94 "); // // U+2014, EM DASH + caption.remove(titleSeparator + appname); + caption.remove(QStringLiteral(" – ") + appname); // EN dash (Firefox) + caption.remove(QStringLiteral(" - ") + appname); // classic minus :-) + + caption = caption.toHtmlEscaped(); + appname = appname.toHtmlEscaped(); + hostname = hostname.toHtmlEscaped(); + QString pidString = QString::number(pid); // format pid ourself as it does not make sense to format an ID according to locale settings + + const QString question = (caption == appname) + ? xi18nc("@info", + "%1 is not responding. Do you want to terminate this application?" + "Terminating this application will close all of its windows. Any unsaved data will be lost.", + appname) + : xi18nc("@info \"window title\" of application name is not responding.", + "\"%1\" of %2 is not responding. Do you want to terminate this application?" + "Terminating this application will close all of its windows. Any unsaved data will be lost.", + caption, appname); + + KGuiItem continueButton = KGuiItem(i18nc("@action:button Terminate app", "&Terminate %1", appname), QStringLiteral("application-exit")); + KGuiItem cancelButton = KGuiItem(i18nc("@action:button Wait for frozen app to maybe respond again", "&Wait Longer"), QStringLiteral("chronometer")); + + if (isX11) { + QX11Info::setAppUserTime(timestamp); + } + + auto *dialog = new KMessageDialog(KMessageDialog::WarningContinueCancel, question); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->setModal(true); + dialog->setCaption(i18nc("@title:window", "Not Responding")); + + QIcon icon; + if (service) { + const QIcon appIcon = QIcon::fromTheme(service->icon()); + if (!appIcon.isNull()) { + // emblem-warning is non-standard, fall back to emblem-important if necessary. + const QIcon warningBadge = QIcon::fromTheme(QStringLiteral("emblem-warning"), QIcon::fromTheme(QStringLiteral("emblem-important"))); + + icon = KIconUtils::addOverlay(appIcon, warningBadge, qApp->isRightToLeft() ? Qt::BottomLeftCorner : Qt::BottomRightCorner); + } + } + dialog->setIcon(icon); // null icon will result in default warning icon. + dialog->setButtons(continueButton, KGuiItem(), cancelButton); + + QStringList details{ + i18nc("@info", "Process ID: %1", pidString)}; + if (!isLocal) { + details << i18nc("@info", "Host name: %1", hostname); + } + dialog->setDetails(details.join(QLatin1Char('\n'))); + dialog->winId(); + + KWindowSystem::setMainWindow(dialog->windowHandle(), windowHandle); + + QObject::connect(dialog, &QDialog::finished, &app, [pid, hostname, isLocal](int result) { + if (result == KMessageBox::PrimaryAction) { +#if defined(Q_OS_LINUX) + writeApplicationNotResponding(pid); +#endif + if (!isLocal) { + QStringList lst; + lst << hostname << QStringLiteral("kill") << QString::number(pid); + QProcess::startDetached(QStringLiteral("xon"), lst); + } else { + // First try to abort so KCrash (or other handlers) and/or coredumpd can kick in and record the malfunction. + // This specifically allows application authors to notice that something is broken. + if (::kill(pid, SIGABRT) == 0 && hasPidAborted(pid)) { + return; + } + + // If that did not work send a kill. Kill cannot be ignored and always terminates. + if (::kill(pid, SIGKILL) && errno == EPERM) { + // If killing failed on permissions try again with the polkit helper. + KAuth::Action killer(QStringLiteral("org.kde.ksysguard.processlisthelper.sendsignal")); + killer.setHelperId(QStringLiteral("org.kde.ksysguard.processlisthelper")); + killer.addArgument(QStringLiteral("pid0"), pid); + killer.addArgument(QStringLiteral("pidcount"), 1); + killer.addArgument(QStringLiteral("signal"), SIGKILL); + if (killer.isValid()) { + qDebug() << "Using KAuth to kill pid: " << pid; + auto *job = killer.execute(); + job->exec(); + } else { + qDebug() << "KWin process killer action not valid"; + } + } + } + } + + qApp->quit(); + }); + + dialog->show(); + dialog->windowHandle()->requestActivate(); + + return app.exec(); +} diff --git a/local/recipes/kde/kwin/source/src/helpers/killer/org.kde.kwin.killer.desktop.in b/local/recipes/kde/kwin/source/src/helpers/killer/org.kde.kwin.killer.desktop.in new file mode 100755 index 0000000000..e1d60d7e14 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/helpers/killer/org.kde.kwin.killer.desktop.in @@ -0,0 +1,91 @@ +[Desktop Entry] +Type=Application +Name=KWin Kill Helper +Name[ar]=مساعد قاتل كوين +Name[be]=Памагаты па забіцці працэсаў KWin +Name[bg]=Помощник за прекратяване на приложения на KWin +Name[ca]=Ajudant de matar del KWin +Name[ca@valencia]=Ajudant de matar de KWin +Name[da]=KWin drab-hjælperen +Name[de]=KWin-Hilfsprogramm zum Beenden +Name[el]=Βοηθός τερματισμού εφαρμογής KWin +Name[en_GB]=KWin Kill Helper +Name[eo]=KWin Kill-Helpilo +Name[es]=Aplicación auxiliar Kill de KWin +Name[eu]=KWin akabatzeko laguntzailea +Name[fi]=KWinin sulkemisavustaja +Name[fr]=Assistant pour fermer KWin de force +Name[gl]=Asistente de matar de KWin +Name[he]=מסייע למחסל של KWin +Name[hu]=KWin kilövési segéd +Name[ia]=Adjutante a occider (Kill) de KWin +Name[is]=KWin drápsaðstoð +Name[it]=Assistente Kill di KWin +Name[ja]=KWin 強制終了ヘルパー +Name[ka]=KWin-ის მოკვლის დამხმარე პროგრამა +Name[ko]=KWin 강제 종료 도우미 +Name[lt]=KWin nutraukimo pagelbiklis +Name[lv]=„KWin“ nobeigšanas palīgs +Name[nb]=KWin-hjelper for tvangsavslutting +Name[nl]=KWin-hulpprogramma voor kill (afbreken) +Name[nn]=KWin-hjelpar for tvangsavslutting +Name[pl]=Narzędzie pomocnicze kończące KWin +Name[pt_BR]=Utilitário de ajuda do KWin para matar janelas +Name[ro]=Ajutător de omor KWin +Name[ru]=Вспомогательная программа для закрытия окон +Name[sa]=KWin किल हेल्पर +Name[sk]=KWin Kill Helper +Name[sl]=Kwin pomočnik odstranjevalnika +Name[sv]=Kwin hjälpverktyg för terminering +Name[ta]=கேவின் கட்டாயநிறுத்த உதவிநிரல் +Name[tr]=KWin Öldürme Yardımcısı +Name[uk]=Допоміжний засіб переривання KWin +Name[zh_CN]=KWin 强制终止辅助程序 +Name[zh_TW]=KWin 強制中止工具程式 +Comment=Prompts whether to kill an unresponsive window +Comment[ar]=يمكنك من قتل النوافذ التي لا تستجيب +Comment[be]=Падказвае, ці трэба закрываць акно, якое не адказвае +Comment[bg]=Запитва дали да прекрати принудително прозорец, който не реагира +Comment[ca]=Demana si cal matar una finestra que no respon +Comment[ca@valencia]=Demana si cal matar una finestra que no respon +Comment[cs]=Vybídne k zabití nereagujícího okna +Comment[da]=Spørger, om et vindue, der ikke svarer, skal dræbes +Comment[de]=Fragt, ob ein nicht antwortendes Fenster geschlossen werden soll +Comment[en_GB]=Prompts whether to kill an unresponsive window +Comment[eo]=Pridemandas ĉu ĉesigi nerespondantan fenestron +Comment[es]=Pregunta si se debe matar una ventana que no responde +Comment[eu]=Erantzuten ez duen leiho bat akabatzeko aukera ematen du +Comment[fi]=Kysyy, halutaanko vastaamaton ikkuna sulkea +Comment[fr]=Demande si une fenêtre ne répondant pas doit être fermer de force +Comment[gl]=Pregunta se matar unha xanela que non responde. +Comment[he]=שואל האם לחסל חלון לא פעיל +Comment[hu]=Rákérdez, hogy ki kell-e lőni egy nem reagáló ablakot +Comment[ia]=Demanda si occider un fenestra non respondente +Comment[is]=Spyr hvort drepa eigi glugga sem svarar ekki +Comment[it]=Chiede se chiudere una finestra che non risponde +Comment[ja]=応答していないウィンドウを強制終了するかどうか質問します +Comment[ka]=გკითხავთ, მოვკლა თუ არა ფანჯარა, რომელიც არ პასუხობს +Comment[ko]=응답하지 않는 창 강제 종료 여부 묻기 +Comment[lt]=Klausia, ar nutraukti nereaguojančio lango darbą +Comment[lv]=Vaicā, vai nobeigt nereaģējošu logu +Comment[nb]=Spør om du vil tvangsavslutte låste vinduer +Comment[nl]=Vraagt of een niet responderend venster afgebroken moet worden +Comment[nn]=Spør om du vil tvangsavslutta låste vindauge +Comment[pl]=Pyta czy zakończyć okno, które nie odpowiada +Comment[pt_BR]=Pergunta se uma janela que não responde deve ser encerrada +Comment[ro]=Întreabă dacă să fie omorâtă o fereastră ce nu răspunde +Comment[ru]=Запрос подтверждения завершения работы зависших приложений +Comment[sa]=अप्रतिसादं विण्डो मारयितव्यं वा इति प्रेरयति +Comment[sk]=Výzvy na zabitie nereagujúceho okna +Comment[sl]=Pozove, ali naj se odstrani neodzivno okno +Comment[sv]=Frågar om ett fönster som inte svarar ska termineras +Comment[ta]=பதிலளிக்காத சாளரத்தை கட்டாயமாக நிறுத்த வேண்டுமா என கேட்கும் +Comment[tr]=Yanıt vermeyen bir pencereyi öldürüp öldürmek istemediğinizi sorar +Comment[uk]=Надсилає запит щодо переривання програми у вікні, яке не відповідає на запити +Comment[zh_CN]=提示是否强制终止一个没有响应的窗口 +Comment[zh_TW]=詢問是否要強制中止某個沒有回應的視窗 +Exec=@KWIN_KILLER_BIN@ +Terminal=false +NoDisplay=true +Icon=tools-report-bug +StartupNotify=false diff --git a/local/recipes/kde/kwin/source/src/helpers/wayland_wrapper/CMakeLists.txt b/local/recipes/kde/kwin/source/src/helpers/wayland_wrapper/CMakeLists.txt new file mode 100644 index 0000000000..49078fd1ca --- /dev/null +++ b/local/recipes/kde/kwin/source/src/helpers/wayland_wrapper/CMakeLists.txt @@ -0,0 +1,23 @@ +add_executable(kwin_wayland_wrapper) +target_sources(kwin_wayland_wrapper PRIVATE + kwin_wrapper.cpp + wl-socket.c +) + +ecm_qt_declare_logging_category(kwin_wayland_wrapper + HEADER + wrapper_logging.h + IDENTIFIER + KWIN_WRAPPER + CATEGORY_NAME + kwin_wayland_wrapper + DEFAULT_SEVERITY + Warning +) + +target_link_libraries(kwin_wayland_wrapper Qt::Core Qt::DBus KF6::DBusAddons KF6::CoreAddons) +if (KWIN_BUILD_X11) + target_link_libraries(kwin_wayland_wrapper KWinXwaylandCommon) +endif() +set_property(TARGET kwin_wayland_wrapper PROPERTY C_STANDARD 11) +install(TARGETS kwin_wayland_wrapper ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/local/recipes/kde/kwin/source/src/helpers/wayland_wrapper/kwin_wrapper.cpp b/local/recipes/kde/kwin/source/src/helpers/wayland_wrapper/kwin_wrapper.cpp new file mode 100644 index 0000000000..55af30b21f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/helpers/wayland_wrapper/kwin_wrapper.cpp @@ -0,0 +1,214 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/** + * This tiny executable creates a socket, then starts kwin passing it the FD to the wayland socket + * along with the name of the socket to use + * On any non-zero kwin exit kwin gets restarted. + * + * After restart kwin is relaunched but now with the KWIN_RESTART_COUNT env set to an incrementing counter + * + * It's somewhat akin to systemd socket activation, but we also need the lock file, finding the next free socket + * and so on, hence our own binary. + * + * Usage kwin_wayland_wrapper [argForKwin] [argForKwin] ... + */ + +#include "config-kwin.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "wl-socket.h" +#include "wrapper_logging.h" + +#if KWIN_BUILD_X11 +#include "xauthority.h" +#include "xwaylandsocket.h" +#endif + +using namespace std::chrono_literals; + +class KWinWrapper : public QObject +{ + Q_OBJECT +public: + KWinWrapper(QObject *parent); + ~KWinWrapper(); + + void run(); + void restart(); + void terminate(std::chrono::milliseconds timeout); + +private: + wl_socket *m_socket; + + int m_crashCount = 0; + QProcess *m_kwinProcess = nullptr; + + const std::chrono::microseconds m_watchdogInterval; + bool m_watchdogIntervalOk; + +#if KWIN_BUILD_X11 + std::unique_ptr m_xwlSocket; + QTemporaryFile m_xauthorityFile; +#endif +}; + +KWinWrapper::KWinWrapper(QObject *parent) + : QObject(parent) + , m_kwinProcess(new QProcess(this)) + , m_watchdogInterval(std::chrono::microseconds(qEnvironmentVariableIntValue("WATCHDOG_USEC", &m_watchdogIntervalOk) / 2)) +{ + m_socket = wl_socket_create(); + if (!m_socket) { + qFatal("Could not create wayland socket"); + } + +#if KWIN_BUILD_X11 + if (qApp->arguments().contains(QLatin1String("--xwayland"))) { + m_xwlSocket = std::make_unique(KWin::XwaylandSocket::OperationMode::TransferFdsOnExec); + if (!m_xwlSocket->isValid()) { + qCWarning(KWIN_WRAPPER) << "Failed to create Xwayland connection sockets"; + m_xwlSocket.reset(); + } + if (m_xwlSocket) { + if (!qEnvironmentVariableIsSet("KWIN_WAYLAND_NO_XAUTHORITY")) { + if (!generateXauthorityFile(m_xwlSocket->display(), &m_xauthorityFile)) { + qCWarning(KWIN_WRAPPER) << "Failed to create an Xauthority file"; + } + } + } + } +#endif +} + +KWinWrapper::~KWinWrapper() +{ + wl_socket_destroy(m_socket); + terminate(30s); +} + +void KWinWrapper::run() +{ + m_kwinProcess->setProgram("kwin_wayland"); + + QStringList args; + + args << "--wayland-fd" << QString::number(wl_socket_get_fd(m_socket)); + args << "--socket" << QString::fromUtf8(wl_socket_get_display_name(m_socket)); + +#if KWIN_BUILD_X11 + if (m_xwlSocket) { + const auto xwaylandFileDescriptors = m_xwlSocket->fileDescriptors(); + for (const int &fileDescriptor : xwaylandFileDescriptors) { + args << "--xwayland-fd" << QString::number(fileDescriptor); + } + args << "--xwayland-display" << m_xwlSocket->name(); + if (m_xauthorityFile.open()) { + args << "--xwayland-xauthority" << m_xauthorityFile.fileName(); + } + } +#endif + + // attach our main process arguments + // the first entry is dropped as it will be our program name + args << qApp->arguments().mid(1); + + m_kwinProcess->setProcessChannelMode(QProcess::ForwardedChannels); + m_kwinProcess->setArguments(args); + + connect(m_kwinProcess, QOverload::of(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) { + if (exitCode == 0) { + qApp->quit(); + return; + } else if (exitCode == 133) { + m_crashCount = 0; + } else { + m_crashCount++; + } + + if (m_crashCount > 10) { + qApp->quit(); + return; + } + qputenv("KWIN_RESTART_COUNT", QByteArray::number(m_crashCount)); + // restart + m_kwinProcess->start(); + }); + + m_kwinProcess->start(); + + QProcessEnvironment env; + env.insert("WAYLAND_DISPLAY", QString::fromUtf8(wl_socket_get_display_name(m_socket))); +#if KWIN_BUILD_X11 + if (m_xwlSocket) { + env.insert("DISPLAY", m_xwlSocket->name()); + if (m_xauthorityFile.open()) { + env.insert("XAUTHORITY", m_xauthorityFile.fileName()); + } + } +#endif + auto envSyncJob = new KUpdateLaunchEnvironmentJob(env); + connect(envSyncJob, &KUpdateLaunchEnvironmentJob::finished, this, []() { + // The service name is merely there to indicate to the world that we're up and ready with all envs exported + QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.KWinWrapper")); + }); +} + +void KWinWrapper::terminate(std::chrono::milliseconds timeout) +{ + if (m_kwinProcess) { + disconnect(m_kwinProcess, nullptr, this, nullptr); + m_kwinProcess->terminate(); + m_kwinProcess->waitForFinished(timeout.count() / 2); + if (m_kwinProcess->state() != QProcess::NotRunning) { + m_kwinProcess->kill(); + m_kwinProcess->waitForFinished(timeout.count() / 2); + } + } +} + +void KWinWrapper::restart() +{ + terminate(m_watchdogIntervalOk ? std::chrono::duration_cast(m_watchdogInterval) : 30000ms); + m_kwinProcess->start(); +} + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + app.setQuitLockEnabled(false); // don't exit when the first KJob finishes + + KSignalHandler::self()->watchSignal(SIGTERM); + KSignalHandler::self()->watchSignal(SIGHUP); + + KWinWrapper wrapper(&app); + wrapper.run(); + + QObject::connect(KSignalHandler::self(), &KSignalHandler::signalReceived, &app, [&app, &wrapper](int signal) { + if (signal == SIGTERM) { + app.quit(); + } else if (signal == SIGHUP) { // The systemd service will issue SIGHUP when it's locked up so that we can restarted + wrapper.restart(); + } + }); + + return app.exec(); +} + +#include "kwin_wrapper.moc" diff --git a/local/recipes/kde/kwin/source/src/helpers/wayland_wrapper/wl-socket.c b/local/recipes/kde/kwin/source/src/helpers/wayland_wrapper/wl-socket.c new file mode 100644 index 0000000000..f94b7028e6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/helpers/wayland_wrapper/wl-socket.c @@ -0,0 +1,172 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 + SPDX-FileCopyrightText: 2008 Kristian Høgsberg + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#define _DEFAULT_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* This is the size of the char array in struct sock_addr_un. + * No Wayland socket can be created with a path longer than this, + * including the null terminator. + */ +#ifndef UNIX_PATH_MAX +#define UNIX_PATH_MAX 108 +#endif + +#define LOCK_SUFFIX ".lock" +#define LOCK_SUFFIXLEN 5 + +struct wl_socket { + int fd; + int fd_lock; + struct sockaddr_un addr; + char lock_addr[UNIX_PATH_MAX + LOCK_SUFFIXLEN]; + char display_name[20]; +}; + +static struct wl_socket *wl_socket_alloc(void) +{ + struct wl_socket *s; + + s = malloc(sizeof *s); + if (!s) + return NULL; + + s->fd = -1; + s->fd_lock = -1; + + return s; +} + +static int wl_socket_lock(struct wl_socket *socket) +{ + struct stat socket_stat; + + snprintf(socket->lock_addr, sizeof socket->lock_addr, "%s%s", socket->addr.sun_path, LOCK_SUFFIX); + + socket->fd_lock = open(socket->lock_addr, O_CREAT | O_CLOEXEC | O_RDWR, (S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP)); + + if (socket->fd_lock < 0) { + printf("unable to open lockfile %s check permissions\n", socket->lock_addr); + goto err; + } + + if (flock(socket->fd_lock, LOCK_EX | LOCK_NB) < 0) { + printf("unable to lock lockfile %s, maybe another compositor is running\n", socket->lock_addr); + goto err_fd; + } + + if (lstat(socket->addr.sun_path, &socket_stat) < 0) { + if (errno != ENOENT) { + printf("did not manage to stat file %s\n", socket->addr.sun_path); + goto err_fd; + } + } else if (socket_stat.st_mode & S_IWUSR || socket_stat.st_mode & S_IWGRP) { + unlink(socket->addr.sun_path); + } + + return 0; +err_fd: + close(socket->fd_lock); + socket->fd_lock = -1; +err: + *socket->lock_addr = 0; + /* we did not set this value here, but without lock the + * socket won't be created anyway. This prevents the + * wl_socket_destroy from unlinking already existing socket + * created by other compositor */ + *socket->addr.sun_path = 0; + + return -1; +} + +void wl_socket_destroy(struct wl_socket *s) +{ + if (s->addr.sun_path[0]) + unlink(s->addr.sun_path); + if (s->fd >= 0) + close(s->fd); + if (s->lock_addr[0]) + unlink(s->lock_addr); + if (s->fd_lock >= 0) + close(s->fd_lock); + + free(s); +} + +const char *wl_socket_get_display_name(struct wl_socket *s) +{ + return s->display_name; +} + +int wl_socket_get_fd(struct wl_socket *s) +{ + return s->fd; +} + +struct wl_socket *wl_socket_create() +{ + struct wl_socket *s; + int displayno = 0; + int name_size; + + /* A reasonable number of maximum default sockets. If + * you need more than this, use the explicit add_socket API. */ + const int MAX_DISPLAYNO = 32; + const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); + if (!runtime_dir) { + printf("XDG_RUNTIME_DIR not set"); + return NULL; + } + + s = wl_socket_alloc(); + if (s == NULL) + return NULL; + + do { + snprintf(s->display_name, sizeof s->display_name, "wayland-%d", displayno); + s->addr.sun_family = AF_LOCAL; + name_size = snprintf(s->addr.sun_path, sizeof s->addr.sun_path, "%s/%s", runtime_dir, s->display_name) + 1; + assert(name_size > 0); + + if (name_size > (int)sizeof s->addr.sun_path) { + goto fail; + } + + if (wl_socket_lock(s) < 0) + continue; + + s->fd = socket(PF_LOCAL, SOCK_STREAM, 0); + + int size = SUN_LEN(&s->addr); + int ret = bind(s->fd, (struct sockaddr*)&s->addr, size); + if (ret < 0) { + goto fail; + } + ret = listen(s->fd, 128); + if (ret < 0) { + goto fail; + } + return s; + } while (displayno++ < MAX_DISPLAYNO); + +fail: + wl_socket_destroy(s); + return NULL; +} diff --git a/local/recipes/kde/kwin/source/src/helpers/wayland_wrapper/wl-socket.h b/local/recipes/kde/kwin/source/src/helpers/wayland_wrapper/wl-socket.h new file mode 100644 index 0000000000..ac69ca955c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/helpers/wayland_wrapper/wl-socket.h @@ -0,0 +1,39 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Allocate and create a socket + * It is bound and accepted + */ +struct wl_socket *wl_socket_create(); + +/** + * Returns the file descriptor for the socket + */ +int wl_socket_get_fd(struct wl_socket *); + +/** + * Returns the name of the socket, i.e "wayland-0" + */ +char *wl_socket_get_display_name(struct wl_socket *); + +/** + * Cleanup resources and close the FD + */ +void wl_socket_destroy(struct wl_socket *socket); + +#ifdef __cplusplus +} +#endif diff --git a/local/recipes/kde/kwin/source/src/hide_cursor_spy.cpp b/local/recipes/kde/kwin/source/src/hide_cursor_spy.cpp new file mode 100644 index 0000000000..11ebb6bf2c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/hide_cursor_spy.cpp @@ -0,0 +1,69 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "hide_cursor_spy.h" +#include "cursor.h" +#include "input_event.h" +#include "main.h" + +namespace KWin +{ + +void HideCursorSpy::pointerMotion(PointerMotionEvent *event) +{ + if (!event->warp) { + showCursor(); + } +} + +void HideCursorSpy::pointerButton(PointerButtonEvent *event) +{ + showCursor(); +} + +void HideCursorSpy::pointerAxis(KWin::PointerAxisEvent *event) +{ + showCursor(); +} + +void HideCursorSpy::touchDown(TouchDownEvent *event) +{ + hideCursor(); +} + +void HideCursorSpy::tabletToolProximityEvent(TabletToolProximityEvent *event) +{ + if (event->type == TabletToolProximityEvent::Type::LeaveProximity) { + // If the tablet is in relative/mouse mode, keep it on the screen even if the pen is no longer in proximity + if (!event->device->tabletToolIsRelative()) { + hideCursor(); + } + } else { + showCursor(); + } +} + +void HideCursorSpy::showCursor() +{ + if (!m_cursorHidden) { + return; + } + m_cursorHidden = false; + Cursors::self()->showCursor(); +} + +void HideCursorSpy::hideCursor() +{ + if (m_cursorHidden) { + return; + } + m_cursorHidden = true; + Cursors::self()->hideCursor(); +} + +} diff --git a/local/recipes/kde/kwin/source/src/hide_cursor_spy.h b/local/recipes/kde/kwin/source/src/hide_cursor_spy.h new file mode 100644 index 0000000000..f52bc4e4fb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/hide_cursor_spy.h @@ -0,0 +1,31 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "input_event_spy.h" + +namespace KWin +{ + +class HideCursorSpy : public InputEventSpy +{ +public: + void pointerMotion(KWin::PointerMotionEvent *event) override; + void pointerButton(KWin::PointerButtonEvent *event) override; + void pointerAxis(KWin::PointerAxisEvent *event) override; + void touchDown(TouchDownEvent *event) override; + void tabletToolProximityEvent(KWin::TabletToolProximityEvent *event) override; + +private: + void showCursor(); + void hideCursor(); + + bool m_cursorHidden = false; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/idle_inhibition.cpp b/local/recipes/kde/kwin/source/src/idle_inhibition.cpp new file mode 100644 index 0000000000..8923a5915d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/idle_inhibition.cpp @@ -0,0 +1,100 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "idle_inhibition.h" +#include "input.h" +#include "virtualdesktops.h" +#include "wayland/surface.h" +#include "window.h" +#include "workspace.h" + +#include +#include + +namespace KWin +{ + +IdleInhibition::IdleInhibition(QObject *parent) + : QObject(parent) +{ + // Workspace is created after the wayland server is initialized. + connect(kwinApp(), &Application::workspaceCreated, this, &IdleInhibition::slotWorkspaceCreated); +} + +IdleInhibition::~IdleInhibition() = default; + +void IdleInhibition::registerClient(Window *client) +{ + if (!client->surface()) { + return; + } + + auto updateInhibit = [this, client] { + update(client); + }; + + m_connections[client] = connect(client->surface(), &SurfaceInterface::inhibitsIdleChanged, this, updateInhibit); + connect(client, &Window::desktopsChanged, this, updateInhibit); + connect(client, &Window::minimizedChanged, this, updateInhibit); + connect(client, &Window::hiddenChanged, this, updateInhibit); + connect(client, &Window::closed, this, [this, client]() { + uninhibit(client); + auto it = m_connections.find(client); + if (it != m_connections.end()) { + disconnect(it.value()); + m_connections.erase(it); + } + }); + + updateInhibit(); +} + +void IdleInhibition::inhibit(Window *client) +{ + input()->addIdleInhibitor(client); + // TODO: notify powerdevil? +} + +void IdleInhibition::uninhibit(Window *client) +{ + input()->removeIdleInhibitor(client); +} + +void IdleInhibition::update(Window *client) +{ + if (client->isInternal() || client->isUnmanaged()) { + return; + } + + // TODO: Don't honor the idle inhibitor object if the shell client is not + // on the current activity (currently, activities are not supported). + const bool visible = client->isShown() && client->isOnCurrentDesktop(); + if (visible && client->surface() && client->surface()->inhibitsIdle()) { + inhibit(client); + } else { + uninhibit(client); + } +} + +void IdleInhibition::slotWorkspaceCreated() +{ + connect(workspace(), &Workspace::windowAdded, this, &IdleInhibition::registerClient); + connect(workspace(), &Workspace::currentDesktopChanged, this, &IdleInhibition::slotDesktopChanged); +} + +void IdleInhibition::slotDesktopChanged() +{ + workspace()->forEachWindow([this](Window *c) { + update(c); + }); +} + +} + +#include "moc_idle_inhibition.cpp" diff --git a/local/recipes/kde/kwin/source/src/idle_inhibition.h b/local/recipes/kde/kwin/source/src/idle_inhibition.h new file mode 100644 index 0000000000..6c3492afe0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/idle_inhibition.h @@ -0,0 +1,40 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include +#include + +namespace KWin +{ +class Window; + +class IdleInhibition : public QObject +{ + Q_OBJECT +public: + explicit IdleInhibition(QObject *parent = nullptr); + ~IdleInhibition() override; + + void registerClient(Window *client); + +private Q_SLOTS: + void slotWorkspaceCreated(); + void slotDesktopChanged(); + +private: + void inhibit(Window *client); + void uninhibit(Window *client); + void update(Window *client); + + QMap m_connections; +}; +} diff --git a/local/recipes/kde/kwin/source/src/idledetector.cpp b/local/recipes/kde/kwin/source/src/idledetector.cpp new file mode 100644 index 0000000000..117119a8f7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/idledetector.cpp @@ -0,0 +1,94 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "idledetector.h" +#include "input.h" + +using namespace std::chrono_literals; + +namespace KWin +{ + +IdleDetector::IdleDetector(std::chrono::milliseconds timeout, OperatingMode mode, QObject *parent) + : QObject(parent) + , m_timeout(timeout) + , m_mode(mode) +{ + Q_ASSERT(timeout >= 0ms); + m_timer.start(timeout, this); + + input()->addIdleDetector(this); +} + +IdleDetector::~IdleDetector() +{ + if (input()) { + input()->removeIdleDetector(this); + } +} + +void IdleDetector::timerEvent(QTimerEvent *event) +{ + if (event->timerId() == m_timer.timerId()) { + m_timer.stop(); + markAsIdle(); + } +} + +IdleDetector::OperatingMode IdleDetector::mode() const +{ + return m_mode; +} + +bool IdleDetector::isInhibited() const +{ + return m_isInhibited; +} + +void IdleDetector::setInhibited(bool inhibited) +{ + if (m_mode == OperatingMode::IgnoresInhibitors) { + return; + }; + + if (m_isInhibited == inhibited) { + return; + } + m_isInhibited = inhibited; + if (inhibited) { + m_timer.stop(); + } else { + m_timer.start(m_timeout, this); + } +} + +void IdleDetector::activity() +{ + if (!m_isInhibited) { + m_timer.start(m_timeout, this); + markAsResumed(); + } +} + +void IdleDetector::markAsIdle() +{ + if (!m_isIdle) { + m_isIdle = true; + Q_EMIT idle(); + } +} + +void IdleDetector::markAsResumed() +{ + if (m_isIdle) { + m_isIdle = false; + Q_EMIT resumed(); + } +} + +} // namespace KWin + +#include "moc_idledetector.cpp" diff --git a/local/recipes/kde/kwin/source/src/idledetector.h b/local/recipes/kde/kwin/source/src/idledetector.h new file mode 100644 index 0000000000..760c069911 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/idledetector.h @@ -0,0 +1,55 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include +#include + +namespace KWin +{ + +class KWIN_EXPORT IdleDetector : public QObject +{ + Q_OBJECT + +public: + enum class OperatingMode { + FollowsInhibitors, + IgnoresInhibitors, + }; + + explicit IdleDetector(std::chrono::milliseconds timeout, OperatingMode mode, QObject *parent = nullptr); + ~IdleDetector() override; + + void activity(); + + OperatingMode mode() const; + + bool isInhibited() const; + void setInhibited(bool inhibited); + +Q_SIGNALS: + void idle(); + void resumed(); + +protected: + void timerEvent(QTimerEvent *event) override; + +private: + void markAsIdle(); + void markAsResumed(); + + QBasicTimer m_timer; + std::chrono::milliseconds m_timeout; + bool m_isIdle = false; + bool m_isInhibited = false; + OperatingMode m_mode = OperatingMode::FollowsInhibitors; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/input.cpp b/local/recipes/kde/kwin/source/src/input.cpp new file mode 100644 index 0000000000..fa509b0c82 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/input.cpp @@ -0,0 +1,3768 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + SPDX-FileCopyrightText: 2018 Roman Gilg + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "input.h" + +#include "backends/fakeinput/fakeinputbackend.h" +#include "core/inputbackend.h" +#include "core/inputdevice.h" +#include "core/session.h" +#include "effect/effecthandler.h" +#include "gestures.h" +#include "globalshortcuts.h" +#include "hide_cursor_spy.h" +#include "idledetector.h" +#include "input_event.h" +#include "input_event_spy.h" +#include "inputmethod.h" +#include "keyboard_input.h" +#include "main.h" +#include "mousebuttons.h" +#include "pointer_input.h" +#include "tablet_input.h" +#include "touch_input.h" +#include "wayland/abstract_data_source.h" +#include "wayland/xdgtopleveldrag_v1.h" +#if KWIN_BUILD_X11 +#include "x11window.h" +#endif +#if KWIN_BUILD_TABBOX +#include "tabbox/tabbox.h" +#endif +#include "core/output.h" +#include "core/outputbackend.h" +#include "cursor.h" +#include "cursorsource.h" +#include "internalwindow.h" +#include "popup_input_filter.h" +#include "screenedge.h" +#include "screenedgegestures.h" +#include "virtualdesktops.h" +#include "wayland/display.h" +#include "wayland/inputmethod_v1.h" +#include "wayland/seat.h" +#include "wayland/surface.h" +#include "wayland/tablet_v2.h" +#include "wayland_server.h" +#include "workspace.h" +#include "xdgactivationv1.h" +#include "xkb.h" +#include "xwayland/xwayland_interface.h" + +#include +#include +#include + +// screenlocker +#if KWIN_BUILD_SCREENLOCKER +#include +#endif +// Qt +#include +#include +#include +#include + +#include + +#include "osd.h" +#include "wayland/xdgshell.h" +#include +#include + +using namespace std::literals; + +namespace KWin +{ + +InputEventFilter::InputEventFilter(InputFilterOrder::Order weight) + : m_weight(weight) +{ +} + +InputEventFilter::~InputEventFilter() +{ + if (input()) { + input()->uninstallInputEventFilter(this); + } +} + +int InputEventFilter::weight() const +{ + return m_weight; +} + +bool InputEventFilter::pointerMotion(PointerMotionEvent *event) +{ + return false; +} + +bool InputEventFilter::pointerButton(PointerButtonEvent *event) +{ + return false; +} + +bool InputEventFilter::pointerFrame() +{ + return false; +} + +bool InputEventFilter::pointerAxis(PointerAxisEvent *event) +{ + return false; +} + +bool InputEventFilter::keyboardKey(KeyboardKeyEvent *event) +{ + return false; +} + +bool InputEventFilter::touchDown(TouchDownEvent *event) +{ + return false; +} + +bool InputEventFilter::touchMotion(TouchMotionEvent *event) +{ + return false; +} + +bool InputEventFilter::touchUp(TouchUpEvent *event) +{ + return false; +} + +bool InputEventFilter::touchCancel() +{ + return false; +} + +bool InputEventFilter::touchFrame() +{ + return false; +} + +bool InputEventFilter::pinchGestureBegin(PointerPinchGestureBeginEvent *event) +{ + return false; +} + +bool InputEventFilter::pinchGestureUpdate(PointerPinchGestureUpdateEvent *event) +{ + return false; +} + +bool InputEventFilter::pinchGestureEnd(PointerPinchGestureEndEvent *event) +{ + return false; +} + +bool InputEventFilter::pinchGestureCancelled(PointerPinchGestureCancelEvent *event) +{ + return false; +} + +bool InputEventFilter::swipeGestureBegin(PointerSwipeGestureBeginEvent *event) +{ + return false; +} + +bool InputEventFilter::swipeGestureUpdate(PointerSwipeGestureUpdateEvent *event) +{ + return false; +} + +bool InputEventFilter::swipeGestureEnd(PointerSwipeGestureEndEvent *event) +{ + return false; +} + +bool InputEventFilter::swipeGestureCancelled(PointerSwipeGestureCancelEvent *event) +{ + return false; +} + +bool InputEventFilter::holdGestureBegin(PointerHoldGestureBeginEvent *event) +{ + return false; +} + +bool InputEventFilter::holdGestureEnd(PointerHoldGestureEndEvent *event) +{ + return false; +} + +bool InputEventFilter::holdGestureCancelled(PointerHoldGestureCancelEvent *event) +{ + return false; +} + +bool InputEventFilter::switchEvent(SwitchEvent *event) +{ + return false; +} + +bool InputEventFilter::tabletToolProximityEvent(TabletToolProximityEvent *event) +{ + return false; +} + +bool InputEventFilter::tabletToolAxisEvent(TabletToolAxisEvent *event) +{ + return false; +} + +bool InputEventFilter::tabletToolTipEvent(TabletToolTipEvent *event) +{ + return false; +} + +bool InputEventFilter::tabletToolButtonEvent(TabletToolButtonEvent *event) +{ + return false; +} + +bool InputEventFilter::tabletPadButtonEvent(TabletPadButtonEvent *event) +{ + return false; +} + +bool InputEventFilter::tabletPadStripEvent(TabletPadStripEvent *event) +{ + return false; +} + +bool InputEventFilter::tabletPadRingEvent(TabletPadRingEvent *event) +{ + return false; +} + +bool InputEventFilter::tabletPadDialEvent(TabletPadDialEvent *event) +{ + return false; +} + +bool InputEventFilter::passToInputMethod(KeyboardKeyEvent *event) +{ + static QStringList s_deviceSkipsInputMethods = qEnvironmentVariable("KWIN_DEVICE_SKIPS_INPUT_METHOD").split(','); + if (!kwinApp()->inputMethod()) { + return false; + } + if (event->device && s_deviceSkipsInputMethods.contains(event->device->name())) { + return false; + } + if (auto keyboardGrab = kwinApp()->inputMethod()->keyboardGrab()) { + const auto timestamp = std::chrono::duration_cast(event->timestamp); + keyboardGrab->sendKey(waylandServer()->display()->nextSerial(), std::chrono::duration_cast(timestamp).count(), event->nativeScanCode, event->state); + return true; + } else { + kwinApp()->inputMethod()->commitPendingText(); + return false; + } +} + +class VirtualTerminalFilter : public InputEventFilter +{ +public: + VirtualTerminalFilter() + : InputEventFilter(InputFilterOrder::VirtualTerminal) + { + } + bool keyboardKey(KeyboardKeyEvent *event) override + { + // really on press and not on release? X11 switches on press. + if (event->state == KeyboardKeyState::Pressed) { + const xkb_keysym_t keysym = event->nativeVirtualKey; + if (keysym >= XKB_KEY_XF86Switch_VT_1 && keysym <= XKB_KEY_XF86Switch_VT_12) { + kwinApp()->session()->switchTo(keysym - XKB_KEY_XF86Switch_VT_1 + 1); + return true; + } + } + return false; + } +}; + +#if KWIN_BUILD_SCREENLOCKER +class LockScreenFilter : public InputEventFilter +{ +public: + LockScreenFilter() + : InputEventFilter(InputFilterOrder::LockScreen) + { + } + bool pointerMotion(PointerMotionEvent *event) override + { + if (!waylandServer()->isScreenLocked()) { + return false; + } + + ScreenLocker::KSldApp::self()->userActivity(); + + auto window = input()->findToplevel(event->position); + if (window && window->isClient() && window->isLockScreen()) { + workspace()->activateWindow(window); + } + + auto seat = waylandServer()->seat(); + if (pointerSurfaceAllowed()) { + // TODO: should the pointer position always stay in sync, i.e. not do the check? + seat->setTimestamp(event->timestamp); + seat->notifyPointerMotion(event->position); + } + return true; + } + bool pointerButton(PointerButtonEvent *event) override + { + if (!waylandServer()->isScreenLocked()) { + return false; + } + + ScreenLocker::KSldApp::self()->userActivity(); + + auto window = input()->findToplevel(event->position); + if (window && window->isClient() && window->isLockScreen()) { + workspace()->activateWindow(window); + } + + auto seat = waylandServer()->seat(); + if (pointerSurfaceAllowed()) { + // TODO: can we leak presses/releases here when we move the mouse in between from an allowed surface to + // disallowed one or vice versa? + seat->setTimestamp(event->timestamp); + seat->notifyPointerButton(event->nativeButton, event->state); + } + return true; + } + bool pointerFrame() override + { + if (!waylandServer()->isScreenLocked()) { + return false; + } + auto seat = waylandServer()->seat(); + if (pointerSurfaceAllowed()) { + seat->notifyPointerFrame(); + } + return true; + } + bool pointerAxis(PointerAxisEvent *event) override + { + if (!waylandServer()->isScreenLocked()) { + return false; + } + + ScreenLocker::KSldApp::self()->userActivity(); + + auto seat = waylandServer()->seat(); + if (pointerSurfaceAllowed()) { + seat->setTimestamp(event->timestamp); + seat->notifyPointerAxis(event->orientation, event->delta, event->deltaV120, event->source, event->inverted); + } + return true; + } + bool keyboardKey(KeyboardKeyEvent *event) override + { + if (!waylandServer()->isScreenLocked()) { + return false; + } + + // FIXME: Ideally we want to move all whitelisted global shortcuts here and process it here instead of lockscreen + if (event->key == Qt::Key_PowerOff) { + // globalshortcuts want to use this + return false; + } + + ScreenLocker::KSldApp::self()->userActivity(); + + // send event to KSldApp for global accel + // if event is set to accepted it means a whitelisted shortcut was triggered + // in that case we filter it out and don't process it further + QKeyEvent keyEvent(event->state == KeyboardKeyState::Released ? QEvent::KeyRelease : QEvent::KeyPress, + event->key, + event->modifiers, + event->nativeScanCode, + event->nativeVirtualKey, + 0, + event->text, + event->state == KeyboardKeyState::Repeated); + keyEvent.setAccepted(false); + QCoreApplication::sendEvent(ScreenLocker::KSldApp::self(), &keyEvent); + if (keyEvent.isAccepted()) { + return true; + } + + // continue normal processing + input()->keyboard()->update(); + if (!keyboardSurfaceAllowed()) { + // don't pass event to seat + return true; + } + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->timestamp); + seat->notifyKeyboardKey(event->nativeScanCode, event->state, event->serial); + return true; + } + bool touchDown(TouchDownEvent *event) override + { + if (!waylandServer()->isScreenLocked()) { + return false; + } + + ScreenLocker::KSldApp::self()->userActivity(); + + Window *window = input()->findToplevel(event->pos); + if (window && surfaceAllowed(window->surface())) { + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->time); + seat->notifyTouchDown(window->surface(), window->bufferGeometry().topLeft(), event->id, event->pos); + } + return true; + } + bool touchMotion(TouchMotionEvent *event) override + { + if (!waylandServer()->isScreenLocked()) { + return false; + } + + ScreenLocker::KSldApp::self()->userActivity(); + + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->time); + seat->notifyTouchMotion(event->id, event->pos); + return true; + } + bool touchUp(TouchUpEvent *event) override + { + if (!waylandServer()->isScreenLocked()) { + return false; + } + + ScreenLocker::KSldApp::self()->userActivity(); + + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->time); + seat->notifyTouchUp(event->id); + return true; + } + bool pinchGestureBegin(PointerPinchGestureBeginEvent *event) override + { + // no touchpad multi-finger gestures on lock screen + return waylandServer()->isScreenLocked(); + } + bool pinchGestureUpdate(PointerPinchGestureUpdateEvent *event) override + { + // no touchpad multi-finger gestures on lock screen + return waylandServer()->isScreenLocked(); + } + bool pinchGestureEnd(PointerPinchGestureEndEvent *event) override + { + // no touchpad multi-finger gestures on lock screen + return waylandServer()->isScreenLocked(); + } + bool pinchGestureCancelled(PointerPinchGestureCancelEvent *event) override + { + // no touchpad multi-finger gestures on lock screen + return waylandServer()->isScreenLocked(); + } + + bool swipeGestureBegin(PointerSwipeGestureBeginEvent *event) override + { + // no touchpad multi-finger gestures on lock screen + return waylandServer()->isScreenLocked(); + } + bool swipeGestureUpdate(PointerSwipeGestureUpdateEvent *event) override + { + // no touchpad multi-finger gestures on lock screen + return waylandServer()->isScreenLocked(); + } + bool swipeGestureEnd(PointerSwipeGestureEndEvent *event) override + { + // no touchpad multi-finger gestures on lock screen + return waylandServer()->isScreenLocked(); + } + bool swipeGestureCancelled(PointerSwipeGestureCancelEvent *event) override + { + // no touchpad multi-finger gestures on lock screen + return waylandServer()->isScreenLocked(); + } + bool holdGestureBegin(PointerHoldGestureBeginEvent *event) override + { + // no touchpad multi-finger gestures on lock screen + return waylandServer()->isScreenLocked(); + } + bool holdGestureEnd(PointerHoldGestureEndEvent *event) override + { + // no touchpad multi-finger gestures on lock screen + return waylandServer()->isScreenLocked(); + } + bool holdGestureCancelled(PointerHoldGestureCancelEvent *event) override + { + // no touchpad multi-finger gestures on lock screen + return waylandServer()->isScreenLocked(); + } + +private: + bool surfaceAllowed(SurfaceInterface *s) const + { + if (s) { + if (Window *t = waylandServer()->findWindow(s)) { + return t->isLockScreen() || t->isInputMethod() || t->isLockScreenOverlay(); + } + return false; + } + return true; + } + bool pointerSurfaceAllowed() const + { + return surfaceAllowed(waylandServer()->seat()->focusedPointerSurface()); + } + bool keyboardSurfaceAllowed() const + { + return surfaceAllowed(waylandServer()->seat()->focusedKeyboardSurface()); + } +}; +#endif + +class EffectsFilter : public InputEventFilter +{ +public: + EffectsFilter() + : InputEventFilter(InputFilterOrder::Effects) + { + } + bool pointerMotion(PointerMotionEvent *event) override + { + if (!effects) { + return false; + } + QMouseEvent mouseEvent(QEvent::MouseMove, + event->position, + event->position, + Qt::NoButton, + event->buttons, + event->modifiers); + mouseEvent.setTimestamp(std::chrono::duration_cast(event->timestamp).count()); + mouseEvent.setAccepted(false); + return effects->checkInputWindowEvent(&mouseEvent); + } + bool pointerButton(PointerButtonEvent *event) override + { + if (!effects) { + return false; + } + QMouseEvent mouseEvent(event->state == PointerButtonState::Pressed ? QEvent::MouseButtonPress : QEvent::MouseButtonRelease, + event->position, + event->position, + event->button, + event->buttons, + event->modifiers); + mouseEvent.setTimestamp(std::chrono::duration_cast(event->timestamp).count()); + mouseEvent.setAccepted(false); + return effects->checkInputWindowEvent(&mouseEvent); + } + bool pointerAxis(PointerAxisEvent *event) override + { + if (!effects) { + return false; + } + QWheelEvent wheelEvent(event->position, + event->position, + QPoint(), + (event->orientation == Qt::Horizontal) ? QPoint(event->delta, 0) : QPoint(0, event->delta), + event->buttons, + event->modifiers, + Qt::NoScrollPhase, + event->inverted); + wheelEvent.setAccepted(false); + return effects->checkInputWindowEvent(&wheelEvent); + } + bool keyboardKey(KeyboardKeyEvent *event) override + { + if (!effects || !effects->hasKeyboardGrab()) { + return false; + } + waylandServer()->seat()->setFocusedKeyboardSurface(nullptr); + if (!passToInputMethod(event)) { + QKeyEvent keyEvent(event->state == KeyboardKeyState::Released ? QEvent::KeyRelease : QEvent::KeyPress, + event->key, + event->modifiers, + event->nativeScanCode, + event->nativeVirtualKey, + 0, + event->text, + event->state == KeyboardKeyState::Repeated); + keyEvent.setAccepted(false); + effects->grabbedKeyboardEvent(&keyEvent); + } + return true; + } + bool touchDown(TouchDownEvent *event) override + { + if (!effects) { + return false; + } + return effects->touchDown(event->id, event->pos, event->time); + } + bool touchMotion(TouchMotionEvent *event) override + { + if (!effects) { + return false; + } + return effects->touchMotion(event->id, event->pos, event->time); + } + bool touchUp(TouchUpEvent *event) override + { + if (!effects) { + return false; + } + return effects->touchUp(event->id, event->time); + } + bool touchCancel() override + { + effects->touchCancel(); + return false; + } + bool tabletToolProximityEvent(TabletToolProximityEvent *event) override + { + if (!effects) { + return false; + } + return effects->tabletToolProximityEvent(event); + } + bool tabletToolAxisEvent(TabletToolAxisEvent *event) override + { + if (!effects) { + return false; + } + return effects->tabletToolAxisEvent(event); + } + bool tabletToolTipEvent(TabletToolTipEvent *event) override + { + if (!effects) { + return false; + } + return effects->tabletToolTipEvent(event); + } + bool tabletToolButtonEvent(TabletToolButtonEvent *event) override + { + if (!effects) { + return false; + } + return effects->tabletToolButtonEvent(event->button, event->pressed, event->tool, event->time); + } + bool tabletPadButtonEvent(TabletPadButtonEvent *event) override + { + if (!effects) { + return false; + } + return effects->tabletPadButtonEvent(event->button, event->pressed, event->time, event->device); + } + bool tabletPadStripEvent(TabletPadStripEvent *event) override + { + if (!effects) { + return false; + } + return effects->tabletPadStripEvent(event->number, event->position, event->isFinger, event->time, event->device); + } + bool tabletPadRingEvent(TabletPadRingEvent *event) override + { + if (!effects) { + return false; + } + return effects->tabletPadRingEvent(event->number, event->position, event->isFinger, event->time, event->device); + } + bool tabletPadDialEvent(TabletPadDialEvent *event) override + { + if (!effects) { + return false; + } + return effects->tabletPadDialEvent(event->number, event->delta, event->time, event->device); + } +}; + +class MoveResizeFilter : public InputEventFilter +{ +public: + MoveResizeFilter() + : InputEventFilter(InputFilterOrder::InteractiveMoveResize) + { + } + bool pointerMotion(PointerMotionEvent *event) override + { + Window *window = workspace()->moveResizeWindow(); + if (!window) { + return false; + } + window->updateInteractiveMoveResize(event->position, event->modifiers); + return true; + } + bool pointerButton(PointerButtonEvent *event) override + { + Window *window = workspace()->moveResizeWindow(); + if (!window) { + return false; + } + if (event->state == PointerButtonState::Released) { + if (event->buttons == Qt::NoButton) { + window->endInteractiveMoveResize(); + } + } + return true; + } + bool pointerAxis(PointerAxisEvent *event) override + { + // filter out while moving a window + return workspace()->moveResizeWindow() != nullptr; + } + bool keyboardKey(KeyboardKeyEvent *event) override + { + Window *window = workspace()->moveResizeWindow(); + if (!window) { + return false; + } + if (event->state == KeyboardKeyState::Repeated || event->state == KeyboardKeyState::Pressed) { + window->keyPressEvent(QKeyCombination{event->modifiers, event->key}); + if (window->isInteractiveMove() || window->isInteractiveResize()) { + // only update if mode didn't end + window->updateInteractiveMoveResize(input()->globalPointer(), input()->keyboardModifiers()); + } + } + return true; + } + + bool touchDown(TouchDownEvent *event) override + { + Window *window = workspace()->moveResizeWindow(); + if (!window) { + return false; + } + return true; + } + + bool touchMotion(TouchMotionEvent *event) override + { + Window *window = workspace()->moveResizeWindow(); + if (!window) { + return false; + } + if (!m_set) { + m_id = event->id; + m_set = true; + } + if (m_id == event->id) { + window->updateInteractiveMoveResize(event->pos, input()->keyboardModifiers()); + } + return true; + } + + bool touchUp(TouchUpEvent *event) override + { + Window *window = workspace()->moveResizeWindow(); + if (!window) { + return false; + } + if (m_id == event->id || !m_set) { + window->endInteractiveMoveResize(); + m_set = false; + // pass through to update decoration filter later on + return false; + } + m_set = false; + return true; + } + + bool tabletToolProximityEvent(TabletToolProximityEvent *event) override + { + Window *window = workspace()->moveResizeWindow(); + if (!window) { + return false; + } + + return true; + } + + bool tabletToolTipEvent(TabletToolTipEvent *event) override + { + Window *window = workspace()->moveResizeWindow(); + if (!window) { + return false; + } + if (!input()->tablet()->haveImplicitGrab()) { + window->endInteractiveMoveResize(); + } + return true; + } + + bool tabletToolAxisEvent(TabletToolAxisEvent *event) override + { + Window *window = workspace()->moveResizeWindow(); + if (!window) { + return false; + } + + window->updateInteractiveMoveResize(event->position, input()->keyboardModifiers()); + return true; + } + + bool tabletToolButtonEvent(TabletToolButtonEvent *event) override + { + Window *window = workspace()->moveResizeWindow(); + if (!window) { + return false; + } + + if (!input()->tablet()->haveImplicitGrab()) { + window->endInteractiveMoveResize(); + } + + return true; + } + +private: + qint32 m_id = 0; + bool m_set = false; +}; + +class WindowSelectorFilter : public InputEventFilter +{ +public: + WindowSelectorFilter() + : InputEventFilter(InputFilterOrder::WindowSelector) + { + } + bool pointerMotion(PointerMotionEvent *event) override + { + return m_active; + } + bool pointerButton(PointerButtonEvent *event) override + { + if (!m_active) { + return false; + } + if (event->state == PointerButtonState::Released) { + if (event->buttons == Qt::NoButton) { + if (event->button == Qt::RightButton) { + cancel(); + } else { + accept(event->position); + } + } + } + return true; + } + bool pointerAxis(PointerAxisEvent *event) override + { + // filter out while selecting a window + return m_active; + } + bool keyboardKey(KeyboardKeyEvent *event) override + { + if (!m_active) { + return false; + } + waylandServer()->seat()->setFocusedKeyboardSurface(nullptr); + + if (event->state == KeyboardKeyState::Repeated || event->state == KeyboardKeyState::Pressed) { + // x11 variant does this on key press, so do the same + if (event->key == Qt::Key_Escape) { + cancel(); + } else if (event->key == Qt::Key_Enter || event->key == Qt::Key_Return || event->key == Qt::Key_Space) { + accept(input()->globalPointer()); + } + if (input()->supportsPointerWarping()) { + int mx = 0; + int my = 0; + if (event->key == Qt::Key_Left) { + mx = -10; + } + if (event->key == Qt::Key_Right) { + mx = 10; + } + if (event->key == Qt::Key_Up) { + my = -10; + } + if (event->key == Qt::Key_Down) { + my = 10; + } + if (event->modifiers & Qt::ControlModifier) { + mx /= 10; + my /= 10; + } + input()->warpPointer(input()->globalPointer() + QPointF(mx, my)); + } + } + // filter out while selecting a window + return true; + } + + bool touchDown(TouchDownEvent *event) override + { + if (!isActive()) { + return false; + } + m_touchPoints.insert(event->id, event->pos); + return true; + } + + bool touchMotion(TouchMotionEvent *event) override + { + if (!isActive()) { + return false; + } + auto it = m_touchPoints.find(event->id); + if (it != m_touchPoints.end()) { + *it = event->pos; + } + return true; + } + + bool touchUp(TouchUpEvent *event) override + { + if (!isActive()) { + return false; + } + auto it = m_touchPoints.find(event->id); + if (it != m_touchPoints.end()) { + const auto pos = it.value(); + m_touchPoints.erase(it); + if (m_touchPoints.isEmpty()) { + accept(pos); + } + } + return true; + } + + bool tabletToolTipEvent(TabletToolTipEvent *event) override + { + if (!isActive()) { + return false; + } + + if (event->type == TabletToolTipEvent::Release) { + accept(event->position); + } + + return true; + } + + bool isActive() const + { + return m_active; + } + void start(std::function callback) + { + Q_ASSERT(!m_active); + m_active = true; + m_callback = callback; + input()->keyboard()->update(); + input()->touch()->cancel(); + } + void start(std::function callback) + { + Q_ASSERT(!m_active); + m_active = true; + m_pointSelectionFallback = callback; + input()->keyboard()->update(); + input()->touch()->cancel(); + } + +private: + void deactivate() + { + m_active = false; + m_callback = std::function(); + m_pointSelectionFallback = std::function(); + input()->pointer()->removeWindowSelectionCursor(); + input()->keyboard()->update(); + m_touchPoints.clear(); + } + void cancel() + { + if (m_callback) { + m_callback(nullptr); + } + if (m_pointSelectionFallback) { + m_pointSelectionFallback(QPoint(-1, -1)); + } + deactivate(); + } + void accept(const QPointF &pos) + { + if (m_callback) { + // TODO: this ignores shaped windows + m_callback(input()->findToplevel(pos)); + } + if (m_pointSelectionFallback) { + m_pointSelectionFallback(pos.toPoint()); + } + deactivate(); + } + + bool m_active = false; + std::function m_callback; + std::function m_pointSelectionFallback; + QMap m_touchPoints; +}; + +class MouseWheelAccumulator +{ +public: + qreal accumulate(PointerAxisEvent *event) + { + const qreal delta = event->deltaV120 != 0 ? event->deltaV120 / 120.0 : event->delta / 15.0; + if (std::signbit(m_scrollDistance) != std::signbit(delta)) { + m_scrollDistance = 0; + } + m_scrollDistance += delta; + if (std::abs(m_scrollDistance) >= 1.0) { + const qreal ret = m_scrollDistance; + m_scrollDistance = std::fmod(m_scrollDistance, 1.0f); + return ret - m_scrollDistance; + } else { + return 0; + } + } + +private: + qreal m_scrollDistance = 0; +}; + +#if KWIN_BUILD_GLOBALSHORTCUTS +class GlobalShortcutFilter : public InputEventFilter +{ +public: + GlobalShortcutFilter() + : InputEventFilter(InputFilterOrder::GlobalShortcut) + { + m_powerDown.setSingleShot(true); + m_powerDown.setInterval(1000); + } + + bool pointerButton(PointerButtonEvent *event) override + { + if (event->state == PointerButtonState::Pressed) { + if (input()->shortcuts()->processPointerPressed(event->modifiers, event->buttons)) { + return true; + } + } + return false; + } + bool pointerAxis(PointerAxisEvent *event) override + { + if (event->modifiers == Qt::NoModifier) { + return false; + } + PointerAxisDirection direction = PointerAxisUp; + if (event->orientation == Qt::Horizontal) { + if (event->delta > 0) { + direction = PointerAxisRight; + } else if (event->delta < 0) { + direction = PointerAxisLeft; + } + return input()->shortcuts()->processAxis(event->modifiers, direction, m_horizontalAccumulator.accumulate(event)); + } else { + if (event->delta > 0) { + direction = PointerAxisDown; + } else if (event->delta < 0) { + direction = PointerAxisUp; + } + return input()->shortcuts()->processAxis(event->modifiers, direction, m_verticalAccumulator.accumulate(event)); + } + } + bool keyboardKey(KeyboardKeyEvent *event) override + { + if (event->key == Qt::Key_PowerOff) { + const auto modifiers = event->modifiersRelevantForGlobalShortcuts; + if (event->state == KeyboardKeyState::Pressed) { + auto passToShortcuts = [modifiers] { + input()->shortcuts()->processKey(modifiers, Qt::Key_PowerDown, KeyboardKeyState::Pressed); + }; + QObject::connect(&m_powerDown, &QTimer::timeout, input()->shortcuts(), passToShortcuts, Qt::SingleShotConnection); + m_powerDown.start(); + return true; + } else if (event->state == KeyboardKeyState::Released) { + if (m_powerDown.isActive()) { + bool ret = input()->shortcuts()->processKey(modifiers, event->key, KeyboardKeyState::Pressed); + ret |= input()->shortcuts()->processKey(modifiers, event->key, KeyboardKeyState::Released); + m_powerDown.stop(); + return ret; + } else { + return input()->shortcuts()->processKey(modifiers, event->key, event->state); + } + } + } else if (event->state == KeyboardKeyState::Repeated || event->state == KeyboardKeyState::Pressed) { + if (!waylandServer()->isKeyboardShortcutsInhibited()) { + if (input()->shortcuts()->processKey(event->modifiersRelevantForGlobalShortcuts, event->key, event->state)) { + input()->keyboard()->addFilteredKey(event->nativeScanCode); + return true; + } + } + } else if (event->state == KeyboardKeyState::Released) { + if (!waylandServer()->isKeyboardShortcutsInhibited()) { + return input()->shortcuts()->processKey(event->modifiersRelevantForGlobalShortcuts, event->key, event->state); + } + } + return false; + } + bool swipeGestureBegin(PointerSwipeGestureBeginEvent *event) override + { + m_touchpadGestureFingerCount = event->fingerCount; + if (m_touchpadGestureFingerCount >= 3) { + input()->shortcuts()->processSwipeStart(DeviceType::Touchpad, event->fingerCount); + return true; + } else { + return false; + } + } + bool swipeGestureUpdate(PointerSwipeGestureUpdateEvent *event) override + { + if (m_touchpadGestureFingerCount >= 3) { + input()->shortcuts()->processSwipeUpdate(DeviceType::Touchpad, event->delta); + return true; + } else { + return false; + } + } + bool swipeGestureCancelled(PointerSwipeGestureCancelEvent *event) override + { + if (m_touchpadGestureFingerCount >= 3) { + input()->shortcuts()->processSwipeCancel(DeviceType::Touchpad); + return true; + } else { + return false; + } + } + bool swipeGestureEnd(PointerSwipeGestureEndEvent *event) override + { + if (m_touchpadGestureFingerCount >= 3) { + input()->shortcuts()->processSwipeEnd(DeviceType::Touchpad); + return true; + } else { + return false; + } + } + bool pinchGestureBegin(PointerPinchGestureBeginEvent *event) override + { + m_touchpadGestureFingerCount = event->fingerCount; + if (m_touchpadGestureFingerCount >= 3) { + input()->shortcuts()->processPinchStart(event->fingerCount); + return true; + } else { + return false; + } + } + bool pinchGestureUpdate(PointerPinchGestureUpdateEvent *event) override + { + if (m_touchpadGestureFingerCount >= 3) { + input()->shortcuts()->processPinchUpdate(event->scale, event->angleDelta, event->delta); + return true; + } else { + return false; + } + } + bool pinchGestureEnd(PointerPinchGestureEndEvent *event) override + { + if (m_touchpadGestureFingerCount >= 3) { + input()->shortcuts()->processPinchEnd(); + return true; + } else { + return false; + } + } + bool pinchGestureCancelled(PointerPinchGestureCancelEvent *event) override + { + if (m_touchpadGestureFingerCount >= 3) { + input()->shortcuts()->processPinchCancel(); + return true; + } else { + return false; + } + } + bool touchDown(TouchDownEvent *event) override + { + if (m_gestureTaken) { + input()->shortcuts()->processSwipeCancel(DeviceType::Touchscreen); + m_gestureCancelled = true; + return true; + } else { + if (m_touchPoints.isEmpty()) { + m_lastTouchDownTime = event->time; + } else { + if (event->time - m_lastTouchDownTime > 250ms) { + m_gestureCancelled = true; + return false; + } + m_lastTouchDownTime = event->time; + auto output = workspace()->outputAt(event->pos); + auto physicalSize = output->orientateSize(output->physicalSize()); + if (!physicalSize.isValid()) { + physicalSize = QSize(190, 100); + } + float xfactor = physicalSize.width() / (float)output->geometry().width(); + float yfactor = physicalSize.height() / (float)output->geometry().height(); + bool distanceMatch = std::any_of(m_touchPoints.constBegin(), m_touchPoints.constEnd(), [event, xfactor, yfactor](const auto &point) { + QPointF p = event->pos - point; + return std::abs(xfactor * p.x()) + std::abs(yfactor * p.y()) < 50; + }); + if (!distanceMatch) { + m_gestureCancelled = true; + return false; + } + } + m_touchPoints.insert(event->id, event->pos); + if (m_touchPoints.count() >= 3 && !m_gestureCancelled) { + m_gestureTaken = true; + m_syntheticCancel = true; + input()->processFilters(&InputEventFilter::touchCancel); + m_syntheticCancel = false; + input()->shortcuts()->processSwipeStart(DeviceType::Touchscreen, m_touchPoints.count()); + return true; + } + } + return false; + } + + bool touchMotion(TouchMotionEvent *event) override + { + if (m_gestureTaken) { + if (m_gestureCancelled) { + return true; + } + auto output = workspace()->outputAt(event->pos); + const auto physicalSize = output->orientateSize(output->physicalSize()); + const float xfactor = physicalSize.width() / (float)output->geometry().width(); + const float yfactor = physicalSize.height() / (float)output->geometry().height(); + + auto &point = m_touchPoints[event->id]; + const QPointF dist = event->pos - point; + const QPointF delta = QPointF(xfactor * dist.x(), yfactor * dist.y()); + input()->shortcuts()->processSwipeUpdate(DeviceType::Touchscreen, 5 * delta / m_touchPoints.size()); + point = event->pos; + return true; + } + return false; + } + + bool touchUp(TouchUpEvent *event) override + { + m_touchPoints.remove(event->id); + if (m_gestureTaken) { + if (!m_gestureCancelled) { + input()->shortcuts()->processSwipeEnd(DeviceType::Touchscreen); + m_gestureCancelled = true; + } + m_gestureTaken &= m_touchPoints.count() > 0; + m_gestureCancelled &= m_gestureTaken; + return true; + } else { + m_gestureCancelled &= m_touchPoints.count() > 0; + return false; + } + } + + bool touchCancel() override + { + if (m_syntheticCancel) { + return false; + } + const bool oldGestureTaken = m_gestureTaken; + m_gestureTaken = false; + m_gestureCancelled = false; + m_touchPoints.clear(); + return oldGestureTaken; + } + + bool touchFrame() override + { + return m_gestureTaken; + } + + bool tabletToolTipEvent(TabletToolTipEvent *event) override + { + if (event->type == TabletToolTipEvent::Type::Press) { + input()->shortcuts()->cancelModiferOnlySequence(); + } + return false; + } + + bool tabletToolButtonEvent(TabletToolButtonEvent *event) override + { + if (event->pressed) { + input()->shortcuts()->cancelModiferOnlySequence(); + } + return false; + } + +private: + bool m_gestureTaken = false; + bool m_gestureCancelled = false; + bool m_syntheticCancel = false; + std::chrono::microseconds m_lastTouchDownTime = std::chrono::microseconds::zero(); + QPointF m_lastAverageDistance; + QMap m_touchPoints; + int m_touchpadGestureFingerCount = 0; + MouseWheelAccumulator m_horizontalAccumulator; + MouseWheelAccumulator m_verticalAccumulator; + + QTimer m_powerDown; +}; +#endif + +namespace +{ + +static std::optional globalWindowAction(Qt::MouseButton button, Qt::KeyboardModifiers modifiers) +{ + if (modifiers != options->commandAllModifier()) { + return std::nullopt; + } + if (workspace()->globalShortcutsDisabled()) { + return std::nullopt; + } + switch (button) { + case Qt::LeftButton: + return options->commandAll1(); + case Qt::MiddleButton: + return options->commandAll2(); + case Qt::RightButton: + return options->commandAll3(); + default: + return std::nullopt; + } +} + +static std::optional windowActionForPointerButtonPress(PointerButtonEvent *event, Window *window) +{ + if (!input()->pointer()->isConstrained()) { + if (const auto command = globalWindowAction(event->button, event->modifiersRelevantForShortcuts)) { + return command; + } + } + + return window->getMousePressCommand(event->button); +} + +static std::optional windowActionForTouchDown(Window *window) +{ + if (const auto command = globalWindowAction(Qt::LeftButton, input()->modifiersRelevantForGlobalShortcuts())) { + return command; + } else { + return window->getMousePressCommand(Qt::LeftButton); + } +} + +static std::optional windowActionForTabletTipDown(Window *window) +{ + if (const auto command = globalWindowAction(Qt::LeftButton, input()->modifiersRelevantForGlobalShortcuts())) { + return command; + } else { + return window->getMousePressCommand(Qt::LeftButton); + } +} + +static std::optional windowActionForTabletButtonPress(TabletToolButtonEvent *event, Window *window) +{ + const auto button = event->button == BTN_STYLUS ? Qt::MiddleButton : Qt::RightButton; + if (const auto command = globalWindowAction(button, input()->modifiersRelevantForGlobalShortcuts())) { + return command; + } else { + return window->getMousePressCommand(button); + } +} + +std::optional globalWindowWheelAction(PointerAxisEvent *event) +{ + if (event->orientation != Qt::Vertical) { + return std::nullopt; + } + if (event->modifiersRelevantForGlobalShortcuts != options->commandAllModifier()) { + return std::nullopt; + } + if (input()->pointer()->isConstrained() || workspace()->globalShortcutsDisabled()) { + return std::nullopt; + } + const auto ret = options->operationWindowMouseWheel(-event->delta); + if (ret == Options::MouseCommand::MouseNothing) { + return std::nullopt; + } else { + return ret; + } +} + +std::optional windowWheelCommand(PointerAxisEvent *event, Window *window) +{ + if (const auto globalCommand = globalWindowWheelAction(event)) { + return globalCommand; + } else if (const auto command = window->getWheelCommand(event->orientation)) { + return command; + } else { + return std::nullopt; + } +} +} + +class InternalWindowEventFilter : public InputEventFilter +{ +public: + InternalWindowEventFilter() + : InputEventFilter(InputFilterOrder::InternalWindow) + { + m_touchDevice = std::make_unique(QLatin1String("some touchscreen"), 0, QInputDevice::DeviceType::TouchScreen, + QPointingDevice::PointerType::Finger, QInputDevice::Capability::Position, + 10, 0, kwinApp()->session()->seat(), QPointingDeviceUniqueId()); + QWindowSystemInterface::registerInputDevice(m_touchDevice.get()); + + m_tabletDevice = std::make_unique(QLatin1String("some tablet"), 0, QInputDevice::DeviceType::Stylus, + QPointingDevice::PointerType::Pen, QInputDevice::Capability::Position | QInputDevice::Capability::ZPosition | QInputDevice::Capability::Pressure, + 10, 0, kwinApp()->session()->seat(), QPointingDeviceUniqueId()); + QWindowSystemInterface::registerInputDevice(m_tabletDevice.get()); + } + bool pointerMotion(PointerMotionEvent *event) override + { + if (!input()->pointer()->focus() || !input()->pointer()->focus()->isInternal()) { + return false; + } + QWindow *internal = static_cast(input()->pointer()->focus())->handle(); + if (!internal) { + // the handle can be nullptr if the tooltip gets closed while focus updates are blocked + return false; + } + QWindowSystemInterface::handleMouseEvent(internal, + event->position - internal->position(), + event->position, + event->buttons, + Qt::NoButton, + QEvent::MouseMove, + event->modifiers); + return true; + } + bool pointerButton(PointerButtonEvent *event) override + { + if (!input()->pointer()->focus() || !input()->pointer()->focus()->isInternal()) { + return false; + } + QWindow *internal = static_cast(input()->pointer()->focus())->handle(); + if (!internal) { + // the handle can be nullptr if the tooltip gets closed while focus updates are blocked + return false; + } + QWindowSystemInterface::handleMouseEvent(internal, + event->position - internal->position(), + event->position, + event->buttons, + event->button, + event->state == PointerButtonState::Pressed ? QEvent::MouseButtonPress : QEvent::MouseButtonRelease, + event->modifiers); + return true; + } + bool pointerAxis(PointerAxisEvent *event) override + { + if (!input()->pointer()->focus() || !input()->pointer()->focus()->isInternal()) { + return false; + } + QWindow *internal = static_cast(input()->pointer()->focus())->handle(); + if (!internal) { + // the handle can be nullptr if the tooltip gets closed while focus updates are blocked + return false; + } + const auto timestamp = std::chrono::duration_cast(event->timestamp); + QWindowSystemInterface::handleWheelEvent(internal, + timestamp.count(), + event->position - internal->position(), + event->position, + QPoint(), + ((event->orientation == Qt::Horizontal) ? QPoint(event->delta, 0) : QPoint(0, event->delta)) * -1, + event->modifiers, + Qt::NoScrollPhase, + Qt::MouseEventNotSynthesized, + event->inverted); + return true; + } + + bool touchDown(TouchDownEvent *event) override + { + auto seat = waylandServer()->seat(); + if (seat->isTouchSequence()) { + // something else is getting the events + return false; + } + if (!input()->touch()->focus() || !input()->touch()->focus()->isInternal()) { + return false; + } + + const qreal contactAreaWidth = 8; + const qreal contactAreaHeight = 8; + + auto &touchPoint = m_touchPoints.emplaceBack(QWindowSystemInterface::TouchPoint{}); + touchPoint.id = event->id; + touchPoint.area = QRectF(event->pos.x() - contactAreaWidth / 2, event->pos.y() - contactAreaHeight / 2, contactAreaWidth, contactAreaHeight); + touchPoint.state = QEventPoint::State::Pressed; + touchPoint.pressure = 1; + + QWindow *internal = static_cast(input()->touch()->focus())->handle(); + QWindowSystemInterface::handleTouchEvent(internal, m_touchDevice.get(), m_touchPoints, input()->keyboardModifiers()); + + touchPoint.state = QEventPoint::State::Stationary; + return true; + } + + bool touchMotion(TouchMotionEvent *event) override + { + auto it = std::ranges::find_if(m_touchPoints, [event](const auto &touchPoint) { + return touchPoint.id == event->id; + }); + if (it == m_touchPoints.end()) { + return false; + } + + it->area.moveCenter(event->pos); + it->state = QEventPoint::State::Updated; + + if (auto internalWindow = qobject_cast(input()->touch()->focus())) { + QWindowSystemInterface::handleTouchEvent(internalWindow->handle(), m_touchDevice.get(), m_touchPoints, input()->keyboardModifiers()); + } + + it->state = QEventPoint::State::Stationary; + return true; + } + bool touchUp(TouchUpEvent *event) override + { + auto it = std::ranges::find_if(m_touchPoints, [event](const auto &touchPoint) { + return touchPoint.id == event->id; + }); + if (it == m_touchPoints.end()) { + return false; + } + + it->pressure = 0; + it->state = QEventPoint::State::Released; + + if (auto internalWindow = qobject_cast(input()->touch()->focus())) { + QWindowSystemInterface::handleTouchEvent(internalWindow->handle(), m_touchDevice.get(), m_touchPoints, input()->keyboardModifiers()); + } + + m_touchPoints.erase(it); + return true; + } + bool touchCancel() override + { + if (!m_touchPoints.isEmpty()) { + m_touchPoints.clear(); + QWindowSystemInterface::handleTouchCancelEvent(nullptr, m_touchDevice.get()); + } + return false; + } + + bool tabletToolProximityEvent(TabletToolProximityEvent *event) override + { + if (!input()->tablet()->focus() || !input()->tablet()->focus()->isInternal()) { + return false; + } + + // handleTabletEnterLeaveProximityEvent has lots of parameters, most of which are ignored, so don't bother with them + QWindowSystemInterface::handleTabletEnterLeaveProximityEvent(nullptr, + m_tabletDevice.get(), event->type == TabletToolProximityEvent::EnterProximity); + + return true; + } + + bool tabletToolAxisEvent(TabletToolAxisEvent *event) override + { + if (!input()->tablet()->focus() || !input()->tablet()->focus()->isInternal()) { + return false; + } + + QWindow *internal = static_cast(input()->tablet()->focus())->handle(); + if (!internal) { + return true; + } + + const QPointF globalPos = event->position; + const QPointF localPos = globalPos - internal->position(); + + QWindowSystemInterface::handleTabletEvent(internal, std::chrono::duration_cast(event->timestamp).count(), m_tabletDevice.get(), localPos, globalPos, event->buttons, event->pressure, event->xTilt, event->yTilt, event->sliderPosition, event->rotation, event->distance, input()->keyboardModifiers()); + + return true; + } + + bool tabletToolTipEvent(TabletToolTipEvent *event) override + { + if (!input()->tablet()->focus() || !input()->tablet()->focus()->isInternal()) { + return false; + } + + QWindow *internal = static_cast(input()->tablet()->focus())->handle(); + if (!internal) { + return true; + } + + const QPointF globalPos = event->position; + const QPointF localPos = globalPos - internal->position(); + const Qt::MouseButtons buttons = event->type == TabletToolTipEvent::Press ? Qt::LeftButton : Qt::NoButton; + + QWindowSystemInterface::handleTabletEvent(internal, std::chrono::duration_cast(event->timestamp).count(), m_tabletDevice.get(), localPos, globalPos, buttons, event->pressure, event->xTilt, event->yTilt, event->sliderPosition, event->rotation, event->distance, input()->keyboardModifiers()); + + return true; + } + +private: + std::unique_ptr m_touchDevice; + std::unique_ptr m_tabletDevice; + QList m_touchPoints; +}; + +class DecorationEventFilter : public InputEventFilter +{ +public: + DecorationEventFilter() + : InputEventFilter(InputFilterOrder::Decoration) + { + } + bool pointerMotion(PointerMotionEvent *event) override + { + auto decoration = input()->pointer()->decoration(); + if (!decoration) { + return false; + } + const QPointF p = event->position - decoration->window()->pos(); + QHoverEvent e(QEvent::HoverMove, p, p); + QCoreApplication::instance()->sendEvent(decoration->decoration(), &e); + decoration->window()->processDecorationMove(p, event->position); + return true; + } + bool pointerButton(PointerButtonEvent *event) override + { + auto decoration = input()->pointer()->decoration(); + if (!decoration) { + return false; + } + + Window *window = decoration->window(); + const QPointF globalPos = event->position; + const QPointF p = event->position - window->pos(); + + if (event->state == PointerButtonState::Pressed) { + if (const auto command = windowActionForPointerButtonPress(event, window)) { + if (window->performMousePressCommand(*command, event->position)) { + return true; + } + } + } + + QMouseEvent e(event->state == PointerButtonState::Pressed ? QEvent::MouseButtonPress : QEvent::MouseButtonRelease, p, event->position, event->button, event->buttons, event->modifiers); + e.setTimestamp(std::chrono::duration_cast(event->timestamp).count()); + e.setAccepted(false); + QCoreApplication::sendEvent(decoration->decoration(), &e); + if (e.isAccepted()) { + // if a non-active window is closed through the decoration, it should be allowed to activate itself + // TODO use the event serial instead, once that's plumbed through + const uint32_t serial = waylandServer()->display()->nextSerial(); + const QString token = waylandServer()->xdgActivationIntegration()->requestPrivilegedToken(nullptr, serial, waylandServer()->seat(), window->desktopFileName()); + workspace()->setActivationToken(token, serial, window->desktopFileName()); + } + if (!e.isAccepted() && event->state == PointerButtonState::Pressed) { + window->processDecorationButtonPress(p, globalPos, event->button); + } + if (event->state == PointerButtonState::Released) { + window->processDecorationButtonRelease(event->button); + } + return true; + } + bool pointerAxis(PointerAxisEvent *event) override + { + auto decoration = input()->pointer()->decoration(); + if (!decoration) { + return false; + } + if (const auto command = globalWindowWheelAction(event)) { + if (m_accumulator.accumulate(event)) { + if (decoration->window()->performMousePressCommand(*command, event->position)) { + return true; + } + } else if (decoration->window()->mousePressCommandConsumesEvent(*command)) { + return true; + } + } + const QPointF localPos = event->position - decoration->window()->pos(); + QWheelEvent e(localPos, event->position, QPoint(), + (event->orientation == Qt::Horizontal) ? QPoint(event->delta, 0) : QPoint(0, event->delta), + event->buttons, + event->modifiers, + Qt::NoScrollPhase, + false); + e.setAccepted(false); + QCoreApplication::sendEvent(decoration, &e); + if (e.isAccepted()) { + return true; + } + if ((event->orientation == Qt::Vertical) && decoration->window()->titlebarPositionUnderMouse()) { + if (float delta = m_accumulator.accumulate(event)) { + decoration->window()->performMousePressCommand(options->operationTitlebarMouseWheel(delta * -1), + event->position); + } + } + return true; + } + bool touchDown(TouchDownEvent *event) override + { + auto seat = waylandServer()->seat(); + if (seat->isTouchSequence()) { + return false; + } + if (input()->touch()->decorationPressId() != -1) { + // already on a decoration, ignore further touch points, but filter out + return true; + } + auto decoration = input()->touch()->decoration(); + if (!decoration) { + return false; + } + + input()->touch()->setDecorationPressId(event->id); + m_lastGlobalTouchPos = event->pos; + m_lastLocalTouchPos = event->pos - decoration->window()->pos(); + + QHoverEvent hoverEvent(QEvent::HoverMove, m_lastLocalTouchPos, m_lastLocalTouchPos); + QCoreApplication::sendEvent(decoration->decoration(), &hoverEvent); + + QMouseEvent e(QEvent::MouseButtonPress, m_lastLocalTouchPos, event->pos, Qt::LeftButton, Qt::LeftButton, input()->keyboardModifiers()); + e.setAccepted(false); + QCoreApplication::sendEvent(decoration->decoration(), &e); + if (!e.isAccepted()) { + decoration->window()->processDecorationButtonPress(m_lastLocalTouchPos, m_lastGlobalTouchPos, Qt::LeftButton); + } + return true; + } + bool touchMotion(TouchMotionEvent *event) override + { + auto decoration = input()->touch()->decoration(); + if (!decoration) { + return false; + } + if (input()->touch()->decorationPressId() == -1) { + return false; + } + if (input()->touch()->decorationPressId() != qint32(event->id)) { + // ignore, but filter out + return true; + } + m_lastGlobalTouchPos = event->pos; + m_lastLocalTouchPos = event->pos - decoration->window()->pos(); + + QHoverEvent e(QEvent::HoverMove, m_lastLocalTouchPos, m_lastLocalTouchPos); + QCoreApplication::instance()->sendEvent(decoration->decoration(), &e); + decoration->window()->processDecorationMove(m_lastLocalTouchPos, event->pos); + return true; + } + bool touchUp(TouchUpEvent *event) override + { + auto decoration = input()->touch()->decoration(); + if (!decoration) { + // can happen when quick tiling + if (input()->touch()->decorationPressId() == event->id) { + m_lastGlobalTouchPos = QPointF(); + m_lastLocalTouchPos = QPointF(); + input()->touch()->setDecorationPressId(-1); + return true; + } + return false; + } + if (input()->touch()->decorationPressId() == -1) { + return false; + } + if (input()->touch()->decorationPressId() != qint32(event->id)) { + // ignore, but filter out + return true; + } + + // send mouse up + QMouseEvent e(QEvent::MouseButtonRelease, m_lastLocalTouchPos, m_lastGlobalTouchPos, Qt::LeftButton, Qt::MouseButtons(), input()->keyboardModifiers()); + e.setAccepted(false); + QCoreApplication::sendEvent(decoration->decoration(), &e); + decoration->window()->processDecorationButtonRelease(Qt::LeftButton); + + QHoverEvent leaveEvent(QEvent::HoverLeave, QPointF(), QPointF()); + QCoreApplication::sendEvent(decoration->decoration(), &leaveEvent); + + m_lastGlobalTouchPos = QPointF(); + m_lastLocalTouchPos = QPointF(); + input()->touch()->setDecorationPressId(-1); + return true; + } + + bool tabletToolProximityEvent(TabletToolProximityEvent *event) override + { + auto decoration = input()->tablet()->decoration(); + if (!decoration) { + return false; + } + if (event->type == TabletToolProximityEvent::EnterProximity) { + const QPointF p = event->position - decoration->window()->pos(); + QHoverEvent e(QEvent::HoverMove, p, p); + QCoreApplication::instance()->sendEvent(decoration->decoration(), &e); + decoration->window()->processDecorationMove(p, event->position); + } else { + QHoverEvent leaveEvent(QEvent::HoverLeave, QPointF(), QPointF()); + QCoreApplication::sendEvent(decoration->decoration(), &leaveEvent); + } + + return true; + } + + bool tabletToolAxisEvent(TabletToolAxisEvent *event) override + { + auto decoration = input()->tablet()->decoration(); + if (!decoration) { + return false; + } + const QPointF p = event->position - decoration->window()->pos(); + + QHoverEvent e(QEvent::HoverMove, p, p); + QCoreApplication::instance()->sendEvent(decoration->decoration(), &e); + decoration->window()->processDecorationMove(p, event->position); + + return true; + } + + bool tabletToolTipEvent(TabletToolTipEvent *event) override + { + auto decoration = input()->tablet()->decoration(); + if (!decoration) { + return false; + } + const QPointF globalPos = event->position; + const QPointF p = event->position - decoration->window()->pos(); + + const bool isPressed = event->type == TabletToolTipEvent::Press; + QMouseEvent e(isPressed ? QEvent::MouseButtonPress : QEvent::MouseButtonRelease, + p, + event->position, + Qt::LeftButton, + isPressed ? Qt::LeftButton : Qt::MouseButtons(), + input()->keyboardModifiers()); + e.setAccepted(false); + QCoreApplication::sendEvent(decoration->decoration(), &e); + if (!e.isAccepted() && isPressed) { + decoration->window()->processDecorationButtonPress(p, globalPos, Qt::LeftButton); + } + if (event->type == TabletToolTipEvent::Release) { + decoration->window()->processDecorationButtonRelease(Qt::LeftButton); + } + + return true; + } + +private: + QPointF m_lastGlobalTouchPos; + QPointF m_lastLocalTouchPos; + MouseWheelAccumulator m_accumulator; +}; + +#if KWIN_BUILD_TABBOX +class TabBoxInputFilter : public InputEventFilter +{ +public: + TabBoxInputFilter() + : InputEventFilter(InputFilterOrder::TabBox) + { + } + bool pointerMotion(PointerMotionEvent *event) override + { + if (!workspace()->tabbox() || !workspace()->tabbox()->isGrabbed()) { + return false; + } + QMouseEvent mouseEvent(QEvent::MouseMove, + event->position, + event->position, + Qt::NoButton, + event->buttons, + event->modifiers); + mouseEvent.setTimestamp(std::chrono::duration_cast(event->timestamp).count()); + mouseEvent.setAccepted(false); + return workspace()->tabbox()->handleMouseEvent(&mouseEvent); + } + bool pointerButton(PointerButtonEvent *event) override + { + if (!workspace()->tabbox() || !workspace()->tabbox()->isGrabbed()) { + return false; + } + QMouseEvent mouseEvent(event->state == PointerButtonState::Pressed ? QEvent::MouseButtonPress : QEvent::MouseButtonRelease, + event->position, + event->position, + event->button, + event->buttons, + event->modifiers); + mouseEvent.setTimestamp(std::chrono::duration_cast(event->timestamp).count()); + mouseEvent.setAccepted(false); + return workspace()->tabbox()->handleMouseEvent(&mouseEvent); + } + bool keyboardKey(KeyboardKeyEvent *event) override + { + if (!workspace()->tabbox() || !workspace()->tabbox()->isGrabbed()) { + return false; + } + + if (event->state == KeyboardKeyState::Repeated || event->state == KeyboardKeyState::Pressed) { + workspace()->tabbox()->keyPress(*event); + } else if (event->modifiersRelevantForGlobalShortcuts == Qt::NoModifier) { + workspace()->tabbox()->modifiersReleased(); + return false; + } + return true; + } + bool pointerAxis(PointerAxisEvent *event) override + { + if (!workspace()->tabbox() || !workspace()->tabbox()->isGrabbed()) { + return false; + } + QWheelEvent wheelEvent(event->position, + event->position, + QPoint(), + (event->orientation == Qt::Horizontal) ? QPoint(event->delta, 0) : QPoint(0, event->delta), + event->buttons, + event->modifiers, + Qt::NoScrollPhase, + event->inverted); + wheelEvent.setAccepted(false); + return workspace()->tabbox()->handleWheelEvent(&wheelEvent); + } +}; +#endif + +class ScreenEdgeInputFilter : public InputEventFilter +{ +public: + ScreenEdgeInputFilter() + : InputEventFilter(InputFilterOrder::ScreenEdge) + { + } + bool pointerMotion(PointerMotionEvent *event) override + { + workspace()->screenEdges()->handlePointerMotion(event->position, event->timestamp); + // always forward + return false; + } + bool touchDown(TouchDownEvent *event) override + { + return workspace()->screenEdges()->gestureRecognizer()->touchDown(event->id, event->pos); + } + bool touchMotion(TouchMotionEvent *event) override + { + return workspace()->screenEdges()->gestureRecognizer()->touchMotion(event->id, event->pos); + } + bool touchUp(TouchUpEvent *event) override + { + return workspace()->screenEdges()->gestureRecognizer()->touchUp(event->id); + } + bool touchCancel() override + { + workspace()->screenEdges()->gestureRecognizer()->touchCancel(); + return false; + } +}; + +/** + * This filter implements window actions. If the event should not be passed to the + * current window it will filter out the event + */ +class WindowActionInputFilter : public InputEventFilter +{ +public: + WindowActionInputFilter() + : InputEventFilter(InputFilterOrder::WindowAction) + { + } + bool pointerButton(PointerButtonEvent *event) override + { + if (event->state == PointerButtonState::Pressed) { + Window *window = input()->pointer()->focus(); + if (!window || !window->isClient()) { + return false; + } + if (const auto command = windowActionForPointerButtonPress(event, window)) { + return window->performMousePressCommand(*command, event->position); + } + return false; + } else { + // because of implicit pointer grab while a button is pressed, this may need to + // target a different window than the one with pointer focus + Window *window = input()->pointer()->hover(); + if (!window || !window->isClient()) { + return false; + } + if (const auto command = window->getMouseReleaseCommand(event->button)) { + window->performMouseReleaseCommand(*command, event->position); + } + } + return false; + } + bool pointerAxis(PointerAxisEvent *event) override + { + if (event->orientation != Qt::Vertical) { + // only actions on vertical scroll + return false; + } + Window *window = input()->pointer()->focus(); + if (!window || !window->isClient()) { + return false; + } + const auto command = windowWheelCommand(event, window); + if (!command) { + return false; + } + if (!m_accumulator.accumulate(event)) { + return window->mousePressCommandConsumesEvent(*command); + } + return window->performMousePressCommand(*command, event->position); + } + bool touchDown(TouchDownEvent *event) override + { + auto seat = waylandServer()->seat(); + if (seat->isTouchSequence()) { + return false; + } + Window *window = input()->touch()->focus(); + if (!window || !window->isClient()) { + return false; + } + if (const auto command = windowActionForTouchDown(window)) { + return window->performMousePressCommand(*command, event->pos); + } + return false; + } + bool touchUp(TouchUpEvent *event) override + { + Window *window = input()->touch()->focus(); + if (!window || !window->isClient()) { + return false; + } + const auto command = window->getMouseReleaseCommand(Qt::LeftButton); + if (command) { + window->performMouseReleaseCommand(*command, input()->touch()->position()); + } + return false; + } + bool tabletToolTipEvent(TabletToolTipEvent *event) override + { + Window *window = input()->tablet()->focus(); + if (!window || !window->isClient()) { + return false; + } + + if (event->type == TabletToolTipEvent::Press) { + if (const auto command = windowActionForTabletTipDown(window)) { + return window->performMousePressCommand(*command, event->position); + } + } else { + const auto command = window->getMouseReleaseCommand(Qt::LeftButton); + if (command) { + window->performMouseReleaseCommand(*command, event->position); + } + } + + return false; + } + bool tabletToolButtonEvent(TabletToolButtonEvent *event) override + { + Window *window = input()->tablet()->focus(); + if (!window || !window->isClient()) { + return false; + } + + if (event->pressed) { + if (const auto command = windowActionForTabletButtonPress(event, window)) { + return window->performMousePressCommand(*command, input()->tablet()->position()); + } + } else { + const auto command = window->getMouseReleaseCommand(event->button == BTN_STYLUS ? Qt::MiddleButton : Qt::RightButton); + if (command) { + window->performMouseReleaseCommand(*command, input()->tablet()->position()); + } + } + + return false; + } + +private: + MouseWheelAccumulator m_accumulator; +}; + +class InputMethodEventFilter : public InputEventFilter +{ +public: + InputMethodEventFilter() + : InputEventFilter(InputFilterOrder::InputMethod) + { + } + + bool pointerButton(PointerButtonEvent *event) override + { + auto inputMethod = kwinApp()->inputMethod(); + if (!inputMethod) { + return false; + } + if (event->state != PointerButtonState::Pressed) { + return false; + } + + // clicking on an on screen keyboard shouldn't flush, check we're clicking on our target window + if (input()->pointer()->focus() != inputMethod->activeWindow()) { + return false; + } + + inputMethod->commitPendingText(); + return false; + } + + bool touchDown(TouchDownEvent *event) override + { + auto inputMethod = kwinApp()->inputMethod(); + if (!inputMethod) { + return false; + } + if (input()->findToplevel(event->pos) != inputMethod->activeWindow()) { + return false; + } + + inputMethod->commitPendingText(); + return false; + } + + bool keyboardKey(KeyboardKeyEvent *event) override + { + return passToInputMethod(event); + } +}; + +/** + * The remaining default input filter which forwards events to other windows + */ +class ForwardInputFilter : public InputEventFilter +{ +public: + ForwardInputFilter() + : InputEventFilter(InputFilterOrder::Forward) + { + } + bool pointerMotion(PointerMotionEvent *event) override + { + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->timestamp); + seat->notifyPointerMotion(event->position); + // absolute motion events confuse games and Wayland doesn't have a warp event yet + // -> send a relative motion event with a zero delta to signal the warp instead + if (event->warp) { + seat->relativePointerMotion(QPointF(0, 0), QPointF(0, 0), event->timestamp); + } else if (!event->delta.isNull()) { + seat->relativePointerMotion(event->delta, event->deltaUnaccelerated, event->timestamp); + } + return true; + } + bool pointerButton(PointerButtonEvent *event) override + { + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->timestamp); + seat->notifyPointerButton(event->nativeButton, event->state); + return true; + } + bool pointerFrame() override + { + auto seat = waylandServer()->seat(); + seat->notifyPointerFrame(); + return true; + } + bool pointerAxis(PointerAxisEvent *event) override + { + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->timestamp); + seat->notifyPointerAxis(event->orientation, event->delta, event->deltaV120, event->source, event->inverted); + return true; + } + bool keyboardKey(KeyboardKeyEvent *event) override + { + input()->keyboard()->update(); + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->timestamp); + seat->notifyKeyboardKey(event->nativeScanCode, event->state, event->serial); + return true; + } + bool touchDown(TouchDownEvent *event) override + { + auto seat = waylandServer()->seat(); + auto w = input()->findToplevel(event->pos); + if (!w) { + qCCritical(KWIN_CORE) << "Could not touch down, there's no window under" << event->pos; + return false; + } + seat->setTimestamp(event->time); + auto tp = seat->notifyTouchDown(w->surface(), w->bufferGeometry().topLeft(), event->id, event->pos); + if (!tp) { + qCCritical(KWIN_CORE) << "Could not touch down" << event->pos; + return false; + } + QObject::connect(w, &Window::bufferGeometryChanged, tp, [w, tp]() { + tp->setSurfacePosition(w->bufferGeometry().topLeft()); + }); + return true; + } + bool touchMotion(TouchMotionEvent *event) override + { + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->time); + seat->notifyTouchMotion(event->id, event->pos); + return true; + } + bool touchUp(TouchUpEvent *event) override + { + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->time); + seat->notifyTouchUp(event->id); + return true; + } + bool touchCancel() override + { + waylandServer()->seat()->notifyTouchCancel(); + return true; + } + bool touchFrame() override + { + waylandServer()->seat()->notifyTouchFrame(); + return true; + } + bool pinchGestureBegin(PointerPinchGestureBeginEvent *event) override + { + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->time); + seat->startPointerPinchGesture(event->fingerCount); + return true; + } + bool pinchGestureUpdate(PointerPinchGestureUpdateEvent *event) override + { + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->time); + seat->updatePointerPinchGesture(event->delta, event->scale, event->angleDelta); + return true; + } + bool pinchGestureEnd(PointerPinchGestureEndEvent *event) override + { + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->time); + seat->endPointerPinchGesture(); + return true; + } + bool pinchGestureCancelled(PointerPinchGestureCancelEvent *event) override + { + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->time); + seat->cancelPointerPinchGesture(); + return true; + } + + bool swipeGestureBegin(PointerSwipeGestureBeginEvent *event) override + { + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->time); + seat->startPointerSwipeGesture(event->fingerCount); + return true; + } + bool swipeGestureUpdate(PointerSwipeGestureUpdateEvent *event) override + { + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->time); + seat->updatePointerSwipeGesture(event->delta); + return true; + } + bool swipeGestureEnd(PointerSwipeGestureEndEvent *event) override + { + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->time); + seat->endPointerSwipeGesture(); + return true; + } + bool swipeGestureCancelled(PointerSwipeGestureCancelEvent *event) override + { + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->time); + seat->cancelPointerSwipeGesture(); + return true; + } + bool holdGestureBegin(PointerHoldGestureBeginEvent *event) override + { + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->time); + seat->startPointerHoldGesture(event->fingerCount); + return true; + } + bool holdGestureEnd(PointerHoldGestureEndEvent *event) override + { + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->time); + seat->endPointerHoldGesture(); + return true; + } + bool holdGestureCancelled(PointerHoldGestureCancelEvent *event) override + { + auto seat = waylandServer()->seat(); + seat->setTimestamp(event->time); + seat->cancelPointerHoldGesture(); + return true; + } + + bool tabletToolProximityEvent(TabletToolProximityEvent *event) override + { + Window *window = input()->tablet()->focus(); + if (!window || !window->surface()) { + return false; + } + + TabletSeatV2Interface *seat = waylandServer()->tabletManagerV2()->seat(waylandServer()->seat()); + TabletToolV2Interface *tool = seat->tool(event->tool); + TabletV2Interface *tablet = seat->tablet(event->device); + + const auto [surface, surfaceLocalPos] = window->surface()->mapToInputSurface(window->mapToLocal(event->position)); + tool->setCurrentSurface(surface); + + if (!tool->isClientSupported() || !tablet->isSurfaceSupported(surface)) { + return emulateTabletEvent(event); + } + + if (event->type == TabletToolProximityEvent::EnterProximity) { + tool->sendProximityIn(tablet); + tool->sendMotion(surfaceLocalPos); + } else { + tool->sendProximityOut(); + } + + if (tool->hasCapability(TabletToolV2Interface::Tilt)) { + tool->sendTilt(event->xTilt, event->yTilt); + } + if (tool->hasCapability(TabletToolV2Interface::Rotation)) { + tool->sendRotation(event->rotation); + } + if (tool->hasCapability(TabletToolV2Interface::Distance)) { + tool->sendDistance(event->distance); + } + if (tool->hasCapability(TabletToolV2Interface::Slider)) { + tool->sendSlider(event->sliderPosition); + } + + tool->sendFrame(std::chrono::duration_cast(event->timestamp).count()); + return true; + } + + bool tabletToolAxisEvent(TabletToolAxisEvent *event) override + { + Window *window = input()->tablet()->focus(); + if (!window || !window->surface()) { + return false; + } + + TabletSeatV2Interface *seat = waylandServer()->tabletManagerV2()->seat(waylandServer()->seat()); + TabletToolV2Interface *tool = seat->tool(event->tool); + TabletV2Interface *tablet = seat->tablet(event->device); + + const auto [surface, surfaceLocalPos] = window->surface()->mapToInputSurface(window->mapToLocal(event->position)); + tool->setCurrentSurface(surface); + + if (!tool->isClientSupported() || !tablet->isSurfaceSupported(surface)) { + return emulateTabletEvent(event); + } + + tool->sendMotion(surfaceLocalPos); + + if (tool->hasCapability(TabletToolV2Interface::Pressure)) { + tool->sendPressure(event->pressure); + } + if (tool->hasCapability(TabletToolV2Interface::Tilt)) { + tool->sendTilt(event->xTilt, event->yTilt); + } + if (tool->hasCapability(TabletToolV2Interface::Rotation)) { + tool->sendRotation(event->rotation); + } + if (tool->hasCapability(TabletToolV2Interface::Distance)) { + tool->sendDistance(event->distance); + } + if (tool->hasCapability(TabletToolV2Interface::Slider)) { + tool->sendSlider(event->sliderPosition); + } + + tool->sendFrame(std::chrono::duration_cast(event->timestamp).count()); + return true; + } + + bool tabletToolTipEvent(TabletToolTipEvent *event) override + { + Window *window = input()->tablet()->focus(); + if (!window || !window->surface()) { + return false; + } + + TabletSeatV2Interface *seat = waylandServer()->tabletManagerV2()->seat(waylandServer()->seat()); + TabletToolV2Interface *tool = seat->tool(event->tool); + TabletV2Interface *tablet = seat->tablet(event->device); + + const auto [surface, surfaceLocalPos] = window->surface()->mapToInputSurface(window->mapToLocal(event->position)); + tool->setCurrentSurface(surface); + + if (!tool->isClientSupported() || !tablet->isSurfaceSupported(surface)) { + return emulateTabletEvent(event); + } + + if (event->type == TabletToolTipEvent::Press) { + tool->sendMotion(surfaceLocalPos); + tool->sendDown(); + } else { + tool->sendUp(); + } + + if (tool->hasCapability(TabletToolV2Interface::Pressure)) { + tool->sendPressure(event->pressure); + } + if (tool->hasCapability(TabletToolV2Interface::Tilt)) { + tool->sendTilt(event->xTilt, event->yTilt); + } + if (tool->hasCapability(TabletToolV2Interface::Rotation)) { + tool->sendRotation(event->rotation); + } + if (tool->hasCapability(TabletToolV2Interface::Distance)) { + tool->sendDistance(event->distance); + } + if (tool->hasCapability(TabletToolV2Interface::Slider)) { + tool->sendSlider(event->sliderPosition); + } + + tool->sendFrame(std::chrono::duration_cast(event->timestamp).count()); + return true; + } + + bool emulateTabletEvent(TabletToolProximityEvent *event) + { + // Tablet input emulation is deprecated. It will be removed in the near future. + static bool emulateInput = qEnvironmentVariableIntValue("KWIN_WAYLAND_EMULATE_TABLET") == 1; + if (!emulateInput) { + return false; + } + + switch (event->type) { + case TabletToolProximityEvent::EnterProximity: + input()->pointer()->processMotionAbsolute(event->position, event->timestamp); + break; + case TabletToolProximityEvent::LeaveProximity: + break; + } + return true; + } + + bool emulateTabletEvent(TabletToolTipEvent *event) + { + // Tablet input emulation is deprecated. It will be removed in the near future. + static bool emulateInput = qEnvironmentVariableIntValue("KWIN_WAYLAND_EMULATE_TABLET") == 1; + if (!emulateInput) { + return false; + } + + switch (event->type) { + case TabletToolTipEvent::Press: + input()->pointer()->processButton(qtMouseButtonToButton(Qt::LeftButton), + PointerButtonState::Pressed, event->timestamp); + break; + case TabletToolTipEvent::Release: + input()->pointer()->processButton(qtMouseButtonToButton(Qt::LeftButton), + PointerButtonState::Released, event->timestamp); + break; + } + return true; + } + + bool emulateTabletEvent(TabletToolAxisEvent *event) + { + // Tablet input emulation is deprecated. It will be removed in the near future. + static bool emulateInput = qEnvironmentVariableIntValue("KWIN_WAYLAND_EMULATE_TABLET") == 1; + if (!emulateInput) { + return false; + } + + input()->pointer()->processMotionAbsolute(event->position, event->timestamp); + + return true; + } + + bool tabletToolButtonEvent(TabletToolButtonEvent *event) override + { + TabletToolV2Interface *tool = waylandServer()->tabletManagerV2()->seat(waylandServer()->seat())->tool(event->tool); + if (!tool->isClientSupported()) { + return false; + } + tool->sendButton(event->button, event->pressed); + return true; + } + + TabletPadV2Interface *findAndAdoptPad(InputDevice *device) const + { + Window *window = workspace()->activeWindow(); + TabletSeatV2Interface *seat = waylandServer()->tabletManagerV2()->seat(waylandServer()->seat()); + if (!window || !window->surface() || !seat->isClientSupported(window->surface()->client())) { + return nullptr; + } + + TabletPadV2Interface *pad = seat->pad(device); + if (!pad) { + return nullptr; + } + + TabletV2Interface *tablet = seat->matchingTablet(pad); + if (!tablet) { + return nullptr; + } + + pad->setCurrentSurface(window->surface(), tablet); + return pad; + } + + bool tabletPadButtonEvent(TabletPadButtonEvent *event) override + { + auto pad = findAndAdoptPad(event->device); + if (!pad) { + return false; + } + + auto group = pad->group(event->group); + if (event->isModeSwitch) { + group->setCurrentMode(event->mode); + const auto milliseconds = std::chrono::duration_cast(event->time).count(); + group->sendModeSwitch(milliseconds); + // TODO send button to app? + } + + pad->sendButton(event->time, event->button, event->pressed); + return true; + } + + bool tabletPadRingEvent(TabletPadRingEvent *event) override + { + auto pad = findAndAdoptPad(event->device); + if (!pad) { + return false; + } + auto ring = pad->group(event->group)->ring(event->number); + + if (event->isFinger && event->position == -1) { + ring->sendStop(); + } else { + ring->sendAngle(event->position); + } + + if (event->isFinger) { + ring->sendSource(TabletPadRingV2Interface::SourceFinger); + } + ring->sendFrame(std::chrono::duration_cast(event->time).count()); + return true; + } + + bool tabletPadStripEvent(TabletPadStripEvent *event) override + { + auto pad = findAndAdoptPad(event->device); + if (!pad) { + return false; + } + auto strip = pad->group(event->group)->strip(event->number); + + strip->sendPosition(event->position); + if (event->isFinger) { + strip->sendSource(TabletPadStripV2Interface::SourceFinger); + } + strip->sendFrame(std::chrono::duration_cast(event->time).count()); + return true; + } + + bool tabletPadDialEvent(TabletPadDialEvent *event) override + { + auto pad = findAndAdoptPad(event->device); + if (!pad) { + return false; + } + auto dial = pad->group(event->group)->dial(event->number); + + dial->sendDelta(event->delta); + + dial->sendFrame(std::chrono::duration_cast(event->time).count()); + return true; + } +}; + +static AbstractDropHandler *dropHandler(Window *window) +{ + auto surface = window->surface(); + if (!surface) { + return nullptr; + } + auto seat = waylandServer()->seat(); + auto dropTarget = seat->dropHandlerForSurface(surface); + if (dropTarget) { + return dropTarget; + } +#if KWIN_BUILD_X11 + if (qobject_cast(window) && kwinApp()->xwayland()) { + return kwinApp()->xwayland()->xwlDropHandler(); + } +#endif + + return nullptr; +} + +class DragAndDropInputFilter : public QObject, public InputEventFilter +{ + Q_OBJECT +public: + DragAndDropInputFilter() + : InputEventFilter(InputFilterOrder::DragAndDrop) + { + connect(waylandServer()->seat(), &SeatInterface::dragRequested, this, [](AbstractDataSource *source, SurfaceInterface *origin, quint32 serial, DragAndDropIcon *dragIcon) { + if (auto window = waylandServer()->findWindow(origin->mainSurface())) { + QMatrix4x4 transformation = window->inputTransformation(); + transformation.translate(-QVector3D(origin->mapToMainSurface(QPointF(0, 0)))); + + if (waylandServer()->seat()->hasImplicitPointerGrab(serial)) { + if (waylandServer()->seat()->startPointerDrag(source, origin, waylandServer()->seat()->pointerPos(), transformation, serial, dragIcon)) { + return; + } + } + + if (const auto touchPoint = waylandServer()->seat()->touchPointByImplicitGrabSerial(serial)) { + if (waylandServer()->seat()->startTouchDrag(source, origin, touchPoint->position, transformation, serial, dragIcon)) { + return; + } + } + + if (waylandServer()->tabletManagerV2()->seat(waylandServer()->seat())->hasImplicitGrab(serial)) { + if (waylandServer()->seat()->startTabletDrag(source, origin, input()->tablet()->position(), transformation, serial, dragIcon)) { + return; + } + } + } + + if (source) { + source->dndCancelled(); + } + }); + + connect(waylandServer()->seat(), &SeatInterface::dragStarted, this, [this]() { + AbstractDataSource *dragSource = waylandServer()->seat()->dragSource(); + if (!dragSource) { + return; + } + + dragSource->setKeyboardModifiers(input()->keyboardModifiers()); + connect(input(), &InputRedirection::keyboardModifiersChanged, dragSource, [dragSource](Qt::KeyboardModifiers mods) { + dragSource->setKeyboardModifiers(mods); + }); + auto toplevelDrag = waylandServer()->seat()->xdgTopleveldrag(); + if (!toplevelDrag) { + return; + } + setupDraggedToplevel(); + connect(toplevelDrag, &XdgToplevelDragV1Interface::toplevelChanged, this, &DragAndDropInputFilter::setupDraggedToplevel); + }); + + connect(waylandServer()->seat(), &SeatInterface::dragEnded, this, [this] { + m_dragTarget = nullptr; + m_lastPos.reset(); + if (m_currentToplevelDragWindow) { + m_currentToplevelDragWindow->setKeepAbove(m_wasKeepAbove); + m_currentToplevelDragWindow->endInteractiveMoveResize(); + workspace()->raiseWindow(m_currentToplevelDragWindow); + workspace()->requestFocus(m_currentToplevelDragWindow); + m_currentToplevelDragWindow = nullptr; + } + }); + + m_raiseTimer.setSingleShot(true); + m_raiseTimer.setInterval(1000); + connect(&m_raiseTimer, &QTimer::timeout, this, &DragAndDropInputFilter::raiseDragTarget); + } + + bool pointerMotion(PointerMotionEvent *event) override + { + auto seat = waylandServer()->seat(); + if (!seat->isDragPointer()) { + return false; + } + if (seat->isDragTouch()) { + return true; + } + + motion(event->position, event->timestamp); + return true; + } + + bool pointerButton(PointerButtonEvent *event) override + { + auto seat = waylandServer()->seat(); + if (!seat->isDragPointer()) { + return false; + } + if (seat->isDragTouch()) { + return true; + } + seat->setTimestamp(event->timestamp); + if (event->state == PointerButtonState::Pressed) { + seat->notifyPointerButton(event->nativeButton, event->state); + } else { + raiseDragTarget(); + m_dragTarget = nullptr; + seat->notifyPointerButton(event->nativeButton, event->state); + } + // TODO: should we pass through effects? + return true; + } + + bool pointerFrame() override + { + auto seat = waylandServer()->seat(); + if (!seat->isDragPointer()) { + return false; + } + if (seat->isDragTouch()) { + return true; + } + + seat->notifyPointerFrame(); + return true; + } + + bool touchDown(TouchDownEvent *event) override + { + auto seat = waylandServer()->seat(); + if (seat->isDragPointer()) { + return true; + } + return seat->isDragTouch(); + } + bool touchMotion(TouchMotionEvent *event) override + { + auto seat = waylandServer()->seat(); + if (seat->isDragPointer()) { + return true; + } + if (!seat->isDragTouch()) { + return false; + } + + const auto touchPoint = seat->touchPointByImplicitGrabSerial(*seat->dragSerial()); + if (!touchPoint || touchPoint->id != event->id) { + return true; + } + + motion(event->pos, event->time); + return true; + } + bool touchUp(TouchUpEvent *event) override + { + auto seat = waylandServer()->seat(); + if (!seat->isDragTouch()) { + return false; + } + seat->setTimestamp(event->time); + const auto touchPoint = seat->touchPointByImplicitGrabSerial(*seat->dragSerial()); + if (touchPoint && touchPoint->id == event->id) { + seat->endDrag(); + raiseDragTarget(); + } + seat->notifyTouchUp(event->id); + return true; + } + bool touchCancel() override + { + auto seat = waylandServer()->seat(); + if (!seat->isDragTouch()) { + return false; + } + seat->cancelDrag(); + seat->notifyTouchCancel(); + return true; + } + bool keyboardKey(KeyboardKeyEvent *event) override + { + if (event->key != Qt::Key_Escape) { + return false; + } + + auto seat = waylandServer()->seat(); + if (!seat->isDrag()) { + return false; + } + + seat->cancelDrag(); + + return true; + } + + bool tabletToolProximityEvent(TabletToolProximityEvent *event) override + { + SeatInterface *seat = waylandServer()->seat(); + if (!seat->isDragTablet()) { + return false; + } + + return true; + } + + bool tabletToolAxisEvent(TabletToolAxisEvent *event) override + { + SeatInterface *seat = waylandServer()->seat(); + if (!seat->isDragTablet()) { + return false; + } + + TabletToolV2Interface *dragTool = waylandServer()->tabletManagerV2()->seat(seat)->toolByImplicitGrabSerial(*seat->dragSerial()); + if (!dragTool || dragTool->device() != event->tool) { + return true; + } + + motion(event->position, event->timestamp); + return true; + } + + bool tabletToolTipEvent(TabletToolTipEvent *event) override + { + SeatInterface *seat = waylandServer()->seat(); + if (!seat->isDragTablet()) { + return false; + } + + if (event->type == TabletToolTipEvent::Release) { + seat->endDrag(); + } + + return true; + } + + bool tabletToolButtonEvent(TabletToolButtonEvent *event) override + { + SeatInterface *seat = waylandServer()->seat(); + if (!seat->isDragTablet()) { + return false; + } + + return true; + } + +private: + void raiseDragTarget() + { + m_raiseTimer.stop(); + if (m_dragTarget) { + workspace()->raiseWindow(m_dragTarget); + } + } + + Window *pickDragTarget(const QPointF &pos) const + { + const QList stacking = workspace()->stackingOrder(); + if (stacking.isEmpty()) { + return nullptr; + } + auto it = stacking.end(); + do { + --it; + Window *window = (*it); + if (auto toplevelDrag = waylandServer()->seat()->xdgTopleveldrag(); toplevelDrag && toplevelDrag->toplevel() && toplevelDrag->toplevel()->surface() == window->surface()) { + continue; + } + if (window->isDeleted()) { + continue; + } + if (!window->isClient()) { + continue; + } + if (!window->isOnCurrentActivity() || !window->isOnCurrentDesktop() || window->isMinimized() || window->isHidden() || window->isHiddenByShowDesktop()) { + continue; + } + if (!window->readyForPainting()) { + continue; + } + if (window->hitTest(pos)) { + return window; + } + } while (it != stacking.begin()); + return nullptr; + } + + void setupDraggedToplevel() + { + auto toplevelDrag = waylandServer()->seat()->xdgTopleveldrag(); + if (!toplevelDrag) { + return; + } + auto window = toplevelDrag->toplevel() ? waylandServer()->findWindow(toplevelDrag->toplevel()->surface()) : nullptr; + if (window == m_currentToplevelDragWindow || !window) { + return; + } + if (!window->readyForPainting()) { + connect(window, &Window::readyForPaintingChanged, this, &DragAndDropInputFilter::setupDraggedToplevel, Qt::SingleShotConnection); + return; + } + const auto currentPosition = waylandServer()->seat()->isDragPointer() ? input()->pointer()->pos() : input()->tablet()->position(); + m_wasKeepAbove = window->keepAbove(); + window->move(currentPosition - waylandServer()->seat()->xdgTopleveldrag()->offset()); + window->setKeepAbove(true); + window->performMousePressCommand(Options::MouseMove, currentPosition); + m_currentToplevelDragWindow = window; + } + + void motion(const QPointF &position, std::chrono::microseconds time) + { + SeatInterface *seat = waylandServer()->seat(); + seat->setTimestamp(time); + + if (auto window = m_currentToplevelDragWindow) { + window->updateInteractiveMoveResize(position, input()->keyboardModifiers()); + } + + Window *dragTarget = pickDragTarget(position); + if (dragTarget) { + if (dragTarget != m_dragTarget) { + workspace()->requestFocus(dragTarget); + m_raiseTimer.start(); + } + if (!m_lastPos || (position - *m_lastPos).manhattanLength() > 10) { + m_lastPos = position; + // reset timer to delay raising the window + m_raiseTimer.start(); + } + } + m_dragTarget = dragTarget; + + if (auto *xwl = kwinApp()->xwayland()) { + if (xwl->dragMoveFilter(dragTarget, position)) { + return; + } + } + + if (dragTarget && dragTarget->surface()) { + const auto [effectiveSurface, offset] = dragTarget->surface()->mapToInputSurface(dragTarget->mapToLocal(position)); + if (seat->dragSurface() != effectiveSurface) { + QMatrix4x4 inputTransformation = dragTarget->inputTransformation(); + inputTransformation.translate(-QVector3D(effectiveSurface->mapToMainSurface(QPointF(0, 0)))); + seat->setDragTarget(dropHandler(dragTarget), effectiveSurface, position, inputTransformation); + } else { + seat->notifyDragMotion(position); + } + } else { + // no window at that place, if we have a surface we need to reset + seat->setDragTarget(nullptr, nullptr, QPointF(), QMatrix4x4()); + } + } + + std::optional m_lastPos = std::nullopt; + QPointer m_dragTarget; + QTimer m_raiseTimer; + QPointer m_currentToplevelDragWindow = nullptr; + bool m_wasKeepAbove = false; +}; + +KWIN_SINGLETON_FACTORY(InputRedirection) + +InputRedirection::InputRedirection(QObject *parent) + : QObject(parent) + , m_keyboard(new KeyboardInputRedirection(this)) + , m_pointer(new PointerInputRedirection(this)) + , m_tablet(new TabletInputRedirection(this)) + , m_touch(new TouchInputRedirection(this)) +#if KWIN_BUILD_GLOBALSHORTCUTS + , m_shortcuts(new GlobalShortcutsManager(this)) +#endif +{ + setupInputBackends(); + + connect(kwinApp(), &Application::workspaceCreated, this, &InputRedirection::setupWorkspace); +} + +InputRedirection::~InputRedirection() +{ + m_inputBackends.clear(); + m_inputDevices.clear(); + + s_self = nullptr; +} + +void InputRedirection::installInputEventFilter(InputEventFilter *filter) +{ + Q_ASSERT(!m_filters.contains(filter)); + + auto it = std::lower_bound(m_filters.begin(), m_filters.end(), filter, [](InputEventFilter *a, InputEventFilter *b) { + return a->weight() < b->weight(); + }); + m_filters.insert(it, filter); +} + +void InputRedirection::uninstallInputEventFilter(InputEventFilter *filter) +{ + m_filters.removeOne(filter); +} + +void InputRedirection::installInputEventSpy(InputEventSpy *spy) +{ + m_spies << spy; +} + +void InputRedirection::uninstallInputEventSpy(InputEventSpy *spy) +{ + m_spies.removeOne(spy); +} + +void InputRedirection::init() +{ + m_inputConfigWatcher = KConfigWatcher::create(kwinApp()->inputConfig()); + connect(m_inputConfigWatcher.data(), &KConfigWatcher::configChanged, + this, &InputRedirection::handleInputConfigChanged); +#if KWIN_BUILD_GLOBALSHORTCUTS + m_shortcuts->init(); +#endif +} + +void InputRedirection::setupWorkspace() +{ + connect(workspace(), &Workspace::outputsChanged, this, &InputRedirection::updateScreens); + + m_keyboard->init(); + m_pointer->init(); + m_touch->init(); + m_tablet->init(); + + updateLeds(m_keyboard->xkb()->leds()); + connect(m_keyboard, &KeyboardInputRedirection::ledsChanged, this, &InputRedirection::updateLeds); + + setupInputFilters(); + updateScreens(); +} + +void InputRedirection::updateScreens() +{ + for (const auto &backend : m_inputBackends) { + backend->updateScreens(); + } +} + +QObject *InputRedirection::lastInputHandler() const +{ + return m_lastInputDevice; +} + +void InputRedirection::setLastInteractionSerial(uint32_t serial) +{ + m_lastInteractionSerial = serial; + workspace()->setWasUserInteraction(); +} + +uint32_t InputRedirection::lastInteractionSerial() const +{ + return m_lastInteractionSerial; +} + +void InputRedirection::setLastInputHandler(QObject *device) +{ + m_lastInputDevice = device; +} + +class UserActivitySpy : public InputEventSpy +{ +public: + void pointerMotion(PointerMotionEvent *event) override + { + if (!event->warp) { + notifyActivity(); + } + } + void pointerButton(PointerButtonEvent *event) override + { + notifyActivity(); + } + void pointerAxis(PointerAxisEvent *event) override + { + notifyActivity(); + } + + void keyboardKey(KeyboardKeyEvent *event) override + { + notifyActivity(); + } + + void touchDown(TouchDownEvent *event) override + { + notifyActivity(); + } + void touchMotion(TouchMotionEvent *event) override + { + notifyActivity(); + } + void touchUp(TouchUpEvent *event) override + { + notifyActivity(); + } + + void pinchGestureBegin(PointerPinchGestureBeginEvent *event) override + { + notifyActivity(); + } + void pinchGestureUpdate(PointerPinchGestureUpdateEvent *event) override + { + notifyActivity(); + } + void pinchGestureEnd(PointerPinchGestureEndEvent *event) override + { + notifyActivity(); + } + void pinchGestureCancelled(PointerPinchGestureCancelEvent *event) override + { + notifyActivity(); + } + + void swipeGestureBegin(PointerSwipeGestureBeginEvent *event) override + { + notifyActivity(); + } + void swipeGestureUpdate(PointerSwipeGestureUpdateEvent *event) override + { + notifyActivity(); + } + void swipeGestureEnd(PointerSwipeGestureEndEvent *event) override + { + notifyActivity(); + } + void swipeGestureCancelled(PointerSwipeGestureCancelEvent *event) override + { + notifyActivity(); + } + + void holdGestureBegin(PointerHoldGestureBeginEvent *event) override + { + notifyActivity(); + } + void holdGestureEnd(PointerHoldGestureEndEvent *event) override + { + notifyActivity(); + } + void holdGestureCancelled(PointerHoldGestureCancelEvent *event) override + { + notifyActivity(); + } + + void tabletToolProximityEvent(TabletToolProximityEvent *event) override + { + notifyActivity(); + } + void tabletToolAxisEvent(TabletToolAxisEvent *event) override + { + notifyActivity(); + } + void tabletToolTipEvent(TabletToolTipEvent *event) override + { + notifyActivity(); + } + void tabletToolButtonEvent(TabletToolButtonEvent *event) override + { + notifyActivity(); + } + void tabletPadButtonEvent(TabletPadButtonEvent *event) override + { + notifyActivity(); + } + void tabletPadStripEvent(TabletPadStripEvent *event) override + { + notifyActivity(); + } + void tabletPadRingEvent(TabletPadRingEvent *event) override + { + notifyActivity(); + } + void tabletPadDialEvent(TabletPadDialEvent *event) override + { + notifyActivity(); + } + +private: + void notifyActivity() + { + input()->simulateUserActivity(); + } +}; + +void InputRedirection::setupInputFilters() +{ + if (kwinApp()->session()->capabilities() & Session::Capability::SwitchTerminal) { + m_virtualTerminalFilter = std::make_unique(); + installInputEventFilter(m_virtualTerminalFilter.get()); + } + + m_hideCursorSpy = std::make_unique(); + installInputEventSpy(m_hideCursorSpy.get()); + + m_userActivitySpy = std::make_unique(); + installInputEventSpy(m_userActivitySpy.get()); + +#if KWIN_BUILD_SCREENLOCKER + m_lockscreenFilter = std::make_unique(); + installInputEventFilter(m_lockscreenFilter.get()); +#endif + + if (kwinApp()->supportsGlobalShortcuts()) { + m_screenEdgeFilter = std::make_unique(); + installInputEventFilter(m_screenEdgeFilter.get()); + } + + m_dragAndDropFilter = std::make_unique(); + installInputEventFilter(m_dragAndDropFilter.get()); + + m_windowSelector = std::make_unique(); + installInputEventFilter(m_windowSelector.get()); + +#if KWIN_BUILD_TABBOX + m_tabboxFilter = std::make_unique(); + installInputEventFilter(m_tabboxFilter.get()); +#endif +#if KWIN_BUILD_GLOBALSHORTCUTS + if (kwinApp()->supportsGlobalShortcuts()) { + m_globalShortcutFilter = std::make_unique(); + installInputEventFilter(m_globalShortcutFilter.get()); + } +#endif + + m_effectsFilter = std::make_unique(); + installInputEventFilter(m_effectsFilter.get()); + + m_interactiveMoveResizeFilter = std::make_unique(); + installInputEventFilter(m_interactiveMoveResizeFilter.get()); + + m_popupFilter = std::make_unique(); + installInputEventFilter(m_popupFilter.get()); + + m_decorationFilter = std::make_unique(); + installInputEventFilter(m_decorationFilter.get()); + + m_windowActionFilter = std::make_unique(); + installInputEventFilter(m_windowActionFilter.get()); + + m_internalWindowFilter = std::make_unique(); + installInputEventFilter(m_internalWindowFilter.get()); + + m_inputKeyboardFilter = std::make_unique(); + installInputEventFilter(m_inputKeyboardFilter.get()); + + m_forwardFilter = std::make_unique(); + installInputEventFilter(m_forwardFilter.get()); +} + +void InputRedirection::handleInputConfigChanged(const KConfigGroup &group) +{ + if (group.name() == QLatin1String("Keyboard")) { + m_keyboard->reconfigure(); + } +} + +void InputRedirection::updateLeds(LEDs leds) +{ + if (m_leds != leds) { + m_leds = leds; + + for (InputDevice *device : std::as_const(m_inputDevices)) { + device->setLeds(leds); + } + } +} + +void InputRedirection::addInputDevice(InputDevice *device) +{ + connect(device, &InputDevice::keyChanged, m_keyboard, &KeyboardInputRedirection::processKey); + + connect(device, &InputDevice::pointerMotionAbsolute, + m_pointer, &PointerInputRedirection::processMotionAbsolute); + connect(device, &InputDevice::pointerMotion, + m_pointer, &PointerInputRedirection::processMotion); + connect(device, &InputDevice::pointerButtonChanged, + m_pointer, &PointerInputRedirection::processButton); + connect(device, &InputDevice::pointerAxisChanged, + m_pointer, &PointerInputRedirection::processAxis); + connect(device, &InputDevice::pointerFrame, + m_pointer, &PointerInputRedirection::processFrame); + connect(device, &InputDevice::pinchGestureBegin, + m_pointer, &PointerInputRedirection::processPinchGestureBegin); + connect(device, &InputDevice::pinchGestureUpdate, + m_pointer, &PointerInputRedirection::processPinchGestureUpdate); + connect(device, &InputDevice::pinchGestureEnd, + m_pointer, &PointerInputRedirection::processPinchGestureEnd); + connect(device, &InputDevice::pinchGestureCancelled, + m_pointer, &PointerInputRedirection::processPinchGestureCancelled); + connect(device, &InputDevice::swipeGestureBegin, + m_pointer, &PointerInputRedirection::processSwipeGestureBegin); + connect(device, &InputDevice::swipeGestureUpdate, + m_pointer, &PointerInputRedirection::processSwipeGestureUpdate); + connect(device, &InputDevice::swipeGestureEnd, + m_pointer, &PointerInputRedirection::processSwipeGestureEnd); + connect(device, &InputDevice::swipeGestureCancelled, + m_pointer, &PointerInputRedirection::processSwipeGestureCancelled); + connect(device, &InputDevice::holdGestureBegin, + m_pointer, &PointerInputRedirection::processHoldGestureBegin); + connect(device, &InputDevice::holdGestureEnd, + m_pointer, &PointerInputRedirection::processHoldGestureEnd); + connect(device, &InputDevice::holdGestureCancelled, + m_pointer, &PointerInputRedirection::processHoldGestureCancelled); + + connect(device, &InputDevice::touchDown, m_touch, &TouchInputRedirection::processDown); + connect(device, &InputDevice::touchUp, m_touch, &TouchInputRedirection::processUp); + connect(device, &InputDevice::touchMotion, m_touch, &TouchInputRedirection::processMotion); + connect(device, &InputDevice::touchCanceled, m_touch, &TouchInputRedirection::cancel); + connect(device, &InputDevice::touchFrame, m_touch, &TouchInputRedirection::frame); + + connect(device, &InputDevice::switchToggle, this, [this](SwitchState state, std::chrono::microseconds time, InputDevice *device) { + SwitchEvent event{ + .device = device, + .state = state, + .timestamp = time, + }; + processSpies(&InputEventSpy::switchEvent, &event); + processFilters(&InputEventFilter::switchEvent, &event); + }); + + connect(device, &InputDevice::tabletToolAxisEvent, + m_tablet, &TabletInputRedirection::tabletToolAxisEvent); + connect(device, &InputDevice::tabletToolProximityEvent, + m_tablet, &TabletInputRedirection::tabletToolProximityEvent); + connect(device, &InputDevice::tabletToolTipEvent, + m_tablet, &TabletInputRedirection::tabletToolTipEvent); + connect(device, &InputDevice::tabletToolAxisEventRelative, + m_tablet, &TabletInputRedirection::tabletToolAxisEventRelative); + connect(device, &InputDevice::tabletToolButtonEvent, + m_tablet, &TabletInputRedirection::tabletToolButtonEvent); + connect(device, &InputDevice::tabletPadButtonEvent, + m_tablet, &TabletInputRedirection::tabletPadButtonEvent); + connect(device, &InputDevice::tabletPadRingEvent, + m_tablet, &TabletInputRedirection::tabletPadRingEvent); + connect(device, &InputDevice::tabletPadStripEvent, + m_tablet, &TabletInputRedirection::tabletPadStripEvent); + connect(device, &InputDevice::tabletPadDialEvent, + m_tablet, &TabletInputRedirection::tabletPadDialEvent); + + device->setLeds(m_leds); + + m_inputDevices.append(device); + Q_EMIT deviceAdded(device); + + updateAvailableInputDevices(); +} + +void InputRedirection::removeInputDevice(InputDevice *device) +{ + m_inputDevices.removeOne(device); + Q_EMIT deviceRemoved(device); + + updateAvailableInputDevices(); +} + +void InputRedirection::updateAvailableInputDevices() +{ + const bool hasKeyboard = std::any_of(m_inputDevices.constBegin(), m_inputDevices.constEnd(), [](InputDevice *device) { + return device->isKeyboard(); + }); + if (m_hasKeyboard != hasKeyboard) { + m_hasKeyboard = hasKeyboard; + Q_EMIT hasKeyboardChanged(hasKeyboard); + } + + const bool hasPointer = std::any_of(m_inputDevices.constBegin(), m_inputDevices.constEnd(), [](InputDevice *device) { + return device->isPointer(); + }); + if (m_hasPointer != hasPointer) { + m_hasPointer = hasPointer; + Q_EMIT hasPointerChanged(hasPointer); + } + + const bool hasTouch = std::any_of(m_inputDevices.constBegin(), m_inputDevices.constEnd(), [](InputDevice *device) { + return device->isTouch(); + }); + if (m_hasTouch != hasTouch) { + m_hasTouch = hasTouch; + Q_EMIT hasTouchChanged(hasTouch); + } + + const bool hasTabletModeSwitch = std::any_of(m_inputDevices.constBegin(), m_inputDevices.constEnd(), [](InputDevice *device) { + return device->isTabletModeSwitch(); + }); + if (m_hasTabletModeSwitch != hasTabletModeSwitch) { + m_hasTabletModeSwitch = hasTabletModeSwitch; + Q_EMIT hasTabletModeSwitchChanged(hasTabletModeSwitch); + } +} + +void InputRedirection::addInputBackend(std::unique_ptr &&inputBackend) +{ + connect(inputBackend.get(), &InputBackend::deviceAdded, this, &InputRedirection::addInputDevice); + connect(inputBackend.get(), &InputBackend::deviceRemoved, this, &InputRedirection::removeInputDevice); + + inputBackend->setConfig(kwinApp()->inputConfig()); + inputBackend->initialize(); + + m_inputBackends.push_back(std::move(inputBackend)); +} + +void InputRedirection::setupInputBackends() +{ + std::unique_ptr inputBackend = kwinApp()->outputBackend()->createInputBackend(); + if (inputBackend) { + addInputBackend(std::move(inputBackend)); + } + addInputBackend(std::make_unique(waylandServer()->display())); +} + +bool InputRedirection::hasPointer() const +{ + return m_hasPointer; +} + +bool InputRedirection::hasTouch() const +{ + return m_hasTouch; +} + +bool InputRedirection::hasTabletModeSwitch() +{ + return m_hasTabletModeSwitch; +} + +Qt::MouseButtons InputRedirection::qtButtonStates() const +{ + return m_pointer->buttons(); +} + +void InputRedirection::simulateUserActivity() +{ + const auto idleDetectors = m_idleDetectors; // the detector list can potentially change + for (IdleDetector *idleDetector : idleDetectors) { + idleDetector->activity(); + } +} + +void InputRedirection::addIdleDetector(IdleDetector *detector) +{ + Q_ASSERT(!m_idleDetectors.contains(detector)); + detector->setInhibited(!m_idleInhibitors.isEmpty()); + m_idleDetectors.append(detector); +} + +void InputRedirection::removeIdleDetector(IdleDetector *detector) +{ + m_idleDetectors.removeOne(detector); +} + +QList InputRedirection::idleInhibitors() const +{ + return m_idleInhibitors; +} + +void InputRedirection::addIdleInhibitor(Window *inhibitor) +{ + if (!m_idleInhibitors.contains(inhibitor)) { + m_idleInhibitors.append(inhibitor); + for (IdleDetector *idleDetector : std::as_const(m_idleDetectors)) { + idleDetector->setInhibited(true); + } + } +} + +void InputRedirection::removeIdleInhibitor(Window *inhibitor) +{ + if (m_idleInhibitors.removeOne(inhibitor) && m_idleInhibitors.isEmpty()) { + for (IdleDetector *idleDetector : std::as_const(m_idleDetectors)) { + idleDetector->setInhibited(false); + } + } +} + +Window *InputRedirection::findToplevel(const QPointF &pos) +{ + if (!Workspace::self()) { + return nullptr; + } + const bool isScreenLocked = waylandServer() && waylandServer()->isScreenLocked(); + if (!isScreenLocked) { + // if an effect overrides the cursor we don't have a window to focus + if (effects && effects->isMouseInterception()) { + return nullptr; + } + } + const QList &stacking = Workspace::self()->stackingOrder(); + if (stacking.isEmpty()) { + return nullptr; + } + auto it = stacking.end(); + do { + --it; + Window *window = (*it); + if (window->isDeleted()) { + // a deleted window doesn't get mouse events + continue; + } + if (!window->isOnCurrentActivity() || !window->isOnCurrentDesktop() || window->isMinimized() || window->isHidden() || window->isHiddenByShowDesktop()) { + continue; + } + if (!window->readyForPainting()) { + continue; + } + if (isScreenLocked) { + if (!window->isLockScreen() && !window->isInputMethod() && !window->isLockScreenOverlay()) { + continue; + } + } + if (window->hitTest(pos)) { + return window; + } + } while (it != stacking.begin()); + return nullptr; +} + +Qt::KeyboardModifiers InputRedirection::keyboardModifiers() const +{ + return m_keyboard->modifiers(); +} + +Qt::KeyboardModifiers InputRedirection::modifiersRelevantForGlobalShortcuts() const +{ + return m_keyboard->modifiersRelevantForGlobalShortcuts(); +} + +void InputRedirection::registerPointerShortcut(Qt::KeyboardModifiers modifiers, Qt::MouseButton pointerButtons, QAction *action) +{ +#if KWIN_BUILD_GLOBALSHORTCUTS + m_shortcuts->registerPointerShortcut(action, modifiers, pointerButtons); +#endif +} + +void InputRedirection::registerAxisShortcut(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis, QAction *action) +{ +#if KWIN_BUILD_GLOBALSHORTCUTS + m_shortcuts->registerAxisShortcut(action, modifiers, axis); +#endif +} + +void InputRedirection::registerTouchpadSwipeShortcut(SwipeDirection direction, uint fingerCount, QAction *action, std::function cb) +{ +#if KWIN_BUILD_GLOBALSHORTCUTS + m_shortcuts->registerTouchpadSwipe(direction, fingerCount, action, cb); +#endif +} + +void InputRedirection::registerTouchpadPinchShortcut(PinchDirection direction, uint fingerCount, QAction *onUp, std::function progressCallback) +{ +#if KWIN_BUILD_GLOBALSHORTCUTS + m_shortcuts->registerTouchpadPinch(direction, fingerCount, onUp, progressCallback); +#endif +} + +void InputRedirection::registerTouchscreenSwipeShortcut(SwipeDirection direction, uint fingerCount, QAction *action, std::function progressCallback) +{ +#if KWIN_BUILD_GLOBALSHORTCUTS + m_shortcuts->registerTouchscreenSwipe(direction, fingerCount, action, progressCallback); +#endif +} + +void InputRedirection::forceRegisterTouchscreenSwipeShortcut(SwipeDirection direction, uint fingerCount, QAction *action, std::function progressCallback) +{ +#if KWIN_BUILD_GLOBALSHORTCUTS + m_shortcuts->forceRegisterTouchscreenSwipe(direction, fingerCount, action, progressCallback); +#endif +} + +void InputRedirection::warpPointer(const QPointF &pos) +{ + m_pointer->warp(pos); +} + +bool InputRedirection::supportsPointerWarping() const +{ + return m_pointer->supportsWarping(); +} + +QPointF InputRedirection::globalPointer() const +{ + return m_pointer->pos(); +} + +std::optional InputRedirection::implicitGrabPositionBySerial(SeatInterface *seat, uint32_t serial) const +{ + if (seat->hasImplicitPointerGrab(serial)) { + return m_pointer->pos(); + } + if (seat->hasImplicitTouchGrab(serial)) { + return m_touch->position(); + } + if (waylandServer()->tabletManagerV2()->seat(seat)->hasImplicitGrab(serial)) { + return m_tablet->position(); + } + + return std::nullopt; +} + +void InputRedirection::startInteractiveWindowSelection(std::function callback, const QByteArray &cursorName) +{ + if (!m_windowSelector || m_windowSelector->isActive()) { + callback(nullptr); + return; + } + m_windowSelector->start(callback); + m_pointer->setWindowSelectionCursor(cursorName); +} + +void InputRedirection::startInteractivePositionSelection(std::function callback) +{ + if (!m_windowSelector || m_windowSelector->isActive()) { + callback(QPoint(-1, -1)); + return; + } + m_windowSelector->start(callback); + m_pointer->setWindowSelectionCursor(QByteArray()); +} + +bool InputRedirection::isSelectingWindow() const +{ + return m_windowSelector ? m_windowSelector->isActive() : false; +} + +InputDeviceHandler::InputDeviceHandler(InputRedirection *input) + : QObject(input) +{ +} + +InputDeviceHandler::~InputDeviceHandler() = default; + +void InputDeviceHandler::init() +{ + connect(workspace(), &Workspace::stackingOrderChanged, this, &InputDeviceHandler::update); + connect(workspace(), &Workspace::windowMinimizedChanged, this, &InputDeviceHandler::update); + connect(VirtualDesktopManager::self(), &VirtualDesktopManager::currentChanged, this, &InputDeviceHandler::update); +} + +bool InputDeviceHandler::setHover(Window *window) +{ + if (m_hover.window == window) { + return false; + } + auto old = m_hover.window; + disconnect(m_hover.surfaceCreatedConnection); + m_hover.surfaceCreatedConnection = QMetaObject::Connection(); + + m_hover.window = window; + return true; +} + +void InputDeviceHandler::setFocus(Window *window) +{ + if (m_focus.window == window) { + return; + } + + Window *oldFocus = m_focus.window; + if (oldFocus) { + disconnect(oldFocus, &Window::closed, this, &InputDeviceHandler::update); + } + + m_focus.window = window; + if (window) { + connect(window, &Window::closed, this, &InputDeviceHandler::update); + } + + focusUpdate(oldFocus, window); +} + +void InputDeviceHandler::setDecoration(Decoration::DecoratedWindowImpl *decoration) +{ + if (m_focus.decoration != decoration) { + auto oldDeco = m_focus.decoration; + m_focus.decoration = decoration; + cleanupDecoration(oldDeco.data(), m_focus.decoration.data()); + Q_EMIT decorationChanged(); + } +} + +void InputDeviceHandler::updateFocus() +{ + Window *focus = m_hover.window; + + if (m_focus.decoration) { + focus = nullptr; + } else if (m_hover.window && !m_hover.window->surface() && !m_hover.window->isInternal()) { + // The surface has not yet been created (special XWayland case). + // Therefore listen for its creation. + if (!m_hover.surfaceCreatedConnection) { + m_hover.surfaceCreatedConnection = connect(m_hover.window, &Window::surfaceChanged, + this, &InputDeviceHandler::update); + } + focus = nullptr; + } + + setFocus(focus); +} + +void InputDeviceHandler::updateDecoration() +{ + Decoration::DecoratedWindowImpl *decoration = nullptr; + auto hover = m_hover.window.data(); + if (hover && hover->decoratedWindow()) { + if (!hover->clientGeometry().contains(position())) { + // input device above decoration + decoration = hover->decoratedWindow(); + } + } + + setDecoration(decoration); +} + +void InputDeviceHandler::update() +{ + if (!m_inited) { + return; + } + + Window *window = nullptr; + if (positionValid()) { + window = input()->findToplevel(position()); + } + // Always set the window at the position of the input device. + setHover(window); + + if (focusUpdatesBlocked()) { + workspace()->updateFocusMousePosition(position()); + return; + } + + updateDecoration(); + updateFocus(); + + workspace()->updateFocusMousePosition(position()); +} + +Window *InputDeviceHandler::hover() const +{ + return m_hover.window.data(); +} + +Window *InputDeviceHandler::focus() const +{ + return m_focus.window.data(); +} + +Decoration::DecoratedWindowImpl *InputDeviceHandler::decoration() const +{ + return m_focus.decoration; +} + +} // namespace + +#include "input.moc" + +#include "moc_input.cpp" diff --git a/local/recipes/kde/kwin/source/src/input.h b/local/recipes/kde/kwin/source/src/input.h new file mode 100644 index 0000000000..e6ce8a97ee --- /dev/null +++ b/local/recipes/kde/kwin/source/src/input.h @@ -0,0 +1,546 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + SPDX-FileCopyrightText: 2018 Roman Gilg + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "config-kwin.h" + +#include "core/inputdevice.h" +#include +#include +#include + +#include +#include +#include + +#include + +class KGlobalAccelInterface; +class QAction; +class QKeySequence; +class QMouseEvent; +class QKeyEvent; +class QWheelEvent; + +namespace KWin +{ +class IdleDetector; +class Window; +class GlobalShortcutsManager; +class InputEventFilter; +class InputEventSpy; +class KeyboardInputRedirection; +class PointerInputRedirection; +class SeatInterface; +class TabletInputRedirection; +class TouchInputRedirection; +class WindowSelectorFilter; +struct SwitchEvent; +struct TabletToolTipEvent; +struct TabletToolAxisEvent; +struct PointerAxisEvent; +struct PointerButtonEvent; +struct PointerMotionEvent; +struct PointerSwipeGestureBeginEvent; +struct PointerSwipeGestureUpdateEvent; +struct PointerSwipeGestureEndEvent; +struct PointerSwipeGestureCancelEvent; +struct PointerPinchGestureBeginEvent; +struct PointerPinchGestureUpdateEvent; +struct PointerPinchGestureEndEvent; +struct PointerPinchGestureCancelEvent; +struct PointerHoldGestureBeginEvent; +struct PointerHoldGestureEndEvent; +struct PointerHoldGestureCancelEvent; +struct KeyboardKeyEvent; +struct TabletToolProximityEvent; +struct TabletToolTipEvent; +struct TabletToolButtonEvent; +struct TabletPadButtonEvent; +struct TabletPadStripEvent; +struct TabletPadRingEvent; +struct TabletPadDialEvent; +struct TouchDownEvent; +struct TouchMotionEvent; +struct TouchUpEvent; + +namespace Decoration +{ +class DecoratedWindowImpl; +} + +class InputBackend; +class InputDevice; + +/** + * @brief This class is responsible for redirecting incoming input to the surface which currently + * has input or send enter/leave events. + * + * In addition input is intercepted before passed to the surfaces to have KWin internal areas + * getting input first (e.g. screen edges) and filter the input event out if we currently have + * a full input grab. + */ +class KWIN_EXPORT InputRedirection : public QObject +{ + Q_OBJECT +public: + ~InputRedirection() override; + void init(); + + /** + * @return const QPointF& The current global pointer position + */ + QPointF globalPointer() const; + Qt::MouseButtons qtButtonStates() const; + Qt::KeyboardModifiers keyboardModifiers() const; + Qt::KeyboardModifiers modifiersRelevantForGlobalShortcuts() const; + + void registerPointerShortcut(Qt::KeyboardModifiers modifiers, Qt::MouseButton pointerButtons, QAction *action); + void registerAxisShortcut(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis, QAction *action); + void registerTouchpadSwipeShortcut(SwipeDirection direction, uint32_t fingerCount, QAction *onUp, std::function progressCallback = {}); + void registerTouchpadPinchShortcut(PinchDirection direction, uint32_t fingerCount, QAction *onUp, std::function progressCallback = {}); + void registerTouchscreenSwipeShortcut(SwipeDirection direction, uint32_t fingerCount, QAction *action, std::function progressCallback = {}); + void forceRegisterTouchscreenSwipeShortcut(SwipeDirection direction, uint32_t fingerCount, QAction *action, std::function progressCallback = {}); + + bool supportsPointerWarping() const; + void warpPointer(const QPointF &pos); + + std::optional implicitGrabPositionBySerial(SeatInterface *seat, uint32_t serial) const; + + void installInputEventFilter(InputEventFilter *filter); + void uninstallInputEventFilter(InputEventFilter *filter); + + /** + * Installs the @p spy for spying on events. + */ + void installInputEventSpy(InputEventSpy *spy); + + /** + * Uninstalls the @p spy. This happens automatically when deleting an InputEventSpy. + */ + void uninstallInputEventSpy(InputEventSpy *spy); + + void simulateUserActivity(); + + void addIdleDetector(IdleDetector *detector); + void removeIdleDetector(IdleDetector *detector); + + QList idleInhibitors() const; + void addIdleInhibitor(Window *inhibitor); + void removeIdleInhibitor(Window *inhibitor); + + Window *findToplevel(const QPointF &pos); + GlobalShortcutsManager *shortcuts() const + { + return m_shortcuts; + } + + /** + * Sends an event through all InputFilters. + * The method is invoked on each input filter. Processing is stopped if + * a filter returns @c true for it + */ + void processFilters(auto method, const auto &...args) + { + for (const auto filter : std::as_const(m_filters)) { + if ((filter->*method)(args...)) { + return; + } + } + } + + /** + * Sends an event through all input event spies. + * The method is invoked on each InputEventSpy. + */ + void processSpies(auto method, const auto &...args) + { + for (const auto spy : std::as_const(m_spies)) { + (spy->*method)(args...); + } + } + + KeyboardInputRedirection *keyboard() const + { + return m_keyboard; + } + PointerInputRedirection *pointer() const + { + return m_pointer; + } + TabletInputRedirection *tablet() const + { + return m_tablet; + } + TouchInputRedirection *touch() const + { + return m_touch; + } + + /** + * Specifies which was the device that triggered the last input event + */ + void setLastInputHandler(QObject *device); + QObject *lastInputHandler() const; + + void setLastInteractionSerial(uint32_t serial); + uint32_t lastInteractionSerial() const; + + QList devices() const; + + bool hasAlphaNumericKeyboard(); + bool hasPointer() const; + bool hasTouch() const; + bool hasTabletModeSwitch(); + + void startInteractiveWindowSelection(std::function callback, const QByteArray &cursorName); + void startInteractivePositionSelection(std::function callback); + bool isSelectingWindow() const; + + void addInputDevice(InputDevice *device); + void removeInputDevice(InputDevice *device); + void addInputBackend(std::unique_ptr &&inputBackend); + +Q_SIGNALS: + void deviceAdded(InputDevice *device); + void deviceRemoved(InputDevice *device); + /** + * @brief Emitted when the global pointer position changed + * + * @param pos The new global pointer position. + */ + void globalPointerChanged(const QPointF &pos); + /** + * @brief Emitted when the state of a pointer button changed. + * + * @param button The button which changed + * @param state The new button state + */ + void pointerButtonStateChanged(uint32_t button, PointerButtonState state); + /** + * @brief Emitted when a pointer axis changed + * + * @param axis The axis on which the even occurred + * @param delta The delta of the event. + */ + void pointerAxisChanged(PointerAxis axis, qreal delta); + /** + * @brief Emitted when the modifiers changes. + * + * Only emitted for the mask which is provided by Qt::KeyboardModifiers, if other modifiers + * change signal is not emitted + * + * @param newMods The new modifiers state + * @param oldMods The previous modifiers state + */ + void keyboardModifiersChanged(Qt::KeyboardModifiers newMods, Qt::KeyboardModifiers oldMods); + /** + * @brief Emitted when the state of a key changed. + * + * @param keyCode The keycode of the key which changed + * @param state The new key state + */ + void keyStateChanged(quint32 keyCode, KeyboardKeyState state); + + void hasKeyboardChanged(bool set); + void hasAlphaNumericKeyboardChanged(bool set); + void hasPointerChanged(bool set); + void hasTouchChanged(bool set); + void hasTabletModeSwitchChanged(bool set); + +private Q_SLOTS: + void handleInputConfigChanged(const KConfigGroup &group); + void updateScreens(); + +private: + void setupInputBackends(); + void setupWorkspace(); + void setupInputFilters(); + void updateLeds(LEDs leds); + void updateAvailableInputDevices(); + KeyboardInputRedirection *m_keyboard; + PointerInputRedirection *m_pointer; + TabletInputRedirection *m_tablet; + TouchInputRedirection *m_touch; + QObject *m_lastInputDevice = nullptr; + uint32_t m_lastInteractionSerial = 0; + + GlobalShortcutsManager *m_shortcuts; + + std::vector> m_inputBackends; + QList m_inputDevices; + + QList m_idleDetectors; + QList m_idleInhibitors; + std::unique_ptr m_windowSelector; + + QList m_filters; + QList m_spies; + KConfigWatcher::Ptr m_inputConfigWatcher; + + std::unique_ptr m_virtualTerminalFilter; + std::unique_ptr m_dragAndDropFilter; +#if KWIN_BUILD_SCREENLOCKER + std::unique_ptr m_lockscreenFilter; +#endif + std::unique_ptr m_screenEdgeFilter; + std::unique_ptr m_tabboxFilter; + std::unique_ptr m_globalShortcutFilter; + std::unique_ptr m_effectsFilter; + std::unique_ptr m_interactiveMoveResizeFilter; + std::unique_ptr m_popupFilter; + std::unique_ptr m_decorationFilter; + std::unique_ptr m_windowActionFilter; + std::unique_ptr m_internalWindowFilter; + std::unique_ptr m_inputKeyboardFilter; + std::unique_ptr m_forwardFilter; + + std::unique_ptr m_hideCursorSpy; + std::unique_ptr m_userActivitySpy; + + LEDs m_leds; + bool m_hasKeyboard = false; + bool m_hasPointer = false; + bool m_hasTouch = false; + bool m_hasTabletModeSwitch = false; + + KWIN_SINGLETON(InputRedirection) + friend InputRedirection *input(); + friend class DecorationEventFilter; + friend class InternalWindowEventFilter; + friend class ForwardInputFilter; +}; + +namespace InputFilterOrder +{ +enum Order { + PlaceholderOutput, + Dpms, + ButtonRebind, + SlowKeys, + BounceKeys, + StickyKeys, + MouseKeys, + EisInput, + + VirtualTerminal, + LockScreen, + ScreenEdge, + DragAndDrop, + WindowSelector, + TabBox, + GlobalShortcut, + Effects, + InteractiveMoveResize, + Popup, + Decoration, + WindowAction, + XWayland, + InternalWindow, + InputMethod, + Forward, +}; +} + +/** + * Base class for filtering input events inside InputRedirection. + * + * The idea behind the InputEventFilter is to have task oriented + * filters. E.g. there is one filter taking care of a locked screen, + * one to take care of interacting with window decorations, etc. + * + * A concrete subclass can reimplement the virtual methods and decide + * whether an event should be filtered out or not by returning either + * @c true or @c false. E.g. the lock screen filter can easily ensure + * that all events are filtered out. + * + * As soon as a filter returns @c true the processing is stopped. If + * a filter returns @c false the next one is invoked. This means a filter + * installed early gets to see more events than a filter installed later on. + * + * Deleting an instance of InputEventFilter automatically uninstalls it from + * InputRedirection. + */ +class KWIN_EXPORT InputEventFilter +{ +public: + /** + * Construct and install the InputEventFilter + * @param weight The position in the input chain, lower values come first. + * @note the filter is not installed automatically + */ + InputEventFilter(InputFilterOrder::Order weight); + /** + * @brief ~InputEventFilter + * This will uninstall the event filter if needed + */ + virtual ~InputEventFilter(); + + /** + * The position in the input chain, lower values come first. + */ + int weight() const; + + virtual bool pointerMotion(PointerMotionEvent *event); + virtual bool pointerButton(PointerButtonEvent *event); + virtual bool pointerFrame(); + /** + * Event filter for pointer axis events. + * + * @param event The event information about the axis event + * @return @c true to stop further event processing, @c false to pass to next filter + */ + virtual bool pointerAxis(PointerAxisEvent *event); + /** + * Event filter for keyboard events. + * + * @param event The event information about the key event + * @return @c true to stop further event processing, @c false to pass to next filter. + */ + virtual bool keyboardKey(KeyboardKeyEvent *event); + virtual bool touchDown(TouchDownEvent *event); + virtual bool touchMotion(TouchMotionEvent *event); + virtual bool touchUp(TouchUpEvent *event); + virtual bool touchCancel(); + virtual bool touchFrame(); + + virtual bool pinchGestureBegin(PointerPinchGestureBeginEvent *event); + virtual bool pinchGestureUpdate(PointerPinchGestureUpdateEvent *event); + virtual bool pinchGestureEnd(PointerPinchGestureEndEvent *event); + virtual bool pinchGestureCancelled(PointerPinchGestureCancelEvent *event); + + virtual bool swipeGestureBegin(PointerSwipeGestureBeginEvent *event); + virtual bool swipeGestureUpdate(PointerSwipeGestureUpdateEvent *event); + virtual bool swipeGestureEnd(PointerSwipeGestureEndEvent *event); + virtual bool swipeGestureCancelled(PointerSwipeGestureCancelEvent *event); + + virtual bool holdGestureBegin(PointerHoldGestureBeginEvent *event); + virtual bool holdGestureEnd(PointerHoldGestureEndEvent *event); + virtual bool holdGestureCancelled(PointerHoldGestureCancelEvent *event); + + virtual bool switchEvent(SwitchEvent *event); + + virtual bool tabletToolProximityEvent(TabletToolProximityEvent *event); + virtual bool tabletToolAxisEvent(TabletToolAxisEvent *event); + virtual bool tabletToolTipEvent(TabletToolTipEvent *event); + virtual bool tabletToolButtonEvent(TabletToolButtonEvent *event); + virtual bool tabletPadButtonEvent(TabletPadButtonEvent *event); + virtual bool tabletPadStripEvent(TabletPadStripEvent *event); + virtual bool tabletPadRingEvent(TabletPadRingEvent *event); + virtual bool tabletPadDialEvent(TabletPadDialEvent *event); + +protected: + bool passToInputMethod(KeyboardKeyEvent *event); + +private: + int m_weight = 0; +}; + +class KWIN_EXPORT InputDeviceHandler : public QObject +{ + Q_OBJECT +public: + ~InputDeviceHandler() override; + virtual void init(); + + void update(); + + /** + * @brief First Window currently at the position of the input device + * according to the stacking order. + * @return Window* at device position. + * + * This will be null if no window is at the position + */ + Window *hover() const; + /** + * @brief Window currently having pointer input focus (this might + * be different from the Window at the position of the pointer). + * @return Window* with pointer focus. + * + * This will be null if no window has focus + */ + Window *focus() const; + + /** + * @brief The Decoration currently receiving events. + * @return decoration with pointer focus. + */ + Decoration::DecoratedWindowImpl *decoration() const; + + virtual QPointF position() const = 0; + + void setFocus(Window *window); + void setDecoration(Decoration::DecoratedWindowImpl *decoration); + +Q_SIGNALS: + void decorationChanged(); + +protected: + explicit InputDeviceHandler(InputRedirection *parent); + + virtual void cleanupDecoration(Decoration::DecoratedWindowImpl *old, Decoration::DecoratedWindowImpl *now) = 0; + + virtual void focusUpdate(Window *old, Window *now) = 0; + + /** + * Certain input devices can be in a state of having no valid + * position. An example are touch screens when no finger/pen + * is resting on the surface (no touch point). + */ + virtual bool positionValid() const + { + return true; + } + virtual bool focusUpdatesBlocked() + { + return false; + } + + inline bool inited() const + { + return m_inited; + } + inline void setInited(bool set) + { + m_inited = set; + } + +private: + bool setHover(Window *window); + void updateFocus(); + void updateDecoration(); + + struct + { + QPointer window; + QMetaObject::Connection surfaceCreatedConnection; + } m_hover; + + struct + { + QPointer window; + QPointer decoration; + } m_focus; + + bool m_inited = false; +}; + +inline InputRedirection *input() +{ + return InputRedirection::s_self; +} + +inline QList InputRedirection::devices() const +{ + return m_inputDevices; +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/input_event.cpp b/local/recipes/kde/kwin/source/src/input_event.cpp new file mode 100644 index 0000000000..81b891959e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/input_event.cpp @@ -0,0 +1,9 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "input_event.h" diff --git a/local/recipes/kde/kwin/source/src/input_event.h b/local/recipes/kde/kwin/source/src/input_event.h new file mode 100644 index 0000000000..a9c726c118 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/input_event.h @@ -0,0 +1,273 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "input.h" + +#include + +namespace KWin +{ + +class InputDevice; +class InputDeviceTabletTool; + +struct PointerMotionEvent +{ + InputDevice *device; + QPointF position; + QPointF delta; + QPointF deltaUnaccelerated; + bool warp; + Qt::MouseButtons buttons; + Qt::KeyboardModifiers modifiers; + Qt::KeyboardModifiers modifiersRelevantForShortcuts; + std::chrono::microseconds timestamp; +}; + +struct PointerButtonEvent +{ + InputDevice *device; + QPointF position; + PointerButtonState state; + Qt::MouseButton button; + quint32 nativeButton; + Qt::MouseButtons buttons; + Qt::KeyboardModifiers modifiers; + Qt::KeyboardModifiers modifiersRelevantForShortcuts; + std::chrono::microseconds timestamp; +}; + +struct PointerAxisEvent +{ + InputDevice *device; + QPointF position; + qreal delta; + qint32 deltaV120; + Qt::Orientation orientation; + PointerAxisSource source; + Qt::MouseButtons buttons; + Qt::KeyboardModifiers modifiers; + Qt::KeyboardModifiers modifiersRelevantForGlobalShortcuts; + bool inverted; + std::chrono::microseconds timestamp; +}; + +struct PointerSwipeGestureBeginEvent +{ + int fingerCount; + std::chrono::microseconds time; +}; + +struct PointerSwipeGestureUpdateEvent +{ + QPointF delta; + std::chrono::microseconds time; +}; + +struct PointerSwipeGestureEndEvent +{ + std::chrono::microseconds time; +}; + +struct PointerSwipeGestureCancelEvent +{ + std::chrono::microseconds time; +}; + +struct PointerPinchGestureBeginEvent +{ + int fingerCount; + std::chrono::microseconds time; +}; + +struct PointerPinchGestureUpdateEvent +{ + qreal scale; + qreal angleDelta; + QPointF delta; + std::chrono::microseconds time; +}; + +struct PointerPinchGestureEndEvent +{ + std::chrono::microseconds time; +}; + +struct PointerPinchGestureCancelEvent +{ + std::chrono::microseconds time; +}; + +struct PointerHoldGestureBeginEvent +{ + int fingerCount; + std::chrono::microseconds time; +}; + +struct PointerHoldGestureEndEvent +{ + std::chrono::microseconds time; +}; + +struct PointerHoldGestureCancelEvent +{ + std::chrono::microseconds time; +}; + +struct TouchDownEvent +{ + qint32 id; + QPointF pos; + std::chrono::microseconds time; +}; + +struct TouchMotionEvent +{ + qint32 id; + QPointF pos; + std::chrono::microseconds time; +}; + +struct TouchUpEvent +{ + qint32 id; + std::chrono::microseconds time; +}; + +struct KeyboardKeyEvent +{ + InputDevice *device; + KeyboardKeyState state; + Qt::Key key; + quint32 nativeScanCode; + quint32 nativeVirtualKey; + QString text; + Qt::KeyboardModifiers modifiers; + Qt::KeyboardModifiers modifiersRelevantForGlobalShortcuts; + std::chrono::microseconds timestamp; + uint32_t serial; +}; + +struct SwitchEvent +{ + InputDevice *device; + SwitchState state; + std::chrono::microseconds timestamp; +}; + +struct TabletToolProximityEvent +{ +public: + enum Type { + EnterProximity, + LeaveProximity, + }; + + Type type; + InputDevice *device; + qreal rotation; + QPointF position; + qreal sliderPosition; + qreal xTilt; + qreal yTilt; + qreal distance; + std::chrono::microseconds timestamp; + InputDeviceTabletTool *tool; +}; + +struct TabletToolTipEvent +{ +public: + enum Type { + Press, + Release, + }; + + Type type; + InputDevice *device; + qreal rotation; + QPointF position; + qreal pressure; + qreal sliderPosition; + qreal xTilt; + qreal yTilt; + qreal distance; + std::chrono::microseconds timestamp; + InputDeviceTabletTool *tool; +}; + +struct TabletToolAxisEvent +{ +public: + InputDevice *device; + qreal rotation; + QPointF position; + Qt::MouseButtons buttons; + qreal pressure; + qreal sliderPosition; + qreal xTilt; + qreal yTilt; + qreal distance; + std::chrono::microseconds timestamp; + InputDeviceTabletTool *tool; +}; + +struct TabletToolButtonEvent +{ + InputDevice *device; + uint button; + bool pressed; + InputDeviceTabletTool *tool; + std::chrono::microseconds time; +}; + +struct TabletPadButtonEvent +{ + InputDevice *device; + uint button; + bool pressed; + quint32 group; + quint32 mode; + bool isModeSwitch; + std::chrono::microseconds time; +}; + +struct TabletPadStripEvent +{ + InputDevice *device; + int number; + qreal position; + bool isFinger; + quint32 group; + quint32 mode; + std::chrono::microseconds time; +}; + +struct TabletPadRingEvent +{ + InputDevice *device; + int number; + qreal position; + bool isFinger; + quint32 group; + quint32 mode; + std::chrono::microseconds time; +}; + +struct TabletPadDialEvent +{ + InputDevice *device; + int number; + double delta; + quint32 group; + std::chrono::microseconds time; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/input_event_spy.cpp b/local/recipes/kde/kwin/source/src/input_event_spy.cpp new file mode 100644 index 0000000000..2098ac5c1c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/input_event_spy.cpp @@ -0,0 +1,133 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "input_event_spy.h" +#include "input.h" + +#include + +namespace KWin +{ + +InputEventSpy::InputEventSpy() = default; + +InputEventSpy::~InputEventSpy() +{ + if (input()) { + input()->uninstallInputEventSpy(this); + } +} + +void InputEventSpy::pointerMotion(PointerMotionEvent *event) +{ +} + +void InputEventSpy::pointerButton(PointerButtonEvent *event) +{ +} + +void InputEventSpy::pointerAxis(PointerAxisEvent *event) +{ +} + +void InputEventSpy::keyboardKey(KeyboardKeyEvent *event) +{ +} + +void InputEventSpy::touchDown(TouchDownEvent *event) +{ +} + +void InputEventSpy::touchMotion(TouchMotionEvent *event) +{ +} + +void InputEventSpy::touchUp(TouchUpEvent *event) +{ +} + +void InputEventSpy::pinchGestureBegin(PointerPinchGestureBeginEvent *event) +{ +} + +void InputEventSpy::pinchGestureUpdate(PointerPinchGestureUpdateEvent *event) +{ +} + +void InputEventSpy::pinchGestureEnd(PointerPinchGestureEndEvent *event) +{ +} + +void InputEventSpy::pinchGestureCancelled(PointerPinchGestureCancelEvent *event) +{ +} + +void InputEventSpy::swipeGestureBegin(PointerSwipeGestureBeginEvent *event) +{ +} + +void InputEventSpy::swipeGestureUpdate(PointerSwipeGestureUpdateEvent *event) +{ +} + +void InputEventSpy::swipeGestureEnd(PointerSwipeGestureEndEvent *event) +{ +} + +void InputEventSpy::swipeGestureCancelled(PointerSwipeGestureCancelEvent *event) +{ +} + +void InputEventSpy::holdGestureBegin(PointerHoldGestureBeginEvent *event) +{ +} + +void InputEventSpy::holdGestureEnd(PointerHoldGestureEndEvent *event) +{ +} + +void InputEventSpy::holdGestureCancelled(PointerHoldGestureCancelEvent *event) +{ +} + +void InputEventSpy::switchEvent(SwitchEvent *event) +{ +} + +void InputEventSpy::tabletToolProximityEvent(TabletToolProximityEvent *event) +{ +} + +void InputEventSpy::tabletToolAxisEvent(TabletToolAxisEvent *event) +{ +} + +void InputEventSpy::tabletToolTipEvent(TabletToolTipEvent *event) +{ +} + +void InputEventSpy::tabletToolButtonEvent(TabletToolButtonEvent *event) +{ +} + +void InputEventSpy::tabletPadButtonEvent(TabletPadButtonEvent *event) +{ +} + +void InputEventSpy::tabletPadStripEvent(TabletPadStripEvent *event) +{ +} + +void InputEventSpy::tabletPadRingEvent(TabletPadRingEvent *event) +{ +} + +void InputEventSpy::tabletPadDialEvent(TabletPadDialEvent *event) +{ +} +} diff --git a/local/recipes/kde/kwin/source/src/input_event_spy.h b/local/recipes/kde/kwin/source/src/input_event_spy.h new file mode 100644 index 0000000000..1ec7fb269b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/input_event_spy.h @@ -0,0 +1,108 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include + +#include +#include + +class QPointF; + +namespace KWin +{ +struct KeyboardKeyEvent; +struct PointerAxisEvent; +struct PointerButtonEvent; +struct PointerMotionEvent; +struct PointerSwipeGestureBeginEvent; +struct PointerSwipeGestureUpdateEvent; +struct PointerSwipeGestureEndEvent; +struct PointerSwipeGestureCancelEvent; +struct PointerPinchGestureBeginEvent; +struct PointerPinchGestureUpdateEvent; +struct PointerPinchGestureEndEvent; +struct PointerPinchGestureCancelEvent; +struct PointerHoldGestureBeginEvent; +struct PointerHoldGestureEndEvent; +struct PointerHoldGestureCancelEvent; +struct SwitchEvent; +struct TabletToolProximityEvent; +struct TabletToolAxisEvent; +struct TabletToolTipEvent; +struct TabletToolButtonEvent; +struct TabletPadButtonEvent; +struct TabletPadStripEvent; +struct TabletPadRingEvent; +struct TabletPadDialEvent; +struct TouchDownEvent; +struct TouchMotionEvent; +struct TouchUpEvent; + +/** + * Base class for spying on input events inside InputRedirection. + * + * This class is quite similar to InputEventFilter, except that it does not + * support event filtering. Each InputEventSpy gets to see all input events, + * the processing happens prior to sending events through the InputEventFilters. + * + * Deleting an instance of InputEventSpy automatically uninstalls it from + * InputRedirection. + */ +class KWIN_EXPORT InputEventSpy +{ +public: + InputEventSpy(); + virtual ~InputEventSpy(); + + virtual void pointerMotion(PointerMotionEvent *event); + virtual void pointerButton(PointerButtonEvent *event); + /** + * Event spy for pointer axis events. + * + * @param event The event information about the axis event + */ + virtual void pointerAxis(PointerAxisEvent *event); + /** + * Event spy for keyboard events. + * + * @param event The event information about the key event + */ + virtual void keyboardKey(KeyboardKeyEvent *event); + + virtual void touchDown(TouchDownEvent *event); + virtual void touchMotion(TouchMotionEvent *event); + virtual void touchUp(TouchUpEvent *event); + + virtual void pinchGestureBegin(PointerPinchGestureBeginEvent *event); + virtual void pinchGestureUpdate(PointerPinchGestureUpdateEvent *event); + virtual void pinchGestureEnd(PointerPinchGestureEndEvent *event); + virtual void pinchGestureCancelled(PointerPinchGestureCancelEvent *event); + + virtual void swipeGestureBegin(PointerSwipeGestureBeginEvent *event); + virtual void swipeGestureUpdate(PointerSwipeGestureUpdateEvent *event); + virtual void swipeGestureEnd(PointerSwipeGestureEndEvent *event); + virtual void swipeGestureCancelled(PointerSwipeGestureCancelEvent *event); + + virtual void holdGestureBegin(PointerHoldGestureBeginEvent *event); + virtual void holdGestureEnd(PointerHoldGestureEndEvent *event); + virtual void holdGestureCancelled(PointerHoldGestureCancelEvent *event); + + virtual void switchEvent(SwitchEvent *event); + + virtual void tabletToolProximityEvent(TabletToolProximityEvent *event); + virtual void tabletToolAxisEvent(TabletToolAxisEvent *event); + virtual void tabletToolTipEvent(TabletToolTipEvent *event); + virtual void tabletToolButtonEvent(TabletToolButtonEvent *event); + virtual void tabletPadButtonEvent(TabletPadButtonEvent *event); + virtual void tabletPadStripEvent(TabletPadStripEvent *event); + virtual void tabletPadRingEvent(TabletPadRingEvent *event); + virtual void tabletPadDialEvent(TabletPadDialEvent *event); +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/inputmethod.cpp b/local/recipes/kde/kwin/source/src/inputmethod.cpp new file mode 100644 index 0000000000..4ce54592cb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/inputmethod.cpp @@ -0,0 +1,1109 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "inputmethod.h" + +#include "config-kwin.h" + +#include "effect/effecthandler.h" +#include "input.h" +#include "input_event.h" +#include "inputpanelv1window.h" +#include "keyboard_input.h" +#include "utils/common.h" +#include "virtualkeyboard_dbus.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" +#if KWIN_BUILD_SCREENLOCKER +#include +#endif + +#include "internalinputmethodcontext.h" +#include "pointer_input.h" +#include "tablet_input.h" +#include "touch_input.h" +#include "wayland/display.h" +#include "wayland/inputmethod_v1.h" +#include "wayland/keyboard.h" +#include "wayland/seat.h" +#include "wayland/surface.h" +#include "wayland/textinput_v1.h" +#include "wayland/textinput_v3.h" +#include "xkb.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace KWin +{ + +static QList textToKey(const QString &inputString) +{ + QList result; + + for (int i = 0; i < inputString.size(); ++i) { + char32_t cp = inputString[i].unicode(); + + // Handle surrogate pair (two QChars → one codepoint) + if (inputString[i].isHighSurrogate() && i + 1 < inputString.size() + && inputString[i + 1].isLowSurrogate()) { + cp = QChar::surrogateToUcs4(inputString[i].unicode(), + inputString[i + 1].unicode()); + i++; // skip the low surrogate + } + + xkb_keysym_t ks = xkb_utf32_to_keysym(cp); + if (ks != XKB_KEY_NoSymbol) { + result.append(ks); + } else { + qCWarning(KWIN_VIRTUALKEYBOARD) << "No keysym for U+" << Qt::hex << (int)cp << "\n"; + } + } + return result; +} + +InputMethod::InputMethod() +{ + m_internalContext = new InternalInputMethodContext(this); + + m_enabled = kwinApp()->config()->group(QStringLiteral("Wayland")).readEntry("VirtualKeyboardEnabled", true); + // this is actually too late. Other processes are started before init, + // so might miss the availability of text input + // but without Workspace we don't have the window listed at all + if (workspace()) { + init(); + } else { + connect(kwinApp(), &Application::workspaceCreated, this, &InputMethod::init); + } +} + +InputMethod::~InputMethod() +{ + stopInputMethod(); +} + +void InputMethod::init() +{ + // Stop restarting the input method if it starts crashing very frequently + m_inputMethodCrashTimer.setInterval(20000); + m_inputMethodCrashTimer.setSingleShot(true); + connect(&m_inputMethodCrashTimer, &QTimer::timeout, this, [this] { + m_inputMethodCrashes = 0; + }); +#if KWIN_BUILD_SCREENLOCKER + connect(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::aboutToLock, this, &InputMethod::hide); +#endif + + new VirtualKeyboardDBus(this); + qCDebug(KWIN_VIRTUALKEYBOARD) << "Registering the DBus interface"; + + new TextInputManagerV1Interface(waylandServer()->display(), this); + new TextInputManagerV2Interface(waylandServer()->display(), this); + new TextInputManagerV3Interface(waylandServer()->display(), this); + + connect(waylandServer()->seat(), &SeatInterface::focusedKeyboardSurfaceAboutToChange, this, &InputMethod::commitPendingText); + connect(waylandServer()->seat(), &SeatInterface::focusedTextInputSurfaceChanged, this, &InputMethod::handleFocusedSurfaceChanged); + + TextInputV1Interface *textInputV1 = waylandServer()->seat()->textInputV1(); + connect(textInputV1, &TextInputV1Interface::requestShowInputPanel, this, &InputMethod::show); + connect(textInputV1, &TextInputV1Interface::requestHideInputPanel, this, &InputMethod::hide); + connect(textInputV1, &TextInputV1Interface::surroundingTextChanged, this, &InputMethod::surroundingTextChanged); + connect(textInputV1, &TextInputV1Interface::contentTypeChanged, this, &InputMethod::contentTypeChanged); + connect(textInputV1, &TextInputV1Interface::stateUpdated, this, &InputMethod::textInputInterfaceV1StateUpdated); + connect(textInputV1, &TextInputV1Interface::reset, this, &InputMethod::textInputInterfaceV1Reset); + connect(textInputV1, &TextInputV1Interface::invokeAction, this, &InputMethod::invokeAction); + connect(textInputV1, &TextInputV1Interface::enabledChanged, this, &InputMethod::textInputInterfaceV1EnabledChanged); + connect(textInputV1, &TextInputV1Interface::cursorRectangleChanged, this, &InputMethod::cursorRectangleChanged); + + TextInputV2Interface *textInputV2 = waylandServer()->seat()->textInputV2(); + connect(textInputV2, &TextInputV2Interface::requestShowInputPanel, this, &InputMethod::show); + connect(textInputV2, &TextInputV2Interface::requestHideInputPanel, this, &InputMethod::hide); + connect(textInputV2, &TextInputV2Interface::surroundingTextChanged, this, &InputMethod::surroundingTextChanged); + connect(textInputV2, &TextInputV2Interface::contentTypeChanged, this, &InputMethod::contentTypeChanged); + connect(textInputV2, &TextInputV2Interface::stateUpdated, this, &InputMethod::textInputInterfaceV2StateUpdated); + connect(textInputV2, &TextInputV2Interface::enabledChanged, this, &InputMethod::textInputInterfaceV2EnabledChanged); + connect(textInputV2, &TextInputV2Interface::cursorRectangleChanged, this, &InputMethod::cursorRectangleChanged); + + TextInputV3Interface *textInputV3 = waylandServer()->seat()->textInputV3(); + connect(textInputV3, &TextInputV3Interface::surroundingTextChanged, this, &InputMethod::surroundingTextChanged); + connect(textInputV3, &TextInputV3Interface::contentTypeChanged, this, &InputMethod::contentTypeChanged); + connect(textInputV3, &TextInputV3Interface::stateCommitted, this, &InputMethod::stateCommitted); + connect(textInputV3, &TextInputV3Interface::enabledChanged, this, &InputMethod::textInputInterfaceV3EnabledChanged); + connect(textInputV3, &TextInputV3Interface::enableRequested, this, &InputMethod::textInputInterfaceV3EnableRequested); + connect(textInputV3, &TextInputV3Interface::cursorRectangleChanged, this, &InputMethod::cursorRectangleChanged); + + connect(m_internalContext, &InternalInputMethodContext::surroundingTextChanged, this, &InputMethod::surroundingTextChanged); + connect(m_internalContext, &InternalInputMethodContext::contentTypeChanged, this, &InputMethod::contentTypeChanged); + connect(m_internalContext, &InternalInputMethodContext::enabledChanged, this, &InputMethod::refreshActive); + connect(m_internalContext, &InternalInputMethodContext::showInputPanelRequested, this, &InputMethod::show); + connect(m_internalContext, &InternalInputMethodContext::hideInputPanelRequested, this, &InputMethod::hide); + + connect(input()->keyboard()->xkb(), &Xkb::modifierStateChanged, this, [this]() { + m_hasPendingModifiers = true; + }); +} + +void InputMethod::show() +{ + m_shouldShowPanel = true; + + // If the panel has something to display, show it (e.g. if we hid it from kwin rather than the IM hiding itself) + // Otherwise, ensure the input context is current and the IM will see to having itself shown if there's something to show. + // If there's no context available, then nothing will be shown + if (m_panel && !m_panel->wasUnmapped()) { + m_panel->show(); + updateInputPanelState(); + } else { + // making the input context current will trigger the IM + // to show the panel (if there is something to show) + if (!isActive()) { + refreshActive(); + } + + // refreshActive affects the result of isActive + if (isActive()) { + adoptInputMethodContext(); + } + } +} + +void InputMethod::hide() +{ + m_shouldShowPanel = false; + if (m_panel) { + m_panel->hide(); + updateInputPanelState(); + } +} + +bool InputMethod::shouldShowOnActive() const +{ + static bool alwaysShowIm = qEnvironmentVariableIntValue("KWIN_IM_SHOW_ALWAYS") != 0; + return alwaysShowIm || input()->touch() == input()->lastInputHandler() + || input()->tablet() == input()->lastInputHandler(); +} + +void InputMethod::refreshActive() +{ + auto seat = waylandServer()->seat(); + auto t1 = seat->textInputV1(); + auto t2 = seat->textInputV2(); + auto t3 = seat->textInputV3(); + + bool active = false; + if (auto focusedSurface = seat->focusedTextInputSurface()) { + auto client = focusedSurface->client(); + if ((t1->clientSupportsTextInput(client) && t1->isEnabled()) || (t2->clientSupportsTextInput(client) && t2->isEnabled()) || (t3->clientSupportsTextInput(client) && t3->isEnabled())) { + active = true; + } + } + if (m_internalContext->isEnabled()) { + active = true; + } + setActive(active); +} + +void InputMethod::forwardKeyToEffects(KWin::KeyboardKeyState state, int keyCode, int keySym) +{ + if (!input()->keyboard()) { + return; + } + auto xkb = input()->keyboard()->xkb(); + + QKeyEvent event(state == KWin::KeyboardKeyState::Released ? QEvent::KeyRelease : QEvent::KeyPress, + xkb->toQtKey(keySym, keyCode, Qt::KeyboardModifiers()), + xkb->modifiers(), + keyCode, + keySym, + 0, + xkb->toString(keySym)); + waylandServer()->seat()->setFocusedKeyboardSurface(nullptr); + effects->grabbedKeyboardEvent(&event); +} + +void InputMethod::commitPendingText() +{ + if (!m_pendingText.isEmpty()) { + commitString(m_serial++, m_pendingText); + m_pendingText = QString(); + auto imContext = waylandServer()->inputMethod()->context(); + if (imContext) { + imContext->sendReset(); + } + } +} + +RectF InputMethod::cursorRectangle() const +{ + RectF localCursorRect; + auto t1 = waylandServer()->seat()->textInputV1(); + auto t2 = waylandServer()->seat()->textInputV2(); + auto t3 = waylandServer()->seat()->textInputV3(); + auto inputContext = waylandServer()->inputMethod()->context(); + if (!inputContext) { + return {}; + } + if (t1 && t1->isEnabled()) { + localCursorRect = t1->cursorRectangle(); + } + if (t2 && t2->isEnabled()) { + localCursorRect = t2->cursorRectangle(); + } + if (t3 && t3->isEnabled()) { + localCursorRect = t3->cursorRectangle(); + } + + if (auto textWindow = waylandServer()->findWindow(waylandServer()->seat()->focusedTextInputSurface())) { + return localCursorRect.translated(textWindow->bufferGeometry().topLeft()); + } + return {}; +} + +void InputMethod::setActive(bool active) +{ + const bool wasActive = waylandServer()->inputMethod()->context(); + if (wasActive && !active) { + waylandServer()->inputMethod()->sendDeactivate(); + } + + if (active) { + if (!m_enabled) { + return; + } + + if (!wasActive) { + waylandServer()->inputMethod()->sendActivate(); + } + adoptInputMethodContext(); + } else { + updateInputPanelState(); + } + + if (wasActive != isActive()) { + Q_EMIT activeChanged(active); + } +} + +InputPanelV1Window *InputMethod::panel() const +{ + return m_panel; +} + +void InputMethod::setPanel(InputPanelV1Window *panel) +{ + Q_ASSERT(panel->isInputMethod()); + if (m_panel) { + qCDebug(KWIN_VIRTUALKEYBOARD) << "Replacing input panel" << m_panel << "with" << panel; + m_panel->destroyWindow(); + } + + m_panel = panel; + connect(m_panel, &Window::closed, this, [this]() { + m_panel.clear(); + updateInputPanelState(); + Q_EMIT visibleChanged(); + }); + connect(m_panel, &Window::frameGeometryChanged, this, &InputMethod::updateInputPanelState); + connect(m_panel, &Window::hiddenChanged, this, &InputMethod::updateInputPanelState); + connect(m_panel, &Window::hiddenChanged, this, &InputMethod::visibleChanged); + connect(m_panel, &Window::readyForPaintingChanged, this, &InputMethod::visibleChanged); + Q_EMIT visibleChanged(); + updateInputPanelState(); + Q_EMIT panelChanged(); + + if (m_shouldShowPanel) { + show(); + } +} + +void InputMethod::setTrackedWindow(Window *trackedWindow) +{ + // Reset the old window virtual keyboard geom if necessary + // Old and new windows could be the same if focus moves between subsurfaces + if (m_trackedWindow == trackedWindow) { + return; + } + if (m_trackedWindow) { + m_trackedWindow->setVirtualKeyboardGeometry(Rect()); + disconnect(m_trackedWindow, &Window::frameGeometryChanged, this, &InputMethod::updateInputPanelState); + disconnect(m_trackedWindow, &Window::frameGeometryChanged, this, &InputMethod::cursorRectangleChanged); + } + m_trackedWindow = trackedWindow; + m_shouldShowPanel = false; + if (m_trackedWindow) { + connect(m_trackedWindow, &Window::frameGeometryChanged, this, &InputMethod::updateInputPanelState, Qt::QueuedConnection); + connect(m_trackedWindow, &Window::frameGeometryChanged, this, &InputMethod::cursorRectangleChanged); + } + updateInputPanelState(); + Q_EMIT activeWindowChanged(); +} + +void InputMethod::handleFocusedSurfaceChanged() +{ + resetPendingPreedit(); + m_pendingText = QString(); + + auto seat = waylandServer()->seat(); + SurfaceInterface *focusedSurface = seat->focusedTextInputSurface(); + + setTrackedWindow(waylandServer()->findWindow(focusedSurface)); + + const auto client = focusedSurface ? focusedSurface->client() : nullptr; + bool ret = seat->textInputV1()->clientSupportsTextInput(client) + || seat->textInputV2()->clientSupportsTextInput(client) + || seat->textInputV3()->clientSupportsTextInput(client) + || m_internalContext->isEnabled(); + + if (ret != m_activeClientSupportsTextInput) { + m_activeClientSupportsTextInput = ret; + Q_EMIT activeClientSupportsTextInputChanged(); + } + + refreshActive(); +} + +void InputMethod::surroundingTextChanged() +{ + auto t2 = waylandServer()->seat()->textInputV2(); + auto t3 = waylandServer()->seat()->textInputV3(); + auto inputContext = waylandServer()->inputMethod()->context(); + if (!inputContext) { + return; + } + if (t2 && t2->isEnabled()) { + inputContext->sendSurroundingText(t2->surroundingText(), t2->surroundingTextCursorPosition(), t2->surroundingTextSelectionAnchor()); + return; + } + if (t3 && t3->isEnabled()) { + inputContext->sendSurroundingText(t3->surroundingText(), t3->surroundingTextCursorPosition(), t3->surroundingTextSelectionAnchor()); + return; + } + if (m_internalContext->isEnabled()) { + inputContext->sendSurroundingText(m_internalContext->surroundingText(), m_internalContext->cursorPosition(), m_internalContext->anchorPosition()); + return; + } +} + +void InputMethod::contentTypeChanged() +{ + auto t1 = waylandServer()->seat()->textInputV1(); + auto t2 = waylandServer()->seat()->textInputV2(); + auto t3 = waylandServer()->seat()->textInputV3(); + auto inputContext = waylandServer()->inputMethod()->context(); + if (!inputContext) { + return; + } + if (t1 && t1->isEnabled()) { + inputContext->sendContentType(t1->contentHints(), t1->contentPurpose()); + } + if (t2 && t2->isEnabled()) { + inputContext->sendContentType(t2->contentHints(), t2->contentPurpose()); + } + if (t3 && t3->isEnabled()) { + inputContext->sendContentType(t3->contentHints(), t3->contentPurpose()); + } +} + +void InputMethod::textInputInterfaceV1Reset() +{ + if (!m_enabled) { + return; + } + auto t1 = waylandServer()->seat()->textInputV1(); + auto inputContext = waylandServer()->inputMethod()->context(); + if (!inputContext) { + return; + } + if (!t1 || !t1->isEnabled()) { + return; + } + inputContext->sendReset(); +} + +void InputMethod::invokeAction(quint32 button, quint32 index) +{ + if (!m_enabled) { + return; + } + auto t1 = waylandServer()->seat()->textInputV1(); + auto inputContext = waylandServer()->inputMethod()->context(); + if (!inputContext) { + return; + } + if (!t1 || !t1->isEnabled()) { + return; + } + inputContext->sendInvokeAction(button, index); +} + +void InputMethod::textInputInterfaceV1StateUpdated(quint32 serial) +{ + if (!m_enabled) { + return; + } + auto t1 = waylandServer()->seat()->textInputV1(); + auto inputContext = waylandServer()->inputMethod()->context(); + if (!inputContext) { + return; + } + if (!t1 || !t1->isEnabled()) { + return; + } + inputContext->sendCommitState(serial); +} + +void InputMethod::textInputInterfaceV2StateUpdated(quint32 serial, TextInputV2Interface::UpdateReason reason) +{ + if (!m_enabled) { + return; + } + + auto t2 = waylandServer()->seat()->textInputV2(); + auto inputContext = waylandServer()->inputMethod()->context(); + if (!inputContext) { + return; + } + if (!t2 || !t2->isEnabled()) { + return; + } + if (m_panel && shouldShowOnActive()) { + m_panel->allow(); + } + switch (reason) { + case TextInputV2Interface::UpdateReason::StateChange: + break; + case TextInputV2Interface::UpdateReason::StateEnter: + case TextInputV2Interface::UpdateReason::StateFull: + adoptInputMethodContext(); + break; + case TextInputV2Interface::UpdateReason::StateReset: + inputContext->sendReset(); + break; + } +} + +void InputMethod::textInputInterfaceV1EnabledChanged() +{ + if (!m_enabled) { + return; + } + + refreshActive(); +} + +void InputMethod::textInputInterfaceV2EnabledChanged() +{ + if (!m_enabled) { + return; + } + + refreshActive(); +} + +void InputMethod::textInputInterfaceV3EnabledChanged() +{ + if (!m_enabled) { + return; + } + + auto t3 = waylandServer()->seat()->textInputV3(); + refreshActive(); + if (t3->isEnabled()) { + show(); + } else { + // reset value of preedit when textinput is disabled + resetPendingPreedit(); + m_pendingText = QString(); + } + auto context = waylandServer()->inputMethod()->context(); + if (context) { + context->sendReset(); + adoptInputMethodContext(); + } +} + +void InputMethod::stateCommitted(uint32_t serial) +{ + if (!isEnabled()) { + return; + } + TextInputV3Interface *textInputV3 = waylandServer()->seat()->textInputV3(); + if (!textInputV3) { + return; + } + + if (auto inputContext = waylandServer()->inputMethod()->context()) { + inputContext->sendCommitState(serial); + } +} + +void InputMethod::setEnabled(bool enabled) +{ + if (m_enabled == enabled) { + return; + } + m_enabled = enabled; + Q_EMIT enabledChanged(m_enabled); + + // send OSD message + QDBusMessage msg = QDBusMessage::createMethodCall( + QStringLiteral("org.kde.plasmashell"), + QStringLiteral("/org/kde/osdService"), + QStringLiteral("org.kde.osdService"), + QStringLiteral("virtualKeyboardEnabledChanged")); + msg.setArguments({enabled}); + QDBusConnection::sessionBus().asyncCall(msg); + if (!m_enabled) { + hide(); + stopInputMethod(); + } else { + startInputMethod(); + } + // save value into config + kwinApp()->config()->group(QStringLiteral("Wayland")).writeEntry("VirtualKeyboardEnabled", m_enabled); + kwinApp()->config()->sync(); +} + +void InputMethod::keysymReceived(quint32 serial, quint32 time, quint32 sym, KeyboardKeyState state, quint32 modifiers) +{ + if (auto t1 = waylandServer()->seat()->textInputV1(); t1 && t1->isEnabled()) { + if (state != KeyboardKeyState::Released) { + t1->keysymPressed(time, sym, modifiers); + } else { + t1->keysymReleased(time, sym, modifiers); + } + return; + } + + auto t2 = waylandServer()->seat()->textInputV2(); + if (t2 && t2->isEnabled()) { + if (state != KeyboardKeyState::Released) { + t2->keysymPressed(sym, modifiers); + } else { + t2->keysymReleased(sym, modifiers); + } + return; + } + + if (effects && effects->hasKeyboardGrab()) { + std::optional keyCode = input()->keyboard()->xkb()->keycodeFromKeysym(sym); + forwardKeyToEffects(state, keyCode.value_or(Xkb::KeyCode{}).keyCode, sym); + return; + } + + if (state == KeyboardKeyState::Pressed) { + forwardKeySym(sym); + // reset any modifiers to the actual state + input()->keyboard()->xkb()->forwardModifiers(); + } +} + +void InputMethod::commitString(qint32 serial, const QString &text) +{ + if (auto t1 = waylandServer()->seat()->textInputV1(); t1 && t1->isEnabled()) { + t1->commitString(text); + t1->setPreEditCursor(0); + t1->preEdit({}, {}); + return; + } + if (auto t2 = waylandServer()->seat()->textInputV2(); t2 && t2->isEnabled()) { + t2->commitString(text); + t2->setPreEditCursor(0); + t2->preEdit({}, {}); + return; + } else if (auto t3 = waylandServer()->seat()->textInputV3(); t3 && t3->isEnabled()) { + t3->sendPreEditString(QString(), 0, 0); + t3->commitString(text); + t3->done(); + return; + } else if (m_internalContext->isEnabled()) { + m_internalContext->handlePreeditText({}, 0, 0); + m_internalContext->handleCommitString(text); + } else { + // The application has no way of communicating with the input method. + // So instead, try to convert what we get from the input method into + // keycodes and send those as fake input to the client. + + const QList keySyms = textToKey(text); + + // First, send all the extracted keys as pressed keys to the client. + for (const xkb_keysym_t &keySym : keySyms) { + forwardKeySym(keySym); + } + + // reset any modifiers to the actual state + input()->keyboard()->xkb()->forwardModifiers(); + } +} + +void InputMethod::forwardKeySym(int keySym) +{ + std::optional keyCode = input()->keyboard()->xkb()->keycodeFromKeysym(keySym); + if (!keyCode) { + qCWarning(KWIN_VIRTUALKEYBOARD) << "Could not map keysym " << keySym << "to keycode. Trying custom keymap"; + static const uint unmappedKeyCode = 247; + auto temporaryKeymap = input()->keyboard()->xkb()->keymapContentsForKeysym(unmappedKeyCode, keySym); + if (temporaryKeymap.isEmpty()) { + return; + } + waylandServer()->seat()->keyboard()->setKeymap(temporaryKeymap); + waylandServer()->seat()->notifyKeyboardKey(unmappedKeyCode, KeyboardKeyState::Pressed, waylandServer()->display()->nextSerial()); + waylandServer()->seat()->notifyKeyboardKey(unmappedKeyCode, KeyboardKeyState::Released, waylandServer()->display()->nextSerial()); + waylandServer()->seat()->keyboard()->setKeymap(input()->keyboard()->xkb()->keymapContents()); + } else { + waylandServer()->seat()->notifyKeyboardModifiers(keyCode->modifiers , 0, 0, input()->keyboard()->xkb()->currentLayout()); + waylandServer()->seat()->notifyKeyboardKey(keyCode->keyCode, KeyboardKeyState::Pressed, waylandServer()->display()->nextSerial()); + waylandServer()->seat()->notifyKeyboardKey(keyCode->keyCode, KeyboardKeyState::Released, waylandServer()->display()->nextSerial()); + } +} + +void InputMethod::deleteSurroundingText(int32_t index, uint32_t length) +{ + // zwp_input_method_v1 Delete surrounding text interface is designed for text-input-v1. + // The parameter has different meaning in text-input-v{2,3}. + // Current cursor is at index 0. + // The actually deleted text range is [index, index + length]. + // In v{2,3}'s before/after style, text to be deleted with v{2,3} interface is [-before, after]. + // And before/after are all unsigned, which make it impossible to do certain things. + // Those request will be ignored. + + // Verify we can handle such request. + if (index > 0 || index + static_cast(length) < 0) { + return; + } + const quint32 before = -index; + const quint32 after = index + length; + + auto t1 = waylandServer()->seat()->textInputV1(); + if (t1 && t1->isEnabled()) { + t1->deleteSurroundingText(index, length); + } + auto t2 = waylandServer()->seat()->textInputV2(); + if (t2 && t2->isEnabled()) { + t2->deleteSurroundingText(before, after); + } + auto t3 = waylandServer()->seat()->textInputV3(); + if (t3 && t3->isEnabled()) { + t3->deleteSurroundingText(before, after); + t3->done(); + } + if (internalContext()->isEnabled()) { + internalContext()->handleDeleteSurroundingText(before, after); + } +} + +void InputMethod::setCursorPosition(qint32 index, qint32 anchor) +{ + auto t1 = waylandServer()->seat()->textInputV1(); + if (t1 && t1->isEnabled()) { + t1->setCursorPosition(index, anchor); + } + auto t2 = waylandServer()->seat()->textInputV2(); + if (t2 && t2->isEnabled()) { + t2->setCursorPosition(index, anchor); + } +} + +void InputMethod::setLanguage(uint32_t serial, const QString &language) +{ + auto t1 = waylandServer()->seat()->textInputV1(); + if (t1 && t1->isEnabled()) { + t1->setLanguage(language); + } + auto t2 = waylandServer()->seat()->textInputV2(); + if (t2 && t2->isEnabled()) { + t2->setLanguage(language); + } +} + +void InputMethod::setTextDirection(uint32_t serial, Qt::LayoutDirection direction) +{ + auto t1 = waylandServer()->seat()->textInputV1(); + if (t1 && t1->isEnabled()) { + t1->setTextDirection(direction); + } + auto t2 = waylandServer()->seat()->textInputV2(); + if (t2 && t2->isEnabled()) { + t2->setTextDirection(direction); + } +} + +void InputMethod::setPreeditCursor(qint32 index) +{ + auto t1 = waylandServer()->seat()->textInputV1(); + if (t1 && t1->isEnabled()) { + t1->setPreEditCursor(index); + } + auto t2 = waylandServer()->seat()->textInputV2(); + if (t2 && t2->isEnabled()) { + t2->setPreEditCursor(index); + } + auto t3 = waylandServer()->seat()->textInputV3(); + if (t3 && t3->isEnabled()) { + preedit.cursor = index; + } +} + +void InputMethod::setPreeditStyling(quint32 index, quint32 length, quint32 style) +{ + auto t1 = waylandServer()->seat()->textInputV1(); + if (t1 && t1->isEnabled()) { + t1->preEditStyling(index, length, style); + } + auto t2 = waylandServer()->seat()->textInputV2(); + if (t2 && t2->isEnabled()) { + t2->preEditStyling(index, length, style); + } + auto t3 = waylandServer()->seat()->textInputV3(); + if (t3 && t3->isEnabled()) { + // preedit style: highlight(4) or selection(6) + if (style == 4 || style == 6) { + preedit.highlightRanges.emplace_back(index, index + length); + } + } +} + +void InputMethod::setPreeditString(uint32_t serial, const QString &text, const QString &commit) +{ + auto t1 = waylandServer()->seat()->textInputV1(); + if (t1 && t1->isEnabled()) { + t1->preEdit(text, commit); + } + auto t2 = waylandServer()->seat()->textInputV2(); + if (t2 && t2->isEnabled()) { + t2->preEdit(text, commit); + } + auto t3 = waylandServer()->seat()->textInputV3(); + if (t3 && t3->isEnabled()) { + m_pendingText = commit; + if (!text.isEmpty()) { + quint32 cursor = 0, cursorEnd = 0; + if (preedit.cursor > 0) { + cursor = cursorEnd = preedit.cursor; + } + // Check if we can convert highlight style to a range of selection. + if (!preedit.highlightRanges.empty()) { + std::sort(preedit.highlightRanges.begin(), preedit.highlightRanges.end()); + // Check if starting point matches. + if (preedit.highlightRanges.front().first == cursor) { + quint32 end = preedit.highlightRanges.front().second; + bool nonContinousHighlight = false; + for (size_t i = 1; i < preedit.highlightRanges.size(); i++) { + if (end >= preedit.highlightRanges[i].first) { + end = std::max(end, preedit.highlightRanges[i].second); + } else { + nonContinousHighlight = true; + break; + } + } + if (!nonContinousHighlight) { + cursorEnd = end; + } + } + } + t3->sendPreEditString(text, cursor, cursorEnd); + } else { + t3->sendPreEditString(text, 0, 0); + } + t3->done(); + } + if (m_internalContext->isEnabled()) { + m_internalContext->handlePreeditText(text, preedit.cursor, preedit.cursor); + } + resetPendingPreedit(); +} + +void InputMethod::key(quint32 /*serial*/, quint32 time, quint32 keyCode, KeyboardKeyState state) +{ + if (!input()->keyboard()) { + return; + } + if (effects && effects->hasKeyboardGrab()) { + Xkb *xkb = input()->keyboard()->xkb(); + const xkb_keysym_t keySym = xkb->toKeysym(keyCode); + forwardKeyToEffects(state, keyCode, keySym); + return; + } + + waylandServer()->seat()->notifyKeyboardKey(keyCode, + state, + waylandServer()->display()->nextSerial()); +} + +void InputMethod::modifiers(quint32 serial, quint32 mods_depressed, quint32 mods_latched, quint32 mods_locked, quint32 group) +{ + auto xkb = input()->keyboard()->xkb(); + xkb->updateModifiers(mods_depressed, mods_latched, mods_locked, group); +} + +void InputMethod::forwardModifiers(ForwardModifiersForce force) +{ + const bool sendModifiers = m_hasPendingModifiers || force == Force; + m_hasPendingModifiers = false; + if (!sendModifiers) { + return; + } + auto xkb = input()->keyboard()->xkb(); + if (m_keyboardGrab) { + m_keyboardGrab->sendModifiers(waylandServer()->display()->nextSerial(), + xkb->modifierState().depressed, + xkb->modifierState().latched, + xkb->modifierState().locked, + xkb->currentLayout()); + } +} + +void InputMethod::adoptInputMethodContext() +{ + auto inputContext = waylandServer()->inputMethod()->context(); + + TextInputV1Interface *t1 = waylandServer()->seat()->textInputV1(); + TextInputV2Interface *t2 = waylandServer()->seat()->textInputV2(); + TextInputV3Interface *t3 = waylandServer()->seat()->textInputV3(); + + if (t1 && t1->isEnabled()) { + inputContext->sendSurroundingText(t1->surroundingText(), t1->surroundingTextCursorPosition(), t1->surroundingTextSelectionAnchor()); + inputContext->sendPreferredLanguage(t1->preferredLanguage()); + inputContext->sendContentType(t1->contentHints(), t2->contentPurpose()); + connect(inputContext, &InputMethodContextV1Interface::language, this, &InputMethod::setLanguage); + connect(inputContext, &InputMethodContextV1Interface::textDirection, this, &InputMethod::setTextDirection); + } else if (t2 && t2->isEnabled()) { + inputContext->sendSurroundingText(t2->surroundingText(), t2->surroundingTextCursorPosition(), t2->surroundingTextSelectionAnchor()); + inputContext->sendPreferredLanguage(t2->preferredLanguage()); + inputContext->sendContentType(t2->contentHints(), t2->contentPurpose()); + connect(inputContext, &InputMethodContextV1Interface::language, this, &InputMethod::setLanguage); + connect(inputContext, &InputMethodContextV1Interface::textDirection, this, &InputMethod::setTextDirection); + } else if (t3 && t3->isEnabled()) { + inputContext->sendSurroundingText(t3->surroundingText(), t3->surroundingTextCursorPosition(), t3->surroundingTextSelectionAnchor()); + inputContext->sendContentType(t3->contentHints(), t3->contentPurpose()); + } else if (m_internalContext->isEnabled()) { + inputContext->sendSurroundingText(m_internalContext->surroundingText(), m_internalContext->cursorPosition(), m_internalContext->anchorPosition()); + inputContext->sendContentType(TextInputContentHint::Latin, TextInputContentPurpose::Normal); + } else { + // When we have neither text-input-v2 nor text-input-v3 we can only send + // fake key events, not more complex text. So ask the input method to + // only send basic characters without any pre-editing. + inputContext->sendSurroundingText(QString(), 0, 0); + inputContext->sendContentType(TextInputContentHint::Latin, TextInputContentPurpose::Normal); + } + + inputContext->sendCommitState(m_serial++); + + connect(inputContext, &InputMethodContextV1Interface::keysym, this, &InputMethod::keysymReceived, Qt::UniqueConnection); + connect(inputContext, &InputMethodContextV1Interface::key, this, &InputMethod::key, Qt::UniqueConnection); + connect(inputContext, &InputMethodContextV1Interface::modifiers, this, &InputMethod::modifiers, Qt::UniqueConnection); + connect(inputContext, &InputMethodContextV1Interface::commitString, this, &InputMethod::commitString, Qt::UniqueConnection); + connect(inputContext, &InputMethodContextV1Interface::deleteSurroundingText, this, &InputMethod::deleteSurroundingText, Qt::UniqueConnection); + connect(inputContext, &InputMethodContextV1Interface::cursorPosition, this, &InputMethod::setCursorPosition, Qt::UniqueConnection); + connect(inputContext, &InputMethodContextV1Interface::preeditStyling, this, &InputMethod::setPreeditStyling, Qt::UniqueConnection); + connect(inputContext, &InputMethodContextV1Interface::preeditString, this, &InputMethod::setPreeditString, Qt::UniqueConnection); + connect(inputContext, &InputMethodContextV1Interface::preeditCursor, this, &InputMethod::setPreeditCursor, Qt::UniqueConnection); + connect(inputContext, &InputMethodContextV1Interface::keyboardGrabRequested, this, &InputMethod::installKeyboardGrab, Qt::UniqueConnection); + connect(inputContext, &InputMethodContextV1Interface::modifiersMap, this, &InputMethod::updateModifiersMap, Qt::UniqueConnection); +} + +void InputMethod::updateInputPanelState() +{ + if (m_panel && shouldShowOnActive()) { + m_panel->allow(); + } + + RectF overlap = RectF(0, 0, 0, 0); + if (m_trackedWindow) { + const bool bottomKeyboard = m_panel && m_panel->mode() != InputPanelV1Window::Mode::Overlay && m_panel->isShown(); + m_trackedWindow->setVirtualKeyboardGeometry(bottomKeyboard ? m_panel->frameGeometry() : RectF()); + + if (m_panel && m_panel->mode() != InputPanelV1Window::Mode::Overlay) { + overlap = m_trackedWindow->frameGeometry() & m_panel->frameGeometry(); + overlap.moveTo(m_trackedWindow->mapToLocal(overlap.topLeft())); + } + } + + auto t = waylandServer()->seat()->textInputV2(); + if (!t) { + return; + } + t->setInputPanelState(m_panel && m_panel->isShown(), overlap.toRect()); +} + +void InputMethod::setInputMethodCommand(const QString &command) +{ + if (m_inputMethodCommand == command) { + return; + } + + m_inputMethodCommand = command; + + if (m_enabled) { + startInputMethod(); + } + Q_EMIT availableChanged(); +} + +void InputMethod::stopInputMethod() +{ + if (!m_inputMethodProcess) { + return; + } + disconnect(m_inputMethodProcess, nullptr, this, nullptr); + + m_inputMethodProcess->terminate(); + if (!m_inputMethodProcess->waitForFinished()) { + m_inputMethodProcess->kill(); + m_inputMethodProcess->waitForFinished(); + } + m_inputMethodProcess->deleteLater(); + m_inputMethodProcess = nullptr; + + waylandServer()->destroyInputMethodConnection(); +} + +void InputMethod::startInputMethod() +{ + stopInputMethod(); + if (m_inputMethodCommand.isEmpty() || kwinApp()->isTerminating()) { + return; + } + + QStringList arguments = KShell::splitArgs(m_inputMethodCommand); + if (arguments.isEmpty()) { + qWarning("Failed to launch the input method server: %s is an invalid command", qPrintable(m_inputMethodCommand)); + return; + } + + const QString program = arguments.takeFirst(); + int socket = waylandServer()->createInputMethodConnection(); + if (socket < 0) { + qWarning("Failed to create the input method connection"); + return; + } + socket = dup(socket); + + QProcessEnvironment environment = kwinApp()->processStartupEnvironment(); + environment.insert(QStringLiteral("WAYLAND_SOCKET"), QString::number(socket)); + environment.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("wayland")); + // When we use Maliit as virtual keyboard, we want KWin to handle the animation + // since that works a lot better. So we need to tell Maliit to not do client side + // animation. + environment.insert(QStringLiteral("MALIIT_ENABLE_ANIMATIONS"), QStringLiteral("0")); + + if (qEnvironmentVariableIntValue("KWIN_IM_WAYLAND_DEBUG") == 1) { + environment.insert("WAYLAND_DEBUG", QByteArrayLiteral("1")); + } + + m_inputMethodProcess = new QProcess(this); + m_inputMethodProcess->setProcessChannelMode(QProcess::ForwardedErrorChannel); + m_inputMethodProcess->setProcessEnvironment(environment); + m_inputMethodProcess->setProgram(program); + m_inputMethodProcess->setArguments(arguments); + m_inputMethodProcess->start(); + close(socket); + connect(m_inputMethodProcess, QOverload::of(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) { + if (exitStatus == QProcess::CrashExit) { + m_inputMethodCrashes++; + m_inputMethodCrashTimer.start(); + qWarning() << "Input Method crashed" << m_inputMethodProcess->program() << m_inputMethodProcess->arguments() << exitCode << exitStatus; + if (m_inputMethodCrashes < 5) { + startInputMethod(); + } else { + qWarning() << "Input Method keeps crashing, please fix" << m_inputMethodProcess->program() << m_inputMethodProcess->arguments(); + stopInputMethod(); + } + } + }); +} +bool InputMethod::isActive() const +{ + return waylandServer()->inputMethod()->context(); +} + +InputMethodGrabV1 *InputMethod::keyboardGrab() +{ + return isActive() ? m_keyboardGrab : nullptr; +} + +void InputMethod::installKeyboardGrab(InputMethodGrabV1 *keyboardGrab) +{ + auto xkb = input()->keyboard()->xkb(); + m_keyboardGrab = keyboardGrab; + // Send repeat info based on the current focused client's keyboard version. + if (const auto keyboard = waylandServer()->seat()->keyboard()) { + const auto focusedKeyboardSurface = keyboard->focusedSurface(); + auto effectiveRate = keyboard->keyRepeatRate(); + // If focused keyboard surface's wl_keyboard object accepts compositor repetition, + // disable input method side repetition, otherwise send the current repeat rate. + if (!focusedKeyboardSurface || keyboard->clientUseCompositorRepetition(focusedKeyboardSurface->client())) { + effectiveRate = 0; + } + keyboardGrab->sendRepeatInfo(effectiveRate, keyboard->keyRepeatDelay()); + } + keyboardGrab->sendKeymap(xkb->keymapContents()); + forwardModifiers(Force); +} + +void InputMethod::updateModifiersMap(const QByteArray &modifiers) +{ + TextInputV2Interface *t2 = waylandServer()->seat()->textInputV2(); + + if (t2 && t2->isEnabled()) { + t2->setModifiersMap(modifiers); + } +} + +bool InputMethod::isVisible() const +{ + return m_panel && m_panel->isShown() && m_panel->readyForPainting(); +} + +bool InputMethod::isAvailable() const +{ + return !m_inputMethodCommand.isEmpty(); +} + +Window *InputMethod::activeWindow() const +{ + return m_trackedWindow; +} + +void InputMethod::resetPendingPreedit() +{ + preedit.cursor = 0; + preedit.highlightRanges.clear(); +} + +bool InputMethod::activeClientSupportsTextInput() const +{ + return m_activeClientSupportsTextInput; +} + +void InputMethod::forceActivate() +{ + setActive(true); + show(); +} + +void InputMethod::textInputInterfaceV3EnableRequested() +{ + refreshActive(); + show(); +} +} + +#include "moc_inputmethod.cpp" diff --git a/local/recipes/kde/kwin/source/src/inputmethod.h b/local/recipes/kde/kwin/source/src/inputmethod.h new file mode 100644 index 0000000000..f0df33d068 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/inputmethod.h @@ -0,0 +1,163 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "input_event.h" +#include "wayland/textinput_v2.h" + +#include +#include +#include + +#include +#include + +class QProcess; + +namespace KWin +{ + +class Window; +class InputPanelV1Window; +class InputMethodGrabV1; +class InternalInputMethodContext; + +/** + * This class implements the zwp_input_method_unstable_v1, which is currently used to provide + * the Virtual Keyboard using supported input method client (maliit-keyboard e.g.) + **/ +class KWIN_EXPORT InputMethod : public QObject +{ + Q_OBJECT +public: + enum ForwardModifiersForce { + NoForce = 0, + Force = 1, + }; + + InputMethod(); + ~InputMethod() override; + + void init(); + void setEnabled(bool enable); + bool isEnabled() const + { + return m_enabled; + } + bool isActive() const; + void setActive(bool active); + void hide(); + void show(); + bool isVisible() const; + bool isAvailable() const; + Window *activeWindow() const; + + InputPanelV1Window *panel() const; + void setPanel(InputPanelV1Window *panel); + void setInputMethodCommand(const QString &path); + + InputMethodGrabV1 *keyboardGrab(); + bool shouldShowOnActive() const; + + void forwardModifiers(ForwardModifiersForce force); + bool activeClientSupportsTextInput() const; + void forceActivate(); + + void commitPendingText(); + + // for use by the QPA + InternalInputMethodContext *internalContext() const + { + return m_internalContext; + } + + RectF cursorRectangle() const; + +Q_SIGNALS: + void panelChanged(); + void activeChanged(bool active); + void enabledChanged(bool enabled); + void visibleChanged(); + void availableChanged(); + void activeClientSupportsTextInputChanged(); + void cursorRectangleChanged(); + void activeWindowChanged(); + +private Q_SLOTS: + // textinput interface slots + void handleFocusedSurfaceChanged(); + void surroundingTextChanged(); + void contentTypeChanged(); + void textInputInterfaceV1EnabledChanged(); + void textInputInterfaceV2EnabledChanged(); + void textInputInterfaceV3EnabledChanged(); + void stateCommitted(uint32_t serial); + void textInputInterfaceV1StateUpdated(quint32 serial); + void textInputInterfaceV1Reset(); + void invokeAction(quint32 button, quint32 index); + void textInputInterfaceV2StateUpdated(quint32 serial, KWin::TextInputV2Interface::UpdateReason reason); + void textInputInterfaceV3EnableRequested(); + + // inputcontext slots + void setPreeditString(uint32_t serial, const QString &text, const QString &commit); + void setPreeditStyling(quint32 index, quint32 length, quint32 style); + void setPreeditCursor(qint32 index); + void key(quint32 serial, quint32 time, quint32 key, KWin::KeyboardKeyState state); + void modifiers(quint32 serial, quint32 mods_depressed, quint32 mods_latched, quint32 mods_locked, quint32 group); + +private: + void updateInputPanelState(); + void adoptInputMethodContext(); + void commitString(qint32 serial, const QString &text); + void keysymReceived(quint32 serial, quint32 time, quint32 sym, KeyboardKeyState state, quint32 modifiers); + void deleteSurroundingText(int32_t index, uint32_t length); + void setCursorPosition(qint32 index, qint32 anchor); + void setLanguage(uint32_t serial, const QString &language); + void setTextDirection(uint32_t serial, Qt::LayoutDirection direction); + void startInputMethod(); + void stopInputMethod(); + void setTrackedWindow(Window *trackedWindow); + void installKeyboardGrab(InputMethodGrabV1 *keyboardGrab); + void updateModifiersMap(const QByteArray &modifiers); + + bool touchEventTriggered() const; + void resetPendingPreedit(); + void refreshActive(); + void forwardKeyToEffects(KWin::KeyboardKeyState state, int keyCode, int keySym); + void forwardKeySym(int keySym); + + // buffered till the preedit text is set + struct + { + qint32 cursor = 0; + std::vector> highlightRanges; + } preedit; + + // In some IM cases pre-edit text should be submitted when a user changes focus. In some it should be discarded + // TextInputV3 does not have a flag for this, so we have to handle it in the compositor + QString m_pendingText = QString(); + + bool m_enabled = true; + quint32 m_serial = 0; + QPointer m_panel; + QPointer m_trackedWindow; + QPointer m_keyboardGrab; + + QProcess *m_inputMethodProcess = nullptr; + QTimer m_inputMethodCrashTimer; + uint m_inputMethodCrashes = 0; + QString m_inputMethodCommand; + + InternalInputMethodContext *m_internalContext = nullptr; + bool m_hasPendingModifiers = false; + bool m_activeClientSupportsTextInput = false; + bool m_shouldShowPanel = false; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/inputpanelv1integration.cpp b/local/recipes/kde/kwin/source/src/inputpanelv1integration.cpp new file mode 100644 index 0000000000..db6f0cebb9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/inputpanelv1integration.cpp @@ -0,0 +1,32 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "inputpanelv1integration.h" +#include "inputpanelv1window.h" +#include "wayland/display.h" +#include "wayland/inputmethod_v1.h" +#include "wayland_server.h" + +namespace KWin +{ + +InputPanelV1Integration::InputPanelV1Integration(QObject *parent) + : WaylandShellIntegration(parent) +{ + InputPanelV1Interface *shell = new InputPanelV1Interface(waylandServer()->display(), this); + + connect(shell, &InputPanelV1Interface::inputPanelSurfaceAdded, + this, &InputPanelV1Integration::createWindow); +} + +void InputPanelV1Integration::createWindow(InputPanelSurfaceV1Interface *shellSurface) +{ + Q_EMIT windowCreated(new InputPanelV1Window(shellSurface)); +} + +} // namespace KWin + +#include "moc_inputpanelv1integration.cpp" diff --git a/local/recipes/kde/kwin/source/src/inputpanelv1integration.h b/local/recipes/kde/kwin/source/src/inputpanelv1integration.h new file mode 100644 index 0000000000..61b0d4d091 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/inputpanelv1integration.h @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "waylandshellintegration.h" + +namespace KWin +{ + +class InputPanelSurfaceV1Interface; + +class InputPanelV1Integration : public WaylandShellIntegration +{ + Q_OBJECT + +public: + explicit InputPanelV1Integration(QObject *parent = nullptr); + + void createWindow(InputPanelSurfaceV1Interface *shellSurface); +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/inputpanelv1window.cpp b/local/recipes/kde/kwin/source/src/inputpanelv1window.cpp new file mode 100644 index 0000000000..108733562c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/inputpanelv1window.cpp @@ -0,0 +1,259 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "inputpanelv1window.h" +#include "core/output.h" +#include "core/pixelgrid.h" +#include "inputmethod.h" +#include "wayland/output.h" +#include "wayland/seat.h" +#include "wayland/surface.h" +#include "wayland/textinput_v1.h" +#include "wayland/textinput_v2.h" +#include "wayland/textinput_v3.h" +#include "wayland_server.h" +#include "workspace.h" + +namespace KWin +{ + +InputPanelV1Window::InputPanelV1Window(InputPanelSurfaceV1Interface *panelSurface) + : WaylandWindow(panelSurface->surface()) + , m_panelSurface(panelSurface) +{ + setOutput(workspace()->activeOutput()); + setMoveResizeOutput(workspace()->activeOutput()); + setSkipSwitcher(true); + setSkipPager(true); + setSkipTaskbar(true); + + connect(surface(), &SurfaceInterface::aboutToBeDestroyed, this, &InputPanelV1Window::destroyWindow); + connect(surface(), &SurfaceInterface::sizeChanged, this, &InputPanelV1Window::reposition); + connect(surface(), &SurfaceInterface::inputChanged, this, &InputPanelV1Window::reposition); + connect(surface(), &SurfaceInterface::mapped, this, &InputPanelV1Window::handleMapped); + connect(surface(), &SurfaceInterface::unmapped, this, &InputPanelV1Window::handleUnmapped); + + connect(panelSurface, &InputPanelSurfaceV1Interface::topLevel, this, &InputPanelV1Window::showTopLevel); + connect(panelSurface, &InputPanelSurfaceV1Interface::overlayPanel, this, &InputPanelV1Window::showOverlayPanel); + connect(panelSurface, &InputPanelSurfaceV1Interface::aboutToBeDestroyed, this, &InputPanelV1Window::destroyWindow); + + connect(workspace(), &Workspace::outputsChanged, this, &InputPanelV1Window::reposition); + connect(kwinApp()->inputMethod(), &InputMethod::cursorRectangleChanged, this, &InputPanelV1Window::reposition); + + m_rescalingTimer.setSingleShot(true); + m_rescalingTimer.setInterval(0); + connect(&m_rescalingTimer, &QTimer::timeout, this, [this]() { + setTargetScale(nextTargetScale()); + }); + connect(&m_rescalingTimer, &QTimer::timeout, this, &InputPanelV1Window::reposition); + + kwinApp()->inputMethod()->setPanel(this); +} + +void InputPanelV1Window::showOverlayPanel() +{ + m_mode = Mode::Overlay; + maybeShow(); +} + +void InputPanelV1Window::showTopLevel(OutputInterface *output, InputPanelSurfaceV1Interface::Position position) +{ + m_mode = Mode::VirtualKeyboard; + maybeShow(); +} + +void InputPanelV1Window::allow() +{ + m_allowed = true; + maybeShow(); +} + +void InputPanelV1Window::show() +{ + m_requestedToBeShown = true; + maybeShow(); +} + +void InputPanelV1Window::hide() +{ + m_requestedToBeShown = false; + if (readyForPainting() && m_mode != Mode::Overlay) { + setHidden(true); + } +} + +void InputPanelV1Window::resetPosition() +{ + Q_ASSERT(!isDeleted()); + switch (m_mode) { + case Mode::None: { + // should never happen + }; break; + case Mode::VirtualKeyboard: { + // maliit creates a fullscreen overlay so use the input shape as the window geometry. + m_windowGeometry = surface()->input().boundingRect(); + + const auto activeOutput = workspace()->activeOutput(); + RectF availableArea; + if (waylandServer()->isScreenLocked()) { + availableArea = workspace()->clientArea(FullScreenArea, this, activeOutput); + } else { + availableArea = workspace()->clientArea(MaximizeArea, this, activeOutput); + } + + RectF geo = m_windowGeometry; + + // if it fits, align within available area + if (geo.width() < availableArea.width()) { + geo.moveLeft(availableArea.left() + (availableArea.width() - geo.width()) / 2); + } else { // otherwise align to be centred within the screen + const RectF outputArea = activeOutput->geometry(); + geo.moveLeft(outputArea.left() + (outputArea.width() - geo.width()) / 2); + } + + geo.moveBottom(availableArea.bottom()); + + moveResize(snapToPixels(geo, targetScale())); + } break; + case Mode::Overlay: { + const RectF cursorRectangle = kwinApp()->inputMethod()->cursorRectangle(); + const RectF screen = Workspace::self()->clientArea(PlacementArea, this, cursorRectangle.bottomLeft()); + + m_windowGeometry = RectF(QPointF(0, 0), surface()->size()); + + RectF popupRect(cursorRectangle.left(), + cursorRectangle.top() + cursorRectangle.height(), + m_windowGeometry.width(), + m_windowGeometry.height()); + if (popupRect.left() < screen.left()) { + popupRect.moveLeft(screen.left()); + } + if (popupRect.right() > screen.right()) { + popupRect.moveRight(screen.right()); + } + if (popupRect.top() < screen.top() || popupRect.bottom() > screen.bottom()) { + const RectF flippedPopupRect(cursorRectangle.left(), + cursorRectangle.top() - m_windowGeometry.height(), + m_windowGeometry.width(), + m_windowGeometry.height()); + + // if it still doesn't fit we should continue with the unflipped version + if (flippedPopupRect.top() >= screen.top() && flippedPopupRect.bottom() <= screen.bottom()) { + popupRect.moveTop(flippedPopupRect.top()); + } + } + if (popupRect.top() < screen.top()) { + popupRect.moveTop(screen.top()); + } + if (popupRect.bottom() > screen.bottom()) { + popupRect.moveBottom(screen.bottom()); + } + + moveResize(snapToPixels(popupRect, targetScale())); + + } break; + } +} + +void InputPanelV1Window::reposition() +{ + if (!readyForPainting()) { + return; + } + + resetPosition(); +} + +void InputPanelV1Window::destroyWindow() +{ + m_panelSurface->disconnect(this); + m_panelSurface->surface()->disconnect(this); + disconnect(workspace(), &Workspace::outputsChanged, this, &InputPanelV1Window::reposition); + disconnect(kwinApp()->inputMethod(), &InputMethod::cursorRectangleChanged, this, &InputPanelV1Window::reposition); + + markAsDeleted(); + Q_EMIT closed(); + + m_rescalingTimer.stop(); + StackingUpdatesBlocker blocker(workspace()); + waylandServer()->removeWindow(this); + + unref(); +} + +WindowType InputPanelV1Window::windowType() const +{ + return WindowType::Utility; +} + +RectF InputPanelV1Window::frameRectToBufferRect(const RectF &rect) const +{ + return RectF(rect.topLeft() - m_windowGeometry.topLeft(), surface()->size()); +} + +void InputPanelV1Window::moveResizeInternal(const RectF &rect, MoveResizeMode mode) +{ + updateGeometry(rect); +} + +void InputPanelV1Window::doSetNextTargetScale() +{ + if (isDeleted()) { + return; + } + surface()->setPreferredBufferScale(nextTargetScale()); + // re-align the surface with the new target scale + m_rescalingTimer.start(); +} + +void InputPanelV1Window::doSetPreferredBufferTransform() +{ + if (isDeleted()) { + return; + } + surface()->setPreferredBufferTransform(preferredBufferTransform()); +} + +void InputPanelV1Window::doSetPreferredColorDescription() +{ + if (isDeleted()) { + return; + } + surface()->setPreferredColorDescription(preferredColorDescription()); +} + +void InputPanelV1Window::handleMapped() +{ + m_wasEverMapped = true; + maybeShow(); +} + +void InputPanelV1Window::handleUnmapped() +{ + setHidden(true); +} + +bool InputPanelV1Window::wasUnmapped() const +{ + return m_wasEverMapped && !surface()->isMapped(); +} + +void InputPanelV1Window::maybeShow() +{ + const bool shouldShow = m_mode == Mode::Overlay || (m_mode == Mode::VirtualKeyboard && m_allowed && m_requestedToBeShown); + if (shouldShow && !isDeleted() && surface()->isMapped()) { + resetPosition(); + markAsMapped(); + setHidden(false); + } +} + +} // namespace KWin + +#include "moc_inputpanelv1window.cpp" diff --git a/local/recipes/kde/kwin/source/src/inputpanelv1window.h b/local/recipes/kde/kwin/source/src/inputpanelv1window.h new file mode 100644 index 0000000000..8e93dc26ae --- /dev/null +++ b/local/recipes/kde/kwin/source/src/inputpanelv1window.h @@ -0,0 +1,105 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "wayland/inputmethod_v1.h" +#include "waylandwindow.h" +#include + +namespace KWin +{ +class LogicalOutput; + +class InputPanelV1Window : public WaylandWindow +{ + Q_OBJECT +public: + InputPanelV1Window(InputPanelSurfaceV1Interface *panelSurface); + + enum class Mode { + None, + VirtualKeyboard, + Overlay, + }; + Q_ENUM(Mode) + + void destroyWindow() override; + bool isPlaceable() const override + { + return false; + } + bool isCloseable() const override + { + return false; + } + bool isResizable() const override + { + return false; + } + bool isMovable() const override + { + return false; + } + bool isMovableAcrossScreens() const override + { + return false; + } + bool acceptsFocus() const override + { + return false; + } + void closeWindow() override + { + } + bool wantsInput() const override + { + return false; + } + bool isInputMethod() const override + { + return true; + } + WindowType windowType() const override; + RectF frameRectToBufferRect(const RectF &rect) const override; + + Mode mode() const + { + return m_mode; + } + void allow(); + void show(); + void hide(); + bool wasUnmapped() const; + +protected: + void moveResizeInternal(const RectF &rect, MoveResizeMode mode) override; + void doSetNextTargetScale() override; + void doSetPreferredBufferTransform() override; + void doSetPreferredColorDescription() override; + +private: + void showTopLevel(OutputInterface *output, InputPanelSurfaceV1Interface::Position position); + void showOverlayPanel(); + void resetPosition(); + void reposition(); + void handleMapped(); + void handleUnmapped(); + void maybeShow(); + + RectF m_windowGeometry; + Mode m_mode = Mode::None; + bool m_allowed = false; + bool m_requestedToBeShown = false; + bool m_wasEverMapped = false; + const QPointer m_panelSurface; + QTimer m_rescalingTimer; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/internalwindow.cpp b/local/recipes/kde/kwin/source/src/internalwindow.cpp new file mode 100644 index 0000000000..c03d03696b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/internalwindow.cpp @@ -0,0 +1,496 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Martin Flöser + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "internalwindow.h" +#include "decorations/decorationbridge.h" +#include "scene/windowitem.h" +#include "workspace.h" + +#include + +#include +#include + +#include + +Q_DECLARE_METATYPE(NET::WindowType) + +static const QByteArray s_skipClosePropertyName = QByteArrayLiteral("KWIN_SKIP_CLOSE_ANIMATION"); +static const QByteArray s_shadowEnabledPropertyName = QByteArrayLiteral("kwin_shadow_enabled"); + +namespace KWin +{ + +InternalWindow::InternalWindow(QWindow *handle) + : m_handle(handle) + , m_internalWindowFlags(handle->flags()) +{ + connect(m_handle, &QWindow::windowTitleChanged, this, &InternalWindow::setCaption); + connect(m_handle, &QWindow::opacityChanged, this, &InternalWindow::setOpacity); + connect(m_handle, &QWindow::destroyed, this, &InternalWindow::destroyWindow); + + setOutput(workspace()->activeOutput()); + setMoveResizeOutput(workspace()->activeOutput()); + setCaption(m_handle->title()); + setIcon(QIcon::fromTheme(QStringLiteral("kwin"))); + setOnAllDesktops(true); + setOpacity(m_handle->opacity()); + setSkipCloseAnimation(m_handle->property(s_skipClosePropertyName).toBool()); + updateColorScheme(); + updateShadow(); + + setMoveResizeGeometry(m_handle->geometry()); + commitGeometry(m_handle->geometry()); + + updateDecoration(true); + + m_handle->installEventFilter(this); +} + +InternalWindow::~InternalWindow() +{ +} + +std::unique_ptr InternalWindow::createItem(Item *parentItem) +{ + return std::make_unique(this, parentItem); +} + +bool InternalWindow::isClient() const +{ + return true; +} + +bool InternalWindow::hitTest(const QPointF &point) const +{ + if (!Window::hitTest(point)) { + return false; + } + + const QRegion mask = m_handle->mask(); + if (!mask.isEmpty() && !mask.contains(mapToLocal(point).toPoint())) { + return false; + } else if (m_handle->property("outputOnly").toBool()) { + return false; + } + + return true; +} + +void InternalWindow::pointerEnterEvent(const QPointF &globalPos) +{ + Window::pointerEnterEvent(globalPos); + + QEnterEvent enterEvent(pos(), pos(), globalPos); + QCoreApplication::sendEvent(m_handle, &enterEvent); +} + +void InternalWindow::pointerLeaveEvent() +{ + if (!m_handle) { + return; + } + Window::pointerLeaveEvent(); + + QEvent event(QEvent::Leave); + QCoreApplication::sendEvent(m_handle, &event); +} + +bool InternalWindow::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == m_handle && event->type() == QEvent::DynamicPropertyChange) { + QDynamicPropertyChangeEvent *pe = static_cast(event); + if (pe->propertyName() == s_skipClosePropertyName) { + setSkipCloseAnimation(m_handle->property(s_skipClosePropertyName).toBool()); + } + if (pe->propertyName() == s_shadowEnabledPropertyName) { + updateShadow(); + } + } + return false; +} + +qreal InternalWindow::bufferScale() const +{ + if (m_handle) { + return m_handle->devicePixelRatio(); + } + return 1; +} + +void InternalWindow::doSetNextTargetScale() +{ + setTargetScale(nextTargetScale()); +} + +QString InternalWindow::captionNormal() const +{ + return m_captionNormal; +} + +QString InternalWindow::captionSuffix() const +{ + return m_captionSuffix; +} + +QSizeF InternalWindow::minSize() const +{ + return m_handle->minimumSize(); +} + +QSizeF InternalWindow::maxSize() const +{ + return m_handle->maximumSize(); +} + +WindowType InternalWindow::windowType() const +{ + return WindowType::Normal; +} + +void InternalWindow::killWindow() +{ + // We don't kill our internal windows. +} + +bool InternalWindow::isPopupWindow() const +{ + if (Window::isPopupWindow()) { + return true; + } + return m_internalWindowFlags.testFlag(Qt::Popup); +} + +QString InternalWindow::windowRole() const +{ + return QString(); +} + +void InternalWindow::closeWindow() +{ + if (!isDeleted()) { + QWindowSystemInterface::handleCloseEvent(m_handle); + } +} + +bool InternalWindow::isCloseable() const +{ + return true; +} + +bool InternalWindow::isMovable() const +{ + if (!options->interactiveWindowMoveEnabled()) { + return false; + } + return !m_internalWindowFlags.testFlag(Qt::BypassWindowManagerHint) && !m_internalWindowFlags.testFlag(Qt::Popup); +} + +bool InternalWindow::isMovableAcrossScreens() const +{ + return !m_internalWindowFlags.testFlag(Qt::BypassWindowManagerHint) && !m_internalWindowFlags.testFlag(Qt::Popup); +} + +bool InternalWindow::isResizable() const +{ + return true; +} + +bool InternalWindow::isPlaceable() const +{ + return !m_internalWindowFlags.testFlag(Qt::BypassWindowManagerHint) && !m_internalWindowFlags.testFlag(Qt::Popup); +} + +bool InternalWindow::wantsInput() const +{ + return false; +} + +bool InternalWindow::isInternal() const +{ + return true; +} + +bool InternalWindow::isLockScreen() const +{ + if (m_handle) { + return m_handle->property("org_kde_ksld_emergency").toBool(); + } + return false; +} + +bool InternalWindow::isOutline() const +{ + if (m_handle) { + return m_handle->property("__kwin_outline").toBool(); + } + return false; +} + +RectF InternalWindow::resizeWithChecks(const RectF &geometry, const QSizeF &size) const +{ + if (!m_handle) { + return geometry; + } + const RectF area = workspace()->clientArea(WorkArea, this, geometry.center()); + return RectF(moveResizeGeometry().topLeft(), size.boundedTo(area.size())); +} + +void InternalWindow::moveResizeInternal(const RectF &rect, MoveResizeMode mode) +{ + const QSize requestedSize = nextFrameSizeToClientSize(rect.size()).toSize(); + if (clientSize().toSize() == requestedSize) { + commitGeometry(rect); + } + + const QRect nativeRect = nextFrameRectToClientRect(rect).toRect(); + if (m_handle->geometry() != nativeRect) { + const QRect oldNativeRect = m_handle->geometry(); + + QWindowSystemInterface::handleGeometryChange(m_handle, nativeRect); + if (m_handle->isExposed() && oldNativeRect.size() != nativeRect.size()) { + QWindowSystemInterface::handleExposeEvent(m_handle, QRect(QPoint(), nativeRect.size())); + } + } +} + +DecorationPolicy InternalWindow::decorationPolicy() const +{ + return m_decorationPolicy; +} + +void InternalWindow::setDecorationPolicy(DecorationPolicy policy) +{ + if (m_decorationPolicy == policy) { + return; + } + m_decorationPolicy = policy; + updateDecoration(true); + Q_EMIT decorationPolicyChanged(); +} + +void InternalWindow::createDecoration(const RectF &oldGeometry) +{ + std::shared_ptr decoration(Workspace::self()->decorationBridge()->createDecoration(this)); + if (decoration) { + decoration->apply(decoration->nextState()->clone()); + connect(decoration.get(), &KDecoration3::Decoration::nextStateChanged, this, [this](auto state) { + if (!isDeleted()) { + m_decoration.decoration->apply(state->clone()); + } + }); + } + + setDecoration(decoration); + moveResize(RectF(oldGeometry.topLeft(), nextClientSizeToFrameSize(clientSize()))); +} + +void InternalWindow::destroyDecoration() +{ + const QSizeF clientSize = nextFrameSizeToClientSize(moveResizeGeometry().size()); + setDecoration(nullptr); + resize(clientSize); +} + +DecorationMode InternalWindow::preferredDecorationMode() const +{ + if (!Decoration::DecorationBridge::hasPlugin()) { + return DecorationMode::Client; + } else if (isRequestedFullScreen()) { + return DecorationMode::None; + } + + switch (m_decorationPolicy) { + case DecorationPolicy::None: + return DecorationMode::None; + case DecorationPolicy::Server: + return DecorationMode::Server; + case DecorationPolicy::PreferredByClient: + if (m_internalWindowFlags.testFlag(Qt::FramelessWindowHint) || m_internalWindowFlags.testFlag(Qt::Popup)) { + return DecorationMode::Client; + } else { + return DecorationMode::Server; + } + } + + return DecorationMode::Client; +} + +void InternalWindow::updateDecoration(bool check_workspace_pos, bool force) +{ + const bool wantsDecoration = preferredDecorationMode() == DecorationMode::Server; + if (!force && isDecorated() == wantsDecoration) { + return; + } + + const RectF oldFrameGeometry = frameGeometry(); + if (force) { + destroyDecoration(); + } + + if (wantsDecoration) { + createDecoration(oldFrameGeometry); + } else { + destroyDecoration(); + } + + updateShadow(); + + if (check_workspace_pos) { + checkWorkspacePosition(oldFrameGeometry); + } +} + +void InternalWindow::invalidateDecoration() +{ + updateDecoration(true, true); +} + +void InternalWindow::destroyWindow() +{ + m_handle->removeEventFilter(this); + m_handle->disconnect(this); + + markAsDeleted(); + stopDelayedInteractiveMoveResize(); + if (isInteractiveMoveResize()) { + leaveInteractiveMoveResize(); + Q_EMIT interactiveMoveResizeFinished(); + } + + Q_EMIT closed(); + + workspace()->removeInternalWindow(this); + m_handle = nullptr; + + unref(); +} + +bool InternalWindow::hasPopupGrab() const +{ + return !m_handle->flags().testFlag(Qt::WindowTransparentForInput) && m_handle->flags().testFlag(Qt::Popup) && !m_handle->flags().testFlag(Qt::ToolTip); +} + +void InternalWindow::popupDone() +{ + m_handle->close(); +} + +GraphicsBuffer *InternalWindow::graphicsBuffer() const +{ + return m_graphicsBufferRef.buffer(); +} + +OutputTransform InternalWindow::bufferTransform() const +{ + return m_bufferTransform; +} + +void InternalWindow::present(const InternalWindowFrame &frame) +{ + RectF geometry(clientRectToFrameRect(m_handle->geometry())); + if (isInteractiveResize()) { + geometry = interactiveMoveResizeGravity().apply(geometry, moveResizeGeometry()); + } + + commitGeometry(geometry); + + m_graphicsBufferRef = frame.buffer; + m_bufferTransform = frame.bufferTransform; + + Q_EMIT presented(frame); + + markAsMapped(); +} + +QWindow *InternalWindow::handle() const +{ + return m_handle; +} + +bool InternalWindow::acceptsFocus() const +{ + return false; +} + +bool InternalWindow::belongsToSameApplication(const Window *other, SameApplicationChecks checks) const +{ + const InternalWindow *otherInternal = qobject_cast(other); + if (!otherInternal) { + return false; + } + if (otherInternal == this) { + return true; + } + return otherInternal->handle()->isAncestorOf(handle()) || handle()->isAncestorOf(otherInternal->handle()); +} + +void InternalWindow::updateCaption() +{ + const QString suffix = shortcutCaptionSuffix(); + if (m_captionSuffix != suffix) { + m_captionSuffix = suffix; + Q_EMIT captionChanged(); + } +} + +void InternalWindow::commitGeometry(const RectF &rect) +{ + // The client geometry and the buffer geometry are the same. + const RectF oldClientGeometry = m_clientGeometry; + const RectF oldFrameGeometry = m_frameGeometry; + const LogicalOutput *oldOutput = m_output; + + Q_EMIT frameGeometryAboutToChange(); + + m_clientGeometry = frameRectToClientRect(rect); + m_frameGeometry = rect; + m_bufferGeometry = m_clientGeometry; + + if (oldClientGeometry == m_clientGeometry && oldFrameGeometry == m_frameGeometry) { + return; + } + + m_output = workspace()->outputAt(rect.center()); + + if (oldClientGeometry != m_clientGeometry) { + Q_EMIT bufferGeometryChanged(oldClientGeometry); + Q_EMIT clientGeometryChanged(oldClientGeometry); + } + if (oldFrameGeometry != m_frameGeometry) { + Q_EMIT frameGeometryChanged(oldFrameGeometry); + } + if (oldOutput != m_output) { + Q_EMIT outputChanged(); + } +} + +void InternalWindow::setCaption(const QString &caption) +{ + if (m_captionNormal == caption) { + return; + } + + m_captionNormal = caption; + Q_EMIT captionNormalChanged(); + Q_EMIT captionChanged(); +} + +void InternalWindow::markAsMapped() +{ + if (!ready_for_painting) { + setupCompositing(); + setReadyForPainting(); + workspace()->addInternalWindow(this); + } +} + +} + +#include "moc_internalwindow.cpp" diff --git a/local/recipes/kde/kwin/source/src/internalwindow.h b/local/recipes/kde/kwin/source/src/internalwindow.h new file mode 100644 index 0000000000..f38395fabb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/internalwindow.h @@ -0,0 +1,103 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Martin Flöser + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "core/graphicsbuffer.h" +#include "window.h" + +namespace KWin +{ + +struct InternalWindowFrame +{ + GraphicsBuffer *buffer = nullptr; + Region bufferDamage; + OutputTransform bufferTransform = OutputTransform::Normal; +}; + +class KWIN_EXPORT InternalWindow : public Window +{ + Q_OBJECT + +public: + explicit InternalWindow(QWindow *handle); + ~InternalWindow() override; + + bool eventFilter(QObject *watched, QEvent *event) override; + + QString captionNormal() const override; + QString captionSuffix() const override; + QSizeF minSize() const override; + QSizeF maxSize() const override; + WindowType windowType() const override; + void killWindow() override; + bool isClient() const override; + bool isPopupWindow() const override; + QString windowRole() const override; + void closeWindow() override; + bool isCloseable() const override; + bool isMovable() const override; + bool isMovableAcrossScreens() const override; + bool isResizable() const override; + bool isPlaceable() const override; + bool wantsInput() const override; + bool isInternal() const override; + bool isLockScreen() const override; + bool isOutline() const override; + RectF resizeWithChecks(const RectF &geometry, const QSizeF &size) const override; + DecorationPolicy decorationPolicy() const override; + void setDecorationPolicy(DecorationPolicy policy) override; + void invalidateDecoration() override; + void destroyWindow() override; + bool hasPopupGrab() const override; + void popupDone() override; + bool hitTest(const QPointF &point) const override; + void pointerEnterEvent(const QPointF &globalPos) override; + void pointerLeaveEvent() override; + + GraphicsBuffer *graphicsBuffer() const; + OutputTransform bufferTransform() const; + + void present(const InternalWindowFrame &frame); + qreal bufferScale() const; + void doSetNextTargetScale() override; + QWindow *handle() const; + +Q_SIGNALS: + void presented(const InternalWindowFrame &frame); + +protected: + bool acceptsFocus() const override; + bool belongsToSameApplication(const Window *other, SameApplicationChecks checks) const override; + void updateCaption() override; + void moveResizeInternal(const RectF &rect, MoveResizeMode mode) override; + std::unique_ptr createItem(Item *parentItem) override; + +private: + void commitGeometry(const RectF &rect); + void setCaption(const QString &caption); + void markAsMapped(); + DecorationMode preferredDecorationMode() const; + void updateDecoration(bool check_workspace_pos, bool force = false); + void createDecoration(const RectF &oldGeometry); + void destroyDecoration(); + + QWindow *m_handle = nullptr; + QString m_captionNormal; + QString m_captionSuffix; + Qt::WindowFlags m_internalWindowFlags = Qt::WindowFlags(); + DecorationPolicy m_decorationPolicy = DecorationPolicy::PreferredByClient; + GraphicsBufferRef m_graphicsBufferRef; + OutputTransform m_bufferTransform = OutputTransform::Normal; + + Q_DISABLE_COPY(InternalWindow) +}; + +} diff --git a/local/recipes/kde/kwin/source/src/kcms/CMakeLists.txt b/local/recipes/kde/kwin/source/src/kcms/CMakeLists.txt new file mode 100644 index 0000000000..4bcdc44d3d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/CMakeLists.txt @@ -0,0 +1,18 @@ +remove_definitions(-DQT_NO_CAST_FROM_ASCII -DQT_STRICT_ITERATORS -DQT_NO_CAST_FROM_BYTEARRAY -DQT_NO_KEYWORDS) + +add_subdirectory(common) + +add_subdirectory(animations) +add_subdirectory(options) +add_subdirectory(decoration) +add_subdirectory(rules) +add_subdirectory(screenedges) +add_subdirectory(scripts) +add_subdirectory(desktop) +add_subdirectory(effects) +add_subdirectory(virtualkeyboard) +add_subdirectory(xwayland) + +if (KWIN_BUILD_TABBOX) + add_subdirectory(tabbox) +endif() diff --git a/local/recipes/kde/kwin/source/src/kcms/common/CMakeLists.txt b/local/recipes/kde/kwin/source/src/kcms/common/CMakeLists.txt new file mode 100644 index 0000000000..a252660b57 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/common/CMakeLists.txt @@ -0,0 +1,41 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcmkwincommon\") + +set(kcmkwincommon_SRC + effectsmodel.cpp +) + +qt_add_dbus_interface(kcmkwincommon_SRC + ${KWin_SOURCE_DIR}/src/org.kde.kwin.Effects.xml kwin_effects_interface +) + +add_library(kcmkwincommon SHARED ${kcmkwincommon_SRC}) + +target_link_libraries(kcmkwincommon + Qt::Core + Qt::DBus + Qt::Quick + KF6::CoreAddons + KF6::ConfigCore + KF6::I18n + KF6::Package + KF6::KCMUtils +) + +set_target_properties(kcmkwincommon PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION 6 +) + +install(TARGETS kcmkwincommon ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP) + +set(kcm_kwin4_genericscripted_SRCS genericscriptedconfig.cpp) +qt_add_dbus_interface(kcm_kwin4_genericscripted_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +add_library(kcm_kwin4_genericscripted MODULE ${kcm_kwin4_genericscripted_SRCS}) +target_link_libraries(kcm_kwin4_genericscripted + KF6::KCMUtils #KCModule + KF6::I18n + Qt::DBus + Qt::UiTools +) +install(TARGETS kcm_kwin4_genericscripted DESTINATION ${KDE_INSTALL_PLUGINDIR}/kwin/effects/configs) diff --git a/local/recipes/kde/kwin/source/src/kcms/common/Messages.sh b/local/recipes/kde/kwin/source/src/kcms/common/Messages.sh new file mode 100644 index 0000000000..b4e10c655f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/common/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.cpp` -o $podir/kcmkwincommon.pot diff --git a/local/recipes/kde/kwin/source/src/kcms/common/effectsmodel.cpp b/local/recipes/kde/kwin/source/src/kcms/common/effectsmodel.cpp new file mode 100644 index 0000000000..cf0d089f93 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/common/effectsmodel.cpp @@ -0,0 +1,627 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Antonis Tsiapaliokas + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "effectsmodel.h" + +#include "config-kwin.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +static QString translatedCategory(const QString &category) +{ + static const QList knownCategories = { + QStringLiteral("Accessibility"), + QStringLiteral("Appearance"), + QStringLiteral("Focus"), + QStringLiteral("Show Desktop Animation"), + QStringLiteral("Tools"), + QStringLiteral("Virtual Desktop Switching Animation"), + QStringLiteral("Window Management"), + QStringLiteral("Window Open/Close Animation")}; + + static const QList translatedCategories = { + i18nc("Category of Desktop Effects, used as section header", "Accessibility"), + i18nc("Category of Desktop Effects, used as section header", "Appearance"), + i18nc("Category of Desktop Effects, used as section header", "Focus"), + i18nc("Category of Desktop Effects, used as section header", "Peek at Desktop Animation"), + i18nc("Category of Desktop Effects, used as section header", "Tools"), + i18nc("Category of Desktop Effects, used as section header", "Virtual Desktop Switching Animation"), + i18nc("Category of Desktop Effects, used as section header", "Window Management"), + i18nc("Category of Desktop Effects, used as section header", "Window Open/Close Animation")}; + + const int index = knownCategories.indexOf(category); + if (index == -1) { + qDebug() << "Unknown category '" << category << "' and thus not translated"; + return category; + } + + return translatedCategories[index]; +} + +static EffectsModel::Status effectStatus(bool enabled) +{ + return enabled ? EffectsModel::Status::Enabled : EffectsModel::Status::Disabled; +} + +EffectsModel::EffectsModel(QObject *parent) + : QAbstractItemModel(parent) +{ +} + +QHash EffectsModel::roleNames() const +{ + QHash roleNames; + roleNames[NameRole] = "NameRole"; + roleNames[DescriptionRole] = "DescriptionRole"; + roleNames[AuthorNameRole] = "AuthorNameRole"; + roleNames[AuthorEmailRole] = "AuthorEmailRole"; + roleNames[LicenseRole] = "LicenseRole"; + roleNames[VersionRole] = "VersionRole"; + roleNames[CategoryRole] = "CategoryRole"; + roleNames[ServiceNameRole] = "ServiceNameRole"; + roleNames[IconNameRole] = "IconNameRole"; + roleNames[StatusRole] = "StatusRole"; + roleNames[WebsiteRole] = "WebsiteRole"; + roleNames[SupportedRole] = "SupportedRole"; + roleNames[ExclusiveRole] = "ExclusiveRole"; + roleNames[ConfigurableRole] = "ConfigurableRole"; + roleNames[EnabledByDefaultRole] = "EnabledByDefaultRole"; + roleNames[EnabledByDefaultFunctionRole] = "EnabledByDefaultFunctionRole"; + roleNames[ConfigModuleRole] = "ConfigModuleRole"; + return roleNames; +} + +QModelIndex EffectsModel::index(int row, int column, const QModelIndex &parent) const +{ + if (parent.isValid() || column > 0 || column < 0 || row < 0 || row >= m_effects.count()) { + return {}; + } + + return createIndex(row, column); +} + +QModelIndex EffectsModel::parent(const QModelIndex &child) const +{ + return {}; +} + +int EffectsModel::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +int EffectsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_effects.count(); +} + +QVariant EffectsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return {}; + } + + const EffectData effect = m_effects.at(index.row()); + switch (role) { + case Qt::DisplayRole: + case NameRole: + return effect.name; + case DescriptionRole: + return effect.description; + case AuthorNameRole: + return effect.authorName; + case AuthorEmailRole: + return effect.authorEmail; + case LicenseRole: + return effect.license; + case VersionRole: + return effect.version; + case CategoryRole: + return effect.category; + case ServiceNameRole: + return effect.serviceName; + case IconNameRole: + return effect.iconName; + case StatusRole: + return static_cast(effect.status); + case WebsiteRole: + return effect.website; + case SupportedRole: + return effect.supported; + case ExclusiveRole: + return effect.exclusiveGroup; + case InternalRole: + return effect.internal; + case ConfigurableRole: + return !effect.configModule.isEmpty(); + case EnabledByDefaultRole: + return effect.enabledByDefault; + case EnabledByDefaultFunctionRole: + return effect.enabledByDefaultFunction; + case ConfigModuleRole: + return effect.configModule; + default: + return {}; + } +} + +bool EffectsModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid()) { + return QAbstractItemModel::setData(index, value, role); + } + + if (role == StatusRole) { + // note: whenever the StatusRole is modified (even to the same value) the entry + // gets marked as changed and will get saved to the config file. This means the + // config file could get polluted + EffectData &data = m_effects[index.row()]; + data.status = Status(value.toInt()); + data.changed = data.status != data.originalStatus; + Q_EMIT dataChanged(index, index); + + if (data.status == Status::Enabled && !data.exclusiveGroup.isEmpty()) { + // need to disable all other exclusive effects in the same category + for (int i = 0; i < m_effects.size(); ++i) { + if (i == index.row()) { + continue; + } + EffectData &otherData = m_effects[i]; + if (otherData.exclusiveGroup == data.exclusiveGroup) { + otherData.status = Status::Disabled; + otherData.changed = otherData.status != otherData.originalStatus; + Q_EMIT dataChanged(this->index(i, 0), this->index(i, 0)); + } + } + } + + return true; + } + + return QAbstractItemModel::setData(index, value, role); +} + +void EffectsModel::loadBuiltInEffects(const KConfigGroup &kwinConfig) +{ + const QString rootDirectory = QStandardPaths::locate(QStandardPaths::GenericDataLocation, + QStringLiteral("kwin-wayland/builtin-effects"), + QStandardPaths::LocateDirectory); + + const QStringList nameFilters{QStringLiteral("*.json")}; + QDirIterator it(rootDirectory, nameFilters, QDir::Files); + while (it.hasNext()) { + it.next(); + + const KPluginMetaData metaData = KPluginMetaData::fromJsonFile(it.filePath()); + if (!metaData.isValid()) { + continue; + } + + EffectData effect; + effect.name = metaData.name(); + effect.description = metaData.description(); + effect.authorName = i18n("KWin development team"); + effect.authorEmail = QString(); // not used at all + effect.license = metaData.license(); + effect.version = metaData.version(); + effect.untranslatedCategory = metaData.category(); + effect.category = translatedCategory(metaData.category()); + effect.serviceName = metaData.pluginId(); + effect.iconName = metaData.iconName(); + effect.enabledByDefault = metaData.isEnabledByDefault(); + effect.supported = true; + effect.enabledByDefaultFunction = false; + effect.internal = false; + effect.configModule = metaData.value(QStringLiteral("X-KDE-ConfigModule")); + effect.website = QUrl(metaData.website()); + + if (metaData.rawData().contains("org.kde.kwin.effect")) { + const QJsonObject d(metaData.rawData().value("org.kde.kwin.effect").toObject()); + effect.exclusiveGroup = d.value("exclusiveGroup").toString(); + effect.enabledByDefaultFunction = d.value("enabledByDefaultMethod").toBool(); + effect.internal = d.value("internal").toBool(); + } + + const QString enabledKey = QStringLiteral("%1Enabled").arg(effect.serviceName); + if (kwinConfig.hasKey(enabledKey)) { + effect.status = effectStatus(kwinConfig.readEntry(effect.serviceName + "Enabled", effect.enabledByDefault)); + } else if (effect.enabledByDefaultFunction) { + effect.status = Status::EnabledUndeterminded; + } else { + effect.status = effectStatus(effect.enabledByDefault); + } + + effect.originalStatus = effect.status; + + if (shouldStore(effect)) { + m_pendingEffects << effect; + } + } +} + +void EffectsModel::loadJavascriptEffects(const KConfigGroup &kwinConfig) +{ + const QStringList prefixes{ + QStringLiteral("kwin-wayland/effects"), + QStringLiteral("kwin/effects"), + }; + for (const QString &prefix : prefixes) { + const auto plugins = KPackage::PackageLoader::self()->listPackages(QStringLiteral("KWin/Effect"), prefix); + for (const KPluginMetaData &plugin : plugins) { + EffectData effect; + + effect.name = plugin.name(); + effect.description = plugin.description(); + const auto authors = plugin.authors(); + effect.authorName = !authors.isEmpty() ? authors.first().name() : QString(); + effect.authorEmail = !authors.isEmpty() ? authors.first().emailAddress() : QString(); + effect.license = plugin.license(); + effect.version = plugin.version(); + effect.untranslatedCategory = plugin.category(); + effect.category = translatedCategory(plugin.category()); + effect.serviceName = plugin.pluginId(); + effect.iconName = plugin.iconName(); + effect.status = effectStatus(kwinConfig.readEntry(effect.serviceName + "Enabled", plugin.isEnabledByDefault())); + effect.originalStatus = effect.status; + effect.enabledByDefault = plugin.isEnabledByDefault(); + effect.enabledByDefaultFunction = false; + effect.website = QUrl(plugin.website()); + effect.supported = true; + effect.exclusiveGroup = plugin.value(QStringLiteral("X-KWin-Exclusive-Category")); + effect.internal = plugin.value(QStringLiteral("X-KWin-Internal"), false); + + if (const QString configModule = plugin.value(QStringLiteral("X-KDE-ConfigModule")); !configModule.isEmpty()) { + if (configModule == QLatin1StringView("kcm_kwin4_genericscripted")) { + const QString xmlFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, prefix + QLatin1Char('/') + plugin.pluginId() + QLatin1String("/contents/config/main.xml")); + const QString uiFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, prefix + QLatin1Char('/') + plugin.pluginId() + QLatin1String("/contents/ui/config.ui")); + if (QFileInfo::exists(xmlFile) && QFileInfo::exists(uiFile)) { + effect.configModule = configModule; + effect.configArgs = QVariantList{plugin.pluginId(), QStringLiteral("KWin/Effect")}; + } + } else { + effect.configModule = configModule; + } + } + + if (shouldStore(effect)) { + m_pendingEffects << effect; + } + } + } +} + +void EffectsModel::loadPluginEffects(const KConfigGroup &kwinConfig) +{ + const auto pluginEffects = KPluginMetaData::findPlugins(QStringLiteral("kwin/effects/plugins")); + for (const KPluginMetaData &pluginEffect : pluginEffects) { + if (!pluginEffect.isValid()) { + continue; + } + EffectData effect; + effect.name = pluginEffect.name(); + effect.description = pluginEffect.description(); + effect.license = pluginEffect.license(); + effect.version = pluginEffect.version(); + effect.untranslatedCategory = pluginEffect.category(); + effect.category = translatedCategory(pluginEffect.category()); + effect.serviceName = pluginEffect.pluginId(); + effect.iconName = pluginEffect.iconName(); + effect.enabledByDefault = pluginEffect.isEnabledByDefault(); + effect.supported = true; + effect.enabledByDefaultFunction = false; + effect.internal = false; + effect.configModule = pluginEffect.value(QStringLiteral("X-KDE-ConfigModule")); + + for (int i = 0; i < pluginEffect.authors().count(); ++i) { + effect.authorName.append(pluginEffect.authors().at(i).name()); + effect.authorEmail.append(pluginEffect.authors().at(i).emailAddress()); + if (i + 1 < pluginEffect.authors().count()) { + effect.authorName.append(", "); + effect.authorEmail.append(", "); + } + } + + if (pluginEffect.rawData().contains("org.kde.kwin.effect")) { + const QJsonObject d(pluginEffect.rawData().value("org.kde.kwin.effect").toObject()); + effect.exclusiveGroup = d.value("exclusiveGroup").toString(); + effect.enabledByDefaultFunction = d.value("enabledByDefaultMethod").toBool(); + } + + effect.website = QUrl(pluginEffect.website()); + + const QString enabledKey = QStringLiteral("%1Enabled").arg(effect.serviceName); + if (kwinConfig.hasKey(enabledKey)) { + effect.status = effectStatus(kwinConfig.readEntry(effect.serviceName + "Enabled", effect.enabledByDefault)); + } else if (effect.enabledByDefaultFunction) { + effect.status = Status::EnabledUndeterminded; + } else { + effect.status = effectStatus(effect.enabledByDefault); + } + + effect.originalStatus = effect.status; + + if (shouldStore(effect)) { + m_pendingEffects << effect; + } + } +} + +void EffectsModel::load(LoadOptions options) +{ + KConfigGroup kwinConfig(KSharedConfig::openConfig("kwinrc"), QStringLiteral("Plugins")); + + m_pendingEffects.clear(); + loadBuiltInEffects(kwinConfig); + loadJavascriptEffects(kwinConfig); + loadPluginEffects(kwinConfig); + + std::sort(m_pendingEffects.begin(), m_pendingEffects.end(), + [](const EffectData &a, const EffectData &b) { + if (a.category == b.category) { + if (a.exclusiveGroup == b.exclusiveGroup) { + return a.name < b.name; + } + return a.exclusiveGroup < b.exclusiveGroup; + } + return a.category < b.category; + }); + + auto commit = [this, options] { + if (options == LoadOptions::KeepDirty) { + for (const EffectData &oldEffect : std::as_const(m_effects)) { + if (!oldEffect.changed) { + continue; + } + auto effectIt = std::find_if(m_pendingEffects.begin(), m_pendingEffects.end(), + [oldEffect](const EffectData &data) { + return data.serviceName == oldEffect.serviceName; + }); + if (effectIt == m_pendingEffects.end()) { + continue; + } + effectIt->status = oldEffect.status; + effectIt->changed = effectIt->status != effectIt->originalStatus; + } + } + + beginResetModel(); + m_effects = m_pendingEffects; + m_pendingEffects.clear(); + endResetModel(); + + Q_EMIT loaded(); + }; + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + + if (interface.isValid()) { + QStringList effectNames; + effectNames.reserve(m_pendingEffects.count()); + for (const EffectData &data : std::as_const(m_pendingEffects)) { + effectNames.append(data.serviceName); + } + + const int serial = ++m_lastSerial; + + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(interface.areEffectsSupported(effectNames), this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [=, this](QDBusPendingCallWatcher *self) { + self->deleteLater(); + + if (m_lastSerial != serial) { + return; + } + + const QDBusPendingReply> reply = *self; + if (reply.isError()) { + commit(); + return; + } + + const QList supportedValues = reply.value(); + if (supportedValues.count() != effectNames.count()) { + return; + } + + for (int i = 0; i < effectNames.size(); ++i) { + const bool supported = supportedValues.at(i); + const QString effectName = effectNames.at(i); + + auto it = std::find_if(m_pendingEffects.begin(), m_pendingEffects.end(), + [effectName](const EffectData &data) { + return data.serviceName == effectName; + }); + if (it == m_pendingEffects.end()) { + continue; + } + + if ((*it).supported != supported) { + (*it).supported = supported; + } + } + + commit(); + }); + } else { + commit(); + } +} + +void EffectsModel::setExcludeExclusiveGroups(const QStringList &exclusiveGroups) +{ + m_excludeExclusiveGroups = exclusiveGroups; +} + +void EffectsModel::setExcludeEffects(const QStringList &effects) +{ + m_excludeEffects = effects; +} + +void EffectsModel::updateEffectStatus(const QModelIndex &rowIndex, Status effectState) +{ + setData(rowIndex, static_cast(effectState), StatusRole); +} + +void EffectsModel::save() +{ + KConfigGroup kwinConfig(KSharedConfig::openConfig("kwinrc"), QStringLiteral("Plugins")); + + for (EffectData &effect : m_effects) { + if (!effect.changed) { + continue; + } + + effect.changed = false; + effect.originalStatus = effect.status; + + const QString key = effect.serviceName + QStringLiteral("Enabled"); + const bool shouldEnable = (effect.status != Status::Disabled); + const bool restoreToDefault = effect.enabledByDefaultFunction + ? effect.status == Status::EnabledUndeterminded + : shouldEnable == effect.enabledByDefault; + if (restoreToDefault) { + kwinConfig.deleteEntry(key, KConfig::Notify); + } else { + kwinConfig.writeEntry(key, shouldEnable, KConfig::Notify); + } + } + + kwinConfig.sync(); +} + +void EffectsModel::defaults(const QModelIndex &index) +{ + const auto &effect = m_effects.at(index.row()); + if (effect.enabledByDefaultFunction && effect.status != Status::EnabledUndeterminded) { + updateEffectStatus(index, Status::EnabledUndeterminded); + } else if (static_cast(effect.status) != effect.enabledByDefault) { + updateEffectStatus(index, effect.enabledByDefault ? Status::Enabled : Status::Disabled); + } +} + +void EffectsModel::defaults() +{ + for (int row = 0; row < rowCount(); ++row) { + defaults(index(row, 0)); + } +} + +bool EffectsModel::isDefaults(const QModelIndex &index) const +{ + const auto &effect = m_effects.at(index.row()); + if (effect.enabledByDefaultFunction && effect.status != Status::EnabledUndeterminded) { + return false; + } + if (static_cast(effect.status) != effect.enabledByDefault) { + return false; + } + return true; +} + +bool EffectsModel::isDefaults() const +{ + for (int row = 0; row < rowCount(); ++row) { + if (!isDefaults(index(row, 0))) { + return false; + } + } + return true; +} + +bool EffectsModel::needsSave() const +{ + return std::any_of(m_effects.constBegin(), m_effects.constEnd(), + [](const EffectData &data) { + return data.changed; + }); +} + +QModelIndex EffectsModel::findByPluginId(const QString &pluginId) const +{ + auto it = std::find_if(m_effects.constBegin(), m_effects.constEnd(), + [pluginId](const EffectData &data) { + return data.serviceName == pluginId; + }); + if (it == m_effects.constEnd()) { + return {}; + } + return index(std::distance(m_effects.constBegin(), it), 0); +} + +void EffectsModel::requestConfigure(const QModelIndex &index, QQuickItem *context) +{ + if (!index.isValid()) { + return; + } + + const EffectData &effect = m_effects.at(index.row()); + Q_ASSERT(!effect.configModule.isEmpty()); + + KCMultiDialog *dialog = new KCMultiDialog(); + dialog->addModule(KPluginMetaData(QStringLiteral("kwin/effects/configs/") + effect.configModule), effect.configArgs); + dialog->setAttribute(Qt::WA_DeleteOnClose); + + if (context && context->window()) { + dialog->winId(); // so it creates windowHandle + dialog->windowHandle()->setTransientParent(QQuickRenderControl::renderWindowFor(context->window())); + dialog->setWindowModality(Qt::WindowModal); + } + + dialog->open(); +} + +bool EffectsModel::shouldStore(const EffectData &data) const +{ + if (data.internal) { + return false; + } + + if (m_excludeExclusiveGroups.contains(data.exclusiveGroup)) { + return false; + } + + if (m_excludeEffects.contains(data.serviceName)) { + return false; + } + + if (std::any_of(m_pendingEffects.cbegin(), m_pendingEffects.cend(), [&](const EffectData &effect) { + return effect.serviceName == data.serviceName; + })) { + return false; + } + + return true; +} + +} + +#include "moc_effectsmodel.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/common/effectsmodel.h b/local/recipes/kde/kwin/source/src/kcms/common/effectsmodel.h new file mode 100644 index 0000000000..e3fd1c1dd1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/common/effectsmodel.h @@ -0,0 +1,286 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Antonis Tsiapaliokas + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +#include +#include +#include +#include + +namespace KWin +{ + +class KWIN_EXPORT EffectsModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + /** + * This enum type is used to specify data roles. + */ + enum AdditionalRoles { + /** + * The user-friendly name of the effect. + */ + NameRole = Qt::UserRole + 1, + /** + * The description of the effect. + */ + DescriptionRole, + /** + * The name of the effect's author. If there are several authors, they + * will be comma separated. + */ + AuthorNameRole, + /** + * The email of the effect's author. If there are several authors, the + * emails will be comma separated. + */ + AuthorEmailRole, + /** + * The license of the effect. + */ + LicenseRole, + /** + * The version of the effect. + */ + VersionRole, + /** + * The category of the effect. + */ + CategoryRole, + /** + * The service name(plugin name) of the effect. + */ + ServiceNameRole, + /** + * The icon name of the effect. + */ + IconNameRole, + /** + * Whether the effect is enabled or disabled. + */ + StatusRole, + /** + * Link to the home page of the effect. + */ + WebsiteRole, + /** + * Whether the effect is supported. + */ + SupportedRole, + /** + * The exclusive group of the effect. + */ + ExclusiveRole, + /** + * Whether the effect is internal. + */ + InternalRole, + /** + * Whether the effect has a KCM. + */ + ConfigurableRole, + /** + * Whether the effect is enabled by default. + */ + EnabledByDefaultRole, + /** + * Id of the effect's config module, empty if the effect has no config. + */ + ConfigModuleRole, + /** + * Whether the effect has a function to determine if the effect is enabled by default. + */ + EnabledByDefaultFunctionRole, + }; + Q_ENUM(AdditionalRoles); + + /** + * This enum type is used to specify the status of a given effect. + */ + enum class Status { + /** + * The effect is disabled. + */ + Disabled = Qt::Unchecked, + /** + * An enable function is used to determine whether the effect is enabled. + * For example, such function can be useful to disable the blur effect + * when running in a virtual machine. + */ + EnabledUndeterminded = Qt::PartiallyChecked, + /** + * The effect is enabled. + */ + Enabled = Qt::Checked + }; + + explicit EffectsModel(QObject *parent = nullptr); + + // Reimplemented from QAbstractItemModel. + QHash roleNames() const override; + QModelIndex index(int row, int column, const QModelIndex &parent = {}) const override; + QModelIndex parent(const QModelIndex &child) const override; + int rowCount(const QModelIndex &parent = {}) const override; + int columnCount(const QModelIndex &parent = {}) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + + /** + * Specify exclusive groups that the model should not store. + * + * @param exclusiveGroups A list of exclusive group strings for the model to ignore. + */ + void setExcludeExclusiveGroups(const QStringList &exclusiveGroups); + + /** + * Specify effect IDs that the model should not store. + * + * @param effects A list of effects by ServiceNameRole. + */ + void setExcludeEffects(const QStringList &effects); + + /** + * Changes the status of a given effect. + * + * @param rowIndex An effect represented by the given index. + * @param effectState The new state. + * @note In order to actually apply the change, you have to call save(). + */ + void updateEffectStatus(const QModelIndex &rowIndex, Status effectState); + + /** + * This enum type is used to specify load options. + */ + enum class LoadOptions { + None, + /** + * Do not discard unsaved changes when reloading the model. + */ + KeepDirty + }; + + /** + * Loads effects. + * + * You have to call this method in order to populate the model. + */ + void load(LoadOptions options = LoadOptions::None); + + /** + * Saves status of each modified effect. + */ + void save(); + + /** + * Resets the status of the effect to the default state. + * + * @note In order to actually apply the change, you have to call save(). + */ + void defaults(const QModelIndex &index); + + /** + * Resets the status of each effect to the default state. + * + * @note In order to actually apply the change, you have to call save(). + */ + void defaults(); + + /** + * Whether the status of the effect is its default state. + */ + bool isDefaults(const QModelIndex &index) const; + + /** + * Whether the status of each effect is its default state. + */ + bool isDefaults() const; + + /** + * Whether the model has unsaved changes. + */ + bool needsSave() const; + + /** + * Finds an effect with the given plugin id. + */ + QModelIndex findByPluginId(const QString &pluginId) const; + + /** + * Shows a configuration dialog for a given effect. + * + * @param index An effect represented by the given index. + * @param context The context in which to open configuration dialog. + */ + void requestConfigure(const QModelIndex &index, QQuickItem *context); + +Q_SIGNALS: + /** + * This signal is emitted when the model is loaded or reloaded. + * + * @see load + */ + void loaded(); + +protected: + struct EffectData + { + QString name; + QString description; + QString authorName; + QString authorEmail; + QString license; + QString version; + QString untranslatedCategory; + QString category; + QString serviceName; + QString iconName; + Status status; + Status originalStatus; + bool enabledByDefault; + bool enabledByDefaultFunction; + QUrl website; + bool supported; + QString exclusiveGroup; + bool internal; + bool changed = false; + QString configModule; + QVariantList configArgs; + }; + + /** + * Returns whether the given effect should be stored in the model. + * + * @param data The effect. + * @returns @c true if the effect should be stored, otherwise @c false. + */ + virtual bool shouldStore(const EffectData &data) const; + +private: + void loadBuiltInEffects(const KConfigGroup &kwinConfig); + void loadJavascriptEffects(const KConfigGroup &kwinConfig); + void loadPluginEffects(const KConfigGroup &kwinConfig); + + QList m_effects; + QList m_pendingEffects; + QStringList m_excludeExclusiveGroups; + QStringList m_excludeEffects; + int m_lastSerial = -1; + + Q_DISABLE_COPY(EffectsModel) +}; + +} diff --git a/local/recipes/kde/kwin/source/src/kcms/common/genericscriptedconfig.cpp b/local/recipes/kde/kwin/source/src/kcms/common/genericscriptedconfig.cpp new file mode 100644 index 0000000000..f114e5a778 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/common/genericscriptedconfig.cpp @@ -0,0 +1,197 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "genericscriptedconfig.h" + +#include "config-kwin.h" + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace KWin +{ + +QObject *GenericScriptedConfigFactory::create(const char *iface, QWidget *parentWidget, QObject *parent, const QVariantList &args) +{ + if (qstrcmp(iface, "KCModule") == 0) { + if (args.count() < 2) { + qWarning() << Q_FUNC_INFO << "expects two arguments (plugin id, package type)"; + return nullptr; + } + + const QString pluginId = args.at(0).toString(); + const QString packageType = args.at(1).toString(); + + if (packageType == QLatin1StringView("KWin/Effect")) { + return new ScriptedEffectConfig(pluginId, parentWidget, args); + } else if (packageType == QLatin1StringView("KWin/Script")) { + return new ScriptingConfig(pluginId, parentWidget, args); + } else { + qWarning() << Q_FUNC_INFO << "got unknown package type:" << packageType; + } + } + + return nullptr; +} + +GenericScriptedConfig::GenericScriptedConfig(const QString &keyword, QWidget *parent, const QVariantList &args) + : KCModule(parent, KPluginMetaData()) + , m_packageName(keyword) + , m_translator(new KLocalizedTranslator(this)) +{ + QCoreApplication::instance()->installTranslator(m_translator); +} + +GenericScriptedConfig::~GenericScriptedConfig() +{ +} + +void GenericScriptedConfig::createUi() +{ + QVBoxLayout *layout = new QVBoxLayout(widget()); + + QString packageRoot = QStandardPaths::locate(QStandardPaths::GenericDataLocation, + QLatin1String("kwin-wayland/") + typeName() + QLatin1Char('/') + m_packageName, + QStandardPaths::LocateDirectory); + if (packageRoot.isEmpty()) { + packageRoot = QStandardPaths::locate(QStandardPaths::GenericDataLocation, + QLatin1String("kwin/") + typeName() + QLatin1Char('/') + m_packageName, + QStandardPaths::LocateDirectory); + } + if (packageRoot.isEmpty()) { + layout->addWidget(new QLabel(i18nc("Error message", "Could not locate package metadata"))); + return; + } + + const KPluginMetaData metaData = KPluginMetaData::fromJsonFile(packageRoot + QLatin1String("/metadata.json")); + if (!metaData.isValid()) { + layout->addWidget(new QLabel(i18nc("Required file does not exist", "%1 does not contain a valid metadata.json file", qPrintable(packageRoot)))); + return; + } + + const QString kconfigXTFile = packageRoot + QLatin1String("/contents/config/main.xml"); + if (!QFileInfo::exists(kconfigXTFile)) { + layout->addWidget(new QLabel(i18nc("Required file does not exist", "%1 does not exist", qPrintable(kconfigXTFile)))); + return; + } + + const QString uiPath = packageRoot + QLatin1String("/contents/ui/config.ui"); + if (!QFileInfo::exists(uiPath)) { + layout->addWidget(new QLabel(i18nc("Required file does not exist", "%1 does not exist", qPrintable(uiPath)))); + return; + } + + const QString localePath = packageRoot + QLatin1String("/contents/locale"); + if (QFileInfo::exists(localePath)) { + KLocalizedString::addDomainLocaleDir(metaData.value("X-KWin-Config-TranslationDomain").toUtf8(), localePath); + } + + QFile xmlFile(kconfigXTFile); + KConfigGroup cg = configGroup(); + KConfigLoader *configLoader = new KConfigLoader(cg, &xmlFile, this); + // load the ui file + QUiLoader *loader = new QUiLoader(this); + loader->setLanguageChangeEnabled(true); + QFile uiFile(uiPath); + m_translator->setTranslationDomain(metaData.value("X-KWin-Config-TranslationDomain")); + + uiFile.open(QFile::ReadOnly); + QWidget *customConfigForm = loader->load(&uiFile, widget()); + + if (!customConfigForm) { + auto errorLabel = new QLabel(i18n("Error loading widget from %1: %2", uiPath, loader->errorString())); + layout->addWidget(errorLabel); + return; + } + + m_translator->addContextToMonitor(customConfigForm->objectName()); + uiFile.close(); + + // send a custom event to the translator to retranslate using our translator + QEvent le(QEvent::LanguageChange); + QCoreApplication::sendEvent(customConfigForm, &le); + + layout->addWidget(customConfigForm); + addConfig(configLoader, customConfigForm); +} + +void GenericScriptedConfig::save() +{ + KCModule::save(); + reload(); +} + +void GenericScriptedConfig::reload() +{ +} + +ScriptedEffectConfig::ScriptedEffectConfig(const QString &keyword, QWidget *parent, const QVariantList &args) + : GenericScriptedConfig(keyword, parent, args) +{ + createUi(); +} + +ScriptedEffectConfig::~ScriptedEffectConfig() +{ +} + +QString ScriptedEffectConfig::typeName() const +{ + return QStringLiteral("effects"); +} + +KConfigGroup ScriptedEffectConfig::configGroup() +{ + return KSharedConfig::openConfig(KWIN_CONFIG)->group(QLatin1String("Effect-") + packageName()); +} + +void ScriptedEffectConfig::reload() +{ + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(packageName()); +} + +ScriptingConfig::ScriptingConfig(const QString &keyword, QWidget *parent, const QVariantList &args) + : GenericScriptedConfig(keyword, parent, args) +{ + createUi(); +} + +ScriptingConfig::~ScriptingConfig() +{ +} + +KConfigGroup ScriptingConfig::configGroup() +{ + return KSharedConfig::openConfig(KWIN_CONFIG)->group(QLatin1String("Script-") + packageName()); +} + +QString ScriptingConfig::typeName() const +{ + return QStringLiteral("scripts"); +} + +void ScriptingConfig::reload() +{ + // TODO: what to call +} + +} // namespace + +#include "moc_genericscriptedconfig.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/common/genericscriptedconfig.h b/local/recipes/kde/kwin/source/src/kcms/common/genericscriptedconfig.h new file mode 100644 index 0000000000..e2528778a1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/common/genericscriptedconfig.h @@ -0,0 +1,85 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +class KLocalizedTranslator; + +namespace KWin +{ + +class GenericScriptedConfigFactory : public KPluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.kde.KPluginFactory" FILE "genericscriptedconfig.json") + Q_INTERFACES(KPluginFactory) + +protected: + QObject *create(const char *iface, QWidget *parentWidget, QObject *parent, const QVariantList &args) override; +}; + +class GenericScriptedConfig : public KCModule +{ + Q_OBJECT + +public: + GenericScriptedConfig(const QString &keyword, QWidget *parent, const QVariantList &args); + ~GenericScriptedConfig() override; + +public Q_SLOTS: + void save() override; + +protected: + const QString &packageName() const; + void createUi(); + virtual QString typeName() const = 0; + virtual KConfigGroup configGroup() = 0; + virtual void reload(); + +private: + QString m_packageName; + KLocalizedTranslator *m_translator; +}; + +class ScriptedEffectConfig : public GenericScriptedConfig +{ + Q_OBJECT +public: + ScriptedEffectConfig(const QString &keyword, QWidget *parent, const QVariantList &args); + ~ScriptedEffectConfig() override; + +protected: + QString typeName() const override; + KConfigGroup configGroup() override; + void reload() override; +}; + +class ScriptingConfig : public GenericScriptedConfig +{ + Q_OBJECT +public: + ScriptingConfig(const QString &keyword, QWidget *parent, const QVariantList &args); + ~ScriptingConfig() override; + +protected: + QString typeName() const override; + KConfigGroup configGroup() override; + void reload() override; +}; + +inline const QString &GenericScriptedConfig::packageName() const +{ + return m_packageName; +} + +} diff --git a/local/recipes/kde/kwin/source/src/kcms/common/genericscriptedconfig.json b/local/recipes/kde/kwin/source/src/kcms/common/genericscriptedconfig.json new file mode 100644 index 0000000000..fa43a2e0c3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/common/genericscriptedconfig.json @@ -0,0 +1,4 @@ +{ + "Type": "Service", + "X-KDE-Library": "kcm_kwin4_genericscripted" +} diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/CMakeLists.txt b/local/recipes/kde/kwin/source/src/kcms/decoration/CMakeLists.txt new file mode 100644 index 0000000000..208d119b73 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/CMakeLists.txt @@ -0,0 +1,54 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_kwindecoration\") + +add_subdirectory(declarative-plugin) + +set(kcmkwindecoration_SRCS + declarative-plugin/buttonsmodel.cpp + decorationmodel.cpp + kcm.cpp + utils.cpp +) + +kcmutils_generate_module_data( + kcmkwindecoration_SRCS + MODULE_DATA_HEADER kwindecorationdata.h + MODULE_DATA_CLASS_NAME KWinDecorationData + SETTINGS_HEADERS kwindecorationsettings.h + SETTINGS_CLASSES KWinDecorationSettings +) + +kconfig_add_kcfg_files(kcmkwindecoration_SRCS kwindecorationsettings.kcfgc GENERATE_MOC) + +kcmutils_add_qml_kcm(kcm_kwindecoration SOURCES ${kcmkwindecoration_SRCS}) + +target_link_libraries(kcm_kwindecoration PRIVATE + KDecoration3::KDecoration + KF6::I18n + KF6::KCMUtils + KF6::KCMUtilsQuick + Qt::Quick + Qt::DBus +) + +set(kwin-applywindowdecoration_SRCS + kwin-applywindowdecoration.cpp + decorationmodel.cpp + utils.cpp +) + +kconfig_add_kcfg_files(kwin-applywindowdecoration_SRCS kwindecorationsettings.kcfgc GENERATE_MOC) +add_executable(kwin-applywindowdecoration ${kwin-applywindowdecoration_SRCS}) + +target_link_libraries(kwin-applywindowdecoration + KDecoration3::KDecoration + Qt::DBus + KF6::I18n + KF6::KCMUtils +) + +configure_file(window-decorations.knsrc.cmake ${CMAKE_CURRENT_BINARY_DIR}/window-decorations.knsrc) + +install(FILES kwindecorationsettings.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/window-decorations.knsrc DESTINATION ${KDE_INSTALL_KNSRCDIR}) +install(TARGETS kwin-applywindowdecoration DESTINATION ${KDE_INSTALL_LIBEXECDIR}) diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/Messages.sh b/local/recipes/kde/kwin/source/src/kcms/decoration/Messages.sh new file mode 100644 index 0000000000..97ccd22454 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name "*.cpp" -o -name "*.qml"` -o $podir/kcm_kwindecoration.pot diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/CMakeLists.txt b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/CMakeLists.txt new file mode 100644 index 0000000000..9abe9841dc --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/CMakeLists.txt @@ -0,0 +1,27 @@ +ecm_add_qml_module(kdecorationprivatedeclarative URI org.kde.kwin.private.kdecoration DEPENDENCIES QtCore QtQuick GENERATE_PLUGIN_SOURCE) + +target_sources(kdecorationprivatedeclarative PRIVATE + previewbutton.cpp + previewbridge.cpp + previewclient.cpp + previewitem.cpp + previewsettings.cpp + buttonsmodel.cpp + ../../../decorations/decorationpalette.cpp + ../../../decorations/decorations_logging.cpp + types.h +) + +target_link_libraries(kdecorationprivatedeclarative PRIVATE + KDecoration3::KDecoration + KDecoration3::KDecoration3Private + Qt::DBus + Qt::Quick + KF6::CoreAddons + KF6::KCMUtils + KF6::I18n + KF6::Service + KF6::ColorScheme +) + +ecm_finalize_qml_module(kdecorationprivatedeclarative) diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/buttonsmodel.cpp b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/buttonsmodel.cpp new file mode 100644 index 0000000000..526f2aa3e0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/buttonsmodel.cpp @@ -0,0 +1,189 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "buttonsmodel.h" + +#include + +#include + +namespace KDecoration3 +{ + +namespace Preview +{ + +ButtonsModel::ButtonsModel(const QList &buttons, QObject *parent) + : QAbstractListModel(parent) + , m_buttons(buttons) +{ +} + +ButtonsModel::ButtonsModel(QObject *parent) + : ButtonsModel(QList({ + DecorationButtonType::Menu, + DecorationButtonType::ApplicationMenu, + DecorationButtonType::OnAllDesktops, + DecorationButtonType::Minimize, + DecorationButtonType::Maximize, + DecorationButtonType::Close, + DecorationButtonType::ContextHelp, + DecorationButtonType::KeepBelow, + DecorationButtonType::KeepAbove, + DecorationButtonType::ExcludeFromCapture, + DecorationButtonType::Spacer, + }), + parent) +{ +} + +ButtonsModel::~ButtonsModel() = default; + +int ButtonsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_buttons.count(); +} + +static QString buttonToName(DecorationButtonType type) +{ + switch (type) { + case DecorationButtonType::Menu: + return i18n("Window menu"); + case DecorationButtonType::ApplicationMenu: + return i18n("Application menu"); + case DecorationButtonType::OnAllDesktops: + return i18n("On all desktops"); + case DecorationButtonType::Minimize: + return i18n("Minimize"); + case DecorationButtonType::Maximize: + return i18n("Maximize"); + case DecorationButtonType::Close: + return i18n("Close"); + case DecorationButtonType::ContextHelp: + return i18n("Context help"); + case DecorationButtonType::KeepBelow: + return i18n("Keep below other windows"); + case DecorationButtonType::KeepAbove: + return i18n("Keep above other windows"); + case DecorationButtonType::ExcludeFromCapture: + return i18n("Hide from screencast"); + case DecorationButtonType::Spacer: + return i18n("Spacer"); + default: + return QString(); + } +} + +QVariant ButtonsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_buttons.count() || index.column() != 0) { + return QVariant(); + } + switch (role) { + case Qt::DisplayRole: + return buttonToName(m_buttons.at(index.row())); + case Qt::UserRole: + return QVariant::fromValue(int(m_buttons.at(index.row()))); + } + return QVariant(); +} + +QHash ButtonsModel::roleNames() const +{ + QHash roles; + roles.insert(Qt::DisplayRole, QByteArrayLiteral("display")); + roles.insert(Qt::UserRole, QByteArrayLiteral("button")); + return roles; +} + +void ButtonsModel::remove(int row) +{ + if (row < 0 || row >= m_buttons.count()) { + return; + } + beginRemoveRows(QModelIndex(), row, row); + m_buttons.removeAt(row); + endRemoveRows(); +} + +void ButtonsModel::down(int index) +{ + if (m_buttons.count() < 2 || index == m_buttons.count() - 1) { + return; + } + beginMoveRows(QModelIndex(), index, index, QModelIndex(), index + 2); + m_buttons.insert(index + 1, m_buttons.takeAt(index)); + endMoveRows(); +} + +void ButtonsModel::up(int index) +{ + if (m_buttons.count() < 2 || index == 0) { + return; + } + beginMoveRows(QModelIndex(), index, index, QModelIndex(), index - 1); + m_buttons.insert(index - 1, m_buttons.takeAt(index)); + endMoveRows(); +} + +void ButtonsModel::add(DecorationButtonType type) +{ + beginInsertRows(QModelIndex(), m_buttons.count(), m_buttons.count()); + m_buttons.append(type); + endInsertRows(); +} + +void ButtonsModel::add(int index, int type) +{ + beginInsertRows(QModelIndex(), index, index); + m_buttons.insert(index, KDecoration3::DecorationButtonType(type)); + endInsertRows(); +} + +void ButtonsModel::move(int sourceIndex, int targetIndex) +{ + if (sourceIndex == std::max(0, targetIndex)) { + return; + } + + /* When moving an item down, the destination index needs to be incremented + by one, as explained in the documentation: + https://doc.qt.io/qt-5/qabstractitemmodel.html#beginMoveRows */ + if (targetIndex > sourceIndex) { + // Row will be moved down + beginMoveRows(QModelIndex(), sourceIndex, sourceIndex, QModelIndex(), targetIndex + 1); + } else { + beginMoveRows(QModelIndex(), sourceIndex, sourceIndex, QModelIndex(), std::max(0, targetIndex)); + } + + m_buttons.move(sourceIndex, std::max(0, targetIndex)); + endMoveRows(); +} + +void ButtonsModel::clear() +{ + beginResetModel(); + m_buttons.clear(); + endResetModel(); +} + +void ButtonsModel::replace(const QList &buttons) +{ + if (buttons.isEmpty()) { + return; + } + + beginResetModel(); + m_buttons = buttons; + endResetModel(); +} + +} +} + +#include "moc_buttonsmodel.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/buttonsmodel.h b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/buttonsmodel.h new file mode 100644 index 0000000000..c51fc0282f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/buttonsmodel.h @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#pragma once + +#include +#include + +#include + +namespace KDecoration3 +{ + +namespace Preview +{ +class PreviewBridge; + +class ButtonsModel : public QAbstractListModel +{ + Q_OBJECT + QML_ELEMENT +public: + explicit ButtonsModel(const QList &buttons, QObject *parent = nullptr); + explicit ButtonsModel(QObject *parent = nullptr); + ~ButtonsModel() override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + + QList buttons() const + { + return m_buttons; + } + + Q_INVOKABLE void clear(); + Q_INVOKABLE void remove(int index); + Q_INVOKABLE void up(int index); + Q_INVOKABLE void down(int index); + Q_INVOKABLE void move(int sourceIndex, int targetIndex); + + void replace(const QList &buttons); + void add(DecorationButtonType type); + Q_INVOKABLE void add(int index, int type); + +private: + QList m_buttons; +}; + +} +} diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewbridge.cpp b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewbridge.cpp new file mode 100644 index 0000000000..92bd0205ed --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewbridge.cpp @@ -0,0 +1,231 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "previewbridge.h" +#include "previewclient.h" +#include "previewitem.h" +#include "previewsettings.h" + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KDecoration3 +{ +namespace Preview +{ + +static const QString s_pluginName = QStringLiteral("org.kde.kdecoration3"); +static const QString s_kcmName = QStringLiteral("org.kde.kdecoration3.kcm"); + +PreviewBridge::PreviewBridge(QObject *parent) + : DecorationBridge(parent) + , m_lastCreatedClient(nullptr) + , m_lastCreatedSettings(nullptr) + , m_valid(false) +{ + connect(this, &PreviewBridge::pluginChanged, this, &PreviewBridge::createFactory); +} + +PreviewBridge::~PreviewBridge() = default; + +std::unique_ptr PreviewBridge::createClient(DecoratedWindow *client, Decoration *decoration) +{ + auto ptr = std::unique_ptr(new PreviewClient(client, decoration)); + m_lastCreatedClient = ptr.get(); + return ptr; +} + +std::unique_ptr PreviewBridge::settings(DecorationSettings *parent) +{ + auto ptr = std::unique_ptr(new PreviewSettings(parent)); + m_lastCreatedSettings = ptr.get(); + return ptr; +} + +void PreviewBridge::registerPreviewItem(PreviewItem *item) +{ + m_previewItems.append(item); +} + +void PreviewBridge::unregisterPreviewItem(PreviewItem *item) +{ + m_previewItems.removeAll(item); +} + +void PreviewBridge::setPlugin(const QString &plugin) +{ + if (m_plugin == plugin) { + return; + } + m_plugin = plugin; + Q_EMIT pluginChanged(); +} + +QString PreviewBridge::theme() const +{ + return m_theme; +} + +void PreviewBridge::setTheme(const QString &theme) +{ + if (m_theme == theme) { + return; + } + m_theme = theme; + Q_EMIT themeChanged(); +} + +QString PreviewBridge::kcmoduleName() const +{ + return m_kcmoduleName; +} + +void PreviewBridge::setKcmoduleName(const QString &kcmoduleName) +{ + if (m_kcmoduleName == kcmoduleName) { + return; + } + m_kcmoduleName = kcmoduleName; + Q_EMIT themeChanged(); +} + +QString PreviewBridge::plugin() const +{ + return m_plugin; +} + +void PreviewBridge::createFactory() +{ + m_factory.clear(); + + if (m_plugin.isNull()) { + setValid(false); + qWarning() << "Plugin not set"; + return; + } + + const auto offers = KPluginMetaData::findPlugins(s_pluginName); + auto item = std::find_if(offers.constBegin(), offers.constEnd(), [this](const auto &plugin) { + return plugin.pluginId() == m_plugin; + }); + if (item != offers.constEnd()) { + m_factory = KPluginFactory::loadFactory(*item).plugin; + } + + setValid(!m_factory.isNull()); +} + +bool PreviewBridge::isValid() const +{ + return m_valid; +} + +void PreviewBridge::setValid(bool valid) +{ + if (m_valid == valid) { + return; + } + m_valid = valid; + Q_EMIT validChanged(); +} + +Decoration *PreviewBridge::createDecoration(QObject *parent) +{ + if (!m_valid) { + return nullptr; + } + QVariantMap args({{QStringLiteral("bridge"), QVariant::fromValue(this)}}); + if (!m_theme.isNull()) { + args.insert(QStringLiteral("theme"), m_theme); + } + return m_factory->create(parent, QVariantList({args})); +} + +DecorationButton *PreviewBridge::createButton(KDecoration3::Decoration *decoration, KDecoration3::DecorationButtonType type, QObject *parent) +{ + if (!m_valid) { + return nullptr; + } + return m_factory->create(parent, QVariantList({QVariant::fromValue(type), QVariant::fromValue(decoration)})); +} + +void PreviewBridge::configure(QQuickItem *ctx) +{ + if (!m_valid) { + qWarning() << "Cannot show an invalid decoration's configuration dialog"; + return; + } + + KCMultiDialog *dialog = new KCMultiDialog; + dialog->setAttribute(Qt::WA_DeleteOnClose); + + if (m_lastCreatedClient) { + dialog->setWindowTitle(m_lastCreatedClient->caption()); + } + + QVariantMap args; + if (!m_theme.isNull()) { + args.insert(QStringLiteral("theme"), m_theme); + } + Q_ASSERT(!m_kcmoduleName.isEmpty()); + + dialog->addModule(KPluginMetaData(s_kcmName + QLatin1Char('/') + m_kcmoduleName), {args}); + + connect(dialog, &KCMultiDialog::configCommitted, this, [this] { + if (m_lastCreatedSettings) { + Q_EMIT m_lastCreatedSettings->decorationSettings()->reconfigured(); + } + // Send signal to all kwin instances + QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/KWin"), + QStringLiteral("org.kde.KWin"), + QStringLiteral("reloadConfig")); + QDBusConnection::sessionBus().send(message); + }); + + if (ctx->window()) { + dialog->winId(); // so it creates windowHandle + dialog->windowHandle()->setTransientParent(QQuickRenderControl::renderWindowFor(ctx->window())); + dialog->setModal(true); + } + + dialog->show(); +} + +BridgeItem::BridgeItem(QObject *parent) + : QObject(parent) + , m_bridge(new PreviewBridge()) +{ + connect(m_bridge, &PreviewBridge::themeChanged, this, &BridgeItem::themeChanged); + connect(m_bridge, &PreviewBridge::pluginChanged, this, &BridgeItem::pluginChanged); + connect(m_bridge, &PreviewBridge::validChanged, this, &BridgeItem::validChanged); + connect(m_bridge, &PreviewBridge::kcmoduleNameChanged, this, &BridgeItem::kcmoduleNameChanged); +} + +BridgeItem::~BridgeItem() +{ + m_bridge->deleteLater(); +} + +} +} + +#include "moc_previewbridge.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewbridge.h b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewbridge.h new file mode 100644 index 0000000000..670ca75c35 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewbridge.h @@ -0,0 +1,149 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#pragma once + +#include +#include + +#include +#include +#include + +class QQuickItem; + +class KPluginFactory; + +namespace KDecoration3 +{ +namespace Preview +{ + +class PreviewClient; +class PreviewItem; +class PreviewSettings; + +class PreviewBridge : public KDecoration3::DecorationBridge +{ + Q_OBJECT + QML_ANONYMOUS + + Q_PROPERTY(QString plugin READ plugin WRITE setPlugin NOTIFY pluginChanged) + Q_PROPERTY(QString theme READ theme WRITE setTheme NOTIFY themeChanged) + Q_PROPERTY(QString kcmoduleName READ kcmoduleName WRITE setKcmoduleName NOTIFY kcmoduleNameChanged) + Q_PROPERTY(bool valid READ isValid NOTIFY validChanged) +public: + explicit PreviewBridge(QObject *parent = nullptr); + ~PreviewBridge() override; + std::unique_ptr createClient(DecoratedWindow *client, Decoration *decoration) override; + std::unique_ptr settings(DecorationSettings *parent) override; + + PreviewClient *lastCreatedClient() + { + return m_lastCreatedClient; + } + PreviewSettings *lastCreatedSettings() + { + return m_lastCreatedSettings; + } + + void registerPreviewItem(PreviewItem *item); + void unregisterPreviewItem(PreviewItem *item); + + void setPlugin(const QString &plugin); + QString plugin() const; + void setKcmoduleName(const QString &name); + QString kcmoduleName() const; + void setTheme(const QString &theme); + QString theme() const; + bool isValid() const; + + KDecoration3::Decoration *createDecoration(QObject *parent = nullptr); + KDecoration3::DecorationButton *createButton(KDecoration3::Decoration *decoration, KDecoration3::DecorationButtonType type, QObject *parent = nullptr); + +public Q_SLOTS: + void configure(QQuickItem *ctx); + +Q_SIGNALS: + void pluginChanged(); + void themeChanged(); + void validChanged(); + void kcmoduleNameChanged(); + +private: + void createFactory(); + void setValid(bool valid); + PreviewClient *m_lastCreatedClient; + PreviewSettings *m_lastCreatedSettings; + QList m_previewItems; + QString m_plugin; + QString m_theme; + QString m_kcmoduleName; + QPointer m_factory; + bool m_valid; +}; + +class BridgeItem : public QObject +{ + Q_OBJECT + QML_NAMED_ELEMENT(Bridge) + Q_PROPERTY(QString plugin READ plugin WRITE setPlugin NOTIFY pluginChanged) + Q_PROPERTY(QString theme READ theme WRITE setTheme NOTIFY themeChanged) + Q_PROPERTY(QString kcmoduleName READ kcmoduleName WRITE setKcmoduleName NOTIFY kcmoduleNameChanged) + Q_PROPERTY(bool valid READ isValid NOTIFY validChanged) + Q_PROPERTY(KDecoration3::Preview::PreviewBridge *bridge READ bridge CONSTANT) + +public: + explicit BridgeItem(QObject *parent = nullptr); + ~BridgeItem() override; + + void setPlugin(const QString &plugin) + { + m_bridge->setPlugin(plugin); + } + QString plugin() const + { + return m_bridge->plugin(); + } + void setTheme(const QString &theme) + { + m_bridge->setTheme(theme); + } + QString kcmoduleName() const + { + return m_bridge->kcmoduleName(); + } + void setKcmoduleName(const QString &name) + { + m_bridge->setKcmoduleName(name); + } + QString theme() const + { + return m_bridge->theme(); + } + bool isValid() const + { + return m_bridge->isValid(); + } + + PreviewBridge *bridge() const + { + return m_bridge; + } + +Q_SIGNALS: + void pluginChanged(); + void themeChanged(); + void kcmoduleNameChanged(); + void validChanged(); + +private: + PreviewBridge *m_bridge; +}; + +} +} + +Q_DECLARE_METATYPE(KDecoration3::Preview::PreviewBridge *) diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewbutton.cpp b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewbutton.cpp new file mode 100644 index 0000000000..5b5be11bf0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewbutton.cpp @@ -0,0 +1,149 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "previewbutton.h" +#include "previewbridge.h" +#include "previewclient.h" +#include "previewsettings.h" + +#include + +#include + +namespace KDecoration3 +{ + +namespace Preview +{ + +PreviewButtonItem::PreviewButtonItem(QQuickItem *parent) + : QQuickPaintedItem(parent) +{ +} + +PreviewButtonItem::~PreviewButtonItem() = default; + +void PreviewButtonItem::setType(int type) +{ + setType(KDecoration3::DecorationButtonType(type)); +} + +void PreviewButtonItem::setType(KDecoration3::DecorationButtonType type) +{ + if (m_type == type) { + return; + } + m_type = type; + Q_EMIT typeChanged(); +} + +KDecoration3::DecorationButtonType PreviewButtonItem::type() const +{ + return m_type; +} + +PreviewBridge *PreviewButtonItem::bridge() const +{ + return m_bridge.data(); +} + +void PreviewButtonItem::setBridge(PreviewBridge *bridge) +{ + if (m_bridge == bridge) { + return; + } + m_bridge = bridge; + Q_EMIT bridgeChanged(); +} + +Settings *PreviewButtonItem::settings() const +{ + return m_settings.data(); +} + +void PreviewButtonItem::setSettings(Settings *settings) +{ + if (m_settings == settings) { + return; + } + m_settings = settings; + Q_EMIT settingsChanged(); +} + +int PreviewButtonItem::typeAsInt() const +{ + return int(m_type); +} + +void PreviewButtonItem::componentComplete() +{ + QQuickPaintedItem::componentComplete(); + createButton(); +} + +void PreviewButtonItem::createButton() +{ + if (m_type == KDecoration3::DecorationButtonType::Custom || m_decoration || !m_settings || !m_bridge) { + return; + } + m_decoration = m_bridge->createDecoration(this); + if (!m_decoration) { + return; + } + auto client = m_bridge->lastCreatedClient(); + client->setMinimizable(true); + client->setMaximizable(true); + client->setActive(false); + client->setProvidesContextHelp(true); + m_decoration->setSettings(m_settings->settings()); + m_decoration->create(); + m_decoration->init(); + m_decoration->apply(m_decoration->nextState()->clone()); + m_button = m_bridge->createButton(m_decoration, m_type); + connect(this, &PreviewButtonItem::widthChanged, this, &PreviewButtonItem::syncGeometry); + connect(this, &PreviewButtonItem::heightChanged, this, &PreviewButtonItem::syncGeometry); + syncGeometry(); +} + +void PreviewButtonItem::syncGeometry() +{ + if (!m_button) { + return; + } + m_button->setGeometry(QRect(0, 0, width(), height())); +} + +void PreviewButtonItem::paint(QPainter *painter) +{ + if (!m_button) { + return; + } + + const QRect rect(0, 0, width(), height()); + if (type() == KDecoration3::DecorationButtonType::Spacer) { + static const QIcon icon = QIcon::fromTheme(QStringLiteral("distribute-horizontal")); + icon.paint(painter, rect); + } else if (type() == KDecoration3::DecorationButtonType::ExcludeFromCapture) { + static const QIcon icon = QIcon::fromTheme(QStringLiteral("view-private")); + icon.paint(painter, rect); + } else { + m_button->paint(painter, rect); + } + + painter->setCompositionMode(QPainter::CompositionMode_SourceAtop); + painter->fillRect(rect, m_color); +} + +void PreviewButtonItem::setColor(const QColor &color) +{ + m_color = color; + m_color.setAlpha(127); + update(); +} + +} +} + +#include "moc_previewbutton.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewbutton.h b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewbutton.h new file mode 100644 index 0000000000..d5ebac9dcd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewbutton.h @@ -0,0 +1,73 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#pragma once + +#include +#include +#include +#include + +namespace KDecoration3 +{ +class Decoration; + +namespace Preview +{ +class PreviewBridge; +class Settings; + +class PreviewButtonItem : public QQuickPaintedItem +{ + Q_OBJECT + QML_NAMED_ELEMENT(Button) + Q_PROPERTY(KDecoration3::Preview::PreviewBridge *bridge READ bridge WRITE setBridge NOTIFY bridgeChanged) + Q_PROPERTY(KDecoration3::Preview::Settings *settings READ settings WRITE setSettings NOTIFY settingsChanged) + Q_PROPERTY(int type READ typeAsInt WRITE setType NOTIFY typeChanged) + Q_PROPERTY(QColor color READ color WRITE setColor) + +public: + explicit PreviewButtonItem(QQuickItem *parent = nullptr); + ~PreviewButtonItem() override; + void paint(QPainter *painter) override; + + PreviewBridge *bridge() const; + void setBridge(PreviewBridge *bridge); + + Settings *settings() const; + void setSettings(Settings *settings); + + KDecoration3::DecorationButtonType type() const; + int typeAsInt() const; + void setType(KDecoration3::DecorationButtonType type); + void setType(int type); + + const QColor &color() const + { + return m_color; + } + void setColor(const QColor &color); + +Q_SIGNALS: + void bridgeChanged(); + void typeChanged(); + void settingsChanged(); + +protected: + void componentComplete() override; + +private: + void createButton(); + void syncGeometry(); + QColor m_color; + QPointer m_bridge; + QPointer m_settings; + KDecoration3::Decoration *m_decoration = nullptr; + KDecoration3::DecorationButton *m_button = nullptr; + KDecoration3::DecorationButtonType m_type = KDecoration3::DecorationButtonType::Custom; +}; + +} // Preview +} // KDecoration3 diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewclient.cpp b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewclient.cpp new file mode 100644 index 0000000000..6e7001554a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewclient.cpp @@ -0,0 +1,458 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "previewclient.h" +#include +#include + +#include +#include +#include +#include + +namespace KDecoration3 +{ +namespace Preview +{ + +PreviewClient::PreviewClient(DecoratedWindow *c, Decoration *decoration) + : QObject(decoration) + , DecoratedWindowPrivateV4(c, decoration) + , m_icon(QIcon::fromTheme(QStringLiteral("start-here-kde"))) + , m_iconName(m_icon.name()) + , m_palette(QStringLiteral("kdeglobals")) + , m_active(true) + , m_closeable(true) + , m_keepBelow(false) + , m_keepAbove(false) + , m_excludeFromCapture(false) + , m_maximizable(true) + , m_maximizedHorizontally(false) + , m_maximizedVertically(false) + , m_minimizable(true) + , m_modal(false) + , m_movable(true) + , m_resizable(true) + , m_shadeable(true) + , m_shaded(false) + , m_providesContextHelp(false) + , m_onAllDesktops(false) + , m_width(0) + , m_height(0) + , m_bordersTopEdge(false) + , m_bordersLeftEdge(false) + , m_bordersRightEdge(false) + , m_bordersBottomEdge(false) +{ + connect(this, &PreviewClient::captionChanged, c, &DecoratedWindow::captionChanged); + connect(this, &PreviewClient::activeChanged, c, &DecoratedWindow::activeChanged); + connect(this, &PreviewClient::closeableChanged, c, &DecoratedWindow::closeableChanged); + connect(this, &PreviewClient::keepAboveChanged, c, &DecoratedWindow::keepAboveChanged); + connect(this, &PreviewClient::keepBelowChanged, c, &DecoratedWindow::keepBelowChanged); + connect(this, &PreviewClient::excludeFromCaptureChanged, c, &DecoratedWindow::excludeFromCaptureChanged); + connect(this, &PreviewClient::maximizableChanged, c, &DecoratedWindow::maximizeableChanged); + connect(this, &PreviewClient::maximizedChanged, c, &DecoratedWindow::maximizedChanged); + connect(this, &PreviewClient::maximizedVerticallyChanged, c, &DecoratedWindow::maximizedVerticallyChanged); + connect(this, &PreviewClient::maximizedHorizontallyChanged, c, &DecoratedWindow::maximizedHorizontallyChanged); + connect(this, &PreviewClient::minimizableChanged, c, &DecoratedWindow::minimizeableChanged); + connect(this, &PreviewClient::movableChanged, c, &DecoratedWindow::moveableChanged); + connect(this, &PreviewClient::onAllDesktopsChanged, c, &DecoratedWindow::onAllDesktopsChanged); + connect(this, &PreviewClient::resizableChanged, c, &DecoratedWindow::resizeableChanged); + connect(this, &PreviewClient::shadeableChanged, c, &DecoratedWindow::shadeableChanged); + connect(this, &PreviewClient::shadedChanged, c, &DecoratedWindow::shadedChanged); + connect(this, &PreviewClient::providesContextHelpChanged, c, &DecoratedWindow::providesContextHelpChanged); + connect(this, &PreviewClient::widthChanged, c, &DecoratedWindow::widthChanged); + connect(this, &PreviewClient::heightChanged, c, &DecoratedWindow::heightChanged); + connect(this, &PreviewClient::iconChanged, c, &DecoratedWindow::iconChanged); + connect(this, &PreviewClient::paletteChanged, c, &DecoratedWindow::paletteChanged); + connect(this, &PreviewClient::maximizedVerticallyChanged, this, [this]() { + Q_EMIT maximizedChanged(isMaximized()); + }); + connect(this, &PreviewClient::maximizedHorizontallyChanged, this, [this]() { + Q_EMIT maximizedChanged(isMaximized()); + }); + connect(this, &PreviewClient::iconNameChanged, this, [this]() { + m_icon = QIcon::fromTheme(m_iconName); + Q_EMIT iconChanged(m_icon); + }); + connect(&m_palette, &KWin::Decoration::DecorationPalette::changed, this, [this]() { + Q_EMIT paletteChanged(m_palette.palette()); + }); + auto emitEdgesChanged = [this, c]() { + Q_EMIT c->adjacentScreenEdgesChanged(adjacentScreenEdges()); + }; + connect(this, &PreviewClient::bordersTopEdgeChanged, this, emitEdgesChanged); + connect(this, &PreviewClient::bordersLeftEdgeChanged, this, emitEdgesChanged); + connect(this, &PreviewClient::bordersRightEdgeChanged, this, emitEdgesChanged); + connect(this, &PreviewClient::bordersBottomEdgeChanged, this, emitEdgesChanged); + auto emitSizeChanged = [c]() { + Q_EMIT c->sizeChanged(c->size()); + }; + connect(this, &PreviewClient::widthChanged, this, emitSizeChanged); + connect(this, &PreviewClient::heightChanged, this, emitSizeChanged); + + qApp->installEventFilter(this); +} + +PreviewClient::~PreviewClient() = default; + +void PreviewClient::setIcon(const QIcon &pixmap) +{ + m_icon = pixmap; + Q_EMIT iconChanged(m_icon); +} + +qreal PreviewClient::width() const +{ + return m_width; +} + +qreal PreviewClient::height() const +{ + return m_height; +} + +QSizeF PreviewClient::size() const +{ + return QSize(m_width, m_height); +} + +QString PreviewClient::caption() const +{ + return m_caption; +} + +QIcon PreviewClient::icon() const +{ + return m_icon; +} + +QString PreviewClient::iconName() const +{ + return m_iconName; +} + +bool PreviewClient::isActive() const +{ + return m_active; +} + +bool PreviewClient::isCloseable() const +{ + return m_closeable; +} + +bool PreviewClient::isKeepAbove() const +{ + return m_keepAbove; +} + +bool PreviewClient::isKeepBelow() const +{ + return m_keepBelow; +} + +bool PreviewClient::isExcludedFromCapture() const +{ + return m_excludeFromCapture; +} + +bool PreviewClient::isMaximizeable() const +{ + return m_maximizable; +} + +bool PreviewClient::isMaximized() const +{ + return isMaximizedHorizontally() && isMaximizedVertically(); +} + +bool PreviewClient::isMaximizedHorizontally() const +{ + return m_maximizedHorizontally; +} + +bool PreviewClient::isMaximizedVertically() const +{ + return m_maximizedVertically; +} + +bool PreviewClient::isMinimizeable() const +{ + return m_minimizable; +} + +bool PreviewClient::isModal() const +{ + return m_modal; +} + +bool PreviewClient::isMoveable() const +{ + return m_movable; +} + +bool PreviewClient::isOnAllDesktops() const +{ + return m_onAllDesktops; +} + +bool PreviewClient::isResizeable() const +{ + return m_resizable; +} + +bool PreviewClient::isShadeable() const +{ + return m_shadeable; +} + +bool PreviewClient::isShaded() const +{ + return m_shaded; +} + +bool PreviewClient::providesContextHelp() const +{ + return m_providesContextHelp; +} + +QPalette PreviewClient::palette() const +{ + return m_palette.palette(); +} + +QColor PreviewClient::color(ColorGroup group, ColorRole role) const +{ + return m_palette.color(group, role); +} + +Qt::Edges PreviewClient::adjacentScreenEdges() const +{ + Qt::Edges edges; + if (m_bordersBottomEdge) { + edges |= Qt::BottomEdge; + } + if (m_bordersLeftEdge) { + edges |= Qt::LeftEdge; + } + if (m_bordersRightEdge) { + edges |= Qt::RightEdge; + } + if (m_bordersTopEdge) { + edges |= Qt::TopEdge; + } + return edges; +} + +QString PreviewClient::windowClass() const +{ + return QString(); +} + +qreal PreviewClient::scale() const +{ + return 1; +} + +qreal PreviewClient::nextScale() const +{ + return 1; +} + +QString PreviewClient::applicationMenuServiceName() const +{ + return QString(); +} + +QString PreviewClient::applicationMenuObjectPath() const +{ + return QString(); +} + +bool PreviewClient::hasApplicationMenu() const +{ + return true; +} + +bool PreviewClient::isApplicationMenuActive() const +{ + return false; +} + +bool PreviewClient::bordersBottomEdge() const +{ + return m_bordersBottomEdge; +} + +bool PreviewClient::bordersLeftEdge() const +{ + return m_bordersLeftEdge; +} + +bool PreviewClient::bordersRightEdge() const +{ + return m_bordersRightEdge; +} + +bool PreviewClient::bordersTopEdge() const +{ + return m_bordersTopEdge; +} + +void PreviewClient::setBordersBottomEdge(bool enabled) +{ + if (m_bordersBottomEdge == enabled) { + return; + } + m_bordersBottomEdge = enabled; + Q_EMIT bordersBottomEdgeChanged(enabled); +} + +void PreviewClient::setBordersLeftEdge(bool enabled) +{ + if (m_bordersLeftEdge == enabled) { + return; + } + m_bordersLeftEdge = enabled; + Q_EMIT bordersLeftEdgeChanged(enabled); +} + +void PreviewClient::setBordersRightEdge(bool enabled) +{ + if (m_bordersRightEdge == enabled) { + return; + } + m_bordersRightEdge = enabled; + Q_EMIT bordersRightEdgeChanged(enabled); +} + +void PreviewClient::setBordersTopEdge(bool enabled) +{ + if (m_bordersTopEdge == enabled) { + return; + } + m_bordersTopEdge = enabled; + Q_EMIT bordersTopEdgeChanged(enabled); +} + +void PreviewClient::requestShowToolTip(const QString &text) +{ +} + +void PreviewClient::requestHideToolTip() +{ +} + +void PreviewClient::requestClose() +{ + Q_EMIT closeRequested(); +} + +void PreviewClient::requestContextHelp() +{ +} + +void PreviewClient::requestToggleMaximization(Qt::MouseButtons buttons) +{ + if (buttons.testFlag(Qt::LeftButton)) { + const bool set = !isMaximized(); + setMaximizedHorizontally(set); + setMaximizedVertically(set); + } else if (buttons.testFlag(Qt::RightButton)) { + setMaximizedHorizontally(!isMaximizedHorizontally()); + } else if (buttons.testFlag(Qt::MiddleButton)) { + setMaximizedVertically(!isMaximizedVertically()); + } +} + +void PreviewClient::requestMinimize() +{ + Q_EMIT minimizeRequested(); +} + +void PreviewClient::requestToggleKeepAbove() +{ + setKeepAbove(!isKeepAbove()); +} + +void PreviewClient::requestToggleKeepBelow() +{ + setKeepBelow(!isKeepBelow()); +} + +void PreviewClient::requestToggleExcludeFromCapture() +{ + setExcludeFromCapture(!isExcludedFromCapture()); +} + +void PreviewClient::requestShowWindowMenu(const QRect &rect) +{ + Q_EMIT showWindowMenuRequested(); +} + +void PreviewClient::requestShowApplicationMenu(const QRect &rect, int actionId) +{ +} + +void PreviewClient::showApplicationMenu(int actionId) +{ +} + +void PreviewClient::requestToggleOnAllDesktops() +{ + m_onAllDesktops = !m_onAllDesktops; + Q_EMIT onAllDesktopsChanged(m_onAllDesktops); +} + +void PreviewClient::requestToggleShade() +{ + setShaded(!isShaded()); +} + +#define SETTER(type, name, variable) \ + void PreviewClient::name(type variable) \ + { \ + if (m_##variable == variable) { \ + return; \ + } \ + m_##variable = variable; \ + Q_EMIT variable##Changed(m_##variable); \ + } + +#define SETTER2(name, variable) SETTER(bool, name, variable) + +SETTER(const QString &, setCaption, caption) +SETTER(const QString &, setIconName, iconName) +SETTER(int, setWidth, width) +SETTER(int, setHeight, height) + +SETTER2(setActive, active) +SETTER2(setCloseable, closeable) +SETTER2(setMaximizable, maximizable) +SETTER2(setKeepBelow, keepBelow) +SETTER2(setKeepAbove, keepAbove) +SETTER2(setExcludeFromCapture, excludeFromCapture) +SETTER2(setMaximizedHorizontally, maximizedHorizontally) +SETTER2(setMaximizedVertically, maximizedVertically) +SETTER2(setMinimizable, minimizable) +SETTER2(setModal, modal) +SETTER2(setMovable, movable) +SETTER2(setResizable, resizable) +SETTER2(setShadeable, shadeable) +SETTER2(setShaded, shaded) +SETTER2(setProvidesContextHelp, providesContextHelp) + +#undef SETTER2 +#undef SETTER + +void PreviewClient::popup(const KDecoration3::Positioner &positioner, QMenu *menu) +{ +} + +} // namespace Preview +} // namespace KDecoration3 + +#include "moc_previewclient.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewclient.h b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewclient.h new file mode 100644 index 0000000000..c653924d23 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewclient.h @@ -0,0 +1,206 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#pragma once + +#include "../../../decorations/decorationpalette.h" + +#include +#include +#include +#include + +class QAbstractItemModel; + +namespace KDecoration3 +{ +namespace Preview +{ +class PreviewClient : public QObject, public DecoratedWindowPrivateV4 +{ + Q_OBJECT + QML_ANONYMOUS + Q_PROPERTY(KDecoration3::Decoration *decoration READ decoration CONSTANT) + Q_PROPERTY(QString caption READ caption WRITE setCaption NOTIFY captionChanged) + Q_PROPERTY(QIcon icon READ icon WRITE setIcon NOTIFY iconChanged) + Q_PROPERTY(QString iconName READ iconName WRITE setIconName NOTIFY iconNameChanged) + Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged) + Q_PROPERTY(bool closeable READ isCloseable WRITE setCloseable NOTIFY closeableChanged) + Q_PROPERTY(bool keepAbove READ isKeepAbove WRITE setKeepAbove NOTIFY keepAboveChanged) + Q_PROPERTY(bool keepBelow READ isKeepBelow WRITE setKeepBelow NOTIFY keepBelowChanged) + Q_PROPERTY(bool maximizable READ isMaximizeable WRITE setMaximizable NOTIFY maximizableChanged) + Q_PROPERTY(bool maximized READ isMaximized NOTIFY maximizedChanged) + Q_PROPERTY(bool maximizedVertically READ isMaximizedVertically WRITE setMaximizedVertically NOTIFY maximizedVerticallyChanged) + Q_PROPERTY(bool maximizedHorizontally READ isMaximizedHorizontally WRITE setMaximizedHorizontally NOTIFY maximizedHorizontallyChanged) + Q_PROPERTY(bool minimizable READ isMinimizeable WRITE setMinimizable NOTIFY minimizableChanged) + Q_PROPERTY(bool modal READ isModal WRITE setModal NOTIFY modalChanged) + Q_PROPERTY(bool movable READ isMoveable WRITE setMovable NOTIFY movableChanged) + Q_PROPERTY(bool onAllDesktops READ isOnAllDesktops NOTIFY onAllDesktopsChanged) + Q_PROPERTY(bool resizable READ isResizeable WRITE setResizable NOTIFY resizableChanged) + Q_PROPERTY(bool shadeable READ isShadeable WRITE setShadeable NOTIFY shadeableChanged) + Q_PROPERTY(bool shaded READ isShaded WRITE setShaded NOTIFY shadedChanged) + Q_PROPERTY(bool providesContextHelp READ providesContextHelp WRITE setProvidesContextHelp NOTIFY providesContextHelpChanged) + Q_PROPERTY(int width READ width WRITE setWidth NOTIFY widthChanged) + Q_PROPERTY(int height READ height WRITE setHeight NOTIFY heightChanged) + Q_PROPERTY(bool bordersTopEdge READ bordersTopEdge WRITE setBordersTopEdge NOTIFY bordersTopEdgeChanged) + Q_PROPERTY(bool bordersLeftEdge READ bordersLeftEdge WRITE setBordersLeftEdge NOTIFY bordersLeftEdgeChanged) + Q_PROPERTY(bool bordersRightEdge READ bordersRightEdge WRITE setBordersRightEdge NOTIFY bordersRightEdgeChanged) + Q_PROPERTY(bool bordersBottomEdge READ bordersBottomEdge WRITE setBordersBottomEdge NOTIFY bordersBottomEdgeChanged) +public: + explicit PreviewClient(DecoratedWindow *client, Decoration *decoration); + ~PreviewClient() override; + + QString caption() const override; + QIcon icon() const override; + bool isActive() const override; + bool isCloseable() const override; + bool isKeepAbove() const override; + bool isKeepBelow() const override; + bool isExcludedFromCapture() const override; + bool isMaximizeable() const override; + bool isMaximized() const override; + bool isMaximizedVertically() const override; + bool isMaximizedHorizontally() const override; + bool isMinimizeable() const override; + bool isModal() const override; + bool isMoveable() const override; + bool isOnAllDesktops() const override; + bool isResizeable() const override; + bool isShadeable() const override; + bool isShaded() const override; + bool providesContextHelp() const override; + + qreal width() const override; + qreal height() const override; + QSizeF size() const override; + QPalette palette() const override; + QColor color(ColorGroup group, ColorRole role) const override; + Qt::Edges adjacentScreenEdges() const override; + QString windowClass() const override; + qreal scale() const override; + qreal nextScale() const override; + QString applicationMenuServiceName() const override; + QString applicationMenuObjectPath() const override; + + bool hasApplicationMenu() const override; + bool isApplicationMenuActive() const override; + + void requestShowToolTip(const QString &text) override; + void requestHideToolTip() override; + void requestClose() override; + void requestContextHelp() override; + void requestToggleMaximization(Qt::MouseButtons buttons) override; + void requestMinimize() override; + void requestToggleKeepAbove() override; + void requestToggleKeepBelow() override; + void requestToggleExcludeFromCapture() override; + void requestToggleShade() override; + void requestShowWindowMenu(const QRect &rect) override; + void requestShowApplicationMenu(const QRect &rect, int actionId) override; + void requestToggleOnAllDesktops() override; + + void showApplicationMenu(int actionId) override; + + void popup(const KDecoration3::Positioner &positioner, QMenu *menu) override; + + void setCaption(const QString &caption); + void setActive(bool active); + void setCloseable(bool closeable); + void setMaximizable(bool maximizable); + void setKeepBelow(bool keepBelow); + void setKeepAbove(bool keepAbove); + void setExcludeFromCapture(bool exclude); + void setMaximizedHorizontally(bool maximized); + void setMaximizedVertically(bool maximized); + void setMinimizable(bool minimizable); + void setModal(bool modal); + void setMovable(bool movable); + void setResizable(bool resizable); + void setShadeable(bool shadeable); + void setShaded(bool shaded); + void setProvidesContextHelp(bool contextHelp); + + void setWidth(int width); + void setHeight(int height); + + QString iconName() const; + void setIconName(const QString &icon); + void setIcon(const QIcon &icon); + + bool bordersTopEdge() const; + bool bordersLeftEdge() const; + bool bordersRightEdge() const; + bool bordersBottomEdge() const; + + void setBordersTopEdge(bool enabled); + void setBordersLeftEdge(bool enabled); + void setBordersRightEdge(bool enabled); + void setBordersBottomEdge(bool enabled); + +Q_SIGNALS: + void captionChanged(const QString &); + void iconChanged(const QIcon &); + void iconNameChanged(const QString &); + void activeChanged(bool); + void closeableChanged(bool); + void keepAboveChanged(bool); + void keepBelowChanged(bool); + void excludeFromCaptureChanged(bool); + void maximizableChanged(bool); + void maximizedChanged(bool); + void maximizedVerticallyChanged(bool); + void maximizedHorizontallyChanged(bool); + void minimizableChanged(bool); + void modalChanged(bool); + void movableChanged(bool); + void onAllDesktopsChanged(bool); + void resizableChanged(bool); + void shadeableChanged(bool); + void shadedChanged(bool); + void providesContextHelpChanged(bool); + void widthChanged(int); + void heightChanged(int); + void paletteChanged(const QPalette &); + void bordersTopEdgeChanged(bool); + void bordersLeftEdgeChanged(bool); + void bordersRightEdgeChanged(bool); + void bordersBottomEdgeChanged(bool); + + void showWindowMenuRequested(); + void showApplicationMenuRequested(); + void minimizeRequested(); + void closeRequested(); + +private: + QString m_caption; + QIcon m_icon; + QString m_iconName; + KWin::Decoration::DecorationPalette m_palette; + bool m_active; + bool m_closeable; + bool m_keepBelow; + bool m_keepAbove; + bool m_excludeFromCapture; + bool m_maximizable; + bool m_maximizedHorizontally; + bool m_maximizedVertically; + bool m_minimizable; + bool m_modal; + bool m_movable; + bool m_resizable; + bool m_shadeable; + bool m_shaded; + bool m_providesContextHelp; + bool m_onAllDesktops; + int m_width; + int m_height; + bool m_bordersTopEdge; + bool m_bordersLeftEdge; + bool m_bordersRightEdge; + bool m_bordersBottomEdge; +}; + +} // namespace Preview +} // namespace KDecoration3 diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewitem.cpp b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewitem.cpp new file mode 100644 index 0000000000..3505487468 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewitem.cpp @@ -0,0 +1,426 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "previewitem.h" +#include "previewbridge.h" +#include "previewclient.h" +#include "previewsettings.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace KDecoration3 +{ +namespace Preview +{ + +PreviewItem::PreviewItem(QQuickItem *parent) + : QQuickPaintedItem(parent) + , m_decoration(nullptr) + , m_windowColor(QPalette().window().color()) +{ + setAcceptHoverEvents(true); + setAcceptedMouseButtons(Qt::AllButtons); + connect(this, &PreviewItem::widthChanged, this, &PreviewItem::syncSize); + connect(this, &PreviewItem::heightChanged, this, &PreviewItem::syncSize); + connect(this, &PreviewItem::bridgeChanged, this, &PreviewItem::createDecoration); + connect(this, &PreviewItem::settingsChanged, this, &PreviewItem::createDecoration); +} + +PreviewItem::~PreviewItem() +{ + if (m_decoration) { + m_decoration->deleteLater(); + } + if (m_bridge) { + m_bridge->unregisterPreviewItem(this); + } +} + +void PreviewItem::componentComplete() +{ + QQuickPaintedItem::componentComplete(); + createDecoration(); + if (m_decoration) { + m_decoration->setSettings(m_settings->settings()); + m_decoration->create(); + m_decoration->init(); + m_decoration->apply(m_decoration->nextState()->clone()); + syncSize(); + } +} + +void PreviewItem::createDecoration() +{ + if (m_bridge.isNull() || m_settings.isNull() || m_decoration) { + return; + } + Decoration *decoration = m_bridge->createDecoration(nullptr); + m_client = m_bridge->lastCreatedClient(); + setDecoration(decoration); +} + +Decoration *PreviewItem::decoration() const +{ + return m_decoration; +} + +void PreviewItem::setDecoration(Decoration *deco) +{ + if (!deco || m_decoration == deco) { + return; + } + + m_decoration = deco; + m_decoration->setProperty("visualParent", QVariant::fromValue(this)); + connect(m_decoration, &Decoration::bordersChanged, this, &PreviewItem::syncSize); + connect(m_decoration, &Decoration::shadowChanged, this, &PreviewItem::syncSize); + connect(m_decoration, &Decoration::shadowChanged, this, &PreviewItem::shadowChanged); + connect(m_decoration, &Decoration::damaged, this, [this]() { + update(); + }); + Q_EMIT decorationChanged(m_decoration); +} + +QColor PreviewItem::windowColor() const +{ + return m_windowColor; +} + +void PreviewItem::setWindowColor(const QColor &color) +{ + if (m_windowColor == color) { + return; + } + m_windowColor = color; + Q_EMIT windowColorChanged(m_windowColor); + update(); +} + +void PreviewItem::paint(QPainter *painter) +{ + if (!m_decoration) { + return; + } + int paddingLeft = 0; + int paddingTop = 0; + int paddingRight = 0; + int paddingBottom = 0; + paintShadow(painter, paddingLeft, paddingRight, paddingTop, paddingBottom); + m_decoration->paint(painter, QRect(0, 0, width(), height())); + if (m_drawBackground) { + painter->fillRect(m_decoration->borderLeft(), m_decoration->borderTop(), + width() - m_decoration->borderLeft() - m_decoration->borderRight() - paddingLeft - paddingRight, + height() - m_decoration->borderTop() - m_decoration->borderBottom() - paddingTop - paddingBottom, + m_windowColor); + } +} + +void PreviewItem::paintShadow(QPainter *painter, int &paddingLeft, int &paddingRight, int &paddingTop, int &paddingBottom) +{ + const auto &shadow = m_decoration->shadow(); + if (!shadow) { + return; + } + + paddingLeft = shadow->paddingLeft(); + paddingTop = shadow->paddingTop(); + paddingRight = shadow->paddingRight(); + paddingBottom = shadow->paddingBottom(); + + const QImage shadowPixmap = shadow->shadow(); + if (shadowPixmap.isNull()) { + return; + } + + const QRect outerRect(-paddingLeft, -paddingTop, width(), height()); + const QRect shadowRect(shadowPixmap.rect()); + + const QSizeF topLeftSize(shadow->topLeftGeometry().size()); + QRectF topLeftTarget( + QPointF(outerRect.x(), outerRect.y()), + topLeftSize); + + const QSizeF topRightSize(shadow->topRightGeometry().size()); + QRectF topRightTarget( + QPointF(outerRect.x() + outerRect.width() - topRightSize.width(), + outerRect.y()), + topRightSize); + + const QSizeF bottomRightSize(shadow->bottomRightGeometry().size()); + QRectF bottomRightTarget( + QPointF(outerRect.x() + outerRect.width() - bottomRightSize.width(), + outerRect.y() + outerRect.height() - bottomRightSize.height()), + bottomRightSize); + + const QSizeF bottomLeftSize(shadow->bottomLeftGeometry().size()); + QRectF bottomLeftTarget( + QPointF(outerRect.x(), + outerRect.y() + outerRect.height() - bottomLeftSize.height()), + bottomLeftSize); + + // Re-distribute the corner tiles so no one of them is overlapping with others. + // By doing this, we assume that shadow's corner tiles are symmetric + // and it is OK to not draw top/right/bottom/left tile between corners. + // For example, let's say top-left and top-right tiles are overlapping. + // In that case, the right side of the top-left tile will be shifted to left, + // the left side of the top-right tile will shifted to right, and the top + // tile won't be rendered. + bool drawTop = true; + if (topLeftTarget.x() + topLeftTarget.width() >= topRightTarget.x()) { + const float halfOverlap = std::abs(topLeftTarget.x() + topLeftTarget.width() - topRightTarget.x()) / 2.0f; + topLeftTarget.setRight(topLeftTarget.right() - std::floor(halfOverlap)); + topRightTarget.setLeft(topRightTarget.left() + std::ceil(halfOverlap)); + drawTop = false; + } + + bool drawRight = true; + if (topRightTarget.y() + topRightTarget.height() >= bottomRightTarget.y()) { + const float halfOverlap = std::abs(topRightTarget.y() + topRightTarget.height() - bottomRightTarget.y()) / 2.0f; + topRightTarget.setBottom(topRightTarget.bottom() - std::floor(halfOverlap)); + bottomRightTarget.setTop(bottomRightTarget.top() + std::ceil(halfOverlap)); + drawRight = false; + } + + bool drawBottom = true; + if (bottomLeftTarget.x() + bottomLeftTarget.width() >= bottomRightTarget.x()) { + const float halfOverlap = std::abs(bottomLeftTarget.x() + bottomLeftTarget.width() - bottomRightTarget.x()) / 2.0f; + bottomLeftTarget.setRight(bottomLeftTarget.right() - std::floor(halfOverlap)); + bottomRightTarget.setLeft(bottomRightTarget.left() + std::ceil(halfOverlap)); + drawBottom = false; + } + + bool drawLeft = true; + if (topLeftTarget.y() + topLeftTarget.height() >= bottomLeftTarget.y()) { + const float halfOverlap = std::abs(topLeftTarget.y() + topLeftTarget.height() - bottomLeftTarget.y()) / 2.0f; + topLeftTarget.setBottom(topLeftTarget.bottom() - std::floor(halfOverlap)); + bottomLeftTarget.setTop(bottomLeftTarget.top() + std::ceil(halfOverlap)); + drawLeft = false; + } + + painter->translate(paddingLeft, paddingTop); + + painter->drawImage(topLeftTarget, shadowPixmap, + QRectF(QPointF(0, 0), topLeftTarget.size())); + + painter->drawImage(topRightTarget, shadowPixmap, + QRectF(QPointF(shadowRect.width() - topRightTarget.width(), 0), + topRightTarget.size())); + + painter->drawImage(bottomRightTarget, shadowPixmap, + QRectF(QPointF(shadowRect.width() - bottomRightTarget.width(), + shadowRect.height() - bottomRightTarget.height()), + bottomRightTarget.size())); + + painter->drawImage(bottomLeftTarget, shadowPixmap, + QRectF(QPointF(0, shadowRect.height() - bottomLeftTarget.height()), + bottomLeftTarget.size())); + + if (drawTop) { + QRectF topTarget(topLeftTarget.x() + topLeftTarget.width(), + topLeftTarget.y(), + topRightTarget.x() - topLeftTarget.x() - topLeftTarget.width(), + topRightTarget.height()); + QRectF topSource(shadow->topGeometry()); + topSource.setHeight(topTarget.height()); + topSource.moveTop(shadowRect.top()); + painter->drawImage(topTarget, shadowPixmap, topSource); + } + + if (drawRight) { + QRectF rightTarget(topRightTarget.x(), + topRightTarget.y() + topRightTarget.height(), + topRightTarget.width(), + bottomRightTarget.y() - topRightTarget.y() - topRightTarget.height()); + QRectF rightSource(shadow->rightGeometry()); + rightSource.setWidth(rightTarget.width()); + rightSource.moveRight(shadowRect.right()); + painter->drawImage(rightTarget, shadowPixmap, rightSource); + } + + if (drawBottom) { + QRectF bottomTarget(bottomLeftTarget.x() + bottomLeftTarget.width(), + bottomLeftTarget.y(), + bottomRightTarget.x() - bottomLeftTarget.x() - bottomLeftTarget.width(), + bottomRightTarget.height()); + QRectF bottomSource(shadow->bottomGeometry()); + bottomSource.setHeight(bottomTarget.height()); + bottomSource.moveBottom(shadowRect.bottom()); + painter->drawImage(bottomTarget, shadowPixmap, bottomSource); + } + + if (drawLeft) { + QRectF leftTarget(topLeftTarget.x(), + topLeftTarget.y() + topLeftTarget.height(), + topLeftTarget.width(), + bottomLeftTarget.y() - topLeftTarget.y() - topLeftTarget.height()); + QRectF leftSource(shadow->leftGeometry()); + leftSource.setWidth(leftTarget.width()); + leftSource.moveLeft(shadowRect.left()); + painter->drawImage(leftTarget, shadowPixmap, leftSource); + } +} + +static QMouseEvent cloneEventWithPadding(QMouseEvent *event, int paddingLeft, int paddingTop) +{ + return QMouseEvent( + event->type(), + event->position() - QPointF(paddingLeft, paddingTop), + event->button(), + event->buttons(), + event->modifiers()); +} + +static QHoverEvent cloneEventWithPadding(QHoverEvent *event, int paddingLeft, int paddingTop) +{ + return QHoverEvent( + event->type(), + event->position() - QPointF(paddingLeft, paddingTop), + event->oldPosF() - QPointF(paddingLeft, paddingTop), + event->modifiers()); +} + +template +void PreviewItem::proxyPassEvent(E *event) const +{ + if (m_decoration) { + const auto &shadow = m_decoration->shadow(); + if (shadow) { + E e = cloneEventWithPadding(event, shadow->paddingLeft(), shadow->paddingTop()); + QCoreApplication::instance()->sendEvent(decoration(), &e); + } else { + QCoreApplication::instance()->sendEvent(decoration(), event); + } + } + // Propagate events to parent + event->ignore(); +} + +void PreviewItem::mouseDoubleClickEvent(QMouseEvent *event) +{ + proxyPassEvent(event); +} + +void PreviewItem::mousePressEvent(QMouseEvent *event) +{ + proxyPassEvent(event); +} + +void PreviewItem::mouseReleaseEvent(QMouseEvent *event) +{ + proxyPassEvent(event); +} + +void PreviewItem::mouseMoveEvent(QMouseEvent *event) +{ + proxyPassEvent(event); +} + +void PreviewItem::hoverEnterEvent(QHoverEvent *event) +{ + proxyPassEvent(event); +} + +void PreviewItem::hoverLeaveEvent(QHoverEvent *event) +{ + proxyPassEvent(event); +} + +void PreviewItem::hoverMoveEvent(QHoverEvent *event) +{ + proxyPassEvent(event); +} + +bool PreviewItem::isDrawingBackground() const +{ + return m_drawBackground; +} + +void PreviewItem::setDrawingBackground(bool set) +{ + if (m_drawBackground == set) { + return; + } + m_drawBackground = set; + Q_EMIT drawingBackgroundChanged(set); +} + +PreviewBridge *PreviewItem::bridge() const +{ + return m_bridge.data(); +} + +void PreviewItem::setBridge(PreviewBridge *bridge) +{ + if (m_bridge == bridge) { + return; + } + if (m_bridge) { + m_bridge->unregisterPreviewItem(this); + } + m_bridge = bridge; + if (m_bridge) { + m_bridge->registerPreviewItem(this); + } + Q_EMIT bridgeChanged(); +} + +Settings *PreviewItem::settings() const +{ + return m_settings.data(); +} + +void PreviewItem::setSettings(Settings *settings) +{ + if (m_settings == settings) { + return; + } + m_settings = settings; + Q_EMIT settingsChanged(); +} + +PreviewClient *PreviewItem::client() +{ + return m_client.data(); +} + +void PreviewItem::syncSize() +{ + if (!m_client) { + return; + } + int widthOffset = 0; + int heightOffset = 0; + auto shadow = m_decoration->shadow(); + if (shadow) { + widthOffset = shadow->paddingLeft() + shadow->paddingRight(); + heightOffset = shadow->paddingTop() + shadow->paddingBottom(); + } + m_client->setWidth(width() - m_decoration->borderLeft() - m_decoration->borderRight() - widthOffset); + m_client->setHeight(height() - m_decoration->borderTop() - m_decoration->borderBottom() - heightOffset); +} + +DecorationShadow *PreviewItem::shadow() const +{ + if (!m_decoration) { + return nullptr; + } + return m_decoration->shadow().get(); +} + +} +} + +#include "moc_previewitem.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewitem.h b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewitem.h new file mode 100644 index 0000000000..1baa5db870 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewitem.h @@ -0,0 +1,90 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#pragma once + +#include +#include + +namespace KDecoration3 +{ +class Decoration; +class DecorationShadow; +class DecorationSettings; + +namespace Preview +{ +class PreviewBridge; +class PreviewClient; +class Settings; + +class PreviewItem : public QQuickPaintedItem +{ + Q_OBJECT + QML_NAMED_ELEMENT(Decoration) + Q_PROPERTY(KDecoration3::Decoration *decoration READ decoration NOTIFY decorationChanged) + Q_PROPERTY(KDecoration3::Preview::PreviewBridge *bridge READ bridge WRITE setBridge NOTIFY bridgeChanged) + Q_PROPERTY(KDecoration3::Preview::Settings *settings READ settings WRITE setSettings NOTIFY settingsChanged) + Q_PROPERTY(KDecoration3::Preview::PreviewClient *client READ client) + Q_PROPERTY(KDecoration3::DecorationShadow *shadow READ shadow NOTIFY shadowChanged) + Q_PROPERTY(QColor windowColor READ windowColor WRITE setWindowColor NOTIFY windowColorChanged) + Q_PROPERTY(bool drawBackground READ isDrawingBackground WRITE setDrawingBackground NOTIFY drawingBackgroundChanged) +public: + PreviewItem(QQuickItem *parent = nullptr); + ~PreviewItem() override; + void paint(QPainter *painter) override; + + KDecoration3::Decoration *decoration() const; + void setDecoration(KDecoration3::Decoration *deco); + + QColor windowColor() const; + void setWindowColor(const QColor &color); + + bool isDrawingBackground() const; + void setDrawingBackground(bool set); + + PreviewBridge *bridge() const; + void setBridge(PreviewBridge *bridge); + + Settings *settings() const; + void setSettings(Settings *settings); + + PreviewClient *client(); + DecorationShadow *shadow() const; + +Q_SIGNALS: + void decorationChanged(KDecoration3::Decoration *deco); + void windowColorChanged(const QColor &color); + void drawingBackgroundChanged(bool); + void bridgeChanged(); + void settingsChanged(); + void shadowChanged(); + +protected: + void mouseDoubleClickEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void hoverEnterEvent(QHoverEvent *event) override; + void hoverLeaveEvent(QHoverEvent *event) override; + void hoverMoveEvent(QHoverEvent *event) override; + void componentComplete() override; + +private: + void paintShadow(QPainter *painter, int &paddingLeft, int &paddingRight, int &paddingTop, int &paddingBottom); + template + void proxyPassEvent(E *event) const; + void syncSize(); + void createDecoration(); + Decoration *m_decoration; + QColor m_windowColor; + bool m_drawBackground = true; + QPointer m_bridge; + QPointer m_settings; + QPointer m_client; +}; + +} // Preview +} // KDecoration3 diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewsettings.cpp b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewsettings.cpp new file mode 100644 index 0000000000..5128c77eef --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewsettings.cpp @@ -0,0 +1,275 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "previewsettings.h" +#include "buttonsmodel.h" +#include "previewbridge.h" + +#include + +#include + +namespace KDecoration3 +{ + +namespace Preview +{ + +BorderSizesModel::BorderSizesModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +BorderSizesModel::~BorderSizesModel() = default; + +QVariant BorderSizesModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_borders.count() || index.column() != 0) { + return QVariant(); + } + if (role != Qt::DisplayRole && role != Qt::UserRole) { + return QVariant(); + } + return QVariant::fromValue(m_borders.at(index.row())); +} + +int BorderSizesModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_borders.count(); +} + +QHash BorderSizesModel::roleNames() const +{ + QHash roles; + roles.insert(Qt::DisplayRole, QByteArrayLiteral("display")); + return roles; +} + +PreviewSettings::PreviewSettings(DecorationSettings *parent) + : QObject() + , DecorationSettingsPrivateV2(parent) + , m_alphaChannelSupported(true) + , m_onAllDesktopsAvailable(true) + , m_closeOnDoubleClick(false) + , m_alwaysShowExcludeFromCapture(false) + , m_leftButtons(new ButtonsModel(QList({DecorationButtonType::Menu, + DecorationButtonType::ApplicationMenu, + DecorationButtonType::OnAllDesktops, + DecorationButtonType::ExcludeFromCapture}), + this)) + , m_rightButtons(new ButtonsModel(QList({DecorationButtonType::ContextHelp, + DecorationButtonType::Minimize, + DecorationButtonType::Maximize, + DecorationButtonType::Close}), + this)) + , m_availableButtons(new ButtonsModel(QList({DecorationButtonType::Menu, + DecorationButtonType::ApplicationMenu, + DecorationButtonType::OnAllDesktops, + DecorationButtonType::Minimize, + DecorationButtonType::Maximize, + DecorationButtonType::Close, + DecorationButtonType::ContextHelp, + DecorationButtonType::KeepBelow, + DecorationButtonType::KeepAbove, + DecorationButtonType::ExcludeFromCapture}), + this)) + , m_borderSizes(new BorderSizesModel(this)) + , m_borderSize(int(BorderSize::Normal)) + , m_font(QFontDatabase::systemFont(QFontDatabase::TitleFont)) +{ + connect(this, &PreviewSettings::alphaChannelSupportedChanged, parent, &DecorationSettings::alphaChannelSupportedChanged); + connect(this, &PreviewSettings::onAllDesktopsAvailableChanged, parent, &DecorationSettings::onAllDesktopsAvailableChanged); + connect(this, &PreviewSettings::closeOnDoubleClickOnMenuChanged, parent, &DecorationSettings::closeOnDoubleClickOnMenuChanged); + connect(this, &PreviewSettings::fontChanged, parent, &DecorationSettings::fontChanged); + auto updateLeft = [this, parent]() { + Q_EMIT parent->decorationButtonsLeftChanged(decorationButtonsLeft()); + }; + auto updateRight = [this, parent]() { + Q_EMIT parent->decorationButtonsRightChanged(decorationButtonsRight()); + }; + connect(m_leftButtons, &QAbstractItemModel::rowsRemoved, this, updateLeft); + connect(m_leftButtons, &QAbstractItemModel::rowsMoved, this, updateLeft); + connect(m_leftButtons, &QAbstractItemModel::rowsInserted, this, updateLeft); + connect(m_rightButtons, &QAbstractItemModel::rowsRemoved, this, updateRight); + connect(m_rightButtons, &QAbstractItemModel::rowsMoved, this, updateRight); + connect(m_rightButtons, &QAbstractItemModel::rowsInserted, this, updateRight); +} + +PreviewSettings::~PreviewSettings() = default; + +QAbstractItemModel *PreviewSettings::availableButtonsModel() const +{ + return m_availableButtons; +} + +QAbstractItemModel *PreviewSettings::leftButtonsModel() const +{ + return m_leftButtons; +} + +QAbstractItemModel *PreviewSettings::rightButtonsModel() const +{ + return m_rightButtons; +} + +bool PreviewSettings::isAlphaChannelSupported() const +{ + return m_alphaChannelSupported; +} + +bool PreviewSettings::isOnAllDesktopsAvailable() const +{ + return m_onAllDesktopsAvailable; +} + +void PreviewSettings::setAlphaChannelSupported(bool supported) +{ + if (m_alphaChannelSupported == supported) { + return; + } + m_alphaChannelSupported = supported; + Q_EMIT alphaChannelSupportedChanged(m_alphaChannelSupported); +} + +void PreviewSettings::setOnAllDesktopsAvailable(bool available) +{ + if (m_onAllDesktopsAvailable == available) { + return; + } + m_onAllDesktopsAvailable = available; + Q_EMIT onAllDesktopsAvailableChanged(m_onAllDesktopsAvailable); +} + +void PreviewSettings::setCloseOnDoubleClickOnMenu(bool enabled) +{ + if (m_closeOnDoubleClick == enabled) { + return; + } + m_closeOnDoubleClick = enabled; + Q_EMIT closeOnDoubleClickOnMenuChanged(enabled); +} + +void PreviewSettings::setAlwaysShowExcludeFromCapture(bool enabled) +{ + if (m_alwaysShowExcludeFromCapture == enabled) { + return; + } + m_alwaysShowExcludeFromCapture = enabled; + Q_EMIT alwaysShowExcludeFromCaptureChanged(enabled); +} + +QList PreviewSettings::decorationButtonsLeft() const +{ + return m_leftButtons->buttons(); +} + +QList PreviewSettings::decorationButtonsRight() const +{ + return m_rightButtons->buttons(); +} + +void PreviewSettings::addButtonToLeft(int row) +{ + QModelIndex index = m_availableButtons->index(row); + if (!index.isValid()) { + return; + } + m_leftButtons->add(index.data(Qt::UserRole).value()); +} + +void PreviewSettings::addButtonToRight(int row) +{ + QModelIndex index = m_availableButtons->index(row); + if (!index.isValid()) { + return; + } + m_rightButtons->add(index.data(Qt::UserRole).value()); +} + +void PreviewSettings::setBorderSizesIndex(int index) +{ + if (m_borderSize == index) { + return; + } + m_borderSize = index; + Q_EMIT borderSizesIndexChanged(index); + Q_EMIT decorationSettings()->borderSizeChanged(borderSize()); +} + +BorderSize PreviewSettings::borderSize() const +{ + return m_borderSizes->index(m_borderSize).data(Qt::UserRole).value(); +} + +void PreviewSettings::setFont(const QFont &font) +{ + if (m_font == font) { + return; + } + m_font = font; + Q_EMIT fontChanged(m_font); +} + +Settings::Settings(QObject *parent) + : QObject(parent) +{ + connect(this, &Settings::bridgeChanged, this, &Settings::createSettings); +} + +Settings::~Settings() = default; + +void Settings::setBridge(PreviewBridge *bridge) +{ + if (m_bridge == bridge) { + return; + } + m_bridge = bridge; + Q_EMIT bridgeChanged(); +} + +PreviewBridge *Settings::bridge() const +{ + return m_bridge.data(); +} + +void Settings::createSettings() +{ + if (m_bridge.isNull()) { + m_settings.reset(); + } else { + m_settings = std::make_shared(m_bridge.get()); + m_previewSettings = m_bridge->lastCreatedSettings(); + m_previewSettings->setBorderSizesIndex(m_borderSize); + connect(this, &Settings::borderSizesIndexChanged, m_previewSettings, &PreviewSettings::setBorderSizesIndex); + } + Q_EMIT settingsChanged(); +} + +std::shared_ptr Settings::settings() const +{ + return m_settings; +} + +DecorationSettings *Settings::settingsPointer() const +{ + return m_settings.get(); +} + +void Settings::setBorderSizesIndex(int index) +{ + if (m_borderSize == index) { + return; + } + m_borderSize = index; + Q_EMIT borderSizesIndexChanged(m_borderSize); +} + +} +} + +#include "moc_previewsettings.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewsettings.h b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewsettings.h new file mode 100644 index 0000000000..e2ef38c946 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/previewsettings.h @@ -0,0 +1,160 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace KDecoration3 +{ + +namespace Preview +{ +class ButtonsModel; +class PreviewBridge; + +class BorderSizesModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit BorderSizesModel(QObject *parent = nullptr); + ~BorderSizesModel() override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + +private: + QList m_borders = QList({BorderSize::None, + BorderSize::NoSides, + BorderSize::Tiny, + BorderSize::Normal, + BorderSize::Large, + BorderSize::VeryLarge, + BorderSize::Huge, + BorderSize::VeryHuge, + BorderSize::Oversized}); +}; + +class PreviewSettings : public QObject, public DecorationSettingsPrivateV2 +{ + Q_OBJECT + Q_PROPERTY(bool onAllDesktopsAvailable READ isOnAllDesktopsAvailable WRITE setOnAllDesktopsAvailable NOTIFY onAllDesktopsAvailableChanged) + Q_PROPERTY(bool alphaChannelSupported READ isAlphaChannelSupported WRITE setAlphaChannelSupported NOTIFY alphaChannelSupportedChanged) + Q_PROPERTY(bool closeOnDoubleClickOnMenu READ isCloseOnDoubleClickOnMenu WRITE setCloseOnDoubleClickOnMenu NOTIFY closeOnDoubleClickOnMenuChanged) + Q_PROPERTY(QAbstractItemModel *leftButtonsModel READ leftButtonsModel CONSTANT) + Q_PROPERTY(QAbstractItemModel *rightButtonsModel READ rightButtonsModel CONSTANT) + Q_PROPERTY(QAbstractItemModel *availableButtonsModel READ availableButtonsModel CONSTANT) + Q_PROPERTY(QAbstractItemModel *borderSizesModel READ borderSizesModel CONSTANT) + Q_PROPERTY(int borderSizesIndex READ borderSizesIndex WRITE setBorderSizesIndex NOTIFY borderSizesIndexChanged) + Q_PROPERTY(QFont font READ font WRITE setFont NOTIFY fontChanged) +public: + explicit PreviewSettings(DecorationSettings *parent); + ~PreviewSettings() override; + bool isAlphaChannelSupported() const override; + bool isOnAllDesktopsAvailable() const override; + bool isCloseOnDoubleClickOnMenu() const override + { + return m_closeOnDoubleClick; + } + bool isAlwaysShowExcludeFromCapture() const override + { + return m_alwaysShowExcludeFromCapture; + } + BorderSize borderSize() const override; + + void setOnAllDesktopsAvailable(bool available); + void setAlphaChannelSupported(bool supported); + void setCloseOnDoubleClickOnMenu(bool enabled); + void setAlwaysShowExcludeFromCapture(bool enabled); + + QAbstractItemModel *leftButtonsModel() const; + QAbstractItemModel *rightButtonsModel() const; + QAbstractItemModel *availableButtonsModel() const; + QAbstractItemModel *borderSizesModel() const + { + return m_borderSizes; + } + + QList decorationButtonsLeft() const override; + QList decorationButtonsRight() const override; + + Q_INVOKABLE void addButtonToLeft(int row); + Q_INVOKABLE void addButtonToRight(int row); + + int borderSizesIndex() const + { + return m_borderSize; + } + void setBorderSizesIndex(int index); + + QFont font() const override + { + return m_font; + } + void setFont(const QFont &font); + +Q_SIGNALS: + void onAllDesktopsAvailableChanged(bool); + void alphaChannelSupportedChanged(bool); + void closeOnDoubleClickOnMenuChanged(bool); + void alwaysShowExcludeFromCaptureChanged(bool); + void borderSizesIndexChanged(int); + void fontChanged(const QFont &); + +private: + bool m_alphaChannelSupported; + bool m_onAllDesktopsAvailable; + bool m_closeOnDoubleClick; + bool m_alwaysShowExcludeFromCapture; + ButtonsModel *m_leftButtons; + ButtonsModel *m_rightButtons; + ButtonsModel *m_availableButtons; + BorderSizesModel *m_borderSizes; + int m_borderSize; + QFont m_font; +}; + +class Settings : public QObject +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(KDecoration3::Preview::PreviewBridge *bridge READ bridge WRITE setBridge NOTIFY bridgeChanged) + Q_PROPERTY(KDecoration3::DecorationSettings *settings READ settingsPointer NOTIFY settingsChanged) + Q_PROPERTY(int borderSizesIndex READ borderSizesIndex WRITE setBorderSizesIndex NOTIFY borderSizesIndexChanged) +public: + explicit Settings(QObject *parent = nullptr); + ~Settings() override; + + PreviewBridge *bridge() const; + void setBridge(PreviewBridge *bridge); + + std::shared_ptr settings() const; + DecorationSettings *settingsPointer() const; + int borderSizesIndex() const + { + return m_borderSize; + } + void setBorderSizesIndex(int index); + +Q_SIGNALS: + void bridgeChanged(); + void settingsChanged(); + void borderSizesIndexChanged(int); + +private: + void createSettings(); + QPointer m_bridge; + std::shared_ptr m_settings; + PreviewSettings *m_previewSettings = nullptr; + int m_borderSize = 3; +}; + +} +} diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/types.h b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/types.h new file mode 100644 index 0000000000..d89170ab11 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/declarative-plugin/types.h @@ -0,0 +1,25 @@ +/* + SPDX-FileCopyrightText: 2024 Nicolas Fella + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#pragma once + +#include + +#include +#include + +struct DecorationForeign +{ + Q_GADGET + QML_ANONYMOUS + QML_FOREIGN(KDecoration3::Decoration) +}; + +struct DecorationShadowForeign +{ + Q_GADGET + QML_ANONYMOUS + QML_FOREIGN(KDecoration3::DecorationShadow) +}; diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/decorationmodel.cpp b/local/recipes/kde/kwin/source/src/kcms/decoration/decorationmodel.cpp new file mode 100644 index 0000000000..ce6dd4435d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/decorationmodel.cpp @@ -0,0 +1,171 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "decorationmodel.h" +#include "utils.h" +// KDecoration3 +#include +#include +#include +// KDE +#include +#include +// Qt +#include + +namespace KDecoration3 +{ + +namespace Configuration +{ +static const QString s_pluginName = QStringLiteral("org.kde.kdecoration3"); + +DecorationsModel::DecorationsModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +DecorationsModel::~DecorationsModel() = default; + +int DecorationsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_plugins.size(); +} + +QVariant DecorationsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.column() != 0 || index.row() < 0 || index.row() >= int(m_plugins.size())) { + return QVariant(); + } + const KDecoration3::DecorationThemeMetaData &d = m_plugins.at(index.row()); + switch (role) { + case Qt::DisplayRole: + return d.visibleName(); + case PluginNameRole: + return d.pluginId(); + case ThemeNameRole: + return d.themeName(); + case ConfigurationRole: + return !d.configurationName().isEmpty(); + case KcmoduleNameRole: + return d.configurationName(); + case RecommendedBorderSizeRole: + return Utils::borderSizeToString(d.borderSize()); + } + return QVariant(); +} + +QHash DecorationsModel::roleNames() const +{ + QHash roles({{Qt::DisplayRole, QByteArrayLiteral("display")}, + {PluginNameRole, QByteArrayLiteral("plugin")}, + {ThemeNameRole, QByteArrayLiteral("theme")}, + {ConfigurationRole, QByteArrayLiteral("configurable")}, + {KcmoduleNameRole, QByteArrayLiteral("kcmoduleName")}, + {RecommendedBorderSizeRole, QByteArrayLiteral("recommendedbordersize")}}); + return roles; +} + +static bool isThemeEngine(const QVariantMap &decoSettingsMap) +{ + auto it = decoSettingsMap.find(QStringLiteral("themes")); + if (it == decoSettingsMap.end()) { + return false; + } + return it.value().toBool(); +} + +static KDecoration3::BorderSize recommendedBorderSize(const QVariantMap &decoSettingsMap) +{ + auto it = decoSettingsMap.find(QStringLiteral("recommendedBorderSize")); + if (it == decoSettingsMap.end()) { + return KDecoration3::BorderSize::Normal; + } + return Utils::stringToBorderSize(it.value().toString()); +} + +static QString themeListKeyword(const QVariantMap &decoSettingsMap) +{ + auto it = decoSettingsMap.find(QStringLiteral("themeListKeyword")); + if (it == decoSettingsMap.end()) { + return QString(); + } + return it.value().toString(); +} + +static QString findKNewStuff(const QVariantMap &decoSettingsMap) +{ + auto it = decoSettingsMap.find(QStringLiteral("KNewStuff")); + if (it == decoSettingsMap.end()) { + return QString(); + } + return it.value().toString(); +} + +void DecorationsModel::init() +{ + beginResetModel(); + m_plugins.clear(); + const auto plugins = KPluginMetaData::findPlugins(s_pluginName); + for (const auto &info : plugins) { + std::unique_ptr themeFinder( + KPluginFactory::instantiatePlugin(info).plugin); + KDecoration3::DecorationThemeMetaData data; + const auto decoSettingsMap = info.rawData().value("org.kde.kdecoration3").toObject().toVariantMap(); + if (themeFinder) { + const QString &kns = findKNewStuff(decoSettingsMap); + if (!kns.isEmpty() && !m_knsProviders.contains(kns)) { + m_knsProviders.append(kns); + } + if (isThemeEngine(decoSettingsMap)) { + const QString keyword = themeListKeyword(decoSettingsMap); + if (keyword.isNull()) { + // We cannot list the themes + continue; + } + const auto themesList = themeFinder->themes(); + for (const KDecoration3::DecorationThemeMetaData &data : themesList) { + m_plugins.emplace_back(data); + } + + // it's a theme engine, we don't want to show this entry + continue; + } + } + + if (decoSettingsMap.contains(QStringLiteral("kcmodule"))) { + qWarning() << "The use of 'kcmodule' is deprecated in favor of 'kcmoduleName', please update" << info.name(); + } + + data.setConfigurationName(info.value("X-KDE-ConfigModule")); + data.setBorderSize(recommendedBorderSize(decoSettingsMap)); + data.setVisibleName(info.name().isEmpty() ? info.pluginId() : info.name()); + data.setPluginId(info.pluginId()); + data.setThemeName(data.visibleName()); + + m_plugins.emplace_back(std::move(data)); + } + endResetModel(); +} + +QModelIndex DecorationsModel::findDecoration(const QString &pluginName, const QString &themeName) const +{ + auto it = std::find_if(m_plugins.cbegin(), m_plugins.cend(), [pluginName, themeName](const KDecoration3::DecorationThemeMetaData &d) { + return d.pluginId() == pluginName && d.themeName() == themeName; + }); + if (it == m_plugins.cend()) { + return QModelIndex(); + } + const auto distance = std::distance(m_plugins.cbegin(), it); + return createIndex(distance, 0); +} + +} +} + +#include "moc_decorationmodel.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/decorationmodel.h b/local/recipes/kde/kwin/source/src/kcms/decoration/decorationmodel.h new file mode 100644 index 0000000000..2f3c2619ef --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/decorationmodel.h @@ -0,0 +1,53 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#pragma once + +#include +#include + +namespace KDecoration3 +{ + +namespace Configuration +{ + +class DecorationsModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum DecorationRole { + PluginNameRole = Qt::UserRole + 1, + ThemeNameRole, + ConfigurationRole, + RecommendedBorderSizeRole, + KcmoduleNameRole, + }; + +public: + explicit DecorationsModel(QObject *parent = nullptr); + ~DecorationsModel() override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + + QModelIndex findDecoration(const QString &pluginName, const QString &themeName = QString()) const; + + QStringList knsProviders() const + { + return m_knsProviders; + } + +public Q_SLOTS: + void init(); + +private: + std::vector m_plugins; + QStringList m_knsProviders; +}; + +} +} diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/kcm.cpp b/local/recipes/kde/kwin/source/src/kcms/decoration/kcm.cpp new file mode 100644 index 0000000000..ff858ed903 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/kcm.cpp @@ -0,0 +1,284 @@ +/* + SPDX-FileCopyrightText: 2019 Valerio Pilo + SPDX-FileCopyrightText: 2019 Cyril Rossi + + SPDX-License-Identifier: LGPL-2.0-only +*/ +#include "kcm.h" + +#include "config-kwin.h" + +#include "declarative-plugin/buttonsmodel.h" +#include "decorationmodel.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "kwindecorationdata.h" +#include "kwindecorationsettings.h" +#include "utils.h" + +K_PLUGIN_FACTORY_WITH_JSON(KCMKWinDecorationFactory, "kcm_kwindecoration.json", registerPlugin(); registerPlugin();) + +Q_DECLARE_METATYPE(KDecoration3::BorderSize) + +namespace +{ +const KDecoration3::BorderSize s_defaultRecommendedBorderSize = KDecoration3::BorderSize::Normal; +} + +KCMKWinDecoration::KCMKWinDecoration(QObject *parent, const KPluginMetaData &metaData) + : KQuickManagedConfigModule(parent, metaData) + , m_themesModel(new KDecoration3::Configuration::DecorationsModel(this)) + , m_proxyThemesModel(new QSortFilterProxyModel(this)) + , m_leftButtonsModel(new KDecoration3::Preview::ButtonsModel(DecorationButtonsList(), this)) + , m_rightButtonsModel(new KDecoration3::Preview::ButtonsModel(DecorationButtonsList(), this)) + , m_availableButtonsModel(new KDecoration3::Preview::ButtonsModel(this)) + , m_data(new KWinDecorationData(this)) + , m_excludeFromCaptureButtonSelected(isExcludeFromCaptureSelected()) +{ + setButtons(Apply | Default | Help); + qmlRegisterAnonymousType("org.kde.kwin.KWinDecoration", 1); + qmlRegisterAnonymousType("org.kde.kwin.KWinDecoration", 1); + qmlRegisterAnonymousType("org.kde.kwin.KWinDecoration", 1); + m_proxyThemesModel->setSourceModel(m_themesModel); + m_proxyThemesModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_proxyThemesModel->setSortCaseSensitivity(Qt::CaseInsensitive); + m_proxyThemesModel->sort(0); + + connect(m_proxyThemesModel, &QSortFilterProxyModel::rowsInserted, this, &KCMKWinDecoration::themeChanged); + connect(m_proxyThemesModel, &QSortFilterProxyModel::rowsRemoved, this, &KCMKWinDecoration::themeChanged); + connect(m_proxyThemesModel, &QSortFilterProxyModel::modelReset, this, &KCMKWinDecoration::themeChanged); + + connect(m_data->settings(), &KWinDecorationSettings::themeChanged, this, &KCMKWinDecoration::themeChanged); + connect(m_data->settings(), &KWinDecorationSettings::borderSizeChanged, this, &KCMKWinDecoration::borderSizeChanged); + + connect(m_data->settings(), &KWinDecorationSettings::borderSizeAutoChanged, this, &KCMKWinDecoration::borderIndexChanged); + connect(this, &KCMKWinDecoration::borderSizeChanged, this, &KCMKWinDecoration::borderIndexChanged); + connect(this, &KCMKWinDecoration::themeChanged, this, &KCMKWinDecoration::borderIndexChanged); + + connect(this, &KCMKWinDecoration::themeChanged, this, [this]() { + if (m_data->settings()->borderSizeAuto()) { + setBorderSize(recommendedBorderSize()); + } + }); + + connect(m_leftButtonsModel, &QAbstractItemModel::rowsInserted, this, &KCMKWinDecoration::onLeftButtonsChanged); + connect(m_leftButtonsModel, &QAbstractItemModel::rowsMoved, this, &KCMKWinDecoration::onLeftButtonsChanged); + connect(m_leftButtonsModel, &QAbstractItemModel::rowsRemoved, this, &KCMKWinDecoration::onLeftButtonsChanged); + connect(m_leftButtonsModel, &QAbstractItemModel::modelReset, this, &KCMKWinDecoration::onLeftButtonsChanged); + + connect(m_rightButtonsModel, &QAbstractItemModel::rowsInserted, this, &KCMKWinDecoration::onRightButtonsChanged); + connect(m_rightButtonsModel, &QAbstractItemModel::rowsMoved, this, &KCMKWinDecoration::onRightButtonsChanged); + connect(m_rightButtonsModel, &QAbstractItemModel::rowsRemoved, this, &KCMKWinDecoration::onRightButtonsChanged); + connect(m_rightButtonsModel, &QAbstractItemModel::modelReset, this, &KCMKWinDecoration::onRightButtonsChanged); + + connect(m_data->settings(), &KWinDecorationSettings::buttonsOnLeftChanged, this, &KCMKWinDecoration::checkExcludeFromCaptureButtonPresence); + connect(m_data->settings(), &KWinDecorationSettings::buttonsOnRightChanged, this, &KCMKWinDecoration::checkExcludeFromCaptureButtonPresence); + + connect(this, &KCMKWinDecoration::borderSizeChanged, this, &KCMKWinDecoration::settingsChanged); + + // Update the themes when the color scheme or a theme's settings change + QDBusConnection::sessionBus() + .connect(QString(), QStringLiteral("/KWin"), QStringLiteral("org.kde.KWin"), QStringLiteral("reloadConfig"), + this, SLOT(reloadKWinSettings())); + + QMetaObject::invokeMethod(m_themesModel, &KDecoration3::Configuration::DecorationsModel::init, Qt::QueuedConnection); +} + +KWinDecorationSettings *KCMKWinDecoration::settings() const +{ + return m_data->settings(); +} + +void KCMKWinDecoration::reloadKWinSettings() +{ + QMetaObject::invokeMethod(m_themesModel, &KDecoration3::Configuration::DecorationsModel::init, Qt::QueuedConnection); +} + +void KCMKWinDecoration::load() +{ + KQuickManagedConfigModule::load(); + + m_leftButtonsModel->replace(Utils::buttonsFromString(settings()->buttonsOnLeft())); + m_rightButtonsModel->replace(Utils::buttonsFromString(settings()->buttonsOnRight())); + + setBorderSize(borderSizeIndexFromString(settings()->borderSize())); + + Q_EMIT themeChanged(); +} + +void KCMKWinDecoration::save() +{ + if (!settings()->borderSizeAuto()) { + settings()->setBorderSize(borderSizeIndexToString(m_borderSizeIndex)); + } else { + settings()->setBorderSize(settings()->defaultBorderSizeValue()); + } + + KQuickManagedConfigModule::save(); + + // Send a signal to all kwin instances + QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/KWin"), + QStringLiteral("org.kde.KWin"), + QStringLiteral("reloadConfig")); + QDBusConnection::sessionBus().send(message); +} + +void KCMKWinDecoration::defaults() +{ + KQuickManagedConfigModule::defaults(); + + setBorderSize(recommendedBorderSize()); + + m_leftButtonsModel->replace(Utils::buttonsFromString(settings()->buttonsOnLeft())); + m_rightButtonsModel->replace(Utils::buttonsFromString(settings()->buttonsOnRight())); +} + +void KCMKWinDecoration::onLeftButtonsChanged() +{ + settings()->setButtonsOnLeft(Utils::buttonsToString(m_leftButtonsModel->buttons())); +} + +void KCMKWinDecoration::onRightButtonsChanged() +{ + settings()->setButtonsOnRight(Utils::buttonsToString(m_rightButtonsModel->buttons())); +} + +QSortFilterProxyModel *KCMKWinDecoration::themesModel() const +{ + return m_proxyThemesModel; +} + +QAbstractListModel *KCMKWinDecoration::leftButtonsModel() +{ + return m_leftButtonsModel; +} + +QAbstractListModel *KCMKWinDecoration::rightButtonsModel() +{ + return m_rightButtonsModel; +} + +QAbstractListModel *KCMKWinDecoration::availableButtonsModel() const +{ + return m_availableButtonsModel; +} + +bool KCMKWinDecoration::excludeFromCaptureButtonSelected() const +{ + return m_excludeFromCaptureButtonSelected; +} + +QStringList KCMKWinDecoration::borderSizesModel() const +{ + // Use index 0 for borderSizeAuto == true + // The rest of indexes get offset by 1 + QStringList model = Utils::getBorderSizeNames().values(); + model.insert(0, i18nc("%1 is the name of a border size", "Theme default (%1)", model.at(recommendedBorderSize()))); + return model; +} + +int KCMKWinDecoration::borderIndex() const +{ + return settings()->borderSizeAuto() ? 0 : m_borderSizeIndex + 1; +} + +void KCMKWinDecoration::setBorderIndex(int index) +{ + const bool borderAuto = (index == 0); + settings()->setBorderSizeAuto(borderAuto); + setBorderSize(borderAuto ? recommendedBorderSize() : index - 1); +} + +int KCMKWinDecoration::borderSize() const +{ + return m_borderSizeIndex; +} + +int KCMKWinDecoration::recommendedBorderSize() const +{ + typedef KDecoration3::Configuration::DecorationsModel::DecorationRole DecoRole; + const QModelIndex proxyIndex = m_proxyThemesModel->index(theme(), 0); + if (proxyIndex.isValid()) { + const QModelIndex index = m_proxyThemesModel->mapToSource(proxyIndex); + if (index.isValid()) { + QVariant ret = m_themesModel->data(index, DecoRole::RecommendedBorderSizeRole); + return Utils::getBorderSizeNames().keys().indexOf(Utils::stringToBorderSize(ret.toString())); + } + } + return Utils::getBorderSizeNames().keys().indexOf(s_defaultRecommendedBorderSize); +} + +int KCMKWinDecoration::theme() const +{ + return m_proxyThemesModel->mapFromSource(m_themesModel->findDecoration(settings()->pluginName(), settings()->theme())).row(); +} + +void KCMKWinDecoration::setBorderSize(int index) +{ + if (m_borderSizeIndex != index) { + m_borderSizeIndex = index; + Q_EMIT borderSizeChanged(); + } +} + +void KCMKWinDecoration::setBorderSize(KDecoration3::BorderSize size) +{ + settings()->setBorderSize(Utils::borderSizeToString(size)); +} + +void KCMKWinDecoration::setTheme(int index) +{ + QModelIndex dataIndex = m_proxyThemesModel->index(index, 0); + if (dataIndex.isValid()) { + settings()->setTheme(m_proxyThemesModel->data(dataIndex, KDecoration3::Configuration::DecorationsModel::ThemeNameRole).toString()); + settings()->setPluginName(m_proxyThemesModel->data(dataIndex, KDecoration3::Configuration::DecorationsModel::PluginNameRole).toString()); + Q_EMIT themeChanged(); + } +} + +bool KCMKWinDecoration::isSaveNeeded() const +{ + return !settings()->borderSizeAuto() && borderSizeIndexFromString(settings()->borderSize()) != m_borderSizeIndex; +} + +int KCMKWinDecoration::borderSizeIndexFromString(const QString &size) const +{ + return Utils::getBorderSizeNames().keys().indexOf(Utils::stringToBorderSize(size)); +} + +QString KCMKWinDecoration::borderSizeIndexToString(int index) const +{ + return Utils::borderSizeToString(Utils::getBorderSizeNames().keys().at(index)); +} + +bool KCMKWinDecoration::isExcludeFromCaptureSelected() const +{ + const auto left = Utils::buttonsFromString(settings()->buttonsOnLeft()); + const auto right = Utils::buttonsFromString(settings()->buttonsOnRight()); + const auto excludeFromCapture = KDecoration3::DecorationButtonType::ExcludeFromCapture; + return left.contains(excludeFromCapture) || right.contains(excludeFromCapture); +} + +void KCMKWinDecoration::checkExcludeFromCaptureButtonPresence() +{ + const bool selected = isExcludeFromCaptureSelected(); + if (m_excludeFromCaptureButtonSelected == selected) { + return; + } + + m_excludeFromCaptureButtonSelected = selected; + Q_EMIT excludeFromCaptureButtonSelectedChanged(); +} + +#include "kcm.moc" +#include "moc_kcm.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/kcm.h b/local/recipes/kde/kwin/source/src/kcms/decoration/kcm.h new file mode 100644 index 0000000000..784d8c9d72 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/kcm.h @@ -0,0 +1,103 @@ +/* + SPDX-FileCopyrightText: 2019 Valerio Pilo + SPDX-FileCopyrightText: 2019 Cyril Rossi + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include +#include + +class QAbstractItemModel; +class QSortFilterProxyModel; +class QQuickItem; + +namespace KDecoration3 +{ +enum class BorderSize; + +namespace Preview +{ +class ButtonsModel; +} +namespace Configuration +{ +class DecorationsModel; +} +} + +class KWinDecorationSettings; +class KWinDecorationData; + +class KCMKWinDecoration : public KQuickManagedConfigModule +{ + Q_OBJECT + Q_PROPERTY(KWinDecorationSettings *settings READ settings CONSTANT) + Q_PROPERTY(QSortFilterProxyModel *themesModel READ themesModel CONSTANT) + Q_PROPERTY(QStringList borderSizesModel READ borderSizesModel NOTIFY themeChanged) + Q_PROPERTY(int borderIndex READ borderIndex WRITE setBorderIndex NOTIFY borderIndexChanged) + Q_PROPERTY(int borderSize READ borderSize NOTIFY borderSizeChanged) + Q_PROPERTY(int recommendedBorderSize READ recommendedBorderSize CONSTANT) + Q_PROPERTY(int theme READ theme WRITE setTheme NOTIFY themeChanged) + Q_PROPERTY(QAbstractListModel *leftButtonsModel READ leftButtonsModel CONSTANT) + Q_PROPERTY(QAbstractListModel *rightButtonsModel READ rightButtonsModel CONSTANT) + Q_PROPERTY(QAbstractListModel *availableButtonsModel READ availableButtonsModel CONSTANT) + Q_PROPERTY(bool excludeFromCaptureButtonSelected READ excludeFromCaptureButtonSelected NOTIFY excludeFromCaptureButtonSelectedChanged) + +public: + KCMKWinDecoration(QObject *parent, const KPluginMetaData &metaData); + + KWinDecorationSettings *settings() const; + QSortFilterProxyModel *themesModel() const; + QAbstractListModel *leftButtonsModel(); + QAbstractListModel *rightButtonsModel(); + QAbstractListModel *availableButtonsModel() const; + bool excludeFromCaptureButtonSelected() const; + QStringList borderSizesModel() const; + int borderIndex() const; + int borderSize() const; + int recommendedBorderSize() const; + int theme() const; + + void setBorderIndex(int index); + void setBorderSize(int index); + void setBorderSize(KDecoration3::BorderSize size); + void setTheme(int index); + +Q_SIGNALS: + void themeChanged(); + void borderIndexChanged(); + void borderSizeChanged(); + void excludeFromCaptureButtonSelectedChanged(); + +public Q_SLOTS: + void load() override; + void save() override; + void defaults() override; + void reloadKWinSettings(); + +private Q_SLOTS: + void onLeftButtonsChanged(); + void onRightButtonsChanged(); + void checkExcludeFromCaptureButtonPresence(); + +private: + bool isSaveNeeded() const override; + bool isExcludeFromCaptureSelected() const; + + int borderSizeIndexFromString(const QString &size) const; + QString borderSizeIndexToString(int index) const; + + KDecoration3::Configuration::DecorationsModel *m_themesModel; + QSortFilterProxyModel *m_proxyThemesModel; + + KDecoration3::Preview::ButtonsModel *m_leftButtonsModel; + KDecoration3::Preview::ButtonsModel *m_rightButtonsModel; + KDecoration3::Preview::ButtonsModel *m_availableButtonsModel; + + int m_borderSizeIndex = -1; + KWinDecorationData *m_data; + bool m_excludeFromCaptureButtonSelected = false; +}; diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/kcm_kwindecoration.json b/local/recipes/kde/kwin/source/src/kcms/decoration/kcm_kwindecoration.json new file mode 100644 index 0000000000..12889b468b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/kcm_kwindecoration.json @@ -0,0 +1,149 @@ +{ + "Categories": "Qt;KDE;X-KDE-settings-looknfeel;", + "KPlugin": { + "BugReportUrl": "https://bugs.kde.org/enter_bug.cgi?product=systemsettings&component=kcm_kwindecoration", + "Description": "Configure window titlebars and borders", + "Description[ar]": "اضبط حدود وشريط عنوان النوافذ", + "Description[az]": "Pəncərə başlıq zolağını və çərçivələrini tənzimləyin", + "Description[be]": "Наладжванне панэляў загалоўкаў і рамкі", + "Description[bg]": "Конфигуриране на заглавни ленти и рамки на прозорци", + "Description[ca@valencia]": "Configura les barres de títols i les vores de les finestres", + "Description[ca]": "Configura les barres de títols i les vores de les finestres", + "Description[cs]": "Nastavit popisky a okraje okna", + "Description[da]": "Konfigurér vinduers titellinjer og -kanter", + "Description[de]": "Titelleiste und Ränder von Fenstern einrichten", + "Description[en_GB]": "Configure window title-bars and borders", + "Description[eo]": "Agordi fenestrajn titolbretojn kaj randojn", + "Description[es]": "Configurar las barras de título y los bordes de las ventanas", + "Description[et]": "Akende tiitliriba ja raami seadistamine", + "Description[eu]": "Konfiguratu leihoen titulu-barrak eta ertzak", + "Description[fi]": "Ikkunoiden otsikkopalkkien ja reunojen asetukset", + "Description[fr]": "Configurer les barres de titre et les bordures des fenêtres", + "Description[gl]": "Configurar as barras de título e os bordos das xanelas.", + "Description[he]": "הגדרת שורות כותרות ומסגרות של חלונות", + "Description[hu]": "Az ablakok címsorainak és szegélyeinek beállítása", + "Description[ia]": "Configura barras de titulo e margines", + "Description[id]": "Konfigurasikan bingkai dan bilah judul jendela", + "Description[is]": "Grunnstilla titilstikur og ramma á gluggum", + "Description[it]": "Configura la barra del titolo e i bordi delle finestre", + "Description[ja]": "ウィンドウのタイトルバーと枠を設定", + "Description[ka]": "ფანჯრის სათაურის ზოლისა და საზღვრების მორგება", + "Description[ko]": "창 제목 표시줄과 경계선 설정", + "Description[lt]": "Konfigūruoti langų antraštės juostas ir rėmelius", + "Description[lv]": "Konfigurēt loga virsrakstjoslas un apmales", + "Description[nb]": "Sett opp tittellinjer og vindusrammer", + "Description[nl]": "Titelbalken en randen van venster configureren", + "Description[nn]": "Set opp tittellinjer og vindaugsrammer", + "Description[pl]": "Ustawienia pasków tytułów i obramowań okien", + "Description[pt]": "Configurar as barras de título e contornos das janelas", + "Description[pt_BR]": "Configure as barras de títulos e bordas da janela", + "Description[ro]": "Configurează barele de titlu și contururile ferestrelor", + "Description[ru]": "Настройка заголовка и границ окон", + "Description[sa]": "विण्डो शीर्षकपट्टिकाः सीमाः च विन्यस्यताम्", + "Description[sk]": "Nastaviť záhlavia a okraje okna", + "Description[sl]": "Prilagodite naslovne vrstice in obrobe oken", + "Description[sv]": "Anpassa namnlister och kanter för fönster", + "Description[ta]": "சாளர விளிம்புகளையும் தலைப்புப்பட்டைகளையும் அமையுங்கள்", + "Description[tr]": "Masaüstü pencere başlık çubukları ve kenarlıklarını yapılandır", + "Description[uk]": "Налаштовування смужок заголовків та рамок вікон", + "Description[vi]": "Cấu hình thanh tiêu đề và viền của cửa sổ", + "Description[zh_CN]": "配置窗口标题栏和边框", + "Description[zh_TW]": "設定視窗標題列和邊框", + "FormFactors": [ + "desktop", + "tablet" + ], + "Icon": "preferences-desktop-theme-windowdecorations", + "Name": "Window Decorations", + "Name[ar]": "زخارف النوافذ", + "Name[az]": "Pəncərə haşiyələri", + "Name[be]": "Аздабленне акон", + "Name[bg]": "Декорации на прозорци", + "Name[ca@valencia]": "Decoració de les finestres", + "Name[ca]": "Decoració de les finestres", + "Name[cs]": "Dekorace oken", + "Name[da]": "Vinduedekorationer", + "Name[de]": "Fensterdekoration", + "Name[en_GB]": "Window Decorations", + "Name[eo]": "Fenestraj Dekoracioj", + "Name[es]": "Decoraciones de las ventanas", + "Name[et]": "Akna dekoratsioonid", + "Name[eu]": "Leiho-apaindurak", + "Name[fi]": "Ikkunakoristelu", + "Name[fr]": "Décorations de fenêtres", + "Name[gl]": "Decoración de xanelas", + "Name[he]": "עיטורי חלונות", + "Name[hu]": "Ablakdekorációk", + "Name[ia]": "Decorationes de fenestra", + "Name[id]": "Dekorasi Jendela", + "Name[is]": "Gluggaskreytingar", + "Name[it]": "Decorazioni delle finestre", + "Name[ja]": "ウィンドウの装飾", + "Name[ka]": "ფანჯრის დეკორაციები", + "Name[ko]": "창 장식", + "Name[lt]": "Langų dekoracijos", + "Name[lv]": "Logu noformējums", + "Name[nb]": "Vindusdekorasjon", + "Name[nl]": "Vensterdecoraties", + "Name[nn]": "Vindaugspynt", + "Name[pl]": "Wygląd okien", + "Name[pt]": "Decorações das Janelas", + "Name[pt_BR]": "Decorações da janela", + "Name[ro]": "Decorațiile ferestrelor", + "Name[ru]": "Оформление окон", + "Name[sa]": "खिडकी सजावट", + "Name[sk]": "Dekorácie okien", + "Name[sl]": "Okrasitev oken", + "Name[sv]": "Fönsterdekorationer", + "Name[ta]": "சாளர விளிம்புகள்", + "Name[tr]": "Pencere Dekorasyonları", + "Name[uk]": "Обрамлення вікон", + "Name[vi]": "Trang trí cửa sổ", + "Name[zh_CN]": "窗口装饰", + "Name[zh_TW]": "視窗裝飾" + }, + "X-DocPath": "kcontrol/kwindecoration/index.html", + "X-KDE-Keywords": "kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration,window decorations,titlebar,window buttons,window border,skin,shadow", + "X-KDE-Keywords[ar]": "كوين,نافذة,مدير,حدود,نمط,موضوع,نظرة,إحساس,تخطيط,زر,مقبض,حافة,KWM,ديكور,زخارف النوافذ,شريط العنوان,أزرار النافذة,حدود النافذة,الجلد,الظل", + "X-KDE-Keywords[bg]": "kwin,прозорец,мениджър,граница,стил,тема,изглед,усещане,оформление,бутон,ръкохватка,ръб,kwm,декорация,декорации на прозорци,заглавна лента,бутони на прозорци,рамка на прозореца,кожа,сянка", + "X-KDE-Keywords[ca@valencia]": "kwin,finestra,gestor,vora,estil,tema,aspecte,comportament,disposició,botó,ansa,vora,kwm,decoració,decoracions de finestra,barra de títol,botons de finestra,vora de finestra,aparença,ombra", + "X-KDE-Keywords[ca]": "kwin,finestra,gestor,vora,estil,tema,aspecte,comportament,disposició,botó,nansa,vora,kwm,decoració,decoracions de finestra,barra de títol,botons de finestra,vora de finestra,aparença,ombra", + "X-KDE-Keywords[cs]": "kwin,okno,správce,okraj,styl,motiv,vzhled,pocit,rozvržení,tlačítko,madlo,okraj,kwm,dekorace,dekorace oken,pruh titulku,tlačítka okna,okraj okna, stín", + "X-KDE-Keywords[de]": "Fenster,Rahmen,Design,Stile,Themes,Optik,Erscheinungsbild,Knöpfe,Ränder,Dekorationen,Schatten", + "X-KDE-Keywords[en_GB]": "kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration,window decorations,titlebar,window buttons,window border,skin,shadow", + "X-KDE-Keywords[es]": "kwin,ventana,gestor,borde,estilo,tema,aspecto,apariencia,organización,disposición,esquema,botón,asa,tirador,borde,kwm,decoración,decoraciones de las ventanas,barra de título,botones de la ventana,borde de la ventana,piel,sombra,sombreado", + "X-KDE-Keywords[eu]": "kwin,leihoa,kudeatzailea,ertza,bazterra,muga,estiloa,gaia,itxura,izaera,antolaera,botoia,heldulekua,kirtena,kwm,apainketa,apaindura,apaingarria,leihoen apaingarriak,titulu-barra,leihoaren botoiak,leihoaren ertzak,azala,itzala", + "X-KDE-Keywords[fi]": "kwin,ikkuna,hallinta,ikkunaohjelma,reuna,reunaviiva,tyyli,teema,ulkoasu,tuntuma,asettelu,painike,nappin,kahva,reuna,laita,kwin,koristelu,koristus,ikkunakoristeet,otsikkorivi,otsikkopalkki,ikkunan reunan,varjo,varjostus", + "X-KDE-Keywords[fr]": "kwin, fenêtre, gestionnaire, bordure, style, thème, apparence, toucher, mise en page, bouton, poignée, bord, kwm, décoration, décorations de fenêtre, barre de titre, boutons de fenêtre, bordure de fenêtre, habillage, ombre", + "X-KDE-Keywords[gl]": "kwin,window,xanela,ventá,fiestra,manager,xestor,manexador,border,bordo,esquina,extremo,style,estilo,theme,tema,look,aparencia,feel,comportamento,layout,disposición,button,botón,handle,asa,edge,kwm,decoration,decoración,window decorations,decoracións de xanelas,decoracións de ventás,decoracións de fiestras,titlebar,barra de título,window buttons,botóns de xanelas,botóns de ventás,botóns de fiestras,window border,bordo de xanela,bordo de ventá,bordo de fiestra,skin,shadow,sombra", + "X-KDE-Keywords[he]": "kwin,מראה,מנהל,גבול,מסגרת,סגנון,ערכת עיצוב,ערכת נושא,תחושה,פריסה,כפתור,ידית,קצה,שול,kwm,עיטור,עיטורי חלון,שורת כותרת,כפתורי חלון,גבול חלון,מסגרת חלון,מעטפת,עור,סקין,צל,הצללה,צללית", + "X-KDE-Keywords[hu]": "kwin,ablak,kezelő,szegély,stílus,téma,megjelenés,kinézet,elrendezés,gomb,kezelő,szél,kwm,dekoráció,ablakdekorációk,címsor,ablakgombok,ablakszegéy,felület,árnyék", + "X-KDE-Keywords[ia]": "kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration,window decorations,titlebar,window buttons,window border,skin,shadow", + "X-KDE-Keywords[is]": "kwin,gluggi,stjóri,jaðar,stílsnið,þema,útlit,tilfinning,framsetning,hnappur,grip,brún,kwm,skreyting,gluggaskreytingar,titilstika,gluggahnappar,gluggabrún,yfirlag,skuggi", + "X-KDE-Keywords[it]": "kwin,finestra,gestore,bordo,stile,tema,aspetto,impressione,disposizione,pulsante,maniglia,bordo,kwm,decorazione,decorazioni finestra,barra del titolo,pulsanti finestra,bordo finestra,tema,ombra", + "X-KDE-Keywords[ja]": "kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration,window decorations,titlebar,window buttons,window border,skin,shadow, ウィンドウ,マネージャー,管理,ボーダー,枠線,境界,スタイル,テーマ,ルックアンドフィール,見た目と操作感,外観,レイアウト,配置,ボタン,ハンドル,エッジ,縁,端,デコレーション,装飾,ウィンドウの装飾,タイトルバー,ウィンドウボタン,ウィンドウの枠,スキン,外装,シャドウ,影,ドロップシャドウ", + "X-KDE-Keywords[ka]": "kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration,window decorations,titlebar,window buttons,window border,skin,shadow,ჩრდილი,სკინი,ფანჯრის საზღვარი,ფანჯრის ღილაკები,ფანჯრის დეკორაცია,დეკორაცია,განლაგება,გარეგნობა,თემა,საზღვარი", + "X-KDE-Keywords[ko]": "창,관리자,경계선,테두리,스타일,테마,레이아웃,단추,핸들,경계선,장식,창 장식,제목 표시줄,창 단추,창 경계선,창 테두리,그림자", + "X-KDE-Keywords[lt]": "kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration,window decorations,titlebar,window buttons,window border,skin,shadow,langas,langų,langu,lango,tvarkytuvė,tvarkytuve,rėmelis,stilius,apipavidalinimas,tema,išvaizda,isvaizda,turinys,išdėstymas,isdestymas,mygtukas,rankenėlė,rankenele,apdoroti,kraštas,lango dekoracija,lango dekoracijos,langų dekoracija,langu dekoracija,langų dekoracijos,langu dekoracijos,antraštės juosta,antrastes juosta,pavadinimo juosta,lango mygtukai,langų mygtukai,langu mygtukai,lango rėmelis,lango remelis,langų rėmeliai,langu remeliai,šešėlis,seselis", + "X-KDE-Keywords[lv]": "kwin,logs,pārvaldnieks,apmale, rāmis,stils,motīvs,izskats,uzvedība,izkārtojums,poga,turis,mala,kwm,dekorācija,logu dekorācijas,virsrakstjosla,loga pogas,loga apmale,āda,ēna", + "X-KDE-Keywords[nb]": "kwin,vindu,behandler,ramme,kantlinje,stil,tema,lås,utforming,knapp,håndtak,kant,kwm,dekorasjon,pynt,vindusdekorasjon,vinduspynt,tittellinje,vindusknapper,vinduskant,vindusramme,tema,drakt,utseende,skygge", + "X-KDE-Keywords[nl]": "kwin,venster,beheerder,rand,stijl,thema,uiterlijk,gevoel,indeling,knop,hendel,rand,kwm,decoratie,vensterdecoraties,titelbalk,vensterknoppen, vensterrand,skin,schaduw", + "X-KDE-Keywords[nn]": "kwin,vindauge,handsamar,ramme,kantlinje,stil,tema,lås,utforming,knapp,handtak,kant,kwm,dekorasjon,pynt,vindaugsdekorasjon,vindaugspynt,tittellinje,vindaugsknappar,vindaugskant,vindaugsramme,tema,drakt,utsjånad,skugge", + "X-KDE-Keywords[pl]": "kwin,okno,menadżer,obramowanie,styl,motyw,wygląd,odczucie,układ,przycisk, uchwyt,krawędź,kwm,ozdoba,ozdoby okien,pasek tytułu,przyciski okna,obramowanie okna,skóra,cień", + "X-KDE-Keywords[pt_BR]": "kwin,janela,gerenciador,borda,estilo,tema,aparência,comportamento,layout,botão,borda,kwm,decoração,decorações de janela,barra de título,botões de janela,borda de janela,skin,sombra", + "X-KDE-Keywords[ro]": "kwin,fereastră,ferestre,gestionar,contur,stil,tematică,aspect,comportament,aranjament,buton,mâner,muchie,kwm,decorație,decorații fereastră,bară de titlu,butoane fereastră,contur fereastră,conturul ferestrei,fațetă,umbră,umbre", + "X-KDE-Keywords[ru]": "kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration,window decorations,titlebar,window buttons,window border,skin,shadow,окно,диспетчер,граница,стиль,тема,внешний вид,удобство,компоновка,кнопка,маркер,край,оформление окон,строка заголовка,заголовок,кнопки окна,граница окна,тень", + "X-KDE-Keywords[sa]": "kwin,खिड़की,प्रबंधक,सीमा,शैली,थीम,देखें,अनुभूति,लेआउट,बटन,हैंडल,धार,kwm,सजावट,खिड़की सजावट,शीर्षकपट्टी,खिड़की बटन,खिड़की सीमा,चमड़ी,छाया", + "X-KDE-Keywords[sk]": "kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration,window decorations,titlebar,window buttons,window border,skin,shadow", + "X-KDE-Keywords[sl]": "kwin,okno,upravljalnik,meja,slog,tema,videz,občutek,postaviten,gumb,ročka,rob,kwm,dekoracija,okenske dekoracije,naslovna vrstica,okenski gumbi,meje oken,preobleka,senca", + "X-KDE-Keywords[sv]": "kwin,fönster,hanterare,kant,stil,tema,utseende,känsla,layout,knapp,grepp,kant,kwm,dekoration,fönsterdekorationer,namnlist,fönsterknappar,fönsterkant,skal,skugga", + "X-KDE-Keywords[tr]": "kwin,pencere,yönetici,kenarlık,biçem,stil,tema,görünüş,his,yerleşim,düzen,düğme,tutaç,tutamak,kenar,kwm,dekorasyon,pencere dekorasyonu,başlık çubuğu,pencere düğmeleri,pencere kenarlığı,tema,gölge", + "X-KDE-Keywords[uk]": "kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration,window decorations,titlebar,window buttons,window border,skin,shadow,вікно,вікна,керування,менеджер,рамка,межа,стиль,тема,вигляд,поведінка,компонування,кнопка,елемент,край,декорації,обрамлення,смужка заголовка,кнопки,рамка,оформлення,тінь", + "X-KDE-Keywords[zh_CN]": "kwin,window,manager,border,style,theme,look,feel,layout,button,handle,edge,kwm,decoration,window decorations,titlebar,window buttons,window border,skin,shadow,chuangkou,guanliqi,chuangkouguanliqi,biankuang,fengge,yangshi,zhuti,jiemianwaiguan,waiguan,guangan,shijuexiaoguo,shixiao,buju,anniu,tuobing,shoubing,bianyuan,zhuangshi,chuangkouzhuangshi,biaotilan,chuangkouanniu,chuangkoubiankuang,chuangkouwaikuang,pifu,touying,yinying,窗口,管理器,窗口管理器,边框,风格,样式,主题,界面外观,外观,观感,视觉效果,视效,布局,按钮,拖柄,手柄,边缘,装饰,窗口装饰,标题栏,窗口按钮,窗口边框,窗口外框,皮肤,投影,阴影", + "X-KDE-Keywords[zh_TW]": "kwin,視窗,視窗管理員,邊框,風格,樣式,主題,佈局,配置,按鈕,邊緣,視窗裝飾,裝飾,標題列,標題欄,標題按鈕,視窗按鈕,陰影", + "X-KDE-OnlyShowOnQtPlatforms": [ + "wayland" + ], + "X-KDE-System-Settings-Parent-Category": "themes", + "X-KDE-Weight": 60 +} diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/kwin-applywindowdecoration.cpp b/local/recipes/kde/kwin/source/src/kcms/decoration/kwin-applywindowdecoration.cpp new file mode 100644 index 0000000000..c3510cf246 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/kwin-applywindowdecoration.cpp @@ -0,0 +1,130 @@ +/* + SPDX-FileCopyrightText: 2021 Dan Leinir Turthra Jensen + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "kwindecorationsettings.h" + +#include "decorationmodel.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + int exitCode{0}; + QCoreApplication::setApplicationName(QStringLiteral("kwin-applywindowdecoration")); + QCoreApplication::setApplicationVersion(QStringLiteral("1.0")); + QCoreApplication::setOrganizationDomain(QStringLiteral("kde.org")); + KLocalizedString::setApplicationDomain(QByteArrayLiteral("kwin-applywindowdecoration")); + + QCommandLineParser *parser = new QCommandLineParser; + parser->addHelpOption(); + parser->setApplicationDescription(i18n("This tool allows you to set the window decoration theme for the currently active session, without accidentally setting it to one that is either not available, or which is already set.")); + parser->addPositionalArgument(QStringLiteral("theme"), i18n("The name of the window decoration theme you wish to set for KWin. Passing a full path will attempt to find a theme in that directory, and then apply that if one can be deduced.")); + parser->addOption(QCommandLineOption(QStringLiteral("list-themes"), i18n("Show all the themes available on the system (and which is the current theme)"))); + parser->process(app); + + KDecoration3::Configuration::DecorationsModel *model = new KDecoration3::Configuration::DecorationsModel(&app); + model->init(); + KWinDecorationSettings *settings = new KWinDecorationSettings(&app); + QTextStream ts(stdout); + if (!parser->positionalArguments().isEmpty()) { + QString requestedTheme{parser->positionalArguments().constFirst()}; + if (requestedTheme.endsWith(QStringLiteral("/*"))) { + // Themes installed through KNewStuff will commonly be given an installed files entry + // which has the main directory name and an asterix to say the cursors are all in that directory, + // and since one of the main purposes of this tool is to allow adopting things from a kns dialog, + // we handle that little weirdness here. + requestedTheme.remove(requestedTheme.length() - 2, 2); + } + + bool themeResolved{true}; + if (requestedTheme.contains(QStringLiteral("/"))) { + themeResolved = false; + if (QFileInfo::exists(requestedTheme) && QFileInfo(requestedTheme).isDir()) { + // Since this is the name of a directory, let's do a bit of checking to see + // if we know enough about it to deduce that this is, in fact, a theme. + QStringList splitTheme = requestedTheme.split(QStringLiteral("/"), Qt::SkipEmptyParts); + if (splitTheme.count() > 3 && splitTheme[splitTheme.count() - 3] == QLatin1StringView("aurorae") && splitTheme[splitTheme.count() - 2] == QLatin1StringView("themes")) { + // We think this is an aurorae theme, but let's just make a little more certain... + QString file(QStringLiteral("aurorae/themes/%1/metadata.desktop").arg(splitTheme.last())); + QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, file); + if (!path.isEmpty() && path == QStringLiteral("%1/metadata.desktop").arg(requestedTheme)) { + requestedTheme = QString("__aurorae__svg__").append(splitTheme.last()); + themeResolved = true; + ts << i18n("Resolved %1 to the KWin Aurorae theme \"%2\", and will attempt to set that as your current theme.") + .arg(parser->positionalArguments().first(), requestedTheme) + << Qt::endl; + } + } + } else { + ts << i18n("You attempted to pass a file path, but this could not be resolved to a theme, and we will have to abort, due to having no theme to set") << Qt::endl; + exitCode = -1; + } + } + + if (settings->theme() == requestedTheme) { + ts << i18n("The requested theme \"%1\" is already set as the window decoration theme.", requestedTheme) << Qt::endl; + // not an error condition, just nothing happens + } else if (themeResolved) { + int index{-1}; + QStringList availableThemes; + for (int i = 0; i < model->rowCount(); ++i) { + const QString themeName = model->data(model->index(i), KDecoration3::Configuration::DecorationsModel::ThemeNameRole).toString(); + if (requestedTheme == themeName) { + index = i; + break; + } + availableThemes << themeName; + } + if (index > -1) { + settings->setTheme(model->data(model->index(index), KDecoration3::Configuration::DecorationsModel::ThemeNameRole).toString()); + settings->setPluginName(model->data(model->index(index), KDecoration3::Configuration::DecorationsModel::PluginNameRole).toString()); + if (settings->save()) { + // Send a signal to all kwin instances + QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/KWin"), + QStringLiteral("org.kde.KWin"), + QStringLiteral("reloadConfig")); + QDBusConnection::sessionBus().send(message); + ts << i18n("Successfully applied the cursor theme %1 to your current Plasma session", + model->data(model->index(index), KDecoration3::Configuration::DecorationsModel::ThemeNameRole).toString()) + << Qt::endl; + } else { + ts << i18n("Failed to save your theme settings - the reason is unknown, but this is an unrecoverable error. You may find that simply trying again will work."); + exitCode = -1; + } + } else { + ts << i18n("Could not find theme \"%1\". The theme should be one of the following options: %2", requestedTheme, availableThemes.join(QStringLiteral(", "))) << Qt::endl; + exitCode = -1; + } + } + } else if (parser->isSet(QStringLiteral("list-themes"))) { + ts << i18n("You have the following KWin window decoration themes on your system:") << Qt::endl; + for (int i = 0; i < model->rowCount(); ++i) { + const QString displayName = model->data(model->index(i), Qt::DisplayRole).toString(); + const QString themeName = model->data(model->index(i), KDecoration3::Configuration::DecorationsModel::ThemeNameRole).toString(); + if (settings->theme() == themeName) { + ts << QStringLiteral(" * %1 (theme name: %2 - current theme for this Plasma session)").arg(displayName, themeName) << Qt::endl; + } else { + ts << QStringLiteral(" * %1 (theme name: %2)").arg(displayName, themeName) << Qt::endl; + } + } + } else { + parser->showHelp(); + } + QTimer::singleShot(0, &app, [&app, &exitCode]() { + app.exit(exitCode); + }); + + return app.exec(); +} diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/kwindecorationsettings.kcfg b/local/recipes/kde/kwin/source/src/kcms/decoration/kwindecorationsettings.kcfg new file mode 100644 index 0000000000..9ff5a8ec88 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/kwindecorationsettings.kcfg @@ -0,0 +1,45 @@ + + + + + + + org.kde.breeze + + + + Breeze + + + + Normal + + + + true + + + + false + + + + true + + + + MSE + + + + HIAX + + + + false + + + diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/kwindecorationsettings.kcfgc b/local/recipes/kde/kwin/source/src/kcms/decoration/kwindecorationsettings.kcfgc new file mode 100644 index 0000000000..8207257813 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/kwindecorationsettings.kcfgc @@ -0,0 +1,7 @@ +File=kwindecorationsettings.kcfg +ClassName=KWinDecorationSettings +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true +Notifiers=buttonsOnLeft,buttonsOnRight,theme diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/ui/ButtonGroup.qml b/local/recipes/kde/kwin/source/src/kcms/decoration/ui/ButtonGroup.qml new file mode 100644 index 0000000000..cfd81ad84e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/ui/ButtonGroup.qml @@ -0,0 +1,65 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +import QtQuick + +import org.kde.kirigami as Kirigami +import org.kde.kwin.private.kdecoration as KDecoration + +ListView { + id: view + property string key + property bool dragActive: false + property int iconSize: Kirigami.Units.iconSizes.small + orientation: ListView.Horizontal + interactive: false + spacing: Kirigami.Units.smallSpacing + implicitHeight: iconSize + implicitWidth: count * (iconSize + Kirigami.Units.smallSpacing) - Math.min(1, count) * Kirigami.Units.smallSpacing + delegate: Item { + width: view.iconSize + height: view.iconSize + KDecoration.Button { + id: button + property int itemIndex: index + property var buttonsModel: parent.ListView.view.model + bridge: bridgeItem.bridge + settings: settingsItem + type: model["button"] + width: view.iconSize + height: view.iconSize + anchors.fill: Drag.active ? undefined : parent + Drag.keys: [ "decoButtonRemove", view.key ] + Drag.active: dragArea.drag.active + Drag.onActiveChanged: view.dragActive = Drag.active + color: palette.windowText + opacity: parent.enabled ? 1.0 : 0.3 + } + MouseArea { + id: dragArea + cursorShape: drag.target.Drag.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor + anchors.fill: parent + drag.target: button + onReleased: { + if (drag.target.Drag.target) { + drag.target.Drag.drop(); + } else { + drag.target.Drag.cancel(); + } + } + } + } + add: Transition { + NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: Kirigami.Units.longDuration/2 } + NumberAnimation { property: "scale"; from: 0; to: 1.0; duration: Kirigami.Units.longDuration/2 } + } + move: Transition { + NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: Kirigami.Units.longDuration/2 } + NumberAnimation { property: "scale"; from: 0; to: 1.0; duration: Kirigami.Units.longDuration/2 } + } + displaced: Transition { + NumberAnimation { properties: "x,y"; duration: Kirigami.Units.longDuration; easing.type: Easing.OutBounce } + } +} diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/ui/Buttons.qml b/local/recipes/kde/kwin/source/src/kcms/decoration/ui/Buttons.qml new file mode 100644 index 0000000000..ab423ae3f6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/ui/Buttons.qml @@ -0,0 +1,264 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Valerio Pilo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 + +import org.kde.kcmutils as KCM +import org.kde.kirigami as Kirigami +import org.kde.kwin.private.kdecoration as KDecoration + +// Fake Window +Rectangle { + id: baseLayout + + readonly property int buttonIconSize: Kirigami.Units.iconSizes.medium + readonly property int titleBarSpacing: Kirigami.Units.largeSpacing + readonly property bool draggingTitlebarButtons: leftButtonsView.dragActive || rightButtonsView.dragActive + readonly property bool hideDragHint: draggingTitlebarButtons || availableButtonsGrid.dragActive + + color: palette.base + radius: Kirigami.Units.cornerRadius + + KDecoration.Bridge { + id: bridgeItem + plugin: "org.kde.breeze" + } + KDecoration.Settings { + id: settingsItem + bridge: bridgeItem.bridge + } + + ColumnLayout { + anchors.fill: parent + + // Fake titlebar + Rectangle { + Layout.fillWidth: true + implicitHeight: buttonPreviewRow.implicitHeight + 2 * baseLayout.titleBarSpacing + radius: Kirigami.Units.cornerRadius + gradient: Gradient { + GradientStop { position: 0.0; color: palette.midlight } + GradientStop { position: 1.0; color: palette.window } + } + + RowLayout { + id: buttonPreviewRow + anchors { + margins: baseLayout.titleBarSpacing + left: parent.left + right: parent.right + top: parent.top + } + + ButtonGroup { + id: leftButtonsView + iconSize: baseLayout.buttonIconSize + model: kcm.leftButtonsModel + key: "decoButtonLeft" + + Rectangle { + visible: stateBindingButtonLeft.nonDefaultHighlightVisible + anchors.fill: parent + Layout.margins: Kirigami.Units.smallSpacing + color: "transparent" + border.color: Kirigami.Theme.neutralTextColor + border.width: 1 + radius: Kirigami.Units.cornerRadius + } + + KCM.SettingStateBinding { + id: stateBindingButtonLeft + configObject: kcm.settings + settingName: "buttonsOnLeft" + } + } + + QQC2.Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font.bold: true + text: i18n("Titlebar") + } + ButtonGroup { + id: rightButtonsView + iconSize: baseLayout.buttonIconSize + model: kcm.rightButtonsModel + key: "decoButtonRight" + + Rectangle { + visible: stateBindingButtonRight.nonDefaultHighlightVisible + anchors.fill: parent + Layout.margins: Kirigami.Units.smallSpacing + color: "transparent" + border.color: Kirigami.Theme.neutralTextColor + border.width: 1 + radius: Kirigami.Units.cornerRadius + } + + KCM.SettingStateBinding { + id: stateBindingButtonRight + configObject: kcm.settings + settingName: "buttonsOnRight" + } + } + } + DropArea { + id: titleBarDropArea + anchors { + fill: parent + margins: -baseLayout.titleBarSpacing + } + keys: [ "decoButtonAdd", "decoButtonRight", "decoButtonLeft" ] + onEntered: { + drag.accept(); + } + onDropped: { + var view = undefined; + var left = drag.x - (leftButtonsView.x + leftButtonsView.width); + var right = drag.x - rightButtonsView.x; + if (Math.abs(left) <= Math.abs(right)) { + if (leftButtonsView.enabled) { + view = leftButtonsView; + } + } else { + if (rightButtonsView.enabled) { + view = rightButtonsView; + } + } + if (!view) { + return; + } + var point = mapToItem(view, drag.x, drag.y); + var index = 0 + for(var childIndex = 0 ; childIndex < (view.count - 1) ; childIndex++) { + var child = view.contentItem.children[childIndex] + if (child.x > point.x) { + break + } + index = childIndex + 1 + } + if (drop.keys.indexOf("decoButtonAdd") !== -1) { + view.model.add(index, drag.source.type); + } else if (drop.keys.indexOf("decoButtonLeft") !== -1) { + if (view === leftButtonsView) { + // move in same view + if (index !== drag.source.itemIndex) { + drag.source.buttonsModel.move(drag.source.itemIndex, index); + } + } else { + // move to right view + view.model.add(index, drag.source.type); + drag.source.buttonsModel.remove(drag.source.itemIndex); + } + } else if (drop.keys.indexOf("decoButtonRight") !== -1) { + if (view === rightButtonsView) { + // move in same view + if (index !== drag.source.itemIndex) { + drag.source.buttonsModel.move(drag.source.itemIndex, index); + } + } else { + // move to left view + view.model.add(index, drag.source.type); + drag.source.buttonsModel.remove(drag.source.itemIndex); + } + } + } + } + } + GridView { + id: availableButtonsGrid + property bool dragActive: false + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: availableButtonsGrid.cellHeight * 2 + Layout.margins: Kirigami.Units.largeSpacing + cellWidth: Kirigami.Units.gridUnit * 6 + cellHeight: Kirigami.Units.gridUnit * 6 + model: kcm.availableButtonsModel + interactive: false + + delegate: ColumnLayout { + width: availableButtonsGrid.cellWidth - Kirigami.Units.largeSpacing + height: availableButtonsGrid.cellHeight - Kirigami.Units.largeSpacing + opacity: baseLayout.draggingTitlebarButtons ? 0.15 : 1.0 + + Rectangle { + Layout.alignment: Qt.AlignHCenter + color: palette.window + radius: Kirigami.Units.cornerRadius + implicitWidth: baseLayout.buttonIconSize + Kirigami.Units.largeSpacing + implicitHeight: baseLayout.buttonIconSize + Kirigami.Units.largeSpacing + + KDecoration.Button { + id: availableButton + anchors.centerIn: Drag.active ? undefined : parent + bridge: bridgeItem.bridge + settings: settingsItem + type: model["button"] + width: baseLayout.buttonIconSize + height: baseLayout.buttonIconSize + Drag.keys: [ "decoButtonAdd" ] + Drag.active: dragArea.drag.active + Drag.onActiveChanged: availableButtonsGrid.dragActive = Drag.active + color: palette.windowText + } + MouseArea { + id: dragArea + anchors.fill: availableButton + drag.target: availableButton + cursorShape: availableButton.Drag.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor + onReleased: { + if (availableButton.Drag.target) { + availableButton.Drag.drop(); + } else { + availableButton.Drag.cancel(); + } + } + } + } + QQC2.Label { + id: iconLabel + text: model["display"] + Layout.fillWidth: true + Layout.fillHeight: true + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + elide: Text.ElideRight + wrapMode: Text.Wrap + } + } + DropArea { + anchors.fill: parent + keys: [ "decoButtonRemove" ] + onEntered: { + drag.accept(); + } + onDropped: { + drag.source.buttonsModel.remove(drag.source.itemIndex); + } + Kirigami.Heading { + text: i18n("Drop button here to remove it") + font.weight: Font.Bold + level: 2 + anchors.centerIn: parent + opacity: baseLayout.draggingTitlebarButtons ? 1.0 : 0.0 + } + } + } + Text { + id: dragHint + readonly property real dragHintOpacitiy: enabled ? 1.0 : 0.3 + color: palette.windowText + opacity: baseLayout.hideDragHint ? 0.0 : dragHintOpacitiy + Layout.fillWidth: true + Layout.margins: Kirigami.Units.gridUnit * 3 + horizontalAlignment: Text.AlignHCenter + text: i18n("Drag buttons between here and the titlebar") + } + } +} diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/ui/ConfigureTitlebar.qml b/local/recipes/kde/kwin/source/src/kcms/decoration/ui/ConfigureTitlebar.qml new file mode 100644 index 0000000000..809e08701c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/ui/ConfigureTitlebar.qml @@ -0,0 +1,93 @@ +/* + SPDX-FileCopyrightText: 2019 Valerio Pilo + SPDX-FileCopyrightText: 2023 Joshua Goins + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 + +import org.kde.kcmutils as KCM +import org.kde.kirigami as Kirigami + +KCM.AbstractKCM { + title: i18n("Titlebar Buttons") + + framedView: false + + Rectangle { + anchors.fill: parent + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.View + color: Kirigami.Theme.backgroundColor + + Buttons { + anchors.fill: parent + anchors.margins: Kirigami.Units.largeSpacing + } + } + + footer: ColumnLayout { + QQC2.CheckBox { + id: closeOnDoubleClickOnMenuCheckBox + text: i18nc("checkbox label", "Close windows by double clicking the window menu button") + checked: kcm.settings.closeOnDoubleClickOnMenu + onToggled: { + kcm.settings.closeOnDoubleClickOnMenu = checked; + infoLabel.visible = checked; + } + + KCM.SettingStateBinding { + configObject: kcm.settings + settingName: "closeOnDoubleClickOnMenu" + } + } + + Kirigami.InlineMessage { + Layout.fillWidth: true + id: infoLabel + type: Kirigami.MessageType.Information + text: i18nc("popup tip", "Click and hold on the window menu button to show the menu.") + showCloseButton: true + visible: false + } + + QQC2.CheckBox { + id: showToolTipsCheckBox + text: i18nc("checkbox label", "Show titlebar button tooltips") + checked: kcm.settings.showToolTips + onToggled: kcm.settings.showToolTips = checked + + KCM.SettingStateBinding { + configObject: kcm.settings + settingName: "showToolTips" + } + } + RowLayout { + spacing: Kirigami.Units.smallSpacing + + QQC2.CheckBox { + id: alwaysShowExcludeFromCaptureCheckBox + text: i18nc("@option:check", "Always show \"Hide from Screencast\" button") + checked: kcm.settings.alwaysShowExcludeFromCapture + onToggled: kcm.settings.alwaysShowExcludeFromCapture = checked + + KCM.SettingStateBinding { + configObject: kcm.settings + settingName: "alwaysShowExcludeFromCapture" + extraEnabledConditions: kcm.excludeFromCaptureButtonSelected + } + } + + Kirigami.ContextualHelpButton { + readonly property string helpText: i18nc("@info:tooltip", "When unchecked the button only appears if the window is hidden from screencast") + readonly property string additionalHelpText: i18nc("@info:tooltip", "To enable this option, add the \"Hide from screencast\" button to the titlebar first") + toolTipText: alwaysShowExcludeFromCaptureCheckBox.enabled + ? helpText + : helpText + "\n\n" + additionalHelpText + } + } + } +} diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/ui/Themes.qml b/local/recipes/kde/kwin/source/src/kcms/decoration/ui/Themes.qml new file mode 100644 index 0000000000..ddb2ccfef6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/ui/Themes.qml @@ -0,0 +1,117 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Valerio Pilo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +import QtQuick + +import org.kde.kcmutils as KCM +import org.kde.kirigami as Kirigami +import org.kde.kwin.private.kdecoration as KDecoration + +KCM.GridView { + function updateDecoration(item, marginTopLeft, marginBottomRight) { + const mainMargin = Kirigami.Units.largeSpacing; + const shd = item.shadow; + item.anchors.leftMargin = mainMargin + marginTopLeft - (shd ? shd.paddingLeft : 0); + item.anchors.rightMargin = mainMargin + marginBottomRight - (shd ? shd.paddingRight : 0); + item.anchors.topMargin = mainMargin + marginTopLeft - (shd ? shd.paddingTop : 0); + item.anchors.bottomMargin = mainMargin + marginBottomRight - (shd ? shd.paddingBottom : 0); + } + + view.model: kcm.themesModel + view.currentIndex: kcm.theme + view.onContentHeightChanged: view.positionViewAtIndex(view.currentIndex, GridView.Visible) + + view.implicitCellWidth: Kirigami.Units.gridUnit * 18 + + framedView: false + + view.delegate: KCM.GridDelegate { + id: delegate + text: model.display + + thumbnailAvailable: true + thumbnail: Rectangle { + anchors.fill: parent + color: palette.base + clip: true + + KDecoration.Bridge { + id: bridgeItem + plugin: model.plugin + theme: model.theme + kcmoduleName: model.kcmoduleName + } + KDecoration.Settings { + id: settingsItem + bridge: bridgeItem.bridge + Component.onCompleted: { + settingsItem.borderSizesIndex = kcm.borderSize; + } + } + KDecoration.Decoration { + id: inactivePreview + bridge: bridgeItem.bridge + settings: settingsItem + anchors.fill: parent + onShadowChanged: updateDecoration(inactivePreview, 0, client.decoration.titleBar.height) + Component.onCompleted: { + client.active = false; + client.caption = model.display; + updateDecoration(inactivePreview, 0, client.decoration.titleBar.height); + } + } + KDecoration.Decoration { + id: activePreview + bridge: bridgeItem.bridge + settings: settingsItem + anchors.fill: parent + onShadowChanged: updateDecoration(activePreview, client.decoration.titleBar.height, 0) + Component.onCompleted: { + client.active = true; + client.caption = model.display; + updateDecoration(activePreview, client.decoration.titleBar.height, 0); + } + } + MouseArea { + anchors.fill: parent + onClicked: delegate.clicked() + onDoubleClicked: delegate.doubleClicked() + } + Connections { + target: kcm + function onBorderSizeChanged() { + settingsItem.borderSizesIndex = kcm.borderSize; + } + } + } + actions: [ + Kirigami.Action { + icon.name: "edit-entry" + tooltip: i18n("Edit %1 Theme…", model.display) + enabled: model.configurable + onTriggered: { + kcm.theme = index; + view.currentIndex = index; + bridgeItem.bridge.configure(delegate); + } + } + ] + + onClicked: { + kcm.theme = index; + view.currentIndex = index; + } + onDoubleClicked: { + kcm.save(); + } + } + Connections { + target: kcm + function onThemeChanged() { + view.currentIndex = kcm.theme; + } + } +} diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/ui/main.qml b/local/recipes/kde/kwin/source/src/kcms/decoration/ui/main.qml new file mode 100644 index 0000000000..b00cff82f4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/ui/main.qml @@ -0,0 +1,76 @@ +/* + SPDX-FileCopyrightText: 2019 Valerio Pilo + SPDX-FileCopyrightText: 2023 Joshua Goins + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 + +import org.kde.kcmutils as KCM +import org.kde.kirigami as Kirigami +import org.kde.newstuff as NewStuff + +KCM.AbstractKCM { + id: root + + title: kcm.name + + framedView: false + + implicitWidth: Kirigami.Units.gridUnit * 48 + implicitHeight: Kirigami.Units.gridUnit * 33 + + actions: [ + Kirigami.Action { + id: borderSizeComboBox + text: i18nc("Selector label", "Window border size:") + + displayComponent: RowLayout { + QQC2.ComboBox { + id: borderSizeComboBox + currentIndex: kcm.borderIndex + flat: true + model: kcm.borderSizesModel + + onActivated: kcm.borderIndex = currentIndex + + KCM.SettingHighlighter { + highlight: kcm.borderIndex !== 0 + } + } + } + }, + Kirigami.Action { + icon.name: "configure" + text: i18nc("button text", "Configure Titlebar Buttons…") + + onTriggered: kcm.push("ConfigureTitlebar.qml") + }, + NewStuff.Action { + configFile: "window-decorations.knsrc" + text: i18nc("@action:button as in, \"Get New Window Decorations\"", "Get New…") + + onEntryEvent: (entry, event) => { + if (event === NewStuff.Engine.StatusChangedEvent) { + kcm.reloadKWinSettings(); + } else if (event === NewStuff.Engine.EntryAdoptedEvent) { + kcm.load(); + } + } + } + ] + + Themes { + id: themes + anchors.fill: parent + + KCM.SettingStateBinding { + configObject: kcm.settings + settingName: "pluginName" + target: themes + } + } +} diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/utils.cpp b/local/recipes/kde/kwin/source/src/kcms/decoration/utils.cpp new file mode 100644 index 0000000000..ae8be6603f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/utils.cpp @@ -0,0 +1,107 @@ +/* + SPDX-FileCopyrightText: 2019 Valerio Pilo + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#include "utils.h" + +#include +#include + +namespace +{ +const QMap s_borderSizes{ + {QStringLiteral("None"), KDecoration3::BorderSize::None}, + {QStringLiteral("NoSides"), KDecoration3::BorderSize::NoSides}, + {QStringLiteral("Tiny"), KDecoration3::BorderSize::Tiny}, + {QStringLiteral("Normal"), KDecoration3::BorderSize::Normal}, + {QStringLiteral("Large"), KDecoration3::BorderSize::Large}, + {QStringLiteral("VeryLarge"), KDecoration3::BorderSize::VeryLarge}, + {QStringLiteral("Huge"), KDecoration3::BorderSize::Huge}, + {QStringLiteral("VeryHuge"), KDecoration3::BorderSize::VeryHuge}, + {QStringLiteral("Oversized"), KDecoration3::BorderSize::Oversized}}; +const QMap s_borderSizeNames{ + {KDecoration3::BorderSize::None, i18n("No window borders")}, + {KDecoration3::BorderSize::NoSides, i18n("No side window borders")}, + {KDecoration3::BorderSize::Tiny, i18n("Tiny window borders")}, + {KDecoration3::BorderSize::Normal, i18n("Normal window borders")}, + {KDecoration3::BorderSize::Large, i18n("Large window borders")}, + {KDecoration3::BorderSize::VeryLarge, i18n("Very large window borders")}, + {KDecoration3::BorderSize::Huge, i18n("Huge window borders")}, + {KDecoration3::BorderSize::VeryHuge, i18n("Very huge window borders")}, + {KDecoration3::BorderSize::Oversized, i18n("Oversized window borders")}}; + +const QHash s_buttonNames{ + {KDecoration3::DecorationButtonType::Menu, QChar('M')}, + {KDecoration3::DecorationButtonType::ApplicationMenu, QChar('N')}, + {KDecoration3::DecorationButtonType::OnAllDesktops, QChar('S')}, + {KDecoration3::DecorationButtonType::ContextHelp, QChar('H')}, + {KDecoration3::DecorationButtonType::Minimize, QChar('I')}, + {KDecoration3::DecorationButtonType::Maximize, QChar('A')}, + {KDecoration3::DecorationButtonType::Close, QChar('X')}, + {KDecoration3::DecorationButtonType::KeepAbove, QChar('F')}, + {KDecoration3::DecorationButtonType::KeepBelow, QChar('B')}, + {KDecoration3::DecorationButtonType::ExcludeFromCapture, QChar('E')}, + {KDecoration3::DecorationButtonType::Spacer, QChar('_')}, +}; +} + +namespace Utils +{ + +QString buttonsToString(const DecorationButtonsList &buttons) +{ + auto buttonToString = [](KDecoration3::DecorationButtonType button) -> QChar { + const auto it = s_buttonNames.constFind(button); + if (it != s_buttonNames.constEnd()) { + return it.value(); + } + return QChar(); + }; + QString ret; + for (auto button : buttons) { + ret.append(buttonToString(button)); + } + return ret; +} + +DecorationButtonsList buttonsFromString(const QString &buttons) +{ + DecorationButtonsList ret; + for (auto it = buttons.begin(); it != buttons.end(); ++it) { + for (auto it2 = s_buttonNames.constBegin(); it2 != s_buttonNames.constEnd(); ++it2) { + if (it2.value() == (*it)) { + ret << it2.key(); + } + } + } + return ret; +} + +DecorationButtonsList readDecorationButtons(const KConfigGroup &config, const QString &key, const DecorationButtonsList &defaultValue) +{ + return buttonsFromString(config.readEntry(key, buttonsToString(defaultValue))); +} + +KDecoration3::BorderSize stringToBorderSize(const QString &name) +{ + auto it = s_borderSizes.constFind(name); + if (it == s_borderSizes.constEnd()) { + // non sense values are interpreted just like normal + return KDecoration3::BorderSize::Normal; + } + return it.value(); +} + +QString borderSizeToString(KDecoration3::BorderSize size) +{ + return s_borderSizes.key(size); +} + +const QMap &getBorderSizeNames() +{ + return s_borderSizeNames; +} + +} // namespace Utils diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/utils.h b/local/recipes/kde/kwin/source/src/kcms/decoration/utils.h new file mode 100644 index 0000000000..ab89a3f375 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/utils.h @@ -0,0 +1,27 @@ +/* + SPDX-FileCopyrightText: 2019 Valerio Pilo + + SPDX-License-Identifier: LGPL-2.0-only +*/ + +#pragma once + +#include +#include + +#include + +using DecorationButtonsList = QList; + +namespace Utils +{ + +QString buttonsToString(const DecorationButtonsList &buttons); +DecorationButtonsList buttonsFromString(const QString &buttons); +DecorationButtonsList readDecorationButtons(const KConfigGroup &config, const QString &key, const DecorationButtonsList &defaultValue); + +KDecoration3::BorderSize stringToBorderSize(const QString &name); +QString borderSizeToString(KDecoration3::BorderSize size); + +const QMap &getBorderSizeNames(); +} diff --git a/local/recipes/kde/kwin/source/src/kcms/decoration/window-decorations.knsrc.cmake b/local/recipes/kde/kwin/source/src/kcms/decoration/window-decorations.knsrc.cmake new file mode 100644 index 0000000000..2dba27b9d3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/decoration/window-decorations.knsrc.cmake @@ -0,0 +1,69 @@ +[KNewStuff3] +Name=Window Decorations +Name[ar]=زخارف النوافذ +Name[az]=Pəncərə dekorasiyası +Name[bg]=Декорации на прозорците +Name[bs]=Dekoracije prozora +Name[ca]=Decoració de les finestres +Name[ca@valencia]=Decoració de les finestres +Name[cs]=Dekorace oken +Name[da]=Vinduesdekorationer +Name[de]=Fensterdekoration +Name[el]=Διακοσμήσεις παραθύρου +Name[en_GB]=Window Decorations +Name[es]=Decoraciones de las ventanas +Name[et]=Akna dekoratsioonid +Name[eu]=Leiho-apaindurak +Name[fi]=Ikkunakehykset +Name[fr]=Décorations de fenêtres +Name[ga]=Maisiúcháin Fhuinneog +Name[gl]=Decoración da xanela +Name[he]=מסגרת חלון +Name[hi]=विंडो सजावट +Name[hr]=Ukrasi prozora +Name[hu]=Ablakdekorációk +Name[ia]=Decorationes de fenestra +Name[id]=Dekorasi Window +Name[is]=Gluggaskreytingar +Name[it]=Decorazioni delle finestre +Name[ja]=ウィンドウの飾り +Name[kk]=Терезенің безендірулері +Name[km]=ការ​តុបតែង​បង្អួច +Name[kn]=ವಿಂಡೋ ಅಲಂಕಾರಗಳು +Name[ko]=창 장식 +Name[lt]=Langų dekoracijos +Name[lv]=Logu dekorācijas +Name[mr]=चौकट सजावट +Name[nb]=Vinduspynt +Name[nds]=Finstern opfladusen +Name[nl]=Vensterdecoraties +Name[nn]=Vindaugspynt +Name[pa]=ਵਿੰਡੋ ਸਜਾਵਟ +Name[pl]=Wygląd okien +Name[pt]=Decorações das Janelas +Name[pt_BR]=Decorações da janela +Name[ro]=Decorații fereastră +Name[ru]=Оформление окон +Name[si]=කවුළු සැරසිලි +Name[sk]=Dekorácie okien +Name[sl]=Okraski oken +Name[sr]=Декорације прозора +Name[sr@ijekavian]=Декорације прозора +Name[sr@ijekavianlatin]=Dekoracije prozora +Name[sr@latin]=Dekoracije prozora +Name[sv]=Fönsterdekorationer +Name[th]=ส่วนตกแต่งหน้าต่าง +Name[tr]=Pencere Dekorasyonları +Name[ug]=كۆزنەك بېزەكلىرى +Name[uk]=Обрамлення вікон +Name[wa]=Gåyotaedjes des fniesses +Name[x-test]=xxWindow Decorationsxx +Name[zh_CN]=窗口装饰元素 +Name[zh_TW]=視窗裝飾 + +ProvidersUrl=https://autoconfig.kde.org/ocs/providers.xml +ContentWarning=Executables +Categories=Plasma 6 Window Decorations +TargetDir=aurorae/themes +Uncompress=archive +AdoptionCommand=@KDE_INSTALL_FULL_LIBEXECDIR@/kwin-applywindowdecoration %f diff --git a/local/recipes/kde/kwin/source/src/kcms/desktop/CMakeLists.txt b/local/recipes/kde/kwin/source/src/kcms/desktop/CMakeLists.txt new file mode 100644 index 0000000000..78cfbb85a6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/desktop/CMakeLists.txt @@ -0,0 +1,29 @@ +# KI18N Translation Domain for this library. +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_kwin_virtualdesktops\") + +########### next target ############### + +set(kcm_kwin_virtualdesktops_PART_SRCS + ../../virtualdesktopsdbustypes.cpp + animationsmodel.cpp + desktopsmodel.cpp + virtualdesktops.cpp + virtualdesktopsdata.cpp +) + +kconfig_add_kcfg_files(kcm_kwin_virtualdesktops_PART_SRCS virtualdesktopssettings.kcfgc GENERATE_MOC) + +kcmutils_add_qml_kcm(kcm_kwin_virtualdesktops SOURCES ${kcm_kwin_virtualdesktops_PART_SRCS}) + +target_link_libraries(kcm_kwin_virtualdesktops PRIVATE + Qt::DBus + + KF6::I18n + KF6::KCMUtils + KF6::KCMUtilsQuick + KF6::XmlGui + + kcmkwincommon +) + +install(FILES virtualdesktopssettings.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) diff --git a/local/recipes/kde/kwin/source/src/kcms/desktop/Messages.sh b/local/recipes/kde/kwin/source/src/kcms/desktop/Messages.sh new file mode 100644 index 0000000000..b1a90c6a24 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/desktop/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.cpp -o -name \*.qml` -o $podir/kcm_kwin_virtualdesktops.pot diff --git a/local/recipes/kde/kwin/source/src/kcms/desktop/animationsmodel.cpp b/local/recipes/kde/kwin/source/src/kcms/desktop/animationsmodel.cpp new file mode 100644 index 0000000000..09152a058f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/desktop/animationsmodel.cpp @@ -0,0 +1,178 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "animationsmodel.h" + +#include + +namespace KWin +{ + +AnimationsModel::AnimationsModel(QObject *parent) + : EffectsModel(parent) +{ + connect(this, &EffectsModel::loaded, this, [this]() { + setAnimationEnabled(modelAnimationEnabled()); + setAnimationIndex(modelAnimationIndex()); + loadDefaults(); + }); + connect(this, &AnimationsModel::animationIndexChanged, this, [this]() { + const QModelIndex index_ = index(m_animationIndex, 0); + if (!index_.isValid()) { + return; + } + const bool configurable = index_.data(ConfigurableRole).toBool(); + if (configurable != m_currentConfigurable) { + m_currentConfigurable = configurable; + Q_EMIT currentConfigurableChanged(); + } + }); +} + +bool AnimationsModel::animationEnabled() const +{ + return m_animationEnabled; +} + +void AnimationsModel::setAnimationEnabled(bool enabled) +{ + if (m_animationEnabled != enabled) { + m_animationEnabled = enabled; + Q_EMIT animationEnabledChanged(); + } +} + +int AnimationsModel::animationIndex() const +{ + return m_animationIndex; +} + +void AnimationsModel::setAnimationIndex(int index) +{ + if (m_animationIndex != index) { + m_animationIndex = index; + Q_EMIT animationIndexChanged(); + } +} + +bool AnimationsModel::currentConfigurable() const +{ + return m_currentConfigurable; +} + +bool AnimationsModel::defaultAnimationEnabled() const +{ + return m_defaultAnimationEnabled; +} + +int AnimationsModel::defaultAnimationIndex() const +{ + return m_defaultAnimationIndex; +} + +bool AnimationsModel::shouldStore(const EffectData &data) const +{ + return data.untranslatedCategory.contains( + QStringLiteral("Virtual Desktop Switching Animation"), Qt::CaseInsensitive); +} + +EffectsModel::Status AnimationsModel::status(int row) const +{ + return Status(data(index(row, 0), static_cast(StatusRole)).toInt()); +} + +void AnimationsModel::loadDefaults() +{ + for (int i = 0; i < rowCount(); ++i) { + const QModelIndex rowIndex = index(i, 0); + if (rowIndex.data(EnabledByDefaultRole).toBool()) { + m_defaultAnimationEnabled = true; + m_defaultAnimationIndex = i; + Q_EMIT defaultAnimationEnabledChanged(); + Q_EMIT defaultAnimationIndexChanged(); + break; + } + } +} + +bool AnimationsModel::modelAnimationEnabled() const +{ + for (int i = 0; i < rowCount(); ++i) { + if (status(i) != Status::Disabled) { + return true; + } + } + + return false; +} + +int AnimationsModel::modelAnimationIndex() const +{ + for (int i = 0; i < rowCount(); ++i) { + if (status(i) != Status::Disabled) { + return i; + } + } + + return 0; +} + +void AnimationsModel::load() +{ + EffectsModel::load(); +} + +void AnimationsModel::save() +{ + for (int i = 0; i < rowCount(); ++i) { + const auto status = (m_animationEnabled && i == m_animationIndex) + ? EffectsModel::Status::Enabled + : EffectsModel::Status::Disabled; + updateEffectStatus(index(i, 0), status); + } + + EffectsModel::save(); +} + +void AnimationsModel::defaults() +{ + EffectsModel::defaults(); + setAnimationEnabled(modelAnimationEnabled()); + setAnimationIndex(modelAnimationIndex()); +} + +bool AnimationsModel::isDefaults() const +{ + // effect at m_animationIndex index may not be the current saved selected effect + const bool enabledByDefault = index(m_animationIndex, 0).data(EnabledByDefaultRole).toBool(); + return enabledByDefault; +} + +bool AnimationsModel::needsSave() const +{ + KConfigGroup kwinConfig(KSharedConfig::openConfig("kwinrc"), QStringLiteral("Plugins")); + + for (int i = 0; i < rowCount(); ++i) { + const QModelIndex index_ = index(i, 0); + const bool enabledConfig = kwinConfig.readEntry( + index_.data(ServiceNameRole).toString() + QLatin1String("Enabled"), + index_.data(EnabledByDefaultRole).toBool()); + const bool enabled = (m_animationEnabled && i == m_animationIndex); + + if (enabled != enabledConfig) { + return true; + } + } + + return false; +} + +} + +#include "moc_animationsmodel.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/desktop/animationsmodel.h b/local/recipes/kde/kwin/source/src/kcms/desktop/animationsmodel.h new file mode 100644 index 0000000000..ba9f871a69 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/desktop/animationsmodel.h @@ -0,0 +1,71 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effectsmodel.h" + +namespace KWin +{ + +class AnimationsModel : public EffectsModel +{ + Q_OBJECT + Q_PROPERTY(bool animationEnabled READ animationEnabled WRITE setAnimationEnabled NOTIFY animationEnabledChanged) + Q_PROPERTY(int animationIndex READ animationIndex WRITE setAnimationIndex NOTIFY animationIndexChanged) + Q_PROPERTY(bool currentConfigurable READ currentConfigurable NOTIFY currentConfigurableChanged) + Q_PROPERTY(bool defaultAnimationEnabled READ defaultAnimationEnabled NOTIFY defaultAnimationEnabledChanged) + Q_PROPERTY(int defaultAnimationIndex READ defaultAnimationIndex NOTIFY defaultAnimationIndexChanged) + +public: + explicit AnimationsModel(QObject *parent = nullptr); + + bool animationEnabled() const; + void setAnimationEnabled(bool enabled); + + int animationIndex() const; + void setAnimationIndex(int index); + + bool currentConfigurable() const; + + bool defaultAnimationEnabled() const; + int defaultAnimationIndex() const; + + void load(); + void save(); + void defaults(); + bool isDefaults() const; + bool needsSave() const; + +Q_SIGNALS: + void animationEnabledChanged(); + void animationIndexChanged(); + void currentConfigurableChanged(); + void defaultAnimationEnabledChanged(); + void defaultAnimationIndexChanged(); + +protected: + bool shouldStore(const EffectData &data) const override; + +private: + Status status(int row) const; + void loadDefaults(); + bool modelAnimationEnabled() const; + int modelAnimationIndex() const; + + bool m_animationEnabled = false; + bool m_defaultAnimationEnabled = false; + int m_animationIndex = -1; + int m_defaultAnimationIndex = -1; + bool m_currentConfigurable = false; + + Q_DISABLE_COPY(AnimationsModel) +}; + +} diff --git a/local/recipes/kde/kwin/source/src/kcms/desktop/desktopsmodel.cpp b/local/recipes/kde/kwin/source/src/kcms/desktop/desktopsmodel.cpp new file mode 100644 index 0000000000..eea367bea1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/desktop/desktopsmodel.cpp @@ -0,0 +1,688 @@ +/* + SPDX-FileCopyrightText: 2018 Eike Hein + SPDX-FileCopyrightText: 2018 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "desktopsmodel.h" + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +static const QString s_serviceName(QStringLiteral("org.kde.KWin")); +static const QString s_virtualDesktopsInterface(QStringLiteral("org.kde.KWin.VirtualDesktopManager")); +static const QString s_virtDesktopsPath(QStringLiteral("/VirtualDesktopManager")); +static const QString s_fdoPropertiesInterface(QStringLiteral("org.freedesktop.DBus.Properties")); + +DesktopsModel::DesktopsModel(QObject *parent) + : QAbstractListModel(parent) + , m_userModified(false) + , m_serverModified(false) + , m_serverSideRows(-1) + , m_rows(-1) +{ + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + m_serviceWatcher = new QDBusServiceWatcher(s_serviceName, + QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange); + + QObject::connect(m_serviceWatcher, &QDBusServiceWatcher::serviceRegistered, + this, [this]() { + reset(); + }); + + QObject::connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, + this, [this]() { + QDBusConnection::sessionBus().disconnect( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("desktopCreated"), + this, + SLOT(desktopCreated(QString, KWin::DBusDesktopDataStruct))); + + QDBusConnection::sessionBus().disconnect( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("desktopRemoved"), + this, + SLOT(desktopRemoved(QString))); + + QDBusConnection::sessionBus().disconnect( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("desktopDataChanged"), + this, + SLOT(desktopDataChanged(QString, KWin::DBusDesktopDataStruct))); + + QDBusConnection::sessionBus().disconnect( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("rowsChanged"), + this, + SLOT(desktopRowsChanged(uint))); + }); + + reset(); +} + +DesktopsModel::~DesktopsModel() +{ +} + +QHash DesktopsModel::roleNames() const +{ + QHash roles = QAbstractItemModel::roleNames(); + + QMetaEnum e = metaObject()->enumerator(metaObject()->indexOfEnumerator("AdditionalRoles")); + + for (int i = 0; i < e.keyCount(); ++i) { + roles.insert(e.value(i), e.key(i)); + } + + return roles; +} + +QVariant DesktopsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() > (m_desktops.count() - 1)) { + return QVariant(); + } + + if (role == Qt::DisplayRole) { + return m_names.value(m_desktops.at(index.row())); + } else if (role == Id) { + return m_desktops.at(index.row()); + } else if (role == DesktopRow) { + const int rows = std::max(m_rows, 1); + const int perRow = std::ceil((qreal)m_desktops.count() / (qreal)rows); + + return (index.row() / perRow) + 1; + + } else if (role == IsDefault) { + // According to defaults(), first desktop is default + return index.row() == 0; + } + + return QVariant(); +} + +int DesktopsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return m_desktops.count(); +} + +bool DesktopsModel::ready() const +{ + return !m_desktops.isEmpty(); +} + +QString DesktopsModel::error() const +{ + return m_error; +} + +bool DesktopsModel::userModified() const +{ + return m_userModified; +} + +bool DesktopsModel::serverModified() const +{ + return m_serverModified; +} + +int DesktopsModel::rows() const +{ + return m_rows; +} + +void DesktopsModel::setRows(int rows) +{ + if (!ready()) { + return; + } + + if (m_rows != rows) { + m_rows = rows; + + Q_EMIT rowsChanged(); + Q_EMIT dataChanged(index(0, 0), index(m_desktops.count() - 1, 0), QList{DesktopRow}); + + updateModifiedState(); + } +} + +int DesktopsModel::desktopCount() const +{ + return rowCount(); +} + +QString DesktopsModel::createDesktopName() const +{ + const QStringList nameValues = m_names.values(); + for (int index = 1;; ++index) { + const QString desktopName = i18ncp("A numbered name for virtual desktops", "Desktop %1", "Desktop %1", index); + if (!nameValues.contains(desktopName)) { + return desktopName; + } + } +} + +void DesktopsModel::createDesktop() +{ + if (!ready()) { + return; + } + + beginInsertRows(QModelIndex(), m_desktops.count(), m_desktops.count()); + + const QString &dummyId = QUuid::createUuid().toString(QUuid::WithoutBraces); + + m_desktops.append(dummyId); + m_names[dummyId] = createDesktopName(); + + endInsertRows(); + Q_EMIT desktopCountChanged(); + + updateModifiedState(); +} + +void DesktopsModel::removeDesktop(const QString &id) +{ + if (!ready() || !m_desktops.contains(id)) { + return; + } + + const int desktopIndex = m_desktops.indexOf(id); + + beginRemoveRows(QModelIndex(), desktopIndex, desktopIndex); + + m_desktops.removeAt(desktopIndex); + m_names.remove(id); + + endRemoveRows(); + Q_EMIT desktopCountChanged(); + + updateModifiedState(); +} + +void DesktopsModel::setDesktopName(const QString &id, const QString &name) +{ + if (!ready() || !m_desktops.contains(id)) { + return; + } + + m_names[id] = name; + + const QModelIndex &idx = index(m_desktops.indexOf(id), 0); + + Q_EMIT dataChanged(idx, idx, QList{Qt::DisplayRole}); + + updateModifiedState(); +} + +void DesktopsModel::syncWithServer() +{ + auto callFinished = [this](QDBusPendingCallWatcher *call) { + QDBusPendingReply reply = *call; + + if (reply.isError()) { + handleCallError(); + } + + --m_pendingCalls; + + call->deleteLater(); + }; + + if (m_desktops.count() > m_serverSideDesktops.count()) { + auto call = QDBusMessage::createMethodCall( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("createDesktop")); + + const int newIndex = m_serverSideDesktops.count(); + + call.setArguments({(uint)newIndex, m_names.value(m_desktops.at(newIndex))}); + + ++m_pendingCalls; + QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call); + + const auto *watcher = new QDBusPendingCallWatcher(pending, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished); + + return; // The change-handling slot will call syncWithServer() again, + // until everything is in sync. + } + + if (m_desktops.count() < m_serverSideDesktops.count()) { + QStringListIterator i(m_serverSideDesktops); + + i.toBack(); + + while (i.hasPrevious()) { + const QString &previous = i.previous(); + + if (!m_desktops.contains(previous)) { + auto call = QDBusMessage::createMethodCall( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("removeDesktop")); + + call.setArguments({previous}); + + ++m_pendingCalls; + QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call); + + const QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished); + + return; // The change-handling slot will call syncWithServer() again, + // until everything is in sync. + } + } + } + + // Sync ids. Replace dummy ids in the process. + for (int i = 0; i < m_serverSideDesktops.count(); ++i) { + const QString oldId = m_desktops.at(i); + const QString &newId = m_serverSideDesktops.at(i); + m_desktops[i] = newId; + m_names[newId] = m_names.take(oldId); + } + + Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), QList{Qt::DisplayRole}); + + // Sync names. + if (m_names != m_serverSideNames) { + QHashIterator i(m_names); + + while (i.hasNext()) { + i.next(); + + if (i.value() != m_serverSideNames.value(i.key())) { + auto call = QDBusMessage::createMethodCall( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("setDesktopName")); + + call.setArguments({i.key(), i.value()}); + + ++m_pendingCalls; + QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call); + + const QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished); + + break; + } + } + + return; // The change-handling slot will call syncWithServer() again, + // until everything is in sync.. + } + + // Sync rows. + if (m_rows != m_serverSideRows) { + auto call = QDBusMessage::createMethodCall( + s_serviceName, + s_virtDesktopsPath, + s_fdoPropertiesInterface, + QStringLiteral("Set")); + + call.setArguments({s_virtualDesktopsInterface, + QStringLiteral("rows"), QVariant::fromValue(QDBusVariant(QVariant((uint)m_rows)))}); + + ++m_pendingCalls; + QDBusPendingCall pending = QDBusConnection::sessionBus().asyncCall(call); + + const QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pending, this); + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, this, callFinished); + } +} + +void DesktopsModel::reset() +{ + auto getAllAndConnectCall = QDBusMessage::createMethodCall( + s_serviceName, + s_virtDesktopsPath, + s_fdoPropertiesInterface, + QStringLiteral("GetAll")); + + getAllAndConnectCall.setArguments({s_virtualDesktopsInterface}); + + QDBusConnection::sessionBus().callWithCallback( + getAllAndConnectCall, + this, + SLOT(getAllAndConnect(QDBusMessage)), + SLOT(handleCallError())); +} + +bool DesktopsModel::needsSave() const +{ + return m_userModified; +} + +bool DesktopsModel::isDefaults() const +{ + return m_rows == 2 && m_desktops.count() == 1; +} + +void DesktopsModel::defaults() +{ + beginResetModel(); + // default is 1 desktop with 2 rows + // see kwin/virtualdesktops.cpp VirtualDesktopGrid::VirtualDesktopGrid + while (m_desktops.count() > 1) { + const auto desktop = m_desktops.takeLast(); + m_names.remove(desktop); + } + setRows(2); + + endResetModel(); + + m_userModified = true; + updateModifiedState(); +} + +void DesktopsModel::load() +{ + beginResetModel(); + m_desktops = m_serverSideDesktops; + m_names = m_serverSideNames; + setRows(m_serverSideRows); + endResetModel(); + + m_userModified = true; + updateModifiedState(); +} + +void DesktopsModel::getAllAndConnect(const QDBusMessage &msg) +{ + const QVariantMap &data = qdbus_cast(msg.arguments().at(0).value()); + + const KWin::DBusDesktopDataVector &desktops = qdbus_cast( + data.value(QStringLiteral("desktops")).value()); + + const int newServerSideRows = data.value(QStringLiteral("rows")).toUInt(); + QStringList newServerSideDesktops; + QHash newServerSideNames; + + for (const KWin::DBusDesktopDataStruct &d : desktops) { + newServerSideDesktops.append(d.id); + newServerSideNames[d.id] = d.name; + } + + // If the server-side state changed during a KWin restart, and the + // user had made notifications, the model should notify about the + // change. + if (m_serverSideDesktops != newServerSideDesktops + || m_serverSideNames != newServerSideNames + || m_serverSideRows != newServerSideRows) { + if (!m_serverSideDesktops.isEmpty() || m_userModified) { + m_serverModified = true; + Q_EMIT serverModifiedChanged(); + } + + m_serverSideDesktops = newServerSideDesktops; + m_serverSideNames = newServerSideNames; + m_serverSideRows = newServerSideRows; + } + + // For the case KWin restarts while the KCM was open: If the user had + // made no modifications, just reset to the server data. E.g. perhaps + // the user intentionally nuked the KWin config while it was down, so + // we should follow. + if (!m_userModified || m_desktops.empty()) { + beginResetModel(); + m_desktops = m_serverSideDesktops; + m_names = m_serverSideNames; + m_rows = m_serverSideRows; + endResetModel(); + + Q_EMIT rowsChanged(); + } + + Q_EMIT readyChanged(); + + auto handleConnectionError = [this]() { + m_error = i18n("There was an error connecting to the compositor."); + Q_EMIT errorChanged(); + }; + + bool connected = QDBusConnection::sessionBus().connect( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("desktopCreated"), + this, + SLOT(desktopCreated(QString, KWin::DBusDesktopDataStruct))); + + if (!connected) { + handleConnectionError(); + + return; + } + + connected = QDBusConnection::sessionBus().connect( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("desktopRemoved"), + this, + SLOT(desktopRemoved(QString))); + + if (!connected) { + handleConnectionError(); + + return; + } + + connected = QDBusConnection::sessionBus().connect( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("desktopDataChanged"), + this, + SLOT(desktopDataChanged(QString, KWin::DBusDesktopDataStruct))); + + if (!connected) { + handleConnectionError(); + + return; + } + + connected = QDBusConnection::sessionBus().connect( + s_serviceName, + s_virtDesktopsPath, + s_virtualDesktopsInterface, + QStringLiteral("rowsChanged"), + this, + SLOT(desktopRowsChanged(uint))); + + if (!connected) { + handleConnectionError(); + + return; + } +} + +void DesktopsModel::desktopCreated(const QString &id, const KWin::DBusDesktopDataStruct &data) +{ + m_serverSideDesktops.insert(data.position, id); + m_serverSideNames[data.id] = data.name; + + // If the user didn't make any changes, we can just stay in sync. + if (!m_userModified) { + beginInsertRows(QModelIndex(), data.position, data.position); + + m_desktops = m_serverSideDesktops; + m_names = m_serverSideNames; + + endInsertRows(); + } else { + // Remove dummy data. + const QString dummyId = m_desktops.at(data.position); + m_desktops[data.position] = id; + m_names.remove(dummyId); + m_names[id] = data.name; + const QModelIndex &idx = index(data.position, 0); + Q_EMIT dataChanged(idx, idx, QList{Id}); + + updateModifiedState(/* server */ true); + } +} + +void DesktopsModel::desktopRemoved(const QString &id) +{ + const int desktopIndex = m_serverSideDesktops.indexOf(id); + + m_serverSideDesktops.removeAt(desktopIndex); + m_serverSideNames.remove(id); + + // If the user didn't make any changes, we can just stay in sync. + if (!m_userModified) { + beginRemoveRows(QModelIndex(), desktopIndex, desktopIndex); + + m_desktops = m_serverSideDesktops; + m_names = m_serverSideNames; + + endRemoveRows(); + } else { + updateModifiedState(/* server */ true); + } +} + +void DesktopsModel::desktopDataChanged(const QString &id, const KWin::DBusDesktopDataStruct &data) +{ + const int desktopIndex = m_serverSideDesktops.indexOf(id); + + m_serverSideDesktops[desktopIndex] = id; + m_serverSideNames[id] = data.name; + + // If the user didn't make any changes, we can just stay in sync. + if (!m_userModified) { + m_desktops = m_serverSideDesktops; + m_names = m_serverSideNames; + + const QModelIndex &idx = index(desktopIndex, 0); + + Q_EMIT dataChanged(idx, idx, QList{Qt::DisplayRole}); + } else { + updateModifiedState(/* server */ true); + } +} + +void DesktopsModel::desktopRowsChanged(uint rows) +{ + // Unfortunately we sometimes get this signal from the server with an unchanged value. + if ((int)rows == m_serverSideRows) { + return; + } + + m_serverSideRows = rows; + + // If the user didn't make any changes, we can just stay in sync. + if (!m_userModified) { + m_rows = m_serverSideRows; + + Q_EMIT rowsChanged(); + Q_EMIT dataChanged(index(0, 0), index(m_desktops.count() - 1, 0), QList{DesktopRow}); + } else { + updateModifiedState(/* server */ true); + } +} + +void DesktopsModel::updateModifiedState(bool server) +{ + // Count is the same but contents are not: The user may have + // removed and created new desktops in the UI, but there were + // no changes to send to the server because number and names + // have remained the same. In that case we can just clean + // that up here. + if (m_desktops.count() == m_serverSideDesktops.count() + && m_desktops != m_serverSideDesktops) { + + for (int i = 0; i < m_serverSideDesktops.count(); ++i) { + const QString oldId = m_desktops.at(i); + const QString &newId = m_serverSideDesktops.at(i); + m_desktops[i] = newId; + m_names[newId] = m_names.take(oldId); + } + + Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), QList{Qt::DisplayRole}); + } + + if (m_desktops == m_serverSideDesktops + && m_names == m_serverSideNames + && m_rows == m_serverSideRows) { + + m_userModified = false; + Q_EMIT userModifiedChanged(); + + m_serverModified = false; + Q_EMIT serverModifiedChanged(); + } else { + if (m_pendingCalls > 0) { + m_serverModified = false; + Q_EMIT serverModifiedChanged(); + + syncWithServer(); + } else if (server) { + m_serverModified = true; + Q_EMIT serverModifiedChanged(); + } else { + m_userModified = true; + Q_EMIT userModifiedChanged(); + } + } +} + +void DesktopsModel::handleCallError() +{ + if (m_pendingCalls > 0) { + + m_serverModified = false; + Q_EMIT serverModifiedChanged(); + + m_error = i18n("There was an error saving the settings to the compositor."); + Q_EMIT errorChanged(); + } else { + m_error = i18n("There was an error requesting information from the compositor."); + Q_EMIT errorChanged(); + } +} + +} + +#include "moc_desktopsmodel.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/desktop/desktopsmodel.h b/local/recipes/kde/kwin/source/src/kcms/desktop/desktopsmodel.h new file mode 100644 index 0000000000..f90416bc74 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/desktop/desktopsmodel.h @@ -0,0 +1,125 @@ +/* + SPDX-FileCopyrightText: 2018 Eike Hein + SPDX-FileCopyrightText: 2018 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "virtualdesktopsdbustypes.h" + +class QDBusArgument; +class QDBusMessage; +class QDBusServiceWatcher; + +namespace KWin +{ + +/** + * @short An item model around KWin's D-Bus API for virtual desktops. + * + * The model initially gets the state from KWin and populates. + * + * As long as the user makes no changes, KWin-side changes are directly + * exposed in the model. + * + * If the user makes changes (see the `userModified` property), it stops + * exposing KWin-side changes live, but it keeps track of the KWin-side + * changes, so it can figure out and apply the delta when `syncWithServer` + * is called. + * + * When KWin-side changes happen while the model is user-modified, the + * model signals this via the `serverModified` property. A call to + * `syncWithServer` will overwrite the KWin-side changes. + * + * After synchronization, the model tracks Kwin-side changes again, + * until the user makes further changes. + * + * @author Eike Hein + */ + +class DesktopsModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(bool ready READ ready NOTIFY readyChanged) + Q_PROPERTY(QString error READ error NOTIFY errorChanged) + Q_PROPERTY(bool userModified READ userModified NOTIFY userModifiedChanged) + Q_PROPERTY(bool serverModified READ serverModified NOTIFY serverModifiedChanged) + Q_PROPERTY(int rows READ rows WRITE setRows NOTIFY rowsChanged) + Q_PROPERTY(int desktopCount READ desktopCount NOTIFY desktopCountChanged) + +public: + enum AdditionalRoles { + Id = Qt::UserRole + 1, + DesktopRow, + IsDefault, + }; + Q_ENUM(AdditionalRoles) + + explicit DesktopsModel(QObject *parent = nullptr); + ~DesktopsModel() override; + + QHash roleNames() const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = {}) const override; + + bool ready() const; + QString error() const; + + bool userModified() const; + bool serverModified() const; + + int rows() const; + void setRows(int rows); + + int desktopCount() const; + + QString createDesktopName() const; + Q_INVOKABLE void createDesktop(); + Q_INVOKABLE void removeDesktop(const QString &id); + Q_INVOKABLE void setDesktopName(const QString &id, const QString &name); + + Q_INVOKABLE void syncWithServer(); + + bool needsSave() const; + void load(); + void defaults(); + bool isDefaults() const; + +Q_SIGNALS: + void readyChanged() const; + void errorChanged() const; + void userModifiedChanged() const; + void serverModifiedChanged() const; + void rowsChanged() const; + void desktopCountChanged(); + +protected Q_SLOTS: + void reset(); + void getAllAndConnect(const QDBusMessage &msg); + void desktopCreated(const QString &id, const KWin::DBusDesktopDataStruct &data); + void desktopRemoved(const QString &id); + void desktopDataChanged(const QString &id, const KWin::DBusDesktopDataStruct &data); + void desktopRowsChanged(uint rows); + void updateModifiedState(bool server = false); + void handleCallError(); + +private: + QDBusServiceWatcher *m_serviceWatcher; + QString m_error; + bool m_userModified; + bool m_serverModified; + QStringList m_serverSideDesktops; + QHash m_serverSideNames; + int m_serverSideRows; + QStringList m_desktops; + QHash m_names; + int m_rows; + int m_pendingCalls = 0; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/kcms/desktop/kcm_kwin_virtualdesktops.json b/local/recipes/kde/kwin/source/src/kcms/desktop/kcm_kwin_virtualdesktops.json new file mode 100644 index 0000000000..d7326adb98 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/desktop/kcm_kwin_virtualdesktops.json @@ -0,0 +1,149 @@ +{ + "Categories": "Qt;KDE;X-KDE-settings-translations;", + "KPlugin": { + "BugReportUrl": "https://bugs.kde.org/enter_bug.cgi?product=systemsettings&component=kcm_kwinvirtualdesktops", + "Description": "Configure navigation, number and layout of virtual desktops", + "Description[ar]": "اضبط عدد وتخطيط أسطح المكتب الافتراضية وكيفية التنقل بينها", + "Description[az]": "Virtual iş masalarının istiqamətini, sayını və maketini tənzimləyin", + "Description[be]": "Наладжванне навігацыі, колькасці і макета кампанавання віртуальных працоўных сталоў", + "Description[bg]": "Конфигуриране на навигация, брой и подредба на виртуални работни плотове", + "Description[ca@valencia]": "Configura la navegació, el nombre i la disposició dels escriptoris virtuals", + "Description[ca]": "Configura la navegació, el nombre i la disposició dels escriptoris virtuals", + "Description[cs]": "Nastavit ovládání, počet a rozvržení virtuálních pracovních ploch", + "Description[da]": "Konfigurér navigation, antal og layout af virtuelel skrivebord", + "Description[de]": "Navigation, Anzahl und Layout virtueller Arbeitsflächen einrichten", + "Description[en_GB]": "Configure navigation, number and layout of virtual desktops", + "Description[eo]": "Agordu navigadon, nombron kaj aranĝon de virtualaj labortabloj", + "Description[es]": "Configurar la navegación, el número y la disposición de los escritorios virtuales", + "Description[et]": "Virtuaalsete töölaudade vahel liikumise, nende arvu ja paigutuse seadistamine", + "Description[eu]": "Konfiguratu nabigatzea, alegiazko mahaigainen kopurua eta antolamendua", + "Description[fi]": "Virtuaalityöpöytien määrä-, asettelu- ja liikkumisasetukset", + "Description[fr]": "Configurer la navigation, le nombre et la disposition des bureaux virtuels", + "Description[gl]": "Configurar a navegación, cantidade e disposición dos escritorios virtuais.", + "Description[he]": "הגדרת ניווט, מספר ופריסת שולחנות עבודה וירטואליים", + "Description[hu]": "A virtuális asztalok számának, elrendezésének, és a köztük történő navigációnak a beállítása", + "Description[ia]": "Configura navigation, numero e disposition de scriptorios virtual", + "Description[id]": "Konfigurasikan navigasi, nomor dan tataletak desktop virtual", + "Description[is]": "Grunnstilla flettingu, fjölda og framsetningu sýndarskjáborða", + "Description[it]": "Configura navigazione, numero e disposizione dei desktop virtuali", + "Description[ja]": "仮想デスクトップの数、レイアウト、仮想デスクトップ間の移動を設定", + "Description[ka]": "ვირტუალური სამუშაო მაგიდებს რიცხვის, ნავიგაციისა და განლაგების მორგება", + "Description[ko]": "가상 바탕 화면 탐색, 개수, 레이아웃 설정", + "Description[lt]": "Konfigūruoti virtualių darbalaukių naršymą, skaičių ir išdėstymą", + "Description[lv]": "Konfigurēt virtuālo darbvirsmu navigāciju, skaitu un izvietojumu", + "Description[nb]": "Sett opp navigering, nummer og visning av virtuelle skrivebord", + "Description[nl]": "Navigatie, aantal en indeling van virtuele bureaubladen configureren", + "Description[nn]": "Set opp navigering, nummer og vising av virtuelle skrivebord", + "Description[pl]": "Ustawienia poruszania się, liczby oraz układu wirtualnych klawiatur", + "Description[pt]": "Configurar a navegação, o número e a disposição dos ecrãs virtuais", + "Description[pt_BR]": "Configura a navegação, quantidade e layout das áreas de trabalho virtuais", + "Description[ro]": "Configurează navigarea, numărul și aranjamentul birourilor virtuale", + "Description[ru]": "Число, расположение и способ переключения рабочих столов", + "Description[sa]": "वर्चुअल् डेस्कटॉप् इत्यस्य नेविगेशनं, संख्यां, लेआउट् च विन्यस्यताम्", + "Description[sk]": "Nastaviť navigáciu, počet a rozloženie virtuálnych plôch", + "Description[sl]": "Prilagodite krmarjenje, število in postavitev navideznih namizij", + "Description[sv]": "Anpassa navigering, antal och layout av virtuella skrivbord", + "Description[ta]": "பணிமேடைகளின் எண்ணிக்கை, தளவமைப்பு, மற்றும் அவற்றுக்கிடையேயான உலாவலை அமைக்கும்", + "Description[tr]": "Dolaşımı, sanal masaüstlerinin sayısını ve yerleşimini yapılandır", + "Description[uk]": "Налаштовування навігації, кількості та компонування віртуальних стільниць", + "Description[vi]": "Cấu hình điều hướng, số lượng và bố cục của các bàn làm việc ảo", + "Description[zh_CN]": "配置虚拟桌面的数量、排列方式和切换方式", + "Description[zh_TW]": "設定虛擬桌面的導覽、數量與佈局", + "FormFactors": [ + "desktop" + ], + "Icon": "preferences-desktop-virtual", + "Name": "Virtual Desktops", + "Name[ar]": "أسطح المكتب الافتراضية", + "Name[ast]": "Escritorios virtuales", + "Name[az]": "Virtual iş masaları", + "Name[be]": "Віртуальныя працоўныя сталы", + "Name[bg]": "Виртуални работни плотове", + "Name[ca@valencia]": "Escriptoris virtuals", + "Name[ca]": "Escriptoris virtuals", + "Name[cs]": "Virtuální plochy", + "Name[da]": "Virtuelle skriveborde", + "Name[de]": "Virtuelle Arbeitsflächen", + "Name[en_GB]": "Virtual Desktops", + "Name[eo]": "Virtualaj Labortabloj", + "Name[es]": "Escritorios virtuales", + "Name[et]": "Virtuaalsed töölauad", + "Name[eu]": "Alegiazko mahaigaina", + "Name[fi]": "Virtuaalityöpöydät", + "Name[fr]": "Bureaux virtuels", + "Name[ga]": "Deasca Fíorúla", + "Name[gl]": "Escritorios virtuais", + "Name[he]": "שולחנות עבודה וירטואליים", + "Name[hu]": "Virtuális asztalok", + "Name[ia]": "Scriptorios virtual", + "Name[id]": "Desktop Virtual", + "Name[is]": "Sýndarskjáborð", + "Name[it]": "Desktop virtuali", + "Name[ja]": "仮想デスクトップ", + "Name[ka]": "ვირტუალური სამუშაო მაგიდები", + "Name[ko]": "가상 바탕 화면", + "Name[lt]": "Virtualūs darbalaukiai", + "Name[lv]": "Virtuālās darbvirsmas", + "Name[nb]": "Virtuelle skrivebord", + "Name[nl]": "Virtuele bureaubladen", + "Name[nn]": "Virtuelle skrivebord", + "Name[pl]": "Pulpity wirtualne", + "Name[pt]": "Ecrãs Virtuais", + "Name[pt_BR]": "Áreas de trabalho virtuais", + "Name[ro]": "Birouri virtuale", + "Name[ru]": "Виртуальные рабочие столы", + "Name[sa]": "आभासी डेस्कटॉप्स", + "Name[sk]": "Virtuálne pracovné plochy", + "Name[sl]": "Navidezna namizja", + "Name[sv]": "Virtuella skrivbord", + "Name[ta]": "மெய்நிகர் பணிமேடைகள்", + "Name[tr]": "Sanal Masaüstleri", + "Name[uk]": "Віртуальні стільниці", + "Name[vi]": "Bàn làm việc ảo", + "Name[zh_CN]": "虚拟桌面", + "Name[zh_TW]": "虛擬桌面" + }, + "X-DocPath": "kcontrol/desktop/index.html", + "X-KDE-Keywords": "desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings,indicator,navigation,cube,slide,workspaces", + "X-KDE-Keywords[ar]": "سطح المكتب,أسطح المكتب,رقم,سطح المكتب الافتراضي,أسطح مكتب متعددة,جهاز استدعاء,أداة جهاز استدعاء,أداة صغيرة لجهاز استدعاء,إعدادات جهاز استدعاء,مؤشر,التنقل,مكعب,شريحة,مساحات عمل", + "X-KDE-Keywords[bg]": "работен плот,настолни компютри,номер,виртуален работен плот,множество настолни компютри,пейджър,уиджет за пейджър,аплет за пейджър,настройки на пейджър,индикатор,навигация,куб,слайд,работни пространства", + "X-KDE-Keywords[ca@valencia]": "escriptori,escriptoris,número,escriptori virtual,escriptoris múltiples,paginador,giny paginador,miniaplicació de paginador,configuració del paginador,indicador,navegació,cub,diapositiva,espais de treball", + "X-KDE-Keywords[ca]": "escriptori,escriptoris,número,escriptori virtual,escriptoris múltiples,paginador,giny paginador,miniaplicació de paginador,configuració del paginador,indicador,navegació,cub,diapositiva,espais de treball", + "X-KDE-Keywords[de]": "Arbeitsfläche,Arbeitsflächen,Desktop,Anzahl,Virtuelle Arbeitsfläche,Mehrere Arbeitsflächen,Arbeitsflächenumschalter,Arbeitsflächenumschalter-Bedienelement,Arbeitsflächenumschalter-Miniprogramm,Arbeitsflächenumschalter-Einstellungen", + "X-KDE-Keywords[en_GB]": "desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings,indicator,navigation,cube,slide,workspaces", + "X-KDE-Keywords[es]": "escritorio,escritorios,número,escritorio virtual,múltiples escritorios,paginador,widget del paginador,elemento gráfico del paginador,miniaplicación del paginador,preferencias del paginador,ajustes del paginador,indicador,navegación,cubo,deslizar,espacios de trabajo", + "X-KDE-Keywords[eu]": "mahaigaina,mahaigainak,zenbakia,alegiazko mahaigaina,mahaigain birtuala,mahaigain anizkoitzak,orrialdekatzailea,trepeta orrialdekatzailea,aplikaziotxo aldekatzailea,orrialdekatzailearen ezarpenak,adierazlea,nabigazioa,nabigatzea,kuboa,irristatu,labaindu,languneak", + "X-KDE-Keywords[fi]": "työpöytä,työpöydät,määrä,virtuaalityöpöytä,monta työpöytää,sivutin,sivutinsovelma,sivutus,sivutusasetukset,osoitin,navigointi,liikkuminen,kuutio,liuku,työtilat", + "X-KDE-Keywords[fr]": "bureau, bureaux, numéro, bureau virtuel, bureaux multiples,téléavertisseur, composant graphique de téléavertisseur, applet de téléavertisseur, paramètres de téléavertisseur,indicateur,navigation,cube,diapositive,espaces de travail", + "X-KDE-Keywords[gl]": "escritorio,escritorios,número,escritorio virtual,escritorios múltiplos,paxinador, trebello paxinador, miniaplicativo paxinador,configuración do paxinador,indicator,indicador,navigation,navegación,cube,cubo,slide,diapositiva,workspaces,espazos de traballo", + "X-KDE-Keywords[he]": "שולחן עבודה,שולחנות עבודה,דסקטופ,דסקטופים,שולחן עבודה וירטואלי,ריבוי שולחנות עבודה,דפדפן,וידג׳ט דפדפן,יישומון דפדוף,יישומונית דפדוף,הגדרות דפדפן,מחוון,ניווט,קובייה,שקופית,מרחבי עבודה", + "X-KDE-Keywords[hu]": "asztal,asztalok,szám,virtuális asztal,több asztal,lapozó,lapozó elem,lapozó kisalkalmazás,lapozóbeállítások,jelző,navigáció,kocka,csúszás,munkaterületek", + "X-KDE-Keywords[ia]": "scriptorio,scriptorios,numero,scriptorio virtual,scriptorio multiple,pager, widget de pager, applet de pager, preferentias de pager, indicator, navigation, cubo, glissamento, spatios de labor", + "X-KDE-Keywords[is]": "skjáborð,fjöldi,sýndarskjábroð,mörg skjáborð,flettir,flettigræja,flettismáforrit,stillinger flettis,vísir,flakk,teningur,renna,vinnusvæði", + "X-KDE-Keywords[it]": "desktop,desktop,numero,desktop virtuale,desktop multipli,selettore dei desktop,oggetto del selettore dei desktop,applet selettore dei desktop,impostazioni selettore dei desktop,indicatore,navigazione,cubo,diapositiva,aree di lavoro", + "X-KDE-Keywords[ja]": "desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings,indicator,navigation,cube,slide,workspaces, デスクトップ,複数デスクトップ,仮想デスクトップ,仮想画面,ワークスペース,作業領域,デスクトップの数,デスクトップの追加,仮想デスクトップの管理,ページャー,ページャーウィジェット,ページャーアプレット,ページャー設定,インジケーター,ナビゲーション,移動,操作,キューブ,回転立方体,スライド,切り替え", + "X-KDE-Keywords[ka]": "desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings,indicator,navigation,cube,slide,workspaces,სამუშაო მაგიდა,ვიჯეტები,ვირტუალური სამუშაო მაგიდა,რიცხვი,აპლეტები", + "X-KDE-Keywords[ko]": "데스크톱,바탕 화면,가상 바탕 화면,가상 데스크톱,호출기,호출기 위젯,호출기 애플릿,호출기 설정,표시기,탐색,큐브,슬라이드,작업 공간", + "X-KDE-Keywords[lt]": "desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings,indicator,navigation,cube,slide,workspaces,darbalaukis,darbalaukiai,darbastalis,darbastaliai,darbalaukių,darbastalių,darbalaukiu,darbastaliu,skaičius,skaicius,virtualus darbalaukis,keli darbalaukiai,keletas darbalaukių,keletas darbalaukiu,darbalaukių perjungiklis,darbalaukiu perjungiklis,darbalaukių perjungiklio valdiklis,darbalaukių perjungiklio valdiklis,puslapių perjungiklis,puslapiu perjungiklis,puslapių perjungiklio valdiklis,puslapiu perjungiklio valdiklis,puslapių perjungiklio programėlė,puslapiu perjungiklio programele,puslapių perjungiklio nuostatos,puslapiu perjungiklio nuostatos,indikatorius,naršymas,narsymas,kubas,slinkti,slinkimas,darbo sritis,darbo sritys", + "X-KDE-Keywords[lv]": "darbvirsma,darbvirsmas,skaits,virtuālā darbvirsma,vairākas darbvirsmas,lapotājs,lapotāja logdaļa,lapotāja sīklietotne,lapotāja iestatījumi,indikators,navigācija,kubs,slīdnis,darbvietas", + "X-KDE-Keywords[nb]": "skrivebord,mengde,tall,virtuelt skrivebord,flere skrivebord,bytter,bytteelement,bytteinnstillinger,bytteoppsett,indikator,navigering,navigasjon,kube,gliding,arbeidsområde", + "X-KDE-Keywords[nl]": "bureaublad,bureaubladen,aantal,virtueel bureaublad,meerdere bureaubladen,pager,pager-widget,pager-applet,pager-instellingen,indicator,navigatie,kubus,dia,werkruimten", + "X-KDE-Keywords[nn]": "skrivebord,mengd,tal,virtuelt skrivebord,fleire skrivebord,vekslar,vekslarelement,vekslarelement,vekslerinnstillinger,vekslaroppsett,indikator,navigering,navigasjon,kube,gliding,arbeidsområde", + "X-KDE-Keywords[pl]": "pulpit,pulpity,liczba,pulpity wirtualne,wiele pulpitów,pager,element pagera,aplet pagera,ustawienia pagera,wskaźnik,poruszanie się,kostka,slajd,przestrzenie pracy", + "X-KDE-Keywords[pt_BR]": "área de trabalho,áreas de trabalho,número,área de trabalho virtual,múltiplas áreas de trabalho,paginador,widget de paginador,applet de paginador,configurações de paginador,indicador,navegação,cubo,deslizar,espaços de trabalho", + "X-KDE-Keywords[ro]": "birou,birouri,număr,birou virtual,birouri multiple,paginator,control paginator,miniaplicație paginator,configurări paginator,indicator,navigare,cub,glisare,spații de lucru", + "X-KDE-Keywords[ru]": "desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings,indicator,navigation,cube,slide,workspaces,рабочий стол,рабочие столы,число,виртуальный рабочий стол,несколько рабочих столов,переключатель рабочих столов,виджет переключателя рабочих столов,апплет переключателя рабочих столов,параметры переключения,настройка переключения,индикатор,навигация,куб,прокрутка,скольжение,рабочие пространства", + "X-KDE-Keywords[sa]": "डेस्कटॉप,डेस्कटॉप, संख्या, आभासी डेस्कटॉप,बहु डेस्कटॉप, पेजर, पेजर विजेट, पेजर एप्लेट, पेजर सेटिंग्स, सूचक, नेविगेशन, घन, स्लाइड, कार्यक्षेत्र", + "X-KDE-Keywords[sk]": "desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings,indicator,navigation,cube,slide,workspaces", + "X-KDE-Keywords[sl]": "namizje,namizja,število,navidezno namizje,več namizij,pozivnik,pripomoček pozivnika,aplet pozivnika,nastavitve pozivnika,indikator,krmarjenje,kocka,diapozitiv,delovni prostori", + "X-KDE-Keywords[sv]": "skrivbord,nummer,virtuellt skrivbord,flera skrivbord,skrivbordsväljare,skrivbordsväljarkomponent, skrivbordsväljarminiprogram, skrivbordsväljarinställningar,indikator,navigering,kub,bild,arbetsytor", + "X-KDE-Keywords[tr]": "masaüstü,masaüstleri,sayı,numara,sanal masaüstü,çoklu masaüstleri,sayfalayıcı,araç takımı,uygulamacık,ayarlar,gösterge,dolaşım,gezinti,küp,kaydır,çalışma alanları", + "X-KDE-Keywords[uk]": "desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings,indicator,navigation,cube,slide,workspaces,стільниця,стільниці,кількість,віртуальна стільниця,перемикач,пейджер,віджет перемикача,віджет пейджера,аплет перемикання,аплет перемикача,параметри перемикання,параметри перемикача,індикатор,навігація,куб,ковзання,слайд,простори", + "X-KDE-Keywords[zh_CN]": "desktop,desktops,number,virtual desktop,multiple desktops,pager,pager widget,pager applet,pager settings,zhuomian,xunizhuomian,duozhuomian,qiehuan,xunizhuomianqiehuanqi,qiehuanqi,qiehuanqizujian,qiehuanqishezhi,zhishiqi,zhishise,tishiqi,tishise,daohang,fangkuai,zhengfangti,huadong,gongzuoqu,gongzuokongjian,桌面,虚拟桌面,多桌面,切换,虚拟桌面切换器,切换器,切换器组件,切换器设置,指示器,指示色,提示器,提示色,导航,方块,正方体,滑动,工作区,工作空间", + "X-KDE-Keywords[zh_TW]": "桌面,虛擬桌面,桌面管理器,虛擬桌面管理器,指示器,瀏覽,工作空間", + "X-KDE-OnlyShowOnQtPlatforms": [ + "wayland" + ], + "X-KDE-System-Settings-Parent-Category": "windowmanagement", + "X-KDE-Weight": 60 +} diff --git a/local/recipes/kde/kwin/source/src/kcms/desktop/ui/main.qml b/local/recipes/kde/kwin/source/src/kcms/desktop/ui/main.qml new file mode 100644 index 0000000000..0c4c5b5314 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/desktop/ui/main.qml @@ -0,0 +1,337 @@ +/* + SPDX-FileCopyrightText: 2018 Eike Hein + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts + +import org.kde.kcmutils as KCM +import org.kde.kirigami as Kirigami + +KCM.ScrollViewKCM { + id: root + + implicitWidth: Kirigami.Units.gridUnit * 35 + implicitHeight: Kirigami.Units.gridUnit * 30 + + actions: [ + Kirigami.Action { + displayComponent: RowLayout { + spacing: Kirigami.Units.smallSpacing + + QQC2.Label { + text: i18nc("@text:label Number of rows, label associated to a number input field", "Rows:") + } + QQC2.SpinBox { + id: rowsSpinBox + + from: 1 + to: 20 + editable: true + value: kcm.desktopsModel.rows + onValueModified: kcm.desktopsModel.rows = value + + KCM.SettingHighlighter { + highlight: kcm.desktopsModel.rows !== 2 + } + Connections { + target: kcm.desktopsModel + + function onReadyChanged() { + rowsSpinBox.value = kcm.desktopsModel.rows; + } + function onRowsChanged() { + rowsSpinBox.value = kcm.desktopsModel.rows; + } + } + } + } + }, + Kirigami.Action { + text: i18nc("@action:button", "Add Desktop") + icon.name: "list-add" + displayHint: Kirigami.DisplayHint.KeepVisible + onTriggered: kcm.desktopsModel.createDesktop() + } + ] + + Component { + id: desktopsListItemComponent + + QQC2.ItemDelegate { + width: ListView.view.width + down: false // Disable press effect + hoverEnabled: false + + // use alternating background colors to visually connect list items' + // left and right side content elements + Kirigami.Theme.useAlternateBackgroundColor: true + + Keys.onPressed: (event) => { + if (event.key == Qt.Key_F2) { + renameAction.triggered() + event.accepted = true + } + } + + contentItem: StackLayout { + Kirigami.TitleSubtitleWithActions { + title: renameLayout.visible ? "" : model ? model.display : "" + elide: Text.ElideRight + displayHint: QQC2.Button.IconOnly + actions: [ + Kirigami.Action { + id: renameAction + icon.name: "edit-entry-symbolic" + enabled: !renameLayout.visible + text: i18nc("@action:button", "Rename") + onTriggered: { + renameLayout.visible = true; + renameField.forceActiveFocus(); + } + tooltip: text + }, + Kirigami.Action { + enabled: model && !model.IsMissing && desktopsList.count !== 1 && !renameLayout.visible + icon.name: "edit-delete-remove-symbolic" + text: i18nc("@info:tooltip", "Remove") + onTriggered: { + kcm.desktopsModel.removeDesktop(model.Id); + } + } + ] + } + RowLayout { + id: renameLayout + QQC2.TextField { + id: renameField + onEditingFinished: acceptEditButton.clicked() + Keys.onEscapePressed: discardEditButton.clicked() + Keys.onUpPressed: () => { /* don't move to previous delegate */ } + Keys.onDownPressed: () => { /* don't move to next delegate */ } + } + QQC2.Button { + id: acceptEditButton + icon.name: "dialog-ok-apply" + text: i18nc("@info:tooltip", "Apply new name") + onClicked: { + Qt.callLater(kcm.desktopsModel.setDesktopName, model.Id, renameField.text); + renameLayout.visible = false; + } + QQC2.ToolTip.text: text + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + display: QQC2.Button.IconOnly + } + QQC2.Button { + id: discardEditButton + icon.name: "dialog-cancel-symbolic" + text: i18nc("@info:tooltip", "Cancel rename") + onClicked: { + renameLayout.visible = false; + } + QQC2.ToolTip.text: text + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + display: QQC2.Button.IconOnly + } + + onVisibleChanged: { + if (visible) { + renameField.text = model ? model.display : ""; + renameField.selectAll(); + } + } + } + } + } + } + + component DelegateButton: QQC2.ToolButton { + display: QQC2.AbstractButton.IconOnly + QQC2.ToolTip.text: text + QQC2.ToolTip.visible: hovered + } + + header: ColumnLayout { + id: messagesLayout + + spacing: Kirigami.Units.largeSpacing + + Kirigami.InlineMessage { + Layout.fillWidth: true + + type: Kirigami.MessageType.Error + + text: kcm.desktopsModel.error + + visible: kcm.desktopsModel.error !== "" + } + + Kirigami.InlineMessage { + Layout.fillWidth: true + + type: Kirigami.MessageType.Information + + text: i18n("Virtual desktops have been changed outside this settings application. Saving now will overwrite the changes.") + + visible: kcm.desktopsModel.serverModified + } + } + + view: ListView { + id: desktopsList + + clip: true + + model: kcm.desktopsModel.ready ? kcm.desktopsModel : null + + section.property: "DesktopRow" + section.delegate: Kirigami.ListSectionHeader { + width: desktopsList.width + label: i18n("Row %1", section) + } + + delegate: desktopsListItemComponent + reuseItems: true + } + + extraFooterTopPadding: true // re-add separator line + footer: ColumnLayout { + Kirigami.FormLayout { + + QQC2.CheckBox { + id: navWraps + + Kirigami.FormData.label: i18n("Options:") + + text: i18n("Navigation wraps around") + enabled: !kcm.virtualDesktopsSettings.isImmutable("rollOverDesktops") + checked: kcm.virtualDesktopsSettings.rollOverDesktops + onToggled: kcm.virtualDesktopsSettings.rollOverDesktops = checked + + KCM.SettingStateBinding { + configObject: kcm.virtualDesktopsSettings + settingName: "rollOverDesktops" + } + } + + RowLayout { + Layout.fillWidth: true + + QQC2.CheckBox { + id: animationEnabled + Layout.fillWidth: true + + text: i18n("Show animation when switching:") + + checked: kcm.animationsModel.animationEnabled + + onToggled: kcm.animationsModel.animationEnabled = checked + + KCM.SettingHighlighter { + highlight: kcm.animationsModel.animationEnabled !== kcm.animationsModel.defaultAnimationEnabled + } + } + + QQC2.ComboBox { + enabled: animationEnabled.checked + + model: kcm.animationsModel + textRole: "NameRole" + currentIndex: kcm.animationsModel.animationIndex + onActivated: kcm.animationsModel.animationIndex = currentIndex + + KCM.SettingHighlighter { + highlight: kcm.animationsModel.animationIndex !== kcm.animationsModel.defaultAnimationIndex + } + } + + QQC2.Button { + enabled: animationEnabled.checked && kcm.animationsModel.currentConfigurable + + icon.name: "configure" + + onClicked: kcm.configureAnimation() + } + + QQC2.Button { + enabled: animationEnabled.checked + + icon.name: "dialog-information" + + onClicked: kcm.showAboutAnimation() + } + + Item { + Layout.fillWidth: true + } + } + + RowLayout { + Layout.fillWidth: true + + QQC2.CheckBox { + id: osdEnabled + + text: i18n("Show on-screen display when switching:") + + checked: kcm.virtualDesktopsSettings.desktopChangeOsdEnabled + + onToggled: kcm.virtualDesktopsSettings.desktopChangeOsdEnabled = checked + + KCM.SettingStateBinding { + configObject: kcm.virtualDesktopsSettings + settingName: "desktopChangeOsdEnabled" + } + } + + QQC2.SpinBox { + id: osdDuration + + from: 0 + to: 10000 + stepSize: 100 + + textFromValue: (value, locale) => i18n("%1 ms", value) + valueFromText: (text, locale) => Number.fromLocaleString(locale, text.split(" ")[0]) + + value: kcm.virtualDesktopsSettings.popupHideDelay + + onValueModified: kcm.virtualDesktopsSettings.popupHideDelay = value + + KCM.SettingStateBinding { + configObject: kcm.virtualDesktopsSettings + settingName: "popupHideDelay" + extraEnabledConditions: osdEnabled.checked + } + } + } + + RowLayout { + Layout.fillWidth: true + + Item { + Layout.preferredWidth: Kirigami.Units.gridUnit + } + + QQC2.CheckBox { + id: osdTextOnly + text: i18n("Show desktop layout indicators") + checked: !kcm.virtualDesktopsSettings.textOnly + onToggled: kcm.virtualDesktopsSettings.textOnly = !checked + + KCM.SettingStateBinding { + configObject: kcm.virtualDesktopsSettings + settingName: "textOnly" + extraEnabledConditions: osdEnabled.checked + } + } + } + } + } +} + diff --git a/local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktops.cpp b/local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktops.cpp new file mode 100644 index 0000000000..e1d24b8eca --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktops.cpp @@ -0,0 +1,165 @@ +/* + SPDX-FileCopyrightText: 2018 Eike Hein + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "virtualdesktops.h" +#include "animationsmodel.h" +#include "desktopsmodel.h" +#include "virtualdesktopsdata.h" +#include "virtualdesktopssettings.h" + +#include +#include +#include +#include +#include +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(VirtualDesktopsFactory, + "kcm_kwin_virtualdesktops.json", + registerPlugin(); + registerPlugin();) + +namespace KWin +{ + +VirtualDesktops::VirtualDesktops(QObject *parent, const KPluginMetaData &metaData) + : KQuickManagedConfigModule(parent, metaData) + , m_data(new VirtualDesktopsData(this)) +{ + qmlRegisterAnonymousType("org.kde.kwin.kcm.desktop", 0); + + setButtons(Apply | Default | Help); + + QObject::connect(m_data->desktopsModel(), &KWin::DesktopsModel::userModifiedChanged, + this, &VirtualDesktops::settingsChanged); + connect(m_data->animationsModel(), &AnimationsModel::animationEnabledChanged, + this, &VirtualDesktops::settingsChanged); + connect(m_data->animationsModel(), &AnimationsModel::animationIndexChanged, + this, &VirtualDesktops::settingsChanged); +} + +VirtualDesktops::~VirtualDesktops() +{ +} + +QAbstractItemModel *VirtualDesktops::desktopsModel() const +{ + return m_data->desktopsModel(); +} + +QAbstractItemModel *VirtualDesktops::animationsModel() const +{ + return m_data->animationsModel(); +} + +VirtualDesktopsSettings *VirtualDesktops::virtualDesktopsSettings() const +{ + return m_data->settings(); +} + +void VirtualDesktops::load() +{ + KQuickManagedConfigModule::load(); + + m_data->desktopsModel()->load(); + m_data->animationsModel()->load(); +} + +void VirtualDesktops::save() +{ + KQuickManagedConfigModule::save(); + + m_data->desktopsModel()->syncWithServer(); + m_data->animationsModel()->save(); + + QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/KWin"), + QStringLiteral("org.kde.KWin"), QStringLiteral("reloadConfig")); + QDBusConnection::sessionBus().send(message); +} + +void VirtualDesktops::defaults() +{ + KQuickManagedConfigModule::defaults(); + + m_data->desktopsModel()->defaults(); + m_data->animationsModel()->defaults(); +} + +bool VirtualDesktops::isDefaults() const +{ + return m_data->isDefaults(); +} + +void VirtualDesktops::configureAnimation() +{ + const QModelIndex index = m_data->animationsModel()->index(m_data->animationsModel()->animationIndex(), 0); + if (!index.isValid()) { + return; + } + + m_data->animationsModel()->requestConfigure(index, nullptr); +} + +void VirtualDesktops::showAboutAnimation() +{ + const QModelIndex index = m_data->animationsModel()->index(m_data->animationsModel()->animationIndex(), 0); + if (!index.isValid()) { + return; + } + + const QString name = index.data(AnimationsModel::NameRole).toString(); + const QString comment = index.data(AnimationsModel::DescriptionRole).toString(); + const QString author = index.data(AnimationsModel::AuthorNameRole).toString(); + const QString email = index.data(AnimationsModel::AuthorEmailRole).toString(); + const QString website = index.data(AnimationsModel::WebsiteRole).toString(); + const QString version = index.data(AnimationsModel::VersionRole).toString(); + const QString license = index.data(AnimationsModel::LicenseRole).toString(); + const QString icon = index.data(AnimationsModel::IconNameRole).toString(); + + const KAboutLicense::LicenseKey licenseType = KAboutLicense::byKeyword(license).key(); + + KAboutData aboutData( + name, // Plugin name + name, // Display name + version, // Version + comment, // Short description + licenseType, // License + QString(), // Copyright statement + QString(), // Other text + website.toLatin1() // Home page + ); + aboutData.setProgramLogo(icon); + + const QStringList authors = author.split(','); + const QStringList emails = email.split(','); + + if (authors.count() == emails.count()) { + int i = 0; + for (const QString &author : authors) { + if (!author.isEmpty()) { + aboutData.addAuthor(i18n(author.toUtf8()), QString(), emails[i]); + } + i++; + } + } + + QPointer aboutPlugin = new KAboutApplicationDialog(aboutData); + aboutPlugin->exec(); + + delete aboutPlugin; +} + +bool VirtualDesktops::isSaveNeeded() const +{ + return m_data->animationsModel()->needsSave() || m_data->desktopsModel()->needsSave(); +} + +} + +#include "moc_virtualdesktops.cpp" +#include "virtualdesktops.moc" diff --git a/local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktops.h b/local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktops.h new file mode 100644 index 0000000000..115912a2cd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktops.h @@ -0,0 +1,55 @@ +/* + SPDX-FileCopyrightText: 2018 Eike Hein + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +class VirtualDesktopsSettings; + +namespace KWin +{ +class VirtualDesktopsData; +class AnimationsModel; +class DesktopsModel; + +class VirtualDesktops : public KQuickManagedConfigModule +{ + Q_OBJECT + + Q_PROPERTY(QAbstractItemModel *desktopsModel READ desktopsModel CONSTANT) + Q_PROPERTY(QAbstractItemModel *animationsModel READ animationsModel CONSTANT) + Q_PROPERTY(VirtualDesktopsSettings *virtualDesktopsSettings READ virtualDesktopsSettings CONSTANT) + +public: + explicit VirtualDesktops(QObject *parent, const KPluginMetaData &metaData); + ~VirtualDesktops() override; + + QAbstractItemModel *desktopsModel() const; + + QAbstractItemModel *animationsModel() const; + + VirtualDesktopsSettings *virtualDesktopsSettings() const; + + bool isDefaults() const override; + bool isSaveNeeded() const override; + +public Q_SLOTS: + void load() override; + void save() override; + void defaults() override; + + void configureAnimation(); + void showAboutAnimation(); + +private: + VirtualDesktopsData *m_data; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktopsdata.cpp b/local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktopsdata.cpp new file mode 100644 index 0000000000..78c1b66baa --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktopsdata.cpp @@ -0,0 +1,54 @@ +/* + SPDX-FileCopyrightText: 2021 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "virtualdesktopsdata.h" + +#include "animationsmodel.h" +#include "desktopsmodel.h" +#include "virtualdesktopssettings.h" + +namespace KWin +{ + +VirtualDesktopsData::VirtualDesktopsData(QObject *parent) + : KCModuleData(parent) + , m_settings(new VirtualDesktopsSettings(this)) + , m_desktopsModel(new DesktopsModel(this)) + , m_animationsModel(new AnimationsModel(this)) +{ + // Default behavior of KCModuleData is to emit loaded signal after being initialized. + // To handle asynchronous load of EffectsModel we disable default behavior and + // emit loaded signal when EffectsModel is actually loaded. + disconnect(this, &KCModuleData::aboutToLoad, nullptr, nullptr); + connect(m_animationsModel, &EffectsModel::loaded, this, &KCModuleData::loaded); + + m_desktopsModel->load(); + m_animationsModel->load(); +} + +bool VirtualDesktopsData::isDefaults() const +{ + return m_animationsModel->isDefaults() && m_desktopsModel->isDefaults() && m_settings->isDefaults(); +} + +VirtualDesktopsSettings *VirtualDesktopsData::settings() const +{ + return m_settings; +} + +DesktopsModel *VirtualDesktopsData::desktopsModel() const +{ + return m_desktopsModel; +} + +AnimationsModel *VirtualDesktopsData::animationsModel() const +{ + return m_animationsModel; +} + +} + +#include "moc_virtualdesktopsdata.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktopsdata.h b/local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktopsdata.h new file mode 100644 index 0000000000..5e7cde91d4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktopsdata.h @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2021 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +class VirtualDesktopsSettings; + +namespace KWin +{ + +class AnimationsModel; +class DesktopsModel; + +class VirtualDesktopsData : public KCModuleData +{ + Q_OBJECT + +public: + explicit VirtualDesktopsData(QObject *parent); + + bool isDefaults() const override; + + VirtualDesktopsSettings *settings() const; + DesktopsModel *desktopsModel() const; + AnimationsModel *animationsModel() const; + +private: + VirtualDesktopsSettings *m_settings; + DesktopsModel *m_desktopsModel; + AnimationsModel *m_animationsModel; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktopssettings.kcfg b/local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktopssettings.kcfg new file mode 100644 index 0000000000..e4ac07f90a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktopssettings.kcfg @@ -0,0 +1,29 @@ + + + + + + + false + + + + + + false + + + + + + 1000 + + + + false + + + diff --git a/local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktopssettings.kcfgc b/local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktopssettings.kcfgc new file mode 100644 index 0000000000..469abdee91 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/desktop/virtualdesktopssettings.kcfgc @@ -0,0 +1,6 @@ +File=virtualdesktopssettings.kcfg +ClassName=VirtualDesktopsSettings +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true diff --git a/local/recipes/kde/kwin/source/src/kcms/effects/CMakeLists.txt b/local/recipes/kde/kwin/source/src/kcms/effects/CMakeLists.txt new file mode 100644 index 0000000000..0e9fa1691b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/effects/CMakeLists.txt @@ -0,0 +1,27 @@ +# KI18N Translation Domain for this library. +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_kwin_effects\") + +########### next target ############### + +set(kcm_kwin_effects_PART_SRCS + kcm.cpp + effectsfilterproxymodel.cpp + desktopeffectsdata.cpp +) + +kcmutils_add_qml_kcm(kcm_kwin_effects SOURCES ${kcm_kwin_effects_PART_SRCS}) + +target_link_libraries(kcm_kwin_effects PRIVATE + Qt::DBus + Qt::Quick + + KF6::KCMUtils + KF6::I18n + KF6::KCMUtils + KF6::KCMUtilsQuick + KF6::XmlGui + + kcmkwincommon +) + +install(FILES kwineffect.knsrc DESTINATION ${KDE_INSTALL_KNSRCDIR}) diff --git a/local/recipes/kde/kwin/source/src/kcms/effects/Messages.sh b/local/recipes/kde/kwin/source/src/kcms/effects/Messages.sh new file mode 100644 index 0000000000..6ff6bf8be3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/effects/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.cpp -o -name \*.qml` -o $podir/kcm_kwin_effects.pot diff --git a/local/recipes/kde/kwin/source/src/kcms/effects/desktopeffectsdata.cpp b/local/recipes/kde/kwin/source/src/kcms/effects/desktopeffectsdata.cpp new file mode 100644 index 0000000000..62cafc40a9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/effects/desktopeffectsdata.cpp @@ -0,0 +1,39 @@ +/* + SPDX-FileCopyrightText: 2021 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "desktopeffectsdata.h" +#include "effectsmodel.h" + +namespace KWin +{ + +DesktopEffectsData::DesktopEffectsData(QObject *parent) + : KCModuleData(parent) + , m_model(new EffectsModel(this)) + +{ + disconnect(this, &KCModuleData::aboutToLoad, nullptr, nullptr); + connect(m_model, &EffectsModel::loaded, this, &KCModuleData::loaded); + + // These are handled in kcm_animations + m_model->setExcludeExclusiveGroups({"toplevel-open-close-animation", "maximize", "minimize", "fullscreen", "show-desktop", "desktop-animations"}); + m_model->setExcludeEffects({"fadingpopups", "slidingpopups", "login", "logout"}); + + m_model->load(); +} + +DesktopEffectsData::~DesktopEffectsData() +{ +} + +bool DesktopEffectsData::isDefaults() const +{ + return m_model->isDefaults(); +} + +} + +#include "moc_desktopeffectsdata.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/effects/desktopeffectsdata.h b/local/recipes/kde/kwin/source/src/kcms/effects/desktopeffectsdata.h new file mode 100644 index 0000000000..7ad6928694 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/effects/desktopeffectsdata.h @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2021 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +namespace KWin +{ +class EffectsModel; + +class DesktopEffectsData : public KCModuleData +{ + Q_OBJECT + +public: + explicit DesktopEffectsData(QObject *parent); + ~DesktopEffectsData() override; + + bool isDefaults() const override; + +private: + EffectsModel *m_model; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/kcms/effects/effectsfilterproxymodel.cpp b/local/recipes/kde/kwin/source/src/kcms/effects/effectsfilterproxymodel.cpp new file mode 100644 index 0000000000..8034c841c1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/effects/effectsfilterproxymodel.cpp @@ -0,0 +1,73 @@ +/* + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "effectsfilterproxymodel.h" + +#include "effectsmodel.h" + +namespace KWin +{ + +EffectsFilterProxyModel::EffectsFilterProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) +{ +} + +EffectsFilterProxyModel::~EffectsFilterProxyModel() +{ +} + +QString EffectsFilterProxyModel::query() const +{ + return m_query; +} + +void EffectsFilterProxyModel::setQuery(const QString &query) +{ + if (m_query != query) { + m_query = query; + Q_EMIT queryChanged(); + invalidateFilter(); + } +} + +bool EffectsFilterProxyModel::excludeUnsupported() const +{ + return m_excludeUnsupported; +} + +void EffectsFilterProxyModel::setExcludeUnsupported(bool exclude) +{ + if (m_excludeUnsupported != exclude) { + m_excludeUnsupported = exclude; + Q_EMIT excludeUnsupportedChanged(); + invalidateFilter(); + } +} + +bool EffectsFilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + const QModelIndex idx = sourceModel()->index(sourceRow, 0, sourceParent); + + if (!m_query.isEmpty()) { + const bool matches = idx.data(EffectsModel::NameRole).toString().contains(m_query, Qt::CaseInsensitive) || idx.data(EffectsModel::DescriptionRole).toString().contains(m_query, Qt::CaseInsensitive) || idx.data(EffectsModel::CategoryRole).toString().contains(m_query, Qt::CaseInsensitive); + if (!matches) { + return false; + } + } + + if (m_excludeUnsupported) { + if (!idx.data(EffectsModel::SupportedRole).toBool()) { + return false; + } + } + + return true; +} + +} // namespace KWin + +#include "moc_effectsfilterproxymodel.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/effects/effectsfilterproxymodel.h b/local/recipes/kde/kwin/source/src/kcms/effects/effectsfilterproxymodel.h new file mode 100644 index 0000000000..dba4c5ba99 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/effects/effectsfilterproxymodel.h @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +namespace KWin +{ + +class EffectsFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(QAbstractItemModel *sourceModel READ sourceModel WRITE setSourceModel) + Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged) + Q_PROPERTY(bool excludeUnsupported READ excludeUnsupported WRITE setExcludeUnsupported NOTIFY excludeUnsupportedChanged) + +public: + explicit EffectsFilterProxyModel(QObject *parent = nullptr); + ~EffectsFilterProxyModel() override; + + QString query() const; + void setQuery(const QString &query); + + bool excludeUnsupported() const; + void setExcludeUnsupported(bool exclude); + +Q_SIGNALS: + void queryChanged(); + void excludeUnsupportedChanged(); + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + +private: + QString m_query; + bool m_excludeUnsupported = true; + + Q_DISABLE_COPY(EffectsFilterProxyModel) +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/kcms/effects/kcm.cpp b/local/recipes/kde/kwin/source/src/kcms/effects/kcm.cpp new file mode 100644 index 0000000000..27d2c84e9b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/effects/kcm.cpp @@ -0,0 +1,87 @@ +/* + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kcm.h" +#include "desktopeffectsdata.h" +#include "effectsfilterproxymodel.h" +#include "effectsmodel.h" + +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(DesktopEffectsKCMFactory, + "kcm_kwin_effects.json", + registerPlugin(); + registerPlugin();) + +namespace KWin +{ + +DesktopEffectsKCM::DesktopEffectsKCM(QObject *parent, const KPluginMetaData &metaData) + : KQuickConfigModule(parent, metaData) + , m_model(new EffectsModel(this)) +{ + qmlRegisterType("org.kde.private.kcms.kwin.effects", 1, 0, "EffectsFilterProxyModel"); + + setButtons(Apply | Default | Help); + + connect(m_model, &EffectsModel::dataChanged, this, &DesktopEffectsKCM::updateNeedsSave); + connect(m_model, &EffectsModel::loaded, this, &DesktopEffectsKCM::updateNeedsSave); + + // These are handled in kcm_animations + m_model->setExcludeExclusiveGroups({"toplevel-open-close-animation", "maximize", "minimize", "fullscreen", "show-desktop", "desktop-animations"}); + m_model->setExcludeEffects({"fadingpopups", "slidingpopups", "login", "logout"}); +} + +DesktopEffectsKCM::~DesktopEffectsKCM() +{ +} + +QAbstractItemModel *DesktopEffectsKCM::effectsModel() const +{ + return m_model; +} + +void DesktopEffectsKCM::load() +{ + m_model->load(); + setNeedsSave(false); +} + +void DesktopEffectsKCM::save() +{ + m_model->save(); + setNeedsSave(false); +} + +void DesktopEffectsKCM::defaults() +{ + m_model->defaults(); + updateNeedsSave(); +} + +void DesktopEffectsKCM::onGHNSEntriesChanged() +{ + m_model->load(EffectsModel::LoadOptions::KeepDirty); +} + +void DesktopEffectsKCM::configure(const QString &pluginId, QQuickItem *context) const +{ + const QModelIndex index = m_model->findByPluginId(pluginId); + m_model->requestConfigure(index, context); +} + +void DesktopEffectsKCM::updateNeedsSave() +{ + setNeedsSave(m_model->needsSave()); + setRepresentsDefaults(m_model->isDefaults()); +} + +} // namespace KWin + +#include "kcm.moc" + +#include "moc_kcm.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/effects/kcm.h b/local/recipes/kde/kwin/source/src/kcms/effects/kcm.h new file mode 100644 index 0000000000..94639622d7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/effects/kcm.h @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +namespace KWin +{ + +class EffectsModel; + +class DesktopEffectsKCM : public KQuickConfigModule +{ + Q_OBJECT + Q_PROPERTY(QAbstractItemModel *effectsModel READ effectsModel CONSTANT) + +public: + explicit DesktopEffectsKCM(QObject *parent, const KPluginMetaData &metaData); + ~DesktopEffectsKCM() override; + + QAbstractItemModel *effectsModel() const; + +public Q_SLOTS: + void load() override; + void save() override; + void defaults() override; + + void onGHNSEntriesChanged(); + void configure(const QString &pluginId, QQuickItem *context) const; + +private Q_SLOTS: + void updateNeedsSave(); + +private: + EffectsModel *m_model; + + Q_DISABLE_COPY(DesktopEffectsKCM) +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/kcms/effects/kcm_kwin_effects.json b/local/recipes/kde/kwin/source/src/kcms/effects/kcm_kwin_effects.json new file mode 100644 index 0000000000..53fe182b8c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/effects/kcm_kwin_effects.json @@ -0,0 +1,140 @@ +{ + "KPlugin": { + "BugReportUrl": "https://bugs.kde.org/enter_bug.cgi?product=systemsettings&component=kcm_kwineffects", + "Description": "Configure compositor settings for desktop effects", + "Description[ar]": "اضبط إعدادات المركب لتأثيرات سطح المكتب", + "Description[az]": "İş masası effektləri üçün düzücünün ayarlarını tənzimləyin", + "Description[be]": "Наладжванне сродку кампазітынгу для эфектаў працоўнага стала", + "Description[bg]": "Конфигуриране на настройките на композитора за ефекти на работния плот", + "Description[ca@valencia]": "Configura la configuració del compositor per als efectes de l'escriptori", + "Description[ca]": "Configura l'arranjament del compositor per als efectes d'escriptori", + "Description[cs]": "Nastavení kompozitoru pro efekty pracovní plochy", + "Description[da]": "Konfigurér kompositørindstillinger for skrivebordseffekter", + "Description[de]": "Compositor-Einstellungen für Arbeitsflächen-Effekte einrichten", + "Description[en_GB]": "Configure compositor settings for desktop effects", + "Description[eo]": "Agordi kompostajn agordojn por labortablaj efikoj", + "Description[es]": "Configurar las preferencias del compositor para los efectos del escritorio", + "Description[et]": "Komposiitori seadistamine töölauaefektide tarbeks", + "Description[eu]": "Konfiguratu konposatzailearen ezarpenak mahaigaineko efektuetarako", + "Description[fi]": "Koostamisasetukset työpöytätehosteille", + "Description[fr]": "Configurer les paramètres du compositeur pour les effets de bureau", + "Description[gl]": "Configurar o compositor para os efectos de escritorio.", + "Description[he]": "הגדרת אפקטים של שולחן עבודה בהגדרות המנהל החלונאי", + "Description[hu]": "A kompozitor beállításainak konfigurálása az asztali effektusokhoz", + "Description[ia]": "Configura preferentias de compositor pro le effectos de scriptorio", + "Description[id]": "Konfigurasikan pengaturan kompositor untuk efek desktop", + "Description[is]": "Grunnstilla skjásamsetningu fyrir skjáborðsbrellur", + "Description[it]": "Configura impostazioni del compositore per gli effetti del desktop", + "Description[ja]": "デスクトップ効果のためのコンポジタ設定", + "Description[ka]": "კომპოზიტორის მორგება სამუშაო მაგიდის ეფექტებისთვის", + "Description[ko]": "데스크톱 효과에 사용되는 컴포지터 설정", + "Description[lt]": "Konfigūruoti darbalaukio efektams skirtas kompozitoriaus nuostatas", + "Description[lv]": "Konfigurēt kompozitora iestatījumus darbvirsmas efektiem", + "Description[nb]": "Sett opp sammensetterinnstillinger for skrivebordseffekter", + "Description[nl]": "Instellingen van compositor configureren voor bureaubladeffecten", + "Description[nn]": "Samansetjarinnstillingar for skrivebordseffektar", + "Description[pl]": "Ustawienia kompozytora dla efektów pulpitu", + "Description[pt]": "Configurar as definições dos efeitos do ecrã do compositor", + "Description[pt_BR]": "Defina as configurações do compositor para os efeitos da área de trabalho", + "Description[ro]": "Configurează opțiunile compozitorului pentru efecte de birou", + "Description[ru]": "Настройка модуля обеспечения эффектов рабочего стола", + "Description[sa]": "डेस्कटॉप् इफेक्ट्स् कृते कम्पोजिटर सेटिंग्स् विन्यस्यताम्", + "Description[sk]": "Nastavenia kompozítora pre efekty plochy", + "Description[sl]": "Prilagodite nastavitve skladanja za učinke namizja", + "Description[sv]": "Anpassa sammansättningsinställningar för skrivbordseffekter", + "Description[ta]": "பணிமேடை அசைவூட்டங்களுக்கான சாளரநிரல் அமைப்புகளை மாற்றுங்கள்", + "Description[tr]": "Masaüstü efektleri için bileşikleştirici ayarlarını yapılandır", + "Description[uk]": "Налаштовування параметрів засобу композиції для ефектів стільниці", + "Description[vi]": "Cấu hình các thiết lập trình kết hợp cho các hiệu ứng bàn làm việc", + "Description[zh_CN]": "配置桌面特效的显示合成器设置", + "Description[zh_TW]": "設定合成器的桌面效果選項", + "FormFactors": [ + "desktop", + "tablet" + ], + "Icon": "preferences-desktop-effects", + "Name": "Desktop Effects", + "Name[ar]": "تأثيرات سطح المكتب", + "Name[az]": "İş masası effektləri", + "Name[be]": "Эфекты працоўнага стала", + "Name[bg]": "Ефекти на работния плот", + "Name[ca@valencia]": "Efectes de l'escriptori", + "Name[ca]": "Efectes d'escriptori", + "Name[cs]": "Efekty na ploše", + "Name[da]": "Skrivebordseffekter", + "Name[de]": "Arbeitsflächen-Effekte", + "Name[en_GB]": "Desktop Effects", + "Name[eo]": "Labortablo-Efikoj", + "Name[es]": "Efectos del escritorio", + "Name[et]": "Töölauaefektid", + "Name[eu]": "Mahaigaineko efektuak", + "Name[fi]": "Työpöytätehosteet", + "Name[fr]": "Effets de bureau", + "Name[gl]": "Efectos de escritorio", + "Name[he]": "אפקטים של שולחן עבודה", + "Name[hu]": "Asztali effektusok", + "Name[ia]": "Effectos de scriptorio", + "Name[id]": "Efek Desktop", + "Name[is]": "Skjáborðsbrellur", + "Name[it]": "Effetti del desktop", + "Name[ja]": "デスクトップ効果", + "Name[ka]": "სამუშაო მაგიდის ეფექტები", + "Name[ko]": "데스크톱 효과", + "Name[lt]": "Darbalaukio efektai", + "Name[lv]": "Darbvirsmu efekti", + "Name[nb]": "Skrivebordseffekter", + "Name[nl]": "Bureaubladeffecten", + "Name[nn]": "Skrivebords­effektar", + "Name[pl]": "Efekty pulpitu", + "Name[pt]": "Efeitos do Ecrã", + "Name[pt_BR]": "Efeitos da área de trabalho", + "Name[ro]": "Efecte de birou", + "Name[ru]": "Эффекты рабочего стола", + "Name[sa]": "डेस्कटॉप प्रभाव", + "Name[sk]": "Efekty plochy", + "Name[sl]": "Namizni učinki", + "Name[sv]": "Skrivbordseffekter", + "Name[ta]": "பணிமேடை அசைவூட்டங்கள்", + "Name[tr]": "Masaüstü Efektleri", + "Name[uk]": "Ефекти стільниці", + "Name[vi]": "Hiệu ứng bàn làm việc", + "Name[zh_CN]": "桌面特效", + "Name[zh_TW]": "桌面效果" + }, + "X-DocPath": "kcontrol/kwineffects/index.html", + "X-KDE-Keywords": "kwin,window,manager,effect,3D effects,2D effects,graphical effects,desktop effects,animations,various animations,window management effects,animations,desktop animations,rendering,render,snap helper effect,track mouse effect,blur effect,fall apart effect,highlight window effect,mouse mark effect,screenshot effect,sheet effect,thumbnail aside effect,translucency,translucency effect,transparency,window geometry effect,wobbly windows effect,startup feedback effect,dialog parent effect,dim inactive effect,dim screen effect,slide back effect,eye candy,candy,cover switch effect,desktop cube effect,desktop cube animation effect,desktop grid effect,background contrast effect,bling,fading,movement,motion,overview windows effect,accessibility,cursor,pointer,mouse,hide cursor,hide cursor effect,hide pointer,hide pointer effect,hide mouse,hide mouse effect", + "X-KDE-Keywords[ar]": "كوين،نافذة،مدير،تأثير،تأثيرات ثلاثية الأبعاد،تأثيرات ثنائية الأبعاد،تأثيرات رسومية،تأثيرات سطح المكتب،رسوم متحركة،رسوم متحركة مختلفة،تأثيرات إدارة النوافذ،تأثير تبديل النوافذ،تأثير تبديل سطح المكتب،رسوم متحركة،رسوم متحركة لسطح المكتب،برامج تشغيل،إعدادات برنامج التشغيل،عرض,تأثير مساعد المفاجئة,تأثير تتبع مسار الفأرة,تأثير التمويه,تأثير التلاشي,تأثير تلاشي سطح المكتب,تأثير الانهيار,تأثير الانزلاق,تأثير النافذة المميزة,تأثير تسجيل الدخول,تأثير تسجيل الخروج,سحر تأثير المصباح,تقليل تأثير الحركة,تأثير علامة الفأرة,تأثير المقياس,تأثير لقطة الشاشة,تأثير الورقة,تأثير الشريحة,تأثير النوافذ المنبثقة المنزلقة,تأثير الصورة المصغرة جانبًا,الشفافية,تأثير الشفافية,الشفافية,تأثير هندسة النافذة,تأثير النوافذ المتذبذبة,تأثير ردود الفعل عند بدء التشغيل,تأثير الحوار الأصلي,تأثير خافت غير نشط,تأثير الشاشة الخافت,تأثير الشريحة الخلفية,العين حلوى,حلوى,تأثير تبديل الغطاء,تأثير مكعب سطح المكتب,تأثير رسوم متحركة لمكعب سطح المكتب,تأثير شبكة سطح المكتب,تأثير تبديل,تأثير تباين الخلفية,بريق,بهتان,حركة,حركة,تأثير نظرة عامة على النوافذ,إمكانية الوصول,مؤشر,مؤشر,فأر,إخفاء المؤشر,إخفاء تأثير المؤشر,إخفاء المؤشر,إخفاء تأثير المؤشر,إخفاء الفأرة,إخفاء تأثير الفأرة", + "X-KDE-Keywords[bg]": "kwin,прозорци,мениджър,ефект,3D ефекти,2D ефекти,графични ефекти,ефекти за работния плот,анимации,различни анимации,ефекти за управление на прозорци,анимации,анимации за работния плот,рендиране,рендиране,инвертиращ ефект,ефект на огледало,ефект на лупа,ефект на помощник за прищракване,ефект на проследяване на мишката,ефект на мащабиране,ефект на размазване,ефект на разпадане,ефект на маркиране на прозорец,ефект на маркировка на мишката,ефект на екранна снимка,ефект на лист,ефект на миниатюра настрани,прозрачност,ефект на прозрачност,прозрачност,ефект на геометрия на прозорец,ефект на трептящи прозорци,ефект на обратна връзка при стартиране,ефект на родителски диалогов прозорец,ефект на затъмняване на неактивност,ефект на затъмняване на екрана,ефект на плъзгане назад,приятен за окото,бонбон,ефект на показване на FPS,ефект на показване на боя,ефект на превключване на капака,ефект на куб на работния плот,анимационен ефект на куб на работния плот,ефект на мрежа на работния плот,ефект на контраст на фона,блясък,избледняване,движение,движение,прозорци за преглед ефект, достъпност, курсор, показалец, мишка, скриване на курсора, скриване на ефекта на курсора, скриване на показалеца, скриване на ефекта на показалеца, скриване на мишката, скриване на ефекта на мишката", + "X-KDE-Keywords[ca@valencia]": "kwin,finestra,gestor,efecte,efectes 3D,efectes 2D,efectes gràfics,efectes de l'escriptori,animacions,animacions diverses,efectes de gestió de finestres,animacions,animacions d'escriptori,renderització,representació,render,efecte d'ajuda d'ajust,efecte de seguiment de ratolí,efecte de difuminat,efecte de trencament,efecte de ressaltat de finestra,efecte de marca de ratolí,efecte de captura de pantalla,efecte de full,efecte de miniatures al costat,translucidesa,efecte de translucidesa,transparència,efecte de geometria de les finestres,efecte de finestres sacsades,efecte de retroacció en iniciar,efecte de diàleg principal,efecte d'enfosquir les inactives,efecte de pantalla enfosquida,efecte de lliscar cap arrere,vistositat,vistós,efecte de canvi de tapa,efecte d'animació de cub d'escriptori,efecte de quadrícula d'escriptori,efecte de contrast de fons,bling,esvaïment,moviment,efecte de vista general de finestres,accessibilitat,cursor,punter,ratolí,oculta cursor,efecte d'ocultació de cursor", + "X-KDE-Keywords[ca]": "kwin,finestra,gestor,efecte,efectes 3D,efectes 2D,efectes gràfics,efectes d'escriptori,animacions,animacions diverses,efectes de gestió de finestres,animacions,animacions d'escriptori,renderització,representació,render,efecte d'ajuda d'ajust,efecte de seguiment de ratolí,efecte de difuminat,efecte de trencament,efecte de ressaltat de finestra,efecte de marca de ratolí,efecte de captura de pantalla,efecte de full,efecte de miniatures al costat,translucidesa,efecte de translucidesa,transparència,efecte de geometria de les finestres,efecte de finestres sacsejades,efecte de retroacció en iniciar,efecte de diàleg principal,efecte d'enfosquir les inactives,efecte de pantalla enfosquida,efecte de lliscar cap enrere,vistositat,vistós,efecte de canvi de tapa,efecte d'animació de cub d'escriptori,efecte de quadrícula d'escriptori,efecte de contrast de fons,bling,esvaïment,moviment,efecte de vista general de finestres,accessibilitat,cursor,punter,ratolí,oculta cursor,efecte d'ocultació de cursor", + "X-KDE-Keywords[de]": "kwin,Fenster,Manager,Effekt,3D-Effekte,2D-Effekte,Grafische Effekte,Arbeitsflächen-Effekte,Animationen,Verschiedene Animationen,Fensterverwaltungseffekte,Animationen,Arbeitsflächen-Amimationen,Rendern,Rendern,Weichzeichnereffekt,Auseinanderfallen-Effekt,Fenster-Hervorheben-Effekt,Mausspureffekt,Bildschirmfotoeffekt,Schweben-Effekt,Seitliche-Vorschaubilder-Effekt,halbdurchleuchtend,Transparenzeffekt,Transparenz,Fenstergrößeneffekt,Wabernde-Fenster-Effekt,Programmstartanzeige-Effekt,Eltern-Fenster-Effekt,„Inaktive abdunkeln“-Effekt,„Bildschirm abdunkeln“-Effekt,„Nach hinten gleiten“-Effekt,Augenweide,Süßigkeiten,„Abdeckung umschalten“-Effekt,Desktopwürfel-Effekt,Desktopwürfel-Animationseffekt,Arbeitsflächen-Raster-Effekt,Hintergrundkontrast-Effekt,Klunker,überblenden,Verschiebung,Bewegung,Fensterübersicht-Effekt,Zugangshilfen,Eingabezeiger,Cursor,Zeiger,Maus,Zeiger ausblenden,„Zeiger ausblenden“-Effekt,Maus ausblenden,„Maus ausblenden“-Effekt", + "X-KDE-Keywords[es]": "kwin,ventana,gestor,efecto,efectos 3D,efectos 2D,efectos gráficos,efectos del escritorio,animaciones,animaciones varias,animaciones diversas,efectos de gestión de ventanas,animaciones,animaciones del escritorio,renderizado,renderizar,efecto auxiliar de ajuste,efecto de seguimiento del ratón,efecto de desenfoque,defecto de desmoronar,efecto de resaltar ventana,efecto de marcar con el ratón,efecto de captura de pantalla,efecto de hoja,efecto de miniaturas laterales,transparencia,transparente,efecto de transparencia,efecto de geometría de la ventana,efecto de ventanas tambaleantes,efecto de notificación de lanzamiento,efecto de diálogo padre,efecto de oscurecer inactiva,efecto de atenuar inactiva,efecto de desenfocar inactiva,efecto de oscurecer pantalla,efecto de atenuar pantalla,efecto de desenfocar pantalla,efecto de deslizar hacia atrás,atractivo,efecto de selección de carátulas,efecto de selección de portadas,efecto del cubo del escritorio,efecto de animación del cubo del escritorio,efecto de la cuadrícula de escritorios,efecto de contraste del fondo,desvanecimiento,desvanecer,atenuar,atenuación,movimiento,efecto de resumen de ventanas,efecto de vista general de ventanas,accesibilidad,cursor,puntero,ratón,ocultar el cursor,efecto de ocultar el cursor,ocultar el puntero,efecto de ocultar el puntero,ocultar el ratón,efecto de ocultar el ratón", + "X-KDE-Keywords[eu]": "kwin,leihoa,kudeatzailea,efektua,3D efektuak,2D efektuak,efektu grafikoak,mahaigaineko efektuak,animazioak,hainbat animazio,leiho kudeaketa efektua,animazioak,mahaigaineko animazioak,errendatzea,errendatu,atxikitzeko efektu laguntzailea,saguaren jarraipen efektua,zoom efektua,lausotze efektua,birrintze efektua,leihoa nabarmentzeko efektua,saguaren arrasto efektua,pantaila-argazki efektua,orri efektua,koadro txikia alboan efektua,zeharrargitasuna,zeharrargitasun efektua,gardentasuna,leihoaren geometria efektua,leiho dardarti efektua,abiatze berrelikadura efektua, guraso elkarrizketa-koadro efektua,ahuldu ez-aktiboa efektua,ahuldu pantaila efektua,irristatu atzerantz efektua,begiaren gozagarri,gozagarria,azal aldaketa efektua,mahaigain kubo efektua,mahaigain kubo animazio efektua,atzeko planoaren kontrastea efektua,dir-dir,itzaleztatu,koloregabetu,mugimendua,higidura,leihoen ikuspegi orokor efektua,irisgarritasuna, kurtsorea, erakuslea,sagua,ezkutatu sagua,sagua ezkutatzeko efektua, ezkutatu erakuslea,erakuslea ezkutatzeko efektua,ezkutatu sagua,sagua ezkutatzeko efektua", + "X-KDE-Keywords[fi]": "kwin,ikkuna,hallinta,tehoste,3D-tehosteet,2D-tehosteet,graafiset tehosteet,työpöytätehosteet,animoinnit,animaatiot,tehoste,ikkunatehoste,ikkunatehosteet,ikkunanvaihtotehoste,työpöytävaihtotehoste,työpöydänvaihtotehoste,vaihtotehoste,ajurit,ajuriasetukset,hahmonnus,renderöinti,käänteinen,suurennuslasi,tarttuminen,kiinnittyminen,jäljitä hiiri,suurennustehoste,sumennustehoste,häivytystehoste,häivytä työpöytä,uloskirjautumistehoste,taikalampputehoste,pienennystehoste,hiiren jälki,skaalaus,ruutukaappaustehoste,näyttökuvatehoste,näyttökaappaustehoste,liuku,liukuvat ponnahdusikkunat,pienoiskuva sivulla,läpinäkyvyys,läpikuultavuus,ikkunageometria,huojuvat ikkunat,käynnistyspalaute,emoikkuna,himmennä passiivinen,himmennä näyttö,liu’uta taakse,esitä ikkunat,ikkunan koon muuttaminen,muuta ikkunan kokoa,silmäkarkki,näytä FPS,näytä maalaus,näytä piirto,kansikuva,työpöytäkuutio,työpöytäkuution animointi,työpöytäruudukko,näytä ikkunat,taustakontrasti,taustan kontrasti,bling,häivytys,liike,yleiskuva,ikkunayleiskuva,saavutettavuus,esteettömyys,tiili,laatta,laatoitus,osoitin,kohdistin,hiiri,piilota osoitin,piilota kohdistin,piilota hiiri,piilota osoitin -tehoste,piilota hiiri -tehoste", + "X-KDE-Keywords[fr]": "kwin, fenêtre, gestionnaire, effet, effets 3D, effets 2D, effets graphiques, effets de bureau, animations, diverses animations, effets de gestion de fenêtres, animations, animations de bureau, rendu, effet d'aide à l'aimantage, effet de suivi de la souris, effet de flou, effet d'éclatement en morceaux, effet de surbrillance,effet de marquage de souris, effet de capture d'écran, effet de feuille, effet de vignette latérale, translucidité, effet de transparence, effet de géométrie de fenêtres, effet de fenêtres en gélatine, effet de retour au démarrage, effet parent de boite de dialogue, effet de réglage inactif, effet de fond enchaîné, plaisir des yeux, régal pour les yeux, effet de coupure de couverture,effet de cube de bureau, effet d'animation de cube de bureau, effet de grille de bureau, effet de contraste d'arrière-plan, cliquant, en fondu, mouvement, effet de vue d'ensemble de fenêtres, accessibilité, curseur, pointeur, souris, masquer le curseur, effet de masquage du pointeur, masquage du pointeur, effet de masquage de la souris, effet de masquage de la souris", + "X-KDE-Keywords[he]": "kwin,חלון,מנהל,אפקט,אפקטים תלת־ממדיים,אפקטים דו־ממדיים,אפקטים גרפיים,אפקטי שולחן עבודה,אנימציות,הנפשות,אנימציות שונות,הנפשות שונות,אפקטי ניהול חלונות,אפקט החלפת חלון,אפקט החלפת שולחן עבודה,אנימציות,הנפשות,אנימציות שולחן עבודה,הנפשות שולחן עבודה,מנהלי התקנים,הגדרות מנהל התקן,רינדור,עיבוד גרפי,אפקט עזר הצמדה,אפקט מעקב עכבר,אפקט טשטוש,אפקט עמעום,אפקט עמעום שולחן עבודה,אפקט התפרקות,אפקט גלישה,אפקט הדגשת חלון,אפקט כניסה,אפקט יציאה,אפקט מנורת קסם,אפקט הנפשת מזעור,אפקט סימון עכבר,אפקט שינוי קנה מידה,אפקט צילום מסך,אפקט גיליון,אפקט החלקה,אפקט חלונות קופצים מחליקים,אפקט תמונה ממוזערת בצד,שקיפות למחצה,אפקט שקיפות למחצה,שקיפות,אפקט גאומטריית חלון,אפקט חלונות מתנודדים,אפקטמשוב הפעלה,אפקט חלונית הורה,אפקט עמעום־לא־פעיל,אפקט עמעום־מסך,אפקט החלקה־לאחור,קישוטים,ממתק,אפקט החלפת כיסוי,אפקט קוביית שולחן עבודה,אפקט הנפשת־קוביית־שולחן־עבודה,אפקט רשת שולחן עבודה,אפקטהחלפת היפוך,אפקט הצגת חלונות,אפקט שינוי־גודל־חלון,אפקט ניגודיות רקע,נצנוץ,דהייה,תנועה,תנועה,אפקט סקירת חלונות,נגישות,אריח,אפקט עורך־אריחים,סמן,מצביע,עכבר,הסתרת סמן,אפקט הסתרת סמן,הסתרת מצביע,אפקט הסתרת מצביע,הסתרת עכבר,אפקט הסתרת עכבר, אפקט הצגת חלונות,אפקט עורך פריסה,הנפשות,הנפשה,אפקט החלפת חלונות,אפקט החלפת שולחנות עבודה,מנהלי התקנים,הגדרות מנהלי התקנים,אפקט עמעום,אפקט עמעום שולחן עבודה,אפקט התפרקות,אפקט גלישה,אפקט החלקה,אפקט הדגשת חלון,", + "X-KDE-Keywords[ia]": "kwin,window,manager,effect,3D effects,2D effects,graphical effects,desktop effects,animations,various animations,window management effects,animations,desktop animations,rendering,render,snap helper effect,track mouse effect,blur effect,fall apart effect,highlight window effect,mouse mark effect,screenshot effect,sheet effect,thumbnail aside effect,translucency,translucency effect,transparency,window geometry effect,wobbly windows effect,startup feedback effect,dialog parent effect,dim inactive effect,dim screen effect,slide back effect,eye candy,candy,cover switch effect,desktop cube effect,desktop cube animation effect,desktop grid effect,background contrast effect,bling,fading,movement,motion,overview windows effect,accessibility,cursor,pointer,mouse,hide cursor,hide cursor effect,hide pointer,hide pointer effect,hide mouse,hide mouse effect", + "X-KDE-Keywords[is]": "kwin,gluggi,stjóri,brella,3D brellur,2D brellur,sjónræn brella,skjáborðbrella,hreyfiáhrif,ýmis hreyfiáhrif,gluggastjórnunaráhrif,gluggaskiptaáhrif,skjáborðshreyfiáhrif,reklar,reklastillingar,myndgerð,myndgera,umsnúa brellu,stækkunarglersáhrif,stækkunaráhrif,smelluaðstoðaráhrif,músarrakningaráhrif,aðdráttaráhrif,móðuáhrif,dofaáhrif,deyfa skjáborð,detta í sundur,renniáhrif,lýsa upp glugga,innskráningaráhrif,útskráningaráhrif,töfralampaáhrif,hreyfiáhrif við að fela,músarmerkisáhrif,stærðaráhrif,skjámyndaráhrif,síðuáhrif,renniáhrif,renniáhrif sprettiglugga,smámyndir til hliðar,gagnsæri,gagnsæisáhrif,gegnsæi,stærð og lögun glugga,linir gluggar,endurgjöf við ræsingu,yfirgluggi svarglugga,dimma óvirka, dimma skjá,renna aftur fyrir,augnakonfekt,konfekt,sýna ramma á sekúndu,sýna málun,síðuskiptir,skjáborðsteningur,hreyfiáhrif skjáborðstenings,desktop grid effect,flip switch effect,present windows effect,resize window effect,background contrast effect,bling,fading,movement,motion,overview windows effect,accessibility,tile,tiling editor effect,bendill,mús,fela bendil,áhrif við að fela bendil,fela mús,áhrif við að fela mús", + "X-KDE-Keywords[it]": "kwin,finestra,gestore,effetto,effetti 3D,effetti 2D,effetti grafici,effetti desktop,animazioni,animazioni varie,effetti di gestione finestre,animazioni,animazioni desktop,rendering,render,effetto snap helper,effetto traccia mouse,effetto sfocatura,effetto disgregazione,effetto evidenziazione finestra, effetto marcatore mouse, effetto schermata,effetto foglio,effetto miniatura a lato,traslucenza,effetto traslucenza,trasparenza,effetto geometria finestra,effetto finestre traballanti,effetto feedback avvio,effetto finestra padre,effetto oscuramento inattivo, effetto oscuramento schermo,effetto scorrimento indietro,effetto eye candy,gradevole,effetto interruttore copertura,effetto cubo desktop,effetto animazione cubo desktop,effetto griglia del desktop,effetto contrasto sfondo,bling,dissolvenza,movimento,effetto panoramica finestre,accessibilità,cursore,puntatore,mouse,nascondi cursore,effetto nascondi cursore,nascondi puntatore,effetto nascondi puntatore,nascondi mouse,effetto nascondi mouse", + "X-KDE-Keywords[ja]": "kwin,window,manager,effect,3D effects,2D effects,graphical effects,desktop effects,animations,various animations,window management effects,animations,desktop animations,rendering,render,snap helper effect,track mouse effect,blur effect,fall apart effect,highlight window effect,mouse mark effect,screenshot effect,sheet effect,thumbnail aside effect,translucency,translucency effect,transparency,window geometry effect,wobbly windows effect,startup feedback effect,dialog parent effect,dim inactive effect,dim screen effect,slide back effect,eye candy,candy,cover switch effect,desktop cube effect,desktop cube animation effect,desktop grid effect,background contrast effect,bling,fading,movement,motion,overview windows effect,accessibility,cursor,pointer,mouse,hide cursor,hide cursor effect,hide pointer,hide pointer effect,hide mouse,hide mouse effect, ウィンドウ,マネージャー,管理,エフェクト,効果,演出,3Dエフェクト,2Dエフェクト,3D効果,2D効果,3D演出,2D演出,グラフィカルエフェクト,デスクトップエフェクト,アニメーション,動画,ウィンドウ管理エフェクト,デスクトップアニメーション,レンダリング,描画,スナップヘルパー,マウストラック,ぼかし,ブラー,バラバラ,崩壊,強調,マウスマーク,スクリーンショット,シート,サムネイル表示,半透明,透過,ウィンドウ幾何学,揺れるウィンドウ,プルプル,ふにゃふにゃ,ぐにゃぐにゃ,ぐらぐら,ふにふに,起動効果,起動演出,起動時の演出,ダイアログペアレント,非アクティブを暗く,スクリーンを暗く,スライドバック,ぬるぬる,ヌルヌル,見た目の美しさ,カバースイッチ,デスクトップキューブ,回転立方体,デスクトップグリッド,一覧,背景のコントラスト,装飾,フェーディング,動き,モーション,オーバービュー,概要,アクセシビリティ,補助,バリアフリー,カーソル,ポインター,マウス,カーソルを隠す,ポインターを隠す,マウスを隠す", + "X-KDE-Keywords[ka]": "kwin,window,manager,effect,3D effects,2D effects,graphical effects,desktop effects,animations,various animations,window management effects,window switching effect,desktop switching effect,animations,desktop animations,drivers,driver settings,rendering,render,invert effect,looking glass effect,magnifier effect,snap helper effect,track mouse effect,zoom effect,blur effect,fade effect,fade desktop effect,fall apart effect,glide effect,highlight window effect,login effect,logout effect,magic lamp effect,minimize animation effect,mouse mark effect,scale effect,screenshot effect,sheet effect,slide effect,sliding popups effect,thumbnail aside effect,translucency,translucency effect,transparency,window geometry effect,wobbly windows effect,startup feedback effect,dialog parent effect,dim inactive effect,dim screen effect,slide back effect,eye candy,candy,show FPS effect,show paint effect,cover switch effect,desktop cube effect,desktop cube animation effect,desktop grid effect,flip switch effect,present windows effect,resize window effect,background contrast effect,bling,fading,movement,motion,overview windows effect,accessibility,tile,tiling editor effect,cursor,pointer,mouse,hide cursor,hide cursor effect,hide pointer,hide pointer effect,hide mouse,hide mouse effect,ეფექტები,სამუშაო მაგიდის ეფექტები,თაგუნას დამალვა,თაგუნას დამალვის ეფექტი", + "X-KDE-Keywords[ko]": "창,관리자,효과,3D 효과,2D 효과,그래픽 효과,데스크톱 효과,애니메이션,다양한 애니메이션,창 관리 효과,애니메이션,데스크톱 애니메이션,렌더,렌더링,스냅 도우미 효과,마우스 추적 효과,블러 효과,흐림 효과,추락 효과,창 강조 효과,마우스 자취 효과,스크린샷 효과,시트 효과,섬네일 효과,투명도,불투명도,불투명도 효과,투명도 효과,창 크기 효과,흔들리는 창 효과,실행 피드백 효과,대화 상자 부모 효과,비활성 어둡게 효과,화면 어둡게 효과,아이캔디,캔디,커버 전환 효과,데스크톱 큐브 효과,바탕 화면 큐브 효과,데스크톱 큐브 애니메이션 효과,바탕 화면 애니메이션 효과,데스크톱 그리드 효과,바탕 화면 그리드 효과,페이드,이동,움직임,창 멀리 보기 효과,접근성,커서,포인터,마우스,커서 숨기기,커서 숨기기 효과,포인터 숨기기,포인터 숨기기 효과,마우스 숨기기,마우스 숨기기 효과", + "X-KDE-Keywords[lt]": "kwin,window,manager,effect,3D effects,2D effects,graphical effects,desktop effects,animations,various animations,window management effects,animations,desktop animations,rendering,render,snap helper effect,track mouse effect,blur effect,fall apart effect,highlight window effect,mouse mark effect,screenshot effect,sheet effect,thumbnail aside effect,translucency,translucency effect,transparency,window geometry effect,wobbly windows effect,startup feedback effect,dialog parent effect,dim inactive effect,dim screen effect,slide back effect,eye candy,candy,cover switch effect,desktop cube effect,desktop cube animation effect,desktop grid effect,background contrast effect,bling,fading,movement,motion,overview windows effect,accessibility,cursor,pointer,mouse,hide cursor,hide cursor effect,hide pointer,hide pointer effect,hide mouse,hide mouse effect,langų,langu,tvarkytuvė,tvarkytuve,efektas,efektai,trimačiai efektai,trimaciai efektai,3D efektai,2D efektai,dvimačiai efektai,dvimaciai efektai,grafiniai efektai,grafikos efektai,darbalaukio efektai,darbastalio efektai,animacijos,įvairios animacijos,ivairios animacijos,skirtingos animacijos,langų tvarkymo efektai,langu tvarkymo efektai,langų valdymo efektai,langu valdymo efektai,darbalaukio animacijos,darbastalio animacijos,atvaizdavimas,atvaizduoti,pritraukimo pagelbiklio efektas,pelės sekimo efektas,peles sekimo efektas,pelės sekimas,peles sekimas,suliejimo efektas,suliejimas,subyrėjimas,subyrejimas,subyrėjimo efektas,subyrejimo efektas,langų paryškinimo efektas,langu paryskinimo efektas,lango paryškinimas,lango paryskinimas,lango paryškinimo efektas,lango paryskinimo efektas,žymėjimo pele efektas,zymejimo pele efektas,žymėjimas pele,zymejimas pele,ekrano kopijos efektas,lakšto efektas,laksto efektas,lakštas,lakstas,šoninės miniatiūros efektas,sonines miniatiuros efektas,šoninė miniatiūra,sonine miniatiura,dalinis permatomumas,dalinio permatomumo efektas,permatomumas,lango geometrijos efektas,lango geometrija,svirduliuojantys langai,svirduliuojančių langų efektas,svirduliuojanciu langu efektas,paleidimo grįžtamasis ryšys,paleidimo griztamasis rysys,paleidimo grįžtamojo ryšio efektas,paleidimo griztamojo rysio efektas,viršesnis dialogo langas,virsesnis dialogo langas,viršesnio dialogo lango efektas,virsesnio dialogo lango efektas,pasyvių langų pritemdymas,pasyviu langu pritemdymas,pasyviu langu pritemdymo efektas,pasyvių langų pritemdymo efektas,ekrano pritemdymas,ekrano pritemdymo efektas,slinkimas į antrąjį planą,slinkimas i antraji plana,slinkimo į antrąjį planą efektas,slinkimo i antraji plana efektas,viršelio perjungimo efektas,virselio perjungimo efektas,darbalaukio kubas,viršelio perjungimas,virselio perjungimas,darbastalio kubas,darbalaukio kubo efektas,darbastalio kubo efektas,darbalaukio kubo animacijos efektas,darbalaukio tinklelis,darbalaukio tinklelio efektas,fono kontrasto efektas,fono kontrastas,blizgesys,išnykimas,judėjimas,isnykimas,judejimas,perkėlimas,perkelimas,judesys,apžvalga,apzvalga,langų apžvalga,langu apzvalga,langų apžvalgos efektas,langu apzvalgos efektas,prieinamumas,žymeklis,zymeklis,rodyklė,rodykle,pelė,pele,pelės,peles,slėpti žymeklį,slepti zymekli,žymeklio slėpimo efektas,zymeklio slepimo efektas,slėpti rodyklę,slepti rodykle,slėpti pelės rodyklę,slepti peles rodykle,rodyklės slėpimo efektas,rodykles slepimo efektas,slėpti pelę,slepti pele,pelės slėpimo efektas,peles slepimo efektas", + "X-KDE-Keywords[lv]": "kwin,logs,pārvaldnieks,efekts,3D efekti,2D efekti,grafiskie efekti,darbvirsmas efekti,animācijas,dažādas animācijas,logu pārvaldnieka efekti,animācijas,darbvirsmas animācijas,atveidošana,atveidot,pielipšanas palīgefekts,peles izsekošanas efekts,aizmiglošanas efekts,sabrukšanas efekts,logu izcelšanas efekts,peles atzīmēšanas efekts,ekrānattēla efekts,loksnes efekts,sīktēli uz malu efekts,caurspīdīgums,caurspīdīguma efekts,caurspīdība,logu ģeometrijas efekts,ļodzīgo logu efekts,palaišanas vizualizēšanas efekts,lodziņa vecāka efekts,aptumšot neaktīvo efekts,aptumšot ekrānu efekts,slidināt aizmugurē efekts,izgreznošana,greznumi,vāciņu pārslēgšanas efekts,darbvirsmas kuba efekts,darbvirsmas kuba animācijas efekts,darbvirsmas režģa efekts,fona kontrasta efekts,greznumi,izgaišana,pārvietošanās,kustība,pārskata logu efekts,piekļūstamība,kursors,rādītājs,pele,slēpt kursoru,kursora slēpšanas efekts,slēpt rādītāju,rādītāja slēpšanas efekts,slēpt peli,peles slēpšanas efekts", + "X-KDE-Keywords[nl]": "kwin,venster,beheerder,effect,3D effecten,2D effecten,grafische effecten,bureaubladeffecten,animaties,verschillende animaties,vensterbeheereffecten,animaties,bureaubladanimaties,renderen,render,vastklik-helper-effect,muis-volgen-effect,vervagingseffect,bureaubladvervagingseffect,uiteenvallen-effect,vensteraccentueringseffect,muismarkeringseffect,schermafdrukeffect,papiervel-effect,miniaturen-opzij-effect,doorzichtigheid,doorzichtigheidseffect,transparantie,venster-geometrie-effect,wiebelende vensterseffect,terugkoppeling-van-opstarten-effect,dialoogoudereffect,inactief-dimmen-effect,scherm-dimmen-effect,terug-glijden-effect,oogversiering,versiering,deksel-schakelaar-effect,bureaublad-kubus-effect,bureaubladkubus-animatie-effect,bureaublad-raster-effect,schakelaar-omzetten-effect,achtergrondcontrast-effect,bling,vervagen,verplaatsing,beweging,vensteroverzicht-effect,toegankelijkheid,cursor,aanwijzer,muis,cursor verbergen,cursor verbergen-effect,aanwijzer verbergen,aanwijzer verbergen-effect,muis verbergen,muis verbergen-effect", + "X-KDE-Keywords[pt_BR]": "kwin, gerenciador de janelas, efeito, efeitos 3D, efeitos 2D, efeitos gráficos, efeitos de desktop, animações, animações diversas, efeitos de gerenciamento de janelas, animações, animações de desktop, renderização, renderização, efeito auxiliar de encaixe, efeito de rastreamento do mouse, efeito de desfoque, efeito de desintegração, efeito de destaque da janela, efeito de marca do mouse, efeito de captura de tela, efeito de planilha, efeito de miniatura ao lado, translucidez, efeito de translucidez, transparência, efeito de geometria da janela, efeito de janelas trêmulas, efeito de feedback de inicialização, efeito de caixa de diálogo principal, efeito de escurecimento inativo, efeito de tela escurecida, efeito de deslizar para trás, colírio para os olhos, colírio, efeito de troca de capa, efeito de cubo da área de trabalho, efeito de animação de cubo da área de trabalho, efeito de grade da área de trabalho, efeito de contraste de fundo, brilho, desbotamento, movimento, movimento, efeito de janelas de visão geral,acessibilidade, cursor, ponteiro, mouse, ocultar cursor, efeito de ocultar cursor, ocultar ponteiro, efeito de ocultar ponteiro, ocultar mouse, efeito de ocultar mouse", + "X-KDE-Keywords[ro]": "kwin,fereastră,gestionar,efect,efecte 3D,efecte 2D,efecte grafice,efecte de birou,efecte birou,efectele biroului,animații,diverse animații,efecte de gestiune a ferestrelor,animații de birou,animații pentru birou,animații birou,randare,efect de ajutător magnetic,efect de urmărire a mausului,efect de urmărire a șoricelului,efect de încețoșare,efect de dezintegrare,efect de evidențiere a ferestrei,efect de urmă de maus,efect de urme de șoricel,efect de captură a ecranului,efect captură ecran,efect de foaie,efect de miniatură lateral,transluciditate,efect de transluciditate,transparență,efect de geometrie a ferestrei,efect de geometrie fereastră,efect de ferestre tremurătoare,efect de reacție la pornire,efect de părinte dialog,efect de părinte al dialogului,efect de întunecare inactive,efect de întunecare ecran,efect de alunecare înapoi,plăcut pentru ochi,zorzoane,efect de comutare copertă,efect cub de birou,efect de animație cub de birou,efect grilă de birou,efect contrast fundal,dispariție,mutare,mișcare,efect privire generală ferestre,efect de sumar,accesibilitate,cursor,indicator,maus,șoricel,ascunde cursorul,efect de ascundere a cursorului,ascunde indicatorul,efect de ascundere a indicatorului,ascunde mausul,ascunde șoricelul,efect ascundere maus,efect ascundere șoricel", + "X-KDE-Keywords[ru]": "kwin,window,manager,effect,3D effects,2D effects,graphical effects,desktop effects,animations,various animations,window management effects,animations,desktop animations,rendering,render,snap helper effect,track mouse effect,blur effect,fall apart effect,highlight window effect,mouse mark effect,screenshot effect,sheet effect,thumbnail aside effect,translucency,translucency effect,transparency,window geometry effect,wobbly windows effect,startup feedback effect,dialog parent effect,dim inactive effect,dim screen effect,slide back effect,eye candy,candy,cover switch effect,desktop cube effect,desktop cube animation effect,desktop grid effect,background contrast effect,bling,fading,movement,motion,overview windows effect,accessibility,cursor,pointer,mouse,hide cursor,hide cursor effect,hide pointer,hide pointer effect,hide mouse,hide mouse effect,окно,диспетчер,эффект,трёхмерные эффекты,двухмерные эффекты,графические эффекты,графика,эффекты рабочего стола,анимация,различная анимация,эффекты управления окнами,управление окнами,анимация рабочих столов,рендеринг,отрисовка,эффект разметки экрана,разметка экрана,эффект сетки на экране,сетка на экране,эффект поиска курсора мыши,поиск курсора мыши,эффект размытия,размытие,эффект распада,распад,эффект подсветки окон,подсветка окон,эффект рисования мышью,рисование мышью,эффект снимка экрана,снимок экрана,эффект листа,лист,эффект показа миниатюр окон с краю экрана,показать миниатюру окна с краю экрана,эффект полупрозрачности,полупрозрачность,эффект прозрачности,прозрачность,эффект геометрии окон,геометрия окон,эффект колышущихся окон,колыхание окон,эффект болтающихся окон,болтание окон,эффект отклика при запуске,отклик запуска приложений,эффект затемнения основного окна,затемнение основного окна,эффект затемнения неактивных окон,затемнение неактивных окон,эффект затемнения экрана,затемнение экрана,эффект соскальзывания,соскальзывание,эффект декоративности,декор,эффект карусели,карусель,эффект куба рабочих столов,куб рабочих столов,эффект анимации куба рабочих столов,анимация куба рабочих столов,эффект всех рабочих столов,все рабочие столы,эффект контраста фона,контраст фона,украшения,исчезание,движение,перемещение,эффект обзора окон,обзор окон,специальные возможности,курсор,указатель,мышь,скрыть курсор,эффект скрытия курсора,скрытие курсора,скрыть указатель,эффект скрытия указателя,скрытие указателя,скрыть мышь,эффект скрытия мыши,скрытие мыши", + "X-KDE-Keywords[sk]": "kwin,window,manager,effect,3D effects,2D effects,graphical effects,desktop effects,animations,various animations,window management effects,animations,desktop animations,rendering,render,snap helper effect,track mouse effect,blur effect,fall apart effect,highlight window effect,mouse mark effect,screenshot effect,sheet effect,thumbnail aside effect,translucency,translucency effect,transparency,window geometry effect,wobbly windows effect,startup feedback effect,dialog parent effect,dim inactive effect,dim screen effect,slide back effect,eye candy,candy,cover switch effect,desktop cube effect,desktop cube animation effect,desktop grid effect,background contrast effect,bling,fading,movement,motion,overview windows effect,accessibility,cursor,pointer,mouse,hide cursor,hide cursor effect,hide pointer,hide pointer effect,hide mouse,hide mouse effect", + "X-KDE-Keywords[sl]": "kwin,upravljalnik,okna,učinek,3D-učinki,2D-učinki,grafični učinki,učinki namizja,animacije,različne animacije,učinki upravljanja oken,animacije,animacije namizja,upodabljanje,upodobitev,učinek pomočnika za pripenjanje,učinek sledenja miški,učinek zameglitve,učinek razpadanja,učinek označenega okna,učinek sledi miške,učinek posnetka zaslona,učinek lista,učinek pomika sličic na stran,prosojnost,učinek prosojnosti,prosojnost,učinek geometrije okna,učinek majavih oken,učinek povratnih informacij ob zagonu,učinek nadrejenega pogovornega okna,učinek zatemnjene neaktivnosti,učinek zatemnjenega zaslona,učinek zdrsa nazaj,lepota za oči,sladkarija,učinek preklopa pokrova,učinek kocke namizne mize,učinek animacije kocke namizne mize,učinek mreže namizne mize,učinek kontrasta ozadja,bleščanje,bledenje,gibanje,učinek preglednih oken,dostopnost,kazalec,kazalec,miška,skrij kazalec,učinek skrivanja kazalca,skrij kazalec,učinek skrivanja kazalca,skrij miško,učinek skrivanja miške", + "X-KDE-Keywords[sv]": "kwin,fönster,hanterare,effekt,tredimensionella effekter,tvådimensionella effekter,grafiska effekter,skrivbordseffekter,animeringar,olika animeringar,fönsterhanteringseffekter,animeringar,skrivbordsanimeringar,drivrutiner,drivrutinsinställningar,återgivning,återge,låshjälpeffekt,musföljningseffekt,suddighetseffekt,fall sönder-effekt,markeringsfönstereffekt,musmarkeringseffekt,skärmbildseffekt, bladeffekt,miniatyrbild vid sidan om-effekt,genomskinlighet,genomskinlighetseffekt,transparens, fönstergeometrieffekt, ostadiga fönstereffekt,startgensvarseffekt,dialogrutors ägareffekt,dämpa inaktiva-effekt, dämpa skärmen-effekt,glid tillbaka-effekt,ögongodis,godis,visa ramar/s-effekt,visa uppritningseffekt,byte med ruta-effekt, skrivbordskubeffekt,animeringseffekt för skrivbordskub,skrivbordsrutnätseffekt,bakgrundskontrasteffekt,bling,borttoning,förflyttning,rörelse, översiktsfönstereffekt,handikappstöd,markör,pekare,mus,dölj markör,dölj marköreffekt,dölj pekare,dölj pekareffekt,dölj mus,dölj museffekt", + "X-KDE-Keywords[tr]": "kwin,pencere,yönetici,efekt,3b efektler,3d efektler,2b efektler,2d efektler,grafik efektler,masaüstü efektleri,canlandırma,animasyon,çeşitli animasyon,çeşitli canlandırma,pencere yönetimi efektleri,pencere değiştirme efektleri,masaüstü canlandırmaları,masaüstü animasyonları,sürücüler,sürücü ayarları,sunum,oluşturma,çizim,tutturma yardımcısı efekti,fareyi izle efekti,bulanıklaştırma efekti,solma efekti,masaüstü solma efekti,düşme efekti,süzülme efekti,pencereyi vurgula efekti,oturum aç efekti,oturumu kapat efekti,sihirli lamba efekti,küçültme canlandırması,fare işareti efekti,ölçek efekti,ekran görüntüsü efekti,sayfa efekti,kaydırma efekti,kayan açılır pencereler efekti,küçük görsel yana efekti,yarısaydamlık,yarısaydamlık efekti,saydamlık,pencere geometrisi efekti,sallanan pencereler efekti,başlangıç bildirimi efekti,üst iletişim kutusu efekti,etkin olmayan pencereyi karart efekti,geri kaydır efekti,janjan efekti,küp efekti,küp canlandırması,küp animasyonu,masaüstü ızgarası efekti,pencereleri sun efekti,pencereyi yeniden boyutlandır efekti,arka plan karşıtlık efekti,solma,taşıma efekti,erişilebilirlik döşemesi,döşeme düzenleyicisi,imleç,işaretçi,imleci gizle,işaretçiyi gizle,fareyi gizle", + "X-KDE-Keywords[uk]": "kwin,window,manager,effect,3D effects,2D effects,graphical effects,desktop effects,animations,various animations,window management effects,animations,desktop animations,rendering,render,snap helper effect,track mouse effect,blur effect,fall apart effect,glide effect,highlight window effect,mouse mark effect,screenshot effect,sheet effect,thumbnail aside effect,translucency,translucency effect,transparency,window geometry effect,wobbly windows effect,startup feedback effect,dialog parent effect,dim inactive effect,dim screen effect,slide back effect,eye candy,candy,cover switch effect,desktop cube effect,desktop cube animation effect,desktop grid effect,background contrast effect,bling,fading,movement,motion,overview windows effect,accessibility,cursor,pointer,mouse,hide cursor,hide cursor effect,hide pointer,hide pointer effect,hide mouse,hide mouse effect,квін,вікно,керування,ефект,просторові,плоскі,двовимірні,графічні ефекти,ефекти стільниці,анімації,різні анімації,ефекти керування вікнами,анімації,стільничні анімації,обробка,обробляти,ефект прилипання,ефект стеження за мишею,ефект масштабування,ефект розмивання,ефект підсвічування вікна,ефект входу,ефект виходу,ефект магічної лампи,ефект анімації мінімізації,ефект позначки миші,ефект знімка екрана,ефект зсування,ефект аркуша,ефект проковзування,ефект бічної мініатюри,прозорість,ефект прозорості,ефект геометрії вікна,ефект желе,ефект супроводу запуску,ефект батьківського вікна,ефект притлумення неактивного,ефект притлумлення екрана,зручність,ефект перемикання обкладинок,ефект куба стільниць,ефект анімації куба стільниць,ефект таблиці стільниць,ефект перемикання дзеркал,ефект презентації вікон,ефект зміни розмірів вікон,ефект контрастності тла,тьмянішання,пересування,курсор,вказівник,приховування вказівника,ефект приховування вказівника,приховати вказівник,миша,ефект приховування миші", + "X-KDE-Keywords[zh_CN]": "kwin,window,manager,effect,3D effects,2D effects,graphical effects,desktop effects,animations,various animations,window management effects,animations,desktop animations,rendering,render,snap helper effect,track mouse effect,zoom effect,blur effect,fall apart effect,highlight window effect,mouse mark effect,screenshot effect,sheet effect,thumbnail aside effect,translucency,translucency effect,transparency,window geometry effect,wobbly windows effect,startup feedback effect,dialog parent effect,dim inactive effect,dim screen effect,slide back effect,eye candy,candy,cover switch effect,desktop cube effect,desktop cube animation effect,desktop grid effect,background contrast effect,bling,fading,movement,motion,overview windows effect,accessibility,cursor,pointer,mouse,hide cursor,hide cursor effect,hide pointer,hide pointer effect,hide mouse,hide mouse effect,chuangkou,guanli,texiao,3D texiao,2D texiao,tuxingtexiao,zhuomiantexiao,donghua,chuangkouguanlitexiao,zhuomiandonghua,xuanran,shubiaogenzong,mohutexiao,posuitexiao,gaoliangchuangkou,shubiaobiaoji,jieping,suolvetuzhibian,toumingtexiao,yaobaichuangkou,qidongfankuitexiao,chengxuqidongdongxiao,andanduihuakuangtexiao,andanpingmutexiao,shijuexiaoguo,xianshi FPS texiao,xianshihuizhiquyutexiao,fengmianqiehuantexiao,zhuomianlifangtitexiao,zhuomianlifangtidonghuatexiao,zhanshichuangkoutexiao,tiaozhengchuangkoudaxiaotexiao,beijingduibitexiao,shanguang,shanyao,shandong,xiaoshi,jianbian,jianru,jianchu,dongtai,zonglan,gailan,chuangkougailantexiao,wuzhangai,fuzhugongneng,wuzhangaifuzhu,citie,pintie,cizhuan,tukuai,citiebianjiqixiaoguo,guangbiao,zhizhen,shubiao,yincangguangbiao,yincangguangbiaoxiaoguo,yincangzhizhen,yincangzhizhenxiaoguo,yincangshubiao,yincangshubiaoxiaoguo,窗口,管理,特效,3D 特效,2D 特效,图形特效,桌面特效,动画,窗口管理特效,桌面动画,渲染,鼠标跟踪,模糊特效,破碎特效,高亮窗口,鼠标标记,截屏,缩略图置边,透明特效,摇摆窗口,启动反馈特效,程序启动动效,黯淡对话框特效,黯淡屏幕特效,视觉效果,封面切换特效,桌面立方体特效,桌面立方体动画特效,背景对比特效,闪光,闪耀,闪动,消失,渐变,渐入,渐出,动态,总览,概览,窗口概览特效,无障碍,辅助功能,无障碍辅助,光标,指针,鼠标,隐藏光标,隐藏光标效果,隐藏指针,隐藏指针效果,隐藏鼠标,隐藏鼠标效果", + "X-KDE-Keywords[zh_TW]": "kwin,視窗,視窗管理員,效果,3D 效果,2D 效果,圖形效果,桌面效果,動畫,各種動畫,視窗管理效果,桌面動畫,渲染,繪製,貼齊助手效果,追蹤滑鼠效果,模糊效果,粉碎效果,突顯視窗效果,滑鼠標記效果,螢幕截圖效果,薄紙效果,縮圖在一旁效果,透明效果,透明,視窗幾何效果,擺動視窗效果,啟動回饋效果,對話框上層效果,暗化非作用中效果,暗化螢幕效果,滑動到後方效果,裝飾,封面切換效果,桌面方塊效果,桌面方塊動畫效果,桌面格線效果,背景對比效果,淡化,移動,總覽效果,無障礙使用,游標,指標,滑鼠,隱藏游標,隱藏游標效果,隱藏指標,隱藏指標效果,隱藏滑鼠,隱藏滑鼠效果", + "X-KDE-OnlyShowOnQtPlatforms": [ + "wayland" + ], + "X-KDE-System-Settings-Parent-Category": "windowmanagement", + "X-KDE-Weight": 30 +} diff --git a/local/recipes/kde/kwin/source/src/kcms/effects/kwineffect.knsrc b/local/recipes/kde/kwin/source/src/kcms/effects/kwineffect.knsrc new file mode 100644 index 0000000000..fedb0701c5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/effects/kwineffect.knsrc @@ -0,0 +1,65 @@ +[KNewStuff3] +Name=Desktop Effects +Name[ar]=تأثيرات سطح المكتب +Name[az]=İş masası effektləri +Name[be]=Эфекты працоўнага стала +Name[bg]=Ефекти на работния плот +Name[bs]=Efekti površi +Name[ca]=Efectes d'escriptori +Name[ca@valencia]=Efectes de l'escriptori +Name[cs]=Efekty na ploše +Name[da]=Skrivebordseffekter +Name[de]=Arbeitsflächen-Effekte +Name[el]=Εφέ επιφάνειας εργασίας +Name[en_GB]=Desktop Effects +Name[eo]=Labortablo-Efikoj +Name[es]=Efectos del escritorio +Name[et]=Töölauaefektid +Name[eu]=Mahaigaineko efektuak +Name[fi]=Työpöytätehosteet +Name[fr]=Effets de bureau +Name[gl]=Efectos do escritorio +Name[he]=אפקטים של שולחן עבודה +Name[hu]=Asztali effektusok +Name[ia]=Effectos de scriptorio +Name[id]=Efek Desktop +Name[is]=Skjáborðsbrellur +Name[it]=Effetti del desktop +Name[ja]=デスクトップ効果 +Name[ka]=სამუშაო მაგიდის ეფექტები +Name[ko]=데스크톱 효과 +Name[lt]=Darbalaukio efektai +Name[lv]=Darbvirsmas efekti +Name[nb]=Skrivebordseffekter +Name[nds]=Schriefdischeffekten +Name[nl]=Bureaubladeffecten +Name[nn]=Skrivebords­effektar +Name[pa]=ਡੈਸਕਟਾਪ ਪਰਭਾਵ +Name[pl]=Efekty pulpitu +Name[pt]=Efeitos do Ecrã +Name[pt_BR]=Efeitos da área de trabalho +Name[ro]=Efecte de birou +Name[ru]=Эффекты +Name[sa]=डेस्कटॉप प्रभाव +Name[se]=Čállinbeavdeeffeavttat +Name[sk]=Efekty plochy +Name[sl]=Učinki namizja +Name[sr]=Ефекти површи +Name[sr@ijekavian]=Ефекти површи +Name[sr@ijekavianlatin]=Efekti površi +Name[sr@latin]=Efekti površi +Name[sv]=Skrivbordseffekter +Name[ta]=பணிமேடை அசைவூட்டங்கள் +Name[tg]=Таъсирҳои мизи корӣ +Name[tr]=Masaüstü Efektleri +Name[uk]=Ефекти стільниці +Name[vi]=Hiệu ứng bàn làm việc +Name[zh_CN]=桌面特效 +Name[zh_TW]=桌面效果 + +ProvidersUrl=https://autoconfig.kde.org/ocs/providers.xml +ContentWarning=Executables +Categories=KWin Effects Plasma 6 +StandardResource=tmp +Uncompress=kpackage +KPackageStructure=KWin/Effect diff --git a/local/recipes/kde/kwin/source/src/kcms/effects/ui/Effect.qml b/local/recipes/kde/kwin/source/src/kcms/effects/ui/Effect.qml new file mode 100644 index 0000000000..0b3fdb2bac --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/effects/ui/Effect.qml @@ -0,0 +1,117 @@ +/* + SPDX-FileCopyrightText: 2013 Antonis Tsiapaliokas + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + SPDX-FileCopyrightText: 2023 ivan tkachenko + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami +import org.kde.kcmutils as KCM + +QQC2.ItemDelegate { + id: listItem + + hoverEnabled: true + + onClicked: { + if (ListView.isCurrentItem) { + // Collapse list item + ListView.view.currentIndex = -1; + } else { + // Expand list item + ListView.view.currentIndex = index; + } + } + + contentItem: RowLayout { + spacing: Kirigami.Units.smallSpacing + + QQC2.RadioButton { + readonly property bool _exclusive: model.ExclusiveRole != "" + property bool _toggled: false + + checked: model.StatusRole + visible: _exclusive + QQC2.ButtonGroup.group: _exclusive ? effectsList.findButtonGroup(model.ExclusiveRole) : null + + onToggled: { + model.StatusRole = checked ? Qt.Checked : Qt.Unchecked; + _toggled = true; + } + + onClicked: { + // Uncheck the radio button if it's clicked. + if (checked && !_toggled) { + model.StatusRole = Qt.Unchecked; + } + _toggled = false; + } + + KCM.SettingHighlighter { + highlight: model.EnabledByDefaultFunctionRole ? parent.checkState !== Qt.PartiallyChecked : parent.checked !== model.EnabledByDefaultRole + } + } + + QQC2.CheckBox { + checkState: model.StatusRole + visible: model.ExclusiveRole == "" + + onToggled: model.StatusRole = checkState + + KCM.SettingHighlighter { + highlight: model.EnabledByDefaultFunctionRole ? parent.checkState !== Qt.PartiallyChecked : parent.checked !== model.EnabledByDefaultRole + } + } + + ColumnLayout { + Layout.topMargin: Kirigami.Units.smallSpacing + Layout.bottomMargin: Kirigami.Units.smallSpacing + Layout.rightMargin: configureButton.visible ? undefined : parent.spacing + configureButton.implicitWidth + spacing: 0 + + Kirigami.Heading { + Layout.fillWidth: true + + level: 5 + text: model.NameRole + wrapMode: Text.Wrap + } + + QQC2.Label { + Layout.fillWidth: true + + text: model.DescriptionRole + opacity: listItem.hovered ? 0.8 : 0.6 + wrapMode: Text.Wrap + } + + QQC2.Label { + id: aboutItem + + Layout.fillWidth: true + + text: i18n("Author: %1\nLicense: %2", model.AuthorNameRole, model.LicenseRole) + opacity: listItem.hovered ? 0.8 : 0.6 + visible: listItem.ListView.isCurrentItem + wrapMode: Text.Wrap + } + } + + QQC2.ToolButton { + id: configureButton + visible: model.ConfigurableRole + enabled: model.StatusRole != Qt.Unchecked + icon.name: "configure" + text: i18nc("@info:tooltip", "Configure…") + display: QQC2.AbstractButton.IconOnly + QQC2.ToolTip.text: text + QQC2.ToolTip.visible: hovered + onClicked: kcm.configure(model.ServiceNameRole, listItem) + } + } +} diff --git a/local/recipes/kde/kwin/source/src/kcms/effects/ui/main.qml b/local/recipes/kde/kwin/source/src/kcms/effects/ui/main.qml new file mode 100644 index 0000000000..1410d17458 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/effects/ui/main.qml @@ -0,0 +1,113 @@ +/* + SPDX-FileCopyrightText: 2013 Antonis Tsiapaliokas + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + SPDX-FileCopyrightText: 2023 ivan tkachenko + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts + +import org.kde.kcmutils +import org.kde.config +import org.kde.kirigami as Kirigami +import org.kde.newstuff as NewStuff + +import org.kde.private.kcms.kwin.effects as Private + +ScrollViewKCM { + implicitHeight: Kirigami.Units.gridUnit * 30 + implicitWidth: Kirigami.Units.gridUnit * 40 + + actions: NewStuff.Action { + text: i18nc("@action:button get new KWin effects", "Get New…") + visible: KAuthorized.authorize(KAuthorized.GHNS) + configFile: "kwineffect.knsrc" + onEntryEvent: (entry, event) => { + if (event === NewStuff.Engine.StatusChangedEvent) { + kcm.onGHNSEntriesChanged() + } + } + } + + header: RowLayout { + spacing: Kirigami.Units.smallSpacing + + Kirigami.SearchField { + id: searchField + + Layout.fillWidth: true + } + + QQC2.ToolButton { + id: filterButton + + icon.name: "view-filter" + + checkable: true + checked: menu.opened + onClicked: menu.popup(filterButton, filterButton.width - menu.width, filterButton.height) + + QQC2.ToolTip { + text: i18n("Configure Filter") + } + } + + QQC2.Menu { + id: menu + + modal: true + + QQC2.MenuItem { + checkable: true + checked: searchModel.excludeUnsupported + text: i18n("Exclude unsupported effects") + + onToggled: searchModel.excludeUnsupported = checked + } + } + } + + view: ListView { + id: effectsList + + // { string name: QQC2.ButtonGroup group } + property var _buttonGroups: new Map() + + clip: true + + model: Private.EffectsFilterProxyModel { + id: searchModel + + query: searchField.text + sourceModel: kcm.effectsModel + } + + delegate: Effect { + width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin + } + + section.property: "CategoryRole" + section.delegate: Kirigami.ListSectionHeader { + width: ListView.view.width - ListView.view.leftMargin - ListView.view.rightMargin + text: section + } + + Component { + id: buttonGroupComponent + + QQC2.ButtonGroup {} + } + + function findButtonGroup(name: string): QQC2.ButtonGroup { + let group = _buttonGroups.get(name); + if (group === undefined) { + group = buttonGroupComponent.createObject(this); + _buttonGroups.set(name, group); + } + return group; + } + } +} diff --git a/local/recipes/kde/kwin/source/src/kcms/options/AUTHORS b/local/recipes/kde/kwin/source/src/kcms/options/AUTHORS new file mode 100644 index 0000000000..745e9eefac --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/AUTHORS @@ -0,0 +1,12 @@ +Please use https://bugs.kde.org to report bugs. +The following authors may have retired by the time you read this :-) + +KWM Configuration Module: + + Pat Dowler (dowler@pt1B1106.FSH.UVic.CA) + + Bernd Wuebben + +Conversion to kcontrol applet: + + Matthias Hoelzer (hoelzer@physik.uni-wuerzburg.de) diff --git a/local/recipes/kde/kwin/source/src/kcms/options/CMakeLists.txt b/local/recipes/kde/kwin/source/src/kcms/options/CMakeLists.txt new file mode 100644 index 0000000000..17b95ba986 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/CMakeLists.txt @@ -0,0 +1,35 @@ +########### next target ############### +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcmkwm\") + +set(kcm_kwinoptions_PART_SRCS + main.cpp + mouse.cpp + windows.cpp +) + +ki18n_wrap_ui(kcm_kwinoptions_PART_SRCS + actions.ui + advanced.ui + focus.ui + mouse.ui + moving.ui + pip.ui +) + +kcmutils_generate_module_data( + kcm_kwinoptions_PART_SRCS + MODULE_DATA_HEADER kwinoptionsdata.h + MODULE_DATA_CLASS_NAME KWinOptionsData + SETTINGS_HEADERS kwinoptions_settings.h kwinoptions_kdeglobals_settings.h + SETTINGS_CLASSES KWinOptionsSettings KWinOptionsKDEGlobalsSettings +) + +kconfig_add_kcfg_files(kcm_kwinoptions_PART_SRCS kwinoptions_settings.kcfgc GENERATE_MOC) +kconfig_add_kcfg_files(kcm_kwinoptions_PART_SRCS kwinoptions_kdeglobals_settings.kcfgc GENERATE_MOC) + +qt_add_dbus_interface(kcm_kwinoptions_PART_SRCS ${KWin_SOURCE_DIR}/src/org.kde.kwin.Effects.xml kwin_effects_interface) + +kcoreaddons_add_plugin(kcm_kwinoptions SOURCES ${kcm_kwinoptions_PART_SRCS} INSTALL_NAMESPACE "plasma/kcms/systemsettings_qwidgets") +kcmutils_generate_desktop_file(kcm_kwinoptions) +target_link_libraries(kcm_kwinoptions kwin Qt::DBus KF6::KCMUtils KF6::I18n KF6::WindowSystem) diff --git a/local/recipes/kde/kwin/source/src/kcms/options/ChangeLog b/local/recipes/kde/kwin/source/src/kcms/options/ChangeLog new file mode 100644 index 0000000000..1767cdecba --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/ChangeLog @@ -0,0 +1,51 @@ +1999-03-06 Mario Weilguni + + * changes for Qt 2.0 + +1998-11-29 Alex Zepeda + + * pics/Makefile.am, pics/mini/Makefile.am: Install icons from their + "proper" subdirectories. + +1998-11-20 Cristian Tibirna + + * advanced.[cpp,h]: fixed bugs. Mostly a disgusting one: + no lists saving for the special options (Decor, Focus a.o.) + +1998-11-09 Cristian Tibirna + + * advanced.[cpp,h] : new tab for some of the last of the + kwm's options which remained out of the GUI config: + CtrlTab, TraverseAll, AltTabeMode, Button3Grab and + the filter lists for decorations, focus, stickiness, + session management ignore ( I kinda disklike the solution + I got for the latest) + +1998-11-06 Cristian Tibirna + + * titlebar.[cpp,h] : added title alignment config + +1998-10-23 Cristian Tibirna + + * titlebar.cpp: completed what Matthias started (took out + useless checks) + * widows.cpp: make autoRaise toggling clearer + +1998-10-22 Matthias Ettrich + + * titlebar.cpp: less options on titlebar doubleclick + +1998-10-21 Cristian Tibirna + + * desktop.[cpp,h]: now with consistent layout use + resizeEvent() deleted + +1998-10-19 Cristian Tibirna + + * windows.[cpp,h]: now with consistent layout use + resizeEvent() deleted + +1998-10-18 Cristian Tibirna + + * titlebar.[cpp,h]: fixed the (in)activetitleebar pixmap selection + 1998-10-21 (still buggy, don't quite understand why) diff --git a/local/recipes/kde/kwin/source/src/kcms/options/Messages.sh b/local/recipes/kde/kwin/source/src/kcms/options/Messages.sh new file mode 100644 index 0000000000..e0a423e3cc --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/Messages.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -name \*.ui` >> rc.cpp || exit 11 +$XGETTEXT *.cpp -o $podir/kcmkwm.pot diff --git a/local/recipes/kde/kwin/source/src/kcms/options/actions.ui b/local/recipes/kde/kwin/source/src/kcms/options/actions.ui new file mode 100644 index 0000000000..c695860f15 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/actions.ui @@ -0,0 +1,553 @@ + + + KWinActionsConfigForm + + + + 0 + 0 + 600 + 500 + + + + + + + true + + + Inactive Inner Window Actions + + + + Qt::AlignHCenter|Qt::AlignTop + + + + + &Left click: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandWindow1 + + + + + + + In this row you can customize left click behavior when clicking into an inactive inner window ('inner' means: not titlebar, not frame). + + + + Activate, pass click and raise on release + + + + + Activate, raise and pass click + + + + + Activate and pass click + + + + + Activate + + + + + Activate and raise + + + + + + + + &Middle click: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandWindow2 + + + + + + + In this row you can customize middle click behavior when clicking into an inactive inner window ('inner' means: not titlebar, not frame). + + + + Activate, raise and pass click + + + + + Activate and pass click + + + + + Activate + + + + + Activate and raise + + + + + + + + &Right click: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandWindow3 + + + + + + + In this row you can customize right click behavior when clicking into an inactive inner window ('inner' means: not titlebar, not frame). + + + + Activate, raise and pass click + + + + + Activate and pass click + + + + + Activate + + + + + Activate and raise + + + + + + + + Mouse &wheel: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandWindowWheel + + + + + + + In this row you can customize behavior when scrolling into an inactive inner window ('inner' means: not titlebar, not frame). + + + + Scroll + + + + + Activate and scroll + + + + + Activate, raise and scroll + + + + + + + + + + + true + + + Inner Window, Titlebar and Frame Actions + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + Mo&difier key: + + + kcfg_CommandAllKey + + + + + + + Here you select whether holding the Meta key or Alt key will allow you to perform the following actions. + + + + Meta + + + + + Alt + + + + + + + + + + + 0 + 0 + + + + + 24 + 0 + + + + + + + + Qt::AlignCenter + + + + + + + + + L&eft click: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandAll1 + + + + + + + In this row you can customize left click behavior when clicking into the titlebar or the frame. + + + + Move + + + + + Activate, raise and move + + + + + Toggle raise and lower + + + + + Resize + + + + + Raise + + + + + Lower + + + + + Minimize + + + + + Decrease opacity + + + + + Increase opacity + + + + + Window menu + + + + + Do nothing + + + + + + + + Middle &click: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandAll2 + + + + + + + In this row you can customize middle click behavior when clicking into the titlebar or the frame. + + + + Move + + + + + Activate, raise and move + + + + + Toggle raise and lower + + + + + Resize + + + + + Raise + + + + + Lower + + + + + Minimize + + + + + Decrease opacity + + + + + Increase opacity + + + + + Window menu + + + + + Do nothing + + + + + + + + Right clic&k: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandAll3 + + + + + + + In this row you can customize right click behavior when clicking into the titlebar or the frame. + + + + Move + + + + + Activate, raise and move + + + + + Toggle raise and lower + + + + + Resize + + + + + Raise + + + + + Lower + + + + + Minimize + + + + + Decrease opacity + + + + + Increase opacity + + + + + Window menu + + + + + Do nothing + + + + + + + + Mo&use wheel: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandAllWheel + + + + + + + Here you can customize KDE's behavior when scrolling with the mouse wheel in a window while pressing the modifier key. + + + + Raise/lower + + + + + Maximize/restore + + + + + Keep above/below + + + + + Move to previous/next desktop + + + + + Change opacity + + + + + Do nothing + + + + + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/kcms/options/advanced.ui b/local/recipes/kde/kwin/source/src/kcms/options/advanced.ui new file mode 100644 index 0000000000..beddb5b00b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/advanced.ui @@ -0,0 +1,117 @@ + + + KWinAdvancedConfigForm + + + + 0 + 0 + 1001 + 297 + + + + + Qt::AlignHCenter|Qt::AlignTop + + + + + Window &placement: + + + kcfg_Placement + + + + + + + <html><head/><body><p>The placement policy determines where a new window will appear on the desktop.</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">Smart</span> will try to achieve a minimum overlap of windows</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">Maximizing</span> will try to maximize every window to fill the whole screen. It might be useful to selectively affect placement of some windows using the window-specific settings.</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">Random</span> will use a random position</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">Centered</span> will place the window centered</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">Zero-cornered</span> will place the window in the top-left corner</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">Under mouse</span> will place the window under the pointer</li></ul></body></html> + + + + Minimal Overlapping + + + + + Maximized + + + + + Random + + + + + Centered + + + + + In Top-Left Corner + + + + + Under mouse + + + + + + + + When turned on, apps which are able to remember the positions of their windows are allowed to do so. This will override the window placement mode defined above. + + + Allow apps to remember the positions of their own windows, if they support it + + + + + + + Virtual Desktop behavior: + + + kcfg_ActivationDesktopPolicy + + + + + + + When activating a window on a different Virtual Desktop: + + + + + + + <html><head/><body><p>This setting controls what happens when an open window located on a Virtual Desktop other than the current one is activated. </p><p><span style=" font-style:italic;">Switch to that Virtual Desktop</span> will switch to the Virtual Desktop where the window is currently located. </p><p><span style=" font-style:italic;">Bring window to current Virtual Desktop</span> will cause the window to jump to the active Virtual Desktop. </p></body></html> + + + + Switch to that Virtual Desktop + + + + + Bring window to current Virtual Desktop + + + + + Do nothing + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/kcms/options/focus.ui b/local/recipes/kde/kwin/source/src/kcms/options/focus.ui new file mode 100644 index 0000000000..5b091a20eb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/focus.ui @@ -0,0 +1,284 @@ + + + KWinFocusConfigForm + + + + 0 + 0 + 600 + 500 + + + + + + + Qt::AlignHCenter|Qt::AlignTop + + + + + Window &activation policy: + + + windowFocusPolicy + + + + + + + With this option you can specify how and when windows will be focused. + + + + Click to focus + + + + + Click to focus (mouse precedence) + + + + + Focus follows mouse + + + + + Focus follows mouse (mouse precedence) + + + + + Focus under mouse + + + + + Focus strictly under mouse + + + + + + + + &Delay focus by: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_DelayFocusInterval + + + + + + + This is the delay after which the window the mouse pointer is over will automatically receive focus. + + + ms + + + 0 + + + 3000 + + + 100 + + + + + + + Focus &stealing prevention: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_FocusStealingPreventionLevel + + + + + + + <html><head/><body><p>This option specifies how much KWin will try to prevent unwanted focus stealing caused by unexpected activation of new windows. (Note: This feature does not work with the <span style=" font-style:italic;">Focus under mouse</span> or <span style=" font-style:italic;">Focus strictly under mouse</span> focus policies.) </p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">None:</span> Prevention is turned off and new windows always become activated.</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">Low:</span> Prevention is enabled; when some window does not have support for the underlying mechanism and KWin cannot reliably decide whether to activate the window or not, it will be activated. This setting may have both worse and better results than the medium level, depending on the applications.</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">Medium:</span> Prevention is enabled.</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">High:</span> New windows get activated only if no window is currently active or if they belong to the currently active application. This setting is probably not really usable when not using mouse focus policy.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-style:italic;">Extreme:</span> All windows must be explicitly activated by the user.</li></ul><p>Windows that are prevented from stealing focus are marked as demanding attention, which by default means their taskbar entry will be highlighted. This can be changed in the Notifications control module.</p></body></html> + + + + None + + + + + Low + + + + + Medium + + + + + High + + + + + Extreme + + + + + + + + Raising windows: + + + + + + + When this option is enabled, the active window will be brought to the front when you click somewhere into the window contents. To change it for inactive windows, you need to change the settings in the Actions tab. + + + &Click raises active window + + + + + + + + + When this option is enabled, a window in the background will automatically come to the front when the mouse pointer has been over it for some time. + + + &Raise on hover, delayed by: + + + + + + + false + + + This is the delay after which the window that the mouse pointer is over will automatically come to the front. + + + ms + + + 0 + + + 3000 + + + 100 + + + + + + + + + Multiscreen behavior: + + + + + + + When this option is enabled, focus operations are limited only to the active screen + + + &Separate screen focus + + + + + + + + 280 + 0 + + + + Window activation policy description + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + kcfg_AutoRaise + toggled(bool) + kcfg_AutoRaiseInterval + setEnabled(bool) + + + 338 + 189 + + + 485 + 189 + + + + + kcfg_AutoRaise + toggled(bool) + kcfg_ClickRaise + setDisabled(bool) + + + 338 + 189 + + + 333 + 155 + + + + + diff --git a/local/recipes/kde/kwin/source/src/kcms/options/kcm_kwinoptions.json b/local/recipes/kde/kwin/source/src/kcms/options/kcm_kwinoptions.json new file mode 100644 index 0000000000..5262756ffe --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/kcm_kwinoptions.json @@ -0,0 +1,137 @@ +{ + "Categories": "Qt;KDE;X-KDE-settings-looknfeel;", + "KPlugin": { + "BugReportUrl": "https://bugs.kde.org/enter_bug.cgi?product=systemsettings&component=kcm_kwinoptions", + "Description": "Configure window actions and behavior", + "Description[ar]": "اضبط سلوك النوافذ وإجراءاتها", + "Description[az]": "Pəncərə fəaliyyətləri və davranışlarını tənzimləyin", + "Description[be]": "Наладжванне дзеянняў і паводзін акон", + "Description[bg]": "Настройки на поведение и действия на прозорците", + "Description[ca@valencia]": "Configura les accions i comportament de les finestres", + "Description[ca]": "Configura les accions i comportament de les finestres", + "Description[cs]": "Nastavte činností a chování oken", + "Description[da]": "Konfigurér vindueshandlinger og -opførsel", + "Description[de]": "Fenster-Aktionen und -verhalten einrichten", + "Description[en_GB]": "Configure window actions and behaviour", + "Description[eo]": "Agordi fenestrajn agojn kaj konduton", + "Description[es]": "Configurar las acciones y el comportamiento de las ventanas", + "Description[et]": "Akende toimingute ja käitumise seadistamine", + "Description[eu]": "Konfiguratu leihoaren ekintzak eta jokabidea", + "Description[fi]": "Ikkunoiden toiminnan asetukset", + "Description[fr]": "Configurer les actions et le comportement des fenêtres", + "Description[gl]": "Configurar o comportamento e as accións das xanelas.", + "Description[he]": "הגדרות פעולות והתנהגות חלון", + "Description[hu]": "Az ablakműveletek és -működés beállítása", + "Description[ia]": "Cnfigura comportamento e actiones de fenestra", + "Description[id]": "Konfigurasikan perilaku dan aksi jendela", + "Description[is]": "Grunnstilla aðgerðir og hegðun glugga", + "Description[it]": "Configura azioni e comportamento delle finestre", + "Description[ja]": "ウィンドウのアクションと挙動を設定", + "Description[ka]": "ფანჯრის ქმედებისა და ქცევის მორგება", + "Description[ko]": "창 동작과 행동 설정", + "Description[lt]": "Konfigūruoti langų elgseną ir veiksmus su jais", + "Description[lv]": "Konfigurēt logu darbības un uzvedību", + "Description[nb]": "Sett opp utseende og atferd for vindu", + "Description[nl]": "Vensteracties en gedrag configureren", + "Description[nn]": "Set opp utsjånad og åtferd for vindauge", + "Description[pl]": "Ustawienia działań i zachowań okien", + "Description[pt]": "Configurar as acções e comportamento das janelas", + "Description[pt_BR]": "Configure as ações e comportamento das janelas", + "Description[ro]": "Configurează acțiunile și comportamentul ferestrelor", + "Description[ru]": "Настройка поведения окон", + "Description[sa]": "विण्डो क्रियाः व्यवहारः च विन्यस्यताम्", + "Description[sk]": "Nastaviť akcie a správanie okien", + "Description[sl]": "Prilagodite dejavnosti in vedenje oken", + "Description[sv]": "Anpassa fönsteråtgärder och beteende", + "Description[ta]": "சாளர நடத்தையையும் செயல்களையும் அமையுங்கள்", + "Description[tr]": "Pencere eylemlerini ve davranışını yapılandır", + "Description[uk]": "Налаштовування реакції та поведінки вікон", + "Description[vi]": "Cấu hình các hành động và ứng xử của cửa sổ", + "Description[zh_CN]": "配置窗口操作和行为", + "Description[zh_TW]": "設定視窗動作與行為", + "Icon": "preferences-system-windows-actions", + "Name": "Window Behavior", + "Name[ar]": "سلوك النوافذ", + "Name[az]": "Pəncərə davranışı", + "Name[be]": "Паводзіны акон", + "Name[bg]": "Поведение на прозорците", + "Name[ca@valencia]": "Comportament de les finestres", + "Name[ca]": "Comportament de les finestres", + "Name[cs]": "Chování oken", + "Name[da]": "Vinduesopførsel", + "Name[de]": "Fensterverhalten", + "Name[en_GB]": "Window Behaviour", + "Name[eo]": "Fenestra Konduto", + "Name[es]": "Comportamiento de las ventanas", + "Name[et]": "Akende käitumine", + "Name[eu]": "Leihoen jokabidea", + "Name[fi]": "Ikkunoiden toiminta", + "Name[fr]": "Comportement des fenêtres", + "Name[gl]": "Comportamento das xanelas", + "Name[he]": "התנהגות חלון", + "Name[hu]": "Ablakműködés", + "Name[ia]": "Comportamento de fenestra", + "Name[id]": "Perilaku Jendela", + "Name[is]": "Gluggahegðun", + "Name[it]": "Comportamento delle finestre", + "Name[ja]": "ウィンドウの挙動", + "Name[ka]": "ფანჯრის ქცევა", + "Name[ko]": "창 동작", + "Name[lt]": "Langų elgsena", + "Name[lv]": "Logu uzvedība", + "Name[nb]": "Vindusatferd", + "Name[nl]": "Venstergedrag", + "Name[nn]": "Vindaugs­åtferd", + "Name[pl]": "Zachowania okien", + "Name[pt]": "Comportamento das Janelas", + "Name[pt_BR]": "Comportamento das janelas", + "Name[ro]": "Comportament ferestre", + "Name[ru]": "Поведение окон", + "Name[sa]": "खिडकी व्यवहार", + "Name[sk]": "Správanie okien", + "Name[sl]": "Vedenje oken", + "Name[sv]": "Fönsterbeteende", + "Name[ta]": "சாளர நடத்தை", + "Name[tr]": "Pencere Davranışı", + "Name[uk]": "Поведінка вікон", + "Name[vi]": "Ứng xử của cửa sổ", + "Name[zh_CN]": "窗口行为", + "Name[zh_TW]": "視窗行為" + }, + "X-DocPath": "kcontrol/windowbehaviour/index.html", + "X-KDE-Keywords": "focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick,desktop,snap,movement,activation,activate,pip,picture in picture,picture-in-picture", + "X-KDE-Keywords[ar]": "التركيز,الموضع,سلوك النافذة,إجراءات النافذة,الرسوم المتحركة,الرفع,الرفع الآليّ,النوافذ,الإطار,شريط العنوان,النقر المزدوج,سطح المكتب,الالتقاط,الحركة,نشط,التنشيط,صورة في صورة,pip", + "X-KDE-Keywords[bg]": "фокус,разположение,поведение на прозореца,действия на прозореца,анимация,повдигане,автоматично повдигане,прозорци,рамка,заглавна лента,двойно щракване,работен плот,щракване,движение,активиране,активиране", + "X-KDE-Keywords[ca@valencia]": "focus,emplaçament,comportament de les finestres,accions de les finestres,animació,elevació,elevació automàtica,finestres,marc,barra de títol,doble clic,escriptori,ajusta,moviment,activació,activa,pip,imatge en la imatge", + "X-KDE-Keywords[ca]": "focus,emplaçament,comportament de les finestres,accions de les finestres,animació,elevació,elevació automàtica,finestres,marc,barra de títol,doble clic,escriptori,ajusta,moviment,activació,activa,pip,imatge en la imatge", + "X-KDE-Keywords[de]": "Aktivierung,Fokus,Platzierung,Fensterverhalten,Fensteraktionen,Animation,Nach vorne,Automatisch nach vorne,Fenster,Rahmen,Titelleiste,Doppelklick,Arbeitsfläche,Einrasten,Verschiebung,Bewegung,Aktivierung,aktivieren,pip,bib,Bild in Bild,Bild-in-Bild", + "X-KDE-Keywords[es]": "foco,colocación,ubicación,situación,comportamiento de la ventana,acciones de la ventana,animación,elevar,primer plano,elevar automáticamente,pasar a primer plano,ventanas,marco,borde,barra de título,doble clic,escritorio,ajuste,movimiento,imagen sobre imagen,imagen dentro de imagen", + "X-KDE-Keywords[eu]": "fokua,arreta,jartzea,leihoaren jokabidea,leihoaren ekintzak,animazioa,altxatu,automatikoki altxatu,leihoak,markoa,fotograma,titulu-barra,klik bikoitza,mahaigaina,atxiki,mugimendua,aktibazioa,aktibatu,pip,picture in picture,picture-in-picture,irudia irudian,irudia-irudian", + "X-KDE-Keywords[fi]": "kohdistus,kohdistaminen,fokus,sijoittaminen,sijoitus,ikkunan toiminta,ikkunatoiminnot,animointi,animaatio,nosta,automaattinosto,nostaminen,kehys,otsikkorivi,otsikkopalkki,kaksoisnapsautus,kaksoisnapsauttaminen,työpöytä,kiinnitä,kiinnitys,liike,siirtyminen,aktivoi,aktivointi,aktivaatio,kuva kuvassa,pip", + "X-KDE-Keywords[fr]": "focus, placement, comportement des fenêtres, actions des fenêtres, animation, relance, relance automatique, fenêtres, cadre, barre de titre, double-clic, bureau, aimantage, mouvement,activation, activer, pip, image dans l'image, image-dans-image", + "X-KDE-Keywords[he]": "מיקוד,הצבה,מיקום,מקום,התנהגות חלון,פעולות חלון,הנפשה,הגבהה,הרמה,הגבהה אוטומטית,הרמה אוטומטית,חלונות,מסגרת,שורת כותרת,לחיצה כפולה,דאבלקליק,דאבל קליק,הצמדה,תנועה,תזוזה,הפעלה,מופעל,פועל,פעיל,פעילות,פיפ,תמונה בתוך תמונה,תמונה-בתוך-תמונה,תב\"ת,תב״ת", + "X-KDE-Keywords[ia]": "focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick,desktop,snap,movement,activation,activate,pip,picture in picture,picture-in-picture", + "X-KDE-Keywords[is]": "fókus,staðsetning,gluggahegðun,gluggaaðgerðir,hreyfiáhrif,setja fremst,setjasjálfkrafa fremst,gluggar,rammi,titilstika,tvísmella,skjáborð,smella saman,hreyfing,virkjun,virkja,pip,mynd í mynd, mynd-í-mynd", + "X-KDE-Keywords[it]": "fuoco,posizionamento,comportamento della finestra,azioni delle finestre,animazione,sollevamento,sollevamento automatico,finestre,riquadro,barra del titolo,doppio clic,desktop,snap,movimento,attivazione,attivare,pip,picture in picture,picture-in-picture", + "X-KDE-Keywords[ja]": "focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick,desktop,snap,movement,activation,activate,pip,picture in picture,picture-in-picture, フォーカス,焦点,配置,ウィンドウの挙動,ウィンドウの振る舞い,ウィンドウアクション,操作,アニメーション,前面に出す,最前面,自動的に前面に出す,オートレイズ,ウィンドウ,枠,フレーム,タイトルバー,ダブルクリック,デスクトップ,スナップ,吸着,移動,アクティベーション,アクティブ化,有効化,ピクチャーインピクチャー,", + "X-KDE-Keywords[ka]": "focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick,desktop,snap,movement,shading,shade,unshade,unshading,activation,activate,ფოკუსი, მოთავსება,ფანჯრის ქცევა,ფანჯრების ქცევა,ანიმაცია,ამოწევა,ავტოამოწევა,ფანჯრები,ჩარჩო,სათაურის ზოლი,სამუშაო მაგიდა,მიმაგრება,დაჩრდილვა;გააქტიურება;სურათი-სურათში", + "X-KDE-Keywords[ko]": "포커스,초점,위치,창 행동,애니메이션,올리기,창,프레임,제목 표시줄,바탕 화면,데스크톱,이동,말아 올리기,풀어 내리기,활성화,활성,클릭,두 번 클릭,스냅,이동,활성화,활성,화면 속 화면", + "X-KDE-Keywords[lt]": "focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick,desktop,snap,movement,activation,activate,pip,picture in picture,picture-in-picture,fokusavimas,išdėstymas,isdestymas,langų elgsena,langu elgsena,lango elgsena,langų elgesys,langu elgesys,langų veiksmai,langu veiksmai,lango veiksmai,veiksmai su langu,veiksmai su langais,animacija,animacijos,iškėlimas,iškelti,iskelimas,iskelti,automatinis iškėlimas,automatinis iskelimas,langai,rėmelis,remelis,antraštės juosta,antrastes juosta,pavadinimo juosta,dvikartis spustelėjimas,dvikartis spustelejimas,dvigubas spustelėjimas,dvigubas spustelejimas,spustelėjimas du kartus,spustelėjimas du kartus,spustelėjimas dukart,spustelejimas dukart,darbalaukis,darbastalis,pritraukimas,perkėlimas,perkelimas,aktyvavimas,aktyvinimas,aktyvuoti,aktyvinti,vaizdas vaizde", + "X-KDE-Keywords[lv]": "fokuss,novietojums,logu uzvedība,logu darbības,animācija,pacelt,automātiski pacelt,logi,rāmis,virsrakstjosla,dubultklikšķis,darbvirsma,pielipšana,pārvietošanās,aktivizēšana,aktivizēt,pip,attēls attēlā", + "X-KDE-Keywords[nl]": "focus,plaatsing,venstergedrag,vensteracties,animatie,omhoog brengen, automatisch omhoog brengen,vensters,frame,titelbalk,dubbel-klik,bureaublad,vastklikken,verplaatsing,activatie,activeren,pip,picture in picture, picture-in-picture", + "X-KDE-Keywords[pt_BR]": "foco,posicionamento,comportamento da janela,ações da janela,animação,elevação,elevação automática,janelas,quadro,barra de título,clique duplo,área de trabalho,encaixe,movimento,ativação,ativar,pip,picture in picture,picture-in-picture", + "X-KDE-Keywords[ro]": "focalizare,plasament,amplasare,comportament fereastră,comportament ferestre,acțiuni fereastră,animație,ridică,ridică automat,ridicare automată,ferestre,cadru,chenar,bară de titlu,clic dublu,dublu clic,birou,magnetic,lipire,mișcare,activare,activează,imagine în imagine,imagine-în-imagine", + "X-KDE-Keywords[ru]": "focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick,desktop,snap,movement,activation,activate,фокус,расположение,размещение,поведение окон,действия окна,анимация,поднять,поднять автоматически,автоматическое поднятие,окна,рамка,строка заголовка,заголовок,двойной щелчок,рабочий стол,привязка,прилипание,перемещение,активация,активировать", + "X-KDE-Keywords[sk]": "focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick,desktop,snap,movement,activation,activate,pip,picture in picture,picture-in-picture", + "X-KDE-Keywords[sl]": "fokus,postavitev,vedenje okna,dejanja oken,animacija,dvig,samodejni dvig,okna,okvir,naslovna vrstica,dvoklik,namizje,pripenjanje,gibanje,aktivacija,aktiviraj,pip,slika v sliki,slika-v-sliki", + "X-KDE-Keywords[sv]": "fokus,placering,fönsterbeteende,fönsteråtgärder,animering,höjning,automatisk höjning,fönster,ram,namnlist,dubbelklicka,skrivbord,lås,rörelse,aktivering,aktivera,bib,bild i bild, bild-i-bild", + "X-KDE-Keywords[tr]": "odak,yerleşim,pencere davranışı,pencere eylemleri,canlandırma,animasyon,yükselt,çerçeve,başlık çubuğu,çift tık,masaüstü,tuttur,hareket,taşı,etkinleştir,pip,picture in picture,picture-in-picture,resim içinde resim,görüntü içinde görüntü,ekran içinde ekran", + "X-KDE-Keywords[uk]": "focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick,desktop,snap,movement,activation,activate,pip,picture in picture,picture-in-picture,фокус,розташування,місце,вікно,поведінка,поведінка вікон,дії,реакція,дії з вікнами,реакція вікон,анімація,підняти,підняття,автоматична,автоматично,рамка,заголовок,смужка заголовка,клацання,подвійне,стільниця,прилипання,рух,активація,активувати,зображення у зображенні,зображення-у-зображенні", + "X-KDE-Keywords[zh_CN]": "focus,placement,window behavior,window actions,animation,raise,auto raise,windows,frame,titlebar,doubleclick,desktop,snap,movement,activation,activate,jiaodian,fangzhi,weizhi,chuangkouxingwei,chuangkoucaozuo,donghua,tisheng,zidongtisheng,chuangkou,kuangjia,biaotilan,shuangji,zhuomian,xifu,yidong,jihuo,pip,picture in picture,picture-in-picture,焦点,放置,位置,窗口行为,窗口操作,动画,提升,自动提升,窗口,框架,标题栏,双击,桌面,吸附,移动,激活,画中画", + "X-KDE-Keywords[zh_TW]": "焦點,聚焦,視窗行為,視窗動作,動畫,抬升,提升,自動抬升,視窗,框架,邊框,標題列,標題欄,雙擊,連點,桌面,貼齊,移動,觸發,pip,子母畫面,畫中畫", + "X-KDE-OnlyShowOnQtPlatforms": [ + "wayland" + ], + "X-KDE-System-Settings-Parent-Category": "windowmanagement", + "X-KDE-Weight": 10 +} diff --git a/local/recipes/kde/kwin/source/src/kcms/options/kwinoptions_kdeglobals_settings.kcfg b/local/recipes/kde/kwin/source/src/kcms/options/kwinoptions_kdeglobals_settings.kcfg new file mode 100644 index 0000000000..2a1c486b03 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/kwinoptions_kdeglobals_settings.kcfg @@ -0,0 +1,15 @@ + + + + + + + + true + + + + diff --git a/local/recipes/kde/kwin/source/src/kcms/options/kwinoptions_kdeglobals_settings.kcfgc b/local/recipes/kde/kwin/source/src/kcms/options/kwinoptions_kdeglobals_settings.kcfgc new file mode 100644 index 0000000000..b2e1ccb7f2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/kwinoptions_kdeglobals_settings.kcfgc @@ -0,0 +1,6 @@ +File=kwinoptions_kdeglobals_settings.kcfg +ClassName=KWinOptionsKDEGlobalsSettings +IncludeFiles=options.h +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/local/recipes/kde/kwin/source/src/kcms/options/kwinoptions_settings.kcfg b/local/recipes/kde/kwin/source/src/kcms/options/kwinoptions_settings.kcfg new file mode 100644 index 0000000000..797fb8336f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/kwinoptions_settings.kcfg @@ -0,0 +1,367 @@ + + + + + + + 10 + 0 + 100 + + + + 10 + 0 + 100 + + + + 0 + 0 + 100 + + + + false + + + + + + + + + + + + Centered + + + + + + + + SwitchToOtherDesktop + + + + Maximize + + + + + + + + + + + + + + Maximize + + + + + + + + + MaximizeVerticalOnly + + + + + + + + + MaximizeHorizontalOnly + + + + + + + + + ClickToFocus + + + + + + + + + + false + + + + 750 + 0 + + + + 300 + 0 + + + + false + + + + true + + + + true + + + + 1 + 0 + 4 + + + + + + + + + + Qt::BottomRightCorner + + + + 20 + + + + + + + + Raise + + + + + + + + + + + + + Nothing + + + + + + + + + + + + + OperationsMenu + + + + + + + + + + + + + Nothing + + + + + + + + + + + + ActivateAndRaise + + + + + + + + + + + + + + + + Nothing + + + + + + + + + + + + + + + + OperationsMenu + + + + + + + + + + + + + + + + ActivateRaiseOnReleaseAndPassClick + + + + + + + + + + + ActivatePassClick + + + + + + + + + + ActivatePassClick + + + + + + + + + + Scroll + + + + + + + + + Meta + + + + + + + + Move + + + + + + + + + + + + + + + + + ToggleRaiseAndLower + + + + + + + + + + + + + + + + + Resize + + + + + + + + + + + + + + + + + Nothing + + + + + + + + + + + + true + + + + diff --git a/local/recipes/kde/kwin/source/src/kcms/options/kwinoptions_settings.kcfgc b/local/recipes/kde/kwin/source/src/kcms/options/kwinoptions_settings.kcfgc new file mode 100644 index 0000000000..dc559eccbe --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/kwinoptions_settings.kcfgc @@ -0,0 +1,6 @@ +File=kwinoptions_settings.kcfg +ClassName=KWinOptionsSettings +IncludeFiles=options.h +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/local/recipes/kde/kwin/source/src/kcms/options/main.cpp b/local/recipes/kde/kwin/source/src/kcms/options/main.cpp new file mode 100644 index 0000000000..4ae37855a2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/main.cpp @@ -0,0 +1,214 @@ +/* + SPDX-FileCopyrightText: 2001 Waldo Bastian + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "main.h" + +#include +// Added by qt3to4: +#include + +#include + +#include +#include +#include + +#include "kwinoptions_kdeglobals_settings.h" +#include "kwinoptions_settings.h" +#include "kwinoptionsdata.h" +#include "mouse.h" +#include "windows.h" + +K_PLUGIN_CLASS_WITH_JSON(KWinOptions, "kcm_kwinoptions.json") + +KWinOptions::KWinOptions(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) +{ + mSettings = new KWinOptionsSettings(this); + + QVBoxLayout *layout = new QVBoxLayout(widget()); + layout->setContentsMargins(0, 0, 0, 0); + tab = new QTabWidget(widget()); + tab->setDocumentMode(true); + tab->tabBar()->setExpanding(true); + layout->addWidget(tab); + + const auto connectKCM = [this](KCModule *mod) { + connect(mod, &KCModule::needsSaveChanged, this, &KWinOptions::updateUnmanagedState); + connect(this, &KCModule::defaultsIndicatorsVisibleChanged, mod, [mod, this]() { + mod->setDefaultsIndicatorsVisible(defaultsIndicatorsVisible()); + }); + }; + + mFocus = new KFocusConfig(mSettings, widget()); + mFocus->setObjectName(QLatin1String("KWin Focus Config")); + tab->addTab(mFocus->widget(), i18n("&Focus")); + connectKCM(mFocus); + + mTitleBarActions = new KTitleBarActionsConfig(mSettings, widget()); + mTitleBarActions->setObjectName(QLatin1String("KWin TitleBar Actions")); + tab->addTab(mTitleBarActions->widget(), i18n("Titlebar A&ctions")); + connectKCM(mTitleBarActions); + + mWindowActions = new KWindowActionsConfig(mSettings, widget()); + mWindowActions->setObjectName(QLatin1String("KWin Window Actions")); + tab->addTab(mWindowActions->widget(), i18n("W&indow Actions")); + connectKCM(mWindowActions); + + mMoving = new KMovingConfig(mSettings, widget()); + mMoving->setObjectName(QLatin1String("KWin Moving")); + tab->addTab(mMoving->widget(), i18n("Mo&vement")); + connectKCM(mMoving); + + mAdvanced = new KAdvancedConfig(mSettings, new KWinOptionsKDEGlobalsSettings(this), widget()); + mAdvanced->setObjectName(QLatin1String("KWin Advanced")); + tab->addTab(mAdvanced->widget(), i18n("Adva&nced")); + connectKCM(mAdvanced); + + if (qEnvironmentVariableIntValue("KWIN_WAYLAND_SUPPORT_XX_PIP_V1")) { + mPip = new KPipConfig(mSettings, widget()); + mPip->setObjectName(QLatin1String("KWin Picture-in-Picture")); + tab->addTab(mPip->widget(), i18n("&Picture-in-picture")); + connectKCM(mPip); + } +} + +void KWinOptions::load() +{ + KCModule::load(); + + mTitleBarActions->load(); + mWindowActions->load(); + mMoving->load(); + mAdvanced->load(); + // mFocus is last because it may send unmanagedWidgetStateChanged + // that need to have the final word + mFocus->load(); + if (mPip) { + mPip->load(); + } +} + +void KWinOptions::save() +{ + KCModule::save(); + + mFocus->save(); + mTitleBarActions->save(); + mWindowActions->save(); + mMoving->save(); + mAdvanced->save(); + if (mPip) { + mPip->save(); + } + + // Send signal to all kwin instances + QDBusMessage message = + QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); +} + +void KWinOptions::defaults() +{ + KCModule::defaults(); + + mTitleBarActions->defaults(); + mWindowActions->defaults(); + mMoving->defaults(); + mAdvanced->defaults(); + // mFocus is last because it may send unmanagedWidgetDefaulted + // that need to have the final word + mFocus->defaults(); + if (mPip) { + mPip->defaults(); + } +} + +void KWinOptions::updateUnmanagedState() +{ + bool changed = false; + changed |= mFocus->needsSave(); + changed |= mTitleBarActions->needsSave(); + changed |= mWindowActions->needsSave(); + changed |= mMoving->needsSave(); + changed |= mAdvanced->needsSave(); + changed |= mPip ? mPip->needsSave() : false; + + unmanagedWidgetChangeState(changed); + + bool isDefault = true; + isDefault &= mFocus->representsDefaults(); + isDefault &= mTitleBarActions->representsDefaults(); + isDefault &= mWindowActions->representsDefaults(); + isDefault &= mMoving->representsDefaults(); + isDefault &= mAdvanced->representsDefaults(); + isDefault &= mPip ? mPip->representsDefaults() : true; + + unmanagedWidgetDefaultState(isDefault); +} + +KActionsOptions::KActionsOptions(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) +{ + mSettings = new KWinOptionsSettings(this); + + QVBoxLayout *layout = new QVBoxLayout(widget()); + layout->setContentsMargins(0, 0, 0, 0); + tab = new QTabWidget(widget()); + layout->addWidget(tab); + + mTitleBarActions = new KTitleBarActionsConfig(mSettings, widget()); + mTitleBarActions->setObjectName(QLatin1String("KWin TitleBar Actions")); + tab->addTab(mTitleBarActions->widget(), i18n("&Titlebar Actions")); + connect(mTitleBarActions, &KCModule::needsSaveChanged, this, [this]() { + setNeedsSave(mTitleBarActions->needsSave()); + }); + connect(mTitleBarActions, &KCModule::representsDefaultsChanged, this, [this]() { + setRepresentsDefaults(mTitleBarActions->representsDefaults()); + }); + + mWindowActions = new KWindowActionsConfig(mSettings, widget()); + mWindowActions->setObjectName(QLatin1String("KWin Window Actions")); + tab->addTab(mWindowActions->widget(), i18n("Window Actio&ns")); + connect(mWindowActions, &KCModule::needsSaveChanged, this, [this]() { + setNeedsSave(mWindowActions->needsSave()); + }); + connect(mWindowActions, &KCModule::representsDefaultsChanged, this, [this]() { + setRepresentsDefaults(mWindowActions->representsDefaults()); + }); +} + +void KActionsOptions::load() +{ + mTitleBarActions->load(); + mWindowActions->load(); +} + +void KActionsOptions::save() +{ + mTitleBarActions->save(); + mWindowActions->save(); + + setNeedsSave(false); + // Send signal to all kwin instances + QDBusMessage message = QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); +} + +void KActionsOptions::defaults() +{ + mTitleBarActions->defaults(); + mWindowActions->defaults(); +} + +void KActionsOptions::moduleChanged(bool state) +{ + setNeedsSave(state); +} + +#include "main.moc" + +#include "moc_main.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/options/main.h b/local/recipes/kde/kwin/source/src/kcms/options/main.h new file mode 100644 index 0000000000..394d165c0f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/main.h @@ -0,0 +1,75 @@ +/* + main.h + + SPDX-FileCopyrightText: 2001 Waldo Bastian + + Requires the Qt widget libraries, available at no cost at + https://www.qt.io + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +class KWinOptionsSettings; +class KWinOptionsKDEGlobalsSettings; +class KFocusConfig; +class KTitleBarActionsConfig; +class KWindowActionsConfig; +class KAdvancedConfig; +class KMovingConfig; +class KPipConfig; + +class KWinOptions : public KCModule +{ + Q_OBJECT + +public: + explicit KWinOptions(QObject *parent, const KPluginMetaData &data); + + void load() override; + void save() override; + void defaults() override; + +protected Q_SLOTS: + void updateUnmanagedState(); + +private: + QTabWidget *tab; + + KFocusConfig *mFocus; + KTitleBarActionsConfig *mTitleBarActions; + KWindowActionsConfig *mWindowActions; + KMovingConfig *mMoving; + KAdvancedConfig *mAdvanced; + KPipConfig *mPip = nullptr; + + KWinOptionsSettings *mSettings; +}; + +class KActionsOptions : public KCModule +{ + Q_OBJECT + +public: + KActionsOptions(QObject *parent, const KPluginMetaData &data); + + void load() override; + void save() override; + void defaults() override; + +protected Q_SLOTS: + + void moduleChanged(bool state); + +private: + QTabWidget *tab; + + KTitleBarActionsConfig *mTitleBarActions; + KWindowActionsConfig *mWindowActions; + + KWinOptionsSettings *mSettings; +}; diff --git a/local/recipes/kde/kwin/source/src/kcms/options/mouse.cpp b/local/recipes/kde/kwin/source/src/kcms/options/mouse.cpp new file mode 100644 index 0000000000..0821373538 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/mouse.cpp @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 1998 Matthias Ettrich + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "mouse.h" +#include "kwinoptions_settings.h" + +KWinMouseConfigForm::KWinMouseConfigForm(QWidget *parent) + : QWidget(parent) +{ + setupUi(parent); +} + +KWinActionsConfigForm::KWinActionsConfigForm(QWidget *parent) + : QWidget(parent) +{ + setupUi(parent); +} + +KTitleBarActionsConfig::KTitleBarActionsConfig(KWinOptionsSettings *settings, QWidget *parent) + : KCModule(parent, KPluginMetaData()) + , m_ui(new KWinMouseConfigForm(widget())) +{ + if (settings) { + initialize(settings); + } +} + +void KTitleBarActionsConfig::initialize(KWinOptionsSettings *settings) +{ + m_settings = settings; + addConfig(m_settings, widget()); +} + +KWindowActionsConfig::KWindowActionsConfig(KWinOptionsSettings *settings, QWidget *parent) + : KCModule(parent, KPluginMetaData()) + , m_ui(new KWinActionsConfigForm(widget())) +{ + if (settings) { + initialize(settings); + } +} + +void KWindowActionsConfig::initialize(KWinOptionsSettings *settings) +{ + m_settings = settings; + addConfig(m_settings, widget()); +} + +#include "moc_mouse.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/options/mouse.h b/local/recipes/kde/kwin/source/src/kcms/options/mouse.h new file mode 100644 index 0000000000..66c2f7f02a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/mouse.h @@ -0,0 +1,69 @@ +/* + mouse.h + + SPDX-FileCopyrightText: 1998 Matthias Ettrich + + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +class KConfig; + +#include +#include + +#include "ui_actions.h" +#include "ui_mouse.h" + +class KWinOptionsSettings; + +class KWinMouseConfigForm : public QWidget, public Ui::KWinMouseConfigForm +{ + Q_OBJECT + +public: + explicit KWinMouseConfigForm(QWidget *parent); +}; + +class KWinActionsConfigForm : public QWidget, public Ui::KWinActionsConfigForm +{ + Q_OBJECT + +public: + explicit KWinActionsConfigForm(QWidget *parent); +}; + +class KTitleBarActionsConfig : public KCModule +{ + Q_OBJECT + +public: + KTitleBarActionsConfig(KWinOptionsSettings *settings, QWidget *parent); + +protected: + void initialize(KWinOptionsSettings *settings); + +private: + KWinMouseConfigForm *m_ui; + KWinOptionsSettings *m_settings; +}; + +class KWindowActionsConfig : public KCModule +{ + Q_OBJECT + +public: + KWindowActionsConfig(KWinOptionsSettings *settings, QWidget *parent); + + bool isDefaults() const; + bool isSaveNeeded() const; + +protected: + void initialize(KWinOptionsSettings *settings); + +private: + KWinActionsConfigForm *m_ui; + KWinOptionsSettings *m_settings; +}; diff --git a/local/recipes/kde/kwin/source/src/kcms/options/mouse.ui b/local/recipes/kde/kwin/source/src/kcms/options/mouse.ui new file mode 100644 index 0000000000..8e777dd70c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/mouse.ui @@ -0,0 +1,709 @@ + + + KWinMouseConfigForm + + + + 0 + 0 + 600 + 500 + + + + + + + Titlebar Actions + + + true + + + + Qt::AlignHCenter|Qt::AlignTop + + + + + &Double-click: + + + kcfg_TitlebarDoubleClickCommand + + + + + + + Behavior on <em>double</em> click into the titlebar. + + + + Maximize + + + + + Vertically maximize + + + + + Horizontally maximize + + + + + Minimize + + + + + Lower + + + + + Close + + + + + Show on all desktops + + + + + Do nothing + + + + + + + + Mouse &wheel: + + + kcfg_CommandTitlebarWheel + + + + + + + Behavior on <em>mouse wheel</em> scroll over the titlebar. + + + + Raise/lower + + + + + Maximize/restore + + + + + Keep above/below + + + + + Move to previous/next desktop + + + + + Change opacity + + + + + Do nothing + + + + + + + + + + + Titlebar and Frame Actions + + + true + + + Qt::AlignCenter + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + Active + + + Qt::AlignCenter + + + + + + + &Left click: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandActiveTitlebar1 + + + + + + + Inactive + + + Qt::AlignCenter + + + + + + + &Middle click: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandActiveTitlebar2 + + + + + + + &Right click: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_CommandActiveTitlebar3 + + + + + + + + 0 + 0 + + + + Behavior on <em>left</em> click into the titlebar or frame of an <em>active</em> window. + + + + Raise + + + + + Lower + + + + + Toggle raise and lower + + + + + Minimize + + + + + Close + + + + + Window menu + + + + + Do nothing + + + + + + + + Behavior on <em>left</em> click into the titlebar or frame of an <em>inactive</em> window. + + + + Activate and raise + + + + + Activate and lower + + + + + Activate + + + + + Raise + + + + + Lower + + + + + Toggle raise and lower + + + + + Minimize + + + + + Close + + + + + Window menu + + + + + Do nothing + + + + + + + + + 0 + 0 + + + + Behavior on <em>left</em> click into the titlebar or frame of an <em>active</em> window. + + + + Raise + + + + + Lower + + + + + Toggle raise and lower + + + + + Minimize + + + + + Close + + + + + Window menu + + + + + Do nothing + + + + + + + + Behavior on <em>left</em> click into the titlebar or frame of an <em>inactive</em> window. + + + + Activate and raise + + + + + Activate and lower + + + + + Activate + + + + + Raise + + + + + Lower + + + + + Toggle raise and lower + + + + + Minimize + + + + + Close + + + + + Window menu + + + + + Do nothing + + + + + + + + + 0 + 0 + + + + Behavior on <em>left</em> click into the titlebar or frame of an <em>active</em> window. + + + + Raise + + + + + Lower + + + + + Toggle raise and lower + + + + + Minimize + + + + + Close + + + + + Window menu + + + + + Do nothing + + + + + + + + Behavior on <em>left</em> click into the titlebar or frame of an <em>inactive</em> window. + + + + Activate and raise + + + + + Activate and lower + + + + + Activate + + + + + Raise + + + + + Lower + + + + + Toggle raise and lower + + + + + Minimize + + + + + Close + + + + + Window menu + + + + + Do nothing + + + + + + + + Maximize window by double clicking its frame + + + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + + Maximize Button Actions + + + true + + + + Qt::AlignHCenter|Qt::AlignTop + + + + + Behavior on <em>left</em> click onto the maximize button. + + + L&eft click: + + + kcfg_MaximizeButtonLeftClickCommand + + + + + + + Behavior on <em>left</em> click onto the maximize button. + + + + Maximize + + + + + Vertically maximize + + + + + Horizontally maximize + + + + + + + + Behavior on <em>middle</em> click onto the maximize button. + + + Middle c&lick: + + + kcfg_MaximizeButtonMiddleClickCommand + + + + + + + Behavior on <em>middle</em> click onto the maximize button. + + + + Maximize + + + + + Vertically maximize + + + + + Horizontally maximize + + + + + + + + Behavior on <em>right</em> click onto the maximize button. + + + Right clic&k: + + + kcfg_MaximizeButtonRightClickCommand + + + + + + + Behavior on <em>right</em> click onto the maximize button. + + + + Maximize + + + + + Vertically maximize + + + + + Horizontally maximize + + + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + kcfg_TitlebarDoubleClickCommand + kcfg_CommandTitlebarWheel + kcfg_CommandActiveTitlebar1 + kcfg_CommandInactiveTitlebar1 + kcfg_CommandActiveTitlebar2 + kcfg_CommandInactiveTitlebar2 + kcfg_CommandActiveTitlebar3 + kcfg_CommandInactiveTitlebar3 + kcfg_MaximizeButtonLeftClickCommand + kcfg_MaximizeButtonMiddleClickCommand + kcfg_MaximizeButtonRightClickCommand + + + + diff --git a/local/recipes/kde/kwin/source/src/kcms/options/moving.ui b/local/recipes/kde/kwin/source/src/kcms/options/moving.ui new file mode 100644 index 0000000000..a61a5a60cc --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/moving.ui @@ -0,0 +1,134 @@ + + + KWinMovingConfigForm + + + + 0 + 0 + 600 + 500 + + + + + Qt::AlignHCenter|Qt::AlignTop + + + + + Screen &edge snap zone: + + + kcfg_BorderSnapZone + + + + + + + Here you can set the snap zone for screen edges, i.e. the 'strength' of the magnetic field which will make windows snap to the border when moved near it. + + + None + + + px + + + 0 + + + 100 + + + 10 + + + + + + + &Window snap zone: + + + kcfg_WindowSnapZone + + + + + + + Here you can set the snap zone for windows, i.e. the 'strength' of the magnetic field which will make windows snap to each other when they are moved near another window. + + + None + + + px + + + 0 + + + 100 + + + 10 + + + + + + + &Center snap zone: + + + kcfg_CenterSnapZone + + + + + + + Here you can set the snap zone for the screen center, i.e. the 'strength' of the magnetic field which will make windows snap to the center of the screen when moved near it. + + + None + + + px + + + 0 + + + 100 + + + + + + + &Snap windows: + + + kcfg_SnapOnlyWhenOverlapping + + + + + + + Here you can set that windows will be only snapped if you try to overlap them, i.e. they will not be snapped if the windows comes only near another window or border. + + + Only when overlapping + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/kcms/options/windows.cpp b/local/recipes/kde/kwin/source/src/kcms/options/windows.cpp new file mode 100644 index 0000000000..8905225659 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/windows.cpp @@ -0,0 +1,297 @@ +/* + windows.cpp + + SPDX-FileCopyrightText: 1997 Patrick Dowler + SPDX-FileCopyrightText: 2001 Waldo Bastian + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "kwinoptions_settings.h" +#include "windows.h" +#include + +#include "kwinoptions_kdeglobals_settings.h" +#include "kwinoptions_settings.h" + +#define CLICK_TO_FOCUS 0 +#define CLICK_TO_FOCUS_MOUSE_PRECEDENT 1 +#define FOCUS_FOLLOWS_MOUSE 2 +#define FOCUS_FOLLOWS_MOUSE_PRECEDENT 3 +#define FOCUS_UNDER_MOUSE 4 +#define FOCUS_STRICTLY_UNDER_MOUSE 5 + +namespace +{ +constexpr int defaultFocusPolicyIndex = CLICK_TO_FOCUS; +} + +KWinFocusConfigForm::KWinFocusConfigForm(QWidget *parent) + : QWidget(parent) +{ + setupUi(parent); +} + +KFocusConfig::KFocusConfig(KWinOptionsSettings *settings, QWidget *parent) + : KCModule(parent, KPluginMetaData()) + , m_ui(new KWinFocusConfigForm(widget())) +{ + if (settings) { + initialize(settings); + } +} + +void KFocusConfig::initialize(KWinOptionsSettings *settings) +{ + m_settings = settings; + addConfig(m_settings, widget()); + + connect(m_ui->windowFocusPolicy, qOverload(&QComboBox::currentIndexChanged), this, &KFocusConfig::focusPolicyChanged); + connect(m_ui->windowFocusPolicy, qOverload(&QComboBox::currentIndexChanged), this, &KFocusConfig::updateDefaultIndicator); + connect(this, SIGNAL(defaultsIndicatorsVisibleChanged(bool)), this, SLOT(updateDefaultIndicator())); + + connect(qApp, &QGuiApplication::screenAdded, this, &KFocusConfig::updateMultiScreen); + connect(qApp, &QGuiApplication::screenRemoved, this, &KFocusConfig::updateMultiScreen); + updateMultiScreen(); +} + +void KFocusConfig::updateMultiScreen() +{ + m_ui->multiscreenBehaviorLabel->setVisible(QApplication::screens().count() > 1); + m_ui->kcfg_SeparateScreenFocus->setVisible(QApplication::screens().count() > 1); +} + +void KFocusConfig::updateDefaultIndicator() +{ + const bool isDefault = m_ui->windowFocusPolicy->currentIndex() == defaultFocusPolicyIndex; + m_ui->windowFocusPolicy->setProperty("_kde_highlight_neutral", defaultsIndicatorsVisible() && !isDefault); + m_ui->windowFocusPolicy->update(); +} + +void KFocusConfig::updateFocusPolicyExplanatoryText() +{ + const int focusPolicy = m_ui->windowFocusPolicy->currentIndex(); + switch (focusPolicy) { + case CLICK_TO_FOCUS: + m_ui->windowFocusPolicyDescriptionLabel->setText(i18n("Click to focus: A window becomes active when you click into it. This behavior is common on other operating systems and likely what you want.")); + break; + case CLICK_TO_FOCUS_MOUSE_PRECEDENT: + m_ui->windowFocusPolicyDescriptionLabel->setText(i18n("Click to focus (mouse precedence): Mostly the same as Click to focus. If an active window has to be chosen by the system (eg. because the currently active one was closed) the window under the mouse is the preferred candidate. Unusual, but possible variant of Click to focus.")); + break; + case FOCUS_FOLLOWS_MOUSE: + m_ui->windowFocusPolicyDescriptionLabel->setText(i18n("Focus follows mouse: Moving the mouse onto a window will activate it. Eg. windows randomly appearing under the mouse will not gain the focus. Focus stealing prevention takes place as usual. Think as Click to focus just without having to actually click.")); + break; + case FOCUS_FOLLOWS_MOUSE_PRECEDENT: + m_ui->windowFocusPolicyDescriptionLabel->setText(i18n("This is mostly the same as Focus follows mouse. If an active window has to be chosen by the system (eg. because the currently active one was closed) the window under the mouse is the preferred candidate. Choose this, if you want a hover controlled focus.")); + break; + case FOCUS_UNDER_MOUSE: + m_ui->windowFocusPolicyDescriptionLabel->setText(i18n("Focus under mouse: The focus always remains on the window under the mouse.
Warning: Focus stealing prevention and the tabbox ('Alt+Tab') contradict the activation policy and will not work. You very likely want to use Focus follows mouse (mouse precedence) instead!")); + break; + case FOCUS_STRICTLY_UNDER_MOUSE: + m_ui->windowFocusPolicyDescriptionLabel->setText(i18n("Focus strictly under mouse: The focus is always on the window under the mouse (in doubt nowhere) very much like the focus behavior in an unmanaged legacy X11 environment.
Warning: Focus stealing prevention and the tabbox ('Alt+Tab') contradict the activation policy and will not work. You very likely want to use Focus follows mouse (mouse precedence) instead!")); + break; + } +} + +void KFocusConfig::focusPolicyChanged() +{ + int selectedFocusPolicy = 0; + bool selectedNextFocusPrefersMouseItem = false; + const bool loadedNextFocusPrefersMouseItem = m_settings->nextFocusPrefersMouse(); + + updateFocusPolicyExplanatoryText(); + + int focusPolicy = m_ui->windowFocusPolicy->currentIndex(); + switch (focusPolicy) { + case CLICK_TO_FOCUS: + selectedFocusPolicy = KWinOptionsSettings::EnumFocusPolicy::ClickToFocus; + break; + case CLICK_TO_FOCUS_MOUSE_PRECEDENT: + selectedFocusPolicy = KWinOptionsSettings::EnumFocusPolicy::ClickToFocus; + selectedNextFocusPrefersMouseItem = true; + break; + case FOCUS_FOLLOWS_MOUSE: + selectedFocusPolicy = KWinOptionsSettings::EnumFocusPolicy::FocusFollowsMouse; + break; + case FOCUS_FOLLOWS_MOUSE_PRECEDENT: + selectedFocusPolicy = KWinOptionsSettings::EnumFocusPolicy::FocusFollowsMouse; + selectedNextFocusPrefersMouseItem = true; + break; + case FOCUS_UNDER_MOUSE: + selectedFocusPolicy = KWinOptionsSettings::EnumFocusPolicy::FocusUnderMouse; + break; + case FOCUS_STRICTLY_UNDER_MOUSE: + selectedFocusPolicy = KWinOptionsSettings::EnumFocusPolicy::FocusStrictlyUnderMouse; + break; + } + + unmanagedWidgetChangeState(m_settings->focusPolicy() != selectedFocusPolicy || loadedNextFocusPrefersMouseItem != selectedNextFocusPrefersMouseItem); + + unmanagedWidgetDefaultState(focusPolicy == defaultFocusPolicyIndex); + + // the auto raise related widgets are: autoRaise + m_ui->kcfg_AutoRaise->setEnabled(focusPolicy != CLICK_TO_FOCUS && focusPolicy != CLICK_TO_FOCUS_MOUSE_PRECEDENT); + + m_ui->kcfg_FocusStealingPreventionLevel->setDisabled(focusPolicy == FOCUS_UNDER_MOUSE || focusPolicy == FOCUS_STRICTLY_UNDER_MOUSE); + + // the delayed focus related widgets are: delayFocus + m_ui->delayFocusOnLabel->setEnabled(focusPolicy != CLICK_TO_FOCUS); + m_ui->kcfg_DelayFocusInterval->setEnabled(focusPolicy != CLICK_TO_FOCUS); +} + +void KFocusConfig::load(void) +{ + KCModule::load(); + + const bool loadedNextFocusPrefersMouseItem = m_settings->nextFocusPrefersMouse(); + + int focusPolicy = m_settings->focusPolicy(); + + switch (focusPolicy) { + // the ClickToFocus and FocusFollowsMouse have special values when + // NextFocusPrefersMouse is true + case KWinOptionsSettings::EnumFocusPolicy::ClickToFocus: + m_ui->windowFocusPolicy->setCurrentIndex(CLICK_TO_FOCUS + loadedNextFocusPrefersMouseItem); + break; + case KWinOptionsSettings::EnumFocusPolicy::FocusFollowsMouse: + m_ui->windowFocusPolicy->setCurrentIndex(FOCUS_FOLLOWS_MOUSE + loadedNextFocusPrefersMouseItem); + break; + default: + // +2 to ignore the two special values + m_ui->windowFocusPolicy->setCurrentIndex(focusPolicy + 2); + break; + } + updateFocusPolicyExplanatoryText(); +} + +void KFocusConfig::save(void) +{ + KCModule::save(); + + int idxFocusPolicy = m_ui->windowFocusPolicy->currentIndex(); + switch (idxFocusPolicy) { + case CLICK_TO_FOCUS: + case CLICK_TO_FOCUS_MOUSE_PRECEDENT: + m_settings->setFocusPolicy(KWinOptionsSettings::EnumFocusPolicy::ClickToFocus); + break; + case FOCUS_FOLLOWS_MOUSE: + case FOCUS_FOLLOWS_MOUSE_PRECEDENT: + // the ClickToFocus and FocusFollowsMouse have special values when + // NextFocusPrefersMouse is true + m_settings->setFocusPolicy(KWinOptionsSettings::EnumFocusPolicy::FocusFollowsMouse); + break; + case FOCUS_UNDER_MOUSE: + m_settings->setFocusPolicy(KWinOptionsSettings::EnumFocusPolicy::FocusUnderMouse); + break; + case FOCUS_STRICTLY_UNDER_MOUSE: + m_settings->setFocusPolicy(KWinOptionsSettings::EnumFocusPolicy::FocusStrictlyUnderMouse); + break; + } + m_settings->setNextFocusPrefersMouse(idxFocusPolicy == CLICK_TO_FOCUS_MOUSE_PRECEDENT || idxFocusPolicy == FOCUS_FOLLOWS_MOUSE_PRECEDENT); + + m_settings->save(); +} + +void KFocusConfig::defaults() +{ + KCModule::defaults(); + m_ui->windowFocusPolicy->setCurrentIndex(defaultFocusPolicyIndex); +} + +KWinAdvancedConfigForm::KWinAdvancedConfigForm(QWidget *parent) + : QWidget(parent) +{ + setupUi(parent); +} + +KWinPipConfigForm::KWinPipConfigForm(QWidget *parent) + : QWidget(parent) +{ + setupUi(parent); +} + +KAdvancedConfig::KAdvancedConfig(KWinOptionsSettings *settings, KWinOptionsKDEGlobalsSettings *globalSettings, QWidget *parent) + : KCModule(parent, KPluginMetaData()) + , m_ui(new KWinAdvancedConfigForm(widget())) +{ + if (settings && globalSettings) { + initialize(settings, globalSettings); + } +} + +void KAdvancedConfig::initialize(KWinOptionsSettings *settings, KWinOptionsKDEGlobalsSettings *globalSettings) +{ + m_settings = settings; + addConfig(m_settings, widget()); + addConfig(globalSettings, widget()); + + m_ui->kcfg_Placement->setItemData(KWinOptionsSettings::PlacementChoices::Smart, "Smart"); + m_ui->kcfg_Placement->setItemData(KWinOptionsSettings::PlacementChoices::Maximizing, "Maximizing"); + m_ui->kcfg_Placement->setItemData(KWinOptionsSettings::PlacementChoices::Random, "Random"); + m_ui->kcfg_Placement->setItemData(KWinOptionsSettings::PlacementChoices::Centered, "Centered"); + m_ui->kcfg_Placement->setItemData(KWinOptionsSettings::PlacementChoices::ZeroCornered, "ZeroCornered"); + m_ui->kcfg_Placement->setItemData(KWinOptionsSettings::PlacementChoices::UnderMouse, "UnderMouse"); + + // Don't show the option to prevent apps from remembering their window + // positions on Wayland because it doesn't work on Wayland and the feature + // will eventually be implemented in a different way there. + // This option lives in the kdeglobals file because it is consumed by + // kxmlgui. + m_ui->kcfg_AllowKDEAppsToRememberWindowPositions->setVisible(KWindowSystem::isPlatformX11()); + + m_ui->kcfg_ActivationDesktopPolicy->setItemData(KWinOptionsSettings::ActivationDesktopPolicyChoices::SwitchToOtherDesktop, "SwitchToOtherDesktop"); + m_ui->kcfg_ActivationDesktopPolicy->setItemData(KWinOptionsSettings::ActivationDesktopPolicyChoices::BringToCurrentDesktop, "BringToCurrentDesktop"); +} + +KWinMovingConfigForm::KWinMovingConfigForm(QWidget *parent) + : QWidget(parent) +{ + setupUi(parent); +} + +KMovingConfig::KMovingConfig(KWinOptionsSettings *settings, QWidget *parent) + : KCModule(parent, KPluginMetaData()) + , m_ui(new KWinMovingConfigForm(widget())) +{ + if (settings) { + initialize(settings); + } +} + +void KMovingConfig::initialize(KWinOptionsSettings *settings) +{ + m_settings = settings; + addConfig(m_settings, widget()); +} + +KPipConfig::KPipConfig(KWinOptionsSettings *settings, QWidget *parent) + : KCModule(parent, KPluginMetaData()) + , m_ui(new KWinPipConfigForm(widget())) +{ + if (settings) { + initialize(settings); + } +} + +void KPipConfig::initialize(KWinOptionsSettings *settings) +{ + m_settings = settings; + addConfig(m_settings, widget()); +} + +#include "moc_windows.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/options/windows.h b/local/recipes/kde/kwin/source/src/kcms/options/windows.h new file mode 100644 index 0000000000..19ed314800 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/options/windows.h @@ -0,0 +1,132 @@ +/* + windows.h + + SPDX-FileCopyrightText: 1997 Patrick Dowler + SPDX-FileCopyrightText: 2001 Waldo Bastian + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include "ui_advanced.h" +#include "ui_focus.h" +#include "ui_moving.h" +#include "ui_pip.h" + +class QRadioButton; +class QCheckBox; +class QPushButton; +class QGroupBox; +class QLabel; +class QSlider; +class QGroupBox; +class QSpinBox; + +class KColorButton; + +class KWinOptionsSettings; +class KWinOptionsKDEGlobalsSettings; + +class KWinFocusConfigForm : public QWidget, public Ui::KWinFocusConfigForm +{ + Q_OBJECT + +public: + explicit KWinFocusConfigForm(QWidget *parent); +}; + +class KWinMovingConfigForm : public QWidget, public Ui::KWinMovingConfigForm +{ + Q_OBJECT + +public: + explicit KWinMovingConfigForm(QWidget *parent); +}; + +class KWinAdvancedConfigForm : public QWidget, public Ui::KWinAdvancedConfigForm +{ + Q_OBJECT + +public: + explicit KWinAdvancedConfigForm(QWidget *parent); +}; + +class KWinPipConfigForm : public QWidget, public Ui::KWinPipConfigForm +{ + Q_OBJECT + +public: + explicit KWinPipConfigForm(QWidget *parent); +}; + +class KFocusConfig : public KCModule +{ + Q_OBJECT +public: + KFocusConfig(KWinOptionsSettings *settings, QWidget *parent); + + void load() override; + void save() override; + void defaults() override; + +protected: + void initialize(KWinOptionsSettings *settings); + +private Q_SLOTS: + void focusPolicyChanged(); + void updateMultiScreen(); + void updateDefaultIndicator(); + +private: + KWinFocusConfigForm *m_ui; + KWinOptionsSettings *m_settings; + + void updateFocusPolicyExplanatoryText(); +}; + +class KMovingConfig : public KCModule +{ + Q_OBJECT +public: + KMovingConfig(KWinOptionsSettings *settings, QWidget *parent); + +protected: + void initialize(KWinOptionsSettings *settings); + +private: + KWinOptionsSettings *m_settings; + KWinMovingConfigForm *m_ui; +}; + +class KAdvancedConfig : public KCModule +{ + Q_OBJECT +public: + KAdvancedConfig(KWinOptionsSettings *settings, KWinOptionsKDEGlobalsSettings *globalSettings, QWidget *parent); + +protected: + void initialize(KWinOptionsSettings *settings, KWinOptionsKDEGlobalsSettings *globalSettings); + +private: + KWinAdvancedConfigForm *m_ui; + KWinOptionsSettings *m_settings; +}; + +class KPipConfig : public KCModule +{ + Q_OBJECT + +public: + KPipConfig(KWinOptionsSettings *settings, QWidget *parent); + +protected: + void initialize(KWinOptionsSettings *settings); + +private: + KWinOptionsSettings *m_settings; + KWinPipConfigForm *m_ui; +}; diff --git a/local/recipes/kde/kwin/source/src/kcms/rules/CMakeLists.txt b/local/recipes/kde/kwin/source/src/kcms/rules/CMakeLists.txt new file mode 100644 index 0000000000..d8ee1e0ec6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/rules/CMakeLists.txt @@ -0,0 +1,46 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_kwinrules\") +add_definitions(-DKCMRULES) + +set(kwinrules_SRCS + ../../rulebooksettings.cpp + ../../rules.cpp + ../../utils/common.cpp + ../../virtualdesktopsdbustypes.cpp + optionsmodel.cpp + ruleitem.cpp + rulesmodel.cpp + rulebookmodel.cpp +) + +kconfig_add_kcfg_files(kwinrules_SRCS ../../rulesettings.kcfgc) +kconfig_add_kcfg_files(kwinrules_SRCS ../../rulebooksettingsbase.kcfgc) + +add_library(KWinRulesObjects STATIC ${kwinrules_SRCS}) +set_property(TARGET KWinRulesObjects PROPERTY POSITION_INDEPENDENT_CODE ON) + +if (KWIN_BUILD_X11) + set(kwin_kcm_rules_XCB_LIBS + XCB::XCB + XCB::XFIXES + ) +endif() + +set(kcm_libs + Qt::Quick + + KF6::KCMUtils + KF6::I18n + KF6::KCMUtilsQuick + KF6::WindowSystem + KF6::XmlGui + KF6::ColorScheme +) + +if (KWIN_BUILD_ACTIVITIES) + set(kcm_libs ${kcm_libs} Plasma::Activities) +endif() +target_link_libraries(KWinRulesObjects ${kcm_libs} ${kwin_kcm_rules_XCB_LIBS}) + +kcmutils_add_qml_kcm(kcm_kwinrules SOURCES kcmrules.cpp) +target_link_libraries(kcm_kwinrules PRIVATE KWinRulesObjects) diff --git a/local/recipes/kde/kwin/source/src/kcms/rules/Messages.sh b/local/recipes/kde/kwin/source/src/kcms/rules/Messages.sh new file mode 100644 index 0000000000..cbb5e588c5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/rules/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.cpp -o -name \*.h -o -name \*.qml` -o $podir/kcm_kwinrules.pot diff --git a/local/recipes/kde/kwin/source/src/kcms/rules/kcm_kwinrules.json b/local/recipes/kde/kwin/source/src/kcms/rules/kcm_kwinrules.json new file mode 100644 index 0000000000..e702d5105b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/rules/kcm_kwinrules.json @@ -0,0 +1,148 @@ +{ + "Categories": "Qt;KDE;X-KDE-settings-looknfeel;", + "KPlugin": { + "BugReportUrl": "https://bugs.kde.org/enter_bug.cgi?product=systemsettings&component=kcm_kwinrules", + "Description": "Individual Window Behavior", + "Description[ar]": "سلوك النافذ المفردة", + "Description[az]": "Fərdi pəncərə davranışı", + "Description[be]": "Паводзіны асобных акон", + "Description[bg]": "Индивидуално поведение на прозорците", + "Description[ca@valencia]": "Comportament individual de les finestres", + "Description[ca]": "Comportament individual de les finestres", + "Description[cs]": "Chování individuálních oken", + "Description[da]": "Individuel vinduesopførsel", + "Description[de]": "Individuelles Fensterverhalten", + "Description[en_GB]": "Individual Window Behaviour", + "Description[eo]": "Individua Fenestra Konduto", + "Description[es]": "Comportamiento de ventanas individuales", + "Description[et]": "Konkreetse akna käitumine", + "Description[eu]": "Leihoen banakako portaera", + "Description[fi]": "Yksittäisen ikkunan toiminta", + "Description[fr]": "Comportement individuel des fenêtres", + "Description[gl]": "Comportamento individual das xanelas.", + "Description[he]": "התנהגות חלון יחיד", + "Description[hu]": "Egyedi ablakműködés", + "Description[ia]": "Comportamento de fenestra individual", + "Description[id]": "Perilaku Jendela Individu", + "Description[is]": "Hegðun einstakra glugga", + "Description[it]": "Comportamento della singola finestra", + "Description[ja]": "個別のウィンドウの挙動", + "Description[ka]": "ინდივიდუალური ფანჯრების ქცევა", + "Description[ko]": "개별 창 동작", + "Description[lt]": "Atskira lango elgsena", + "Description[lv]": "Uzvedība atsevišķiem logiem", + "Description[nb]": "Atferd for enkeltvindu", + "Description[nl]": "Individueel venstergedrag", + "Description[nn]": "Åtferd for einskildvindauge", + "Description[pl]": "Wyjątkowe okna", + "Description[pt]": "Comportamento da Janela Individual", + "Description[pt_BR]": "Comportamento das janelas individuais", + "Description[ro]": "Comportament al ferestrelor individuale", + "Description[ru]": "Особые параметры конкретных окон", + "Description[sa]": "व्यक्तिगत खिडकी व्यवहार", + "Description[sk]": "Individuálne správanie okien", + "Description[sl]": "Individualno vedenje oken", + "Description[sv]": "Individuellt fönsterbeteende", + "Description[ta]": "தனிப்பட்ட சாளரங்களின் நடத்தை", + "Description[tr]": "Bireysel Pencere Davranışı", + "Description[uk]": "Поведінка окремих вікон", + "Description[vi]": "Ứng xử của riêng từng cửa sổ", + "Description[zh_CN]": "个别窗口行为", + "Description[zh_TW]": "個別視窗行為", + "FormFactors": [ + "desktop", + "tablet" + ], + "Icon": "preferences-system-windows-actions", + "Name": "Window Rules", + "Name[ar]": "قواعد النوافذ", + "Name[az]": "Pəncərə qaydası", + "Name[be]": "Правілы для акон", + "Name[bg]": "Правила за прозорци", + "Name[ca@valencia]": "Regles de les finestres", + "Name[ca]": "Regles de les finestres", + "Name[cs]": "Pravidla oken", + "Name[da]": "Vinduesroller", + "Name[de]": "Fensterregeln", + "Name[en_GB]": "Window Rules", + "Name[eo]": "Fenestraj Reguloj", + "Name[es]": "Reglas de las ventanas", + "Name[et]": "Aknareeglid", + "Name[eu]": "Leihoaren arauak", + "Name[fi]": "Ikkunasäännöt", + "Name[fr]": "Règles de fenêtres", + "Name[gl]": "Regras de xanelas", + "Name[he]": "כללי חלון", + "Name[hu]": "Ablakszabályok", + "Name[ia]": "Regulas de fenestra", + "Name[id]": "Peraturan Jendela", + "Name[is]": "Gluggareglur", + "Name[it]": "Regole delle finestre", + "Name[ja]": "ウィンドウのルール", + "Name[ka]": "ფანჯრების წესები", + "Name[ko]": "창 규칙", + "Name[lt]": "Langų taisyklės", + "Name[lv]": "Logu noteikumi", + "Name[nb]": "Vindusregler", + "Name[nl]": "Vensterregels", + "Name[nn]": "Vindaugsreglar", + "Name[pl]": "Zasady okien", + "Name[pt]": "Regras das Janelas", + "Name[pt_BR]": "Regras das janelas", + "Name[ro]": "Reguli ferestre", + "Name[ru]": "Особые параметры окон", + "Name[sa]": "विण्डो नियम", + "Name[sk]": "Pravidlá okien", + "Name[sl]": "Pravila oken", + "Name[sv]": "Fönsterregler", + "Name[ta]": "சாளர விதிமுறைகள்", + "Name[tr]": "Pencere Kuralları", + "Name[uk]": "Правила вікон", + "Name[vi]": "Luật cửa sổ", + "Name[zh_CN]": "窗口规则", + "Name[zh_TW]": "視窗規則" + }, + "X-DocPath": "kcontrol/windowspecific/index.html", + "X-KDE-Keywords": "size,position,state,window behavior,windows,specific,workarounds,remember,rules,window rules,apps,applications", + "X-KDE-Keywords[ar]": "الحجم,الموضع,الحالة,سلوك النافذة,النوافذ,محدد,الحلول البديلة,التذكر,القواعد,قواعد النافذة,التطبيقات,التطبيقات", + "X-KDE-Keywords[bg]": "размер,позиция,състояние,поведение на прозореца,прозорци,специфични,заобикалящи решения,запомнете,правила,правила за прозорец,приложения,приложения", + "X-KDE-Keywords[ca@valencia]": "mida,posició,estat,comportament de les finestres,finestres,específic,solució temporal,recorda,regles,regles de finestres,apps,aplicacions", + "X-KDE-Keywords[ca]": "mida,posició,estat,comportament de les finestres,finestres,específic,solució temporal,recorda,regles,regles de finestres,apps,aplicacions", + "X-KDE-Keywords[de]": "Größe,Position,Zustand,Fensterverhalten,Fenster,spezifisch,Übergangslösungen,Notlösungen,erinnern,Regeln,Fensterregeln,Anwendungen,Programme", + "X-KDE-Keywords[en_GB]": "size,position,state,window behavior,windows,specific,workarounds,remember,rules,window rules,apps,applications", + "X-KDE-Keywords[es]": "tamaño,posición,estado,comportamiento de la ventana,ventanas,específico,método alternativo,solución alternativa,recordar,reglas,reglas de la ventana,apps,aplicaciones", + "X-KDE-Keywords[eu]": "neurria,tamaina,kokalekua,egoera,leihoaren jokabidea,leihoak,jakina,zehatza,konponbideak,gogoratu,oroitu,araua,leihoaren arauak,aplikazioak", + "X-KDE-Keywords[fi]": "koko,sijainti,paikka,tila,ikkunan toiminta,ikkunoiden toiminta,ikkunat,muista,muistisäännöt,säännöt,ikkunasäännöt,sovellukset,ohjelmat", + "X-KDE-Keywords[fr]": "taille, position, état, comportement des fenêtres, fenêtres,spécifiques,solutions de contournement, mémoriser, règles, règles de fenêtre, applications", + "X-KDE-Keywords[gl]": "tamaño,posición,estado,comportamento da xanela,xanelas,específico,regras,window rules,apps,aplis,applications,aplicacións", + "X-KDE-Keywords[he]": "גודל,מקום,מיקום,הצבה,מצב,התנהגות חלון,חלונות,מסוים,מסוימים,מעקפים,שמירה,זכירה,כללי חלון,יישומים,יישומונים", + "X-KDE-Keywords[hu]": "méret,pozíció,állapot,ablakműködés,ablakok,specifikus,kerülőút,emlékeztető,szabályok,ablakszabályok,alkalmazások", + "X-KDE-Keywords[ia]": "grandor,position,stato,comportamento de fenestra,fenestras,specific,workarounds,memora,regulas, regulas de fenestra,apps,applicationes", + "X-KDE-Keywords[is]": "stærð,staðsetning,staða,gluggahegðun,gluggar,tilgreint,aukalausnir,muna,reglur,gluggareglur,forrit,hugbúnaður", + "X-KDE-Keywords[it]": "dimensione,posizione,stato,comportamento della finestra,finestre,specifico,espedienti,ricorda,regole,regole delle finestre,applicazioni", + "X-KDE-Keywords[ja]": "size,position,state,window behavior,windows,specific,workarounds,remember,rules,window rules,apps,applications, サイズ,大きさ,位置,状態,ウィンドウの動作,ウィンドウ動作,ウィンドウ,特定の,ワークアラウンド,記憶,保存,ルール,ウィンドウルール,アプリ,アプリケーション", + "X-KDE-Keywords[ka]": "size,position,state,window behavior,windows,specific,workarounds,remember,rules,window rules,apps,applications,აპლიკაციები,აპები,ფანჯრის წესები,წესები,დამახსოვრება,ფანჯრები,ფანჯრების ქცევა,მდებარეობა,ზომა", + "X-KDE-Keywords[ko]": "크기,위치,창 행동,창,창 지정,규칙,프로그램,앱", + "X-KDE-Keywords[lt]": "size,position,state,window behavior,windows,specific,workarounds,remember,rules,window rules,apps,applications,dydis,pozicija,būsena,lango elgsena,langų elgsena,langu elgsena,langų elgesys,langu elgesys,langai,konkretūs,konkretus,apėjimai,apejimai,prisiminti,taisyklės,taisykles,langų taisyklės,langu taisykles,lango taisyklės,lango taisykles,programos", + "X-KDE-Keywords[lv]": "izmērs,pozīcija,stāvoklis,logu uzvedība,logi,specifiski,apiešana,atcerēties,noteikumi,logu noteikumi,lietotnes,programmas", + "X-KDE-Keywords[nb]": "størrelse,plassering,tilstand,vindusatferd,vindu,enkelt,løsninger,unntak,husk,regler,vindusregler,apper,applikasjoner,program,brukerprogram", + "X-KDE-Keywords[nl]": "grootte,positie,status,venstergedrag,vensters,specifiek,omwegen,onthouden,regels,vensterregels,apps,toepassingen", + "X-KDE-Keywords[nn]": "storleik,plassering,tilstand,vindaugsåtferd,vindauge,einskild,løysingar,unntak,hugs,reglar,vindaugsreglar,appar,applikasjonar,program,brukarprogram", + "X-KDE-Keywords[pl]": "rozmiar,pozycja,stan,zachowanie okna,okna,specyficzne,obejścia,zapamiętaj,zasady,zasady okien,appki,aplikacje", + "X-KDE-Keywords[pt_BR]": "tamanho,posição,estado,comportamento de janela,janelas,específico,soluções alternativas,lembrar,regras,regras de janela,aplicativos,aplicações", + "X-KDE-Keywords[ro]": "dimensiune,mărime,poziție,stare,comportament fereastră,ferestre,specific,soluții alternative,ține minte,reguli,reguli fereastră,reguli ferestre,aplicații", + "X-KDE-Keywords[ru]": "size,position,state,window behavior,windows,specific,workarounds,remember,rules,window rules,apps,applications,размер,позиция,положение,состояние,поведение окон,окна,конкретный,обходные пути,запомнить,правила,правила окон,приложения", + "X-KDE-Keywords[sa]": "आकार,स्थिति,स्थिति,विंडो व्यवहार,विंडोज,विशिष्ट,कार्यपरिहार,स्मरण,नियम,विंडो नियम,एप्स,अनुप्रयोग", + "X-KDE-Keywords[sk]": "size,position,state,window behavior,windows,specific,workarounds,remember,rules,window rules,apps,applications", + "X-KDE-Keywords[sl]": "velikost,položaj,stanje,vedenje oken,okna,specifično,rešitve,zapomni si,pravila,pravila oken,aplikacije,programi", + "X-KDE-Keywords[sv]": "storlek,position,tillstånd,fönsterbeteende,fönster,specifik,lösningar,komma ihåg,regler,fönsterregler,program,applikationer", + "X-KDE-Keywords[tr]": "boyut,konum,durum,davranış,pencereler,başka yöntem,anımsa,kural,uygulama,app,uygulamalar", + "X-KDE-Keywords[uk]": "size,position,state,window behavior,windows,specific,workarounds,remember,rules,window rules,apps,applications,розмір,розташування,місце,стан,поведінка,вікно,вікна,поведінка вікон,окрема,специфічна,окремо,запам’ятати,пам’ять,правило,правила,правила вікон,програми", + "X-KDE-Keywords[zh_CN]": "size,position,state,window behavior,windows,specific,workarounds,remember,rules,daxiao,weizhi,chuangkouxingwei,teding,gebie,zhiding,jizhu,jiyi,jilu,guize,chuangkouguize,yingyong,yingyongchengxu,大小,位置,窗口行为,特定,个别,指定,记住,记忆,记录,规则,窗口规则,应用,应用程序", + "X-KDE-Keywords[zh_TW]": "大小,尺寸,位置,狀態,視窗行為,視窗,特定,專用,記住,規則,視窗規則,應用程式,程式", + "X-KDE-OnlyShowOnQtPlatforms": [ + "wayland" + ], + "X-KDE-System-Settings-Parent-Category": "windowmanagement", + "X-KDE-Weight": 40 +} diff --git a/local/recipes/kde/kwin/source/src/kcms/rules/kcmrules.cpp b/local/recipes/kde/kwin/source/src/kcms/rules/kcmrules.cpp new file mode 100644 index 0000000000..9621c4439a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/rules/kcmrules.cpp @@ -0,0 +1,495 @@ +/* + SPDX-FileCopyrightText: 2004 Lubos Lunak + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "kcmrules.h" +#include "rulesettings.h" + +#include +#include +#include +#include + +#include +#include +#include + +namespace KWin +{ + +KCMKWinRules::KCMKWinRules(QObject *parent, const KPluginMetaData &metaData, const QVariantList &arguments) + : KQuickConfigModule(parent, metaData) + , m_ruleBookModel(new RuleBookModel(this)) + , m_rulesModel(new RulesModel(this)) +{ + QStringList argList; + for (const QVariant &arg : arguments) { + argList << arg.toString(); + } + parseArguments(argList); + + connect(m_rulesModel, &RulesModel::descriptionChanged, this, [this] { + if (m_editIndex.isValid()) { + m_ruleBookModel->setDescriptionAt(m_editIndex.row(), m_rulesModel->description()); + } + }); + connect(m_rulesModel, &RulesModel::dataChanged, this, [this] { + Q_EMIT m_ruleBookModel->dataChanged(m_editIndex, m_editIndex, {}); + }); + connect(m_ruleBookModel, &RuleBookModel::dataChanged, this, &KCMKWinRules::updateNeedsSave); +} + +void KCMKWinRules::parseArguments(const QStringList &args) +{ + // When called from window menu, "uuid" and "whole-app" are set in arguments list + bool nextArgIsUuid = false; + QUuid uuid = QUuid(); + + // TODO: Use a better argument parser + for (const QString &arg : args) { + if (arg == QLatin1String("uuid")) { + nextArgIsUuid = true; + } else if (nextArgIsUuid) { + uuid = QUuid(arg); + nextArgIsUuid = false; + } else if (arg.startsWith("uuid=")) { + uuid = QUuid(arg.mid(strlen("uuid="))); + } else if (arg == QLatin1String("whole-app")) { + m_wholeApp = true; + } + } + + if (uuid.isNull()) { + qDebug() << "Invalid window uuid."; + return; + } + + // Get the Window properties + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"), + QStringLiteral("/KWin"), + QStringLiteral("org.kde.KWin"), + QStringLiteral("getWindowInfo")); + message.setArguments({uuid.toString()}); + QDBusPendingReply async = QDBusConnection::sessionBus().asyncCall(message); + + QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this); + connect(callWatcher, &QDBusPendingCallWatcher::finished, this, [this, uuid](QDBusPendingCallWatcher *self) { + QDBusPendingReply reply = *self; + self->deleteLater(); + if (!reply.isValid() || reply.value().isEmpty()) { + qDebug() << "Error retrieving properties for window" << uuid; + return; + } + qDebug() << "Retrieved properties for window" << uuid; + m_winProperties = reply.value(); + + if (m_alreadyLoaded) { + createRuleFromProperties(); + } + }); +} + +void KCMKWinRules::load() +{ + m_ruleBookModel->load(); + + if (!m_winProperties.isEmpty() && !m_alreadyLoaded) { + createRuleFromProperties(); + } else { + m_editIndex = QModelIndex(); + Q_EMIT editIndexChanged(); + } + + m_alreadyLoaded = true; + + updateNeedsSave(); +} + +void KCMKWinRules::save() +{ + m_ruleBookModel->save(); + + // Notify kwin to reload configuration + QDBusMessage message = QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); +} + +void KCMKWinRules::updateNeedsSave() +{ + setNeedsSave(m_ruleBookModel->isSaveNeeded()); + Q_EMIT needsSaveChanged(); +} + +void KCMKWinRules::createRuleFromProperties() +{ + if (m_winProperties.isEmpty()) { + return; + } + + QModelIndex matchedIndex = findRuleWithProperties(m_winProperties, m_wholeApp); + if (!matchedIndex.isValid()) { + m_ruleBookModel->insertRow(0); + fillSettingsFromProperties(m_ruleBookModel->ruleSettingsAt(0), m_winProperties, m_wholeApp); + matchedIndex = m_ruleBookModel->index(0); + updateNeedsSave(); + } + + editRule(matchedIndex.row()); + m_rulesModel->setSuggestedProperties(m_winProperties); + + m_winProperties.clear(); +} + +int KCMKWinRules::editIndex() const +{ + if (!m_editIndex.isValid()) { + return -1; + } + return m_editIndex.row(); +} + +void KCMKWinRules::setRuleDescription(int index, const QString &description) +{ + if (index < 0 || index >= m_ruleBookModel->rowCount()) { + return; + } + + if (m_editIndex.row() == index) { + m_rulesModel->setDescription(description); + return; + } + m_ruleBookModel->setDescriptionAt(index, description); + + updateNeedsSave(); +} + +void KCMKWinRules::editRule(int index) +{ + if (index < 0 || index >= m_ruleBookModel->rowCount()) { + return; + } + + m_editIndex = m_ruleBookModel->index(index); + Q_EMIT editIndexChanged(); + + m_rulesModel->setSettings(m_ruleBookModel->ruleSettingsAt(m_editIndex.row())); + + // Set the active page to rules editor (0:RulesList, 1:RulesEditor) + setCurrentIndex(1); +} + +void KCMKWinRules::createRule() +{ + const int newIndex = m_ruleBookModel->rowCount(); + m_ruleBookModel->insertRow(newIndex); + + updateNeedsSave(); + + editRule(newIndex); +} + +void KCMKWinRules::removeRule(int index) +{ + if (index < 0 || index >= m_ruleBookModel->rowCount()) { + return; + } + + m_ruleBookModel->removeRow(index); + + Q_EMIT editIndexChanged(); + updateNeedsSave(); +} + +void KCMKWinRules::moveRule(int sourceIndex, int destIndex) +{ + const int lastIndex = m_ruleBookModel->rowCount() - 1; + if (sourceIndex == destIndex + || (sourceIndex < 0 || sourceIndex > lastIndex) + || (destIndex < 0 || destIndex > lastIndex)) { + return; + } + + m_ruleBookModel->moveRow(QModelIndex(), sourceIndex, QModelIndex(), destIndex); + + Q_EMIT editIndexChanged(); + updateNeedsSave(); +} + +void KCMKWinRules::duplicateRule(int index) +{ + if (index < 0 || index >= m_ruleBookModel->rowCount()) { + return; + } + + const int newIndex = index + 1; + const QString newDescription = i18n("Copy of %1", m_ruleBookModel->descriptionAt(index)); + + m_ruleBookModel->insertRow(newIndex); + m_ruleBookModel->setRuleSettingsAt(newIndex, *(m_ruleBookModel->ruleSettingsAt(index))); + m_ruleBookModel->setDescriptionAt(newIndex, newDescription); + + updateNeedsSave(); +} + +void KCMKWinRules::exportToFile(const QUrl &path, const QList &indexes) +{ + if (indexes.isEmpty()) { + return; + } + + const auto config = KSharedConfig::openConfig(path.toLocalFile(), KConfig::SimpleConfig); + + const QStringList groups = config->groupList(); + for (const QString &groupName : groups) { + config->deleteGroup(groupName); + } + + for (int index : indexes) { + if (index < 0 || index > m_ruleBookModel->rowCount()) { + continue; + } + const RuleSettings *origin = m_ruleBookModel->ruleSettingsAt(index); + RuleSettings exported(config, origin->description()); + + RuleBookModel::copySettingsTo(&exported, *origin); + exported.save(); + } +} + +void KCMKWinRules::importFromFile(const QUrl &path) +{ + const auto config = KSharedConfig::openConfig(path.toLocalFile(), KConfig::SimpleConfig); + const QStringList groups = config->groupList(); + if (groups.isEmpty()) { + return; + } + + for (const QString &groupName : groups) { + RuleSettings settings(config, groupName); + + const bool remove = settings.deleteRule(); + const QString importDescription = settings.description(); + if (importDescription.isEmpty()) { + continue; + } + + // Try to find a rule with the same description to replace + int newIndex = -2; + for (int index = 0; index < m_ruleBookModel->rowCount(); index++) { + if (m_ruleBookModel->descriptionAt(index) == importDescription) { + newIndex = index; + break; + } + } + + if (remove) { + m_ruleBookModel->removeRow(newIndex); + continue; + } + + if (newIndex < 0) { + newIndex = m_ruleBookModel->rowCount(); + m_ruleBookModel->insertRow(newIndex); + } + + m_ruleBookModel->setRuleSettingsAt(newIndex, settings); + + // Reset rule editor if the current rule changed when importing + if (m_editIndex.row() == newIndex) { + m_rulesModel->setSettings(m_ruleBookModel->ruleSettingsAt(newIndex)); + } + } + + updateNeedsSave(); +} + +// Code adapted from original `findRule()` method in `kwin_rules_dialog::main.cpp` +QModelIndex KCMKWinRules::findRuleWithProperties(const QVariantMap &info, bool wholeApp) const +{ + const QString wmclass_class = info.value("resourceClass").toString(); + const QString wmclass_name = info.value("resourceName").toString(); + const QString role = info.value("role").toString(); + const WindowType type = static_cast(info.value("type").toInt()); + const QString title = info.value("caption").toString(); + const QString machine = info.value("clientMachine").toString(); + const bool isLocalHost = info.value("localhost").toBool(); + const QString tag = info.value("tag").toString(); + + int bestMatchRow = -1; + int bestMatchScore = 0; + + for (int row = 0; row < m_ruleBookModel->rowCount(); row++) { + const RuleSettings *settings = m_ruleBookModel->ruleSettingsAt(row); + + // If the rule doesn't match try the next one + const Rules rule = Rules(settings); + /* clang-format off */ + if (!rule.matchWMClass(wmclass_class, wmclass_name) + || !rule.matchType(type) + || !rule.matchRole(role) + || !rule.matchTitle(title) + || !rule.matchClientMachine(machine, isLocalHost)) { + continue; + } + /* clang-format on */ + + if (settings->wmclassmatch() != Rules::ExactMatch) { + continue; // too generic + } + + // Now that the rule matches the window, check the quality of the match + // It stablishes a quality depending on the match policy of the rule + int score = 0; + bool generic = true; + + // from now on, it matches the app - now try to match for a specific window + if (settings->wmclasscomplete()) { + score += 1; + generic = false; // this can be considered specific enough (old X apps) + } + if (!wholeApp) { + if (settings->windowrolematch() != Rules::UnimportantMatch) { + score += settings->windowrolematch() == Rules::ExactMatch ? 5 : 1; + generic = false; + } + if (settings->titlematch() != Rules::UnimportantMatch) { + score += settings->titlematch() == Rules::ExactMatch ? 3 : 1; + generic = false; + } + if (settings->tagmatch() != Rules::UnimportantMatch) { + score += settings->tagmatch() == Rules::ExactMatch ? 3 : 1; + generic = false; + } + if (settings->types() != NET::AllTypesMask) { + // Checks that type fits the mask, and only one of the types + int bits = 0; + for (unsigned int bit = 1; bit < 1U << 31; bit <<= 1) { + if (settings->types() & bit) { + ++bits; + } + } + if (bits == 1) { + score += 2; + } + } + if (generic) { // ignore generic rules, use only the ones that are for this window + continue; + } + } else { + if (settings->types() == NET::AllTypesMask) { + score += 2; + } + } + + if (score > bestMatchScore) { + bestMatchRow = row; + bestMatchScore = score; + } + } + + if (bestMatchRow < 0) { + return QModelIndex(); + } + return m_ruleBookModel->index(bestMatchRow); +} + +// Code adapted from original `findRule()` method in `kwin_rules_dialog::main.cpp` +void KCMKWinRules::fillSettingsFromProperties(RuleSettings *settings, const QVariantMap &info, bool wholeApp) const +{ + const QString wmclass_class = info.value("resourceClass").toString(); + const QString wmclass_name = info.value("resourceName").toString(); + const QString role = info.value("role").toString(); + const NET::WindowType type = static_cast(info.value("type").toInt()); + const QString title = info.value("caption").toString(); + const QString machine = info.value("clientMachine").toString(); + const QString tag = info.value("tag").toString(); + + settings->setDefaults(); + + if (wholeApp) { + if (!wmclass_class.isEmpty()) { + settings->setDescription(i18n("Application settings for %1", wmclass_class)); + } + // TODO maybe exclude some types? If yes, then also exclude them when searching. + settings->setTypes(NET::AllTypesMask); + settings->setTitlematch(Rules::UnimportantMatch); + settings->setClientmachine(machine); // set, but make unimportant + settings->setClientmachinematch(Rules::UnimportantMatch); + settings->setWindowrolematch(Rules::UnimportantMatch); + if (wmclass_name == wmclass_class) { + settings->setWmclasscomplete(false); + settings->setWmclass(wmclass_class); + settings->setWmclassmatch(Rules::ExactMatch); + } else { + // WM_CLASS components differ - perhaps the app got -name argument + settings->setWmclasscomplete(true); + settings->setWmclass(QStringLiteral("%1 %2").arg(wmclass_name, wmclass_class)); + settings->setWmclassmatch(Rules::ExactMatch); + } + return; + } + + if (!wmclass_class.isEmpty()) { + settings->setDescription(i18n("Window settings for %1", wmclass_class)); + } + if (type == NET::Unknown) { + settings->setTypes(NET::NormalMask); + } else { + settings->setTypes(NET::WindowTypeMask(1 << type)); // convert type to its mask + } + settings->setTitle(title); // set, but make unimportant + settings->setTitlematch(Rules::UnimportantMatch); + settings->setClientmachine(machine); // set, but make unimportant + settings->setClientmachinematch(Rules::UnimportantMatch); + if (!role.isEmpty() && role != "unknown" && role != "unnamed") { // Qt sets this if not specified + settings->setWindowrole(role); + settings->setWindowrolematch(Rules::ExactMatch); + if (wmclass_name == wmclass_class) { + settings->setWmclasscomplete(false); + settings->setWmclass(wmclass_class); + settings->setWmclassmatch(Rules::ExactMatch); + } else { + // WM_CLASS components differ - perhaps the app got -name argument + settings->setWmclasscomplete(true); + settings->setWmclass(QStringLiteral("%1 %2").arg(wmclass_name, wmclass_class)); + settings->setWmclassmatch(Rules::ExactMatch); + } + } else { // no role set + if (wmclass_name != wmclass_class) { + // WM_CLASS components differ - perhaps the app got -name argument + settings->setWmclasscomplete(true); + settings->setWmclass(QStringLiteral("%1 %2").arg(wmclass_name, wmclass_class)); + settings->setWmclassmatch(Rules::ExactMatch); + } else { + // This is a window that has no role set, and both components of WM_CLASS + // match (possibly only differing in case), which most likely means either + // the application doesn't give a damn about distinguishing its various + // windows, or it's an app that uses role for that, but this window + // lacks it for some reason. Use non-complete WM_CLASS matching, also + // include window title in the matching, and pray it causes many more positive + // matches than negative matches. + // WM_CLASS components differ - perhaps the app got -name argument + settings->setTitlematch(Rules::ExactMatch); + settings->setWmclasscomplete(false); + settings->setWmclass(wmclass_class); + settings->setWmclassmatch(Rules::ExactMatch); + } + } + settings->setTag(tag); + if (tag.isEmpty()) { + settings->setTagmatch(Rules::UnimportantMatch); + } else { + settings->setTagmatch(Rules::ExactMatch); + } +} + +K_PLUGIN_CLASS_WITH_JSON(KCMKWinRules, "kcm_kwinrules.json"); + +} // namespace + +#include "kcmrules.moc" + +#include "moc_kcmrules.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/rules/kcmrules.h b/local/recipes/kde/kwin/source/src/kcms/rules/kcmrules.h new file mode 100644 index 0000000000..377820de33 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/rules/kcmrules.h @@ -0,0 +1,69 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include "rulebookmodel.h" +#include "rulesmodel.h" + +#include + +namespace KWin +{ +class RuleSettings; + +class KCMKWinRules : public KQuickConfigModule +{ + Q_OBJECT + + Q_PROPERTY(RuleBookModel *ruleBookModel MEMBER m_ruleBookModel CONSTANT) + Q_PROPERTY(RulesModel *rulesModel MEMBER m_rulesModel CONSTANT) + Q_PROPERTY(int editIndex READ editIndex NOTIFY editIndexChanged) + +public: + explicit KCMKWinRules(QObject *parent, const KPluginMetaData &metaData, const QVariantList &arguments); + + Q_INVOKABLE void setRuleDescription(int index, const QString &description); + Q_INVOKABLE void editRule(int index); + + Q_INVOKABLE void createRule(); + Q_INVOKABLE void removeRule(int index); + Q_INVOKABLE void moveRule(int sourceIndex, int destIndex); + Q_INVOKABLE void duplicateRule(int index); + + Q_INVOKABLE void exportToFile(const QUrl &path, const QList &indexes); + Q_INVOKABLE void importFromFile(const QUrl &path); + +public Q_SLOTS: + void load() override; + void save() override; + +Q_SIGNALS: + void editIndexChanged(); + +private Q_SLOTS: + void updateNeedsSave(); + +private: + int editIndex() const; + void parseArguments(const QStringList &args); + void createRuleFromProperties(); + + QModelIndex findRuleWithProperties(const QVariantMap &info, bool wholeApp) const; + void fillSettingsFromProperties(RuleSettings *settings, const QVariantMap &info, bool wholeApp) const; + +private: + RuleBookModel *m_ruleBookModel; + RulesModel *m_rulesModel; + + QPersistentModelIndex m_editIndex; + + bool m_alreadyLoaded = false; + QVariantMap m_winProperties; + bool m_wholeApp = false; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/kcms/rules/optionsmodel.cpp b/local/recipes/kde/kwin/source/src/kcms/rules/optionsmodel.cpp new file mode 100644 index 0000000000..80f788899c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/rules/optionsmodel.cpp @@ -0,0 +1,249 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "optionsmodel.h" + +#include + +namespace KWin +{ + +QHash OptionsModel::roleNames() const +{ + return { + {Qt::DisplayRole, QByteArrayLiteral("display")}, + {Qt::DecorationRole, QByteArrayLiteral("decoration")}, + {Qt::ToolTipRole, QByteArrayLiteral("tooltip")}, + {ValueRole, QByteArrayLiteral("value")}, + {IconNameRole, QByteArrayLiteral("iconName")}, + {OptionTypeRole, QByteArrayLiteral("optionType")}, + {BitMaskRole, QByteArrayLiteral("bitMask")}, + }; +} + +int OptionsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_data.size(); +} + +QVariant OptionsModel::data(const QModelIndex &index, int role) const +{ + if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) { + return QVariant(); + } + + const Data item = m_data.at(index.row()); + + switch (role) { + case Qt::DisplayRole: + return item.text; + case Qt::UserRole: + return item.value; + case Qt::DecorationRole: + return item.icon; + case IconNameRole: + return item.icon.name(); + case Qt::ToolTipRole: + return item.description; + case OptionTypeRole: + return item.optionType; + case BitMaskRole: + return bitMask(index.row()); + } + return QVariant(); +} + +int OptionsModel::selectedIndex() const +{ + return m_index; +} + +int OptionsModel::indexOf(const QVariant &value) const +{ + for (int index = 0; index < m_data.count(); index++) { + if (m_data.at(index).value == value) { + return index; + } + } + return -1; +} + +QString OptionsModel::textOfValue(const QVariant &value) const +{ + int index = indexOf(value); + if (index < 0 || index >= m_data.count()) { + return QString(); + } + return m_data.at(index).text; +} + +QVariant OptionsModel::value() const +{ + if (m_data.isEmpty()) { + return QVariant(); + } + if (m_data.at(m_index).optionType == SelectAllOption) { + return allValues(); + } + return m_data.at(m_index).value; +} + +void OptionsModel::setValue(QVariant value) +{ + if (this->value() == value) { + return; + } + int index = indexOf(value); + if (index >= 0 && index != m_index) { + m_index = index; + Q_EMIT selectedIndexChanged(index); + } +} + +void OptionsModel::resetValue() +{ + m_index = 0; + Q_EMIT selectedIndexChanged(m_index); +} + +bool OptionsModel::useFlags() const +{ + return m_useFlags; +}; + +uint OptionsModel::bitMask(int index) const +{ + const Data item = m_data.at(index); + + if (item.optionType == SelectAllOption) { + return allOptionsMask(); + } + if (m_useFlags) { + return item.value.toUInt(); + } + return 1u << index; +} + +QVariant OptionsModel::allValues() const +{ + if (m_useFlags) { + return allOptionsMask(); + } + + QVariantList list; + for (const Data &item : std::as_const(m_data)) { + if (item.optionType == NormalOption) { + list << item.value; + } + } + return list; +} + +uint OptionsModel::allOptionsMask() const +{ + uint mask = 0; + for (int index = 0; index < m_data.count(); index++) { + if (m_data.at(index).optionType == NormalOption) { + mask += bitMask(index); + } + } + return mask; +} + +void OptionsModel::updateModelData(const QList &data) +{ + beginResetModel(); + m_data = data; + endResetModel(); + Q_EMIT modelUpdated(); +} + +RulePolicy::Type RulePolicy::type() const +{ + return m_type; +} + +int RulePolicy::value() const +{ + if (m_type == RulePolicy::NoPolicy) { + return Rules::Apply; // To simplify external checks when rule has no policy + } + return OptionsModel::value().toInt(); +} + +QString RulePolicy::policyKey(const QString &key) const +{ + switch (m_type) { + case NoPolicy: + return QString(); + case StringMatch: + return QStringLiteral("%1match").arg(key); + case SetRule: + case ForceRule: + return QStringLiteral("%1rule").arg(key); + } + + return QString(); +} + +QList RulePolicy::policyOptions(RulePolicy::Type type) +{ + static const auto stringMatchOptions = QList{ + {Rules::UnimportantMatch, i18n("Unimportant")}, + {Rules::ExactMatch, i18n("Exact match")}, + {Rules::SubstringMatch, i18n("Substring match")}, + {Rules::RegExpMatch, i18n("Regular expression")}}; + + static const auto setRuleOptions = QList{ + {Rules::Apply, + i18n("Apply initially"), + i18n("The window property will be only set to the given value after the window is created." + "\nNo further changes will be affected.")}, + {Rules::ApplyNow, + i18n("Apply now"), + i18n("The window property will be set to the given value immediately and will not be affected later" + "\n(this action will be deleted afterwards).")}, + {Rules::Remember, + i18n("Remember"), + i18n("The value of the window property will be remembered and, every time the window" + " is created, the last remembered value will be applied.")}, + {Rules::DontAffect, + i18n("Do not affect"), + i18n("The window property will not be affected and therefore the default handling for it will be used." + "\nSpecifying this will block more generic window settings from taking effect.")}, + {Rules::Force, + i18n("Force"), + i18n("The window property will be always forced to the given value.")}, + {Rules::ForceTemporarily, + i18n("Force temporarily"), + i18n("The window property will be forced to the given value until it is hidden" + "\n(this action will be deleted after the window is hidden).")}}; + + static auto forceRuleOptions = QList{ + setRuleOptions.at(4), // Rules::Force + setRuleOptions.at(5), // Rules::ForceTemporarily + setRuleOptions.at(3), // Rules::DontAffect + }; + + switch (type) { + case NoPolicy: + return {}; + case StringMatch: + return stringMatchOptions; + case SetRule: + return setRuleOptions; + case ForceRule: + return forceRuleOptions; + } + return {}; +} + +} // namespace + +#include "moc_optionsmodel.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/rules/optionsmodel.h b/local/recipes/kde/kwin/source/src/kcms/rules/optionsmodel.h new file mode 100644 index 0000000000..fee2fd7765 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/rules/optionsmodel.h @@ -0,0 +1,129 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include + +#include +#include +#include + +namespace KWin +{ + +class OptionsModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int selectedIndex READ selectedIndex NOTIFY selectedIndexChanged) + Q_PROPERTY(int allOptionsMask READ allOptionsMask NOTIFY modelUpdated) + Q_PROPERTY(int useFlags READ useFlags CONSTANT) + +public: + enum OptionsRole { + ValueRole = Qt::UserRole, + IconNameRole, + OptionTypeRole, // The type of an option item, defaults to NormalOption + BitMaskRole, + }; + Q_ENUM(OptionsRole) + + enum OptionType { + NormalOption = 0, /**< Normal option */ + ExclusiveOption, /**< An exclusive option, so all other option items are deselected when this one is selected */ + SelectAllOption, /**< All option items are selected when this option item is selected */ + }; + Q_ENUM(OptionType) + + struct Data + { + Data(const QVariant &value, const QString &text, const QIcon &icon = {}, const QString &description = {}, OptionType optionType = NormalOption) + : value(value) + , text(text) + , icon(icon) + , description(description) + , optionType(optionType) + { + } + Data(const QVariant &value, const QString &text, const QString &description) + : value(value) + , text(text) + , description(description) + { + } + + QVariant value; + QString text; + QIcon icon; + QString description; + OptionType optionType = NormalOption; + }; + +public: + OptionsModel(QList data = {}, bool useFlags = false) + : QAbstractListModel() + , m_data(data) + , m_index(0) + , m_useFlags(useFlags){}; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + QVariant value() const; + void setValue(QVariant value); + void resetValue(); + + bool useFlags() const; + QVariant allValues() const; + uint allOptionsMask() const; + + void updateModelData(const QList &data); + + Q_INVOKABLE int indexOf(const QVariant &value) const; + Q_INVOKABLE QString textOfValue(const QVariant &value) const; + int selectedIndex() const; + uint bitMask(int index) const; + +Q_SIGNALS: + void selectedIndexChanged(int index); + void modelUpdated(); + +public: + QList m_data; + +protected: + int m_index = 0; + bool m_useFlags = false; +}; + +class RulePolicy : public OptionsModel +{ +public: + enum Type { + NoPolicy, + StringMatch, + SetRule, + ForceRule + }; + +public: + RulePolicy(Type type) + : OptionsModel(policyOptions(type)) + , m_type(type){}; + + Type type() const; + int value() const; + QString policyKey(const QString &key) const; + +private: + static QList policyOptions(RulePolicy::Type type); + +private: + Type m_type; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/kcms/rules/rulebookmodel.cpp b/local/recipes/kde/kwin/source/src/kcms/rules/rulebookmodel.cpp new file mode 100644 index 0000000000..f31edd8567 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/rules/rulebookmodel.cpp @@ -0,0 +1,204 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "rulebookmodel.h" + +namespace KWin +{ + +RuleBookModel::RuleBookModel(QObject *parent) + : QAbstractListModel(parent) + , m_ruleBook(new RuleBookSettings(this)) +{ +} + +RuleBookModel::~RuleBookModel() +{ +} + +QHash RuleBookModel::roleNames() const +{ + auto roles = QAbstractListModel::roleNames(); + roles.insert(DescriptionRole, QByteArray("display")); + roles.insert(EnabledRole, QByteArray("isEnabled")); + return roles; +} + +int RuleBookModel::rowCount(const QModelIndex &parent) const +{ + return m_ruleBook->ruleCount(); +} + +QVariant RuleBookModel::data(const QModelIndex &index, int role) const +{ + if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) { + return QVariant(); + } + + if (index.row() < 0 || index.row() >= rowCount()) { + return QVariant(); + } + + const RuleSettings *settings = m_ruleBook->ruleSettingsAt(index.row()); + + switch (role) { + case RuleBookModel::DescriptionRole: + return settings->description(); + case RuleBookModel::EnabledRole: + return settings->enabled(); + } + + return QVariant(); +} + +bool RuleBookModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) { + return false; + } + + RuleSettings *settings = m_ruleBook->ruleSettingsAt(index.row()); + + switch (role) { + case RuleBookModel::DescriptionRole: + if (settings->description() == value.toString()) { + return true; + } + settings->setDescription(value.toString()); + break; + case RuleBookModel::EnabledRole: + if (settings->enabled() == value.toBool()) { + return true; + } + settings->setEnabled(value.toBool()); + break; + default: + return false; + } + + Q_EMIT dataChanged(index, index, {role}); + + return true; +} + +bool RuleBookModel::insertRows(int row, int count, const QModelIndex &parent) +{ + if (row < 0 || row > rowCount() || parent.isValid()) { + return false; + } + + beginInsertRows(parent, row, row + count - 1); + for (int i = 0; i < count; i++) { + RuleSettings *settings = m_ruleBook->insertRuleSettingsAt(row + i); + settings->setWmclassmatch(Rules::ExactMatch); // We want ExactMatch as default for new rules in the UI + } + endInsertRows(); + + return true; +} + +bool RuleBookModel::removeRows(int row, int count, const QModelIndex &parent) +{ + if (row < 0 || row > rowCount() || parent.isValid()) { + return false; + } + + beginRemoveRows(parent, row, row + count - 1); + for (int i = 0; i < count; i++) { + m_ruleBook->removeRuleSettingsAt(row + i); + } + endRemoveRows(); + + return true; +} + +bool RuleBookModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int count, + const QModelIndex &destinationParent, int destinationChild) +{ + if (sourceParent != destinationParent || sourceParent != QModelIndex()) { + return false; + } + + const bool isMoveDown = destinationChild > sourceRow; + // QAbstractItemModel::beginMoveRows(): when moving rows down in the same parent, + // the rows will be placed before the destinationChild index. + if (!beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, + destinationParent, isMoveDown ? destinationChild + 1 : destinationChild)) { + return false; + } + + for (int i = 0; i < count; i++) { + m_ruleBook->moveRuleSettings(isMoveDown ? sourceRow : sourceRow + i, destinationChild); + } + + endMoveRows(); + return true; +} + +QString RuleBookModel::descriptionAt(int row) const +{ + Q_ASSERT(row >= 0 && row < rowCount()); + return m_ruleBook->ruleSettingsAt(row)->description(); +} + +RuleSettings *RuleBookModel::ruleSettingsAt(int row) const +{ + Q_ASSERT(row >= 0 && row < rowCount()); + return m_ruleBook->ruleSettingsAt(row); +} + +void RuleBookModel::setDescriptionAt(int row, const QString &description) +{ + Q_ASSERT(row >= 0 && row < rowCount()); + if (description == m_ruleBook->ruleSettingsAt(row)->description()) { + return; + } + + m_ruleBook->ruleSettingsAt(row)->setDescription(description); + + Q_EMIT dataChanged(index(row), index(row), {}); +} + +void RuleBookModel::setRuleSettingsAt(int row, const RuleSettings &settings) +{ + Q_ASSERT(row >= 0 && row < rowCount()); + + copySettingsTo(ruleSettingsAt(row), settings); + + Q_EMIT dataChanged(index(row), index(row), {}); +} + +void RuleBookModel::load() +{ + beginResetModel(); + + m_ruleBook->load(); + + endResetModel(); +} + +void RuleBookModel::save() +{ + m_ruleBook->save(); +} + +bool RuleBookModel::isSaveNeeded() +{ + return m_ruleBook->usrIsSaveNeeded(); +} + +void RuleBookModel::copySettingsTo(RuleSettings *dest, const RuleSettings &source) +{ + dest->setDefaults(); + const KConfigSkeletonItem::List itemList = source.items(); + for (const KConfigSkeletonItem *item : itemList) { + dest->findItem(item->name())->setProperty(item->property()); + } +} + +} // namespace + +#include "moc_rulebookmodel.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/rules/rulebookmodel.h b/local/recipes/kde/kwin/source/src/kcms/rules/rulebookmodel.h new file mode 100644 index 0000000000..3ec03697b2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/rules/rulebookmodel.h @@ -0,0 +1,57 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include "rulebooksettings.h" +#include "rulesettings.h" + +#include + +namespace KWin +{ + +class RuleBookModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum { + DescriptionRole = Qt::DisplayRole, + EnabledRole = Qt::UserRole, + }; + + explicit RuleBookModel(QObject *parent = nullptr); + ~RuleBookModel(); + + QHash roleNames() const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + + bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; + bool moveRows(const QModelIndex &sourceParent, int sourceRow, int count, + const QModelIndex &destinationParent, int destinationChild) override; + + QString descriptionAt(int row) const; + void setDescriptionAt(int row, const QString &description); + + RuleSettings *ruleSettingsAt(int row) const; + void setRuleSettingsAt(int row, const RuleSettings &settings); + + void load(); + void save(); + bool isSaveNeeded(); + + // Helper function to copy RuleSettings properties + static void copySettingsTo(RuleSettings *dest, const RuleSettings &source); + +private: + RuleBookSettings *m_ruleBook; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/kcms/rules/ruleitem.cpp b/local/recipes/kde/kwin/source/src/kcms/rules/ruleitem.cpp new file mode 100644 index 0000000000..b246a212d9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/rules/ruleitem.cpp @@ -0,0 +1,213 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "ruleitem.h" + +namespace KWin +{ + +RuleItem::RuleItem(const QString &key, + const RulePolicy::Type policyType, + const RuleItem::Type type, + const QString &name, + const QString §ion, + const QIcon &icon, + const QString &description) + : m_key(key) + , m_type(type) + , m_name(name) + , m_section(section) + , m_icon(icon) + , m_description(description) + , m_flags(NoFlags) + , m_enabled(false) + , m_policy(std::make_unique(policyType)) +{ + reset(); +} + +void RuleItem::reset() +{ + m_enabled = hasFlag(AlwaysEnabled) || hasFlag(StartEnabled); + m_value = typedValue(QVariant()); + m_suggestedValue = QVariant(); + m_policy->resetValue(); + if (m_options) { + m_options->resetValue(); + } +} + +QString RuleItem::key() const +{ + return m_key; +} + +QString RuleItem::name() const +{ + return m_name; +} + +QString RuleItem::section() const +{ + return m_section; +} + +QString RuleItem::iconName() const +{ + return m_icon.name(); +} + +QIcon RuleItem::icon() const +{ + return m_icon; +} + +QString RuleItem::description() const +{ + return m_description; +} + +bool RuleItem::isEnabled() const +{ + return m_enabled; +} + +void RuleItem::setEnabled(bool enabled) +{ + m_enabled = (enabled && !hasFlag(SuggestionOnly)) || hasFlag(AlwaysEnabled); +} + +bool RuleItem::hasFlag(RuleItem::Flags flag) const +{ + return m_flags.testFlag(flag); +} + +void RuleItem::setFlag(RuleItem::Flags flag, bool active) +{ + m_flags.setFlag(flag, active); +} + +RuleItem::Type RuleItem::type() const +{ + return m_type; +} + +QVariant RuleItem::value() const +{ + if (m_options && m_type == Option) { + return m_options->value(); + } + return m_value; +} + +void RuleItem::setValue(QVariant value) +{ + if (m_options && m_type == Option) { + m_options->setValue(typedValue(value)); + } + m_value = typedValue(value); +} + +QVariant RuleItem::suggestedValue() const +{ + return m_suggestedValue; +} + +void RuleItem::setSuggestedValue(QVariant value) +{ + m_suggestedValue = value.isNull() ? QVariant() : typedValue(value); +} + +QVariant RuleItem::options() const +{ + if (!m_options) { + return QVariant(); + } + return QVariant::fromValue(m_options.get()); +} + +void RuleItem::setOptionsData(const QList &data) +{ + if (m_type != Option && m_type != OptionList && m_type != NetTypes) { + return; + } + if (!m_options) { + m_options = std::make_unique(QList{}, m_type == NetTypes); + } + m_options->updateModelData(data); + m_options->setValue(m_value); +} + +int RuleItem::policy() const +{ + return m_policy->value(); +} + +void RuleItem::setPolicy(int policy) +{ + m_policy->setValue(policy); +} + +RulePolicy::Type RuleItem::policyType() const +{ + return m_policy->type(); +} + +QVariant RuleItem::policyModel() const +{ + return QVariant::fromValue(m_policy.get()); +} + +QString RuleItem::policyKey() const +{ + return m_policy->policyKey(m_key); +} + +QVariant RuleItem::typedValue(const QVariant &value) const +{ + switch (type()) { + case Undefined: + case Option: + if (value.typeId() == QMetaType::QStringList) { + // The setting is defined as a `StringList`, but we need a `QString` + // to check for a single option. Main case: Virtual Desktops on X11 + return value.toString(); + } + return value; + case Boolean: + return value.toBool(); + case Integer: + case Percentage: + return value.toInt(); + case NetTypes: { + const uint typesMask = m_options ? value.toUInt() & m_options->allOptionsMask() : 0; // filter by the allowed mask in the model + if (typesMask == 0 || typesMask == m_options->allOptionsMask()) { // if no types or all of them are selected + return 0U - 1; // return an all active mask (NET:AllTypesMask) + } + return typesMask; + } + case Point: { + const QPoint point = value.toPoint(); + return (point == invalidPoint) ? QPoint(0, 0) : point; + } + case Size: + return value.toSize(); + case String: + if (value.typeId() == QMetaType::QStringList && !value.toStringList().isEmpty()) { + return value.toStringList().at(0).trimmed(); + } + return value.toString().trimmed(); + case Shortcut: + return value.toString(); + case OptionList: + return value.toStringList(); + } + return value; +} + +} // namespace + +#include "moc_ruleitem.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/rules/ruleitem.h b/local/recipes/kde/kwin/source/src/kcms/rules/ruleitem.h new file mode 100644 index 0000000000..d02e5b15dc --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/rules/ruleitem.h @@ -0,0 +1,108 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include "optionsmodel.h" + +#include +#include + +namespace KWin +{ + +class RuleItem : public QObject +{ + Q_OBJECT + +public: + enum Type { + Undefined, + Boolean, + String, + Integer, + Option, + NetTypes, + Percentage, + Point, + Size, + Shortcut, + OptionList, + }; + Q_ENUM(Type) + + enum Flags { + NoFlags = 0, + AlwaysEnabled = 1u << 0, + StartEnabled = 1u << 1, + AffectsWarning = 1u << 2, + AffectsDescription = 1u << 3, + SuggestionOnly = 1u << 4, + AllFlags = 0b11111 + }; + +public: + RuleItem(){}; + RuleItem(const QString &key, + const RulePolicy::Type policyType, + const Type type, + const QString &name, + const QString §ion, + const QIcon &icon = QIcon::fromTheme("window"), + const QString &description = QString("")); + + QString key() const; + QString name() const; + QString section() const; + QIcon icon() const; + QString iconName() const; + QString description() const; + + bool isEnabled() const; + void setEnabled(bool enabled); + + bool hasFlag(RuleItem::Flags flag) const; + void setFlag(RuleItem::Flags flag, bool active = true); + + Type type() const; + QVariant value() const; + void setValue(QVariant value); + QVariant suggestedValue() const; + void setSuggestedValue(QVariant value); + + QVariant options() const; + void setOptionsData(const QList &data); + + RulePolicy::Type policyType() const; + int policy() const; // int belongs to anonymous enum in Rules:: + void setPolicy(int policy); // int belongs to anonymous enum in Rules:: + QVariant policyModel() const; + QString policyKey() const; + + void reset(); + +private: + QVariant typedValue(const QVariant &value) const; + +private: + QString m_key; + RuleItem::Type m_type; + QString m_name; + QString m_section; + QIcon m_icon; + QString m_description; + QFlags m_flags; + + bool m_enabled; + + QVariant m_value; + QVariant m_suggestedValue; + + std::unique_ptr m_policy; + std::unique_ptr m_options; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/kcms/rules/rulesmodel.cpp b/local/recipes/kde/kwin/source/src/kcms/rules/rulesmodel.cpp new file mode 100644 index 0000000000..3c2cfa67d6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/rules/rulesmodel.cpp @@ -0,0 +1,993 @@ +/* + SPDX-FileCopyrightText: 2004 Lubos Lunak + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "rulesmodel.h" + +#if KWIN_BUILD_ACTIVITIES +#include "activities.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace KWin +{ + +RulesModel::RulesModel(QObject *parent) + : QAbstractListModel(parent) +{ + qmlRegisterUncreatableType("org.kde.kcms.kwinrules", 1, 0, "RuleItem", + QStringLiteral("Do not create objects of type RuleItem")); + qmlRegisterUncreatableType("org.kde.kcms.kwinrules", 1, 0, "RulesModel", + QStringLiteral("Do not create objects of type RulesModel")); + qmlRegisterUncreatableType("org.kde.kcms.kwinrules", 1, 0, "OptionsModel", + QStringLiteral("Do not create objects of type OptionsModel")); + + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + populateRuleList(); +} + +RulesModel::~RulesModel() +{ +} + +QHash RulesModel::roleNames() const +{ + return { + {KeyRole, QByteArrayLiteral("key")}, + {NameRole, QByteArrayLiteral("name")}, + {IconRole, QByteArrayLiteral("icon")}, + {IconNameRole, QByteArrayLiteral("iconName")}, + {SectionRole, QByteArrayLiteral("section")}, + {DescriptionRole, QByteArrayLiteral("description")}, + {EnabledRole, QByteArrayLiteral("enabled")}, + {SelectableRole, QByteArrayLiteral("selectable")}, + {ValueRole, QByteArrayLiteral("value")}, + {TypeRole, QByteArrayLiteral("type")}, + {PolicyRole, QByteArrayLiteral("policy")}, + {PolicyModelRole, QByteArrayLiteral("policyModel")}, + {OptionsModelRole, QByteArrayLiteral("options")}, + {SuggestedValueRole, QByteArrayLiteral("suggested")}, + }; +} + +int RulesModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_ruleList.size(); +} + +QVariant RulesModel::data(const QModelIndex &index, int role) const +{ + if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) { + return QVariant(); + } + + const RuleItem *rule = m_ruleList.at(index.row()); + + switch (role) { + case KeyRole: + return rule->key(); + case NameRole: + return rule->name(); + case IconRole: + return rule->icon(); + case IconNameRole: + return rule->iconName(); + case DescriptionRole: + return rule->description(); + case SectionRole: + return rule->section(); + case EnabledRole: + return rule->isEnabled(); + case SelectableRole: + return !rule->hasFlag(RuleItem::AlwaysEnabled) && !rule->hasFlag(RuleItem::SuggestionOnly); + case ValueRole: + return rule->value(); + case TypeRole: + return rule->type(); + case PolicyRole: + return rule->policy(); + case PolicyModelRole: + return rule->policyModel(); + case OptionsModelRole: + return rule->options(); + case SuggestedValueRole: + return rule->suggestedValue(); + } + return QVariant(); +} + +bool RulesModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) { + return false; + } + + RuleItem *rule = m_ruleList.at(index.row()); + + switch (role) { + case EnabledRole: + if (value.toBool() == rule->isEnabled()) { + return true; + } + rule->setEnabled(value.toBool()); + break; + case ValueRole: + if (rule->hasFlag(RuleItem::SuggestionOnly)) { + processSuggestion(rule->key(), value); + } + if (value == rule->value()) { + return true; + } + rule->setValue(value); + break; + case PolicyRole: + if (value.toInt() == rule->policy()) { + return true; + } + rule->setPolicy(value.toInt()); + break; + case SuggestedValueRole: + if (value == rule->suggestedValue()) { + return true; + } + rule->setSuggestedValue(value); + break; + default: + return false; + } + + writeToSettings(rule); + + Q_EMIT dataChanged(index, index, QList{role}); + if (rule->hasFlag(RuleItem::AffectsDescription)) { + Q_EMIT descriptionChanged(); + } + if (rule->hasFlag(RuleItem::AffectsWarning)) { + Q_EMIT warningMessagesChanged(); + } + + return true; +} + +QModelIndex RulesModel::indexOf(const QString &key) const +{ + const QModelIndexList indexes = match(index(0), RulesModel::KeyRole, key, 1, Qt::MatchFixedString); + if (indexes.isEmpty()) { + return QModelIndex(); + } + return indexes.at(0); +} + +RuleItem *RulesModel::addRule(RuleItem *rule) +{ + m_ruleList << rule; + m_rules.insert(rule->key(), rule); + + return rule; +} + +bool RulesModel::hasRule(const QString &key) const +{ + return m_rules.contains(key); +} + +RuleItem *RulesModel::ruleItem(const QString &key) const +{ + return m_rules.value(key); +} + +QString RulesModel::description() const +{ + const QString desc = m_rules["description"]->value().toString(); + if (!desc.isEmpty()) { + return desc; + } + return defaultDescription(); +} + +void RulesModel::setDescription(const QString &description) +{ + setData(indexOf("description"), description, RulesModel::ValueRole); +} + +QString RulesModel::defaultDescription() const +{ + const QString wmclass = m_rules["wmclass"]->value().toString(); + const QString title = m_rules["title"]->isEnabled() ? m_rules["title"]->value().toString() : QString(); + + if (!title.isEmpty()) { + return i18n("Window settings for %1", title); + } + if (m_rules["tag"]->isEnabled()) { + return i18n("Window settings for %1", m_rules["tag"]->value().toString()); + } + if (!wmclass.isEmpty()) { + return i18n("Settings for %1", wmclass); + } + + return i18n("New window settings"); +} + +void RulesModel::processSuggestion(const QString &key, const QVariant &value) +{ + if (key == QLatin1String("wmclasshelper")) { + setData(indexOf("wmclass"), value, RulesModel::ValueRole); + setData(indexOf("wmclasscomplete"), true, RulesModel::ValueRole); + } +} + +QStringList RulesModel::warningMessages() const +{ + QStringList messages; + + if (wmclassWarning()) { + messages << i18n("You have specified the window class as unimportant.\n" + "This means the settings will possibly apply to windows from all applications." + " If you really want to create a generic setting, it is recommended" + " you at least limit the window types to avoid special window types."); + } + + if (geometryWarning()) { + messages << i18n("Some applications set their own geometry after starting," + " overriding your initial settings for size and position. " + "To enforce these settings, also force the property \"%1\" to \"Yes\".", + m_rules["ignoregeometry"]->name()); + } + + if (opacityWarning()) { + messages << i18n("Readability may be impaired with extremely low opacity values. At 0%, the window becomes invisible."); + } + + return messages; +} + +bool RulesModel::wmclassWarning() const +{ + const bool no_wmclass = !m_rules["wmclass"]->isEnabled() + || m_rules["wmclass"]->policy() == Rules::UnimportantMatch; + const bool alltypes = !m_rules["types"]->isEnabled() + || (m_rules["types"]->value() == 0) + || (m_rules["types"]->value() == NET::AllTypesMask) + || ((m_rules["types"]->value().toInt() | (1 << NET::Override)) == 0x3FF); + + return (no_wmclass && alltypes); +} + +bool RulesModel::geometryWarning() const +{ + const bool ignoregeometry = m_rules["ignoregeometry"]->isEnabled() + && m_rules["ignoregeometry"]->policy() == Rules::Force + && m_rules["ignoregeometry"]->value() == true; + + const bool initialPos = m_rules["position"]->isEnabled() + && (m_rules["position"]->policy() == Rules::Apply + || m_rules["position"]->policy() == Rules::Remember); + + const bool initialSize = m_rules["size"]->isEnabled() + && (m_rules["size"]->policy() == Rules::Apply + || m_rules["size"]->policy() == Rules::Remember); + + const bool initialPlacement = m_rules["placement"]->isEnabled() + && m_rules["placement"]->policy() == Rules::Force; + + return (!ignoregeometry && (initialPos || initialSize || initialPlacement)); +} + +bool RulesModel::opacityWarning() const +{ + auto opacityActive = m_rules["opacityactive"]; + const bool lowOpacityActive = opacityActive->isEnabled() + && opacityActive->policy() != Rules::Unused && opacityActive->policy() != Rules::DontAffect + && opacityActive->value().toInt() < 25; + + auto opacityInactive = m_rules["opacityinactive"]; + const bool lowOpacityInactive = opacityInactive->isEnabled() + && opacityActive->policy() != Rules::Unused && opacityActive->policy() != Rules::DontAffect + && opacityInactive->value().toInt() < 25; + + return lowOpacityActive || lowOpacityInactive; +} + +RuleSettings *RulesModel::settings() const +{ + return m_settings; +} + +void RulesModel::setSettings(RuleSettings *settings) +{ + if (m_settings == settings) { + return; + } + + beginResetModel(); + + m_settings = settings; + + for (RuleItem *rule : std::as_const(m_ruleList)) { + const KConfigSkeletonItem *configItem = m_settings->findItem(rule->key()); + const KConfigSkeletonItem *configPolicyItem = m_settings->findItem(rule->policyKey()); + + rule->reset(); + + if (!configItem) { + continue; + } + + const bool isEnabled = configPolicyItem ? configPolicyItem->property() != Rules::Unused + : !configItem->property().toString().isEmpty(); + rule->setEnabled(isEnabled); + + const QVariant value = configItem->property(); + rule->setValue(value); + + if (configPolicyItem) { + const int policy = configPolicyItem->property().toInt(); + rule->setPolicy(policy); + } + } + + endResetModel(); + + Q_EMIT descriptionChanged(); + Q_EMIT warningMessagesChanged(); +} + +void RulesModel::writeToSettings(RuleItem *rule) +{ + KConfigSkeletonItem *configItem = m_settings->findItem(rule->key()); + KConfigSkeletonItem *configPolicyItem = m_settings->findItem(rule->policyKey()); + + if (!configItem) { + return; + } + + if (rule->isEnabled()) { + configItem->setProperty(rule->value()); + if (configPolicyItem) { + configPolicyItem->setProperty(rule->policy()); + } + } else { + configItem->setDefault(); + if (configPolicyItem) { + configPolicyItem->setDefault(); + } + } +} + +void RulesModel::populateRuleList() +{ + qDeleteAll(m_ruleList); + m_ruleList.clear(); + + // Rule description + auto description = addRule(new RuleItem(QLatin1String("description"), + RulePolicy::NoPolicy, RuleItem::String, + i18n("Description"), i18n("Window matching"), + QIcon::fromTheme("entry-edit"))); + description->setFlag(RuleItem::AlwaysEnabled); + description->setFlag(RuleItem::AffectsDescription); + + // Window matching + auto wmclass = addRule(new RuleItem(QLatin1String("wmclass"), + RulePolicy::StringMatch, RuleItem::String, + i18n("Window class (application)"), i18n("Window matching"), + QIcon::fromTheme("window"))); + wmclass->setFlag(RuleItem::AlwaysEnabled); + wmclass->setFlag(RuleItem::AffectsDescription); + wmclass->setFlag(RuleItem::AffectsWarning); + + auto wmclasscomplete = addRule(new RuleItem(QLatin1String("wmclasscomplete"), + RulePolicy::NoPolicy, RuleItem::Boolean, + i18n("Match whole window class"), i18n("Window matching"), + QIcon::fromTheme("window"))); + wmclasscomplete->setFlag(RuleItem::AlwaysEnabled); + + // Helper item to store the detected whole window class when detecting properties + auto wmclasshelper = addRule(new RuleItem(QLatin1String("wmclasshelper"), + RulePolicy::NoPolicy, RuleItem::String, + i18n("Whole window class"), i18n("Window matching"), + QIcon::fromTheme("window"))); + wmclasshelper->setFlag(RuleItem::SuggestionOnly); + + auto types = addRule(new RuleItem(QLatin1String("types"), + RulePolicy::NoPolicy, RuleItem::NetTypes, + i18n("Window types"), i18n("Window matching"), + QIcon::fromTheme("window-duplicate"))); + types->setOptionsData(windowTypesModelData()); + types->setFlag(RuleItem::AlwaysEnabled); + types->setFlag(RuleItem::AffectsWarning); + + addRule(new RuleItem(QLatin1String("windowrole"), + RulePolicy::StringMatch, RuleItem::String, + i18n("Window role"), i18n("Window matching"), + QIcon::fromTheme("dialog-object-properties"))); + + auto title = addRule(new RuleItem(QLatin1String("title"), + RulePolicy::StringMatch, RuleItem::String, + i18n("Window title"), i18n("Window matching"), + QIcon::fromTheme("edit-comment"))); + title->setFlag(RuleItem::AffectsDescription); + + addRule(new RuleItem(QLatin1String("clientmachine"), + RulePolicy::StringMatch, RuleItem::String, + i18n("Machine (hostname)"), i18n("Window matching"), + QIcon::fromTheme("computer"))); + + auto tag = addRule(new RuleItem(QLatin1String("tag"), + RulePolicy::StringMatch, RuleItem::String, + i18n("Window tag"), i18n("Window matching"), + QIcon::fromTheme("edit-comment"))); + tag->setFlag(RuleItem::AffectsDescription); + + // Size & Position + auto position = addRule(new RuleItem(QLatin1String("position"), + RulePolicy::SetRule, RuleItem::Point, + i18n("Position"), i18n("Size & Position"), + QIcon::fromTheme("transform-move"))); + position->setFlag(RuleItem::AffectsWarning); + + auto size = addRule(new RuleItem(QLatin1String("size"), + RulePolicy::SetRule, RuleItem::Size, + i18n("Size"), i18n("Size & Position"), + QIcon::fromTheme("transform-scale"))); + size->setFlag(RuleItem::AffectsWarning); + + addRule(new RuleItem(QLatin1String("maximizehoriz"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Maximized horizontally"), i18n("Size & Position"), + QIcon::fromTheme("resizecol"))); + + addRule(new RuleItem(QLatin1String("maximizevert"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Maximized vertically"), i18n("Size & Position"), + QIcon::fromTheme("resizerow"))); + + RuleItem *desktops; + if (KWindowSystem::isPlatformX11()) { + // Single selection of Virtual Desktop on X11 + desktops = new RuleItem(QLatin1String("desktops"), + RulePolicy::SetRule, RuleItem::Option, + i18n("Virtual desktop"), i18n("Size & Position"), + QIcon::fromTheme("virtual-desktops")); + } else { + // Multiple selection on Wayland + desktops = new RuleItem(QLatin1String("desktops"), + RulePolicy::SetRule, RuleItem::OptionList, + i18n("Virtual desktops"), i18n("Size & Position"), + QIcon::fromTheme("virtual-desktops")); + } + addRule(desktops); + desktops->setOptionsData(virtualDesktopsModelData()); + + connect(this, &RulesModel::virtualDesktopsUpdated, this, [this]() { + m_rules["desktops"]->setOptionsData(virtualDesktopsModelData()); + const QModelIndex index = indexOf("desktops"); + Q_EMIT dataChanged(index, index, {OptionsModelRole}); + }); + + updateVirtualDesktops(); + +#if KWIN_BUILD_ACTIVITIES + m_activities = new KActivities::Consumer(this); + + auto activity = addRule(new RuleItem(QLatin1String("activity"), + RulePolicy::SetRule, RuleItem::OptionList, + i18n("Activities"), i18n("Size & Position"), + QIcon::fromTheme("activities"))); + activity->setOptionsData(activitiesModelData()); + + // Activities consumer may update the available activities later + auto updateActivities = [this]() { + m_rules["activity"]->setOptionsData(activitiesModelData()); + const QModelIndex index = indexOf("activity"); + Q_EMIT dataChanged(index, index, {OptionsModelRole}); + }; + connect(m_activities, &KActivities::Consumer::activitiesChanged, this, updateActivities); + connect(m_activities, &KActivities::Consumer::serviceStatusChanged, this, updateActivities); +#endif + + addRule(new RuleItem(QLatin1String("screen"), + RulePolicy::SetRule, RuleItem::Integer, + i18n("Screen"), i18n("Size & Position"), + QIcon::fromTheme("osd-shutd-screen"))); + + addRule(new RuleItem(QLatin1String("fullscreen"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Fullscreen"), i18n("Size & Position"), + QIcon::fromTheme("view-fullscreen"))); + + addRule(new RuleItem(QLatin1String("minimize"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Minimized"), i18n("Size & Position"), + QIcon::fromTheme("window-minimize"))); + + auto placement = addRule(new RuleItem(QLatin1String("placement"), + RulePolicy::ForceRule, RuleItem::Option, + i18n("Initial placement"), i18n("Size & Position"), + QIcon::fromTheme("region"))); + placement->setOptionsData(placementModelData()); + placement->setFlag(RuleItem::AffectsWarning); + + auto ignoregeometry = addRule(new RuleItem(QLatin1String("ignoregeometry"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Ignore requested geometry"), i18n("Size & Position"), + QIcon::fromTheme("view-time-schedule-baselined-remove"), + xi18nc("@info:tooltip", + "Some applications can set their own geometry, overriding the window manager preferences. " + "Setting this property overrides their placement requests." + "" + "This affects Size and Position " + "but not Maximized or Fullscreen states." + "" + "Note that the position can also be used to map to a different Screen"))); + ignoregeometry->setFlag(RuleItem::AffectsWarning); + + addRule(new RuleItem(QLatin1String("minsize"), + RulePolicy::ForceRule, RuleItem::Size, + i18n("Minimum Size"), i18n("Size & Position"), + QIcon::fromTheme("transform-scale"))); + + addRule(new RuleItem(QLatin1String("maxsize"), + RulePolicy::ForceRule, RuleItem::Size, + i18n("Maximum Size"), i18n("Size & Position"), + QIcon::fromTheme("transform-scale"))); + + addRule(new RuleItem(QLatin1String("strictgeometry"), + RulePolicy::ForceRule, RuleItem::Boolean, + i18n("Obey geometry restrictions"), i18n("Size & Position"), + QIcon::fromTheme("transform-crop-and-resize"), + xi18nc("@info:tooltip", "Some apps like video players or terminals can ask KWin to constrain them to " + "certain aspect ratios or only grow by values larger than the dimensions of one " + "character. Use this property to ignore such restrictions and allow those windows " + "to be resized to arbitrary sizes." + "" + "This can be helpful for windows that can't quite fit the full screen area when " + "maximized."))); + + // Arrangement & Access + addRule(new RuleItem(QLatin1String("above"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Keep above other windows"), i18n("Arrangement & Access"), + QIcon::fromTheme("window-keep-above"))); + + addRule(new RuleItem(QLatin1String("below"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Keep below other windows"), i18n("Arrangement & Access"), + QIcon::fromTheme("window-keep-below"))); + + addRule(new RuleItem(QLatin1String("skiptaskbar"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Skip taskbar"), i18n("Arrangement & Access"), + QIcon::fromTheme("kt-show-statusbar"), + i18nc("@info:tooltip", "Controls whether or not the window appears in the Task Manager."))); + + addRule(new RuleItem(QLatin1String("skippager"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Skip pager"), i18n("Arrangement & Access"), + QIcon::fromTheme("org.kde.plasma.pager"), + i18nc("@info:tooltip", "Controls whether or not the window appears in the Virtual Desktop manager."))); + + addRule(new RuleItem(QLatin1String("skipswitcher"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("Skip switcher"), i18n("Arrangement & Access"), + QIcon::fromTheme("preferences-system-windows-effect-flipswitch"), + xi18nc("@info:tooltip", "Controls whether or not the window appears in the Alt+Tab window list."))); + + addRule(new RuleItem(QLatin1String("shortcut"), + RulePolicy::SetRule, RuleItem::Shortcut, + i18n("Shortcut"), i18n("Arrangement & Access"), + QIcon::fromTheme("configure-shortcuts"))); + + // Appearance & Fixes + addRule(new RuleItem(QLatin1String("noborder"), + RulePolicy::SetRule, RuleItem::Boolean, + i18n("No titlebar and frame"), i18n("Appearance & Fixes"), + QIcon::fromTheme("dialog-cancel"))); + + auto decocolor = addRule(new RuleItem(QLatin1String("decocolor"), + RulePolicy::ForceRule, RuleItem::Option, + i18n("Titlebar color scheme"), i18n("Appearance & Fixes"), + QIcon::fromTheme("preferences-desktop-theme"))); + decocolor->setOptionsData(colorSchemesModelData()); + + auto opacityactive = addRule(new RuleItem(QLatin1String("opacityactive"), + RulePolicy::ForceRule, RuleItem::Percentage, + i18n("Active opacity"), i18n("Appearance & Fixes"), + QIcon::fromTheme("edit-opacity"))); + opacityactive->setFlag(RuleItem::AffectsWarning); + auto opacityinactive = addRule(new RuleItem(QLatin1String("opacityinactive"), + RulePolicy::ForceRule, RuleItem::Percentage, + i18n("Inactive opacity"), i18n("Appearance & Fixes"), + QIcon::fromTheme("edit-opacity"))); + opacityinactive->setFlag(RuleItem::AffectsWarning); + + auto fsplevel = addRule(new RuleItem(QLatin1String("fsplevel"), + RulePolicy::ForceRule, RuleItem::Option, + i18n("Focus stealing prevention"), i18n("Appearance & Fixes"), + QIcon::fromTheme("preferences-system-windows-effect-glide"), + xi18nc("@info:tooltip", "KWin tries to prevent windows that were opened without direct user action from raising " + "themselves and taking focus while you're currently interacting with another window. This " + "property can be used to change the level of focus stealing prevention applied to " + "individual windows and apps." + "" + "Here's what will happen to a window opened without your direct action at each level of " + "focus stealing prevention:" + "" + "" + "None: The window will be raised and focused." + "Low: Focus stealing prevention will be applied, " + "but in the case of a situation KWin considers ambiguous, the window will be raised and " + "focused." + "Normal: Focus stealing prevention will be " + "applied, but in the case of a situation KWin considers ambiguous, the window will " + "not be raised and focused." + "High: The window will only be raised and focused " + "if it belongs to the same app as the currently-focused window." + "Extreme: The window will never be raised and " + "focused." + ""))); + fsplevel->setOptionsData(focusModelData()); + + auto fpplevel = addRule(new RuleItem(QLatin1String("fpplevel"), + RulePolicy::ForceRule, RuleItem::Option, + i18n("Focus protection"), i18n("Appearance & Fixes"), + QIcon::fromTheme("preferences-system-windows-effect-minimize"), + xi18nc("@info:tooltip", "This property controls the focus protection level of the currently active " + "window. It is used to override the focus stealing prevention applied to new windows that " + "are opened without your direct action." + "" + "Here's what happens to new windows that are opened without your direct action at each " + "level of focus protection while the window with this property applied to it has focus:" + "" + "" + "None: Newly-opened windows always raise " + "themselves and take focus." + "Low: Focus stealing prevention will be applied " + "to the newly-opened window, but in the case of a situation KWin considers ambiguous, the " + "window will be raised and focused." + "Normal: Focus stealing prevention will be applied " + "to the newly-opened window, but in the case of a situation KWin considers ambiguous, the " + "window will not be raised and focused." + "High: Newly-opened windows will only raise " + "themselves and take focus if they belongs to the same app as the currently-focused " + "window." + "Extreme: Newly-opened windows never raise " + "themselves and take focus." + ""))); + fpplevel->setOptionsData(focusModelData()); + + addRule(new RuleItem(QLatin1String("acceptfocus"), + RulePolicy::ForceRule, RuleItem::Boolean, + i18n("Accept focus"), i18n("Appearance & Fixes"), + QIcon::fromTheme("preferences-desktop-cursors"), + i18n("Controls whether or not the window becomes focused when clicked."))); + + addRule(new RuleItem(QLatin1String("disableglobalshortcuts"), + RulePolicy::ForceRule, RuleItem::Boolean, + i18n("Ignore global shortcuts"), i18n("Appearance & Fixes"), + QIcon::fromTheme("input-keyboard-virtual-off"), + xi18nc("@info:tooltip", "Use this property to prevent global keyboard shortcuts from working while " + "the window is focused. This can be useful for apps like emulators or virtual " + "machines that handle some of the same shortcuts themselves." + "" + "Note that you won't be able to Alt+Tab out of the window " + "or use any other global shortcuts such as Alt+Space to " + "activate KRunner."))); + + addRule(new RuleItem(QLatin1String("closeable"), + RulePolicy::ForceRule, RuleItem::Boolean, + i18n("Closeable"), i18n("Appearance & Fixes"), + QIcon::fromTheme("dialog-close"))); + + addRule(new RuleItem(QLatin1String("desktopfile"), + RulePolicy::SetRule, RuleItem::String, + i18n("Desktop file name"), i18n("Appearance & Fixes"), + QIcon::fromTheme("application-x-desktop"))); + + addRule(new RuleItem(QLatin1String("blockcompositing"), + RulePolicy::ForceRule, RuleItem::Boolean, + i18n("Block compositing"), i18n("Appearance & Fixes"), + QIcon::fromTheme("composite-track-on"))); + + auto layer = addRule(new RuleItem(QLatin1String("layer"), + RulePolicy::ForceRule, RuleItem::Option, + i18n("Layer"), i18n("Appearance & Fixes"), + QIcon::fromTheme("view-sort"))); + layer->setOptionsData(layerModelData()); + + addRule(new RuleItem(QLatin1String("adaptivesync"), + RulePolicy::ForceRule, RuleItem::Boolean, + i18n("Adaptive sync"), i18n("Appearance & Fixes"), + QIcon::fromTheme("monitor-symbolic"))); + + addRule(new RuleItem(QLatin1String("tearing"), + RulePolicy::ForceRule, RuleItem::Boolean, + i18n("Allow tearing"), i18n("Appearance & Fixes"), + QIcon::fromTheme("monitor-symbolic"))); +} + +const QHash RulesModel::x11PropertyHash() +{ + static const auto propertyToRule = QHash{ + {"caption", "title"}, + {"role", "windowrole"}, + {"clientMachine", "clientmachine"}, + {"maximizeHorizontal", "maximizehoriz"}, + {"maximizeVertical", "maximizevert"}, + {"minimized", "minimize"}, + {"fullscreen", "fullscreen"}, + {"keepAbove", "above"}, + {"keepBelow", "below"}, + {"noBorder", "noborder"}, + {"skipTaskbar", "skiptaskbar"}, + {"skipPager", "skippager"}, + {"skipSwitcher", "skipswitcher"}, + {"desktopFile", "desktopfile"}, + {"desktops", "desktops"}, + {"layer", "layer"}, + }; + return propertyToRule; +}; + +void RulesModel::setSuggestedProperties(const QVariantMap &info) +{ + // Properties that cannot be directly applied via x11PropertyHash + const QPoint position = QPoint(info.value("x").toInt(), info.value("y").toInt()); + const QSize size = QSize(info.value("width").toInt(), info.value("height").toInt()); + + m_rules["position"]->setSuggestedValue(position); + m_rules["size"]->setSuggestedValue(size); + m_rules["minsize"]->setSuggestedValue(size); + m_rules["maxsize"]->setSuggestedValue(size); + + NET::WindowType window_type = static_cast(info.value("type", 0).toInt()); + if (window_type == NET::Unknown) { + window_type = NET::Normal; + } + m_rules["types"]->setSuggestedValue(1 << window_type); + + const QString wmsimpleclass = info.value("resourceClass").toString(); + const QString wmcompleteclass = QStringLiteral("%1 %2").arg(info.value("resourceName").toString(), + info.value("resourceClass").toString()); + + // This window is not providing the class according to spec (WM_CLASS on X11, appId on Wayland) + // Notify the user that this is a bug within the application, so there's nothing we can do + if (wmsimpleclass.isEmpty()) { + Q_EMIT showErrorMessage(i18n("Window class not available"), + xi18nc("@info", "This application is not providing a class for the window, " + "so KWin cannot use it to match and apply any rules. " + "If you still want to apply some rules to it, " + "try to match other properties like the window title instead." + "Please consider reporting this bug to the application's developers.")); + } + + m_rules["wmclass"]->setSuggestedValue(wmsimpleclass); + m_rules["wmclasshelper"]->setSuggestedValue(wmcompleteclass); + +#if KWIN_BUILD_ACTIVITIES + const QStringList activities = info.value("activities").toStringList(); + m_rules["activity"]->setSuggestedValue(activities.isEmpty() ? QStringList{Activities::nullUuid()} + : activities); +#endif + + const auto ruleForProperty = x11PropertyHash(); + for (QString &property : info.keys()) { + if (!ruleForProperty.contains(property)) { + continue; + } + const QString ruleKey = ruleForProperty.value(property, QString()); + Q_ASSERT(hasRule(ruleKey)); + + m_rules[ruleKey]->setSuggestedValue(info.value(property)); + } + + Q_EMIT dataChanged(index(0), index(rowCount() - 1), {RulesModel::SuggestedValueRole}); +} + +QList RulesModel::windowTypesModelData() const +{ + static const auto modelData = QList{ + // TODO: Find/create better icons + {0, i18n("All window types"), {}, {}, OptionsModel::SelectAllOption}, + {1 << NET::Normal, i18n("Normal window"), QIcon::fromTheme("window")}, + {1 << NET::Dialog, i18n("Dialog window"), QIcon::fromTheme("window-duplicate")}, + {1 << NET::Utility, i18n("Utility window"), QIcon::fromTheme("dialog-object-properties")}, + {1 << NET::Dock, i18n("Dock (panel)"), QIcon::fromTheme("value-decrease")}, // see bug 492341 + {1 << NET::Toolbar, i18n("Toolbar"), QIcon::fromTheme("tools")}, + {1 << NET::Menu, i18n("Torn-off menu"), QIcon::fromTheme("overflow-menu-left")}, + {1 << NET::Splash, i18n("Splash screen"), QIcon::fromTheme("embosstool")}, + {1 << NET::Desktop, i18n("Desktop"), QIcon::fromTheme("desktop")}, + // {1 << NET::Override, i18n("Unmanaged Window")}, deprecated + {1 << NET::TopMenu, i18n("Standalone menubar"), QIcon::fromTheme("application-menu")}, + {1 << NET::OnScreenDisplay, i18n("On-screen display"), QIcon::fromTheme("osd-duplicate")}}; + + return modelData; +} + +QList RulesModel::virtualDesktopsModelData() const +{ + QList modelData; + modelData << OptionsModel::Data{ + QString(), + i18n("All desktops"), + QIcon::fromTheme("window-pin"), + i18nc("@info:tooltip in the virtual desktop list", "Make the window available on all desktops"), + OptionsModel::ExclusiveOption, + }; + for (const DBusDesktopDataStruct &desktop : m_virtualDesktops) { + modelData << OptionsModel::Data{ + desktop.id, + QString::number(desktop.position + 1).rightJustified(2) + QStringLiteral(": ") + desktop.name, + QIcon::fromTheme("virtual-desktops")}; + } + return modelData; +} + +QList RulesModel::activitiesModelData() const +{ +#if KWIN_BUILD_ACTIVITIES + QList modelData; + + modelData << OptionsModel::Data{ + Activities::nullUuid(), + i18n("All activities"), + QIcon::fromTheme("activities"), + i18nc("@info:tooltip in the activity list", "Make the window available on all activities"), + OptionsModel::ExclusiveOption, + }; + + const auto activities = m_activities->activities(); + if (m_activities->serviceStatus() == KActivities::Consumer::Running) { + for (const QString &activityId : activities) { + const KActivities::Info info(activityId); + modelData << OptionsModel::Data{activityId, info.name(), QIcon::fromTheme(info.icon())}; + } + } + + return modelData; +#else + return {}; +#endif +} + +QList RulesModel::placementModelData() const +{ + static const auto modelData = QList{ + {PlacementDefault, i18n("Default")}, + {PlacementNone, i18n("No placement")}, + {PlacementSmart, i18n("Minimal overlapping")}, + {PlacementMaximizing, i18n("Maximized")}, + {PlacementCentered, i18n("Centered")}, + {PlacementRandom, i18n("Random")}, + {PlacementZeroCornered, i18n("In top-left corner")}, + {PlacementUnderMouse, i18n("Under mouse")}, + {PlacementOnMainWindow, i18n("On main window")}}; + return modelData; +} + +QList RulesModel::focusModelData() const +{ + static const auto modelData = QList{ + {0, i18n("None")}, + {1, i18n("Low")}, + {2, i18n("Normal")}, + {3, i18n("High")}, + {4, i18n("Extreme")}}; + return modelData; +} + +QList RulesModel::colorSchemesModelData() const +{ + QList modelData; + + QAbstractItemModel *schemesModel = KColorSchemeManager::instance()->model(); + + // Skip row 0, which is Default scheme + for (int r = 1; r < schemesModel->rowCount(); r++) { + const QModelIndex index = schemesModel->index(r, 0); + modelData << OptionsModel::Data{ + QFileInfo(index.data(Qt::UserRole).toString()).baseName(), + index.data(Qt::DisplayRole).toString(), + index.data(Qt::DecorationRole).value()}; + } + + return modelData; +} + +QList RulesModel::layerModelData() const +{ + static const auto modelData = QList{ + {DesktopLayer, i18n("Desktop")}, + {BelowLayer, i18n("Below")}, + {NormalLayer, i18n("Normal")}, + {AboveLayer, i18n("Above")}, + {NotificationLayer, i18n("Notification")}, + {ActiveLayer, i18n("Fullscreen")}, + {PopupLayer, i18n("Popup")}, + {CriticalNotificationLayer, i18n("Critical notification")}, + {OnScreenDisplayLayer, i18n("On-screen display")}, + {OverlayLayer, i18n("Overlay")}, + }; + return modelData; +} + +void RulesModel::detectWindowProperties(int milliseconds) +{ + QTimer::singleShot(milliseconds, this, &RulesModel::selectX11Window); +} + +void RulesModel::selectX11Window() +{ + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"), + QStringLiteral("/KWin"), + QStringLiteral("org.kde.KWin"), + QStringLiteral("queryWindowInfo")); + + QDBusPendingReply async = QDBusConnection::sessionBus().asyncCall(message); + + QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this); + connect(callWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *self) { + QDBusPendingReply reply = *self; + self->deleteLater(); + if (!reply.isValid()) { + if (reply.error().name() == QLatin1String("org.kde.KWin.Error.InvalidWindow")) { + Q_EMIT showErrorMessage(i18n("Unmanaged window"), + i18n("Could not detect window properties. The window is not managed by KWin.")); + } + return; + } + const QVariantMap windowInfo = reply.value(); + setSuggestedProperties(windowInfo); + Q_EMIT showSuggestions(); + }); +} + +void RulesModel::updateVirtualDesktops() +{ + QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"), + QStringLiteral("/VirtualDesktopManager"), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("Get")); + message.setArguments(QVariantList{ + QStringLiteral("org.kde.KWin.VirtualDesktopManager"), + QStringLiteral("desktops")}); + + QDBusPendingReply async = QDBusConnection::sessionBus().asyncCall(message); + + QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this); + connect(callWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *self) { + QDBusPendingReply reply = *self; + self->deleteLater(); + if (!reply.isValid()) { + return; + } + m_virtualDesktops = qdbus_cast(reply.value()); + Q_EMIT virtualDesktopsUpdated(); + }); +} + +} // namespace + +#include "moc_rulesmodel.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/rules/rulesmodel.h b/local/recipes/kde/kwin/source/src/kcms/rules/rulesmodel.h new file mode 100644 index 0000000000..2f037a21e6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/rules/rulesmodel.h @@ -0,0 +1,120 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include "ruleitem.h" +#include +#include +#include + +#include +#include +#include + +#if KWIN_BUILD_ACTIVITIES +#include +#endif + +namespace KWin +{ + +class RulesModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(QString description READ description WRITE setDescription NOTIFY descriptionChanged) + Q_PROPERTY(QStringList warningMessages READ warningMessages NOTIFY warningMessagesChanged) + +public: + enum RulesRole { + NameRole = Qt::DisplayRole, + DescriptionRole = Qt::ToolTipRole, + IconRole = Qt::DecorationRole, + IconNameRole = Qt::UserRole + 1, + KeyRole, + SectionRole, + EnabledRole, + SelectableRole, + ValueRole, + TypeRole, + PolicyRole, + PolicyModelRole, + OptionsModelRole, + SuggestedValueRole + }; + Q_ENUM(RulesRole) + +public: + explicit RulesModel(QObject *parent = nullptr); + ~RulesModel(); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + + QModelIndex indexOf(const QString &key) const; + bool hasRule(const QString &key) const; + RuleItem *ruleItem(const QString &key) const; + + RuleSettings *settings() const; + void setSettings(RuleSettings *settings); + + void setSuggestedProperties(const QVariantMap &info); + + QString description() const; + void setDescription(const QString &description); + QStringList warningMessages() const; + + Q_INVOKABLE void detectWindowProperties(int milliseconds); + +Q_SIGNALS: + void descriptionChanged(); + void warningMessagesChanged(); + + void showSuggestions(); + void showErrorMessage(const QString &title, const QString &message); + + void virtualDesktopsUpdated(); + +private: + void populateRuleList(); + RuleItem *addRule(RuleItem *rule); + void writeToSettings(RuleItem *rule); + + QString defaultDescription() const; + void processSuggestion(const QString &key, const QVariant &value); + + bool wmclassWarning() const; + bool geometryWarning() const; + bool opacityWarning() const; + + static const QHash x11PropertyHash(); + void updateVirtualDesktops(); + + QList windowTypesModelData() const; + QList virtualDesktopsModelData() const; + QList activitiesModelData() const; + QList placementModelData() const; + QList focusModelData() const; + QList colorSchemesModelData() const; + QList layerModelData() const; + +private Q_SLOTS: + void selectX11Window(); + +private: + QList m_ruleList; + QHash m_rules; + DBusDesktopDataVector m_virtualDesktops; +#if KWIN_BUILD_ACTIVITIES + KActivities::Consumer *m_activities; +#endif + RuleSettings *m_settings; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/kcms/rules/ui/FileDialogLoader.qml b/local/recipes/kde/kwin/source/src/kcms/rules/ui/FileDialogLoader.qml new file mode 100644 index 0000000000..d2a671ee82 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/rules/ui/FileDialogLoader.qml @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtCore +import QtQuick +import QtQuick.Dialogs as QtDialogs + +Loader { + id: root + active: false + + property string title : i18n("Select File"); + property string lastFolder : "" + property bool isSaveDialog : false + + signal fileSelected(string path) + + sourceComponent: QtDialogs.FileDialog { + id: fileDialog + + title: root.title + fileMode: root.isSaveDialog ? QtDialogs.FileDialog.SaveFile : QtDialogs.FileDialog.OpenFile + currentFolder: root.lastFolder || StandardPaths.standardLocations(StandardPaths.HomeLocation)[0] + nameFilters: [ i18n("KWin Rules (*.kwinrule)") ] + defaultSuffix: "*.kwinrule" + + Component.onCompleted: { + open(); + } + + onAccepted: { + root.lastFolder = currentFolder; + if (selectedFile != "") { + root.fileSelected(selectedFile); + } + root.active = false; + } + + onRejected: { + root.active = false; + } + } +} diff --git a/local/recipes/kde/kwin/source/src/kcms/rules/ui/OptionsComboBox.qml b/local/recipes/kde/kwin/source/src/kcms/rules/ui/OptionsComboBox.qml new file mode 100644 index 0000000000..f5cd9993c6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/rules/ui/OptionsComboBox.qml @@ -0,0 +1,122 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 + +import org.kde.kirigami as Kirigami +import org.kde.kcms.kwinrules + + +QQC2.ComboBox { + id: optionsCombo + + textRole: "display" + valueRole: "value" + + property bool multipleChoice: false + property int selectionMask: 0 + readonly property int allOptionsMask: model.allOptionsMask + + currentIndex: multipleChoice ? -1 : model.selectedIndex + + displayText: { + if (!multipleChoice) { + return currentText; + } + const selectionCount = selectionMask.toString(2).replace(/0/g, '').length; + const optionsCount = allOptionsMask.toString(2).replace(/0/g, '').length; + switch (selectionCount) { + case 0: + return i18n("None selected"); + case 1: + const selectedBit = selectionMask.toString(2).length - 1; + const selectedIndex = (model.useFlags) ? model.indexOf(selectionMask) : selectedBit + return model.data(model.index(selectedIndex, 0), Qt.DisplayRole); + case optionsCount: + return i18n("All selected"); + } + return i18np("%1 selected", "%1 selected", selectionCount); + } + + function toggleOption(index: int) { + const optionType = model.index(index, 0).data(OptionsModel.OptionTypeRole); + const bitMask = model.index(index, 0).data(OptionsModel.BitMaskRole); + + if (optionType === OptionsModel.ExclusiveOption) { + // Radio Button. Activate only the exclusive option + selectionMask = bitMask; + } else { + // CheckBox. Toggle the option indicated by the mask + const wasChecked = (selectionMask & bitMask) == bitMask + if (wasChecked) { + selectionMask &= ~bitMask; + } else { + selectionMask |= bitMask; + } + selectionMask &= allOptionsMask; + } + activated(index); + } + + Keys.onSpacePressed: event => { + if (down && multipleChoice) { + toggleOption(highlightedIndex); + event.accepted = true; + return; + } + // Default to the regular event handling + event.accepted = false; + } + + delegate: QQC2.ItemDelegate { + id: delegateItem + + highlighted: optionsCombo.highlightedIndex == index + width: parent.width + + contentItem: RowLayout { + QQC2.RadioButton { + id: radioButton + visible: multipleChoice && model.optionType === OptionsModel.ExclusiveOption + checked: (selectionMask & bitMask) == bitMask + } + QQC2.CheckBox { + id: checkBox + visible: multipleChoice && model.optionType !== OptionsModel.ExclusiveOption + checked: (selectionMask & model.bitMask) == model.bitMask + } + Kirigami.Icon { + source: model.decoration + Layout.preferredHeight: Kirigami.Units.iconSizes.small + Layout.preferredWidth: Kirigami.Units.iconSizes.small + } + QQC2.Label { + text: model.display + color: highlighted ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor + Layout.fillWidth: true + horizontalAlignment: Text.AlignLeft + } + } + + MouseArea { + anchors.fill: contentItem + enabled: multipleChoice + onClicked: optionsCombo.toggleOption(index) + } + + QQC2.ToolTip { + text: model.tooltip + visible: hovered && (model.tooltip.length > 0) + } + + Component.onCompleted: { + //FIXME: work around bug https://bugs.kde.org/show_bug.cgi?id=403153 + optionsCombo.popup.width = Math.max(implicitWidth, optionsCombo.width, optionsCombo.popup.width); + } + } +} diff --git a/local/recipes/kde/kwin/source/src/kcms/rules/ui/RuleItemDelegate.qml b/local/recipes/kde/kwin/source/src/kcms/rules/ui/RuleItemDelegate.qml new file mode 100644 index 0000000000..0d7bb1c6fc --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/rules/ui/RuleItemDelegate.qml @@ -0,0 +1,113 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import org.kde.kcmutils as KCM + +QQC2.ItemDelegate { + id: ruleDelegate + + width: ListView.view.width + highlighted: false + hoverEnabled: false + down: false + + contentItem: RowLayout { + + Kirigami.Icon { + id: itemIcon + source: model.icon + Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium + Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium + Layout.rightMargin: Kirigami.Units.smallSpacing + Layout.alignment: Qt.AlignVCenter + } + + RowLayout { + Layout.preferredWidth: 10 * Kirigami.Units.gridUnit + Layout.preferredHeight: Kirigami.Units.iconSizes.medium + spacing: Kirigami.Units.smallSpacing + + QQC2.Label { + id: label + text: model.name + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + + HoverHandler { + id: labelHover + enabled: label.truncated + } + + QQC2.ToolTip.text: model.name + QQC2.ToolTip.visible: labelHover.hovered + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + } + + Kirigami.ContextualHelpButton { + Layout.alignment: Qt.AlignVCenter + visible: model.description.length > 0 + toolTipText: model.description + } + } + + RowLayout { + // This layout keeps the width constant between delegates, independent of items visibility + Layout.fillWidth: true + Layout.preferredWidth: 20 * Kirigami.Units.gridUnit + Layout.minimumWidth: 13 * Kirigami.Units.gridUnit + + OptionsComboBox { + id: policyCombo + Layout.preferredWidth: 50 // 50% + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + + visible: count > 0 + + model: policyModel + onActivated: { + policy = currentValue; + } + } + + ValueEditor { + id: valueEditor + Layout.preferredWidth: 50 // 50% + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + + ruleValue: model.value + ruleOptions: model.options + controlType: model.type + + onValueEdited: (value) => { + model.value = value; + } + } + + QQC2.ToolButton { + id: itemEnabled + icon.name: "edit-delete" + visible: model.selectable + Layout.alignment: Qt.AlignVCenter + onClicked: { + model.enabled = false; + } + } + } + + QQC2.ToolTip { + text: model.description + visible: hovered && (text.length > 0) + } + } +} diff --git a/local/recipes/kde/kwin/source/src/kcms/rules/ui/RulesEditor.qml b/local/recipes/kde/kwin/source/src/kcms/rules/ui/RulesEditor.qml new file mode 100644 index 0000000000..82dc4dbc46 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/rules/ui/RulesEditor.qml @@ -0,0 +1,350 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import org.kde.kcmutils as KCM +import org.kde.kitemmodels +import org.kde.kcms.kwinrules + + +KCM.ScrollViewKCM { + id: rulesEditor + + title: kcm.rulesModel.description + + view: ListView { + id: rulesView + clip: true + + model: enabledRulesModel + delegate: RuleItemDelegate { + ListView.onAdd: { + // Try to position the new added item into the visible view + // FIXME: It only works when moving towards the end of the list + ListView.view.currentIndex = index + } + } + section { + property: "section" + delegate: Kirigami.ListSectionHeader { + width: ListView.view.width + label: section + } + } + + highlightRangeMode: ListView.ApplyRange + + add: Transition { + NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: Kirigami.Units.longDuration * 3 } + } + removeDisplaced: Transition { + NumberAnimation { property: "y"; duration: Kirigami.Units.longDuration } + } + + // We need to center on the free space below contentItem, not the full + // ListView. This invisible item helps make that positioning work no + // matter the window height + Item { + anchors { + left: parent.left + right: parent.right + top: parent.contentItem.bottom + bottom: parent.bottom + } + visible: rulesView.count <= 4 + + Kirigami.PlaceholderMessage { + id: hintArea + anchors.centerIn: parent + width: parent.width - (Kirigami.Units.largeSpacing * 4) + text: i18n("No window properties changed") + explanation: xi18nc("@info", "Click the Add Property... button below to add some window properties that will be affected by the rule") + } + } + } + + header: ColumnLayout { + visible: warningList.count > 0 + Repeater { + id: warningList + model: kcm.rulesModel.warningMessages + + delegate: Kirigami.InlineMessage { + text: modelData + visible: true + Layout.fillWidth: true + } + } + } + + footer: RowLayout { + QQC2.Button { + text: i18n("Add Property...") + icon.name: "list-add" + onClicked: { + propertySheet.visible = true; + } + } + Item { + Layout.fillWidth: true + } + QQC2.Button { + id: detectButton + text: i18n("Detect Window Properties") + icon.name: "edit-find" + enabled: !propertySheet.visible && !errorDialog.visible + onClicked: { + overlayModel.onlySuggestions = true; + kcm.rulesModel.detectWindowProperties(Math.max(delaySpin.value * 1000, + Kirigami.Units.shortDuration)); + } + } + QQC2.SpinBox { + id: delaySpin + enabled: detectButton.enabled + Layout.preferredWidth: Math.max(metricsInstant.advanceWidth, metricsAfter.advanceWidth) + Kirigami.Units.gridUnit * 4 + from: 0 + to: 30 + textFromValue: (value, locale) => { + return (value == 0) ? i18n("Instantly") + : i18np("After %1 second", "After %1 seconds", value) + } + valueFromText: (text, locale) => { + const number = Number.parseInt(text.match(/[0-9]+/), 10); + return isNaN(number) ? 0 : number; + } + + TextMetrics { + id: metricsInstant + font: delaySpin.font + text: i18n("Instantly") + } + TextMetrics { + id: metricsAfter + font: delaySpin.font + text: i18np("After %1 second", "After %1 seconds", 99) + } + } + + } + + Connections { + target: kcm.rulesModel + function onShowSuggestions() { + if (errorDialog.visible) { + return; + } + overlayModel.onlySuggestions = true; + propertySheet.visible = true; + } + function onShowErrorMessage(title, message) { + errorDialog.title = title + errorDialog.message = message + errorDialog.open() + } + } + + Kirigami.Dialog { + id: errorDialog + + property alias message: errorLabel.text + + preferredWidth: rulesEditor.width - Kirigami.Units.gridUnit * 6 + maximumWidth: Kirigami.Units.gridUnit * 35 + footer: null // Just use the close button on top + + ColumnLayout { + // Wrap it in a Layout so we can apply margins to the text while keeping implicit sizes + Kirigami.Heading { + id: errorLabel + level: 4 + wrapMode: Text.WordWrap + + Layout.fillWidth: true + Layout.margins: Kirigami.Units.largeSpacing + } + } + } + + Kirigami.OverlaySheet { + id: propertySheet + + title: i18n("Add property to the rule") + + footer: Kirigami.SearchField { + id: searchField + horizontalAlignment: Text.AlignLeft + } + + ListView { + id: overlayView + model: overlayModel + Layout.preferredWidth: Kirigami.Units.gridUnit * 28 + clip: true + reuseItems: true + + section { + property: "section" + delegate: Kirigami.ListSectionHeader { + label: section + width: ListView.view.width + height: implicitHeight + } + } + + delegate: QQC2.ItemDelegate { + id: propertyDelegate + highlighted: false + width: ListView.view.width + + contentItem: RowLayout { + Kirigami.Icon { + source: model.icon + Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium + Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium + Layout.alignment: Qt.AlignVCenter + } + QQC2.Label { + id: itemNameLabel + text: model.name + horizontalAlignment: Qt.AlignLeft + Layout.preferredWidth: implicitWidth + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + } + QQC2.Label { + id: suggestedLabel + text: formatValue(model.suggested, model.type, model.options) + horizontalAlignment: Text.AlignRight + elide: Text.ElideRight + opacity: 0.7 + Layout.maximumWidth: propertyDelegate.width - itemNameLabel.implicitWidth - Kirigami.Units.gridUnit * 6 + Layout.alignment: Qt.AlignVCenter + QQC2.ToolTip { + text: suggestedLabel.text + visible: hovered && suggestedLabel.truncated + } + } + + Kirigami.ContextualHelpButton { + Layout.rightMargin: Kirigami.Units.largeSpacing + visible: model.description.length > 0 + toolTipText: model.description + } + + QQC2.ToolButton { + icon.name: (model.enabled) ? "dialog-ok-apply" : "list-add" + onClicked: addProperty(); + Layout.preferredWidth: implicitWidth + Layout.leftMargin: -Kirigami.Units.smallSpacing + Layout.rightMargin: -Kirigami.Units.smallSpacing + Layout.alignment: Qt.AlignVCenter + } + } + + onClicked: { + addProperty(); + propertySheet.close(); + } + + function addProperty() { + model.enabled = true; + if (model.suggested != null) { + model.value = model.suggested; + model.suggested = null; + } + } + } + + Kirigami.PlaceholderMessage { + anchors.centerIn: parent + width: parent.width - (Kirigami.Units.largeSpacing * 4) + visible: overlayModel.count === 0 + text: { + if (searchField.text.length === 0) { + return i18nc("@info:placeholder", "No properties left to add"); + } + return i18nc("@info:placeholder %1 is a filter text introduced by the user", "No properties match \"%1\"", searchField.text); + } + } + } + + onVisibleChanged: { + searchField.text = ""; + if (visible) { + searchField.forceActiveFocus(); + } else { + overlayModel.onlySuggestions = false; + } + } + } + + function formatValue(value, type, options) { + if (value == null) { + return ""; + } + switch (type) { + case RuleItem.Boolean: + return value ? i18n("Yes") : i18n("No"); + case RuleItem.Percentage: + return i18n("%1 %", value); + case RuleItem.Point: + return i18nc("Coordinates (x, y)", "(%1, %2)", value.x, value.y); + case RuleItem.Size: + return i18nc("Size (width, height)", "(%1, %2)", value.width, value.height); + case RuleItem.Option: + return options.textOfValue(value); + case RuleItem.NetTypes: + const selectedValue = value.toString(2).length - 1; + return options.textOfValue(selectedValue); + case RuleItem.OptionList: + return Array.from(value, item => options.textOfValue(item) ).join(", "); + } + return value; + } + + KSortFilterProxyModel { + id: enabledRulesModel + sourceModel: kcm.rulesModel + filterRowCallback: (source_row, source_parent) => { + const index = sourceModel.index(source_row, 0, source_parent); + return sourceModel.data(index, RulesModel.EnabledRole); + } + } + + KSortFilterProxyModel { + id: overlayModel + sourceModel: kcm.rulesModel + + property bool onlySuggestions: false + onOnlySuggestionsChanged: { + invalidateFilter(); + } + + filterString: searchField.text.trim().toLowerCase() + filterRowCallback: (source_row, source_parent) => { + const index = sourceModel.index(source_row, 0, source_parent); + + const hasSuggestion = sourceModel.data(index, RulesModel.SuggestedValueRole) != null; + const isOptional = sourceModel.data(index, RulesModel.SelectableRole); + const isEnabled = sourceModel.data(index, RulesModel.EnabledRole); + + const showItem = hasSuggestion || (!onlySuggestions && isOptional && !isEnabled); + + if (!showItem) { + return false; + } + if (filterString.length > 0) { + return sourceModel.data(index, RulesModel.NameRole).toLowerCase().includes(filterString) + } + return true; + } + } + +} diff --git a/local/recipes/kde/kwin/source/src/kcms/rules/ui/ValueEditor.qml b/local/recipes/kde/kwin/source/src/kcms/rules/ui/ValueEditor.qml new file mode 100644 index 0000000000..a3df75c365 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/rules/ui/ValueEditor.qml @@ -0,0 +1,231 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 + +import org.kde.kirigami as Kirigami +import org.kde.kquickcontrols as KQC +import org.kde.kcms.kwinrules + + +Loader { + id: valueEditor + focus: true + + property var ruleValue + property var ruleOptions + property int controlType + + signal valueEdited(var value) + + sourceComponent: { + switch (controlType) { + case RuleItem.Boolean: return booleanEditor + case RuleItem.String: return stringEditor + case RuleItem.Integer: return integerEditor + case RuleItem.Option: return optionEditor + case RuleItem.NetTypes: return netTypesEditor + case RuleItem.Percentage: return percentageEditor + case RuleItem.Point: return coordinateEditor + case RuleItem.Size: return coordinateEditor + case RuleItem.Shortcut: return shortcutEditor + case RuleItem.OptionList: return optionListEditor + default: return emptyEditor + } + } + + Component { + id: emptyEditor + Item {} + } + + Component { + id: booleanEditor + RowLayout { + Item { + Layout.fillWidth: true + } + QQC2.RadioButton { + text: i18n("Yes") + checked: ruleValue + Layout.margins: Kirigami.Units.smallSpacing + onToggled: valueEditor.valueEdited(checked) + } + QQC2.RadioButton { + text: i18n("No") + checked: !ruleValue + Layout.margins: Kirigami.Units.smallSpacing + onToggled: valueEditor.valueEdited(!checked) + } + } + } + + Component { + id: stringEditor + QQC2.TextField { + id: stringTextField + property bool isTextEdited: false + horizontalAlignment: Text.AlignLeft + onTextEdited: { valueEditor.valueEdited(text); } + Connections { + target: valueEditor + function onRuleValueChanged() { + if (!stringTextField.activeFocus) { // Protects from self-updating when editing + stringTextField.text = valueEditor.ruleValue + } + } + } + Component.onCompleted: { this.text = valueEditor.ruleValue } + } + } + + Component { + id: integerEditor + QQC2.SpinBox { + editable: true + value: ruleValue + onValueModified: valueEditor.valueEdited(value) + } + } + + Component { + id: optionEditor + OptionsComboBox { + model: ruleOptions + onActivated: (index) => { + valueEditor.valueEdited(currentValue); + } + } + } + + Component { + id: netTypesEditor + OptionsComboBox { + model: ruleOptions + multipleChoice: true + // Filter the provided value with the options mask + selectionMask: ruleValue & model.allOptionsMask + onActivated: { + valueEditor.valueEdited(selectionMask); + } + } + } + + Component { + id: optionListEditor + OptionsComboBox { + id: optionListCombo + model: ruleOptions + multipleChoice: true + + onActivated: { + const selectionList = [] + for (let i = 0; i < count; i++) { + if (selectionMask & (1 << i)) { + selectionList.push(model.data(model.index(i,0), Qt.UserRole)) + } + } + valueEditor.valueEdited(selectionList); + } + + function updateSelectionMask() { + selectionMask = 0 + for (let i = 0; i < count; i++) { + if (ruleValue.includes(model.data(model.index(i,0), Qt.UserRole))) { + selectionMask += 1 << i + } + } + } + + onModelChanged: updateSelectionMask() + Component.onCompleted: updateSelectionMask() + Connections { + target: valueEditor + function onRuleValueChanged() { + optionListCombo.updateSelectionMask() + } + } + } + } + + Component { + id: percentageEditor + RowLayout { + QQC2.Slider { + id: slider + Layout.fillWidth: true + from: 0 + to: 100 + value: ruleValue + onMoved: valueEditor.valueEdited(Math.round(slider.value)) + } + QQC2.Label { + text: i18n("%1 %", Math.round(slider.value)) + horizontalAlignment: Qt.AlignRight + Layout.minimumWidth: maxPercentage.width + Kirigami.Units.smallSpacing + Layout.margins: Kirigami.Units.smallSpacing + } + TextMetrics { + id: maxPercentage + text: i18n("%1 %", 100) + } + } + } + + Component { + id: coordinateEditor + RowLayout { + id: coordItem + spacing: Kirigami.Units.smallSpacing + + readonly property bool isSize: controlType == RuleItem.Size + readonly property var coord: (isSize) ? Qt.size(coordX.value, coordY.value) + : Qt.point(coordX.value, coordY.value) + + QQC2.SpinBox { + id: coordX + editable: true + Layout.preferredWidth: 50 // 50% + Layout.fillWidth: true + from: (isSize) ? 0 : -32767 + to: 32767 + value: (isSize) ? ruleValue.width : ruleValue.x + onValueModified: valueEditor.valueEdited(coord) + } + QQC2.Label { + id: coordSeparator + Layout.preferredWidth: implicitWidth + text: i18nc("(x, y) coordinates separator in size/position","x") + horizontalAlignment: Text.AlignHCenter + } + QQC2.SpinBox { + id: coordY + editable: true + from: coordX.from + to: coordX.to + Layout.preferredWidth: 50 // 50% + Layout.fillWidth: true + value: (isSize) ? ruleValue.height : ruleValue.y + onValueModified: valueEditor.valueEdited(coord) + } + } + } + + Component { + id: shortcutEditor + RowLayout { + Item { + Layout.fillWidth: true + } + KQC.KeySequenceItem { + keySequence: ruleValue + onCaptureFinished: valueEditor.valueEdited(keySequence) + } + } + } +} diff --git a/local/recipes/kde/kwin/source/src/kcms/rules/ui/main.qml b/local/recipes/kde/kwin/source/src/kcms/rules/ui/main.qml new file mode 100644 index 0000000000..bc7bee2483 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/rules/ui/main.qml @@ -0,0 +1,269 @@ +/* + SPDX-FileCopyrightText: 2020 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import QtQml.Models +import org.kde.kcmutils as KCM +import org.kde.kirigami as Kirigami + +KCM.ScrollViewKCM { + id: rulesListKCM + + implicitWidth: Kirigami.Units.gridUnit * 35 + implicitHeight: Kirigami.Units.gridUnit * 35 + + KCM.ConfigModule.columnWidth: Kirigami.Units.gridUnit * 23 + KCM.ConfigModule.buttons: KCM.ConfigModule.Help | KCM.ConfigModule.Apply + + property var selectedIndexes: [] + + actions: [ + Kirigami.Action { + enabled: !exportInfo.visible + text: i18n("Add New…") + icon.name: "list-add-symbolic" + displayHint: Kirigami.DisplayHint.KeepVisible + onTriggered: kcm.createRule(); + }, + Kirigami.Action { + enabled: !exportInfo.visible + text: i18n("Import…") + icon.name: "document-import-symbolic" + onTriggered: importDialog.active = true; + }, + Kirigami.Action { + text: checked ? i18n("Cancel Export") : i18n("Export…") + icon.name: exportInfo.visible ? "dialog-cancel-symbolic" : "document-export-symbolic" + checkable: true + checked: exportInfo.visible + onToggled: { + selectedIndexes = []; + exportInfo.visible = checked; + } + } + ] + + // Manage KCM pages + Connections { + target: kcm + function onEditIndexChanged() { + if (kcm.editIndex < 0) { + // If no rule is being edited, hide RulesEdidor page + kcm.pop(); + } else if (kcm.depth < 2) { + // Add the RulesEditor page if it wasn't already + kcm.push("RulesEditor.qml"); + } + } + } + + view: ListView { + id: ruleBookView + clip: true + + model: kcm.ruleBookModel + currentIndex: kcm.editIndex + delegate: RuleBookDelegate {} + reuseItems: true + + highlightMoveDuration: Kirigami.Units.longDuration + + displaced: Transition { + NumberAnimation { + properties: "y" + duration: Kirigami.Units.longDuration + } + } + + Kirigami.PlaceholderMessage { + visible: ruleBookView.count === 0 + anchors.centerIn: parent + width: parent.width - (Kirigami.Units.largeSpacing * 4) + icon.name: "preferences-system-windows-actions" + text: i18n("No rules for specific windows are currently set"); + explanation: xi18nc("@info", "Click Add New… to add some") + } + } + + header: Kirigami.InlineMessage { + id: exportInfo + icon.source: "document-export" + showCloseButton: true + text: i18n("Select the rules to export") + actions: [ + Kirigami.Action { + icon.name: "dialog-ok-apply" + text: checked ? i18n("Unselect All") : i18n("Select All") + checkable: true + checked: selectedIndexes.length === ruleBookView.count + onToggled: { + if (checked) { + selectedIndexes = [...Array(ruleBookView.count).keys()] + } else { + selectedIndexes = []; + } + } + } + , + Kirigami.Action { + icon.name: "document-save" + text: i18n("Save Rules") + enabled: selectedIndexes.length > 0 + onTriggered: { + exportDialog.active = true; + } + } + ] + } + + Keys.onEscapePressed: event => { + if (exportInfo.visible) { + exportInfo.visible = false; + return; + } + event.accepted = false; + } + + component RuleBookDelegate : Item { + // External item required to make Kirigami.ListItemDragHandle work + width: ruleBookView.width + implicitHeight: ruleBookItem.implicitHeight + + ListView.onPooled: { + if (descriptionField.activeFocus) { + // If the description was being edited when the item is pooled, finish the edition + ruleBookItem.forceActiveFocus(); + } + } + + QQC2.ItemDelegate { + id: ruleBookItem + + width: ruleBookView.width + down: false // Disable press effect + + contentItem: RowLayout { + Kirigami.ListItemDragHandle { + visible: !exportInfo.visible + listItem: ruleBookItem + listView: ruleBookView + onMoveRequested: (oldIndex, newIndex) => { + kcm.moveRule(oldIndex, newIndex); + } + } + + QQC2.CheckBox { + visible: !exportInfo.visible + checked: model?.isEnabled ?? true + onToggled: model.isEnabled = checked + } + + QQC2.TextField { + id: descriptionField + Layout.minimumWidth: Kirigami.Units.gridUnit * 2 + Layout.fillWidth: true + background: Item {} + horizontalAlignment: Text.AlignLeft + text: model && model.display + onEditingFinished: { + kcm.setRuleDescription(index, text); + } + Keys.onPressed: event => { + switch (event.key) { + case Qt.Key_Escape: + // On key reset to model data before losing focus + text = model.display; + case Qt.Key_Enter: + case Qt.Key_Return: + case Qt.Key_Tab: + ruleBookItem.forceActiveFocus(); + event.accepted = true; + break; + } + } + + MouseArea { + anchors.fill: parent + enabled: exportInfo.visible + cursorShape: enabled ? Qt.PointingHandCursor : Qt.IBeamCursor + onClicked: { + itemSelectionCheck.toggle(); + itemSelectionCheck.toggled(); + } + } + } + + DelegateButton { + text: i18n("Edit") + icon.name: "edit-entry" + onClicked: kcm.editRule(index); + } + + DelegateButton { + text: i18n("Duplicate") + icon.name: "edit-duplicate" + onClicked: kcm.duplicateRule(index); + } + + DelegateButton { + text: i18n("Delete") + icon.name: "entry-delete" + onClicked: kcm.removeRule(index); + } + + QQC2.CheckBox { + id: itemSelectionCheck + visible: exportInfo.visible + checked: selectedIndexes.includes(index) + onToggled: { + var position = selectedIndexes.indexOf(index); + if (checked) { + if (position < 0) { selectedIndexes.push(index); } + } else { + if (position >= 0) { selectedIndexes.splice(position, 1); } + } + selectedIndexesChanged(); + } + } + } + } + } + + component DelegateButton: QQC2.ToolButton { + visible: !exportInfo.visible + display: QQC2.AbstractButton.IconOnly + QQC2.ToolTip.text: text + QQC2.ToolTip.visible: hovered + } + + FileDialogLoader { + id: importDialog + title: i18n("Import Rules") + isSaveDialog: false + onLastFolderChanged: { + exportDialog.lastFolder = lastFolder; + } + onFileSelected: path => { + kcm.importFromFile(path); + } + } + + FileDialogLoader { + id: exportDialog + title: i18n("Export Rules") + isSaveDialog: true + onLastFolderChanged: { + importDialog.lastFolder = lastFolder; + } + onFileSelected: path => { + selectedIndexes.sort(); + kcm.exportToFile(path, selectedIndexes); + exportInfo.visible = false; + } + } +} diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/CMakeLists.txt b/local/recipes/kde/kwin/source/src/kcms/screenedges/CMakeLists.txt new file mode 100644 index 0000000000..46d3b08f0d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/CMakeLists.txt @@ -0,0 +1,59 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcmkwinscreenedges\") + +set(kcm_screenedges_SRCS + monitor.cpp + screenpreviewwidget.cpp + kwinscreenedge.cpp + kwinscreenedgeconfigform.cpp +) + +qt_add_dbus_interface(kcm_screenedges_SRCS ${KWin_SOURCE_DIR}/src/org.kde.kwin.Effects.xml kwin_effects_interface) + +set(kcm_kwinscreenedges_PART_SRCS main.cpp ${kcm_screenedges_SRCS}) + +kcmutils_generate_module_data( + kcm_kwinscreenedges_PART_SRCS + MODULE_DATA_HEADER kwinscreenedgedata.h + MODULE_DATA_CLASS_NAME KWinScreenEdgeData + SETTINGS_HEADERS kwinscreenedgesettings.h + SETTINGS_CLASSES KWinScreenEdgeSettings + NAMESPACE KWin +) + +ki18n_wrap_ui(kcm_kwinscreenedges_PART_SRCS main.ui) +kconfig_add_kcfg_files(kcm_kwinscreenedges_PART_SRCS kwinscreenedgesettings.kcfgc kwinscreenedgescriptsettings.kcfgc kwinscreenedgeeffectsettings.kcfgc) +kcoreaddons_add_plugin(kcm_kwinscreenedges SOURCES ${kcm_kwinscreenedges_PART_SRCS} INSTALL_NAMESPACE "plasma/kcms/systemsettings_qwidgets") +set(kcm_screenedges_LIBS + Qt::DBus + + KF6::ConfigCore + KF6::KCMUtils + KF6::I18n + KF6::Package +) +target_link_libraries(kcm_kwinscreenedges ${X11_LIBRARIES} ${kcm_screenedges_LIBS}) + +qt_add_dbus_interface(kcm_kwintouchscreen_dbus_SRCS ${KWin_SOURCE_DIR}/src/org.kde.KWin.InputDeviceManager.xml inputdevicemanager_interface) + +set(kcm_kwintouchscreenedges_PART_SRCS + touch.cpp + kwintouchscreenedgeconfigform.cpp + kwintouchscreenmoduledata.cpp + ${kcm_screenedges_SRCS} + ${kcm_kwintouchscreen_dbus_SRCS} +) + +kcmutils_generate_module_data( + kcm_kwintouchscreenedges_PART_SRCS + MODULE_DATA_HEADER kwintouchscreendata.h + MODULE_DATA_CLASS_NAME KWinTouchScreenData + SETTINGS_HEADERS kwintouchscreensettings.h + SETTINGS_CLASSES KWinTouchScreenSettings + NAMESPACE KWin +) + +ki18n_wrap_ui(kcm_kwintouchscreenedges_PART_SRCS main.ui touch.ui) +kconfig_add_kcfg_files(kcm_kwintouchscreenedges_PART_SRCS kwintouchscreensettings.kcfgc kwintouchscreenscriptsettings.kcfgc kwintouchscreenedgeeffectsettings.kcfgc) +kcoreaddons_add_plugin(kcm_kwintouchscreen SOURCES ${kcm_kwintouchscreenedges_PART_SRCS} INSTALL_NAMESPACE "plasma/kcms/systemsettings_qwidgets") +target_link_libraries(kcm_kwintouchscreen ${X11_LIBRARIES} ${kcm_screenedges_LIBS}) diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/Messages.sh b/local/recipes/kde/kwin/source/src/kcms/screenedges/Messages.sh new file mode 100644 index 0000000000..2ab0771d80 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/Messages.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash +$EXTRACTRC *.ui >> rc.cpp || exit 11 +$XGETTEXT *.cpp -o $podir/kcmkwinscreenedges.pot +rm -f rc.cpp diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kcm_kwinscreenedges.json b/local/recipes/kde/kwin/source/src/kcms/screenedges/kcm_kwinscreenedges.json new file mode 100644 index 0000000000..85e4fb1c9d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kcm_kwinscreenedges.json @@ -0,0 +1,143 @@ +{ + "KPlugin": { + "BugReportUrl": "https://bugs.kde.org/enter_bug.cgi?product=systemsettings&component=kcm_kwinscreenedges", + "Description": "Configure active screen corners and edges", + "Description[ar]": "اضبط زوايا وحواف الشاشة", + "Description[az]": "Aktiv ekran künclərini və kənarlarını tənzimləyin", + "Description[be]": "Наладжванне актыўных кутоў і краёў экрана", + "Description[bg]": "Настройки на активните краища и ъгли на екрана", + "Description[ca@valencia]": "Configura els cantons i vores actives de la pantalla", + "Description[ca]": "Configura les cantonades i vores actives de la pantalla", + "Description[cs]": "Nastavit aktivní hrany a okraje obrazovky", + "Description[da]": "Konfigurér aktive skærmhjørner og -kanter", + "Description[de]": "Aktive Bildschirmränder und -ecken einrichten", + "Description[en_GB]": "Configure active screen corners and edges", + "Description[eo]": "Agordi aktivajn ekranajn angulojn kaj randojn", + "Description[es]": "Configurar las esquinas y los bordes activos de la pantalla", + "Description[et]": "Aktiivsete ekraani nurkade ja servade seadistamine", + "Description[eu]": "Konfiguratu pantailaren izkina eta ertz aktiboak", + "Description[fi]": "Aktiivisen näytön kulmien ja reunojen asetukset", + "Description[fr]": "Configurer les coins et les bords de l'écran actif", + "Description[gl]": "Configurar os bordos e esquinas activos da pantalla.", + "Description[he]": "הגדרת פינות וקצוות מסך פעילים", + "Description[hu]": "Az aktív képernyőszélek és sarkok beállítása", + "Description[ia]": "Configura margines e angulos de schermo active", + "Description[id]": "Konfigurasikan tepian dan sudut layar aktif", + "Description[is]": "Grunnstilla virk skjáhorn og brúnir", + "Description[it]": "Configura angoli e bordi attivi dello schermo", + "Description[ja]": "スクリーンエッジ機能を設定", + "Description[ka]": "აქტიური ეკრანის კუთხისა და კიდეების მორგება", + "Description[ko]": "활성 화면 경계와 꼭짓점 설정", + "Description[lt]": "Konfigūruoti aktyvius ekrano kampus ir kraštus", + "Description[lv]": "Konfigurēt aktīvā ekrāna stūrus un malas", + "Description[nb]": "Sett opp aktive skjermhjørne og skjermkanter", + "Description[nl]": "Actieve schermhoeken en -randen configureren", + "Description[nn]": "Set opp aktive skjermhjørne og skjermkantar", + "Description[pl]": "Ustawienia czułych narożników i krawędzi ekranu", + "Description[pt]": "Configurar os cantos e extremos activos do ecrã", + "Description[pt_BR]": "Configurar os cantos de bordas da tela ativa", + "Description[ro]": "Configurează colțurile și marginile active ale ecranului", + "Description[ru]": "Настройка действий для краёв и углов экрана", + "Description[sa]": "सक्रियपर्दे कोणान् किनारेश्च विन्यस्यताम्", + "Description[sk]": "Nastavenie aktívnych okrajov obrazovky a hrán", + "Description[sl]": "Prilagodite aktivna oglišča in robove zaslona", + "Description[sv]": "Anpassa aktiva skärmhörn och kanter", + "Description[ta]": "திரையின் ஓரங்களையும் மூலைகளையும் அமையுங்கள்", + "Description[tr]": "Etkin ekran köşeleri ve kenarlarını yapılandır", + "Description[uk]": "Налаштовування активних кутів і країв екрана", + "Description[vi]": "Cấu hình các góc và cạnh của màn hình hoạt động", + "Description[zh_CN]": "配置活动屏幕的四角和边缘操作", + "Description[zh_TW]": "設定作用中螢幕角落與邊緣", + "Icon": "preferences-desktop-gestures-screenedges", + "Name": "Screen Edges", + "Name[ar]": "حواف الشاشة", + "Name[az]": "Ekran kənarları", + "Name[be]": "Краі экрана", + "Name[bg]": "Краища на екрана", + "Name[ca@valencia]": "Vores de la pantalla", + "Name[ca]": "Vores de la pantalla", + "Name[cs]": "Hrany obrazovky", + "Name[da]": "Skærmkanter", + "Name[de]": "Bildschirmränder", + "Name[en_GB]": "Screen Edges", + "Name[eo]": "Ekranaj randoj", + "Name[es]": "Bordes de la pantalla", + "Name[et]": "Ekraani servad", + "Name[eu]": "Pantailaren ertzak", + "Name[fi]": "Näytön reunat", + "Name[fr]": "Bords de l'écran", + "Name[gl]": "Bordos da pantalla", + "Name[he]": "קצוות חלון", + "Name[hu]": "Képernyőszélek", + "Name[ia]": "Margines de schermo", + "Name[id]": "Tepian Layar", + "Name[is]": "Skjájaðrar", + "Name[it]": "Bordi dello schermo", + "Name[ja]": "スクリーンエッジ", + "Name[ka]": "ეკრანის კიდეები", + "Name[ko]": "화면 경계", + "Name[lt]": "Ekrano kraštai", + "Name[lv]": "Ekrāna malas", + "Name[nb]": "Skjermkanter", + "Name[nl]": "Schermranden", + "Name[nn]": "Skjerm­kantar", + "Name[pl]": "Krawędzie ekranu", + "Name[pt]": "Extremos do Ecrã", + "Name[pt_BR]": "Bordas da tela", + "Name[ro]": "Marginile ecranului", + "Name[ru]": "Края экрана", + "Name[sa]": "स्क्रीन एज्स्", + "Name[sk]": "Okraje obrazovky", + "Name[sl]": "Robovi zaslona", + "Name[sv]": "Skärmkanter", + "Name[ta]": "திரையின் ஓரங்கள்", + "Name[tr]": "Ekran Kenarları", + "Name[uk]": "Краї екрана", + "Name[vi]": "Cạnh màn hình", + "Name[zh_CN]": "屏幕边缘", + "Name[zh_TW]": "螢幕邊緣" + }, + "X-DocPath": "kcontrol/kwinscreenedges/index.html", + "X-KDE-Keywords": "kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,borders,screen borders,desktop borders,desktop corners,corners,sticky corners,sticky edges,edge barriers,corner barriers", + "X-KDE-Keywords[ar]": "كوين،نافذة،مدير،تأثير،زاوية،حافة،حدود،إجراء،تبديل،سطح المكتب،حواف شاشة كوين،حواف سطح المكتب،حواف الشاشة،تكبير النوافذ،تجانب النوافذ،جانب الشاشة،سلوك الشاشة،تبديل سطح المكتب،سطح المكتب الافتراضي،شاشة زوايا,حدود,حدود الشاشة,حدود سطح المكتب,زوايا سطح المكتب,زوايا,زوايا لاصقة,حواف لاصقة,حواجز الحافة,حواجز الزاوية", + "X-KDE-Keywords[bg]": "kwin,прозорец,мениджър,ефект,ъгъл,ръб,граница,действие,превключвател,работен плот,ръбове на екрана на kwin,ръбове на работния плот,ръбове на екрана,увеличаване на прозорците,прозорци с плочки,страна на екрана,поведение на екрана,превключване на работния плот,виртуален работен плот,екран ъгли,граници,граници на екрана,граници на работния плот,ъгли на работния плот,ъгли,лепкави ъгли,лепкави ръбове,бариери по ръбовете,бариери по ъглите", + "X-KDE-Keywords[ca@valencia]": "kwin,finestra,gestor,efecte,cantó,vora,acció,commutació,escriptori,vores de pantalla del kwin,vores d'escriptori,vores de pantalla,maximitzar les finestres,mosaic de finestres,costat de pantalla,comportament de pantalla,canviar escriptori,escriptori virtual,cantons de pantalla,vores,vores de pantalla,vores d'escriptori,cantons d'escriptori,cantons,cantons apegaloses,vores apegaloses,barreres de vores,barreres de cantons", + "X-KDE-Keywords[ca]": "kwin,finestra,gestor,efecte,cantonada,vora,acció,commutació,escriptori,vores de pantalla del kwin,vores d'escriptori,vores de pantalla,maximitzar les finestres,mosaic de finestres,costat de pantalla,comportament de pantalla,commutar escriptori,escriptori virtual,cantonades de pantalla,vores,vores de pantalla,vores d'escriptori,cantonades d'escriptori,cantonades,cantonades apegaloses,vores apegaloses,barreres de vores,barreres de cantonades", + "X-KDE-Keywords[de]": "KWin,Fenster,Verwaltung,Effekt,Kante,Rand,Aktion,Wechseln,Desktop,Arbeitsfläche,KWin Bildschirmkanten,Desktopkanten,Bildschirmkanten,Fenster maximieren,Fenster kacheln,Bildschirmseite,Bildschirmverhalten,Desktop wechseln,Arbeitsfläche wechseln,Virtueller Desktop,Virtuelle Arbeitsfläche,Bildschirmecken,Ränder,Bildschirmränder,Arbeitsflächenecken,Ecken,klebende Ecken,klebende Ränder,Eckenbarriere,Randbarriere", + "X-KDE-Keywords[en_GB]": "kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,borders,screen borders,desktop borders,desktop corners,corners,sticky corners,sticky edges,edge barriers,corner barriers", + "X-KDE-Keywords[es]": "kwin,ventana,gestor,efecto,esquina,borde,acción,cambio,cambiar,escritorio,bordes de la pantalla de kwin,bordes del escritorio,bordes de la pantalla,maximizar ventanas,ventanas en mosaico,lado de la pantalla,comportamiento de la pantalla,cambiar escritorio,escritorio virtual,esquinas de la pantalla,bordes,bordes de la pantalla,bordes del escritorio,esquinas del escritorio,esquinas,esquinas adhesivas,bordes adhesivos,barreras de los bordes,barreras de las esquinas", + "X-KDE-Keywords[eu]": "kwin,leihoa,kudeatzailea,efektua,izkina,ertza,muga,bazterra,ekintza,aldatu,mahaigaina,kwin pantaila-ertzak,mahaiganaren ertzak,pantailaren ertzak,maximizatu leihoak,teilakatu leihoak,pantailaren alboa,pantailaren jokabidea,aldatu mahaigaina,alegiazko mahaigaina,mahaigain birtuala,pantailaren izkinak,mugak,pantailaren mugak,mahaigainaren mugak,mahaigainaren izkinak,izkinak,izkina itsaskorrak,ertz itsaskorrak,ertzeko hesiak,izkinako hesiak", + "X-KDE-Keywords[fi]": "kwin,ikkuna,hallinta,tehoste,nurkka,kulma,reuna,laita,toiminta,toiminto,vaihto,vaihda,työpöytä,työpöydän reunat,työpöydän laidat,suurenna ikkunat,laatoita ikkunat,asettele ikkunat,asettelu,näytön toiminta,vaihda työpöytää,työpöytävaihto,virtuaalityöpöytä,tahmeat reunat,tahmeat laidat,laitaesteet,nurkkaesteet,kulmaesteet", + "X-KDE-Keywords[fr]": "kwin, fenêtre, gestionnaire, effet, coin, bord, bords, bordure, action, commutateur, bureau, bords d'écran de KWin, bords de bureau, bords d'écran,maximiser les fenêtres, fenêtres en mosaïque, côté de l'écran, comportement de l'écran, changer de bureau, bureau virtuel, coins d'écran, bordures, bordures d'écran, bordures de bureau, coins de bureau, coins, coins aimantés, bords aimantés, barrières de bord, barrières de coin", + "X-KDE-Keywords[gl]": "kwin,xanela,xestor,efecto,esquina,beira,bordo,bordo,acción,trocar,escritorio,bordo do escritorio,maximizar xanelas,escritorio virtual,esquinas da pantalla,borders,bordos,extremos,screen borders,desktop borders,desktop corners,corners,esquinas,sticky corners,pegañentas,pegañentos,sticky edges,edge barriers,barreiras,límites,corner barriers", + "X-KDE-Keywords[he]": "kwin,חלון,מנהל,אפקט,פינה,קצה,גבול,פעולה,החלפה,שולחן־עבודה,קצוות מסך של kwin,קצוות שולחן עבודה,קצוות מסך,הגדלת חלונות,אריחי חלונות,צד המסך,התנהגות מסך,החלפת שולחן עבודה,שולחן־עבודה וירטואלי,פינות מסך,גבולות,גבולות מסך,גבולותשולחן עבודה,פינות שולחן עבודה,פינות,פינות דביקות,קצוות דביקים,מחסומי קצה,מחסומי פינה,מחסומי גבול,מסגרות גבול,מסגרות קצה", + "X-KDE-Keywords[hu]": "kwin,ablak,kezelő,effektusok,sarok,szegély,keret,művelet,váltás,asztal,kwin képernyőszélek,asztalszélek,képernyőszélek,ablakok maximalizálása,mozaikablakok,képernyő oldala,képernyőműködés,asztalváltás,virtuális asztal,képernyősarkok,szegélyek,képernyőszegélyek,asztalszegélyek,asztalsarkok,sarkok,ragadós sarkok,ragadós élek,élkorlátok,sarokkorlátok", + "X-KDE-Keywords[ia]": "kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,borders,screen borders,desktop borders,desktop corners,corners,sticky corners,sticky edges,edge barriers,corner barriers", + "X-KDE-Keywords[is]": "kwin,gluggi,stjóri,brella,horn,jaðar,brún,aðgerð,skiipta,skjáborð,kwin skjájaðrar,skjáborðsjaðrar,skjájaðrar,fullstækka glugga,reitaraða gluggum,hlið á skjá,skjáhegðun,skipta um skjáborð,sýndarskjáborð,skjáhorn,brúnir,skjábrúnir,skjáborðsbrúnir,skjáborðshorn,horn,festihorn, ,brúnaþröskuldar,hornþröskuldar", + "X-KDE-Keywords[it]": "kwin,finestra,gestore,effetto,angolo,bordo,bordo,azione,cambia,desktop,bordi dello schermo di kwin,bordi del desktop,bordi dello schermo,ingrandisci le finestre,affianca le finestre,lato dello schermo,comportamento dello schermo,cambia desktop,desktop virtuale,angoli dello schermo,bordi,bordi dello schermo,bordi del desktop,angoli del desktop,angoli,angoli fissi,bordi fissi,barriere dei bordi,barriere degli angoli", + "X-KDE-Keywords[ja]": "kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,borders,screen borders,desktop borders,desktop corners,corners,sticky corners,sticky edges,edge barriers,corner barriers,ウィンドウ,マネージャー,管理,エフェクト,効果,演出,隅,コーナー,角,エッジ,端,境界,縁,アクション,操作,切り替え,デスクトップ,スクリーンエッジ,画面の端,画面の縁,ウィンドウの最大化,タイル表示,並べる,画面の脇,画面の挙動,デスクトップの切り替え,仮想デスクトップ,画面の四隅,ボーダー,枠線,吸い付き,吸い付く,粘着コーナー,粘着エッジ,エッジバリア,バリア,コーナーバリア,エッジバリア", + "X-KDE-Keywords[ka]": "kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,borders,screen borders,desktop borders,desktop corners,corners,sticky corners,sticky edges,edge barriers,corner barriers,ფანჯარა,მმართველი,ეფექტები,წიბო,კუთხე,საზღვარი,ქმედება,ეკრანის კუთხეები,სამუშაო მაგიდის კუთხეები", + "X-KDE-Keywords[ko]": "창,관리자,효과,모서리,경계선,테두리,동작,액션,전환,kwin 화면 경계,화면 경계,창 최대화,최대화,바둑판식 배열,화면 행동,데스크톱 전환,가상 데스크톱,화면 모서리,꼭짓점,바탕 화면 전환,가상 바탕 화면,화면 모서리", + "X-KDE-Keywords[lt]": "kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,borders,screen borders,desktop borders,desktop corners,corners,sticky corners,sticky edges,edge barriers,corner barriers,langų,langu,tvarkytuvė,tvarkytuve,efektas,efektai,kampas,kraštas,krastas,rėmelis,remelis,veiksmas,perjungti,darbalaukis,darbalaukį,darbalauki,darbastalis,darbastalį,darbastali,kwin ekrano kraštai,kwin ekrano krastai,darbalaukio kraštai,darbalaukio krastai,ekrano kraštai,ekrano krastai,išskleisti langus,isskleisti langus,iškloti langus,iskloti langus,ekrano šonas,ekrano sonas,ekrano pusė,ekrano puse,ekrano elgsena,ekrano elgesys,perjungti darbalaukį,perjungti darbalauki,virtualus darbalaukis,virtualus darbastalis,ekrano kampai,kraštai,krastai,ekrano rėmelis,ekrano remelis,darbalaukio kraštai,darbalaukio krastai,darbalaukio rėmelis,darbalaukio remelis,darbalaukio kampai,darbastalio kampai,kampai,lipnūs kampai,lipnus kampai,lipnūs kraštai,lipnus krastai,krašto barjerai,krasto barjerai,krašto kliūtys,krasto kliutys,kampo barjerai,kampo kliūtys,kampo kliutys", + "X-KDE-Keywords[lv]": "kwin,logs,pārvaldnieks,efekts,stūris,mala,apmale,darbība,slēdzis,darbvirsma,kwin ekrāna malas,darbvirsmas malas,ekrāna malas,maksimizēt logus,flīzēt logus,ekrāna mala,ekrāna uzvedība,pārslēgt darbvirsmu,virtuālā darbvirsma,ekrāna stūri,robežas,ekrāna robežas,darbvirsmas robežas,darbvirsmas stūri,stūri,lipīgie stūri,lipīgās malas,malu barjeras,stūru barjeras", + "X-KDE-Keywords[nb]": "kwin,vindu,behandler,effekt,kant,hjørne,ramme,handling,bytte,skrivebord,kwin skjermkanter,skrivebordskanter,skjermkanter,maksimere vindu,flislegge vindu,skjermside,skjermatferd,bytte skrivebord,virtuelt skrivebord,skjermhjørne,kanter,ramme,hjørne,skrivebordskanter,skrivebordshjørner,hjørnebarriere,kantbarriere", + "X-KDE-Keywords[nl]": "kwin,venster,beheerder,effect,hoek,rand,grens,actie,schakelaar,bureaublad,kwin-schermrand,bureaubladranden,schermrand,vensters maximaliseren,vensters schuin achter elkaar zetten,zijkant van het scherm,schermgedrag,bureaublad wisselen,virtueel bureaublad,schermhoeken,randen,schermranden,bureaubladranden,bureaubladhoeken,hoeken,plakkende hoeken,plakkende randen,randbarrières,hoekbarrières", + "X-KDE-Keywords[nn]": "kwin,vindauge,handsamar,effekt,kant,hjørne,ramme,handling,byte,skrivebord,kwin skjermkantar,skrivebordskantar,skjermkantar,maksimera vindauge,flisleggja vindauge,skjermside,skjermåtferd,byte skrivebord,virtuelt skrivebord,skjermhjørne,kantar,ramme,hjørne,skrivebordskantar,skrivebordshjørner,hjørnebarriere,kantbarriere", + "X-KDE-Keywords[pl]": "kwin,okno,menadżer,efekt,narożnik,krawędź,granica,działanie,przełącz,pulpit,krawędzie ekranu kwin,krawędzie pulpitu,krawędzie ekranu,zmaksymalizuj okna,okna do kafelków,bok ekranu,zachowanie ekranu,przełącz pulpit,pulpit wirtualny,narożniki ekranu,krawędzie,krawędzie ekranu,krawędzie pulpitu,narożniki pulpitu,narożniki,lepkie narożniki,lepkie krawędzie,bariery krawędziowe,bariery narożnikowe", + "X-KDE-Keywords[pt_BR]": "kwin,janela,gerenciador,efeito,canto,borda,ação,trocar,área de trabalho,limites de tela kwin,limites de área de trabalho,limites de tela,maximizar janelas,ajustar janelas,lado da tela,comportamento da tela,trocar área de trabalho,área de trabalho virtual,cantos da tela,bordas da tela,bordas de área de trabalho,cantos de área de trabalho,cantos,bordas fixas,bordas fixas,barreiras de borda,barreiras de canto", + "X-KDE-Keywords[ro]": "kwin,gestionar,ferestre,efect,colț,margine,muchie,contur,acțiune,schimbă,birou,margini ecran kwin,margini birou,marginile biroului,marginile ecranului,maximizează ferestre,mozaichează ferestre,latura ecranului,comportament ecran,schimbă biroul,biroul virtual,colțuri ecran,colțurile ecranului,contururi,contururile ecranelor,contururile birourilor,contururi birou,colțuri birou,colțurile biroului,colțuri,colțuri lipicioase,margini lipicioase,barieră margine,bariere margini,barieră colț,barieră la colț,bariere colțuri,bariere la colțuri", + "X-KDE-Keywords[ru]": "kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,borders,screen borders,desktop borders,desktop corners,corners,sticky corners,sticky edges,edge barriers,corner barriers,окно,диспетчер,эффект,угол,край,граница,действие,переключатель,рабочий стол,края экрана kwin,края экрана,края рабочего стола,развернуть окна,распахнуть окна,сторона экрана,поведение экрана,переключить рабочий стол,виртуальный рабочий стол,углы экрана,границы,границы экрана,границы рабочего стола,углы рабочего стола,углы,липкие углы,липкие края,краевые барьеры,угловые барьеры", + "X-KDE-Keywords[sa]": "kwin,विंडो,प्रबन्धक,प्रभाव,कोने,धार,सीमा,क्रिया,स्विच,डेस्कटॉप,kwin स्क्रीन किनारे,डेस्कटॉप किनारे,स्क्रीन किनारे,विंडोज,टाइल विंडोज,स्क्रीन के पक्ष,स्क्रीन व्यवहार,स्विच डेस्कटॉप,वर्चुअल डेस्कटॉप,स्क्रीन कोण,सीमा,स्क्रीन सीमा,डेस्कटॉप सीमा,डेस्कटॉप कोण,कोने,चिपचिपा कोणाः,चिपचिपाधाराः,धारबाधाः,कोणबाधाः", + "X-KDE-Keywords[sk]": "kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,borders,screen borders,desktop borders,desktop corners,corners,sticky corners,sticky edges,edge barriers,corner barriers", + "X-KDE-Keywords[sl]": "kwin,okno,upravitelj,učinek,kotiček,rob,obroba,dejanje,stikalo,namizje,kwin robovi zaslona,robovi namizja,robovi zaslona,maksimiraj okna,okroži okna,stran zaslona,vedenje zaslona,preklopi namizje,virtualno namizje,zaslon vogali,obrobe,obrobe zaslona, ​obrobe namizja, vogali namizja,vogali,lepljivi vogali,lepljivi robovi,robne ovire,kotne ovire", + "X-KDE-Keywords[sv]": "kwin,fönster,hanterare,effekt,hörn,kant,kant,åtgärd,byta,skrivbord,kwin-skärmkanter,skrivbordskanter,skärmkanter,maximera fönster,fönster sida vid sida,sidan av skärmen,skärmbeteende,byt skrivbord,virtuellt skrivbord,skärm hörn,kanter,skärmkanter,skrivbordskanter,skrivbordshörn,hörn,klistriga hörn,klistriga kanter,kantbarriärer,hörnbarriärer", + "X-KDE-Keywords[tr]": "kwin,pencere,yönetici,efekt,köşe,kenar,kenarlık,eylem,değiştir,masaüstü,kwin ekran kenarları,ekran kenarları,ekranı kapla,simge durumuna küçült,küçült,döşe,ekranın kenarı,ekran davranışı,masaüstü kenarları,yapışkan köşeler,yapışkan kenarlar,kenar bariyerleri,köşe bariyerleri", + "X-KDE-Keywords[uk]": "kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,borders,screen borders,desktop borders,desktop corners,corners,sticky corners,sticky edges,edge barriers,corner barriers,квін,вікно,керування,засіб,ефект,кут,край,рамка,дія,перемикання,стільниця,краї екрана,краї стільниці,максимізація,максимізувати,мозаїка,бік екрана,поведінка,перемикання,віртуальна,кути екрана,рамки,кути,липкі кути,бар'єри краю,бар'єри кута", + "X-KDE-Keywords[zh_CN]": "kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,chuangkou,guanli,texiao,sijiao,bianjiao,jiaoluo,bianyuan,caozuo,qiehuan,zhuomian,kwin pingmubianyuan,zhuomianbianyuan,zuidahuachuangkou,pingpuchuangkou,pingmuxingwei,zhuomianqiehuan,xunizhuomian,xunizhuomianqiehuan,pingmujiaoluo,pingmubianjiao,biankuang,pingmubiankuang,bianyan,pingmubianyan,zhuomianbiankuang,zhuomianbianyan,zhuomianbianyuan,zhuomiansijiao,zhuomianjiaoluo,zhuomianbianjiao,zhanxing,zhanzhi,zhanxingjiao,zhanxingjiaoluo,zhanxingpingmujiaoluo,zhanxingbian,zhanxingbianyuan,zhanxingpingmubianyuan,zhangai,bianyuanzhangai,pingmubianyuanzhangai,jiaoluozhangai,pingmujiaoluozhangai,窗口,管理,特效,四角,边角,角落,边缘,操作,切换,桌面,kwin 屏幕边缘,桌面边缘,最大化窗口,平铺窗口,屏幕行为,桌面切换,虚拟桌面,虚拟桌面切换,屏幕角落,屏幕边角,边框,屏幕边框,边沿,屏幕边沿,桌面边框,桌面边沿,桌面边缘,桌面四角,桌面角落,桌面边角,粘性,粘滞,粘性角,粘性角落,粘性屏幕角落,粘性边,粘性边缘,粘性屏幕边缘,障碍,边缘障碍,屏幕边缘障碍,角落障碍,屏幕角落障碍", + "X-KDE-Keywords[zh_TW]": "kwin,視窗,視窗管理員,效果,角落,邊緣,邊框,動作,切換,桌面,螢幕邊緣,桌面邊緣,最大化視窗,鋪排視窗,螢幕行為,切換桌面,虛擬桌面,螢幕角落,角落屏障,邊緣屏障", + "X-KDE-OnlyShowOnQtPlatforms": [ + "wayland" + ], + "X-KDE-System-Settings-Parent-Category": "display", + "X-KDE-Weight": 100 +} diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kcm_kwintouchscreen.json b/local/recipes/kde/kwin/source/src/kcms/screenedges/kcm_kwintouchscreen.json new file mode 100644 index 0000000000..744cb19fd1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kcm_kwintouchscreen.json @@ -0,0 +1,143 @@ +{ + "KPlugin": { + "BugReportUrl": "https://bugs.kde.org/enter_bug.cgi?product=systemsettings&component=kcm_kwintouchscreen", + "Description": "Configure touch screen swipe gestures", + "Description[ar]": "اضبط إيماءات شاشة اللمس ", + "Description[az]": "Toxunma ekranı jestlərini tənzimləyin", + "Description[be]": "Наладжванне жэстаў вядзення пальцам па сэнсарным экране", + "Description[bg]": "Конфигуриране на жестове за плъзгане на сензорен екран", + "Description[ca@valencia]": "Configura els gestos de lliscament en la pantalla tàctil", + "Description[ca]": "Configura els gestos de lliscament en la pantalla tàctil", + "Description[cs]": "Nastavit gesta tahem na dotykové obrazovce", + "Description[da]": "Konfigurér swipegestus for touchskærme", + "Description[de]": "Wischgesten für Touchscreens einrichten", + "Description[en_GB]": "Configure touch screen swipe gestures", + "Description[eo]": "Agordi tuŝekranajn glitajn gestojn", + "Description[es]": "Configurar los gestos de deslizamiento de la pantalla táctil", + "Description[et]": "Puuteekraani žestide seadistamine", + "Description[eu]": "Konfiguratu ukimen-pantailako kolpe arinen keinuak", + "Description[fi]": "Kosketusnäytön pyyhkäisyeleiden asetukset", + "Description[fr]": "Configurer les gestes de balayage de l'écran tactile", + "Description[gl]": "Configurar os acenos de pantalla táctil.", + "Description[he]": "הגדרת מחוות החלקה למסך מגע", + "Description[hu]": "Az érintőképernyő áthúzó kézmozdulatainak beállítása", + "Description[ia]": "Configura gestos e glissar de schermo tactil", + "Description[id]": "Konfigurasikan gestur usapan layar sentuh", + "Description[is]": "Grunnstilla hreyfiskipanir á snertiskjá", + "Description[it]": "Configura gesti dello schermo a sfioramento", + "Description[ja]": "スワイプジェスチャー機能を設定", + "Description[ka]": "სენსორიანი ეკრანის გასმის ჟესტების მორგება", + "Description[ko]": "터치스크린 밀기 제스처 설정", + "Description[lt]": "Konfigūruoti jutiklinio ekrano braukymo gestus", + "Description[lv]": "Konfigurēt skārienekrāna vilkšanas žestus", + "Description[nb]": "Sett opp fingerbevegelser på trykkskjerm", + "Description[nl]": "Veeggebaren voor aanraakscherm configureren", + "Description[nn]": "Set opp fingerrørsler på trykkskjerm", + "Description[pl]": "Ustawienia gestów na ekranie dotykowym", + "Description[pt]": "Configurar os gestos de deslizamento do ecrã táctil", + "Description[pt_BR]": "Configurar os gestos da tela sensível ao toque", + "Description[ro]": "Configurează gesturi de tragere pe ecran tactil", + "Description[ru]": "Настройка действий при проведении по сенсорному экрану", + "Description[sa]": "स्पर्शपर्दे स्वाइप् इशारान् विन्यस्यताम्", + "Description[sk]": "Nastaviť ťahacie gestá dotykovej obrazovky", + "Description[sl]": "Prilagodite geste drsenja po zaslonu z dotikom", + "Description[sv]": "Anpassa draggester för pekskärm", + "Description[ta]": "தொடுதிரையில் பயன்படுத்தக்கூடிய சைகைகளை அமையுங்கள்", + "Description[tr]": "Dokunmatik ekran süpürme hareketlerini yapılandır", + "Description[uk]": "Налаштовування жестів на сенсорному екрані", + "Description[vi]": "Cấu hình các động tác hất của màn hình cảm ứng", + "Description[zh_CN]": "配置触摸屏滑动手势", + "Description[zh_TW]": "設定觸控螢幕滑動手勢", + "Icon": "preferences-desktop-gestures-touch", + "Name": "Touchscreen Gestures", + "Name[ar]": "إيماءات شاشة اللمس", + "Name[ast]": "Xestos de la pantalla táctil", + "Name[be]": "Жэсты сэнсарнага экрана", + "Name[bg]": "Жестове за сензорен екран", + "Name[ca@valencia]": "Gestos de pantalla tàctil", + "Name[ca]": "Gestos de pantalla tàctil", + "Name[cs]": "Gesta dotykové obrazovky", + "Name[da]": "Touchskærmsgestus", + "Name[de]": "Wischgesten für Touchscreens", + "Name[en_GB]": "Touchscreen Gestures", + "Name[eo]": "Tuŝekranaj Gestoj", + "Name[es]": "Gestos de la pantalla táctil", + "Name[eu]": "Ukimen-pantailako keinuak", + "Name[fi]": "Kosketusnäyttöeleet", + "Name[fr]": "Gestes de balayage de l'écran tactile", + "Name[gl]": "Acenos de pantalla táctil", + "Name[he]": "מחוות מסך מגע", + "Name[hu]": "Érintőképernyős mozdulatok", + "Name[ia]": "Gestures de schermo tactile", + "Name[id]": "Gestur Layar Sentuh", + "Name[is]": "Hreyfiskipanir á snertiskjá", + "Name[it]": "Gesti dello schermo tattile", + "Name[ja]": "タッチスクリーンジェスチャー", + "Name[ka]": "სენსორული ეკრანის ჟესტები", + "Name[ko]": "터치스크린 제스처", + "Name[lt]": "Jutiklinio ekrano gestai", + "Name[lv]": "Skārienjutības žesti", + "Name[nb]": "Fingerbevegelser", + "Name[nl]": "Veeggebaren voor aanraakscherm", + "Name[nn]": "Fingerrørsler", + "Name[pl]": "Gesty na ekranie dotykowym", + "Name[pt]": "Gestos do Ecrã Táctil", + "Name[pt_BR]": "Gestos da tela sensível ao toque", + "Name[ro]": "Gesturi pe ecran tactil", + "Name[ru]": "Поддержка жестов для сенсорных экранов", + "Name[sa]": "टचस्क्रीन् इशारा", + "Name[sk]": "Gestá dotykovej obrazovky", + "Name[sl]": "Geste po zaslonu z dotikom", + "Name[sv]": "Pekskärmsgester", + "Name[ta]": "தொடுதிரைக்கான சைகைகள்", + "Name[tr]": "Dokunmatik Ekran Hareketleri", + "Name[uk]": "Жести на сенсорній панелі", + "Name[vi]": "Các động tác của màn hình cảm ứng", + "Name[zh_CN]": "触摸屏手势", + "Name[zh_TW]": "觸控螢幕手勢" + }, + "X-DocPath": "kcontrol/kwintouchscreen/index.html", + "X-KDE-Keywords": "kwin,window,manager,effect,edge,border,action,switch,desktop,desktop edges,screen edges,side of screen,screen behavior,touch screen", + "X-KDE-Keywords[ar]": "كوين،نافذة،مدير،تأثير،حافة،حدود،إجراء،تبديل،سطح المكتب،حواف سطح المكتب،حواف الشاشة،جانب الشاشة،سلوك الشاشة،شاشة تعمل باللمس", + "X-KDE-Keywords[bg]": "kwin,прозорец,мениджър,ефект,ръб,рамка,действие,превключвател,работен плот,ръбове на работния плот,ръбове на екрана,страна на екрана,поведение на екрана,сензорен екран", + "X-KDE-Keywords[ca@valencia]": "kwin,finestra,gestor,efecte,vora,acció,canvia,escriptori,vores d'escriptori,vores de pantalla,costat de pantalla,comportament de pantalla,pantalla tàctil", + "X-KDE-Keywords[ca]": "kwin,finestra,gestor,efecte,vora,acció,commuta,escriptori,vores d'escriptori,vores de pantalla,costat de pantalla,comportament de pantalla,pantalla tàctil", + "X-KDE-Keywords[de]": "KWin,Fenster,Verwaltung,Effekt,Kante,Rand,Aktion,umschalten,Arbeitsfläche,Arbeitsflächenränder,Bildschirmränder,Seite des Bildschirms,Bildschirmseite,Bildschirmverhalten,Touchscreen", + "X-KDE-Keywords[en_GB]": "kwin,window,manager,effect,edge,border,action,switch,desktop,desktop edges,screen edges,side of screen,screen behavior,touch screen", + "X-KDE-Keywords[es]": "kwin,ventana,gestor,administrador,efecto,borde,filo,lado,acción,cambio,escritorio,bordes del escritorio,bordes de la pantalla,lados de la pantalla,comportamiento de la ventana,pantalla táctil", + "X-KDE-Keywords[eu]": "kwin,leihoa,kudeatzailea,efektua,ertza,muga,bazterra,ekintza,aldatu,ekintza,mahaigaina,mahaigainaren ertzak,pantailaren ertzak,pantailaren alboa,pantailaren jokabidea,ukimen-pantaila", + "X-KDE-Keywords[fi]": "kwin,ikkuna,hallinta,tehoste,efekti,reuna,laita,toiminto,toiminta,vaihda,vaihto,työpöytä,työpöydän nurkat,työpöydän kulmat,näytön laita,näytön toiminta,näytön käyttäytyminen,kosketusnäyttö", + "X-KDE-Keywords[fr]": "kwin, fenêtre, gestionnaire, effet, bord, bordure, action, commutateur, bureau, bords de bureau, bords de l'écran, côté de l'écran, comportement de l'écran, écran tactile", + "X-KDE-Keywords[gl]": "kwin,window,xanela,manager,xestor,effect,efecto,edge,beira,bordo,contorno,esquina,border,action,acción,switch,cambiar,conmutar,trocar,desktop,escritorio,desktop edges,screen edges,pantalla,side of screen,screen behavior,comportamento,touch screen,táctil", + "X-KDE-Keywords[he]": "kwin,חלון,מנהל,ניהול,אפקט,קצה,שול,גבול,פעולה,החלפה,בורר,שולחן עבודה,קצוות שולחן עבודה,פינות שולחן עבודה,קצוות מסך,פינות מסך,צד המסך,התנהגות מסך,מסך מגע", + "X-KDE-Keywords[hu]": "kwin,ablak,kezelő,effektus,él,szegély,művelet,váltás,asztal,asztalélek,képernyőélek,képernyő oldala,képernyőműködés,érintőképernyő", + "X-KDE-Keywords[ia]": "kwin,window,manager,effect,edge,border,action,switch,desktop,desktop edges,screen edges,side of screen,screen behavior,touch screen", + "X-KDE-Keywords[is]": "kwin,gluggi,stjóri,brella,jaðar,brún,aðgerð,skipta,skjáborð,skjáborðsjaðrar,skjájaðrar,hliðar á skjá,skjáhegðun,snertiskjár", + "X-KDE-Keywords[it]": "kwin,finestra,gestore,effetto,angolo,bordo,azione,selettore,desktop,bordi desktop,bordi schermo,lato dello schermo,comportamento schermo,schermo a sfioramento", + "X-KDE-Keywords[ja]": "kwin,window,manager,effect,edge,border,action,switch,desktop,desktop edges,screen edges,side of screen,screen behavior,touch screen,ウィンドウ,マネージャー,管理,効果,演出,エッジ,角,端,隅,ボーダー,境界,アクション,スイッチ,切り替え,デスクトップ,デスクトップの角,デスクトップの隅,デスクトップの端,画面の角,画面の隅,画面の端,画面の動作,タッチスクリーン", + "X-KDE-Keywords[ka]": "kwin,window,manager,effect,edge,border,action,switch,desktop,desktop edges,screen edges,side of screen,screen behavior,touch screen,სენსორული ეკრანი,ეკრანის ქცევა,ეკრანის წიბოები,ეკრანის კუთხეები,გადართვა,სამუშაო მაგიდა,წიბო,ქმედება,ეფექტი,მართვა,ფანჯარა", + "X-KDE-Keywords[ko]": "창,관리자,효과,경계,경계선,동작,액션,데스크톱,화면 경계,경계,터치,터치 스크린,터치스크린,바탕 화면", + "X-KDE-Keywords[lt]": "kwin,window,manager,effect,edge,border,action,switch,desktop,desktop edges,screen edges,side of screen,screen behavior,touch screen,langų,langu,tvarkytuvė,tvarkytuve,efektas,efektai,kraštas,krastas,kraštai,krastai,rėmelis,remelis,veiksmas,perjungti,darbalaukis,darbalaukį,darbalauki,darbastalis,darbastalį,darbastali,darbalaukio kraštai,darbalaukio krastai,ekrano kraštai,ekrano krastai,ekrano šonas,ekrano sonas,ekrano elgsena,ekrano elgesys,jutiklinis ekranas,liečiamas ekranas,lieciamas ekranas", + "X-KDE-Keywords[lv]": "kwin,logs,pārvaldība,efekts,malas,apmale,darbība,pārslēgt,darbvirsma,darbvirsmas malas,ekrāna malas,ekrāna mala,ekrāna uzvedība,skārienjutīgs ekrāns", + "X-KDE-Keywords[nb]": "kwin,vindu,behandler,effekt,kant,ramme,handling,bytte,skrivebord,skrivebordkanter,skjermkanter,skjermside,skjermatferd,trykkskjerm", + "X-KDE-Keywords[nl]": "kwin,vensterbeheerder,effect,rand,grens,actie,schakelaar,bureaublad,bureaubladranden,schermranden,zijkant van scherm,schermgedrag,aanraakscherm", + "X-KDE-Keywords[nn]": "kwin,vindauge,handsamar,effekt,kant,ramme,handling,byte,skrivebord,skrivebordkantar,skjermkantar,skjermside,skjermåtferd,trykkskjerm", + "X-KDE-Keywords[pl]": "kwin,okno,menadżer,efekt,krawędź,obramowanie,działanie,przełącz,pulpit,krawędzie pulpitu,krawędzie ekranu,strona ekranu,zachowanie ekranu,ekran dotykowy", + "X-KDE-Keywords[pt_BR]": "kwin,janela,gerenciador,efeito,borda,ação,trocar,área de trabalho,limites de área de trabalho,limites de tela,lado da tela,comportamento da tela,tela sensível ao toque,touchscreen, touch screen", + "X-KDE-Keywords[ro]": "kwin,fereastră,gestionar,efect,margine,muchie,contur,acțiune,schimbă,birou,margini birou,marginile biroului,marginile ecranului,latura ecranului,comportament ecran,ecran tactil", + "X-KDE-Keywords[ru]": "kwin,window,manager,effect,edge,border,action,switch,desktop,desktop edges,screen edges,side of screen,screen behavior,touch screen,окно,диспетчер,эффект,край,граница,действие,переключатель,рабочий стол,края рабочего стола,края экрана,сторона экрана,поведение экрана,прикосновение,касаться,коснуться", + "X-KDE-Keywords[sa]": "kwin,विंडो,प्रबंधक,प्रभाव,धार,सीमा,क्रिया,स्विच,डेस्कटॉप,डेस्कटॉप किनारे,स्क्रीन किनारे,स्क्रीन के पक्ष,स्क्रीन व्यवहार,स्पर्श स्क्रीन", + "X-KDE-Keywords[sk]": "kwin,window,manager,effect,edge,border,action,switch,desktop,desktop edges,screen edges,side of screen,screen behavior,touch screen", + "X-KDE-Keywords[sl]": "kwin,okno,upravljalnik,učinek,rob,obroba,dejanje,stikalo,namizje,robovi namizja,robovi zaslona,stran zaslona,vedenje zaslona,zaslon na dotik", + "X-KDE-Keywords[sv]": "kwin,fönster,hanterare,effekt,kant,kant,åtgärd,byta,skrivbord,skrivbordskanter,skärmkanter,sidan av skärmen,skärmbeteende,pekskärm", + "X-KDE-Keywords[tr]": "kwin,pencere,yönetici,efekt,kenar,kenarlık,eylem,değiştir,masaüstü,masaüstü kenarları,ekran kenarları,ekranın kenarı,ekran davranışı,dokunmatik ekran", + "X-KDE-Keywords[uk]": "kwin,window,manager,effect,corner,edge,border,action,switch,desktop,desktop edges,screen edges,side of screen,screen behavior,touch screen,вікно,керування,край,кут,межа,сторона,бік,дія,перемикання,стільниця,плитка,край екрана,поведінка екрана,перемикання стільниць,віртуальна стільниця,сенсорна панель", + "X-KDE-Keywords[zh_CN]": "kwin,window,manager,effect,corner,edge,border,action,switch,desktop,kwin screen edges,desktop edges,screen edges,maximize windows,tile windows,side of screen,screen behavior,switch desktop,virtual desktop,screen corners,chuangkou,guanli,texiao,sijiao,bianyuan,caozuo,qiehuan,zhuomian,kwin pingmubianyuan,zhuomianbianyuan,zuidahuachuangkou,pingpuchuangkou,pingmuxingwei,zhuomianqiehuan,xunizhuomian,xunizhuomianqiehuan,pingmujiaoluo,chumo,chumoping,chukong,chukongping,pingmu,窗口,管理,特效,四角,边缘,操作,切换,桌面,kwin 屏幕边缘,桌面边缘,最大化窗口,平铺窗口,屏幕行为,桌面切换,虚拟桌面,虚拟桌面切换,屏幕角落,触摸,触摸屏,触控,触控屏,屏幕", + "X-KDE-Keywords[zh_TW]": "kwin,視窗,視窗管理員,效果,邊緣,角落,邊框,動作,切換,桌面,桌面邊緣,觸控螢幕,螢幕行為", + "X-KDE-OnlyShowOnQtPlatforms": [ + "wayland" + ], + "X-KDE-System-Settings-Parent-Category": "hardware-input-touchscreen", + "X-KDE-System-Settings-Uses-ModuleData": true, + "X-KDE-Weight": 20 +} diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedge.cpp b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedge.cpp new file mode 100644 index 0000000000..719dcc0c9f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedge.cpp @@ -0,0 +1,224 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwinscreenedge.h" + +#include "monitor.h" + +namespace KWin +{ + +KWinScreenEdge::KWinScreenEdge(QWidget *parent) + : QWidget(parent) +{ + QMetaObject::invokeMethod(this, &KWinScreenEdge::createConnection, Qt::QueuedConnection); +} + +KWinScreenEdge::~KWinScreenEdge() +{ +} + +void KWinScreenEdge::monitorHideEdge(ElectricBorder border, bool hidden) +{ + const int edge = KWinScreenEdge::electricBorderToMonitorEdge(border); + monitor()->setEdgeHidden(edge, hidden); +} + +void KWinScreenEdge::monitorEnableEdge(ElectricBorder border, bool enabled) +{ + const int edge = KWinScreenEdge::electricBorderToMonitorEdge(border); + monitor()->setEdgeEnabled(edge, enabled); +} + +void KWinScreenEdge::monitorAddItem(const QString &item) +{ + for (int i = 0; i < 8; i++) { + monitor()->addEdgeItem(i, item); + } +} + +void KWinScreenEdge::monitorItemSetEnabled(int index, bool enabled) +{ + for (int i = 0; i < 8; i++) { + monitor()->setEdgeItemEnabled(i, index, enabled); + } +} + +void KWinScreenEdge::monitorChangeEdge(const QList &borderList, int index) +{ + for (int border : borderList) { + monitorChangeEdge(static_cast(border), index); + } +} + +void KWinScreenEdge::monitorChangeEdge(ElectricBorder border, int index) +{ + if (ELECTRIC_COUNT == border || ElectricNone == border) { + return; + } + m_reference[border] = index; + monitor()->selectEdgeItem(KWinScreenEdge::electricBorderToMonitorEdge(border), index); +} + +QList KWinScreenEdge::monitorCheckEffectHasEdge(int index) const +{ + QList list; + if (monitor()->selectedEdgeItem(Monitor::Top) == index) { + list.append(ElectricTop); + } + if (monitor()->selectedEdgeItem(Monitor::TopRight) == index) { + list.append(ElectricTopRight); + } + if (monitor()->selectedEdgeItem(Monitor::Right) == index) { + list.append(ElectricRight); + } + if (monitor()->selectedEdgeItem(Monitor::BottomRight) == index) { + list.append(ElectricBottomRight); + } + if (monitor()->selectedEdgeItem(Monitor::Bottom) == index) { + list.append(ElectricBottom); + } + if (monitor()->selectedEdgeItem(Monitor::BottomLeft) == index) { + list.append(ElectricBottomLeft); + } + if (monitor()->selectedEdgeItem(Monitor::Left) == index) { + list.append(ElectricLeft); + } + if (monitor()->selectedEdgeItem(Monitor::TopLeft) == index) { + list.append(ElectricTopLeft); + } + + if (list.isEmpty()) { + list.append(ElectricNone); + } + return list; +} + +int KWinScreenEdge::selectedEdgeItem(ElectricBorder border) const +{ + return monitor()->selectedEdgeItem(KWinScreenEdge::electricBorderToMonitorEdge(border)); +} + +void KWinScreenEdge::monitorChangeDefaultEdge(ElectricBorder border, int index) +{ + if (ELECTRIC_COUNT == border || ElectricNone == border) { + return; + } + m_default[border] = index; +} + +void KWinScreenEdge::monitorChangeDefaultEdge(const QList &borderList, int index) +{ + for (int border : borderList) { + monitorChangeDefaultEdge(static_cast(border), index); + } +} + +void KWinScreenEdge::reload() +{ + for (auto it = m_reference.cbegin(); it != m_reference.cend(); ++it) { + monitor()->selectEdgeItem(KWinScreenEdge::electricBorderToMonitorEdge(it.key()), it.value()); + } + onChanged(); +} + +void KWinScreenEdge::setDefaults() +{ + for (auto it = m_default.cbegin(); it != m_default.cend(); ++it) { + monitor()->selectEdgeItem(KWinScreenEdge::electricBorderToMonitorEdge(it.key()), it.value()); + } + onChanged(); +} + +int KWinScreenEdge::electricBorderToMonitorEdge(ElectricBorder border) +{ + switch (border) { + case ElectricTop: + return Monitor::Top; + case ElectricTopRight: + return Monitor::TopRight; + case ElectricRight: + return Monitor::Right; + case ElectricBottomRight: + return Monitor::BottomRight; + case ElectricBottom: + return Monitor::Bottom; + case ElectricBottomLeft: + return Monitor::BottomLeft; + case ElectricLeft: + return Monitor::Left; + case ElectricTopLeft: + return Monitor::TopLeft; + default: // ELECTRIC_COUNT and ElectricNone + return Monitor::None; + } +} + +ElectricBorder KWinScreenEdge::monitorEdgeToElectricBorder(int edge) +{ + const Monitor::Edges monitorEdge = static_cast(edge); + switch (monitorEdge) { + case Monitor::Left: + return ElectricLeft; + case Monitor::Right: + return ElectricRight; + case Monitor::Top: + return ElectricTop; + case Monitor::Bottom: + return ElectricBottom; + case Monitor::TopLeft: + return ElectricTopLeft; + case Monitor::TopRight: + return ElectricTopRight; + case Monitor::BottomLeft: + return ElectricBottomLeft; + case Monitor::BottomRight: + return ElectricBottomRight; + default: + return ElectricNone; + } +} + +void KWinScreenEdge::onChanged() +{ + bool needSave = isSaveNeeded(); + for (auto it = m_reference.cbegin(); it != m_reference.cend(); ++it) { + needSave |= it.value() != monitor()->selectedEdgeItem(KWinScreenEdge::electricBorderToMonitorEdge(it.key())); + } + Q_EMIT saveNeededChanged(needSave); + + bool defaults = isDefault(); + for (auto it = m_default.cbegin(); it != m_default.cend(); ++it) { + defaults &= it.value() == monitor()->selectedEdgeItem(KWinScreenEdge::electricBorderToMonitorEdge(it.key())); + } + Q_EMIT defaultChanged(defaults); +} + +void KWinScreenEdge::createConnection() +{ + connect(monitor(), + &Monitor::changed, + this, + &KWinScreenEdge::onChanged); +} + +bool KWinScreenEdge::isSaveNeeded() const +{ + return false; +} + +bool KWinScreenEdge::isDefault() const +{ + return true; +} + +} // namespace + +#include "moc_kwinscreenedge.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedge.h b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedge.h new file mode 100644 index 0000000000..47da91d5aa --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedge.h @@ -0,0 +1,71 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include + +#include "effect/globals.h" + +namespace KWin +{ + +class Monitor; + +class KWinScreenEdge : public QWidget +{ + Q_OBJECT + +public: + explicit KWinScreenEdge(QWidget *parent = nullptr); + ~KWinScreenEdge() override; + + void monitorHideEdge(ElectricBorder border, bool hidden); + void monitorEnableEdge(ElectricBorder border, bool enabled); + + void monitorAddItem(const QString &item); + void monitorItemSetEnabled(int index, bool enabled); + + QList monitorCheckEffectHasEdge(int index) const; + int selectedEdgeItem(ElectricBorder border) const; + + void monitorChangeEdge(ElectricBorder border, int index); + void monitorChangeEdge(const QList &borderList, int index); + + void monitorChangeDefaultEdge(ElectricBorder border, int index); + void monitorChangeDefaultEdge(const QList &borderList, int index); + + // revert to reference settings and assess for saveNeeded and default changed + virtual void reload(); + // reset to default settings and assess for saveNeeded and default changed + virtual void setDefaults(); + +public Q_SLOTS: + void onChanged(); + void createConnection(); + +Q_SIGNALS: + void saveNeededChanged(bool isNeeded); + void defaultChanged(bool isDefault); + +private: + virtual Monitor *monitor() const = 0; + virtual bool isSaveNeeded() const; + virtual bool isDefault() const; + + // internal use, return Monitor::None if border equals ELECTRIC_COUNT or ElectricNone + static int electricBorderToMonitorEdge(ElectricBorder border); + static ElectricBorder monitorEdgeToElectricBorder(int edge); + +private: + QHash m_reference; // reference settings + QHash m_default; // default settings +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgeconfigform.cpp b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgeconfigform.cpp new file mode 100644 index 0000000000..e531959a0b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgeconfigform.cpp @@ -0,0 +1,135 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwinscreenedgeconfigform.h" +#include "ui_main.h" + +namespace KWin +{ + +KWinScreenEdgesConfigForm::KWinScreenEdgesConfigForm(QWidget *parent) + : KWinScreenEdge(parent) + , ui(std::make_unique()) +{ + ui->setupUi(this); + + connect(ui->kcfg_ElectricBorderDelay, qOverload(&QSpinBox::valueChanged), this, &KWinScreenEdgesConfigForm::sanitizeCooldown); + + // Visual feedback of action group conflicts + connect(ui->kcfg_ElectricBorders, qOverload(&QComboBox::currentIndexChanged), this, &KWinScreenEdgesConfigForm::groupChanged); + connect(ui->kcfg_ElectricBorderMaximize, &QCheckBox::stateChanged, this, &KWinScreenEdgesConfigForm::groupChanged); + connect(ui->kcfg_ElectricBorderTiling, &QCheckBox::stateChanged, this, &KWinScreenEdgesConfigForm::groupChanged); + + connect(ui->remainActiveOnFullscreen, &QCheckBox::stateChanged, this, &KWinScreenEdgesConfigForm::onChanged); + connect(ui->electricBorderCornerRatioSpin, qOverload(&QSpinBox::valueChanged), this, &KWinScreenEdgesConfigForm::onChanged); + connect(ui->electricBorderCornerRatioSpin, qOverload(&QSpinBox::valueChanged), this, &KWinScreenEdgesConfigForm::updateDefaultIndicators); +} + +KWinScreenEdgesConfigForm::~KWinScreenEdgesConfigForm() = default; + +void KWinScreenEdgesConfigForm::setElectricBorderCornerRatio(double value) +{ + m_referenceCornerRatio = value; + ui->electricBorderCornerRatioSpin->setValue(m_referenceCornerRatio * 100.); +} + +void KWinScreenEdgesConfigForm::setDefaultElectricBorderCornerRatio(double value) +{ + m_defaultCornerRatio = value; + updateDefaultIndicators(); +} + +void KWinScreenEdgesConfigForm::setRemainActiveOnFullscreen(bool remainActive) +{ + m_remainActiveOnFullscreen = remainActive; + ui->remainActiveOnFullscreen->setChecked(remainActive); + updateDefaultIndicators(); +} + +double KWinScreenEdgesConfigForm::electricBorderCornerRatio() const +{ + return ui->electricBorderCornerRatioSpin->value() / 100.; +} + +void KWinScreenEdgesConfigForm::setElectricBorderCornerRatioEnabled(bool enable) +{ + ui->electricBorderCornerRatioSpin->setEnabled(enable); +} + +void KWinScreenEdgesConfigForm::reload() +{ + ui->electricBorderCornerRatioSpin->setValue(m_referenceCornerRatio * 100.); + KWinScreenEdge::reload(); +} + +void KWinScreenEdgesConfigForm::setDefaults() +{ + ui->electricBorderCornerRatioSpin->setValue(m_defaultCornerRatio * 100.); + ui->remainActiveOnFullscreen->setChecked(false); + KWinScreenEdge::setDefaults(); +} + +void KWinScreenEdgesConfigForm::setDefaultsIndicatorsVisible(bool visible) +{ + if (m_defaultIndicatorVisible != visible) { + m_defaultIndicatorVisible = visible; + updateDefaultIndicators(); + } +} + +bool KWinScreenEdgesConfigForm::remainActiveOnFullscreen() const +{ + return ui->remainActiveOnFullscreen->isChecked(); +} + +Monitor *KWinScreenEdgesConfigForm::monitor() const +{ + return ui->monitor; +} + +bool KWinScreenEdgesConfigForm::isSaveNeeded() const +{ + return m_referenceCornerRatio != electricBorderCornerRatio() || m_remainActiveOnFullscreen != remainActiveOnFullscreen(); +} + +bool KWinScreenEdgesConfigForm::isDefault() const +{ + return m_defaultCornerRatio == electricBorderCornerRatio() && m_remainActiveOnFullscreen == false; +} + +void KWinScreenEdgesConfigForm::sanitizeCooldown() +{ + ui->kcfg_ElectricBorderCooldown->setMinimum(ui->kcfg_ElectricBorderDelay->value() + 50); +} + +void KWinScreenEdgesConfigForm::groupChanged() +{ + // Monitor conflicts + bool hide = false; + if (ui->kcfg_ElectricBorders->currentIndex() == 2) { + hide = true; + } + monitorHideEdge(ElectricTop, hide); + monitorHideEdge(ElectricRight, hide); + monitorHideEdge(ElectricBottom, hide); + monitorHideEdge(ElectricLeft, hide); +} + +void KWinScreenEdgesConfigForm::updateDefaultIndicators() +{ + ui->electricBorderCornerRatioSpin->setProperty("_kde_highlight_neutral", m_defaultIndicatorVisible && (electricBorderCornerRatio() != m_defaultCornerRatio)); + ui->electricBorderCornerRatioSpin->update(); + ui->remainActiveOnFullscreen->setProperty("_kde_highlight_neutral", m_defaultIndicatorVisible && remainActiveOnFullscreen() == true); + ui->remainActiveOnFullscreen->update(); +} + +} // namespace + +#include "moc_kwinscreenedgeconfigform.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgeconfigform.h b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgeconfigform.h new file mode 100644 index 0000000000..6837c1652b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgeconfigform.h @@ -0,0 +1,71 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "kwinscreenedge.h" + +namespace Ui +{ +class KWinScreenEdgesConfigUI; +} + +namespace KWin +{ + +class KWinScreenEdgesConfigForm : public KWinScreenEdge +{ + Q_OBJECT + +public: + KWinScreenEdgesConfigForm(QWidget *parent = nullptr); + ~KWinScreenEdgesConfigForm() override; + + void setRemainActiveOnFullscreen(bool remainActive); + bool remainActiveOnFullscreen() const; + + // value is between 0. and 1. + void setElectricBorderCornerRatio(double value); + void setDefaultElectricBorderCornerRatio(double value); + + // return value between 0. and 1. + double electricBorderCornerRatio() const; + + void setElectricBorderCornerRatioEnabled(bool enable); + + void reload() override; + void setDefaults() override; + +public Q_SLOTS: + void setDefaultsIndicatorsVisible(bool visible); + +protected: + Monitor *monitor() const override; + bool isSaveNeeded() const override; + bool isDefault() const override; + +private Q_SLOTS: + void sanitizeCooldown(); + void groupChanged(); + void updateDefaultIndicators(); + +private: + bool m_remainActiveOnFullscreen = false; + + // electricBorderCornerRatio value between 0. and 1. + double m_referenceCornerRatio = 0.; + double m_defaultCornerRatio = 0.; + + bool m_defaultIndicatorVisible = false; + + std::unique_ptr ui; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgeeffectsettings.kcfg b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgeeffectsettings.kcfg new file mode 100644 index 0000000000..0383bd1962 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgeeffectsettings.kcfg @@ -0,0 +1,14 @@ + + + + + + + + ElectricNone + + + diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgeeffectsettings.kcfgc b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgeeffectsettings.kcfgc new file mode 100644 index 0000000000..daa2db3eea --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgeeffectsettings.kcfgc @@ -0,0 +1,7 @@ +File=kwinscreenedgeeffectsettings.kcfg +NameSpace=KWin +ClassName=KWinScreenEdgeEffectSettings +IncludeFiles=\"effect/globals.h\" +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgescriptsettings.kcfg b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgescriptsettings.kcfg new file mode 100644 index 0000000000..dd5f8ff41b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgescriptsettings.kcfg @@ -0,0 +1,14 @@ + + + + + + + + ElectricNone + + + diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgescriptsettings.kcfgc b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgescriptsettings.kcfgc new file mode 100644 index 0000000000..4b4079ad38 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgescriptsettings.kcfgc @@ -0,0 +1,7 @@ +File=kwinscreenedgescriptsettings.kcfg +NameSpace=KWin +ClassName=KWinScreenEdgeScriptSettings +IncludeFiles=\"effect/globals.h\" +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgesettings.kcfg b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgesettings.kcfg new file mode 100644 index 0000000000..099b0319c6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgesettings.kcfg @@ -0,0 +1,98 @@ + + + + + + 0 + + + 75 + + + 350 + + + true + + + true + + + 0.25 + + + true + + + + + None + + + None + + + None + + + None + + + None + + + None + + + None + + + None + + + + + ElectricNone + + + ElectricNone + + + ElectricNone + + + + + ElectricTopLeft + + + ElectricNone + + + + + ElectricNone + + + ElectricNone + + + + + false + + + + + true + + + 100 + 0 + 1000 + + + diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgesettings.kcfgc b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgesettings.kcfgc new file mode 100644 index 0000000000..d98eedefd7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwinscreenedgesettings.kcfgc @@ -0,0 +1,7 @@ +File=kwinscreenedgesettings.kcfg +NameSpace=KWin +ClassName=KWinScreenEdgeSettings +IncludeFiles=\"effect/globals.h\" +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenedgeconfigform.cpp b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenedgeconfigform.cpp new file mode 100644 index 0000000000..950b335b22 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenedgeconfigform.cpp @@ -0,0 +1,33 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwintouchscreenedgeconfigform.h" +#include "ui_touch.h" + +namespace KWin +{ + +KWinTouchScreenEdgeConfigForm::KWinTouchScreenEdgeConfigForm(QWidget *parent) + : KWinScreenEdge(parent) + , ui(std::make_unique()) +{ + ui->setupUi(this); +} + +KWinTouchScreenEdgeConfigForm::~KWinTouchScreenEdgeConfigForm() = default; + +Monitor *KWinTouchScreenEdgeConfigForm::monitor() const +{ + return ui->monitor; +} + +} // namespace + +#include "moc_kwintouchscreenedgeconfigform.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenedgeconfigform.h b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenedgeconfigform.h new file mode 100644 index 0000000000..cd902bb5ff --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenedgeconfigform.h @@ -0,0 +1,38 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "kwinscreenedge.h" + +namespace Ui +{ +class KWinTouchScreenConfigUi; +} + +namespace KWin +{ + +class KWinTouchScreenEdgeConfigForm : public KWinScreenEdge +{ + Q_OBJECT + +public: + KWinTouchScreenEdgeConfigForm(QWidget *parent = nullptr); + ~KWinTouchScreenEdgeConfigForm() override; + +protected: + Monitor *monitor() const override; + +private: + std::unique_ptr ui; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenedgeeffectsettings.kcfg b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenedgeeffectsettings.kcfg new file mode 100644 index 0000000000..9ee303e358 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenedgeeffectsettings.kcfg @@ -0,0 +1,14 @@ + + + + + + + + ElectricNone + + + diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenedgeeffectsettings.kcfgc b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenedgeeffectsettings.kcfgc new file mode 100644 index 0000000000..40149ffe53 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenedgeeffectsettings.kcfgc @@ -0,0 +1,7 @@ +File=kwintouchscreenedgeeffectsettings.kcfg +NameSpace=KWin +ClassName=KWinTouchScreenEdgeEffectSettings +IncludeFiles=\"effect/globals.h\" +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenscriptsettings.kcfg b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenscriptsettings.kcfg new file mode 100644 index 0000000000..e9e42a5ce4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenscriptsettings.kcfg @@ -0,0 +1,14 @@ + + + + + + + + ElectricNone + + + diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenscriptsettings.kcfgc b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenscriptsettings.kcfgc new file mode 100644 index 0000000000..fa07e70dbf --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreenscriptsettings.kcfgc @@ -0,0 +1,7 @@ +File=kwintouchscreenscriptsettings.kcfg +NameSpace=KWin +ClassName=KWinTouchScreenScriptSettings +IncludeFiles=\"effect/globals.h\" +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreensettings.kcfg b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreensettings.kcfg new file mode 100644 index 0000000000..4851e14ee3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreensettings.kcfg @@ -0,0 +1,48 @@ + + + + + + None + + + None + + + None + + + None + + + + + ElectricNone + + + ElectricNone + + + ElectricNone + + + + + ElectricNone + + + ElectricNone + + + + + ElectricNone + + + ElectricNone + + + diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreensettings.kcfgc b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreensettings.kcfgc new file mode 100644 index 0000000000..e60ed93727 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/kwintouchscreensettings.kcfgc @@ -0,0 +1,7 @@ +File=kwintouchscreensettings.kcfg +NameSpace=KWin +ClassName=KWinTouchScreenSettings +IncludeFiles=\"effect/globals.h\" +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/main.cpp b/local/recipes/kde/kwin/source/src/kcms/screenedges/main.cpp new file mode 100644 index 0000000000..cfa682631d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/main.cpp @@ -0,0 +1,406 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Martin Gräßlin + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "main.h" +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "kwinscreenedgeconfigform.h" +#include "kwinscreenedgedata.h" +#include "kwinscreenedgeeffectsettings.h" +#include "kwinscreenedgescriptsettings.h" +#include "kwinscreenedgesettings.h" + +K_PLUGIN_FACTORY_WITH_JSON(KWinScreenEdgesConfigFactory, "kcm_kwinscreenedges.json", registerPlugin(); registerPlugin();) + +namespace KWin +{ + +KWinScreenEdgesConfig::KWinScreenEdgesConfig(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) + , m_form(new KWinScreenEdgesConfigForm(widget())) + , m_config(KSharedConfig::openConfig("kwinrc")) + , m_data(new KWinScreenEdgeData(this)) +{ + QVBoxLayout *layout = new QVBoxLayout(widget()); + layout->addWidget(m_form); + + addConfig(m_data->settings(), m_form); + + monitorInit(); + + connect(this, &KWinScreenEdgesConfig::defaultsIndicatorsVisibleChanged, m_form, [this]() { + m_form->setDefaultsIndicatorsVisible(defaultsIndicatorsVisible()); + }); + connect(m_form, &KWinScreenEdgesConfigForm::saveNeededChanged, this, &KWinScreenEdgesConfig::unmanagedWidgetChangeState); + connect(m_form, &KWinScreenEdgesConfigForm::defaultChanged, this, &KWinScreenEdgesConfig::unmanagedWidgetDefaultState); +} + +KWinScreenEdgesConfig::~KWinScreenEdgesConfig() +{ +} + +void KWinScreenEdgesConfig::load() +{ + KCModule::load(); + m_data->settings()->load(); + for (KWinScreenEdgeScriptSettings *setting : std::as_const(m_scriptSettings)) { + setting->load(); + } + for (KWinScreenEdgeEffectSettings *setting : std::as_const(m_effectSettings)) { + setting->load(); + } + + monitorLoadSettings(); + monitorLoadDefaultSettings(); + m_form->setRemainActiveOnFullscreen(m_data->settings()->remainActiveOnFullscreen()); + m_form->setElectricBorderCornerRatio(m_data->settings()->electricBorderCornerRatio()); + m_form->setDefaultElectricBorderCornerRatio(m_data->settings()->defaultElectricBorderCornerRatioValue()); + m_form->reload(); +} + +void KWinScreenEdgesConfig::save() +{ + monitorSaveSettings(); + m_data->settings()->setRemainActiveOnFullscreen(m_form->remainActiveOnFullscreen()); + m_data->settings()->setElectricBorderCornerRatio(m_form->electricBorderCornerRatio()); + m_data->settings()->save(); + for (KWinScreenEdgeScriptSettings *setting : std::as_const(m_scriptSettings)) { + setting->save(); + } + for (KWinScreenEdgeEffectSettings *setting : std::as_const(m_effectSettings)) { + setting->save(); + } + + // Reload saved settings to ScreenEdge UI + monitorLoadSettings(); + m_form->setElectricBorderCornerRatio(m_data->settings()->electricBorderCornerRatio()); + m_form->setRemainActiveOnFullscreen(m_data->settings()->remainActiveOnFullscreen()); + m_form->reload(); + + // Reload KWin. + QDBusMessage message = QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); + // and reconfigure the effects + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + + interface.reconfigureEffect(QStringLiteral("overview")); + interface.reconfigureEffect(QStringLiteral("windowview")); + + for (const auto &effectId : std::as_const(m_effects)) { + interface.reconfigureEffect(effectId); + } + + KCModule::save(); +} + +void KWinScreenEdgesConfig::defaults() +{ + m_form->setDefaults(); + + KCModule::defaults(); +} + +//----------------------------------------------------------------------------- +// Monitor + +static QList listBuiltinEffects() +{ + const QString rootDirectory = QStandardPaths::locate(QStandardPaths::GenericDataLocation, + QStringLiteral("kwin-wayland/builtin-effects"), + QStandardPaths::LocateDirectory); + + QList ret; + + const QStringList nameFilters{QStringLiteral("*.json")}; + QDirIterator it(rootDirectory, nameFilters, QDir::Files); + while (it.hasNext()) { + it.next(); + if (const KPluginMetaData metaData = KPluginMetaData::fromJsonFile(it.filePath()); metaData.isValid()) { + ret.append(metaData); + } + } + + return ret; +} + +static QList listScriptedEffects() +{ + return KPackage::PackageLoader::self()->listPackages(QStringLiteral("KWin/Effect"), QStringLiteral("kwin-wayland/effects/")) + + KPackage::PackageLoader::self()->listPackages(QStringLiteral("KWin/Effect"), QStringLiteral("kwin/effects/")); +} + +void KWinScreenEdgesConfig::monitorInit() +{ + m_form->monitorAddItem(i18n("No Action")); + m_form->monitorAddItem(i18n("Peek at Desktop")); + m_form->monitorAddItem(i18n("Lock Screen")); + m_form->monitorAddItem(i18n("Show KRunner")); + m_form->monitorAddItem(i18n("Activity Manager")); + m_form->monitorAddItem(i18n("Application Launcher")); + + // TODO: Find a better way to get the display name of the present windows, + // Maybe install metadata.json files? + const QString presentWindowsName = i18n("Present Windows"); + m_form->monitorAddItem(i18n("%1 - All Desktops", presentWindowsName)); + m_form->monitorAddItem(i18n("%1 - Current Desktop", presentWindowsName)); + m_form->monitorAddItem(i18n("%1 - Current Application", presentWindowsName)); + + m_form->monitorAddItem(i18n("Overview")); + m_form->monitorAddItem(i18n("Grid")); + + m_form->monitorAddItem(i18n("Toggle window switching")); + m_form->monitorAddItem(i18n("Toggle alternative window switching")); + + KConfigGroup config(m_config, QStringLiteral("Plugins")); + const auto effects = listBuiltinEffects() << listScriptedEffects(); + + for (const KPluginMetaData &effect : effects) { + if (!effect.value(QStringLiteral("X-KWin-Border-Activate"), false)) { + continue; + } + + if (!config.readEntry(effect.pluginId() + QStringLiteral("Enabled"), effect.isEnabledByDefault())) { + continue; + } + m_effects << effect.pluginId(); + m_form->monitorAddItem(effect.name()); + m_effectSettings[effect.pluginId()] = new KWinScreenEdgeEffectSettings(effect.pluginId(), this); + } + + const auto scripts = KPackage::PackageLoader::self()->listPackages(QStringLiteral("KWin/Script"), QStringLiteral("kwin-wayland/scripts/")) + + KPackage::PackageLoader::self()->listPackages(QStringLiteral("KWin/Script"), QStringLiteral("kwin/scripts/")); + + for (const KPluginMetaData &script : scripts) { + if (script.value(QStringLiteral("X-KWin-Border-Activate"), false) != true) { + continue; + } + + if (!config.readEntry(script.pluginId() + QStringLiteral("Enabled"), script.isEnabledByDefault())) { + continue; + } + m_scripts << script.pluginId(); + m_form->monitorAddItem(script.name()); + m_scriptSettings[script.pluginId()] = new KWinScreenEdgeScriptSettings(script.pluginId(), this); + } + + monitorShowEvent(); +} + +void KWinScreenEdgesConfig::monitorLoadSettings() +{ + // Load ElectricBorderActions + m_form->monitorChangeEdge(ElectricTop, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->top())); + m_form->monitorChangeEdge(ElectricTopRight, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->topRight())); + m_form->monitorChangeEdge(ElectricRight, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->right())); + m_form->monitorChangeEdge(ElectricBottomRight, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->bottomRight())); + m_form->monitorChangeEdge(ElectricBottom, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->bottom())); + m_form->monitorChangeEdge(ElectricBottomLeft, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->bottomLeft())); + m_form->monitorChangeEdge(ElectricLeft, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->left())); + m_form->monitorChangeEdge(ElectricTopLeft, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->topLeft())); + + // Load effect-specific actions: + + // PresentWindows BorderActivateAll + m_form->monitorChangeEdge(m_data->settings()->borderActivateAll(), PresentWindowsAll); + + // PresentWindows BorderActivate + m_form->monitorChangeEdge(m_data->settings()->borderActivatePresentWindows(), PresentWindowsCurrent); + + // PresentWindows BorderActivateClass + m_form->monitorChangeEdge(m_data->settings()->borderActivateClass(), PresentWindowsClass); + + // Overview + m_form->monitorChangeEdge(m_data->settings()->borderActivateOverview(), Overview); + m_form->monitorChangeEdge(m_data->settings()->borderActivateGrid(), Grid); + + // TabBox + m_form->monitorChangeEdge(m_data->settings()->borderActivateTabBox(), TabBox); + // Alternative TabBox + m_form->monitorChangeEdge(m_data->settings()->borderAlternativeActivate(), TabBoxAlternative); + + // Dynamically loaded effects + int lastIndex = EffectCount; + for (int i = 0; i < m_effects.size(); i++) { + m_form->monitorChangeEdge(m_effectSettings[m_effects[i]]->borderActivate(), lastIndex); + ++lastIndex; + } + + // Scripts + for (int i = 0; i < m_scripts.size(); i++) { + m_form->monitorChangeEdge(m_scriptSettings[m_scripts[i]]->borderActivate(), lastIndex); + ++lastIndex; + } +} + +void KWinScreenEdgesConfig::monitorLoadDefaultSettings() +{ + // Load ElectricBorderActions + m_form->monitorChangeDefaultEdge(ElectricTop, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->defaultTopValue())); + m_form->monitorChangeDefaultEdge(ElectricTopRight, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->defaultTopRightValue())); + m_form->monitorChangeDefaultEdge(ElectricRight, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->defaultRightValue())); + m_form->monitorChangeDefaultEdge(ElectricBottomRight, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->defaultBottomRightValue())); + m_form->monitorChangeDefaultEdge(ElectricBottom, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->defaultBottomValue())); + m_form->monitorChangeDefaultEdge(ElectricBottomLeft, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->defaultBottomLeftValue())); + m_form->monitorChangeDefaultEdge(ElectricLeft, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->defaultLeftValue())); + m_form->monitorChangeDefaultEdge(ElectricTopLeft, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->defaultTopLeftValue())); + + // Load effect-specific actions: + + // PresentWindows BorderActivateAll + m_form->monitorChangeDefaultEdge(m_data->settings()->defaultBorderActivateAllValue(), PresentWindowsAll); + + // PresentWindows BorderActivate + m_form->monitorChangeDefaultEdge(m_data->settings()->defaultBorderActivatePresentWindowsValue(), PresentWindowsCurrent); + + // PresentWindows BorderActivateClass + m_form->monitorChangeDefaultEdge(m_data->settings()->defaultBorderActivateClassValue(), PresentWindowsClass); + + // Overview + m_form->monitorChangeDefaultEdge(m_data->settings()->defaultBorderActivateOverviewValue(), Overview); + m_form->monitorChangeDefaultEdge(m_data->settings()->defaultBorderActivateGridValue(), Grid); + + // TabBox + m_form->monitorChangeDefaultEdge(m_data->settings()->defaultBorderActivateTabBoxValue(), TabBox); + // Alternative TabBox + m_form->monitorChangeDefaultEdge(m_data->settings()->defaultBorderAlternativeActivateValue(), TabBoxAlternative); +} + +void KWinScreenEdgesConfig::monitorSaveSettings() +{ + // Save ElectricBorderActions + m_data->settings()->setTop(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricTop))); + m_data->settings()->setTopRight(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricTopRight))); + m_data->settings()->setRight(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricRight))); + m_data->settings()->setBottomRight(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricBottomRight))); + m_data->settings()->setBottom(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricBottom))); + m_data->settings()->setBottomLeft(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricBottomLeft))); + m_data->settings()->setLeft(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricLeft))); + m_data->settings()->setTopLeft(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricTopLeft))); + + // Save effect-specific actions: + + // Present Windows + m_data->settings()->setBorderActivateAll(m_form->monitorCheckEffectHasEdge(PresentWindowsAll)); + m_data->settings()->setBorderActivatePresentWindows(m_form->monitorCheckEffectHasEdge(PresentWindowsCurrent)); + m_data->settings()->setBorderActivateClass(m_form->monitorCheckEffectHasEdge(PresentWindowsClass)); + + // Overview + m_data->settings()->setBorderActivateOverview(m_form->monitorCheckEffectHasEdge(Overview)); + m_data->settings()->setBorderActivateGrid(m_form->monitorCheckEffectHasEdge(Grid)); + + // TabBox + m_data->settings()->setBorderActivateTabBox(m_form->monitorCheckEffectHasEdge(TabBox)); + m_data->settings()->setBorderAlternativeActivate(m_form->monitorCheckEffectHasEdge(TabBoxAlternative)); + + // Dynamically loaded effects + int lastIndex = EffectCount; + for (int i = 0; i < m_effects.size(); i++) { + m_effectSettings[m_effects[i]]->setBorderActivate(m_form->monitorCheckEffectHasEdge(lastIndex)); + ++lastIndex; + } + + // Scripts + for (int i = 0; i < m_scripts.size(); i++) { + m_scriptSettings[m_scripts[i]]->setBorderActivate(m_form->monitorCheckEffectHasEdge(lastIndex)); + ++lastIndex; + } +} + +void KWinScreenEdgesConfig::monitorShowEvent() +{ + // Check if they are enabled + KConfigGroup config(m_config, QStringLiteral("Plugins")); + + // Present Windows + bool enabled = config.readEntry("windowviewEnabled", true); + m_form->monitorItemSetEnabled(PresentWindowsCurrent, enabled); + m_form->monitorItemSetEnabled(PresentWindowsAll, enabled); + + // Overview + const bool overviewEnabled = config.readEntry("overviewEnabled", true); + m_form->monitorItemSetEnabled(Overview, overviewEnabled); + m_form->monitorItemSetEnabled(Grid, overviewEnabled); + + // tabbox, depends on reasonable focus policy. + KConfigGroup config2(m_config, QStringLiteral("Windows")); + QString focusPolicy = config2.readEntry("FocusPolicy", QString()); + bool reasonable = focusPolicy != "FocusStrictlyUnderMouse" && focusPolicy != "FocusUnderMouse"; + m_form->monitorItemSetEnabled(TabBox, reasonable); + m_form->monitorItemSetEnabled(TabBoxAlternative, reasonable); + + // Disable Edge if ElectricBorders group entries are immutable + m_form->monitorEnableEdge(ElectricTop, !m_data->settings()->isTopImmutable()); + m_form->monitorEnableEdge(ElectricTopRight, !m_data->settings()->isTopRightImmutable()); + m_form->monitorEnableEdge(ElectricRight, !m_data->settings()->isRightImmutable()); + m_form->monitorEnableEdge(ElectricBottomRight, !m_data->settings()->isBottomRightImmutable()); + m_form->monitorEnableEdge(ElectricBottom, !m_data->settings()->isBottomImmutable()); + m_form->monitorEnableEdge(ElectricBottomLeft, !m_data->settings()->isBottomLeftImmutable()); + m_form->monitorEnableEdge(ElectricLeft, !m_data->settings()->isLeftImmutable()); + m_form->monitorEnableEdge(ElectricTopLeft, !m_data->settings()->isTopLeftImmutable()); + + // Disable ElectricBorderCornerRatio if entry is immutable + m_form->setElectricBorderCornerRatioEnabled(!m_data->settings()->isElectricBorderCornerRatioImmutable()); +} + +ElectricBorderAction KWinScreenEdgesConfig::electricBorderActionFromString(const QString &string) +{ + QString lowerName = string.toLower(); + if (lowerName == QLatin1StringView("showdesktop")) { + return ElectricActionShowDesktop; + } + if (lowerName == QLatin1StringView("lockscreen")) { + return ElectricActionLockScreen; + } + if (lowerName == QLatin1StringView("krunner")) { + return ElectricActionKRunner; + } + if (lowerName == QLatin1StringView("activitymanager")) { + return ElectricActionActivityManager; + } + if (lowerName == QLatin1StringView("applicationlauncher")) { + return ElectricActionApplicationLauncher; + } + return ElectricActionNone; +} + +QString KWinScreenEdgesConfig::electricBorderActionToString(int action) +{ + switch (action) { + case 1: + return QStringLiteral("ShowDesktop"); + case 2: + return QStringLiteral("LockScreen"); + case 3: + return QStringLiteral("KRunner"); + case 4: + return QStringLiteral("ActivityManager"); + case 5: + return QStringLiteral("ApplicationLauncher"); + default: + return QStringLiteral("None"); + } +} + +} // namespace + +#include "main.moc" + +#include "moc_main.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/main.h b/local/recipes/kde/kwin/source/src/kcms/screenedges/main.h new file mode 100644 index 0000000000..c6c854ca5e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/main.h @@ -0,0 +1,68 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include "effect/globals.h" + +namespace KWin +{ +class KWinScreenEdgeData; +class KWinScreenEdgesConfigForm; +class KWinScreenEdgeScriptSettings; +class KWinScreenEdgeEffectSettings; + +class KWinScreenEdgesConfig : public KCModule +{ + Q_OBJECT + +public: + explicit KWinScreenEdgesConfig(QObject *parent, const KPluginMetaData &data); + ~KWinScreenEdgesConfig() override; + +public Q_SLOTS: + void save() override; + void load() override; + void defaults() override; + +private: + KWinScreenEdgesConfigForm *m_form; + KSharedConfigPtr m_config; + QStringList m_effects; // list of effect IDs ordered in the list they are presented in the menu + QStringList m_scripts; // list of script IDs ordered in the list they are presented in the menu + QHash m_scriptSettings; + QHash m_effectSettings; + KWinScreenEdgeData *m_data; + + enum EffectActions { + PresentWindowsAll = ELECTRIC_ACTION_COUNT, // Start at the end of built in actions + PresentWindowsCurrent, + PresentWindowsClass, + Overview, + Grid, + TabBox, + TabBoxAlternative, + EffectCount + }; + + void monitorInit(); + void monitorLoadSettings(); + void monitorLoadDefaultSettings(); + void monitorSaveSettings(); + void monitorShowEvent(); + + static ElectricBorderAction electricBorderActionFromString(const QString &string); + static QString electricBorderActionToString(int action); +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/main.ui b/local/recipes/kde/kwin/source/src/kcms/screenedges/main.ui new file mode 100644 index 0000000000..67bf4ca75a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/main.ui @@ -0,0 +1,401 @@ + + + KWinScreenEdgesConfigUI + + + + 0 + 0 + 500 + 525 + + + + + 500 + 525 + + + + + + + You can trigger an action by pushing the mouse cursor against the corresponding screen edge or corner. + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 20 + + + + + + + + + 200 + 200 + + + + Qt::StrongFocus + + + + + + + Qt::AlignHCenter|Qt::AlignTop + + + + + &Maximize: + + + kcfg_ElectricBorderMaximize + + + + + + + Windows dragged to top edge + + + + + + + &Tile: + + + kcfg_ElectricBorderTiling + + + + + + + Windows dragged to left or right edge + + + + + + + Behavior: + + + + + + + Remain active when windows are fullscreen + + + + + + + Trigger &quarter tiling in: + + + electricBorderCornerRatioSpin + + + + + + + + + false + + + % + + + Outer + + + 1 + + + 49 + + + + + + + false + + + of the screen + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 4 + + + + + + + + Change desktop when the mouse cursor is pushed against the edge of the screen + + + &Switch desktop on edge: + + + kcfg_ElectricBorders + + + + + + + + Disabled + + + + + Only when moving windows + + + + + Always enabled + + + + + + + + Amount of time required for the mouse cursor to be pushed against the edge of the screen before the action is triggered + + + Activation &delay: + + + kcfg_ElectricBorderDelay + + + + + + + ms + + + 1000 + + + 50 + + + 0 + + + + + + + true + + + Amount of time required after triggering an action until the next trigger can occur + + + &Reactivation delay: + + + kcfg_ElectricBorderCooldown + + + + + + + true + + + ms + + + 1000 + + + 50 + + + 0 + + + + + + + &Corner barrier: + + + kcfg_CornerBarrier + + + + + + + Here you can enable or disable the virtual corner barrier between screens. The barrier prevents the cursor from moving to another screen when it is already touching a screen corner. This makes it easier to trigger user interface elements like maximized windows' close buttons when using multiple screens. + + + Prevents cursors from crossing at screen corners. + + + + + + + &Edge barrier: + + + kcfg_EdgeBarrier + + + + + + + Here you can set size of the edge barrier between different screens. The barrier adds additional distance you have to move your pointer before it crosses the edge onto the other screen. This makes it easier to access user interface elements like Plasma Panels that are located on an edge between screens. + + + Additional distance cursor needs to travel to cross screen edges. + + + None + + + px + + + 0 + + + 1000 + + + 100 + + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 0 + 0 + + + + + + + + + KWin::Monitor + QWidget +
monitor.h
+ 1 +
+
+ + + + kcfg_ElectricBorderTiling + toggled(bool) + label_1 + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + + kcfg_ElectricBorderTiling + toggled(bool) + electricBorderCornerRatioSpin + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + + kcfg_ElectricBorderTiling + toggled(bool) + electricBorderCornerRatioLabel + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + +
diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/monitor.cpp b/local/recipes/kde/kwin/source/src/kcms/screenedges/monitor.cpp new file mode 100644 index 0000000000..1daef2088c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/monitor.cpp @@ -0,0 +1,288 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "monitor.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +static QScreen *screenFromWidget(const QWidget *widget) +{ + QScreen *screen = widget->screen(); + if (screen) { + return screen; + } + + return QGuiApplication::primaryScreen(); +} + +Monitor::Monitor(QWidget *parent) + : ScreenPreviewWidget(parent) +{ + for (auto &popup : m_popups) { + popup = std::make_unique(this); + } + m_scene = std::make_unique(this); + m_view = std::make_unique(m_scene.get(), this); + m_view->setBackgroundBrush(Qt::black); + m_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_view->setFocusPolicy(Qt::NoFocus); + m_view->setFrameShape(QFrame::NoFrame); + for (size_t i = 0; i < m_items.size(); i++) { + m_items[i] = std::make_unique(this); + m_scene->addItem(m_items[i].get()); + m_hidden[i] = false; + m_actionGroups[i] = std::make_unique(this); + } + QRect avail = screenFromWidget(this)->geometry(); + setMinimumContentWidth(20 * 3 + 5 * 2); // 3 buttons in a row and some spacing between them + setRatio((qreal)avail.width() / (qreal)avail.height()); + checkSize(); +} + +Monitor::~Monitor() = default; + +void Monitor::clear() +{ + for (size_t i = 0; i < m_popups.size(); i++) { + m_popups[i]->clear(); + m_items[i]->setActive(false); + setEdgeHidden(i, false); + m_actionGroups[i] = std::make_unique(this); + } +} + +void Monitor::resizeEvent(QResizeEvent *e) +{ + ScreenPreviewWidget::resizeEvent(e); + checkSize(); +} + +bool Monitor::event(QEvent *event) +{ + const bool r = ScreenPreviewWidget::event(event); + if (event->type() == QEvent::ScreenChangeInternal) { + QRect avail = screenFromWidget(this)->geometry(); + setRatio((qreal)avail.width() / (qreal)avail.height()); + checkSize(); + } + return r; +} + +void Monitor::checkSize() +{ + QRect contentsRect = previewRect(); + m_view->setGeometry(contentsRect); + m_scene->setSceneRect(QRect(QPoint(0, 0), contentsRect.size())); + const int x2 = (contentsRect.width() - 20) / 2; + const int x3 = contentsRect.width() - 20; + const int y2 = (contentsRect.height() - 20) / 2; + const int y3 = contentsRect.height() - 20; + m_items[0]->setRect(0, y2, 20, 20); + m_items[1]->setRect(x3, y2, 20, 20); + m_items[2]->setRect(x2, 0, 20, 20); + m_items[3]->setRect(x2, y3, 20, 20); + m_items[4]->setRect(0, 0, 20, 20); + m_items[5]->setRect(x3, 0, 20, 20); + m_items[6]->setRect(0, y3, 20, 20); + m_items[7]->setRect(x3, y3, 20, 20); +} + +void Monitor::setEdgeEnabled(int edge, bool enabled) +{ + for (QAction *action : std::as_const(m_popupActions[edge])) { + action->setEnabled(enabled); + } +} + +void Monitor::setEdgeHidden(int edge, bool set) +{ + m_hidden[edge] = set; + if (set) { + m_items[edge]->hide(); + } else { + m_items[edge]->show(); + } +} + +bool Monitor::edgeHidden(int edge) const +{ + return m_hidden[edge]; +} + +void Monitor::addEdgeItem(int edge, const QString &item) +{ + QAction *act = m_popups[edge]->addAction(item); + act->setCheckable(true); + m_popupActions[edge].append(act); + m_actionGroups[edge]->addAction(act); + if (m_popupActions[edge].count() == 1) { + act->setChecked(true); + m_items[edge]->setToolTip(item); + } + m_items[edge]->setActive(!m_popupActions[edge].front()->isChecked()); +} + +void Monitor::setEdgeItemEnabled(int edge, int index, bool enabled) +{ + m_popupActions[edge][index]->setEnabled(enabled); +} + +bool Monitor::edgeItemEnabled(int edge, int index) const +{ + return m_popupActions[edge][index]->isEnabled(); +} + +void Monitor::selectEdgeItem(int edge, int index) +{ + m_popupActions[edge][index]->setChecked(true); + m_items[edge]->setActive(!m_popupActions[edge].front()->isChecked()); + QString actionText = m_popupActions[edge][index]->text(); + // remove accelerators added by KAcceleratorManager + actionText = KLocalizedString::removeAcceleratorMarker(actionText); + m_items[edge]->setToolTip(actionText); +} + +int Monitor::selectedEdgeItem(int edge) const +{ + const auto &actions = m_popupActions[edge]; + for (QAction *act : actions) { + if (act->isChecked()) { + return actions.indexOf(act); + } + } + Q_UNREACHABLE(); +} + +void Monitor::popup(Corner *c, QPoint pos) +{ + for (size_t i = 0; i < m_items.size(); i++) { + if (m_items[i].get() == c) { + if (m_popupActions[i].empty()) { + return; + } + if (QAction *a = m_popups[i]->exec(pos)) { + selectEdgeItem(i, m_popupActions[i].indexOf(a)); + Q_EMIT changed(); + Q_EMIT edgeSelectionChanged(i, m_popupActions[i].indexOf(a)); + c->setToolTip(KLocalizedString::removeAcceleratorMarker(a->text())); + } + return; + } + } + Q_UNREACHABLE(); +} + +void Monitor::flip(Corner *c, QPoint pos) +{ + for (size_t i = 0; i < m_items.size(); i++) { + if (m_items[i].get() == c) { + if (m_popupActions[i].empty()) { + m_items[i]->setActive(m_items[i]->brush() != Qt::green); + } else { + popup(c, pos); + } + return; + } + } + Q_UNREACHABLE(); +} + +Monitor::Corner::Corner(Monitor *m) + : m_monitor(m) + , m_button(std::make_unique()) +{ + m_button->setImageSet(m->svgImageSet()); + m_button->setImagePath("widgets/button"); + setAcceptHoverEvents(true); +} + +Monitor::Corner::~Corner() = default; + +void Monitor::Corner::contextMenuEvent(QGraphicsSceneContextMenuEvent *e) +{ + m_monitor->popup(this, e->screenPos()); +} + +void Monitor::Corner::mousePressEvent(QGraphicsSceneMouseEvent *e) +{ + m_monitor->flip(this, e->screenPos()); +} + +void Monitor::Corner::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + if (m_hover) { + m_button->setElementPrefix("normal"); + + qreal left, top, right, bottom; + m_button->getMargins(left, top, right, bottom); + + m_button->setElementPrefix("active"); + qreal activeLeft, activeTop, activeRight, activeBottom; + m_button->getMargins(activeLeft, activeTop, activeRight, activeBottom); + + QRectF activeRect = QRectF(QPointF(0, 0), rect().size()); + activeRect.adjust(left - activeLeft, top - activeTop, + -(right - activeRight), -(bottom - activeBottom)); + m_button->setElementPrefix("active"); + m_button->resizeFrame(activeRect.size()); + m_button->paintFrame(painter, rect().topLeft() + activeRect.topLeft()); + } else { + m_button->setElementPrefix(m_active ? "pressed" : "normal"); + m_button->resizeFrame(rect().size()); + m_button->paintFrame(painter, rect().topLeft()); + } + + if (m_active) { + QPainterPath roundedRect; + painter->setRenderHint(QPainter::Antialiasing); + roundedRect.addRoundedRect(rect().adjusted(5, 5, -5, -5), 2, 2); + painter->fillPath(roundedRect, QApplication::palette().text()); + } +} + +void Monitor::Corner::hoverEnterEvent(QGraphicsSceneHoverEvent *e) +{ + m_hover = true; + update(); +} + +void Monitor::Corner::hoverLeaveEvent(QGraphicsSceneHoverEvent *e) +{ + m_hover = false; + update(); +} + +void Monitor::Corner::setActive(bool active) +{ + m_active = active; + update(); +} + +bool Monitor::Corner::active() const +{ + return m_active; +} +} // namespace + +#include "moc_monitor.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/monitor.h b/local/recipes/kde/kwin/source/src/kcms/screenedges/monitor.h new file mode 100644 index 0000000000..c167de5684 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/monitor.h @@ -0,0 +1,106 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "screenpreviewwidget.h" + +#include +#include +#include +#include +#include + +class QAction; +class QGraphicsView; +class QGraphicsScene; +class QMenu; + +namespace KSvg +{ +class FrameSvg; +} + +namespace KWin +{ + +class Monitor : public ScreenPreviewWidget +{ + Q_OBJECT +public: + explicit Monitor(QWidget *parent); + ~Monitor(); + + void setEdgeEnabled(int edge, bool enabled); + void setEdgeHidden(int edge, bool set); + bool edgeHidden(int edge) const; + void clear(); + void addEdgeItem(int edge, const QString &item); + void setEdgeItemEnabled(int edge, int index, bool enabled); + bool edgeItemEnabled(int edge, int index) const; + void selectEdgeItem(int edge, int index); + int selectedEdgeItem(int edge) const; + + enum Edges { + Left, + Right, + Top, + Bottom, + TopLeft, + TopRight, + BottomLeft, + BottomRight, + None + }; +Q_SIGNALS: + void changed(); + void edgeSelectionChanged(int edge, int index); + +protected: + void resizeEvent(QResizeEvent *e) override; + bool event(QEvent *event) override; + +private: + class Corner; + void popup(Corner *c, QPoint pos); + void flip(Corner *c, QPoint pos); + void checkSize(); + std::unique_ptr m_scene; + std::unique_ptr m_view; + std::array, 8> m_items; + std::array m_hidden; + std::array, 8> m_popups; + std::array, 8> m_popupActions; + std::array, 8> m_actionGroups; +}; + +class Monitor::Corner : public QGraphicsRectItem +{ +public: + Corner(Monitor *m); + ~Corner() override; + void setActive(bool active); + bool active() const; + +protected: + void contextMenuEvent(QGraphicsSceneContextMenuEvent *e) override; + void mousePressEvent(QGraphicsSceneMouseEvent *e) override; + void hoverEnterEvent(QGraphicsSceneHoverEvent *e) override; + void hoverLeaveEvent(QGraphicsSceneHoverEvent *e) override; + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; + +private: + Monitor *const m_monitor; + const std::unique_ptr m_button; + bool m_active = false; + bool m_hover = false; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/screenpreviewwidget.cpp b/local/recipes/kde/kwin/source/src/kcms/screenedges/screenpreviewwidget.cpp new file mode 100644 index 0000000000..2da238184e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/screenpreviewwidget.cpp @@ -0,0 +1,161 @@ +/* + SPDX-FileCopyrightText: 2009 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "screenpreviewwidget.h" + +#include +#include +#include + +#include +#include + +#include +#include + +#include + +class ScreenPreviewWidgetPrivate +{ +public: + ScreenPreviewWidgetPrivate(ScreenPreviewWidget *screen) + : q(screen) + , ratio(1) + , minimumContentWidth(0) + { + } + + ~ScreenPreviewWidgetPrivate() + { + } + + void updateRect(const QRectF &rect) + { + q->update(rect.toRect()); + } + + void updateScreenGraphics() + { + int bottomElements = screenGraphics->elementSize("base").height() + screenGraphics->marginSize(KSvg::FrameSvg::BottomMargin); + QRect bounds(QPoint(0, 0), QSize(q->width(), q->height() - bottomElements)); + + QSizeF monitorSize(1.0, 1.0 / ratio); + monitorSize.scale(bounds.size(), Qt::KeepAspectRatio); + + if (monitorSize.isEmpty()) { + return; + } + + const auto minFrameWidth = minimumContentWidth + screenGraphics->marginSize(KSvg::FrameSvg::LeftMargin) + screenGraphics->marginSize(KSvg::FrameSvg::RightMargin); + if (monitorSize.width() < minFrameWidth) { + monitorSize.setWidth(minFrameWidth); + } + + monitorRect = QRect(QPoint(0, 0), monitorSize.toSize()); + monitorRect.moveCenter(bounds.center()); + + screenGraphics->resizeFrame(monitorRect.size()); + + previewRect = screenGraphics->contentsRect().toRect(); + previewRect.moveCenter(bounds.center()); + } + + ScreenPreviewWidget *q; + std::unique_ptr svgImageSet; + KSvg::FrameSvg *screenGraphics; + QPixmap preview; + QRect monitorRect; + qreal ratio; + qreal minimumContentWidth; + QRect previewRect; +}; + +ScreenPreviewWidget::ScreenPreviewWidget(QWidget *parent) + : QWidget(parent) + , d(std::make_unique(this)) +{ + d->svgImageSet = std::make_unique(); + d->svgImageSet->setBasePath("plasma/desktoptheme"); + d->screenGraphics = new KSvg::FrameSvg(this); + d->screenGraphics->setImageSet(d->svgImageSet.get()); + d->screenGraphics->setImagePath("widgets/monitor"); + d->updateScreenGraphics(); +} + +ScreenPreviewWidget::~ScreenPreviewWidget() = default; + +void ScreenPreviewWidget::setPreview(const QPixmap &preview) +{ + d->preview = preview; + + update(); +} + +const QPixmap ScreenPreviewWidget::preview() const +{ + return d->preview; +} + +void ScreenPreviewWidget::setRatio(const qreal ratio) +{ + d->ratio = ratio; + d->updateScreenGraphics(); +} + +qreal ScreenPreviewWidget::ratio() const +{ + return d->ratio; +} + +void ScreenPreviewWidget::setMinimumContentWidth(const qreal minw) +{ + d->minimumContentWidth = minw; + d->updateScreenGraphics(); +} + +qreal ScreenPreviewWidget::minimumContentWidth() const +{ + return d->minimumContentWidth; +} + +QRect ScreenPreviewWidget::previewRect() const +{ + return d->previewRect; +} + +KSvg::ImageSet *ScreenPreviewWidget::svgImageSet() const +{ + return d->svgImageSet.get(); +} + +void ScreenPreviewWidget::resizeEvent(QResizeEvent *e) +{ + d->updateScreenGraphics(); +} + +void ScreenPreviewWidget::paintEvent(QPaintEvent *event) +{ + if (d->monitorRect.size().isEmpty()) { + return; + } + + QPainter painter(this); + QPoint standPosition(d->monitorRect.center().x() - d->screenGraphics->elementSize("base").width() / 2, d->previewRect.bottom()); + + d->screenGraphics->paint(&painter, QRect(standPosition, d->screenGraphics->elementSize("base").toSize()), "base"); + d->screenGraphics->paintFrame(&painter, d->monitorRect.topLeft()); + + painter.save(); + if (!d->preview.isNull()) { + painter.setRenderHint(QPainter::SmoothPixmapTransform); + painter.drawPixmap(d->previewRect, d->preview, d->preview.rect()); + } + painter.restore(); + + d->screenGraphics->paint(&painter, d->previewRect, "glass"); +} + +#include "moc_screenpreviewwidget.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/screenpreviewwidget.h b/local/recipes/kde/kwin/source/src/kcms/screenedges/screenpreviewwidget.h new file mode 100644 index 0000000000..c1afc5cb5c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/screenpreviewwidget.h @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2009 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#pragma once + +#include + +namespace KSvg +{ +class ImageSet; +} + +class ScreenPreviewWidgetPrivate; + +class ScreenPreviewWidget : public QWidget +{ + Q_OBJECT + +public: + ScreenPreviewWidget(QWidget *parent); + ~ScreenPreviewWidget() override; + + void setPreview(const QPixmap &preview); + const QPixmap preview() const; + + void setRatio(const qreal ratio); + qreal ratio() const; + + void setMinimumContentWidth(qreal minw); + qreal minimumContentWidth() const; + + QRect previewRect() const; + KSvg::ImageSet *svgImageSet() const; + +protected: + void resizeEvent(QResizeEvent *event) override; + void paintEvent(QPaintEvent *event) override; + +private: + void updateRect(const QRectF &rect); + + const std::unique_ptr d; +}; diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/touch.cpp b/local/recipes/kde/kwin/source/src/kcms/screenedges/touch.cpp new file mode 100644 index 0000000000..35638db3f6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/touch.cpp @@ -0,0 +1,349 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Martin Gräßlin + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "touch.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "kwintouchscreendata.h" +#include "kwintouchscreenedgeconfigform.h" +#include "kwintouchscreenedgeeffectsettings.h" +#include "kwintouchscreenmoduledata.h" +#include "kwintouchscreenscriptsettings.h" +#include "kwintouchscreensettings.h" + +K_PLUGIN_FACTORY_WITH_JSON(KWinScreenEdgesConfigFactory, "kcm_kwintouchscreen.json", registerPlugin(); registerPlugin();) + +namespace KWin +{ + +KWinScreenEdgesConfig::KWinScreenEdgesConfig(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) + , m_form(new KWinTouchScreenEdgeConfigForm(widget())) + , m_config(KSharedConfig::openConfig("kwinrc")) + , m_data(new KWinTouchScreenData(this)) +{ + QVBoxLayout *layout = new QVBoxLayout(widget()); + layout->addWidget(m_form); + + monitorInit(); + + connect(m_form, &KWinTouchScreenEdgeConfigForm::saveNeededChanged, this, &KWinScreenEdgesConfig::unmanagedWidgetChangeState); + connect(m_form, &KWinTouchScreenEdgeConfigForm::defaultChanged, this, &KWinScreenEdgesConfig::unmanagedWidgetDefaultState); +} + +KWinScreenEdgesConfig::~KWinScreenEdgesConfig() +{ +} + +void KWinScreenEdgesConfig::load() +{ + KCModule::load(); + m_data->settings()->load(); + for (KWinTouchScreenScriptSettings *setting : std::as_const(m_scriptSettings)) { + setting->load(); + } + for (KWinTouchScreenEdgeEffectSettings *setting : std::as_const(m_effectSettings)) { + setting->load(); + } + + monitorLoadSettings(); + monitorLoadDefaultSettings(); + m_form->reload(); +} + +void KWinScreenEdgesConfig::save() +{ + monitorSaveSettings(); + m_data->settings()->save(); + for (KWinTouchScreenScriptSettings *setting : std::as_const(m_scriptSettings)) { + setting->save(); + } + for (KWinTouchScreenEdgeEffectSettings *setting : std::as_const(m_effectSettings)) { + setting->save(); + } + + // Reload saved settings to ScreenEdge UI + monitorLoadSettings(); + m_form->reload(); + + // Reload KWin. + QDBusMessage message = QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); + // and reconfigure the effects + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("windowview")); + interface.reconfigureEffect(QStringLiteral("overview")); + for (const auto &effectId : std::as_const(m_effects)) { + interface.reconfigureEffect(effectId); + } + + KCModule::save(); +} + +void KWinScreenEdgesConfig::defaults() +{ + m_form->setDefaults(); + + KCModule::defaults(); +} + +//----------------------------------------------------------------------------- +// Monitor + +void KWinScreenEdgesConfig::monitorInit() +{ + m_form->monitorHideEdge(ElectricTopLeft, true); + m_form->monitorHideEdge(ElectricTopRight, true); + m_form->monitorHideEdge(ElectricBottomRight, true); + m_form->monitorHideEdge(ElectricBottomLeft, true); + + m_form->monitorAddItem(i18n("No Action")); + m_form->monitorAddItem(i18n("Peek at Desktop")); + m_form->monitorAddItem(i18n("Lock Screen")); + m_form->monitorAddItem(i18n("Show KRunner")); + m_form->monitorAddItem(i18n("Activity Manager")); + m_form->monitorAddItem(i18n("Application Launcher")); + + // TODO: Find a better way to get the display name of the present windows, + // Maybe install metadata.json files? + const QString presentWindowsName = i18n("Present Windows"); + m_form->monitorAddItem(i18n("%1 - All Desktops", presentWindowsName)); + m_form->monitorAddItem(i18n("%1 - Current Desktop", presentWindowsName)); + m_form->monitorAddItem(i18n("%1 - Current Application", presentWindowsName)); + + m_form->monitorAddItem(i18n("Overview")); + m_form->monitorAddItem(i18n("Grid")); + + m_form->monitorAddItem(i18n("Toggle window switching")); + m_form->monitorAddItem(i18n("Toggle alternative window switching")); + + KConfigGroup config(m_config, QStringLiteral("Plugins")); + const auto effects = KPackage::PackageLoader::self()->listPackages(QStringLiteral("KWin/Script"), QStringLiteral("kwin-wayland/builtin-effects/")) + + KPackage::PackageLoader::self()->listPackages(QStringLiteral("KWin/Script"), QStringLiteral("kwin-wayland/effects/")) + + KPackage::PackageLoader::self()->listPackages(QStringLiteral("KWin/Script"), QStringLiteral("kwin/effects/")); + + for (const KPluginMetaData &effect : effects) { + if (!effect.value(QStringLiteral("X-KWin-Border-Activate"), false)) { + continue; + } + + if (!config.readEntry(effect.pluginId() + QStringLiteral("Enabled"), effect.isEnabledByDefault())) { + continue; + } + m_effects << effect.pluginId(); + m_form->monitorAddItem(effect.name()); + m_effectSettings[effect.pluginId()] = new KWinTouchScreenEdgeEffectSettings(effect.pluginId(), this); + } + + const auto scripts = KPackage::PackageLoader::self()->listPackages(QStringLiteral("KWin/Script"), QStringLiteral("kwin-wayland/scripts/")) + + KPackage::PackageLoader::self()->listPackages(QStringLiteral("KWin/Script"), QStringLiteral("kwin/scripts/")); + + for (const KPluginMetaData &script : scripts) { + if (script.value(QStringLiteral("X-KWin-Border-Activate"), false) != true) { + continue; + } + + if (!config.readEntry(script.pluginId() + QStringLiteral("Enabled"), script.isEnabledByDefault())) { + continue; + } + m_scripts << script.pluginId(); + m_form->monitorAddItem(script.name()); + m_scriptSettings[script.pluginId()] = new KWinTouchScreenScriptSettings(script.pluginId(), this); + } + + monitorShowEvent(); +} + +void KWinScreenEdgesConfig::monitorLoadSettings() +{ + // Load ElectricBorderActions + m_form->monitorChangeEdge(ElectricTop, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->top())); + m_form->monitorChangeEdge(ElectricRight, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->right())); + m_form->monitorChangeEdge(ElectricBottom, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->bottom())); + m_form->monitorChangeEdge(ElectricLeft, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->left())); + + // Load effect-specific actions: + + // Present Windows BorderActivateAll + m_form->monitorChangeEdge(m_data->settings()->touchBorderActivateAll(), PresentWindowsAll); + // PresentWindows BorderActivate + m_form->monitorChangeEdge(m_data->settings()->touchBorderActivatePresentWindows(), PresentWindowsCurrent); + // PresentWindows BorderActivateClass + m_form->monitorChangeEdge(m_data->settings()->touchBorderActivateClass(), PresentWindowsClass); + + // Overview + m_form->monitorChangeEdge(m_data->settings()->touchBorderActivateOverview(), Overview); + m_form->monitorChangeEdge(m_data->settings()->touchBorderActivateGrid(), Grid); + + // TabBox BorderActivate + m_form->monitorChangeEdge(m_data->settings()->touchBorderActivateTabBox(), TabBox); + // Alternative TabBox + m_form->monitorChangeEdge(m_data->settings()->touchBorderAlternativeActivate(), TabBoxAlternative); + + // Dynamically loaded effects + int lastIndex = EffectCount; + for (int i = 0; i < m_effects.size(); i++) { + m_form->monitorChangeEdge(m_effectSettings[m_effects[i]]->touchBorderActivate(), lastIndex); + ++lastIndex; + } + + // Scripts + for (int i = 0; i < m_scripts.size(); i++) { + m_form->monitorChangeEdge(m_scriptSettings[m_scripts[i]]->touchBorderActivate(), lastIndex); + ++lastIndex; + } +} + +void KWinScreenEdgesConfig::monitorLoadDefaultSettings() +{ + m_form->monitorChangeDefaultEdge(ElectricTop, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->defaultTopValue())); + m_form->monitorChangeDefaultEdge(ElectricRight, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->defaultRightValue())); + m_form->monitorChangeDefaultEdge(ElectricBottom, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->defaultBottomValue())); + m_form->monitorChangeDefaultEdge(ElectricLeft, KWinScreenEdgesConfig::electricBorderActionFromString(m_data->settings()->defaultLeftValue())); + + // Present Windows BorderActivateAll + m_form->monitorChangeDefaultEdge(m_data->settings()->defaultTouchBorderActivateAllValue(), PresentWindowsAll); + // PresentWindows BorderActivate + m_form->monitorChangeDefaultEdge(m_data->settings()->defaultTouchBorderActivatePresentWindowsValue(), PresentWindowsCurrent); + // PresentWindows BorderActivateClass + m_form->monitorChangeDefaultEdge(m_data->settings()->defaultTouchBorderActivateClassValue(), PresentWindowsClass); + + // Overview + m_form->monitorChangeDefaultEdge(m_data->settings()->defaultTouchBorderActivateOverviewValue(), Overview); + m_form->monitorChangeDefaultEdge(m_data->settings()->defaultTouchBorderActivateGridValue(), Grid); + + // TabBox BorderActivate + m_form->monitorChangeDefaultEdge(m_data->settings()->defaultTouchBorderActivateTabBoxValue(), TabBox); + // Alternative TabBox + m_form->monitorChangeDefaultEdge(m_data->settings()->defaultTouchBorderAlternativeActivateValue(), TabBoxAlternative); +} + +void KWinScreenEdgesConfig::monitorSaveSettings() +{ + // Save ElectricBorderActions + m_data->settings()->setTop(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricTop))); + m_data->settings()->setRight(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricRight))); + m_data->settings()->setBottom(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricBottom))); + m_data->settings()->setLeft(KWinScreenEdgesConfig::electricBorderActionToString(m_form->selectedEdgeItem(ElectricLeft))); + + // Save effect-specific actions: + + // Present Windows + m_data->settings()->setTouchBorderActivateAll(m_form->monitorCheckEffectHasEdge(PresentWindowsAll)); + m_data->settings()->setTouchBorderActivatePresentWindows(m_form->monitorCheckEffectHasEdge(PresentWindowsCurrent)); + m_data->settings()->setTouchBorderActivateClass(m_form->monitorCheckEffectHasEdge(PresentWindowsClass)); + + // Overview + m_data->settings()->setTouchBorderActivateOverview(m_form->monitorCheckEffectHasEdge(Overview)); + m_data->settings()->setTouchBorderActivateGrid(m_form->monitorCheckEffectHasEdge(Grid)); + + // TabBox + m_data->settings()->setTouchBorderActivateTabBox(m_form->monitorCheckEffectHasEdge(TabBox)); + m_data->settings()->setTouchBorderAlternativeActivate(m_form->monitorCheckEffectHasEdge(TabBoxAlternative)); + + // Dynamically loaded effects + int lastIndex = EffectCount; + for (int i = 0; i < m_effects.size(); i++) { + m_effectSettings[m_effects[i]]->setTouchBorderActivate(m_form->monitorCheckEffectHasEdge(lastIndex)); + ++lastIndex; + } + + // Scripts + for (int i = 0; i < m_scripts.size(); i++) { + m_scriptSettings[m_scripts[i]]->setTouchBorderActivate(m_form->monitorCheckEffectHasEdge(lastIndex)); + ++lastIndex; + } +} + +void KWinScreenEdgesConfig::monitorShowEvent() +{ + // Check if they are enabled + KConfigGroup config(m_config, QStringLiteral("Plugins")); + + // Present Windows + bool enabled = config.readEntry("windowviewEnabled", true); + m_form->monitorItemSetEnabled(PresentWindowsCurrent, enabled); + m_form->monitorItemSetEnabled(PresentWindowsAll, enabled); + + // Overview + const bool overviewEnabled = config.readEntry("overviewEnabled", true); + m_form->monitorItemSetEnabled(Overview, overviewEnabled); + m_form->monitorItemSetEnabled(Grid, overviewEnabled); + + // tabbox, depends on reasonable focus policy. + KConfigGroup config2(m_config, QStringLiteral("Windows")); + QString focusPolicy = config2.readEntry("FocusPolicy", QString()); + bool reasonable = focusPolicy != "FocusStrictlyUnderMouse" && focusPolicy != "FocusUnderMouse"; + m_form->monitorItemSetEnabled(TabBox, reasonable); + m_form->monitorItemSetEnabled(TabBoxAlternative, reasonable); + + // Disable Edge if TouchEdges group entries are immutable + m_form->monitorEnableEdge(ElectricTop, !m_data->settings()->isTopImmutable()); + m_form->monitorEnableEdge(ElectricRight, !m_data->settings()->isRightImmutable()); + m_form->monitorEnableEdge(ElectricBottom, !m_data->settings()->isBottomImmutable()); + m_form->monitorEnableEdge(ElectricLeft, !m_data->settings()->isLeftImmutable()); +} + +ElectricBorderAction KWinScreenEdgesConfig::electricBorderActionFromString(const QString &string) +{ + QString lowerName = string.toLower(); + if (lowerName == QLatin1StringView("showdesktop")) { + return ElectricActionShowDesktop; + } + if (lowerName == QLatin1StringView("lockscreen")) { + return ElectricActionLockScreen; + } + if (lowerName == QLatin1StringView("krunner")) { + return ElectricActionKRunner; + } + if (lowerName == QLatin1StringView("activitymanager")) { + return ElectricActionActivityManager; + } + if (lowerName == QLatin1StringView("applicationlauncher")) { + return ElectricActionApplicationLauncher; + } + return ElectricActionNone; +} + +QString KWinScreenEdgesConfig::electricBorderActionToString(int action) +{ + switch (action) { + case 1: + return QStringLiteral("ShowDesktop"); + case 2: + return QStringLiteral("LockScreen"); + case 3: + return QStringLiteral("KRunner"); + case 4: + return QStringLiteral("ActivityManager"); + case 5: + return QStringLiteral("ApplicationLauncher"); + default: + return QStringLiteral("None"); + } +} + +} // namespace + +#include "touch.moc" + +#include "moc_touch.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/touch.h b/local/recipes/kde/kwin/source/src/kcms/screenedges/touch.h new file mode 100644 index 0000000000..164e794fc2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/touch.h @@ -0,0 +1,70 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include "effect/globals.h" + +class QShowEvent; + +namespace KWin +{ +class KWinTouchScreenData; +class KWinTouchScreenEdgeConfigForm; +class KWinTouchScreenScriptSettings; +class KWinTouchScreenEdgeEffectSettings; + +class KWinScreenEdgesConfig : public KCModule +{ + Q_OBJECT + +public: + explicit KWinScreenEdgesConfig(QObject *parent, const KPluginMetaData &data); + ~KWinScreenEdgesConfig() override; + +public Q_SLOTS: + void save() override; + void load() override; + void defaults() override; + +private: + KWinTouchScreenEdgeConfigForm *m_form; + KSharedConfigPtr m_config; + QStringList m_effects; // list of effect IDs ordered in the list they are presented in the menu + QStringList m_scripts; // list of script IDs ordered in the list they are presented in the menu + QHash m_scriptSettings; + QHash m_effectSettings; + KWinTouchScreenData *m_data; + + enum EffectActions { + PresentWindowsAll = ELECTRIC_ACTION_COUNT, // Start at the end of built in actions + PresentWindowsCurrent, + PresentWindowsClass, + Overview, + Grid, + TabBox, + TabBoxAlternative, + EffectCount + }; + + void monitorInit(); + void monitorLoadSettings(); + void monitorLoadDefaultSettings(); + void monitorSaveSettings(); + void monitorShowEvent(); + + static ElectricBorderAction electricBorderActionFromString(const QString &string); + static QString electricBorderActionToString(int action); +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/kcms/screenedges/touch.ui b/local/recipes/kde/kwin/source/src/kcms/screenedges/touch.ui new file mode 100644 index 0000000000..b30233bac9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/screenedges/touch.ui @@ -0,0 +1,72 @@ + + + KWinTouchScreenConfigUi + + + + 0 + 0 + 500 + 500 + + + + + + + You can trigger an action by swiping from the screen edge towards the center of the screen. + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 20 + + + + + + + + + 200 + 200 + + + + Qt::StrongFocus + + + + + + + Qt::Vertical + + + + + + + + KWin::Monitor + QWidget +
monitor.h
+ 1 +
+
+ + +
diff --git a/local/recipes/kde/kwin/source/src/kcms/scripts/CMakeLists.txt b/local/recipes/kde/kwin/source/src/kcms/scripts/CMakeLists.txt new file mode 100644 index 0000000000..7daa804751 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/scripts/CMakeLists.txt @@ -0,0 +1,21 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_kwin_scripts\") + +kcmutils_add_qml_kcm(kcm_kwin_scripts INSTALL_NAMESPACE plasma/kcms/systemsettings) + +target_sources(kcm_kwin_scripts PRIVATE + module.cpp + kwinscriptsdata.cpp +) + +target_link_libraries(kcm_kwin_scripts PRIVATE + Qt::DBus + + KF6::I18n + KF6::KCMUtilsCore + KF6::KCMUtils + KF6::Package + KF6::KCMUtilsQuick +) + +install(FILES kwinscripts.knsrc DESTINATION ${KDE_INSTALL_KNSRCDIR}) diff --git a/local/recipes/kde/kwin/source/src/kcms/scripts/Messages.sh b/local/recipes/kde/kwin/source/src/kcms/scripts/Messages.sh new file mode 100755 index 0000000000..20ff4d4f9d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/scripts/Messages.sh @@ -0,0 +1,4 @@ +#!bin/sh +$EXTRACTRC `find . -name \*.rc -o -name \*.ui -o -name \*.kcfg` >> rc.cpp +$XGETTEXT `find . -name \*.cc -o -name \*.cpp -o -name \*.h -o -name \*.qml` -o $podir/kcm_kwin_scripts.pot +rm -f rc.cpp diff --git a/local/recipes/kde/kwin/source/src/kcms/scripts/kcm_kwin_scripts.json b/local/recipes/kde/kwin/source/src/kcms/scripts/kcm_kwin_scripts.json new file mode 100644 index 0000000000..140ffcd2e8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/scripts/kcm_kwin_scripts.json @@ -0,0 +1,144 @@ +{ + "KPlugin": { + "BugReportUrl": "https://bugs.kde.org/enter_bug.cgi?product=systemsettings&component=kcm_kwinscripts", + "Description": "Manage KWin scripts", + "Description[ar]": "أدر سكربتات كوين", + "Description[az]": "Kwin skriptlərini idarə edin", + "Description[be]": "Кіраванне скрыптамі KWin", + "Description[bg]": "Управление на скриптове на KWin", + "Description[ca@valencia]": "Gestiona els scripts de KWin", + "Description[ca]": "Gestiona els scripts del KWin", + "Description[cs]": "Spravovat skripty KWinu", + "Description[da]": "Adminstrér KWin-scripts", + "Description[de]": "KWin-Skripte verwalten", + "Description[en_GB]": "Manage KWin scripts", + "Description[eo]": "Administri KWin-skriptoj", + "Description[es]": "Gestión de guiones de KWin", + "Description[et]": "KWini skriptide haldamine", + "Description[eu]": "Kudeatu KWin gidoiak", + "Description[fi]": "Hallitse KWin-skriptejä", + "Description[fr]": "Gérer les scripts pour KWin", + "Description[gl]": "Xestiona os guións de KWin.", + "Description[he]": "ניהול סקריפטים של KWin", + "Description[hu]": "KWin szkriptek kezelése", + "Description[ia]": "Gere scriptos de KWin", + "Description[id]": "Kelola skrip KWin", + "Description[is]": "Umsjón með KWin-skriftum", + "Description[it]": "Gestisci gli script di KWin", + "Description[ja]": "Kwin スクリプトを管理", + "Description[ka]": "KWin-ის სკრიპტების მართვა", + "Description[ko]": "KWin 스크립트 관리", + "Description[lt]": "Tvarkyti KWin scenarijus", + "Description[lv]": "Pārvaldīt „KWin“ skriptus", + "Description[nb]": "Håndter KWin-skript", + "Description[nl]": "KWin-scripts beheren", + "Description[nn]": "Handsam KWin-skript", + "Description[pl]": "Zarządzanie skryptami KWin", + "Description[pt]": "Gerir os programas do KWin", + "Description[pt_BR]": "Gerencia os scripts do KWin", + "Description[ro]": "Gestionare scripturi KWin", + "Description[ru]": "Управление сценариями KWin", + "Description[sa]": "KWin स्क्रिप्ट् प्रबन्धयन्तु", + "Description[sk]": "Spravovať KWin skripty", + "Description[sl]": "Upravljajte skripte KWin", + "Description[sv]": "Hantera Kwin-skript", + "Description[ta]": "கேவின்னுக்கான சிறுநிரல்களை நிர்வகியுங்கள்", + "Description[tr]": "KWin Betiklerini Yönet", + "Description[uk]": "Керування скриптами KWin", + "Description[vi]": "Quản lí các kịch bản KWin", + "Description[zh_CN]": "管理 KWin 脚本", + "Description[zh_TW]": "管理 KWin 文稿", + "Icon": "preferences-system-windows-actions", + "Name": "KWin Scripts", + "Name[ar]": "سكربتات كوين", + "Name[az]": "Kwin skriptləri", + "Name[be]": "Скрыпты KWin", + "Name[bg]": "Скриптове на KWin", + "Name[ca@valencia]": "Scripts de KWin", + "Name[ca]": "Scripts del KWin", + "Name[cs]": "Skripty KWinu", + "Name[da]": "KWin-scripts", + "Name[de]": "KWin-Skripte", + "Name[en_GB]": "KWin Scripts", + "Name[eo]": "KWin-Skriptoj", + "Name[es]": "Guiones de KWin", + "Name[et]": "KWini skriptid", + "Name[eu]": "KWin gidoiak", + "Name[fi]": "KWin-skriptit", + "Name[fr]": "Scripts pour KWin", + "Name[ga]": "Scripteanna KWin", + "Name[gl]": "Guións de KWin", + "Name[he]": "סקריפטים של KWin", + "Name[hu]": "KWin szkriptek", + "Name[ia]": "Script de KWin", + "Name[id]": "Skrip KWin", + "Name[is]": "KWin-skriftur", + "Name[it]": "Script di KWin", + "Name[ja]": "KWin スクリプト", + "Name[ka]": "KWin-ის სკრიპტები", + "Name[ko]": "KWin 스크립트", + "Name[lt]": "KWin scenarijai", + "Name[lv]": "„KWin“ skripti", + "Name[nb]": "KWin-skript", + "Name[nl]": "KWin-scripts", + "Name[nn]": "KWin-skript", + "Name[pl]": "Skrypty KWin", + "Name[pt]": "Programas do KWin", + "Name[pt_BR]": "Scripts do KWin", + "Name[ro]": "Scripturi KWin", + "Name[ru]": "Сценарии KWin", + "Name[sa]": "KWin स्क्रिप्ट्", + "Name[sk]": "KWin skripty", + "Name[sl]": "Skripti KWin", + "Name[sv]": "Kwin-skript", + "Name[ta]": "கேவின் சிறுநிரல்கள்", + "Name[tr]": "KWin Betikleri", + "Name[uk]": "Скрипти KWin", + "Name[vi]": "Kịch bản KWin", + "Name[zh_CN]": "KWin 脚本", + "Name[zh_TW]": "KWin 文稿" + }, + "X-KDE-Keywords": "kwin script,plugins,window manager", + "X-KDE-Keywords[ar]": "كوين البرنامج النصي,والإضافات,ومدير النافذة", + "X-KDE-Keywords[bg]": "kwin скрипт,плъгини,мениджър на прозорци", + "X-KDE-Keywords[ca@valencia]": "script de kwin,connectors,gestor de finestres", + "X-KDE-Keywords[ca]": "script de kwin,connectors,gestor de finestres", + "X-KDE-Keywords[cs]": "skript kwinu,moduly,správce oken", + "X-KDE-Keywords[de]": "KWin-Skript,Module,Fensterverwaltung", + "X-KDE-Keywords[en_GB]": "kwin script,plugins,window manager", + "X-KDE-Keywords[es]": "guion de kwin,complementos,gestor de ventanas,administrador de ventanas", + "X-KDE-Keywords[eu]": "kwin gidoia,pluginak,leiho kudeatzailea", + "X-KDE-Keywords[fi]": "kwin-scripti,liitännäinen,liitännäiset,ikkunaohjelma,ikkunahallinta", + "X-KDE-Keywords[fr]": "script pour KWin, modules externes, gestionnaire de fenêtres", + "X-KDE-Keywords[gl]": "kwin script,script de kwin,guión de kwin,plugins,complementos,extensións,window manager,xestor de xanelas,xestor de ventás,xestor de fiestras", + "X-KDE-Keywords[he]": "סקריפט kwin,תוספים,מנהל חלונות", + "X-KDE-Keywords[hu]": "kwin szkript,bővítmények,ablakkezelő", + "X-KDE-Keywords[ia]": "script, plugins, gerente de fenestra de kwin", + "X-KDE-Keywords[is]": "kwin-skrifta,viðbætur,gluggastjóri", + "X-KDE-Keywords[it]": "script di kwin,estensioni,gestore delle finestre", + "X-KDE-Keywords[ja]": "kwin script,plugins,window manager,kwin スクリプト,プラグイン,ウィンドウ管理,ウィンドウマネージャー", + "X-KDE-Keywords[ka]": "kwin script,plugins,window manager,დამატებები,ფანჯრების მართვა,kwin-ის სკრიპტი", + "X-KDE-Keywords[ko]": "kwin 스크립트,플러그인,창 관리자", + "X-KDE-Keywords[lt]": "kwin scenarijus,kwin scenarijai,įskiepiai,iskiepiai,langų tvarkytuvė,langu tvarkytuve", + "X-KDE-Keywords[lv]": "kwin skripts,spraudņi,logu pārvaldnieks", + "X-KDE-Keywords[nb]": "kwin-skript,tillegg,programtillegg,vindusbehandler", + "X-KDE-Keywords[nl]": "kwin-script,plug-ins,vensterbeheerder", + "X-KDE-Keywords[nn]": "kwin-skript,tillegg,programtillegg,vindaugshandsamar", + "X-KDE-Keywords[pl]": "skrypt kwin,wtyczki,zarządzanie oknami", + "X-KDE-Keywords[pt_BR]": "kwin script,plugins,gerenciador de janelas", + "X-KDE-Keywords[ro]": "script kwin,extensii,gestionar ferestre,gestionar de ferestre", + "X-KDE-Keywords[ru]": "kwin script,plugins,window manager,сценарий,модули,диспетчер окон", + "X-KDE-Keywords[sa]": "kwin स्क्रिप्ट,प्लगिन्,विण्डो प्रबन्धक", + "X-KDE-Keywords[sk]": "kwin script,plugins,window manager", + "X-KDE-Keywords[sl]": "skript kwin,vtičniki,upravljalnik okna", + "X-KDE-Keywords[sv]": "kwin-skript,insticksprogram,fönsterhanterare", + "X-KDE-Keywords[tr]": "kwin betiği,eklentiler,pencere yöneticisi", + "X-KDE-Keywords[uk]": "kwin script,plugins,window manager,скрипт квін,додатки,керування,вікна", + "X-KDE-Keywords[zh_CN]": "kwin script,plugins,window manager,jiaoben,chajian,chuangkouguanliqi,kwin 脚本,插件,窗口管理器", + "X-KDE-Keywords[zh_TW]": "kwin 文稿,kwin 指令稿,插件,外掛程式,視窗管理員", + "X-KDE-OnlyShowOnQtPlatforms": [ + "wayland" + ], + "X-KDE-System-Settings-Parent-Category": "windowmanagement", + "X-KDE-Weight": 50 +} diff --git a/local/recipes/kde/kwin/source/src/kcms/scripts/kwinscripts.knsrc b/local/recipes/kde/kwin/source/src/kcms/scripts/kwinscripts.knsrc new file mode 100644 index 0000000000..c57381a6e9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/scripts/kwinscripts.knsrc @@ -0,0 +1,61 @@ +[KNewStuff3] +Name=Window Manager Scripts +Name[ar]=سكربتات مدير النوافذ +Name[az]=Pəncərə Meneceri skriptləri +Name[be]=Скрыпты кіраўніка акон +Name[bg]=Скриптове на мениджър на прозорци +Name[ca]=Scripts del gestor de finestres +Name[ca@valencia]=Scripts del gestor de finestres +Name[cs]=Skripty správce oken +Name[da]=Vindueshåndteringsscripts +Name[de]=Fensterverwaltungs-Skripte +Name[el]=Σενάρια διαχειριστή παραθύρων +Name[en_GB]=Window Manager Scripts +Name[eo]=Fenestraj Administraj Skriptoj +Name[es]=Guiones del gestor de ventanas +Name[et]=Aknahalduri skriptid +Name[eu]=Leiho kudeatzailearen gidoiak +Name[fi]=Ikkunointiohjelman skriptit +Name[fr]=Scripts du gestionnaire de fenêtres +Name[gl]=Guións do xestor de xanelas +Name[he]=סקריפטים של מנהל חלונות +Name[hu]=Ablakkezelő szkriptek +Name[ia]=Gerente de scripts de fenestra +Name[id]=Skrip Pengelola Jendela +Name[is]=Gluggastjóraskriftur +Name[it]=Script del gestore delle finestre +Name[ja]=ウィンドウマネージャスクリプト +Name[ka]=ფანჯრების მმართველის სკრიპტები +Name[ko]=창 관리자 스크립트 +Name[lt]=Langų tvarkytuvės scenarijai +Name[lv]=Logu pārvaldnieka skripti +Name[nb]=Skript for vindusbehandler +Name[nl]=Scripts van vensterbeheerder +Name[nn]=Skript for vindaugshandsamar +Name[pa]=ਵਿੰਡੋ ਮੈਨੇਜਰ ਸਕ੍ਰਿਪਟਾਂ +Name[pl]=Skrypty zarządzania oknami +Name[pt]=Programas do Gestor de Janelas +Name[pt_BR]=Scripts do gerenciador de janelas +Name[ro]=Scripturi pentru gestionar de ferestre +Name[ru]=Сценарии для диспетчера окон KWin +Name[sa]=विण्डो प्रबन्धक स्क्रिप्ट् +Name[sk]=Skripty správcu okien +Name[sl]=Skripti upravljalnika oken +Name[sr]=Скрипте менаџера прозора +Name[sr@ijekavian]=Скрипте менаџера прозора +Name[sr@ijekavianlatin]=Skripte menadžera prozora +Name[sr@latin]=Skripte menadžera prozora +Name[sv]=Fönsterhanteringsskript +Name[ta]=சாளர நிர்வாக சிறுநிரல்கள் +Name[tr]=Pencere Yöneticisi Betikleri +Name[uk]=Скрипти засобу керування вікнами +Name[vi]=Các kịch bản trình quản lí cửa sổ +Name[zh_CN]=窗口管理器脚本 +Name[zh_TW]=視窗管理員指令稿 + +ProvidersUrl=https://autoconfig.kde.org/ocs/providers.xml +ContentWarning=Executables +Categories=Kwin Scripts Plasma 6 +StandardResource=tmp +Uncompress=kpackage +KPackageStructure=KWin/Script diff --git a/local/recipes/kde/kwin/source/src/kcms/scripts/kwinscriptsdata.cpp b/local/recipes/kde/kwin/source/src/kcms/scripts/kwinscriptsdata.cpp new file mode 100644 index 0000000000..64cd3429ba --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/scripts/kwinscriptsdata.cpp @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwinscriptsdata.h" + +#include "config-kwin.h" + +#include +#include +#include +#include +#include + +KWinScriptsData::KWinScriptsData(QObject *parent) + : KCModuleData(parent) + , m_kwinConfig(KSharedConfig::openConfig("kwinrc")) +{ +} + +QList KWinScriptsData::pluginMetaDataList() const +{ + return KPackage::PackageLoader::self()->findPackages(QStringLiteral("KWin/Script"), QStringLiteral("kwin-wayland/scripts/")) + + KPackage::PackageLoader::self()->findPackages(QStringLiteral("KWin/Script"), QStringLiteral("kwin/scripts/")); +} + +bool KWinScriptsData::isDefaults() const +{ + QList plugins = pluginMetaDataList(); + KConfigGroup cfgGroup(m_kwinConfig, QStringLiteral("Plugins")); + for (auto &plugin : plugins) { + if (cfgGroup.readEntry(plugin.pluginId() + QLatin1String("Enabled"), plugin.isEnabledByDefault()) != plugin.isEnabledByDefault()) { + return false; + } + } + + return true; +} + +#include "moc_kwinscriptsdata.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/scripts/kwinscriptsdata.h b/local/recipes/kde/kwin/source/src/kcms/scripts/kwinscriptsdata.h new file mode 100644 index 0000000000..b34b4c7adc --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/scripts/kwinscriptsdata.h @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include +#include +#include + +class KWinScriptsData : public KCModuleData +{ + Q_OBJECT + +public: + KWinScriptsData(QObject *parent); + + bool isDefaults() const override; + + QList pluginMetaDataList() const; + +private: + KSharedConfigPtr m_kwinConfig; +}; diff --git a/local/recipes/kde/kwin/source/src/kcms/scripts/module.cpp b/local/recipes/kde/kwin/source/src/kcms/scripts/module.cpp new file mode 100644 index 0000000000..c76f6b2627 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/scripts/module.cpp @@ -0,0 +1,156 @@ +/* + SPDX-FileCopyrightText: 2011 Tamas Krutki + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "module.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "kwinscriptsdata.h" + +Module::Module(QObject *parent, const KPluginMetaData &data) + : KQuickConfigModule(parent, data) + , m_kwinScriptsData(new KWinScriptsData(this)) + , m_model(new KPluginModel(this)) +{ + // Hide the help button, because there is no help + setButtons(Apply | Default); + connect(m_model, &KPluginModel::isSaveNeededChanged, this, [this]() { + setNeedsSave(m_model->isSaveNeeded() || !m_pendingDeletions.isEmpty()); + }); + connect(m_model, &KPluginModel::defaulted, this, [this](bool defaulted) { + setRepresentsDefaults(defaulted); + }); + m_model->setConfig(KSharedConfig::openConfig("kwinrc")->group(QStringLiteral("Plugins"))); +} + +void Module::onGHNSEntriesChanged() +{ + m_model->clear(); + m_model->addPlugins(m_kwinScriptsData->pluginMetaDataList(), QString()); +} + +void Module::importScript() +{ + QFileDialog *dialog = new QFileDialog; + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->setFileMode(QFileDialog::ExistingFile); + dialog->setWindowTitle(i18n("Import KWin Script")); + dialog->setNameFilter(i18n("KWin Script (*.kwinscript)")); + + connect(dialog, &QFileDialog::accepted, this, [this, dialog] { + using namespace KPackage; + + if (dialog->selectedFiles().isEmpty()) { + return; + } + + auto job = PackageJob::update(QStringLiteral("KWin/Script"), dialog->selectedFiles().first()); + connect(job, &KJob::result, this, [job, this]() { + if (job->error() != KJob::NoError) { + setErrorMessage(i18nc("Placeholder is error message returned from the install service", "Cannot import selected script.\n%1", job->errorString())); + return; + } + + m_infoMessage = i18nc("Placeholder is name of the script that was imported", "The script \"%1\" was successfully imported.", job->package().metadata().name()); + m_errorMessage.clear(); + Q_EMIT messageChanged(); + + m_model->clear(); + m_model->addPlugins(m_kwinScriptsData->pluginMetaDataList(), QString()); + + setNeedsSave(false); + }); + }); + + dialog->open(); +} + +void Module::configure(const KPluginMetaData &data) +{ + auto dialog = new KCMultiDialog(); + dialog->addModule(data, QVariantList{data.pluginId(), QStringLiteral("KWin/Script")}); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->show(); +} + +void Module::togglePendingDeletion(const KPluginMetaData &data) +{ + if (m_pendingDeletions.contains(data)) { + m_pendingDeletions.removeOne(data); + } else { + m_pendingDeletions.append(data); + } + setNeedsSave(m_model->isSaveNeeded() || !m_pendingDeletions.isEmpty()); + Q_EMIT pendingDeletionsChanged(); +} + +void Module::defaults() +{ + m_model->defaults(); + m_pendingDeletions.clear(); + Q_EMIT pendingDeletionsChanged(); + + setNeedsSave(m_model->isSaveNeeded()); +} + +void Module::load() +{ + m_model->clear(); + m_model->addPlugins(m_kwinScriptsData->pluginMetaDataList(), QString()); + m_pendingDeletions.clear(); + Q_EMIT pendingDeletionsChanged(); + + setNeedsSave(false); +} + +void Module::save() +{ + using namespace KPackage; + for (const KPluginMetaData &info : std::as_const(m_pendingDeletions)) { + // We can get the package root from the entry path + QDir root = QFileInfo(info.fileName()).dir(); + root.cdUp(); + KJob *uninstallJob = PackageJob::uninstall(QStringLiteral("KWin/Script"), info.pluginId(), root.absolutePath()); + connect(uninstallJob, &KJob::result, this, [this, uninstallJob]() { + if (!uninstallJob->errorString().isEmpty()) { + setErrorMessage(i18n("Error when uninstalling KWin Script: %1", uninstallJob->errorString())); + } else { + load(); // Make sure to reload the KCM to deleted entries to disappear + } + }); + } + + m_infoMessage.clear(); + Q_EMIT messageChanged(); + m_pendingDeletions.clear(); + Q_EMIT pendingDeletionsChanged(); + + m_model->save(); + QDBusMessage message = QDBusMessage::createMethodCall("org.kde.KWin", "/Scripting", "org.kde.kwin.Scripting", "start"); + QDBusConnection::sessionBus().asyncCall(message); + + setNeedsSave(false); +} + +K_PLUGIN_FACTORY_WITH_JSON(KcmKWinScriptsFactory, "kcm_kwin_scripts.json", + registerPlugin(); + registerPlugin();) + +#include "module.moc" + +#include "moc_module.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/scripts/module.h b/local/recipes/kde/kwin/source/src/kcms/scripts/module.h new file mode 100644 index 0000000000..ff291dd6c0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/scripts/module.h @@ -0,0 +1,83 @@ +/* + SPDX-FileCopyrightText: 2011 Tamas Krutki + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +class KJob; +class KWinScriptsData; + +class Module : public KQuickConfigModule +{ + Q_OBJECT + + Q_PROPERTY(QAbstractItemModel *model READ model CONSTANT) + Q_PROPERTY(QList pendingDeletions READ pendingDeletions NOTIFY pendingDeletionsChanged) + Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY messageChanged) + Q_PROPERTY(QString infoMessage READ infoMessage NOTIFY messageChanged) +public: + explicit Module(QObject *parent, const KPluginMetaData &data); + + void load() override; + void save() override; + void defaults() override; + + QAbstractItemModel *model() const + { + return m_model; + } + + Q_INVOKABLE void togglePendingDeletion(const KPluginMetaData &data); + Q_INVOKABLE bool canDeleteEntry(const KPluginMetaData &data) + { + return QFileInfo(data.fileName()).isWritable(); + } + + QList pendingDeletions() + { + return m_pendingDeletions; + } + + QString errorMessage() const + { + return m_errorMessage; + } + QString infoMessage() const + { + return m_infoMessage; + } + void setErrorMessage(const QString &message) + { + m_infoMessage.clear(); + m_errorMessage = message; + Q_EMIT messageChanged(); + } + + /** + * Called when the import script button is clicked. + */ + Q_INVOKABLE void importScript(); + Q_INVOKABLE void onGHNSEntriesChanged(); + + Q_INVOKABLE void configure(const KPluginMetaData &data); + +Q_SIGNALS: + void messageChanged(); + void pendingDeletionsChanged(); + +private: + KWinScriptsData *m_kwinScriptsData; + QList m_pendingDeletions; + KPluginModel *m_model; + QString m_errorMessage; + QString m_infoMessage; +}; diff --git a/local/recipes/kde/kwin/source/src/kcms/scripts/ui/main.qml b/local/recipes/kde/kwin/source/src/kcms/scripts/ui/main.qml new file mode 100644 index 0000000000..b1fae7319e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/scripts/ui/main.qml @@ -0,0 +1,73 @@ +/* + SPDX-FileCopyrightText: 2013 Antonis Tsiapaliokas + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick +import QtQuick.Layouts + +import org.kde.config +import org.kde.kirigami as Kirigami +import org.kde.newstuff as NewStuff +import org.kde.kcmutils as KCMUtils + +KCMUtils.ScrollViewKCM { + implicitWidth: Kirigami.Units.gridUnit * 22 + implicitHeight: Kirigami.Units.gridUnit * 20 + + actions: [ + Kirigami.Action { + icon.name: "document-import" + text: i18n("Install from File…") + onTriggered: kcm.importScript() + }, + NewStuff.Action { + text: i18nc("@action:button get new KWin scripts", "Get New…") + visible: KAuthorized.authorize(KAuthorized.GHNS) + configFile: "kwinscripts.knsrc" + onEntryEvent: (entry, event) => { + if (event === NewStuff.Engine.StatusChangedEvent) { + kcm.onGHNSEntriesChanged() + } + } + } + ] + + header: ColumnLayout { + spacing: Kirigami.Units.smallSpacing + + Kirigami.InlineMessage { + Layout.fillWidth: true + visible: kcm.errorMessage || kcm.infoMessage + type: kcm.errorMessage ? Kirigami.MessageType.Error : Kirigami.MessageType.Information + text: kcm.errorMessage || kcm.infoMessage + } + + Kirigami.SearchField { + Layout.fillWidth: true + id: searchField + } + } + + view: KCMUtils.PluginSelector { + id: selector + sourceModel: kcm.model + query: searchField.text + + delegate: KCMUtils.PluginDelegate { + onConfigTriggered: kcm.configure(model.config) + additionalActions: [ + Kirigami.Action { + enabled: kcm.canDeleteEntry(model.metaData) + icon.name: kcm.pendingDeletions.indexOf(model.metaData) === -1 ? "delete" : "edit-undo" + text: i18nc("@info:tooltip", "Delete…") + displayHint: Kirigami.DisplayHint.IconOnly + + onTriggered: kcm.togglePendingDeletion(model.metaData) + } + ] + } + } +} diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/CMakeLists.txt b/local/recipes/kde/kwin/source/src/kcms/tabbox/CMakeLists.txt new file mode 100644 index 0000000000..d09c1894bd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/CMakeLists.txt @@ -0,0 +1,58 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_kwintabbox\") + +set(kcm_kwintabbox_PART_SRCS + ${KWin_SOURCE_DIR}/src/tabbox/tabboxconfig.cpp + main.cpp + kwintabboxconfigform.cpp + kwintabboxdata.cpp + shortcutsettings.cpp +) + +ki18n_wrap_ui(kcm_kwintabbox_PART_SRCS main.ui) +qt_add_dbus_interface(kcm_kwintabbox_PART_SRCS ${KWin_SOURCE_DIR}/src/org.kde.kwin.Effects.xml kwin_effects_interface) + +kconfig_add_kcfg_files(kcm_kwintabbox_PART_SRCS kwintabboxsettings.kcfgc kwinswitcheffectsettings.kcfgc) +kcoreaddons_add_plugin(kcm_kwintabbox SOURCES ${kcm_kwintabbox_PART_SRCS} INSTALL_NAMESPACE "plasma/kcms/systemsettings_qwidgets") + +kcmutils_generate_desktop_file(kcm_kwintabbox) +target_link_libraries(kcm_kwintabbox + KF6::GlobalAccel + KF6::I18n + KF6::KCMUtils + KF6::NewStuffWidgets + KF6::Package + KF6::Service + KF6::XmlGui # For kkeysequencewidget + KF6::WidgetsAddons +) +if (KWIN_BUILD_X11) + target_link_libraries(kcm_kwintabbox + XCB::XCB + ) +endif() + +# TabBox preview helper +add_executable(kwin-tabbox-preview + layoutpreview.cpp + thumbnailitem.cpp +) +qt_add_resources(kwin-tabbox-preview "thumbnails" + PREFIX "/kwin-tabbox-preview" + BASE "thumbnails" + FILES + thumbnails/falkon.png + thumbnails/kmail.png + thumbnails/systemsettings.png + thumbnails/dolphin.png + thumbnails/desktop.png +) +target_link_libraries(kwin-tabbox-preview + Qt::Quick + KF6::I18n + KF6::I18nQml +) +install(TARGETS kwin-tabbox-preview DESTINATION ${KDE_INSTALL_LIBEXECDIR}) + +# KNewStuff +install(FILES kwinswitcher.knsrc DESTINATION ${KDE_INSTALL_KNSRCDIR}) diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/Messages.sh b/local/recipes/kde/kwin/source/src/kcms/tabbox/Messages.sh new file mode 100644 index 0000000000..b4dd7b37f7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/Messages.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash +$EXTRACTRC *.ui >> rc.cpp || exit 11 +$XGETTEXT *.cpp -o $podir/kcm_kwintabbox.pot +rm -f rc.cpp diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/kcm_kwintabbox.json b/local/recipes/kde/kwin/source/src/kcms/tabbox/kcm_kwintabbox.json new file mode 100644 index 0000000000..aef4173a11 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/kcm_kwintabbox.json @@ -0,0 +1,143 @@ +{ + "KPlugin": { + "BugReportUrl": "https://bugs.kde.org/enter_bug.cgi?product=systemsettings&component=kcm_kwintabbox", + "Description": "Navigation Through Windows", + "Description[ar]": "التنقل بين النوافذ", + "Description[az]": "Pəncərələr arası naviqasiya", + "Description[be]": "Навігацыя па вокнах", + "Description[bg]": "Навигация през прозорците", + "Description[ca@valencia]": "Navegueu per les finestres", + "Description[ca]": "Navegació per les finestres", + "Description[cs]": "Navigace skrz okna", + "Description[da]": "Navigering gennem vinduer", + "Description[de]": "Zwischen Fenstern wechseln", + "Description[en_GB]": "Navigation Through Windows", + "Description[eo]": "Navigado Tra Fenestroj", + "Description[es]": "Navegación a través de las ventanas", + "Description[et]": "Akende vahel liikumine", + "Description[eu]": "Leihoen artean nabigatzea", + "Description[fi]": "Ikkunanavigointi", + "Description[fr]": "Navigation parmi les fenêtres", + "Description[gl]": "Navegación polas xanelas.", + "Description[he]": "ניווט בין חלונות", + "Description[hu]": "Navigáció az ablakok között", + "Description[ia]": "Navigation per fenestras", + "Description[id]": "Navigasi Melalui Jendela", + "Description[is]": "Flett á milli glugga", + "Description[it]": "Navigazione tra le finestre", + "Description[ja]": "ウィンドウ間を移動", + "Description[ka]": "ფანჯრებში ნავიგაცია", + "Description[ko]": "창간 탐색", + "Description[lt]": "Naršymas po langus", + "Description[lv]": "Pārvietoties cauri logiem", + "Description[nb]": "Bla gjennom vinduer", + "Description[nl]": "Navigatie door vensters", + "Description[nn]": "Bla gjennom vindauge", + "Description[pl]": "Przełączanie pomiędzy oknami", + "Description[pt]": "Navegação pelas Janelas", + "Description[pt_BR]": "Navegação pelas janelas", + "Description[ro]": "Navigare printre ferestre", + "Description[ru]": "Настройка переключателя окон", + "Description[sa]": "विण्डोजद्वारा नेविगेशनम्", + "Description[sk]": "Navigácia cez okná", + "Description[sl]": "Krmarjenje med okni", + "Description[sv]": "Navigering via fönster", + "Description[ta]": "சாளரங்களுக்கிடையே உலாவல்", + "Description[tr]": "Pencereler Arası Dolaşım", + "Description[uk]": "Навігація вікнами", + "Description[vi]": "Điều hướng qua các cửa sổ", + "Description[zh_CN]": "配置窗口切换器选项", + "Description[zh_TW]": "在視窗間探索", + "Icon": "preferences-system-tabbox", + "Name": "Task Switcher", + "Name[ar]": "مبدّل المهام", + "Name[az]": "Tapşırıq dəyişdirici", + "Name[be]": "Сродак пераключэння задач", + "Name[bg]": "Превключване на задачи", + "Name[ca@valencia]": "Commutador de tasques", + "Name[ca]": "Commutador de tasques", + "Name[cs]": "Přepínač úloh", + "Name[da]": "Opgaveskifter", + "Name[de]": "Anwendungsumschalter", + "Name[en_GB]": "Task Switcher", + "Name[eo]": "Taskŝanĝilo", + "Name[es]": "Selector de tareas", + "Name[et]": "Ülesannete vahetaja", + "Name[eu]": "Ataza-trukatzailea", + "Name[fi]": "Tehtävävaihto", + "Name[fr]": "Sélecteur de tâches", + "Name[gl]": "Selector de tarefa", + "Name[he]": "מחליף משימות", + "Name[hu]": "Feladatváltó", + "Name[ia]": "Commutator de carga", + "Name[id]": "Pengalih Tugas", + "Name[is]": "Gluggaskiptir", + "Name[it]": "Selettore delle attività", + "Name[ja]": "タスクスイッチャー", + "Name[ka]": "ამოცანების გადამრთველი", + "Name[ko]": "작업 전환기", + "Name[lt]": "Užduočių perjungiklis", + "Name[lv]": "Uzdevumu pārslēdzējs", + "Name[nb]": "Oppgavebytter", + "Name[nl]": "Taakschakelaar", + "Name[nn]": "Oppgåve­vekslar", + "Name[pl]": "Przełącznik zadań", + "Name[pt]": "Selector de Tarefas", + "Name[pt_BR]": "Seletor de tarefas", + "Name[ro]": "Comutator de sarcini", + "Name[ru]": "Переключение окон", + "Name[sa]": "कार्य स्विचर", + "Name[sk]": "Prepínač úloh", + "Name[sl]": "Preklopnik opravil", + "Name[sv]": "Aktivitetsbyte", + "Name[ta]": "பணி மாற்றி", + "Name[tr]": "Görev Değiştiricisi", + "Name[uk]": "Перемикання задач", + "Name[vi]": "Trình chuyển tác vụ", + "Name[zh_CN]": "任务切换器", + "Name[zh_TW]": "工作切換器" + }, + "X-DocPath": "kcontrol/kwintabbox/index.html", + "X-KDE-Keywords": "window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab,apps changer,application changer,app,task,switch,task switcher", + "X-KDE-Keywords[ar]": "نافذة,نوافذ,مبدل,مبدل نوافذ,تاب,ألت تاب,مغير التطبيقات,مهمة,مهام", + "X-KDE-Keywords[bg]": "прозорец,прозорци,превключвател,превключвател на прозорци,превключване,превключване на прозорци,alttab,alt-tab,alt+tab,alt раздел,смяна на приложения,смяна на приложения,приложение,задача,превключвател,превключвател на задачи", + "X-KDE-Keywords[ca@valencia]": "finestra,finestres,commutador,commutador de finestres,commutació,commutació de finestres,alttab,alt-tab,alt+tab,alt tab,canviador d'aplicacions,canvi d'aplicacions,app,aplicació,tasca,commutador,commutador de tasques", + "X-KDE-Keywords[ca]": "finestra,finestres,commutador,commutador de finestres,commutació,commutació de finestres,alttab,alt-tab,alt+tab,alt tab,canviador d'aplicacions,canvi d'aplicacions,app,aplicació,tasca,commutador,commutador de tasques", + "X-KDE-Keywords[de]": "fenster,umschalter,fensterumschalter,alttab,alt-tab,alt+tab,alt tab,Appwechsler,Anwendungswechsler,Anwendung,Aufgabe,Aufgabenumschalter", + "X-KDE-Keywords[en_GB]": "window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab,apps changer,application changer,app,task,switch,task switcher", + "X-KDE-Keywords[es]": "ventana,ventanas,cambiador,selector,selector de ventana,cambiador de ventana,cambio,selección,cambio de ventana,alttab,alt-tab,alt+tab,alt tab,cambiador de aplicaciones,selector de aplicaciones,app,tarea,cambiador de tareas,selector de tareas", + "X-KDE-Keywords[eu]": "leihoa,leihoak,aldarazlea,leiho aldarazlea,aldatzea,leihoa aldatzea,alttab,alt-tab,alt+tab,alt tab,aplikazio aldarazlea,aplikazioa,ataza,aldatu,ataza aldarazlea", + "X-KDE-Keywords[fi]": "ikkuna,ikkunat,vaihto,vaihtaja,vaihtaminen,vaihda,ikkunavaihto,alttab,alt-tab,alt+tab,alt tab,tehtävävaihto,sovellusvaihto,ohjelmavaihto", + "X-KDE-Keywords[fr]": "fenêtre, fenêtres, commutateur, commutateur de fenêtre, changement, changement de fenêtre, alt-tab, alt tab, alt+tab, changement d'applications, changement d'application, commutateur d'applications, basculer, tâche, commutateur, sélecteur de tâches", + "X-KDE-Keywords[gl]": "xanela,xanelas,alternador,cambiar,trocar de xanela,alttab,alt-tab,alt+tab,alt tab,apps changer,cambiador de aplicacións,application changer,app,aplicación,apli,task,tarefa,traballo,switch,cambiar,task switcher,cambiador de tarefa,cambiador de traballo,trocador,trocador de tarefa,trocador de traballo", + "X-KDE-Keywords[he]": "חלון,חלונות,בורר,בורר חלונות,בוחר חלונות,החלפה,מעבר,העברה,החלפת חלונות,מעבר חלונות,alttab,alt-tab,alt+tab,alt tab,בורר יישומים,מחליף יישומים,יישום,משימה,החלפה,בורר משימות", + "X-KDE-Keywords[hu]": "ablak,ablakok,váltó,ablakváltó,váltás,ablakváltás,alttab,alt-tab,alt+tab,alt tab,alkalmazásáváltó,alkalmazás,feladat,vált,feladatváltó", + "X-KDE-Keywords[ia]": "fenestra,fenestras,commutator,commutator de fenestra,commutar,commutar fenestra,alttab,alt-tab,alt+tab,alt tab, modificator de application, app, carga, commuta, commutator de carga", + "X-KDE-Keywords[is]": "gluggi,gluggar,skiptir,gluggaskiptir,skipting,gluggaskipting,alttab,alt-tab,alt+tab,alt tab,forritabreytir,forrit,verkefni,skipta,verkefnaskiptir", + "X-KDE-Keywords[it]": "finestra,finestre,selettore,selettore delle finestre,selezione,selezione delle finestre,alttab,alt-tab,alt+tab,alt tab,selettore delle applicazioni,cambio applicazione,applicazione,attività,selettore delle attività", + "X-KDE-Keywords[ja]": "window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab,apps changer,application changer,app,task,switch,task switcher,ウィンドウ,スイッチャー,ウィンドウスイッチャー,切り替え,ウィンドウの切り替え,アプリケーションスイッチャー,アプリの切り替え,タスク,タスクスイッチャー,タスクの切り替え", + "X-KDE-Keywords[ka]": "window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab,apps changer,application changer,app,task,switch,task switcher,ფანჯარა,ფანჯრები,ფანჯრის გადართვა,გადართვა,აპები,ამოცანა,ამოცანის გადართვა", + "X-KDE-Keywords[ko]": "창,창 전환,전환기,앱 전환,프로그램 전환,작업,전환,작업 전환기", + "X-KDE-Keywords[lt]": "window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab,apps changer,application changer,app,task,switch,task switcher,langas,langai,langų,langu,lango,perjungiklis,langų perjungiklis,langu perjungiklis,perjungimas,langų perjungimas,langu perjungimas,lango perjungimas,programų keitiklis,programu keitiklis,programa,užduotis,uzduotis,perjungti,užduočių perjungiklis,uzduociu perjungiklis", + "X-KDE-Keywords[lv]": "logs,logi,pārslēdzējs,logu pārslēdzējs,pārslēgšana,logu pārslēgšana,alttab,alt-tab,alt+tab,alt tab,programmu maiņa,programmu mainītājs,lietotne,uzdevums,pārslēgt,uzdevumu pārslēdzējs", + "X-KDE-Keywords[nb]": "vindu,vinduer,bytte,vindusbytter,bytte,vindusbytter,alttab,alt-tab,alt+tab,alt tab,appbytter,programbytter,appveksler,programveksler,app,oppgave,bytte,veksle,oppgavebytter,oppgaveveksler", + "X-KDE-Keywords[nl]": "venster,vensters,wisselaar,vensterwisselaar,omschakelen,wisselen van venter,alttab,alt-tab,alt+tab,alt tab,wisselaar van toepassing,app,taak,schakelaar,takenwisselaar", + "X-KDE-Keywords[nn]": "vindauge,vindauge,byte,vindaugsbytar,byte,vindaugsbytar,alttab,alt-tab,alt+tab,alt tab,appbytar,programbytar,appvekslar,programvekslar,app,oppgåve,byte,veksla,oppgåvebytar,oppgåvevekslar", + "X-KDE-Keywords[pl]": "okno,okna,przełącznik,przełącznik okien,przełączanie,przełączanie okien,alttab,alt-tab,alt+tab,alt tab,zmieniacz apek,zmieniacz aplikacji,aplikacja,zadanie,przełącz,przełącznik zadań", + "X-KDE-Keywords[pt_BR]": "janela,janelas,trocador de janela,trocador de janelas,troca,troca de janela,alttab,alt-tab,alt+tab,alt tab,alterador de aplicativos,alterador de aplicação,aplicativo,tarefa,trocar,trocador de tarefas", + "X-KDE-Keywords[ro]": "fereastră,ferestre,comutator,schimbător,comutator ferestre,schimbare,comutare,schimbarea ferestrelor,alttab,alt-tab,alt+tab,alt tab,schimbare aplicații,schimbător de aplicații,aplicație,sarcină,schimbă,schimbător de sarcini,comutator de sarcini", + "X-KDE-Keywords[ru]": "window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab,apps changer,application changer,app,task,switch,task switcher,окно,окна,переключатель,переключатель окон,переключение,переключение окон,переключатель приложений,приложение,задача,переключить,переключатель задач", + "X-KDE-Keywords[sa]": "विंडो,विंडोज,स्विचर,विंडो स्विचर,स्विचिंग,विंडो स्विचिंग,alttab,alt-टैब,alt+टैब,alt टैब,एप्स चेंजर,एप्लिकेशन चेंजर,एप्लिकेशन,कार्य,स्विच,कार्य स्विचर", + "X-KDE-Keywords[sk]": "window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab,apps changer,application changer,app,task,switch,task switcher", + "X-KDE-Keywords[sl]": "okno,okna,preklopnik,preklopnik oken,preklapljanje,preklop oken,alttab,alt-tab,alt+tab,alt zavihek,izmenjevalec aplikacij,izmenjevalnik aplikacij, aplikacija,opravilo,stikalo,preklopnik opravil", + "X-KDE-Keywords[sv]": "fönster,byte,fönsterbyte,alttabulator,alt-tabulator,alt+tabulator,alt tabulator,programbytare,program,aktivitet,aktivitetsbyte", + "X-KDE-Keywords[tr]": "pencere,pencereler,değiştirici,pencere değiştiricisi,değiştirme,alttab,alt-tab,alt+tab,alt tab, uygulama değiştiricisi,uygulama,app,görev,değiştir,görev değiştiricisi,commandtab,command-tab,command+tab,command tab", + "X-KDE-Keywords[uk]": "window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab,apps changer,application changer,app,task,switch,task switcher,вікно,вікна,перемикач,перемикач вікон,перемикання,перемикання вікон,альттаб,альт-таб,альт+таб,зміна програм,програма,завдання,перемикання,перемикання завдань", + "X-KDE-Keywords[zh_CN]": "window,windows,switcher,window switcher,switching,window switching,alttab,alt-tab,alt+tab,alt tab,apps changer,application changer,app,task,switch,task switcher,chuangkou,qiehuanqi,chuangkouqiehuanqi,qiehuan,chuangkouqiehuan,yingyongqiehuanqi,yingyongchengxuqiehuanqi,yingyong,yingyongchengxu,renwu,renwuqiehuanqi,窗口,切换器,窗口切换器,切换,窗口切换,应用切换器,应用程序切换器,应用,应用程序,任务,任务切换器", + "X-KDE-Keywords[zh_TW]": "視窗,切換器,切換,視窗切換,alttab,應用程式切換,應用程式,程式,工作,工作切換", + "X-KDE-OnlyShowOnQtPlatforms": [ + "wayland" + ], + "X-KDE-System-Settings-Parent-Category": "windowmanagement", + "X-KDE-Weight": 20 +} diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/kwinswitcheffectsettings.kcfg b/local/recipes/kde/kwin/source/src/kcms/tabbox/kwinswitcheffectsettings.kcfg new file mode 100644 index 0000000000..ac7450b3c7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/kwinswitcheffectsettings.kcfg @@ -0,0 +1,17 @@ + + + + + + + + false + + + false + + + diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/kwinswitcheffectsettings.kcfgc b/local/recipes/kde/kwin/source/src/kcms/tabbox/kwinswitcheffectsettings.kcfgc new file mode 100644 index 0000000000..3ee296287f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/kwinswitcheffectsettings.kcfgc @@ -0,0 +1,6 @@ +File=kwinswitcheffectsettings.kcfg +NameSpace=KWin::TabBox +ClassName=SwitchEffectSettings +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/kwinswitcher.knsrc b/local/recipes/kde/kwin/source/src/kcms/tabbox/kwinswitcher.knsrc new file mode 100644 index 0000000000..30ea502998 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/kwinswitcher.knsrc @@ -0,0 +1,50 @@ +[KNewStuff3] +Name=Task Switcher Styles +Name[ar]=أنماط مبدّل المهام +Name[be]=Стылі для сродку пераключэння задач +Name[bg]=Стилове на превключване на задачи +Name[ca]=Estils del commutador de tasques +Name[ca@valencia]=Estils del commutador de tasques +Name[cs]=Styly přepínače úloh +Name[da]=Opgaveskifterstile +Name[de]=Anwendungsumschalter-Stile +Name[el]=Θέματα εναλλάκτη εργασιών +Name[en_GB]=Task Switcher Styles +Name[eo]=Stiloj de Task-Ŝanĝilo +Name[es]=Estilos del selector de tareas +Name[eu]=Ataza-aldarazlearen estiloak +Name[fi]=Tehtävävaihtotyylit +Name[fr]=Styles du commutateur de tâches +Name[gl]=Estilos de selector de tarefa +Name[he]=סגנונות מחליף משימות +Name[hu]=Feladatváltó stílusok +Name[ia]=Stilos de Commutator de carga +Name[is]=Stílsnið fyrir gluggaskipti +Name[it]=Stile del selettore delle attività +Name[ka]=ამოცანების გადამრთველის სტილები +Name[ko]=작업 전환기 스타일 +Name[lt]=Užduočių perjungiklio stiliai +Name[lv]=Uzdevumu pārslēdzēja stili +Name[nb]=Oppgavebytter-stiler +Name[nl]=Stijlen van taakschakelaar +Name[nn]=Oppgåvevekslar-stilar +Name[pl]=Wyglądy przełącznika zadań +Name[pt_BR]=Estilos do seletor de tarefas +Name[ro]=Stiluri pentru comutatorul de sarcini +Name[ru]=Стили переключения окон +Name[sa]=कार्य स्विचर शैल्याः +Name[sk]=Štýly prepínača úloh +Name[sl]=Slogi preklopa med opravili +Name[sv]=Stilar för aktivitetsbyte +Name[ta]=பணி மாற்றி வகைகள் +Name[tr]=Görev Değiştiricisi Biçemleri +Name[uk]=Стилі перемикання задач +Name[zh_CN]=任务切换器样式 +Name[zh_TW]=工作切換器風格 + +ProvidersUrl=https://autoconfig.kde.org/ocs/providers.xml +ContentWarning=Executables +Categories=Kwin Switching Layouts Plasma 6 +StandardResource=tmp +Uncompress=kpackage +KPackageStructure=KWin/WindowSwitcher diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxconfigform.cpp b/local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxconfigform.cpp new file mode 100644 index 0000000000..47c3eef841 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxconfigform.cpp @@ -0,0 +1,394 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + SPDX-FileCopyrightText: 2020 Cyril Rossi + SPDX-FileCopyrightText: 2023 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwintabboxconfigform.h" +#include "kwintabboxsettings.h" +#include "shortcutsettings.h" +#include "ui_main.h" + +namespace KWin +{ + +using namespace TabBox; + +KWinTabBoxConfigForm::KWinTabBoxConfigForm(TabboxType type, TabBoxSettings *config, ShortcutSettings *shortcutsConfig, QWidget *parent) + : QWidget(parent) + , m_config(config) + , m_shortcuts(shortcutsConfig) + , ui(new Ui::KWinTabBoxConfigForm) +{ + ui->setupUi(this); + + if (QApplication::screens().count() < 2) { + ui->filterScreens->hide(); + ui->screenFilter->hide(); + } + + connect(this, &KWinTabBoxConfigForm::configChanged, this, &KWinTabBoxConfigForm::updateDefaultIndicators); + + connect(ui->effectPreviewButton, &QPushButton::clicked, this, &KWinTabBoxConfigForm::effectPreviewClicked); + + connect(ui->filterScreens, &QAbstractButton::clicked, this, &KWinTabBoxConfigForm::onFilterScreen); + connect(ui->currentScreen, &QAbstractButton::clicked, this, &KWinTabBoxConfigForm::onFilterScreen); + connect(ui->otherScreens, &QAbstractButton::clicked, this, &KWinTabBoxConfigForm::onFilterScreen); + + connect(ui->filterDesktops, &QAbstractButton::clicked, this, &KWinTabBoxConfigForm::onFilterDesktop); + connect(ui->currentDesktop, &QAbstractButton::clicked, this, &KWinTabBoxConfigForm::onFilterDesktop); + connect(ui->otherDesktops, &QAbstractButton::clicked, this, &KWinTabBoxConfigForm::onFilterDesktop); + + connect(ui->filterActivities, &QAbstractButton::clicked, this, &KWinTabBoxConfigForm::onFilterActivites); + connect(ui->currentActivity, &QAbstractButton::clicked, this, &KWinTabBoxConfigForm::onFilterActivites); + connect(ui->otherActivities, &QAbstractButton::clicked, this, &KWinTabBoxConfigForm::onFilterActivites); + + connect(ui->filterMinimization, &QAbstractButton::clicked, this, &KWinTabBoxConfigForm::onFilterMinimization); + connect(ui->visibleWindows, &QAbstractButton::clicked, this, &KWinTabBoxConfigForm::onFilterMinimization); + connect(ui->hiddenWindows, &QAbstractButton::clicked, this, &KWinTabBoxConfigForm::onFilterMinimization); + + connect(ui->oneAppWindow, &QAbstractButton::clicked, this, &KWinTabBoxConfigForm::onApplicationMode); + connect(ui->orderMinimized, &QAbstractButton::clicked, this, &KWinTabBoxConfigForm::onOrderMinimizedMode); + connect(ui->showDesktop, &QAbstractButton::clicked, this, &KWinTabBoxConfigForm::onShowDesktopMode); + + connect(ui->switchingModeCombo, &QComboBox::activated, this, &KWinTabBoxConfigForm::onSwitchingMode); + connect(ui->effectCombo, &QComboBox::activated, this, &KWinTabBoxConfigForm::onEffectCombo); + + auto initShortcutWidget = [this](KKeySequenceWidget *primary, KKeySequenceWidget *alternate, const QString &name) { + primary->setCheckActionCollections({m_shortcuts->actionCollection()}); + primary->setProperty("shortcutAction", name); + connect(primary, &KKeySequenceWidget::keySequenceChanged, this, [this, name](const QKeySequence &seq) { + if (m_shortcuts->primaryShortcut(name) != seq) { + m_shortcuts->setShortcuts(name, {seq, m_shortcuts->alternateShortcut(name)}); + Q_EMIT configChanged(); + } + }); + + alternate->setCheckActionCollections({m_shortcuts->actionCollection()}); + alternate->setProperty("shortcutAction", name); + connect(alternate, &KKeySequenceWidget::keySequenceChanged, this, [this, name](const QKeySequence &seq) { + if (m_shortcuts->alternateShortcut(name) != seq) { + m_shortcuts->setShortcuts(name, {m_shortcuts->primaryShortcut(name), seq}); + Q_EMIT configChanged(); + } + }); + }; + + if (TabboxType::Main == type) { + initShortcutWidget(ui->scAll, ui->scAllAlternate, QStringLiteral("Walk Through Windows")); + initShortcutWidget(ui->scAllReverse, ui->scAllReverseAlternate, QStringLiteral("Walk Through Windows (Reverse)")); + initShortcutWidget(ui->scCurrent, ui->scCurrentAlternate, QStringLiteral("Walk Through Windows of Current Application")); + initShortcutWidget(ui->scCurrentReverse, ui->scCurrentReverseAlternate, QStringLiteral("Walk Through Windows of Current Application (Reverse)")); + } else if (TabboxType::Alternative == type) { + initShortcutWidget(ui->scAll, ui->scAllAlternate, QStringLiteral("Walk Through Windows Alternative")); + initShortcutWidget(ui->scAllReverse, ui->scAllReverseAlternate, QStringLiteral("Walk Through Windows Alternative (Reverse)")); + initShortcutWidget(ui->scCurrent, ui->scCurrentAlternate, QStringLiteral("Walk Through Windows of Current Application Alternative")); + initShortcutWidget(ui->scCurrentReverse, ui->scCurrentReverseAlternate, QStringLiteral("Walk Through Windows of Current Application Alternative (Reverse)")); + } + + updateUiFromConfig(); +} + +KWinTabBoxConfigForm::~KWinTabBoxConfigForm() +{ + delete ui; +} + +TabBoxSettings *KWinTabBoxConfigForm::config() const +{ + return m_config; +} + +bool KWinTabBoxConfigForm::highlightWindows() const +{ + return ui->kcfg_HighlightWindows->isChecked(); +} + +bool KWinTabBoxConfigForm::showTabBox() const +{ + return ui->kcfg_ShowTabBox->isChecked(); +} + +int KWinTabBoxConfigForm::filterScreen() const +{ + if (ui->filterScreens->isChecked()) { + return ui->currentScreen->isChecked() ? TabBoxConfig::OnlyCurrentScreenClients : TabBoxConfig::ExcludeCurrentScreenClients; + } else { + return TabBoxConfig::IgnoreMultiScreen; + } +} + +int KWinTabBoxConfigForm::filterDesktop() const +{ + if (ui->filterDesktops->isChecked()) { + return ui->currentDesktop->isChecked() ? TabBoxConfig::OnlyCurrentDesktopClients : TabBoxConfig::ExcludeCurrentDesktopClients; + } else { + return TabBoxConfig::AllDesktopsClients; + } +} + +int KWinTabBoxConfigForm::filterActivities() const +{ + if (ui->filterActivities->isChecked()) { + return ui->currentActivity->isChecked() ? TabBoxConfig::OnlyCurrentActivityClients : TabBoxConfig::ExcludeCurrentActivityClients; + } else { + return TabBoxConfig::AllActivitiesClients; + } +} + +int KWinTabBoxConfigForm::filterMinimization() const +{ + if (ui->filterMinimization->isChecked()) { + return ui->visibleWindows->isChecked() ? TabBoxConfig::ExcludeMinimizedClients : TabBoxConfig::OnlyMinimizedClients; + } else { + return TabBoxConfig::IgnoreMinimizedStatus; + } +} + +int KWinTabBoxConfigForm::applicationMode() const +{ + return ui->oneAppWindow->isChecked() ? TabBoxConfig::OneWindowPerApplication : TabBoxConfig::AllWindowsAllApplications; +} + +int KWinTabBoxConfigForm::orderMinimizedMode() const +{ + return ui->orderMinimized->isChecked() ? TabBoxConfig::GroupByMinimized : TabBoxConfig::NoGroupByMinimized; +} + +int KWinTabBoxConfigForm::showDesktopMode() const +{ + return ui->showDesktop->isChecked() ? TabBoxConfig::ShowDesktopClient : TabBoxConfig::DoNotShowDesktopClient; +} + +int KWinTabBoxConfigForm::switchingMode() const +{ + return ui->switchingModeCombo->currentIndex(); +} + +QString KWinTabBoxConfigForm::layoutName() const +{ + return ui->effectCombo->currentData().toString(); +} + +void KWinTabBoxConfigForm::setFilterScreen(TabBox::TabBoxConfig::ClientMultiScreenMode mode) +{ + ui->filterScreens->setChecked(mode != TabBoxConfig::IgnoreMultiScreen); + ui->currentScreen->setChecked(mode == TabBoxConfig::OnlyCurrentScreenClients); + ui->otherScreens->setChecked(mode == TabBoxConfig::ExcludeCurrentScreenClients); +} + +void KWinTabBoxConfigForm::setFilterDesktop(TabBox::TabBoxConfig::ClientDesktopMode mode) +{ + ui->filterDesktops->setChecked(mode != TabBoxConfig::AllDesktopsClients); + ui->currentDesktop->setChecked(mode == TabBoxConfig::OnlyCurrentDesktopClients); + ui->otherDesktops->setChecked(mode == TabBoxConfig::ExcludeCurrentDesktopClients); +} + +void KWinTabBoxConfigForm::setFilterActivities(TabBox::TabBoxConfig::ClientActivitiesMode mode) +{ + ui->filterActivities->setChecked(mode != TabBoxConfig::AllActivitiesClients); + ui->currentActivity->setChecked(mode == TabBoxConfig::OnlyCurrentActivityClients); + ui->otherActivities->setChecked(mode == TabBoxConfig::ExcludeCurrentActivityClients); +} + +void KWinTabBoxConfigForm::setFilterMinimization(TabBox::TabBoxConfig::ClientMinimizedMode mode) +{ + ui->filterMinimization->setChecked(mode != TabBoxConfig::IgnoreMinimizedStatus); + ui->visibleWindows->setChecked(mode == TabBoxConfig::ExcludeMinimizedClients); + ui->hiddenWindows->setChecked(mode == TabBoxConfig::OnlyMinimizedClients); +} + +void KWinTabBoxConfigForm::setApplicationMode(TabBox::TabBoxConfig::ClientApplicationsMode mode) +{ + ui->oneAppWindow->setChecked(mode == TabBoxConfig::OneWindowPerApplication); +} + +void KWinTabBoxConfigForm::setOrderMinimizedMode(TabBox::TabBoxConfig::OrderMinimizedMode mode) +{ + ui->orderMinimized->setChecked(mode == TabBoxConfig::GroupByMinimized); +} + +void KWinTabBoxConfigForm::setShowDesktopMode(TabBox::TabBoxConfig::ShowDesktopMode mode) +{ + ui->showDesktop->setChecked(mode == TabBoxConfig::ShowDesktopClient); +} + +void KWinTabBoxConfigForm::setSwitchingModeChanged(TabBox::TabBoxConfig::ClientSwitchingMode mode) +{ + ui->switchingModeCombo->setCurrentIndex(mode); +} + +void KWinTabBoxConfigForm::setLayoutName(const QString &layoutName) +{ + const int index = ui->effectCombo->findData(layoutName); + if (index >= 0) { + ui->effectCombo->setCurrentIndex(index); + } +} + +void KWinTabBoxConfigForm::setEffectComboModel(QStandardItemModel *model) +{ + // We don't want to lose the config layout when resetting the combo model + const QString layout = m_config->layoutName(); + ui->effectCombo->setModel(model); + setLayoutName(layout); +} + +QVariant KWinTabBoxConfigForm::effectComboCurrentData(int role) const +{ + return ui->effectCombo->currentData(role); +} + +void KWinTabBoxConfigForm::onFilterScreen() +{ + m_config->setMultiScreenMode(filterScreen()); + Q_EMIT configChanged(); +} + +void KWinTabBoxConfigForm::onFilterDesktop() +{ + m_config->setDesktopMode(filterDesktop()); + Q_EMIT configChanged(); +} + +void KWinTabBoxConfigForm::onFilterActivites() +{ + m_config->setActivitiesMode(filterActivities()); + Q_EMIT configChanged(); +} + +void KWinTabBoxConfigForm::onFilterMinimization() +{ + m_config->setMinimizedMode(filterMinimization()); + Q_EMIT configChanged(); +} + +void KWin::KWinTabBoxConfigForm::onApplicationMode() +{ + m_config->setApplicationsMode(applicationMode()); + Q_EMIT configChanged(); +} + +void KWinTabBoxConfigForm::onOrderMinimizedMode() +{ + m_config->setOrderMinimizedMode(orderMinimizedMode()); + Q_EMIT configChanged(); +} + +void KWinTabBoxConfigForm::onShowDesktopMode() +{ + m_config->setShowDesktopMode(showDesktopMode()); + Q_EMIT configChanged(); +} + +void KWinTabBoxConfigForm::onSwitchingMode() +{ + m_config->setSwitchingMode(switchingMode()); + Q_EMIT configChanged(); +} + +void KWinTabBoxConfigForm::onEffectCombo() +{ + if (!ui->kcfg_ShowTabBox->isChecked()) { + return; + } + + m_config->setLayoutName(layoutName()); + Q_EMIT configChanged(); +} + +void KWinTabBoxConfigForm::updateUiFromConfig() +{ + setFilterScreen(static_cast(m_config->multiScreenMode())); + setFilterDesktop(static_cast(m_config->desktopMode())); + setFilterActivities(static_cast(m_config->activitiesMode())); + setFilterMinimization(static_cast(m_config->minimizedMode())); + setApplicationMode(static_cast(m_config->applicationsMode())); + setOrderMinimizedMode(static_cast(m_config->orderMinimizedMode())); + setShowDesktopMode(static_cast(m_config->showDesktopMode())); + setSwitchingModeChanged(static_cast(m_config->switchingMode())); + setLayoutName(m_config->layoutName()); + + for (const auto &widget : {ui->scAll, ui->scAllReverse, ui->scCurrent, ui->scCurrentReverse}) { + const QString actionName = widget->property("shortcutAction").toString(); + widget->setKeySequence(m_shortcuts->primaryShortcut(actionName)); + } + for (const auto &widget : {ui->scAllAlternate, ui->scAllReverseAlternate, ui->scCurrentAlternate, ui->scCurrentReverseAlternate}) { + const QString actionName = widget->property("shortcutAction").toString(); + widget->setKeySequence(m_shortcuts->alternateShortcut(actionName)); + } + + updateDefaultIndicators(); +} + +void KWinTabBoxConfigForm::setEnabledUi() +{ + ui->kcfg_HighlightWindows->setEnabled(!m_config->isHighlightWindowsImmutable()); + + ui->filterScreens->setEnabled(!m_config->isMultiScreenModeImmutable()); + ui->currentScreen->setEnabled(!m_config->isMultiScreenModeImmutable()); + ui->otherScreens->setEnabled(!m_config->isMultiScreenModeImmutable()); + + ui->filterDesktops->setEnabled(!m_config->isDesktopModeImmutable()); + ui->currentDesktop->setEnabled(!m_config->isDesktopModeImmutable()); + ui->otherDesktops->setEnabled(!m_config->isDesktopModeImmutable()); + + ui->filterActivities->setEnabled(!m_config->isActivitiesModeImmutable()); + ui->currentActivity->setEnabled(!m_config->isActivitiesModeImmutable()); + ui->otherActivities->setEnabled(!m_config->isActivitiesModeImmutable()); + + ui->filterMinimization->setEnabled(!m_config->isMinimizedModeImmutable()); + ui->visibleWindows->setEnabled(!m_config->isMinimizedModeImmutable()); + ui->hiddenWindows->setEnabled(!m_config->isMinimizedModeImmutable()); + + ui->oneAppWindow->setEnabled(!m_config->isApplicationsModeImmutable()); + ui->orderMinimized->setEnabled(!m_config->isOrderMinimizedModeImmutable()); + ui->showDesktop->setEnabled(!m_config->isShowDesktopModeImmutable()); + ui->switchingModeCombo->setEnabled(!m_config->isSwitchingModeImmutable()); + ui->effectCombo->setEnabled(!m_config->isLayoutNameImmutable()); +} + +void KWinTabBoxConfigForm::setDefaultIndicatorVisible(bool show) +{ + m_showDefaultIndicator = show; + updateDefaultIndicators(); +} + +void KWinTabBoxConfigForm::updateDefaultIndicators() +{ + applyDefaultIndicator({ui->filterScreens, ui->currentScreen, ui->otherScreens}, + m_config->multiScreenMode() == m_config->defaultMultiScreenModeValue()); + applyDefaultIndicator({ui->filterDesktops, ui->currentDesktop, ui->otherDesktops}, + m_config->desktopMode() == m_config->defaultDesktopModeValue()); + applyDefaultIndicator({ui->filterActivities, ui->currentActivity, ui->otherActivities}, + m_config->activitiesMode() == m_config->defaultActivitiesModeValue()); + applyDefaultIndicator({ui->filterMinimization, ui->visibleWindows, ui->hiddenWindows}, + m_config->minimizedMode() == m_config->defaultMinimizedModeValue()); + applyDefaultIndicator({ui->oneAppWindow}, m_config->applicationsMode() == m_config->defaultApplicationsModeValue()); + applyDefaultIndicator({ui->orderMinimized}, m_config->orderMinimizedMode() == m_config->defaultOrderMinimizedModeValue()); + applyDefaultIndicator({ui->showDesktop}, m_config->showDesktopMode() == m_config->defaultShowDesktopModeValue()); + applyDefaultIndicator({ui->switchingModeCombo}, m_config->switchingMode() == m_config->defaultSwitchingModeValue()); + applyDefaultIndicator({ui->effectCombo}, m_config->layoutName() == m_config->defaultLayoutNameValue()); + + for (const auto &widget : {ui->scAll, ui->scAllAlternate, ui->scAllReverse, ui->scAllReverseAlternate, ui->scCurrent, ui->scCurrentAlternate, ui->scCurrentReverse, ui->scCurrentReverseAlternate}) { + const QString actionName = widget->property("shortcutAction").toString(); + applyDefaultIndicator({widget}, m_shortcuts->isDefault(actionName)); + } +} + +void KWinTabBoxConfigForm::applyDefaultIndicator(QList widgets, bool isDefault) +{ + for (auto widget : widgets) { + widget->setProperty("_kde_highlight_neutral", m_showDefaultIndicator && !isDefault); + widget->update(); + } +} + +} // namespace + +#include "moc_kwintabboxconfigform.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxconfigform.h b/local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxconfigform.h new file mode 100644 index 0000000000..a6ea79d5d0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxconfigform.h @@ -0,0 +1,111 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + SPDX-FileCopyrightText: 2020 Cyril Rossi + SPDX-FileCopyrightText: 2023 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include "tabbox/tabboxconfig.h" + +namespace Ui +{ +class KWinTabBoxConfigForm; +} + +namespace KWin +{ + +namespace TabBox +{ +class TabBoxSettings; +class ShortcutSettings; +} + +class KWinTabBoxConfigForm : public QWidget +{ + Q_OBJECT + +public: + enum class TabboxType { + Main, + Alternative, + }; + + enum EffectComboRole { + LayoutPath = Qt::UserRole + 1, + }; + + explicit KWinTabBoxConfigForm(TabboxType type, TabBox::TabBoxSettings *config, TabBox::ShortcutSettings *shortcutsConfig, QWidget *parent = nullptr); + ~KWinTabBoxConfigForm() override; + + TabBox::TabBoxSettings *config() const; + bool highlightWindows() const; + + void updateUiFromConfig(); + void setDefaultIndicatorVisible(bool visible); + + // EffectCombo Data Model + void setEffectComboModel(QStandardItemModel *model); + QVariant effectComboCurrentData(int role = Qt::UserRole) const; + +Q_SIGNALS: + void configChanged(); + void effectPreviewClicked(); + +private Q_SLOTS: + void onFilterScreen(); + void onFilterDesktop(); + void onFilterActivites(); + void onFilterMinimization(); + void onApplicationMode(); + void onOrderMinimizedMode(); + void onShowDesktopMode(); + void onSwitchingMode(); + void onEffectCombo(); + void updateDefaultIndicators(); + +private: + void setEnabledUi(); + void applyDefaultIndicator(QList widgets, bool visible); + + // UI property getters + bool showTabBox() const; + int filterScreen() const; + int filterDesktop() const; + int filterActivities() const; + int filterMinimization() const; + int applicationMode() const; + int orderMinimizedMode() const; + int showDesktopMode() const; + int switchingMode() const; + QString layoutName() const; + + // UI property setters + void setFilterScreen(TabBox::TabBoxConfig::ClientMultiScreenMode mode); + void setFilterDesktop(TabBox::TabBoxConfig::ClientDesktopMode mode); + void setFilterActivities(TabBox::TabBoxConfig::ClientActivitiesMode mode); + void setFilterMinimization(TabBox::TabBoxConfig::ClientMinimizedMode mode); + void setApplicationMode(TabBox::TabBoxConfig::ClientApplicationsMode mode); + void setOrderMinimizedMode(TabBox::TabBoxConfig::OrderMinimizedMode mode); + void setShowDesktopMode(TabBox::TabBoxConfig::ShowDesktopMode mode); + void setSwitchingModeChanged(TabBox::TabBoxConfig::ClientSwitchingMode mode); + void setLayoutName(const QString &layoutName); + +private: + TabBox::TabBoxSettings *m_config = nullptr; + TabBox::ShortcutSettings *m_shortcuts = nullptr; + bool m_showDefaultIndicator = false; + + Ui::KWinTabBoxConfigForm *ui; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxdata.cpp b/local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxdata.cpp new file mode 100644 index 0000000000..c8c7fd7a57 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxdata.cpp @@ -0,0 +1,49 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kwintabboxdata.h" + +#include "kwinswitcheffectsettings.h" +#include "kwintabboxsettings.h" +#include "shortcutsettings.h" + +namespace KWin +{ +namespace TabBox +{ + +KWinTabboxData::KWinTabboxData(QObject *parent) + : KCModuleData(parent) + , m_tabBoxConfig(new TabBoxSettings(QStringLiteral("TabBox"), this)) + , m_tabBoxAlternativeConfig(new TabBoxSettings(QStringLiteral("TabBoxAlternative"), this)) + , m_shortcutConfig(new ShortcutSettings(this)) +{ + registerSkeleton(m_tabBoxConfig); + registerSkeleton(m_tabBoxAlternativeConfig); + registerSkeleton(m_shortcutConfig); +} + +TabBoxSettings *KWinTabboxData::tabBoxConfig() const +{ + return m_tabBoxConfig; +} + +TabBoxSettings *KWinTabboxData::tabBoxAlternativeConfig() const +{ + return m_tabBoxAlternativeConfig; +} + +ShortcutSettings *KWinTabboxData::shortcutConfig() const +{ + return m_shortcutConfig; +} +} +} + +#include "moc_kwintabboxdata.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxdata.h b/local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxdata.h new file mode 100644 index 0000000000..89af34835d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxdata.h @@ -0,0 +1,43 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 Cyril Rossi + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +namespace KWin +{ +namespace TabBox +{ +class TabBoxSettings; +class SwitchEffectSettings; +class ShortcutSettings; + +class KWinTabboxData : public KCModuleData +{ + Q_OBJECT + +public: + explicit KWinTabboxData(QObject *parent); + + TabBoxSettings *tabBoxConfig() const; + TabBoxSettings *tabBoxAlternativeConfig() const; + ShortcutSettings *shortcutConfig() const; + +private: + TabBoxSettings *m_tabBoxConfig; + TabBoxSettings *m_tabBoxAlternativeConfig; + ShortcutSettings *m_shortcutConfig; +}; + +} + +} diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxsettings.kcfg b/local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxsettings.kcfg new file mode 100644 index 0000000000..7fe359ce87 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxsettings.kcfg @@ -0,0 +1,44 @@ + + + + + + + + TabBoxConfig::defaultDesktopMode() + + + TabBoxConfig::defaultActivitiesMode() + + + TabBoxConfig::defaultApplicationsMode() + + + TabBoxConfig::defaultOrderMinimizedMode() + + + TabBoxConfig::defaultMinimizedMode() + + + TabBoxConfig::defaultShowDesktopMode() + + + TabBoxConfig::defaultMultiScreenMode() + + + TabBoxConfig::defaultSwitchingMode() + + + TabBoxConfig::defaultLayoutName() + + + TabBoxConfig::defaultShowTabBox() + + + TabBoxConfig::defaultHighlightWindow() + + + diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxsettings.kcfgc b/local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxsettings.kcfgc new file mode 100644 index 0000000000..c1a82b53b7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/kwintabboxsettings.kcfgc @@ -0,0 +1,7 @@ +File=kwintabboxsettings.kcfg +NameSpace=KWin::TabBox +ClassName=TabBoxSettings +IncludeFiles=\"tabbox/tabboxconfig.h\" +Mutators=true +DefaultValueGetters=true +ParentInConstructor=true diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/layoutpreview.cpp b/local/recipes/kde/kwin/source/src/kcms/tabbox/layoutpreview.cpp new file mode 100644 index 0000000000..0718633df0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/layoutpreview.cpp @@ -0,0 +1,328 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2025 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +// own +#include "layoutpreview.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + QCoreApplication::setAttribute(Qt::AA_DisableSessionManager, true); + QGuiApplication app(argc, argv); + + auto parser = std::make_unique(); + parser->setApplicationDescription(i18n("Launch an interactive preview for a tabbox switcher")); + parser->addPositionalArgument(QStringLiteral("path"), i18n("Path to the Window Switcher QML main file")); + parser->addOption(QCommandLineOption(QStringLiteral("show-desktop"), i18n("Show also a thumbnail for the desktop"))); + parser->addHelpOption(); + + parser->process(app); + + if (parser->positionalArguments().isEmpty()) { + parser->showHelp(-1); + } + + const QString path = parser->positionalArguments().first(); + const bool showDesktop = parser->isSet(QStringLiteral("show-desktop")); + + auto preview = new KWin::TabBox::LayoutPreview(path, showDesktop); + if (!preview->isLoaded()) { + return -1; + } + + QObject::connect(preview, &QObject::destroyed, [&app]() { + app.exit(); + }); + + return app.exec(); +} + +namespace KWin +{ +namespace TabBox +{ + +LayoutPreview::LayoutPreview(const QString &path, bool showDesktopThumbnail, QObject *parent) + : QObject(parent) + , m_item(nullptr) +{ + QQmlEngine *engine = new QQmlEngine(this); + engine->setProperty("_kirigamiTheme", QStringLiteral("KirigamiPlasmaStyle")); + KLocalization::setupLocalizedContext(engine); + QQmlComponent *component = new QQmlComponent(engine, this); + + qmlRegisterType("org.kde.kwin", 3, 0, "WindowThumbnail"); + qmlRegisterType("org.kde.kwin", 3, 0, "TabBoxSwitcher"); + qmlRegisterType("org.kde.kwin", 3, 0, "DesktopBackground"); + qmlRegisterAnonymousType("org.kde.kwin", 3); + + component->loadUrl(QUrl::fromLocalFile(path)); + if (component->isError()) { + qWarning() << "Error loading tabbox preview:" << component->errorString(); + return; + } + + QObject *item = component->create(); + + auto findSwitcher = [item]() -> SwitcherItem * { + if (!item) { + return nullptr; + } + if (SwitcherItem *i = qobject_cast(item)) { + return i; + } else if (QQuickWindow *w = qobject_cast(item)) { + return w->contentItem()->findChild(); + } + return item->findChild(); + }; + + if (SwitcherItem *switcher = findSwitcher()) { + m_item = switcher; + static_cast(switcher->model())->showDesktopThumbnail(showDesktopThumbnail); + switcher->setVisible(true); + } + + auto findWindow = [item]() -> QQuickWindow * { + if (!item) { + return nullptr; + } + if (QQuickWindow *w = qobject_cast(item)) { + return w; + } + return item->findChild(); + }; + + if (QQuickWindow *w = findWindow()) { + w->setKeyboardGrabEnabled(true); + w->installEventFilter(this); + } +} + +LayoutPreview::~LayoutPreview() +{ +} + +bool LayoutPreview::eventFilter(QObject *object, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + QKeyEvent *keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Escape || keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Space) { + object->deleteLater(); + deleteLater(); + } + if (m_item && keyEvent->key() == Qt::Key_Tab) { + m_item->incrementIndex(); + } + if (m_item && keyEvent->key() == Qt::Key_Backtab) { + m_item->decrementIndex(); + } + } else if (event->type() == QEvent::FocusOut) { + object->deleteLater(); + deleteLater(); + } + return QObject::eventFilter(object, event); +} + +bool LayoutPreview::isLoaded() const +{ + return m_item != nullptr; +} + +ExampleClientModel::ExampleClientModel(QObject *parent) + : QAbstractListModel(parent) +{ + init(); +} + +ExampleClientModel::~ExampleClientModel() +{ +} + +void ExampleClientModel::init() +{ + m_thumbnails << ThumbnailInfo{ + WindowThumbnailItem::Dolphin, + i18nc("The name of KDE's file manager in this language, if translated", "Dolphin"), + QStringLiteral("system-file-manager")}; + m_thumbnails << ThumbnailInfo{ + WindowThumbnailItem::Konqueror, + i18nc("The name of KDE's web browser in this language, if translated", "Konqueror"), + QStringLiteral("konqueror")}; + m_thumbnails << ThumbnailInfo{ + WindowThumbnailItem::KMail, + i18nc("The name of KDE's email client in this language, if translated", "KMail"), + QStringLiteral("kmail")}; + m_thumbnails << ThumbnailInfo{ + WindowThumbnailItem::Systemsettings, + i18nc("The name of KDE's System Settings app in this language, if translated", "System Settings"), + QStringLiteral("systemsettings")}; +} + +void ExampleClientModel::showDesktopThumbnail(bool showDesktop) +{ + const ThumbnailInfo desktopThumbnail = ThumbnailInfo{WindowThumbnailItem::Desktop, i18n("Peek at Desktop"), QStringLiteral("desktop")}; + const int desktopIndex = m_thumbnails.indexOf(desktopThumbnail); + if (showDesktop == (desktopIndex >= 0)) { + return; + } + + Q_EMIT beginResetModel(); + if (showDesktop) { + m_thumbnails << desktopThumbnail; + } else { + m_thumbnails.removeAt(desktopIndex); + } + Q_EMIT endResetModel(); +} + +QVariant ExampleClientModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= rowCount()) { + return QVariant(); + } + + const ThumbnailInfo &item = m_thumbnails.at(index.row()); + + switch (role) { + case Qt::DisplayRole: + case CaptionRole: + return item.caption; + case MinimizedRole: + return false; + case DesktopNameRole: + return i18nc("An example Desktop Name", "Desktop 1"); + case IconRole: + return item.icon; + case WindowIdRole: + return item.wId; + case CloseableRole: + return item.wId != WindowThumbnailItem::Desktop; + } + return QVariant(); +} + +QString ExampleClientModel::longestCaption() const +{ + QString caption; + for (const auto &item : m_thumbnails) { + if (item.caption.size() > caption.size()) { + caption = item.caption; + } + } + return caption; +} + +int ExampleClientModel::rowCount(const QModelIndex &parent) const +{ + return m_thumbnails.size(); +} + +QHash ExampleClientModel::roleNames() const +{ + return { + {CaptionRole, QByteArrayLiteral("caption")}, + {MinimizedRole, QByteArrayLiteral("minimized")}, + {DesktopNameRole, QByteArrayLiteral("desktopName")}, + {IconRole, QByteArrayLiteral("icon")}, + {WindowIdRole, QByteArrayLiteral("windowId")}, + {CloseableRole, QByteArrayLiteral("closeable")}, + }; +} + +SwitcherItem::SwitcherItem(QObject *parent) + : QObject(parent) + , m_model(new ExampleClientModel(this)) + , m_item(nullptr) + , m_currentIndex(0) + , m_visible(false) +{ +} + +SwitcherItem::~SwitcherItem() +{ +} + +void SwitcherItem::setVisible(bool visible) +{ + if (m_visible == visible) { + return; + } + m_visible = visible; + Q_EMIT visibleChanged(); +} + +void SwitcherItem::setItem(QObject *item) +{ + m_item = item; + Q_EMIT itemChanged(); +} + +void SwitcherItem::setCurrentIndex(int index) +{ + if (m_currentIndex == index) { + return; + } + m_currentIndex = index; + Q_EMIT currentIndexChanged(m_currentIndex); +} + +QRect SwitcherItem::screenGeometry() const +{ + const QScreen *primaryScreen = qApp->primaryScreen(); + return primaryScreen->geometry(); +} + +void SwitcherItem::incrementIndex() +{ + setCurrentIndex((m_currentIndex + 1) % m_model->rowCount()); +} + +void SwitcherItem::decrementIndex() +{ + int index = m_currentIndex - 1; + if (index < 0) { + index = m_model->rowCount() - 1; + } + setCurrentIndex(index); +} + +DesktopBackground::DesktopBackground(QQuickItem *parent) + : WindowThumbnailItem(parent) +{ + setWId(WindowThumbnailItem::Desktop); + + connect(this, &QQuickItem::windowChanged, this, &DesktopBackground::stretchToScreen); + stretchToScreen(); +}; + +void DesktopBackground::stretchToScreen() +{ + const QQuickWindow *w = window(); + if (!w) { + return; + } + const QScreen *screen = w->screen(); + if (!screen) { + return; + } + setImplicitSize(screen->size().width(), screen->size().height()); +}; + +} // namespace KWin +} // namespace TabBox + +#include "moc_layoutpreview.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/layoutpreview.h b/local/recipes/kde/kwin/source/src/kcms/tabbox/layoutpreview.h new file mode 100644 index 0000000000..7c0fcfbfbc --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/layoutpreview.h @@ -0,0 +1,175 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include +#include + +#include "thumbnailitem.h" + +namespace KWin +{ + +namespace TabBox +{ + +class SwitcherItem; + +class LayoutPreview : public QObject +{ + Q_OBJECT +public: + explicit LayoutPreview(const QString &path, bool showDesktopThumbnail = false, QObject *parent = nullptr); + ~LayoutPreview() override; + + bool eventFilter(QObject *object, QEvent *event) override; + bool isLoaded() const; + +private: + SwitcherItem *m_item; +}; + +class ExampleClientModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum { + CaptionRole = Qt::UserRole + 1, + MinimizedRole, + DesktopNameRole, + IconRole, + WindowIdRole, + CloseableRole, + }; + + explicit ExampleClientModel(QObject *parent = nullptr); + ~ExampleClientModel() override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + Q_INVOKABLE QString longestCaption() const; + + void showDesktopThumbnail(bool showDesktop); + +private: + struct ThumbnailInfo + { + WindowThumbnailItem::Thumbnail wId; + QString caption; + QString icon; + + bool operator==(const ThumbnailInfo &other) const + { + return wId == other.wId; + } + }; + + void init(); + QList m_thumbnails; +}; + +class SwitcherItem : public QObject +{ + Q_OBJECT + Q_PROPERTY(QAbstractItemModel *model READ model NOTIFY modelChanged) + Q_PROPERTY(QRect screenGeometry READ screenGeometry NOTIFY screenGeometryChanged) + Q_PROPERTY(bool visible READ isVisible NOTIFY visibleChanged) + Q_PROPERTY(bool allDesktops READ isAllDesktops NOTIFY allDesktopsChanged) + Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) + + /** + * The main QML item that will be displayed in the Dialog + */ + Q_PROPERTY(QObject *item READ item WRITE setItem NOTIFY itemChanged) + + Q_CLASSINFO("DefaultProperty", "item") +public: + SwitcherItem(QObject *parent = nullptr); + ~SwitcherItem() override; + + QAbstractItemModel *model() const; + QRect screenGeometry() const; + bool isVisible() const; + bool isAllDesktops() const; + int currentIndex() const; + void setCurrentIndex(int index); + QObject *item() const; + void setItem(QObject *item); + + void setVisible(bool visible); + void incrementIndex(); + void decrementIndex(); + +Q_SIGNALS: + void visibleChanged(); + void currentIndexChanged(int index); + void modelChanged(); + void allDesktopsChanged(); + void screenGeometryChanged(); + void itemChanged(); + + void aboutToShow(); + void aboutToHide(); + +private: + QAbstractItemModel *m_model; + QObject *m_item; + int m_currentIndex; + bool m_visible; +}; + +inline QAbstractItemModel *SwitcherItem::model() const +{ + return m_model; +} + +inline bool SwitcherItem::isVisible() const +{ + return m_visible; +} + +inline bool SwitcherItem::isAllDesktops() const +{ + return true; +} + +inline int SwitcherItem::currentIndex() const +{ + return m_currentIndex; +} + +inline QObject *SwitcherItem::item() const +{ + return m_item; +} + +class DesktopBackground : public WindowThumbnailItem +{ + Q_OBJECT + Q_PROPERTY(QVariant activity MEMBER m_activity) + Q_PROPERTY(QVariant desktop MEMBER m_desktop) + Q_PROPERTY(QString outputName MEMBER m_outputName) + +public: + DesktopBackground(QQuickItem *parent = nullptr); + +private Q_SLOTS: + void stretchToScreen(); + +private: + // Just for mock-up purposes. + QVariant m_activity; + QVariant m_desktop; + QString m_outputName; +}; + +} // namespace TabBox +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/main.cpp b/local/recipes/kde/kwin/source/src/kcms/tabbox/main.cpp new file mode 100644 index 0000000000..4fe4dfb6c8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/main.cpp @@ -0,0 +1,318 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + SPDX-FileCopyrightText: 2020 Cyril Rossi + SPDX-FileCopyrightText: 2023 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "main.h" + +#include "config-kwin.h" + +#include + +// Qt +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// KDE +#include +#include +#include +#include +#include +// Plasma +#include +#include + +// own +#include "kwintabboxconfigform.h" +#include "kwintabboxdata.h" +#include "kwintabboxsettings.h" +#include "shortcutsettings.h" + +#include + +K_PLUGIN_FACTORY_WITH_JSON(KWinTabBoxConfigFactory, "kcm_kwintabbox.json", registerPlugin(); registerPlugin();) + +namespace KWin +{ + +using namespace TabBox; + +KWinTabBoxConfig::KWinTabBoxConfig(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) + , m_config(KSharedConfig::openConfig("kwinrc")) + , m_data(new KWinTabboxData(this)) +{ + QTabWidget *tabWidget = new QTabWidget(widget()); + tabWidget->setDocumentMode(true); + tabWidget->tabBar()->setExpanding(true); + + m_primaryTabBoxUi = new KWinTabBoxConfigForm(KWinTabBoxConfigForm::TabboxType::Main, + m_data->tabBoxConfig(), + m_data->shortcutConfig(), + tabWidget); + m_alternativeTabBoxUi = new KWinTabBoxConfigForm(KWinTabBoxConfigForm::TabboxType::Alternative, + m_data->tabBoxAlternativeConfig(), + m_data->shortcutConfig(), + tabWidget); + tabWidget->addTab(m_primaryTabBoxUi, i18n("Main")); + tabWidget->addTab(m_alternativeTabBoxUi, i18n("Alternative")); + + KNSWidgets::Button *ghnsButton = new KNSWidgets::Button(i18n("Get New Task Switcher Styles…"), QStringLiteral("kwinswitcher.knsrc"), widget()); + connect(ghnsButton, &KNSWidgets::Button::dialogFinished, this, [this](auto changedEntries) { + if (!changedEntries.isEmpty()) { + initLayoutLists(); + } + }); + + QHBoxLayout *buttonBar = new QHBoxLayout(); + QStyle *style = widget()->style(); + + buttonBar->setContentsMargins(style->pixelMetric(QStyle::PM_LayoutLeftMargin), 0, style->pixelMetric(QStyle::PM_LayoutRightMargin), style->pixelMetric(QStyle::PM_LayoutBottomMargin)); + QSpacerItem *buttonBarSpacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); + buttonBar->addItem(buttonBarSpacer); + buttonBar->addWidget(ghnsButton); + + QVBoxLayout *layout = new QVBoxLayout(widget()); + layout->setContentsMargins(0, 0, 0, 0); + KTitleWidget *infoLabel = new KTitleWidget(tabWidget); + infoLabel->setText(i18n("Focus policy settings limit the functionality of navigating through windows."), + KTitleWidget::InfoMessage); + infoLabel->setIcon(KTitleWidget::InfoMessage, KTitleWidget::ImageLeft); + layout->addWidget(infoLabel, 0); + layout->addWidget(tabWidget, 1); + KSeparator *separator = new KSeparator(); + layout->addWidget(separator); + layout->addLayout(buttonBar); + widget()->setLayout(layout); + + // Hide the separator if KNS is disabled. + separator->setVisible(!ghnsButton->isHidden()); + + addConfig(m_data->tabBoxConfig(), m_primaryTabBoxUi); + addConfig(m_data->tabBoxAlternativeConfig(), m_alternativeTabBoxUi); + + initLayoutLists(); + + createConnections(m_primaryTabBoxUi); + createConnections(m_alternativeTabBoxUi); + + // check focus policy - we don't offer configs for unreasonable focus policies + KConfigGroup config(m_config, QStringLiteral("Windows")); + QString policy = config.readEntry("FocusPolicy", "ClickToFocus"); + if ((policy == "FocusUnderMouse") || (policy == "FocusStrictlyUnderMouse")) { + tabWidget->setEnabled(false); + infoLabel->show(); + } else { + infoLabel->hide(); + } +} + +KWinTabBoxConfig::~KWinTabBoxConfig() +{ +} + +static QList availableLnFPackages() +{ + QList packages; + QStringList paths; + const QStringList dataPaths = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); + + for (const QString &path : dataPaths) { + QDir dir(path + QLatin1String("/plasma/look-and-feel")); + paths << dir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot); + } + + const auto &p = paths; + for (const QString &path : p) { + KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel")); + pkg.setPath(path); + pkg.setFallbackPackage(KPackage::Package()); + if (!pkg.filePath("defaults").isEmpty()) { + KSharedConfigPtr conf = KSharedConfig::openConfig(pkg.filePath("defaults")); + KConfigGroup cg = KConfigGroup(conf, QStringLiteral("kwinrc")); + cg = KConfigGroup(&cg, QStringLiteral("WindowSwitcher")); + if (!cg.readEntry("LayoutName", QString()).isEmpty()) { + packages << pkg; + } + } + } + + return packages; +} + +void KWinTabBoxConfig::initLayoutLists() +{ + auto model = std::make_unique(); + + auto addToModel = [model = model.get()](const QString &name, const QString &pluginId, const QString &path) { + QStandardItem *item = new QStandardItem(name); + item->setData(pluginId, Qt::UserRole); + item->setData(path, KWinTabBoxConfigForm::LayoutPath); + model->appendRow(item); + }; + + const auto lnfPackages = availableLnFPackages(); + for (const auto &package : lnfPackages) { + const auto &metaData = package.metadata(); + const QString switcherFile = package.filePath("windowswitcher", QStringLiteral("WindowSwitcher.qml")); + if (switcherFile.isEmpty()) { + // Skip lnfs that don't actually ship a switcher + continue; + } + + addToModel(metaData.name(), metaData.pluginId(), switcherFile); + } + + const QStringList packageRoots{ + QStringLiteral("kwin-wayland/tabbox"), + QStringLiteral("kwin/tabbox"), + }; + for (const QString &packageRoot : packageRoots) { + const QList offers = KPackage::PackageLoader::self()->listPackages(QStringLiteral("KWin/WindowSwitcher"), packageRoot); + for (const auto &offer : offers) { + const QString pluginName = offer.pluginId(); + const QString scriptFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, + packageRoot + QLatin1Char('/') + pluginName + QLatin1String("/contents/ui/main.qml")); + if (scriptFile.isEmpty()) { + qWarning() << "scriptfile is null" << pluginName; + continue; + } + + addToModel(offer.name(), pluginName, scriptFile); + } + } + + model->sort(0); + + m_primaryTabBoxUi->setEffectComboModel(model.get()); + m_alternativeTabBoxUi->setEffectComboModel(model.get()); + + m_switcherModel = std::move(model); +} + +void KWinTabBoxConfig::createConnections(KWinTabBoxConfigForm *form) +{ + connect(form, &KWinTabBoxConfigForm::effectPreviewClicked, this, &KWinTabBoxConfig::showPreview); + connect(form, &KWinTabBoxConfigForm::configChanged, this, &KWinTabBoxConfig::updateUnmanagedState); + + connect(this, &KWinTabBoxConfig::defaultsIndicatorsVisibleChanged, form, [form, this]() { + form->setDefaultIndicatorVisible(defaultsIndicatorsVisible()); + }); +} + +void KWinTabBoxConfig::updateUnmanagedState() +{ + const bool isNeedSave = m_data->tabBoxConfig()->isSaveNeeded() + || m_data->tabBoxAlternativeConfig()->isSaveNeeded() + || m_data->shortcutConfig()->isSaveNeeded(); + + unmanagedWidgetChangeState(isNeedSave); + + const bool isDefault = m_data->tabBoxConfig()->isDefaults() + && m_data->tabBoxAlternativeConfig()->isDefaults() + && m_data->shortcutConfig()->isDefaults(); + + unmanagedWidgetDefaultState(isDefault); +} + +void KWinTabBoxConfig::load() +{ + KCModule::load(); + + m_data->tabBoxConfig()->load(); + m_data->tabBoxAlternativeConfig()->load(); + m_data->shortcutConfig()->load(); + + m_primaryTabBoxUi->updateUiFromConfig(); + m_alternativeTabBoxUi->updateUiFromConfig(); + + updateUnmanagedState(); +} + +void KWinTabBoxConfig::save() +{ + m_data->tabBoxConfig()->save(); + m_data->tabBoxAlternativeConfig()->save(); + m_data->shortcutConfig()->save(); + + KCModule::save(); + updateUnmanagedState(); + + // Reload KWin. + QDBusMessage message = QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); +} + +void KWinTabBoxConfig::defaults() +{ + m_data->tabBoxConfig()->setDefaults(); + m_data->tabBoxAlternativeConfig()->setDefaults(); + m_data->shortcutConfig()->setDefaults(); + + m_primaryTabBoxUi->updateUiFromConfig(); + m_alternativeTabBoxUi->updateUiFromConfig(); + + KCModule::defaults(); + updateUnmanagedState(); +} + +void KWinTabBoxConfig::showPreview() +{ + auto form = qobject_cast(sender()); + Q_ASSERT(form); + + // The process will close when losing focus, but check in case of multiple calls + if (m_previewProcess && m_previewProcess->state() != QProcess::NotRunning) { + return; + } + + // Launch the preview helper executable with the required env var + // that allows the PlasmaDialog to position itself + // QT_WAYLAND_DISABLE_FIXED_POSITIONS=1 kwin-tabbox-preview [--show-desktop] + + const QString previewHelper = QStandardPaths::findExecutable("kwin-tabbox-preview", {LIBEXEC_DIR}); + if (previewHelper.isEmpty()) { + qWarning() << "Cannot find tabbox preview helper executable \"kwin-tabbox-preview\" in" << LIBEXEC_DIR; + return; + } + + QStringList args; + args << form->effectComboCurrentData(KWinTabBoxConfigForm::LayoutPath).toString(); + if (form->config()->showDesktopMode()) { + args << QStringLiteral("--show-desktop"); + } + + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert(QStringLiteral("QT_WAYLAND_DISABLE_FIXED_POSITIONS"), + QStringLiteral("1")); + + m_previewProcess = std::make_unique(); + m_previewProcess->setArguments(args); + m_previewProcess->setProgram(previewHelper); + m_previewProcess->setProcessEnvironment(env); + m_previewProcess->setProcessChannelMode(QProcess::ForwardedChannels); + m_previewProcess->start(); +} + +} // namespace + +#include "main.moc" + +#include "moc_main.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/main.h b/local/recipes/kde/kwin/source/src/kcms/tabbox/main.h new file mode 100644 index 0000000000..e77ec63af2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/main.h @@ -0,0 +1,62 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + SPDX-FileCopyrightText: 2020 Cyril Rossi + SPDX-FileCopyrightText: 2023 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include +#include + +class QProcess; + +namespace KWin +{ +class KWinTabBoxConfigForm; +namespace TabBox +{ +class KWinTabboxData; +class TabBoxSettings; +} + +class KWinTabBoxConfig : public KCModule +{ + Q_OBJECT + +public: + explicit KWinTabBoxConfig(QObject *parent, const KPluginMetaData &data); + ~KWinTabBoxConfig() override; + +public Q_SLOTS: + void save() override; + void load() override; + void defaults() override; + +private Q_SLOTS: + void updateUnmanagedState(); + void showPreview(); + +private: + void initLayoutLists(); + void createConnections(KWinTabBoxConfigForm *form); + +private: + KWinTabBoxConfigForm *m_primaryTabBoxUi = nullptr; + KWinTabBoxConfigForm *m_alternativeTabBoxUi = nullptr; + std::unique_ptr m_switcherModel; + KSharedConfigPtr m_config; + + TabBox::KWinTabboxData *m_data; + + std::unique_ptr m_previewProcess; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/main.ui b/local/recipes/kde/kwin/source/src/kcms/tabbox/main.ui new file mode 100644 index 0000000000..3c2bae9453 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/main.ui @@ -0,0 +1,823 @@ + + + KWinTabBoxConfigForm + + + + 0 + 0 + 658 + 418 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Content + + + true + + + + + + Include "Peek at Desktop" entry + + + + + + + + 0 + 0 + + + + + Recently used + + + + + Stacking order + + + + + + + + Only one window per application + + + false + + + + + + + Order minimized windows after unminimized windows + + + false + + + + + + + Sort order: + + + switchingModeCombo + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Filter windows by + + + true + + + + + + Virtual desktops + + + false + + + + + + + false + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 24 + 20 + + + + + + + + Current desktop + + + + + + + All other desktops + + + + + + + + + + Activities + + + false + + + + + + + false + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 24 + 20 + + + + + + + + Current activity + + + + + + + All other activities + + + + + + + + + + Screens + + + false + + + + + + + false + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 24 + 20 + + + + + + + + Current screen + + + + + + + All other screens + + + + + + + + + + Minimization + + + false + + + + + + + false + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 24 + 20 + + + + + + + + Visible windows + + + + + + + Hidden windows + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Shortcuts + + + true + + + + + + Forward + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + + + + + + 75 + true + + + + All windows + + + Qt::AlignCenter + + + + + + + Reverse + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Forward + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Reverse + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + or + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + or + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + 75 + true + + + + Current application + + + Qt::AlignCenter + + + + + + + + + + + + + or + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + or + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + Visualization + + + true + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + The effect to replace the list window when desktop effects are active. + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + + true + + + + + + + The currently selected window will be highlighted by fading out all other windows. This option requires desktop effects to be active. + + + Show selected window + + + + + + + + + + Qt::Vertical + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + KKeySequenceWidget + QWidget +
kkeysequencewidget.h
+
+
+ + kcfg_HighlightWindows + kcfg_ShowTabBox + effectCombo + effectPreviewButton + switchingModeCombo + showDesktop + oneAppWindow + filterDesktops + currentDesktop + otherDesktops + filterActivities + currentActivity + otherActivities + filterScreens + currentScreen + otherScreens + filterMinimization + visibleWindows + hiddenWindows + + + + + filterDesktops + toggled(bool) + desktopFilter + setEnabled(bool) + + + 541 + 172 + + + 701 + 197 + + + + + filterActivities + toggled(bool) + activityFilter + setEnabled(bool) + + + 543 + 222 + + + 701 + 247 + + + + + filterScreens + toggled(bool) + screenFilter + setEnabled(bool) + + + 555 + 272 + + + 701 + 297 + + + + + filterMinimization + toggled(bool) + minimizationFilter + setEnabled(bool) + + + 558 + 322 + + + 701 + 347 + + + + + kcfg_ShowTabBox + toggled(bool) + widget_6 + setEnabled(bool) + + + 164 + 125 + + + 230 + 108 + + + + +
diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/shortcutsettings.cpp b/local/recipes/kde/kwin/source/src/kcms/tabbox/shortcutsettings.cpp new file mode 100644 index 0000000000..8045f627a5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/shortcutsettings.cpp @@ -0,0 +1,176 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "shortcutsettings.h" + +#include + +#include +#include +#include + +// Implementation of a KConfigSkeletonItem that uses KGlobalAccel to retrieve and store +// shortcut settings instead of storing them in a config file +class ShortcutItem : public KConfigSkeletonItem +{ +public: + ShortcutItem(QAction *action, KActionCollection *actionCollection); + + void readConfig(KConfig *config) override; + void writeConfig(KConfig *config) override; + + void readDefault(KConfig *config) override; + void setDefault() override; + void swapDefault() override; + + bool isEqual(const QVariant &p) const override; + QVariant property() const override; + void setProperty(const QVariant &p) override; + +private: + KActionCollection *m_actionCollection = nullptr; + QAction *m_action = nullptr; + QList m_savedShortcuts; +}; + +ShortcutItem::ShortcutItem(QAction *action, KActionCollection *actionCollection) + : KConfigSkeletonItem(actionCollection->componentName(), action->text()) + , m_actionCollection(actionCollection) + , m_action(action) +{ + setGetDefaultImpl([this] { + return QVariant::fromValue(m_actionCollection->defaultShortcuts(m_action)); + }); + + setIsDefaultImpl([this] { + return m_action->shortcuts() == m_actionCollection->defaultShortcuts(m_action); + }); + + setIsSaveNeededImpl([this] { + return (m_action->shortcuts() != m_savedShortcuts); + }); +} + +void ShortcutItem::readConfig(KConfig *config) +{ + m_savedShortcuts = KGlobalAccel::self()->globalShortcut(m_actionCollection->componentName(), m_action->objectName()); + m_action->setShortcuts(m_savedShortcuts); +} + +void ShortcutItem::writeConfig(KConfig *config) +{ + m_savedShortcuts = m_action->shortcuts(); + KGlobalAccel::self()->setShortcut(m_action, m_action->shortcuts(), KGlobalAccel::NoAutoloading); +} + +void ShortcutItem::readDefault(KConfig *config) +{ +} + +void ShortcutItem::setDefault() +{ + m_action->setShortcuts(m_actionCollection->defaultShortcuts(m_action)); +} + +void ShortcutItem::swapDefault() +{ + const QList previousShortcut = m_action->shortcuts(); + m_action->setShortcuts(m_actionCollection->defaultShortcuts(m_action)); + m_actionCollection->setDefaultShortcuts(m_action, previousShortcut); +} + +bool ShortcutItem::isEqual(const QVariant &p) const +{ + if (!p.canConvert>()) { + return false; + } + return m_action->shortcuts() == p.value>(); +} + +QVariant ShortcutItem::property() const +{ + return QVariant::fromValue>(m_action->shortcuts()); +} + +void ShortcutItem::setProperty(const QVariant &p) +{ + m_action->setShortcuts(p.value>()); +} + +namespace KWin +{ +namespace TabBox +{ + +ShortcutSettings::ShortcutSettings(QObject *parent) + : KConfigSkeleton(nullptr, parent) + , m_actionCollection(new KActionCollection(this, QStringLiteral("kwin"))) +{ + m_actionCollection->setConfigGroup("Navigation"); + m_actionCollection->setConfigGlobal(true); + + auto addShortcut = [this](const KLocalizedString &name, const QList &shortcuts = QList()) { + const QString untranslatedName = QString::fromUtf8(name.untranslatedText()); + QAction *action = m_actionCollection->addAction(untranslatedName); + action->setObjectName(untranslatedName); + action->setProperty("isConfigurationAction", true); + action->setText(name.toString()); + + m_actionCollection->setDefaultShortcuts(action, shortcuts); + + addItem(new ShortcutItem(action, m_actionCollection)); + }; + + // TabboxType::Main + addShortcut(ki18nd("kwin", "Walk Through Windows"), {Qt::META | Qt::Key_Tab, Qt::ALT | Qt::Key_Tab}); + addShortcut(ki18nd("kwin", "Walk Through Windows (Reverse)"), {Qt::META | Qt::SHIFT | Qt::Key_Tab, Qt::ALT | Qt::SHIFT | Qt::Key_Tab}); + addShortcut(ki18nd("kwin", "Walk Through Windows of Current Application"), {Qt::META | Qt::Key_QuoteLeft, Qt::ALT | Qt::Key_QuoteLeft}); + addShortcut(ki18nd("kwin", "Walk Through Windows of Current Application (Reverse)"), {Qt::META | Qt::Key_AsciiTilde, Qt::ALT | Qt::Key_AsciiTilde}); + // TabboxType::Alternative + addShortcut(ki18nd("kwin", "Walk Through Windows Alternative")); + addShortcut(ki18nd("kwin", "Walk Through Windows Alternative (Reverse)")); + addShortcut(ki18nd("kwin", "Walk Through Windows of Current Application Alternative")); + addShortcut(ki18nd("kwin", "Walk Through Windows of Current Application Alternative (Reverse)")); +} + +KActionCollection *ShortcutSettings::actionCollection() const +{ + return m_actionCollection; +} + +QKeySequence ShortcutSettings::primaryShortcut(const QString &name) const +{ + QAction *action = m_actionCollection->action(name); + Q_ASSERT(action); + return action->shortcuts().value(0); +} + +QKeySequence ShortcutSettings::alternateShortcut(const QString &name) const +{ + QAction *action = m_actionCollection->action(name); + Q_ASSERT(action); + return action->shortcuts().value(1); +} + +void ShortcutSettings::setShortcuts(const QString &name, const QList &shortcuts) +{ + QAction *action = m_actionCollection->action(name); + Q_ASSERT(action); + action->setShortcuts(shortcuts); +} + +bool ShortcutSettings::isDefault(const QString &name) const +{ + QAction *action = m_actionCollection->action(name); + Q_ASSERT(action); + return action->shortcuts() == m_actionCollection->defaultShortcuts(action); +} + +} // namespace TabBox +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/shortcutsettings.h b/local/recipes/kde/kwin/source/src/kcms/tabbox/shortcutsettings.h new file mode 100644 index 0000000000..162ffa07ed --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/shortcutsettings.h @@ -0,0 +1,42 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +class KActionCollection; + +namespace KWin +{ +namespace TabBox +{ + +class ShortcutSettings : public KConfigSkeleton +{ +public: + explicit ShortcutSettings(QObject *parent); + + KActionCollection *actionCollection() const; + + QKeySequence primaryShortcut(const QString &name) const; + QKeySequence alternateShortcut(const QString &name) const; + + void setShortcuts(const QString &name, const QList &shortcuts); + + bool isDefault(const QString &name) const; + +private: + KActionCollection *m_actionCollection = nullptr; +}; + +} // namespace TabBox +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnailitem.cpp b/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnailitem.cpp new file mode 100644 index 0000000000..321b3e85a6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnailitem.cpp @@ -0,0 +1,117 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011, 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "thumbnailitem.h" + +#include "config-kwin.h" + +// Qt +#include +#include +#include +#include + +namespace KWin +{ + +WindowThumbnailItem::WindowThumbnailItem(QQuickItem *parent) + : QQuickItem(parent) + , m_wId(0) + , m_image() + , m_sourceSize(QSize()) +{ + setFlag(ItemHasContents); +} + +WindowThumbnailItem::~WindowThumbnailItem() +{ +} + +void WindowThumbnailItem::setWId(qulonglong wId) +{ + m_wId = wId; + Q_EMIT wIdChanged(wId); + findImage(); +} + +void WindowThumbnailItem::findImage() +{ + QString imagePath; + switch (m_wId) { + case Konqueror: + imagePath = QStringLiteral(":/kwin-tabbox-preview/falkon.png"); + break; + case Systemsettings: + imagePath = QStringLiteral(":/kwin-tabbox-preview/systemsettings.png"); + break; + case KMail: + imagePath = QStringLiteral(":/kwin-tabbox-preview/kmail.png"); + break; + case Dolphin: + imagePath = QStringLiteral(":/kwin-tabbox-preview/dolphin.png"); + break; + case Desktop: + // Use the current default desktop wallpaper + imagePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "wallpapers/Next/contents/images/1280x800.png"); + if (imagePath.isNull()) { + imagePath = QStringLiteral(":/kwin-tabbox-preview/desktop.png"); + } + break; + default: + // ignore + break; + } + if (imagePath.isNull()) { + m_image = QImage(); + } else { + m_image = QImage(imagePath); + } + + setImplicitSize(m_image.width(), m_image.height()); +} + +QSGNode *WindowThumbnailItem::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData) +{ + auto *node = static_cast(oldNode); + if (!node) { + node = window()->createImageNode(); + node->setOwnsTexture(true); + qsgnode_set_description(node, QStringLiteral("windowthumbnail")); + node->setFiltering(QSGTexture::Linear); + } + + node->setTexture(window()->createTextureFromImage(m_image)); + + const QSize size(m_image.size().scaled(boundingRect().size().toSize(), Qt::KeepAspectRatio)); + const qreal x = boundingRect().x() + (boundingRect().width() - size.width()) / 2; + const qreal y = boundingRect().y() + (boundingRect().height() - size.height()) / 2; + + node->setRect(QRectF(QPointF(x, y), size)); + + return node; +} + +QSize WindowThumbnailItem::sourceSize() const +{ + return m_sourceSize; +} + +void WindowThumbnailItem::setSourceSize(const QSize &size) +{ + if (m_sourceSize == size) { + return; + } + m_sourceSize = size; + update(); + Q_EMIT sourceSizeChanged(); +} + +} // namespace KWin + +#include "moc_thumbnailitem.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnailitem.h b/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnailitem.h new file mode 100644 index 0000000000..fb9225845a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnailitem.h @@ -0,0 +1,54 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011, 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +namespace KWin +{ + +class WindowThumbnailItem : public QQuickItem +{ + Q_OBJECT + Q_PROPERTY(qulonglong wId READ wId WRITE setWId NOTIFY wIdChanged SCRIPTABLE true) + Q_PROPERTY(QSize sourceSize READ sourceSize WRITE setSourceSize NOTIFY sourceSizeChanged) +public: + explicit WindowThumbnailItem(QQuickItem *parent = nullptr); + ~WindowThumbnailItem() override; + + qulonglong wId() const + { + return m_wId; + } + QSize sourceSize() const; + void setWId(qulonglong wId); + void setSourceSize(const QSize &size); + QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData) override; + + enum Thumbnail { + Konqueror = 1, + KMail, + Systemsettings, + Dolphin, + Desktop, + }; +Q_SIGNALS: + void wIdChanged(qulonglong wid); + void sourceSizeChanged(); + +private: + void findImage(); + qulonglong m_wId; + QImage m_image; + QSize m_sourceSize; +}; + +} // KWin diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/desktop.png b/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/desktop.png new file mode 100644 index 0000000000..f31f8ed130 Binary files /dev/null and b/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/desktop.png differ diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/dolphin.png b/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/dolphin.png new file mode 100644 index 0000000000..03baaafd61 Binary files /dev/null and b/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/dolphin.png differ diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/falkon.png b/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/falkon.png new file mode 100644 index 0000000000..64bb716366 Binary files /dev/null and b/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/falkon.png differ diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/kmail.png b/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/kmail.png new file mode 100644 index 0000000000..1c74a3a60d Binary files /dev/null and b/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/kmail.png differ diff --git a/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/systemsettings.png b/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/systemsettings.png new file mode 100644 index 0000000000..69f204bf11 Binary files /dev/null and b/local/recipes/kde/kwin/source/src/kcms/tabbox/thumbnails/systemsettings.png differ diff --git a/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/CMakeLists.txt b/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/CMakeLists.txt new file mode 100644 index 0000000000..c8be3686e8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/CMakeLists.txt @@ -0,0 +1,22 @@ +#SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalzez +#SPDX-License-Identifier: BSD-3-Clause + +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_virtualkeyboard\") + +kcmutils_generate_module_data( + kcm_virtualkeyboard_PART_SRCS + MODULE_DATA_HEADER virtualkeyboarddata.h + MODULE_DATA_CLASS_NAME VirtualKeyboardData + SETTINGS_HEADERS virtualkeyboardsettings.h + SETTINGS_CLASSES VirtualKeyboardSettings +) + +kconfig_add_kcfg_files(kcm_virtualkeyboard_PART_SRCS virtualkeyboardsettings.kcfgc GENERATE_MOC) +kcmutils_add_qml_kcm(kcm_virtualkeyboard SOURCES kcmvirtualkeyboard.cpp ${kcm_virtualkeyboard_PART_SRCS}) + +target_link_libraries(kcm_virtualkeyboard PRIVATE + KF6::I18n + KF6::KCMUtils + KF6::Service + KF6::KCMUtilsQuick +) diff --git a/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/Messages.sh b/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/Messages.sh new file mode 100644 index 0000000000..9e07411868 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.cpp -o -name \*.h -o -name \*.qml` -o $podir/kcm_virtualkeyboard.pot diff --git a/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/kcm_virtualkeyboard.json b/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/kcm_virtualkeyboard.json new file mode 100644 index 0000000000..936f15c28e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/kcm_virtualkeyboard.json @@ -0,0 +1,146 @@ +{ + "Categories": "Qt;KDE;", + "KPlugin": { + "BugReportUrl": "https://bugs.kde.org/enter_bug.cgi?product=systemsettings&component=kcm_kwinvirtualkeyboard", + "Description": "Select which virtual keyboard to use", + "Description[ar]": "اختر أيا من لوحات المفاتيح الافتراضية لاستعمالها", + "Description[az]": "İstifadə edəcəyiniz virtual klaviaturanı seçin", + "Description[be]": "Віртуальная клавіятура для выкарыстання", + "Description[bg]": "Избиране на виртуална клавиатура, която да се използва", + "Description[ca@valencia]": "Trieu el teclat virtual que s'ha d'utilitzar", + "Description[ca]": "Seleccioneu el teclat virtual a usar", + "Description[cs]": "Zvolte, kterou virtuální klávesnici použít", + "Description[da]": "Vælg hvilket virtuelt tastatur, der skal bruges", + "Description[de]": "Wählen Sie die zu verwendende virtuelle Tastatur", + "Description[en_GB]": "Select which virtual keyboard to use", + "Description[eo]": "Elektu kiun virtualan klavaron uzi", + "Description[es]": "Seleccionar el teclado virtual que se debe usar", + "Description[et]": "Kasutatava virtuaalse klaviatuuri valimine", + "Description[eu]": "Hautatu erabili beharreko alegiazko teklatua", + "Description[fi]": "Valitse käytettävä näyttönäppäimistö", + "Description[fr]": "Sélectionner quel clavier virtuel à utiliser", + "Description[gl]": "Seleccionar o teclado virtual para usar.", + "Description[he]": "נא לבחור באיזו מקלדת וירטואלית להשתמש", + "Description[hu]": "A használandó virtuális billentyűzet kiválasztása", + "Description[ia]": "Selectiona qual claviero virtual usar", + "Description[id]": "Pilih keyboard virtual apa yang digunakan", + "Description[is]": "Velja hvaða sýndarlyklaborð á að nota", + "Description[it]": "Seleziona quale tastiera virtuale utilizzare", + "Description[ja]": "使用する仮想キーボードを選択", + "Description[ka]": "აირჩიეთ ვირტუალური კლავიატურა", + "Description[ko]": "사용할 가상 키보드 선택", + "Description[lt]": "Pasirinkti, kurią virtualią klaviatūrą naudoti", + "Description[lv]": "Atlasīt, kuru virtuālo tastatūru izmantot", + "Description[nb]": "Velg hvilke virtuelle tastatur du vil bruke", + "Description[nl]": "Het te gebruiken virtuele toetsenbord selecteren", + "Description[nn]": "Vel kva virtuelle tastatur du vil bruka", + "Description[pl]": "Wybierz której klawiatury ekranowej użyć", + "Description[pt]": "Seleccione qual o teclado virtual a usar", + "Description[pt_BR]": "Selecione qual teclado virtual a ser usado", + "Description[ro]": "Alegeți ce tastatură virtuală să fie folosită", + "Description[ru]": "Выбор виртуальной клавиатуры", + "Description[sa]": "कस्य वर्चुअल् कीबोर्डस्य उपयोगः करणीयः इति चिनोतु", + "Description[sk]": "Vyberte, ktorá virtuálna klávesnica sa má použiť", + "Description[sl]": "Izberite, katere navidezne tipkovnice želite uporabljati", + "Description[sv]": "Välj vilket virtuellt tangentbord som ska användas", + "Description[ta]": "பயன்படுத்தவேண்டிய மெய்நிகர் விசைப்பலகையை தேர்ந்தெடுங்கள்", + "Description[tr]": "Hangi sanal klavyenin kullanılacağını seçin", + "Description[uk]": "Вибір віртуальної клавіатури", + "Description[vi]": "Chọn bàn phím ảo để dùng", + "Description[zh_CN]": "选择要使用的虚拟键盘", + "Description[zh_TW]": "選取要使用的虛擬鍵盤", + "Icon": "input-keyboard-virtual", + "Name": "Virtual Keyboard", + "Name[ar]": "لوحة مفاتيح افتراضية", + "Name[ast]": "Tecláu virtual", + "Name[az]": "Virtual klaviatura", + "Name[be]": "Віртуальная клавіятура", + "Name[bg]": "Виртуална клавиатура", + "Name[ca@valencia]": "Teclat virtual", + "Name[ca]": "Teclat virtual", + "Name[cs]": "Virtuální klávesnice", + "Name[da]": "Virtuelt tastatur", + "Name[de]": "Virtuelle Tastatur", + "Name[en_GB]": "Virtual Keyboard", + "Name[eo]": "Virtuala Klavaro", + "Name[es]": "Teclado virtual", + "Name[et]": "Virtuaalne klaviatuur", + "Name[eu]": "Alegiazko teklatua", + "Name[fi]": "Näyttönäppäimistö", + "Name[fr]": "Clavier virtuel", + "Name[ga]": "Méarchlár Fíorúil", + "Name[gl]": "Teclado virtual", + "Name[he]": "מקלדת וירטואלית", + "Name[hu]": "Virtuális billentyűzet", + "Name[ia]": "Claviero virtual", + "Name[id]": "Keyboard Virtual", + "Name[is]": "Sýndarlyklaborð", + "Name[it]": "Tastiera virtuale", + "Name[ja]": "仮想キーボード", + "Name[ka]": "ვირტუალური კლავიატურა", + "Name[ko]": "가상 키보드", + "Name[lt]": "Virtuali klaviatūra", + "Name[lv]": "Virtuālā tastatūra", + "Name[nb]": "Virtuelt tastatur", + "Name[nl]": "Virtueel toetsenbord", + "Name[nn]": "Virtuelt tastatur", + "Name[pl]": "Klawiatura ekranowa", + "Name[pt]": "Teclado Virtual", + "Name[pt_BR]": "Teclado virtual", + "Name[ro]": "Tastatură virtuală", + "Name[ru]": "Виртуальная клавиатура", + "Name[sa]": "आभासी कीबोर्ड", + "Name[sk]": "Virtuálna klávesnica", + "Name[sl]": "Navidezna tipkovnica", + "Name[sv]": "Virtuellt tangentbord", + "Name[ta]": "மெய்நிகர் விசைப்பலகை", + "Name[tr]": "Sanal Klavye", + "Name[uk]": "Віртуальна клавіатура", + "Name[vi]": "Bàn phím ảo", + "Name[zh_CN]": "虚拟键盘", + "Name[zh_TW]": "虛擬鍵盤" + }, + "X-DocPath": "kcontrol/kwinvirtualkeyboard/index.html", + "X-KDE-Keywords": "software keyboard,virtual keyboard,touch keyboard,on screen keyboard,input methods", + "X-KDE-Keywords[ar]": "لوحة مفاتيح برمجية,لوحة مفاتيح افتراضية,لوحة مفاتيح تعمل باللمس,لوحة مفاتيح على الشاشة,طرق الإدخال", + "X-KDE-Keywords[bg]": "софтуерна клавиатура,виртуална клавиатура,сензорна клавиатура,екранна клавиатура,методи за въвеждане", + "X-KDE-Keywords[ca@valencia]": "teclat de programari,teclat virtual,teclat tàctil,teclat en pantalla,mètodes d'entrada", + "X-KDE-Keywords[ca]": "teclat de programari,teclat virtual,teclat tàctil,teclat en pantalla,mètodes d'entrada", + "X-KDE-Keywords[de]": "Software-Tastatur,virtuelle Tastatur,Touchscreen-Tastatur,Bildschirmtastatur,Eingabemethoden", + "X-KDE-Keywords[en_GB]": "software keyboard,virtual keyboard,touch keyboard,on screen keyboard,input methods", + "X-KDE-Keywords[es]": "teclado por software,teclado de software,teclado software,teclado virtual,teclado táctil,teclado en pantalla,métodos de entrada", + "X-KDE-Keywords[eu]": "software teklatua,alegiazko teklatua,teklatu birtuala,ukimen-teklatua,pantailako teklatua,sarrerako metodoak", + "X-KDE-Keywords[fi]": "ohjelmallinen näppäimistö,näyttönäppäimistö,virtuaalinäppäimistö,kosketusnäppäimistö,syötetavat,syötemetodit", + "X-KDE-Keywords[fr]": "clavier logiciel, clavier virtuel, clavier tactile, clavier à l'écran, méthodes de saisie", + "X-KDE-Keywords[gl]": "software keyboard,virtual keyboard,teclado virtual,touch keyboard,teclado táctil,on screen keyboard,teclado en pantalla,input methods,métodos de entrada", + "X-KDE-Keywords[he]": "מקלדת תוכנה,מקלדת וירטואלית,מקלדת מגע,מקלדת על המסך,שיטות קלט", + "X-KDE-Keywords[hu]": "szoftveres billentyűzet,virtuális billentyűzet,érintőbillentyűzet,képernyőbillentyűzet,beviteli módok", + "X-KDE-Keywords[ia]": "claviero software, claviero virtual, claviero a toccar, claviero sur schermo, methodos de entrata", + "X-KDE-Keywords[is]": "lyklaborðsforrit,sýndarlyklaborð,snertilyklaborð,lyklaborð á skjá, innsláttaraðferð", + "X-KDE-Keywords[it]": "tastiera software,tastiera virtuale,tastiera tattile,tastiera su schermo,metodi di inserimento", + "X-KDE-Keywords[ja]": "software keyboard,virtual keyboard,touch keyboard,on screen keyboard,input methods,ソフトウェアキーボード,仮想キーボード,タッチキーボード,オンスクリーンキーボード,入力方法,入力メゾット", + "X-KDE-Keywords[ka]": "software keyboard,virtual keyboard,touch keyboard,on screen keyboard,input methods,პროგრამული კლავიატურა,ვირტუალური კლავიატურა,კლავიატურა ეკრანზე,შეყვანის მეთოდები", + "X-KDE-Keywords[ko]": "소프트웨어 키보드,가상 키보드,터치 키보드,화면상 키보드,온스크린 키보드,화상 키보드,입력기", + "X-KDE-Keywords[lt]": "software keyboard,virtual keyboard,touch keyboard,on screen keyboard,input methods,programinės įrangos klaviatūra,programines irangos klaviatura,virtuali klaviatūra,virtuali klaviatura,ekraninė klaviatūra,ekranine klaviatura,jutiklinė klaviatūra,jutikline klaviatura,liečiamoji klaviatūra,lieciamoji klaviatura,lieciama klaviatura,liečiama klaviatūra,įvesties metodai,ivesties metodai,įvedimo metodai,ivedimo metodai", + "X-KDE-Keywords[lv]": "programmatūras tastatūra,virtuālā tastatūra,skārienjutīga tastatūra,ekrāna tastatūra,ievades metodes", + "X-KDE-Keywords[nb]": "programtastatur,virtuelt tastatur,fingertastatur,skjermtastatur,inndatametode", + "X-KDE-Keywords[nl]": "software-toetsenbord,virtueel toetsenbord,aanraaktoetsenbord,toetsenbord op scherm,invoermethoden", + "X-KDE-Keywords[nn]": "programtastatur,virtuelt tastatur,fingertastatur,skjermtastatur,inndatametode", + "X-KDE-Keywords[pl]": "klawiatura programowa,klawiatura wirtualna,klawiatura dotykowa,klawiatura ekranowa,sposoby wprowadzania", + "X-KDE-Keywords[pt_BR]": "teclado de software,teclado virtual,teclado sensível ao toque,teclado na tela,métodos de entrada", + "X-KDE-Keywords[ro]": "tastatură software,tastatură virtuală,tastieră virtuală,tastatură tactilă,tastare prin atingere,tastatură pe ecran,metodă de introducere,metode de introducere", + "X-KDE-Keywords[ru]": "software keyboard,virtual keyboard,touch keyboard,on screen keyboard,input methods,программная клавиатура,виртуальная клавиатура,сенсорная клавиатура,экранная клавиатура,способы ввода", + "X-KDE-Keywords[sa]": "सॉफ्टवेयर कीबोर्ड,वर्चुअल कीबोर्ड,टच कीबोर्ड,स्क्रीन कीबोर्ड पर,इनपुट विधियों", + "X-KDE-Keywords[sk]": "software keyboard,virtual keyboard,touch keyboard,on screen keyboard,input methods", + "X-KDE-Keywords[sl]": "programska tipkovnica,virtualna tipkovnica,tipkovnica na dotik,zaslonska tipkovnica,načini vnosa", + "X-KDE-Keywords[sv]": "programvarutangentbord,virtuellt tangentbord,pektangentbord,skärmtangentbord,inmatningsmetoder", + "X-KDE-Keywords[tr]": "yazılım klavyesi,sanal klavye,dokunmatik klavye,ekran klavyesi,giriş yöntemi,giriş yöntemleri", + "X-KDE-Keywords[uk]": "software keyboard,virtual keyboard,touch keyboard,on screen keyboard,input methods,програмна клавіатура,віртуальна клавіатура,сенсорна клавіатура,екранна клавіатура,способи введення", + "X-KDE-Keywords[zh_CN]": "software keyboard,virtual keyboard,touch keyboard,on screen keyboard,input methods,ruanjianjianpan,xunijianpan,pingmujianpan,pingshangjianpan,shurufangshi,shurufa,软件键盘,虚拟键盘,屏幕键盘,屏上键盘,输入方式,输入法", + "X-KDE-Keywords[zh_TW]": "軟體鍵盤,虛擬鍵盤,觸控鍵盤,螢幕鍵盤,輸入法", + "X-KDE-OnlyShowOnQtPlatforms": [ + "wayland" + ], + "X-KDE-System-Settings-Parent-Category": "keyboard", + "X-KDE-Weight": 100 +} diff --git a/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/kcmvirtualkeyboard.cpp b/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/kcmvirtualkeyboard.cpp new file mode 100644 index 0000000000..d1ea442c9d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/kcmvirtualkeyboard.cpp @@ -0,0 +1,94 @@ +/* + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kcmvirtualkeyboard.h" + +#include +#include +#include +#include +#include + +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(KcmVirtualKeyboardFactory, "kcm_virtualkeyboard.json", registerPlugin(); registerPlugin();) + +KcmVirtualKeyboard::KcmVirtualKeyboard(QObject *parent, const KPluginMetaData &metaData) + : KQuickManagedConfigModule(parent, metaData) + , m_data(new VirtualKeyboardData(this)) + , m_model(new VirtualKeyboardsModel(this)) +{ + qmlRegisterAnonymousType("org.kde.kwin.virtualkeyboardsettings", 1); +} + +KcmVirtualKeyboard::~KcmVirtualKeyboard() = default; + +VirtualKeyboardSettings *KcmVirtualKeyboard::settings() const +{ + return m_data->settings(); +} + +VirtualKeyboardsModel::VirtualKeyboardsModel(QObject *parent) + : QAbstractListModel(parent) +{ + m_services = KApplicationTrader::query([](const KService::Ptr &service) { + return service->property("X-KDE-Wayland-VirtualKeyboard"); + }); + + m_services.prepend({}); +} + +QHash VirtualKeyboardsModel::roleNames() const +{ + QHash ret = QAbstractListModel::roleNames(); + ret.insert(DesktopFileNameRole, "desktopFileName"); + return ret; +} + +QVariant VirtualKeyboardsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.parent().isValid() || index.row() > m_services.count()) { + return {}; + } + + const KService::Ptr service = m_services[index.row()]; + switch (role) { + case Qt::DisplayRole: + return service ? service->name() : i18n("None"); + case Qt::DecorationRole: + return service ? service->icon() : QStringLiteral("edit-none"); + case Qt::ToolTipRole: + return service ? service->comment() : i18n("Do not use any virtual keyboard"); + case DesktopFileNameRole: + return service ? QStandardPaths::locate(QStandardPaths::ApplicationsLocation, service->desktopEntryName() + QLatin1String(".desktop")) : QString(); + } + return {}; +} + +int VirtualKeyboardsModel::inputMethodIndex(const QString &desktopFile) const +{ + if (desktopFile.isEmpty()) { + return 0; + } + + int i = 0; + for (const auto &service : m_services) { + if (service && desktopFile.endsWith(service->desktopEntryName() + QLatin1String(".desktop"))) { + return i; + } + ++i; + } + return -1; +} + +int VirtualKeyboardsModel::rowCount(const QModelIndex &parent) const +{ + return parent.isValid() ? 0 : m_services.count(); +} + +#include "kcmvirtualkeyboard.moc" +#include "moc_kcmvirtualkeyboard.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/kcmvirtualkeyboard.h b/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/kcmvirtualkeyboard.h new file mode 100644 index 0000000000..729943d8db --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/kcmvirtualkeyboard.h @@ -0,0 +1,57 @@ +/* + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +class KDesktopFile; +class VirtualKeyboardData; +class VirtualKeyboardSettings; + +class VirtualKeyboardsModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum Roles { + DesktopFileNameRole = Qt::UserRole + 1, + }; + Q_ENUM(Roles) + + VirtualKeyboardsModel(QObject *parent = nullptr); + QVariant data(const QModelIndex &index, int role) const override; + int rowCount(const QModelIndex &parent) const override; + + QHash roleNames() const override; + + Q_SCRIPTABLE int inputMethodIndex(const QString &desktopFile) const; + +private: + KService::List m_services; +}; + +class KcmVirtualKeyboard : public KQuickManagedConfigModule +{ + Q_OBJECT + Q_PROPERTY(VirtualKeyboardSettings *settings READ settings CONSTANT) + Q_PROPERTY(QAbstractItemModel *model READ keyboardsModel CONSTANT) + +public: + explicit KcmVirtualKeyboard(QObject *parent, const KPluginMetaData &metaData); + ~KcmVirtualKeyboard() override; + + VirtualKeyboardSettings *settings() const; + VirtualKeyboardsModel *keyboardsModel() const + { + return m_model; + } + +private: + VirtualKeyboardData *m_data; + VirtualKeyboardsModel *const m_model; +}; diff --git a/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/ui/main.qml b/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/ui/main.qml new file mode 100644 index 0000000000..414cce0c68 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/ui/main.qml @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + + +import QtQuick +import QtQuick.Layouts +import org.kde.kirigami as Kirigami +import org.kde.kcmutils as KCM + +KCM.GridViewKCM { + id: root + + view.model: kcm.model + view.currentIndex: kcm.model.inputMethodIndex(kcm.settings.inputMethod) + + KCM.SettingStateBinding { + configObject: kcm.settings + settingName: "InputMethod" + } + + view.delegate: KCM.GridDelegate { + text: model.display + toolTip: model.toolTip + + thumbnailAvailable: model.decoration + thumbnail: Kirigami.Icon { + anchors.fill: parent + source: model.decoration + } + onClicked: { + kcm.settings.inputMethod = model.desktopFileName; + } + onDoubleClicked: { + kcm.save(); + } + } +} diff --git a/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/virtualkeyboardsettings.kcfg b/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/virtualkeyboardsettings.kcfg new file mode 100644 index 0000000000..63203a701a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/virtualkeyboardsettings.kcfg @@ -0,0 +1,10 @@ + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/virtualkeyboardsettings.kcfgc b/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/virtualkeyboardsettings.kcfgc new file mode 100644 index 0000000000..84247fc1b3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/virtualkeyboard/virtualkeyboardsettings.kcfgc @@ -0,0 +1,7 @@ +File=virtualkeyboardsettings.kcfg +ClassName=VirtualKeyboardSettings +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true +Notifiers=true diff --git a/local/recipes/kde/kwin/source/src/kcms/xwayland/CMakeLists.txt b/local/recipes/kde/kwin/source/src/kcms/xwayland/CMakeLists.txt new file mode 100644 index 0000000000..69aa3ae774 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/xwayland/CMakeLists.txt @@ -0,0 +1,23 @@ +#SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalzez +#SPDX-License-Identifier: BSD-3-Clause + +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_kwinxwayland\") + +kcmutils_generate_module_data( + kcm_kwinxwayland_PART_SRCS + MODULE_DATA_HEADER kwinxwaylanddata.h + MODULE_DATA_CLASS_NAME KWinXwaylandData + SETTINGS_HEADERS kwinxwaylandsettings.h + SETTINGS_CLASSES KWinXwaylandSettings +) + +kconfig_add_kcfg_files(kcm_kwinxwayland_PART_SRCS kwinxwaylandsettings.kcfgc GENERATE_MOC) +kcmutils_add_qml_kcm(kcm_kwinxwayland SOURCES kcmkwinxwayland.cpp ${kcm_kwinxwayland_PART_SRCS}) + +target_link_libraries(kcm_kwinxwayland PRIVATE + KF6::I18n + KF6::KCMUtils + KF6::KCMUtilsQuick + Qt::DBus + Wayland::Client +) diff --git a/local/recipes/kde/kwin/source/src/kcms/xwayland/Messages.sh b/local/recipes/kde/kwin/source/src/kcms/xwayland/Messages.sh new file mode 100644 index 0000000000..c43cd42c2e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/xwayland/Messages.sh @@ -0,0 +1,2 @@ +#! /usr/bin/env bash +$XGETTEXT `find . -name \*.cpp -o -name \*.h -o -name \*.qml` -o $podir/kcm_kwinxwayland.pot diff --git a/local/recipes/kde/kwin/source/src/kcms/xwayland/kcm_kwinxwayland.json b/local/recipes/kde/kwin/source/src/kcms/xwayland/kcm_kwinxwayland.json new file mode 100644 index 0000000000..538ced8cff --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/xwayland/kcm_kwinxwayland.json @@ -0,0 +1,139 @@ +{ + "Categories": "Qt;KDE;", + "KPlugin": { + "BugReportUrl": "https://bugs.kde.org/enter_bug.cgi?product=systemsettings&component=kcm_kwinxwayland", + "Description": "Select which keys will be globally available to legacy X11 apps", + "Description[ar]": "حدد أي المفاتيح التي ستكون متوفرة لتطبيقات اكس 11 القديمة", + "Description[be]": "Абярыце клавішы, якія будуць глабальна даступныя для састарэлых праграм X11", + "Description[bg]": "Изберете, кои клавиши да бъдат достъпни на глобално ниво за X11 приложения с минали версии", + "Description[ca@valencia]": "Trieu les tecles que estaran disponibles globalment a les aplicacions X11 antigues", + "Description[ca]": "Seleccioneu les tecles que seran disponibles globalment a les aplicacions X11 antigues", + "Description[cs]": "Vyberte, které klávesy budou globálně dostupné pro staré aplikace X11", + "Description[da]": "Vælg hvilke taster, der vil være globalt tilgængelige for ældre X11-apps", + "Description[de]": "Wählen Sie, welche Tasten für alte X11-Anwendungen global zur Verfügung stehen", + "Description[en_GB]": "Select which keys will be globally available to legacy X11 apps", + "Description[eo]": "Elektu kiuj ŝlosiloj estos ĉie haveblaj al heredaj X11-aplikoj", + "Description[es]": "Seleccionar las teclas que estarán disponibles globalmente para las aplicaciones X11 heredadas", + "Description[eu]": "Hautatu ze tekla egongo diren X11 ondare aplikazioetarako globalki erabilgarri", + "Description[fi]": "Valitsee, mitkä näppäimet ovat yleisesti vanhojen X11-sovellusten käytettävissä", + "Description[fr]": "Sélectionnez quelles touches doivent être totalement disponibles pour les applications anciennes sous X11", + "Description[gl]": "Seleccionar que teclas estarán dispoñíbeis globalmente para aplicacións antigas de X11.", + "Description[he]": "נא לבחור אילו מקשים יהיו זמינים באופן מקיף ליישומי X11 מיושנים", + "Description[hu]": "Az örökölt X11 alkalmazások számára globálisan elérhető billentyűk kiválasztása.", + "Description[ia]": "Selige qual claves essera disponibile globalmente a apps de X11 legacy (hereditage)", + "Description[id]": "Pilih kunci mana yang akan secara global tersedia untuk aplikasi lawas X11", + "Description[is]": "Velja hvaða lyklar verða almennt í boði fyrir eldri X11-forrit", + "Description[it]": "Seleziona quali tasti saranno disponibili a livello globale per le applicazioni X11 originali", + "Description[ja]": "レガシーな X11 アプリケーションがグローバルに利用できるキーを選択", + "Description[ka]": "აირჩიეთ, რომელი ღილაკები იქნება გლობალურად ხელმისაწვდომი მოძველებული X11 აპებისთვის", + "Description[ko]": "레거시 X11 앱에 전역적으로 전달할 키 선택", + "Description[lt]": "Pasirinkti, kurie mygtukai bus visuotinai prieinami pasenusioms X11 programoms", + "Description[lv]": "Atlasīt, kuri taustiņi būs globāli pieejami X11 programmām", + "Description[nb]": "Velg hvilke tastetrykk som skal være globalt tilgjengelige for eldre X11-program", + "Description[nl]": "Selecteer welke toetsen globaal beschikbaar zijn aan verouderde X11 apps", + "Description[nn]": "Vel kva tastetrykk som skal vera globalt tilgjengelege for eldre X11-program", + "Description[pl]": "Wybierz klawisze, które będą globalnie dostępne dla starych aplikacji X11", + "Description[pt]": "Seleccione as teclas que estarão disponíveis globalmente para as aplicações antigas em X11", + "Description[pt_BR]": "Selecione quais chaves estarão disponíveis globalmente para aplicativos X11 legados", + "Description[ro]": "Alegeți care taste să fie disponibile global pentru aplicații X11 moștenite", + "Description[ru]": "Поддержка передачи нажатий клавиш в устаревшие приложения X11", + "Description[sa]": "विरासतां X11 एप्स् कृते वैश्विकरूपेण कोऽपि कीलः उपलभ्यते इति चिनोतु", + "Description[sk]": "Výber klávesov, ktoré budú globálne dostupné pre staršie aplikácie X11", + "Description[sl]": "Izberite tipke, ki bodo globalno na voljo za tradicionalne aplikacije X11", + "Description[sv]": "Välj vilka tangenter som ska vara globalt tillgängliga för äldre X11-program", + "Description[ta]": "பழைய X11 செயலிகள் எவ்விசைகளை எப்போது வேண்டுமானாலும் கண்டறிய முடியுமென்பதை அமையுங்கள்", + "Description[tr]": "Eski X11 uygulamalarına hangi düğmelerin global olarak kullanılabilir olacağını seç", + "Description[uk]": "Вибір клавіш, які будуть доступні на загальному рівні програмам X11", + "Description[vi]": "Chọn các phím khả dụng toàn cục cho các ứng dụng X11 đời cũ", + "Description[zh_CN]": "选择对旧式 X11 应用程序全局可用的按键", + "Description[zh_TW]": "選擇舊式 X11 應用程式可以全域使用的按鍵", + "Icon": "xorg", + "Name": "Legacy X11 App Support", + "Name[ar]": "إتاحة تطبيقات اكس11 القديمة", + "Name[be]": "Падтрымка састарэлых праграм X11", + "Name[bg]": "Поддръжка на минали версии X11 приложения", + "Name[ca@valencia]": "Implementació de les aplicacions X11 antigues", + "Name[ca]": "Implementació de les aplicacions X11 antigues", + "Name[cs]": "Podpora starých aplikací X11", + "Name[da]": "Understøttelse af ældre X11-app", + "Name[de]": "Unterstützung für alte X11-Anwendungen", + "Name[en_GB]": "Legacy X11 App Support", + "Name[eo]": "Legacy X11 App Subteno", + "Name[es]": "Compatibilidad de aplicaciones X11 heredadas", + "Name[eu]": "X11 ondare aplikazioen euskarria", + "Name[fi]": "Vanhojen X11-sovellusten tuki", + "Name[fr]": "Prise en charge des applications anciennes sous X11", + "Name[gl]": "Compatibilidade con aplicacións antigas de X11", + "Name[he]": "תמיכה ביישומי X11 מיושנים", + "Name[hu]": "Örökölt X11 alkalmazástámogatás", + "Name[ia]": "Legacy X11 App Support (Supporto de App de X11 de Hereditage)", + "Name[id]": "Dukungan Aplikasi Lawas X11", + "Name[is]": "Stuðningur við eldri X11-forrit", + "Name[it]": "Supporto per le applicazioni X11 originali", + "Name[ja]": "レガシー X11 アプリケーションのサポート", + "Name[ka]": "მოძველებული X11 პროგრამების მხარდაჭერა", + "Name[ko]": "레거시 X11 앱 지원", + "Name[lt]": "Pasenusių X11 programų palaikymas", + "Name[lv]": "Veco X11 programmu atbalsts", + "Name[nb]": "Støtte for eldre X11-program", + "Name[nl]": "Verouderde ondersteuning voor X11 App", + "Name[nn]": "Støtte for eldre X11-program", + "Name[pl]": "Wsparcie starych aplikacji X11", + "Name[pt]": "Suporte para Aplicações de X11 Antigas", + "Name[pt_BR]": "Suporte a aplicativos X11 legados", + "Name[ro]": "Suport pentru aplicații moștenite X11", + "Name[ru]": "Поддержка устаревших приложений X11", + "Name[sa]": "विरासत X11 एप्लिकेशन समर्थन", + "Name[sk]": "Podpora starších aplikácií X11", + "Name[sl]": "Podpora tradicionalnim aplikacijam X11", + "Name[sv]": "Stöd för äldre X11-program", + "Name[ta]": "பழைய X11 செயலிகளுக்கான ஆதரவு", + "Name[tr]": "Eski X11 Uygulamaları Desteği", + "Name[uk]": "Підтримка застарілих програм X11", + "Name[vi]": "Hỗ trợ ứng dụng X11 đời cũ", + "Name[zh_CN]": "旧式 X11 应用程序支持", + "Name[zh_TW]": "舊式 X11 應用程式支援" + }, + "X-KDE-Keywords": "xwayland,global,keys,forward", + "X-KDE-Keywords[ar]": "إكسوايلاند,العالمية,مفاتيح,إلى الأمام", + "X-KDE-Keywords[bg]": "xwayland,глобално,ключове,напред", + "X-KDE-Keywords[ca@valencia]": "xwayland,global,tecles,reenviament", + "X-KDE-Keywords[ca]": "xwayland,global,tecles,reenviament", + "X-KDE-Keywords[de]": "Xwayland,Global,Tasten,weiterleiten", + "X-KDE-Keywords[en_GB]": "xwayland,global,keys,forward", + "X-KDE-Keywords[es]": "xwayland,global,teclas,reenvío", + "X-KDE-Keywords[eu]": "xwayland,globala,teklak,birbidali", + "X-KDE-Keywords[fi]": "xwayland,järjestelmänlaajuiset,yleiset,näppäimet,eteenpäin", + "X-KDE-Keywords[fr]": "xwayland, global, clés, avant", + "X-KDE-Keywords[gl]": "xwayland,global,keys,teclas,forward", + "X-KDE-Keywords[he]": "xwayland,כללי,מפתחות,העברה,מקיף,כוללני,גלובלי", + "X-KDE-Keywords[hu]": "xwayland,globális,billentyűk,továbbítás", + "X-KDE-Keywords[ia]": "xwayland,global,keys,forward", + "X-KDE-Keywords[is]": "xwayland,altækt,lyklar,áfram", + "X-KDE-Keywords[it]": "xwayland,globale,tasti,inoltro", + "X-KDE-Keywords[ja]": "xwayland,global,keys,forward,グローバル,キー,フォーワード", + "X-KDE-Keywords[ka]": "xwayland,global,keys,forward,გლობალური,ღილაკები,წინ", + "X-KDE-Keywords[ko]": "전역,키,전달", + "X-KDE-Keywords[lt]": "xwayland,global,keys,forward,visuotiniai,klavišai,klavisai,pirmyn", + "X-KDE-Keywords[lv]": "xwayland,globāli,taustiņi,uz priekšu", + "X-KDE-Keywords[nb]": "xwayland,global,taster,videresende", + "X-KDE-Keywords[nl]": "xwayland,globaal,sleutels,verder", + "X-KDE-Keywords[nn]": "xwayland,global,tastar,vidaresenda", + "X-KDE-Keywords[pl]": "xwayland,globalne,klawisze,przekierowywanie", + "X-KDE-Keywords[pt_BR]": "xwayland,global,teclas,encaminhar", + "X-KDE-Keywords[ro]": "xwayland,global,taste,înaintare", + "X-KDE-Keywords[ru]": "xwayland,global,keys,forward,глобальный,клавиши,вперёд", + "X-KDE-Keywords[sa]": "xwayland,global,keys,अग्रे", + "X-KDE-Keywords[sk]": "xwayland,global,keys,forward", + "X-KDE-Keywords[sl]": "xwayland,globalno,tipke,naprej", + "X-KDE-Keywords[sv]": "xwayland,global,tangenter,vidarebefordra", + "X-KDE-Keywords[tr]": "xwayland,global,düğmeler,tuşlar,ileri", + "X-KDE-Keywords[uk]": "xwayland,global,keys,forward,іксвейленд,загальне,клавіші,переспрямування,вперед", + "X-KDE-Keywords[zh_CN]": "xwayland,global,keys,forward,quanju,anjian,jian,jianwei,zhuanfa,chuandi,全局,按键,键,键位,转发,传递", + "X-KDE-Keywords[zh_TW]": "xwayland,全域,案件,轉送", + "X-KDE-OnlyShowOnQtPlatforms": [ + "wayland" + ], + "X-KDE-System-Settings-Parent-Category": "applications-permissions", + "X-KDE-Weight": 20 +} diff --git a/local/recipes/kde/kwin/source/src/kcms/xwayland/kcmkwinxwayland.cpp b/local/recipes/kde/kwin/source/src/kcms/xwayland/kcmkwinxwayland.cpp new file mode 100644 index 0000000000..d2b94025f3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/xwayland/kcmkwinxwayland.cpp @@ -0,0 +1,54 @@ +/* + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kcmkwinxwayland.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +K_PLUGIN_FACTORY_WITH_JSON(KcmXwaylandFactory, "kcm_kwinxwayland.json", registerPlugin(); registerPlugin();) + +KcmXwayland::KcmXwayland(QObject *parent, const KPluginMetaData &metaData) + : KQuickManagedConfigModule(parent, metaData) + , m_data(new KWinXwaylandData(this)) + , m_settings(new KWinXwaylandSettings(m_data)) +{ + registerSettings(m_settings); + qmlRegisterAnonymousType("org.kde.kwin.kwinxwaylandsettings", 1); +} + +void KcmXwayland::logout() const +{ + auto method = QDBusMessage::createMethodCall(QStringLiteral("org.kde.LogoutPrompt"), + QStringLiteral("/LogoutPrompt"), + QStringLiteral("org.kde.LogoutPrompt"), + QStringLiteral("promptLogout")); + QDBusConnection::sessionBus().asyncCall(method); +} + +void KcmXwayland::save() +{ + bool modifiedXwaylandEis = m_settings->xwaylandEisNoPromptItem()->isSaveNeeded(); + KQuickManagedConfigModule::save(); + if (modifiedXwaylandEis) { + Q_EMIT showLogoutMessage(); + } +} + +KcmXwayland::~KcmXwayland() = default; + +#include "kcmkwinxwayland.moc" + +#include "moc_kcmkwinxwayland.cpp" diff --git a/local/recipes/kde/kwin/source/src/kcms/xwayland/kcmkwinxwayland.h b/local/recipes/kde/kwin/source/src/kcms/xwayland/kcmkwinxwayland.h new file mode 100644 index 0000000000..1f678ff0e7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/xwayland/kcmkwinxwayland.h @@ -0,0 +1,42 @@ +/* + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include + +class KWinXwaylandData; + +class KcmXwayland : public KQuickManagedConfigModule +{ + Q_OBJECT + Q_PROPERTY(KWinXwaylandSettings *settings READ settings CONSTANT) + +public: + explicit KcmXwayland(QObject *parent, const KPluginMetaData &metaData); + ~KcmXwayland() override; + + KWinXwaylandSettings *settings() const + { + return m_settings; + } + + void save() override; + + Q_INVOKABLE void logout() const; + +Q_SIGNALS: + void showLogoutMessage(); + +private: + void refresh(); + + KWinXwaylandData *const m_data; + KWinXwaylandSettings *const m_settings; +}; diff --git a/local/recipes/kde/kwin/source/src/kcms/xwayland/kwinxwaylandsettings.kcfg b/local/recipes/kde/kwin/source/src/kcms/xwayland/kwinxwaylandsettings.kcfg new file mode 100644 index 0000000000..930d31a2c9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/xwayland/kwinxwaylandsettings.kcfg @@ -0,0 +1,24 @@ + + + + + + + + + + + + Combinations + + + false + + + false + + + diff --git a/local/recipes/kde/kwin/source/src/kcms/xwayland/kwinxwaylandsettings.kcfgc b/local/recipes/kde/kwin/source/src/kcms/xwayland/kwinxwaylandsettings.kcfgc new file mode 100644 index 0000000000..4344685fce --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/xwayland/kwinxwaylandsettings.kcfgc @@ -0,0 +1,9 @@ +File=kwinxwaylandsettings.kcfg +ClassName=KWinXwaylandSettings +Mutators=true +DefaultValueGetters=true +GenerateProperties=true +ParentInConstructor=true +Notifiers=true +ItemAccessors=true +GlobalEnums=true diff --git a/local/recipes/kde/kwin/source/src/kcms/xwayland/ui/main.qml b/local/recipes/kde/kwin/source/src/kcms/xwayland/ui/main.qml new file mode 100644 index 0000000000..87da4dd1be --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kcms/xwayland/ui/main.qml @@ -0,0 +1,286 @@ +/* + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 +import org.kde.kirigami as Kirigami +import org.kde.kcmutils as KCM +import org.kde.kwin.kwinxwaylandsettings +import org.kde.kquickcontrols + +KCM.SimpleKCM { + id: root + + implicitWidth: Kirigami.Units.gridUnit * 48 + implicitHeight: Kirigami.Units.gridUnit * 33 + + header: Kirigami.InlineMessage { + id: takeEffectNextTimeMsg + Layout.fillWidth: true + type: Kirigami.MessageType.Information + position: Kirigami.InlineMessage.Position.Header + text: i18nc("@info", "Changes will take effect the next time you log in.") + actions: [ + Kirigami.Action { + icon.name: "system-log-out-symbolic" + text: i18nc("@action:button", "Log Out Now") + onTriggered: { + kcm.logout() + } + } + ] + Connections { + target: kcm + function onShowLogoutMessage() { + takeEffectNextTimeMsg.visible = true; + } + } + } + + ColumnLayout { + id: column + spacing: 0 + + QQC2.Label { + Layout.fillWidth: true + Layout.margins: Kirigami.Units.gridUnit + text: xi18nc("@info:usagetip", "Legacy X11 apps with global shortcuts and other features accessed while running in the background need to be able to listen for keystrokes all the time.If you use any of these apps, you can choose your preferred balance of security and compatibility here.") + wrapMode: Text.Wrap + } + + Kirigami.Separator { + Layout.fillWidth: true + Layout.leftMargin: Kirigami.Units.gridUnit + Layout.rightMargin: Kirigami.Units.gridUnit + } + + Kirigami.FormLayout { + id: keySnoopingLayout + + Layout.leftMargin: Kirigami.Units.gridUnit + Layout.rightMargin: Kirigami.Units.gridUnit + + twinFormLayouts: bottomLayout + + QQC2.ButtonGroup { id: keyboardSnoopingGroup } + + ColumnLayout { + Kirigami.FormData.label: i18nc("@title:group", "Listening for keystrokes:") + Kirigami.FormData.buddyFor: prohibited + Layout.fillWidth: true + spacing : 0 + + QQC2.RadioButton { + id: prohibited + text: i18nc("@option:radio Listening for keystrokes is prohibited", "Prohibited") + QQC2.ButtonGroup.group: keyboardSnoopingGroup + checked: kcm.settings.xwaylandEavesdrops === 0 + onToggled: if (checked) kcm.settings.xwaylandEavesdrops = 0 + + KCM.SettingStateBinding { + configObject: kcm.settings + settingName: "xwaylandEavesdrops" + } + } + QQC2.Label { + Layout.fillWidth: true + leftPadding: Application.layoutDirection === Qt.LeftToRight ? + prohibited.contentItem.leftPadding : padding + rightPadding: Application.layoutDirection === Qt.RightToLeft ? + prohibited.contentItem.rightPadding : padding + text: i18nc("@info:usagetip", "Most secure; global shortcuts will not work in X11 apps") + textFormat: Text.PlainText + wrapMode: Text.Wrap + elide: Text.ElideRight + font: Kirigami.Theme.smallFont + opacity: 0.8 + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing : 0 + QQC2.RadioButton { + id: modifierKeysOnly + text: i18nc("@option:radio Listening for keystrokes is allowed for…", "Only the Meta, Control, Alt, and Shift keys") + QQC2.ButtonGroup.group: keyboardSnoopingGroup + checked: kcm.settings.xwaylandEavesdrops === 1 + onToggled: if (checked) kcm.settings.xwaylandEavesdrops = 1 + + KCM.SettingStateBinding { + configObject: kcm.settings + settingName: "xwaylandEavesdrops" + } + } + QQC2.Label { + Layout.fillWidth: true + leftPadding: Application.layoutDirection === Qt.LeftToRight ? + modifierKeysOnly.contentItem.leftPadding : padding + rightPadding: Application.layoutDirection === Qt.RightToLeft ? + modifierKeysOnly.contentItem.rightPadding : padding + text: i18nc("@info:usagetip", "High security; push-to-talk and other modifier-only global shortcuts will work in X11 apps") + textFormat: Text.PlainText + wrapMode: Text.Wrap + elide: Text.ElideRight + font: Kirigami.Theme.smallFont + opacity: 0.8 + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing : 0 + QQC2.RadioButton { + id: charsWithModifierKeys + text: i18nc("@option:radio Listening for keystrokes is allowed for…", "As above, plus any key pressed while the Control, Alt, or Meta key is also pressed") + QQC2.ButtonGroup.group: keyboardSnoopingGroup + checked: kcm.settings.xwaylandEavesdrops === 2 + onToggled: if (checked) kcm.settings.xwaylandEavesdrops = 2 + + KCM.SettingStateBinding { + configObject: kcm.settings + settingName: "xwaylandEavesdrops" + } + } + QQC2.Label { + Layout.fillWidth: true + leftPadding: Application.layoutDirection === Qt.LeftToRight ? + charsWithModifierKeys.contentItem.leftPadding : padding + rightPadding: Application.layoutDirection === Qt.RightToLeft ? + charsWithModifierKeys.contentItem.rightPadding : padding + text: i18nc("@info:usagetip", "Moderate security; all global shortcuts will work in X11 apps") + textFormat: Text.PlainText + wrapMode: Text.Wrap + elide: Text.ElideRight + font: Kirigami.Theme.smallFont + opacity: 0.8 + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing : 0 + QQC2.RadioButton { + id: always + text: i18nc("@option:radio Listening for keystrokes is always allowed","Always allowed") + QQC2.ButtonGroup.group: keyboardSnoopingGroup + checked: kcm.settings.xwaylandEavesdrops === 3 + onToggled: if (checked) kcm.settings.xwaylandEavesdrops = 3 + + KCM.SettingStateBinding { + configObject: kcm.settings + settingName: "xwaylandEavesdrops" + } + } + QQC2.Label { + Layout.fillWidth: true + leftPadding: Application.layoutDirection === Qt.LeftToRight ? + always.contentItem.leftPadding : padding + rightPadding: Application.layoutDirection === Qt.RightToLeft ? + always.contentItem.rightPadding : padding + text: i18nc("@info:usagetip", "Least secure; all X11 apps will be able to see any text you type into any application") + textFormat: Text.PlainText + wrapMode: Text.Wrap + elide: Text.ElideRight + font: Kirigami.Theme.smallFont + opacity: 0.8 + } + } + } + + Kirigami.InlineMessage { + Layout.fillWidth: true + Layout.margins: Kirigami.Units.gridUnit + type: Kirigami.MessageType.Warning + text: i18nc("@info:usagetip", "Note that using this setting will reduce system security and permit malicious software to steal passwords and spy on the text that you type. Make sure you understand and accept this risk.") + visible: always.checked + } + Kirigami.FormLayout { + id: bottomLayout + + Layout.leftMargin: Kirigami.Units.gridUnit + Layout.rightMargin: Kirigami.Units.gridUnit + + twinFormLayouts: keySnoopingLayout + + Item { + Kirigami.FormData.isSection: true + } + + ColumnLayout { + Kirigami.FormData.label: i18nc("@title:group", "Listening for mouse buttons:") + Kirigami.FormData.buddyFor: mouseButtonSnooping + Layout.fillWidth: true + spacing: 0 + QQC2.CheckBox { + id: mouseButtonSnooping + text: i18nc("@option:radio Listening for mouse buttons is allowed","Allowed") + checked: kcm.settings.xwaylandEavesdropsMouse + onToggled: kcm.settings.xwaylandEavesdropsMouse = checked + enabled: !prohibited.checked + + KCM.SettingStateBinding { + configObject: kcm.settings + settingName: "xwaylandEavesdropsMouse" + } + } + QQC2.Label { + Layout.fillWidth: true + leftPadding: Application.layoutDirection === Qt.LeftToRight ? + mouseButtonSnooping.contentItem.leftPadding : padding + rightPadding: Application.layoutDirection === Qt.RightToLeft ? + mouseButtonSnooping.contentItem.rightPadding : padding + text: i18nc("@info:usagetip", "Moderate security; global shortcuts involving mouse buttons will work in X11 apps") + textFormat: Text.PlainText + wrapMode: Text.Wrap + elide: Text.ElideRight + font: Kirigami.Theme.smallFont + opacity: 0.8 + } + } + + Item { + Kirigami.FormData.isSection: true + } + + ColumnLayout { + Kirigami.FormData.label: i18nc("@title:group", "Control of pointer and keyboard:") + Kirigami.FormData.buddyFor: totalControl + Layout.fillWidth: true + spacing : 0 + QQC2.CheckBox { + id: totalControl + text: i18nc("@option:check Allow control of pointer and keyboard without asking for permission", "Allow without asking for permission") + checked: kcm.settings.xwaylandEisNoPrompt + onToggled: kcm.settings.xwaylandEisNoPrompt = checked + } + QQC2.Label { + Layout.fillWidth: true + leftPadding: Application.layoutDirection === Qt.LeftToRight ? + totalControl.contentItem.leftPadding : padding + rightPadding: Application.layoutDirection === Qt.RightToLeft ? + totalControl.contentItem.rightPadding : padding + text: i18nc("@info:usagetip", "Least secure; all X11 apps will be able to take control of the computer") + textFormat: Text.PlainText + wrapMode: Text.Wrap + elide: Text.ElideRight + font: Kirigami.Theme.smallFont + opacity: 0.8 + } + } + } + + Kirigami.InlineMessage { + Layout.fillWidth: true + Layout.margins: Kirigami.Units.gridUnit + type: Kirigami.MessageType.Warning + text: i18nc("@info:usagetip", "Note that using this setting will drastically reduce system security and permit malicious software to take complete control of your pointer and keyboard. Make sure you understand and accept this risk.") + visible: totalControl.checked + } + } +} diff --git a/local/recipes/kde/kwin/source/src/keyboard_input.cpp b/local/recipes/kde/kwin/source/src/keyboard_input.cpp new file mode 100644 index 0000000000..23108b2d85 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/keyboard_input.cpp @@ -0,0 +1,348 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013, 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "keyboard_input.h" + +#include "config-kwin.h" + +#include "input_event.h" +#include "input_event_spy.h" +#include "inputmethod.h" +#include "keyboard_layout.h" +#include "keyboard_repeat.h" +#include "wayland/datadevice.h" +#include "wayland/display.h" +#include "wayland/keyboard.h" +#include "wayland/seat.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" +#include "xkb.h" +// screenlocker +#if KWIN_BUILD_SCREENLOCKER +#include +#endif +#if KWIN_BUILD_TABBOX +#include "tabbox/tabbox.h" +#endif +// Frameworks +#include +// Qt +#include + +#include + +namespace KWin +{ + +KeyboardInputRedirection::KeyboardInputRedirection(InputRedirection *parent) + : QObject(parent) + , m_input(parent) + , m_xkb(new Xkb(kwinApp()->followLocale1())) +{ + connect(m_xkb.get(), &Xkb::ledsChanged, this, &KeyboardInputRedirection::ledsChanged); + m_xkb->setSeat(waylandServer()->seat()); +} + +KeyboardInputRedirection::~KeyboardInputRedirection() = default; + +Xkb *KeyboardInputRedirection::xkb() const +{ + return m_xkb.get(); +} + +Qt::KeyboardModifiers KeyboardInputRedirection::modifiers() const +{ + return m_xkb->modifiers(); +} + +Qt::KeyboardModifiers KeyboardInputRedirection::modifiersRelevantForGlobalShortcuts() const +{ + return m_xkb->modifiersRelevantForGlobalShortcuts(); +} + +KeyboardLayout *KeyboardInputRedirection::keyboardLayout() const +{ + return m_keyboardLayout; +} + +QList KeyboardInputRedirection::pressedKeys() const +{ + return m_pressedKeys; +} + +QList KeyboardInputRedirection::filteredKeys() const +{ + return m_filteredKeys; +} + +QList KeyboardInputRedirection::unfilteredKeys() const +{ + QList ret = m_pressedKeys; + for (const uint32_t &key : m_filteredKeys) { + ret.removeOne(key); + } + return ret; +} + +void KeyboardInputRedirection::addFilteredKey(uint32_t key) +{ + if (!m_filteredKeys.contains(key)) { + m_filteredKeys.append(key); + } +} + +class KeyStateChangedSpy : public InputEventSpy +{ +public: + KeyStateChangedSpy(InputRedirection *input) + : m_input(input) + { + } + + void keyboardKey(KeyboardKeyEvent *event) override + { + if (event->state == KeyboardKeyState::Repeated) { + return; + } + Q_EMIT m_input->keyStateChanged(event->nativeScanCode, event->state); + } + +private: + InputRedirection *m_input; +}; + +class ModifiersChangedSpy : public InputEventSpy +{ +public: + ModifiersChangedSpy(InputRedirection *input) + : m_input(input) + , m_modifiers() + { + } + + void keyboardKey(KeyboardKeyEvent *event) override + { + if (event->state == KeyboardKeyState::Repeated) { + return; + } + + const Qt::KeyboardModifiers mods = event->modifiers; + if (mods == m_modifiers) { + return; + } + Q_EMIT m_input->keyboardModifiersChanged(mods, m_modifiers); + m_modifiers = mods; + } + +private: + InputRedirection *m_input; + Qt::KeyboardModifiers m_modifiers; +}; + +void KeyboardInputRedirection::init() +{ + Q_ASSERT(!m_inited); + m_inited = true; + const auto config = kwinApp()->kxkbConfig(); + m_xkb->setNumLockConfig(kwinApp()->inputConfig()); + m_xkb->setConfig(config); + + waylandServer()->seat()->setHasKeyboard(true); + + m_input->installInputEventSpy(new KeyStateChangedSpy(m_input)); + m_modifiersChangedSpy = new ModifiersChangedSpy(m_input); + m_input->installInputEventSpy(m_modifiersChangedSpy); + m_keyboardLayout = new KeyboardLayout(m_xkb.get(), config); + m_keyboardLayout->init(); + m_input->installInputEventSpy(m_keyboardLayout); + + m_keyRepeatSpy = new KeyboardRepeat(m_xkb.get()); + connect(m_keyRepeatSpy, &KeyboardRepeat::keyRepeat, this, + std::bind(&KeyboardInputRedirection::processKey, this, std::placeholders::_1, KeyboardKeyState::Repeated, std::placeholders::_2, nullptr)); + + connect(workspace(), &QObject::destroyed, this, [this] { + m_inited = false; + }); + connect(waylandServer(), &QObject::destroyed, this, [this] { + m_inited = false; + }); + connect(workspace(), &Workspace::windowActivated, this, [this] { + disconnect(m_activeWindowSurfaceChangedConnection); + if (auto window = workspace()->activeWindow()) { + m_activeWindowSurfaceChangedConnection = connect(window, &Window::surfaceChanged, this, &KeyboardInputRedirection::update); + } else { + m_activeWindowSurfaceChangedConnection = QMetaObject::Connection(); + } + update(); + }); +#if KWIN_BUILD_SCREENLOCKER + if (kwinApp()->supportsLockScreen()) { + connect(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged, this, &KeyboardInputRedirection::update); + } +#endif + + reconfigure(); +} + +void KeyboardInputRedirection::reconfigure() +{ + if (!m_inited) { + return; + } + if (waylandServer()->seat()->keyboard()) { + const auto config = kwinApp()->inputConfig()->group(QStringLiteral("Keyboard")); + const int delay = config.readEntry("RepeatDelay", 600); + const int rate = std::ceil(config.readEntry("RepeatRate", 25.0)); + const QString repeatMode = config.readEntry("KeyRepeat", "repeat"); + // when the clients will repeat the character or turn repeat key events into an accent character selection, we want + // to tell the clients that we are indeed repeating keys. + const bool enabled = repeatMode == QLatin1String("accent") || repeatMode == QLatin1String("repeat"); + + waylandServer()->seat()->keyboard()->setRepeatInfo(enabled ? rate : 0, delay); + } +} + +Window *KeyboardInputRedirection::pickFocus() const +{ + if (waylandServer()->isScreenLocked()) { + const QList &stacking = Workspace::self()->stackingOrder(); + if (!stacking.isEmpty()) { + auto it = stacking.end(); + do { + --it; + Window *t = (*it); + if (t->isDeleted()) { + // a deleted window doesn't get mouse events + continue; + } + if (!t->isLockScreen()) { + continue; + } + if (!t->readyForPainting()) { + continue; + } + return t; + } while (it != stacking.begin()); + } + return nullptr; + } + + if (input()->isSelectingWindow()) { + return nullptr; + } + +#if KWIN_BUILD_TABBOX + if (workspace()->tabbox()->isGrabbed()) { + return nullptr; + } +#endif + + return workspace()->activeWindow(); +} + +void KeyboardInputRedirection::update() +{ + if (!m_inited) { + return; + } + auto seat = waylandServer()->seat(); + + // TODO: this needs better integration + Window *found = pickFocus(); + if (found && found->surface()) { + if (found->surface() != seat->focusedKeyboardSurface()) { + seat->setFocusedKeyboardSurface(found->surface(), unfilteredKeys()); + } + } else { + seat->setFocusedKeyboardSurface(nullptr); + } +} + +static constexpr std::array s_modifierKeys = { + Qt::Key_Control, + Qt::Key_Alt, + Qt::Key_AltGr, + Qt::Key_Meta, + Qt::Key_CapsLock, + Qt::Key_NumLock, + Qt::Key_Shift, + Qt::Key_ScrollLock, +}; + +void KeyboardInputRedirection::processKey(uint32_t key, KeyboardKeyState state, std::chrono::microseconds time, InputDevice *device) +{ + input()->setLastInputHandler(this); + if (!m_inited) { + return; + } + + m_keyRepeatSpy->keyboardKey(key, state, time); + + if (!waylandServer()->isKeyboardShortcutsInhibited()) { + const bool ret = m_a11yKeyboardMonitor.processKey(key, state, time); + if (ret) { + return; + } + } + + if (state == KeyboardKeyState::Pressed) { + if (!m_pressedKeys.contains(key)) { + m_pressedKeys.append(key); + } + } else if (state == KeyboardKeyState::Released) { + m_pressedKeys.removeOne(key); + } + + const quint32 previousLayout = m_xkb->currentLayout(); + if (state != KeyboardKeyState::Repeated) { + m_xkb->updateKey(key, state); + } + + const xkb_keysym_t keySym = m_xkb->toKeysym(key); + const Qt::KeyboardModifiers globalShortcutsModifiers = m_xkb->modifiersRelevantForGlobalShortcuts(key); + + KeyboardKeyEvent event{ + .device = device, + .state = state, + .key = m_xkb->toQtKey(keySym, key, globalShortcutsModifiers ? Qt::ControlModifier : Qt::KeyboardModifiers()), + .nativeScanCode = key, + .nativeVirtualKey = keySym, + .text = m_xkb->toString(m_xkb->currentKeysym()), + .modifiers = m_xkb->modifiers(), + .modifiersRelevantForGlobalShortcuts = m_xkb->modifiersRelevantForGlobalShortcuts(key), + .timestamp = time, + .serial = waylandServer()->display()->nextSerial(), + }; + if (state == KeyboardKeyState::Pressed && !std::ranges::contains(s_modifierKeys, event.key)) { + input()->setLastInteractionSerial(event.serial); + if (auto f = pickFocus()) { + f->setLastUsageSerial(event.serial); + } + } + + m_input->processSpies(&InputEventSpy::keyboardKey, &event); + m_input->processFilters(&InputEventFilter::keyboardKey, &event); + + if (state == KeyboardKeyState::Released) { + m_filteredKeys.removeOne(key); + } + + m_xkb->forwardModifiers(); + if (auto *inputmethod = kwinApp()->inputMethod()) { + inputmethod->forwardModifiers(InputMethod::NoForce); + } + + if (event.modifiersRelevantForGlobalShortcuts == Qt::KeyboardModifier::NoModifier && state != KeyboardKeyState::Released) { + m_keyboardLayout->checkLayoutChange(previousLayout); + } +} + +} + +#include "moc_keyboard_input.cpp" diff --git a/local/recipes/kde/kwin/source/src/keyboard_input.h b/local/recipes/kde/kwin/source/src/keyboard_input.h new file mode 100644 index 0000000000..a0af687d5b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/keyboard_input.h @@ -0,0 +1,94 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013, 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "a11ykeyboardmonitor.h" +#include "input.h" + +#include +#include +#include + +#include + +class QWindow; +struct xkb_context; +struct xkb_keymap; +struct xkb_state; +struct xkb_compose_table; +struct xkb_compose_state; +typedef uint32_t xkb_mod_index_t; +typedef uint32_t xkb_led_index_t; +typedef uint32_t xkb_keysym_t; +typedef uint32_t xkb_layout_index_t; + +namespace KWin +{ + +class Window; +class InputDevice; +class InputRedirection; +class KeyboardLayout; +class ModifiersChangedSpy; +class Xkb; +class KeyboardRepeat; + +class KWIN_EXPORT KeyboardInputRedirection : public QObject +{ + Q_OBJECT +public: + explicit KeyboardInputRedirection(InputRedirection *parent); + ~KeyboardInputRedirection() override; + + void init(); + void reconfigure(); + + void update(); + + /** + * @internal + */ + void processKey(uint32_t key, KeyboardKeyState state, std::chrono::microseconds time, InputDevice *device = nullptr); + + Xkb *xkb() const; + Qt::KeyboardModifiers modifiers() const; + Qt::KeyboardModifiers modifiersRelevantForGlobalShortcuts() const; + KeyboardLayout *keyboardLayout() const; + QList pressedKeys() const; + + /** + * Keys that have been captured by input event filters and should not be reported to clients. + */ + QList filteredKeys() const; + void addFilteredKey(uint32_t key); + + /** + * Pressed keys without the filtered keys. + */ + QList unfilteredKeys() const; + +Q_SIGNALS: + void ledsChanged(KWin::LEDs); + +private: + Window *pickFocus() const; + + InputRedirection *m_input; + bool m_inited = false; + const std::unique_ptr m_xkb; + QMetaObject::Connection m_activeWindowSurfaceChangedConnection; + ModifiersChangedSpy *m_modifiersChangedSpy = nullptr; + KeyboardLayout *m_keyboardLayout = nullptr; + QList m_pressedKeys; + QList m_filteredKeys; + A11yKeyboardMonitor m_a11yKeyboardMonitor; + KeyboardRepeat *m_keyRepeatSpy; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/keyboard_layout.cpp b/local/recipes/kde/kwin/source/src/keyboard_layout.cpp new file mode 100644 index 0000000000..943fe01677 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/keyboard_layout.cpp @@ -0,0 +1,268 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016, 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "keyboard_layout.h" +#include "input_event.h" +#include "keyboard_input.h" +#include "keyboard_layout_switching.h" +#include "xkb.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +KeyboardLayout::KeyboardLayout(Xkb *xkb, const KSharedConfigPtr &config) + : QObject() + , m_xkb(xkb) + , m_configWatcher(KConfigWatcher::create(config)) + , m_configGroup(config->group(QStringLiteral("Layout"))) +{ +} + +KeyboardLayout::~KeyboardLayout() = default; + +static QString translatedLayout(const QString &layout) +{ + return i18nd("xkeyboard-config", layout.toUtf8().constData()); +} + +void KeyboardLayout::init() +{ + QAction *switchKeyboardAction = new QAction(this); + switchKeyboardAction->setObjectName(QStringLiteral("Switch to Next Keyboard Layout")); + switchKeyboardAction->setProperty("componentName", QStringLiteral("KDE Keyboard Layout Switcher")); + switchKeyboardAction->setProperty("componentDisplayName", i18n("Keyboard Layout Switcher")); + const QKeySequence sequence = QKeySequence(Qt::META | Qt::ALT | Qt::Key_K); + KGlobalAccel::self()->setDefaultShortcut(switchKeyboardAction, QList({sequence})); + KGlobalAccel::self()->setShortcut(switchKeyboardAction, QList({sequence})); + + connect(switchKeyboardAction, &QAction::triggered, this, &KeyboardLayout::switchToNextLayout); + + QAction *switchLastUsedKeyboardAction = new QAction(this); + switchLastUsedKeyboardAction->setObjectName(QStringLiteral("Switch to Last-Used Keyboard Layout")); + switchLastUsedKeyboardAction->setProperty("componentName", QStringLiteral("KDE Keyboard Layout Switcher")); + switchLastUsedKeyboardAction->setProperty("componentDisplayName", i18n("Keyboard Layout Switcher")); + const QKeySequence sequenceLastUsed = QKeySequence(Qt::META | Qt::ALT | Qt::Key_L); + KGlobalAccel::self()->setDefaultShortcut(switchLastUsedKeyboardAction, QList({sequenceLastUsed})); + KGlobalAccel::self()->setShortcut(switchLastUsedKeyboardAction, QList({sequenceLastUsed})); + + connect(switchLastUsedKeyboardAction, &QAction::triggered, this, &KeyboardLayout::switchToLastUsedLayout); + + connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, &KeyboardLayout::handleXkbConfigChanged); + + reconfigure(); + + m_dbusInterface = new KeyboardLayoutDBusInterface(m_xkb, m_configGroup, this); + connect(this, &KeyboardLayout::layoutChanged, + m_dbusInterface, &KeyboardLayoutDBusInterface::layoutChanged); + // TODO: the signal might be emitted even if the list didn't change + connect(this, &KeyboardLayout::layoutsReconfigured, m_dbusInterface, &KeyboardLayoutDBusInterface::layoutListChanged); +} + +void KeyboardLayout::switchToNextLayout() +{ + const quint32 previousLayout = m_xkb->currentLayout(); + m_xkb->switchToNextLayout(); + checkLayoutChange(previousLayout); +} + +void KeyboardLayout::switchToPreviousLayout() +{ + const quint32 previousLayout = m_xkb->currentLayout(); + m_xkb->switchToPreviousLayout(); + checkLayoutChange(previousLayout); +} + +void KeyboardLayout::switchToLayout(xkb_layout_index_t index) +{ + const quint32 previousLayout = m_xkb->currentLayout(); + m_xkb->switchToLayout(index); + checkLayoutChange(previousLayout); +} + +void KeyboardLayout::switchToLastUsedLayout() +{ + const quint32 count = m_xkb->numberOfLayouts(); + if (!m_lastUsedLayout.has_value() || *m_lastUsedLayout >= count) { + switchToPreviousLayout(); + } else { + switchToLayout(*m_lastUsedLayout); + } +} + +void KeyboardLayout::handleXkbConfigChanged(const KConfigGroup &group) +{ + if (group.name() == QStringLiteral("Layout")) { + reconfigure(); + } +} + +void KeyboardLayout::reconfigure() +{ + if (m_configGroup.isValid()) { + m_configGroup.config()->reparseConfiguration(); + const QString policyKey = m_configGroup.readEntry("SwitchMode", QStringLiteral("Global")); + m_xkb->reconfigure(); + if (!m_policy || m_policy->name() != policyKey) { + m_policy = KeyboardLayoutSwitching::Policy::create(m_xkb, this, m_configGroup, policyKey); + } + } else { + m_xkb->reconfigure(); + } + resetLayout(); +} + +void KeyboardLayout::resetLayout() +{ + m_layout = m_xkb->currentLayout(); + loadShortcuts(); + + Q_EMIT layoutsReconfigured(); +} + +void KeyboardLayout::loadShortcuts() +{ + qDeleteAll(m_layoutShortcuts); + m_layoutShortcuts.clear(); + const QString componentName = QStringLiteral("KDE Keyboard Layout Switcher"); + const QString componentDisplayName = i18n("Keyboard Layout Switcher"); + const quint32 count = m_xkb->numberOfLayouts(); + for (uint i = 0; i < count; ++i) { + // layout name is translated in the action name in keyboard kcm! + const QString action = QStringLiteral("Switch keyboard layout to %1").arg(translatedLayout(m_xkb->layoutName(i))); + const auto shortcuts = KGlobalAccel::self()->globalShortcut(componentName, action); + if (shortcuts.isEmpty()) { + continue; + } + QAction *a = new QAction(this); + a->setObjectName(action); + a->setProperty("componentName", componentName); + a->setProperty("componentDisplayName", componentDisplayName); + connect(a, &QAction::triggered, this, + std::bind(&KeyboardLayout::switchToLayout, this, i)); + KGlobalAccel::self()->setShortcut(a, shortcuts, KGlobalAccel::Autoloading); + m_layoutShortcuts << a; + } +} + +void KeyboardLayout::checkLayoutChange(uint previousLayout) +{ + // Get here on key event or DBus call. + // m_layout - layout saved last time OSD occurred + // previousLayout - actual layout just before potential layout change + // We need OSD if current layout deviates from any of these + const uint currentLayout = m_xkb->currentLayout(); + if (m_layout != currentLayout || previousLayout != currentLayout) { + m_lastUsedLayout = std::optional{previousLayout}; + m_layout = currentLayout; + notifyLayoutChange(); + Q_EMIT layoutChanged(currentLayout); + } +} + +void KeyboardLayout::notifyLayoutChange() +{ + // notify OSD service about the new layout + QDBusMessage msg = QDBusMessage::createMethodCall( + QStringLiteral("org.kde.plasmashell"), + QStringLiteral("/org/kde/osdService"), + QStringLiteral("org.kde.osdService"), + QStringLiteral("kbdLayoutChanged")); + + msg << translatedLayout(m_xkb->layoutName()); + + QDBusConnection::sessionBus().asyncCall(msg); +} + +static const QString s_keyboardService = QStringLiteral("org.kde.keyboard"); +static const QString s_keyboardObject = QStringLiteral("/Layouts"); + +KeyboardLayoutDBusInterface::KeyboardLayoutDBusInterface(Xkb *xkb, const KConfigGroup &configGroup, KeyboardLayout *parent) + : QObject(parent) + , m_xkb(xkb) + , m_configGroup(configGroup) + , m_keyboardLayout(parent) +{ + qRegisterMetaType>("QList"); + qDBusRegisterMetaType(); + qDBusRegisterMetaType>(); + + QDBusConnection::sessionBus().registerObject(s_keyboardObject, this, QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals); + QDBusConnection::sessionBus().registerService(s_keyboardService); +} + +KeyboardLayoutDBusInterface::~KeyboardLayoutDBusInterface() +{ + QDBusConnection::sessionBus().unregisterService(s_keyboardService); +} + +void KeyboardLayoutDBusInterface::switchToNextLayout() +{ + m_keyboardLayout->switchToNextLayout(); +} + +void KeyboardLayoutDBusInterface::switchToPreviousLayout() +{ + m_keyboardLayout->switchToPreviousLayout(); +} + +bool KeyboardLayoutDBusInterface::setLayout(uint index) +{ + const quint32 previousLayout = m_xkb->currentLayout(); + if (!m_xkb->switchToLayout(index)) { + return false; + } + m_keyboardLayout->checkLayoutChange(previousLayout); + return true; +} + +uint KeyboardLayoutDBusInterface::getLayout() const +{ + return m_xkb->currentLayout(); +} + +QList KeyboardLayoutDBusInterface::getLayoutsList() const +{ + // TODO: - should be handled by layout applet itself, it has nothing to do with KWin + const QStringList displayNames = m_configGroup.readEntry("DisplayNames", QStringList()); + + QList ret; + const int layoutsSize = m_xkb->numberOfLayouts(); + const int displayNamesSize = displayNames.size(); + for (int i = 0; i < layoutsSize; ++i) { + ret.append({m_xkb->layoutShortName(i), i < displayNamesSize ? displayNames.at(i) : QString(), translatedLayout(m_xkb->layoutName(i))}); + } + return ret; +} + +QDBusArgument &operator<<(QDBusArgument &argument, const KeyboardLayoutDBusInterface::LayoutNames &layoutNames) +{ + argument.beginStructure(); + argument << layoutNames.shortName << layoutNames.displayName << layoutNames.longName; + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, KeyboardLayoutDBusInterface::LayoutNames &layoutNames) +{ + argument.beginStructure(); + argument >> layoutNames.shortName >> layoutNames.displayName >> layoutNames.longName; + argument.endStructure(); + return argument; +} + +} + +#include "moc_keyboard_layout.cpp" diff --git a/local/recipes/kde/kwin/source/src/keyboard_layout.h b/local/recipes/kde/kwin/source/src/keyboard_layout.h new file mode 100644 index 0000000000..2768d137d6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/keyboard_layout.h @@ -0,0 +1,110 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016, 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "input_event_spy.h" +#include +#include +#include +#include + +#include +#include +#include +typedef uint32_t xkb_layout_index_t; + +class QAction; +class QDBusArgument; + +namespace KWin +{ +class Xkb; +class KeyboardLayoutDBusInterface; + +namespace KeyboardLayoutSwitching +{ +class Policy; +} + +class KWIN_EXPORT KeyboardLayout : public QObject, public InputEventSpy +{ + Q_OBJECT +public: + explicit KeyboardLayout(Xkb *xkb, const KSharedConfigPtr &config); + + ~KeyboardLayout() override; + + void init(); + + void checkLayoutChange(uint previousLayout); + void switchToNextLayout(); + void switchToPreviousLayout(); + void switchToLastUsedLayout(); + void resetLayout(); + +Q_SIGNALS: + void layoutChanged(uint index); + void layoutsReconfigured(); + +private Q_SLOTS: + void handleXkbConfigChanged(const KConfigGroup &group); + +private: + void notifyLayoutChange(); + void switchToLayout(xkb_layout_index_t index); + void loadShortcuts(); + void reconfigure(); + Xkb *m_xkb; + xkb_layout_index_t m_layout = 0; + KConfigWatcher::Ptr m_configWatcher; + KConfigGroup m_configGroup; + QList m_layoutShortcuts; + KeyboardLayoutDBusInterface *m_dbusInterface = nullptr; + std::unique_ptr m_policy; + std::optional m_lastUsedLayout; +}; + +class KeyboardLayoutDBusInterface : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.KeyboardLayouts") + +public: + explicit KeyboardLayoutDBusInterface(Xkb *xkb, const KConfigGroup &configGroup, KeyboardLayout *parent); + ~KeyboardLayoutDBusInterface() override; + + struct LayoutNames + { + QString shortName; + QString displayName; + QString longName; + }; + +public Q_SLOTS: + void switchToNextLayout(); + void switchToPreviousLayout(); + bool setLayout(uint index); + uint getLayout() const; + QList getLayoutsList() const; + +Q_SIGNALS: + void layoutChanged(uint index); + void layoutListChanged(); + +private: + Xkb *m_xkb; + const KConfigGroup &m_configGroup; + KeyboardLayout *m_keyboardLayout; +}; + +QDBusArgument &operator<<(QDBusArgument &argument, const KeyboardLayoutDBusInterface::LayoutNames &layoutNames); +const QDBusArgument &operator>>(const QDBusArgument &argument, KeyboardLayoutDBusInterface::LayoutNames &layoutNames); + +} +Q_DECLARE_METATYPE(KWin::KeyboardLayoutDBusInterface::LayoutNames) diff --git a/local/recipes/kde/kwin/source/src/keyboard_layout_switching.cpp b/local/recipes/kde/kwin/source/src/keyboard_layout_switching.cpp new file mode 100644 index 0000000000..e5545363e0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/keyboard_layout_switching.cpp @@ -0,0 +1,331 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "keyboard_layout_switching.h" +#include "keyboard_layout.h" +#include "virtualdesktops.h" +#include "window.h" +#include "workspace.h" +#include "xkb.h" + +namespace KWin +{ + +namespace KeyboardLayoutSwitching +{ + +Policy::Policy(Xkb *xkb, KeyboardLayout *layout, const KConfigGroup &config) + : QObject(layout) + , m_config(config) + , m_xkb(xkb) + , m_layout(layout) +{ + connect(m_layout, &KeyboardLayout::layoutsReconfigured, this, &Policy::clearCache); + connect(m_layout, &KeyboardLayout::layoutChanged, this, &Policy::layoutChanged); +} + +Policy::~Policy() = default; + +void Policy::setLayout(uint index) +{ + const uint previousLayout = m_xkb->currentLayout(); + m_xkb->switchToLayout(index); + const uint currentLayout = m_xkb->currentLayout(); + if (previousLayout != currentLayout) { + Q_EMIT m_layout->layoutChanged(currentLayout); + } +} + +std::unique_ptr Policy::create(Xkb *xkb, KeyboardLayout *layout, const KConfigGroup &config, const QString &policy) +{ + if (policy.toLower() == QLatin1StringView("desktop")) { + return std::make_unique(xkb, layout, config); + } + if (policy.toLower() == QLatin1StringView("window")) { + return std::make_unique(xkb, layout); + } + if (policy.toLower() == QLatin1StringView("winclass")) { + return std::make_unique(xkb, layout, config); + } + return std::make_unique(xkb, layout, config); +} + +const char Policy::defaultLayoutEntryKeyPrefix[] = "LayoutDefault"; +const QString Policy::defaultLayoutEntryKey() const +{ + return QLatin1String(defaultLayoutEntryKeyPrefix) % name() % QLatin1Char('_'); +} + +void Policy::clearLayouts() +{ + const QStringList layoutEntryList = m_config.keyList().filter(defaultLayoutEntryKeyPrefix); + for (const auto &layoutEntry : layoutEntryList) { + m_config.deleteEntry(layoutEntry); + } +} + +const QString GlobalPolicy::defaultLayoutEntryKey() const +{ + return QLatin1String(defaultLayoutEntryKeyPrefix) % name(); +} + +GlobalPolicy::GlobalPolicy(Xkb *xkb, KeyboardLayout *_layout, const KConfigGroup &config) + : Policy(xkb, _layout, config) +{ + connect(workspace()->sessionManager(), &SessionManager::prepareSessionSaveRequested, this, [this, xkb](const QString &name) { + clearLayouts(); + if (const uint layout = xkb->currentLayout()) { + m_config.writeEntry(defaultLayoutEntryKey(), layout); + } + }); + + connect(workspace()->sessionManager(), &SessionManager::loadSessionRequested, this, [this, xkb](const QString &name) { + if (xkb->numberOfLayouts() > 1) { + setLayout(m_config.readEntry(defaultLayoutEntryKey(), 0)); + } + }); +} + +GlobalPolicy::~GlobalPolicy() = default; + +VirtualDesktopPolicy::VirtualDesktopPolicy(Xkb *xkb, KeyboardLayout *layout, const KConfigGroup &config) + : Policy(xkb, layout, config) +{ + connect(VirtualDesktopManager::self(), &VirtualDesktopManager::currentChanged, + this, &VirtualDesktopPolicy::desktopChanged); + + connect(workspace()->sessionManager(), &SessionManager::prepareSessionSaveRequested, this, [this](const QString &name) { + clearLayouts(); + + for (auto i = m_layouts.constBegin(); i != m_layouts.constEnd(); ++i) { + if (const uint layout = *i) { + m_config.writeEntry(defaultLayoutEntryKey() % i.key()->id(), layout); + } + } + }); + + connect(workspace()->sessionManager(), &SessionManager::loadSessionRequested, this, [this, xkb](const QString &name) { + if (xkb->numberOfLayouts() > 1) { + const auto &desktops = VirtualDesktopManager::self()->desktops(); + for (KWin::VirtualDesktop *const desktop : desktops) { + const uint layout = m_config.readEntry(defaultLayoutEntryKey() % desktop->id(), 0u); + if (layout) { + m_layouts.insert(desktop, layout); + connect(desktop, &VirtualDesktop::aboutToBeDestroyed, this, [this, desktop]() { + m_layouts.remove(desktop); + }); + } + } + desktopChanged(); + } + }); +} + +VirtualDesktopPolicy::~VirtualDesktopPolicy() = default; + +void VirtualDesktopPolicy::clearCache() +{ + m_layouts.clear(); +} + +namespace +{ +template +quint32 getLayout(const T &layouts, const U &reference) +{ + auto it = layouts.constFind(reference); + if (it == layouts.constEnd()) { + return 0; + } else { + return it.value(); + } +} +} + +void VirtualDesktopPolicy::desktopChanged() +{ + auto d = VirtualDesktopManager::self()->currentDesktop(); + if (!d) { + return; + } + setLayout(getLayout(m_layouts, d)); +} + +void VirtualDesktopPolicy::layoutChanged(uint index) +{ + auto d = VirtualDesktopManager::self()->currentDesktop(); + if (!d) { + return; + } + auto it = m_layouts.find(d); + if (it == m_layouts.end()) { + m_layouts.insert(d, index); + connect(d, &VirtualDesktop::aboutToBeDestroyed, this, [this, d]() { + m_layouts.remove(d); + }); + } else { + if (it.value() == index) { + return; + } + it.value() = index; + } +} + +WindowPolicy::WindowPolicy(KWin::Xkb *xkb, KWin::KeyboardLayout *layout) + : Policy(xkb, layout) +{ + connect(workspace(), &Workspace::windowActivated, this, [this](Window *window) { + if (!window) { + return; + } + // ignore some special types + if (window->isDesktop() || window->isDock()) { + return; + } + setLayout(getLayout(m_layouts, window)); + }); +} + +WindowPolicy::~WindowPolicy() +{ +} + +void WindowPolicy::clearCache() +{ + m_layouts.clear(); +} + +void WindowPolicy::layoutChanged(uint index) +{ + auto window = workspace()->activeWindow(); + if (!window) { + return; + } + // ignore some special types + if (window->isDesktop() || window->isDock()) { + return; + } + + auto it = m_layouts.find(window); + if (it == m_layouts.end()) { + m_layouts.insert(window, index); + connect(window, &Window::closed, this, [this, window]() { + m_layouts.remove(window); + }); + } else { + if (it.value() == index) { + return; + } + it.value() = index; + } +} + +ApplicationPolicy::ApplicationPolicy(KWin::Xkb *xkb, KWin::KeyboardLayout *layout, const KConfigGroup &config) + : Policy(xkb, layout, config) +{ + connect(workspace(), &Workspace::windowActivated, this, &ApplicationPolicy::windowActivated); + + connect(workspace()->sessionManager(), &SessionManager::prepareSessionSaveRequested, this, [this](const QString &name) { + clearLayouts(); + + for (auto i = m_layouts.constBegin(); i != m_layouts.constEnd(); ++i) { + if (const uint layout = *i) { + const QString desktopFileName = i.key()->desktopFileName(); + if (!desktopFileName.isEmpty()) { + m_config.writeEntry(defaultLayoutEntryKey() % desktopFileName, layout); + } + } + } + }); + + connect(workspace()->sessionManager(), &SessionManager::loadSessionRequested, this, [this, xkb](const QString &name) { + if (xkb->numberOfLayouts() > 1) { + const QString keyPrefix = defaultLayoutEntryKey(); + const QStringList keyList = m_config.keyList().filter(keyPrefix); + for (const QString &key : keyList) { + m_layoutsRestored.insert( + QStringView(key).mid(keyPrefix.size()).toLatin1(), + m_config.readEntry(key, 0)); + } + } + m_layoutsRestored.squeeze(); + }); +} + +ApplicationPolicy::~ApplicationPolicy() +{ +} + +void ApplicationPolicy::windowActivated(Window *window) +{ + if (!window) { + return; + } + // ignore some special types + if (window->isDesktop() || window->isDock()) { + return; + } + auto it = m_layouts.constFind(window); + if (it != m_layouts.constEnd()) { + setLayout(it.value()); + return; + }; + for (it = m_layouts.constBegin(); it != m_layouts.constEnd(); it++) { + if (Window::belongToSameApplication(window, it.key())) { + const uint layout = it.value(); + setLayout(layout); + layoutChanged(layout); + return; + } + } + setLayout(m_layoutsRestored.take(window->desktopFileName())); + if (const uint index = m_xkb->currentLayout()) { + layoutChanged(index); + } +} + +void ApplicationPolicy::clearCache() +{ + m_layouts.clear(); +} + +void ApplicationPolicy::layoutChanged(uint index) +{ + auto window = workspace()->activeWindow(); + if (!window) { + return; + } + // ignore some special types + if (window->isDesktop() || window->isDock()) { + return; + } + + auto it = m_layouts.find(window); + if (it == m_layouts.end()) { + m_layouts.insert(window, index); + connect(window, &Window::closed, this, [this, window]() { + m_layouts.remove(window); + }); + } else { + if (it.value() == index) { + return; + } + it.value() = index; + } + // update all layouts for the application + for (it = m_layouts.begin(); it != m_layouts.end(); it++) { + if (Window::belongToSameApplication(it.key(), window)) { + it.value() = index; + } + } +} + +} +} + +#include "moc_keyboard_layout_switching.cpp" diff --git a/local/recipes/kde/kwin/source/src/keyboard_layout_switching.h b/local/recipes/kde/kwin/source/src/keyboard_layout_switching.h new file mode 100644 index 0000000000..7d4d56c471 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/keyboard_layout_switching.h @@ -0,0 +1,143 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include +#include +#include + +namespace KWin +{ + +class Window; +class KeyboardLayout; +class Xkb; +class VirtualDesktop; + +namespace KeyboardLayoutSwitching +{ + +class Policy : public QObject +{ + Q_OBJECT +public: + ~Policy() override; + + virtual QString name() const = 0; + + static std::unique_ptr create(Xkb *xkb, KeyboardLayout *layout, const KConfigGroup &config, const QString &policy); + +protected: + explicit Policy(Xkb *xkb, KeyboardLayout *layout, const KConfigGroup &config = KConfigGroup()); + virtual void clearCache() = 0; + virtual void layoutChanged(uint index) = 0; + + void setLayout(uint index); + + KConfigGroup m_config; + virtual const QString defaultLayoutEntryKey() const; + void clearLayouts(); + + static const char defaultLayoutEntryKeyPrefix[]; + Xkb *m_xkb; + +private: + KeyboardLayout *m_layout; +}; + +class GlobalPolicy : public Policy +{ + Q_OBJECT +public: + explicit GlobalPolicy(Xkb *xkb, KeyboardLayout *layout, const KConfigGroup &config); + ~GlobalPolicy() override; + + QString name() const override + { + return QStringLiteral("Global"); + } + +protected: + void clearCache() override + { + } + void layoutChanged(uint index) override + { + } + +private: + const QString defaultLayoutEntryKey() const override; +}; + +class VirtualDesktopPolicy : public Policy +{ + Q_OBJECT +public: + explicit VirtualDesktopPolicy(Xkb *xkb, KeyboardLayout *layout, const KConfigGroup &config); + ~VirtualDesktopPolicy() override; + + QString name() const override + { + return QStringLiteral("Desktop"); + } + +protected: + void clearCache() override; + void layoutChanged(uint index) override; + +private: + void desktopChanged(); + QHash m_layouts; +}; + +class WindowPolicy : public Policy +{ + Q_OBJECT +public: + explicit WindowPolicy(Xkb *xkb, KeyboardLayout *layout); + ~WindowPolicy() override; + + QString name() const override + { + return QStringLiteral("Window"); + } + +protected: + void clearCache() override; + void layoutChanged(uint index) override; + +private: + QHash m_layouts; +}; + +class ApplicationPolicy : public Policy +{ + Q_OBJECT +public: + explicit ApplicationPolicy(Xkb *xkb, KeyboardLayout *layout, const KConfigGroup &config); + ~ApplicationPolicy() override; + + QString name() const override + { + return QStringLiteral("WinClass"); + } + +protected: + void clearCache() override; + void layoutChanged(uint index) override; + +private: + void windowActivated(Window *window); + QHash m_layouts; + QHash m_layoutsRestored; +}; + +} +} diff --git a/local/recipes/kde/kwin/source/src/keyboard_repeat.cpp b/local/recipes/kde/kwin/source/src/keyboard_repeat.cpp new file mode 100644 index 0000000000..8976fe6407 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/keyboard_repeat.cpp @@ -0,0 +1,64 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016, 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "keyboard_repeat.h" +#include "input_event.h" +#include "keyboard_input.h" +#include "wayland/keyboard.h" +#include "wayland/seat.h" +#include "wayland_server.h" +#include "xkb.h" + +#include + +namespace KWin +{ + +KeyboardRepeat::KeyboardRepeat(Xkb *xkb) + : QObject() + , m_timer(new QTimer(this)) + , m_xkb(xkb) +{ + connect(m_timer, &QTimer::timeout, this, &KeyboardRepeat::handleKeyRepeat); +} + +KeyboardRepeat::~KeyboardRepeat() = default; + +void KeyboardRepeat::handleKeyRepeat() +{ + // TODO: don't depend on WaylandServer + if (waylandServer()->seat()->keyboard()->keyRepeatRate() != 0) { + m_timer->setInterval(1000 / waylandServer()->seat()->keyboard()->keyRepeatRate()); + } + // TODO: better time + Q_EMIT keyRepeat(m_key, m_time); +} + +void KeyboardRepeat::keyboardKey(uint32_t key, KeyboardKeyState state, std::chrono::microseconds time) +{ + if (state == KeyboardKeyState::Repeated) { + return; + } + if (state == KeyboardKeyState::Pressed) { + // TODO: don't get these values from WaylandServer + if (m_xkb->shouldKeyRepeat(key) && waylandServer()->seat()->keyboard()->keyRepeatDelay() != 0) { + m_timer->setInterval(waylandServer()->seat()->keyboard()->keyRepeatDelay()); + m_key = key; + m_time = time; + m_timer->start(); + } + } else if (state == KeyboardKeyState::Released) { + if (key == m_key) { + m_timer->stop(); + } + } +} + +} + +#include "moc_keyboard_repeat.cpp" diff --git a/local/recipes/kde/kwin/source/src/keyboard_repeat.h b/local/recipes/kde/kwin/source/src/keyboard_repeat.h new file mode 100644 index 0000000000..5ff9665f8a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/keyboard_repeat.h @@ -0,0 +1,41 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016, 2017 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "input.h" + +#include + +class QTimer; + +namespace KWin +{ +class Xkb; + +class KeyboardRepeat : public QObject +{ + Q_OBJECT +public: + explicit KeyboardRepeat(Xkb *xkb); + ~KeyboardRepeat() override; + + void keyboardKey(uint32_t key, KeyboardKeyState state, std::chrono::microseconds time); + +Q_SIGNALS: + void keyRepeat(quint32 key, std::chrono::microseconds time); + +private: + void handleKeyRepeat(); + QTimer *m_timer; + Xkb *m_xkb; + std::chrono::microseconds m_time; + quint32 m_key = 0; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/killprompt.cpp b/local/recipes/kde/kwin/source/src/killprompt.cpp new file mode 100644 index 0000000000..6256e49c0f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/killprompt.cpp @@ -0,0 +1,112 @@ +/* + * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "killprompt.h" + +#include "client_machine.h" +#include "wayland/display.h" +#include "wayland/seat.h" +#include "wayland/xdgforeign_v2.h" +#include "wayland_server.h" +#include "xdgactivationv1.h" +#include "xdgshellwindow.h" +#if KWIN_BUILD_X11 +#include "x11window.h" +#endif + +#include +#include +#include + +namespace KWin +{ + +KillPrompt::KillPrompt(Window *window) + : m_window(window) +{ +#if KWIN_BUILD_X11 + Q_ASSERT(qobject_cast(window) || qobject_cast(window)); +#else + Q_ASSERT(qobject_cast(window)); +#endif + + m_process.setProcessChannelMode(QProcess::ForwardedChannels); + + const QFileInfo binaryInfo(KWIN_KILLER_BIN); + const QFileInfo buildDirBinary{QDir{QCoreApplication::applicationDirPath()}, binaryInfo.fileName()}; + + if (buildDirBinary.exists()) { + m_process.setProgram(buildDirBinary.absoluteFilePath()); + } else { + m_process.setProgram(KWIN_KILLER_BIN); + } +} + +bool KillPrompt::isRunning() const +{ + return m_process.state() == QProcess::Running; +} + +void KillPrompt::start(quint32 timestamp) +{ + if (isRunning()) { + return; + } + + QProcessEnvironment env = kwinApp()->processStartupEnvironment(); + + QString wid; + QString timestampString; + QString hostname = QStringLiteral("localhost"); + QString appId = !m_window->desktopFileName().isEmpty() ? m_window->desktopFileName() : m_window->resourceClass(); + QString platform; + +#if KWIN_BUILD_X11 + if (auto *x11Window = qobject_cast(m_window)) { + platform = QStringLiteral("xcb"); + wid = QString::number(x11Window->window()); + timestampString = QString::number(timestamp); + if (!x11Window->clientMachine()->isLocal()) { + hostname = x11Window->clientMachine()->hostName(); + } + } else +#endif + if (auto *xdgToplevel = qobject_cast(m_window)) { + platform = QStringLiteral("wayland"); + auto *exported = waylandServer()->exportAsForeign(xdgToplevel->surface()); + wid = exported->handle(); + + env.remove(QStringLiteral("QT_WAYLAND_RECONNECT")); + } + + QStringList args{ + QStringLiteral("-platform"), + platform, + QStringLiteral("--pid"), + QString::number(m_window->pid()), + QStringLiteral("--windowname"), + m_window->captionNormal(), + QStringLiteral("--applicationname"), + appId, + QStringLiteral("--wid"), + wid, + QStringLiteral("--hostname"), + hostname, + QStringLiteral("--timestamp"), + timestampString, + }; + + m_process.setArguments(args); + m_process.setProcessEnvironment(env); + m_process.start(); +} + +void KillPrompt::quit() +{ + m_process.terminate(); +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/killprompt.h b/local/recipes/kde/kwin/source/src/killprompt.h new file mode 100644 index 0000000000..64f41f9016 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/killprompt.h @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2023 Kai Uwe Broulik + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include + +namespace KWin +{ + +class Window; + +class KillPrompt +{ +public: + /** + * @brief Creates a kill helper process. + * @param window The window to kill, must be an X11Window or XdgToplevelWindow. + */ + explicit KillPrompt(Window *window); + + /** + * @brief Whether the kill helper process is currently running. + */ + bool isRunning() const; + + /** + * @brief Starts the kill helper process. + * @param timestamp The X activation timestamp. + */ + void start(quint32 timestamp = 0); + /** + * @brief Terminate the kill helper process. + */ + void quit(); + +private: + Window *m_window = nullptr; + QProcess m_process; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/killwindow.cpp b/local/recipes/kde/kwin/source/src/killwindow.cpp new file mode 100644 index 0000000000..9e94ea06b0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/killwindow.cpp @@ -0,0 +1,43 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "killwindow.h" +#include "main.h" +#include "osd.h" +#include "window.h" + +#include + +namespace KWin +{ + +KillWindow::KillWindow() +{ +} + +KillWindow::~KillWindow() +{ +} + +void KillWindow::start() +{ + OSD::show(i18n("Select window to force close with left click or enter.\nEscape or right click to cancel."), + QStringLiteral("window-close")); + kwinApp()->startInteractiveWindowSelection( + [](KWin::Window *window) { + OSD::hide(); + if (window) { + window->killWindow(); + } + }, + QByteArrayLiteral("pirate")); +} + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/killwindow.h b/local/recipes/kde/kwin/source/src/killwindow.h new file mode 100644 index 0000000000..77d771f508 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/killwindow.h @@ -0,0 +1,26 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +namespace KWin +{ + +class KillWindow +{ +public: + KillWindow(); + ~KillWindow(); + + void start(); +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/kscreenintegration.cpp b/local/recipes/kde/kwin/source/src/kscreenintegration.cpp new file mode 100644 index 0000000000..79e8917b87 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kscreenintegration.cpp @@ -0,0 +1,264 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kscreenintegration.h" +#include "utils/common.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace KWin +{ +namespace KScreenIntegration +{ +/// See KScreen::Output::hashMd5 +static QString outputHash(BackendOutput *output) +{ + if (!output->edid().hash().isEmpty()) { + return output->edid().hash(); + } else { + return output->name(); + } +} + +/// See KScreen::Config::connectedOutputsHash in libkscreen +QString connectedOutputsHash(const QList &outputs, bool isLidClosed) +{ + QStringList hashedOutputs; + hashedOutputs.reserve(outputs.count()); + for (auto output : std::as_const(outputs)) { + if (output->isPlaceholder() || output->isNonDesktop()) { + continue; + } + if (output->isInternal() && isLidClosed) { + continue; + } + hashedOutputs << outputHash(output); + } + std::sort(hashedOutputs.begin(), hashedOutputs.end()); + const auto hash = QCryptographicHash::hash(hashedOutputs.join(QString()).toLatin1(), QCryptographicHash::Md5); + return QString::fromLatin1(hash.toHex()); +} + +static QHash outputsConfig(const QList &outputs, const QString &hash) +{ + const QString kscreenJsonPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kscreen/") % hash); + if (kscreenJsonPath.isEmpty()) { + return {}; + } + + QFile f(kscreenJsonPath); + if (!f.open(QIODevice::ReadOnly)) { + qCWarning(KWIN_CORE) << "Could not open file" << kscreenJsonPath; + return {}; + } + + QJsonParseError error; + const auto doc = QJsonDocument::fromJson(f.readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(KWIN_CORE) << "Failed to parse" << kscreenJsonPath << error.errorString(); + return {}; + } + + QHash duplicate; + QHash outputHashes; + for (BackendOutput *output : outputs) { + const QString hash = outputHash(output); + const auto it = std::find_if(outputHashes.cbegin(), outputHashes.cend(), [hash](const auto &value) { + return value == hash; + }); + if (it == outputHashes.cend()) { + duplicate[output] = false; + } else { + duplicate[output] = true; + duplicate[it.key()] = true; + } + outputHashes[output] = hash; + } + + QHash ret; + const auto outputsJson = doc.array(); + for (const auto &outputJson : outputsJson) { + const auto outputObject = outputJson.toObject(); + const auto id = outputObject[QLatin1String("id")]; + const auto output = std::find_if(outputs.begin(), outputs.end(), [&duplicate, &id, &outputObject](BackendOutput *output) { + if (outputHash(output) != id.toString()) { + return false; + } + if (duplicate[output]) { + // can't distinguish between outputs by hash alone, need to look at connector names + const auto metadata = outputObject[QLatin1String("metadata")]; + const auto outputName = metadata[QLatin1String("name")].toString(); + return outputName == output->name(); + } else { + return true; + } + }); + if (output != outputs.end()) { + ret[*output] = outputObject; + } + } + return ret; +} + +static std::optional globalOutputConfig(BackendOutput *output) +{ + const QString kscreenPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kscreen/")); + if (kscreenPath.isEmpty()) { + return std::nullopt; + } + const auto hash = outputHash(output); + // use connector specific data if available, unspecific data if not + QFile f(kscreenPath % hash % output->name()); + if (!f.open(QIODevice::ReadOnly)) { + f.setFileName(kscreenPath % hash); + if (!f.open(QIODevice::ReadOnly)) { + qCWarning(KWIN_CORE) << "Could not open file" << f.fileName(); + return std::nullopt; + } + } + + QJsonParseError error; + const auto doc = QJsonDocument::fromJson(f.readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(KWIN_CORE) << "Failed to parse" << f.fileName() << error.errorString(); + return std::nullopt; + } + return doc.object(); +} + +/// See KScreen::BackendOutput::Rotation +enum Rotation { + None = 1, + Left = 2, + Inverted = 4, + Right = 8, +}; + +OutputTransform toKWinTransform(int rotation) +{ + switch (Rotation(rotation)) { + case None: + return OutputTransform::Normal; + case Left: + return OutputTransform::Rotate90; + case Inverted: + return OutputTransform::Rotate180; + case Right: + return OutputTransform::Rotate270; + default: + Q_UNREACHABLE(); + } +} + +std::shared_ptr parseMode(BackendOutput *output, const QJsonObject &modeInfo) +{ + const QJsonObject size = modeInfo["size"].toObject(); + const QSize modeSize = QSize(size["width"].toInt(), size["height"].toInt()); + const uint32_t refreshRate = std::round(modeInfo["refresh"].toDouble() * 1000); + + const auto modes = output->modes(); + auto it = std::find_if(modes.begin(), modes.end(), [&modeSize, &refreshRate](const auto &mode) { + return mode->size() == modeSize && mode->refreshRate() == refreshRate; + }); + return (it != modes.end()) ? *it : nullptr; +} + +std::optional readOutputConfig(const QList &outputs, const QString &hash) +{ + const auto outputsInfo = outputsConfig(outputs, hash); + if (outputsInfo.isEmpty()) { + return std::nullopt; + } + OutputConfiguration cfg; + // default position goes from left to right + QPoint pos(0, 0); + for (const auto &output : std::as_const(outputs)) { + if (output->isPlaceholder() || output->isNonDesktop()) { + continue; + } + auto props = cfg.changeSet(output); + const QJsonObject outputInfo = outputsInfo[output]; + const auto globalOutputInfo = globalOutputConfig(output); + qCDebug(KWIN_CORE) << "Reading output configuration for " << output; + if (!outputInfo.isEmpty() || globalOutputInfo.has_value()) { + // settings that are per output setup: + props->enabled = outputInfo["enabled"].toBool(true); + if (outputInfo["primary"].toBool()) { + if (!props->enabled) { + qCWarning(KWIN_CORE) << "KScreen config would disable the primary output!"; + return std::nullopt; + } + } else if (int prio = outputInfo["priority"].toInt(); prio > 0) { + if (!props->enabled) { + qCWarning(KWIN_CORE) << "KScreen config would disable an output with priority!"; + return std::nullopt; + } + props->priority = prio; + } + if (const QJsonObject pos = outputInfo["pos"].toObject(); !pos.isEmpty()) { + props->pos = QPoint(pos["x"].toInt(), pos["y"].toInt()); + } + + // settings that are independent of per output setups: + const auto &globalInfo = globalOutputInfo ? globalOutputInfo.value() : outputInfo; + if (const QJsonValue scale = globalInfo["scale"]; !scale.isUndefined()) { + props->scale = scale.toDouble(1.); + } + if (const QJsonValue rotation = globalInfo["rotation"]; !rotation.isUndefined()) { + props->transform = KScreenIntegration::toKWinTransform(rotation.toInt()); + props->manualTransform = props->transform; + } + if (const QJsonValue overscan = globalInfo["overscan"]; !overscan.isUndefined()) { + props->overscan = globalInfo["overscan"].toInt(); + } + if (const QJsonValue vrrpolicy = globalInfo["vrrpolicy"]; !vrrpolicy.isUndefined()) { + props->vrrPolicy = static_cast(vrrpolicy.toInt()); + } + if (const QJsonValue rgbrange = globalInfo["rgbrange"]; !rgbrange.isUndefined()) { + props->rgbRange = static_cast(rgbrange.toInt()); + } + + if (const QJsonObject modeInfo = globalInfo["mode"].toObject(); !modeInfo.isEmpty()) { + if (auto mode = KScreenIntegration::parseMode(output, modeInfo)) { + props->mode = mode; + } + } + } else { + props->enabled = true; + props->pos = pos; + props->transform = output->panelOrientation(); + } + const auto mode = props->mode.value_or(output->currentMode()).lock(); + if (!mode) { + qCWarning(KWIN_CORE) << "Every enabled output should have a mode"; + continue; + } + const double width = mode->size().width() / props->scale.value_or(output->scale()); + pos.setX(pos.x() + std::round(width)); + } + + bool allDisabled = std::all_of(outputs.begin(), outputs.end(), [&cfg](const auto &output) { + return !cfg.changeSet(output)->enabled.value_or(output->isEnabled()); + }); + if (allDisabled) { + qCWarning(KWIN_CORE) << "KScreen config would disable all outputs!"; + return std::nullopt; + } + return cfg; +} +} +} diff --git a/local/recipes/kde/kwin/source/src/kscreenintegration.h b/local/recipes/kde/kwin/source/src/kscreenintegration.h new file mode 100644 index 0000000000..786705a1f5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kscreenintegration.h @@ -0,0 +1,24 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "core/backendoutput.h" +#include "core/outputconfiguration.h" + +#include +#include + +namespace KWin +{ +namespace KScreenIntegration +{ + +QString connectedOutputsHash(const QList &outputs, bool isLidClosed); +std::optional readOutputConfig(const QList &outputs, const QString &hash); +} +} diff --git a/local/recipes/kde/kwin/source/src/kwin.kcfg b/local/recipes/kde/kwin/source/src/kwin.kcfg new file mode 100644 index 0000000000..b3deb52cc4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kwin.kcfg @@ -0,0 +1,353 @@ + + + config-kwin.h + + + + Nothing + + + Meta + + + Nothing + + + Raise + + + Nothing + + + Operations menu + + + Activate and raise + + + Nothing + + + Operations menu + + + Activate, pass click and raise on release + + + Activate and pass click + + + Activate and pass click + + + Scroll + + + Move + + + Toggle raise and lower + + + Resize + + + true + + + + + None + + + None + + + None + + + None + + + None + + + None + + + None + + + None + + + + + false + + + + + + + + + Options::ClickToFocus + + + false + + + true + + + false + + + 1 + 0 + 4 + + + + + + + + + + + + + + + [] { + #if KWIN_BUILD_DECORATIONS + return PlacementCentered; + #else + return PlacementMaximizing; + #endif + }() + + + + + + + + KWin::Options::ActivationDesktopPolicy::SwitchToOtherDesktop + + + false + + + 750 + + + 300 + + + true + + + 10 + + + 10 + + + 0 + + + false + + + 0 + + + 75 + + + 350 + + + 1 + + + true + + + true + + + true + + + 0.25 + 0.0 + 1.0 + + + Maximize + + + Maximize + + + Maximize (vertical only) + + + Maximize (horizontal only) + + + 5000 + + + false + + + false + + + true + + + false + + + true + + + + + + + + + Qt::BottomRightCorner + + + 20 + + + false + + + + + true + + + 100 + 0 + 1000 + + + + + false + + + 8 + 1 + + + + + OpenGL + + + true + + + 0 + + + true + + + + + 90 + + + 1 + + + 1 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + true + + + true + + + true + + + thumbnail_grid + + + + + 1 + 0 + + + + + + true + + + true + + + + + + + + + XwaylandCrashPolicy::Restart + + + 3 + + + + + + + + + + Combinations + + + false + + + false + + + diff --git a/local/recipes/kde/kwin/source/src/kwin.notifyrc b/local/recipes/kde/kwin/source/src/kwin.notifyrc new file mode 100644 index 0000000000..464b7d6d4d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/kwin.notifyrc @@ -0,0 +1,287 @@ +[Global] +IconName=kwin +Comment=KWin Window Manager +Comment[ar]=مدير النوافذ كوين +Comment[az]=KWin pəncərə Meneceri +Comment[be]=Кіраўнік акон KWin +Comment[bg]=Мениджър на прозорци KWin +Comment[bs]=Menadžer prozora K‑vin +Comment[ca]=Gestor de finestres KWin +Comment[ca@valencia]=Gestor de finestres KWin +Comment[cs]=Správce oken KWin +Comment[da]=KWin vindueshåndtering +Comment[de]=KWin-Fensterverwaltung +Comment[el]=Διαχειριστής παραθύρων Kwin +Comment[en_GB]=KWin Window Manager +Comment[eo]=KWin Fenestra Administranto +Comment[es]=Gestor de ventanas KWin +Comment[et]=Kwini aknahaldur +Comment[eu]=KWin leiho-kudeatzailea +Comment[fi]=KWin-ikkunointiohjelma +Comment[fr]=Gestionnaire de fenêtres KWin +Comment[ga]=Bainisteoir Fuinneog KWin +Comment[gl]=Xestor de xanelas KWin. +Comment[gu]=KWin વિન્ડો સંચાલક +Comment[he]=מנהל החלונות KWin +Comment[hi]=केविन विंडो प्रबंधक +Comment[hr]=Upravitelj prozora KWin +Comment[hu]=KWin ablakkezelő +Comment[ia]=Gerente de fenestra KWin +Comment[id]=Pengelola Jendela KWin +Comment[is]=KWin gluggastjóri +Comment[it]=Gestore delle finestre KWin +Comment[ja]=KWin ウィンドウマネージャ +Comment[ka]=KWin ფანჯარათმმართველი +Comment[kk]=KWin терезе менеджері +Comment[km]=កម្មវិធី​គ្រប់គ្រង​បង្អួច KWin +Comment[kn]=ಕೆವಿನ್(KWin) ವಿಂಡೋ ವ್ಯವಸ್ಥಾಪಕ +Comment[ko]=KWin 창 관리자 +Comment[lt]=KWin langų tvarkytuvė +Comment[lv]=„KWin“ logu pārvaldnieks +Comment[mr]=के-विन चौकट व्यवस्थापक +Comment[nb]=KWin vindusbehandler +Comment[nds]=KWin-Finsterpleger +Comment[nl]=KWin vensterbeheerder +Comment[nn]=KWin vindaugshandsamar +Comment[pa]=KWin ਵਿੰਡੋ ਮੈਨੇਜਰ +Comment[pl]=Zarządzanie oknami KWin +Comment[pt]=Gestor de Janelas KWin +Comment[pt_BR]=Gerenciador de janelas KWin +Comment[ro]=Gestionar de ferestre KWin +Comment[ru]=Диспетчер окон KWin +Comment[sa]=KWin विण्डो प्रबन्धक +Comment[si]=KWin කවුළු කළමනාකරු +Comment[sk]=Správca okien KWin +Comment[sl]=Upravljalnik oken KWin +Comment[sr]=Менаџер прозора К‑вин +Comment[sr@ijekavian]=Менаџер прозора К‑вин +Comment[sr@ijekavianlatin]=Menadžer prozora KWin +Comment[sr@latin]=Menadžer prozora KWin +Comment[sv]=Kwin fönsterhanterare +Comment[ta]=கேவின் சாளர மேலாளர் +Comment[th]=ตัวจัดการหน้าต่าง KWin +Comment[tr]=KWin Pencere Yöneticisi +Comment[ug]=KWin كۆزنەك باشقۇرغۇچ +Comment[uk]=Керування вікнами KWin +Comment[vi]=Trình quản lí cửa sổ KWin +Comment[wa]=Manaedjeu des fniesses KWin +Comment[zh_CN]=KWin 窗口管理器 +Comment[zh_TW]=KWin 視窗管理員 + +[Event/graphicsreset] +Name=Graphics Reset +Name[ar]=تصفّير الرسوميات +Name[az]=Qrafikanın sıfırlanması +Name[be]=Скід графікі +Name[bg]=Връщане на стандартни настройки на графиката +Name[bs]=Reset grafike +Name[ca]=Reinici dels gràfics +Name[ca@valencia]=Reinici dels gràfics +Name[cs]=Resetovat grafiku +Name[da]=Grafiknulstilling +Name[de]=Grafik-Reset +Name[el]=Επαναφορά γραφικών +Name[en_GB]=Graphics Reset +Name[eo]=Grafika Restarigi +Name[es]=Reinicio gráfico +Name[et]=Graafika lähtestamine +Name[eu]=Grafikoak berrezarri +Name[fi]=Grafiikan nollaus +Name[fr]=Réinitialisation graphique +Name[gl]=Restabelecemento dos gráficos +Name[he]=איפוס גרפיקה +Name[hu]=Grafikai visszaállítás +Name[ia]=Reinitia Graphic +Name[id]=Pengaturan Ulang Grafik +Name[is]=Myndendurstilling +Name[it]=Azzeramento grafica +Name[ja]=グラフィックのリセット +Name[ka]=გრაფიკის საწყის მნიშვნელობაზე დაყენება +Name[kk]=Графиканы ысыру +Name[ko]=그래픽 초기화 +Name[lt]=Grafikos atstatymas +Name[lv]=Grafikas atiestatīšana +Name[nb]=Bilde tilbakestilt +Name[nds]=Grafik-Torüchsetten +Name[nl]=Grafische reset +Name[nn]=Grafikk tilbakestilt +Name[pa]=ਗਰਾਫਿਕਸ ਮੁੜ-ਸੈੱਟ +Name[pl]=Ponowny rozruch grafiki +Name[pt]=Reinício Gráfico +Name[pt_BR]=Reinício gráfico +Name[ro]=Reinițializare grafică +Name[ru]=Сброс графики +Name[sa]=ग्राफिक्स रीसेट +Name[sk]=Grafické vynulovanie +Name[sl]=Ponastavitev grafike +Name[sr]=Ресетовање графике +Name[sr@ijekavian]=Ресетовање графике +Name[sr@ijekavianlatin]=Resetovanje grafike +Name[sr@latin]=Resetovanje grafike +Name[sv]=Grafikåterställning +Name[ta]=வரைநிரல் மீட்டமைப்பு +Name[th]=กราฟิกรีเซ็ต +Name[tr]=Grafik Sıfırlama +Name[uk]=Скидання графіки +Name[vi]=Đặt lại đồ hoạ +Name[zh_CN]=图形功能重置 +Name[zh_TW]=圖形重設 +Comment=A graphics reset event occurred +Comment[ar]=حدث عملية تصفير للرسوميات +Comment[az]=Qrafik sistemdə qəza baş verdi +Comment[be]=Адбылася падзея скіду графікі +Comment[bg]=Извърши се връщане на стандартни настройки на графиката +Comment[bs]=Grafički reset događaj se desio +Comment[ca]=Ha ocorregut un esdeveniment de reinici dels gràfics +Comment[ca@valencia]=Ha ocorregut un esdeveniment de reinici dels gràfics +Comment[cs]=Nastala událost resetování grafiky +Comment[da]=En grafiknulstillingshændelse fandt sted +Comment[de]=Ein Zurücksetzen der Grafik ist aufgetreten +Comment[el]=Συνέβη μια επαναφορά των γραφικών +Comment[en_GB]=A graphics reset event occurred +Comment[eo]=Okazis grafika rekomencigita evento +Comment[es]=Ha ocurrido un evento de reinicio gráfico +Comment[et]=Toimus graafika lähtestamise sündmus +Comment[eu]=Grafikoak berrezartzeko gertaera bat jazo da +Comment[fi]=Sattui grafiikan nollaustapahtuma +Comment[fr]=Un évènement de réinitialisation graphique est intervenu +Comment[gl]=Aconteceu un evento de restabelecemento de gráficos. +Comment[he]=התרחש אירוע איפוס הגרפיקה +Comment[hu]=Egy grafikai visszaállítás esemény történt +Comment[ia]=Il necessita un evento de reinitiar le graphic +Comment[id]=Sebuah peristiwa set ulang grafik yang terjadi +Comment[is]=Myndendurstillingaratvik kom upp +Comment[it]=Si è verificato un evento di azzeramento della grafica +Comment[ja]=グラフィックのリセットイベントが発生しました +Comment[ka]=გრაფიკის საწყის მნიშვნელობაზე დაყენება +Comment[kk]=Графиканы ысыру оқиғасы болды +Comment[ko]=그래픽 초기화 이벤트가 발생함 +Comment[lt]=Įvyko grafikos atstatymo įvykis +Comment[lv]=Noticis grafikas atiestatīšanas notikums +Comment[nb]=Det har skjedd en bildetilbakestilling +Comment[nds]=Dat geev en Grafik-Torüchsett-Begeefnis +Comment[nl]=Een gebeurtenis van een grafische reset deed zich voor +Comment[nn]=Det har skjedd ei grafikktilbakestilling +Comment[pl]=Nastąpiło zdarzenie ponownego rozruchu systemu graficznego +Comment[pt]=Ocorreu um evento de reinício gráfico +Comment[pt_BR]=Ocorreu um evento de reinício gráfico +Comment[ro]=A intervenit un eveniment de reinițializare a graficii +Comment[ru]=Произошёл сброс графической системы +Comment[sa]=ग्राफिक्स् रीसेट् इवेण्ट् अभवत् +Comment[sk]=Nastala chyba grafického vynulovania +Comment[sl]=Prišlo je do dogodka ponastavitve grafike +Comment[sr]=Дошло је до ресетовања графике +Comment[sr@ijekavian]=Дошло је до ресетовања графике +Comment[sr@ijekavianlatin]=Došlo je do resetovanja grafike +Comment[sr@latin]=Došlo je do resetovanja grafike +Comment[sv]=En grafikåterställningshändelse inträffade +Comment[ta]=வரைநிரல் மீட்டமைப்பு (graphics reset) நிகழ்ந்துள்ளது +Comment[th]=เกิดเหตุการณ์การรีเซ็ตกราฟิก +Comment[tr]=Bir grafik sıfırlama olayı oluştu +Comment[uk]=Сталася подія відновлення початкового стану графіки +Comment[vi]=Một sự kiện đặt lại đồ hoạ đã xảy ra +Comment[zh_CN]=发生了一次图形功能重置事件 +Comment[zh_TW]=發生了圖形重設事件 +Action=Popup + +[Event/xwaylandcrash] +Name=Xwayland Crash +Name[ar]=إنهيار ويلاند_اكس +Name[az]=Xwayland Qəzası +Name[be]=Збой Xwayland +Name[bg]=Срив на Xwayland +Name[ca]=Fallada de l'Xwayland +Name[ca@valencia]=Fallada d'XWayland +Name[cs]=Pád Xwaylandu +Name[da]=Xwayland-nedbrud +Name[de]=Xwayland-Absturz +Name[el]=Κατάρρευση Xwayland +Name[en_GB]=Xwayland Crash +Name[eo]=Xwayland Kraŝo +Name[es]=Fallo de Xwayland +Name[et]=Xwaylandi krahh +Name[eu]=Xwayland kraskatzea +Name[fi]=XWayland kaatui +Name[fr]=Plantage « XWayland » +Name[gl]=Quebra de Xwayland +Name[he]=קריסה של Xwayland +Name[hu]=Xwayland összeomlás +Name[ia]=Xwayland Crash +Name[id]=Kemogokan Xwayland +Name[is]=Xwayland hrun +Name[it]=Chiusura inattesa di Xwayland +Name[ja]=Xwayland クラッシュ +Name[ka]=Xwayland -ის ავარია +Name[ko]=Xwayland 충돌 +Name[lt]=Xwayland strigtis +Name[lv]=„Xwayland“ avārija +Name[nb]=Xwayland-krasj +Name[nl]=Xwayland-crash +Name[nn]=Xwayland-krasj +Name[pl]=Usterka Xwayland +Name[pt]=Estoiro do Xwayland +Name[pt_BR]=Falha no Xwayland +Name[ro]=Prăbușire Xwayland +Name[ru]=Аварийное завершение Xwayland +Name[sa]=Xwayland दुर्घटना +Name[sk]=Pád Xwayland-u +Name[sl]=Sesutje Xwayland +Name[sv]=Krasch av Xwayland +Name[ta]=Xwayland முறிவு +Name[th]=Xwayland ล่ม +Name[tr]=Xwayland Çöktü +Name[uk]=Аварія Xwayland +Name[vi]=Sự cố Xwayland +Name[zh_CN]=Xwayland 崩溃 +Name[zh_TW]=Xwayland 崩潰 +Comment=Xwayland has crashed +Comment[ar]=إنهار ويلاند_اكس +Comment[ast]=Xwayland cascó +Comment[az]=Xwayland qəzaya uğradı +Comment[be]=Адбыўся збой Xwayland +Comment[bg]=Xwayland се срина +Comment[ca]=L'Xwayland ha fallat +Comment[ca@valencia]=XWayland ha fallat +Comment[cs]=Xwayland spadnul +Comment[da]=Xwayland har brudt ned +Comment[de]=Xwayland ist abgestürzt +Comment[el]=Το Xwayland κατάρρευσε +Comment[en_GB]=Xwayland has crashed +Comment[eo]=Xwayland paneis +Comment[es]=Xwayland ha fallado +Comment[et]=Xwayland lõpetas krahhiga +Comment[eu]=Xwayland kraskatu egin da +Comment[fi]=XWayland on kaatunut +Comment[fr]=Plantage de « XWayland » +Comment[gl]=Xwayland quebrou. +Comment[he]=Xwayland קרס +Comment[hu]=Az Xwayland összeomlott +Comment[ia]=Xwayland ha fracassate +Comment[id]=Xwayland telah mogok +Comment[is]=Xwayland hrundi +Comment[it]=Xwayland è stato terminato in modo inatteso +Comment[ja]=Xwayland がクラッシュしました +Comment[ka]=Xwayland ავარიულად დასრულდა +Comment[ko]=Xwayland가 충돌함 +Comment[lt]=Xwayland užstrigo +Comment[lv]=„Xwayland“ ir avarējis +Comment[nb]=Xwayland krasjet +Comment[nl]=Xwayland is gecrasht +Comment[nn]=Xwayland krasja +Comment[pl]=Xwayland napotkał usterkę +Comment[pt]=O Xwayland estoirou +Comment[pt_BR]=O Xwayland falhou +Comment[ro]=Xwayland s-a prăbușit +Comment[ru]=Произошло аварийное завершение Xwayland +Comment[sa]=Xwayland दुर्घटना अभवत् +Comment[sk]=Xwayland spadol +Comment[sl]=Xwayland se je sesul +Comment[sv]=Xwayland har kraschat +Comment[ta]=Xwayland முறிவடைந்துள்ளது +Comment[th]=Xwayland ล่ม +Comment[tr]=Xwayland çöktü +Comment[uk]=Xwayland завершив роботу в аварійному режимі +Comment[vi]=Xwayland gặp sự cố +Comment[zh_CN]=Xwayland 已经崩溃 +Comment[zh_TW]=Xwayland 已崩潰 +Action=Popup diff --git a/local/recipes/kde/kwin/source/src/layers.cpp b/local/recipes/kde/kwin/source/src/layers.cpp new file mode 100644 index 0000000000..29f40865b8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/layers.cpp @@ -0,0 +1,724 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +// SELI zmenit doc + +/* + + This file contains things relevant to stacking order and layers. + + Design: + + Normal unconstrained stacking order, as requested by the user (by clicking + on windows to raise them, etc.), is in Workspace::unconstrained_stacking_order. + That list shouldn't be used at all, except for building + Workspace::stacking_order. The building is done + in Workspace::constrainedStackingOrder(). Only Workspace::stackingOrder() should + be used to get the stacking order, because it also checks the stacking order + is up to date. + All clients are also stored in Workspace::clients (except for isDesktop() clients, + as those are very special, and are stored in Workspace::desktops), in the order + the clients were created. + + Every window has one layer assigned in which it is. There are 7 layers, + from bottom : DesktopLayer, BelowLayer, NormalLayer, DockLayer, AboveLayer, NotificationLayer, + ActiveLayer, CriticalNotificationLayer, and OnScreenDisplayLayer (see also NETWM sect.7.10.). + The layer a window is in depends on the window type, and on other things like whether the window + is active. We extend the layers provided in NETWM by the NotificationLayer, OnScreenDisplayLayer, + and CriticalNotificationLayer. + The NoficationLayer contains notification windows which are kept above all windows except the active + fullscreen window. The CriticalNotificationLayer contains notification windows which are important + enough to keep them even above fullscreen windows. The OnScreenDisplayLayer is used for eg. volume + and brightness change feedback and is kept above all windows since it provides immediate response + to a user action. + + NET::Splash clients belong to the Normal layer. NET::TopMenu clients + belong to Dock layer. Clients that are both NET::Dock and NET::KeepBelow + are in the Normal layer in order to keep the 'allow window to cover + the panel' Kicker setting to work as intended (this may look like a slight + spec violation, but a) I have no better idea, b) the spec allows adjusting + the stacking order if the WM thinks it's a good idea . We put all + NET::KeepAbove above all Docks too, even though the spec suggests putting + them in the same layer. + + Most transients are in the same layer as their mainwindow, + see Workspace::constrainedStackingOrder(), they may also be in higher layers, but + they should never be below their mainwindow. + + Currently the things that affect client in which layer a client + belongs: KeepAbove/Keep Below flags, window type, fullscreen + state and whether the client is active, mainclient (transiency). + + Make sure updateStackingOrder() is called in order to make + Workspace::stackingOrder() up to date and propagated to the world. + Using Workspace::blockStackingUpdates() (or the StackingUpdatesBlocker + helper class) it's possible to temporarily disable updates + and the stacking order will be updated once after it's allowed again. + +*/ + +#include "compositor.h" +#include "focuschain.h" +#include "internalwindow.h" +#include "rules.h" +#include "screenedge.h" +#include "tabbox/tabbox.h" +#include "utils/common.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "workspace.h" +#if KWIN_BUILD_X11 +#include "group.h" +#include "netinfo.h" +#include "x11window.h" +#endif + +#include + +#include + +namespace KWin +{ + +//******************************* +// Workspace +//******************************* + +void Workspace::updateStackingOrder(bool propagate_new_windows) +{ + if (m_blockStackingUpdates > 0) { + if (propagate_new_windows) { + m_blockedPropagatingNewWindows = true; + } + return; + } + QList new_stacking_order = constrainedStackingOrder(); + bool changed = (force_restacking || new_stacking_order != stacking_order); + force_restacking = false; + stacking_order = new_stacking_order; + if (changed || propagate_new_windows) { +#if KWIN_BUILD_X11 + propagateWindows(propagate_new_windows); +#endif + + for (int i = 0; i < stacking_order.size(); ++i) { + stacking_order[i]->setStackingOrder(i); + } + + Q_EMIT stackingOrderChanged(); + } +} + +#if KWIN_BUILD_X11 + +/** + * Propagates the managed windows to the world. + * Called ONLY from updateStackingOrder(). + */ +void Workspace::propagateWindows(bool propagate_new_windows) +{ + if (!rootInfo()) { + return; + } + // restack the windows according to the stacking order + // supportWindow > electric borders > windows > hidden windows + QList newWindowStack; + + // Stack all windows under the support window. The support window is + // not used for anything (besides the NETWM property), and it's not shown, + // but it was lowered after kwin startup. Stacking all windows below + // it ensures that no window will be ever shown above override-redirect + // windows (e.g. popups). + newWindowStack << rootInfo()->supportWindow(); + + newWindowStack << manual_overlays; + + newWindowStack.reserve(newWindowStack.size() + stacking_order.size()); + + for (int i = stacking_order.size() - 1; i >= 0; --i) { + X11Window *window = qobject_cast(stacking_order.at(i)); + if (!window || window->isDeleted() || window->isUnmanaged() || window->hiddenPreview()) { + continue; + } + + newWindowStack << window->window(); + } + + newWindowStack << *m_guardWindow; + + // when having hidden previews, stack hidden windows below everything else + // (as far as pure X stacking order is concerned), in order to avoid having + // these windows that should be unmapped to interfere with other windows + for (int i = stacking_order.size() - 1; i >= 0; --i) { + X11Window *window = qobject_cast(stacking_order.at(i)); + if (!window || window->isDeleted() || window->isUnmanaged() || !window->hiddenPreview()) { + continue; + } + newWindowStack << window->window(); + } + + // TODO isn't it too inefficient to restack always all windows? + // TODO don't restack not visible windows? + Q_ASSERT(newWindowStack.at(0) == rootInfo()->supportWindow()); + Xcb::restackWindows(newWindowStack); + + QList cl; + if (propagate_new_windows) { + cl.reserve(manual_overlays.size() + m_windows.size()); + for (const auto win : std::as_const(manual_overlays)) { + cl.push_back(win); + } + for (Window *window : std::as_const(m_windows)) { + X11Window *x11Window = qobject_cast(window); + if (x11Window && !x11Window->isUnmanaged()) { + cl.push_back(x11Window->window()); + } + } + rootInfo()->setClientList(cl.constData(), cl.size()); + } + + cl.clear(); + for (auto it = stacking_order.constBegin(); it != stacking_order.constEnd(); ++it) { + X11Window *window = qobject_cast(*it); + if (window && !window->isDeleted() && !window->isUnmanaged()) { + cl.push_back(window->window()); + } + } + for (const auto win : std::as_const(manual_overlays)) { + cl.push_back(win); + } + rootInfo()->setClientListStacking(cl.constData(), cl.size()); +} +#endif + +/** + * Returns topmost visible window. Windows on the dock, the desktop + * or of any other special kind are excluded. Also if the window + * doesn't accept focus it's excluded. + */ +// TODO misleading name for this method, too many slightly different ways to use it +Window *Workspace::topWindowOnDesktop(VirtualDesktop *desktop, LogicalOutput *output, bool unconstrained, bool only_normal) const +{ + // TODO Q_ASSERT( block_stacking_updates == 0 ); + QList list; + if (!unconstrained) { + list = stacking_order; + } else { + list = unconstrained_stacking_order; + } + for (int i = list.size() - 1; i >= 0; --i) { + auto window = list.at(i); + if (!window->isClient() || window->isDeleted()) { + continue; + } + if (window->isOnDesktop(desktop) && window->isShown() && window->isOnCurrentActivity()) { + if (output && window->output() != output) { + continue; + } + if (!only_normal) { + return window; + } + if (window->wantsTabFocus() && !window->isSpecialWindow()) { + return window; + } + } + } + return nullptr; +} + +Window *Workspace::findDesktop(VirtualDesktop *desktop, LogicalOutput *output) const +{ + // TODO Q_ASSERT( block_stacking_updates == 0 ); + for (int i = stacking_order.size() - 1; i >= 0; i--) { + auto window = stacking_order.at(i); + if (window->isDeleted()) { + continue; + } + if (window->isClient() && window->isOnDesktop(desktop) && window->isOnOutput(output) && window->isDesktop() && window->isShown()) { + return window; + } + } + return nullptr; +} + +#if KWIN_BUILD_X11 +static Layer layerForWindow(const X11Window *window) +{ + Layer layer = window->layer(); + + // Desktop windows cannot be promoted to upper layers. + if (layer == DesktopLayer) { + return layer; + } + + if (const Group *group = window->group()) { + const auto members = group->members(); + for (const X11Window *member : members) { + if (member == window) { + continue; + } else if (member->output() != window->output()) { + continue; + } + if (member->layer() == ActiveLayer) { + return ActiveLayer; + } + } + } + + return layer; +} +#endif + +static Layer computeLayer(const Window *window) +{ +#if KWIN_BUILD_X11 + if (auto x11Window = qobject_cast(window)) { + return layerForWindow(x11Window); + } +#endif + return window->layer(); +} + +bool Workspace::areConstrained(const Window *below, const Window *above) const +{ + for (const auto constraint : std::as_const(m_constraints)) { + if (constraint->below == below) { + if (constraint->above == above) { + return true; + } else { + for (const auto child : std::as_const(constraint->children)) { + if (areConstrained(child->below, above)) { + return true; + } + } + } + } + } + + return false; +} + +void Workspace::raiseOrLowerWindow(Window *window) +{ + if (!window->isOnCurrentDesktop()) { + return; + } + + VirtualDesktop *desktop = VirtualDesktopManager::self()->currentDesktop(); + LogicalOutput *output = options->isSeparateScreenFocus() ? window->output() : nullptr; + Layer layer = computeLayer(window); + + bool topmost = false; + for (auto it = unconstrained_stacking_order.crbegin(); it != unconstrained_stacking_order.crend(); ++it) { + if (layer != computeLayer(*it) || !(*it)->isClient() || (*it)->isDeleted()) { + continue; + } + if ((*it)->isOnDesktop(desktop) && (*it)->isShown() && (*it)->isOnCurrentActivity()) { + if (output && (*it)->output() != output) { + continue; + } + if ((*it)->wantsTabFocus() && !(*it)->isSpecialWindow()) { + if (*it == window) { + topmost = true; + break; + } else if (areConstrained(window, *it)) { + continue; // window `it' must be above us anyway, ignore it + } else { + break; + } + } + } + } + + if (topmost) { + lowerWindow(window); + } else { + raiseWindow(window); + } +} + +void Workspace::lowerWindow(Window *window, bool nogroup) +{ + if (window->isDeleted()) { + qCWarning(KWIN_CORE) << "Workspace::lowerWindow: closed window" << window << "cannot be restacked"; + return; + } + + window->cancelAutoRaise(); + + StackingUpdatesBlocker blocker(this); + + if (nogroup || (!window->isTransient() && window->transients().isEmpty())) { + unconstrained_stacking_order.removeAll(window); + unconstrained_stacking_order.prepend(window); + } else { + auto mainWindows = window->allMainWindows(); + Window *parent; + if (mainWindows.isEmpty()) { + parent = window; + } else { + parent = ensureStackingOrder(mainWindows).front(); + } + QList windows{parent}; + for (int i = 0; i < windows.size(); ++i) { + if (!windows[i]->transients().isEmpty()) { + windows << windows[i]->transients(); + } + } + windows = ensureStackingOrder(windows); + for (int i = windows.size() - 1; i >= 0; --i) { + lowerWindow(windows[i], true); + } + } +} + +void Workspace::lowerWindowWithinApplication(Window *window) +{ + if (window->isDeleted()) { + qCWarning(KWIN_CORE) << "Workspace::lowerWindowWithinApplication: closed window" << window << "cannot be restacked"; + return; + } + + window->cancelAutoRaise(); + + StackingUpdatesBlocker blocker(this); + + unconstrained_stacking_order.removeAll(window); + bool lowered = false; + // first try to put it below the bottom-most window of the application + for (auto it = unconstrained_stacking_order.begin(); it != unconstrained_stacking_order.end(); ++it) { + auto other = *it; + if (!other->isClient() || other->isDeleted()) { + continue; + } + if (Window::belongToSameApplication(other, window)) { + unconstrained_stacking_order.insert(it, window); + lowered = true; + break; + } + } + if (!lowered) { + unconstrained_stacking_order.prepend(window); + } + // ignore mainwindows +} + +void Workspace::raiseWindow(Window *window, bool nogroup) +{ + if (window->isDeleted()) { + qCWarning(KWIN_CORE) << "Workspace::raiseWindow: closed window" << window << "cannot be restacked"; + return; + } + + window->cancelAutoRaise(); + + StackingUpdatesBlocker blocker(this); + + if (!nogroup && window->isTransient()) { + QList transients; + Window *transient_parent = window; + while ((transient_parent = transient_parent->transientFor())) { + transients.prepend(transient_parent); + } + for (const auto &transient_parent : std::as_const(transients)) { + raiseWindow(transient_parent, true); + } + } + + unconstrained_stacking_order.removeAll(window); + unconstrained_stacking_order.append(window); +} + +void Workspace::raiseWindowWithinApplication(Window *window) +{ + if (window->isDeleted()) { + qCWarning(KWIN_CORE) << "Workspace::raiseWindowWithinApplication: closed window" << window << "cannot be restacked"; + return; + } + + window->cancelAutoRaise(); + + StackingUpdatesBlocker blocker(this); + // ignore mainwindows + + // first try to put it above the top-most window of the application + for (int i = unconstrained_stacking_order.size() - 1; i > -1; --i) { + auto other = unconstrained_stacking_order.at(i); + if (!other->isClient() || other->isDeleted()) { + continue; + } + if (other == window) { // don't lower it just because it asked to be raised + return; + } + if (Window::belongToSameApplication(other, window)) { + unconstrained_stacking_order.removeAll(window); + unconstrained_stacking_order.insert(unconstrained_stacking_order.indexOf(other) + 1, window); // insert after the found one + break; + } + } +} + +#if KWIN_BUILD_X11 +void Workspace::raiseWindowRequest(Window *window, NET::RequestSource src, xcb_timestamp_t timestamp) +{ + if (src == NET::FromTool || allowFullClientRaising(window, timestamp)) { + raiseWindow(window); + } else { + raiseWindowWithinApplication(window); + window->demandAttention(); + } +} + +void Workspace::lowerWindowRequest(X11Window *window, NET::RequestSource src, xcb_timestamp_t /*timestamp*/) +{ + // If the window has support for all this focus stealing prevention stuff, + // do only lowering within the application, as that's the more logical + // variant of lowering when application requests it. + // No demanding of attention here of course. + if (src == NET::FromTool || !window->hasUserTimeSupport()) { + lowerWindow(window); + } else { + lowerWindowWithinApplication(window); + } +} +#endif + +void Workspace::stackBelow(Window *window, Window *reference) +{ + if (window->isDeleted()) { + qCWarning(KWIN_CORE) << "Workspace::stackBelow: closed window" << window << "cannot be restacked"; + return; + } + + Q_ASSERT(unconstrained_stacking_order.contains(reference)); + if (reference == window) { + return; + } + + unconstrained_stacking_order.removeAll(window); + unconstrained_stacking_order.insert(unconstrained_stacking_order.indexOf(reference), window); + + m_focusChain->moveAfterWindow(window, reference); + updateStackingOrder(); +} + +void Workspace::stackAbove(Window *window, Window *reference) +{ + if (window->isDeleted()) { + qCWarning(KWIN_CORE) << "Workspace::stackAbove: closed window" << window << "cannot be restacked"; + return; + } + + Q_ASSERT(unconstrained_stacking_order.contains(reference)); + if (reference == window) { + return; + } + + unconstrained_stacking_order.removeAll(window); + unconstrained_stacking_order.insert(unconstrained_stacking_order.indexOf(reference) + 1, window); + + m_focusChain->moveBeforeWindow(window, reference); + updateStackingOrder(); +} + +void Workspace::restackWindowUnderActive(Window *window) +{ + if (!m_activeWindow || m_activeWindow == window || m_activeWindow->layer() != window->layer()) { + raiseWindow(window); + return; + } + + Window *reference = m_activeWindow; + if (!Window::belongToSameApplication(reference, window)) { + // put in the stacking order below _all_ windows belonging to the active application + for (int i = 0; i < unconstrained_stacking_order.size(); ++i) { + auto other = unconstrained_stacking_order.at(i); + if (other->isClient() && other->layer() == window->layer() && Window::belongToSameApplication(reference, other)) { + reference = other; + break; + } + } + } + + stackBelow(window, reference); +} + +#if KWIN_BUILD_X11 +void Workspace::restoreSessionStackingOrder(X11Window *window) +{ + if (window->sessionStackingOrder() < 0) { + return; + } + StackingUpdatesBlocker blocker(this); + unconstrained_stacking_order.removeAll(window); + for (auto it = unconstrained_stacking_order.begin(); it != unconstrained_stacking_order.end(); ++it) { + X11Window *current = qobject_cast(*it); + if (!current || current->isDeleted() || current->isUnmanaged()) { + continue; + } + if (current->sessionStackingOrder() > window->sessionStackingOrder()) { + unconstrained_stacking_order.insert(it, window); + return; + } + } + unconstrained_stacking_order.append(window); +} +#endif + +/** + * Returns a stacking order based upon \a list that fulfills certain contained. + */ +QList Workspace::constrainedStackingOrder() +{ + // Sort the windows based on their layers while preserving their relative order in the + // unconstrained stacking order. + std::array, NumLayers> windows; + for (Window *window : std::as_const(unconstrained_stacking_order)) { + const Layer layer = computeLayer(window); + windows[layer] << window; + } + + QList stacking; + stacking.reserve(unconstrained_stacking_order.count()); + for (uint layer = FirstLayer; layer < NumLayers; ++layer) { + stacking += windows[layer]; + } + + // Apply the stacking order constraints. First, we enqueue the root constraints, i.e. + // the ones that are not affected by other constraints. + QList constraints; + constraints.reserve(m_constraints.count()); + for (Constraint *constraint : std::as_const(m_constraints)) { + if (constraint->parents.isEmpty()) { + constraint->enqueued = true; + constraints.append(constraint); + } else { + constraint->enqueued = false; + } + } + + // Preserve the relative order of transient siblings in the unconstrained stacking order. + auto constraintComparator = [&stacking](Constraint *a, Constraint *b) { + return stacking.indexOf(a->above) > stacking.indexOf(b->above); + }; + std::sort(constraints.begin(), constraints.end(), constraintComparator); + + // Once we've enqueued all the root constraints, we traverse the constraints tree in + // the reverse breadth-first search fashion. A constraint is applied only if its condition is + // not met. + while (!constraints.isEmpty()) { + Constraint *constraint = constraints.takeFirst(); + + const int belowIndex = stacking.indexOf(constraint->below); + const int aboveIndex = stacking.indexOf(constraint->above); + if (belowIndex == -1 || aboveIndex == -1) { + continue; + } else if (aboveIndex < belowIndex) { + stacking.removeAt(aboveIndex); + stacking.insert(belowIndex, constraint->above); + } + + // Preserve the relative order of transient siblings in the unconstrained stacking order. + QList children = constraint->children; + std::sort(children.begin(), children.end(), constraintComparator); + + for (Constraint *child : std::as_const(children)) { + if (!child->enqueued) { + child->enqueued = true; + constraints.append(child); + } + } + } + + return stacking; +} + +void Workspace::blockStackingUpdates(bool block) +{ + if (block) { + if (m_blockStackingUpdates == 0) { + m_blockedPropagatingNewWindows = false; + } + ++m_blockStackingUpdates; + } else // !block + if (--m_blockStackingUpdates == 0) { + updateStackingOrder(m_blockedPropagatingNewWindows); + } +} + +namespace +{ +template +QList ensureStackingOrderInList(const QList &stackingOrder, const QList &list) +{ + static_assert(std::is_base_of::value, + "U must be derived from T"); + // TODO Q_ASSERT( block_stacking_updates == 0 ); + if (list.count() < 2) { + return list; + } + // TODO is this worth optimizing? + QList result = list; + for (auto it = stackingOrder.begin(); it != stackingOrder.end(); ++it) { + T *window = qobject_cast(*it); + if (!window) { + continue; + } + if (result.removeAll(window) != 0) { + result.append(window); + } + } + return result; +} +} + +#if KWIN_BUILD_X11 +// Ensure list is in stacking order +QList Workspace::ensureStackingOrder(const QList &list) const +{ + return ensureStackingOrderInList(stacking_order, list); +} +#endif + +QList Workspace::ensureStackingOrder(const QList &list) const +{ + return ensureStackingOrderInList(stacking_order, list); +} + +QList Workspace::unconstrainedStackingOrder() const +{ + return unconstrained_stacking_order; +} + +#if KWIN_BUILD_X11 +bool Workspace::updateXStackingOrder() +{ + // we use our stacking order for managed windows, but X's for override-redirect windows + Xcb::Tree tree(kwinApp()->x11RootWindow()); + if (tree.isNull()) { + return false; + } + xcb_window_t *windows = tree.children(); + + const auto count = tree.data()->children_len; + bool changed = false; + for (unsigned int i = 0; i < count; ++i) { + auto window = findUnmanaged(windows[i]); + if (window) { + unconstrained_stacking_order.removeAll(window); + unconstrained_stacking_order.append(window); + changed = true; + } + } + return changed; +} +#endif + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/layershellv1integration.cpp b/local/recipes/kde/kwin/source/src/layershellv1integration.cpp new file mode 100644 index 0000000000..47be46ac2d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/layershellv1integration.cpp @@ -0,0 +1,223 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "layershellv1integration.h" +#include "core/output.h" +#include "layershellv1window.h" +#include "wayland/display.h" +#include "wayland/layershell_v1.h" +#include "wayland/output.h" +#include "wayland_server.h" +#include "workspace.h" + +namespace KWin +{ + +static const Qt::Edges AnchorHorizontal = Qt::LeftEdge | Qt::RightEdge; +static const Qt::Edges AnchorVertical = Qt::TopEdge | Qt::BottomEdge; + +LayerShellV1Integration::LayerShellV1Integration(QObject *parent) + : WaylandShellIntegration(parent) +{ + LayerShellV1Interface *shell = new LayerShellV1Interface(waylandServer()->display(), this); + connect(shell, &LayerShellV1Interface::surfaceCreated, + this, &LayerShellV1Integration::createWindow); + + connect(workspace(), &Workspace::aboutToRearrange, this, &LayerShellV1Integration::rearrange); +} + +void LayerShellV1Integration::createWindow(LayerSurfaceV1Interface *shellSurface) +{ + LogicalOutput *output; + if (OutputInterface *preferredOutput = shellSurface->output()) { + if (preferredOutput->isRemoved()) { + shellSurface->sendClosed(); + return; + } + output = preferredOutput->handle(); + } else { + output = workspace()->activeOutput(); + } + + Q_EMIT windowCreated(new LayerShellV1Window(shellSurface, output, this)); +} + +void LayerShellV1Integration::recreateWindow(LayerSurfaceV1Interface *shellSurface) +{ + destroyWindow(shellSurface); + createWindow(shellSurface); +} + +void LayerShellV1Integration::destroyWindow(LayerSurfaceV1Interface *shellSurface) +{ + const QList windows = waylandServer()->windows(); + for (Window *window : windows) { + LayerShellV1Window *layerShellWindow = qobject_cast(window); + if (layerShellWindow && layerShellWindow->shellSurface() == shellSurface) { + layerShellWindow->destroyWindow(); + break; + } + } +} + +static void adjustWorkArea(const LayerSurfaceV1Interface *shellSurface, Rect *workArea) +{ + switch (shellSurface->exclusiveEdge()) { + case Qt::LeftEdge: + workArea->adjust(shellSurface->leftMargin() + shellSurface->exclusiveZone(), 0, 0, 0); + break; + case Qt::RightEdge: + workArea->adjust(0, 0, -shellSurface->rightMargin() - shellSurface->exclusiveZone(), 0); + break; + case Qt::TopEdge: + workArea->adjust(0, shellSurface->topMargin() + shellSurface->exclusiveZone(), 0, 0); + break; + case Qt::BottomEdge: + workArea->adjust(0, 0, 0, -shellSurface->bottomMargin() - shellSurface->exclusiveZone()); + break; + } +} + +static void rearrangeLayer(const QList &windows, Rect *workArea, + LayerSurfaceV1Interface::Layer layer, bool exclusive) +{ + for (LayerShellV1Window *window : windows) { + LayerSurfaceV1Interface *shellSurface = window->shellSurface(); + + if (shellSurface->layer() != layer) { + continue; + } + if (exclusive != (shellSurface->exclusiveZone() > 0)) { + continue; + } + + Rect bounds; + if (shellSurface->exclusiveZone() == -1) { + bounds = window->desiredOutput()->geometry(); + } else { + bounds = *workArea; + } + + Rect geometry(QPoint(0, 0), shellSurface->desiredSize()); + + if ((shellSurface->anchor() & AnchorHorizontal) && geometry.width() == 0) { + geometry.setLeft(bounds.left()); + geometry.setWidth(bounds.width()); + } else if (shellSurface->anchor() & Qt::LeftEdge) { + geometry.moveLeft(bounds.left()); + } else if (shellSurface->anchor() & Qt::RightEdge) { + geometry.moveRight(bounds.right()); + } else { + geometry.moveLeft(bounds.left() + (bounds.width() - geometry.width()) / 2); + } + + if ((shellSurface->anchor() & AnchorVertical) && geometry.height() == 0) { + geometry.setTop(bounds.top()); + geometry.setHeight(bounds.height()); + } else if (shellSurface->anchor() & Qt::TopEdge) { + geometry.moveTop(bounds.top()); + } else if (shellSurface->anchor() & Qt::BottomEdge) { + geometry.moveBottom(bounds.bottom()); + } else { + geometry.moveTop(bounds.top() + (bounds.height() - geometry.height()) / 2); + } + + if ((shellSurface->anchor() & AnchorHorizontal) == AnchorHorizontal) { + geometry.adjust(shellSurface->leftMargin(), 0, -shellSurface->rightMargin(), 0); + } else if (shellSurface->anchor() & Qt::LeftEdge) { + geometry.translate(shellSurface->leftMargin(), 0); + } else if (shellSurface->anchor() & Qt::RightEdge) { + geometry.translate(-shellSurface->rightMargin(), 0); + } + + if ((shellSurface->anchor() & AnchorVertical) == AnchorVertical) { + geometry.adjust(0, shellSurface->topMargin(), 0, -shellSurface->bottomMargin()); + } else if (shellSurface->anchor() & Qt::TopEdge) { + geometry.translate(0, shellSurface->topMargin()); + } else if (shellSurface->anchor() & Qt::BottomEdge) { + geometry.translate(0, -shellSurface->bottomMargin()); + } + + // Move the window's bottom if its virtual keyboard is overlapping it + if (shellSurface->exclusiveZone() >= 0 && !window->virtualKeyboardGeometry().isEmpty() && geometry.bottom() > window->virtualKeyboardGeometry().top()) { + geometry.setBottom(window->virtualKeyboardGeometry().top()); + } + + window->updateLayer(); + + if (geometry.isValid()) { + window->place(geometry); + } else { + qCWarning(KWIN_CORE) << "Closing a layer shell window due to invalid geometry"; + window->closeWindow(); + continue; + } + + if (exclusive && shellSurface->exclusiveZone() > 0) { + adjustWorkArea(shellSurface, workArea); + } + } +} + +static int weightForWindow(const LayerShellV1Window *window) +{ + return (window->shellSurface()->anchor() & AnchorHorizontal) == AnchorHorizontal ? 1 : 0; +} + +static QList windowsForOutput(LogicalOutput *output) +{ + QList result; + const QList windows = waylandServer()->windows(); + for (Window *window : windows) { + LayerShellV1Window *layerShellWindow = qobject_cast(window); + if (!layerShellWindow || layerShellWindow->desiredOutput() != output) { + continue; + } + if (layerShellWindow->shellSurface()->isCommitted()) { + result.append(layerShellWindow); + } + } + std::stable_sort(result.begin(), result.end(), [](LayerShellV1Window *a, LayerShellV1Window *b) { + if (a->layer() < b->layer()) { + return false; + } else if (a->layer() > b->layer()) { + return true; + } else { + return weightForWindow(a) > weightForWindow(b); + } + }); + return result; +} + +static void rearrangeOutput(LogicalOutput *output) +{ + const QList windows = windowsForOutput(output); + if (!windows.isEmpty()) { + Rect workArea = output->geometry(); + + rearrangeLayer(windows, &workArea, LayerSurfaceV1Interface::OverlayLayer, true); + rearrangeLayer(windows, &workArea, LayerSurfaceV1Interface::TopLayer, true); + rearrangeLayer(windows, &workArea, LayerSurfaceV1Interface::BottomLayer, true); + rearrangeLayer(windows, &workArea, LayerSurfaceV1Interface::BackgroundLayer, true); + + rearrangeLayer(windows, &workArea, LayerSurfaceV1Interface::OverlayLayer, false); + rearrangeLayer(windows, &workArea, LayerSurfaceV1Interface::TopLayer, false); + rearrangeLayer(windows, &workArea, LayerSurfaceV1Interface::BottomLayer, false); + rearrangeLayer(windows, &workArea, LayerSurfaceV1Interface::BackgroundLayer, false); + } +} + +void LayerShellV1Integration::rearrange() +{ + const QList outputs = workspace()->outputs(); + for (LogicalOutput *output : outputs) { + rearrangeOutput(output); + } +} + +} // namespace KWin + +#include "moc_layershellv1integration.cpp" diff --git a/local/recipes/kde/kwin/source/src/layershellv1integration.h b/local/recipes/kde/kwin/source/src/layershellv1integration.h new file mode 100644 index 0000000000..4042ef23d6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/layershellv1integration.h @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "waylandshellintegration.h" + +namespace KWin +{ + +class LayerSurfaceV1Interface; + +class LayerShellV1Integration : public WaylandShellIntegration +{ + Q_OBJECT + +public: + explicit LayerShellV1Integration(QObject *parent = nullptr); + + void rearrange(); + + void createWindow(LayerSurfaceV1Interface *shellSurface); + void recreateWindow(LayerSurfaceV1Interface *shellSurface); + void destroyWindow(LayerSurfaceV1Interface *shellSurface); +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/layershellv1window.cpp b/local/recipes/kde/kwin/source/src/layershellv1window.cpp new file mode 100644 index 0000000000..b0c278687a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/layershellv1window.cpp @@ -0,0 +1,407 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "layershellv1window.h" +#include "core/output.h" +#include "core/pixelgrid.h" +#include "layershellv1integration.h" +#include "screenedge.h" +#include "wayland/layershell_v1.h" +#include "wayland/output.h" +#include "wayland/screenedge_v1.h" +#include "wayland/surface.h" +#include "wayland_server.h" +#include "workspace.h" + +namespace KWin +{ + +static WindowType scopeToType(const QString &scope) +{ + static const QHash scopeToType{ + {QStringLiteral("desktop"), WindowType::Desktop}, + {QStringLiteral("dock"), WindowType::Dock}, + {QStringLiteral("crititical-notification"), WindowType::CriticalNotification}, + {QStringLiteral("notification"), WindowType::Notification}, + {QStringLiteral("tooltip"), WindowType::Tooltip}, + {QStringLiteral("on-screen-display"), WindowType::OnScreenDisplay}, + {QStringLiteral("dialog"), WindowType::Dialog}, + {QStringLiteral("splash"), WindowType::Splash}, + {QStringLiteral("utility"), WindowType::Utility}, + }; + return scopeToType.value(scope.toLower(), WindowType::Normal); +} + +LayerShellV1Window::LayerShellV1Window(LayerSurfaceV1Interface *shellSurface, + LogicalOutput *output, + LayerShellV1Integration *integration) + : WaylandWindow(shellSurface->surface()) + , m_desiredOutput(output) + , m_integration(integration) + , m_shellSurface(shellSurface) + , m_windowType(scopeToType(shellSurface->scope())) +{ + setOutput(output); + setMoveResizeOutput(output); + setSkipSwitcher(!isDesktop()); + setSkipPager(true); + setSkipTaskbar(true); + + connect(shellSurface, &LayerSurfaceV1Interface::aboutToBeDestroyed, + this, &LayerShellV1Window::destroyWindow); + connect(shellSurface->surface(), &SurfaceInterface::aboutToBeDestroyed, + this, &LayerShellV1Window::destroyWindow); + + connect(workspace(), &Workspace::outputRemoved, + this, &LayerShellV1Window::handleOutputRemoved); + + connect(shellSurface->surface(), &SurfaceInterface::sizeChanged, + this, &LayerShellV1Window::handleSizeChanged); + connect(shellSurface->surface(), &SurfaceInterface::unmapped, + this, &LayerShellV1Window::handleUnmapped); + connect(shellSurface->surface(), &SurfaceInterface::committed, + this, &LayerShellV1Window::handleCommitted); + + connect(shellSurface, &LayerSurfaceV1Interface::desiredSizeChanged, + this, &LayerShellV1Window::scheduleRearrange); + connect(shellSurface, &LayerSurfaceV1Interface::layerChanged, + this, &LayerShellV1Window::scheduleRearrange); + connect(shellSurface, &LayerSurfaceV1Interface::marginsChanged, + this, &LayerShellV1Window::scheduleRearrange); + connect(shellSurface, &LayerSurfaceV1Interface::anchorChanged, + this, &LayerShellV1Window::scheduleRearrange); + connect(shellSurface, &LayerSurfaceV1Interface::exclusiveZoneChanged, + this, &LayerShellV1Window::scheduleRearrange); + connect(shellSurface, &LayerSurfaceV1Interface::acceptsFocusChanged, + this, &LayerShellV1Window::handleAcceptsFocusChanged); + connect(shellSurface, &LayerSurfaceV1Interface::configureAcknowledged, + this, &LayerShellV1Window::handleConfigureAcknowledged); + + m_rescalingTimer.setSingleShot(true); + m_rescalingTimer.setInterval(0); + connect(&m_rescalingTimer, &QTimer::timeout, this, &LayerShellV1Window::handleTargetScaleChange); +} + +LayerSurfaceV1Interface *LayerShellV1Window::shellSurface() const +{ + return m_shellSurface; +} + +LogicalOutput *LayerShellV1Window::desiredOutput() const +{ + return m_desiredOutput; +} + +void LayerShellV1Window::scheduleRearrange() +{ + workspace()->scheduleRearrange(); +} + +WindowType LayerShellV1Window::windowType() const +{ + return m_windowType; +} + +bool LayerShellV1Window::isPlaceable() const +{ + return false; +} + +bool LayerShellV1Window::isCloseable() const +{ + return true; +} + +bool LayerShellV1Window::isMovable() const +{ + return false; +} + +bool LayerShellV1Window::isMovableAcrossScreens() const +{ + return false; +} + +bool LayerShellV1Window::isResizable() const +{ + return false; +} + +bool LayerShellV1Window::wantsInput() const +{ + return acceptsFocus() && readyForPainting(); +} + +StrutRect LayerShellV1Window::strutRect(StrutArea area) const +{ + switch (area) { + case StrutAreaLeft: + if (m_shellSurface->exclusiveEdge() == Qt::LeftEdge) { + return StrutRect(m_moveResizeGeometry.x(), + m_moveResizeGeometry.y(), + m_shellSurface->exclusiveZone(), + m_moveResizeGeometry.height(), + StrutAreaLeft); + } + return StrutRect(); + case StrutAreaRight: + if (m_shellSurface->exclusiveEdge() == Qt::RightEdge) { + return StrutRect(m_moveResizeGeometry.x() + m_moveResizeGeometry.width() - m_shellSurface->exclusiveZone(), + m_moveResizeGeometry.y(), + m_shellSurface->exclusiveZone(), + m_moveResizeGeometry.height(), + StrutAreaRight); + } + return StrutRect(); + case StrutAreaTop: + if (m_shellSurface->exclusiveEdge() == Qt::TopEdge) { + return StrutRect(m_moveResizeGeometry.x(), + m_moveResizeGeometry.y(), + m_moveResizeGeometry.width(), + m_shellSurface->exclusiveZone(), + StrutAreaTop); + } + return StrutRect(); + case StrutAreaBottom: + if (m_shellSurface->exclusiveEdge() == Qt::BottomEdge) { + return StrutRect(m_moveResizeGeometry.x(), + m_moveResizeGeometry.y() + m_moveResizeGeometry.height() - m_shellSurface->exclusiveZone(), + m_moveResizeGeometry.width(), + m_shellSurface->exclusiveZone(), + StrutAreaBottom); + } + return StrutRect(); + default: + return StrutRect(); + } +} + +bool LayerShellV1Window::hasStrut() const +{ + return m_shellSurface->exclusiveZone() > 0; +} + +void LayerShellV1Window::destroyWindow() +{ + if (m_screenEdge) { + m_screenEdge->disconnect(this); + } + m_shellSurface->disconnect(this); + m_shellSurface->surface()->disconnect(this); + + disconnect(workspace(), &Workspace::outputRemoved, + this, &LayerShellV1Window::handleOutputRemoved); + + markAsDeleted(); + Q_EMIT closed(); + + m_rescalingTimer.stop(); + cleanTabBox(); + StackingUpdatesBlocker blocker(workspace()); + cleanGrouping(); + waylandServer()->removeWindow(this); + scheduleRearrange(); + unref(); +} + +void LayerShellV1Window::closeWindow() +{ + if (!isDeleted()) { + m_shellSurface->sendClosed(); + } +} + +Layer LayerShellV1Window::belongsToLayer() const +{ + switch (m_shellSurface->layer()) { + case LayerSurfaceV1Interface::BackgroundLayer: + return DesktopLayer; + case LayerSurfaceV1Interface::BottomLayer: + return BelowLayer; + case LayerSurfaceV1Interface::TopLayer: + return AboveLayer; + case LayerSurfaceV1Interface::OverlayLayer: + return OverlayLayer; + default: + Q_UNREACHABLE(); + } +} + +bool LayerShellV1Window::acceptsFocus() const +{ + return !isDeleted() && m_shellSurface->acceptsFocus(); +} + +void LayerShellV1Window::moveResizeInternal(const RectF &rect, MoveResizeMode mode) +{ + const QSize requestedClientSize = nextFrameSizeToClientSize(rect.size()).toSize(); + + if (!m_configureEvents.isEmpty()) { + const LayerShellV1ConfigureEvent &lastLayerShellV1ConfigureEvent = m_configureEvents.constLast(); + if (lastLayerShellV1ConfigureEvent.size != requestedClientSize) { + const quint32 serial = m_shellSurface->sendConfigure(requestedClientSize); + m_configureEvents.append({serial, requestedClientSize}); + } + } else if (requestedClientSize != clientSize()) { + const quint32 serial = m_shellSurface->sendConfigure(requestedClientSize); + m_configureEvents.append({serial, requestedClientSize}); + } else { + updateGeometry(rect); + return; + } + + // The surface position is updated synchronously. + RectF updateRect = m_frameGeometry; + updateRect.moveTopLeft(rect.topLeft()); + updateGeometry(updateRect); +} + +void LayerShellV1Window::doSetNextTargetScale() +{ + if (isDeleted()) { + return; + } + surface()->setPreferredBufferScale(nextTargetScale()); + setTargetScale(nextTargetScale()); + m_rescalingTimer.start(); +} + +void LayerShellV1Window::doSetPreferredBufferTransform() +{ + if (isDeleted()) { + return; + } + surface()->setPreferredBufferTransform(preferredBufferTransform()); +} + +void LayerShellV1Window::doSetPreferredColorDescription() +{ + if (isDeleted()) { + return; + } + surface()->setPreferredColorDescription(preferredColorDescription()); +} + +void LayerShellV1Window::handleConfigureAcknowledged(quint32 serial) +{ + while (!m_configureEvents.isEmpty()) { + const LayerShellV1ConfigureEvent head = m_configureEvents.takeFirst(); + if (head.serial == serial) { + break; + } + } +} + +void LayerShellV1Window::handleSizeChanged() +{ + updateGeometry(RectF(pos(), clientSizeToFrameSize(surface()->size()))); +} + +void LayerShellV1Window::handleUnmapped() +{ + m_integration->recreateWindow(shellSurface()); +} + +void LayerShellV1Window::handleCommitted() +{ + if (surface()->buffer()) { + markAsMapped(); + } +} + +void LayerShellV1Window::handleAcceptsFocusChanged() +{ + switch (m_shellSurface->layer()) { + case LayerSurfaceV1Interface::TopLayer: + case LayerSurfaceV1Interface::OverlayLayer: + if (wantsInput()) { + workspace()->activateWindow(this); + } + break; + case LayerSurfaceV1Interface::BackgroundLayer: + case LayerSurfaceV1Interface::BottomLayer: + break; + } +} + +void LayerShellV1Window::handleOutputRemoved(LogicalOutput *output) +{ + if (output == m_desiredOutput) { + closeWindow(); + destroyWindow(); + } +} + +void LayerShellV1Window::setVirtualKeyboardGeometry(const RectF &geo) +{ + if (m_virtualKeyboardGeometry == geo) { + return; + } + + m_virtualKeyboardGeometry = geo; + scheduleRearrange(); +} + +void LayerShellV1Window::showOnScreenEdge() +{ + // ShowOnScreenEdge can be called by an Edge, and setHidden could destroy the Edge + // Use the singleshot to avoid use-after-free + QTimer::singleShot(0, this, &LayerShellV1Window::deactivateScreenEdge); +} + +void LayerShellV1Window::installAutoHideScreenEdgeV1(AutoHideScreenEdgeV1Interface *edge) +{ + m_screenEdge = edge; + + connect(edge, &AutoHideScreenEdgeV1Interface::destroyed, + this, &LayerShellV1Window::deactivateScreenEdge); + connect(edge, &AutoHideScreenEdgeV1Interface::activateRequested, + this, &LayerShellV1Window::activateScreenEdge); + connect(edge, &AutoHideScreenEdgeV1Interface::deactivateRequested, + this, &LayerShellV1Window::deactivateScreenEdge); + + connect(this, &LayerShellV1Window::frameGeometryChanged, edge, [this]() { + if (m_screenEdgeActive) { + reserveScreenEdge(); + } + }); +} + +void LayerShellV1Window::reserveScreenEdge() +{ + if (workspace()->screenEdges()->reserve(this, m_screenEdge->border())) { + setHidden(true); + } else { + setHidden(false); + } +} + +void LayerShellV1Window::unreserveScreenEdge() +{ + setHidden(false); + workspace()->screenEdges()->reserve(this, ElectricNone); +} + +void LayerShellV1Window::activateScreenEdge() +{ + m_screenEdgeActive = true; + reserveScreenEdge(); +} + +void LayerShellV1Window::deactivateScreenEdge() +{ + m_screenEdgeActive = false; + unreserveScreenEdge(); +} + +void LayerShellV1Window::handleTargetScaleChange() +{ + moveResize(snapToPixels(m_moveResizeGeometry, m_targetScale)); +} + +} // namespace KWin + +#include "moc_layershellv1window.cpp" diff --git a/local/recipes/kde/kwin/source/src/layershellv1window.h b/local/recipes/kde/kwin/source/src/layershellv1window.h new file mode 100644 index 0000000000..3c443a8da6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/layershellv1window.h @@ -0,0 +1,85 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "waylandwindow.h" + +namespace KWin +{ + +struct LayerShellV1ConfigureEvent +{ + quint32 serial; + QSize size; +}; + +class AutoHideScreenEdgeV1Interface; +class LayerSurfaceV1Interface; +class LogicalOutput; +class LayerShellV1Integration; + +class LayerShellV1Window : public WaylandWindow +{ + Q_OBJECT + +public: + explicit LayerShellV1Window(LayerSurfaceV1Interface *shellSurface, + LogicalOutput *output, + LayerShellV1Integration *integration); + + LayerSurfaceV1Interface *shellSurface() const; + LogicalOutput *desiredOutput() const; + + WindowType windowType() const override; + bool isPlaceable() const override; + bool isCloseable() const override; + bool isMovable() const override; + bool isMovableAcrossScreens() const override; + bool isResizable() const override; + bool wantsInput() const override; + StrutRect strutRect(StrutArea area) const override; + bool hasStrut() const override; + void destroyWindow() override; + void closeWindow() override; + void setVirtualKeyboardGeometry(const RectF &geo) override; + void showOnScreenEdge() override; + + void installAutoHideScreenEdgeV1(AutoHideScreenEdgeV1Interface *edge); + +protected: + Layer belongsToLayer() const override; + bool acceptsFocus() const override; + void moveResizeInternal(const RectF &rect, MoveResizeMode mode) override; + void doSetNextTargetScale() override; + void doSetPreferredBufferTransform() override; + void doSetPreferredColorDescription() override; + +private: + void handleConfigureAcknowledged(quint32 serial); + void handleSizeChanged(); + void handleUnmapped(); + void handleCommitted(); + void handleAcceptsFocusChanged(); + void handleOutputRemoved(LogicalOutput *output); + void scheduleRearrange(); + void activateScreenEdge(); + void deactivateScreenEdge(); + void reserveScreenEdge(); + void unreserveScreenEdge(); + void handleTargetScaleChange(); + + LogicalOutput *m_desiredOutput; + LayerShellV1Integration *m_integration; + LayerSurfaceV1Interface *m_shellSurface; + QPointer m_screenEdge; + bool m_screenEdgeActive = false; + WindowType m_windowType; + QList m_configureEvents; + QTimer m_rescalingTimer; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/lidswitchtracker.cpp b/local/recipes/kde/kwin/source/src/lidswitchtracker.cpp new file mode 100644 index 0000000000..b1a1b37c02 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/lidswitchtracker.cpp @@ -0,0 +1,39 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "lidswitchtracker.h" +#include "core/inputdevice.h" +#include "input_event.h" + +namespace KWin +{ + +LidSwitchTracker::LidSwitchTracker() +{ + input()->installInputEventSpy(this); +} + +bool LidSwitchTracker::isLidClosed() const +{ + return m_isLidClosed; +} + +void LidSwitchTracker::switchEvent(KWin::SwitchEvent *event) +{ + if (event->device->isLidSwitch()) { + const bool state = event->state == SwitchState::On; + if (state != m_isLidClosed) { + m_isLidClosed = state; + Q_EMIT lidStateChanged(); + } + } +} + +} + +#include "moc_lidswitchtracker.cpp" diff --git a/local/recipes/kde/kwin/source/src/lidswitchtracker.h b/local/recipes/kde/kwin/source/src/lidswitchtracker.h new file mode 100644 index 0000000000..3eee6c4f85 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/lidswitchtracker.h @@ -0,0 +1,34 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "input_event_spy.h" + +#include + +namespace KWin +{ + +class LidSwitchTracker : public QObject, InputEventSpy +{ + Q_OBJECT +public: + explicit LidSwitchTracker(); + + bool isLidClosed() const; + +Q_SIGNALS: + void lidStateChanged(); + +private: + void switchEvent(KWin::SwitchEvent *event) override; + + bool m_isLidClosed = false; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/main.cpp b/local/recipes/kde/kwin/source/src/main.cpp new file mode 100644 index 0000000000..059768ffa1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/main.cpp @@ -0,0 +1,630 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "main.h" + +#include "config-kwin.h" + +#if KWIN_BUILD_X11 +#include "atoms.h" +#endif +#include "compositor.h" +#include "core/outputbackend.h" +#include "core/rendertarget.h" +#include "core/session.h" +#include "cursor.h" +#include "cursorsource.h" +#include "effect/effecthandler.h" +#include "input.h" +#include "inputmethod.h" +#include "opengl/gltexture.h" +#include "opengl/glutils.h" +#include "options.h" +#include "outline.h" +#include "pluginmanager.h" +#include "pointer_input.h" +#include "scene/workspacescene.h" +#include "screenedge.h" +#include "sm.h" +#include "tabletmodemanager.h" +#include "wayland/surface.h" +#include "workspace.h" + +#if KWIN_BUILD_X11 +#include "utils/xcbutils.h" +#include "x11eventfilter.h" +#endif + +#include "effect/effecthandler.h" + +// KDE +#include +#include +// Qt +#include +#include +#if KWIN_BUILD_X11 +#include +#endif +#include + +#include + +#if __has_include() +#include +#endif +#include + +#if KWIN_BUILD_X11 +#ifndef XCB_GE_GENERIC +#define XCB_GE_GENERIC 35 +#endif +#endif + +Q_DECLARE_METATYPE(KSharedConfigPtr) + +namespace KWin +{ + +Options *options; +#if KWIN_BUILD_X11 +Atoms *atoms; +#endif +int Application::crashes = 0; + +Application::Application(int &argc, char **argv) + : QApplication(argc, argv) +#if KWIN_BUILD_X11 + , m_eventFilter(new XcbEventFilter()) +#endif + , m_configLock(false) + , m_config(KSharedConfig::openConfig(QStringLiteral("kwinrc"))) + , m_kxkbConfig() + , m_kdeglobals(KSharedConfig::openConfig(QStringLiteral("kdeglobals"))) +{ + qRegisterMetaType("Options::WindowOperation"); + qRegisterMetaType(); + qRegisterMetaType("KWin::SurfaceInterface *"); + qRegisterMetaType(); + qRegisterMetaType(); +} + +void Application::setConfigLock(bool lock) +{ + m_configLock = lock; +} + +void Application::start() +{ + // Prevent KWin from synchronously autostarting kactivitymanagerd + // Indeed, kactivitymanagerd being a QApplication it will depend + // on KWin startup... this is unsatisfactory dependency wise, + // and it turns out that it leads to a deadlock in the Wayland case + setProperty("org.kde.KActivities.core.disableAutostart", true); + + setQuitOnLastWindowClosed(false); + setQuitLockEnabled(false); + + if (!m_config->isImmutable() && m_configLock) { + // TODO: This shouldn't be necessary + // config->setReadOnly( true ); + m_config->reparseConfiguration(); + } + if (!m_kxkbConfig) { + m_kxkbConfig = KSharedConfig::openConfig(QStringLiteral("kxkbrc"), KConfig::NoGlobals); + } + if (!m_inputConfig) { + m_inputConfig = KSharedConfig::openConfig(QStringLiteral("kcminputrc"), KConfig::NoGlobals); + } + + performStartup(); +} + +Application::~Application() +{ + delete options; + destroyAtoms(); + destroyPlatform(); + m_session.reset(); +} + +void Application::destroyAtoms() +{ +#if KWIN_BUILD_X11 + delete atoms; + atoms = nullptr; +#endif +} + +void Application::destroyPlatform() +{ + m_outputBackend.reset(); +} + +void Application::resetCrashesCount() +{ + crashes = 0; +} + +void Application::setCrashCount(int count) +{ + crashes = count; +} + +bool Application::wasCrash() +{ + return crashes > 0; +} + +void Application::createAboutData() +{ + KAboutData aboutData(QStringLiteral("kwin"), // The program name used internally + i18n("KWin"), // A displayable program name string + KWIN_VERSION_STRING, // The program version string + i18n("KDE window manager"), // Short description of what the app does + KAboutLicense::GPL, // The license this code is released under + i18n("(c) 1999-2019, The KDE Developers")); // Copyright Statement + + aboutData.addAuthor(i18n("Matthias Ettrich"), QString(), QStringLiteral("ettrich@kde.org")); + aboutData.addAuthor(i18n("Cristian Tibirna"), QString(), QStringLiteral("tibirna@kde.org")); + aboutData.addAuthor(i18n("Daniel M. Duley"), QString(), QStringLiteral("mosfet@kde.org")); + aboutData.addAuthor(i18n("Luboš Luňák"), QString(), QStringLiteral("l.lunak@kde.org")); + aboutData.addAuthor(i18n("Martin Flöser"), QString(), QStringLiteral("mgraesslin@kde.org")); + aboutData.addAuthor(i18n("David Edmundson"), QStringLiteral("Maintainer"), QStringLiteral("davidedmundson@kde.org")); + aboutData.addAuthor(i18n("Roman Gilg"), QStringLiteral("Maintainer"), QStringLiteral("subdiff@gmail.com")); + aboutData.addAuthor(i18n("Vlad Zahorodnii"), QStringLiteral("Maintainer"), QStringLiteral("vlad.zahorodnii@kde.org")); + aboutData.addAuthor(i18n("Xaver Hugl"), QStringLiteral("Maintainer"), QStringLiteral("xaver.hugl@gmail.com")); + KAboutData::setApplicationData(aboutData); +} + +static const QString s_lockOption = QStringLiteral("lock"); +static const QString s_crashesOption = QStringLiteral("crashes"); + +void Application::setupCommandLine(QCommandLineParser *parser) +{ + QCommandLineOption lockOption(s_lockOption, i18n("Disable configuration options")); + QCommandLineOption crashesOption(s_crashesOption, i18n("Indicate that KWin has recently crashed n times"), QStringLiteral("n")); + + parser->setApplicationDescription(i18n("KDE window manager")); + parser->addOption(lockOption); + parser->addOption(crashesOption); + KAboutData::applicationData().setupCommandLine(parser); +} + +void Application::processCommandLine(QCommandLineParser *parser) +{ + KAboutData aboutData = KAboutData::applicationData(); + aboutData.processCommandLine(parser); + setConfigLock(parser->isSet(s_lockOption)); + Application::setCrashCount(parser->value(s_crashesOption).toInt()); +} + +void Application::setupMalloc() +{ +#ifdef M_TRIM_THRESHOLD + // Prevent fragmentation of the heap by malloc (glibc). + // + // The default threshold is 128*1024, which can result in a large memory usage + // due to fragmentation especially if we use the raster graphicssystem. On the + // otherside if the threshold is too low, free() starts to permanently ask the kernel + // about shrinking the heap. + const int pagesize = sysconf(_SC_PAGESIZE); + mallopt(M_TRIM_THRESHOLD, 5 * pagesize); +#endif // M_TRIM_THRESHOLD +} + +void Application::setupLocalizedString() +{ + KLocalizedString::setApplicationDomain(QByteArrayLiteral("kwin")); +} + +void Application::createWorkspace() +{ + // we want all QQuickWindows with an alpha buffer, do here as Workspace might create QQuickWindows + QQuickWindow::setDefaultAlphaBuffer(true); + + // This tries to detect compositing options and can use GLX. GLX problems + // (X errors) shouldn't cause kwin to abort, so this is out of the + // critical startup section where x errors cause kwin to abort. + + // create workspace. + (void)new Workspace(); + Q_EMIT workspaceCreated(); +} + +void Application::createInput() +{ + auto input = InputRedirection::create(this); + input->init(); +} + +void Application::createAtoms() +{ +#if KWIN_BUILD_X11 + atoms = new Atoms; +#endif +} + +void Application::createOptions() +{ + options = new Options; +} + +void Application::createPlugins() +{ + m_pluginManager = std::make_unique(); +} + +void Application::createInputMethod() +{ + m_inputMethod = std::make_unique(); +} + +void Application::createTabletModeManager() +{ + m_tabletModeManager = std::make_unique(); +} + +TabletModeManager *Application::tabletModeManager() const +{ + return m_tabletModeManager.get(); +} + +#if KWIN_BUILD_X11 +void Application::installNativeX11EventFilter() +{ + installNativeEventFilter(m_eventFilter.get()); +} + +void Application::removeNativeX11EventFilter() +{ + removeNativeEventFilter(m_eventFilter.get()); +} +#endif + +void Application::destroyInput() +{ + delete InputRedirection::self(); +} + +void Application::destroyWorkspace() +{ + delete Workspace::self(); +} + +void Application::destroyCompositor() +{ + delete Compositor::self(); +} + +void Application::destroyPlugins() +{ + m_pluginManager.reset(); +} + +void Application::destroyInputMethod() +{ + m_inputMethod.reset(); +} + +void Application::setXwaylandScale(qreal scale) +{ + Q_ASSERT(scale != 0); + if (scale != m_xwaylandScale) { + m_xwaylandScale = scale; + applyXwaylandScale(); + Q_EMIT xwaylandScaleChanged(); + } +} + +void Application::applyXwaylandScale() +{ + const bool xwaylandClientsScale = KConfig(QStringLiteral("kdeglobals")) + .group(QStringLiteral("KScreen")) + .readEntry("XwaylandClientsScale", true); + + KConfigGroup xwaylandGroup = kwinApp()->config()->group(QStringLiteral("Xwayland")); + if (xwaylandClientsScale) { + xwaylandGroup.writeEntry("Scale", xwaylandScale(), KConfig::Notify); + } else { + xwaylandGroup.deleteEntry("Scale", KConfig::Notify); + } + xwaylandGroup.sync(); + +#if KWIN_BUILD_X11 + if (x11Connection()) { + // rerun the fonts kcm init that does the appropriate xrdb call with the new settings + QProcess::startDetached("kcminit", {"kcm_fonts_init", "kcm_style_init"}); + } +#endif +} + +#if KWIN_BUILD_X11 +void Application::registerEventFilter(X11EventFilter *filter) +{ + if (filter->isGenericEvent()) { + m_genericEventFilters.append(new X11EventFilterContainer(filter)); + } else { + m_eventFilters.append(new X11EventFilterContainer(filter)); + } +} + +static X11EventFilterContainer *takeEventFilter(X11EventFilter *eventFilter, + QList> &list) +{ + for (int i = 0; i < list.count(); ++i) { + X11EventFilterContainer *container = list.at(i); + if (container->filter() == eventFilter) { + return list.takeAt(i); + } + } + return nullptr; +} + +void Application::unregisterEventFilter(X11EventFilter *filter) +{ + X11EventFilterContainer *container = nullptr; + if (filter->isGenericEvent()) { + container = takeEventFilter(filter, m_genericEventFilters); + } else { + container = takeEventFilter(filter, m_eventFilters); + } + delete container; +} + +bool Application::dispatchEvent(xcb_generic_event_t *event) +{ + static const QList s_xcbEerrors({QByteArrayLiteral("Success"), + QByteArrayLiteral("BadRequest"), + QByteArrayLiteral("BadValue"), + QByteArrayLiteral("BadWindow"), + QByteArrayLiteral("BadPixmap"), + QByteArrayLiteral("BadAtom"), + QByteArrayLiteral("BadCursor"), + QByteArrayLiteral("BadFont"), + QByteArrayLiteral("BadMatch"), + QByteArrayLiteral("BadDrawable"), + QByteArrayLiteral("BadAccess"), + QByteArrayLiteral("BadAlloc"), + QByteArrayLiteral("BadColor"), + QByteArrayLiteral("BadGC"), + QByteArrayLiteral("BadIDChoice"), + QByteArrayLiteral("BadName"), + QByteArrayLiteral("BadLength"), + QByteArrayLiteral("BadImplementation"), + QByteArrayLiteral("Unknown")}); + + const uint8_t x11EventType = event->response_type & ~0x80; + if (!x11EventType) { + // let's check whether it's an error from one of the extensions KWin uses + xcb_generic_error_t *error = reinterpret_cast(event); + const QList extensions = Xcb::Extensions::self()->extensions(); + for (const auto &extension : extensions) { + if (error->major_code == extension.majorOpcode) { + QByteArray errorName; + if (error->error_code < s_xcbEerrors.size()) { + errorName = s_xcbEerrors.at(error->error_code); + } else if (error->error_code >= extension.errorBase) { + const int index = error->error_code - extension.errorBase; + if (index >= 0 && index < extension.errorCodes.size()) { + errorName = extension.errorCodes.at(index); + } + } + if (errorName.isEmpty()) { + errorName = QByteArrayLiteral("Unknown"); + } + qCWarning(KWIN_CORE, "XCB error: %d (%s), sequence: %d, resource id: %d, major code: %d (%s), minor code: %d (%s)", + int(error->error_code), errorName.constData(), + int(error->sequence), int(error->resource_id), + int(error->major_code), extension.name.constData(), + int(error->minor_code), + extension.opCodes.size() > error->minor_code ? extension.opCodes.at(error->minor_code).constData() : "Unknown"); + return true; + } + } + return false; + } + + if (x11EventType == XCB_GE_GENERIC) { + xcb_ge_generic_event_t *ge = reinterpret_cast(event); + + // We need to make a shadow copy of the event filter list because an activated event + // filter may mutate it by removing or installing another event filter. + const auto eventFilters = m_genericEventFilters; + + for (X11EventFilterContainer *container : eventFilters) { + if (!container) { + continue; + } + X11EventFilter *filter = container->filter(); + if (filter->extension() == ge->extension && filter->genericEventTypes().contains(ge->event_type) && filter->event(event)) { + return true; + } + } + } else { + // We need to make a shadow copy of the event filter list because an activated event + // filter may mutate it by removing or installing another event filter. + const auto eventFilters = m_eventFilters; + + for (X11EventFilterContainer *container : eventFilters) { + if (!container) { + continue; + } + X11EventFilter *filter = container->filter(); + if (filter->eventTypes().contains(x11EventType) && filter->event(event)) { + return true; + } + } + } + + if (workspace()) { + return workspace()->workspaceEvent(event); + } + + return false; +} + +static quint32 monotonicTime() +{ + timespec ts; + + const int result = clock_gettime(CLOCK_MONOTONIC, &ts); + if (result) { + qCWarning(KWIN_CORE, "Failed to query monotonic time: %s", strerror(errno)); + } + + return ts.tv_sec * 1000 + ts.tv_nsec / 1000000L; +} + +xcb_timestamp_t Application::x11Time() const +{ + return monotonicTime(); +} + +bool XcbEventFilter::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) +{ + if (eventType == "xcb_generic_event_t") { + return kwinApp()->dispatchEvent(static_cast(message)); + } + return false; +} + +#endif + +QProcessEnvironment Application::processStartupEnvironment() const +{ + return m_processEnvironment; +} + +void Application::setProcessStartupEnvironment(const QProcessEnvironment &environment) +{ + m_processEnvironment = environment; +} + +void Application::setOutputBackend(std::unique_ptr &&backend) +{ + Q_ASSERT(!m_outputBackend); + m_outputBackend = std::move(backend); +} + +void Application::setSession(std::unique_ptr &&session) +{ + Q_ASSERT(!m_session); + m_session = std::move(session); +} + +PluginManager *Application::pluginManager() const +{ + return m_pluginManager.get(); +} + +InputMethod *Application::inputMethod() const +{ + return m_inputMethod.get(); +} + +XwaylandInterface *Application::xwayland() const +{ + return nullptr; +} + +static PlatformCursorImage grabCursorOpenGL() +{ + auto scene = Compositor::self()->scene(); + if (!scene) { + return PlatformCursorImage{}; + } + Cursor *cursor = Cursors::self()->currentCursor(); + LogicalOutput *output = workspace()->outputAt(cursor->pos()); + + const auto texture = GLTexture::allocate(GL_RGBA8, (cursor->geometry().size() * output->scale()).toSize()); + if (!texture) { + return PlatformCursorImage{}; + } + texture->setContentTransform(OutputTransform::FlipY); + GLFramebuffer framebuffer(texture.get()); + RenderTarget renderTarget(&framebuffer); + + SceneView sceneView(scene, output, nullptr, nullptr); + ItemTreeView cursorView(&sceneView, scene->cursorItem(), output, nullptr, nullptr); + cursorView.prePaint(); + cursorView.paint(renderTarget, QPoint(), Region::infinite()); + cursorView.postPaint(); + + QImage image = texture->toImage(); + image.setDevicePixelRatio(output->scale()); + + return PlatformCursorImage(image, cursor->hotspot()); +} + +static PlatformCursorImage grabCursorSoftware() +{ + auto scene = Compositor::self()->scene(); + if (!scene) { + return PlatformCursorImage{}; + } + Cursor *cursor = Cursors::self()->currentCursor(); + LogicalOutput *output = workspace()->outputAt(cursor->pos()); + + QImage image((cursor->geometry().size() * output->scale()).toSize(), QImage::Format_ARGB32_Premultiplied); + RenderTarget renderTarget(&image); + + SceneView sceneView(scene, output, nullptr, nullptr); + ItemTreeView cursorView(&sceneView, scene->cursorItem(), output, nullptr, nullptr); + cursorView.prePaint(); + cursorView.paint(renderTarget, QPoint(), Region::infinite()); + cursorView.postPaint(); + + image.setDevicePixelRatio(output->scale()); + return PlatformCursorImage(image, cursor->hotspot()); +} + +PlatformCursorImage Application::cursorImage() const +{ + Cursor *cursor = Cursors::self()->currentCursor(); + if (cursor->geometry().isEmpty()) { + return PlatformCursorImage(); + } + + if (auto shapeSource = qobject_cast(cursor->source())) { + return PlatformCursorImage(shapeSource->image(), shapeSource->hotspot()); + } + + // The cursor content is provided by a client, grab the contents of the cursor scene. + switch (effects->compositingType()) { + case OpenGLCompositing: + return grabCursorOpenGL(); + case QPainterCompositing: + return grabCursorSoftware(); + default: + Q_UNREACHABLE(); + } +} + +void Application::startInteractiveWindowSelection(std::function callback, const QByteArray &cursorName) +{ + if (!input()) { + callback(nullptr); + return; + } + input()->startInteractiveWindowSelection(callback, cursorName); +} + +void Application::startInteractivePositionSelection(std::function callback) +{ + if (!input()) { + callback(QPointF(-1, -1)); + return; + } + input()->startInteractivePositionSelection(callback); +} + +} // namespace + +#include "moc_main.cpp" diff --git a/local/recipes/kde/kwin/source/src/main.h b/local/recipes/kde/kwin/source/src/main.h new file mode 100644 index 0000000000..b9b4fe6365 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/main.h @@ -0,0 +1,393 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "config-kwin.h" + +#include "effect/globals.h" + +#include +#include +// Qt +#include +#include +#include + +#if KWIN_BUILD_X11 +#include +#endif + +class KPluginMetaData; +class QCommandLineParser; + +namespace KWin +{ + +class OutputBackend; +class Session; +class X11EventFilter; +class PluginManager; +class InputMethod; +class TabletModeManager; +class XwaylandInterface; +class Edge; +class ScreenEdges; +class Outline; +class OutlineVisual; +class Compositor; +class Window; + +class XcbEventFilter : public QAbstractNativeEventFilter +{ +public: + bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) override; +}; + +class X11EventFilterContainer : public QObject +{ + Q_OBJECT + +public: + explicit X11EventFilterContainer(X11EventFilter *filter); + + X11EventFilter *filter() const; + +private: + X11EventFilter *m_filter; +}; + +class KWIN_EXPORT Application : public QApplication +{ + Q_OBJECT +#if KWIN_BUILD_X11 + Q_PROPERTY(quint32 x11RootWindow READ x11RootWindow CONSTANT) + Q_PROPERTY(void *x11Connection READ x11Connection NOTIFY x11ConnectionChanged) +#endif + Q_PROPERTY(KSharedConfigPtr config READ config WRITE setConfig) + Q_PROPERTY(KSharedConfigPtr kxkbConfig READ kxkbConfig WRITE setKxkbConfig) +public: + ~Application() override; + + void setConfigLock(bool lock); + + KSharedConfigPtr config() const + { + return m_config; + } + void setConfig(KSharedConfigPtr config) + { + m_config = std::move(config); + } + + KSharedConfigPtr kxkbConfig() const + { + return m_kxkbConfig; + } + void setKxkbConfig(KSharedConfigPtr config) + { + m_kxkbConfig = std::move(config); + } + + KSharedConfigPtr inputConfig() const + { + return m_inputConfig; + } + void setInputConfig(KSharedConfigPtr config) + { + m_inputConfig = std::move(config); + } + + KSharedConfigPtr kdeglobals() const + { + return m_kdeglobals; + } + + void start(); + + void setupCommandLine(QCommandLineParser *parser); + void processCommandLine(QCommandLineParser *parser); + + static void setCrashCount(int count); + static bool wasCrash(); + void resetCrashesCount(); + + /** + * Creates the KAboutData object for the KWin instance and registers it as + * KAboutData::setApplicationData. + */ + static void createAboutData(); + +#if KWIN_BUILD_X11 + /** + * @returns the X11 root window. + */ + xcb_window_t x11RootWindow() const + { + return m_rootWindow; + } + + /** + * @returns the X11 xcb connection + */ + xcb_connection_t *x11Connection() const + { + return m_connection; + } + /** + * Inheriting classes should use this method to set the X11 root window + * before accessing any X11 specific code paths. + */ + void setX11RootWindow(xcb_window_t root) + { + m_rootWindow = root; + } + /** + * Inheriting classes should use this method to set the xcb connection + * before accessing any X11 specific code paths. + */ + void setX11Connection(xcb_connection_t *c) + { + m_connection = c; + } + + /** + * Returns the current X11 server time. + */ + xcb_timestamp_t x11Time() const; + + void registerEventFilter(X11EventFilter *filter); + void unregisterEventFilter(X11EventFilter *filter); + bool dispatchEvent(xcb_generic_event_t *event); + + virtual pid_t xwaylandPid() const + { + return -1; + } + +#endif + + qreal xwaylandScale() const + { + return m_xwaylandScale.value_or(1.0); + } + + void setXwaylandScale(qreal scale); + +#if KWIN_BUILD_ACTIVITIES + bool usesKActivities() const + { + return m_useKActivities; + } + void setUseKActivities(bool use) + { + m_useKActivities = use; + } +#endif + + QProcessEnvironment processStartupEnvironment() const; + void setProcessStartupEnvironment(const QProcessEnvironment &environment); + + OutputBackend *outputBackend() const + { + return m_outputBackend.get(); + } + void setOutputBackend(std::unique_ptr &&backend); + + Session *session() const + { + return m_session.get(); + } + void setSession(std::unique_ptr &&session); + void setFollowLocale1(bool follow) + { + m_followLocale1 = follow; + } + bool followLocale1() const + { + return m_followLocale1; + } + + bool isTerminating() const + { + return m_terminating; + } + + bool initiallyLocked() const; + void setInitiallyLocked(bool locked); + + bool supportsLockScreen() const; + void setSupportsLockScreen(bool set); + + bool supportsGlobalShortcuts() const; + void setSupportsGlobalShortcuts(bool set); + + void installNativeX11EventFilter(); + void removeNativeX11EventFilter(); + + void createAtoms(); + void destroyAtoms(); + + static void setupMalloc(); + static void setupLocalizedString(); + + PluginManager *pluginManager() const; + InputMethod *inputMethod() const; + virtual XwaylandInterface *xwayland() const; + TabletModeManager *tabletModeManager() const; + + /** + * Starts an interactive window selection process. + * + * Once the user selected a window the @p callback is invoked with the selected Window as + * argument. In case the user cancels the interactive window selection or selecting a window is currently + * not possible (e.g. screen locked) the @p callback is invoked with a @c nullptr argument. + * + * During the interactive window selection the cursor is turned into a crosshair cursor unless + * @p cursorName is provided. The argument @p cursorName is a QByteArray instead of Qt::CursorShape + * to support the "pirate" cursor for kill window which is not wrapped by Qt::CursorShape. + * + * The default implementation forwards to InputRedirection. + * + * @param callback The function to invoke once the interactive window selection ends + * @param cursorName The optional name of the cursor shape to use, default is crosshair + */ + virtual void startInteractiveWindowSelection(std::function callback, const QByteArray &cursorName = QByteArray()); + + /** + * Starts an interactive position selection process. + * + * Once the user selected a position on the screen the @p callback is invoked with + * the selected point as argument. In case the user cancels the interactive position selection + * or selecting a position is currently not possible (e.g. screen locked) the @p callback + * is invoked with a point at @c -1 as x and y argument. + * + * During the interactive window selection the cursor is turned into a crosshair cursor. + * + * The default implementation forwards to InputRedirection. + * + * @param callback The function to invoke once the interactive position selection ends + */ + virtual void startInteractivePositionSelection(std::function callback); + + /** + * Returns a PlatformCursorImage. By default this is created by softwareCursor and + * softwareCursorHotspot. An implementing subclass can use this to provide a better + * suited PlatformCursorImage. + * + * @see softwareCursor + * @see softwareCursorHotspot + * @since 5.9 + */ + virtual PlatformCursorImage cursorImage() const; + +Q_SIGNALS: + void x11ConnectionChanged(); + void x11ConnectionAboutToBeDestroyed(); + void xwaylandScaleChanged(); + void workspaceCreated(); + void virtualTerminalCreated(); + +protected: + Application(int &argc, char **argv); + virtual void performStartup() = 0; + + void createInput(); + void createWorkspace(); + void createOptions(); + void createPlugins(); + void createInputMethod(); + void createTabletModeManager(); + void destroyInput(); + void destroyWorkspace(); + void destroyCompositor(); + void destroyPlugins(); + void destroyInputMethod(); + void destroyPlatform(); + void applyXwaylandScale(); + + void setTerminating() + { + m_terminating = true; + } + +protected: + static int crashes; + +private: +#if KWIN_BUILD_X11 + QList> m_eventFilters; + QList> m_genericEventFilters; + std::unique_ptr m_eventFilter; +#endif + bool m_followLocale1 = false; + bool m_configLock; + bool m_initiallyLocked = false; + bool m_supportsLockScreen = true; + bool m_supportsGlobalShortcuts = true; + KSharedConfigPtr m_config; + KSharedConfigPtr m_kxkbConfig; + KSharedConfigPtr m_inputConfig; + KSharedConfigPtr m_kdeglobals; +#if KWIN_BUILD_X11 + xcb_window_t m_rootWindow = XCB_WINDOW_NONE; + xcb_connection_t *m_connection = nullptr; +#endif +#if KWIN_BUILD_ACTIVITIES + bool m_useKActivities = true; +#endif + std::unique_ptr m_session; + std::unique_ptr m_outputBackend; + bool m_terminating = false; + std::optional m_xwaylandScale; + QProcessEnvironment m_processEnvironment; + std::unique_ptr m_pluginManager; + std::unique_ptr m_inputMethod; + std::unique_ptr m_tabletModeManager; +}; + +inline bool Application::initiallyLocked() const +{ + return m_initiallyLocked; +} + +inline void Application::setInitiallyLocked(bool locked) +{ + m_initiallyLocked = locked; +} + +inline bool Application::supportsLockScreen() const +{ + return m_supportsLockScreen; +} + +inline void Application::setSupportsLockScreen(bool set) +{ + m_supportsLockScreen = set; +} + +inline bool Application::supportsGlobalShortcuts() const +{ + return m_supportsGlobalShortcuts; +} + +inline void Application::setSupportsGlobalShortcuts(bool set) +{ + m_supportsGlobalShortcuts = set; +} + +inline static Application *kwinApp() +{ + Q_ASSERT(qobject_cast(QCoreApplication::instance())); + + return static_cast(QCoreApplication::instance()); +} + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/main_wayland.cpp b/local/recipes/kde/kwin/source/src/main_wayland.cpp new file mode 100644 index 0000000000..3245c18a3f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/main_wayland.cpp @@ -0,0 +1,643 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "main_wayland.h" + +#include "config-kwin.h" + +#include "backends/drm/drm_backend.h" +#include "backends/virtual/virtual_backend.h" +#include "backends/wayland/wayland_backend.h" +#include "compositor.h" +#include "core/outputbackend.h" +#include "core/session.h" +#include "effect/effecthandler.h" +#include "inputmethod.h" +#include "tabletmodemanager.h" +#include "utils/realtime.h" +#include "wayland/display.h" +#include "wayland/seat.h" +#include "wayland_server.h" +#include "workspace.h" + +#if KWIN_BUILD_X11 +#include "backends/x11/x11_windowed_backend.h" +#include "xwayland/xwayland.h" +#include "xwayland/xwaylandlauncher.h" +#endif + +// KDE +#include +#include +#include +#include +#include + +// Qt +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +Q_IMPORT_PLUGIN(KWinIntegrationPlugin) +#if KWIN_BUILD_GLOBALSHORTCUTS +Q_IMPORT_PLUGIN(KGlobalAccelImpl) +#endif +Q_IMPORT_PLUGIN(KWindowSystemKWinPlugin) +Q_IMPORT_PLUGIN(KWinIdleTimePoller) + +namespace KWin +{ + +static rlimit originalNofileLimit = { + .rlim_cur = 0, + .rlim_max = 0, +}; + +static bool bumpNofileLimit() +{ + if (getrlimit(RLIMIT_NOFILE, &originalNofileLimit) == -1) { + std::cerr << "Failed to bump RLIMIT_NOFILE limit, getrlimit() failed: " << strerror(errno) << std::endl; + return false; + } + + rlimit limit = originalNofileLimit; + limit.rlim_cur = limit.rlim_max; + + if (setrlimit(RLIMIT_NOFILE, &limit) == -1) { + std::cerr << "Failed to bump RLIMIT_NOFILE limit, setrlimit() failed: " << strerror(errno) << std::endl; + return false; + } + + return true; +} + +static void restoreNofileLimit() +{ + if (setrlimit(RLIMIT_NOFILE, &originalNofileLimit) == -1) { + std::cerr << "Failed to restore RLIMIT_NOFILE limit, legacy apps might be broken" << std::endl; + } +} + +//************************************ +// ApplicationWayland +//************************************ + +ApplicationWayland::ApplicationWayland(int &argc, char **argv) + : Application(argc, argv) +{ +} + +ApplicationWayland::~ApplicationWayland() +{ + setTerminating(); + if (!waylandServer()) { + return; + } + + destroyPlugins(); + + // need to unload all effects prior to destroying X connection as they might do X calls + if (effects) { + effects->unloadAllEffects(); + } +#if KWIN_BUILD_X11 + m_xwayland.reset(); +#endif + destroyWorkspace(); + + destroyInputMethod(); + destroyCompositor(); + destroyInput(); + + delete WaylandServer::self(); +} + +void ApplicationWayland::performStartup() +{ + createOptions(); + + if (!outputBackend()->initialize()) { + std::exit(1); + } + + createInput(); + createInputMethod(); + createTabletModeManager(); + + auto compositor = Compositor::create(); + compositor->createRenderer(); + createWorkspace(); + createPlugins(); + + compositor->start(); + + // Note that we start accepting client connections after creating the Workspace. + if (!waylandServer()->start()) { + qFatal("Failed to initialize the Wayland server, exiting now"); + } + +#if KWIN_BUILD_X11 + if (m_startXWayland) { + m_xwayland = std::make_unique(this); + m_xwayland->xwaylandLauncher()->setListenFDs(m_xwaylandListenFds); + m_xwayland->xwaylandLauncher()->setDisplayName(m_xwaylandDisplay); + m_xwayland->xwaylandLauncher()->setXauthority(m_xwaylandXauthority); + m_xwayland->xwaylandLauncher()->addEnvironmentVariables(m_xwaylandExtraEnvironment); + m_xwayland->xwaylandLauncher()->passFileDescriptors(std::move(m_xwaylandFds)); + m_xwayland->init(); + connect(m_xwayland.get(), &Xwl::Xwayland::started, this, &ApplicationWayland::applyXwaylandScale); + } +#endif + startSession(); +} + +void ApplicationWayland::refreshSettings(const KConfigGroup &group, const QByteArrayList &names) +{ + if (group.name() == "Wayland" && names.contains("InputMethod")) { + KDesktopFile file(group.readPathEntry("InputMethod", QString())); + kwinApp()->inputMethod()->setInputMethodCommand(file.desktopGroup().readEntry("Exec", QString())); + } +} + +void ApplicationWayland::startSession() +{ + KSharedConfig::Ptr kwinSettings = kwinApp()->config(); + m_settingsWatcher = KConfigWatcher::create(kwinSettings); + connect(m_settingsWatcher.data(), &KConfigWatcher::configChanged, this, &ApplicationWayland::refreshSettings); + + if (!m_inputMethodServerToStart.isEmpty()) { + kwinApp()->inputMethod()->setInputMethodCommand(m_inputMethodServerToStart); + } else { + refreshSettings(kwinSettings->group(QStringLiteral("Wayland")), {"InputMethod"}); + } + + // start session + if (!m_sessionArgument.isEmpty()) { + QStringList arguments = KShell::splitArgs(m_sessionArgument); + if (!arguments.isEmpty()) { + QString program = arguments.takeFirst(); + QProcess *p = new QProcess(this); + p->setProcessChannelMode(QProcess::ForwardedChannels); + p->setProcessEnvironment(processStartupEnvironment()); + connect(p, qOverload(&QProcess::finished), this, [p](int code, QProcess::ExitStatus status) { + p->deleteLater(); + if (status == QProcess::CrashExit) { + qWarning() << "Session process has crashed"; + QCoreApplication::exit(-1); + return; + } + + if (code) { + qWarning() << "Session process exited with code" << code; + } + + QCoreApplication::exit(code); + }); + p->setProgram(program); + p->setArguments(arguments); + p->start(); + } else { + qWarning("Failed to launch the session process: %s is an invalid command", + qPrintable(m_sessionArgument)); + } + } + // start the applications passed to us as command line arguments + if (!m_applicationsToStart.isEmpty()) { + for (const QString &application : std::as_const(m_applicationsToStart)) { + QStringList arguments = KShell::splitArgs(application); + if (arguments.isEmpty()) { + qWarning("Failed to launch application: %s is an invalid command", + qPrintable(application)); + continue; + } + QString program = arguments.takeFirst(); + // note: this will kill the started process when we exit + // this is going to happen anyway as we are the wayland and X server the app connects to + QProcess *p = new QProcess(this); + p->setProcessChannelMode(QProcess::ForwardedChannels); + p->setProcessEnvironment(processStartupEnvironment()); + p->setProgram(program); + p->setArguments(arguments); + p->startDetached(); + p->deleteLater(); + } + } +} + +#if KWIN_BUILD_X11 +XwaylandInterface *ApplicationWayland::xwayland() const +{ + return m_xwayland.get(); +} + +pid_t ApplicationWayland::xwaylandPid() const +{ + if (m_xwayland && m_xwayland->xwaylandLauncher()->process() && m_xwayland->xwaylandLauncher()->process()->state() == QProcess::Running) { + return m_xwayland->xwaylandLauncher()->process()->processId(); + } + return -1; +} + +#endif + +} // namespace + +int main(int argc, char *argv[]) +{ + KWin::Application::setupMalloc(); + KWin::Application::setupLocalizedString(); + KWin::gainRealTime(); + + signal(SIGPIPE, SIG_IGN); + + // It's easy to exceed the file descriptor limit because many things are backed using fds + // nowadays, e.g. dmabufs, shm buffers, etc. Bump the RLIMIT_NOFILE limit to handle that. + // Some apps may still use select(), so we reset the limit to its original value in fork(). + if (KWin::bumpNofileLimit()) { + pthread_atfork(nullptr, nullptr, KWin::restoreNofileLimit); + } + + QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); + + // enforce our internal qpa plugin, unfortunately command line switch has precedence + setenv("QT_QPA_PLATFORM", "wayland-org.kde.kwin.qpa", true); + + // The shader (currently) causes a blocking disk flush on load and save of every QQuickWindow + // Because it's on load, it will happen every time not just occasionally + // The gains are minimal, disable until it's fixed + QCoreApplication::setAttribute(Qt::AA_DisableShaderDiskCache); + + KWin::ApplicationWayland a(argc, argv); + + // reset QT_QPA_PLATFORM so we don't propagate it to our children (e.g. apps launched from the overview effect) + qunsetenv("QT_QPA_PLATFORM"); + + KSignalHandler::self()->watchSignal(SIGTERM); + KSignalHandler::self()->watchSignal(SIGINT); + KSignalHandler::self()->watchSignal(SIGHUP); + QObject::connect(KSignalHandler::self(), &KSignalHandler::signalReceived, + &a, &QCoreApplication::exit); + + KWin::Application::createAboutData(); + + KCrash::initialize(); + +#if KWIN_BUILD_X11 + QCommandLineOption xwaylandOption(QStringLiteral("xwayland"), + i18n("Start a rootless Xwayland server.")); +#endif + QCommandLineOption waylandSocketOption(QStringList{QStringLiteral("s"), QStringLiteral("socket")}, + i18n("Name of the Wayland socket to listen on. If not set \"wayland-0\" is used."), + QStringLiteral("socket")); +#if KWIN_BUILD_X11 + QCommandLineOption x11DisplayOption(QStringLiteral("x11-display"), + i18n("The X11 Display to use in windowed mode on platform X11."), + QStringLiteral("display")); +#endif + QCommandLineOption waylandDisplayOption(QStringLiteral("wayland-display"), + i18n("The Wayland Display to use in windowed mode on platform Wayland."), + QStringLiteral("display")); + QCommandLineOption virtualFbOption(QStringLiteral("virtual"), i18n("Render to a virtual framebuffer.")); + QCommandLineOption widthOption(QStringLiteral("width"), + i18n("The width for windowed mode. Default width is 1024."), + QStringLiteral("width")); + widthOption.setDefaultValue(QString::number(1024)); + QCommandLineOption heightOption(QStringLiteral("height"), + i18n("The height for windowed mode. Default height is 768."), + QStringLiteral("height")); + heightOption.setDefaultValue(QString::number(768)); + + QCommandLineOption fullscreenOption(QStringLiteral("fullscreen"), + i18n("Whether or not to make windowed mode fullscreen"), + QStringLiteral("fullscreen")); + + QCommandLineOption scaleOption(QStringLiteral("scale"), + i18n("The scale for windowed mode. Default value is 1."), + QStringLiteral("scale")); + scaleOption.setDefaultValue(QString::number(1)); + + QCommandLineOption outputCountOption(QStringLiteral("output-count"), + i18n("The number of windows to open as outputs in windowed mode. Default value is 1"), + QStringLiteral("count")); + outputCountOption.setDefaultValue(QString::number(1)); + + QCommandLineOption waylandSocketFdOption(QStringLiteral("wayland-fd"), + i18n("Wayland socket to use for incoming connections. This can be combined with --socket to name the socket"), + QStringLiteral("wayland-fd")); + + QCommandLineOption xwaylandListenFdOption(QStringLiteral("xwayland-fd"), + i18n("XWayland socket to use for Xwayland's incoming connections. This can be set multiple times"), + QStringLiteral("xwayland-fds")); + + QCommandLineOption xwaylandDisplayOption(QStringLiteral("xwayland-display"), + i18n("Name of the xwayland display that has been pre-set up"), + "xwayland-display"); + + QCommandLineOption xwaylandXAuthorityOption(QStringLiteral("xwayland-xauthority"), + i18n("Name of the xauthority file "), + "xwayland-xauthority"); + + QCommandLineOption replaceOption(QStringLiteral("replace"), + i18n("Exits this instance so it can be restarted by kwin_wayland_wrapper.")); + + QCommandLineOption drmOption(QStringLiteral("drm"), i18n("Render through drm node.")); + QCommandLineOption locale1Option(QStringLiteral("locale1"), i18n("Extract locale information from locale1 rather than the user's configuration")); + + QCommandLineParser parser; + a.setupCommandLine(&parser); +#if KWIN_BUILD_X11 + parser.addOption(xwaylandOption); +#endif + parser.addOption(waylandSocketOption); + parser.addOption(waylandSocketFdOption); + parser.addOption(xwaylandListenFdOption); + parser.addOption(xwaylandDisplayOption); + parser.addOption(xwaylandXAuthorityOption); + parser.addOption(replaceOption); +#if KWIN_BUILD_X11 + parser.addOption(x11DisplayOption); +#endif + parser.addOption(waylandDisplayOption); + parser.addOption(virtualFbOption); + parser.addOption(widthOption); + parser.addOption(heightOption); + parser.addOption(scaleOption); + parser.addOption(outputCountOption); + parser.addOption(drmOption); + parser.addOption(locale1Option); + parser.addOption(fullscreenOption); + + QCommandLineOption inputMethodOption(QStringLiteral("inputmethod"), + i18n("Input method that KWin starts."), + QStringLiteral("path/to/imserver")); + parser.addOption(inputMethodOption); + +#if KWIN_BUILD_SCREENLOCKER + QCommandLineOption screenLockerOption(QStringLiteral("lockscreen"), + i18n("Starts the session in locked mode.")); + parser.addOption(screenLockerOption); + + QCommandLineOption noScreenLockerOption(QStringLiteral("no-lockscreen"), + i18n("Starts the session without lock screen support.")); + parser.addOption(noScreenLockerOption); +#endif + + QCommandLineOption noGlobalShortcutsOption(QStringLiteral("no-global-shortcuts"), + i18n("Starts the session without global shortcuts support.")); + parser.addOption(noGlobalShortcutsOption); + +#if KWIN_BUILD_ACTIVITIES + QCommandLineOption noActivitiesOption(QStringLiteral("no-kactivities"), + i18n("Disable KActivities integration.")); + parser.addOption(noActivitiesOption); +#endif + + QCommandLineOption exitWithSessionOption(QStringLiteral("exit-with-session"), + i18n("Exit after the session application, which is started by KWin, closed."), + QStringLiteral("/path/to/session")); + parser.addOption(exitWithSessionOption); + + parser.addPositionalArgument(QStringLiteral("applications"), + i18n("Applications to start once Wayland and Xwayland server are started"), + QStringLiteral("[/path/to/application...]")); + + parser.process(a); + a.processCommandLine(&parser); + +#if KWIN_BUILD_ACTIVITIES + if (parser.isSet(noActivitiesOption)) { + a.setUseKActivities(false); + } +#endif + + if (parser.isSet(replaceOption)) { + QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"), QStringLiteral("/KWin"), + QStringLiteral("org.kde.KWin"), QStringLiteral("replace")); + QDBusConnection::sessionBus().call(msg, QDBus::NoBlock); + return 0; + } + + if (parser.isSet(exitWithSessionOption)) { + a.setSessionArgument(parser.value(exitWithSessionOption)); + } + + enum class BackendType { + Kms, +#if KWIN_BUILD_X11 + X11, +#endif + Wayland, + Virtual, + }; + + BackendType backendType; + QSize initialWindowSize; + int outputCount = 1; + qreal outputScale = 1; + + // Decide what backend to use. + if (parser.isSet(drmOption)) { + backendType = BackendType::Kms; +#if KWIN_BUILD_X11 + } else if (parser.isSet(x11DisplayOption)) { + backendType = BackendType::X11; +#endif + } else if (parser.isSet(waylandDisplayOption)) { + backendType = BackendType::Wayland; + } else if (parser.isSet(virtualFbOption)) { + backendType = BackendType::Virtual; + } else { + if (qEnvironmentVariableIsSet("WAYLAND_DISPLAY")) { + qInfo("No backend specified, automatically choosing Wayland because WAYLAND_DISPLAY is set"); + backendType = BackendType::Wayland; +#if KWIN_BUILD_X11 + } else if (qEnvironmentVariableIsSet("DISPLAY")) { + qInfo("No backend specified, automatically choosing X11 because DISPLAY is set"); + backendType = BackendType::X11; +#endif + } else { + qInfo("No backend specified, automatically choosing drm"); + backendType = BackendType::Kms; + } + } + + if (parser.isSet(locale1Option)) { + a.setFollowLocale1(true); + } else { + a.setFollowLocale1(a.config()->group(QStringLiteral("Wayland")).readEntry("FollowLocale1", false)); + } + + bool ok = false; + const int width = parser.value(widthOption).toInt(&ok); + if (!ok) { + std::cerr << "FATAL ERROR incorrect value for width" << std::endl; + return 1; + } + const int height = parser.value(heightOption).toInt(&ok); + if (!ok) { + std::cerr << "FATAL ERROR incorrect value for height" << std::endl; + return 1; + } + const qreal scale = parser.value(scaleOption).toDouble(&ok); + if (!ok || scale <= 0) { + std::cerr << "FATAL ERROR incorrect value for scale" << std::endl; + return 1; + } + const bool fullscreen = parser.isSet(fullscreenOption); + + outputScale = scale; + initialWindowSize = QSize(width, height); + + const int count = parser.value(outputCountOption).toInt(&ok); + if (ok) { + outputCount = std::max(1, count); + } + + switch (backendType) { + case BackendType::Kms: + a.setSession(KWin::Session::create()); + if (!a.session()) { + std::cerr << "FATAl ERROR: could not acquire a session" << std::endl; + return 1; + } + a.setOutputBackend(std::make_unique(a.session())); + break; + case BackendType::Virtual: { + auto outputBackend = std::make_unique(); + for (int i = 0; i < outputCount; ++i) { + outputBackend->addOutput(KWin::VirtualBackend::OutputInfo{ + .geometry = KWin::Rect(QPoint(), initialWindowSize), + .scale = outputScale, + }); + } + a.setSession(KWin::Session::create(KWin::Session::Type::Noop)); + a.setOutputBackend(std::move(outputBackend)); + break; + } +#if KWIN_BUILD_X11 + case BackendType::X11: { + QString display = parser.value(x11DisplayOption); + if (display.isEmpty()) { + display = qgetenv("DISPLAY"); + } + a.setSession(KWin::Session::create(KWin::Session::Type::Noop)); + a.setOutputBackend(std::make_unique(KWin::X11WindowedBackendOptions{ + .display = display, + .outputCount = outputCount, + .outputScale = outputScale, + .outputSize = initialWindowSize, + .fullscreen = fullscreen, + })); + break; + } +#endif + case BackendType::Wayland: { + QString socketName = parser.value(waylandDisplayOption); + if (socketName.isEmpty()) { + socketName = qgetenv("WAYLAND_DISPLAY"); + } + a.setSession(KWin::Session::create(KWin::Session::Type::Noop)); + a.setOutputBackend(std::make_unique(KWin::Wayland::WaylandBackendOptions{ + .socketName = socketName, + .outputCount = outputCount, + .outputScale = outputScale, + .outputSize = initialWindowSize, + .fullscreen = fullscreen, + })); + break; + } + } + + KWin::WaylandServer *server = KWin::WaylandServer::create(); +#if KWIN_BUILD_SCREENLOCKER + if (parser.isSet(screenLockerOption)) { + a.setInitiallyLocked(true); + } else if (parser.isSet(noScreenLockerOption)) { + a.setSupportsLockScreen(false); + } +#endif + if (parser.isSet(noGlobalShortcutsOption)) { + a.setSupportsGlobalShortcuts(false); + } + + const QString socketName = parser.value(waylandSocketOption); + if (parser.isSet(waylandSocketFdOption)) { + bool ok; + int fd = parser.value(waylandSocketFdOption).toInt(&ok); + if (ok) { + // make sure we don't leak this FD to children + fcntl(fd, F_SETFD, FD_CLOEXEC); + server->display()->addSocketFileDescriptor(fd, socketName); + } else { + std::cerr << "FATAL ERROR: could not parse socket FD" << std::endl; + return 1; + } + } else { + // socketName empty is fine here, addSocketName will automatically pick one + if (!server->display()->addSocketName(socketName)) { + std::cerr << "FATAL ERROR: could not add wayland socket " << qPrintable(socketName) << std::endl; + return 1; + } + qInfo() << "Accepting client connections on sockets:" << server->display()->socketNames(); + } + + if (!server->init()) { + std::cerr << "FATAL ERROR: could not create Wayland server" << std::endl; + return 1; + } + + QObject::connect(&a, &KWin::Application::workspaceCreated, server, &KWin::WaylandServer::initWorkspace); + +#if KWIN_BUILD_X11 + if (parser.isSet(xwaylandOption)) { + a.setStartXwayland(true); + + if (parser.isSet(xwaylandListenFdOption)) { + const QStringList fdStrings = parser.values(xwaylandListenFdOption); + for (const QString &fdString : fdStrings) { + bool ok; + int fd = fdString.toInt(&ok); + if (ok) { + // make sure we don't leak this FD to children + fcntl(fd, F_SETFD, FD_CLOEXEC); + a.addXwaylandSocketFileDescriptor(fd); + } + } + if (parser.isSet(xwaylandDisplayOption)) { + a.setXwaylandDisplay(parser.value(xwaylandDisplayOption)); + } else { + std::cerr << "Using xwayland-fd without xwayland-display is undefined" << std::endl; + return 1; + } + if (parser.isSet(xwaylandXAuthorityOption)) { + a.setXwaylandXauthority(parser.value(xwaylandXAuthorityOption)); + } + } + } +#endif + + a.setProcessStartupEnvironment(environment); + a.setApplicationsToStart(parser.positionalArguments()); + a.setInputMethodServerToStart(parser.value(inputMethodOption)); + a.start(); + + return a.exec(); +} + +#include "moc_main_wayland.cpp" diff --git a/local/recipes/kde/kwin/source/src/main_wayland.h b/local/recipes/kde/kwin/source/src/main_wayland.h new file mode 100644 index 0000000000..b345a521f2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/main_wayland.h @@ -0,0 +1,97 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "main.h" +#include +#include + +#include "utils/filedescriptor.h" + +#include + +namespace KWin +{ +namespace Xwl +{ +class Xwayland; +} + +class ApplicationWayland : public Application +{ + Q_OBJECT +public: + ApplicationWayland(int &argc, char **argv); + ~ApplicationWayland() override; + +#if KWIN_BUILD_X11 + void setStartXwayland(bool start) + { + m_startXWayland = start; + } + void addXwaylandSocketFileDescriptor(int fd) + { + m_xwaylandListenFds << fd; + } + void setXwaylandDisplay(const QString &display) + { + m_xwaylandDisplay = display; + } + void setXwaylandXauthority(const QString &xauthority) + { + m_xwaylandXauthority = xauthority; + } + void addExtraXWaylandEnvrionmentVariable(const QString &variable, const QString &value) + { + m_xwaylandExtraEnvironment.insert(variable, value); + } + void passFdToXwayland(FileDescriptor &&fd) + { + m_xwaylandFds.push_back(std::move(fd)); + } + XwaylandInterface *xwayland() const override; + + pid_t xwaylandPid() const override; +#endif + void setApplicationsToStart(const QStringList &applications) + { + m_applicationsToStart = applications; + } + void setInputMethodServerToStart(const QString &inputMethodServer) + { + m_inputMethodServerToStart = inputMethodServer; + } + void setSessionArgument(const QString &session) + { + m_sessionArgument = session; + } + +protected: + void performStartup() override; + +private: + void startSession(); + void refreshSettings(const KConfigGroup &group, const QByteArrayList &names); + + QStringList m_applicationsToStart; + QString m_inputMethodServerToStart; + QString m_sessionArgument; + +#if KWIN_BUILD_X11 + bool m_startXWayland = false; + std::unique_ptr m_xwayland; + QList m_xwaylandListenFds; + QString m_xwaylandDisplay; + QString m_xwaylandXauthority; + QMap m_xwaylandExtraEnvironment; + std::vector m_xwaylandFds; +#endif + KConfigWatcher::Ptr m_settingsWatcher; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/mousebuttons.cpp b/local/recipes/kde/kwin/source/src/mousebuttons.cpp new file mode 100644 index 0000000000..4f67de26c2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/mousebuttons.cpp @@ -0,0 +1,50 @@ +/* + SPDX-FileCopyrightText: 2022 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "mousebuttons.h" +#include +#include + +namespace KWin +{ + +static const QHash s_buttonToQtMouseButton = { + {BTN_LEFT, Qt::LeftButton}, + {BTN_MIDDLE, Qt::MiddleButton}, + {BTN_RIGHT, Qt::RightButton}, + // in QtWayland mapped like that + {BTN_SIDE, Qt::ExtraButton1}, + {BTN_EXTRA, Qt::ExtraButton2}, + {BTN_FORWARD, Qt::ExtraButton3}, + {BTN_BACK, Qt::ExtraButton4}, + {BTN_TASK, Qt::ExtraButton5}, + {0x118, Qt::ExtraButton6}, + {0x119, Qt::ExtraButton7}, + {0x11a, Qt::ExtraButton8}, + {0x11b, Qt::ExtraButton9}, + {0x11c, Qt::ExtraButton10}, + {0x11d, Qt::ExtraButton11}, + {0x11e, Qt::ExtraButton12}, + {0x11f, Qt::ExtraButton13}, +}; + +uint32_t qtMouseButtonToButton(Qt::MouseButton button) +{ + return s_buttonToQtMouseButton.key(button); +} + +Qt::MouseButton buttonToQtMouseButton(uint32_t button) +{ + // all other values get mapped to ExtraButton24 + // this is actually incorrect but doesn't matter in our usage + // KWin internally doesn't use these high extra buttons anyway + // it's only needed for recognizing whether buttons are pressed + // if multiple buttons are mapped to the value the evaluation whether + // buttons are pressed is correct and that's all we care about. + return s_buttonToQtMouseButton.value(button, Qt::ExtraButton24); +} + +} diff --git a/local/recipes/kde/kwin/source/src/mousebuttons.h b/local/recipes/kde/kwin/source/src/mousebuttons.h new file mode 100644 index 0000000000..8f77b44032 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/mousebuttons.h @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2022 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include + +namespace KWin +{ + +uint32_t qtMouseButtonToButton(Qt::MouseButton button); +Qt::MouseButton buttonToQtMouseButton(uint32_t button); + +} diff --git a/local/recipes/kde/kwin/source/src/netinfo.cpp b/local/recipes/kde/kwin/source/src/netinfo.cpp new file mode 100644 index 0000000000..c97fb490ed --- /dev/null +++ b/local/recipes/kde/kwin/source/src/netinfo.cpp @@ -0,0 +1,332 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +// own +#include "netinfo.h" +// kwin +#include "rootinfo_filter.h" +#include "utils/envvar.h" +#include "virtualdesktops.h" +#include "waylandwindow.h" +#include "workspace.h" +#include "x11window.h" +// Qt +#include + +namespace KWin +{ + +std::unique_ptr RootInfo::s_self; + +RootInfo *RootInfo::create() +{ + Q_ASSERT(!s_self); + xcb_window_t supportWindow = xcb_generate_id(kwinApp()->x11Connection()); + const uint32_t values[] = {true}; + xcb_create_window(kwinApp()->x11Connection(), XCB_COPY_FROM_PARENT, supportWindow, kwinApp()->x11RootWindow(), + 0, 0, 1, 1, 0, XCB_COPY_FROM_PARENT, + XCB_COPY_FROM_PARENT, XCB_CW_OVERRIDE_REDIRECT, values); + const uint32_t lowerValues[] = {XCB_STACK_MODE_BELOW}; // See usage in layers.cpp + // we need to do the lower window with a roundtrip, otherwise NETRootInfo is not functioning + UniqueCPtr error(xcb_request_check(kwinApp()->x11Connection(), + xcb_configure_window_checked(kwinApp()->x11Connection(), supportWindow, XCB_CONFIG_WINDOW_STACK_MODE, lowerValues))); + if (error) { + qCDebug(KWIN_CORE) << "Error occurred while lowering support window: " << error->error_code; + } + + const NET::Properties properties = NET::Supported + | NET::SupportingWMCheck + | NET::ClientList + | NET::ClientListStacking + | NET::DesktopGeometry + | NET::NumberOfDesktops + | NET::CurrentDesktop + | NET::ActiveWindow + | NET::WorkArea + | NET::CloseWindow + | NET::DesktopNames + | NET::WMName + | NET::WMVisibleName + | NET::WMDesktop + | NET::WMWindowType + | NET::WMState + | NET::WMIcon + | NET::WMPid + | NET::WMMoveResize + | NET::WMFrameExtents + | NET::WMPing; + const NET::WindowTypes types = NET::NormalMask + | NET::DesktopMask + | NET::DockMask + | NET::ToolbarMask + | NET::MenuMask + | NET::DialogMask + | NET::OverrideMask + | NET::UtilityMask + | NET::SplashMask; // No compositing window types here unless we support them also as managed window types + const NET::States states = NET::Modal + // | NET::Sticky // Large desktops not supported (and probably never will be) + | NET::MaxVert + | NET::MaxHoriz + | NET::Shaded + | NET::SkipTaskbar + | NET::KeepAbove + // | NET::StaysOnTop // The same like KeepAbove + | NET::SkipPager + | NET::Hidden + | NET::FullScreen + | NET::KeepBelow + | NET::DemandsAttention + | NET::SkipSwitcher + | NET::Focused; + NET::Properties2 properties2 = NET::WM2UserTime + | NET::WM2StartupId + | NET::WM2AllowedActions + | NET::WM2RestackWindow + | NET::WM2MoveResizeWindow + | NET::WM2ShowingDesktop + | NET::WM2DesktopLayout + | NET::WM2FullPlacement + | NET::WM2FullscreenMonitors + | NET::WM2KDEShadow + | NET::WM2OpaqueRegion + | NET::WM2GTKFrameExtents + | NET::WM2GTKShowWindowMenu + | NET::WM2Opacity; +#if KWIN_BUILD_ACTIVITIES + properties2 |= NET::WM2Activities; +#endif + const NET::Actions actions = NET::ActionMove + | NET::ActionResize + | NET::ActionMinimize + | NET::ActionShade + // | NET::ActionStick // Sticky state is not supported + | NET::ActionMaxVert + | NET::ActionMaxHoriz + | NET::ActionFullScreen + | NET::ActionChangeDesktop + | NET::ActionClose; + + s_self = std::make_unique(supportWindow, "KWin", properties, types, states, properties2, actions); + return s_self.get(); +} + +void RootInfo::destroy() +{ + if (!s_self) { + return; + } + xcb_window_t supportWindow = s_self->supportWindow(); + s_self.reset(); + xcb_destroy_window(kwinApp()->x11Connection(), supportWindow); +} + +bool RootInfo::desktopEnabled() +{ + static const bool enabled = environmentVariableIntValue("KWIN_XWAYLAND_ENABLE_NETWM_DESKTOP").value_or(0); + return enabled; +} + +RootInfo::RootInfo(xcb_window_t w, const char *name, NET::Properties properties, NET::WindowTypes types, + NET::States states, NET::Properties2 properties2, NET::Actions actions, int scr) + : NETRootInfo(kwinApp()->x11Connection(), w, name, properties, types, states, properties2, actions, scr) + , m_activeWindow(activeWindow()) + , m_eventFilter(std::make_unique(this)) +{ +} + +void RootInfo::changeNumberOfDesktops(int n) +{ + VirtualDesktopManager::self()->setCount(n); +} + +void RootInfo::changeCurrentDesktop(int d) +{ + VirtualDesktopManager::self()->setCurrent(d); +} + +void RootInfo::changeActiveWindow(xcb_window_t w, NET::RequestSource src, xcb_timestamp_t timestamp, xcb_window_t active_window) +{ + Workspace *workspace = Workspace::self(); + if (X11Window *c = workspace->findClient(w)) { + if (timestamp == XCB_CURRENT_TIME) { + timestamp = c->userTime(); + } + if (src != NET::FromApplication && src != FromTool) { + src = NET::FromTool; + } + if (src == NET::FromTool) { + workspace->activateWindow(c, true); // force + } else if (c == workspace->activeWindow()) { + return; // WORKAROUND? With > 1 plasma activities, we cause this ourselves. bug #240673 + } else { // NET::FromApplication + X11Window *c2; + if (c->allowWindowActivation(timestamp, false)) { + workspace->activateWindow(c); + // if activation of the requestor's window would be allowed, allow activation too + } else if (active_window != XCB_WINDOW_NONE + && (c2 = workspace->findClient(active_window)) != nullptr + && c2->allowWindowActivation(timestampCompare(timestamp, c2->userTime() > 0 ? timestamp : c2->userTime()), false)) { + workspace->activateWindow(c); + } else { + c->demandAttention(); + } + } + } +} + +void RootInfo::restackWindow(xcb_window_t w, RequestSource src, xcb_window_t above, int detail, xcb_timestamp_t timestamp) +{ + if (X11Window *c = Workspace::self()->findClient(w)) { + if (timestamp == XCB_CURRENT_TIME) { + timestamp = c->userTime(); + } + if (src != NET::FromApplication && src != FromTool) { + src = NET::FromTool; + } + c->restackWindow(above, detail, src, timestamp); + } +} + +void RootInfo::closeWindow(xcb_window_t w) +{ + X11Window *c = Workspace::self()->findClient(w); + if (c) { + c->closeWindow(); + } +} + +void RootInfo::moveResize(xcb_window_t w, int x_root, int y_root, unsigned long direction, xcb_button_t button, RequestSource source) +{ + X11Window *c = Workspace::self()->findClient(w); + if (c) { + c->NETMoveResize(Xcb::fromXNative(x_root), Xcb::fromXNative(y_root), (Direction)direction, button); + } +} + +void RootInfo::moveResizeWindow(xcb_window_t w, int flags, int x, int y, int width, int height) +{ + X11Window *c = Workspace::self()->findClient(w); + if (c) { + c->NETMoveResizeWindow(flags, Xcb::fromXNative(x), Xcb::fromXNative(y), Xcb::fromXNative(width), Xcb::fromXNative(height)); + } +} + +void RootInfo::showWindowMenu(xcb_window_t w, int device_id, int x_root, int y_root) +{ + if (X11Window *c = Workspace::self()->findClient(w)) { + c->GTKShowWindowMenu(Xcb::fromXNative(x_root), Xcb::fromXNative(y_root)); + } +} + +void RootInfo::gotPing(xcb_window_t w, xcb_timestamp_t timestamp) +{ + if (X11Window *c = Workspace::self()->findClient(w)) { + c->gotPing(timestamp); + } +} + +void RootInfo::changeShowingDesktop(bool showing) +{ + Workspace::self()->setShowingDesktop(showing); +} + +void RootInfo::setActiveClient(Window *client) +{ + xcb_window_t windowId = XCB_WINDOW_NONE; + if (auto x11Window = qobject_cast(client)) { + windowId = x11Window->window(); + } else if (qobject_cast(client)) { + windowId = Workspace::self()->nullFocusWindow(); + } + if (m_activeWindow == windowId) { + return; + } + m_activeWindow = windowId; + setActiveWindow(m_activeWindow); +} + +// **************************************** +// WinInfo +// **************************************** + +WinInfo::WinInfo(X11Window *c, xcb_window_t window, + xcb_window_t rwin, NET::Properties properties, NET::Properties2 properties2) + : NETWinInfo(kwinApp()->x11Connection(), window, rwin, properties, properties2, NET::WindowManager) + , m_client(c) +{ +} + +void WinInfo::changeDesktop(int desktopId) +{ + if (RootInfo::desktopEnabled()) { + if (desktopId == NET::OnAllDesktops) { + Workspace::self()->sendWindowToDesktops(m_client, {}, true); + } else if (VirtualDesktop *desktop = VirtualDesktopManager::self()->desktopForX11Id(desktopId)) { + Workspace::self()->sendWindowToDesktops(m_client, {desktop}, true); + } + } +} + +void WinInfo::changeFullscreenMonitors(NETFullscreenMonitors topology) +{ + m_client->updateFullscreenMonitors(topology); +} + +void WinInfo::changeState(NET::States state, NET::States mask) +{ + mask &= ~NET::Sticky; // KWin doesn't support large desktops, ignore + mask &= ~NET::Hidden; // clients are not allowed to change this directly + state &= mask; // for safety, clear all other bits + + if ((mask & NET::FullScreen) != 0 && (state & NET::FullScreen) == 0) { + m_client->setFullScreen(false); + } + if ((mask & NET::Max) == NET::Max) { + m_client->setMaximize(state & NET::MaxVert, state & NET::MaxHoriz); + } else if (mask & NET::MaxVert) { + m_client->setMaximize(state & NET::MaxVert, m_client->requestedMaximizeMode() & MaximizeHorizontal); + } else if (mask & NET::MaxHoriz) { + m_client->setMaximize(m_client->requestedMaximizeMode() & MaximizeVertical, state & NET::MaxHoriz); + } + + if (mask & NET::KeepAbove) { + m_client->setKeepAbove((state & NET::KeepAbove) != 0); + } + if (mask & NET::KeepBelow) { + m_client->setKeepBelow((state & NET::KeepBelow) != 0); + } + if (mask & NET::SkipTaskbar) { + m_client->setOriginalSkipTaskbar((state & NET::SkipTaskbar) != 0); + } + if (mask & NET::SkipPager) { + m_client->setSkipPager((state & NET::SkipPager) != 0); + } + if (mask & NET::SkipSwitcher) { + m_client->setSkipSwitcher((state & NET::SkipSwitcher) != 0); + } + if (mask & NET::DemandsAttention) { + m_client->demandAttention((state & NET::DemandsAttention) != 0); + } + if (mask & NET::Modal) { + m_client->setModal((state & NET::Modal) != 0); + } + // unsetting fullscreen first, setting it last (because e.g. maximize works only for !isFullScreen() ) + if ((mask & NET::FullScreen) != 0 && (state & NET::FullScreen) != 0) { + m_client->setFullScreen(true); + } +} + +void WinInfo::disable() +{ + m_client = nullptr; // only used when the object is passed to Deleted +} + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/netinfo.h b/local/recipes/kde/kwin/source/src/netinfo.h new file mode 100644 index 0000000000..39c99fe3cc --- /dev/null +++ b/local/recipes/kde/kwin/source/src/netinfo.h @@ -0,0 +1,92 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "config-kwin.h" + +#include "kwin_export.h" + +#if !KWIN_BUILD_X11 +#error Do not include on non-X11 builds +#endif + +#include + +#include +#include + +namespace KWin +{ + +class Window; +class RootInfoFilter; +class X11Window; + +/** + * NET WM Protocol handler class + */ +class KWIN_EXPORT RootInfo : public NETRootInfo +{ +public: + static RootInfo *create(); + static void destroy(); + + static bool desktopEnabled(); + + RootInfo(xcb_window_t w, const char *name, NET::Properties properties, NET::WindowTypes types, + NET::States states, NET::Properties2 properties2, NET::Actions actions, int scr = -1); + + void setActiveClient(Window *client); + +protected: + void changeNumberOfDesktops(int n) override; + void changeCurrentDesktop(int d) override; + void changeActiveWindow(xcb_window_t w, NET::RequestSource src, xcb_timestamp_t timestamp, xcb_window_t active_window) override; + void closeWindow(xcb_window_t w) override; + void moveResize(xcb_window_t w, int x_root, int y_root, unsigned long direction, xcb_button_t button, RequestSource source) override; + void moveResizeWindow(xcb_window_t w, int flags, int x, int y, int width, int height) override; + void showWindowMenu(xcb_window_t w, int device_id, int x_root, int y_root) override; + void gotPing(xcb_window_t w, xcb_timestamp_t timestamp) override; + void restackWindow(xcb_window_t w, RequestSource source, xcb_window_t above, int detail, xcb_timestamp_t timestamp) override; + void changeShowingDesktop(bool showing) override; + +private: + static std::unique_ptr s_self; + friend RootInfo *rootInfo(); + + xcb_window_t m_activeWindow; + std::unique_ptr m_eventFilter; +}; + +inline RootInfo *rootInfo() +{ + return RootInfo::s_self.get(); +} + +/** + * NET WM Protocol handler class + */ +class WinInfo : public NETWinInfo +{ +public: + WinInfo(X11Window *c, xcb_window_t window, + xcb_window_t rwin, NET::Properties properties, NET::Properties2 properties2); + void changeDesktop(int desktop) override; + void changeFullscreenMonitors(NETFullscreenMonitors topology) override; + void changeState(NET::States state, NET::States mask) override; + void disable(); + +private: + X11Window *m_client; +}; + +} // KWin diff --git a/local/recipes/kde/kwin/source/src/onscreennotification.cpp b/local/recipes/kde/kwin/source/src/onscreennotification.cpp new file mode 100644 index 0000000000..d582d2159b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/onscreennotification.cpp @@ -0,0 +1,231 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Graesslin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +*/ +#include "onscreennotification.h" + +#include "config-kwin.h" + +#include "core/rect.h" +#include "input.h" +#include "input_event.h" +#include "input_event_spy.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace KWin +{ + +class OnScreenNotificationInputEventSpy : public InputEventSpy +{ +public: + explicit OnScreenNotificationInputEventSpy(OnScreenNotification *parent); + + void pointerMotion(PointerMotionEvent *event) override; + +private: + OnScreenNotification *m_parent; +}; + +OnScreenNotificationInputEventSpy::OnScreenNotificationInputEventSpy(OnScreenNotification *parent) + : m_parent(parent) +{ +} + +void OnScreenNotificationInputEventSpy::pointerMotion(PointerMotionEvent *event) +{ + m_parent->setContainsPointer(m_parent->geometry().contains(event->position)); +} + +OnScreenNotification::OnScreenNotification(QObject *parent) + : QObject(parent) + , m_timer(new QTimer(this)) +{ + m_timer->setSingleShot(true); + connect(m_timer, &QTimer::timeout, this, std::bind(&OnScreenNotification::setVisible, this, false)); + connect(this, &OnScreenNotification::visibleChanged, this, [this]() { + if (m_visible) { + show(); + } else { + m_timer->stop(); + m_spy.reset(); + m_containsPointer = false; + } + }); +} + +OnScreenNotification::~OnScreenNotification() +{ + if (QQuickWindow *w = qobject_cast(m_mainItem.get())) { + w->hide(); + w->destroy(); + } +} + +void OnScreenNotification::setConfig(KSharedConfigPtr config) +{ + m_config = config; +} + +void OnScreenNotification::setEngine(QQmlEngine *engine) +{ + m_qmlEngine = engine; +} + +bool OnScreenNotification::isVisible() const +{ + return m_visible; +} + +void OnScreenNotification::setVisible(bool visible) +{ + if (m_visible == visible) { + return; + } + m_visible = visible; + Q_EMIT visibleChanged(); +} + +QString OnScreenNotification::message() const +{ + return m_message; +} + +void OnScreenNotification::setMessage(const QString &message) +{ + if (m_message == message) { + return; + } + m_message = message; + Q_EMIT messageChanged(); +} + +QString OnScreenNotification::iconName() const +{ + return m_iconName; +} + +void OnScreenNotification::setIconName(const QString &iconName) +{ + if (m_iconName == iconName) { + return; + } + m_iconName = iconName; + Q_EMIT iconNameChanged(); +} + +int OnScreenNotification::timeout() const +{ + return m_timer->interval(); +} + +void OnScreenNotification::setTimeout(int timeout) +{ + if (m_timer->interval() == timeout) { + return; + } + m_timer->setInterval(timeout); + Q_EMIT timeoutChanged(); +} + +void OnScreenNotification::show() +{ + Q_ASSERT(m_visible); + ensureQmlContext(); + ensureQmlComponent(); + createInputSpy(); + if (m_timer->interval() != 0) { + m_timer->start(); + } +} + +void OnScreenNotification::ensureQmlContext() +{ + Q_ASSERT(m_qmlEngine); + if (m_qmlContext) { + return; + } + m_qmlContext = std::make_unique(m_qmlEngine); + m_qmlContext->setContextProperty(QStringLiteral("osd"), this); +} + +void OnScreenNotification::ensureQmlComponent() +{ + Q_ASSERT(m_config); + Q_ASSERT(m_qmlEngine); + if (m_qmlComponent) { + return; + } + m_qmlComponent = std::make_unique(m_qmlEngine); + const QString fileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, + m_config->group(QStringLiteral("OnScreenNotification")).readEntry("QmlPath", QString(QStringLiteral("kwin-wayland/onscreennotification/plasma/main.qml")))); + if (fileName.isEmpty()) { + return; + } + m_qmlComponent->loadUrl(QUrl::fromLocalFile(fileName)); + if (!m_qmlComponent->isError()) { + m_mainItem.reset(m_qmlComponent->create(m_qmlContext.get())); + } else { + m_qmlComponent.reset(); + } +} + +void OnScreenNotification::createInputSpy() +{ + Q_ASSERT(!m_spy); + if (auto w = qobject_cast(m_mainItem.get())) { + m_spy = std::make_unique(this); + input()->installInputEventSpy(m_spy.get()); + if (!m_animation) { + m_animation = new QPropertyAnimation(w, "opacity", this); + m_animation->setStartValue(1.0); + m_animation->setEndValue(0.0); + m_animation->setDuration(250); + m_animation->setEasingCurve(QEasingCurve::InOutCubic); + } + } +} + +RectF OnScreenNotification::geometry() const +{ + if (QQuickWindow *w = qobject_cast(m_mainItem.get())) { + return w->geometry(); + } + return RectF(); +} + +void OnScreenNotification::setContainsPointer(bool contains) +{ + if (m_containsPointer == contains) { + return; + } + m_containsPointer = contains; + if (!m_animation) { + return; + } + m_animation->setDirection(m_containsPointer ? QAbstractAnimation::Forward : QAbstractAnimation::Backward); + m_animation->start(); +} + +void OnScreenNotification::setSkipCloseAnimation(bool skip) +{ + if (QQuickWindow *w = qobject_cast(m_mainItem.get())) { + w->setProperty("KWIN_SKIP_CLOSE_ANIMATION", skip); + } +} + +} // namespace KWin + +#include "moc_onscreennotification.cpp" diff --git a/local/recipes/kde/kwin/source/src/onscreennotification.h b/local/recipes/kde/kwin/source/src/onscreennotification.h new file mode 100644 index 0000000000..c2928bd80d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/onscreennotification.h @@ -0,0 +1,79 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Graesslin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +*/ + +#pragma once + +#include +#include +#include + +class QPropertyAnimation; +class QTimer; +class QQmlContext; +class QQmlComponent; +class QQmlEngine; + +namespace KWin +{ + +class OnScreenNotificationInputEventSpy; +class RectF; + +class OnScreenNotification : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged) + Q_PROPERTY(QString message READ message WRITE setMessage NOTIFY messageChanged) + Q_PROPERTY(QString iconName READ iconName WRITE setIconName NOTIFY iconNameChanged) + Q_PROPERTY(int timeout READ timeout WRITE setTimeout NOTIFY timeoutChanged) + +public: + explicit OnScreenNotification(QObject *parent = nullptr); + ~OnScreenNotification() override; + bool isVisible() const; + QString message() const; + QString iconName() const; + int timeout() const; + + RectF geometry() const; + + void setVisible(bool m_visible); + void setMessage(const QString &message); + void setIconName(const QString &iconName); + void setTimeout(int timeout); + + void setConfig(KSharedConfigPtr config); + void setEngine(QQmlEngine *engine); + + void setContainsPointer(bool contains); + void setSkipCloseAnimation(bool skip); + +Q_SIGNALS: + void visibleChanged(); + void messageChanged(); + void iconNameChanged(); + void timeoutChanged(); + +private: + void show(); + void ensureQmlContext(); + void ensureQmlComponent(); + void createInputSpy(); + bool m_visible = false; + QString m_message; + QString m_iconName; + QTimer *m_timer; + KSharedConfigPtr m_config; + std::unique_ptr m_qmlContext; + std::unique_ptr m_qmlComponent; + QQmlEngine *m_qmlEngine = nullptr; + std::unique_ptr m_mainItem; + std::unique_ptr m_spy; + QPropertyAnimation *m_animation = nullptr; + bool m_containsPointer = false; +}; +} diff --git a/local/recipes/kde/kwin/source/src/opengl/abstract_opengl_context_attribute_builder.cpp b/local/recipes/kde/kwin/source/src/opengl/abstract_opengl_context_attribute_builder.cpp new file mode 100644 index 0000000000..06d431c5eb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/abstract_opengl_context_attribute_builder.cpp @@ -0,0 +1,30 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "abstract_opengl_context_attribute_builder.h" + +namespace KWin +{ + +QDebug AbstractOpenGLContextAttributeBuilder::operator<<(QDebug dbg) const +{ + QDebugStateSaver saver(dbg); + dbg.nospace() << "\nVersion requested:\t" << isVersionRequested() << "\n"; + if (isVersionRequested()) { + dbg.nospace() << "Version:\t" << majorVersion() << "." << minorVersion() << "\n"; + } + dbg.nospace() << "Robust:\t" << isRobust() << "\n"; + dbg.nospace() << "Reset on video memory purge:\t" << isResetOnVideoMemoryPurge() << "\n"; + dbg.nospace() << "Forward compatible:\t" << isForwardCompatible() << "\n"; + dbg.nospace() << "Core profile:\t" << isCoreProfile() << "\n"; + dbg.nospace() << "Compatibility profile:\t" << isCompatibilityProfile() << "\n"; + dbg.nospace() << "High priority:\t" << isHighPriority(); + return dbg; +} + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/abstract_opengl_context_attribute_builder.h b/local/recipes/kde/kwin/source/src/opengl/abstract_opengl_context_attribute_builder.h new file mode 100644 index 0000000000..0a631e390f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/abstract_opengl_context_attribute_builder.h @@ -0,0 +1,132 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include +#include + +namespace KWin +{ + +class KWIN_EXPORT AbstractOpenGLContextAttributeBuilder +{ +public: + virtual ~AbstractOpenGLContextAttributeBuilder() + { + } + + void setVersion(int major, int minor = 0) + { + m_versionRequested = true; + m_majorVersion = major; + m_minorVersion = minor; + } + + bool isVersionRequested() const + { + return m_versionRequested; + } + + int majorVersion() const + { + return m_majorVersion; + } + + int minorVersion() const + { + return m_minorVersion; + } + + void setRobust(bool robust) + { + m_robust = robust; + } + + bool isRobust() const + { + return m_robust; + } + + void setForwardCompatible(bool forward) + { + m_forwardCompatible = forward; + } + + bool isForwardCompatible() const + { + return m_forwardCompatible; + } + + void setCoreProfile(bool core) + { + m_coreProfile = core; + if (m_coreProfile) { + setCompatibilityProfile(false); + } + } + + bool isCoreProfile() const + { + return m_coreProfile; + } + + void setCompatibilityProfile(bool compatibility) + { + m_compatibilityProfile = compatibility; + if (m_compatibilityProfile) { + setCoreProfile(false); + } + } + + bool isCompatibilityProfile() const + { + return m_compatibilityProfile; + } + + void setResetOnVideoMemoryPurge(bool reset) + { + m_resetOnVideoMemoryPurge = reset; + } + + bool isResetOnVideoMemoryPurge() const + { + return m_resetOnVideoMemoryPurge; + } + + void setHighPriority(bool highPriority) + { + m_highPriority = highPriority; + } + + bool isHighPriority() const + { + return m_highPriority; + } + + virtual std::vector build() const = 0; + + QDebug operator<<(QDebug dbg) const; + +private: + bool m_versionRequested = false; + int m_majorVersion = 0; + int m_minorVersion = 0; + bool m_robust = false; + bool m_forwardCompatible = false; + bool m_coreProfile = false; + bool m_compatibilityProfile = false; + bool m_resetOnVideoMemoryPurge = false; + bool m_highPriority = false; +}; + +inline QDebug operator<<(QDebug dbg, const AbstractOpenGLContextAttributeBuilder *attribs) +{ + return attribs->operator<<(dbg); +} + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/colormanagement.glsl b/local/recipes/kde/kwin/source/src/opengl/colormanagement.glsl new file mode 100644 index 0000000000..d0a87e0f36 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/colormanagement.glsl @@ -0,0 +1,193 @@ +const int sRGB_EOTF = 0; +const int linear_EOTF = 1; +const int PQ_EOTF = 2; +const int gamma22_EOTF = 3; +const int BT1886_EOTF = 4; + +uniform mat4 colorimetryTransform; + +uniform int sourceNamedTransferFunction; +/** + * x: min luminance + * y: max luminance - min luminance + */ +uniform vec2 sourceTransferFunctionParams; + +uniform int destinationNamedTransferFunction; +/** + * x: min luminance + * y: max luminance - min luminance + */ +uniform vec2 destinationTransferFunctionParams; + +// in nits +uniform float sourceReferenceLuminance; +uniform float maxTonemappingLuminance; +uniform float destinationReferenceLuminance; +uniform float maxDestinationLuminance; + +uniform mat4 destinationToLMS; +uniform mat4 lmsToDestination; + +vec3 linearToPq(vec3 linear) { + const float c1 = 0.8359375; + const float c2 = 18.8515625; + const float c3 = 18.6875; + const float m1 = 0.1593017578125; + const float m2 = 78.84375; + vec3 powed = pow(clamp(linear, vec3(0), vec3(1)), vec3(m1)); + vec3 num = vec3(c1) + c2 * powed; + vec3 denum = vec3(1.0) + c3 * powed; + return pow(num / denum, vec3(m2)); +} +vec3 pqToLinear(vec3 pq) { + const float c1 = 0.8359375; + const float c2 = 18.8515625; + const float c3 = 18.6875; + const float m1_inv = 1.0 / 0.1593017578125; + const float m2_inv = 1.0 / 78.84375; + vec3 powed = pow(clamp(pq, vec3(0.0), vec3(1.0)), vec3(m2_inv)); + vec3 num = max(powed - c1, vec3(0.0)); + vec3 den = c2 - c3 * powed; + return pow(num / den, vec3(m1_inv)); +} +float singleLinearToPq(float linear) { + const float c1 = 0.8359375; + const float c2 = 18.8515625; + const float c3 = 18.6875; + const float m1 = 0.1593017578125; + const float m2 = 78.84375; + float powed = pow(clamp(linear, 0.0, 1.0), m1); + float num = c1 + c2 * powed; + float denum = 1.0 + c3 * powed; + return pow(num / denum, m2); +} +float singlePqToLinear(float pq) { + const float c1 = 0.8359375; + const float c2 = 18.8515625; + const float c3 = 18.6875; + const float m1_inv = 1.0 / 0.1593017578125; + const float m2_inv = 1.0 / 78.84375; + float powed = pow(clamp(pq, 0.0, 1.0), m2_inv); + float num = max(powed - c1, 0.0); + float den = c2 - c3 * powed; + return pow(num / den, m1_inv); +} +vec3 srgbToLinear(vec3 color) { + bvec3 isLow = lessThanEqual(color, vec3(0.04045)); + vec3 loPart = color / 12.92; + vec3 hiPart = pow((color + 0.055) / 1.055, vec3(12.0 / 5.0)); +#if __VERSION__ >= 130 + return mix(hiPart, loPart, isLow); +#else + return mix(hiPart, loPart, vec3(isLow.r ? 1.0 : 0.0, isLow.g ? 1.0 : 0.0, isLow.b ? 1.0 : 0.0)); +#endif +} + +vec3 linearToSrgb(vec3 color) { + bvec3 isLow = lessThanEqual(color, vec3(0.0031308)); + vec3 loPart = color * 12.92; + vec3 hiPart = pow(color, vec3(5.0 / 12.0)) * 1.055 - 0.055; +#if __VERSION__ >= 130 + return mix(hiPart, loPart, isLow); +#else + return mix(hiPart, loPart, vec3(isLow.r ? 1.0 : 0.0, isLow.g ? 1.0 : 0.0, isLow.b ? 1.0 : 0.0)); +#endif +} + +const mat3 toICtCp = mat3( + 0.5, 1.613769531250, 4.378173828125, + 0.5, -3.323486328125, -4.245605468750, + 0.0, 1.709716796875, -0.132568359375 +); +const mat3 fromICtCp = mat3( + 1.0, 1.0, 1.0, + 0.00860903703793, -0.008609037037, 0.56031335710680, + 0.11102962500303, -0.111029625003, -0.32062717498732 +); + +vec3 doTonemapping(vec3 color) { + if (maxTonemappingLuminance < maxDestinationLuminance * 1.01) { + // clipping is enough + return clamp(color.rgb, vec3(0.0), vec3(maxDestinationLuminance)); + } + + // convert to ICtCp, to properly split luminance and color + // intensity is PQ-encoded luminance + vec3 lms = (destinationToLMS * vec4(color, 1.0)).rgb; + vec3 lms_PQ = linearToPq(lms / 10000.0); + vec3 ICtCp = toICtCp * lms_PQ; + float luminance = singlePqToLinear(ICtCp.r) * 10000.0; + + // apply tone mapping operation (modified Reinhart) + float relativeLuminance = max(luminance / destinationReferenceLuminance, 0.0); + float inputRange = maxTonemappingLuminance / destinationReferenceLuminance; + float outputRange = maxDestinationLuminance / destinationReferenceLuminance; + float v = (outputRange * (1.0 + inputRange) - inputRange) / pow(inputRange, 2.0); + relativeLuminance = relativeLuminance * (1.0 + relativeLuminance * v) / (1.0 + relativeLuminance); + luminance = relativeLuminance * destinationReferenceLuminance; + + // convert back to rgb + ICtCp.r = singleLinearToPq(luminance / 10000.0); + color = (lmsToDestination * vec4(pqToLinear(fromICtCp * ICtCp), 1.0)).rgb * 10000.0; + // and clip, to ensure out-of-gamut values are clipped to the correct white point + return clamp(color, vec3(0.0), vec3(maxDestinationLuminance)); +} + +vec4 encodingToNits(vec4 color, int sourceTransferFunction, float luminanceOffset, float luminanceScale) { + if (sourceTransferFunction == sRGB_EOTF) { + color.rgb /= max(color.a, 0.001); + color.rgb = srgbToLinear(color.rgb) * luminanceScale + vec3(luminanceOffset); + color.rgb *= color.a; + } else if (sourceTransferFunction == linear_EOTF) { + color.rgb = color.rgb * luminanceScale + vec3(luminanceOffset); + } else if (sourceTransferFunction == PQ_EOTF) { + color.rgb /= max(color.a, 0.001); + color.rgb = pqToLinear(color.rgb) * luminanceScale + vec3(luminanceOffset); + color.rgb *= color.a; + } else if (sourceTransferFunction == gamma22_EOTF) { + color.rgb /= max(color.a, 0.001); + color.rgb = pow(max(color.rgb, vec3(0.0)), vec3(2.2)) * luminanceScale + vec3(luminanceOffset); + color.rgb *= color.a; + } else if (sourceTransferFunction == BT1886_EOTF) { + color.rgb /= max(color.a, 0.001); + // for bt1886, luminanceScale = a, luminanceOffset = b + color.rgb = luminanceScale * pow(max(color.rgb + vec3(luminanceOffset), vec3(0.0)), vec3(2.4)); + color.rgb *= color.a; + } + return color; +} + +vec4 sourceEncodingToNitsInDestinationColorspace(vec4 color) { + color = encodingToNits(color, sourceNamedTransferFunction, sourceTransferFunctionParams.x, sourceTransferFunctionParams.y); + color.rgb = (colorimetryTransform * vec4(color.rgb, 1.0)).rgb; + return vec4(doTonemapping(color.rgb), color.a); +} + +vec4 nitsToEncoding(vec4 color, int destinationTransferFunction, float luminanceOffset, float luminanceScale) { + if (destinationTransferFunction == sRGB_EOTF) { + color.rgb /= max(color.a, 0.001); + color.rgb = linearToSrgb((color.rgb - vec3(luminanceOffset)) / luminanceScale); + color.rgb *= color.a; + } else if (destinationTransferFunction == linear_EOTF) { + color.rgb = (color.rgb - vec3(luminanceOffset)) / luminanceScale; + } else if (destinationTransferFunction == PQ_EOTF) { + color.rgb /= max(color.a, 0.001); + color.rgb = linearToPq((color.rgb - vec3(luminanceOffset)) / luminanceScale); + color.rgb *= color.a; + } else if (destinationTransferFunction == gamma22_EOTF) { + color.rgb /= max(color.a, 0.001); + color.rgb = pow(max((color.rgb - vec3(luminanceOffset)) / luminanceScale, vec3(0.0)), vec3(1.0 / 2.2)); + color.rgb *= color.a; + } else if (destinationTransferFunction == BT1886_EOTF) { + color.rgb /= max(color.a, 0.001); + // for bt1886, luminanceScale = a, luminanceOffset = b + color.rgb = pow(color.rgb / luminanceScale, vec3(1.0 / 2.4)) - vec3(luminanceOffset); + color.rgb *= color.a; + } + return color; +} + +vec4 nitsToDestinationEncoding(vec4 color) { + return nitsToEncoding(color, destinationNamedTransferFunction, destinationTransferFunctionParams.x, destinationTransferFunctionParams.y); +} diff --git a/local/recipes/kde/kwin/source/src/opengl/egl_context_attribute_builder.cpp b/local/recipes/kde/kwin/source/src/opengl/egl_context_attribute_builder.cpp new file mode 100644 index 0000000000..d4c561d27d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/egl_context_attribute_builder.cpp @@ -0,0 +1,79 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "egl_context_attribute_builder.h" +#include + +namespace KWin +{ +std::vector EglContextAttributeBuilder::build() const +{ + std::vector attribs; + if (isVersionRequested()) { + attribs.emplace_back(EGL_CONTEXT_MAJOR_VERSION_KHR); + attribs.emplace_back(majorVersion()); + attribs.emplace_back(EGL_CONTEXT_MINOR_VERSION_KHR); + attribs.emplace_back(minorVersion()); + } + int contextFlags = 0; + if (isRobust()) { + attribs.emplace_back(EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_KHR); + attribs.emplace_back(EGL_LOSE_CONTEXT_ON_RESET_KHR); + contextFlags |= EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR; + if (isResetOnVideoMemoryPurge()) { + attribs.emplace_back(EGL_GENERATE_RESET_ON_VIDEO_MEMORY_PURGE_NV); + attribs.emplace_back(GL_TRUE); + } + } + if (isForwardCompatible()) { + contextFlags |= EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR; + } + if (contextFlags != 0) { + attribs.emplace_back(EGL_CONTEXT_FLAGS_KHR); + attribs.emplace_back(contextFlags); + } + if (isCoreProfile() || isCompatibilityProfile()) { + attribs.emplace_back(EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR); + if (isCoreProfile()) { + attribs.emplace_back(EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR); + } else if (isCompatibilityProfile()) { + attribs.emplace_back(EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT_KHR); + } + } + if (isHighPriority()) { + attribs.emplace_back(EGL_CONTEXT_PRIORITY_LEVEL_IMG); + attribs.emplace_back(EGL_CONTEXT_PRIORITY_HIGH_IMG); + } + attribs.emplace_back(EGL_NONE); + return attribs; +} + +std::vector EglOpenGLESContextAttributeBuilder::build() const +{ + std::vector attribs; + attribs.emplace_back(EGL_CONTEXT_CLIENT_VERSION); + attribs.emplace_back(majorVersion()); + if (isRobust()) { + attribs.emplace_back(EGL_CONTEXT_OPENGL_ROBUST_ACCESS_EXT); + attribs.emplace_back(EGL_TRUE); + attribs.emplace_back(EGL_CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY_EXT); + attribs.emplace_back(EGL_LOSE_CONTEXT_ON_RESET_EXT); + if (isResetOnVideoMemoryPurge()) { + attribs.emplace_back(EGL_GENERATE_RESET_ON_VIDEO_MEMORY_PURGE_NV); + attribs.emplace_back(GL_TRUE); + } + } + if (isHighPriority()) { + attribs.emplace_back(EGL_CONTEXT_PRIORITY_LEVEL_IMG); + attribs.emplace_back(EGL_CONTEXT_PRIORITY_HIGH_IMG); + } + attribs.emplace_back(EGL_NONE); + return attribs; +} + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/egl_context_attribute_builder.h b/local/recipes/kde/kwin/source/src/opengl/egl_context_attribute_builder.h new file mode 100644 index 0000000000..15c8ee54b8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/egl_context_attribute_builder.h @@ -0,0 +1,28 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "abstract_opengl_context_attribute_builder.h" +#include + +namespace KWin +{ + +class KWIN_EXPORT EglContextAttributeBuilder : public AbstractOpenGLContextAttributeBuilder +{ +public: + std::vector build() const override; +}; + +class KWIN_EXPORT EglOpenGLESContextAttributeBuilder : public AbstractOpenGLContextAttributeBuilder +{ +public: + std::vector build() const override; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/eglcontext.cpp b/local/recipes/kde/kwin/source/src/opengl/eglcontext.cpp new file mode 100644 index 0000000000..82e6c67726 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/eglcontext.cpp @@ -0,0 +1,682 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "eglcontext.h" +#include "core/graphicsbuffer.h" +#include "egldisplay.h" +#include "eglimagetexture.h" +#include "glframebuffer.h" +#include "glplatform.h" +#include "glshader.h" +#include "glshadermanager.h" +#include "glvertexbuffer.h" +#include "glvertexbuffer_p.h" +#include "opengl/egl_context_attribute_builder.h" +#include "opengl/eglutils_p.h" +#include "opengl/glutils.h" +#include "utils/common.h" +#include "utils/drm_format_helper.h" + +#include +#include + +namespace KWin +{ + +EglContext *EglContext::s_currentContext = nullptr; + +std::unique_ptr EglContext::create(EglDisplay *display, EGLConfig config, ::EGLContext sharedContext) +{ + auto handle = createContext(display, config, sharedContext); + if (!handle) { + return nullptr; + } + if (!eglMakeCurrent(display->handle(), EGL_NO_SURFACE, EGL_NO_SURFACE, handle)) { + eglDestroyContext(display->handle(), handle); + return nullptr; + } + auto ret = std::make_unique(display, config, handle); + s_currentContext = ret.get(); + if (!ret->checkSupported()) { + return nullptr; + } + return ret; +} + +static QSet getExtensions(EglContext *context) +{ + QSet ret; + if (!context->isOpenGLES() && context->hasVersion(Version(3, 0))) { + int count; + glGetIntegerv(GL_NUM_EXTENSIONS, &count); + + for (int i = 0; i < count; i++) { + const char *name = (const char *)glGetStringi(GL_EXTENSIONS, i); + ret.insert(name); + } + } else { + const QByteArray extensions = (const char *)glGetString(GL_EXTENSIONS); + QList extensionsList = extensions.split(' '); + ret = {extensionsList.constBegin(), extensionsList.constEnd()}; + } + return ret; +} + +static bool checkTextureSwizzleSupport(EglContext *context) +{ + if (context->isOpenGLES()) { + return context->hasVersion(Version(3, 0)); + } else { + return context->hasVersion(Version(3, 3)) || context->hasOpenglExtension(QByteArrayLiteral("GL_ARB_texture_swizzle")); + } +} + +static bool checkTextureStorageSupport(EglContext *context) +{ + if (context->isOpenGLES()) { + return context->hasVersion(Version(3, 0)) || context->hasOpenglExtension(QByteArrayLiteral("GL_EXT_texture_storage")); + } else { + return context->hasVersion(Version(4, 2)) || context->hasOpenglExtension(QByteArrayLiteral("GL_ARB_texture_storage")); + } +} + +static bool checkIndexedQuads(EglContext *context) +{ + if (context->isOpenGLES()) { + const bool haveBaseVertex = context->hasOpenglExtension(QByteArrayLiteral("GL_OES_draw_elements_base_vertex")); + const bool haveCopyBuffer = context->hasVersion(Version(3, 0)); + return haveBaseVertex && haveCopyBuffer && context->hasMapBufferRange(); + } else { + bool haveBaseVertex = context->hasVersion(Version(3, 2)) || context->hasOpenglExtension(QByteArrayLiteral("GL_ARB_draw_elements_base_vertex")); + bool haveCopyBuffer = context->hasVersion(Version(3, 1)) || context->hasOpenglExtension(QByteArrayLiteral("GL_ARB_copy_buffer")); + return haveBaseVertex && haveCopyBuffer && context->hasMapBufferRange(); + } +} + +typedef void (*eglFuncPtr)(); +static eglFuncPtr getProcAddress(const char *name) +{ + return eglGetProcAddress(name); +} + +EglContext::EglContext(EglDisplay *display, EGLConfig config, ::EGLContext context) + : m_versionString((const char *)glGetString(GL_VERSION)) + , m_version(Version::parseString(m_versionString)) + , m_glslVersionString((const char *)glGetString(GL_SHADING_LANGUAGE_VERSION)) + , m_glslVersion(Version::parseString(m_glslVersionString)) + , m_vendor((const char *)glGetString(GL_VENDOR)) + , m_renderer((const char *)glGetString(GL_RENDERER)) + , m_isOpenglES(m_versionString.startsWith("OpenGL ES")) + , m_extensions(getExtensions(this)) + , m_supportsTimerQueries(checkTimerQuerySupport()) + , m_supportsTextureStorage(checkTextureStorageSupport(this)) + , m_supportsTextureSwizzle(checkTextureSwizzleSupport(this)) + , m_supportsARGB32Textures(!m_isOpenglES || hasOpenglExtension(QByteArrayLiteral("GL_EXT_texture_format_BGRA8888"))) + , m_supportsRGTextures(hasVersion(Version(3, 0)) || hasOpenglExtension(QByteArrayLiteral("GL_ARB_texture_rg")) || hasOpenglExtension(QByteArrayLiteral("GL_EXT_texture_rg"))) + , m_supports16BitTextures(!m_isOpenglES || hasOpenglExtension(QByteArrayLiteral("GL_EXT_texture_norm16"))) + , m_supportsBlits(!m_isOpenglES || hasVersion(Version(3, 0))) + , m_supportsPackedDepthStencil(hasVersion(Version(3, 0)) || hasOpenglExtension(QByteArrayLiteral("GL_OES_packed_depth_stencil")) || hasOpenglExtension(QByteArrayLiteral("GL_ARB_framebuffer_object")) || hasOpenglExtension(QByteArrayLiteral("GL_EXT_packed_depth_stencil"))) + , m_supportsGLES24BitDepthBuffers(m_isOpenglES && (hasVersion(Version(3, 0)) || hasOpenglExtension(QByteArrayLiteral("GL_OES_depth24")))) + , m_hasMapBufferRange(hasVersion(Version(3, 0)) || hasOpenglExtension(QByteArrayLiteral("GL_EXT_map_buffer_range")) || hasOpenglExtension(QByteArrayLiteral("GL_ARB_map_buffer_range"))) + , m_haveBufferStorage((!m_isOpenglES && hasVersion(Version(4, 4))) || hasOpenglExtension(QByteArrayLiteral("GL_ARB_buffer_storage")) || hasOpenglExtension(QByteArrayLiteral("GL_EXT_buffer_storage"))) + , m_haveSyncFences((m_isOpenglES && hasVersion(Version(3, 0))) || (!m_isOpenglES && hasVersion(Version(3, 2))) || hasOpenglExtension(QByteArrayLiteral("GL_ARB_sync"))) + , m_supportsIndexedQuads(checkIndexedQuads(this)) + , m_supportsPackInvert(hasOpenglExtension(QByteArrayLiteral("GL_MESA_pack_invert"))) + , m_glPlatform(std::make_unique(m_versionString, m_glslVersionString, m_renderer, m_vendor)) + , m_display(display) + , m_handle(context) + , m_config(config) + , m_shaderManager(std::make_unique()) + , m_streamingBuffer(std::make_unique(GLVertexBuffer::Stream)) + , m_indexBuffer(std::make_unique()) +{ + glResolveFunctions(&getProcAddress); + initDebugOutput(); + if (haveBufferStorage() && haveSyncFences()) { + if (qgetenv("KWIN_PERSISTENT_VBO") != QByteArrayLiteral("0")) { + m_streamingBuffer->setPersistent(); + } + } + // It is not legal to not have a vertex array object bound in a core context + // to make code handling old and new OpenGL versions easier, bind a dummy vao that's used for everything + if (!isOpenGLES() && hasOpenglExtension(QByteArrayLiteral("GL_ARB_vertex_array_object"))) { + glGenVertexArrays(1, &m_vao); + glBindVertexArray(m_vao); + } +} + +EglContext::~EglContext() +{ + const bool current = makeCurrent(); + if (m_vao && current) { + glDeleteVertexArrays(1, &m_vao); + } + m_shaderManager.reset(); + m_streamingBuffer.reset(); + m_indexBuffer.reset(); + doneCurrent(); + eglDestroyContext(m_display->handle(), m_handle); +} + +bool EglContext::makeCurrent() +{ + return makeCurrent(EGL_NO_SURFACE); +} + +bool EglContext::makeCurrent(EGLSurface surface) +{ + if (QOpenGLContext *context = QOpenGLContext::currentContext()) { + // Workaround to tell Qt that no QOpenGLContext is current + context->doneCurrent(); + } + const bool ret = eglMakeCurrent(m_display->handle(), surface, surface, m_handle) == EGL_TRUE; + if (ret) { + Q_ASSERT(m_handle != EGL_NO_CONTEXT); + Q_ASSERT(eglGetCurrentContext() == m_handle); + s_currentContext = this; + } else { + // QOpenGLContext::doneCurrent unset the context, we need to mirror that here! + s_currentContext = nullptr; + qCWarning(KWIN_OPENGL, "Could not make egl context current! %s", qPrintable(getEglErrorString())); + } + return ret; +} + +void EglContext::doneCurrent() const +{ + eglMakeCurrent(m_display->handle(), EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + s_currentContext = nullptr; +} + +EglDisplay *EglContext::displayObject() const +{ + return m_display; +} + +::EGLContext EglContext::handle() const +{ + return m_handle; +} + +EGLConfig EglContext::config() const +{ + return m_config; +} + +bool EglContext::isValid() const +{ + return m_display != nullptr && m_handle != EGL_NO_CONTEXT; +} + +static inline bool shouldUseOpenGLES() +{ + if (qstrcmp(qgetenv("KWIN_COMPOSE"), "O2ES") == 0) { + return true; + } + return QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES; +} + +::EGLContext EglContext::createContext(EglDisplay *display, EGLConfig config, ::EGLContext sharedContext) +{ + const bool haveRobustness = display->hasExtension(QByteArrayLiteral("EGL_EXT_create_context_robustness")); + const bool haveCreateContext = display->hasExtension(QByteArrayLiteral("EGL_KHR_create_context")); + const bool haveContextPriority = display->hasExtension(QByteArrayLiteral("EGL_IMG_context_priority")); + const bool haveResetOnVideoMemoryPurge = display->hasExtension(QByteArrayLiteral("EGL_NV_robustness_video_memory_purge")); + + std::vector> candidates; + if (shouldUseOpenGLES()) { + if (haveCreateContext && haveRobustness && haveContextPriority && haveResetOnVideoMemoryPurge) { + auto glesRobustPriority = std::make_unique(); + glesRobustPriority->setResetOnVideoMemoryPurge(true); + glesRobustPriority->setVersion(2); + glesRobustPriority->setRobust(true); + glesRobustPriority->setHighPriority(true); + candidates.push_back(std::move(glesRobustPriority)); + } + + if (haveCreateContext && haveRobustness && haveContextPriority) { + auto glesRobustPriority = std::make_unique(); + glesRobustPriority->setVersion(2); + glesRobustPriority->setRobust(true); + glesRobustPriority->setHighPriority(true); + candidates.push_back(std::move(glesRobustPriority)); + } + if (haveCreateContext && haveRobustness) { + auto glesRobust = std::make_unique(); + glesRobust->setVersion(2); + glesRobust->setRobust(true); + candidates.push_back(std::move(glesRobust)); + } + if (haveContextPriority) { + auto glesPriority = std::make_unique(); + glesPriority->setVersion(2); + glesPriority->setHighPriority(true); + candidates.push_back(std::move(glesPriority)); + } + auto gles = std::make_unique(); + gles->setVersion(2); + candidates.push_back(std::move(gles)); + } else { + if (haveCreateContext) { + if (haveRobustness && haveContextPriority && haveResetOnVideoMemoryPurge) { + auto robustCorePriority = std::make_unique(); + robustCorePriority->setResetOnVideoMemoryPurge(true); + robustCorePriority->setVersion(3, 1); + robustCorePriority->setRobust(true); + robustCorePriority->setHighPriority(true); + candidates.push_back(std::move(robustCorePriority)); + } + if (haveRobustness && haveContextPriority) { + auto robustCorePriority = std::make_unique(); + robustCorePriority->setVersion(3, 1); + robustCorePriority->setRobust(true); + robustCorePriority->setHighPriority(true); + candidates.push_back(std::move(robustCorePriority)); + } + if (haveRobustness) { + auto robustCore = std::make_unique(); + robustCore->setVersion(3, 1); + robustCore->setRobust(true); + candidates.push_back(std::move(robustCore)); + } + if (haveContextPriority) { + auto corePriority = std::make_unique(); + corePriority->setVersion(3, 1); + corePriority->setHighPriority(true); + candidates.push_back(std::move(corePriority)); + } + auto core = std::make_unique(); + core->setVersion(3, 1); + candidates.push_back(std::move(core)); + } + if (haveRobustness && haveCreateContext && haveContextPriority) { + auto robustPriority = std::make_unique(); + robustPriority->setRobust(true); + robustPriority->setHighPriority(true); + candidates.push_back(std::move(robustPriority)); + } + if (haveRobustness && haveCreateContext) { + auto robust = std::make_unique(); + robust->setRobust(true); + candidates.push_back(std::move(robust)); + } + candidates.emplace_back(new EglContextAttributeBuilder); + } + + for (const auto &candidate : candidates) { + const auto attribs = candidate->build(); + ::EGLContext ctx = eglCreateContext(display->handle(), config, sharedContext, attribs.data()); + if (ctx != EGL_NO_CONTEXT) { + qCDebug(KWIN_OPENGL) << "Created EGL context with attributes:" << candidate.get(); + return ctx; + } + } + qCCritical(KWIN_OPENGL) << "Create Context failed" << getEglErrorString(); + return EGL_NO_CONTEXT; +} + +std::shared_ptr EglContext::importDmaBufAsTexture(const DmaBufAttributes &attributes) const +{ + EGLImageKHR image = m_display->importDmaBufAsImage(attributes); + if (image != EGL_NO_IMAGE_KHR) { + const auto info = FormatInfo::get(attributes.format); + return EGLImageTexture::create(m_display, image, info ? info->openglFormat : GL_RGBA8, QSize(attributes.width, attributes.height), m_display->isExternalOnly(attributes.format, attributes.modifier)); + } else { + qCWarning(KWIN_OPENGL) << "Error creating EGLImageKHR: " << getEglErrorString(); + return nullptr; + } +} + +bool EglContext::checkTimerQuerySupport() const +{ + if (qEnvironmentVariableIsSet("KWIN_NO_TIMER_QUERY")) { + return false; + } + if (m_isOpenglES) { + // 3.0 is required so query functions can be used without "EXT" suffix. + // Timer queries are still not part of the core OpenGL ES specification. + return openglVersion() >= Version(3, 0) && hasOpenglExtension("GL_EXT_disjoint_timer_query"); + } else { + return openglVersion() >= Version(3, 3) || hasOpenglExtension("GL_ARB_timer_query"); + } +} + +bool EglContext::hasVersion(const Version &version) const +{ + return m_version >= version; +} + +QByteArrayView EglContext::openglVersionString() const +{ + return m_versionString; +} + +Version EglContext::openglVersion() const +{ + return m_version; +} + +QByteArrayView EglContext::glslVersionString() const +{ + return m_glslVersionString; +} + +Version EglContext::glslVersion() const +{ + return m_glslVersion; +} + +QByteArrayView EglContext::vendor() const +{ + return m_vendor; +} + +QByteArrayView EglContext::renderer() const +{ + return m_renderer; +} + +bool EglContext::isOpenGLES() const +{ + return m_isOpenglES; +} + +bool EglContext::hasOpenglExtension(QByteArrayView name) const +{ + return std::any_of(m_extensions.cbegin(), m_extensions.cend(), [name](const auto &string) { + return string == name; + }); +} + +bool EglContext::isSoftwareRenderer() const +{ + return m_renderer.contains("softpipe") || m_renderer.contains("Software Rasterizer") || m_renderer.contains("llvmpipe"); +} + +bool EglContext::supportsTimerQueries() const +{ + return m_supportsTimerQueries; +} + +bool EglContext::supportsTextureStorage() const +{ + return m_supportsTextureStorage; +} + +bool EglContext::supportsTextureSwizzle() const +{ + return m_supportsTextureSwizzle; +} + +bool EglContext::supportsARGB32Textures() const +{ + return m_supportsARGB32Textures; +} + +bool EglContext::supportsRGTextures() const +{ + return m_supportsRGTextures; +} + +bool EglContext::supports16BitTextures() const +{ + return m_supports16BitTextures; +} + +bool EglContext::supportsBlits() const +{ + return m_supportsBlits; +} + +bool EglContext::supportsGLES24BitDepthBuffers() const +{ + return m_supportsGLES24BitDepthBuffers; +} + +bool EglContext::haveBufferStorage() const +{ + return m_haveBufferStorage; +} + +bool EglContext::hasMapBufferRange() const +{ + return m_hasMapBufferRange; +} + +bool EglContext::haveSyncFences() const +{ + return m_haveSyncFences; +} + +bool EglContext::supportsPackInvert() const +{ + return m_supportsPackInvert; +} + +ShaderManager *EglContext::shaderManager() const +{ + return m_shaderManager.get(); +} + +GLVertexBuffer *EglContext::streamingVbo() const +{ + return m_streamingBuffer.get(); +} + +IndexBuffer *EglContext::indexBuffer() const +{ + return m_indexBuffer.get(); +} + +GLPlatform *EglContext::glPlatform() const +{ + return m_glPlatform.get(); +} + +bool EglContext::checkSupported() const +{ + const bool supportsGLSL = m_isOpenglES || (hasOpenglExtension("GL_ARB_shader_objects") && hasOpenglExtension("GL_ARB_fragment_shader") && hasOpenglExtension("GL_ARB_vertex_shader")); + const bool supportsNonPowerOfTwoTextures = m_isOpenglES || hasOpenglExtension("GL_ARB_texture_non_power_of_two"); + const bool supports3DTextures = !m_isOpenglES || hasVersion(Version(3, 0)) || hasOpenglExtension("GL_OES_texture_3D"); + const bool supportsFBOs = m_isOpenglES || hasVersion(Version(3, 0)) || hasOpenglExtension("GL_ARB_framebuffer_object") || hasOpenglExtension(QByteArrayLiteral("GL_EXT_framebuffer_object")); + const bool supportsUnpack = !m_isOpenglES || hasOpenglExtension(QByteArrayLiteral("GL_EXT_unpack_subimage")); + + if (!supportsGLSL || !supportsNonPowerOfTwoTextures || !supports3DTextures || !supportsFBOs || !supportsUnpack) { + return false; + } + // some old hardware only supports very limited shaders. To prevent the shaders KWin uses later on from not working, + // test a reasonably complex one here and bail out early if it doesn't work + auto shader = m_shaderManager->shader(ShaderTrait::MapTexture | ShaderTrait::TransformColorspace | ShaderTrait::AdjustSaturation | ShaderTrait::Modulate); + return shader->isValid(); +} + +QSet EglContext::openglExtensions() const +{ + return m_extensions; +} + +EglContext *EglContext::currentContext() +{ + return s_currentContext; +} + +void EglContext::glResolveFunctions(const std::function &resolveFunction) +{ + const bool haveArbRobustness = hasOpenglExtension(QByteArrayLiteral("GL_ARB_robustness")); + const bool haveExtRobustness = hasOpenglExtension(QByteArrayLiteral("GL_EXT_robustness")); + bool robustContext = false; + if (isOpenGLES()) { + if (haveExtRobustness) { + GLint value = 0; + glGetIntegerv(GL_CONTEXT_ROBUST_ACCESS_EXT, &value); + robustContext = (value != 0); + } + } else { + if (haveArbRobustness) { + if (hasVersion(Version(3, 0))) { + GLint value = 0; + glGetIntegerv(GL_CONTEXT_FLAGS, &value); + if (value & GL_CONTEXT_FLAG_ROBUST_ACCESS_BIT_ARB) { + robustContext = true; + } + } else { + robustContext = true; + } + } + } + if (robustContext && haveArbRobustness) { + // See https://www.opengl.org/registry/specs/ARB/robustness.txt + m_glGetGraphicsResetStatus = (glGetGraphicsResetStatus_func)resolveFunction("glGetGraphicsResetStatusARB"); + m_glReadnPixels = (glReadnPixels_func)resolveFunction("glReadnPixelsARB"); + m_glGetnTexImage = (glGetnTexImage_func)resolveFunction("glGetnTexImageARB"); + m_glGetnUniformfv = (glGetnUniformfv_func)resolveFunction("glGetnUniformfvARB"); + } else if (robustContext && haveExtRobustness) { + // See https://www.khronos.org/registry/gles/extensions/EXT/EXT_robustness.txt + m_glGetGraphicsResetStatus = (glGetGraphicsResetStatus_func)resolveFunction("glGetGraphicsResetStatusEXT"); + m_glReadnPixels = (glReadnPixels_func)resolveFunction("glReadnPixelsEXT"); + m_glGetnUniformfv = (glGetnUniformfv_func)resolveFunction("glGetnUniformfvEXT"); + } +} + +void EglContext::initDebugOutput() +{ + const bool have_KHR_debug = hasOpenglExtension(QByteArrayLiteral("GL_KHR_debug")); + const bool have_ARB_debug = hasOpenglExtension(QByteArrayLiteral("GL_ARB_debug_output")); + if (!have_KHR_debug && !have_ARB_debug) { + return; + } + + if (!have_ARB_debug) { + // if we don't have ARB debug, but only KHR debug we need to verify whether the context is a debug context + // it should work without as well, but empirical tests show: no it doesn't + if (isOpenGLES()) { + if (!hasVersion(Version(3, 2))) { + // empirical data shows extension doesn't work + return; + } + } else if (!hasVersion(Version(3, 0))) { + return; + } + // can only be queried with either OpenGL >= 3.0 or OpenGL ES of at least 3.1 + GLint value = 0; + glGetIntegerv(GL_CONTEXT_FLAGS, &value); + if (!(value & GL_CONTEXT_FLAG_DEBUG_BIT)) { + return; + } + } + + // Set the callback function + auto callback = [](GLenum source, GLenum type, GLuint id, + GLenum severity, GLsizei length, + const GLchar *message, + const GLvoid *userParam) { + while (length && std::isspace(message[length - 1])) { + --length; + } + + switch (type) { + case GL_DEBUG_TYPE_ERROR: + case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: + qCWarning(KWIN_OPENGL, "%#x: %.*s", id, length, message); + break; + + case GL_DEBUG_TYPE_OTHER: + case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: + case GL_DEBUG_TYPE_PORTABILITY: + case GL_DEBUG_TYPE_PERFORMANCE: + default: + qCDebug(KWIN_OPENGL, "%#x: %.*s", id, length, message); + break; + } + }; + + glDebugMessageCallback(callback, nullptr); + + // This state exists only in GL_KHR_debug + if (have_KHR_debug) { + glEnable(GL_DEBUG_OUTPUT); + } + + if (qEnvironmentVariableIntValue("KWIN_GL_DEBUG")) { + // Enable all debug messages + glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_TRUE); + // Insert a test message + const QByteArray message = QByteArrayLiteral("OpenGL debug output initialized"); + glDebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_OTHER, 0, + GL_DEBUG_SEVERITY_LOW, message.length(), message.constData()); + } else { + // Only enable error messages + glDebugMessageControl(GL_DONT_CARE, GL_DEBUG_TYPE_ERROR, GL_DONT_CARE, 0, nullptr, GL_TRUE); + glDebugMessageControl(GL_DONT_CARE, GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR, GL_DONT_CARE, 0, nullptr, GL_TRUE); + } +} + +GLenum EglContext::checkGraphicsResetStatus() +{ + if (m_glGetGraphicsResetStatus) { + return m_glGetGraphicsResetStatus(); + } else { + return GL_NO_ERROR; + } +} + +void EglContext::glReadnPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, GLvoid *data) +{ + if (m_glReadnPixels) { + m_glReadnPixels(x, y, width, height, format, type, bufSize, data); + } else { + glReadPixels(x, y, width, height, format, type, data); + } +} + +void EglContext::glGetnTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLsizei bufSize, void *pixels) +{ + if (m_glGetnTexImage) { + m_glGetnTexImage(target, level, format, type, bufSize, pixels); + } else { + glGetTexImage(target, level, format, type, pixels); + } +} + +void EglContext::glGetnUniformfv(GLuint program, GLint location, GLsizei bufSize, GLfloat *params) +{ + if (m_glGetnUniformfv) { + m_glGetnUniformfv(program, location, bufSize, params); + } else { + glGetUniformfv(program, location, params); + } +} + +void EglContext::pushFramebuffer(GLFramebuffer *fbo) +{ + if (fbo != currentFramebuffer()) { + glBindFramebuffer(GL_FRAMEBUFFER, fbo->handle()); + glViewport(0, 0, fbo->size().width(), fbo->size().height()); + } + m_fbos.push(fbo); +} + +GLFramebuffer *EglContext::popFramebuffer() +{ + const auto ret = m_fbos.pop(); + if (const auto fbo = currentFramebuffer(); fbo != ret) { + if (fbo) { + glBindFramebuffer(GL_FRAMEBUFFER, fbo->handle()); + glViewport(0, 0, fbo->size().width(), fbo->size().height()); + } else { + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } + } + return ret; +} + +GLFramebuffer *EglContext::currentFramebuffer() +{ + return m_fbos.empty() ? nullptr : m_fbos.top(); +} +} diff --git a/local/recipes/kde/kwin/source/src/opengl/eglcontext.h b/local/recipes/kde/kwin/source/src/opengl/eglcontext.h new file mode 100644 index 0000000000..aac216189e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/eglcontext.h @@ -0,0 +1,149 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "opengl/gltexture.h" +#include "utils/version.h" + +#include +#include +#include +#include + +namespace KWin +{ + +class EglDisplay; +class ShaderManager; +class IndexBuffer; +class GLPlatform; +class GLFramebuffer; +struct DmaBufAttributes; + +// GL_ARB_robustness / GL_EXT_robustness +using glGetGraphicsResetStatus_func = GLenum (*)(); +using glReadnPixels_func = void (*)(GLint x, GLint y, GLsizei width, GLsizei height, + GLenum format, GLenum type, GLsizei bufSize, GLvoid *data); +using glGetnTexImage_func = void (*)(GLenum target, GLint level, GLenum format, GLenum type, + GLsizei bufSize, void *pixels); +using glGetnUniformfv_func = void (*)(GLuint program, GLint location, GLsizei bufSize, GLfloat *params); + +class KWIN_EXPORT EglContext +{ +public: + EglContext(EglDisplay *display, EGLConfig config, ::EGLContext context); + ~EglContext(); + + bool makeCurrent(); + bool makeCurrent(EGLSurface surface); + void doneCurrent() const; + std::shared_ptr importDmaBufAsTexture(const DmaBufAttributes &attributes) const; + + EglDisplay *displayObject() const; + ::EGLContext handle() const; + EGLConfig config() const; + bool isValid() const; + + bool hasVersion(const Version &version) const; + + QByteArrayView openglVersionString() const; + Version openglVersion() const; + QByteArrayView glslVersionString() const; + Version glslVersion() const; + QByteArrayView vendor() const; + QByteArrayView renderer() const; + bool isOpenGLES() const; + bool hasOpenglExtension(QByteArrayView name) const; + bool isSoftwareRenderer() const; + bool supportsTimerQueries() const; + bool supportsTextureSwizzle() const; + bool supportsTextureStorage() const; + bool supportsARGB32Textures() const; + bool supportsRGTextures() const; + bool supports16BitTextures() const; + bool supportsBlits() const; + bool supportsGLES24BitDepthBuffers() const; + bool hasMapBufferRange() const; + bool haveBufferStorage() const; + bool haveSyncFences() const; + bool supportsPackInvert() const; + ShaderManager *shaderManager() const; + GLVertexBuffer *streamingVbo() const; + IndexBuffer *indexBuffer() const; + GLPlatform *glPlatform() const; + QSet openglExtensions() const; + + /** + * checks whether or not this context supports all the features that KWin requires + */ + bool checkSupported() const; + + GLenum checkGraphicsResetStatus(); + void glReadnPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei bufSize, GLvoid *data); + void glGetnTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLsizei bufSize, void *pixels); + void glGetnUniformfv(GLuint program, GLint location, GLsizei bufSize, GLfloat *params); + + void pushFramebuffer(GLFramebuffer *fbo); + GLFramebuffer *popFramebuffer(); + GLFramebuffer *currentFramebuffer(); + + static EglContext *currentContext(); + static std::unique_ptr create(EglDisplay *display, EGLConfig config, ::EGLContext sharedContext); + +private: + static ::EGLContext createContext(EglDisplay *display, EGLConfig config, ::EGLContext sharedContext); + bool checkTimerQuerySupport() const; + void setShaderManager(ShaderManager *manager); + void setStreamingBuffer(GLVertexBuffer *vbo); + void setIndexBuffer(IndexBuffer *buffer); + typedef void (*resolveFuncPtr)(); + void glResolveFunctions(const std::function &resolveFunction); + void initDebugOutput(); + + static EglContext *s_currentContext; + + const QByteArrayView m_versionString; + const Version m_version; + const QByteArrayView m_glslVersionString; + const Version m_glslVersion; + const QByteArrayView m_vendor; + const QByteArrayView m_renderer; + const bool m_isOpenglES; + const QSet m_extensions; + const bool m_supportsTimerQueries; + const bool m_supportsTextureStorage; + const bool m_supportsTextureSwizzle; + const bool m_supportsARGB32Textures; + const bool m_supportsRGTextures; + const bool m_supports16BitTextures; + const bool m_supportsBlits; + const bool m_supportsPackedDepthStencil; + const bool m_supportsGLES24BitDepthBuffers; + const bool m_hasMapBufferRange; + const bool m_haveBufferStorage; + const bool m_haveSyncFences; + const bool m_supportsIndexedQuads; + const bool m_supportsPackInvert; + const std::unique_ptr m_glPlatform; + glGetGraphicsResetStatus_func m_glGetGraphicsResetStatus = nullptr; + glReadnPixels_func m_glReadnPixels = nullptr; + glGetnTexImage_func m_glGetnTexImage = nullptr; + glGetnUniformfv_func m_glGetnUniformfv = nullptr; + + EglDisplay *const m_display; + const ::EGLContext m_handle; + const EGLConfig m_config; + std::unique_ptr m_shaderManager; + std::unique_ptr m_streamingBuffer; + std::unique_ptr m_indexBuffer; + QStack m_fbos; + uint32_t m_vao = 0; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/egldisplay.cpp b/local/recipes/kde/kwin/source/src/opengl/egldisplay.cpp new file mode 100644 index 0000000000..ec411f76e2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/egldisplay.cpp @@ -0,0 +1,367 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "egldisplay.h" +#include "core/drmdevice.h" +#include "core/graphicsbuffer.h" +#include "opengl/eglutils_p.h" +#include "opengl/glutils.h" +#include "utils/common.h" + +#include +#include +#include + +#ifndef EGL_DRM_RENDER_NODE_FILE_EXT +#define EGL_DRM_RENDER_NODE_FILE_EXT 0x3377 +#endif + +namespace KWin +{ + +bool EglDisplay::shouldUseOpenGLES() +{ + if (qstrcmp(qgetenv("KWIN_COMPOSE"), "O2ES") == 0) { + return true; + } + return QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES; +} + +std::unique_ptr EglDisplay::create(::EGLDisplay display, bool owning) +{ + if (!display) { + return nullptr; + } + EGLint major, minor; + if (eglInitialize(display, &major, &minor) == EGL_FALSE) { + qCWarning(KWIN_OPENGL) << "eglInitialize failed"; + EGLint error = eglGetError(); + if (error != EGL_SUCCESS) { + qCWarning(KWIN_OPENGL) << "Error during eglInitialize " << getEglErrorString(error); + } + return nullptr; + } + EGLint error = eglGetError(); + if (error != EGL_SUCCESS) { + qCWarning(KWIN_OPENGL) << "Error during eglInitialize " << getEglErrorString(error); + return nullptr; + } + qCDebug(KWIN_OPENGL) << "Egl Initialize succeeded"; + if (eglBindAPI(shouldUseOpenGLES() ? EGL_OPENGL_ES_API : EGL_OPENGL_API) == EGL_FALSE) { + qCCritical(KWIN_OPENGL) << "bind OpenGL API failed"; + return nullptr; + } + qCDebug(KWIN_OPENGL) << "EGL version: " << major << "." << minor; + + const auto extensions = QByteArray(eglQueryString(display, EGL_EXTENSIONS)).split(' '); + + const QByteArray requiredExtensions[] = { + QByteArrayLiteral("EGL_KHR_no_config_context"), + QByteArrayLiteral("EGL_KHR_surfaceless_context"), + }; + for (const QByteArray &extensionName : requiredExtensions) { + if (!extensions.contains(extensionName)) { + qCWarning(KWIN_OPENGL) << extensionName << "extension is unsupported"; + return nullptr; + } + } + + return std::make_unique(display, extensions, owning); +} + +static std::optional devIdForFileName(const QString &path) +{ + auto device = DrmDevice::open(path); + if (device) { + return device->deviceId(); + } else { + qCWarning(KWIN_OPENGL, "couldn't find dev node for drm device %s", qPrintable(path)); + return std::nullopt; + } +} + +EglDisplay::EglDisplay(::EGLDisplay display, const QList &extensions, bool owning) + : m_handle(display) + , m_extensions(extensions) + , m_owning(owning) + , m_renderNode(determineRenderNode()) + , m_renderDevNode(devIdForFileName(m_renderNode)) + , m_supportsBufferAge(extensions.contains(QByteArrayLiteral("EGL_EXT_buffer_age")) && qgetenv("KWIN_USE_BUFFER_AGE") != "0") + , m_supportsNativeFence(extensions.contains(QByteArrayLiteral("EGL_ANDROID_native_fence_sync")) + && extensions.contains(QByteArrayLiteral("EGL_KHR_wait_sync"))) +{ + m_functions.createImageKHR = reinterpret_cast(eglGetProcAddress("eglCreateImageKHR")); + m_functions.destroyImageKHR = reinterpret_cast(eglGetProcAddress("eglDestroyImageKHR")); + m_functions.queryDmaBufFormatsEXT = reinterpret_cast(eglGetProcAddress("eglQueryDmaBufFormatsEXT")); + m_functions.queryDmaBufModifiersEXT = reinterpret_cast(eglGetProcAddress("eglQueryDmaBufModifiersEXT")); + + m_importFormats = queryImportFormats(); +} + +EglDisplay::~EglDisplay() +{ + if (m_owning) { + eglTerminate(m_handle); + } +} + +QList EglDisplay::extensions() const +{ + return m_extensions; +} + +::EGLDisplay EglDisplay::handle() const +{ + return m_handle; +} + +bool EglDisplay::hasExtension(const QByteArray &name) const +{ + return m_extensions.contains(name); +} + +static bool checkExtension(const QByteArrayView extensions, const QByteArrayView extension) +{ + for (int i = 0; i < extensions.size();) { + if (extensions[i] == ' ') { + i++; + continue; + } + int next = extensions.indexOf(' ', i); + if (next == -1) { + next = extensions.size(); + } + + const int size = next - i; + if (extension.size() == size && extensions.sliced(i, size) == extension) { + return true; + } + + i = next; + } + + return false; +} + +QString EglDisplay::renderNode() const +{ + return m_renderNode; +} + +bool EglDisplay::supportsBufferAge() const +{ + return m_supportsBufferAge; +} + +bool EglDisplay::supportsNativeFence() const +{ + return m_supportsNativeFence; +} + +EGLImageKHR EglDisplay::importDmaBufAsImage(const DmaBufAttributes &dmabuf) const +{ + QList attribs; + attribs.reserve(6 + dmabuf.planeCount * 10 + 1); + + attribs << EGL_WIDTH << dmabuf.width + << EGL_HEIGHT << dmabuf.height + << EGL_LINUX_DRM_FOURCC_EXT << dmabuf.format; + + attribs << EGL_DMA_BUF_PLANE0_FD_EXT << dmabuf.fd[0].get() + << EGL_DMA_BUF_PLANE0_OFFSET_EXT << dmabuf.offset[0] + << EGL_DMA_BUF_PLANE0_PITCH_EXT << dmabuf.pitch[0]; + if (dmabuf.modifier != DRM_FORMAT_MOD_INVALID) { + attribs << EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT << EGLint(dmabuf.modifier & 0xffffffff) + << EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT << EGLint(dmabuf.modifier >> 32); + } + + if (dmabuf.planeCount > 1) { + attribs << EGL_DMA_BUF_PLANE1_FD_EXT << dmabuf.fd[1].get() + << EGL_DMA_BUF_PLANE1_OFFSET_EXT << dmabuf.offset[1] + << EGL_DMA_BUF_PLANE1_PITCH_EXT << dmabuf.pitch[1]; + if (dmabuf.modifier != DRM_FORMAT_MOD_INVALID) { + attribs << EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT << EGLint(dmabuf.modifier & 0xffffffff) + << EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT << EGLint(dmabuf.modifier >> 32); + } + } + + if (dmabuf.planeCount > 2) { + attribs << EGL_DMA_BUF_PLANE2_FD_EXT << dmabuf.fd[2].get() + << EGL_DMA_BUF_PLANE2_OFFSET_EXT << dmabuf.offset[2] + << EGL_DMA_BUF_PLANE2_PITCH_EXT << dmabuf.pitch[2]; + if (dmabuf.modifier != DRM_FORMAT_MOD_INVALID) { + attribs << EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT << EGLint(dmabuf.modifier & 0xffffffff) + << EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT << EGLint(dmabuf.modifier >> 32); + } + } + + if (dmabuf.planeCount > 3) { + attribs << EGL_DMA_BUF_PLANE3_FD_EXT << dmabuf.fd[3].get() + << EGL_DMA_BUF_PLANE3_OFFSET_EXT << dmabuf.offset[3] + << EGL_DMA_BUF_PLANE3_PITCH_EXT << dmabuf.pitch[3]; + if (dmabuf.modifier != DRM_FORMAT_MOD_INVALID) { + attribs << EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT << EGLint(dmabuf.modifier & 0xffffffff) + << EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT << EGLint(dmabuf.modifier >> 32); + } + } + + attribs << EGL_NONE; + + return createImage(EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs.data()); +} + +EGLImageKHR EglDisplay::importDmaBufAsImage(const DmaBufAttributes &dmabuf, int plane, int format, const QSize &size) const +{ + QList attribs; + attribs.reserve(6 + 1 * 10 + 1); + + attribs << EGL_WIDTH << size.width() + << EGL_HEIGHT << size.height() + << EGL_LINUX_DRM_FOURCC_EXT << format; + + attribs << EGL_DMA_BUF_PLANE0_FD_EXT << dmabuf.fd[plane].get() + << EGL_DMA_BUF_PLANE0_OFFSET_EXT << dmabuf.offset[plane] + << EGL_DMA_BUF_PLANE0_PITCH_EXT << dmabuf.pitch[plane]; + if (dmabuf.modifier != DRM_FORMAT_MOD_INVALID) { + attribs << EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT << EGLint(dmabuf.modifier & 0xffffffff) + << EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT << EGLint(dmabuf.modifier >> 32); + } + attribs << EGL_NONE; + + return createImage(EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs.data()); +} + +QHash EglDisplay::allSupportedDrmFormats() const +{ + return m_importFormats; +} + +QHash> EglDisplay::nonExternalOnlySupportedDrmFormats() const +{ + QHash> ret; + ret.reserve(m_importFormats.size()); + for (auto it = m_importFormats.constBegin(), itEnd = m_importFormats.constEnd(); it != itEnd; ++it) { + ret[it.key()] = it->nonExternalOnlyModifiers; + } + return ret; +} + +bool EglDisplay::isExternalOnly(uint32_t format, uint64_t modifier) const +{ + if (const auto it = m_importFormats.find(format); it != m_importFormats.end()) { + return it->externalOnlyModifiers.contains(modifier); + } else { + return false; + } +} + +QHash EglDisplay::queryImportFormats() const +{ + if (!hasExtension(QByteArrayLiteral("EGL_EXT_image_dma_buf_import")) || !hasExtension(QByteArrayLiteral("EGL_EXT_image_dma_buf_import_modifiers"))) { + return {}; + } + + if (m_functions.queryDmaBufFormatsEXT == nullptr) { + return {}; + } + + EGLint count = 0; + EGLBoolean success = m_functions.queryDmaBufFormatsEXT(m_handle, 0, nullptr, &count); + if (!success || count == 0) { + qCCritical(KWIN_OPENGL) << "eglQueryDmaBufFormatsEXT failed!" << getEglErrorString(); + return {}; + } + QList formats(count); + if (!m_functions.queryDmaBufFormatsEXT(m_handle, count, (EGLint *)formats.data(), &count)) { + qCCritical(KWIN_OPENGL) << "eglQueryDmaBufFormatsEXT with count" << count << "failed!" << getEglErrorString(); + return {}; + } + QHash ret; + for (const auto format : std::as_const(formats)) { + if (m_functions.queryDmaBufModifiersEXT != nullptr) { + EGLint count = 0; + const EGLBoolean success = m_functions.queryDmaBufModifiersEXT(m_handle, format, 0, nullptr, nullptr, &count); + if (success && count > 0) { + DrmFormatInfo drmFormatInfo; + drmFormatInfo.allModifiers.resize(count); + QList externalOnly(count); + if (m_functions.queryDmaBufModifiersEXT(m_handle, format, count, drmFormatInfo.allModifiers.data(), externalOnly.data(), &count)) { + drmFormatInfo.externalOnlyModifiers = drmFormatInfo.allModifiers; + drmFormatInfo.nonExternalOnlyModifiers = drmFormatInfo.allModifiers; + for (int i = drmFormatInfo.allModifiers.size() - 1; i >= 0; i--) { + if (externalOnly[i]) { + drmFormatInfo.nonExternalOnlyModifiers.removeAll(drmFormatInfo.allModifiers[i]); + } else { + drmFormatInfo.externalOnlyModifiers.removeAll(drmFormatInfo.allModifiers[i]); + } + } + if (!drmFormatInfo.allModifiers.empty()) { + if (!drmFormatInfo.allModifiers.contains(DRM_FORMAT_MOD_INVALID)) { + drmFormatInfo.allModifiers.push_back(DRM_FORMAT_MOD_INVALID); + if (!drmFormatInfo.nonExternalOnlyModifiers.empty()) { + drmFormatInfo.nonExternalOnlyModifiers.push_back(DRM_FORMAT_MOD_INVALID); + } else { + drmFormatInfo.externalOnlyModifiers.push_back(DRM_FORMAT_MOD_INVALID); + } + } + ret.insert(format, drmFormatInfo); + } + continue; + } + } + } + DrmFormatInfo drmFormat; + drmFormat.allModifiers = {DRM_FORMAT_MOD_INVALID, DRM_FORMAT_MOD_LINEAR}; + drmFormat.nonExternalOnlyModifiers = {DRM_FORMAT_MOD_INVALID, DRM_FORMAT_MOD_LINEAR}; + ret.insert(format, drmFormat); + } + return ret; +} + +QString EglDisplay::determineRenderNode() const +{ + const char *clientExtensions = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS); + if (checkExtension(clientExtensions, "EGL_EXT_device_query")) { + EGLAttrib eglDeviceAttrib; + if (eglQueryDisplayAttribEXT(m_handle, EGL_DEVICE_EXT, &eglDeviceAttrib)) { + EGLDeviceEXT eglDevice = reinterpret_cast(eglDeviceAttrib); + + const char *deviceExtensions = eglQueryDeviceStringEXT(eglDevice, EGL_EXTENSIONS); + if (checkExtension(deviceExtensions, "EGL_EXT_device_drm_render_node")) { + if (const char *node = eglQueryDeviceStringEXT(eglDevice, EGL_DRM_RENDER_NODE_FILE_EXT)) { + return QString::fromLocal8Bit(node); + } + } + if (checkExtension(deviceExtensions, "EGL_EXT_device_drm")) { + // Fallback to display device. + if (const char *node = eglQueryDeviceStringEXT(eglDevice, EGL_DRM_DEVICE_FILE_EXT)) { + return QString::fromLocal8Bit(node); + } + } + } + } + return QString(); +} + +std::optional EglDisplay::renderDevNode() const +{ + return m_renderDevNode; +} + +EGLImageKHR EglDisplay::createImage(EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLint *attrib_list) const +{ + Q_ASSERT(m_functions.createImageKHR); + return m_functions.createImageKHR(m_handle, ctx, target, buffer, attrib_list); +} + +void EglDisplay::destroyImage(EGLImageKHR image) const +{ + Q_ASSERT(m_functions.destroyImageKHR); + m_functions.destroyImageKHR(m_handle, image); +} +} diff --git a/local/recipes/kde/kwin/source/src/opengl/egldisplay.h b/local/recipes/kde/kwin/source/src/opengl/egldisplay.h new file mode 100644 index 0000000000..67ec0706bd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/egldisplay.h @@ -0,0 +1,86 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "kwin_export.h" + +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +struct DmaBufAttributes; +class GLTexture; + +class KWIN_EXPORT EglDisplay +{ +public: + struct DrmFormatInfo + { + QList allModifiers; + QList nonExternalOnlyModifiers; + QList externalOnlyModifiers; + }; + + EglDisplay(::EGLDisplay display, const QList &extensions, bool owning = true); + ~EglDisplay(); + + QList extensions() const; + ::EGLDisplay handle() const; + bool hasExtension(const QByteArray &name) const; + + QString renderNode() const; + std::optional renderDevNode() const; + + bool supportsBufferAge() const; + bool supportsNativeFence() const; + + QHash> nonExternalOnlySupportedDrmFormats() const; + QHash allSupportedDrmFormats() const; + bool isExternalOnly(uint32_t format, uint64_t modifier) const; + + EGLImageKHR createImage(EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLint *attrib_list) const; + void destroyImage(EGLImageKHR image) const; + + EGLImageKHR importDmaBufAsImage(const DmaBufAttributes &dmabuf) const; + EGLImageKHR importDmaBufAsImage(const DmaBufAttributes &dmabuf, int plane, int format, const QSize &size) const; + + static bool shouldUseOpenGLES(); + static std::unique_ptr create(::EGLDisplay display, bool owning = true); + +private: + QHash queryImportFormats() const; + QString determineRenderNode() const; + + const ::EGLDisplay m_handle; + const QList m_extensions; + const bool m_owning; + const QString m_renderNode; + const std::optional m_renderDevNode; + + const bool m_supportsBufferAge; + const bool m_supportsNativeFence; + QHash m_importFormats; + + struct + { + PFNEGLCREATEIMAGEKHRPROC createImageKHR = nullptr; + PFNEGLDESTROYIMAGEKHRPROC destroyImageKHR = nullptr; + + PFNEGLQUERYDMABUFFORMATSEXTPROC queryDmaBufFormatsEXT = nullptr; + PFNEGLQUERYDMABUFMODIFIERSEXTPROC queryDmaBufModifiersEXT = nullptr; + } m_functions; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/eglimagetexture.cpp b/local/recipes/kde/kwin/source/src/opengl/eglimagetexture.cpp new file mode 100644 index 0000000000..1701337325 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/eglimagetexture.cpp @@ -0,0 +1,49 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "eglimagetexture.h" +#include "egldisplay.h" +#include "opengl/gltexture_p.h" + +#include +#include + +namespace KWin +{ + +EGLImageTexture::EGLImageTexture(EglDisplay *display, EGLImage image, uint textureId, int internalFormat, const QSize &size, uint32_t target) + : GLTexture(target, textureId, internalFormat, size, 1, true, OutputTransform::FlipY) + , m_image(image) + , m_display(display) +{ +} + +EGLImageTexture::~EGLImageTexture() +{ + m_display->destroyImage(m_image); +} + +std::shared_ptr EGLImageTexture::create(EglDisplay *display, EGLImageKHR image, int internalFormat, const QSize &size, bool externalOnly) +{ + if (image == EGL_NO_IMAGE) { + return nullptr; + } + GLuint texture = 0; + glGenTextures(1, &texture); + if (!texture) { + return nullptr; + } + const uint32_t target = externalOnly ? GL_TEXTURE_EXTERNAL_OES : GL_TEXTURE_2D; + glBindTexture(target, texture); + glEGLImageTargetTexture2DOES(target, image); + glBindTexture(target, 0); + return std::make_shared(display, image, texture, internalFormat, size, target); +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/opengl/eglimagetexture.h b/local/recipes/kde/kwin/source/src/opengl/eglimagetexture.h new file mode 100644 index 0000000000..99ffcea0bc --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/eglimagetexture.h @@ -0,0 +1,34 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "opengl/gltexture.h" + +typedef void *EGLImageKHR; +typedef void *EGLClientBuffer; + +namespace KWin +{ + +class EglDisplay; + +class KWIN_EXPORT EGLImageTexture : public GLTexture +{ +public: + explicit EGLImageTexture(EglDisplay *display, EGLImageKHR image, uint textureId, int internalFormat, const QSize &size, uint32_t target); + ~EGLImageTexture() override; + + static std::shared_ptr create(EglDisplay *display, EGLImageKHR image, int internalFormat, const QSize &size, bool externalOnly); + + EGLImageKHR m_image; + EglDisplay *const m_display; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/eglnativefence.cpp b/local/recipes/kde/kwin/source/src/opengl/eglnativefence.cpp new file mode 100644 index 0000000000..261eaf77a7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/eglnativefence.cpp @@ -0,0 +1,84 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "eglnativefence.h" +#include "egldisplay.h" + +#include + +namespace KWin +{ + +#ifndef EGL_ANDROID_native_fence_sync +#define EGL_SYNC_NATIVE_FENCE_ANDROID 0x3144 +#define EGL_NO_NATIVE_FENCE_FD_ANDROID -1 +#endif // EGL_ANDROID_native_fence_sync + +EGLNativeFence::EGLNativeFence(EglDisplay *display) + : m_display(display) +{ + if (!display->supportsNativeFence()) { + m_sync = EGL_NO_SYNC_KHR; + return; + } + + m_sync = eglCreateSyncKHR(display->handle(), EGL_SYNC_NATIVE_FENCE_ANDROID, nullptr); + + if (m_sync != EGL_NO_SYNC_KHR) { + // The native fence will get a valid sync file fd only after a flush. + glFlush(); + m_fileDescriptor = FileDescriptor(eglDupNativeFenceFDANDROID(m_display->handle(), m_sync)); + } +} + +EGLNativeFence::EGLNativeFence(EglDisplay *display, EGLSyncKHR sync) + : m_sync(sync) + , m_display(display) +{ +} + +EGLNativeFence::~EGLNativeFence() +{ + m_fileDescriptor.reset(); + if (m_sync != EGL_NO_SYNC_KHR) { + eglDestroySyncKHR(m_display->handle(), m_sync); + } +} + +bool EGLNativeFence::isValid() const +{ + return m_sync != EGL_NO_SYNC_KHR && m_fileDescriptor.isValid(); +} + +const FileDescriptor &EGLNativeFence::fileDescriptor() const +{ + return m_fileDescriptor; +} + +FileDescriptor &&EGLNativeFence::takeFileDescriptor() +{ + return std::move(m_fileDescriptor); +} + +bool EGLNativeFence::waitSync() const +{ + return eglWaitSync(m_display->handle(), m_sync, 0) == EGL_TRUE; +} + +EGLNativeFence EGLNativeFence::importFence(EglDisplay *display, FileDescriptor &&fd) +{ + EGLint attributes[] = { + EGL_SYNC_NATIVE_FENCE_FD_ANDROID, fd.get(), + EGL_NONE}; + auto ret = eglCreateSyncKHR(display->handle(), EGL_SYNC_NATIVE_FENCE_ANDROID, attributes); + if (ret != EGL_NO_SYNC_KHR) { + // eglCreateSyncKHR takes ownership only on success + fd.take(); + } + return EGLNativeFence(display, ret); +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/opengl/eglnativefence.h b/local/recipes/kde/kwin/source/src/opengl/eglnativefence.h new file mode 100644 index 0000000000..4a14c5f988 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/eglnativefence.h @@ -0,0 +1,41 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "kwin_export.h" +#include "utils/filedescriptor.h" + +namespace KWin +{ + +class EglDisplay; + +class KWIN_EXPORT EGLNativeFence +{ +public: + explicit EGLNativeFence(EglDisplay *display); + explicit EGLNativeFence(EglDisplay *display, EGLSyncKHR sync); + EGLNativeFence(EGLNativeFence &&) = delete; + EGLNativeFence(const EGLNativeFence &) = delete; + ~EGLNativeFence(); + + bool isValid() const; + const FileDescriptor &fileDescriptor() const; + FileDescriptor &&takeFileDescriptor(); + bool waitSync() const; + + static EGLNativeFence importFence(EglDisplay *display, FileDescriptor &&fd); + +private: + EGLSyncKHR m_sync = EGL_NO_SYNC_KHR; + EglDisplay *m_display = nullptr; + FileDescriptor m_fileDescriptor; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/opengl/eglswapchain.cpp b/local/recipes/kde/kwin/source/src/opengl/eglswapchain.cpp new file mode 100644 index 0000000000..e6cf5058a3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/eglswapchain.cpp @@ -0,0 +1,183 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "opengl/eglswapchain.h" +#include "core/graphicsbuffer.h" +#include "core/graphicsbufferallocator.h" +#include "opengl/eglcontext.h" +#include "opengl/glutils.h" +#include "utils/common.h" + +#include +#include + +namespace KWin +{ + +EglSwapchainSlot::EglSwapchainSlot(GraphicsBuffer *buffer, std::unique_ptr &&framebuffer, const std::shared_ptr &texture) + : m_buffer(buffer) + , m_framebuffer(std::move(framebuffer)) + , m_texture(texture) +{ +} + +EglSwapchainSlot::~EglSwapchainSlot() +{ + m_framebuffer.reset(); + m_texture.reset(); + m_buffer->drop(); +} + +GraphicsBuffer *EglSwapchainSlot::buffer() const +{ + return m_buffer; +} + +std::shared_ptr EglSwapchainSlot::texture() const +{ + return m_texture; +} + +GLFramebuffer *EglSwapchainSlot::framebuffer() const +{ + return m_framebuffer.get(); +} + +int EglSwapchainSlot::age() const +{ + return m_age; +} + +bool EglSwapchainSlot::isBusy() const +{ + return m_buffer->isReferenced() || (m_releaseFd.isValid() && !m_releaseFd.isReadable()); +} + +std::shared_ptr EglSwapchainSlot::create(EglContext *context, GraphicsBuffer *buffer) +{ + auto texture = context->importDmaBufAsTexture(*buffer->dmabufAttributes()); + if (!texture) { + buffer->drop(); + return nullptr; + } + auto framebuffer = std::make_unique(texture.get()); + if (!framebuffer->valid()) { + buffer->drop(); + return nullptr; + } + + texture->setFilter(GL_LINEAR); + texture->setWrapMode(GL_CLAMP_TO_EDGE); + + return std::make_shared(buffer, std::move(framebuffer), texture); +} + +EglSwapchain::EglSwapchain(GraphicsBufferAllocator *allocator, EglContext *context, const QSize &size, uint32_t format, uint64_t modifier, const std::shared_ptr &seed) + : m_allocator(allocator) + , m_context(context) + , m_size(size) + , m_format(format) + , m_modifier(modifier) + , m_slots({seed}) +{ +} + +EglSwapchain::~EglSwapchain() +{ +} + +QSize EglSwapchain::size() const +{ + return m_size; +} + +uint32_t EglSwapchain::format() const +{ + return m_format; +} + +uint64_t EglSwapchain::modifier() const +{ + return m_modifier; +} + +std::shared_ptr EglSwapchain::acquire() +{ + const auto it = std::ranges::find_if(std::as_const(m_slots), [](const auto &slot) { + return !slot->isBusy(); + }); + if (it != m_slots.cend()) { + return *it; + } + + GraphicsBuffer *buffer = m_allocator->allocate(GraphicsBufferOptions{ + .size = m_size, + .format = m_format, + .modifiers = {m_modifier}, + }); + if (!buffer) { + qCWarning(KWIN_OPENGL) << "Failed to allocate an egl gbm swapchain graphics buffer"; + return nullptr; + } + + auto slot = EglSwapchainSlot::create(m_context, buffer); + if (!slot) { + return nullptr; + } + m_slots.append(slot); + return slot; +} + +void EglSwapchain::release(std::shared_ptr slot, FileDescriptor &&releaseFence) +{ + slot->m_releaseFd = std::move(releaseFence); + for (qsizetype i = 0; i < m_slots.count(); ++i) { + if (m_slots[i] == slot) { + m_slots[i]->m_age = 1; + } else if (m_slots[i]->m_age > 0) { + m_slots[i]->m_age++; + } + } +} + +void EglSwapchain::resetBufferAge() +{ + for (const auto &slot : std::as_const(m_slots)) { + slot->m_age = 0; + } +} + +std::shared_ptr EglSwapchain::create(GraphicsBufferAllocator *allocator, EglContext *context, const QSize &size, uint32_t format, const QList &modifiers) +{ + if (!context->makeCurrent()) { + return nullptr; + } + + // The seed graphics buffer is used to fixate modifiers. + GraphicsBuffer *seed = allocator->allocate(GraphicsBufferOptions{ + .size = size, + .format = format, + .modifiers = modifiers, + }); + if (!seed) { + return nullptr; + } + const auto first = EglSwapchainSlot::create(context, seed); + if (!first) { + return nullptr; + } + return std::make_shared(std::move(allocator), + context, + size, + format, + seed->dmabufAttributes()->modifier, + first); +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/opengl/eglswapchain.h b/local/recipes/kde/kwin/source/src/opengl/eglswapchain.h new file mode 100644 index 0000000000..43d6faf5f8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/eglswapchain.h @@ -0,0 +1,80 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "kwin_export.h" +#include "utils/filedescriptor.h" + +#include +#include + +#include +#include +#include + +namespace KWin +{ + +class GraphicsBufferAllocator; +class GraphicsBuffer; +class GLFramebuffer; +class GLTexture; +class EglContext; + +class KWIN_EXPORT EglSwapchainSlot +{ +public: + EglSwapchainSlot(GraphicsBuffer *buffer, std::unique_ptr &&framebuffer, const std::shared_ptr &texture); + ~EglSwapchainSlot(); + + GraphicsBuffer *buffer() const; + std::shared_ptr texture() const; + GLFramebuffer *framebuffer() const; + int age() const; + + static std::shared_ptr create(EglContext *context, GraphicsBuffer *buffer); + +private: + bool isBusy() const; + + GraphicsBuffer *m_buffer; + std::unique_ptr m_framebuffer; + std::shared_ptr m_texture; + int m_age = 0; + FileDescriptor m_releaseFd; + friend class EglSwapchain; +}; + +class KWIN_EXPORT EglSwapchain +{ +public: + EglSwapchain(GraphicsBufferAllocator *allocator, EglContext *context, const QSize &size, uint32_t format, uint64_t modifier, const std::shared_ptr &seed); + ~EglSwapchain(); + + QSize size() const; + uint32_t format() const; + uint64_t modifier() const; + + std::shared_ptr acquire(); + void release(std::shared_ptr slot, FileDescriptor &&releaseFence); + + void resetBufferAge(); + + static std::shared_ptr create(GraphicsBufferAllocator *allocator, EglContext *context, const QSize &size, uint32_t format, const QList &modifiers); + +private: + GraphicsBufferAllocator *m_allocator; + EglContext *m_context; + QSize m_size; + uint32_t m_format; + uint64_t m_modifier; + QList> m_slots; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/opengl/eglutils_p.h b/local/recipes/kde/kwin/source/src/opengl/eglutils_p.h new file mode 100644 index 0000000000..6144119bb5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/eglutils_p.h @@ -0,0 +1,56 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2021 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include +#include + +static inline QString getEglErrorString(EGLint errorCode) +{ + switch (errorCode) { + case EGL_SUCCESS: + return QStringLiteral("EGL_SUCCESS"); + case EGL_NOT_INITIALIZED: + return QStringLiteral("EGL_NOT_INITIALIZED"); + case EGL_BAD_ACCESS: + return QStringLiteral("EGL_BAD_ACCESS"); + case EGL_BAD_ALLOC: + return QStringLiteral("EGL_BAD_ALLOC"); + case EGL_BAD_ATTRIBUTE: + return QStringLiteral("EGL_BAD_ATTRIBUTE"); + case EGL_BAD_CONTEXT: + return QStringLiteral("EGL_BAD_CONTEXT"); + case EGL_BAD_CONFIG: + return QStringLiteral("EGL_BAD_CONFIG"); + case EGL_BAD_CURRENT_SURFACE: + return QStringLiteral("EGL_BAD_CURRENT_SURFACE"); + case EGL_BAD_DISPLAY: + return QStringLiteral("EGL_BAD_DISPLAY"); + case EGL_BAD_SURFACE: + return QStringLiteral("EGL_BAD_SURFACE"); + case EGL_BAD_MATCH: + return QStringLiteral("EGL_BAD_MATCH"); + case EGL_BAD_PARAMETER: + return QStringLiteral("EGL_BAD_PARAMETER"); + case EGL_BAD_NATIVE_PIXMAP: + return QStringLiteral("EGL_BAD_NATIVE_PIXMAP"); + case EGL_BAD_NATIVE_WINDOW: + return QStringLiteral("EGL_BAD_NATIVE_WINDOW"); + case EGL_CONTEXT_LOST: + return QStringLiteral("EGL_CONTEXT_LOST"); + default: + return QString::number(errorCode, 16); + } +} + +static inline QString getEglErrorString() +{ + return getEglErrorString(eglGetError()); +} diff --git a/local/recipes/kde/kwin/source/src/opengl/glframebuffer.cpp b/local/recipes/kde/kwin/source/src/opengl/glframebuffer.cpp new file mode 100644 index 0000000000..56633d886e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/glframebuffer.cpp @@ -0,0 +1,310 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006-2007 Rivo Laks + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "glframebuffer.h" +#include "core/rendertarget.h" +#include "core/renderviewport.h" +#include "glplatform.h" +#include "gltexture.h" +#include "glutils.h" +#include "utils/common.h" + +namespace KWin +{ + +GLFramebuffer *GLFramebuffer::currentFramebuffer() +{ + return EglContext::currentContext()->currentFramebuffer(); +} + +void GLFramebuffer::pushFramebuffer(GLFramebuffer *fbo) +{ + EglContext::currentContext()->pushFramebuffer(fbo); +} + +GLFramebuffer *GLFramebuffer::popFramebuffer() +{ + return EglContext::currentContext()->popFramebuffer(); +} + +GLFramebuffer::GLFramebuffer() + : m_colorAttachment(nullptr) +{ +} + +static QString formatFramebufferStatus(GLenum status) +{ + switch (status) { + case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: + // An attachment is the wrong type / is invalid / has 0 width or height + return QStringLiteral("GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT"); + case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: + // There are no images attached to the framebuffer + return QStringLiteral("GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT"); + case GL_FRAMEBUFFER_UNSUPPORTED: + // A format or the combination of formats of the attachments is unsupported + return QStringLiteral("GL_FRAMEBUFFER_UNSUPPORTED"); + case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT: + // Not all attached images have the same width and height + return QStringLiteral("GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT"); + case GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT: + // The color attachments don't have the same format + return QStringLiteral("GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT"); + case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE_EXT: + // The attachments don't have the same number of samples + return QStringLiteral("GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE"); + case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT: + // The draw buffer is missing + return QStringLiteral("GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER"); + case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT: + // The read buffer is missing + return QStringLiteral("GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER"); + default: + return QStringLiteral("Unknown (0x") + QString::number(status, 16) + QStringLiteral(")"); + } +} + +GLFramebuffer::GLFramebuffer(GLTexture *colorAttachment, Attachment attachment) + : m_size(colorAttachment->size()) + , m_colorAttachment(colorAttachment) +{ + GLuint prevFbo = 0; + if (const GLFramebuffer *current = currentFramebuffer()) { + prevFbo = current->handle(); + } + + glGenFramebuffers(1, &m_handle); + glBindFramebuffer(GL_FRAMEBUFFER, m_handle); + + initColorAttachment(colorAttachment); + if (attachment == Attachment::CombinedDepthStencil) { + initDepthStencilAttachment(); + } + + const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + glBindFramebuffer(GL_FRAMEBUFFER, prevFbo); + + if (status != GL_FRAMEBUFFER_COMPLETE) { + // We have an incomplete framebuffer, consider it invalid + qCCritical(KWIN_OPENGL) << "Invalid framebuffer status: " << formatFramebufferStatus(status); + glDeleteFramebuffers(1, &m_handle); + return; + } + + m_valid = true; +} + +GLFramebuffer::GLFramebuffer(GLuint handle, const QSize &size) + : m_handle(handle) + , m_size(size) + , m_valid(true) + , m_foreign(true) + , m_colorAttachment(nullptr) +{ +} + +GLFramebuffer::~GLFramebuffer() +{ + if (!EglContext::currentContext()) { + qCWarning(KWIN_OPENGL, "Could not delete framebuffer because no context is current"); + return; + } + if (!m_foreign && m_valid) { + glDeleteFramebuffers(1, &m_handle); + } + if (m_depthBuffer) { + glDeleteRenderbuffers(1, &m_depthBuffer); + } + if (m_stencilBuffer && m_stencilBuffer != m_depthBuffer) { + glDeleteRenderbuffers(1, &m_stencilBuffer); + } +} + +bool GLFramebuffer::bind() +{ + if (!valid()) { + qCCritical(KWIN_OPENGL) << "Can't enable invalid framebuffer object!"; + return false; + } + + glBindFramebuffer(GL_FRAMEBUFFER, handle()); + glViewport(0, 0, m_size.width(), m_size.height()); + + return true; +} + +void GLFramebuffer::initColorAttachment(GLTexture *colorAttachment) +{ + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + colorAttachment->target(), colorAttachment->texture(), 0); +} + +void GLFramebuffer::initDepthStencilAttachment() +{ + GLuint buffer = 0; + const auto context = EglContext::currentContext(); + // Try to attach a depth/stencil combined attachment. + if (context->supportsBlits()) { + glGenRenderbuffers(1, &buffer); + glBindRenderbuffer(GL_RENDERBUFFER, buffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, m_size.width(), m_size.height()); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, buffer); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, buffer); + + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + glDeleteRenderbuffers(1, &buffer); + } else { + m_depthBuffer = buffer; + m_stencilBuffer = buffer; + return; + } + } + + // Try to attach a depth attachment separately. + GLenum depthFormat; + if (context->isOpenGLES()) { + if (context->supportsGLES24BitDepthBuffers()) { + depthFormat = GL_DEPTH_COMPONENT24; + } else { + depthFormat = GL_DEPTH_COMPONENT16; + } + } else { + depthFormat = GL_DEPTH_COMPONENT; + } + + glGenRenderbuffers(1, &buffer); + glBindRenderbuffer(GL_RENDERBUFFER, buffer); + glRenderbufferStorage(GL_RENDERBUFFER, depthFormat, m_size.width(), m_size.height()); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, buffer); + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + glDeleteRenderbuffers(1, &buffer); + } else { + m_depthBuffer = buffer; + } + + // Try to attach a stencil attachment separately. + GLenum stencilFormat; + if (context->isOpenGLES()) { + stencilFormat = GL_STENCIL_INDEX8; + } else { + stencilFormat = GL_STENCIL_INDEX; + } + + glGenRenderbuffers(1, &buffer); + glBindRenderbuffer(GL_RENDERBUFFER, buffer); + glRenderbufferStorage(GL_RENDERBUFFER, stencilFormat, m_size.width(), m_size.height()); + glFramebufferRenderbuffer(GL_RENDERBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, buffer); + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + glDeleteRenderbuffers(1, &buffer); + } else { + m_stencilBuffer = buffer; + } +} + +void GLFramebuffer::blitFromFramebuffer(const Rect &source, const Rect &destination, GLenum filter, bool flipX, bool flipY) +{ + if (!valid()) { + return; + } + + const GLFramebuffer *top = currentFramebuffer(); + if (!EglContext::currentContext()->supportsBlits()) { + const auto texture = top->colorAttachment(); + if (!texture) { + // can't do anything + return; + } + + GLFramebuffer::pushFramebuffer(this); + + QMatrix4x4 mat; + mat.ortho(QRectF(QPointF(), size())); + // GLTexture::render renders with origin (0, 0), move it to the correct place + mat.translate(destination.x(), destination.y()); + + ShaderBinder binder(ShaderTrait::MapTexture); + binder.shader()->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, mat); + + texture->render(source, Region::infinite(), destination.size(), 1); + + GLFramebuffer::popFramebuffer(); + return; + } + GLFramebuffer::pushFramebuffer(this); + + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, handle()); + glBindFramebuffer(GL_READ_FRAMEBUFFER, top->handle()); + + const Rect s = source.isNull() ? Rect(QPoint(0, 0), top->size()) : source; + const Rect d = destination.isNull() ? Rect(QPoint(0, 0), size()) : destination; + + GLuint srcX0 = s.x(); + GLuint srcY0 = top->size().height() - (s.y() + s.height()); + GLuint srcX1 = s.x() + s.width(); + GLuint srcY1 = top->size().height() - s.y(); + if (flipX) { + std::swap(srcX0, srcX1); + } + if (flipY) { + std::swap(srcY0, srcY1); + } + + const GLuint dstX0 = d.x(); + const GLuint dstY0 = m_size.height() - (d.y() + d.height()); + const GLuint dstX1 = d.x() + d.width(); + const GLuint dstY1 = m_size.height() - d.y(); + + glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, GL_COLOR_BUFFER_BIT, filter); + + GLFramebuffer::popFramebuffer(); +} + +bool GLFramebuffer::blitFromRenderTarget(const RenderTarget &sourceRenderTarget, const RenderViewport &sourceViewport, const Rect &source, const Rect &destination) +{ + OutputTransform transform = sourceRenderTarget.texture() ? sourceRenderTarget.texture()->contentTransform() : OutputTransform(); + + // TODO: Also blit if rotated 180 degrees, it's equivalent to flipping both x and y axis + const bool normal = transform == OutputTransform::Normal; + const bool mirrorX = transform == OutputTransform::FlipX; + const bool mirrorY = transform == OutputTransform::FlipY; + if ((normal || mirrorX || mirrorY) && EglContext::currentContext()->supportsBlits()) { + // either no transformation or flipping only + blitFromFramebuffer(sourceViewport.mapToRenderTarget(source), destination, GL_LINEAR, mirrorX, mirrorY); + return true; + } else { + const auto texture = sourceRenderTarget.texture(); + if (!texture) { + // rotations aren't possible without a texture + return false; + } + + GLFramebuffer::pushFramebuffer(this); + + QMatrix4x4 mat; + mat.ortho(QRectF(QPointF(), size())); + // GLTexture::render renders with origin (0, 0), move it to the correct place + mat.translate(destination.x(), destination.y()); + + ShaderBinder binder(ShaderTrait::MapTexture); + binder.shader()->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, mat); + + texture->render(sourceViewport.mapToRenderTargetTexture(source), Region::infinite(), destination.size(), 1); + + GLFramebuffer::popFramebuffer(); + return true; + } +} + +GLTexture *GLFramebuffer::colorAttachment() const +{ + return m_colorAttachment; +} + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/glframebuffer.h b/local/recipes/kde/kwin/source/src/opengl/glframebuffer.h new file mode 100644 index 0000000000..2f1b4214dd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/glframebuffer.h @@ -0,0 +1,133 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006-2007 Rivo Laks + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "kwin_export.h" + +#include "core/rect.h" + +#include +#include + +namespace KWin +{ + +class GLTexture; +class RenderTarget; +class RenderViewport; + +// Cleans up all resources hold by the GL Context +void KWIN_EXPORT cleanupGL(); + +/** + * @short OpenGL framebuffer object + * + * Framebuffer object enables you to render onto a texture. This texture can + * later be used to e.g. do post-processing of the scene. + * + * @author Rivo Laks + */ +class KWIN_EXPORT GLFramebuffer +{ +public: + enum Attachment { + NoAttachment, + CombinedDepthStencil, + }; + + /** + * Constructs a GLFramebuffer + * @since 5.13 + */ + explicit GLFramebuffer(); + + /** + * Constructs a GLFramebuffer. Note that ensuring the color attachment outlives + * the framebuffer is the responsibility of the caller. + * + * @param colorAttachment texture where the scene will be rendered onto + */ + explicit GLFramebuffer(GLTexture *colorAttachment, Attachment attachment = NoAttachment); + + /** + * Constructs a wrapper for an already created framebuffer object. The GLFramebuffer + * does not take the ownership of the framebuffer object handle. + */ + GLFramebuffer(GLuint handle, const QSize &size); + ~GLFramebuffer(); + + /** + * Returns the framebuffer object handle to this framebuffer object. + */ + GLuint handle() const + { + return m_handle; + } + /** + * Returns the size of the color attachment to this framebuffer object. + */ + QSize size() const + { + return m_size; + } + bool valid() const + { + return m_valid; + } + + /** + * Returns the last bound framebuffer, or @c null if no framebuffer is current. + */ + static GLFramebuffer *currentFramebuffer(); + + static void pushFramebuffer(GLFramebuffer *fbo); + static GLFramebuffer *popFramebuffer(); + + /** + * Blits from @a source rectangle in the current framebuffer to the @a destination rectangle in + * this framebuffer. + * + * Be aware that framebuffer blitting may not be supported on all hardware. Use blitSupported() + * to check whether it is supported. + * + * The @a source and the @a destination rectangles can have different sizes. The @a filter indicates + * what filter will be used in case scaling needs to be performed. + * + * @see blitSupported + * @since 4.8 + */ + void blitFromFramebuffer(const Rect &source = Rect(), const Rect &destination = Rect(), GLenum filter = GL_LINEAR, bool flipX = false, bool flipY = false); + + /** + * Blits from @a source rectangle in logical coordinates in the current framebuffer to the @a destination rectangle in texture-local coordinates + * in this framebuffer, taking into account any transformations the source render target may have + */ + bool blitFromRenderTarget(const RenderTarget &sourceRenderTarget, const RenderViewport &sourceViewport, const Rect &source, const Rect &destination); + + /** + * @returns the color attachment of this fbo. May be nullptr + */ + GLTexture *colorAttachment() const; + +protected: + void initColorAttachment(GLTexture *colorAttachment); + void initDepthStencilAttachment(); + bool bind(); + + GLuint m_handle = 0; + GLuint m_depthBuffer = 0; + GLuint m_stencilBuffer = 0; + QSize m_size; + bool m_valid = false; + bool m_foreign = false; + GLTexture *const m_colorAttachment; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/gllut.cpp b/local/recipes/kde/kwin/source/src/opengl/gllut.cpp new file mode 100644 index 0000000000..af7dd6ccef --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/gllut.cpp @@ -0,0 +1,78 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "gllut.h" +#include "eglcontext.h" +#include "utils/common.h" + +#include + +namespace KWin +{ + +GlLookUpTable::GlLookUpTable(GLuint handle, size_t size) + : m_handle(handle) + , m_size(size) +{ +} + +GlLookUpTable::~GlLookUpTable() +{ + if (!EglContext::currentContext()) { + qCWarning(KWIN_OPENGL, "Could not delete 1D LUT because no context is current"); + return; + } + glDeleteTextures(1, &m_handle); +} + +GLuint GlLookUpTable::handle() const +{ + return m_handle; +} + +size_t GlLookUpTable::size() const +{ + return m_size; +} + +void GlLookUpTable::bind() +{ + glBindTexture(GL_TEXTURE_2D, m_handle); +} + +std::unique_ptr GlLookUpTable::create(const std::function &func, size_t size) +{ + GLuint handle = 0; + glGenTextures(1, &handle); + if (!handle) { + return nullptr; + } + // this uses 2D textures because OpenGL ES doesn't support 1D textures + glBindTexture(GL_TEXTURE_2D, handle); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_LOD, 0); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LOD, 0); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + std::vector data; + data.reserve(4 * size); + for (size_t i = 0; i < size; i++) { + const auto color = func(i); + data.push_back(color.x()); + data.push_back(color.y()); + data.push_back(color.z()); + data.push_back(1); + } + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, size, 1, 0, GL_RGBA, GL_FLOAT, data.data()); + glBindTexture(GL_TEXTURE_2D, 0); + return std::make_unique(handle, size); +} + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/gllut.h b/local/recipes/kde/kwin/source/src/opengl/gllut.h new file mode 100644 index 0000000000..baea1c766e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/gllut.h @@ -0,0 +1,40 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "kwin_export.h" + +#include +#include +#include +#include +#include + +namespace KWin +{ + +class KWIN_EXPORT GlLookUpTable +{ +public: + explicit GlLookUpTable(GLuint handle, size_t size); + ~GlLookUpTable(); + + GLuint handle() const; + size_t size() const; + + void bind(); + + static std::unique_ptr create(const std::function &func, size_t size); + +private: + const GLuint m_handle; + const size_t m_size; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/gllut3D.cpp b/local/recipes/kde/kwin/source/src/opengl/gllut3D.cpp new file mode 100644 index 0000000000..b1139f4577 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/gllut3D.cpp @@ -0,0 +1,94 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "gllut3D.h" +#include "eglcontext.h" +#include "utils/common.h" + +#include + +namespace KWin +{ + +GlLookUpTable3D::GlLookUpTable3D(GLuint handle, size_t xSize, size_t ySize, size_t zSize) + : m_handle(handle) + , m_xSize(xSize) + , m_ySize(ySize) + , m_zSize(zSize) +{ +} + +GlLookUpTable3D::~GlLookUpTable3D() +{ + if (!EglContext::currentContext()) { + qCWarning(KWIN_OPENGL, "Could not delete 3D LUT because no context is current"); + return; + } + glDeleteTextures(1, &m_handle); +} + +GLuint GlLookUpTable3D::handle() const +{ + return m_handle; +} + +size_t GlLookUpTable3D::xSize() const +{ + return m_xSize; +} + +size_t GlLookUpTable3D::ySize() const +{ + return m_ySize; +} + +size_t GlLookUpTable3D::zSize() const +{ + return m_zSize; +} + +void GlLookUpTable3D::bind() +{ + glBindTexture(GL_TEXTURE_3D, m_handle); +} + +std::unique_ptr GlLookUpTable3D::create(const std::function &mapping, size_t xSize, size_t ySize, size_t zSize) +{ + GLuint handle = 0; + glGenTextures(1, &handle); + if (!handle) { + return nullptr; + } + glBindTexture(GL_TEXTURE_3D, handle); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAX_LEVEL, 0); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_LOD, 0); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAX_LOD, 0); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + QVector data; + data.reserve(4 * xSize * ySize * zSize); + for (size_t z = 0; z < zSize; z++) { + for (size_t y = 0; y < ySize; y++) { + for (size_t x = 0; x < xSize; x++) { + const auto color = mapping(x, y, z); + data.push_back(color.x()); + data.push_back(color.y()); + data.push_back(color.z()); + data.push_back(1); + } + } + } + glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA16F, xSize, ySize, zSize, 0, GL_RGBA, GL_FLOAT, data.data()); + glBindTexture(GL_TEXTURE_3D, 0); + return std::make_unique(handle, xSize, ySize, zSize); +} + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/gllut3D.h b/local/recipes/kde/kwin/source/src/opengl/gllut3D.h new file mode 100644 index 0000000000..569c1040c9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/gllut3D.h @@ -0,0 +1,44 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "kwin_export.h" + +#include +#include +#include +#include +#include + +namespace KWin +{ + +class KWIN_EXPORT GlLookUpTable3D +{ +public: + explicit GlLookUpTable3D(GLuint handle, size_t xSize, size_t ySize, size_t zSize); + ~GlLookUpTable3D(); + + GLuint handle() const; + size_t xSize() const; + size_t ySize() const; + size_t zSize() const; + + void bind(); + + static std::unique_ptr create(const std::function &mapping, size_t xSize, size_t ySize, size_t zSize); + +private: + const GLuint m_handle; + const size_t m_xSize; + const size_t m_ySize; + const size_t m_zSize; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/glplatform.cpp b/local/recipes/kde/kwin/source/src/opengl/glplatform.cpp new file mode 100644 index 0000000000..37043cc311 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/glplatform.cpp @@ -0,0 +1,1131 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "opengl/glplatform.h" +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace KWin +{ + +// Extracts the portion of a string that matches a regular expression +static QString extract(const QString &text, const QString &pattern) +{ + const QRegularExpression regexp(pattern); + const QRegularExpressionMatch match = regexp.match(text); + if (!match.hasMatch()) { + return QString(); + } + return match.captured(); +} + +static ChipClass detectRadeonClass(QByteArrayView chipset) +{ + if (chipset.isEmpty()) { + return UnknownRadeon; + } + + if (chipset.contains("R100") + || chipset.contains("RV100") + || chipset.contains("RS100")) { + return R100; + } + + if (chipset.contains("RV200") + || chipset.contains("RS200") + || chipset.contains("R200") + || chipset.contains("RV250") + || chipset.contains("RS300") + || chipset.contains("RV280")) { + return R200; + } + + if (chipset.contains("R300") + || chipset.contains("R350") + || chipset.contains("R360") + || chipset.contains("RV350") + || chipset.contains("RV370") + || chipset.contains("RV380")) { + return R300; + } + + if (chipset.contains("R420") + || chipset.contains("R423") + || chipset.contains("R430") + || chipset.contains("R480") + || chipset.contains("R481") + || chipset.contains("RV410") + || chipset.contains("RS400") + || chipset.contains("RC410") + || chipset.contains("RS480") + || chipset.contains("RS482") + || chipset.contains("RS600") + || chipset.contains("RS690") + || chipset.contains("RS740")) { + return R400; + } + + if (chipset.contains("RV515") + || chipset.contains("R520") + || chipset.contains("RV530") + || chipset.contains("R580") + || chipset.contains("RV560") + || chipset.contains("RV570")) { + return R500; + } + + if (chipset.contains("R600") + || chipset.contains("RV610") + || chipset.contains("RV630") + || chipset.contains("RV670") + || chipset.contains("RV620") + || chipset.contains("RV635") + || chipset.contains("RS780") + || chipset.contains("RS880")) { + return R600; + } + + if (chipset.contains("R700") + || chipset.contains("RV770") + || chipset.contains("RV730") + || chipset.contains("RV710") + || chipset.contains("RV740")) { + return R700; + } + + if (chipset.contains("EVERGREEN") // Not an actual chipset, but returned by R600G in 7.9 + || chipset.contains("CEDAR") + || chipset.contains("REDWOOD") + || chipset.contains("JUNIPER") + || chipset.contains("CYPRESS") + || chipset.contains("HEMLOCK") + || chipset.contains("PALM")) { + return Evergreen; + } + + if (chipset.contains("SUMO") + || chipset.contains("SUMO2") + || chipset.contains("BARTS") + || chipset.contains("TURKS") + || chipset.contains("CAICOS") + || chipset.contains("CAYMAN")) { + return NorthernIslands; + } + + if (chipset.contains("TAHITI") + || chipset.contains("PITCAIRN") + || chipset.contains("VERDE") + || chipset.contains("OLAND") + || chipset.contains("HAINAN")) { + return SouthernIslands; + } + + if (chipset.contains("BONAIRE") + || chipset.contains("KAVERI") + || chipset.contains("KABINI") + || chipset.contains("HAWAII") + || chipset.contains("MULLINS")) { + return SeaIslands; + } + + if (chipset.contains("TONGA") + || chipset.contains("TOPAZ") + || chipset.contains("FIJI") + || chipset.contains("CARRIZO") + || chipset.contains("STONEY")) { + return VolcanicIslands; + } + + if (chipset.contains("POLARIS10") + || chipset.contains("POLARIS11") + || chipset.contains("POLARIS12") + || chipset.contains("VEGAM")) { + return ArcticIslands; + } + + if (chipset.contains("VEGA10") + || chipset.contains("VEGA12") + || chipset.contains("VEGA20") + || chipset.contains("RAVEN") + || chipset.contains("RAVEN2") + || chipset.contains("RENOIR") + || chipset.contains("ARCTURUS")) { + return Vega; + } + + if (chipset.contains("NAVI10") + || chipset.contains("NAVI12") + || chipset.contains("NAVI14")) { + return Navi; + } + + const QString chipset16 = QString::fromLatin1(chipset); + QString name = extract(chipset16, QStringLiteral("HD [0-9]{4}")); // HD followed by a space and 4 digits + if (!name.isEmpty()) { + const int id = QStringView(name).right(4).toInt(); + if (id == 6250 || id == 6310) { // Palm + return Evergreen; + } + + if (id >= 6000 && id < 7000) { + return NorthernIslands; // HD 6xxx + } + + if (id >= 5000 && id < 6000) { + return Evergreen; // HD 5xxx + } + + if (id >= 4000 && id < 5000) { + return R700; // HD 4xxx + } + + if (id >= 2000 && id < 4000) { // HD 2xxx/3xxx + return R600; + } + + return UnknownRadeon; + } + + name = extract(chipset16, QStringLiteral("X[0-9]{3,4}")); // X followed by 3-4 digits + if (!name.isEmpty()) { + const int id = QStringView(name).mid(1, -1).toInt(); + + // X1xxx + if (id >= 1300) { + return R500; + } + + // X7xx, X8xx, X12xx, 2100 + if ((id >= 700 && id < 1000) || id >= 1200) { + return R400; + } + + // X200, X3xx, X5xx, X6xx, X10xx, X11xx + if ((id >= 300 && id < 700) || (id >= 1000 && id < 1200)) { + return R300; + } + + return UnknownRadeon; + } + + name = extract(chipset16, QStringLiteral("\\b[0-9]{4}\\b")); // A group of 4 digits + if (!name.isEmpty()) { + const int id = name.toInt(); + + // 7xxx + if (id >= 7000 && id < 8000) { + return R100; + } + + // 8xxx, 9xxx + if (id >= 8000 && id < 9500) { + return R200; + } + + // 9xxx + if (id >= 9500) { + return R300; + } + + if (id == 2100) { + return R400; + } + } + + return UnknownRadeon; +} + +static ChipClass detectNVidiaClass(const QString &chipset) +{ + QString name = extract(chipset, QStringLiteral("\\bNV[0-9,A-F]{2}\\b")); // NV followed by two hexadecimal digits + if (!name.isEmpty()) { + const int id = QStringView(chipset).mid(2, -1).toInt(nullptr, 16); // Strip the 'NV' from the id + + switch (id & 0xf0) { + case 0x00: + case 0x10: + return NV10; + + case 0x20: + return NV20; + + case 0x30: + return NV30; + + case 0x40: + case 0x60: + return NV40; + + case 0x50: + case 0x80: + case 0x90: + case 0xA0: + return G80; + + default: + return UnknownNVidia; + } + } + + if (chipset.contains(QLatin1String("GeForce2")) || chipset.contains(QLatin1String("GeForce 256"))) { + return NV10; + } + + if (chipset.contains(QLatin1String("GeForce3"))) { + return NV20; + } + + if (chipset.contains(QLatin1String("GeForce4"))) { + if (chipset.contains(QLatin1String("MX 420")) + || chipset.contains(QLatin1String("MX 440")) // including MX 440SE + || chipset.contains(QLatin1String("MX 460")) + || chipset.contains(QLatin1String("MX 4000")) + || chipset.contains(QLatin1String("PCX 4300"))) { + return NV10; + } + + return NV20; + } + + // GeForce 5,6,7,8,9 + name = extract(chipset, QStringLiteral("GeForce (FX |PCX |Go )?\\d{4}(M|\\b)")).trimmed(); + if (!name.isEmpty()) { + if (!name[name.length() - 1].isDigit()) { + name.chop(1); + } + + const int id = QStringView(name).right(4).toInt(); + if (id < 6000) { + return NV30; + } + + if (id >= 6000 && id < 8000) { + return NV40; + } + + if (id >= 8000) { + return G80; + } + + return UnknownNVidia; + } + + // GeForce 100/200/300/400/500 + name = extract(chipset, QStringLiteral("GeForce (G |GT |GTX |GTS )?\\d{3}(M|\\b)")).trimmed(); + if (!name.isEmpty()) { + if (!name[name.length() - 1].isDigit()) { + name.chop(1); + } + + const int id = QStringView(name).right(3).toInt(); + if (id >= 100 && id < 600) { + if (id >= 400) { + return GF100; + } + + return G80; + } + return UnknownNVidia; + } + + return UnknownNVidia; +} +static inline ChipClass detectNVidiaClass(QByteArrayView chipset) +{ + return detectNVidiaClass(QString::fromLatin1(chipset)); +} + +static ChipClass detectIntelClass(QByteArrayView chipset) +{ + // see mesa repository: src/mesa/drivers/dri/intel/intel_context.c + // GL 1.3, DX8? SM ? + if (chipset.contains("845G") + || chipset.contains("830M") + || chipset.contains("852GM/855GM") + || chipset.contains("865G")) { + return I8XX; + } + + // GL 1.4, DX 9.0, SM 2.0 + if (chipset.contains("915G") + || chipset.contains("E7221G") + || chipset.contains("915GM") + || chipset.contains("945G") // DX 9.0c + || chipset.contains("945GM") + || chipset.contains("945GME") + || chipset.contains("Q33") // GL1.5 + || chipset.contains("Q35") + || chipset.contains("G33") + || chipset.contains("965Q") // GMA 3000, but apparently considered gen 4 by the driver + || chipset.contains("946GZ") // GMA 3000, but apparently considered gen 4 by the driver + || chipset.contains("IGD")) { + return I915; + } + + // GL 2.0, DX 9.0c, SM 3.0 + if (chipset.contains("965G") + || chipset.contains("G45/G43") // SM 4.0 + || chipset.contains("965GM") // GL 2.1 + || chipset.contains("965GME/GLE") + || chipset.contains("GM45") + || chipset.contains("Q45/Q43") + || chipset.contains("G41") + || chipset.contains("B43") + || chipset.contains("Ironlake")) { + return I965; + } + + // GL 3.1, CL 1.1, DX 10.1 + if (chipset.contains("Sandybridge") || chipset.contains("SNB GT")) { + return SandyBridge; + } + + // GL4.0, CL1.1, DX11, SM 5.0 + if (chipset.contains("Ivybridge") || chipset.contains("IVB GT")) { + return IvyBridge; + } + + // GL4.0, CL1.2, DX11.1, SM 5.0 + if (chipset.contains("Haswell") || chipset.contains("HSW GT")) { + return Haswell; + } + if (chipset.contains("BYT")) { + return BayTrail; + } + if (chipset.contains("CHV") || chipset.contains("BSW")) { + return Cherryview; + } + if (chipset.contains("BDW GT")) { + return Broadwell; + } + if (chipset.contains("SKL GT")) { + return Skylake; + } + if (chipset.contains("APL")) { + return ApolloLake; + } + if (chipset.contains("KBL GT")) { + return KabyLake; + } + if (chipset.contains("WHL GT")) { + return WhiskeyLake; + } + if (chipset.contains("CML GT")) { + return CometLake; + } + if (chipset.contains("CNL GT")) { + return CannonLake; + } + if (chipset.contains("CFL GT")) { + return CoffeeLake; + } + if (chipset.contains("ICL GT")) { + return IceLake; + } + if (chipset.contains("TGL GT")) { + return TigerLake; + } + + return UnknownIntel; +} + +static ChipClass detectQualcommClass(QByteArrayView chipClass) +{ + if (!chipClass.contains("Adreno")) { + return UnknownChipClass; + } + const auto parts = chipClass.toByteArray().split(' '); + if (parts.count() < 3) { + return UnknownAdreno; + } + bool ok = false; + const int value = parts.at(2).toInt(&ok); + if (ok) { + if (value >= 100 && value < 200) { + return Adreno1XX; + } else if (value >= 200 && value < 300) { + return Adreno2XX; + } else if (value >= 300 && value < 400) { + return Adreno3XX; + } else if (value >= 400 && value < 500) { + return Adreno4XX; + } else if (value >= 500 && value < 600) { + return Adreno5XX; + } + } + return UnknownAdreno; +} + +static ChipClass detectPanfrostClass(QByteArrayView chipClass) +{ + // Keep the list of supported Mali chipset up to date with https://docs.mesa3d.org/drivers/panfrost.html + if (chipClass.contains("T720") || chipClass.contains("T760")) { + return MaliT7XX; + } + + if (chipClass.contains("T820") || chipClass.contains("T830") || chipClass.contains("T860") || chipClass.contains("T880")) { + return MaliT8XX; + } + + if (chipClass.contains("G31") || chipClass.contains("G51") || chipClass.contains("G52") || chipClass.contains("G57") || chipClass.contains("G72") || chipClass.contains("G76")) { + return MaliGXX; + } + + return UnknownPanfrost; +} + +static ChipClass detectLimaClass(QByteArrayView chipClass) +{ + if (chipClass.contains("400")) { + return Mali400; + } else if (chipClass.contains("450")) { + return Mali450; + } else if (chipClass.contains("470")) { + return Mali470; + } + + return UnknownLima; +} + +static ChipClass detectVC4Class(QByteArrayView chipClass) +{ + if (chipClass.contains("2.1")) { + return VC4_2_1; + } + + return UnknownVideoCore4; +} + +static ChipClass detectV3DClass(QByteArrayView chipClass) +{ + if (chipClass.contains("4.2")) { + return V3D_4_2; + } + + return UnknownVideoCore3D; +} + +QString GLPlatform::driverToString(Driver driver) +{ + return QString::fromLatin1(driverToString8(driver)); +} +QByteArray GLPlatform::driverToString8(Driver driver) +{ + switch (driver) { + case Driver_R100: + return QByteArrayLiteral("Radeon"); + case Driver_R200: + return QByteArrayLiteral("R200"); + case Driver_R300C: + return QByteArrayLiteral("R300C"); + case Driver_R300G: + return QByteArrayLiteral("R300G"); + case Driver_R600C: + return QByteArrayLiteral("R600C"); + case Driver_R600G: + return QByteArrayLiteral("R600G"); + case Driver_RadeonSI: + return QByteArrayLiteral("RadeonSI"); + case Driver_Nouveau: + return QByteArrayLiteral("Nouveau"); + case Driver_Intel: + return QByteArrayLiteral("Intel"); + case Driver_NVidia: + return QByteArrayLiteral("NVIDIA"); + case Driver_Catalyst: + return QByteArrayLiteral("Catalyst"); + case Driver_Swrast: + return QByteArrayLiteral("Software rasterizer"); + case Driver_Softpipe: + return QByteArrayLiteral("softpipe"); + case Driver_Llvmpipe: + return QByteArrayLiteral("LLVMpipe"); + case Driver_VirtualBox: + return QByteArrayLiteral("VirtualBox (Chromium)"); + case Driver_VMware: + return QByteArrayLiteral("VMware (SVGA3D)"); + case Driver_Qualcomm: + return QByteArrayLiteral("Qualcomm"); + case Driver_Virgl: + return QByteArrayLiteral("Virgl (virtio-gpu, Qemu/KVM guest)"); + case Driver_Panfrost: + return QByteArrayLiteral("Panfrost"); + case Driver_Lima: + return QByteArrayLiteral("Mali (Lima)"); + case Driver_VC4: + return QByteArrayLiteral("VideoCore IV"); + case Driver_V3D: + return QByteArrayLiteral("VideoCore 3D"); + + default: + return QByteArrayLiteral("Unknown"); + } +} + +QString GLPlatform::chipClassToString(ChipClass chipClass) +{ + return QString::fromLatin1(chipClassToString8(chipClass)); +} +QByteArray GLPlatform::chipClassToString8(ChipClass chipClass) +{ + switch (chipClass) { + case R100: + return QByteArrayLiteral("R100"); + case R200: + return QByteArrayLiteral("R200"); + case R300: + return QByteArrayLiteral("R300"); + case R400: + return QByteArrayLiteral("R400"); + case R500: + return QByteArrayLiteral("R500"); + case R600: + return QByteArrayLiteral("R600"); + case R700: + return QByteArrayLiteral("R700"); + case Evergreen: + return QByteArrayLiteral("EVERGREEN"); + case NorthernIslands: + return QByteArrayLiteral("Northern Islands"); + case SouthernIslands: + return QByteArrayLiteral("Southern Islands"); + case SeaIslands: + return QByteArrayLiteral("Sea Islands"); + case VolcanicIslands: + return QByteArrayLiteral("Volcanic Islands"); + case ArcticIslands: + return QByteArrayLiteral("Arctic Islands"); + case Vega: + return QByteArrayLiteral("Vega"); + case Navi: + return QByteArrayLiteral("Navi"); + + case NV10: + return QByteArrayLiteral("NV10"); + case NV20: + return QByteArrayLiteral("NV20"); + case NV30: + return QByteArrayLiteral("NV30"); + case NV40: + return QByteArrayLiteral("NV40/G70"); + case G80: + return QByteArrayLiteral("G80/G90"); + case GF100: + return QByteArrayLiteral("GF100"); + + case I8XX: + return QByteArrayLiteral("i830/i835"); + case I915: + return QByteArrayLiteral("i915/i945"); + case I965: + return QByteArrayLiteral("i965"); + case SandyBridge: + return QByteArrayLiteral("SandyBridge"); + case IvyBridge: + return QByteArrayLiteral("IvyBridge"); + case Haswell: + return QByteArrayLiteral("Haswell"); + case BayTrail: + return QByteArrayLiteral("Bay Trail"); + case Cherryview: + return QByteArrayLiteral("Cherryview"); + case Broadwell: + return QByteArrayLiteral("Broadwell"); + case ApolloLake: + return QByteArrayLiteral("Apollo Lake"); + case Skylake: + return QByteArrayLiteral("Skylake"); + case GeminiLake: + return QByteArrayLiteral("Gemini Lake"); + case KabyLake: + return QByteArrayLiteral("Kaby Lake"); + case CoffeeLake: + return QByteArrayLiteral("Coffee Lake"); + case WhiskeyLake: + return QByteArrayLiteral("Whiskey Lake"); + case CometLake: + return QByteArrayLiteral("Comet Lake"); + case CannonLake: + return QByteArrayLiteral("Cannon Lake"); + case IceLake: + return QByteArrayLiteral("Ice Lake"); + case TigerLake: + return QByteArrayLiteral("Tiger Lake"); + + case Adreno1XX: + return QByteArrayLiteral("Adreno 1xx series"); + case Adreno2XX: + return QByteArrayLiteral("Adreno 2xx series"); + case Adreno3XX: + return QByteArrayLiteral("Adreno 3xx series"); + case Adreno4XX: + return QByteArrayLiteral("Adreno 4xx series"); + case Adreno5XX: + return QByteArrayLiteral("Adreno 5xx series"); + + case Mali400: + return QByteArrayLiteral("Mali 400 series"); + case Mali450: + return QByteArrayLiteral("Mali 450 series"); + case Mali470: + return QByteArrayLiteral("Mali 470 series"); + + case MaliT7XX: + return QByteArrayLiteral("Mali T7xx series"); + case MaliT8XX: + return QByteArrayLiteral("Mali T8xx series"); + case MaliGXX: + return QByteArrayLiteral("Mali Gxx series"); + + case VC4_2_1: + return QByteArrayLiteral("VideoCore IV"); + case V3D_4_2: + return QByteArrayLiteral("VideoCore 3D"); + + default: + return QByteArrayLiteral("Unknown"); + } +} + +GLPlatform::GLPlatform(QByteArrayView openglVersionString, QByteArrayView glslVersionString, QByteArrayView renderer, QByteArrayView vendor) + : m_openglVersionString(openglVersionString) + , m_glslVersionString(glslVersionString) + , m_rendererString(renderer) + , m_vendorString(vendor) + , m_openglVersion(Version::parseString(openglVersionString)) + , m_glslVersion(Version::parseString(m_glslVersionString)) +{ + // Parse the Mesa version + const auto versionTokens = openglVersionString.toByteArray().split(' '); + const int mesaIndex = versionTokens.indexOf("Mesa"); + if (mesaIndex != -1) { + m_mesaVersion = Version::parseString(versionTokens.at(mesaIndex + 1)); + } + + m_preferBufferSubData = false; + + // Mesa classic drivers + // ==================================================== + + // Radeon + if (renderer.startsWith("Mesa DRI R")) { + // Sample renderer string: Mesa DRI R600 (RV740 94B3) 20090101 x86/MMX/SSE2 TCL DRI2 + const QList tokens = renderer.toByteArray().split(' '); + const QByteArray &chipClass = tokens.at(2); + m_chipset = tokens.at(3).mid(1, -1); // Strip the leading '(' + + if (chipClass == "R100") { + // Vendor: Tungsten Graphics, Inc. + m_driver = Driver_R100; + + } else if (chipClass == "R200") { + // Vendor: Tungsten Graphics, Inc. + m_driver = Driver_R200; + + } else if (chipClass == "R300") { + // Vendor: DRI R300 Project + m_driver = Driver_R300C; + + } else if (chipClass == "R600") { + // Vendor: Advanced Micro Devices, Inc. + m_driver = Driver_R600C; + } + + m_chipClass = detectRadeonClass(m_chipset); + } + + // Intel + else if (renderer.contains("Intel")) { + // Vendor: Tungsten Graphics, Inc. + // Sample renderer string: Mesa DRI Mobile Intel® GM45 Express Chipset GEM 20100328 2010Q1 + + QByteArrayView chipset; + if (renderer.startsWith("Intel(R) Integrated Graphics Device")) { + chipset = "IGD"; + } else { + chipset = renderer; + } + + m_driver = Driver_Intel; + m_chipClass = detectIntelClass(chipset); + } + + // Properietary drivers + // ==================================================== + else if (vendor == "ATI Technologies Inc.") { + m_chipClass = detectRadeonClass(renderer); + m_driver = Driver_Catalyst; + + if (versionTokens.count() > 1 && versionTokens.at(2)[0] == '(') { + m_driverVersion = Version::parseString(versionTokens.at(1)); + } else if (versionTokens.count() > 0) { + m_driverVersion = Version::parseString(versionTokens.at(0)); + } else { + m_driverVersion = Version(0, 0, 0); + } + } + + else if (vendor == "NVIDIA Corporation") { + m_chipClass = detectNVidiaClass(renderer); + m_driver = Driver_NVidia; + + int index = versionTokens.indexOf("NVIDIA"); + if (versionTokens.count() > index) { + m_driverVersion = Version::parseString(versionTokens.at(index + 1)); + } else { + m_driverVersion = Version(0, 0, 0); + } + } + + else if (vendor == "Qualcomm") { + m_driver = Driver_Qualcomm; + m_chipClass = detectQualcommClass(renderer); + } + + else if (renderer.contains("Panfrost")) { + m_driver = Driver_Panfrost; + m_chipClass = detectPanfrostClass(renderer); + } + + else if (renderer.contains("Mali")) { + m_driver = Driver_Lima; + m_chipClass = detectLimaClass(renderer); + } + + else if (renderer.startsWith("VC4 ")) { + m_driver = Driver_VC4; + m_chipClass = detectVC4Class(renderer); + } + + else if (renderer.startsWith("V3D ")) { + m_driver = Driver_V3D; + m_chipClass = detectV3DClass(renderer); + } + + else if (renderer == "Software Rasterizer") { + m_driver = Driver_Swrast; + } + + // Virtual Hardware + // ==================================================== + else if (vendor == "Humper" && renderer == "Chromium") { + // Virtual Box + m_driver = Driver_VirtualBox; + + const int index = versionTokens.indexOf("Chromium"); + if (versionTokens.count() > index) { + m_driverVersion = Version::parseString(versionTokens.at(index + 1)); + } else { + m_driverVersion = Version(0, 0, 0); + } + } + + // Gallium drivers + // ==================================================== + else { + const QList tokens = renderer.toByteArray().split(' '); + if (renderer.contains("Gallium")) { + // Sample renderer string: Gallium 0.4 on AMD RV740 + m_chipset = (tokens.at(3) == "AMD" || tokens.at(3) == "ATI") ? tokens.at(4) : tokens.at(3); + } else { + // The renderer string does not contain "Gallium" anymore. + m_chipset = tokens.at(0); + } + + // R300G + if (vendor == QByteArrayLiteral("X.Org R300 Project")) { + m_chipClass = detectRadeonClass(m_chipset); + m_driver = Driver_R300G; + } + + // R600G + else if (vendor == "X.Org" && (renderer.contains("R6") || renderer.contains("R7") || renderer.contains("RV6") || renderer.contains("RV7") || renderer.contains("RS780") || renderer.contains("RS880") || renderer.contains("CEDAR") || renderer.contains("REDWOOD") || renderer.contains("JUNIPER") || renderer.contains("CYPRESS") || renderer.contains("HEMLOCK") || renderer.contains("PALM") || renderer.contains("EVERGREEN") || renderer.contains("SUMO") || renderer.contains("SUMO2") || renderer.contains("BARTS") || renderer.contains("TURKS") || renderer.contains("CAICOS") || renderer.contains("CAYMAN"))) { + m_chipClass = detectRadeonClass(m_chipset); + m_driver = Driver_R600G; + } + + // RadeonSI + else if ((vendor == "X.Org" || vendor == "AMD") && (renderer.contains("TAHITI") || renderer.contains("PITCAIRN") || renderer.contains("VERDE") || renderer.contains("OLAND") || renderer.contains("HAINAN") || renderer.contains("BONAIRE") || renderer.contains("KAVERI") || renderer.contains("KABINI") || renderer.contains("HAWAII") || renderer.contains("MULLINS") || renderer.contains("TOPAZ") || renderer.contains("TONGA") || renderer.contains("FIJI") || renderer.contains("CARRIZO") || renderer.contains("STONEY") || renderer.contains("POLARIS10") || renderer.contains("POLARIS11") || renderer.contains("POLARIS12") || renderer.contains("VEGAM") || renderer.contains("VEGA10") || renderer.contains("VEGA12") || renderer.contains("VEGA20") || renderer.contains("RAVEN") || renderer.contains("RAVEN2") || renderer.contains("RENOIR") || renderer.contains("ARCTURUS") || renderer.contains("NAVI10") || renderer.contains("NAVI12") || renderer.contains("NAVI14"))) { + m_chipClass = detectRadeonClass(renderer); + m_driver = Driver_RadeonSI; + } + + // Nouveau + else if (vendor == "nouveau") { + m_chipClass = detectNVidiaClass(m_chipset); + m_driver = Driver_Nouveau; + } + + // softpipe + else if (m_chipset == "softpipe") { + m_driver = Driver_Softpipe; + } + + // llvmpipe + else if (m_chipset == "llvmpipe") { + m_driver = Driver_Llvmpipe; + } + + // SVGA3D + else if (vendor == "VMware, Inc." && m_chipset.contains("SVGA3D")) { + m_driver = Driver_VMware; + } + + // virgl + else if (renderer == "virgl") { + m_driver = Driver_Virgl; + } + } + + // Driver/GPU specific features + // ==================================================== + if (isRadeon()) { + if (m_chipClass < R300) { + // fallback to NoCompositing for R100 and R200 + m_recommendedCompositor = NoCompositing; + } else if (m_chipClass < R600) { + // NoCompositing due to NPOT limitations not supported by KWin's shaders + m_recommendedCompositor = NoCompositing; + } else { + m_recommendedCompositor = OpenGLCompositing; + } + + if (driver() == Driver_R600G || (driver() == Driver_R600C && renderer.contains("DRI2"))) { + m_looseBinding = true; + } + } + + if (isNvidia()) { + if (m_driver == Driver_NVidia) { + m_looseBinding = true; + m_preferBufferSubData = true; + } + + if (m_chipClass < NV40) { + m_recommendedCompositor = NoCompositing; + } else { + m_recommendedCompositor = OpenGLCompositing; + } + } + + if (isIntel()) { + // see https://bugs.freedesktop.org/show_bug.cgi?id=80349#c1 + m_looseBinding = false; + + if (m_chipClass < I915) { + m_recommendedCompositor = NoCompositing; + } else { + m_recommendedCompositor = OpenGLCompositing; + } + } + + if (isPanfrost()) { + m_recommendedCompositor = OpenGLCompositing; + } + + if (isLima()) { + m_recommendedCompositor = OpenGLCompositing; + } + + if (isVideoCore4()) { + // OpenGL works, but is much slower than QPainter + m_recommendedCompositor = QPainterCompositing; + } + + if (isVideoCore3D()) { + // OpenGL works, but is much slower than QPainter + m_recommendedCompositor = QPainterCompositing; + } + + if (isMesaDriver()) { + // According to the reference implementation in + // mesa/demos/src/egl/opengles1/texture_from_pixmap + // the mesa egl implementation does not require a strict binding (so far). + m_looseBinding = true; + } + + if (m_driver == Driver_Qualcomm) { + if (m_chipClass == Adreno1XX) { + m_recommendedCompositor = NoCompositing; + } else { + // all other drivers support at least GLES 2 + m_recommendedCompositor = OpenGLCompositing; + } + } + + if (m_chipClass == UnknownChipClass && m_driver == Driver_Unknown) { + // we don't know the hardware. Let's be optimistic and assume OpenGL compatible hardware + m_recommendedCompositor = OpenGLCompositing; + } + + if (isVirtualBox()) { + m_virtualMachine = true; + m_recommendedCompositor = OpenGLCompositing; + } + + if (isVMware()) { + m_virtualMachine = true; + m_recommendedCompositor = OpenGLCompositing; + } + + if (m_driver == Driver_Virgl) { + m_virtualMachine = true; + m_recommendedCompositor = OpenGLCompositing; + } +} + +GLPlatform::~GLPlatform() +{ +} + +Version GLPlatform::glVersion() const +{ + return m_openglVersion; +} + +Version GLPlatform::glslVersion() const +{ + return m_glslVersion; +} + +Version GLPlatform::mesaVersion() const +{ + return m_mesaVersion; +} + +Version GLPlatform::driverVersion() const +{ + if (isMesaDriver()) { + return mesaVersion(); + } + + return m_driverVersion; +} + +Driver GLPlatform::driver() const +{ + return m_driver; +} + +ChipClass GLPlatform::chipClass() const +{ + return m_chipClass; +} + +bool GLPlatform::isMesaDriver() const +{ + return mesaVersion().isValid(); +} + +bool GLPlatform::isRadeon() const +{ + return m_chipClass >= R100 && m_chipClass <= UnknownRadeon; +} + +bool GLPlatform::isNvidia() const +{ + return m_chipClass >= NV10 && m_chipClass <= UnknownNVidia; +} + +bool GLPlatform::isIntel() const +{ + return m_chipClass >= I8XX && m_chipClass <= UnknownIntel; +} + +bool GLPlatform::isVirtualBox() const +{ + return m_driver == Driver_VirtualBox; +} + +bool GLPlatform::isVMware() const +{ + return m_driver == Driver_VMware; +} + +bool GLPlatform::isVirgl() const +{ + return m_driver == Driver_Virgl; +} + +bool GLPlatform::isAdreno() const +{ + return m_chipClass >= Adreno1XX && m_chipClass <= UnknownAdreno; +} + +bool GLPlatform::isPanfrost() const +{ + return m_chipClass >= MaliT7XX && m_chipClass <= UnknownPanfrost; +} + +bool GLPlatform::isLima() const +{ + return m_chipClass >= Mali400 && m_chipClass <= UnknownLima; +} + +bool GLPlatform::isVideoCore4() const +{ + return m_chipClass >= VC4_2_1 && m_chipClass <= UnknownVideoCore4; +} + +bool GLPlatform::isVideoCore3D() const +{ + return m_chipClass >= V3D_4_2 && m_chipClass <= UnknownVideoCore3D; +} + +QByteArrayView GLPlatform::glRendererString() const +{ + return m_rendererString; +} + +QByteArrayView GLPlatform::glVendorString() const +{ + return m_vendorString; +} + +QByteArrayView GLPlatform::glVersionString() const +{ + return m_openglVersionString; +} + +QByteArrayView GLPlatform::glShadingLanguageVersionString() const +{ + return m_glslVersionString; +} + +bool GLPlatform::isLooseBinding() const +{ + return m_looseBinding; +} + +bool GLPlatform::isVirtualMachine() const +{ + return m_virtualMachine; +} + +CompositingType GLPlatform::recommendedCompositor() const +{ + return m_recommendedCompositor; +} + +bool GLPlatform::preferBufferSubData() const +{ + return m_preferBufferSubData; +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/opengl/glplatform.h b/local/recipes/kde/kwin/source/src/opengl/glplatform.h new file mode 100644 index 0000000000..81625ed6b9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/glplatform.h @@ -0,0 +1,335 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/globals.h" +#include "utils/version.h" + +#include +#include +#include + +namespace KWin +{ +// forward declare method +void cleanupGL(); + +class Version; + +enum Driver { + Driver_R100, // Technically "Radeon" + Driver_R200, + Driver_R300C, + Driver_R300G, + Driver_R600C, + Driver_R600G, + Driver_Nouveau, + Driver_Intel, + Driver_NVidia, + Driver_Catalyst, + Driver_Swrast, + Driver_Softpipe, + Driver_Llvmpipe, + Driver_VirtualBox, + Driver_VMware, + Driver_Qualcomm, + Driver_RadeonSI, + Driver_Virgl, + Driver_Panfrost, + Driver_Lima, + Driver_VC4, + Driver_V3D, + Driver_Unknown, +}; + +// clang-format off +enum ChipClass { + // Radeon + R100 = 0, // GL1.3 DX7 2000 + R200, // GL1.4 DX8.1 SM 1.4 2001 + R300, // GL2.0 DX9 SM 2.0 2002 + R400, // GL2.0 DX9b SM 2.0b 2004 + R500, // GL2.0 DX9c SM 3.0 2005 + R600, // GL3.3 DX10 SM 4.0 2006 + R700, // GL3.3 DX10.1 SM 4.1 2008 + Evergreen, // GL4.0 CL1.0 DX11 SM 5.0 2009 + NorthernIslands, // GL4.0 CL1.1 DX11 SM 5.0 2010 + SouthernIslands, // GL4.5 CL1.2 DX11.1 SM 5.1 2012 + SeaIslands, // GL4.5 CL2.0 DX12 SM 6.0 2013 + VolcanicIslands, // GL4.5 CL2.0 DX12 SM 6.0 2015 + ArcticIslands, // GL4.5 CL2.0 DX12 SM 6.0 2016 + Vega, // GL4.6 CL2.0 DX12 SM 6.0 2017 + Navi, // GL4.6 CL2.0 DX12.1 SM 6.4 2019 + UnknownRadeon = 999, + + // NVIDIA + NV10 = 1000, // GL1.2 DX7 1999 + NV20, // GL1.3 DX8 SM 1.1 2001 + NV30, // GL1.5 DX9a SM 2.0 2003 + NV40, // GL2.1 DX9c SM 3.0 2004 + G80, // GL3.3 DX10 SM 4.0 2006 + GF100, // GL4.1 CL1.1 DX11 SM 5.0 2010 + UnknownNVidia = 1999, + + // Intel + I8XX = 2000, // GL1.3 DX7 2001 + I915, // GL1.4/1.5 DX9/DX9c SM 2.0 2004 + I965, // GL2.0/2.1 DX9/DX10 SM 3.0/4.0 2006 + SandyBridge, // Gen6 GL3.1 CL1.1 DX10.1 SM 4.0 2010 + IvyBridge, // Gen7 GL4.0 CL1.1 DX11 SM 5.0 2012 + Haswell, // Gen7 GL4.0 CL1.2 DX11.1 SM 5.0 2013 + BayTrail, // Gen7 GL4.0 CL1.2 DX11.1 SM 5.0 2013 + Cherryview, // Gen8 GL4.0 CL1.2 DX11.2 SM 5.0 2013 + Broadwell, // Gen8 GL4.4 CL2.0 DX11.2 SM 5.0 2014 + ApolloLake, // Gen9 GL4.6 CL3.0 DX12 SM 6.0 2016 + Skylake, // Gen9 GL4.6 CL3.0 DX12 SM 6.0 2015 + GeminiLake, // Gen9 GL4.6 CL3.0 DX12 SM 6.0 2017 + KabyLake, // Gen9 GL4.6 CL3.0 DX12 SM 6.0 2017 + CoffeeLake, // Gen9 GL4.6 CL3.0 DX12 SM 6.0 2018 + WhiskeyLake, // Gen9 GL4.6 GL3.0 DX12 SM 6.0 2018 + CometLake, // Gen9 GL4.6 GL3.0 DX12 SM 6.0 2019 + CannonLake, // Gen10 GL4.6 GL3.0 DX12 SM 6.0 2018 + IceLake, // Gen11 GL4.6 CL3.0 DX12.1 SM 6.0 2019 + TigerLake, // Gen12 GL4.6 CL3.0 DX12.1 SM 6.0 2020 + UnknownIntel = 2999, + + // Qualcomm Adreno + // from https://en.wikipedia.org/wiki/Adreno + Adreno1XX = 3000, // GLES1.1 + Adreno2XX, // GLES2.0 DX9c + Adreno3XX, // GLES3.0 CL1.1 DX11.1 + Adreno4XX, // GLES3.1 CL1.2 DX11.2 + Adreno5XX, // GLES3.1 CL2.0 DX11.2 + UnknownAdreno = 3999, + + // Panfrost Mali + // from https://docs.mesa3d.org/drivers/panfrost.html + MaliT7XX = 4000, // GLES2.0/GLES3.0 + MaliT8XX, // GLES3.0 + MaliGXX, // GLES3.0 + UnknownPanfrost = 4999, + + // Lima Mali + // from https://docs.mesa3d.org/drivers/lima.html + Mali400 = 5000, + Mali450, + Mali470, + UnknownLima = 5999, + + // Broadcom VideoCore IV (e.g. Raspberry Pi 0 to 3), GLES 2.0/2.1 with caveats + VC4_2_1 = 6000, // Found in Raspberry Pi 3B+ + UnknownVideoCore4 = 6999, + + // Broadcom VideoCore 3D (e.g. Raspberry Pi 4, Raspberry Pi 400) + V3D_4_2 = 7000, // Found in Raspberry Pi 400 + UnknownVideoCore3D = 7999, + + UnknownChipClass = 99999, +}; +// clang-format on + +class KWIN_EXPORT GLPlatform +{ +public: + explicit GLPlatform(QByteArrayView openglVersionString, QByteArrayView glslVersionString, QByteArrayView renderer, QByteArrayView vendor); + ~GLPlatform(); + + /** + * Returns the OpenGL version. + */ + Version glVersion() const; + + /** + * Returns the GLSL version if the driver supports GLSL, and 0 otherwise. + */ + Version glslVersion() const; + + /** + * Returns the Mesa version if the driver is a Mesa driver, and 0 otherwise. + */ + Version mesaVersion() const; + + /** + * Returns the driver version. + * + * For Mesa drivers, this is the same as the Mesa version number. + */ + Version driverVersion() const; + + /** + * Returns the driver. + */ + Driver driver() const; + + /** + * Returns the chip class. + */ + ChipClass chipClass() const; + + /** + * Returns true if the driver is a Mesa driver, and false otherwise. + */ + bool isMesaDriver() const; + + /** + * Returns true if the GPU is a Radeon GPU, and false otherwise. + */ + bool isRadeon() const; + + /** + * Returns true if the GPU is an NVIDIA GPU, and false otherwise. + */ + bool isNvidia() const; + + /** + * Returns true if the GPU is an Intel GPU, and false otherwise. + */ + bool isIntel() const; + + /** + * @returns @c true if the "GPU" is a VirtualBox GPU, and @c false otherwise. + * @since 4.10 + */ + bool isVirtualBox() const; + + /** + * @returns @c true if the "GPU" is a VMWare GPU, and @c false otherwise. + * @since 4.10 + */ + bool isVMware() const; + + /** + * @returns @c true if the driver is known to be from a virtual machine. + * @since 4.10 + */ + bool isVirtualMachine() const; + + /** + * @returns @c true if the GPU is a Qualcomm Adreno GPU, and false otherwise + * @since 5.8 + */ + bool isAdreno() const; + + /** + * @returns @c true if the "GPU" is a virtio-gpu (Qemu/KVM) + * @since 5.18 + **/ + bool isVirgl() const; + + /** + * @returns @c true if the "GPU" is a Panfrost Mali GPU + * @since 5.21.5 + **/ + bool isPanfrost() const; + + /** + * @returns @c true if the GPU is a Mali GPU supported by the Lima driver (Mali 400, 450) + * @since 5.27.1 + **/ + bool isLima() const; + + /** + * @returns @c true if the GPU is a Broadcom VideoCore IV (e.g. Raspberry Pi 0 to 3) + * @since 5.27.1 + **/ + bool isVideoCore4() const; + + /** + * @returns @c true if the GPU is a Broadcom VideoCore 3D (e.g. Raspberry Pi 4, 400) + * @since 5.27.1 + **/ + bool isVideoCore3D() const; + + /** + * @returns the GL_VERSION string as provided by the driver. + * @since 4.9 + */ + QByteArrayView glVersionString() const; + /** + * @returns the GL_RENDERER string as provided by the driver. + * @since 4.9 + */ + QByteArrayView glRendererString() const; + /** + * @returns the GL_VENDOR string as provided by the driver. + * @since 4.9 + */ + QByteArrayView glVendorString() const; + /** + * @returns the GL_SHADING_LANGUAGE_VERSION string as provided by the driver. + * If the driver does not support the OpenGL Shading Language a null bytearray is returned. + * @since 4.9 + */ + QByteArrayView glShadingLanguageVersionString() const; + /** + * @returns Whether the driver supports loose texture binding. + * @since 4.9 + */ + bool isLooseBinding() const; + + /** + * @returns The CompositingType recommended by the driver. + * @since 4.10 + */ + CompositingType recommendedCompositor() const; + + /** + * Returns true if glMapBufferRange() is likely to perform worse than glBufferSubData() + * when updating an unused range of a buffer object, and false otherwise. + * + * @since 4.11 + */ + bool preferBufferSubData() const; + + /** + * @returns a human readable form for the @p driver as a QString. + * @since 4.9 + * @see driver + */ + static QString driverToString(Driver driver); + /** + * @returns a human readable form for the @p driver as a QByteArray. + * @since 5.5 + * @see driver + */ + static QByteArray driverToString8(Driver driver); + + /** + * @returns a human readable form for the @p chipClass as a QString. + * @since 4.9 + * @see chipClass + */ + static QString chipClassToString(ChipClass chipClass); + /** + * @returns a human readable form for the @p chipClass as a QByteArray. + * @since 5.5 + * @see chipClass + */ + static QByteArray chipClassToString8(ChipClass chipClass); + +private: + QByteArrayView m_openglVersionString; + QByteArrayView m_glslVersionString; + QByteArrayView m_chipset = QByteArrayLiteral("Unknown"); + QByteArrayView m_rendererString; + QByteArrayView m_vendorString; + Driver m_driver = Driver_Unknown; + ChipClass m_chipClass = UnknownChipClass; + CompositingType m_recommendedCompositor = OpenGLCompositing; + Version m_openglVersion; + Version m_glslVersion; + Version m_mesaVersion; + Version m_driverVersion; + bool m_looseBinding = false; + bool m_virtualMachine = false; + bool m_preferBufferSubData = false; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/opengl/glrendertimequery.cpp b/local/recipes/kde/kwin/source/src/opengl/glrendertimequery.cpp new file mode 100644 index 0000000000..bb6583d98f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/glrendertimequery.cpp @@ -0,0 +1,87 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "glrendertimequery.h" +#include "opengl/eglcontext.h" +#include "utils/common.h" + +namespace KWin +{ + +GLRenderTimeQuery::GLRenderTimeQuery(const std::shared_ptr &context) + : m_context(context) +{ + if (context->supportsTimerQueries()) { + glGenQueries(1, &m_gpuProbe.query); + } +} + +GLRenderTimeQuery::~GLRenderTimeQuery() +{ + if (!m_gpuProbe.query) { + return; + } + const auto previousContext = EglContext::currentContext(); + const auto context = m_context.lock(); + if (!context || !context->makeCurrent()) { + qCWarning(KWIN_OPENGL, "Could not delete render time query because no context is current"); + return; + } + glDeleteQueries(1, &m_gpuProbe.query); + if (previousContext && previousContext != context.get()) { + previousContext->makeCurrent(); + } +} + +void GLRenderTimeQuery::begin() +{ + if (m_gpuProbe.query) { + GLint64 start = 0; + glGetInteger64v(GL_TIMESTAMP, &start); + m_gpuProbe.start = std::chrono::nanoseconds(start); + } + m_cpuProbe.start = std::chrono::steady_clock::now(); +} + +void GLRenderTimeQuery::end() +{ + m_hasResult = true; + + if (m_gpuProbe.query) { + glQueryCounter(m_gpuProbe.query, GL_TIMESTAMP); + } + m_cpuProbe.end = std::chrono::steady_clock::now(); +} + +std::optional GLRenderTimeQuery::query() +{ + Q_ASSERT(m_hasResult); + if (m_gpuProbe.query) { + const auto previousContext = EglContext::currentContext(); + const auto context = m_context.lock(); + if (!context || !context->makeCurrent()) { + return std::nullopt; + } + GLint64 end = 0; + glGetQueryObjecti64v(m_gpuProbe.query, GL_QUERY_RESULT, &end); + m_gpuProbe.end = std::chrono::nanoseconds(end); + if (previousContext && previousContext != context.get()) { + previousContext->makeCurrent(); + } + } + + // timings are pretty unpredictable in the sub-millisecond range; this minimum + // ensures that when CPU or GPU power states change, we don't drop any frames + const std::chrono::nanoseconds minimumTime = std::chrono::milliseconds(2); + const auto end = std::max({m_cpuProbe.start + (m_gpuProbe.end - m_gpuProbe.start), m_cpuProbe.end, m_cpuProbe.start + minimumTime}); + return RenderTimeSpan{ + .start = m_cpuProbe.start, + .end = end, + }; +} +} diff --git a/local/recipes/kde/kwin/source/src/opengl/glrendertimequery.h b/local/recipes/kde/kwin/source/src/opengl/glrendertimequery.h new file mode 100644 index 0000000000..97dde1a92f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/glrendertimequery.h @@ -0,0 +1,54 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include + +#include "core/renderbackend.h" +#include "kwin_export.h" + +namespace KWin +{ + +class EglContext; + +class KWIN_EXPORT GLRenderTimeQuery : public RenderTimeQuery +{ +public: + explicit GLRenderTimeQuery(const std::shared_ptr &context); + ~GLRenderTimeQuery(); + + void begin(); + void end(); + + /** + * fetches the result of the query. If rendering is not done yet, this will block! + */ + std::optional query() override; + +private: + const std::weak_ptr m_context; + bool m_hasResult = false; + + struct + { + std::chrono::steady_clock::time_point start; + std::chrono::steady_clock::time_point end; + } m_cpuProbe; + + struct + { + GLuint query = 0; + std::chrono::nanoseconds start{0}; + std::chrono::nanoseconds end{0}; + } m_gpuProbe; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/glshader.cpp b/local/recipes/kde/kwin/source/src/opengl/glshader.cpp new file mode 100644 index 0000000000..45bfbd8a01 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/glshader.cpp @@ -0,0 +1,511 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006-2007 Rivo Laks + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "glshader.h" +#include "glplatform.h" +#include "glutils.h" +#include "utils/common.h" + +#include + +namespace KWin +{ + +GLShader::GLShader(unsigned int flags) + : m_valid(false) + , m_locationsResolved(false) + , m_explicitLinking(flags & ExplicitLinking) +{ + m_program = glCreateProgram(); +} + +GLShader::GLShader(const QString &vertexfile, const QString &fragmentfile, unsigned int flags) + : m_valid(false) + , m_locationsResolved(false) + , m_explicitLinking(flags & ExplicitLinking) +{ + m_program = glCreateProgram(); + loadFromFiles(vertexfile, fragmentfile); +} + +GLShader::~GLShader() +{ + if (!EglContext::currentContext()) { + qCWarning(KWIN_OPENGL, "Could not delete shader because no context is current"); + return; + } + if (m_program) { + glDeleteProgram(m_program); + } +} + +bool GLShader::loadFromFiles(const QString &vertexFile, const QString &fragmentFile) +{ + QFile vf(vertexFile); + if (!vf.open(QIODevice::ReadOnly)) { + qCCritical(KWIN_OPENGL) << "Couldn't open" << vertexFile << "for reading!"; + return false; + } + const QByteArray vertexSource = vf.readAll(); + + QFile ff(fragmentFile); + if (!ff.open(QIODevice::ReadOnly)) { + qCCritical(KWIN_OPENGL) << "Couldn't open" << fragmentFile << "for reading!"; + return false; + } + const QByteArray fragmentSource = ff.readAll(); + + return load(vertexSource, fragmentSource); +} + +bool GLShader::link() +{ + // Be optimistic + m_valid = true; + + glLinkProgram(m_program); + + // Get the program info log + int maxLength, length; + glGetProgramiv(m_program, GL_INFO_LOG_LENGTH, &maxLength); + + QByteArray log(maxLength, 0); + glGetProgramInfoLog(m_program, maxLength, &length, log.data()); + + // Make sure the program linked successfully + int status; + glGetProgramiv(m_program, GL_LINK_STATUS, &status); + + if (status == 0) { + qCCritical(KWIN_OPENGL) << "Failed to link shader:" + << "\n" + << log; + m_valid = false; + } else if (length > 0) { + qCDebug(KWIN_OPENGL) << "Shader link log:" << log; + } + + return m_valid; +} + +const QByteArray GLShader::prepareSource(GLenum shaderType, const QByteArray &source) const +{ + // Prepare the source code + QByteArray ba; + const auto context = EglContext::currentContext(); + if (context->isOpenGLES() && context->glslVersion() < Version(3, 0)) { + ba.append("precision highp float;\n"); + } + ba.append(source); + if (context->isOpenGLES() && context->glslVersion() >= Version(3, 0)) { + ba.replace("#version 140", "#version 300 es\n\nprecision highp float;\n"); + } + + return ba; +} + +bool GLShader::compile(GLuint program, GLenum shaderType, const QByteArray &source) const +{ + GLuint shader = glCreateShader(shaderType); + + QByteArray preparedSource = prepareSource(shaderType, source); + const char *src = preparedSource.constData(); + glShaderSource(shader, 1, &src, nullptr); + + // Compile the shader + glCompileShader(shader); + + // Get the shader info log + int maxLength, length; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength); + + QByteArray log(maxLength, 0); + glGetShaderInfoLog(shader, maxLength, &length, log.data()); + + // Check the status + int status; + glGetShaderiv(shader, GL_COMPILE_STATUS, &status); + + if (status == 0) { + const char *typeName = (shaderType == GL_VERTEX_SHADER ? "vertex" : "fragment"); + qCCritical(KWIN_OPENGL) << "Failed to compile" << typeName << "shader:" + << "\n" + << log; + size_t line = 0; + const auto split = source.split('\n'); + for (const auto &l : split) { + qCCritical(KWIN_OPENGL).nospace() << "line " << line++ << ":" << l; + } + } else if (length > 0) { + qCDebug(KWIN_OPENGL) << "Shader compile log:" << log; + } + + if (status != 0) { + glAttachShader(program, shader); + } + + glDeleteShader(shader); + return status != 0; +} + +bool GLShader::load(const QByteArray &vertexSource, const QByteArray &fragmentSource) +{ + m_valid = false; + + // Compile the vertex shader + if (!vertexSource.isEmpty()) { + bool success = compile(m_program, GL_VERTEX_SHADER, vertexSource); + + if (!success) { + return false; + } + } + + // Compile the fragment shader + if (!fragmentSource.isEmpty()) { + bool success = compile(m_program, GL_FRAGMENT_SHADER, fragmentSource); + + if (!success) { + return false; + } + } + + if (m_explicitLinking) { + return true; + } + + // link() sets mValid + return link(); +} + +void GLShader::bindAttributeLocation(const char *name, int index) +{ + glBindAttribLocation(m_program, index, name); +} + +void GLShader::bindFragDataLocation(const char *name, int index) +{ + const auto context = EglContext::currentContext(); + if (!context->isOpenGLES() && (context->hasVersion(Version(3, 0)) || context->hasOpenglExtension(QByteArrayLiteral("GL_EXT_gpu_shader4")))) { + glBindFragDataLocation(m_program, index, name); + } +} + +void GLShader::bind() +{ + glUseProgram(m_program); +} + +void GLShader::unbind() +{ + glUseProgram(0); +} + +void GLShader::resolveLocations() +{ + if (m_locationsResolved) { + return; + } + + m_matrix4Locations[Mat4Uniform::TextureMatrix] = uniformLocation("textureMatrix"); + m_matrix4Locations[Mat4Uniform::ProjectionMatrix] = uniformLocation("projection"); + m_matrix4Locations[Mat4Uniform::ModelViewMatrix] = uniformLocation("modelview"); + m_matrix4Locations[Mat4Uniform::ModelViewProjectionMatrix] = uniformLocation("modelViewProjectionMatrix"); + m_matrix4Locations[Mat4Uniform::WindowTransformation] = uniformLocation("windowTransformation"); + m_matrix4Locations[Mat4Uniform::ScreenTransformation] = uniformLocation("screenTransformation"); + m_matrix4Locations[Mat4Uniform::ColorimetryTransformation] = uniformLocation("colorimetryTransform"); + m_matrix4Locations[Mat4Uniform::DestinationToLMS] = uniformLocation("destinationToLMS"); + m_matrix4Locations[Mat4Uniform::LMSToDestination] = uniformLocation("lmsToDestination"); + m_matrix4Locations[Mat4Uniform::YuvToRgb] = uniformLocation("yuvToRgb"); + + m_vec2Locations[Vec2Uniform::Offset] = uniformLocation("offset"); + m_vec2Locations[Vec2Uniform::SourceTransferFunctionParams] = uniformLocation("sourceTransferFunctionParams"); + m_vec2Locations[Vec2Uniform::DestinationTransferFunctionParams] = uniformLocation("destinationTransferFunctionParams"); + + m_vec3Locations[Vec3Uniform::PrimaryBrightness] = uniformLocation("primaryBrightness"); + + m_vec4Locations[Vec4Uniform::ModulationConstant] = uniformLocation("modulation"); + m_vec4Locations[Vec4Uniform::Box] = uniformLocation("box"); + m_vec4Locations[Vec4Uniform::CornerRadius] = uniformLocation("cornerRadius"); + + m_floatLocations[FloatUniform::Saturation] = uniformLocation("saturation"); + m_floatLocations[FloatUniform::MaxDestinationLuminance] = uniformLocation("maxDestinationLuminance"); + m_floatLocations[FloatUniform::SourceReferenceLuminance] = uniformLocation("sourceReferenceLuminance"); + m_floatLocations[FloatUniform::DestinationReferenceLuminance] = uniformLocation("destinationReferenceLuminance"); + m_floatLocations[FloatUniform::MaxTonemappingLuminance] = uniformLocation("maxTonemappingLuminance"); + + m_colorLocations[ColorUniform::Color] = uniformLocation("geometryColor"); + + m_intLocations[IntUniform::TextureWidth] = uniformLocation("textureWidth"); + m_intLocations[IntUniform::TextureHeight] = uniformLocation("textureHeight"); + m_intLocations[IntUniform::Sampler] = uniformLocation("sampler"); + m_intLocations[IntUniform::Sampler1] = uniformLocation("sampler1"); + m_intLocations[IntUniform::SourceNamedTransferFunction] = uniformLocation("sourceNamedTransferFunction"); + m_intLocations[IntUniform::DestinationNamedTransferFunction] = uniformLocation("destinationNamedTransferFunction"); + m_intLocations[IntUniform::Thickness] = uniformLocation("thickness"); + + m_locationsResolved = true; +} + +int GLShader::uniformLocation(const char *name) +{ + const int location = glGetUniformLocation(m_program, name); + return location; +} + +bool GLShader::setUniform(Mat3Uniform uniform, const QMatrix3x3 &value) +{ + resolveLocations(); + return setUniform(m_matrix3Locations[uniform], value); +} + +bool GLShader::setUniform(Mat4Uniform uniform, const QMatrix4x4 &matrix) +{ + resolveLocations(); + return setUniform(m_matrix4Locations[uniform], matrix); +} + +bool GLShader::setUniform(Vec2Uniform uniform, const QVector2D &value) +{ + resolveLocations(); + return setUniform(m_vec2Locations[uniform], value); +} + +bool GLShader::setUniform(Vec3Uniform uniform, const QVector3D &value) +{ + resolveLocations(); + return setUniform(m_vec3Locations[uniform], value); +} + +bool GLShader::setUniform(Vec4Uniform uniform, const QVector4D &value) +{ + resolveLocations(); + return setUniform(m_vec4Locations[uniform], value); +} + +bool GLShader::setUniform(FloatUniform uniform, float value) +{ + resolveLocations(); + return setUniform(m_floatLocations[uniform], value); +} + +bool GLShader::setUniform(IntUniform uniform, int value) +{ + resolveLocations(); + return setUniform(m_intLocations[uniform], value); +} + +bool GLShader::setUniform(ColorUniform uniform, const QVector4D &value) +{ + resolveLocations(); + return setUniform(m_colorLocations[uniform], value); +} + +bool GLShader::setUniform(ColorUniform uniform, const QColor &value) +{ + resolveLocations(); + return setUniform(m_colorLocations[uniform], value); +} + +bool GLShader::setUniform(const char *name, float value) +{ + const int location = uniformLocation(name); + return setUniform(location, value); +} + +bool GLShader::setUniform(const char *name, double value) +{ + const int location = uniformLocation(name); + return setUniform(location, value); +} + +bool GLShader::setUniform(const char *name, int value) +{ + const int location = uniformLocation(name); + return setUniform(location, value); +} + +bool GLShader::setUniform(const char *name, const QVector2D &value) +{ + const int location = uniformLocation(name); + return setUniform(location, value); +} + +bool GLShader::setUniform(const char *name, const QVector3D &value) +{ + const int location = uniformLocation(name); + return setUniform(location, value); +} + +bool GLShader::setUniform(const char *name, const QVector4D &value) +{ + const int location = uniformLocation(name); + return setUniform(location, value); +} + +bool GLShader::setUniform(const char *name, const QMatrix3x3 &value) +{ + const int location = uniformLocation(name); + return setUniform(location, value); +} + +bool GLShader::setUniform(const char *name, const QMatrix4x4 &value) +{ + const int location = uniformLocation(name); + return setUniform(location, value); +} + +bool GLShader::setUniform(const char *name, const QColor &color) +{ + const int location = uniformLocation(name); + return setUniform(location, color); +} + +bool GLShader::setUniform(int location, float value) +{ + if (location >= 0) { + glUniform1f(location, value); + } + return (location >= 0); +} + +bool GLShader::setUniform(int location, double value) +{ + if (location >= 0) { + glUniform1f(location, value); + } + return (location >= 0); +} + +bool GLShader::setUniform(int location, int value) +{ + if (location >= 0) { + glUniform1i(location, value); + } + return (location >= 0); +} + +bool GLShader::setUniform(int location, int xValue, int yValue, int zValue) +{ + if (location >= 0) { + glUniform3i(location, xValue, yValue, zValue); + } + return location >= 0; +} + +bool GLShader::setUniform(int location, const QVector2D &value) +{ + if (location >= 0) { + glUniform2fv(location, 1, (const GLfloat *)&value); + } + return (location >= 0); +} + +bool GLShader::setUniform(int location, const QVector3D &value) +{ + if (location >= 0) { + glUniform3fv(location, 1, (const GLfloat *)&value); + } + return (location >= 0); +} + +bool GLShader::setUniform(int location, const QVector4D &value) +{ + if (location >= 0) { + glUniform4fv(location, 1, (const GLfloat *)&value); + } + return (location >= 0); +} + +bool GLShader::setUniform(int location, const QMatrix3x3 &value) +{ + if (location >= 0) { + glUniformMatrix3fv(location, 1, GL_FALSE, value.constData()); + } + return location >= 0; +} + +bool GLShader::setUniform(int location, const QMatrix4x4 &value) +{ + if (location >= 0) { + glUniformMatrix4fv(location, 1, GL_FALSE, value.constData()); + } + return (location >= 0); +} + +bool GLShader::setUniform(int location, const QColor &color) +{ + if (location >= 0) { + glUniform4f(location, color.redF(), color.greenF(), color.blueF(), color.alphaF()); + } + return (location >= 0); +} + +int GLShader::attributeLocation(const char *name) +{ + int location = glGetAttribLocation(m_program, name); + return location; +} + +bool GLShader::setAttribute(const char *name, float value) +{ + int location = attributeLocation(name); + if (location >= 0) { + glVertexAttrib1f(location, value); + } + return (location >= 0); +} + +QMatrix4x4 GLShader::getUniformMatrix4x4(const char *name) +{ + int location = uniformLocation(name); + if (location >= 0) { + GLfloat m[16]; + EglContext::currentContext()->glGetnUniformfv(m_program, location, sizeof(m), m); + QMatrix4x4 matrix(m[0], m[4], m[8], m[12], + m[1], m[5], m[9], m[13], + m[2], m[6], m[10], m[14], + m[3], m[7], m[11], m[15]); + matrix.optimize(); + return matrix; + } else { + return QMatrix4x4(); + } +} + +static bool s_disableTonemapping = qEnvironmentVariableIntValue("KWIN_DISABLE_TONEMAPPING") == 1; + +void GLShader::setColorspaceUniforms(const std::shared_ptr &src, const std::shared_ptr &dst, RenderingIntent intent) +{ + setUniform(Mat4Uniform::ColorimetryTransformation, src->toOther(*dst, intent)); + setUniform(IntUniform::SourceNamedTransferFunction, src->transferFunction().type); + if (src->transferFunction().type == TransferFunction::BT1886) { + setUniform(Vec2Uniform::SourceTransferFunctionParams, QVector2D(src->transferFunction().bt1886B(), src->transferFunction().bt1886A())); + } else { + setUniform(Vec2Uniform::SourceTransferFunctionParams, QVector2D(src->transferFunction().minLuminance, src->transferFunction().maxLuminance - src->transferFunction().minLuminance)); + } + setUniform(FloatUniform::SourceReferenceLuminance, src->referenceLuminance()); + setUniform(IntUniform::DestinationNamedTransferFunction, dst->transferFunction().type); + if (dst->transferFunction().type == TransferFunction::BT1886) { + setUniform(Vec2Uniform::DestinationTransferFunctionParams, QVector2D(dst->transferFunction().bt1886B(), dst->transferFunction().bt1886A())); + } else { + setUniform(Vec2Uniform::DestinationTransferFunctionParams, QVector2D(dst->transferFunction().minLuminance, dst->transferFunction().maxLuminance - dst->transferFunction().minLuminance)); + } + setUniform(FloatUniform::DestinationReferenceLuminance, dst->referenceLuminance()); + setUniform(FloatUniform::MaxDestinationLuminance, dst->maxHdrLuminance().value_or(10'000)); + if (!s_disableTonemapping && intent == RenderingIntent::Perceptual) { + setUniform(FloatUniform::MaxTonemappingLuminance, src->maxHdrLuminance().value_or(src->referenceLuminance()) * dst->referenceLuminance() / src->referenceLuminance()); + } else { + setUniform(FloatUniform::MaxTonemappingLuminance, dst->maxHdrLuminance().value_or(10'000)); + } + setUniform(Mat4Uniform::DestinationToLMS, dst->containerColorimetry().toLMS()); + setUniform(Mat4Uniform::LMSToDestination, dst->containerColorimetry().fromLMS()); +} +} diff --git a/local/recipes/kde/kwin/source/src/opengl/glshader.h b/local/recipes/kde/kwin/source/src/opengl/glshader.h new file mode 100644 index 0000000000..0fc357abf8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/glshader.h @@ -0,0 +1,177 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006-2007 Rivo Laks + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "core/colorspace.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +class KWIN_EXPORT GLShader +{ +public: + enum Flags { + NoFlags = 0, + ExplicitLinking = (1 << 0) + }; + + GLShader(const QString &vertexfile, const QString &fragmentfile, unsigned int flags = NoFlags); + ~GLShader(); + + bool isValid() const + { + return m_valid; + } + + void bindAttributeLocation(const char *name, int index); + void bindFragDataLocation(const char *name, int index); + + bool link(); + + int uniformLocation(const char *name); + + bool setUniform(const char *name, float value); + bool setUniform(const char *name, double value); + bool setUniform(const char *name, int value); + bool setUniform(const char *name, const QVector2D &value); + bool setUniform(const char *name, const QVector3D &value); + bool setUniform(const char *name, const QVector4D &value); + bool setUniform(const char *name, const QMatrix3x3 &value); + bool setUniform(const char *name, const QMatrix4x4 &value); + bool setUniform(const char *name, const QColor &color); + + bool setUniform(int location, float value); + bool setUniform(int location, double value); + bool setUniform(int location, int value); + bool setUniform(int location, int xValue, int yValue, int zValue); + bool setUniform(int location, const QVector2D &value); + bool setUniform(int location, const QVector3D &value); + bool setUniform(int location, const QVector4D &value); + bool setUniform(int location, const QMatrix3x3 &value); + bool setUniform(int location, const QMatrix4x4 &value); + bool setUniform(int location, const QColor &value); + + int attributeLocation(const char *name); + bool setAttribute(const char *name, float value); + /** + * @return The value of the uniform as a matrix + * @since 4.7 + */ + QMatrix4x4 getUniformMatrix4x4(const char *name); + + enum class Mat3Uniform { + }; + + enum class Mat4Uniform { + TextureMatrix = 0, + ProjectionMatrix, + ModelViewMatrix, + ModelViewProjectionMatrix, + WindowTransformation, + ScreenTransformation, + ColorimetryTransformation, + DestinationToLMS, + LMSToDestination, + YuvToRgb, + MatrixCount + }; + + enum class Vec2Uniform { + Offset, + SourceTransferFunctionParams, + DestinationTransferFunctionParams, + Vec2UniformCount + }; + + enum class Vec3Uniform { + PrimaryBrightness = 0 + }; + + enum class Vec4Uniform { + ModulationConstant, + Box, + CornerRadius, + Vec4UniformCount + }; + + enum class FloatUniform { + Saturation, + MaxDestinationLuminance, + SourceReferenceLuminance, + DestinationReferenceLuminance, + MaxTonemappingLuminance, + FloatUniformCount + }; + + enum class IntUniform { + AlphaToOne, ///< @deprecated no longer used + TextureWidth, + TextureHeight, + SourceNamedTransferFunction, + DestinationNamedTransferFunction, + Sampler, + Sampler1, + Thickness, + IntUniformCount + }; + + enum class ColorUniform { + Color, + ColorUniformCount + }; + + bool setUniform(Mat3Uniform uniform, const QMatrix3x3 &value); + bool setUniform(Mat4Uniform uniform, const QMatrix4x4 &matrix); + bool setUniform(Vec2Uniform uniform, const QVector2D &value); + bool setUniform(Vec3Uniform uniform, const QVector3D &value); + bool setUniform(Vec4Uniform uniform, const QVector4D &value); + bool setUniform(FloatUniform uniform, float value); + bool setUniform(IntUniform uniform, int value); + bool setUniform(ColorUniform uniform, const QVector4D &value); + bool setUniform(ColorUniform uniform, const QColor &value); + + void setColorspaceUniforms(const std::shared_ptr &src, const std::shared_ptr &dst, RenderingIntent intent); + +protected: + GLShader(unsigned int flags = NoFlags); + bool loadFromFiles(const QString &vertexfile, const QString &fragmentfile); + bool load(const QByteArray &vertexSource, const QByteArray &fragmentSource); + const QByteArray prepareSource(GLenum shaderType, const QByteArray &sourceCode) const; + bool compile(GLuint program, GLenum shaderType, const QByteArray &sourceCode) const; + void bind(); + void unbind(); + void resolveLocations(); + +private: + unsigned int m_program; + bool m_valid : 1; + bool m_locationsResolved : 1; + bool m_explicitLinking : 1; + QHash m_matrix3Locations; + QHash m_matrix4Locations; + QHash m_vec2Locations; + QHash m_vec3Locations; + QHash m_vec4Locations; + QHash m_floatLocations; + QHash m_intLocations; + QHash m_colorLocations; + + friend class ShaderManager; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/glshadermanager.cpp b/local/recipes/kde/kwin/source/src/opengl/glshadermanager.cpp new file mode 100644 index 0000000000..1beb2ad52e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/glshadermanager.cpp @@ -0,0 +1,415 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006-2007 Rivo Laks + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "glshadermanager.h" +#include "eglcontext.h" +#include "glplatform.h" +#include "glshader.h" +#include "glvertexbuffer.h" +#include "utils/common.h" + +#include +#include + +namespace KWin +{ + +ShaderManager *ShaderManager::instance() +{ + return EglContext::currentContext()->shaderManager(); +} + +ShaderManager::ShaderManager() +{ +} + +ShaderManager::~ShaderManager() +{ + while (!m_boundShaders.isEmpty()) { + popShader(); + } +} + +QByteArray ShaderManager::generateVertexSource(ShaderTraits traits) const +{ + QByteArray source; + QTextStream stream(&source); + + const auto context = EglContext::currentContext(); + QByteArray attribute, varying; + + if (!context->isOpenGLES()) { + const bool glsl_140 = context->glslVersion() >= Version(1, 40); + + attribute = glsl_140 ? QByteArrayLiteral("in") : QByteArrayLiteral("attribute"); + varying = glsl_140 ? QByteArrayLiteral("out") : QByteArrayLiteral("varying"); + + if (glsl_140) { + stream << "#version 140\n\n"; + } + } else { + const bool glsl_es_300 = context->glslVersion() >= Version(3, 0); + + attribute = glsl_es_300 ? QByteArrayLiteral("in") : QByteArrayLiteral("attribute"); + varying = glsl_es_300 ? QByteArrayLiteral("out") : QByteArrayLiteral("varying"); + + if (glsl_es_300) { + stream << "#version 300 es\n\n"; + } + } + + stream << attribute << " vec4 position;\n"; + if (traits & (ShaderTrait::MapTexture | ShaderTrait::MapExternalTexture | ShaderTrait::MapMultiPlaneTexture)) { + stream << attribute << " vec4 texcoord;\n\n"; + stream << varying << " vec2 texcoord0;\n\n"; + } else { + stream << "\n"; + } + + if (traits & (ShaderTrait::RoundedCorners | ShaderTrait::Border)) { + stream << varying << " vec2 position0;\n\n"; + } + + stream << "uniform mat4 modelViewProjectionMatrix;\n\n"; + + stream << "void main()\n{\n"; + if (traits & (ShaderTrait::MapTexture | ShaderTrait::MapExternalTexture | ShaderTrait::MapMultiPlaneTexture)) { + stream << " texcoord0 = texcoord.st;\n"; + } + + if (traits & (ShaderTrait::RoundedCorners | ShaderTrait::Border)) { + stream << " position0 = position.xy;\n"; + } + + stream << " gl_Position = modelViewProjectionMatrix * position;\n"; + stream << "}\n"; + + stream.flush(); + return source; +} + +QByteArray ShaderManager::generateFragmentSource(ShaderTraits traits) const +{ + QByteArray source; + QTextStream stream(&source); + + const auto context = EglContext::currentContext(); + QByteArray varying, output, textureLookup; + + if (!context->isOpenGLES()) { + const bool glsl_140 = context->glslVersion() >= Version(1, 40); + + if (glsl_140) { + stream << "#version 140\n\n"; + } + + varying = glsl_140 ? QByteArrayLiteral("in") : QByteArrayLiteral("varying"); + textureLookup = glsl_140 ? QByteArrayLiteral("texture") : QByteArrayLiteral("texture2D"); + output = glsl_140 ? QByteArrayLiteral("fragColor") : QByteArrayLiteral("gl_FragColor"); + } else { + const bool glsl_es_300 = context->glslVersion() >= Version(3, 0); + + if (glsl_es_300) { + stream << "#version 300 es\n\n"; + } else { + if (traits & (ShaderTrait::RoundedCorners | ShaderTrait::Border)) { + stream << "#extension GL_OES_standard_derivatives : enable\n\n"; + } + } + + // From the GLSL ES specification: + // + // "The fragment language has no default precision qualifier for floating point types." + stream << "precision highp float;\n\n"; + + varying = glsl_es_300 ? QByteArrayLiteral("in") : QByteArrayLiteral("varying"); + textureLookup = glsl_es_300 ? QByteArrayLiteral("texture") : QByteArrayLiteral("texture2D"); + output = glsl_es_300 ? QByteArrayLiteral("fragColor") : QByteArrayLiteral("gl_FragColor"); + } + + if (traits & ShaderTrait::MapTexture) { + stream << "uniform sampler2D sampler;\n"; + stream << varying << " vec2 texcoord0;\n"; + } else if (traits & ShaderTrait::MapMultiPlaneTexture) { + stream << "uniform sampler2D sampler;\n"; + stream << "uniform sampler2D sampler1;\n"; + stream << varying << " vec2 texcoord0;\n"; + } else if (traits & ShaderTrait::MapExternalTexture) { + stream << "#extension GL_OES_EGL_image_external : require\n\n"; + stream << "uniform samplerExternalOES sampler;\n"; + stream << varying << " vec2 texcoord0;\n"; + } else if (traits & ShaderTrait::UniformColor) { + stream << "uniform vec4 geometryColor;\n"; + } else if (traits & ShaderTrait::Border) { + stream << "#include \"sdf.glsl\"\n"; + + stream << "uniform vec4 box;\n"; + stream << "uniform vec4 cornerRadius;\n"; + stream << "uniform vec4 geometryColor;\n"; + stream << "uniform int thickness;\n"; + stream << varying << " vec2 position0;\n"; + } + + if (traits & ShaderTrait::YuvConversion) { + stream << "uniform mat4 yuvToRgb;\n"; + } + if (traits & ShaderTrait::Modulate) { + stream << "uniform vec4 modulation;\n"; + } + if (traits & ShaderTrait::AdjustSaturation) { + stream << "#include \"saturation.glsl\"\n"; + } + if (traits & ShaderTrait::TransformColorspace) { + stream << "#include \"colormanagement.glsl\"\n"; + } + if (traits & ShaderTrait::RoundedCorners) { + stream << "#include \"sdf.glsl\"\n"; + + stream << "uniform vec4 box;\n"; + stream << "uniform vec4 cornerRadius;\n"; + stream << varying << " vec2 position0;\n"; + } + + if (output != QByteArrayLiteral("gl_FragColor")) { + stream << "\nout vec4 " << output << ";\n"; + } + + stream << "\nvoid main(void)\n{\n"; + stream << " vec4 result;\n"; + if (traits & ShaderTrait::MapTexture) { + stream << " result = " << textureLookup << "(sampler, texcoord0);\n"; + } else if (traits & ShaderTrait::MapMultiPlaneTexture) { + stream << " result = vec4(" << textureLookup << "(sampler, texcoord0).x, " << textureLookup << "(sampler1, texcoord0).rg, 1.0);\n"; + } else if (traits & ShaderTrait::MapExternalTexture) { + // external textures require texture2D for sampling + stream << " result = texture2D(sampler, texcoord0);\n"; + } else if (traits & ShaderTrait::UniformColor) { + stream << " result = geometryColor;\n"; + } else if (traits & ShaderTrait::Border) { + stream << " float inner = sdfRoundedBox(position0, box.xy, box.zw, cornerRadius);\n"; + stream << " float outer = sdfRoundedBox(position0, box.xy, box.zw + vec2(thickness), cornerRadius + vec4(thickness));\n"; + stream << " float f = sdfSubtract(outer, inner);\n"; + stream << " float df = fwidth(f);\n"; + stream << " result = geometryColor * (1.0 - clamp(0.5 + f / df, 0.0, 1.0));\n"; + } + + if (traits & ShaderTrait::YuvConversion) { + stream << "result.rgb = (yuvToRgb * vec4(result.rgb, 1.0)).rgb;"; + } + if (traits & ShaderTrait::RoundedCorners) { + stream << " float f = sdfRoundedBox(position0, box.xy, box.zw, cornerRadius);\n"; + stream << " float df = fwidth(f);\n"; + stream << " result *= 1.0 - clamp(0.5 + f / df, 0.0, 1.0);\n"; + } + if (traits & ShaderTrait::TransformColorspace) { + stream << " result = encodingToNits(result, sourceNamedTransferFunction, sourceTransferFunctionParams.x, sourceTransferFunctionParams.y);\n"; + stream << " result.rgb = (colorimetryTransform * vec4(result.rgb, 1.0)).rgb;\n"; + } + if (traits & ShaderTrait::AdjustSaturation) { + stream << " result = adjustSaturation(result);\n"; + } + if (traits & ShaderTrait::Modulate) { + stream << " result *= modulation;\n"; + } + if (traits & ShaderTrait::TransformColorspace) { + stream << " result.rgb = doTonemapping(result.rgb);\n"; + stream << " result = nitsToDestinationEncoding(result);\n"; + } + + stream << " " << output << " = result;\n"; + stream << "}"; + stream.flush(); + return source; +} + +std::unique_ptr ShaderManager::generateShader(ShaderTraits traits) +{ + return generateCustomShader(traits); +} + +std::optional ShaderManager::preprocess(const QByteArray &src, int recursionDepth) const +{ + recursionDepth++; + if (recursionDepth > 10) { + qCWarning(KWIN_OPENGL, "shader has too many recursive includes!"); + return std::nullopt; + } + QByteArray ret; + ret.reserve(src.size()); + const auto split = src.split('\n'); + for (auto it = split.begin(); it != split.end(); it++) { + const auto &line = *it; + if (line.startsWith("#include \"") && line.endsWith("\"")) { + static constexpr ssize_t includeLength = QByteArrayView("#include \"").size(); + const QByteArray path = ":/opengl/" + line.mid(includeLength, line.size() - includeLength - 1); + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + qCWarning(KWIN_OPENGL, "failed to read include line %s", qPrintable(line)); + return std::nullopt; + } + const auto processed = preprocess(file.readAll(), recursionDepth); + if (!processed) { + return std::nullopt; + } + ret.append(*processed); + } else { + ret.append(line); + ret.append('\n'); + } + } + return ret; +} + +std::unique_ptr ShaderManager::generateCustomShader(ShaderTraits traits, const QByteArray &vertexSource, const QByteArray &fragmentSource) +{ + const auto vertex = preprocess(vertexSource.isEmpty() ? generateVertexSource(traits) : vertexSource); + const auto fragment = preprocess(fragmentSource.isEmpty() ? generateFragmentSource(traits) : fragmentSource); + if (!vertex || !fragment) { + return nullptr; + } + + std::unique_ptr shader{new GLShader(GLShader::ExplicitLinking)}; + shader->load(*vertex, *fragment); + + shader->bindAttributeLocation("position", VA_Position); + shader->bindAttributeLocation("texcoord", VA_TexCoord); + shader->bindFragDataLocation("fragColor", 0); + + shader->link(); + return shader; +} + +static QString resolveShaderFilePath(const QString &filePath) +{ + QString suffix; + QString extension; + + const auto context = EglContext::currentContext(); + const Version coreVersionNumber = context->isOpenGLES() ? Version(3, 0) : Version(1, 40); + if (context->glslVersion() >= coreVersionNumber) { + suffix = QStringLiteral("_core"); + } + + if (filePath.endsWith(QStringLiteral(".frag"))) { + extension = QStringLiteral(".frag"); + } else if (filePath.endsWith(QStringLiteral(".vert"))) { + extension = QStringLiteral(".vert"); + } else { + qCWarning(KWIN_OPENGL) << filePath << "must end either with .vert or .frag"; + return QString(); + } + + const QString prefix = filePath.chopped(extension.size()); + return prefix + suffix + extension; +} + +std::unique_ptr ShaderManager::generateShaderFromFile(ShaderTraits traits, const QString &vertexFile, const QString &fragmentFile) +{ + auto loadShaderFile = [](const QString &filePath) { + QFile file(filePath); + if (file.open(QIODevice::ReadOnly)) { + return file.readAll(); + } + qCCritical(KWIN_OPENGL) << "Failed to read shader " << filePath; + return QByteArray(); + }; + QByteArray vertexSource; + QByteArray fragmentSource; + if (!vertexFile.isEmpty()) { + vertexSource = loadShaderFile(resolveShaderFilePath(vertexFile)); + if (vertexSource.isEmpty()) { + return std::unique_ptr(new GLShader()); + } + } + if (!fragmentFile.isEmpty()) { + fragmentSource = loadShaderFile(resolveShaderFilePath(fragmentFile)); + if (fragmentSource.isEmpty()) { + return std::unique_ptr(new GLShader()); + } + } + return generateCustomShader(traits, vertexSource, fragmentSource); +} + +GLShader *ShaderManager::shader(ShaderTraits traits) +{ + std::unique_ptr &shader = m_shaderHash[traits]; + if (!shader) { + shader = generateShader(traits); + } + return shader.get(); +} + +GLShader *ShaderManager::getBoundShader() const +{ + if (m_boundShaders.isEmpty()) { + return nullptr; + } else { + return m_boundShaders.top(); + } +} + +bool ShaderManager::isShaderBound() const +{ + return !m_boundShaders.isEmpty(); +} + +GLShader *ShaderManager::pushShader(ShaderTraits traits) +{ + GLShader *shader = this->shader(traits); + pushShader(shader); + return shader; +} + +void ShaderManager::pushShader(GLShader *shader) +{ + // only bind shader if it is not already bound + if (shader != getBoundShader()) { + shader->bind(); + } + m_boundShaders.push(shader); +} + +void ShaderManager::popShader() +{ + if (m_boundShaders.isEmpty()) { + return; + } + GLShader *shader = m_boundShaders.pop(); + if (m_boundShaders.isEmpty()) { + // no more shader bound - unbind + shader->unbind(); + } else if (shader != m_boundShaders.top()) { + // only rebind if a different shader is on top of stack + m_boundShaders.top()->bind(); + } +} + +void ShaderManager::bindFragDataLocations(GLShader *shader) +{ + shader->bindFragDataLocation("fragColor", 0); +} + +void ShaderManager::bindAttributeLocations(GLShader *shader) const +{ + shader->bindAttributeLocation("vertex", VA_Position); + shader->bindAttributeLocation("texCoord", VA_TexCoord); +} + +std::unique_ptr ShaderManager::loadShaderFromCode(const QByteArray &vertexSource, const QByteArray &fragmentSource) +{ + std::unique_ptr shader{new GLShader(GLShader::ExplicitLinking)}; + shader->load(vertexSource, fragmentSource); + bindAttributeLocations(shader.get()); + bindFragDataLocations(shader.get()); + shader->link(); + return shader; +} + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/glshadermanager.h b/local/recipes/kde/kwin/source/src/opengl/glshadermanager.h new file mode 100644 index 0000000000..134043a27b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/glshadermanager.h @@ -0,0 +1,232 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006-2007 Rivo Laks + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "kwin_export.h" + +#include +#include +#include +#include +#include + +namespace KWin +{ + +class GLShader; + +enum class ShaderTrait { + MapTexture = (1 << 0), + UniformColor = (1 << 1), + Modulate = (1 << 2), + AdjustSaturation = (1 << 3), + TransformColorspace = (1 << 4), + MapExternalTexture = (1 << 5), + MapMultiPlaneTexture = (1 << 6), + RoundedCorners = (1 << 7), + Border = (1 << 8), + YuvConversion = (1 << 9), +}; + +Q_DECLARE_FLAGS(ShaderTraits, ShaderTrait) + +/** + * @short Manager for Shaders. + * + * This class provides some built-in shaders to be used by both compositing scene and effects. + * The ShaderManager provides methods to bind a built-in or a custom shader and keeps track of + * the shaders which have been bound. When a shader is unbound the previously bound shader + * will be rebound. + * + * @author Martin Gräßlin + * @since 4.7 + */ +class KWIN_EXPORT ShaderManager +{ +public: + explicit ShaderManager(); + ~ShaderManager(); + + /** + * Returns a shader with the given traits, creating it if necessary. + */ + GLShader *shader(ShaderTraits traits); + + /** + * @return The currently bound shader or @c null if no shader is bound. + */ + GLShader *getBoundShader() const; + + /** + * @return @c true if a shader is bound, @c false otherwise + */ + bool isShaderBound() const; + + /** + * Pushes the current shader onto the stack and binds a shader + * with the given traits. + */ + GLShader *pushShader(ShaderTraits traits); + + /** + * Binds the @p shader. + * To unbind the shader use popShader. A previous bound shader will be rebound. + * To bind a built-in shader use the more specific method. + * @param shader The shader to be bound + * @see popShader + */ + void pushShader(GLShader *shader); + + /** + * Unbinds the currently bound shader and rebinds a previous stored shader. + * If there is no previous shader, no shader will be rebound. + * It is not safe to call this method if there is no bound shader. + * @see pushShader + * @see getBoundShader + */ + void popShader(); + + /** + * Creates a GLShader with the specified sources. + * The difference to GLShader is that it does not need to be loaded from files. + * @param vertexSource The source code of the vertex shader + * @param fragmentSource The source code of the fragment shader. + * @return The created shader + */ + std::unique_ptr loadShaderFromCode(const QByteArray &vertexSource, const QByteArray &fragmentSource); + + /** + * Creates a custom shader with the given @p traits and custom @p vertexSource and or @p fragmentSource. + * If the @p vertexSource is empty a vertex shader with the given @p traits is generated. + * If it is not empty the @p vertexSource is used as the source for the vertex shader. + * + * The same applies for argument @p fragmentSource just for the fragment shader. + * + * So if both @p vertesSource and @p fragmentSource are provided the @p traits are ignored. + * If neither are provided a new shader following the @p traits is generated. + * + * @param traits The shader traits for generating the shader + * @param vertexSource optional vertex shader source code to be used instead of shader traits + * @param fragmentSource optional fragment shader source code to be used instead of shader traits + * @return new generated shader + * @since 5.6 + */ + std::unique_ptr generateCustomShader(ShaderTraits traits, const QByteArray &vertexSource = QByteArray(), const QByteArray &fragmentSource = QByteArray()); + + /** + * Creates a custom shader with the given @p traits and custom @p vertexFile and or @p fragmentFile. + * + * If the @p vertexFile is empty a vertex shader with the given @p traits is generated. + * If it is not empty the @p vertexFile is used as the source for the vertex shader. + * + * The same applies for argument @p fragmentFile just for the fragment shader. + * + * So if both @p vertexFile and @p fragmentFile are provided the @p traits are ignored. + * If neither are provided a new shader following the @p traits is generated. + * + * If a custom shader stage is provided and core profile is used, the final file path will + * be resolved by appending "_core" to the basename. + * + * @param traits The shader traits for generating the shader + * @param vertexFile optional vertex shader source code to be used instead of shader traits + * @param fragmentFile optional fragment shader source code to be used instead of shader traits + * @return new generated shader + * @see generateCustomShader + */ + std::unique_ptr generateShaderFromFile(ShaderTraits traits, const QString &vertexFile = QString(), const QString &fragmentFile = QString()); + + /** + * @return a pointer to the ShaderManager instance + */ + static ShaderManager *instance(); + +private: + void bindFragDataLocations(GLShader *shader); + void bindAttributeLocations(GLShader *shader) const; + + std::optional preprocess(const QByteArray &src, int recursionDepth = 0) const; + QByteArray generateVertexSource(ShaderTraits traits) const; + QByteArray generateFragmentSource(ShaderTraits traits) const; + std::unique_ptr generateShader(ShaderTraits traits); + + QStack m_boundShaders; + std::map> m_shaderHash; +}; + +/** + * An helper class to push a Shader on to ShaderManager's stack and ensuring that the Shader + * gets popped again from the stack automatically once the object goes out of life. + * + * How to use: + * @code + * { + * GLShader *myCustomShaderIWantToPush; + * ShaderBinder binder(myCustomShaderIWantToPush); + * // do stuff with the shader being pushed on the stack + * } + * // here the Shader is automatically popped as helper does no longer exist. + * @endcode + * + * @since 4.10 + */ +class KWIN_EXPORT ShaderBinder +{ +public: + /** + * @brief Pushes the given @p shader to the ShaderManager's stack. + * + * @param shader The Shader to push on the stack + * @see ShaderManager::pushShader + */ + explicit ShaderBinder(GLShader *shader); + /** + * @brief Pushes the Shader with the given @p traits to the ShaderManager's stack. + * + * @param traits The traits describing the shader + * @see ShaderManager::pushShader + * @since 5.6 + */ + explicit ShaderBinder(ShaderTraits traits); + ~ShaderBinder(); + + /** + * @return The Shader pushed to the Stack. + */ + GLShader *shader(); + +private: + GLShader *m_shader; +}; + +inline ShaderBinder::ShaderBinder(GLShader *shader) + : m_shader(shader) +{ + ShaderManager::instance()->pushShader(shader); +} + +inline ShaderBinder::ShaderBinder(ShaderTraits traits) + : m_shader(nullptr) +{ + m_shader = ShaderManager::instance()->pushShader(traits); +} + +inline ShaderBinder::~ShaderBinder() +{ + ShaderManager::instance()->popShader(); +} + +inline GLShader *ShaderBinder::shader() +{ + return m_shader; +} + +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::ShaderTraits) diff --git a/local/recipes/kde/kwin/source/src/opengl/gltexture.cpp b/local/recipes/kde/kwin/source/src/opengl/gltexture.cpp new file mode 100644 index 0000000000..4cf6b461b1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/gltexture.cpp @@ -0,0 +1,586 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006-2007 Rivo Laks + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2012 Philipp Knechtges + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "gltexture_p.h" +#include "opengl/glframebuffer.h" +#include "opengl/glplatform.h" +#include "opengl/glutils.h" +#include "utils/common.h" + +#include +#include +#include +#include +#include + +namespace KWin +{ + +// Table of GL formats/types associated with different values of QImage::Format. +// Zero values indicate a direct upload is not feasible. +// +// Note: Blending is set up to expect premultiplied data, so the non-premultiplied +// Format_ARGB32 must be converted to Format_ARGB32_Premultiplied ahead of time. +struct +{ + GLenum internalFormat; + GLenum format; + GLenum type; +} static const formatTable[] = { + {0, 0, 0}, // QImage::Format_Invalid + {0, 0, 0}, // QImage::Format_Mono + {0, 0, 0}, // QImage::Format_MonoLSB + {0, 0, 0}, // QImage::Format_Indexed8 + {GL_RGB8, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV}, // QImage::Format_RGB32 + {0, 0, 0}, // QImage::Format_ARGB32 + {GL_RGBA8, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV}, // QImage::Format_ARGB32_Premultiplied + {GL_RGB8, GL_RGB, GL_UNSIGNED_SHORT_5_6_5}, // QImage::Format_RGB16 + {0, 0, 0}, // QImage::Format_ARGB8565_Premultiplied + {0, 0, 0}, // QImage::Format_RGB666 + {0, 0, 0}, // QImage::Format_ARGB6666_Premultiplied + {GL_RGB5, GL_BGRA, GL_UNSIGNED_SHORT_1_5_5_5_REV}, // QImage::Format_RGB555 + {0, 0, 0}, // QImage::Format_ARGB8555_Premultiplied + {GL_RGB8, GL_RGB, GL_UNSIGNED_BYTE}, // QImage::Format_RGB888 + {GL_RGB4, GL_BGRA, GL_UNSIGNED_SHORT_4_4_4_4_REV}, // QImage::Format_RGB444 + {GL_RGBA4, GL_BGRA, GL_UNSIGNED_SHORT_4_4_4_4_REV}, // QImage::Format_ARGB4444_Premultiplied + {GL_RGB8, GL_RGBA, GL_UNSIGNED_BYTE}, // QImage::Format_RGBX8888 + {0, 0, 0}, // QImage::Format_RGBA8888 + {GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE}, // QImage::Format_RGBA8888_Premultiplied + {GL_RGB10, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV}, // QImage::Format_BGR30 + {GL_RGB10_A2, GL_RGBA, GL_UNSIGNED_INT_2_10_10_10_REV}, // QImage::Format_A2BGR30_Premultiplied + {GL_RGB10, GL_BGRA, GL_UNSIGNED_INT_2_10_10_10_REV}, // QImage::Format_RGB30 + {GL_RGB10_A2, GL_BGRA, GL_UNSIGNED_INT_2_10_10_10_REV}, // QImage::Format_A2RGB30_Premultiplied + {GL_R8, GL_RED, GL_UNSIGNED_BYTE}, // QImage::Format_Alpha8 + {GL_R8, GL_RED, GL_UNSIGNED_BYTE}, // QImage::Format_Grayscale8 + {GL_RGBA16, GL_RGBA, GL_UNSIGNED_SHORT}, // QImage::Format_RGBX64 + {0, 0, 0}, // QImage::Format_RGBA64 + {GL_RGBA16, GL_RGBA, GL_UNSIGNED_SHORT}, // QImage::Format_RGBA64_Premultiplied + {GL_R16, GL_RED, GL_UNSIGNED_SHORT}, // QImage::Format_Grayscale16 + {0, 0, 0}, // QImage::Format_BGR888 +}; + +GLTexture::GLTexture(GLenum target) + : d(std::make_unique()) +{ + d->m_target = target; +} + +GLTexture::GLTexture(GLenum target, GLuint textureId, GLenum internalFormat, const QSize &size, int levels, bool owning, OutputTransform transform) + : GLTexture(target) +{ + d->m_owning = owning; + d->m_texture = textureId; + d->m_scale.setWidth(1.0 / size.width()); + d->m_scale.setHeight(1.0 / size.height()); + d->m_size = size; + d->m_canUseMipmaps = levels > 1; + d->m_mipLevels = levels; + d->m_filter = levels > 1 ? GL_NEAREST_MIPMAP_LINEAR : GL_NEAREST; + d->m_internalFormat = internalFormat; + d->m_textureToBufferTransform = transform; + + d->updateMatrix(); +} + +GLTexture::~GLTexture() +{ +} + +bool GLTexture::create() +{ + if (!isNull()) { + return true; + } + glGenTextures(1, &d->m_texture); + return d->m_texture != GL_NONE; +} + +GLTexturePrivate::GLTexturePrivate() + : m_texture(0) + , m_target(0) + , m_internalFormat(0) + , m_filter(GL_NEAREST) + , m_wrapMode(GL_REPEAT) + , m_canUseMipmaps(false) + , m_filterChanged(true) + , m_wrapModeChanged(false) + , m_owning(true) + , m_mipLevels(1) + , m_unnormalizeActive(0) + , m_normalizeActive(0) +{ +} + +GLTexturePrivate::~GLTexturePrivate() +{ + if (!EglContext::currentContext()) { + qCWarning(KWIN_OPENGL, "Could not delete texture because no context is current"); + return; + } + if (m_texture != 0 && m_owning) { + glDeleteTextures(1, &m_texture); + } +} + +bool GLTexture::isNull() const +{ + return GL_NONE == d->m_texture; +} + +QSize GLTexture::size() const +{ + return d->m_size; +} + +void GLTexture::setSize(const QSize &size) +{ + if (!isNull()) { + return; + } + d->m_size = size; + d->updateMatrix(); +} + +void GLTexture::update(const QImage &image, const Region ®ion, const QPoint &offset) +{ + if (image.isNull() || isNull()) { + return; + } + + Q_ASSERT(d->m_owning); + + const auto context = EglContext::currentContext(); + GLenum glFormat; + GLenum type; + QImage::Format uploadFormat; + if (!context->isOpenGLES()) { + const QImage::Format index = image.format(); + + if (index < sizeof(formatTable) / sizeof(formatTable[0]) && formatTable[index].internalFormat) { + glFormat = formatTable[index].format; + type = formatTable[index].type; + uploadFormat = index; + } else { + glFormat = GL_BGRA; + type = GL_UNSIGNED_INT_8_8_8_8_REV; + uploadFormat = QImage::Format_ARGB32_Premultiplied; + } + } else { + if (context->supportsARGB32Textures()) { + glFormat = GL_BGRA_EXT; + type = GL_UNSIGNED_BYTE; + uploadFormat = QImage::Format_ARGB32_Premultiplied; + } else { + glFormat = GL_RGBA; + type = GL_UNSIGNED_BYTE; + uploadFormat = QImage::Format_RGBA8888_Premultiplied; + } + } + + QImage im = image; + if (im.format() != uploadFormat) { + im.convertTo(uploadFormat); + } + + bind(); + + for (const Rect &rect : region.rects()) { + Q_ASSERT(im.depth() % 8 == 0); + glPixelStorei(GL_UNPACK_ROW_LENGTH, im.bytesPerLine() / (im.depth() / 8)); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, rect.x()); + glPixelStorei(GL_UNPACK_SKIP_ROWS, rect.y()); + + glTexSubImage2D(d->m_target, 0, offset.x() + rect.x(), offset.y() + rect.y(), rect.width(), rect.height(), glFormat, type, im.constBits()); + } + + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); + + unbind(); +} + +void GLTexture::bind() +{ + Q_ASSERT(d->m_texture); + + glBindTexture(d->m_target, d->m_texture); + + if (d->m_filterChanged) { + GLenum minFilter = GL_NEAREST; + GLenum magFilter = GL_NEAREST; + + switch (d->m_filter) { + case GL_NEAREST: + minFilter = magFilter = GL_NEAREST; + break; + + case GL_LINEAR: + minFilter = magFilter = GL_LINEAR; + break; + + case GL_NEAREST_MIPMAP_NEAREST: + case GL_NEAREST_MIPMAP_LINEAR: + magFilter = GL_NEAREST; + minFilter = d->m_canUseMipmaps ? d->m_filter : GL_NEAREST; + break; + + case GL_LINEAR_MIPMAP_NEAREST: + case GL_LINEAR_MIPMAP_LINEAR: + magFilter = GL_LINEAR; + minFilter = d->m_canUseMipmaps ? d->m_filter : GL_LINEAR; + break; + } + + glTexParameteri(d->m_target, GL_TEXTURE_MIN_FILTER, minFilter); + glTexParameteri(d->m_target, GL_TEXTURE_MAG_FILTER, magFilter); + + d->m_filterChanged = false; + } + if (d->m_wrapModeChanged) { + glTexParameteri(d->m_target, GL_TEXTURE_WRAP_S, d->m_wrapMode); + glTexParameteri(d->m_target, GL_TEXTURE_WRAP_T, d->m_wrapMode); + d->m_wrapModeChanged = false; + } +} + +void GLTexture::generateMipmaps() +{ + if (d->m_canUseMipmaps) { + glGenerateMipmap(d->m_target); + } +} + +void GLTexture::unbind() +{ + glBindTexture(d->m_target, 0); +} + +void GLTexture::render(const QSizeF &size) +{ + render(Region::infinite(), size, false); +} + +void GLTexture::render(const Region ®ion, const QSizeF &targetSize, bool hardwareClipping) +{ + const auto rotatedSize = d->m_textureToBufferTransform.map(size()); + render(RectF(QPoint(), rotatedSize), region, targetSize, hardwareClipping); +} + +void GLTexture::render(const RectF &source, const Region ®ion, const QSizeF &targetSize, bool hardwareClipping) +{ + if (targetSize.isEmpty()) { + return; // nothing to paint and m_vbo is likely nullptr and d->m_cachedSize empty as well, #337090 + } + + const QSize destinationSize = targetSize.toSize(); // TODO: toSize is not enough to snap to the pixel grid, fix render() users and drop this toSize + if (destinationSize != d->m_cachedSize || d->m_cachedSource != source || d->m_cachedContentTransform != d->m_textureToBufferTransform) { + d->m_cachedSize = destinationSize; + d->m_cachedSource = source; + d->m_cachedContentTransform = d->m_textureToBufferTransform; + + const float texWidth = (target() == GL_TEXTURE_RECTANGLE_ARB) ? width() : 1.0f; + const float texHeight = (target() == GL_TEXTURE_RECTANGLE_ARB) ? height() : 1.0f; + + const QSize rotatedSize = d->m_textureToBufferTransform.map(size()); + + QMatrix4x4 textureMat; + textureMat.translate(texWidth / 2, texHeight / 2); + // our Y axis is flipped vs OpenGL + textureMat.scale(1, -1); + textureMat *= d->m_textureToBufferTransform.toMatrix(); + textureMat.translate(-texWidth / 2, -texHeight / 2); + textureMat.scale(texWidth / rotatedSize.width(), texHeight / rotatedSize.height()); + + const QPointF p1 = textureMat.map(QPointF(source.x(), source.y())); + const QPointF p2 = textureMat.map(QPointF(source.x(), source.y() + source.height())); + const QPointF p3 = textureMat.map(QPointF(source.x() + source.width(), source.y())); + const QPointF p4 = textureMat.map(QPointF(source.x() + source.width(), source.y() + source.height())); + + if (!d->m_vbo) { + d->m_vbo = std::make_unique(KWin::GLVertexBuffer::Static); + } + const std::array data{ + GLVertex2D{ + .position = QVector2D(0, 0), + .texcoord = QVector2D(p1), + }, + GLVertex2D{ + .position = QVector2D(0, destinationSize.height()), + .texcoord = QVector2D(p2), + }, + GLVertex2D{ + .position = QVector2D(destinationSize.width(), 0), + .texcoord = QVector2D(p3), + }, + GLVertex2D{ + .position = QVector2D(destinationSize.width(), destinationSize.height()), + .texcoord = QVector2D(p4), + }, + }; + d->m_vbo->setVertices(data); + } + bind(); + d->m_vbo->render(region, GL_TRIANGLE_STRIP, hardwareClipping); + unbind(); +} + +GLuint GLTexture::texture() const +{ + return d->m_texture; +} + +GLenum GLTexture::target() const +{ + return d->m_target; +} + +GLenum GLTexture::filter() const +{ + return d->m_filter; +} + +GLenum GLTexture::internalFormat() const +{ + return d->m_internalFormat; +} + +void GLTexture::setFilter(GLenum filter) +{ + if (filter != d->m_filter) { + d->m_filter = filter; + d->m_filterChanged = true; + } +} + +void GLTexture::setWrapMode(GLenum mode) +{ + if (mode != d->m_wrapMode) { + d->m_wrapMode = mode; + d->m_wrapModeChanged = true; + } +} + +void GLTexturePrivate::updateMatrix() +{ + const QMatrix4x4 textureToBufferMatrix = m_textureToBufferTransform.toMatrix(); + + m_matrix[NormalizedCoordinates].setToIdentity(); + m_matrix[UnnormalizedCoordinates].setToIdentity(); + + if (m_target == GL_TEXTURE_RECTANGLE_ARB) { + m_matrix[NormalizedCoordinates].scale(m_size.width(), m_size.height()); + } else { + m_matrix[UnnormalizedCoordinates].scale(1.0 / m_size.width(), 1.0 / m_size.height()); + } + + m_matrix[NormalizedCoordinates].translate(0.5, 0.5); + // our Y axis is flipped vs OpenGL + m_matrix[NormalizedCoordinates].scale(1, -1); + m_matrix[NormalizedCoordinates] *= textureToBufferMatrix; + m_matrix[NormalizedCoordinates].translate(-0.5, -0.5); + + m_matrix[UnnormalizedCoordinates].translate(m_size.width() / 2, m_size.height() / 2); + m_matrix[UnnormalizedCoordinates].scale(1, -1); + m_matrix[UnnormalizedCoordinates] *= textureToBufferMatrix; + m_matrix[UnnormalizedCoordinates].translate(-m_size.width() / 2, -m_size.height() / 2); +} + +void GLTexture::setContentTransform(OutputTransform transform) +{ + if (d->m_textureToBufferTransform != transform) { + d->m_textureToBufferTransform = transform; + d->updateMatrix(); + } +} + +OutputTransform GLTexture::contentTransform() const +{ + return d->m_textureToBufferTransform; +} + +void GLTexture::setSwizzle(GLenum red, GLenum green, GLenum blue, GLenum alpha) +{ + if (!EglContext::currentContext()->isOpenGLES()) { + const GLuint swizzle[] = {red, green, blue, alpha}; + glTexParameteriv(d->m_target, GL_TEXTURE_SWIZZLE_RGBA, (const GLint *)swizzle); + } else { + glTexParameteri(d->m_target, GL_TEXTURE_SWIZZLE_R, red); + glTexParameteri(d->m_target, GL_TEXTURE_SWIZZLE_G, green); + glTexParameteri(d->m_target, GL_TEXTURE_SWIZZLE_B, blue); + glTexParameteri(d->m_target, GL_TEXTURE_SWIZZLE_A, alpha); + } +} + +int GLTexture::width() const +{ + return d->m_size.width(); +} + +int GLTexture::height() const +{ + return d->m_size.height(); +} + +QMatrix4x4 GLTexture::matrix(TextureCoordinateType type) const +{ + return d->m_matrix[type]; +} + +bool GLTexture::supportsSwizzle() +{ + return EglContext::currentContext()->supportsTextureSwizzle(); +} + +bool GLTexture::supportsFormatRG() +{ + return EglContext::currentContext()->supportsRGTextures(); +} + +QImage GLTexture::toImage() +{ + if (target() != GL_TEXTURE_2D) { + return QImage(); + } + QImage ret(size(), QImage::Format_RGBA8888_Premultiplied); + + if (EglContext::currentContext()->isOpenGLES()) { + GLFramebuffer fbo(this); + GLFramebuffer::pushFramebuffer(&fbo); + glReadPixels(0, 0, width(), height(), GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, ret.bits()); + GLFramebuffer::popFramebuffer(); + } else { + GLint currentTextureBinding; + glGetIntegerv(GL_TEXTURE_BINDING_2D, ¤tTextureBinding); + if (GLuint(currentTextureBinding) != texture()) { + glBindTexture(GL_TEXTURE_2D, texture()); + } + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, ret.bits()); + if (GLuint(currentTextureBinding) != texture()) { + glBindTexture(GL_TEXTURE_2D, currentTextureBinding); + } + } + return ret; +} + +std::unique_ptr GLTexture::createNonOwningWrapper(GLuint textureId, GLenum internalFormat, const QSize &size) +{ + return std::unique_ptr(new GLTexture(GL_TEXTURE_2D, textureId, internalFormat, size, 1, false, OutputTransform{})); +} + +std::unique_ptr GLTexture::allocate(GLenum internalFormat, const QSize &size, int levels) +{ + GLuint texture = 0; + glGenTextures(1, &texture); + if (texture == 0) { + qCWarning(KWIN_OPENGL, "generating OpenGL texture handle failed"); + return nullptr; + } + glBindTexture(GL_TEXTURE_2D, texture); + + const auto context = EglContext::currentContext(); + if (!context->isOpenGLES()) { + if (context->supportsTextureStorage()) { + glTexStorage2D(GL_TEXTURE_2D, levels, internalFormat, size.width(), size.height()); + } else { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, levels - 1); + glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, size.width(), size.height(), 0, + GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, nullptr); + } + } else { + // The format parameter in glTexSubImage() must match the internal format + // of the texture, so it's important that we allocate the texture with + // the format that will be used in update() and clear(). + const GLenum format = context->supportsARGB32Textures() ? GL_BGRA_EXT : GL_RGBA; + glTexImage2D(GL_TEXTURE_2D, 0, format, size.width(), size.height(), 0, + format, GL_UNSIGNED_BYTE, nullptr); + + // The internalFormat is technically not correct, but it means that code that calls + // internalFormat() won't need to be specialized for GLES2. + } + glBindTexture(GL_TEXTURE_2D, 0); + return std::unique_ptr(new GLTexture(GL_TEXTURE_2D, texture, internalFormat, size, levels, true, OutputTransform{})); +} + +std::unique_ptr GLTexture::upload(const QImage &image) +{ + if (image.isNull()) { + return nullptr; + } + GLuint texture = 0; + glGenTextures(1, &texture); + if (texture == 0) { + qCWarning(KWIN_OPENGL, "generating OpenGL texture handle failed"); + return nullptr; + } + + const auto context = EglContext::currentContext(); + GLenum internalFormat; + GLenum format; + GLenum type; + QImage::Format uploadFormat; + if (!context->isOpenGLES()) { + const QImage::Format index = image.format(); + if (index < sizeof(formatTable) / sizeof(formatTable[0]) && formatTable[index].internalFormat) { + internalFormat = formatTable[index].internalFormat; + format = formatTable[index].format; + type = formatTable[index].type; + uploadFormat = index; + } else { + internalFormat = GL_RGBA8; + format = GL_BGRA; + type = GL_UNSIGNED_INT_8_8_8_8_REV; + uploadFormat = QImage::Format_ARGB32_Premultiplied; + } + } else { + if (context->supportsARGB32Textures()) { + internalFormat = GL_BGRA_EXT; + format = GL_BGRA_EXT; + type = GL_UNSIGNED_BYTE; + uploadFormat = QImage::Format_ARGB32_Premultiplied; + } else { + internalFormat = GL_RGBA; + format = GL_RGBA; + type = GL_UNSIGNED_BYTE; + uploadFormat = QImage::Format_RGBA8888_Premultiplied; + } + } + + QImage im = image; + if (im.format() != uploadFormat) { + im.convertTo(uploadFormat); + } + + glBindTexture(GL_TEXTURE_2D, texture); + glPixelStorei(GL_UNPACK_ROW_LENGTH, im.bytesPerLine() / (im.depth() / 8)); + if (!context->isOpenGLES()) { + if (context->supportsTextureStorage()) { + glTexStorage2D(GL_TEXTURE_2D, 1, internalFormat, im.width(), im.height()); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, im.width(), im.height(), format, type, im.constBits()); + } else { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); + glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, im.width(), im.height(), 0, format, type, im.constBits()); + } + } else { + glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, im.width(), im.height(), 0, format, type, im.constBits()); + } + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + glBindTexture(GL_TEXTURE_2D, 0); + + return std::unique_ptr(new GLTexture(GL_TEXTURE_2D, texture, internalFormat, image.size(), 1, true, OutputTransform::FlipY)); +} + +std::unique_ptr GLTexture::upload(const QPixmap &pixmap) +{ + return upload(pixmap.toImage()); +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/opengl/gltexture.h b/local/recipes/kde/kwin/source/src/opengl/gltexture.h new file mode 100644 index 0000000000..bb707627d7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/gltexture.h @@ -0,0 +1,136 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006-2007 Rivo Laks + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/output.h" + +#include +#include +#include + +#include + +class QImage; +class QPixmap; + +/** @addtogroup kwineffects */ +/** @{ */ + +namespace KWin +{ + +class GLVertexBuffer; +class GLTexturePrivate; + +enum TextureCoordinateType { + NormalizedCoordinates = 0, + UnnormalizedCoordinates, +}; + +class KWIN_EXPORT GLTexture +{ +public: + explicit GLTexture(GLenum target); + + /** + * Creates the underlying texture object. Returns @c true if the texture has been created + * successfully; otherwise returns @c false. Note that this does not allocate any storage + * for the texture. + */ + bool create(); + virtual ~GLTexture(); + + bool isNull() const; + QSize size() const; + void setSize(const QSize &size); + int width() const; + int height() const; + + /** + * sets the transform between the content and the buffer + */ + void setContentTransform(OutputTransform transform); + + /** + * @returns the transform between the content and the buffer + */ + OutputTransform contentTransform() const; + + /** + * Specifies which component of a texel is placed in each respective + * component of the vector returned to the shader. + * + * Valid values are GL_RED, GL_GREEN, GL_BLUE, GL_ALPHA, GL_ONE and GL_ZERO. + * + * @see swizzleSupported() + * @since 5.2 + */ + void setSwizzle(GLenum red, GLenum green, GLenum blue, GLenum alpha); + + /** + * Returns a matrix that transforms texture coordinates of the given type, + * taking the texture target and the y-inversion flag into account. + * + * @since 4.11 + */ + QMatrix4x4 matrix(TextureCoordinateType type) const; + + void update(const QImage &image, const Region ®ion, const QPoint &offset = QPoint()); + void bind(); + void unbind(); + void render(const QSizeF &size); + void render(const Region ®ion, const QSizeF &size, bool hardwareClipping = false); + void render(const RectF &source, const Region ®ion, const QSizeF &targetSize, bool hardwareClipping = false); + + GLuint texture() const; + GLenum target() const; + GLenum filter() const; + GLenum internalFormat() const; + + QImage toImage(); + + void setFilter(GLenum filter); + void setWrapMode(GLenum mode); + + void generateMipmaps(); + + /** + * Returns true if texture swizzle is supported, and false otherwise + * + * Texture swizzle requires OpenGL 3.3, GL_ARB_texture_swizzle, or OpenGL ES 3.0. + * + * @since 5.2 + */ + static bool supportsSwizzle(); + + /** + * Returns @c true if texture formats R* are supported, and @c false otherwise. + * + * This requires OpenGL 3.0, GL_ARB_texture_rg or OpenGL ES 3.0 or GL_EXT_texture_rg. + * + * @since 5.2.1 + */ + static bool supportsFormatRG(); + + static std::unique_ptr createNonOwningWrapper(GLuint textureId, GLenum internalFormat, const QSize &size); + static std::unique_ptr allocate(GLenum internalFormat, const QSize &size, int levels = 1); + static std::unique_ptr upload(const QImage &image); + static std::unique_ptr upload(const QPixmap &pixmap); + +protected: + explicit GLTexture(GLenum target, GLuint textureId, GLenum internalFormat, const QSize &size, int levels, bool owning, OutputTransform transform); + + const std::unique_ptr d; +}; + +} // namespace + +/** @} */ diff --git a/local/recipes/kde/kwin/source/src/opengl/gltexture_p.h b/local/recipes/kde/kwin/source/src/opengl/gltexture_p.h new file mode 100644 index 0000000000..fee5789db2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/gltexture_p.h @@ -0,0 +1,61 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006-2007 Rivo Laks + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2011 Philipp Knechtges + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "opengl/glutils.h" + +#include +#include +#include +#include +#include + +namespace KWin +{ +// forward declarations +class GLVertexBuffer; + +class KWIN_EXPORT GLTexturePrivate + : public QSharedData +{ +public: + GLTexturePrivate(); + virtual ~GLTexturePrivate(); + + void updateMatrix(); + + GLuint m_texture; + GLenum m_target; + GLenum m_internalFormat; + GLenum m_filter; + GLenum m_wrapMode; + QSize m_size; + QSizeF m_scale; // to un-normalize GL_TEXTURE_2D + QMatrix4x4 m_matrix[2]; + OutputTransform m_textureToBufferTransform; + bool m_canUseMipmaps; + bool m_filterChanged; + bool m_wrapModeChanged; + bool m_owning; + int m_mipLevels; + + int m_unnormalizeActive; // 0 - no, otherwise refcount + int m_normalizeActive; // 0 - no, otherwise refcount + std::unique_ptr m_vbo; + QSizeF m_cachedSize; + RectF m_cachedSource; + OutputTransform m_cachedContentTransform; + + Q_DISABLE_COPY(GLTexturePrivate) +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/opengl/glutils.cpp b/local/recipes/kde/kwin/source/src/opengl/glutils.cpp new file mode 100644 index 0000000000..dd1b2eee24 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/glutils.cpp @@ -0,0 +1,61 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006-2007 Rivo Laks + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "opengl/glutils.h" +#include "glplatform.h" +#include "gltexture_p.h" +#include "utils/common.h" + +namespace KWin +{ + +static QString formatGLError(GLenum err) +{ + switch (err) { + case GL_NO_ERROR: + return QStringLiteral("GL_NO_ERROR"); + case GL_INVALID_ENUM: + return QStringLiteral("GL_INVALID_ENUM"); + case GL_INVALID_VALUE: + return QStringLiteral("GL_INVALID_VALUE"); + case GL_INVALID_OPERATION: + return QStringLiteral("GL_INVALID_OPERATION"); + case GL_STACK_OVERFLOW: + return QStringLiteral("GL_STACK_OVERFLOW"); + case GL_STACK_UNDERFLOW: + return QStringLiteral("GL_STACK_UNDERFLOW"); + case GL_OUT_OF_MEMORY: + return QStringLiteral("GL_OUT_OF_MEMORY"); + default: + return QLatin1String("0x") + QString::number(err, 16); + } +} + +bool checkGLError(const char *txt) +{ + GLenum err = glGetError(); + if (err == GL_CONTEXT_LOST) { + qCWarning(KWIN_OPENGL) << "GL error: context lost"; + return true; + } + bool hasError = false; + while (err != GL_NO_ERROR) { + qCWarning(KWIN_OPENGL) << "GL error (" << txt << "): " << formatGLError(err); + hasError = true; + err = glGetError(); + if (err == GL_CONTEXT_LOST) { + qCWarning(KWIN_OPENGL) << "GL error: context lost"; + break; + } + } + return hasError; +} + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/opengl/glutils.h b/local/recipes/kde/kwin/source/src/opengl/glutils.h new file mode 100644 index 0000000000..d048c0f901 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/glutils.h @@ -0,0 +1,33 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006-2007 Rivo Laks + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "core/colorspace.h" +#include "opengl/eglcontext.h" +#include "opengl/glframebuffer.h" +#include "opengl/glshader.h" +#include "opengl/glshadermanager.h" +#include "opengl/gltexture.h" +#include "opengl/glvertexbuffer.h" + +#include +#include +#include + +namespace KWin +{ + +// detect OpenGL error (add to various places in code to pinpoint the place) +bool KWIN_EXPORT checkGLError(const char *txt); + +} // namespace + +/** @} */ diff --git a/local/recipes/kde/kwin/source/src/opengl/glvertexbuffer.cpp b/local/recipes/kde/kwin/source/src/opengl/glvertexbuffer.cpp new file mode 100644 index 0000000000..46df09b650 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/glvertexbuffer.cpp @@ -0,0 +1,595 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006-2007 Rivo Laks + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "glvertexbuffer.h" +#include "glframebuffer.h" +#include "glplatform.h" +#include "glshader.h" +#include "glshadermanager.h" +#include "glutils.h" +#include "glvertexbuffer_p.h" +#include "utils/common.h" + +#include +#include +#include + +namespace KWin +{ + +// Certain GPUs, especially mobile, require the data copied to the GPU to be aligned to a +// certain amount of bytes. For example, the Mali GPU requires data to be aligned to 8 bytes. +// This function helps ensure that the data is aligned. +template +T align(T value, int bytes) +{ + return (value + bytes - 1) & ~T(bytes - 1); +} + +IndexBuffer::IndexBuffer() +{ + // The maximum number of quads we can render with 16 bit indices is 16,384. + // But we start with 512 and grow the buffer as needed. + glGenBuffers(1, &m_buffer); + accommodate(512); +} + +IndexBuffer::~IndexBuffer() +{ + if (!EglContext::currentContext()) { + qCWarning(KWIN_OPENGL, "Could not delete index buffer because no context is current"); + return; + } + glDeleteBuffers(1, &m_buffer); +} + +void IndexBuffer::accommodate(size_t count) +{ + // Check if we need to grow the buffer. + if (count <= m_count) { + return; + } + Q_ASSERT(m_count * 2 < std::numeric_limits::max() / 4); + const size_t oldCount = m_count; + m_count *= 2; + m_data.reserve(m_count * 6); + for (size_t i = oldCount; i < m_count; i++) { + const uint16_t offset = i * 4; + m_data[i * 6 + 0] = offset + 1; + m_data[i * 6 + 1] = offset + 0; + m_data[i * 6 + 2] = offset + 3; + m_data[i * 6 + 3] = offset + 3; + m_data[i * 6 + 4] = offset + 2; + m_data[i * 6 + 5] = offset + 1; + } + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_buffer); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_count * sizeof(uint16_t), m_data.data(), GL_STATIC_DRAW); +} + +void IndexBuffer::bind() +{ + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_buffer); +} + +// ------------------------------------------------------------------ + +struct VertexAttrib +{ + int size; + GLenum type; + int offset; +}; + +// ------------------------------------------------------------------ + +struct BufferFence +{ + GLsync sync; + intptr_t nextEnd; + + bool signaled() const + { + GLint value; + glGetSynciv(sync, GL_SYNC_STATUS, 1, nullptr, &value); + return value == GL_SIGNALED; + } +}; + +static void deleteAll(std::deque &fences) +{ + for (const BufferFence &fence : fences) { + glDeleteSync(fence.sync); + } + + fences.clear(); +} + +// ------------------------------------------------------------------ + +template +struct FrameSizesArray +{ +public: + FrameSizesArray() + { + m_array.fill(0); + } + + void push(size_t size) + { + m_array[m_index] = size; + m_index = (m_index + 1) % Count; + } + + size_t average() const + { + size_t sum = 0; + for (size_t size : m_array) { + sum += size; + } + return sum / Count; + } + +private: + std::array m_array; + int m_index = 0; +}; + +//********************************* +// GLVertexBufferPrivate +//********************************* +class GLVertexBufferPrivate +{ +public: + GLVertexBufferPrivate(GLVertexBuffer::UsageHint usageHint) + : vertexCount(0) + , persistent(false) + , bufferSize(0) + , bufferEnd(0) + , mappedSize(0) + , frameSize(0) + , nextOffset(0) + , baseAddress(0) + , map(nullptr) + { + glGenBuffers(1, &buffer); + + switch (usageHint) { + case GLVertexBuffer::Dynamic: + usage = GL_DYNAMIC_DRAW; + break; + case GLVertexBuffer::Static: + usage = GL_STATIC_DRAW; + break; + default: + usage = GL_STREAM_DRAW; + break; + } + } + + ~GLVertexBufferPrivate() + { + if (!EglContext::currentContext()) { + qCWarning(KWIN_OPENGL, "Could not delete vertex buffer because no context is current"); + return; + } + deleteAll(fences); + + if (buffer != 0) { + glDeleteBuffers(1, &buffer); + map = nullptr; + } + } + + void bindArrays(); + void unbindArrays(); + void reallocateBuffer(size_t size); + GLvoid *mapNextFreeRange(size_t size); + void reallocatePersistentBuffer(size_t size); + bool awaitFence(intptr_t offset); + GLvoid *getIdleRange(size_t size); + + GLuint buffer; + GLenum usage; + int vertexCount; + QByteArray dataStore; + bool persistent; + size_t bufferSize; + intptr_t bufferEnd; + size_t mappedSize; + size_t frameSize; + intptr_t nextOffset; + intptr_t baseAddress; + uint8_t *map; + std::deque fences; + FrameSizesArray<4> frameSizes; + std::array attrib; + size_t attribStride = 0; + std::bitset<32> enabledArrays; +}; + +void GLVertexBufferPrivate::bindArrays() +{ + glBindBuffer(GL_ARRAY_BUFFER, buffer); + + for (size_t i = 0; i < enabledArrays.size(); i++) { + if (enabledArrays[i]) { + glVertexAttribPointer(i, attrib[i].size, attrib[i].type, GL_FALSE, attribStride, + (const GLvoid *)(baseAddress + attrib[i].offset)); + glEnableVertexAttribArray(i); + } + } +} + +void GLVertexBufferPrivate::unbindArrays() +{ + for (size_t i = 0; i < enabledArrays.size(); i++) { + if (enabledArrays[i]) { + glDisableVertexAttribArray(i); + } + } +} + +void GLVertexBufferPrivate::reallocatePersistentBuffer(size_t size) +{ + if (buffer != 0) { + // This also unmaps and unbinds the buffer + glDeleteBuffers(1, &buffer); + buffer = 0; + + deleteAll(fences); + } + + if (buffer == 0) { + glGenBuffers(1, &buffer); + } + + // Round the size up to 64 kb + size_t minSize = std::max(frameSizes.average() * 3, 128 * 1024); + bufferSize = std::max(size, minSize); + + const GLbitfield storage = GL_DYNAMIC_STORAGE_BIT; + const GLbitfield access = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT; + + glBindBuffer(GL_ARRAY_BUFFER, buffer); + glBufferStorage(GL_ARRAY_BUFFER, bufferSize, nullptr, storage | access); + + map = (uint8_t *)glMapBufferRange(GL_ARRAY_BUFFER, 0, bufferSize, access); + + nextOffset = 0; + bufferEnd = bufferSize; +} + +bool GLVertexBufferPrivate::awaitFence(intptr_t end) +{ + // Skip fences until we reach the end offset + while (!fences.empty() && fences.front().nextEnd < end) { + glDeleteSync(fences.front().sync); + fences.pop_front(); + } + + // We may end up with no fences if a graphics reset occurs. + if (fences.empty()) { + return false; + } + + // Wait on the next fence + const BufferFence &fence = fences.front(); + + if (!fence.signaled()) { + qCDebug(KWIN_OPENGL) << "Stalling on VBO fence"; + const GLenum ret = glClientWaitSync(fence.sync, GL_SYNC_FLUSH_COMMANDS_BIT, 1000000000); + + if (ret == GL_TIMEOUT_EXPIRED || ret == GL_WAIT_FAILED) { + qCCritical(KWIN_OPENGL) << "Wait failed"; + return false; + } + } + + glDeleteSync(fence.sync); + + // Update the end pointer + bufferEnd = fence.nextEnd; + fences.pop_front(); + + return true; +} + +GLvoid *GLVertexBufferPrivate::getIdleRange(size_t size) +{ + if (size > bufferSize) { + reallocatePersistentBuffer(size * 2); + } + + // Handle wrap-around + if ((nextOffset + size > bufferSize)) { + if (auto sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0)) { + nextOffset = 0; + bufferEnd -= bufferSize; + + for (BufferFence &fence : fences) { + fence.nextEnd -= bufferSize; + } + + fences.push_back(BufferFence{ + .sync = sync, + .nextEnd = intptr_t(bufferSize)}); + } else { + return nullptr; + } + } + + if (nextOffset + intptr_t(size) > bufferEnd) { + if (!awaitFence(nextOffset + size)) { + return nullptr; + } + } + + return map + nextOffset; +} + +void GLVertexBufferPrivate::reallocateBuffer(size_t size) +{ + // Round the size up to 4 Kb for streaming/dynamic buffers. + const size_t minSize = 32768; // Minimum size for streaming buffers + const size_t alloc = usage != GL_STATIC_DRAW ? std::max(size, minSize) : size; + + glBufferData(GL_ARRAY_BUFFER, alloc, nullptr, usage); + + bufferSize = alloc; +} + +GLvoid *GLVertexBufferPrivate::mapNextFreeRange(size_t size) +{ + GLbitfield access = GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_RANGE_BIT | GL_MAP_UNSYNCHRONIZED_BIT; + + if ((nextOffset + size) > bufferSize) { + // Reallocate the data store if it's too small. + if (size > bufferSize) { + reallocateBuffer(size); + } else { + access |= GL_MAP_INVALIDATE_BUFFER_BIT; + access ^= GL_MAP_UNSYNCHRONIZED_BIT; + } + + nextOffset = 0; + } + + return glMapBufferRange(GL_ARRAY_BUFFER, nextOffset, size, access); +} + +GLVertexBuffer::GLVertexBuffer(UsageHint hint) + : d(std::make_unique(hint)) +{ +} + +GLVertexBuffer::~GLVertexBuffer() = default; + +void GLVertexBuffer::setData(const void *data, size_t size) +{ + GLvoid *ptr = map(size); + if (!ptr) { + return; + } + memcpy(ptr, data, size); + unmap(); +} + +GLvoid *GLVertexBuffer::map(size_t size) +{ + d->mappedSize = size; + d->frameSize += size; + + if (d->persistent) { + return d->getIdleRange(size); + } + + glBindBuffer(GL_ARRAY_BUFFER, d->buffer); + + const auto context = EglContext::currentContext(); + const bool preferBufferSubData = context->glPlatform()->preferBufferSubData(); + if (context->hasMapBufferRange() && !preferBufferSubData) { + return (GLvoid *)d->mapNextFreeRange(size); + } + + // If we can't map the buffer we allocate local memory to hold the + // buffer data and return a pointer to it. The data will be submitted + // to the actual buffer object when the user calls unmap(). + if (size_t(d->dataStore.size()) < size) { + d->dataStore.resize(size); + } + + return (GLvoid *)d->dataStore.data(); +} + +void GLVertexBuffer::unmap() +{ + if (d->persistent) { + d->baseAddress = d->nextOffset; + d->nextOffset += align(d->mappedSize, 8); + d->mappedSize = 0; + return; + } + + const auto context = EglContext::currentContext(); + const bool preferBufferSubData = context->glPlatform()->preferBufferSubData(); + + if (context->hasMapBufferRange() && !preferBufferSubData) { + glUnmapBuffer(GL_ARRAY_BUFFER); + + d->baseAddress = d->nextOffset; + d->nextOffset += align(d->mappedSize, 8); + } else { + // Upload the data from local memory to the buffer object + if (preferBufferSubData) { + if ((d->nextOffset + d->mappedSize) > d->bufferSize) { + d->reallocateBuffer(d->mappedSize); + d->nextOffset = 0; + } + + glBufferSubData(GL_ARRAY_BUFFER, d->nextOffset, d->mappedSize, d->dataStore.constData()); + + d->baseAddress = d->nextOffset; + d->nextOffset += align(d->mappedSize, 8); + } else { + glBufferData(GL_ARRAY_BUFFER, d->mappedSize, d->dataStore.data(), d->usage); + d->baseAddress = 0; + } + + // Free the local memory buffer if it's unlikely to be used again + if (d->usage == GL_STATIC_DRAW) { + d->dataStore = QByteArray(); + } + } + + d->mappedSize = 0; +} + +void GLVertexBuffer::setVertexCount(int count) +{ + d->vertexCount = count; +} + +void GLVertexBuffer::setAttribLayout(std::span attribs, size_t stride) +{ + d->enabledArrays.reset(); + for (const auto &attrib : attribs) { + Q_ASSERT(attrib.attributeIndex < d->attrib.size()); + d->attrib[attrib.attributeIndex].size = attrib.componentCount; + d->attrib[attrib.attributeIndex].type = attrib.type; + d->attrib[attrib.attributeIndex].offset = attrib.relativeOffset; + d->enabledArrays[attrib.attributeIndex] = true; + } + d->attribStride = stride; +} + +void GLVertexBuffer::render(GLenum primitiveMode) +{ + render(Region::infinite(), primitiveMode, false); +} + +void GLVertexBuffer::render(const Region ®ion, GLenum primitiveMode, bool hardwareClipping) +{ + d->bindArrays(); + draw(region, primitiveMode, 0, d->vertexCount, hardwareClipping); + d->unbindArrays(); +} + +void GLVertexBuffer::bindArrays() +{ + d->bindArrays(); +} + +void GLVertexBuffer::unbindArrays() +{ + d->unbindArrays(); +} + +void GLVertexBuffer::draw(GLenum primitiveMode, int first, int count) +{ + draw(Region::infinite(), primitiveMode, first, count, false); +} + +void GLVertexBuffer::draw(const Region ®ion, GLenum primitiveMode, int first, int count, bool hardwareClipping) +{ + if (primitiveMode == GL_QUADS) { + EglContext::currentContext()->indexBuffer()->bind(); + EglContext::currentContext()->indexBuffer()->accommodate(count / 4); + + count = count * 6 / 4; + + if (!hardwareClipping) { + glDrawElementsBaseVertex(GL_TRIANGLES, count, GL_UNSIGNED_SHORT, nullptr, first); + } else { + // Clip using scissoring + const GLFramebuffer *current = GLFramebuffer::currentFramebuffer(); + for (const Rect &r : region.rects()) { + glScissor(r.x(), current->size().height() - (r.y() + r.height()), r.width(), r.height()); + glDrawElementsBaseVertex(GL_TRIANGLES, count, GL_UNSIGNED_SHORT, nullptr, first); + } + } + return; + } + + if (!hardwareClipping) { + glDrawArrays(primitiveMode, first, count); + } else { + // Clip using scissoring + const GLFramebuffer *current = GLFramebuffer::currentFramebuffer(); + for (const Rect &r : region.rects()) { + glScissor(r.x(), current->size().height() - (r.y() + r.height()), r.width(), r.height()); + glDrawArrays(primitiveMode, first, count); + } + } +} + +void GLVertexBuffer::reset() +{ + d->vertexCount = 0; +} + +void GLVertexBuffer::endOfFrame() +{ + if (!d->persistent) { + return; + } + + // Emit a fence if we have uploaded data + if (d->frameSize > 0) { + d->frameSizes.push(d->frameSize); + d->frameSize = 0; + + // Force the buffer to be reallocated at the beginning of the next frame + // if the average frame size is greater than half the size of the buffer + if (d->frameSizes.average() > d->bufferSize / 2) { + deleteAll(d->fences); + glDeleteBuffers(1, &d->buffer); + + d->buffer = 0; + d->bufferSize = 0; + d->nextOffset = 0; + d->map = nullptr; + } else { + if (auto sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0)) { + d->fences.push_back(BufferFence{ + .sync = sync, + .nextEnd = intptr_t(d->nextOffset + d->bufferSize)}); + } + } + } +} + +void GLVertexBuffer::beginFrame() +{ + if (!d->persistent) { + return; + } + + // Remove finished fences from the list and update the bufferEnd offset + while (d->fences.size() > 1 && d->fences.front().signaled()) { + const BufferFence &fence = d->fences.front(); + glDeleteSync(fence.sync); + + d->bufferEnd = fence.nextEnd; + d->fences.pop_front(); + } +} + +GLVertexBuffer *GLVertexBuffer::streamingBuffer() +{ + return EglContext::currentContext()->streamingVbo(); +} + +void GLVertexBuffer::setPersistent() +{ + d->persistent = true; +} + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/glvertexbuffer.h b/local/recipes/kde/kwin/source/src/opengl/glvertexbuffer.h new file mode 100644 index 0000000000..8a9bf598b2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/glvertexbuffer.h @@ -0,0 +1,288 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006-2007 Rivo Laks + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "kwin_export.h" + +#include "core/region.h" + +#include +#include + +#include +#include +#include +#include + +namespace KWin +{ + +enum VertexAttributeType { + VA_Position = 0, + VA_TexCoord = 1, + VertexAttributeCount = 2, +}; + +struct GLVertex2D +{ + QVector2D position; + QVector2D texcoord; +}; + +struct GLVertex3D +{ + QVector3D position; + QVector2D texcoord; +}; + +/** + * Describes the format of a vertex attribute stored in a buffer object. + * + * The attribute format consists of the attribute index, the number of + * vector components, the data type, and the offset of the first element + * relative to the start of the vertex data. + */ +struct GLVertexAttrib +{ + size_t attributeIndex; + size_t componentCount; + GLenum type; /** The type (e.g. GL_FLOAT) */ + size_t relativeOffset; /** The relative offset of the attribute */ +}; + +class GLVertexBufferPrivate; + +/** + * @short Vertex Buffer Object + * + * This is a short helper class to use vertex buffer objects (VBO). A VBO can be used to buffer + * vertex data and to store them on graphics memory. + * + * @author Martin Gräßlin + * @since 4.6 + */ +class KWIN_EXPORT GLVertexBuffer +{ +public: + /** + * Enum to define how often the vertex data in the buffer object changes. + */ + enum UsageHint { + Dynamic, ///< frequent changes, but used several times for rendering + Static, ///< No changes to data + Stream ///< Data only used once for rendering, updated very frequently + }; + + explicit GLVertexBuffer(UsageHint hint); + ~GLVertexBuffer(); + + /** + * Specifies how interleaved vertex attributes are laid out in + * the buffer object. + * + * Note that the attributes and the stride should be 32 bit aligned + * or a performance penalty may be incurred. + * + * For some hardware the optimal stride is a multiple of 32 bytes. + * + * Example: + * + * struct Vertex { + * QVector3D position; + * QVector2D texcoord; + * }; + * + * const std::array attribs = { + * GLVertexAttrib{ VA_Position, 3, GL_FLOAT, offsetof(Vertex, position) }, + * GLVertexAttrib{ VA_TexCoord, 2, GL_FLOAT, offsetof(Vertex, texcoord) }, + * }; + * + * Vertex vertices[6]; + * vbo->setAttribLayout(std::span(attribs), sizeof(Vertex)); + * vbo->setData(vertices, sizeof(vertices)); + */ + void setAttribLayout(std::span attribs, size_t stride); + + /** + * Uploads data into the buffer object's data store. + */ + void setData(const void *data, size_t sizeInBytes); + + /** + * Sets the number of vertices that will be drawn by the render() method. + */ + void setVertexCount(int count); + + // clang-format off + template + requires std::is_same, GLVertex2D>::value + void setVertices(const T &range) + { + setData(range.data(), range.size() * sizeof(GLVertex2D)); + setVertexCount(range.size()); + setAttribLayout(std::span(GLVertex2DLayout), sizeof(GLVertex2D)); + } + + template + requires std::is_same, GLVertex3D>::value + void setVertices(const T &range) + { + setData(range.data(), range.size() * sizeof(GLVertex3D)); + setVertexCount(range.size()); + setAttribLayout(std::span(GLVertex3DLayout), sizeof(GLVertex3D)); + } + + template + requires std::is_same, QVector2D>::value + void setVertices(const T &range) + { + setData(range.data(), range.size() * sizeof(QVector2D)); + setVertexCount(range.size()); + static constexpr GLVertexAttrib layout{ + .attributeIndex = VA_Position, + .componentCount = 2, + .type = GL_FLOAT, + .relativeOffset = 0, + }; + setAttribLayout(std::span(&layout, 1), sizeof(QVector2D)); + } + // clang-format on + + /** + * Maps an unused range of the data store into the client's address space. + * + * The data store will be reallocated if it is smaller than the given size. + * + * The buffer object is mapped for writing, not reading. Attempts to read from + * the mapped buffer range may result in system errors, including program + * termination. The data in the mapped region is undefined until it has been + * written to. If subsequent GL calls access unwritten memory, the results are + * undefined and system errors, including program termination, may occur. + * + * No GL calls that access the buffer object must be made while the buffer + * object is mapped. The returned pointer must not be passed as a parameter + * value to any GL function. + * + * It is assumed that the GL_ARRAY_BUFFER_BINDING will not be changed while + * the buffer object is mapped. + */ + template + std::optional> map(size_t count) + { + if (const auto m = map(sizeof(T) * count)) { + return std::span(reinterpret_cast(m), count); + } else { + return std::nullopt; + } + } + + /** + * Flushes the mapped buffer range and unmaps the buffer. + */ + void unmap(); + + /** + * Binds the vertex arrays to the context. + */ + void bindArrays(); + + /** + * Disables the vertex arrays. + */ + void unbindArrays(); + + /** + * Draws count vertices beginning with first. + */ + void draw(GLenum primitiveMode, int first, int count); + + /** + * Draws count vertices beginning with first. + */ + void draw(const Region ®ion, GLenum primitiveMode, int first, int count, bool hardwareClipping = false); + + /** + * Renders the vertex data in given @a primitiveMode. + * Please refer to OpenGL documentation of glDrawArrays or glDrawElements for allowed + * values for @a primitiveMode. Best is to use GL_TRIANGLES or similar to be future + * compatible. + */ + void render(GLenum primitiveMode); + /** + * Same as above restricting painting to @a region if @a hardwareClipping is true. + * It's within the caller's responsibility to enable GL_SCISSOR_TEST. + */ + void render(const Region ®ion, GLenum primitiveMode, bool hardwareClipping = false); + + /** + * Resets the instance to default values. + * Useful for shared buffers. + * @since 4.7 + */ + void reset(); + + /** + * Notifies the vertex buffer that we are done painting the frame. + * + * @internal + */ + void endOfFrame(); + + /** + * Notifies the vertex buffer that we are about to paint a frame. + * + * @internal + */ + void beginFrame(); + + void setPersistent(); + + /** + * @return A shared VBO for streaming data + * @since 4.7 + */ + static GLVertexBuffer *streamingBuffer(); + + static constexpr std::array GLVertex2DLayout{ + GLVertexAttrib{ + .attributeIndex = VA_Position, + .componentCount = 2, + .type = GL_FLOAT, + .relativeOffset = offsetof(GLVertex2D, position), + }, + GLVertexAttrib{ + .attributeIndex = VA_TexCoord, + .componentCount = 2, + .type = GL_FLOAT, + .relativeOffset = offsetof(GLVertex2D, texcoord), + }, + }; + static constexpr std::array GLVertex3DLayout{ + GLVertexAttrib{ + .attributeIndex = VA_Position, + .componentCount = 3, + .type = GL_FLOAT, + .relativeOffset = offsetof(GLVertex3D, position), + }, + GLVertexAttrib{ + .attributeIndex = VA_TexCoord, + .componentCount = 2, + .type = GL_FLOAT, + .relativeOffset = offsetof(GLVertex3D, texcoord), + }, + }; + +private: + GLvoid *map(size_t size); + + const std::unique_ptr d; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/glvertexbuffer_p.h b/local/recipes/kde/kwin/source/src/opengl/glvertexbuffer_p.h new file mode 100644 index 0000000000..dd3641ca83 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/glvertexbuffer_p.h @@ -0,0 +1,35 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006-2007 Rivo Laks + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "kwin_export.h" + +#include +#include + +namespace KWin +{ + +class KWIN_EXPORT IndexBuffer +{ +public: + explicit IndexBuffer(); + ~IndexBuffer(); + + void accommodate(size_t count); + void bind(); + +private: + GLuint m_buffer; + size_t m_count = 0; + std::vector m_data; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/icc.frag b/local/recipes/kde/kwin/source/src/opengl/icc.frag new file mode 100644 index 0000000000..049331d33b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/icc.frag @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2023 Xaver Hugl +// SPDX-License-Identifier: GPL-2.0-or-later +#include "colormanagement.glsl" + +precision highp float; +precision highp sampler2D; +precision highp sampler3D; + +in vec2 texcoord0; + +uniform sampler2D src; + +uniform mat4 toXYZD50; + +uniform int Bsize; +uniform sampler2D Bsampler; + +uniform mat4 matrix2; + +uniform int Msize; +uniform sampler2D Msamplrt; + +uniform ivec3 Csize; +uniform sampler3D Csampler; + +uniform int Asize; +uniform sampler2D Asampler; + +vec3 sample1DLut(vec3 input, sampler2D lut, int lutSize) { + float lutOffset = 0.5 / float(lutSize); + float lutScale = 1.0 - lutOffset * 2.0; + float lutR = texture2D(lut, vec2(lutOffset + input.r * lutScale, 0.5)).r; + float lutG = texture2D(lut, vec2(lutOffset + input.g * lutScale, 0.5)).g; + float lutB = texture2D(lut, vec2(lutOffset + input.b * lutScale, 0.5)).b; + return vec3(lutR, lutG, lutB); +} + +void main() +{ + vec4 tex = texture2D(src, texcoord0); + tex = encodingToNits(tex, sourceNamedTransferFunction, sourceTransferFunctionParams.x, sourceTransferFunctionParams.y); + tex.rgb /= max(tex.a, 0.001); + tex.rgb /= maxDestinationLuminance; + tex.rgb = (toXYZD50 * vec4(tex.rgb, 1.0)).rgb; + if (Bsize > 0) { + tex.rgb = sample1DLut(tex.rgb, Bsampler, Bsize); + } + tex.rgb = (matrix2 * vec4(tex.rgb, 1.0)).rgb; + if (Msize > 0) { + tex.rgb = sample1DLut(tex.rgb, Msampler, Msize); + } + if (Csize > 0) { + vec3 lutOffset = vec3(0.5) / vec3(Csize); + vec3 lutScale = vec3(1.0) - lutOffset * 2.0; + tex.rgb = texture3D(Csampler, lutOffset + tex.rgb * lutScale).rgb; + } + if (Asize > 0) { + tex.rgb = sample1DLut(tex.rgb, Asampler, Asize); + } + tex.rgb *= tex.a; + gl_FragColor = tex; +} diff --git a/local/recipes/kde/kwin/source/src/opengl/icc_core.frag b/local/recipes/kde/kwin/source/src/opengl/icc_core.frag new file mode 100644 index 0000000000..233e58abdd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/icc_core.frag @@ -0,0 +1,65 @@ +#version 140 +// SPDX-FileCopyrightText: 2023 Xaver Hugl +// SPDX-License-Identifier: GPL-2.0-or-later +#include "colormanagement.glsl" + +precision highp float; +precision highp sampler2D; +precision highp sampler3D; + +in vec2 texcoord0; + +out vec4 fragColor; + +uniform sampler2D src; + +uniform mat4 toXYZD50; + +uniform int Bsize; +uniform sampler2D Bsampler; + +uniform mat4 matrix2; + +uniform int Msize; +uniform sampler2D Msampler; + +uniform ivec3 Csize; +uniform sampler3D Csampler; + +uniform int Asize; +uniform sampler2D Asampler; + +vec3 sample1DLut(in vec3 srcColor, in sampler2D lut, in int lutSize) { + float lutOffset = 0.5 / float(lutSize); + float lutScale = 1.0 - lutOffset * 2.0; + float lutR = texture(lut, vec2(lutOffset + srcColor.r * lutScale, 0.5)).r; + float lutG = texture(lut, vec2(lutOffset + srcColor.g * lutScale, 0.5)).g; + float lutB = texture(lut, vec2(lutOffset + srcColor.b * lutScale, 0.5)).b; + return vec3(lutR, lutG, lutB); +} + +void main() +{ + vec4 tex = texture(src, texcoord0); + tex = encodingToNits(tex, sourceNamedTransferFunction, sourceTransferFunctionParams.x, sourceTransferFunctionParams.y); + tex.rgb /= max(tex.a, 0.001); + tex.rgb /= maxDestinationLuminance; + tex.rgb = (toXYZD50 * vec4(tex.rgb, 1.0)).rgb; + if (Bsize > 0) { + tex.rgb = sample1DLut(tex.rgb, Bsampler, Bsize); + } + tex.rgb = (matrix2 * vec4(tex.rgb, 1.0)).rgb; + if (Msize > 0) { + tex.rgb = sample1DLut(tex.rgb, Msampler, Msize); + } + if (Csize.x > 0) { + vec3 lutOffset = vec3(0.5) / vec3(Csize); + vec3 lutScale = vec3(1) - lutOffset * 2.0; + tex.rgb = texture(Csampler, lutOffset + tex.rgb * lutScale).rgb; + } + if (Asize > 0) { + tex.rgb = sample1DLut(tex.rgb, Asampler, Asize); + } + tex.rgb *= tex.a; + fragColor = tex; +} diff --git a/local/recipes/kde/kwin/source/src/opengl/icc_shader.cpp b/local/recipes/kde/kwin/source/src/opengl/icc_shader.cpp new file mode 100644 index 0000000000..835904449e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/icc_shader.cpp @@ -0,0 +1,261 @@ +/* + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "icc_shader.h" +#include "core/colorlut3d.h" +#include "core/colortransformation.h" +#include "core/iccprofile.h" +#include "opengl/gllut.h" +#include "opengl/gllut3D.h" +#include "opengl/glshader.h" +#include "opengl/glshadermanager.h" +#include "utils/common.h" + +namespace KWin +{ + +static constexpr size_t lutSize = 1 << 12; + +IccShader::IccShader() + : m_shader(ShaderManager::instance()->generateShaderFromFile(ShaderTrait::MapTexture, QString(), QStringLiteral(":/opengl/icc.frag"))) +{ + m_locations = { + .src = m_shader->uniformLocation("src"), + .toXYZD50 = m_shader->uniformLocation("toXYZD50"), + .bsize = m_shader->uniformLocation("Bsize"), + .bsampler = m_shader->uniformLocation("Bsampler"), + .matrix2 = m_shader->uniformLocation("matrix2"), + .msize = m_shader->uniformLocation("Msize"), + .msampler = m_shader->uniformLocation("Msampler"), + .csize = m_shader->uniformLocation("Csize"), + .csampler = m_shader->uniformLocation("Csampler"), + .asize = m_shader->uniformLocation("Asize"), + .asampler = m_shader->uniformLocation("Asampler"), + }; +} + +IccShader::~IccShader() +{ +} + +bool IccShader::setProfile(const std::shared_ptr &profile, const std::shared_ptr &inputColor, RenderingIntent intent) +{ + if (!profile) { + m_toXYZD50.setToIdentity(); + m_B.reset(); + m_matrix2.setToIdentity(); + m_M.reset(); + m_C.reset(); + m_A.reset(); + return false; + } + if (m_profile != profile || *m_inputColor != *inputColor || m_intent != intent) { + const auto vcgt = profile->vcgt(); + QMatrix4x4 toXYZD50; + std::unique_ptr B; + QMatrix4x4 matrix2; + std::unique_ptr M; + std::unique_ptr C; + std::unique_ptr A; + const ColorDescription linearizedInput(inputColor->containerColorimetry(), TransferFunction(TransferFunction::linear, 0, 1), 1, 0, 1, 1); + const ColorDescription linearizedProfile(profile->colorimetry(), TransferFunction(TransferFunction::linear, 0, 1), 1, 0, 1, 1); + + // NOTE that the MHC2 tag forces the shaper+matrix path, + // as the spec doesn't describe how it would work with BToA. + if (const auto tag = profile->BToATag(intent); tag && profile->mhc2Matrix().isIdentity()) { + if (intent == RenderingIntent::AbsoluteColorimetricNoAdaptation) { + // There's no BToA tag for absolute colorimetric, we have to piece it together ourselves with + // input white point -(absolute colorimetric)-> display white point + // -(relative colorimetric)-> XYZ D50 -(BToA1, also relative colorimetric)-> display white point + + // First, transform from the input color to the display color space in absolute colorimetric mode + const QMatrix4x4 toLinearDisplay = linearizedInput.toOther(linearizedProfile, RenderingIntent::AbsoluteColorimetricNoAdaptation); + + // Now transform that display color space to XYZ D50 in relative colorimetric mode. + // the BToA1 tag goes from XYZ D50 to the native white point of the display, + // so this matrix gets reverted by it + const QMatrix4x4 toXYZ = linearizedProfile.toOther(IccProfile::s_connectionSpace, RenderingIntent::RelativeColorimetric); + + toXYZD50 = toXYZ * toLinearDisplay; + } else { + toXYZD50 = linearizedInput.toOther(IccProfile::s_connectionSpace, intent); + } + // while the above converts to XYZ D50, the encoding the ICC profile tag + // wants is CIEXYZ -> add the (absolute colorimetric) transform to that + toXYZD50 = IccProfile::s_connectionSpace.containerColorimetry().toXYZ() * toXYZD50; + auto it = tag->ops.begin(); + if (it != tag->ops.end() && std::holds_alternative>(it->operation)) { + const auto sample = [&op = std::get>(it->operation)](size_t x) { + const float relativeX = x / double(lutSize - 1); + return op->transform(QVector3D(relativeX, relativeX, relativeX)); + }; + B = GlLookUpTable::create(sample, lutSize); + if (!B) { + return false; + } + it++; + } + if (it != tag->ops.end() && std::holds_alternative(it->operation)) { + matrix2 = std::get(it->operation).mat; + it++; + } + if (it != tag->ops.end() && std::holds_alternative>(it->operation)) { + const auto sample = [&op = std::get>(it->operation)](size_t x) { + const float relativeX = x / double(lutSize - 1); + return op->transform(QVector3D(relativeX, relativeX, relativeX)); + }; + M = GlLookUpTable::create(sample, lutSize); + if (!M) { + return false; + } + it++; + } + if (it != tag->ops.end() && std::holds_alternative>(it->operation)) { + const auto &op = std::get>(it->operation); + const auto sample = [op](size_t x, size_t y, size_t z) { + return op->sample(x, y, z); + }; + C = GlLookUpTable3D::create(sample, op->xSize(), op->ySize(), op->zSize()); + if (!C) { + return false; + } + it++; + } + if (it != tag->ops.end() && std::holds_alternative>(it->operation)) { + const auto sample = [&op = std::get>(it->operation), vcgt](size_t x) { + const float relativeX = x / double(lutSize - 1); + QVector3D ret = op->transform(QVector3D(relativeX, relativeX, relativeX)); + if (vcgt) { + ret = vcgt->transform(ret); + } + return ret; + }; + A = GlLookUpTable::create(sample, lutSize); + if (!A) { + return false; + } + it++; + } else if (vcgt) { + const auto sample = [&vcgt](size_t x) { + const float relativeX = x / double(lutSize - 1); + return vcgt->transform(QVector3D(relativeX, relativeX, relativeX)); + }; + A = GlLookUpTable::create(sample, lutSize); + } + if (it != tag->ops.end()) { + qCCritical(KWIN_OPENGL, "Couldn't represent ICC profile in the ICC shader!"); + return false; + } + } else { + toXYZD50 = linearizedInput.toOther(linearizedProfile, intent); + + if (!profile->mhc2Matrix().isIdentity()) { + // The pipeline for MHC2 is quite weird: + // SourceRGBtoXYZ -> XYZtoXYZAdjust -> XYZtoTargetRGB + // "TargetRGB" is BT.709 or BT.2020, not the actual target primaries. + // As this code path is only used for SDR, we always use BT.709 + toXYZD50 = Colorimetry::BT709.fromXYZ() * profile->mhc2Matrix() * linearizedProfile.containerColorimetry().toXYZ(); + } + + const auto inverseEOTF = profile->inverseTransferFunction(); + const auto sample = [inverseEOTF, vcgt](size_t x) { + const float relativeX = x / double(lutSize - 1); + QVector3D ret(relativeX, relativeX, relativeX); + ret = inverseEOTF->transform(ret); + if (vcgt) { + ret = vcgt->transform(ret); + } + return ret; + }; + A = GlLookUpTable::create(sample, lutSize); + if (!A) { + return false; + } + } + m_toXYZD50 = toXYZD50; + m_B = std::move(B); + m_matrix2 = matrix2; + m_M = std::move(M); + m_C = std::move(C); + m_A = std::move(A); + m_profile = profile; + m_inputColor = inputColor; + m_intent = intent; + } + return true; +} + +GLShader *IccShader::shader() const +{ + return m_shader.get(); +} + +void IccShader::setUniforms(const std::shared_ptr &profile, const std::shared_ptr &inputColor, RenderingIntent intent) +{ + // this failing can be silently ignored, it should only happen with GPU resets and gets corrected later + setProfile(profile, inputColor, intent); + + m_shader->setUniform(m_locations.toXYZD50, m_toXYZD50); + m_shader->setUniform(GLShader::IntUniform::SourceNamedTransferFunction, inputColor->transferFunction().type); + if (inputColor->transferFunction().type == TransferFunction::BT1886) { + m_shader->setUniform(GLShader::Vec2Uniform::SourceTransferFunctionParams, QVector2D(inputColor->transferFunction().bt1886B(), inputColor->transferFunction().bt1886A())); + } else { + m_shader->setUniform(GLShader::Vec2Uniform::SourceTransferFunctionParams, QVector2D(inputColor->transferFunction().minLuminance, inputColor->transferFunction().maxLuminance - inputColor->transferFunction().minLuminance)); + } + m_shader->setUniform(GLShader::FloatUniform::SourceReferenceLuminance, inputColor->referenceLuminance()); + m_shader->setUniform(GLShader::FloatUniform::DestinationReferenceLuminance, inputColor->referenceLuminance()); + m_shader->setUniform(GLShader::FloatUniform::MaxDestinationLuminance, inputColor->transferFunction().maxLuminance); + + glActiveTexture(GL_TEXTURE1); + if (m_B) { + m_shader->setUniform(m_locations.bsize, int(m_B->size())); + m_shader->setUniform(m_locations.bsampler, 1); + m_B->bind(); + } else { + m_shader->setUniform(m_locations.bsize, 0); + m_shader->setUniform(m_locations.bsampler, 1); + glBindTexture(GL_TEXTURE_1D, 0); + } + + m_shader->setUniform(m_locations.matrix2, m_matrix2); + + glActiveTexture(GL_TEXTURE2); + if (m_M) { + m_shader->setUniform(m_locations.msize, int(m_M->size())); + m_shader->setUniform(m_locations.msampler, 2); + m_M->bind(); + } else { + m_shader->setUniform(m_locations.msize, 0); + m_shader->setUniform(m_locations.msampler, 1); + glBindTexture(GL_TEXTURE_1D, 0); + } + + glActiveTexture(GL_TEXTURE3); + if (m_C) { + m_shader->setUniform(m_locations.csize, m_C->xSize(), m_C->ySize(), m_C->zSize()); + m_shader->setUniform(m_locations.csampler, 3); + m_C->bind(); + } else { + m_shader->setUniform(m_locations.csize, 0, 0, 0); + m_shader->setUniform(m_locations.csampler, 3); + glBindTexture(GL_TEXTURE_3D, 0); + } + + glActiveTexture(GL_TEXTURE4); + if (m_A) { + m_shader->setUniform(m_locations.asize, int(m_A->size())); + m_shader->setUniform(m_locations.asampler, 4); + m_A->bind(); + } else { + m_shader->setUniform(m_locations.asize, 0); + m_shader->setUniform(m_locations.asampler, 4); + glBindTexture(GL_TEXTURE_1D, 0); + } + + glActiveTexture(GL_TEXTURE0); + m_shader->setUniform(m_locations.src, 0); +} + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/icc_shader.h b/local/recipes/kde/kwin/source/src/opengl/icc_shader.h new file mode 100644 index 0000000000..fd8ad378d6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/icc_shader.h @@ -0,0 +1,62 @@ +/* + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "core/colorspace.h" + +#include +#include +#include + +namespace KWin +{ + +class IccProfile; +class GLShader; +class GlLookUpTable; +class GlLookUpTable3D; +class GLTexture; + +class KWIN_EXPORT IccShader +{ +public: + explicit IccShader(); + ~IccShader(); + + GLShader *shader() const; + void setUniforms(const std::shared_ptr &profile, const std::shared_ptr &inputColor, RenderingIntent intent); + +private: + bool setProfile(const std::shared_ptr &profile, const std::shared_ptr &inputColor, RenderingIntent intent); + + std::unique_ptr m_shader; + std::shared_ptr m_profile; + RenderingIntent m_intent = RenderingIntent::RelativeColorimetric; + std::shared_ptr m_inputColor = ColorDescription::sRGB; + + QMatrix4x4 m_toXYZD50; + std::unique_ptr m_B; + QMatrix4x4 m_matrix2; + std::unique_ptr m_M; + std::unique_ptr m_C; + std::unique_ptr m_A; + struct Locations + { + int src; + int toXYZD50; + int bsize; + int bsampler; + int matrix2; + int msize; + int msampler; + int csize; + int csampler; + int asize; + int asampler; + }; + Locations m_locations; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/opengl/saturation.glsl b/local/recipes/kde/kwin/source/src/opengl/saturation.glsl new file mode 100644 index 0000000000..0eef6a7e2e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/opengl/saturation.glsl @@ -0,0 +1,9 @@ +uniform float saturation; +uniform vec3 primaryBrightness; + +vec4 adjustSaturation(vec4 color) { + // this calculates the Y component of the XYZ color representation for the color, + // which roughly corresponds to the brightness of the RGB tuple + float Y = dot(color.rgb, primaryBrightness); + return vec4(mix(vec3(Y), color.rgb, saturation), color.a); +} diff --git a/local/recipes/kde/kwin/source/src/options.cpp b/local/recipes/kde/kwin/source/src/options.cpp new file mode 100644 index 0000000000..282d60faa9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/options.cpp @@ -0,0 +1,906 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "options.h" + +#include "config-kwin.h" + +#include "utils/common.h" + +#ifndef KCMRULES + +#include + +#include "settings.h" +#include "workspace.h" +#include + +#endif // KCMRULES + +namespace KWin +{ + +#ifndef KCMRULES + +Options::Options(QObject *parent) + : QObject(parent) + , m_settings(new Settings(kwinApp()->config())) + , m_focusPolicy(ClickToFocus) + , m_nextFocusPrefersMouse(false) + , m_clickRaise(false) + , m_autoRaise(false) + , m_autoRaiseInterval(0) + , m_delayFocusInterval(0) + , m_separateScreenFocus(true) + , m_placement(PlacementNone) + , m_activationDesktopPolicy(Options::defaultActivationDesktopPolicy()) + , m_borderSnapZone(0) + , m_windowSnapZone(0) + , m_centerSnapZone(0) + , m_snapOnlyWhenOverlapping(false) + , m_edgeBarrier(0) + , m_cornerBarrier(0) + , m_rollOverDesktops(false) + , m_focusStealingPreventionLevel(FocusStealingPreventionLevel::None) + , m_killPingTimeout(0) + , m_xwaylandCrashPolicy(Options::defaultXwaylandCrashPolicy()) + , m_xwaylandMaxCrashCount(Options::defaultXwaylandMaxCrashCount()) + , m_xwaylandEavesdrops(Options::defaultXwaylandEavesdrops()) + , m_xwaylandEavesdropsMouse(Options::defaultXwaylandEavesdropsMouse()) + , m_xwaylandEisNoPrompt(Options::defaultXwaylandEisNoPrompt()) + , m_compositingMode(Options::defaultCompositingMode()) + , OpTitlebarDblClick(Options::defaultOperationTitlebarDblClick()) + , CmdActiveTitlebar1(Options::defaultCommandActiveTitlebar1()) + , CmdActiveTitlebar2(Options::defaultCommandActiveTitlebar2()) + , CmdActiveTitlebar3(Options::defaultCommandActiveTitlebar3()) + , CmdInactiveTitlebar1(Options::defaultCommandInactiveTitlebar1()) + , CmdInactiveTitlebar2(Options::defaultCommandInactiveTitlebar2()) + , CmdInactiveTitlebar3(Options::defaultCommandInactiveTitlebar3()) + , CmdTitlebarWheel(Options::defaultCommandTitlebarWheel()) + , CmdWindow1(Options::defaultCommandWindow1()) + , CmdWindow2(Options::defaultCommandWindow2()) + , CmdWindow3(Options::defaultCommandWindow3()) + , CmdWindowWheel(Options::defaultCommandWindowWheel()) + , CmdAll1(Options::defaultCommandAll1()) + , CmdAll2(Options::defaultCommandAll2()) + , CmdAll3(Options::defaultCommandAll3()) + , CmdAllWheel(Options::defaultCommandAllWheel()) + , CmdAllModKey(Options::defaultKeyCmdAllModKey()) + , electric_border_maximize(false) + , electric_border_tiling(false) + , electric_border_corner_ratio(0.0) + , borderless_maximized_windows(false) + , condensed_title(false) +{ + m_settings->setDefaults(); + + loadConfig(); + + m_configWatcher = KConfigWatcher::create(m_settings->sharedConfig()); + connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) { + if (group.name() == QLatin1String("KDE") && names.contains(QByteArrayLiteral("AnimationDurationFactor"))) { + m_settings->load(); + Q_EMIT animationSpeedChanged(); + } else if (group.name() == QLatin1String("Xwayland")) { + workspace()->reconfigure(); + } + }); +} + +Options::~Options() +{ +} + +void Options::setFocusPolicy(FocusPolicy focusPolicy) +{ + if (m_focusPolicy == focusPolicy) { + return; + } + m_focusPolicy = focusPolicy; + Q_EMIT focusPolicyChanged(); + if (m_focusPolicy == ClickToFocus) { + setAutoRaise(false); + setAutoRaiseInterval(0); + setDelayFocusInterval(0); + } +} + +void Options::setNextFocusPrefersMouse(bool nextFocusPrefersMouse) +{ + if (m_nextFocusPrefersMouse == nextFocusPrefersMouse) { + return; + } + m_nextFocusPrefersMouse = nextFocusPrefersMouse; + Q_EMIT nextFocusPrefersMouseChanged(); +} + +void Options::setXwaylandCrashPolicy(XwaylandCrashPolicy crashPolicy) +{ + if (m_xwaylandCrashPolicy == crashPolicy) { + return; + } + m_xwaylandCrashPolicy = crashPolicy; + Q_EMIT xwaylandCrashPolicyChanged(); +} + +void Options::setXwaylandMaxCrashCount(int maxCrashCount) +{ + if (m_xwaylandMaxCrashCount == maxCrashCount) { + return; + } + m_xwaylandMaxCrashCount = maxCrashCount; + Q_EMIT xwaylandMaxCrashCountChanged(); +} + +void Options::setXwaylandEavesdrops(XwaylandEavesdropsMode mode) +{ + if (m_xwaylandEavesdrops == mode) { + return; + } + m_xwaylandEavesdrops = mode; + Q_EMIT xwaylandEavesdropsChanged(); +} + +void Options::setXwaylandEavesdropsMouse(bool eavesdropsMouse) +{ + if (m_xwaylandEavesdropsMouse == eavesdropsMouse) { + return; + } + m_xwaylandEavesdropsMouse = eavesdropsMouse; + Q_EMIT xwaylandEavesdropsMouseChanged(); +} + +void Options::setXWaylandEisNoPrompt(bool doNotPrompt) +{ + if (m_xwaylandEisNoPrompt == doNotPrompt) { + return; + } + m_xwaylandEisNoPrompt = doNotPrompt; + Q_EMIT xwaylandEisNoPromptChanged(); +} + +void Options::setClickRaise(bool clickRaise) +{ + if (m_autoRaise) { + // important: autoRaise implies ClickRaise + clickRaise = true; + } + if (m_clickRaise == clickRaise) { + return; + } + m_clickRaise = clickRaise; + Q_EMIT clickRaiseChanged(); +} + +void Options::setAutoRaise(bool autoRaise) +{ + if (m_focusPolicy == ClickToFocus) { + autoRaise = false; + } + if (m_autoRaise == autoRaise) { + return; + } + m_autoRaise = autoRaise; + if (m_autoRaise) { + // important: autoRaise implies ClickRaise + setClickRaise(true); + } + Q_EMIT autoRaiseChanged(); +} + +void Options::setAutoRaiseInterval(int autoRaiseInterval) +{ + if (m_focusPolicy == ClickToFocus) { + autoRaiseInterval = 0; + } + if (m_autoRaiseInterval == autoRaiseInterval) { + return; + } + m_autoRaiseInterval = autoRaiseInterval; + Q_EMIT autoRaiseIntervalChanged(); +} + +void Options::setDelayFocusInterval(int delayFocusInterval) +{ + if (m_focusPolicy == ClickToFocus) { + delayFocusInterval = 0; + } + if (m_delayFocusInterval == delayFocusInterval) { + return; + } + m_delayFocusInterval = delayFocusInterval; + Q_EMIT delayFocusIntervalChanged(); +} + +void Options::setSeparateScreenFocus(bool separateScreenFocus) +{ + if (m_separateScreenFocus == separateScreenFocus) { + return; + } + m_separateScreenFocus = separateScreenFocus; + Q_EMIT separateScreenFocusChanged(m_separateScreenFocus); +} + +void Options::setPlacement(PlacementPolicy placement) +{ + if (m_placement == placement) { + return; + } + m_placement = placement; + Q_EMIT placementChanged(); +} + +void Options::setActivationDesktopPolicy(ActivationDesktopPolicy activationDesktopPolicy) +{ + if (m_activationDesktopPolicy == activationDesktopPolicy) { + return; + } + m_activationDesktopPolicy = activationDesktopPolicy; + Q_EMIT activationDesktopPolicyChanged(); +} + +void Options::setBorderSnapZone(int borderSnapZone) +{ + if (m_borderSnapZone == borderSnapZone) { + return; + } + m_borderSnapZone = borderSnapZone; + Q_EMIT borderSnapZoneChanged(); +} + +void Options::setWindowSnapZone(int windowSnapZone) +{ + if (m_windowSnapZone == windowSnapZone) { + return; + } + m_windowSnapZone = windowSnapZone; + Q_EMIT windowSnapZoneChanged(); +} + +void Options::setCenterSnapZone(int centerSnapZone) +{ + if (m_centerSnapZone == centerSnapZone) { + return; + } + m_centerSnapZone = centerSnapZone; + Q_EMIT centerSnapZoneChanged(); +} + +void Options::setSnapOnlyWhenOverlapping(bool snapOnlyWhenOverlapping) +{ + if (m_snapOnlyWhenOverlapping == snapOnlyWhenOverlapping) { + return; + } + m_snapOnlyWhenOverlapping = snapOnlyWhenOverlapping; + Q_EMIT snapOnlyWhenOverlappingChanged(); +} + +void Options::setEdgeBarrier(int edgeBarrier) +{ + if (m_edgeBarrier == edgeBarrier) { + return; + } + m_edgeBarrier = edgeBarrier; + Q_EMIT edgeBarrierChanged(); +} + +void Options::setCornerBarrier(bool cornerBarrier) +{ + if (m_cornerBarrier == cornerBarrier) { + return; + } + m_cornerBarrier = cornerBarrier; + Q_EMIT cornerBarrierChanged(); +} + +void Options::setRollOverDesktops(bool rollOverDesktops) +{ + if (m_rollOverDesktops == rollOverDesktops) { + return; + } + m_rollOverDesktops = rollOverDesktops; + Q_EMIT rollOverDesktopsChanged(m_rollOverDesktops); +} + +void Options::setFocusStealingPreventionLevel(FocusStealingPreventionLevel focusStealingPreventionLevel) +{ + if (!focusPolicyIsReasonable()) { + focusStealingPreventionLevel = FocusStealingPreventionLevel::None; + } + if (m_focusStealingPreventionLevel == FocusStealingPreventionLevel(focusStealingPreventionLevel)) { + return; + } + m_focusStealingPreventionLevel = std::max(FocusStealingPreventionLevel::None, std::min(FocusStealingPreventionLevel::Extreme, focusStealingPreventionLevel)); + Q_EMIT focusStealingPreventionLevelChanged(); +} + +void Options::setOperationTitlebarDblClick(WindowOperation operationTitlebarDblClick) +{ + if (OpTitlebarDblClick == operationTitlebarDblClick) { + return; + } + OpTitlebarDblClick = operationTitlebarDblClick; + Q_EMIT operationTitlebarDblClickChanged(); +} + +void Options::setOperationMaxButtonLeftClick(WindowOperation op) +{ + if (opMaxButtonLeftClick == op) { + return; + } + opMaxButtonLeftClick = op; + Q_EMIT operationMaxButtonLeftClickChanged(); +} + +void Options::setOperationMaxButtonRightClick(WindowOperation op) +{ + if (opMaxButtonRightClick == op) { + return; + } + opMaxButtonRightClick = op; + Q_EMIT operationMaxButtonRightClickChanged(); +} + +void Options::setOperationMaxButtonMiddleClick(WindowOperation op) +{ + if (opMaxButtonMiddleClick == op) { + return; + } + opMaxButtonMiddleClick = op; + Q_EMIT operationMaxButtonMiddleClickChanged(); +} + +void Options::setCommandActiveTitlebar1(MouseCommand commandActiveTitlebar1) +{ + if (CmdActiveTitlebar1 == commandActiveTitlebar1) { + return; + } + CmdActiveTitlebar1 = commandActiveTitlebar1; + Q_EMIT commandActiveTitlebar1Changed(); +} + +void Options::setCommandActiveTitlebar2(MouseCommand commandActiveTitlebar2) +{ + if (CmdActiveTitlebar2 == commandActiveTitlebar2) { + return; + } + CmdActiveTitlebar2 = commandActiveTitlebar2; + Q_EMIT commandActiveTitlebar2Changed(); +} + +void Options::setCommandActiveTitlebar3(MouseCommand commandActiveTitlebar3) +{ + if (CmdActiveTitlebar3 == commandActiveTitlebar3) { + return; + } + CmdActiveTitlebar3 = commandActiveTitlebar3; + Q_EMIT commandActiveTitlebar3Changed(); +} + +void Options::setCommandInactiveTitlebar1(MouseCommand commandInactiveTitlebar1) +{ + if (CmdInactiveTitlebar1 == commandInactiveTitlebar1) { + return; + } + CmdInactiveTitlebar1 = commandInactiveTitlebar1; + Q_EMIT commandInactiveTitlebar1Changed(); +} + +void Options::setCommandInactiveTitlebar2(MouseCommand commandInactiveTitlebar2) +{ + if (CmdInactiveTitlebar2 == commandInactiveTitlebar2) { + return; + } + CmdInactiveTitlebar2 = commandInactiveTitlebar2; + Q_EMIT commandInactiveTitlebar2Changed(); +} + +void Options::setCommandInactiveTitlebar3(MouseCommand commandInactiveTitlebar3) +{ + if (CmdInactiveTitlebar3 == commandInactiveTitlebar3) { + return; + } + CmdInactiveTitlebar3 = commandInactiveTitlebar3; + Q_EMIT commandInactiveTitlebar3Changed(); +} + +void Options::setCommandWindow1(MouseCommand commandWindow1) +{ + if (CmdWindow1 == commandWindow1) { + return; + } + CmdWindow1 = commandWindow1; + Q_EMIT commandWindow1Changed(); +} + +void Options::setCommandWindow2(MouseCommand commandWindow2) +{ + if (CmdWindow2 == commandWindow2) { + return; + } + CmdWindow2 = commandWindow2; + Q_EMIT commandWindow2Changed(); +} + +void Options::setCommandWindow3(MouseCommand commandWindow3) +{ + if (CmdWindow3 == commandWindow3) { + return; + } + CmdWindow3 = commandWindow3; + Q_EMIT commandWindow3Changed(); +} + +void Options::setCommandWindowWheel(MouseCommand commandWindowWheel) +{ + if (CmdWindowWheel == commandWindowWheel) { + return; + } + CmdWindowWheel = commandWindowWheel; + Q_EMIT commandWindowWheelChanged(); +} + +void Options::setCommandAll1(MouseCommand commandAll1) +{ + if (CmdAll1 == commandAll1) { + return; + } + CmdAll1 = commandAll1; + Q_EMIT commandAll1Changed(); +} + +void Options::setCommandAll2(MouseCommand commandAll2) +{ + if (CmdAll2 == commandAll2) { + return; + } + CmdAll2 = commandAll2; + Q_EMIT commandAll2Changed(); +} + +void Options::setCommandAll3(MouseCommand commandAll3) +{ + if (CmdAll3 == commandAll3) { + return; + } + CmdAll3 = commandAll3; + Q_EMIT commandAll3Changed(); +} + +void Options::setKeyCmdAllModKey(uint keyCmdAllModKey) +{ + if (CmdAllModKey == keyCmdAllModKey) { + return; + } + CmdAllModKey = keyCmdAllModKey; + Q_EMIT keyCmdAllModKeyChanged(); +} + +void Options::setDoubleClickBorderToMaximize(bool maximize) +{ + if (m_doubleClickBorderToMaximize == maximize) { + return; + } + m_doubleClickBorderToMaximize = maximize; + Q_EMIT doubleClickBorderToMaximizeChanged(); +} + +void Options::setCondensedTitle(bool condensedTitle) +{ + if (condensed_title == condensedTitle) { + return; + } + condensed_title = condensedTitle; + Q_EMIT condensedTitleChanged(); +} + +void Options::setElectricBorderMaximize(bool electricBorderMaximize) +{ + if (electric_border_maximize == electricBorderMaximize) { + return; + } + electric_border_maximize = electricBorderMaximize; + Q_EMIT electricBorderMaximizeChanged(); +} + +void Options::setElectricBorderTiling(bool electricBorderTiling) +{ + if (electric_border_tiling == electricBorderTiling) { + return; + } + electric_border_tiling = electricBorderTiling; + Q_EMIT electricBorderTilingChanged(); +} + +void Options::setElectricBorderCornerRatio(float electricBorderCornerRatio) +{ + if (electric_border_corner_ratio == electricBorderCornerRatio) { + return; + } + electric_border_corner_ratio = electricBorderCornerRatio; + Q_EMIT electricBorderCornerRatioChanged(); +} + +void Options::setElectricBorderAllScreenCorner(bool electricBorderAllScreenCorner) +{ + if (electric_border_all_screen_corner == electricBorderAllScreenCorner) { + return; + } + electric_border_all_screen_corner = electricBorderAllScreenCorner; + Q_EMIT electricBorderAllScreenCornerChanged(); +} + +void Options::setBorderlessMaximizedWindows(bool borderlessMaximizedWindows) +{ + if (borderless_maximized_windows == borderlessMaximizedWindows) { + return; + } + borderless_maximized_windows = borderlessMaximizedWindows; + Q_EMIT borderlessMaximizedWindowsChanged(); +} + +void Options::setKillPingTimeout(int killPingTimeout) +{ + if (m_killPingTimeout == killPingTimeout) { + return; + } + m_killPingTimeout = killPingTimeout; + Q_EMIT killPingTimeoutChanged(); +} + +void Options::setCompositingMode(int compositingMode) +{ + if (m_compositingMode == static_cast(compositingMode)) { + return; + } + m_compositingMode = static_cast(compositingMode); + Q_EMIT compositingModeChanged(); +} + +bool Options::allowTearing() const +{ + return m_allowTearing; +} + +void Options::setAllowTearing(bool allow) +{ + if (allow != m_allowTearing) { + m_allowTearing = allow; + Q_EMIT allowTearingChanged(); + } +} + +bool Options::interactiveWindowMoveEnabled() const +{ + return m_interactiveWindowMoveEnabled; +} + +void Options::setInteractiveWindowMoveEnabled(bool set) +{ + if (set != m_interactiveWindowMoveEnabled) { + m_interactiveWindowMoveEnabled = set; + Q_EMIT interactiveWindowMoveEnabledChanged(); + } +} + +Qt::Corner Options::pictureInPictureHomeCorner() const +{ + return m_pictureInPictureHomeCorner; +} + +void Options::setPictureInPictureHomeCorner(Qt::Corner corner) +{ + if (m_pictureInPictureHomeCorner != corner) { + m_pictureInPictureHomeCorner = corner; + Q_EMIT pictureInPictureHomeCornerChanged(); + } +} + +int Options::pictureInPictureMargin() const +{ + return m_pictureInPictureMargin; +} + +void Options::setPictureInPictureMargin(int margin) +{ + if (m_pictureInPictureMargin != margin) { + m_pictureInPictureMargin = margin; + Q_EMIT pictureInPictureMarginChanged(); + } +} + +bool Options::overlayVirtualKeyboardOnWindows() const +{ + return m_overlayVirtualKeyboardOnWindows; +} + +void Options::setOverlayVirtualKeyboardOnWindows(bool overlay) +{ + if (overlay != m_overlayVirtualKeyboardOnWindows) { + m_overlayVirtualKeyboardOnWindows = overlay; + Q_EMIT overlayVirtualKeyboardOnWindowsChanged(); + } +} + +void Options::reparseConfiguration() +{ + m_settings->config()->reparseConfiguration(); +} + +void Options::updateSettings() +{ + loadConfig(); + + Q_EMIT configChanged(); +} + +void Options::loadConfig() +{ + m_settings->load(); + + syncFromKcfgc(); + + // Electric borders + KConfigGroup config(m_settings->config(), QStringLiteral("Windows")); + OpTitlebarDblClick = windowOperation(config.readEntry("TitlebarDoubleClickCommand", "Maximize"), true); + setOperationMaxButtonLeftClick(windowOperation(config.readEntry("MaximizeButtonLeftClickCommand", "Maximize"), true)); + setOperationMaxButtonMiddleClick(windowOperation(config.readEntry("MaximizeButtonMiddleClickCommand", "Maximize (vertical only)"), true)); + setOperationMaxButtonRightClick(windowOperation(config.readEntry("MaximizeButtonRightClickCommand", "Maximize (horizontal only)"), true)); + + // Mouse bindings + config = KConfigGroup(m_settings->config(), QStringLiteral("MouseBindings")); + // TODO: add properties for missing options + CmdTitlebarWheel = mouseWheelCommand(config.readEntry("CommandTitlebarWheel", "Nothing")); + CmdAllModKey = (config.readEntry("CommandAllKey", "Meta") == QLatin1StringView("Meta")) ? Qt::Key_Meta : Qt::Key_Alt; + CmdAllWheel = mouseWheelCommand(config.readEntry("CommandAllWheel", "Nothing")); + setCommandActiveTitlebar1(mouseCommand(config.readEntry("CommandActiveTitlebar1", "Raise"), true)); + setCommandActiveTitlebar2(mouseCommand(config.readEntry("CommandActiveTitlebar2", "Nothing"), true)); + setCommandActiveTitlebar3(mouseCommand(config.readEntry("CommandActiveTitlebar3", "Operations menu"), true)); + setCommandInactiveTitlebar1(mouseCommand(config.readEntry("CommandInactiveTitlebar1", "Activate and raise"), true)); + setCommandInactiveTitlebar2(mouseCommand(config.readEntry("CommandInactiveTitlebar2", "Nothing"), true)); + setCommandInactiveTitlebar3(mouseCommand(config.readEntry("CommandInactiveTitlebar3", "Operations menu"), true)); + setCommandWindow1(mouseCommand(config.readEntry("CommandWindow1", "Activate, pass click and raise on release"), false)); + setCommandWindow2(mouseCommand(config.readEntry("CommandWindow2", "Activate and pass click"), false)); + setCommandWindow3(mouseCommand(config.readEntry("CommandWindow3", "Activate and pass click"), false)); + setCommandWindowWheel(mouseCommand(config.readEntry("CommandWindowWheel", "Scroll"), false)); + setCommandAll1(mouseCommand(config.readEntry("CommandAll1", "Move"), false)); + setCommandAll2(mouseCommand(config.readEntry("CommandAll2", "Toggle raise and lower"), false)); + setCommandAll3(mouseCommand(config.readEntry("CommandAll3", "Resize"), false)); + + // Compositing + config = KConfigGroup(m_settings->config(), QStringLiteral("Compositing")); + CompositingType compositingMode = NoCompositing; + QString compositingBackend = config.readEntry("Backend", "OpenGL"); + if (compositingBackend == "QPainter") { + compositingMode = QPainterCompositing; + } else { + compositingMode = OpenGLCompositing; + } + + if (const char *c = getenv("KWIN_COMPOSE")) { + switch (c[0]) { + case 'O': + qCDebug(KWIN_CORE) << "Compositing forced to OpenGL mode by environment variable"; + compositingMode = OpenGLCompositing; + break; + case 'Q': + qCDebug(KWIN_CORE) << "Compositing forced to QPainter mode by environment variable"; + compositingMode = QPainterCompositing; + break; + default: + qCDebug(KWIN_CORE) << "Unknown KWIN_COMPOSE mode set, ignoring"; + break; + } + } + setCompositingMode(compositingMode); +} + +void Options::syncFromKcfgc() +{ + setCondensedTitle(m_settings->condensedTitle()); + setFocusPolicy(m_settings->focusPolicy()); + setNextFocusPrefersMouse(m_settings->nextFocusPrefersMouse()); + setSeparateScreenFocus(m_settings->separateScreenFocus()); + setRollOverDesktops(m_settings->rollOverDesktops()); + setFocusStealingPreventionLevel(FocusStealingPreventionLevel(m_settings->focusStealingPreventionLevel())); + setActivationDesktopPolicy(m_settings->activationDesktopPolicy()); + setXwaylandCrashPolicy(m_settings->xwaylandCrashPolicy()); + setXwaylandMaxCrashCount(m_settings->xwaylandMaxCrashCount()); + setXwaylandEavesdrops(XwaylandEavesdropsMode(m_settings->xwaylandEavesdrops())); + setXwaylandEavesdropsMouse(m_settings->xwaylandEavesdropsMouse()); + setXWaylandEisNoPrompt(m_settings->xwaylandEisNoPrompt()); + setPlacement(m_settings->placement()); + setAutoRaise(m_settings->autoRaise()); + setAutoRaiseInterval(m_settings->autoRaiseInterval()); + setDelayFocusInterval(m_settings->delayFocusInterval()); + setClickRaise(m_settings->clickRaise()); + setBorderSnapZone(m_settings->borderSnapZone()); + setWindowSnapZone(m_settings->windowSnapZone()); + setCenterSnapZone(m_settings->centerSnapZone()); + setEdgeBarrier(m_settings->edgeBarrier()); + setCornerBarrier(m_settings->cornerBarrier()); + setSnapOnlyWhenOverlapping(m_settings->snapOnlyWhenOverlapping()); + setKillPingTimeout(m_settings->killPingTimeout()); + setBorderlessMaximizedWindows(m_settings->borderlessMaximizedWindows()); + setElectricBorderMaximize(m_settings->electricBorderMaximize()); + setElectricBorderTiling(m_settings->electricBorderTiling()); + setElectricBorderCornerRatio(m_settings->electricBorderCornerRatio()); + setElectricBorderAllScreenCorner(m_settings->electricBorderAllScreenCorner()); + setAllowTearing(m_settings->allowTearing()); + setInteractiveWindowMoveEnabled(m_settings->interactiveWindowMoveEnabled()); + setOverlayVirtualKeyboardOnWindows(m_settings->overlayVirtualKeyboardOnWindows()); + setDoubleClickBorderToMaximize(m_settings->doubleClickBorderToMaximize()); + setPictureInPictureHomeCorner(m_settings->pictureInPictureHomeCorner()); + setPictureInPictureMargin(m_settings->pictureInPictureMargin()); +} + +// restricted should be true for operations that the user may not be able to repeat +// if the window is moved out of the workspace (e.g. if the user moves a window +// by the titlebar, and moves it too high beneath Kicker at the top edge, they +// may not be able to move it back, unless they know about Meta+LMB) +Options::WindowOperation Options::windowOperation(const QString &name, bool restricted) +{ + if (name == QLatin1StringView("Move")) { + return restricted ? MoveOp : UnrestrictedMoveOp; + } else if (name == QLatin1StringView("Resize")) { + return restricted ? ResizeOp : UnrestrictedResizeOp; + } else if (name == QLatin1StringView("Maximize")) { + return MaximizeOp; + } else if (name == QLatin1StringView("Minimize")) { + return MinimizeOp; + } else if (name == QLatin1StringView("Close")) { + return CloseOp; + } else if (name == QLatin1StringView("OnAllDesktops")) { + return OnAllDesktopsOp; + } else if (name == QLatin1StringView("Maximize (vertical only)")) { + return VMaximizeOp; + } else if (name == QLatin1StringView("Maximize (horizontal only)")) { + return HMaximizeOp; + } else if (name == QLatin1StringView("Lower")) { + return LowerOp; + } + return NoOp; +} + +Options::MouseCommand Options::mouseCommand(const QString &name, bool restricted) +{ + QString lowerName = name.toLower(); + if (lowerName == QLatin1StringView("raise")) { + return MouseRaise; + } + if (lowerName == QLatin1StringView("lower")) { + return MouseLower; + } + if (lowerName == QLatin1StringView("operations menu")) { + return MouseOperationsMenu; + } + if (lowerName == QLatin1StringView("toggle raise and lower")) { + return MouseToggleRaiseAndLower; + } + if (lowerName == QLatin1StringView("activate and raise")) { + return MouseActivateAndRaise; + } + if (lowerName == QLatin1StringView("activate and lower")) { + return MouseActivateAndLower; + } + if (lowerName == QLatin1StringView("activate")) { + return MouseActivate; + } + if (lowerName == QLatin1StringView("activate, pass click and raise on release")) { + return MouseActivateRaiseOnReleaseAndPassClick; + } + if (lowerName == QLatin1StringView("activate, raise and pass click")) { + return MouseActivateRaiseAndPassClick; + } + if (lowerName == QLatin1StringView("activate and pass click")) { + return MouseActivateAndPassClick; + } + if (lowerName == QLatin1StringView("scroll")) { + return MouseNothing; + } + if (lowerName == QLatin1StringView("activate and scroll")) { + return MouseActivateAndPassClick; + } + if (lowerName == QLatin1StringView("activate, raise and scroll")) { + return MouseActivateRaiseAndPassClick; + } + if (lowerName == QLatin1StringView("activate, raise and move")) { + return restricted ? MouseActivateRaiseAndMove : MouseActivateRaiseAndUnrestrictedMove; + } + if (lowerName == QLatin1StringView("move")) { + return restricted ? MouseMove : MouseUnrestrictedMove; + } + if (lowerName == QLatin1StringView("resize")) { + return restricted ? MouseResize : MouseUnrestrictedResize; + } + if (lowerName == QLatin1StringView("minimize")) { + return MouseMinimize; + } + if (lowerName == QLatin1StringView("close")) { + return MouseClose; + } + if (lowerName == QLatin1StringView("increase opacity")) { + return MouseOpacityMore; + } + if (lowerName == QLatin1StringView("decrease opacity")) { + return MouseOpacityLess; + } + if (lowerName == QLatin1StringView("nothing")) { + return MouseNothing; + } + return MouseNothing; +} + +Options::MouseWheelCommand Options::mouseWheelCommand(const QString &name) +{ + QString lowerName = name.toLower(); + if (lowerName == QLatin1StringView("raise/lower")) { + return MouseWheelRaiseLower; + } + if (lowerName == QLatin1StringView("maximize/restore")) { + return MouseWheelMaximizeRestore; + } + if (lowerName == QLatin1StringView("above/below")) { + return MouseWheelAboveBelow; + } + if (lowerName == QLatin1StringView("previous/next desktop")) { + return MouseWheelPreviousNextDesktop; + } + if (lowerName == QLatin1StringView("change opacity")) { + return MouseWheelChangeOpacity; + } + if (lowerName == QLatin1StringView("nothing")) { + return MouseWheelNothing; + } + return MouseWheelNothing; +} + +bool Options::condensedTitle() const +{ + return condensed_title; +} + +Options::MouseCommand Options::wheelToMouseCommand(MouseWheelCommand com, qreal delta) const +{ + switch (com) { + case MouseWheelRaiseLower: + return delta > 0 ? MouseRaise : MouseLower; + case MouseWheelMaximizeRestore: + return delta > 0 ? MouseMaximize : MouseRestore; + case MouseWheelAboveBelow: + return delta > 0 ? MouseAbove : MouseBelow; + case MouseWheelPreviousNextDesktop: + return delta > 0 ? MousePreviousDesktop : MouseNextDesktop; + case MouseWheelChangeOpacity: + return delta > 0 ? MouseOpacityMore : MouseOpacityLess; + default: + return MouseNothing; + } +} +#endif + +double Options::animationTimeFactor() const +{ +#ifndef KCMRULES + return m_settings->animationDurationFactor(); +#else + return 0; +#endif +} + +Options::WindowOperation Options::operationMaxButtonClick(Qt::MouseButtons button) const +{ + return button == Qt::RightButton ? opMaxButtonRightClick : button == Qt::MiddleButton ? opMaxButtonMiddleClick + : opMaxButtonLeftClick; +} + +} // namespace + +#include "moc_options.cpp" diff --git a/local/recipes/kde/kwin/source/src/options.h b/local/recipes/kde/kwin/source/src/options.h new file mode 100644 index 0000000000..b25a4fa717 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/options.h @@ -0,0 +1,945 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/renderloop.h" +#include "main.h" + +#include + +namespace KWin +{ + +// Whether to keep all windows mapped when compositing (i.e. whether to have +// actively updated window pixmaps). +enum XwaylandEavesdropsMode { + None, + NonCharacterKeys, + AllKeysWithModifier, + All +}; + +/** + * This enum type specifies whether the Xwayland server must be restarted after a crash. + */ +enum XwaylandCrashPolicy { + Stop, + Restart, +}; + +/** + * Placement policies. How workspace decides the way windows get positioned + * on the screen. The better the policy, the heavier the resource use. + * Normally you don't have to worry. What the WM adds to the startup time + * is nil compared to the creation of the window itself in the memory + */ +enum PlacementPolicy { + PlacementNone, // not really a placement + PlacementDefault, // special, means to use the global default + PlacementUnknown, // special, means the function should use its default + PlacementRandom, + PlacementSmart, + PlacementCentered, + PlacementZeroCornered, + PlacementUnderMouse, // special + PlacementOnMainWindow, // special + PlacementMaximizing, +}; + +enum class FocusStealingPreventionLevel { + None = 0, + Low = 1, + Medium = 2, + High = 3, + Extreme = 4, +}; + +class Settings; + +class KWIN_EXPORT Options : public QObject +{ + Q_OBJECT + Q_ENUM(XwaylandCrashPolicy) + Q_ENUM(PlacementPolicy) + Q_PROPERTY(FocusPolicy focusPolicy READ focusPolicy WRITE setFocusPolicy NOTIFY focusPolicyChanged) + Q_PROPERTY(XwaylandCrashPolicy xwaylandCrashPolicy READ xwaylandCrashPolicy WRITE setXwaylandCrashPolicy NOTIFY xwaylandCrashPolicyChanged) + Q_PROPERTY(int xwaylandMaxCrashCount READ xwaylandMaxCrashCount WRITE setXwaylandMaxCrashCount NOTIFY xwaylandMaxCrashCountChanged) + Q_PROPERTY(bool nextFocusPrefersMouse READ isNextFocusPrefersMouse WRITE setNextFocusPrefersMouse NOTIFY nextFocusPrefersMouseChanged) + /** + * Whether clicking on a window raises it in FocusFollowsMouse + * mode or not. + */ + Q_PROPERTY(bool clickRaise READ isClickRaise WRITE setClickRaise NOTIFY clickRaiseChanged) + /** + * Whether autoraise is enabled FocusFollowsMouse mode or not. + */ + Q_PROPERTY(bool autoRaise READ isAutoRaise WRITE setAutoRaise NOTIFY autoRaiseChanged) + /** + * Autoraise interval. + */ + Q_PROPERTY(int autoRaiseInterval READ autoRaiseInterval WRITE setAutoRaiseInterval NOTIFY autoRaiseIntervalChanged) + /** + * Delayed focus interval. + */ + Q_PROPERTY(int delayFocusInterval READ delayFocusInterval WRITE setDelayFocusInterval NOTIFY delayFocusIntervalChanged) + /** + * Whether to see Xinerama screens separately for focus (in Alt+Tab, when activating next client) + */ + Q_PROPERTY(bool separateScreenFocus READ isSeparateScreenFocus WRITE setSeparateScreenFocus NOTIFY separateScreenFocusChanged) + Q_PROPERTY(PlacementPolicy placement READ placement WRITE setPlacement NOTIFY placementChanged) + Q_PROPERTY(ActivationDesktopPolicy activationDesktopPolicy READ activationDesktopPolicy WRITE setActivationDesktopPolicy NOTIFY activationDesktopPolicyChanged) + Q_PROPERTY(bool focusPolicyIsReasonable READ focusPolicyIsReasonable NOTIFY focusPolicyIsResonableChanged) + /** + * The size of the zone that triggers snapping on desktop borders. + */ + Q_PROPERTY(int borderSnapZone READ borderSnapZone WRITE setBorderSnapZone NOTIFY borderSnapZoneChanged) + /** + * The size of the zone that triggers snapping with other windows. + */ + Q_PROPERTY(int windowSnapZone READ windowSnapZone WRITE setWindowSnapZone NOTIFY windowSnapZoneChanged) + /** + * The size of the zone that triggers snapping on the screen center. + */ + Q_PROPERTY(int centerSnapZone READ centerSnapZone WRITE setCenterSnapZone NOTIFY centerSnapZoneChanged) + /** + * Snap only when windows will overlap. + */ + Q_PROPERTY(bool snapOnlyWhenOverlapping READ isSnapOnlyWhenOverlapping WRITE setSnapOnlyWhenOverlapping NOTIFY snapOnlyWhenOverlappingChanged) + /** + * The size of the virtual barrier at edges between screens. + */ + Q_PROPERTY(int edgeBarrier READ edgeBarrier WRITE setEdgeBarrier NOTIFY edgeBarrierChanged) + /** + * Whether to enable a cursor barrier at the corners of the screen. + */ + Q_PROPERTY(int cornerBarrier READ cornerBarrier WRITE setCornerBarrier NOTIFY cornerBarrierChanged) + /** + * Whether or not we roll over to the other edge when switching desktops past the edge. + */ + Q_PROPERTY(bool rollOverDesktops READ isRollOverDesktops WRITE setRollOverDesktops NOTIFY rollOverDesktopsChanged) + /** + * 0 - 4 , see Workspace::allowWindowActivation() + */ + Q_PROPERTY(KWin::FocusStealingPreventionLevel focusStealingPreventionLevel READ focusStealingPreventionLevel WRITE setFocusStealingPreventionLevel NOTIFY focusStealingPreventionLevelChanged) + Q_PROPERTY(KWin::Options::WindowOperation operationTitlebarDblClick READ operationTitlebarDblClick WRITE setOperationTitlebarDblClick NOTIFY operationTitlebarDblClickChanged) + Q_PROPERTY(KWin::Options::WindowOperation operationMaxButtonLeftClick READ operationMaxButtonLeftClick WRITE setOperationMaxButtonLeftClick NOTIFY operationMaxButtonLeftClickChanged) + Q_PROPERTY(KWin::Options::WindowOperation operationMaxButtonMiddleClick READ operationMaxButtonMiddleClick WRITE setOperationMaxButtonMiddleClick NOTIFY operationMaxButtonMiddleClickChanged) + Q_PROPERTY(KWin::Options::WindowOperation operationMaxButtonRightClick READ operationMaxButtonRightClick WRITE setOperationMaxButtonRightClick NOTIFY operationMaxButtonRightClickChanged) + Q_PROPERTY(MouseCommand commandActiveTitlebar1 READ commandActiveTitlebar1 WRITE setCommandActiveTitlebar1 NOTIFY commandActiveTitlebar1Changed) + Q_PROPERTY(MouseCommand commandActiveTitlebar2 READ commandActiveTitlebar2 WRITE setCommandActiveTitlebar2 NOTIFY commandActiveTitlebar2Changed) + Q_PROPERTY(MouseCommand commandActiveTitlebar3 READ commandActiveTitlebar3 WRITE setCommandActiveTitlebar3 NOTIFY commandActiveTitlebar3Changed) + Q_PROPERTY(MouseCommand commandInactiveTitlebar1 READ commandInactiveTitlebar1 WRITE setCommandInactiveTitlebar1 NOTIFY commandInactiveTitlebar1Changed) + Q_PROPERTY(MouseCommand commandInactiveTitlebar2 READ commandInactiveTitlebar2 WRITE setCommandInactiveTitlebar2 NOTIFY commandInactiveTitlebar2Changed) + Q_PROPERTY(MouseCommand commandInactiveTitlebar3 READ commandInactiveTitlebar3 WRITE setCommandInactiveTitlebar3 NOTIFY commandInactiveTitlebar3Changed) + Q_PROPERTY(MouseCommand commandWindow1 READ commandWindow1 WRITE setCommandWindow1 NOTIFY commandWindow1Changed) + Q_PROPERTY(MouseCommand commandWindow2 READ commandWindow2 WRITE setCommandWindow2 NOTIFY commandWindow2Changed) + Q_PROPERTY(MouseCommand commandWindow3 READ commandWindow3 WRITE setCommandWindow3 NOTIFY commandWindow3Changed) + Q_PROPERTY(MouseCommand commandWindowWheel READ commandWindowWheel WRITE setCommandWindowWheel NOTIFY commandWindowWheelChanged) + Q_PROPERTY(MouseCommand commandAll1 READ commandAll1 WRITE setCommandAll1 NOTIFY commandAll1Changed) + Q_PROPERTY(MouseCommand commandAll2 READ commandAll2 WRITE setCommandAll2 NOTIFY commandAll2Changed) + Q_PROPERTY(MouseCommand commandAll3 READ commandAll3 WRITE setCommandAll3 NOTIFY commandAll3Changed) + Q_PROPERTY(uint keyCmdAllModKey READ keyCmdAllModKey WRITE setKeyCmdAllModKey NOTIFY keyCmdAllModKeyChanged) + Q_PROPERTY(bool doubleClickBorderToMaximize READ doubleClickBorderToMaximize WRITE setDoubleClickBorderToMaximize NOTIFY doubleClickBorderToMaximizeChanged) + /** + * Whether the visible name should be condensed. + */ + Q_PROPERTY(bool condensedTitle READ condensedTitle WRITE setCondensedTitle NOTIFY condensedTitleChanged) + /** + * Whether a window gets maximized when it reaches top screen edge while being moved. + */ + Q_PROPERTY(bool electricBorderMaximize READ electricBorderMaximize WRITE setElectricBorderMaximize NOTIFY electricBorderMaximizeChanged) + /** + * Whether a window is tiled to half screen when reaching left or right screen edge while been moved. + */ + Q_PROPERTY(bool electricBorderTiling READ electricBorderTiling WRITE setElectricBorderTiling NOTIFY electricBorderTilingChanged) + /** + * Whether each screen has its own active corners instead of limiting to only four corners across all screens. + */ + Q_PROPERTY(bool electricBorderAllScreenCorner READ electricBorderAllScreenCorner WRITE setElectricBorderAllScreenCorner NOTIFY electricBorderAllScreenCornerChanged) + /** + * Whether a window is tiled to half screen when reaching left or right screen edge while been moved. + */ + Q_PROPERTY(float electricBorderCornerRatio READ electricBorderCornerRatio WRITE setElectricBorderCornerRatio NOTIFY electricBorderCornerRatioChanged) + Q_PROPERTY(bool borderlessMaximizedWindows READ borderlessMaximizedWindows WRITE setBorderlessMaximizedWindows NOTIFY borderlessMaximizedWindowsChanged) + /** + * timeout before non-responding application will be killed after attempt to close. + */ + Q_PROPERTY(int killPingTimeout READ killPingTimeout WRITE setKillPingTimeout NOTIFY killPingTimeoutChanged) + Q_PROPERTY(int compositingMode READ compositingMode WRITE setCompositingMode NOTIFY compositingModeChanged) + /** + * 0 = no, 1 = yes when transformed, + * 2 = try trilinear when transformed; else 1, + * -1 = auto + */ + Q_PROPERTY(bool allowTearing READ allowTearing WRITE setAllowTearing NOTIFY allowTearingChanged) + Q_PROPERTY(bool interactiveWindowMoveEnabled READ interactiveWindowMoveEnabled WRITE setInteractiveWindowMoveEnabled NOTIFY interactiveWindowMoveEnabledChanged) + Q_PROPERTY(Qt::Corner pictureInPictureHomeCorner READ pictureInPictureHomeCorner WRITE setPictureInPictureHomeCorner NOTIFY pictureInPictureHomeCornerChanged) + Q_PROPERTY(int pictureInPictureMargin READ pictureInPictureMargin WRITE setPictureInPictureMargin NOTIFY pictureInPictureMarginChanged) + Q_PROPERTY(bool overlayVirtualKeyboardOnWindows READ overlayVirtualKeyboardOnWindows WRITE setOverlayVirtualKeyboardOnWindows NOTIFY overlayVirtualKeyboardOnWindowsChanged) +public: + explicit Options(QObject *parent = nullptr); + ~Options() override; + + void updateSettings(); + + /** + * This enum type is used to specify the focus policy. + * + * Note that FocusUnderMouse and FocusStrictlyUnderMouse are not + * particularly useful. They are only provided for old-fashined + * die-hard UNIX people ;-) + */ + enum FocusPolicy { + /** + * Clicking into a window activates it. This is also the default. + */ + ClickToFocus, + /** + * Moving the mouse pointer actively onto a normal window activates it. + * For convenience, the desktop and windows on the dock are excluded. + * They require clicking. + */ + FocusFollowsMouse, + /** + * The window that happens to be under the mouse pointer becomes active. + * The invariant is: no window can have focus that is not under the mouse. + * This also means that Alt-Tab won't work properly and popup dialogs are + * usually unusable with the keyboard. Note that the desktop and windows on + * the dock are excluded for convenience. They get focus only when clicking + * on it. + */ + FocusUnderMouse, + /** + * This is even worse than FocusUnderMouse. Only the window under the mouse + * pointer is active. If the mouse points nowhere, nothing has the focus. If + * the mouse points onto the desktop, the desktop has focus. The same holds + * for windows on the dock. + */ + FocusStrictlyUnderMouse + }; + Q_ENUM(FocusPolicy) + + FocusPolicy focusPolicy() const + { + return m_focusPolicy; + } + bool isNextFocusPrefersMouse() const + { + return m_nextFocusPrefersMouse; + } + + XwaylandCrashPolicy xwaylandCrashPolicy() const + { + return m_xwaylandCrashPolicy; + } + int xwaylandMaxCrashCount() const + { + return m_xwaylandMaxCrashCount; + } + XwaylandEavesdropsMode xwaylandEavesdrops() const + { + return m_xwaylandEavesdrops; + } + bool xwaylandEavesdropsMouse() const + { + return m_xwaylandEavesdropsMouse; + } + bool xwaylandEisNoPrompt() const + { + return m_xwaylandEisNoPrompt; + } + + /** + * Whether clicking on a window raises it in FocusFollowsMouse + * mode or not. + */ + bool isClickRaise() const + { + return m_clickRaise; + } + + /** + * Whether autoraise is enabled FocusFollowsMouse mode or not. + */ + bool isAutoRaise() const + { + return m_autoRaise; + } + + /** + * Autoraise interval + */ + int autoRaiseInterval() const + { + return m_autoRaiseInterval; + } + + /** + * Delayed focus interval. + */ + int delayFocusInterval() const + { + return m_delayFocusInterval; + } + + /** + * Whether to see Xinerama screens separately for focus (in Alt+Tab, when activating next client) + */ + bool isSeparateScreenFocus() const + { + return m_separateScreenFocus; + } + + PlacementPolicy placement() const + { + return m_placement; + } + + bool focusPolicyIsReasonable() + { + return m_focusPolicy == ClickToFocus || m_focusPolicy == FocusFollowsMouse; + } + + enum ActivationDesktopPolicy { + SwitchToOtherDesktop, + BringToCurrentDesktop, + DoNothing, + }; + Q_ENUM(ActivationDesktopPolicy) + + ActivationDesktopPolicy activationDesktopPolicy() const + { + return m_activationDesktopPolicy; + } + + /** + * The size of the zone that triggers snapping on desktop borders. + */ + int borderSnapZone() const + { + return m_borderSnapZone; + } + + /** + * The size of the zone that triggers snapping with other windows. + */ + int windowSnapZone() const + { + return m_windowSnapZone; + } + + /** + * The size of the zone that triggers snapping on the screen center. + */ + int centerSnapZone() const + { + return m_centerSnapZone; + } + + /** + * Snap only when windows will overlap. + */ + bool isSnapOnlyWhenOverlapping() const + { + return m_snapOnlyWhenOverlapping; + } + + /** + * The size of the virtual barrier at edges between screens. + */ + int edgeBarrier() const + { + return m_edgeBarrier; + } + + /** + * Whether to enable a cursor barrier at the corners of the screen. + */ + int cornerBarrier() const + { + return m_cornerBarrier; + } + + /** + * Whether or not we roll over to the other edge when switching desktops past the edge. + */ + bool isRollOverDesktops() const + { + return m_rollOverDesktops; + } + + /** + * Returns the focus stealing prevention level. + * + * @see allowWindowActivation + */ + FocusStealingPreventionLevel focusStealingPreventionLevel() const + { + return m_focusStealingPreventionLevel; + } + + enum WindowOperation { + MaximizeOp = 5000, + RestoreOp, + MinimizeOp, + MoveOp, + UnrestrictedMoveOp, + ResizeOp, + UnrestrictedResizeOp, + CloseOp, + OnAllDesktopsOp, + KeepAboveOp, + KeepBelowOp, + WindowRulesOp, + ToggleStoreSettingsOp = WindowRulesOp, ///< @obsolete + HMaximizeOp, + VMaximizeOp, + LowerOp, + FullScreenOp, + NoBorderOp, + ExcludeFromCaptureOp, + NoOp, + SetupWindowShortcutOp, + ApplicationRulesOp, + }; + Q_ENUM(WindowOperation) + + WindowOperation operationTitlebarDblClick() const + { + return OpTitlebarDblClick; + } + WindowOperation operationMaxButtonLeftClick() const + { + return opMaxButtonLeftClick; + } + WindowOperation operationMaxButtonRightClick() const + { + return opMaxButtonRightClick; + } + WindowOperation operationMaxButtonMiddleClick() const + { + return opMaxButtonMiddleClick; + } + WindowOperation operationMaxButtonClick(Qt::MouseButtons button) const; + + bool doubleClickBorderToMaximize() const + { + return m_doubleClickBorderToMaximize; + } + + enum MouseCommand { + MouseRaise, + MouseLower, + MouseOperationsMenu, + MouseToggleRaiseAndLower, + MouseActivateAndRaise, + MouseActivateAndLower, + MouseActivate, + MouseActivateRaiseAndPassClick, + MouseActivateAndPassClick, + MouseMove, + MouseUnrestrictedMove, + MouseActivateRaiseAndMove, + MouseActivateRaiseAndUnrestrictedMove, + MouseResize, + MouseUnrestrictedResize, + MouseMaximize, + MouseRestore, + MouseMinimize, + MouseNextDesktop, + MousePreviousDesktop, + MouseAbove, + MouseBelow, + MouseOpacityMore, + MouseOpacityLess, + MouseClose, + MouseNothing, + MouseActivateRaiseOnReleaseAndPassClick, + }; + Q_ENUM(MouseCommand) + + enum MouseWheelCommand { + MouseWheelRaiseLower, + MouseWheelMaximizeRestore, + MouseWheelAboveBelow, + MouseWheelPreviousNextDesktop, + MouseWheelChangeOpacity, + MouseWheelNothing + }; + Q_ENUM(MouseWheelCommand) + + MouseCommand operationTitlebarMouseWheel(qreal delta) const + { + return wheelToMouseCommand(CmdTitlebarWheel, delta); + } + MouseCommand operationWindowMouseWheel(qreal delta) const + { + return wheelToMouseCommand(CmdAllWheel, delta); + } + + MouseCommand commandActiveTitlebar1() const + { + return CmdActiveTitlebar1; + } + MouseCommand commandActiveTitlebar2() const + { + return CmdActiveTitlebar2; + } + MouseCommand commandActiveTitlebar3() const + { + return CmdActiveTitlebar3; + } + MouseCommand commandInactiveTitlebar1() const + { + return CmdInactiveTitlebar1; + } + MouseCommand commandInactiveTitlebar2() const + { + return CmdInactiveTitlebar2; + } + MouseCommand commandInactiveTitlebar3() const + { + return CmdInactiveTitlebar3; + } + MouseCommand commandWindow1() const + { + return CmdWindow1; + } + MouseCommand commandWindow2() const + { + return CmdWindow2; + } + MouseCommand commandWindow3() const + { + return CmdWindow3; + } + MouseCommand commandWindowWheel() const + { + return CmdWindowWheel; + } + MouseCommand commandAll1() const + { + return CmdAll1; + } + MouseCommand commandAll2() const + { + return CmdAll2; + } + MouseCommand commandAll3() const + { + return CmdAll3; + } + MouseWheelCommand commandAllWheel() const + { + return CmdAllWheel; + } + uint keyCmdAllModKey() const + { + return CmdAllModKey; + } + Qt::KeyboardModifier commandAllModifier() const + { + switch (CmdAllModKey) { + case Qt::Key_Alt: + return Qt::AltModifier; + case Qt::Key_Meta: + return Qt::MetaModifier; + default: + Q_UNREACHABLE(); + } + } + + static WindowOperation windowOperation(const QString &name, bool restricted); + static MouseCommand mouseCommand(const QString &name, bool restricted); + static MouseWheelCommand mouseWheelCommand(const QString &name); + + /** + * Returns whether the user prefers his caption clean. + */ + bool condensedTitle() const; + + /** + * @returns true if a window gets maximized when it reaches top screen edge + * while being moved. + */ + bool electricBorderMaximize() const + { + return electric_border_maximize; + } + /** + * @returns true if window is tiled to half screen when reaching left or + * right screen edge while been moved. + */ + bool electricBorderTiling() const + { + return electric_border_tiling; + } + /** + * @returns true if each screen has its own active corners instead of limiting + * to only four corners across all screens. + */ + bool electricBorderAllScreenCorner() const + { + return electric_border_all_screen_corner; + } + /** + * @returns the factor that determines the corner part of the edge (ie. 0.1 means tiny corner) + */ + float electricBorderCornerRatio() const + { + return electric_border_corner_ratio; + } + + bool borderlessMaximizedWindows() const + { + return borderless_maximized_windows; + } + + /** + * Timeout before non-responding application will be killed after attempt to close. + */ + int killPingTimeout() const + { + return m_killPingTimeout; + } + + /** + * Returns the animation time factor for desktop effects. + */ + double animationTimeFactor() const; + + CompositingType compositingMode() const + { + return m_compositingMode; + } + void setCompositingMode(CompositingType mode) + { + m_compositingMode = mode; + } + + bool allowTearing() const; + bool interactiveWindowMoveEnabled() const; + bool overlayVirtualKeyboardOnWindows() const; + + Qt::Corner pictureInPictureHomeCorner() const; + void setPictureInPictureHomeCorner(Qt::Corner corner); + + int pictureInPictureMargin() const; + void setPictureInPictureMargin(int margin); + + // setters + void setFocusPolicy(FocusPolicy focusPolicy); + void setXwaylandCrashPolicy(XwaylandCrashPolicy crashPolicy); + void setXwaylandMaxCrashCount(int maxCrashCount); + void setXwaylandEavesdrops(XwaylandEavesdropsMode mode); + void setXwaylandEavesdropsMouse(bool eavesdropsMouse); + void setXWaylandEisNoPrompt(bool doNotPrompt); + void setNextFocusPrefersMouse(bool nextFocusPrefersMouse); + void setClickRaise(bool clickRaise); + void setAutoRaise(bool autoRaise); + void setAutoRaiseInterval(int autoRaiseInterval); + void setDelayFocusInterval(int delayFocusInterval); + void setSeparateScreenFocus(bool separateScreenFocus); + void setPlacement(PlacementPolicy placement); + void setActivationDesktopPolicy(ActivationDesktopPolicy activationDesktopPolicy); + void setBorderSnapZone(int borderSnapZone); + void setWindowSnapZone(int windowSnapZone); + void setCenterSnapZone(int centerSnapZone); + void setSnapOnlyWhenOverlapping(bool snapOnlyWhenOverlapping); + void setEdgeBarrier(int edgeBarrier); + void setCornerBarrier(bool cornerBarrier); + void setRollOverDesktops(bool rollOverDesktops); + void setFocusStealingPreventionLevel(FocusStealingPreventionLevel focusStealingPreventionLevel); + void setOperationTitlebarDblClick(WindowOperation operationTitlebarDblClick); + void setOperationMaxButtonLeftClick(WindowOperation op); + void setOperationMaxButtonRightClick(WindowOperation op); + void setOperationMaxButtonMiddleClick(WindowOperation op); + void setCommandActiveTitlebar1(MouseCommand commandActiveTitlebar1); + void setCommandActiveTitlebar2(MouseCommand commandActiveTitlebar2); + void setCommandActiveTitlebar3(MouseCommand commandActiveTitlebar3); + void setCommandInactiveTitlebar1(MouseCommand commandInactiveTitlebar1); + void setCommandInactiveTitlebar2(MouseCommand commandInactiveTitlebar2); + void setCommandInactiveTitlebar3(MouseCommand commandInactiveTitlebar3); + void setCommandWindow1(MouseCommand commandWindow1); + void setCommandWindow2(MouseCommand commandWindow2); + void setCommandWindow3(MouseCommand commandWindow3); + void setCommandWindowWheel(MouseCommand commandWindowWheel); + void setCommandAll1(MouseCommand commandAll1); + void setCommandAll2(MouseCommand commandAll2); + void setCommandAll3(MouseCommand commandAll3); + void setKeyCmdAllModKey(uint keyCmdAllModKey); + void setDoubleClickBorderToMaximize(bool maximize); + void setCondensedTitle(bool condensedTitle); + void setElectricBorderMaximize(bool electricBorderMaximize); + void setElectricBorderAllScreenCorner(bool electricBorderAllScreenCorner); + void setElectricBorderTiling(bool electricBorderTiling); + void setElectricBorderCornerRatio(float electricBorderCornerRatio); + void setBorderlessMaximizedWindows(bool borderlessMaximizedWindows); + void setKillPingTimeout(int killPingTimeout); + void setCompositingMode(int compositingMode); + void setAllowTearing(bool allow); + void setInteractiveWindowMoveEnabled(bool set); + void setOverlayVirtualKeyboardOnWindows(bool overlay); + + // default values + static WindowOperation defaultOperationTitlebarDblClick() + { + return MaximizeOp; + } + static WindowOperation defaultOperationMaxButtonLeftClick() + { + return MaximizeOp; + } + static WindowOperation defaultOperationMaxButtonRightClick() + { + return HMaximizeOp; + } + static WindowOperation defaultOperationMaxButtonMiddleClick() + { + return VMaximizeOp; + } + static MouseCommand defaultCommandActiveTitlebar1() + { + return MouseRaise; + } + static MouseCommand defaultCommandActiveTitlebar2() + { + return MouseNothing; + } + static MouseCommand defaultCommandActiveTitlebar3() + { + return MouseOperationsMenu; + } + static MouseCommand defaultCommandInactiveTitlebar1() + { + return MouseActivateAndRaise; + } + static MouseCommand defaultCommandInactiveTitlebar2() + { + return MouseNothing; + } + static MouseCommand defaultCommandInactiveTitlebar3() + { + return MouseOperationsMenu; + } + static MouseCommand defaultCommandWindow1() + { + return MouseActivateRaiseOnReleaseAndPassClick; + } + static MouseCommand defaultCommandWindow2() + { + return MouseActivateAndPassClick; + } + static MouseCommand defaultCommandWindow3() + { + return MouseActivateAndPassClick; + } + static MouseCommand defaultCommandWindowWheel() + { + return MouseNothing; + } + static MouseCommand defaultCommandAll1() + { + return MouseUnrestrictedMove; + } + static MouseCommand defaultCommandAll2() + { + return MouseToggleRaiseAndLower; + } + static MouseCommand defaultCommandAll3() + { + return MouseUnrestrictedResize; + } + static MouseWheelCommand defaultCommandTitlebarWheel() + { + return MouseWheelNothing; + } + static MouseWheelCommand defaultCommandAllWheel() + { + return MouseWheelNothing; + } + static uint defaultKeyCmdAllModKey() + { + return Qt::Key_Alt; + } + static CompositingType defaultCompositingMode() + { + return OpenGLCompositing; + } + static XwaylandCrashPolicy defaultXwaylandCrashPolicy() + { + return XwaylandCrashPolicy::Restart; + } + static int defaultXwaylandMaxCrashCount() + { + return 3; + } + static XwaylandEavesdropsMode defaultXwaylandEavesdrops() + { + return XwaylandEavesdropsMode::AllKeysWithModifier; + } + static bool defaultXwaylandEavesdropsMouse() + { + return false; + } + static bool defaultXwaylandEisNoPrompt() + { + return false; + } + static ActivationDesktopPolicy defaultActivationDesktopPolicy() + { + return ActivationDesktopPolicy::SwitchToOtherDesktop; + } + /** + * Performs loading all settings except compositing related. + */ + void loadConfig(); + void reparseConfiguration(); + + //---------------------- +Q_SIGNALS: + // for properties + void focusPolicyChanged(); + void focusPolicyIsResonableChanged(); + void xwaylandCrashPolicyChanged(); + void xwaylandMaxCrashCountChanged(); + void xwaylandEavesdropsChanged(); + void xwaylandEavesdropsMouseChanged(); + void xwaylandEisNoPromptChanged(); + void nextFocusPrefersMouseChanged(); + void clickRaiseChanged(); + void autoRaiseChanged(); + void autoRaiseIntervalChanged(); + void delayFocusIntervalChanged(); + void separateScreenFocusChanged(bool); + void placementChanged(); + void activationDesktopPolicyChanged(); + void borderSnapZoneChanged(); + void windowSnapZoneChanged(); + void centerSnapZoneChanged(); + void snapOnlyWhenOverlappingChanged(); + void edgeBarrierChanged(); + void cornerBarrierChanged(); + void rollOverDesktopsChanged(bool enabled); + void focusStealingPreventionLevelChanged(); + void operationTitlebarDblClickChanged(); + void operationMaxButtonLeftClickChanged(); + void operationMaxButtonRightClickChanged(); + void operationMaxButtonMiddleClickChanged(); + void commandActiveTitlebar1Changed(); + void commandActiveTitlebar2Changed(); + void commandActiveTitlebar3Changed(); + void commandInactiveTitlebar1Changed(); + void commandInactiveTitlebar2Changed(); + void commandInactiveTitlebar3Changed(); + void commandWindow1Changed(); + void commandWindow2Changed(); + void commandWindow3Changed(); + void commandWindowWheelChanged(); + void commandAll1Changed(); + void commandAll2Changed(); + void commandAll3Changed(); + void keyCmdAllModKeyChanged(); + void doubleClickBorderToMaximizeChanged(); + void condensedTitleChanged(); + void electricBorderMaximizeChanged(); + void electricBorderTilingChanged(); + void electricBorderCornerRatioChanged(); + void electricBorderAllScreenCornerChanged(); + void borderlessMaximizedWindowsChanged(); + void killPingTimeoutChanged(); + void compositingModeChanged(); + void animationSpeedChanged(); + void configChanged(); + void allowTearingChanged(); + void interactiveWindowMoveEnabledChanged(); + void pictureInPictureHomeCornerChanged(); + void pictureInPictureMarginChanged(); + void overlayVirtualKeyboardOnWindowsChanged(); + +private: + void setElectricBorders(int borders); + void syncFromKcfgc(); + std::unique_ptr m_settings; + KConfigWatcher::Ptr m_configWatcher; + + FocusPolicy m_focusPolicy; + bool m_nextFocusPrefersMouse; + bool m_clickRaise; + bool m_autoRaise; + int m_autoRaiseInterval; + int m_delayFocusInterval; + bool m_separateScreenFocus; + PlacementPolicy m_placement; + ActivationDesktopPolicy m_activationDesktopPolicy; + int m_borderSnapZone; + int m_windowSnapZone; + int m_centerSnapZone; + bool m_snapOnlyWhenOverlapping; + int m_edgeBarrier; + bool m_cornerBarrier; + bool m_rollOverDesktops; + FocusStealingPreventionLevel m_focusStealingPreventionLevel; + int m_killPingTimeout; + XwaylandCrashPolicy m_xwaylandCrashPolicy; + int m_xwaylandMaxCrashCount; + XwaylandEavesdropsMode m_xwaylandEavesdrops; + bool m_xwaylandEavesdropsMouse; + bool m_xwaylandEisNoPrompt; + + CompositingType m_compositingMode; + WindowOperation OpTitlebarDblClick; + WindowOperation opMaxButtonRightClick = defaultOperationMaxButtonRightClick(); + WindowOperation opMaxButtonMiddleClick = defaultOperationMaxButtonMiddleClick(); + WindowOperation opMaxButtonLeftClick = defaultOperationMaxButtonRightClick(); + + // mouse bindings + MouseCommand CmdActiveTitlebar1; + MouseCommand CmdActiveTitlebar2; + MouseCommand CmdActiveTitlebar3; + MouseCommand CmdInactiveTitlebar1; + MouseCommand CmdInactiveTitlebar2; + MouseCommand CmdInactiveTitlebar3; + MouseWheelCommand CmdTitlebarWheel; + MouseCommand CmdWindow1; + MouseCommand CmdWindow2; + MouseCommand CmdWindow3; + MouseCommand CmdWindowWheel; + MouseCommand CmdAll1; + MouseCommand CmdAll2; + MouseCommand CmdAll3; + MouseWheelCommand CmdAllWheel; + uint CmdAllModKey; + + bool electric_border_maximize; + bool electric_border_tiling; + bool electric_border_all_screen_corner; + float electric_border_corner_ratio; + bool borderless_maximized_windows; + bool condensed_title; + + bool m_allowTearing = true; + bool m_interactiveWindowMoveEnabled = true; + bool m_overlayVirtualKeyboardOnWindows = false; + bool m_doubleClickBorderToMaximize = true; + + Qt::Corner m_pictureInPictureHomeCorner = Qt::BottomRightCorner; + int m_pictureInPictureMargin = 20; + + MouseCommand wheelToMouseCommand(MouseWheelCommand com, qreal delta) const; +}; + +extern KWIN_EXPORT Options *options; + +} // namespace + +Q_DECLARE_METATYPE(KWin::Options::WindowOperation) diff --git a/local/recipes/kde/kwin/source/src/org.freedesktop.DBus.Properties.xml b/local/recipes/kde/kwin/source/src/org.freedesktop.DBus.Properties.xml new file mode 100644 index 0000000000..0967d899b3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/org.freedesktop.DBus.Properties.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/org.kde.KWin.Plugins.xml b/local/recipes/kde/kwin/source/src/org.kde.KWin.Plugins.xml new file mode 100644 index 0000000000..43a3f736d6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/org.kde.KWin.Plugins.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/org.kde.KWin.Session.xml b/local/recipes/kde/kwin/source/src/org.kde.KWin.Session.xml new file mode 100644 index 0000000000..35adf52741 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/org.kde.KWin.Session.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/org.kde.KWin.VirtualDesktopManager.xml b/local/recipes/kde/kwin/source/src/org.kde.KWin.VirtualDesktopManager.xml new file mode 100644 index 0000000000..3283764f0f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/org.kde.KWin.VirtualDesktopManager.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/org.kde.KWin.xml b/local/recipes/kde/kwin/source/src/org.kde.KWin.xml new file mode 100644 index 0000000000..b073a71cd5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/org.kde.KWin.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/org.kde.kappmenu.xml b/local/recipes/kde/kwin/source/src/org.kde.kappmenu.xml new file mode 100644 index 0000000000..d29d3ee86b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/org.kde.kappmenu.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/org.kde.kwin.Compositing.xml b/local/recipes/kde/kwin/source/src/org.kde.kwin.Compositing.xml new file mode 100644 index 0000000000..7b6ac5a353 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/org.kde.kwin.Compositing.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/org.kde.kwin.Effects.xml b/local/recipes/kde/kwin/source/src/org.kde.kwin.Effects.xml new file mode 100644 index 0000000000..ebb88984e2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/org.kde.kwin.Effects.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/osd.cpp b/local/recipes/kde/kwin/source/src/osd.cpp new file mode 100644 index 0000000000..b46b46910a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/osd.cpp @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Graesslin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +*/ +#include "osd.h" +#include "main.h" +#include "onscreennotification.h" +#include "scripting/scripting.h" +#include "workspace.h" + +#include +#include + +namespace KWin +{ +namespace OSD +{ + +static OnScreenNotification *create() +{ + auto osd = new OnScreenNotification(workspace()); + osd->setConfig(kwinApp()->config()); + osd->setEngine(Scripting::self()->qmlEngine()); + return osd; +} + +static OnScreenNotification *osd() +{ + static OnScreenNotification *s_osd = create(); + return s_osd; +} + +void show(const QString &message, const QString &iconName, int timeout) +{ + if (QThread::currentThread() != qGuiApp->thread()) { + QTimer::singleShot(0, QCoreApplication::instance(), [message, iconName, timeout] { + show(message, iconName, timeout); + }); + return; + } + + auto notification = osd(); + notification->setIconName(iconName); + notification->setMessage(message); + notification->setTimeout(timeout); + notification->setVisible(true); +} + +void show(const QString &message, int timeout) +{ + show(message, QString(), timeout); +} + +void show(const QString &message, const QString &iconName) +{ + show(message, iconName, 0); +} + +void hide(HideFlags flags) +{ + osd()->setSkipCloseAnimation(flags.testFlag(HideFlag::SkipCloseAnimation)); + osd()->setVisible(false); +} +} +} diff --git a/local/recipes/kde/kwin/source/src/osd.h b/local/recipes/kde/kwin/source/src/osd.h new file mode 100644 index 0000000000..b746bd0721 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/osd.h @@ -0,0 +1,28 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Graesslin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +*/ + +#pragma once + +#include +#include + +namespace KWin +{ +namespace OSD +{ + +void show(const QString &message, const QString &iconName = QString()); +void show(const QString &message, int timeout); +void show(const QString &message, const QString &iconName, int timeout); +enum class HideFlag { + SkipCloseAnimation = 1, +}; +Q_DECLARE_FLAGS(HideFlags, HideFlag) +void hide(HideFlags flags = HideFlags()); + +} +} diff --git a/local/recipes/kde/kwin/source/src/outline.cpp b/local/recipes/kde/kwin/source/src/outline.cpp new file mode 100644 index 0000000000..114d51ba27 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/outline.cpp @@ -0,0 +1,180 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Arthur Arlt + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +// own +#include "outline.h" +// KWin +#include "compositor.h" +#include "main.h" +#include "scripting/scripting.h" +#include "utils/common.h" +// Frameworks +#include +// Qt +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +Outline::Outline() + : m_active(false) +{ + connect(Compositor::self(), &Compositor::compositingToggled, this, &Outline::compositingChanged); +} + +Outline::~Outline() = default; + +void Outline::show() +{ + if (!m_visual) { + createHelper(); + } + if (!m_visual) { + // something went wrong + return; + } + m_visual->show(); + m_active = true; + Q_EMIT activeChanged(); +} + +void Outline::hide() +{ + if (!m_active) { + return; + } + m_active = false; + Q_EMIT activeChanged(); + if (!m_visual) { + return; + } + m_visual->hide(); +} + +void Outline::show(const Rect &outlineGeometry) +{ + show(outlineGeometry, Rect()); +} + +void Outline::show(const Rect &outlineGeometry, const Rect &visualParentGeometry) +{ + setGeometry(outlineGeometry); + setVisualParentGeometry(visualParentGeometry); + show(); +} + +void Outline::setGeometry(const Rect &outlineGeometry) +{ + if (m_outlineGeometry == outlineGeometry) { + return; + } + m_outlineGeometry = outlineGeometry; + Q_EMIT geometryChanged(); + Q_EMIT unifiedGeometryChanged(); +} + +void Outline::setVisualParentGeometry(const Rect &visualParentGeometry) +{ + if (m_visualParentGeometry == visualParentGeometry) { + return; + } + m_visualParentGeometry = visualParentGeometry; + Q_EMIT visualParentGeometryChanged(); + Q_EMIT unifiedGeometryChanged(); +} + +Rect Outline::unifiedGeometry() const +{ + return m_outlineGeometry | m_visualParentGeometry; +} + +void Outline::createHelper() +{ + if (m_visual) { + return; + } + m_visual = std::make_unique(this); +} + +void Outline::compositingChanged() +{ + m_visual.reset(); + if (m_active) { + show(); + } +} + +const Rect &Outline::geometry() const +{ + return m_outlineGeometry; +} + +const Rect &Outline::visualParentGeometry() const +{ + return m_visualParentGeometry; +} + +bool Outline::isActive() const +{ + return m_active; +} +OutlineVisual::OutlineVisual(Outline *outline) + : m_outline(outline) + , m_qmlContext() + , m_qmlComponent() + , m_mainItem() +{ +} + +OutlineVisual::~OutlineVisual() +{ +} + +void OutlineVisual::hide() +{ + if (QQuickWindow *w = qobject_cast(m_mainItem.get())) { + w->hide(); + w->destroy(); + } +} + +void OutlineVisual::show() +{ + if (!m_qmlContext) { + m_qmlContext = std::make_unique(Scripting::self()->qmlEngine()); + m_qmlContext->setContextProperty(QStringLiteral("outline"), m_outline); + } + if (!m_qmlComponent) { + m_qmlComponent = std::make_unique(Scripting::self()->qmlEngine()); + const QString fileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, + kwinApp()->config()->group(QStringLiteral("Outline")).readEntry("QmlPath", QStringLiteral("kwin-wayland/outline/plasma/outline.qml"))); + if (fileName.isEmpty()) { + qCDebug(KWIN_CORE) << "Could not locate outline.qml"; + return; + } + m_qmlComponent->loadUrl(QUrl::fromLocalFile(fileName)); + if (m_qmlComponent->isError()) { + qCDebug(KWIN_CORE) << "Component failed to load: " << m_qmlComponent->errors(); + } else { + m_mainItem.reset(m_qmlComponent->create(m_qmlContext.get())); + } + if (auto w = qobject_cast(m_mainItem.get())) { + w->setProperty("__kwin_outline", true); + } + } +} + +} // namespace + +#include "moc_outline.cpp" diff --git a/local/recipes/kde/kwin/source/src/outline.h b/local/recipes/kde/kwin/source/src/outline.h new file mode 100644 index 0000000000..f5df4c2916 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/outline.h @@ -0,0 +1,134 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Arthur Arlt + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/rect.h" + +#include + +#include + +class QQmlContext; +class QQmlComponent; + +namespace KWin +{ +class OutlineVisual; + +/** + * @short This class is used to show the outline of a given geometry. + * + * The class renders an outline by using four windows. One for each border of + * the geometry. It is possible to replace the outline with an effect. If an + * effect is available the effect will be used, otherwise the outline will be + * rendered by using the X implementation. + * + * @author Arthur Arlt + * @since 4.7 + */ +class KWIN_EXPORT Outline : public QObject +{ + Q_OBJECT + Q_PROPERTY(KWin::Rect geometry READ geometry NOTIFY geometryChanged) + Q_PROPERTY(KWin::Rect visualParentGeometry READ visualParentGeometry NOTIFY visualParentGeometryChanged) + Q_PROPERTY(KWin::Rect unifiedGeometry READ unifiedGeometry NOTIFY unifiedGeometryChanged) + Q_PROPERTY(bool active READ isActive NOTIFY activeChanged) +public: + explicit Outline(); + ~Outline() override; + + /** + * Set the outline geometry. + * To show the outline use showOutline. + * @param outlineGeometry The geometry of the outline to be shown + * @see showOutline + */ + void setGeometry(const Rect &outlineGeometry); + + /** + * Set the visual parent geometry. + * This is the geometry from which the will emerge. + * @param visualParentGeometry The visual geometry of the visual parent + * @see showOutline + */ + void setVisualParentGeometry(const Rect &visualParentGeometry); + + /** + * Shows the outline of a window using either an effect or the X implementation. + * To stop the outline process use hideOutline. + * @see hideOutline + */ + void show(); + + /** + * Shows the outline for the given @p outlineGeometry. + * This is the same as setOutlineGeometry followed by showOutline directly. + * To stop the outline process use hideOutline. + * @param outlineGeometry The geometry of the outline to be shown + * @see hideOutline + */ + void show(const Rect &outlineGeometry); + + /** + * Shows the outline for the given @p outlineGeometry animated from @p visualParentGeometry. + * This is the same as setOutlineGeometry followed by setVisualParentGeometry + * and then showOutline. + * To stop the outline process use hideOutline. + * @param outlineGeometry The geometry of the outline to be shown + * @param visualParentGeometry The geometry from where the outline should emerge + * @see hideOutline + * @since 5.10 + */ + void show(const Rect &outlineGeometry, const Rect &visualParentGeometry); + + /** + * Hides shown outline. + * @see showOutline + */ + void hide(); + + const Rect &geometry() const; + const Rect &visualParentGeometry() const; + Rect unifiedGeometry() const; + + bool isActive() const; + +private Q_SLOTS: + void compositingChanged(); + +Q_SIGNALS: + void activeChanged(); + void geometryChanged(); + void unifiedGeometryChanged(); + void visualParentGeometryChanged(); + +private: + void createHelper(); + std::unique_ptr m_visual; + Rect m_outlineGeometry; + Rect m_visualParentGeometry; + bool m_active; +}; + +class KWIN_EXPORT OutlineVisual +{ +public: + OutlineVisual(Outline *outline); + ~OutlineVisual(); + void show(); + void hide(); + +protected: + Outline *const m_outline; + std::unique_ptr m_qmlContext; + std::unique_ptr m_qmlComponent; + std::unique_ptr m_mainItem; +}; +} diff --git a/local/recipes/kde/kwin/source/src/outputconfigurationstore.cpp b/local/recipes/kde/kwin/source/src/outputconfigurationstore.cpp new file mode 100644 index 0000000000..e18e17b7b8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/outputconfigurationstore.cpp @@ -0,0 +1,1554 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "outputconfigurationstore.h" +#include "core/iccprofile.h" +#include "core/inputdevice.h" +#include "core/outputbackend.h" +#include "core/outputconfiguration.h" +#include "input.h" +#include "input_event.h" +#include "kscreenintegration.h" +#include "outputconfiglogging.h" +#include "utils/orientationsensor.h" +#include "workspace.h" + +#include +#include +#include +#include +#include + +namespace KWin +{ + +OutputConfigurationStore::OutputConfigurationStore() +{ + load(); +} + +OutputConfigurationStore::~OutputConfigurationStore() +{ + save(); +} + +void OutputConfigurationStore::clear() +{ + m_setups.clear(); + m_outputs.clear(); +} + +std::optional> OutputConfigurationStore::queryConfig(const QList &outputs, bool isLidClosed, AccelerometerOrientation orientation, bool isTabletMode) +{ + QList relevantOutputs; + std::copy_if(outputs.begin(), outputs.end(), std::back_inserter(relevantOutputs), [](BackendOutput *output) { + return !output->isNonDesktop() && !output->isPlaceholder(); + }); + if (relevantOutputs.isEmpty()) { + return std::nullopt; + } + // assigns uuids, if the outputs don't have one yet + registerOutputs(outputs); + if (const auto opt = findSetup(relevantOutputs, isLidClosed)) { + const auto &[setup, outputStates] = *opt; + auto config = setupToConfig(setup, outputStates); + applyOrientationReading(config, relevantOutputs, orientation, isTabletMode); + applyMirroring(config, relevantOutputs); + storeConfig(relevantOutputs, isLidClosed, config); + return std::make_tuple(config, ConfigType::Preexisting); + } + auto config = generateConfig(relevantOutputs, isLidClosed); + applyOrientationReading(config, relevantOutputs, orientation, isTabletMode); + applyMirroring(config, relevantOutputs); + storeConfig(relevantOutputs, isLidClosed, config); + return std::make_tuple(config, ConfigType::Generated); +} + +void OutputConfigurationStore::applyOrientationReading(OutputConfiguration &config, const QList &outputs, AccelerometerOrientation orientation, bool isTabletMode) +{ + const auto output = std::find_if(outputs.begin(), outputs.end(), [&config](BackendOutput *output) { + return output->isInternal() && config.changeSet(output)->enabled.value_or(output->isEnabled()); + }); + if (output == outputs.end()) { + return; + } + // TODO move other outputs to matching positions + const auto changeset = config.changeSet(*output); + if (!isAutoRotateActive(outputs, isTabletMode)) { + changeset->transform = changeset->manualTransform; + return; + } + // NOTE that udev "corrects" orientation sensors to match the panel orientation, + // the sensor values we get are already relative to the display, not the device. + switch (orientation) { + case AccelerometerOrientation::TopUp: + changeset->transform = OutputTransform::Kind::Normal; + return; + case AccelerometerOrientation::TopDown: + changeset->transform = OutputTransform::Kind::Rotate180; + return; + case AccelerometerOrientation::LeftUp: + changeset->transform = OutputTransform::Kind::Rotate90; + return; + case AccelerometerOrientation::RightUp: + changeset->transform = OutputTransform::Kind::Rotate270; + return; + case AccelerometerOrientation::FaceUp: + case AccelerometerOrientation::FaceDown: + return; + case AccelerometerOrientation::Undefined: + changeset->transform = changeset->manualTransform; + return; + } +} + +static QSize calculatePixelSize(OutputChangeSet *change, BackendOutput *output) +{ + const auto mode = change->mode.value_or(output->currentMode()).lock(); + Q_ASSERT(mode); + const auto transform = change->transform.value_or(output->transform()); + return transform.map(mode->size()); +} + +static double mirrorScale(QSize srcPixelSize, double srcScale, QSize dstPixelSize) +{ + const double wScale = dstPixelSize.width() / double(srcPixelSize.width()); + const double hScale = dstPixelSize.height() / double(srcPixelSize.height()); + return std::min(wScale, hScale) * srcScale; +} + +static QPoint calculateRenderOffset(QSize srcPixelSize, double srcScale, QSize dstResolution, double dstScale) +{ + // the output is mirroring another screen -> center and scale the viewport + const QSize usedArea = (QSizeF(srcPixelSize) / srcScale * dstScale).toSize(); + const QSize offset = (dstResolution - usedArea) / 2; + return QPoint(offset.width(), offset.height()); +} + +void OutputConfigurationStore::applyMirroring(OutputConfiguration &config, const QList &outputs) +{ + const auto enabledOutputs = outputs | std::views::filter([&config](BackendOutput *output) { + return config.changeSet(output)->enabled.value_or(output->isEnabled()); + }) | std::ranges::to(); + for (BackendOutput *output : enabledOutputs) { + auto cfg = config.changeSet(output); + const auto srcIt = std::ranges::find_if(enabledOutputs, [&cfg, &config, output](BackendOutput *other) { + if (output == other) { + return false; + } + const QString source = cfg->replicationSource.value_or(output->replicationSource()); + if (source.isEmpty()) { + return false; + } + const QString uuid = config.changeSet(other)->uuid.value_or(other->uuid()); + return source == uuid; + }); + if (srcIt == enabledOutputs.end()) { + // not mirroring anything + cfg->scale = cfg->scaleSetting.value_or(output->scaleSetting()); + cfg->deviceOffset = QPoint(); + continue; + } + // mirroring -> adjust scale and offset + BackendOutput *source = *srcIt; + const auto srcConfig = config.changeSet(source); + const QSize srcPixelSize = calculatePixelSize(srcConfig.get(), source); + const double srcScale = config.changeSet(source)->scaleSetting.value_or(source->scaleSetting()); + const QSize dstPixelSize = calculatePixelSize(cfg.get(), output); + cfg->scale = mirrorScale(srcPixelSize, srcScale, dstPixelSize); + cfg->deviceOffset = calculateRenderOffset(srcPixelSize, srcScale, dstPixelSize, *cfg->scale); + } +} + +std::optional OutputConfigurationStore::findSetup(const QList &outputs, bool lidClosed) +{ + std::unordered_map outputStates; + for (BackendOutput *output : outputs) { + if (auto opt = findOutputIndex(output, outputs)) { + outputStates[output] = *opt; + } else { + return std::nullopt; + } + } + const auto setup = std::find_if(m_setups.begin(), m_setups.end(), [lidClosed, &outputStates](const auto &setup) { + if (setup.lidClosed != lidClosed || size_t(setup.outputs.size()) != outputStates.size()) { + return false; + } + return std::all_of(outputStates.begin(), outputStates.end(), [&setup](const auto &outputIt) { + return std::any_of(setup.outputs.begin(), setup.outputs.end(), [&outputIt](const auto &outputInfo) { + return outputInfo.outputIndex == outputIt.second; + }); + }); + }); + if (setup == m_setups.end()) { + return std::nullopt; + } else { + return SetupWithOutputs{ + .setup = &*setup, + .globalOutputIndices = outputStates, + }; + } +} + +std::optional OutputConfigurationStore::findOutputIndex(BackendOutput *output, const QList &allOutputs) const +{ + struct Properties + { + std::optional edidIdentifier; + std::optional edidHash; + std::optional mstPath; + std::optional connectorName; + }; + const auto filterBy = [this](const Properties &props) { + std::vector ret; + for (ssize_t i = 0; i < m_outputs.size(); i++) { + const auto &state = m_outputs[i]; + if (props.edidIdentifier.has_value() && state.edidIdentifier != *props.edidIdentifier) { + continue; + } + if (props.edidHash.has_value() && state.edidHash != *props.edidHash) { + continue; + } + if (props.mstPath.has_value() && state.mstPath != *props.mstPath) { + continue; + } + if (props.connectorName.has_value() && state.connectorName != *props.connectorName) { + continue; + } + ret.push_back(i); + } + return ret; + }; + + const bool edidIdUniqueAmongOutputs = !output->edid().identifier().isEmpty() && std::ranges::count_if(allOutputs, [output](BackendOutput *otherOutput) { + return output->edid().identifier() == otherOutput->edid().identifier(); + }) == 1; + const bool edidHashUniqueAmongOutputs = std::ranges::count_if(allOutputs, [output](BackendOutput *otherOutput) { + return output->edid().hash() == otherOutput->edid().hash(); + }) == 1; + const bool mstPathUniqueAmongOutputs = !output->mstPath().isEmpty() && std::ranges::count_if(allOutputs, [output](BackendOutput *other) { + return output->edid().hash() == other->edid().hash() + && output->mstPath() == other->mstPath(); + }) == 1; + + QString edidId = output->edid().identifier(); + auto matches = filterBy(Properties{ + .edidIdentifier = edidId, + .edidHash = std::nullopt, + .mstPath = std::nullopt, + .connectorName = std::nullopt, + }); + if (matches.size() == 1 && edidIdUniqueAmongOutputs) { + // if have an EDID ID that's unique in both outputs and config, + // that's enough information to match the output on its own + return matches.front(); + } else if (matches.empty()) { + // special case: if we failed to parse the EDID in the past, + // we would have no EDID ID but a matching hash + edidId = QString(); + } + // either the EDID ID is not unique, or it's missing entirely + if (edidHashUniqueAmongOutputs) { + // -> narrow down the search with the EDID ID + matches = filterBy(Properties{ + .edidIdentifier = edidId, + .edidHash = output->edid().hash(), + .mstPath = std::nullopt, + .connectorName = std::nullopt, + }); + if (matches.size() == 1) { + return matches.front(); + } else if (matches.empty()) { + return std::nullopt; + } + } + if (mstPathUniqueAmongOutputs) { + // -> narrow down the search with the MST PATH + matches = filterBy(Properties{ + .edidIdentifier = edidId, + .edidHash = output->edid().hash(), + .mstPath = output->mstPath(), + .connectorName = std::nullopt, + }); + if (matches.size() == 1) { + return matches.front(); + } + } + // narrow down the search with the connector name as well + matches = filterBy(Properties{ + .edidIdentifier = edidId, + .edidHash = output->edid().hash(), + .mstPath = output->mstPath(), + .connectorName = output->name(), + }); + if (!matches.empty()) { + // there should never multiple entries where all properties are the same + // so it's safe to just return the first one + return matches.front(); + } else { + return std::nullopt; + } +} + +void OutputConfigurationStore::storeConfig(const QList &allOutputs, bool isLidClosed, const OutputConfiguration &config) +{ + QList relevantOutputs; + std::copy_if(allOutputs.begin(), allOutputs.end(), std::back_inserter(relevantOutputs), [](BackendOutput *output) { + return !output->isNonDesktop() && !output->isPlaceholder(); + }); + if (relevantOutputs.isEmpty()) { + return; + } + registerOutputs(allOutputs); + const auto opt = findSetup(relevantOutputs, isLidClosed); + Setup *setup = nullptr; + if (opt) { + setup = opt->setup; + } else { + m_setups.push_back(Setup{}); + setup = &m_setups.back(); + setup->lidClosed = isLidClosed; + } + for (BackendOutput *output : relevantOutputs) { + auto outputIndex = findOutputIndex(output, allOutputs); + Q_ASSERT(outputIndex.has_value()); + auto outputIt = std::find_if(setup->outputs.begin(), setup->outputs.end(), [outputIndex](const auto &output) { + return output.outputIndex == outputIndex; + }); + if (outputIt == setup->outputs.end()) { + setup->outputs.push_back(SetupState{}); + outputIt = setup->outputs.end() - 1; + } + const std::optional existingUuid = m_outputs[*outputIndex].uuid; + if (const auto changeSet = config.constChangeSet(output)) { + QSize modeSize = changeSet->desiredModeSize.value_or(output->desiredModeSize()); + if (modeSize.isEmpty()) { + modeSize = output->currentMode()->size(); + } + uint32_t refreshRate = changeSet->desiredModeRefreshRate.value_or(output->desiredModeRefreshRate()); + if (refreshRate == 0) { + refreshRate = output->currentMode()->refreshRate(); + } + std::optional flags = changeSet->desiredModeFlags.value_or(output->desiredModeFlags()); + if (!flags) { + flags = output->currentMode()->flags(); + } + m_outputs[*outputIndex] = OutputState{ + .edidIdentifier = output->edid().identifier(), + .connectorName = output->name(), + .edidHash = output->edid().hash(), + .mstPath = output->mstPath(), + .mode = ModeData{ + .size = modeSize, + .refreshRate = refreshRate, + .flags = flags, + }, + .scaleSetting = changeSet->scaleSetting.value_or(output->scaleSetting()), + .transform = changeSet->transform.value_or(output->transform()), + .manualTransform = changeSet->manualTransform.value_or(output->manualTransform()), + .overscan = changeSet->overscan.value_or(output->overscan()), + .rgbRange = changeSet->rgbRange.value_or(output->rgbRange()), + .vrrPolicy = changeSet->vrrPolicy.value_or(output->vrrPolicy()), + .highDynamicRange = changeSet->highDynamicRange.value_or(output->highDynamicRange()), + .referenceLuminance = changeSet->referenceLuminance.value_or(output->referenceLuminance()), + .wideColorGamut = changeSet->wideColorGamut.value_or(output->wideColorGamut()), + .autoRotation = changeSet->autoRotationPolicy.value_or(output->autoRotationPolicy()), + .iccProfilePath = changeSet->iccProfilePath.value_or(output->iccProfilePath()), + .colorProfileSource = changeSet->colorProfileSource.value_or(output->colorProfileSource()), + .maxPeakBrightnessOverride = changeSet->maxPeakBrightnessOverride.value_or(output->maxPeakBrightnessOverride()), + .maxAverageBrightnessOverride = changeSet->maxAverageBrightnessOverride.value_or(output->maxAverageBrightnessOverride()), + .minBrightnessOverride = changeSet->minBrightnessOverride.value_or(output->minBrightnessOverride()), + .sdrGamutWideness = changeSet->sdrGamutWideness.value_or(output->sdrGamutWideness()), + .brightness = changeSet->brightness.value_or(output->brightnessSetting()), + .allowSdrSoftwareBrightness = changeSet->allowSdrSoftwareBrightness.value_or(output->allowSdrSoftwareBrightness()), + .colorPowerTradeoff = changeSet->colorPowerTradeoff.value_or(output->colorPowerTradeoff()), + .uuid = existingUuid, + .detectedDdcCi = changeSet->detectedDdcCi.value_or(output->detectedDdcCi()), + .allowDdcCi = changeSet->allowDdcCi.value_or(output->allowDdcCi()), + .maxBitsPerColor = changeSet->maxBitsPerColor.value_or(output->maxBitsPerColor()), + .edrPolicy = changeSet->edrPolicy.value_or(output->edrPolicy()), + .sharpness = changeSet->sharpness.value_or(output->sharpnessSetting()), + .customModes = changeSet->customModes.value_or(output->customModes()), + .automaticBrightness = changeSet->automaticBrightness.value_or(output->automaticBrightness()), + .autoBrightnessCurve = changeSet->autoBrightnessCurve.value_or(output->autoBrightnessCurve()), + }; + *outputIt = SetupState{ + .outputIndex = *outputIndex, + .position = changeSet->pos.value_or(output->position()), + .enabled = changeSet->enabled.value_or(output->isEnabled()), + .priority = int(output->priority()), + .replicationSource = changeSet->replicationSource.value_or(output->replicationSource()), + }; + } else { + QSize modeSize = output->desiredModeSize(); + if (modeSize.isEmpty()) { + modeSize = output->currentMode()->size(); + } + uint32_t refreshRate = output->desiredModeRefreshRate(); + if (refreshRate == 0) { + refreshRate = output->currentMode()->refreshRate(); + } + std::optional flags = output->desiredModeFlags(); + if (!flags) { + flags = output->currentMode()->flags(); + } + m_outputs[*outputIndex] = OutputState{ + .edidIdentifier = output->edid().identifier(), + .connectorName = output->name(), + .edidHash = output->edid().hash(), + .mstPath = output->mstPath(), + .mode = ModeData{ + .size = modeSize, + .refreshRate = refreshRate, + .flags = flags, + }, + .scaleSetting = output->scaleSetting(), + .transform = output->transform(), + .manualTransform = output->manualTransform(), + .overscan = output->overscan(), + .rgbRange = output->rgbRange(), + .vrrPolicy = output->vrrPolicy(), + .highDynamicRange = output->highDynamicRange(), + .referenceLuminance = output->referenceLuminance(), + .wideColorGamut = output->wideColorGamut(), + .autoRotation = output->autoRotationPolicy(), + .iccProfilePath = output->iccProfilePath(), + .colorProfileSource = output->colorProfileSource(), + .maxPeakBrightnessOverride = output->maxPeakBrightnessOverride(), + .maxAverageBrightnessOverride = output->maxAverageBrightnessOverride(), + .minBrightnessOverride = output->minBrightnessOverride(), + .sdrGamutWideness = output->sdrGamutWideness(), + .brightness = output->brightnessSetting(), + .allowSdrSoftwareBrightness = output->allowSdrSoftwareBrightness(), + .colorPowerTradeoff = output->colorPowerTradeoff(), + .uuid = existingUuid, + .detectedDdcCi = output->detectedDdcCi(), + .allowDdcCi = output->allowDdcCi(), + .maxBitsPerColor = output->maxBitsPerColor(), + .edrPolicy = output->edrPolicy(), + .sharpness = output->sharpnessSetting(), + .customModes = output->customModes(), + .automaticBrightness = output->automaticBrightness(), + .autoBrightnessCurve = output->autoBrightnessCurve(), + }; + *outputIt = SetupState{ + .outputIndex = *outputIndex, + .position = output->position(), + .enabled = output->isEnabled(), + .priority = int(output->priority()), + .replicationSource = output->replicationSource(), + }; + } + } + save(); +} + +OutputConfiguration OutputConfigurationStore::setupToConfig(Setup *setup, const std::unordered_map &outputMap) const +{ + OutputConfiguration ret; + QList> priorities; + for (const auto &[output, outputIndex] : outputMap) { + const OutputState &state = m_outputs[outputIndex]; + const auto &setupState = *std::find_if(setup->outputs.begin(), setup->outputs.end(), [outputIndex = outputIndex](const auto &state) { + return state.outputIndex == outputIndex; + }); + const auto modes = output->modes(); + const auto modeIt = std::find_if(modes.begin(), modes.end(), [&state](const auto &mode) { + return state.mode + && !mode->isRemoved() + && mode->size() == state.mode->size + && mode->refreshRate() == state.mode->refreshRate + && (!state.mode->flags || state.mode->flags == mode->flags()); + }); + std::optional> mode = modeIt == modes.end() ? std::nullopt : std::optional(*modeIt); + if (!mode.has_value() || !*mode) { + mode = chooseMode(output); + qCDebug(KWIN_OUTPUT_CONFIG, "Chose new mode for output %s: %dx%d@%u", + qPrintable(state.edidIdentifier), (*mode)->size().width(), (*mode)->size().height(), (*mode)->refreshRate()); + } + *ret.changeSet(output) = OutputChangeSet{ + .mode = mode, + .desiredModeSize = state.mode.has_value() ? std::make_optional(state.mode->size) : std::nullopt, + .desiredModeRefreshRate = state.mode.has_value() ? std::make_optional(state.mode->refreshRate) : std::nullopt, + .desiredModeFlags = state.mode.has_value() ? std::make_optional(state.mode->flags) : std::nullopt, + .enabled = setupState.enabled, + .pos = setupState.position, + .scale = state.scaleSetting, + .scaleSetting = state.scaleSetting, + .transform = state.transform, + .manualTransform = state.manualTransform, + .overscan = state.overscan, + .rgbRange = state.rgbRange, + .vrrPolicy = state.vrrPolicy, + .highDynamicRange = state.highDynamicRange, + .referenceLuminance = state.referenceLuminance, + .wideColorGamut = state.wideColorGamut, + .autoRotationPolicy = state.autoRotation, + .iccProfilePath = state.iccProfilePath, + .iccProfile = state.iccProfilePath ? IccProfile::load(*state.iccProfilePath).value_or(nullptr) : nullptr, + .maxPeakBrightnessOverride = state.maxPeakBrightnessOverride, + .maxAverageBrightnessOverride = state.maxAverageBrightnessOverride, + .minBrightnessOverride = state.minBrightnessOverride, + .sdrGamutWideness = state.sdrGamutWideness, + .colorProfileSource = state.colorProfileSource, + .brightness = state.brightness, + .allowSdrSoftwareBrightness = state.allowSdrSoftwareBrightness, + .colorPowerTradeoff = state.colorPowerTradeoff, + .uuid = state.uuid, + .replicationSource = setupState.replicationSource, + .detectedDdcCi = state.detectedDdcCi, + .allowDdcCi = state.allowDdcCi, + .maxBitsPerColor = state.maxBitsPerColor, + .edrPolicy = state.edrPolicy, + .sharpness = state.sharpness, + .priority = setupState.priority, + .customModes = state.customModes, + .automaticBrightness = state.automaticBrightness, + .autoBrightnessCurve = state.autoBrightnessCurve, + }; + } + return ret; +} + +std::optional OutputConfigurationStore::generateLidClosedConfig(const QList &outputs) +{ + const auto internalIt = std::find_if(outputs.begin(), outputs.end(), [](BackendOutput *output) { + return output->isInternal(); + }); + if (internalIt == outputs.end()) { + return std::nullopt; + } + const auto setup = findSetup(outputs, false); + if (!setup) { + return std::nullopt; + } + BackendOutput *const internalOutput = *internalIt; + auto config = setupToConfig(setup->setup, setup->globalOutputIndices); + auto internalChangeset = config.changeSet(internalOutput); + if (!internalChangeset->enabled.value_or(internalOutput->isEnabled())) { + return config; + } + + internalChangeset->enabled = false; + + const bool anyEnabled = std::any_of(outputs.begin(), outputs.end(), [&config = config](BackendOutput *output) { + return config.changeSet(output)->enabled.value_or(output->isEnabled()); + }); + if (!anyEnabled) { + return std::nullopt; + } + + const auto getSize = [](OutputChangeSet *changeset, BackendOutput *output) { + auto mode = changeset->mode ? changeset->mode->lock() : nullptr; + if (!mode) { + mode = output->currentMode(); + } + const auto scale = changeset->scale.value_or(output->scale()); + return QSize(std::ceil(mode->size().width() / scale), std::ceil(mode->size().height() / scale)); + }; + const QPoint internalPos = internalChangeset->pos.value_or(internalOutput->position()); + const QSize internalSize = getSize(internalChangeset.get(), internalOutput); + for (BackendOutput *otherOutput : outputs) { + auto changeset = config.changeSet(otherOutput); + QPoint otherPos = changeset->pos.value_or(otherOutput->position()); + if (otherPos.x() >= internalPos.x() + internalSize.width()) { + otherPos.rx() -= std::floor(internalSize.width()); + } + if (otherPos.y() >= internalPos.y() + internalSize.height()) { + otherPos.ry() -= std::floor(internalSize.height()); + } + // make sure this doesn't make outputs overlap, which is neither supported nor expected by users + const QSize otherSize = getSize(changeset.get(), otherOutput); + const bool overlap = std::any_of(outputs.begin(), outputs.end(), [&, &config = config](BackendOutput *output) { + if (otherOutput == output) { + return false; + } + const auto changeset = config.changeSet(output); + const QPoint pos = changeset->pos.value_or(output->position()); + return Rect(pos, otherSize).intersects(Rect(otherPos, getSize(changeset.get(), output))); + }); + if (!overlap) { + changeset->pos = otherPos; + } + } + return config; +} + +std::optional OutputConfigurationStore::findPartialSetup(const QList &outputs, bool lidClosed) +{ + std::unordered_map outputStates; + for (BackendOutput *output : outputs) { + if (auto index = findOutputIndex(output, outputs)) { + outputStates[output] = *index; + } + } + const auto matchCount = [lidClosed, &outputStates](const Setup &setup) -> size_t { + if (setup.lidClosed != lidClosed) { + return 0; + } + // Skip the setup if it contains outputs that aren't currently connected + const bool otherOutputs = std::ranges::any_of(setup.outputs, [&](const SetupState &state) { + return std::ranges::none_of(outputStates, [&state](const auto &outputIt) { + return outputIt.second == state.outputIndex; + }); + }); + if (otherOutputs) { + return 0; + } + // now just count how many outputs are relevant + return std::ranges::count_if(outputStates, [&setup](const auto &outputIt) { + return std::ranges::any_of(setup.outputs, [&outputIt](const auto &outputInfo) { + return outputInfo.outputIndex == outputIt.second; + }); + }); + }; + const auto bestSetup = std::ranges::max_element(m_setups, [&](const auto &left, const auto &right) { + return matchCount(left) < matchCount(right); + }); + if (bestSetup == m_setups.end() || matchCount(*bestSetup) == 0) { + return std::nullopt; + } else { + return SetupWithOutputs{ + .setup = &*bestSetup, + .globalOutputIndices = outputStates, + }; + } +} + +OutputConfiguration OutputConfigurationStore::generateConfig(const QList &outputs, bool isLidClosed) +{ + qCDebug(KWIN_OUTPUT_CONFIG, "Generating new config for %lld outputs", outputs.size()); + if (isLidClosed) { + if (const auto closedConfig = generateLidClosedConfig(outputs)) { + return *closedConfig; + } + } + const auto closestSetup = findPartialSetup(outputs, isLidClosed); + const auto kscreenConfig = KScreenIntegration::readOutputConfig(outputs, KScreenIntegration::connectedOutputsHash(outputs, isLidClosed)); + OutputConfiguration ret; + QPoint rightMostPosition(0, 0); + int priority = 0; + + QList sortedOutputs = outputs; + if (closestSetup.has_value()) { + // place outputs we already have settings for first + std::ranges::partition(sortedOutputs, [&closestSetup](BackendOutput *output) { + return closestSetup->globalOutputIndices.contains(output); + }); + } + for (BackendOutput *output : sortedOutputs) { + const auto kscreenChangeSetPtr = kscreenConfig ? kscreenConfig->constChangeSet(output) : nullptr; + const auto kscreenChangeSet = kscreenChangeSetPtr ? *kscreenChangeSetPtr : OutputChangeSet{}; + + std::optional setupState; + if (closestSetup.has_value()) { + const auto indexIt = closestSetup->globalOutputIndices.find(output); + if (indexIt != closestSetup->globalOutputIndices.end()) { + const auto &[output, index] = *indexIt; + for (const SetupState &state : closestSetup->setup->outputs) { + if (state.outputIndex == index) { + setupState = state; + break; + } + } + } + } + + const auto outputIndex = findOutputIndex(output, outputs); + const bool enable = kscreenChangeSet.enabled.value_or(!isLidClosed || !output->isInternal() || outputs.size() == 1); + const OutputState existingData = outputIndex ? m_outputs[*outputIndex] : OutputState{}; + + const auto modes = output->modes(); + const auto modeIt = std::find_if(modes.begin(), modes.end(), [&existingData](const auto &mode) { + return existingData.mode + && mode->size() == existingData.mode->size + && mode->refreshRate() == existingData.mode->refreshRate + && (!existingData.mode->flags || mode->flags() == existingData.mode->flags); + }); + const auto mode = modeIt == modes.end() ? kscreenChangeSet.mode.value_or(chooseMode(output)).lock() : *modeIt; + + const auto changeset = ret.changeSet(output); + *changeset = { + .mode = mode, + .desiredModeSize = mode->size(), + .desiredModeRefreshRate = mode->refreshRate(), + .desiredModeFlags = mode->flags(), + .enabled = setupState ? setupState->enabled : enable, + .pos = setupState ? setupState->position : rightMostPosition, + // kscreen scale is unreliable because it gets overwritten with the value 1 on Xorg, + // and we don't know if it's from Xorg or the 5.27 Wayland session... so just ignore it + .scaleSetting = existingData.scaleSetting.value_or(chooseScale(output, mode.get())), + .transform = existingData.transform.value_or(kscreenChangeSet.transform.value_or(output->panelOrientation())), + .manualTransform = existingData.manualTransform.value_or(kscreenChangeSet.transform.value_or(output->panelOrientation())), + .overscan = existingData.overscan.value_or(kscreenChangeSet.overscan.value_or(0)), + .rgbRange = existingData.rgbRange.value_or(kscreenChangeSet.rgbRange.value_or(BackendOutput::RgbRange::Automatic)), + .vrrPolicy = existingData.vrrPolicy.value_or(kscreenChangeSet.vrrPolicy.value_or(VrrPolicy::Never)), + .highDynamicRange = existingData.highDynamicRange.value_or(false), + .referenceLuminance = existingData.referenceLuminance.value_or(std::clamp(output->maxAverageBrightnessOverride().value_or(output->advertisedMaxAverageBrightness().value_or(200)), 200.0, 500.0)), + .wideColorGamut = existingData.wideColorGamut.value_or(false), + .autoRotationPolicy = existingData.autoRotation.value_or(BackendOutput::AutoRotationPolicy::InTabletMode), + .colorProfileSource = existingData.colorProfileSource.value_or(BackendOutput::ColorProfileSource::sRGB), + .brightness = existingData.brightness.value_or(1.0), + .allowSdrSoftwareBrightness = existingData.allowSdrSoftwareBrightness.value_or(output->brightnessDevice() == nullptr), + .colorPowerTradeoff = existingData.colorPowerTradeoff.value_or(BackendOutput::ColorPowerTradeoff::PreferEfficiency), + .uuid = existingData.uuid, + .detectedDdcCi = existingData.detectedDdcCi.value_or(false), + .allowDdcCi = existingData.allowDdcCi.value_or(!output->isDdcCiKnownBroken()), + .maxBitsPerColor = existingData.maxBitsPerColor, + .edrPolicy = existingData.edrPolicy.value_or(BackendOutput::EdrPolicy::Always), + .sharpness = existingData.sharpness.value_or(0), + .priority = setupState ? setupState->priority : priority, + .customModes = existingData.customModes, + .automaticBrightness = existingData.automaticBrightness.value_or(false), + // TODO generate a more fitting brightness map per screen? + .autoBrightnessCurve = existingData.autoBrightnessCurve, + }; + if (setupState) { + priority = std::max(setupState->priority + 1, priority); + } else { + priority++; + } + if (*changeset->enabled) { + const auto modeSize = changeset->transform->map(mode->size()); + const QPoint topRight = QPoint(std::ceil(changeset->pos->x() + modeSize.width() / *changeset->scaleSetting), changeset->pos->y()); + if (topRight.x() > rightMostPosition.x() || (topRight.x() == rightMostPosition.x() && topRight.y() < rightMostPosition.y())) { + rightMostPosition = topRight; + } + } + } + return ret; +} + +std::shared_ptr OutputConfigurationStore::chooseMode(BackendOutput *output) const +{ + const auto findBiggestFastest = [](const auto &left, const auto &right) { + const uint64_t leftPixels = left->size().width() * left->size().height(); + const uint64_t rightPixels = right->size().width() * right->size().height(); + if (leftPixels == rightPixels) { + return left->refreshRate() < right->refreshRate(); + } else { + return leftPixels < rightPixels; + } + }; + + const auto modes = output->modes(); + auto notPotentiallyBroken = modes | std::ranges::views::filter([](const auto &mode) { + // generated modes aren't guaranteed to work, so don't choose one as the default + return !mode->isRemoved() && !(mode->flags() & OutputMode::Flag::Generated); + }); + if (notPotentiallyBroken.empty()) { + // there's nothing more we can do + return *std::ranges::max_element(modes, findBiggestFastest); + } + + // try to figure out the native resolution; the biggest preferred mode usually has that + auto preferredOnly = notPotentiallyBroken | std::ranges::views::filter([](const auto &mode) { + return (mode->flags() & OutputMode::Flag::Preferred); + }); + const auto nativeSize = std::ranges::max_element(preferredOnly, findBiggestFastest); + + auto is32by9 = [](const auto &mode) { + const double aspectRatio = mode->size().width() / double(mode->size().height()); + return aspectRatio > 31 / 9.0 && aspectRatio < 33 / 9.0; + }; + + if (nativeSize == preferredOnly.end() || is32by9(*nativeSize)) { + // 32:9 displays often advertise a lower resolution mode as preferred, special case them + auto only32by9 = notPotentiallyBroken | std::ranges::views::filter(is32by9); + const auto best32By9 = std::ranges::max_element(only32by9, findBiggestFastest); + if (best32By9 != only32by9.end()) { + return *best32By9; + } + } + + // Non-default modes have a decent chance of not working on VGA, + // so avoid doing anything out of the ordinary there + const bool isVGA = output->name().contains("VGA"); + if (!isVGA && (nativeSize != preferredOnly.end() || output->edid().likelyNativeResolution())) { + const auto size = nativeSize != preferredOnly.end() ? (*nativeSize)->size() : *output->edid().likelyNativeResolution(); + auto correctSize = notPotentiallyBroken | std::ranges::views::filter([size](const auto &mode) { + return mode->size() == size; + }); + // some high refresh rate displays advertise a 60Hz mode as preferred for compatibility reasons + // ignore that and choose the highest possible refresh rate by default instead + const auto highestRefresh = std::ranges::max_element(correctSize, [](const auto &n, const auto &nPlus1) { + return n->refreshRate() < nPlus1->refreshRate(); + }); + // if the preferred mode size has a refresh rate that's too low for PCs, + // allow falling back to a mode with lower resolution and a more usable refresh rate + if (highestRefresh != correctSize.end() && (*highestRefresh)->refreshRate() >= 50000) { + return *highestRefresh; + } + } + + // even if a higher resolution mode is available, try to pick a more usable refresh rate + auto usableRefreshRates = notPotentiallyBroken | std::ranges::views::filter([](const auto &mode) { + return mode->refreshRate() >= 50000; + }); + const auto usable = std::ranges::max_element(usableRefreshRates, findBiggestFastest); + if (usable != usableRefreshRates.end()) { + return *usable; + } else { + return *std::ranges::max_element(notPotentiallyBroken, findBiggestFastest); + } +} + +double OutputConfigurationStore::chooseScale(BackendOutput *output, OutputMode *mode) const +{ + if (output->physicalSize().height() < 3 || output->physicalSize().width() < 3) { + // A screen less than 3mm wide or tall doesn't make any sense; these are + // all caused by the screen mis-reporting its size. + return 1.0; + } + + // The eye's ability to perceive detail diminishes with distance, so objects + // that are closer can be smaller and their details remain equally + // distinguishable. As a result, each device type has its own ideal physical + // size of items on its screen based on how close the user's eyes are + // expected to be from it on average, and its target DPI value needs to be + // changed accordingly. + // + // The minSize specifies the minimum amount of logical pixels that must be available + // after applying the chosen scale factor. + double targetDpi; + double minSize; + if (output->isInternal()) { + const bool hasLaptopLid = std::ranges::any_of(input()->devices(), [](const auto &device) { + return device->isLidSwitch(); + }); + if (hasLaptopLid) { + // laptop screens: usually closer to the face than desktop monitors + targetDpi = 125; + minSize = 800; + } else { + // phone screens: even closer than laptops + targetDpi = 150; + minSize = 360; + } + } else { + // NOTE that height is checked instead of diagonal size to avoid + // applying the TV heuristic to ultrawide monitors + if (output->physicalSize().height() > 500) { + // This is a pretty big screen, most likely a TV. + // As you generally sit much further away from TVs than desktop monitors, + // user elements on it should also be much larger as well. + // The specific value results in a scale of 200% for 77" 4k TVs. + targetDpi = 30.5; + } else { + // "normal" 1x scale desktop monitor dpi + targetDpi = 96; + } + minSize = 800; + } + + const double dpiX = mode->size().width() / (output->physicalSize().width() / 25.4); + const double maxScaleX = std::clamp(mode->size().width() / minSize, 1.0, 3.0); + const double scaleX = std::clamp(dpiX / targetDpi, 1.0, maxScaleX); + + const double dpiY = mode->size().height() / (output->physicalSize().height() / 25.4); + const double maxScaleY = std::clamp(mode->size().height() / minSize, 1.0, 3.0); + const double scaleY = std::clamp(dpiY / targetDpi, 1.0, maxScaleY); + + double scale = std::min(scaleX, scaleY); + const double steps = 5; + scale = std::round(100.0 * scale / steps) * steps / 100.0; + + // Low-but-not-1 scale factors look like a blurry mess; 1x is better here + if (scale < 1.20) { + scale = 1.0; + } + + return scale; +} + +void OutputConfigurationStore::registerOutputs(const QList &outputs) +{ + for (BackendOutput *output : outputs) { + if (output->isNonDesktop() || output->isPlaceholder()) { + continue; + } + auto index = findOutputIndex(output, outputs); + if (!index) { + index = m_outputs.size(); + m_outputs.push_back(OutputState{}); + } + auto &state = m_outputs[*index]; + state.edidIdentifier = output->edid().identifier(); + state.connectorName = output->name(); + state.edidHash = output->edid().hash(); + state.mstPath = output->mstPath(); + if (!state.uuid.has_value()) { + state.uuid = QUuid::createUuid().toString(QUuid::StringFormat::WithoutBraces); + } + } +} + +void OutputConfigurationStore::load() +{ + const QString jsonPath = QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("kwinoutputconfig.json")); + if (jsonPath.isEmpty()) { + return; + } + + QFile f(jsonPath); + if (!f.open(QIODevice::ReadOnly)) { + qCWarning(KWIN_OUTPUT_CONFIG) << "Could not open file" << jsonPath; + return; + } + QJsonParseError error; + const auto doc = QJsonDocument::fromJson(f.readAll(), &error); + if (error.error != QJsonParseError::NoError) { + qCWarning(KWIN_OUTPUT_CONFIG) << "Failed to parse" << jsonPath << error.errorString(); + return; + } + const auto array = doc.array(); + std::vector objects; + std::transform(array.begin(), array.end(), std::back_inserter(objects), [](const auto &json) { + return json.toObject(); + }); + const auto outputsIt = std::find_if(objects.begin(), objects.end(), [](const auto &obj) { + return obj["name"].toString() == "outputs" && obj["data"].isArray(); + }); + const auto setupsIt = std::find_if(objects.begin(), objects.end(), [](const auto &obj) { + return obj["name"].toString() == "setups" && obj["data"].isArray(); + }); + if (outputsIt == objects.end() || setupsIt == objects.end()) { + return; + } + const auto outputs = (*outputsIt)["data"].toArray(); + + std::vector> outputDatas; + for (const auto &output : outputs) { + const auto data = output.toObject(); + OutputState state; + bool hasIdentifier = false; + if (const auto it = data.find("edidIdentifier"); it != data.end()) { + if (const auto str = it->toString(); !str.isEmpty()) { + state.edidIdentifier = str; + hasIdentifier = true; + } + } + if (const auto it = data.find("edidHash"); it != data.end()) { + if (const auto str = it->toString(); !str.isEmpty()) { + state.edidHash = str; + hasIdentifier = true; + } + } + if (const auto it = data.find("connectorName"); it != data.end()) { + if (const auto str = it->toString(); !str.isEmpty()) { + state.connectorName = str; + hasIdentifier = true; + } + } + if (const auto it = data.find("mstPath"); it != data.end()) { + if (const auto str = it->toString(); !str.isEmpty()) { + state.mstPath = str; + hasIdentifier = true; + } + } + if (!hasIdentifier) { + // without an identifier the settings are useless + // we still have to push something into the list so that the indices stay correct + outputDatas.push_back(std::nullopt); + qCWarning(KWIN_OUTPUT_CONFIG, "BackendOutput in config is missing identifiers"); + continue; + } + const bool hasDuplicate = std::any_of(outputDatas.begin(), outputDatas.end(), [&state](const auto &data) { + return data + && data->edidIdentifier == state.edidIdentifier + && data->edidHash == state.edidHash + && data->mstPath == state.mstPath + && data->connectorName == state.connectorName; + }); + if (hasDuplicate) { + qCWarning(KWIN_OUTPUT_CONFIG) << "Duplicate output found in config for edidIdentifier:" << state.edidIdentifier << "; connectorName:" << state.connectorName << "; mstPath:" << state.mstPath; + outputDatas.push_back(std::nullopt); + continue; + } + if (const auto it = data.find("mode"); it != data.end()) { + const auto obj = it->toObject(); + const int width = obj["width"].toInt(0); + const int height = obj["height"].toInt(0); + const int refreshRate = obj["refreshRate"].toInt(0); + std::optional flags; + if (const auto it = obj.find("flags"); it != obj.end()) { + flags = it->toInt(0); + } + if (width > 0 && height > 0 && refreshRate > 0) { + state.mode = ModeData{ + .size = QSize(width, height), + .refreshRate = uint32_t(refreshRate), + .flags = flags, + }; + qCDebug(KWIN_OUTPUT_CONFIG, "Read mode %dx%d@%u for output %s", width, height, refreshRate, qPrintable(state.edidIdentifier)); + } + } + if (const auto it = data.find("scale"); it != data.end()) { + const double scale = it->toDouble(0); + if (scale > 0 && scale <= 5) { + state.scaleSetting = scale; + } + } + if (const auto it = data.find("transform"); it != data.end()) { + const auto str = it->toString(); + if (str == "Normal") { + state.transform = state.manualTransform = OutputTransform::Kind::Normal; + } else if (str == "Rotated90") { + state.transform = state.manualTransform = OutputTransform::Kind::Rotate90; + } else if (str == "Rotated180") { + state.transform = state.manualTransform = OutputTransform::Kind::Rotate180; + } else if (str == "Rotated270") { + state.transform = state.manualTransform = OutputTransform::Kind::Rotate270; + } else if (str == "Flipped") { + state.transform = state.manualTransform = OutputTransform::Kind::FlipX; + } else if (str == "Flipped90") { + state.transform = state.manualTransform = OutputTransform::Kind::FlipX90; + } else if (str == "Flipped180") { + state.transform = state.manualTransform = OutputTransform::Kind::FlipX180; + } else if (str == "Flipped270") { + state.transform = state.manualTransform = OutputTransform::Kind::FlipX270; + } + } + if (const auto it = data.find("overscan"); it != data.end()) { + const int overscan = it->toInt(-1); + if (overscan >= 0 && overscan <= 100) { + state.overscan = overscan; + } + } + if (const auto it = data.find("rgbRange"); it != data.end()) { + const auto str = it->toString(); + if (str == "Automatic") { + state.rgbRange = BackendOutput::RgbRange::Automatic; + } else if (str == "Limited") { + state.rgbRange = BackendOutput::RgbRange::Limited; + } else if (str == "Full") { + state.rgbRange = BackendOutput::RgbRange::Full; + } + } + if (const auto it = data.find("vrrPolicy"); it != data.end()) { + const auto str = it->toString(); + if (str == "Never") { + state.vrrPolicy = VrrPolicy::Never; + } else if (str == "Automatic") { + state.vrrPolicy = VrrPolicy::Automatic; + } else if (str == "Always") { + state.vrrPolicy = VrrPolicy::Always; + } + } + if (const auto it = data.find("highDynamicRange"); it != data.end() && it->isBool()) { + state.highDynamicRange = it->toBool(); + } + if (const auto it = data.find("sdrBrightness"); it != data.end() && it->isDouble()) { + state.referenceLuminance = it->toInt(200); + } + if (const auto it = data.find("wideColorGamut"); it != data.end() && it->isBool()) { + state.wideColorGamut = it->toBool(); + } + if (const auto it = data.find("autoRotation"); it != data.end()) { + const auto str = it->toString(); + if (str == "Never") { + state.autoRotation = BackendOutput::AutoRotationPolicy::Never; + } else if (str == "InTabletMode") { + state.autoRotation = BackendOutput::AutoRotationPolicy::InTabletMode; + } else if (str == "Always") { + state.autoRotation = BackendOutput::AutoRotationPolicy::Always; + } + } + if (const auto it = data.find("iccProfilePath"); it != data.end()) { + state.iccProfilePath = it->toString(); + } + if (const auto it = data.find("maxPeakBrightnessOverride"); it != data.end() && it->isDouble()) { + state.maxPeakBrightnessOverride = it->toDouble(); + if (*state.maxPeakBrightnessOverride < 50) { + // clearly nonsense + state.maxPeakBrightnessOverride.reset(); + } + } + if (const auto it = data.find("maxAverageBrightnessOverride"); it != data.end() && it->isDouble()) { + state.maxAverageBrightnessOverride = it->toDouble(); + if (*state.maxAverageBrightnessOverride < 50) { + // clearly nonsense + state.maxAverageBrightnessOverride.reset(); + } + } + if (const auto it = data.find("minBrightnessOverride"); it != data.end() && it->isDouble()) { + state.minBrightnessOverride = it->toDouble(); + } + if (const auto it = data.find("sdrGamutWideness"); it != data.end() && it->isDouble()) { + state.sdrGamutWideness = it->toDouble(); + } + if (const auto it = data.find("colorProfileSource"); it != data.end()) { + const auto str = it->toString(); + if (str == "sRGB") { + state.colorProfileSource = BackendOutput::ColorProfileSource::sRGB; + } else if (str == "ICC") { + state.colorProfileSource = BackendOutput::ColorProfileSource::ICC; + } else if (str == "EDID") { + state.colorProfileSource = BackendOutput::ColorProfileSource::EDID; + } + } else { + const bool icc = state.iccProfilePath && !state.iccProfilePath->isEmpty() && !state.highDynamicRange.value_or(false) && !state.wideColorGamut.value_or(false); + if (icc) { + state.colorProfileSource = BackendOutput::ColorProfileSource::ICC; + } else { + state.colorProfileSource = BackendOutput::ColorProfileSource::sRGB; + } + } + if (const auto it = data.find("brightness"); it != data.end() && it->isDouble()) { + state.brightness = std::clamp(it->toDouble(), 0.0, 1.0); + } + if (const auto it = data.find("allowSdrSoftwareBrightness"); it != data.end() && it->isBool()) { + state.allowSdrSoftwareBrightness = it->toBool(); + } + if (const auto it = data.find("colorPowerTradeoff"); it != data.end()) { + const auto str = it->toString(); + if (str == "PreferEfficiency") { + state.colorPowerTradeoff = BackendOutput::ColorPowerTradeoff::PreferEfficiency; + } else if (str == "PreferAccuracy") { + state.colorPowerTradeoff = BackendOutput::ColorPowerTradeoff::PreferAccuracy; + } + } + if (const auto it = data.find("uuid"); it != data.end() && !it->toString().isEmpty()) { + state.uuid = it->toString(); + } + if (const auto it = data.find("detectedDdcCi"); it != data.end() && it->isBool()) { + state.detectedDdcCi = it->toBool(); + } + if (const auto it = data.find("allowDdcCi"); it != data.end() && it->isBool()) { + state.allowDdcCi = it->toBool(); + } + if (const auto it = data.find("maxBitsPerColor"); it != data.end()) { + uint64_t bpc = it->toInteger(0); + if (bpc >= 6 && bpc <= 16) { + state.maxBitsPerColor = bpc; + } + } + if (const auto it = data.find("edrPolicy"); it != data.end()) { + const auto str = it->toString(); + if (str == "never") { + state.edrPolicy = BackendOutput::EdrPolicy::Never; + } else if (str == "always") { + state.edrPolicy = BackendOutput::EdrPolicy::Always; + } + } + if (const auto it = data.find("sharpness"); it != data.end() && it->isDouble()) { + state.sharpness = std::clamp(it->toDouble(), 0.0, 1.0); + } + if (const auto it = data.find("customModes"); it != data.end() && it->isArray()) { + const auto arr = it->toArray(); + QList modes; + for (const auto &value : arr) { + const auto obj = value.toObject(); + const int width = obj["width"].toInt(0); + const int height = obj["height"].toInt(0); + const int refreshRate = obj["refreshRate"].toInt(0); + const int flags = obj["flags"].toInt(0); + if (width > 0 && height > 0 && refreshRate > 0) { + modes.push_back(CustomModeDefinition{ + .size = QSize(width, height), + .refreshRate = uint32_t(refreshRate), + .flags = OutputMode::Flags(flags), + }); + } + } + state.customModes = modes; + } + if (const auto it = data.find("automaticBrightness"); it != data.end() && it->isBool()) { + state.automaticBrightness = it->toBool(); + } + if (const auto it = data.find("autoBrightnessCurve"); it != data.end() && it->isArray()) { + state.autoBrightnessCurve = AutoBrightnessCurve::fromArray(it->toArray()); + } + outputDatas.push_back(state); + } + + const auto setups = (*setupsIt)["data"].toArray(); + for (const auto &s : setups) { + const auto data = s.toObject(); + const auto outputs = data["outputs"].toArray(); + Setup setup; + bool fail = false; + for (const auto &output : outputs) { + const auto outputData = output.toObject(); + SetupState state; + if (const auto it = outputData.find("enabled"); it != outputData.end() && it->isBool()) { + state.enabled = it->toBool(); + } else { + fail = true; + break; + } + if (const auto it = outputData.find("outputIndex"); it != outputData.end()) { + const int index = it->toInt(-1); + if (index <= -1 || size_t(index) >= outputDatas.size() || !outputDatas[index].has_value()) { + fail = true; + break; + } + // the outputs must be unique + const bool unique = std::none_of(setup.outputs.begin(), setup.outputs.end(), [&index](const auto &output) { + return output.outputIndex == size_t(index); + }); + if (!unique) { + fail = true; + break; + } + state.outputIndex = index; + } else { + fail = true; + break; + } + if (const auto it = outputData.find("position"); it != outputData.end()) { + const auto obj = it->toObject(); + const auto x = obj.find("x"); + const auto y = obj.find("y"); + if (x == obj.end() || !x->isDouble() || y == obj.end() || !y->isDouble()) { + fail = true; + break; + } + state.position = QPoint(x->toInt(0), y->toInt(0)); + } else { + fail = true; + break; + } + if (const auto it = outputData.find("priority"); it != outputData.end()) { + state.priority = it->toInt(-1); + if (state.priority < 0 && state.enabled) { + fail = true; + break; + } + } else { + state.priority = INT_MAX; + } + if (const auto it = outputData.find("replicationSource"); it != outputData.end()) { + const QString replicationSource = it->toString(); + const OutputState &sharedState = outputDatas[state.outputIndex].value(); + if (sharedState.uuid != replicationSource) { + state.replicationSource = replicationSource; + } + } + setup.outputs.push_back(state); + } + if (fail || setup.outputs.empty()) { + continue; + } + // one of the outputs must be enabled + const bool noneEnabled = std::none_of(setup.outputs.begin(), setup.outputs.end(), [](const auto &output) { + return output.enabled; + }); + if (noneEnabled) { + continue; + } + setup.lidClosed = data["lidClosed"].toBool(false); + // there must be only one setup that refers to a given set of outputs + const bool alreadyExists = std::any_of(m_setups.begin(), m_setups.end(), [&setup](const auto &other) { + if (setup.lidClosed != other.lidClosed || setup.outputs.size() != other.outputs.size()) { + return false; + } + return std::all_of(setup.outputs.begin(), setup.outputs.end(), [&other](const auto &output) { + return std::any_of(other.outputs.begin(), other.outputs.end(), [&output](const auto &otherOutput) { + return output.outputIndex == otherOutput.outputIndex; + }); + }); + }); + if (alreadyExists) { + continue; + } + m_setups.push_back(setup); + } + + // repair the outputs list in case it's broken + for (size_t i = 0; i < outputDatas.size();) { + if (!outputDatas[i]) { + outputDatas.erase(outputDatas.begin() + i); + for (auto setupIt = m_setups.begin(); setupIt != m_setups.end();) { + const bool broken = std::any_of(setupIt->outputs.begin(), setupIt->outputs.end(), [i](const auto &output) { + return output.outputIndex == i; + }); + if (broken) { + setupIt = m_setups.erase(setupIt); + continue; + } + for (auto &output : setupIt->outputs) { + if (output.outputIndex > i) { + output.outputIndex--; + } + } + setupIt++; + } + } else { + // ensure that uuids are actually unique in the config + const int count = std::ranges::count_if(outputDatas, [i, &outputDatas](const auto &data) { + return data.has_value() && data->uuid == outputDatas[i]->uuid; + }); + if (count > 1) { + // a new uuid will be generated when the data gets used + outputDatas[i]->uuid.reset(); + } + i++; + } + } + + for (const auto &o : outputDatas) { + Q_ASSERT(o); + m_outputs.push_back(*o); + } +} + +void OutputConfigurationStore::save() +{ + QJsonDocument document; + QJsonArray array; + QJsonObject outputs; + outputs["name"] = "outputs"; + QJsonArray outputsData; + for (const auto &output : m_outputs) { + QJsonObject o; + if (!output.edidIdentifier.isEmpty()) { + o["edidIdentifier"] = output.edidIdentifier; + } + if (!output.edidHash.isEmpty()) { + o["edidHash"] = output.edidHash; + } + if (!output.connectorName.isEmpty()) { + o["connectorName"] = output.connectorName; + } + if (!output.mstPath.isEmpty()) { + o["mstPath"] = output.mstPath; + } + if (output.mode) { + QJsonObject mode; + mode["width"] = output.mode->size.width(); + mode["height"] = output.mode->size.height(); + mode["refreshRate"] = int(output.mode->refreshRate); + if (output.mode->flags) { + mode["flags"] = int(*output.mode->flags); + } + o["mode"] = mode; + } + if (output.scaleSetting) { + o["scale"] = *output.scaleSetting; + } + if (output.manualTransform == OutputTransform::Kind::Normal) { + o["transform"] = "Normal"; + } else if (output.manualTransform == OutputTransform::Kind::Rotate90) { + o["transform"] = "Rotated90"; + } else if (output.manualTransform == OutputTransform::Kind::Rotate180) { + o["transform"] = "Rotated180"; + } else if (output.manualTransform == OutputTransform::Kind::Rotate270) { + o["transform"] = "Rotated270"; + } else if (output.manualTransform == OutputTransform::Kind::FlipX) { + o["transform"] = "Flipped"; + } else if (output.manualTransform == OutputTransform::Kind::FlipX90) { + o["transform"] = "Flipped90"; + } else if (output.manualTransform == OutputTransform::Kind::FlipX180) { + o["transform"] = "Flipped180"; + } else if (output.manualTransform == OutputTransform::Kind::FlipX270) { + o["transform"] = "Flipped270"; + } + if (output.overscan) { + o["overscan"] = int(*output.overscan); + } + if (output.rgbRange == BackendOutput::RgbRange::Automatic) { + o["rgbRange"] = "Automatic"; + } else if (output.rgbRange == BackendOutput::RgbRange::Limited) { + o["rgbRange"] = "Limited"; + } else if (output.rgbRange == BackendOutput::RgbRange::Full) { + o["rgbRange"] = "Full"; + } + if (output.vrrPolicy == VrrPolicy::Never) { + o["vrrPolicy"] = "Never"; + } else if (output.vrrPolicy == VrrPolicy::Automatic) { + o["vrrPolicy"] = "Automatic"; + } else if (output.vrrPolicy == VrrPolicy::Always) { + o["vrrPolicy"] = "Always"; + } + if (output.highDynamicRange) { + o["highDynamicRange"] = *output.highDynamicRange; + } + if (output.referenceLuminance) { + o["sdrBrightness"] = int(*output.referenceLuminance); + } + if (output.wideColorGamut) { + o["wideColorGamut"] = *output.wideColorGamut; + } + if (output.autoRotation) { + switch (*output.autoRotation) { + case BackendOutput::AutoRotationPolicy::Never: + o["autoRotation"] = "Never"; + break; + case BackendOutput::AutoRotationPolicy::InTabletMode: + o["autoRotation"] = "InTabletMode"; + break; + case BackendOutput::AutoRotationPolicy::Always: + o["autoRotation"] = "Always"; + break; + } + } + if (output.iccProfilePath) { + o["iccProfilePath"] = *output.iccProfilePath; + } + if (output.maxPeakBrightnessOverride) { + o["maxPeakBrightnessOverride"] = *output.maxPeakBrightnessOverride; + } + if (output.maxAverageBrightnessOverride) { + o["maxAverageBrightnessOverride"] = *output.maxAverageBrightnessOverride; + } + if (output.minBrightnessOverride) { + o["minBrightnessOverride"] = *output.minBrightnessOverride; + } + if (output.sdrGamutWideness) { + o["sdrGamutWideness"] = *output.sdrGamutWideness; + } + if (output.colorProfileSource) { + switch (*output.colorProfileSource) { + case BackendOutput::ColorProfileSource::sRGB: + o["colorProfileSource"] = "sRGB"; + break; + case BackendOutput::ColorProfileSource::ICC: + o["colorProfileSource"] = "ICC"; + break; + case BackendOutput::ColorProfileSource::EDID: + o["colorProfileSource"] = "EDID"; + break; + } + } + if (output.brightness) { + o["brightness"] = *output.brightness; + } + if (output.allowSdrSoftwareBrightness) { + o["allowSdrSoftwareBrightness"] = *output.allowSdrSoftwareBrightness; + } + if (output.colorPowerTradeoff) { + switch (*output.colorPowerTradeoff) { + case BackendOutput::ColorPowerTradeoff::PreferEfficiency: + o["colorPowerTradeoff"] = "PreferEfficiency"; + break; + case BackendOutput::ColorPowerTradeoff::PreferAccuracy: + o["colorPowerTradeoff"] = "PreferAccuracy"; + break; + } + } + if (output.uuid.has_value()) { + o["uuid"] = *output.uuid; + } + if (output.detectedDdcCi) { + o["detectedDdcCi"] = *output.detectedDdcCi; + } + if (output.allowDdcCi) { + o["allowDdcCi"] = *output.allowDdcCi; + } + if (output.maxBitsPerColor.has_value()) { + o["maxBitsPerColor"] = int(*output.maxBitsPerColor); + } + if (output.edrPolicy.has_value()) { + switch (*output.edrPolicy) { + case BackendOutput::EdrPolicy::Never: + o["edrPolicy"] = "never"; + break; + case BackendOutput::EdrPolicy::Always: + o["edrPolicy"] = "always"; + break; + } + } + if (output.sharpness) { + o["sharpness"] = *output.sharpness; + } + if (output.customModes.has_value()) { + QJsonArray modes; + for (const auto &mode : *output.customModes) { + QJsonObject obj; + obj["width"] = mode.size.width(); + obj["height"] = mode.size.height(); + obj["refreshRate"] = int(mode.refreshRate); + obj["flags"] = int(mode.flags); + modes.append(obj); + } + o["customModes"] = modes; + } + if (output.automaticBrightness) { + o["automaticBrightness"] = *output.automaticBrightness; + } + if (output.autoBrightnessCurve) { + o["autoBrightnessCurve"] = output.autoBrightnessCurve->toArray(); + } + outputsData.append(o); + } + outputs["data"] = outputsData; + array.append(outputs); + + QJsonObject setups; + setups["name"] = "setups"; + QJsonArray setupData; + for (const auto &setup : m_setups) { + QJsonObject o; + o["lidClosed"] = setup.lidClosed; + QJsonArray outputs; + for (ssize_t i = 0; i < setup.outputs.size(); i++) { + const auto &output = setup.outputs[i]; + QJsonObject o; + o["enabled"] = output.enabled; + o["outputIndex"] = int(output.outputIndex); + o["priority"] = output.priority; + QJsonObject pos; + pos["x"] = output.position.x(); + pos["y"] = output.position.y(); + o["position"] = pos; + o["replicationSource"] = output.replicationSource; + + outputs.append(o); + } + o["outputs"] = outputs; + + setupData.append(o); + } + setups["data"] = setupData; + array.append(setups); + + const QString path = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/kwinoutputconfig.json"; + QFile f(path); + if (!f.open(QIODevice::WriteOnly)) { + qCWarning(KWIN_OUTPUT_CONFIG, "Couldn't open output config file %s", qPrintable(path)); + return; + } + document.setArray(array); + f.write(document.toJson()); + f.flush(); +} + +bool OutputConfigurationStore::isAutoRotateActive(const QList &outputs, bool isTabletMode) const +{ + const auto internalIt = std::find_if(outputs.begin(), outputs.end(), [](BackendOutput *output) { + return output->isInternal() && output->isEnabled(); + }); + if (internalIt == outputs.end()) { + return false; + } + BackendOutput *internal = *internalIt; + // if output is not on, disable auto-rotate + if (internal->dpmsMode() != BackendOutput::DpmsMode::On) { + return false; + } + switch (internal->autoRotationPolicy()) { + case BackendOutput::AutoRotationPolicy::Never: + return false; + case BackendOutput::AutoRotationPolicy::InTabletMode: + return isTabletMode; + case BackendOutput::AutoRotationPolicy::Always: + return true; + } + Q_UNREACHABLE(); +} + +bool OutputConfigurationStore::isAutoBrightnessActive(const QList &outputs) const +{ + return std::ranges::any_of(outputs, [](BackendOutput *output) { + return output->isEnabled() + && output->automaticBrightness() + && output->dpmsMode() == BackendOutput::DpmsMode::On; + }); +} +} diff --git a/local/recipes/kde/kwin/source/src/outputconfigurationstore.h b/local/recipes/kde/kwin/source/src/outputconfigurationstore.h new file mode 100644 index 0000000000..5e1d0136d0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/outputconfigurationstore.h @@ -0,0 +1,130 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "core/backendoutput.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +enum class AccelerometerOrientation; +class OutputConfiguration; + +class KWIN_EXPORT OutputConfigurationStore +{ +public: + OutputConfigurationStore(); + ~OutputConfigurationStore(); + + void clear(); + + enum class ConfigType { + Preexisting, + Generated, + }; + std::optional> queryConfig(const QList &outputs, bool isLidClosed, AccelerometerOrientation orientation, bool isTabletMode); + void storeConfig(const QList &allOutputs, bool isLidClosed, const OutputConfiguration &config); + + void applyMirroring(OutputConfiguration &config, const QList &outputs); + bool isAutoRotateActive(const QList &outputs, bool isTabletMode) const; + bool isAutoBrightnessActive(const QList &outputs) const; + +private: + OutputConfiguration generateConfig(const QList &outputs, bool isLidClosed); + void registerOutputs(const QList &outputs); + void applyOrientationReading(OutputConfiguration &config, const QList &outputs, AccelerometerOrientation orientation, bool isTabletMode); + std::optional generateLidClosedConfig(const QList &outputs); + std::shared_ptr chooseMode(BackendOutput *output) const; + double chooseScale(BackendOutput *output, OutputMode *mode) const; + void load(); + void save(); + + struct ModeData + { + QSize size; + uint32_t refreshRate; + // TODO: Eventually convert it from std::optional to a regular property. The output flags + // are stored in std::optional because previous versions (6.6.4 and prior) did not save the flags. + std::optional flags; + }; + struct OutputState + { + // identification data. Empty if invalid + QString edidIdentifier; + QString connectorName; + QString edidHash; + QString mstPath; + // actual state + std::optional mode; + std::optional scaleSetting; + std::optional transform; + std::optional manualTransform; + std::optional overscan; + std::optional rgbRange; + std::optional vrrPolicy; + std::optional highDynamicRange; + std::optional referenceLuminance; + std::optional wideColorGamut; + std::optional autoRotation; + std::optional iccProfilePath; + std::optional colorProfileSource; + std::optional maxPeakBrightnessOverride; + std::optional maxAverageBrightnessOverride; + std::optional minBrightnessOverride; + std::optional sdrGamutWideness; + std::optional brightness; + std::optional allowSdrSoftwareBrightness; + std::optional colorPowerTradeoff; + std::optional uuid; + std::optional detectedDdcCi; + std::optional allowDdcCi; + std::optional maxBitsPerColor; + std::optional edrPolicy; + std::optional sharpness; + std::optional> customModes; + std::optional automaticBrightness; + std::optional autoBrightnessCurve; + }; + struct SetupState + { + size_t outputIndex; + QPoint position; + bool enabled; + int priority; + QString replicationSource; + }; + struct Setup + { + bool lidClosed = false; + QList outputs; + }; + + OutputConfiguration setupToConfig(Setup *setup, const std::unordered_map &outputMap) const; + struct SetupWithOutputs + { + Setup *setup; + // this maps to indices in the global m_outputs, not to Setup::outputs + std::unordered_map globalOutputIndices; + }; + std::optional findSetup(const QList &outputs, bool lidClosed); + std::optional findPartialSetup(const QList &outputs, bool lidClosed); + std::optional findOutputIndex(BackendOutput *output, const QList &allOutputs) const; + + QList m_outputs; + QList m_setups; +}; +} diff --git a/local/recipes/kde/kwin/source/src/placeholderinputeventfilter.cpp b/local/recipes/kde/kwin/source/src/placeholderinputeventfilter.cpp new file mode 100644 index 0000000000..c53c10f83a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/placeholderinputeventfilter.cpp @@ -0,0 +1,57 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "placeholderinputeventfilter.h" +#include "input_event.h" +#include "utils/keys.h" + +namespace KWin +{ + +PlaceholderInputEventFilter::PlaceholderInputEventFilter() + : InputEventFilter(InputFilterOrder::PlaceholderOutput) +{ +} + +bool PlaceholderInputEventFilter::pointerMotion(PointerMotionEvent *event) +{ + return true; +} + +bool PlaceholderInputEventFilter::pointerButton(PointerButtonEvent *event) +{ + return true; +} + +bool PlaceholderInputEventFilter::pointerAxis(PointerAxisEvent *event) +{ + return true; +} + +bool PlaceholderInputEventFilter::keyboardKey(KeyboardKeyEvent *event) +{ + return !isMediaKey(event->key); +} + +bool PlaceholderInputEventFilter::touchDown(TouchDownEvent *event) +{ + return true; +} + +bool PlaceholderInputEventFilter::touchMotion(TouchMotionEvent *event) +{ + return true; +} + +bool PlaceholderInputEventFilter::touchUp(TouchUpEvent *event) +{ + return true; +} + +} diff --git a/local/recipes/kde/kwin/source/src/placeholderinputeventfilter.h b/local/recipes/kde/kwin/source/src/placeholderinputeventfilter.h new file mode 100644 index 0000000000..45a701740d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/placeholderinputeventfilter.h @@ -0,0 +1,29 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "input.h" + +namespace KWin +{ + +class PlaceholderInputEventFilter : public InputEventFilter +{ +public: + PlaceholderInputEventFilter(); + bool pointerMotion(PointerMotionEvent *event) override; + bool pointerButton(PointerButtonEvent *event) override; + bool pointerAxis(PointerAxisEvent *event) override; + bool keyboardKey(KeyboardKeyEvent *event) override; + bool touchDown(TouchDownEvent *event) override; + bool touchMotion(TouchMotionEvent *event) override; + bool touchUp(TouchUpEvent *event) override; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/placeholderoutput.cpp b/local/recipes/kde/kwin/source/src/placeholderoutput.cpp new file mode 100644 index 0000000000..488f8d229b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/placeholderoutput.cpp @@ -0,0 +1,57 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "placeholderoutput.h" + +namespace KWin +{ + +PlaceholderOutput::PlaceholderOutput(const QSize &size, qreal scale) +{ + auto mode = std::make_shared(size, 60000); + + m_renderLoop = std::make_unique(this); + m_renderLoop->setRefreshRate(mode->refreshRate()); + m_renderLoop->inhibit(); + + setState(State{ + .scale = scale, + .modes = {mode}, + .currentMode = mode, + .enabled = true, + }); + + setInformation(Information{ + .name = QStringLiteral("Placeholder-1"), + .placeholder = true, + }); +} + +PlaceholderOutput::~PlaceholderOutput() +{ + State state = m_state; + state.enabled = false; + setState(state); +} + +RenderLoop *PlaceholderOutput::renderLoop() const +{ + return m_renderLoop.get(); +} + +bool PlaceholderOutput::testPresentation(const std::shared_ptr &frame) +{ + return false; +} + +bool PlaceholderOutput::present(const QList &layersToUpdate, const std::shared_ptr &frame) +{ + return false; +} + +} // namespace KWin + +#include "moc_placeholderoutput.cpp" diff --git a/local/recipes/kde/kwin/source/src/placeholderoutput.h b/local/recipes/kde/kwin/source/src/placeholderoutput.h new file mode 100644 index 0000000000..ac84ab22a5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/placeholderoutput.h @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/backendoutput.h" + +namespace KWin +{ + +class PlaceholderOutput : public BackendOutput +{ + Q_OBJECT + +public: + PlaceholderOutput(const QSize &size, qreal scale = 1); + ~PlaceholderOutput() override; + + bool testPresentation(const std::shared_ptr &frame) override; + bool present(const QList &layersToUpdate, const std::shared_ptr &frame) override; + RenderLoop *renderLoop() const override; + +private: + std::unique_ptr m_renderLoop; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/placement.cpp b/local/recipes/kde/kwin/source/src/placement.cpp new file mode 100644 index 0000000000..5074285437 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/placement.cpp @@ -0,0 +1,901 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 1997-2002 Cristian Tibirna + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2022 Natalie Clarius + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "placement.h" +#include "cursor.h" +#include "options.h" +#include "rules.h" +#include "virtualdesktops.h" +#include "window.h" +#include "workspace.h" + +#include +#include + +namespace KWin +{ + +Placement::Placement() +{ +} + +/** + * Places the client \a c according to the workspace's layout policy + */ +std::optional Placement::place(const Window *c, const RectF &area) +{ + PlacementPolicy policy = c->rules()->checkPlacement(PlacementDefault); + if (policy != PlacementDefault) { + return place(c, area, policy); + } + + if (c->isUtility()) { + return placeUtility(c, area.toRect(), options->placement()); + } else if (c->isDialog()) { + return placeDialog(c, area.toRect(), options->placement()); + } else if (c->isSplash()) { + return placeOnMainWindow(c, area.toRect()); // on mainwindow, if any, otherwise centered + } else if (c->isOnScreenDisplay() || c->isNotification() || c->isCriticalNotification()) { + return placeOnScreenDisplay(c, area.toRect()); + } else if (c->isTransient() && c->surface()) { + return placeDialog(c, area.toRect(), options->placement()); + } else if (c->isPictureInPicture()) { + return placePictureInPicture(c, area.toRect()); + } else { + return place(c, area, options->placement()); + } +} + +std::optional Placement::place(const Window *c, const RectF &area, PlacementPolicy policy, PlacementPolicy nextPlacement) +{ + if (policy == PlacementUnknown || policy == PlacementDefault) { + policy = options->placement(); + } + + switch (policy) { + case PlacementNone: + return std::nullopt; + case PlacementRandom: + return placeAtRandom(c, area.toRect(), nextPlacement); + case PlacementCentered: + return placeCentered(c, area, nextPlacement); + case PlacementZeroCornered: + return placeZeroCornered(c, area.toRect(), nextPlacement); + case PlacementUnderMouse: + return placeUnderMouse(c, area.toRect(), nextPlacement); + case PlacementOnMainWindow: + return placeOnMainWindow(c, area.toRect(), nextPlacement); + case PlacementMaximizing: + return placeMaximizing(c, area.toRect(), nextPlacement); + default: + return placeSmart(c, area, nextPlacement); + } +} + +/** + * Place the client \a c according to a simply "random" placement algorithm. + */ +std::optional Placement::placeAtRandom(const Window *c, const Rect &area, PlacementPolicy /*next*/) +{ + Q_ASSERT(area.isValid()); + + const QSizeF size = c->size(); + if (size.isEmpty()) { + return std::nullopt; + } + + const int step = 24; + static int px = step; + static int py = 2 * step; + int tx, ty; + + if (px < area.x()) { + px = area.x(); + } + if (py < area.y()) { + py = area.y(); + } + + px += step; + py += 2 * step; + + if (px > area.width() / 2) { + px = area.x() + step; + } + if (py > area.height() / 2) { + py = area.y() + step; + } + tx = px; + ty = py; + if (tx + size.width() > area.right()) { + tx = area.right() - size.width(); + if (tx < 0) { + tx = 0; + } + px = area.x(); + } + if (ty + size.height() > area.bottom()) { + ty = area.bottom() - size.height(); + if (ty < 0) { + ty = 0; + } + py = area.y(); + } + + const RectF placed = cascadeIfCovering(c, RectF(QPointF(tx, ty), size), area); + return placed.topLeft(); +} + +static inline bool isIrrelevant(const Window *window, const Window *regarding, VirtualDesktop *desktop) +{ + return window == regarding + || !window->isClient() + || !window->isShown() + || !window->isOnDesktop(desktop) + || !window->isOnCurrentActivity() + || window->isDesktop(); +}; + +/** + * Place the client \a c according to a really smart placement algorithm :-) + */ +std::optional Placement::placeSmart(const Window *window, const RectF &area, PlacementPolicy /*next*/) +{ + Q_ASSERT(area.isValid()); + + /* + * SmartPlacement by Cristian Tibirna (tibirna@kde.org) + * adapted for kwm (16-19jan98) and for kwin (16Nov1999) using (with + * permission) ideas from fvwm, authored by + * Anthony Martin (amartin@engr.csulb.edu). + * Xinerama supported added by Balaji Ramani (balaji@yablibli.com) + * with ideas from xfce. + */ + + const QSizeF size = window->size(); + if (size.isEmpty()) { + return std::nullopt; + } + + const int none = 0, h_wrong = -1, w_wrong = -2; // overlap types + long int overlap, min_overlap = 0; + int x_optimal, y_optimal; + int possible; + VirtualDesktop *const desktop = window->isOnCurrentDesktop() ? VirtualDesktopManager::self()->currentDesktop() : window->desktops().front(); + + int cxl, cxr, cyt, cyb; // temp coords + int xl, xr, yt, yb; // temp coords + int basket; // temp holder + + // get the maximum allowed windows space + int x = area.left(); + int y = area.top(); + x_optimal = x; + y_optimal = y; + + // client gabarit + int ch = std::ceil(size.height()); + int cw = std::ceil(size.width()); + + // Explicitly converts those to int to avoid accidentally + // mixing ints and qreal in the calculations below. + int area_xr = std::floor(area.x() + area.width()); + int area_yb = std::floor(area.y() + area.height()); + + bool first_pass = true; // CT lame flag. Don't like it. What else would do? + + // loop over possible positions + do { + // test if enough room in x and y directions + if (y + ch > area_yb && ch < area.height()) { + overlap = h_wrong; // this throws the algorithm to an exit + } else if (x + cw > area_xr) { + overlap = w_wrong; + } else { + overlap = none; // initialize + + cxl = x; + cxr = x + cw; + cyt = y; + cyb = y + ch; + for (auto l = workspace()->stackingOrder().constBegin(); l != workspace()->stackingOrder().constEnd(); ++l) { + auto client = *l; + if (isIrrelevant(client, window, desktop)) { + continue; + } + xl = client->x(); + yt = client->y(); + xr = xl + client->width(); + yb = yt + client->height(); + + // if windows overlap, calc the overall overlapping + if ((cxl < xr) && (cxr > xl) && (cyt < yb) && (cyb > yt)) { + xl = std::max(cxl, xl); + xr = std::min(cxr, xr); + yt = std::max(cyt, yt); + yb = std::min(cyb, yb); + if (client->keepAbove()) { + overlap += 16 * (xr - xl) * (yb - yt); + } else if (client->keepBelow() && !client->isDock()) { // ignore KeepBelow windows + overlap += 0; // for placement (see X11Window::belongsToLayer() for Dock) + } else { + overlap += (xr - xl) * (yb - yt); + } + } + } + } + + // CT first time we get no overlap we stop. + if (overlap == none) { + x_optimal = x; + y_optimal = y; + break; + } + + if (first_pass) { + first_pass = false; + min_overlap = overlap; + } + // CT save the best position and the minimum overlap up to now + else if (overlap >= none && overlap < min_overlap) { + min_overlap = overlap; + x_optimal = x; + y_optimal = y; + } + + // really need to loop? test if there's any overlap + if (overlap > none) { + + possible = area_xr; + if (possible - cw > x) { + possible -= cw; + } + + // compare to the position of each client on the same desk + for (auto l = workspace()->stackingOrder().constBegin(); l != workspace()->stackingOrder().constEnd(); ++l) { + auto client = *l; + if (isIrrelevant(client, window, desktop)) { + continue; + } + + xl = client->x(); + yt = client->y(); + xr = xl + client->width(); + yb = yt + client->height(); + + // if not enough room above or under the current tested client + // determine the first non-overlapped x position + if ((y < yb) && (yt < ch + y)) { + + if ((xr > x) && (possible > xr)) { + possible = xr; + } + + basket = xl - cw; + if ((basket > x) && (possible > basket)) { + possible = basket; + } + } + } + x = possible; + } + + // ... else ==> not enough x dimension (overlap was wrong on horizontal) + else if (overlap == w_wrong) { + x = area.left(); + possible = area_yb; + + if (possible - ch > y) { + possible -= ch; + } + + // test the position of each window on the desk + for (auto l = workspace()->stackingOrder().constBegin(); l != workspace()->stackingOrder().constEnd(); ++l) { + auto client = *l; + if (isIrrelevant(client, window, desktop)) { + continue; + } + + xl = client->x(); + yt = client->y(); + xr = xl + client->width(); + yb = yt + client->height(); + + // if not enough room to the left or right of the current tested client + // determine the first non-overlapped y position + if ((yb > y) && (possible > yb)) { + possible = yb; + } + + basket = yt - ch; + if ((basket > y) && (possible > basket)) { + possible = basket; + } + } + y = possible; + } + } while ((overlap != none) && (overlap != h_wrong) && (y < area_yb)); + + if (ch >= area.height()) { + y_optimal = area.top(); + } + + return QPointF(x_optimal, y_optimal); +} + +QPointF Workspace::cascadeOffset(const RectF &area) const +{ + return QPointF(area.width() / 48.0, area.height() / 48.0); +} + +/** + * Place windows centered, on top of all others + */ +std::optional Placement::placeCentered(const Window *c, const RectF &area, PlacementPolicy /*next*/) +{ + Q_ASSERT(area.isValid()); + + const QSizeF size = c->size(); + if (size.isEmpty()) { + return std::nullopt; + } + + const QPoint position(std::max(area.left() + (area.width() - size.width()) / 2, area.left()), + std::max(area.top() + (area.height() - size.height()) / 2, area.top())); + + const RectF placed = cascadeIfCovering(c, RectF(position, size), area); + return placed.topLeft(); +} + +/** + * Place windows in the (0,0) corner, on top of all others + */ +std::optional Placement::placeZeroCornered(const Window *c, const Rect &area, PlacementPolicy /*next*/) +{ + Q_ASSERT(area.isValid()); + + const QSizeF size = c->size(); + if (size.isEmpty()) { + return std::nullopt; + } + + // get the maximum allowed windows space and desk's origin + const RectF placed = cascadeIfCovering(c, RectF(area.topLeft(), size), area); + return placed.topLeft(); +} + +std::optional Placement::placeUtility(const Window *c, const Rect &area, PlacementPolicy /*next*/) +{ + // TODO kwin should try to place utility windows next to their mainwindow, + // preferably at the right edge, and going down if there are more of them + // if there's not enough place outside the mainwindow, it should prefer + // top-right corner + // use the default placement for now + return place(c, area, PlacementDefault); +} + +std::optional Placement::placeOnScreenDisplay(const Window *c, const Rect &area) +{ + Q_ASSERT(area.isValid()); + + const QSizeF size = c->size(); + if (size.isEmpty()) { + return std::nullopt; + } + + // place at lower area of the screen + const int x = area.left() + (area.width() - size.width()) / 2; + const int y = area.top() + 2 * area.height() / 3 - size.height() / 2; + + return QPointF(x, y); +} + +std::optional Placement::placeDialog(const Window *c, const Rect &area, PlacementPolicy nextPlacement) +{ + return placeOnMainWindow(c, area, nextPlacement); +} + +std::optional Placement::placePictureInPicture(const Window *c, const Rect &area) +{ + Q_ASSERT(area.isValid()); + + const QSizeF size = c->size(); + if (size.isEmpty()) { + return std::nullopt; + } + + const int margin = options->pictureInPictureMargin(); + switch (options->pictureInPictureHomeCorner()) { + case Qt::TopLeftCorner: + return QPointF(area.x() + margin, area.y() + margin); + + case Qt::TopRightCorner: + return QPointF(area.x() + area.width() - size.width() - margin, area.y() + margin); + + case Qt::BottomRightCorner: + return QPointF(area.x() + area.width() - size.width() - margin, + area.y() + area.height() - size.height() - margin); + + case Qt::BottomLeftCorner: + return QPointF(area.x() + margin, area.y() + area.height() - size.height() - margin); + } + + Q_UNREACHABLE(); +} + +std::optional Placement::placeUnderMouse(const Window *c, const Rect &area, PlacementPolicy /*next*/) +{ + const QSizeF size = c->size(); + if (size.isEmpty()) { + return std::nullopt; + } + + const QPointF cursorPos = Cursors::self()->mouse()->pos(); + const RectF centered(cursorPos.x() - size.width() / 2, + cursorPos.y() - size.height() / 2, + size.width(), + size.height()); + + const RectF screenArea = workspace()->clientArea(PlacementArea, c, cursorPos); + const RectF placed = cascadeIfCovering(c, c->keepInArea(centered, screenArea), screenArea); + return placed.topLeft(); +} + +std::optional Placement::placeOnMainWindow(const Window *c, const Rect &area, PlacementPolicy nextPlacement) +{ + Q_ASSERT(area.isValid()); + + if (nextPlacement == PlacementUnknown) { + nextPlacement = PlacementCentered; + } + if (nextPlacement == PlacementMaximizing) { // maximize if needed + if (const auto placed = placeMaximizing(c, area, PlacementNone)) { + return placed; + } + } + + const QSizeF size = c->size(); + if (size.isEmpty()) { + return std::nullopt; + } + + auto mainwindows = c->mainWindows(); + Window *place_on = nullptr; + Window *place_on2 = nullptr; + int mains_count = 0; + for (auto it = mainwindows.constBegin(); it != mainwindows.constEnd(); ++it) { + if (mainwindows.count() > 1 && (*it)->isSpecialWindow()) { + continue; // don't consider toolbars etc when placing + } + ++mains_count; + place_on2 = *it; + if ((*it)->isOnCurrentDesktop()) { + if (place_on == nullptr) { + place_on = *it; + } else { + // two or more on current desktop -> center + // That's the default at least. However, with maximizing placement + // policy as the default, the dialog should be either maximized or + // made as large as its maximum size and then placed centered. + // So the nextPlacement argument allows chaining. In this case, nextPlacement + // is Maximizing and it will call placeCentered(). + return place(c, area, PlacementCentered); + } + } + } + if (place_on == nullptr) { + // 'mains_count' is used because it doesn't include ignored mainwindows + if (mains_count != 1) { + return place(c, area, PlacementCentered); + } + place_on = place_on2; // use the only window filtered together with 'mains_count' + } + if (place_on->isDesktop()) { + return place(c, area, PlacementCentered); + } + + RectF geom(QPointF(0, 0), size); + geom.moveCenter(place_on->frameGeometry().center().toPoint()); + + // get area again, because the mainwindow may be on different xinerama screen + const Rect placementArea = workspace()->clientArea(PlacementArea, c, geom.center()).toRect(); + return c->keepInArea(geom, placementArea).topLeft(); // make sure it's kept inside workarea +} + +std::optional Placement::placeMaximizing(const Window *c, const Rect &area, PlacementPolicy nextPlacement) +{ + Q_ASSERT(area.isValid()); + + if (nextPlacement == PlacementUnknown) { + nextPlacement = PlacementCentered; + } + if (c->isMaximizable()) { + return MaximizeFull; + } else { + return place(c, area, nextPlacement); + } +} + +/** + * Cascade the window until it no longer fully overlaps any other window + */ +RectF Placement::cascadeIfCovering(const Window *window, const RectF &geometry, const RectF &area) const +{ + const QPointF offset = workspace()->cascadeOffset(area); + Q_ASSERT(!offset.isNull()); + + VirtualDesktop *const desktop = window->isOnCurrentDesktop() ? VirtualDesktopManager::self()->currentDesktop() : window->desktops().front(); + + RectF possibleGeo = geometry; + bool noOverlap = false; + + // cascade until confirmed no total overlap or not enough space to cascade + while (!noOverlap) { + noOverlap = true; + RectF coveredArea; + // check current position candidate for overlaps with other windows + for (auto l = workspace()->stackingOrder().crbegin(); l != workspace()->stackingOrder().crend(); ++l) { + auto other = *l; + if (isIrrelevant(other, window, desktop) || !other->frameGeometry().intersects(possibleGeo)) { + continue; + } + + if (possibleGeo.contains(other->frameGeometry()) && !coveredArea.contains(other->frameGeometry())) { + // placed window would completely overlap another window which is not already + // covered by other windows: try to cascade it from the topleft of that other + // window + noOverlap = false; + possibleGeo.moveTopLeft(other->pos() + offset); + if (possibleGeo.right() > area.right() || possibleGeo.bottom() > area.bottom()) { + // new cascaded geometry would be out of the bounds of the placement area: + // abort the cascading and keep the window in the original position + return geometry; + } + break; + } + + // keep track of the area occupied by other windows as we go from top to bottom + // in the stacking order, so we don't need to bother trying to avoid overlap with + // windows which are already covered up by other windows anyway + coveredArea |= other->frameGeometry(); + if (coveredArea.contains(area)) { + break; + } + } + } + + return possibleGeo; +} + +const char *Placement::policyToString(PlacementPolicy policy) +{ + const char *const policies[] = { + "NoPlacement", "Default", "XXX should never see", "Random", "Smart", "Centered", + "ZeroCornered", "UnderMouse", "OnMainWindow", "Maximizing"}; + Q_ASSERT(policy < int(sizeof(policies) / sizeof(policies[0]))); + return policies[policy]; +} + +// ******************** +// Workspace +// ******************** + +void Window::packTo(qreal left, qreal top) +{ + workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event; + + exitQuickTileMode(); + + const LogicalOutput *oldOutput = moveResizeOutput(); + move(QPoint(left, top)); + if (moveResizeOutput() != oldOutput) { + sendToOutput(moveResizeOutput()); // checks rule validity + if (requestedMaximizeMode() != MaximizeRestore) { + checkWorkspacePosition(); + } + } +} + +/** + * Moves active window left until in bumps into another window or workarea edge. + */ +void Workspace::slotWindowMoveLeft() +{ + if (m_activeWindow && m_activeWindow->isMovable()) { + const RectF geometry = m_activeWindow->moveResizeGeometry(); + m_activeWindow->packTo(packPositionLeft(m_activeWindow, geometry.left(), true), + geometry.y()); + } +} + +void Workspace::slotWindowMoveRight() +{ + if (m_activeWindow && m_activeWindow->isMovable()) { + const RectF geometry = m_activeWindow->moveResizeGeometry(); + m_activeWindow->packTo(packPositionRight(m_activeWindow, geometry.right(), true) - geometry.width(), + geometry.y()); + } +} + +void Workspace::slotWindowMoveUp() +{ + if (m_activeWindow && m_activeWindow->isMovable()) { + const RectF geometry = m_activeWindow->moveResizeGeometry(); + m_activeWindow->packTo(geometry.x(), + packPositionUp(m_activeWindow, geometry.top(), true)); + } +} + +void Workspace::slotWindowMoveDown() +{ + if (m_activeWindow && m_activeWindow->isMovable()) { + const RectF geometry = m_activeWindow->moveResizeGeometry(); + m_activeWindow->packTo(geometry.x(), + packPositionDown(m_activeWindow, geometry.bottom(), true) - geometry.height()); + } +} + +/** Moves the active window to the center of the screen. */ +void Workspace::slotWindowCenter() +{ + if (m_activeWindow && m_activeWindow->isMovable()) { + const RectF geometry = m_activeWindow->moveResizeGeometry(); + QPointF center = clientArea(MaximizeArea, m_activeWindow).center(); + m_activeWindow->packTo(center.x() - (geometry.width() / 2), + center.y() - (geometry.height() / 2)); + } +} + +void Workspace::slotWindowExpandHorizontal() +{ + if (m_activeWindow) { + m_activeWindow->growHorizontal(); + } +} + +void Window::growHorizontal() +{ + if (!isResizable()) { + return; + } + RectF geom = moveResizeGeometry(); + geom.setRight(workspace()->packPositionRight(this, geom.right(), true)); + QSizeF adjsize = constrainFrameSize(geom.size(), SizeModeFixedW); + if (moveResizeGeometry().size() == adjsize && geom.size() != adjsize && resizeIncrements().width() > 1) { // take care of size increments + qreal newright = workspace()->packPositionRight(this, geom.right() + resizeIncrements().width() - 1, true); + // check that it hasn't grown outside of the area, due to size increments + // TODO this may be wrong? + if (workspace()->clientArea(MovementArea, + this, + QPoint((x() + newright) / 2, moveResizeGeometry().center().y())) + .right() + >= newright) { + geom.setRight(newright); + } + } + geom.setSize(constrainFrameSize(geom.size(), SizeModeFixedW)); + geom.setSize(constrainFrameSize(geom.size(), SizeModeFixedH)); + workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event; + exitQuickTileMode(); + moveResize(geom); +} + +void Workspace::slotWindowShrinkHorizontal() +{ + if (m_activeWindow) { + m_activeWindow->shrinkHorizontal(); + } +} + +void Window::shrinkHorizontal() +{ + if (!isResizable()) { + return; + } + RectF geom = moveResizeGeometry(); + geom.setRight(workspace()->packPositionLeft(this, geom.right(), false) + 1); + if (geom.width() <= 1) { + return; + } + geom.setSize(constrainFrameSize(geom.size(), SizeModeFixedW)); + if (geom.width() > 20) { + workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event; + exitQuickTileMode(); + moveResize(geom); + } +} + +void Workspace::slotWindowExpandVertical() +{ + if (m_activeWindow) { + m_activeWindow->growVertical(); + } +} + +void Window::growVertical() +{ + if (!isResizable()) { + return; + } + RectF geom = moveResizeGeometry(); + geom.setBottom(workspace()->packPositionDown(this, geom.bottom(), true)); + QSizeF adjsize = constrainFrameSize(geom.size(), SizeModeFixedH); + if (moveResizeGeometry().size() == adjsize && geom.size() != adjsize && resizeIncrements().height() > 1) { // take care of size increments + qreal newbottom = workspace()->packPositionDown(this, geom.bottom() + resizeIncrements().height(), true); + // check that it hasn't grown outside of the area, due to size increments + if (workspace()->clientArea(MovementArea, + this, + QPoint(moveResizeGeometry().center().x(), (y() + newbottom) / 2)) + .bottom() + >= newbottom) { + geom.setBottom(newbottom); + } + } + geom.setSize(constrainFrameSize(geom.size(), SizeModeFixedH)); + workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event; + exitQuickTileMode(); + moveResize(geom); +} + +void Workspace::slotWindowShrinkVertical() +{ + if (m_activeWindow) { + m_activeWindow->shrinkVertical(); + } +} + +void Window::shrinkVertical() +{ + if (!isResizable()) { + return; + } + RectF geom = moveResizeGeometry(); + geom.setBottom(workspace()->packPositionUp(this, geom.bottom(), false) + 1); + if (geom.height() <= 1) { + return; + } + geom.setSize(constrainFrameSize(geom.size(), SizeModeFixedH)); + if (geom.height() > 20) { + workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event; + exitQuickTileMode(); + moveResize(geom); + } +} + +void Workspace::quickTileWindow(QuickTileMode mode) +{ + if (!m_activeWindow) { + return; + } + + m_activeWindow->handleQuickTileShortcut(mode); +} + +void Workspace::customQuickTileWindow(QuickTileMode mode) +{ + if (!m_activeWindow) { + return; + } + + m_activeWindow->handleCustomQuickTileShortcut(mode); +} + +qreal Workspace::packPositionLeft(const Window *window, qreal oldX, bool leftEdge) const +{ + qreal newX = clientArea(MaximizeArea, window).left(); + if (oldX <= newX) { // try another Xinerama screen + newX = clientArea(MaximizeArea, + window, + QPointF(window->frameGeometry().left() - 1, window->frameGeometry().center().y())) + .left(); + } + if (oldX <= newX) { + return oldX; + } + VirtualDesktop *const desktop = window->isOnCurrentDesktop() ? VirtualDesktopManager::self()->currentDesktop() : window->desktops().front(); + for (auto it = m_windows.constBegin(), end = m_windows.constEnd(); it != end; ++it) { + if (isIrrelevant(*it, window, desktop)) { + continue; + } + const qreal x = leftEdge ? (*it)->frameGeometry().right() : (*it)->frameGeometry().left() - 1; + if (x > newX && x < oldX + && !(window->frameGeometry().top() > (*it)->frameGeometry().bottom() - 1 // they overlap in Y direction + || window->frameGeometry().bottom() - 1 < (*it)->frameGeometry().top())) { + newX = x; + } + } + return newX; +} + +qreal Workspace::packPositionRight(const Window *window, qreal oldX, bool rightEdge) const +{ + qreal newX = clientArea(MaximizeArea, window).right(); + if (oldX >= newX) { // try another Xinerama screen + newX = clientArea(MaximizeArea, + window, + QPointF(window->frameGeometry().right(), window->frameGeometry().center().y())) + .right(); + } + if (oldX >= newX) { + return oldX; + } + VirtualDesktop *const desktop = window->isOnCurrentDesktop() ? VirtualDesktopManager::self()->currentDesktop() : window->desktops().front(); + for (auto it = m_windows.constBegin(), end = m_windows.constEnd(); it != end; ++it) { + if (isIrrelevant(*it, window, desktop)) { + continue; + } + const qreal x = rightEdge ? (*it)->frameGeometry().left() : (*it)->frameGeometry().right() + 1; + + if (x < newX && x > oldX + && !(window->frameGeometry().top() > (*it)->frameGeometry().bottom() - 1 + || window->frameGeometry().bottom() - 1 < (*it)->frameGeometry().top())) { + newX = x; + } + } + return newX; +} + +qreal Workspace::packPositionUp(const Window *window, qreal oldY, bool topEdge) const +{ + qreal newY = clientArea(MaximizeArea, window).top(); + if (oldY <= newY) { // try another Xinerama screen + newY = clientArea(MaximizeArea, + window, + QPointF(window->frameGeometry().center().x(), window->frameGeometry().top() - 1)) + .top(); + } + if (oldY <= newY) { + return oldY; + } + VirtualDesktop *const desktop = window->isOnCurrentDesktop() ? VirtualDesktopManager::self()->currentDesktop() : window->desktops().front(); + for (auto it = m_windows.constBegin(), end = m_windows.constEnd(); it != end; ++it) { + if (isIrrelevant(*it, window, desktop)) { + continue; + } + const qreal y = topEdge ? (*it)->frameGeometry().bottom() : (*it)->frameGeometry().top() - 1; + if (y > newY && y < oldY + && !(window->frameGeometry().left() > (*it)->frameGeometry().right() - 1 // they overlap in X direction + || window->frameGeometry().right() - 1 < (*it)->frameGeometry().left())) { + newY = y; + } + } + return newY; +} + +qreal Workspace::packPositionDown(const Window *window, qreal oldY, bool bottomEdge) const +{ + qreal newY = clientArea(MaximizeArea, window).bottom(); + if (oldY >= newY) { // try another Xinerama screen + newY = clientArea(MaximizeArea, + window, + QPointF(window->frameGeometry().center().x(), window->frameGeometry().bottom())) + .bottom(); + } + if (oldY >= newY) { + return oldY; + } + VirtualDesktop *const desktop = window->isOnCurrentDesktop() ? VirtualDesktopManager::self()->currentDesktop() : window->desktops().front(); + for (auto it = m_windows.constBegin(), end = m_windows.constEnd(); it != end; ++it) { + if (isIrrelevant(*it, window, desktop)) { + continue; + } + const qreal y = bottomEdge ? (*it)->frameGeometry().top() : (*it)->frameGeometry().bottom() + 1; + if (y < newY && y > oldY + && !(window->frameGeometry().left() > (*it)->frameGeometry().right() - 1 + || window->frameGeometry().right() - 1 < (*it)->frameGeometry().left())) { + newY = y; + } + } + return newY; +} + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/placement.h b/local/recipes/kde/kwin/source/src/placement.h new file mode 100644 index 0000000000..c03da5e1fb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/placement.h @@ -0,0 +1,49 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 1997-2002 Cristian Tibirna + SPDX-FileCopyrightText: 2003 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once +// KWin +#include "options.h" +#include "window.h" +// Qt +#include +#include + +namespace KWin +{ + +class KWIN_EXPORT Placement +{ +public: + explicit Placement(); + + std::optional place(const Window *c, const RectF &area); + std::optional placeSmart(const Window *c, const RectF &area, PlacementPolicy next = PlacementUnknown); + std::optional placeCentered(const Window *c, const RectF &area, PlacementPolicy next = PlacementUnknown); + + RectF cascadeIfCovering(const Window *c, const RectF &geometry, const RectF &area) const; + + static const char *policyToString(PlacementPolicy policy); + +private: + std::optional place(const Window *c, const RectF &area, PlacementPolicy policy, PlacementPolicy nextPlacement = PlacementUnknown); + std::optional placeUnderMouse(const Window *c, const Rect &area, PlacementPolicy next = PlacementUnknown); + std::optional placeOnMainWindow(const Window *c, const Rect &area, PlacementPolicy next = PlacementUnknown); + std::optional placeAtRandom(const Window *c, const Rect &area, PlacementPolicy next = PlacementUnknown); + std::optional placeMaximizing(const Window *c, const Rect &area, PlacementPolicy next = PlacementUnknown); + std::optional placeZeroCornered(const Window *c, const Rect &area, PlacementPolicy next = PlacementUnknown); + std::optional placeDialog(const Window *c, const Rect &area, PlacementPolicy next = PlacementUnknown); + std::optional placeUtility(const Window *c, const Rect &area, PlacementPolicy next = PlacementUnknown); + std::optional placeOnScreenDisplay(const Window *c, const Rect &area); + std::optional placePictureInPicture(const Window *c, const Rect &area); +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/placementtracker.cpp b/local/recipes/kde/kwin/source/src/placementtracker.cpp new file mode 100644 index 0000000000..4ce2d0ce1f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/placementtracker.cpp @@ -0,0 +1,216 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "placementtracker.h" +#include "core/output.h" +#include "window.h" +#include "workspace.h" + +namespace KWin +{ + +PlacementTracker::PlacementTracker(Workspace *workspace) + : m_workspace(workspace) +{ +} + +PlacementTracker::WindowData PlacementTracker::dataForWindow(Window *window) const +{ + return WindowData{ + .outputUuid = window->moveResizeOutput()->uuid(), + .geometry = window->moveResizeGeometry(), + .maximize = window->requestedMaximizeMode(), + .quickTile = window->requestedQuickTileMode(), + .geometryRestore = window->geometryRestore(), + .fullscreen = window->isRequestedFullScreen(), + .fullscreenGeometryRestore = window->fullscreenGeometryRestore(), + .interactiveMoveResizeCount = window->interactiveMoveResizeCount(), + }; +} + +void PlacementTracker::add(Window *window) +{ + if (window->isUnmanaged() || window->isAppletPopup() || window->isSpecialWindow()) { + return; + } + connect(window, &Window::frameGeometryChanged, this, [this, window]() { + saveGeometry(window); + }); + connect(window, &Window::maximizedChanged, this, [this, window]() { + saveMaximize(window); + }); + connect(window, &Window::quickTileModeChanged, this, [this, window]() { + saveQuickTile(window); + }); + connect(window, &Window::fullScreenChanged, this, [this, window]() { + saveFullscreen(window); + }); + connect(window, &Window::interactiveMoveResizeFinished, this, [this, window]() { + saveInteractionCounter(window); + }); + connect(window, &Window::maximizeGeometryRestoreChanged, this, [this, window]() { + saveMaximizeGeometryRestore(window); + }); + connect(window, &Window::fullscreenGeometryRestoreChanged, this, [this, window]() { + saveFullscreenGeometryRestore(window); + }); + WindowData data = dataForWindow(window); + m_data[m_currentKey][window] = data; + m_savedWindows.push_back(window); +} + +void PlacementTracker::remove(Window *window) +{ + if (m_savedWindows.contains(window)) { + disconnect(window, nullptr, this, nullptr); + for (auto &dataMap : m_data) { + dataMap.remove(window); + } + m_savedWindows.removeOne(window); + } +} + +void PlacementTracker::restore(const QString &key) +{ + if (key == m_currentKey) { + return; + } + auto &dataMap = m_data[key]; + auto &oldDataMap = m_data[m_currentKey]; + const auto outputs = m_workspace->outputs(); + + inhibit(); + for (const auto window : std::as_const(m_savedWindows)) { + const auto it = dataMap.find(window); + if (it != dataMap.end()) { + const WindowData &newData = it.value(); + + const auto checkQuickTileMode = [window](QuickTileMode oldMode) { + if (window->requestedQuickTileMode() == oldMode) { + return true; + } + // custom-tiled windows lose their quick tile mode when the output they're on is unplugged + // TODO find some nicer way of handling this? + return window->requestedQuickTileMode() == QuickTileFlag::None && oldMode == QuickTileFlag::Custom; + }; + + // don't touch windows where the user intentionally changed their state + bool restore = window->interactiveMoveResizeCount() == newData.interactiveMoveResizeCount + && window->requestedMaximizeMode() == newData.maximize + && checkQuickTileMode(newData.quickTile) + && window->isFullScreen() == newData.fullscreen; + if (!restore) { + // the logic above can have false negatives if PlacementTracker changed the window state + // to prevent that, also restore if the window still has the same state from that + if (const auto it = m_lastRestoreData.find(window); it != m_lastRestoreData.end()) { + restore = window->interactiveMoveResizeCount() == it->interactiveMoveResizeCount + && window->requestedMaximizeMode() == it->maximize + && checkQuickTileMode(it->quickTile) + && window->isFullScreen() == it->fullscreen + && window->moveResizeOutput()->uuid() == it->outputUuid; + } + } + if (!restore) { + // restore anyways if the output the window was on got removed + if (const auto oldData = oldDataMap.find(window); oldData != oldDataMap.end()) { + restore = std::none_of(outputs.begin(), outputs.end(), [&oldData](const auto output) { + return output->uuid() == oldData->outputUuid; + }); + } + } + if (restore) { + window->setQuickTileMode(newData.quickTile, newData.geometry.center()); + window->setMaximize(newData.maximize & MaximizeMode::MaximizeVertical, newData.maximize & MaximizeMode::MaximizeHorizontal); + window->setFullScreen(newData.fullscreen); + window->moveResize(newData.geometry); + window->setGeometryRestore(newData.geometryRestore); + window->setFullscreenGeometryRestore(newData.fullscreenGeometryRestore); + m_lastRestoreData[window] = dataForWindow(window); + } + } + // ensure data in current map is always up to date + dataMap[window] = dataForWindow(window); + } + uninhibit(); + m_currentKey = key; +} + +void PlacementTracker::init(const QString &key) +{ + m_currentKey = key; +} + +void PlacementTracker::saveGeometry(Window *window) +{ + if (m_inhibitCount == 0) { + auto &data = m_data[m_currentKey][window]; + data.geometry = window->moveResizeGeometry(); + data.outputUuid = window->moveResizeOutput()->uuid(); + } +} + +void PlacementTracker::saveInteractionCounter(Window *window) +{ + if (m_inhibitCount == 0) { + m_data[m_currentKey][window].interactiveMoveResizeCount = window->interactiveMoveResizeCount(); + } +} + +void PlacementTracker::saveMaximize(Window *window) +{ + if (m_inhibitCount == 0) { + auto &data = m_data[m_currentKey][window]; + data.maximize = window->maximizeMode(); + } +} + +void PlacementTracker::saveQuickTile(Window *window) +{ + if (m_inhibitCount == 0) { + auto &data = m_data[m_currentKey][window]; + data.quickTile = window->quickTileMode(); + } +} + +void PlacementTracker::saveFullscreen(Window *window) +{ + if (m_inhibitCount == 0) { + auto &data = m_data[m_currentKey][window]; + data.fullscreen = window->isFullScreen(); + } +} + +void PlacementTracker::saveMaximizeGeometryRestore(Window *window) +{ + if (m_inhibitCount == 0) { + auto &data = m_data[m_currentKey][window]; + data.geometryRestore = window->geometryRestore(); + } +} + +void PlacementTracker::saveFullscreenGeometryRestore(Window *window) +{ + if (m_inhibitCount == 0) { + auto &data = m_data[m_currentKey][window]; + data.fullscreenGeometryRestore = window->fullscreenGeometryRestore(); + } +} + +void PlacementTracker::inhibit() +{ + m_inhibitCount++; +} + +void PlacementTracker::uninhibit() +{ + Q_ASSERT(m_inhibitCount > 0); + m_inhibitCount--; +} +} + +#include "moc_placementtracker.cpp" diff --git a/local/recipes/kde/kwin/source/src/placementtracker.h b/local/recipes/kde/kwin/source/src/placementtracker.h new file mode 100644 index 0000000000..4df17dea91 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/placementtracker.h @@ -0,0 +1,70 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "core/rect.h" +#include "utils/common.h" + +#include +#include +#include +#include + +namespace KWin +{ + +class Window; +class Workspace; + +class PlacementTracker : public QObject +{ + Q_OBJECT +public: + PlacementTracker(Workspace *workspace); + + void add(Window *window); + void remove(Window *window); + + void restore(const QString &key); + void init(const QString &key); + + void inhibit(); + void uninhibit(); + +private: + struct WindowData + { + QString outputUuid; + RectF geometry; + MaximizeMode maximize; + QuickTileMode quickTile; + RectF geometryRestore; + bool fullscreen; + RectF fullscreenGeometryRestore; + uint32_t interactiveMoveResizeCount; + }; + + void saveGeometry(Window *window); + void saveInteractionCounter(Window *window); + void saveMaximize(Window *window); + void saveQuickTile(Window *window); + void saveFullscreen(Window *window); + void saveMaximizeGeometryRestore(Window *window); + void saveFullscreenGeometryRestore(Window *window); + WindowData dataForWindow(Window *window) const; + + QList m_savedWindows; + QHash> m_data; + QHash m_lastRestoreData; + QString m_currentKey; + int m_inhibitCount = 0; + Workspace *const m_workspace; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/plugin.cpp b/local/recipes/kde/kwin/source/src/plugin.cpp new file mode 100644 index 0000000000..31082e1799 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugin.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "plugin.h" + +namespace KWin +{ + +Plugin::Plugin() = default; + +PluginFactory::PluginFactory() = default; + +} // namespace KWin + +#include "moc_plugin.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugin.h b/local/recipes/kde/kwin/source/src/plugin.h new file mode 100644 index 0000000000..ceff5cc71d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugin.h @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "config-kwin.h" + +#include + +#include +#include + +namespace KWin +{ + +#define PluginFactory_iid "org.kde.kwin.PluginFactoryInterface" KWIN_PLUGIN_VERSION_STRING + +/** + * The Plugin class is the baseclass for all binary compositor extensions. + * + * Note that a binary extension must be recompiled with every new KWin release. + */ +class KWIN_EXPORT Plugin : public QObject +{ + Q_OBJECT + +public: + explicit Plugin(); +}; + +/** + * The PluginFactory class creates binary compositor extensions. + */ +class KWIN_EXPORT PluginFactory : public QObject +{ + Q_OBJECT + +public: + explicit PluginFactory(); + + virtual std::unique_ptr create() const = 0; +}; + +} // namespace KWin + +Q_DECLARE_INTERFACE(KWin::PluginFactory, PluginFactory_iid) diff --git a/local/recipes/kde/kwin/source/src/pluginmanager.cpp b/local/recipes/kde/kwin/source/src/pluginmanager.cpp new file mode 100644 index 0000000000..7fbfae99d6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/pluginmanager.cpp @@ -0,0 +1,135 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "pluginmanager.h" +#include "dbusinterface.h" +#include "main.h" +#include "plugin.h" +#include "utils/common.h" + +#include +#include +#include +#include + +namespace KWin +{ + +static const QString s_pluginDirectory = QStringLiteral("kwin/plugins"); + +static QJsonValue readPluginInfo(const QJsonObject &metadata, const QString &key) +{ + return metadata.value(QLatin1String("KPlugin")).toObject().value(key); +} + +PluginManager::PluginManager() +{ + const KConfigGroup config(kwinApp()->config(), QStringLiteral("Plugins")); + + auto checkEnabled = [&config](const QString &pluginId, const QJsonObject &metadata) { + const QString configKey = pluginId + QLatin1String("Enabled"); + if (config.hasKey(configKey)) { + return config.readEntry(configKey, false); + } + return readPluginInfo(metadata, QStringLiteral("EnabledByDefault")).toBool(false); + }; + + const QList plugins = KPluginMetaData::findPlugins(s_pluginDirectory); + for (const KPluginMetaData &metadata : plugins) { + if (m_plugins.find(metadata.pluginId()) != m_plugins.end()) { + qCWarning(KWIN_CORE) << "Conflicting plugin id" << metadata.pluginId(); + continue; + } + if (checkEnabled(metadata.pluginId(), metadata.rawData())) { + loadPlugin(metadata); + } + } + + new PluginManagerDBusInterface(this); +} + +PluginManager::~PluginManager() = default; + +QStringList PluginManager::loadedPlugins() const +{ + QStringList ret; + ret.reserve(m_plugins.size()); + for (const auto &[key, _] : m_plugins) { + ret.push_back(key); + } + return ret; +} + +QStringList PluginManager::availablePlugins() const +{ + const QList plugins = KPluginMetaData::findPlugins(s_pluginDirectory); + + QStringList ret; + ret.reserve(plugins.size()); + + for (const KPluginMetaData &metadata : plugins) { + ret.append(metadata.pluginId()); + } + + return ret; +} + +bool PluginManager::loadPlugin(const QString &pluginId) +{ + if (m_plugins.find(pluginId) != m_plugins.end()) { + qCDebug(KWIN_CORE) << "Plugin with id" << pluginId << "is already loaded"; + return false; + } + const KPluginMetaData metadata = KPluginMetaData::findPluginById(s_pluginDirectory, pluginId); + if (metadata.isValid()) { + if (loadPlugin(metadata)) { + return true; + } + } + return false; +} + +bool PluginManager::loadPlugin(const KPluginMetaData &metadata) +{ + if (!metadata.isValid()) { + qCDebug(KWIN_CORE) << "PluginManager::loadPlugin needs a valid plugin metadata"; + return false; + } + + const QString pluginId = metadata.pluginId(); + QPluginLoader pluginLoader(metadata.fileName()); + if (pluginLoader.metaData().value("IID").toString() != PluginFactory_iid) { + qCWarning(KWIN_CORE) << pluginId << "has mismatching plugin version"; + return false; + } + + std::unique_ptr factory(qobject_cast(pluginLoader.instance())); + if (!factory) { + qCWarning(KWIN_CORE) << "Failed to get plugin factory for" << pluginId << pluginLoader.errorString(); + return false; + } + + if (std::unique_ptr plugin = factory->create()) { + m_plugins[pluginId] = std::move(plugin); + return true; + } else { + return false; + } +} + +void PluginManager::unloadPlugin(const QString &pluginId) +{ + auto it = m_plugins.find(pluginId); + if (it != m_plugins.end()) { + m_plugins.erase(it); + } else { + qCWarning(KWIN_CORE) << "No plugin with the specified id:" << pluginId; + } +} + +} // namespace KWin + +#include "moc_pluginmanager.cpp" diff --git a/local/recipes/kde/kwin/source/src/pluginmanager.h b/local/recipes/kde/kwin/source/src/pluginmanager.h new file mode 100644 index 0000000000..23d8513367 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/pluginmanager.h @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/globals.h" + +#include +#include + +#include + +namespace KWin +{ + +class Plugin; + +/** + * The PluginManager class loads and unloads binary compositor extensions. + */ +class KWIN_EXPORT PluginManager : public QObject +{ + Q_OBJECT + +public: + PluginManager(); + ~PluginManager() override; + + QStringList loadedPlugins() const; + QStringList availablePlugins() const; + +public Q_SLOTS: + bool loadPlugin(const QString &pluginId); + void unloadPlugin(const QString &pluginId); + +private: + bool loadPlugin(const KPluginMetaData &metadata); + + std::map> m_plugins; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/CMakeLists.txt new file mode 100644 index 0000000000..aa0ae241ee --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/CMakeLists.txt @@ -0,0 +1,137 @@ +function(kwin_add_effect_config name) + list(REMOVE_ITEM ARGV ${name}) + kcoreaddons_add_plugin(${name} INSTALL_NAMESPACE "kwin/effects/configs" SOURCES ${ARGV}) + target_compile_definitions(${name} PRIVATE -DTRANSLATION_DOMAIN=\"kwin\") +endfunction() + +# Add a CMake-time check for python3 to avoid failures during build. +find_package (Python3 COMPONENTS Interpreter) +add_feature_info("Python3" Python3_Interpreter_FOUND "Required to strip effects metadata") +set(KSEM_EXE "${CMAKE_CURRENT_SOURCE_DIR}/strip-effect-metadata.py") + +function (kwin_strip_builtin_effect_metadata target metadata) + set(stripped_metadata "${CMAKE_CURRENT_BINARY_DIR}/${metadata}.stripped") + + set(command ${KSEM_EXE} --source=${metadata} --output=${stripped_metadata}) + add_custom_command( + OUTPUT ${stripped_metadata} + COMMAND ${command} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + DEPENDS ${metadata} + COMMENT "Stripping ${metadata}..." + ) + set_property(TARGET ${target} APPEND PROPERTY AUTOGEN_TARGET_DEPENDS ${stripped_metadata}) +endfunction() + +macro(kwin_add_builtin_effect name) + kcoreaddons_add_plugin(${name} STATIC SOURCES ${ARGN} INSTALL_NAMESPACE "kwin/effects/plugins") + target_compile_definitions(${name} PRIVATE -DTRANSLATION_DOMAIN=\"kwin\") + kwin_strip_builtin_effect_metadata(${name} metadata.json) + install(FILES metadata.json DESTINATION ${KDE_INSTALL_DATADIR}/kwin-wayland/builtin-effects/ RENAME ${name}.json) +endmacro() + +function(kwin_add_scripted_effect name source) + kpackage_install_package(${source} ${name} effects kwin-wayland) + + # necessary so tests are found without installing + file(COPY ${source}/contents ${source}/metadata.json DESTINATION ${CMAKE_BINARY_DIR}/bin/kwin-wayland/effects/${name}) +endfunction() + +function(kwin_add_script name source) + kpackage_install_package(${source} ${name} scripts kwin-wayland) + + # Copy the script to the build directory so one can run tests without prior + # make install. FIXME: use add_custom_command. + file(COPY ${source}/contents ${source}/metadata.json DESTINATION ${CMAKE_BINARY_DIR}/bin/kwin-wayland/scripts/${name}) +endfunction() + +add_subdirectory(private) + +add_subdirectory(blendchanges) +add_subdirectory(blur) +add_subdirectory(bouncekeys) +add_subdirectory(buttonrebinds) +add_subdirectory(colorblindnesscorrection) +add_subdirectory(colorpicker) +add_subdirectory(desktopchangeosd) +add_subdirectory(dialogparent) +add_subdirectory(diminactive) +add_subdirectory(dimscreen) +add_subdirectory(eyeonscreen) +add_subdirectory(fade) +add_subdirectory(fadedesktop) +add_subdirectory(fadingpopups) +add_subdirectory(fallapart) +add_subdirectory(frozenapp) +add_subdirectory(fullscreen) +add_subdirectory(glide) +add_subdirectory(hidecursor) +add_subdirectory(highlightwindow) +add_subdirectory(idletime) +add_subdirectory(invert) +add_subdirectory(kpackage) +add_subdirectory(kscreen) +add_subdirectory(login) +add_subdirectory(logout) +add_subdirectory(magiclamp) +add_subdirectory(magnifier) +add_subdirectory(maximize) +add_subdirectory(minimizeall) +add_subdirectory(mouseclick) +add_subdirectory(mousekeys) +add_subdirectory(mousemark) +add_subdirectory(nightlight) +add_subdirectory(outputlocator) +add_subdirectory(overview) +add_subdirectory(qpa) +add_subdirectory(scale) +add_subdirectory(screenedge) +add_subdirectory(screenshot) +add_subdirectory(screentransform) +add_subdirectory(sessionquit) +add_subdirectory(shakecursor) +add_subdirectory(sheet) +add_subdirectory(showcompositing) +add_subdirectory(showfps) +add_subdirectory(showpaint) +add_subdirectory(slide) +add_subdirectory(slideback) +add_subdirectory(slidingpopups) +add_subdirectory(slowkeys) +add_subdirectory(squash) +add_subdirectory(startupfeedback) +add_subdirectory(stickykeys) +add_subdirectory(synchronizeskipswitcher) +add_subdirectory(systembell) +add_subdirectory(thumbnailaside) +add_subdirectory(tileseditor) +add_subdirectory(touchpadshortcuts) +add_subdirectory(touchpoints) +add_subdirectory(trackmouse) +add_subdirectory(translucency) +add_subdirectory(videowall) +add_subdirectory(windowaperture) +add_subdirectory(windowsystem) +add_subdirectory(windowview) +add_subdirectory(wobblywindows) +add_subdirectory(zoom) + +if (KWIN_BUILD_NOTIFICATIONS) + add_subdirectory(keynotification) +endif() + +if (PipeWire_FOUND) + add_subdirectory(screencast) +endif() +if (KWIN_BUILD_RUNNERS) + add_subdirectory(krunner-integration) +endif() +if(TARGET K::KGlobalAccelD) + add_subdirectory(kglobalaccel) +endif() +if (KWIN_BUILD_EIS) + add_subdirectory(eis) +endif() +if (KWIN_BUILD_GAMECONTROLLER) + add_subdirectory(gamecontroller) +endif() diff --git a/local/recipes/kde/kwin/source/src/plugins/blendchanges/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/blendchanges/CMakeLists.txt new file mode 100644 index 0000000000..ebd1b4a8aa --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blendchanges/CMakeLists.txt @@ -0,0 +1,13 @@ +####################################### +# Effect + +set(blendchanges_SOURCES + main.cpp + blendchanges.cpp +) + +kwin_add_builtin_effect(blendchanges ${blendchanges_SOURCES}) +target_link_libraries(blendchanges PRIVATE + kwin + Qt::DBus +) diff --git a/local/recipes/kde/kwin/source/src/plugins/blendchanges/blendchanges.cpp b/local/recipes/kde/kwin/source/src/plugins/blendchanges/blendchanges.cpp new file mode 100644 index 0000000000..2f087ad5cd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blendchanges/blendchanges.cpp @@ -0,0 +1,105 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +// own +#include "blendchanges.h" +#include "effect/effecthandler.h" +#include "opengl/glutils.h" + +#include +#include + +using namespace std::chrono_literals; + +namespace KWin +{ +BlendChanges::BlendChanges() + : CrossFadeEffect() +{ + QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/kde/KWin/BlendChanges"), + QStringLiteral("org.kde.KWin.BlendChanges"), + this, + QDBusConnection::ExportAllSlots); + + m_timeline.setEasingCurve(QEasingCurve::InOutCubic); +} + +BlendChanges::~BlendChanges() = default; + +bool BlendChanges::supported() +{ + return effects->compositingType() == OpenGLCompositing && effects->animationsSupported(); +} + +void KWin::BlendChanges::start(int delay) +{ + int animationDuration = animationTime(400ms); + + if (!supported() || m_state != Off) { + return; + } + if (effects->hasActiveFullScreenEffect()) { + return; + } + + const QList allWindows = effects->stackingOrder(); + for (auto window : allWindows) { + if (!window->isFullScreen()) { + redirect(window); + } + } + + QTimer::singleShot(delay, this, [this, animationDuration]() { + m_timeline.setDuration(std::chrono::milliseconds(animationDuration)); + effects->addRepaintFull(); + m_state = Blending; + }); + + m_state = ShowingCache; +} + +bool BlendChanges::isActive() const +{ + return m_state != Off; +} + +void BlendChanges::postPaintScreen() +{ + if (m_timeline.done()) { + m_timeline.reset(); + m_state = Off; + + const QList allWindows = effects->stackingOrder(); + for (auto window : allWindows) { + unredirect(window); + } + } + effects->addRepaintFull(); +} + +void BlendChanges::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) +{ + data.setCrossFadeProgress(m_timeline.value()); + effects->paintWindow(renderTarget, viewport, w, mask, deviceRegion, data); +} + +void BlendChanges::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + if (m_state == Off) { + return; + } + if (m_state == Blending) { + m_timeline.advance(presentTime); + } + + effects->prePaintScreen(data, presentTime); +} + +} // namespace KWin + +#include "moc_blendchanges.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/blendchanges/blendchanges.h b/local/recipes/kde/kwin/source/src/plugins/blendchanges/blendchanges.h new file mode 100644 index 0000000000..e82c64f039 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blendchanges/blendchanges.h @@ -0,0 +1,57 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2022 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "effect/offscreeneffect.h" +#include "effect/timeline.h" +#include + +namespace KWin +{ + +class BlendChanges : public CrossFadeEffect +{ + Q_OBJECT + +public: + BlendChanges(); + ~BlendChanges() override; + + static bool supported(); + + // Effect interface + void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override; + void postPaintScreen() override; + void paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) override; + + bool isActive() const override; + + int requestedEffectChainPosition() const override + { + return 80; + } + +public Q_SLOTS: + /** + * Called from DBus, this should be called before triggering any changes + * delay (ms) refers to how long to keep the current frame before starting a crossfade + * We should expect all clients to have repainted by the time this expires + */ + void start(int delay = 300); + +private: + TimeLine m_timeline; + enum State { + Off, + ShowingCache, + Blending + }; + State m_state = Off; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/blendchanges/main.cpp b/local/recipes/kde/kwin/source/src/plugins/blendchanges/main.cpp new file mode 100644 index 0000000000..9bf8e36fb0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blendchanges/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2022 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "blendchanges.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(BlendChanges, + "metadata.json.stripped", + return BlendChanges::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/blendchanges/metadata.json b/local/recipes/kde/kwin/source/src/plugins/blendchanges/metadata.json new file mode 100644 index 0000000000..0739c84647 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blendchanges/metadata.json @@ -0,0 +1,104 @@ +{ + "KPlugin": { + "Category": "Appearance", + "Description": "Animates system style changes", + "Description[ar]": "تغيرات نمط تحريكات النظام", + "Description[az]": "Sistem tərzi dəyişikliklərini canlandırır", + "Description[be]": "Анімаванне змены стылю сістэмы", + "Description[bg]": "Анимиране на промените на системния стил", + "Description[ca@valencia]": "Anima els canvis d'estil del sistema", + "Description[ca]": "Anima els canvis d'estil del sistema", + "Description[cs]": "Animuje změny stylu systému", + "Description[da]": "Animerer ændringer i systemets stil", + "Description[de]": "Animiert Änderungen am Systemdesign", + "Description[en_GB]": "Animates system style changes", + "Description[eo]": "Animas sistemajn stilŝanĝojn", + "Description[es]": "Anima los cambios de estilo del sistema", + "Description[et]": "Süsteemistiili muutuste animeerimine", + "Description[eu]": "Sistemako estiloen aldaketak animatzen ditu", + "Description[fi]": "Animoi järjestelmätyylin muutokset", + "Description[fr]": "Anime les modifications de style du système", + "Description[gl]": "Anima os cambios de estilo do sistema.", + "Description[he]": "הנפשת הגדרות סגנון המערכת", + "Description[hu]": "Animálja a rendszerstílus változásait", + "Description[ia]": "Modificationes de stylo de systema animate", + "Description[id]": "Menganimasikan perubahan gaya sistem", + "Description[is]": "Hreyfiáhrif fyrir breytingar á stílsniði tölvu", + "Description[it]": "Anima le modifiche allo stile del sistema", + "Description[ja]": "システムのスタイルの変更をアニメートします", + "Description[ka]": "სისტემის სტილის ცვლილებების ანიმაცია", + "Description[ko]": "시스템 스타일 변경 애니메이션", + "Description[lt]": "Animuoja sistemos stiliaus pakeitimus", + "Description[lv]": "Animē sistēmas stila izmaiņas", + "Description[nb]": "Animer endringer i systemstil", + "Description[nl]": "Systeemstijl wijzigingen animeren", + "Description[nn]": "Animer endringar i systemstil", + "Description[pl]": "Animuje zmiany wyglądu systemu", + "Description[pt]": "Anima as mudanças de estilo do sistema", + "Description[pt_BR]": "Animar alterações de estilo do sistema", + "Description[ro]": "Animează schimbările de stil al sistemului", + "Description[ru]": "Анимация изменения оформления", + "Description[sa]": "प्रणालीशैल्याः परिवर्तनं सजीवं करोति", + "Description[sk]": "Animuje zmeny štýlu systému", + "Description[sl]": "Animirajte spremembe sloga sistema", + "Description[sv]": "Animerar systemets stiländringar", + "Description[ta]": "தோற்றத்திட்டம் மாறும்போது அதை படிப்படியாக மாற்றும்", + "Description[tr]": "Sistem biçemi değişikliklerini canlandırır", + "Description[uk]": "Анімація змін у стилі системи", + "Description[vi]": "Tạo hiệu ứng động cho các thay đổi kiểu cách hệ thống", + "Description[zh_CN]": "动画过渡系统风格更改", + "Description[zh_TW]": "為系統風格變化使用動畫效果", + "EnabledByDefault": true, + "License": "GPL", + "Name": "Blend Changes", + "Name[ar]": "تغييرات الخلط", + "Name[az]": "Dəyişikliklərin birləşdirilməsi", + "Name[be]": "Анімаванне змены стылю", + "Name[bg]": "Смесване на промените", + "Name[ca@valencia]": "Transició de canvis", + "Name[ca]": "Transició de canvis", + "Name[cs]": "Mísit změny", + "Name[da]": "Bland ændringer", + "Name[de]": "Änderungen umblenden", + "Name[en_GB]": "Blend Changes", + "Name[eo]": "Miksi Ŝanĝojn", + "Name[es]": "Mezclar cambios", + "Name[et]": "Muudatuste sulandamine", + "Name[eu]": "Konbinazio aldaketak", + "Name[fi]": "Sekoita muutokset", + "Name[fr]": "Modifications des mélanges", + "Name[gl]": "Mesturar os cambios", + "Name[he]": "ערבול שינויים", + "Name[hu]": "Változáskeverés", + "Name[ia]": "Modificationes de Blend", + "Name[id]": "Blend Changes", + "Name[is]": "Breytingablöndun", + "Name[it]": "Miscela modifiche", + "Name[ja]": "変更を溶け込ませる", + "Name[ka]": "ცვლილებების შერევა", + "Name[ko]": "변경 섞기", + "Name[lt]": "Suderinti pakeitimus", + "Name[lv]": "Apvienot izmaiņas", + "Name[nb]": "Gradvise endringer", + "Name[nl]": "Wijzigingen mengen", + "Name[nn]": "Gradvise endringar", + "Name[pl]": "Przenikanie zmian", + "Name[pt]": "Misturar as Alterações", + "Name[pt_BR]": "Misturar alterações", + "Name[ro]": "Îmbină schimbările", + "Name[ru]": "Плавные переходы", + "Name[sa]": "मिश्रण परिवर्तनम्", + "Name[sk]": "Zmiešať zmeny", + "Name[sl]": "Zmešaj spremembe", + "Name[sv]": "Blanda ändringar", + "Name[ta]": "படிப்படியாக மாற்று", + "Name[tr]": "Değişiklikleri Harmanla", + "Name[uk]": "Злиття змін", + "Name[vi]": "Pha trộn các thay đổi", + "Name[zh_CN]": "平滑过渡更改", + "Name[zh_TW]": "混合變化" + }, + "org.kde.kwin.effect": { + "internal": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/blur/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/blur/CMakeLists.txt new file mode 100644 index 0000000000..0d7b2c692b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blur/CMakeLists.txt @@ -0,0 +1,39 @@ +####################################### +# Effect + +set(blur_SOURCES + blur.cpp + blur.qrc + main.cpp +) + +kconfig_add_kcfg_files(blur_SOURCES + blurconfig.kcfgc +) + +kwin_add_builtin_effect(blur ${blur_SOURCES}) +target_link_libraries(blur PRIVATE + kwin + + KF6::ConfigGui + + KDecoration3::KDecoration +) + +####################################### +# Config +if (KWIN_BUILD_KCMS) + set(kwin_blur_config_SRCS blur_config.cpp) + ki18n_wrap_ui(kwin_blur_config_SRCS blur_config.ui) + kconfig_add_kcfg_files(kwin_blur_config_SRCS blurconfig.kcfgc) + + kwin_add_effect_config(kwin_blur_config ${kwin_blur_config_SRCS}) + + target_link_libraries(kwin_blur_config + KF6::KCMUtils + KF6::CoreAddons + KF6::I18n + Qt::DBus + KWinEffectsInterface + ) +endif() diff --git a/local/recipes/kde/kwin/source/src/plugins/blur/blur.cpp b/local/recipes/kde/kwin/source/src/plugins/blur/blur.cpp new file mode 100644 index 0000000000..3c45db8324 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blur/blur.cpp @@ -0,0 +1,979 @@ +/* + SPDX-FileCopyrightText: 2010 Fredrik Höglund + SPDX-FileCopyrightText: 2011 Philipp Knechtges + SPDX-FileCopyrightText: 2018 Alex Nemeth + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "blur.h" +// KConfigSkeleton +#include "blurconfig.h" + +#include "core/pixelgrid.h" +#include "core/rendertarget.h" +#include "core/renderviewport.h" +#include "effect/effecthandler.h" +#include "opengl/glplatform.h" +#include "scene/backgroundeffectitem.h" +#include "scene/decorationitem.h" +#include "scene/scene.h" +#include "scene/surfaceitem.h" +#include "scene/windowitem.h" +#include "wayland/blur.h" +#include "wayland/contrast.h" +#include "wayland/display.h" +#include "wayland/surface.h" +#include "window.h" + +#if KWIN_BUILD_X11 +#include "utils/xcbutils.h" +#endif + +#include +#include +#include +#include +#include +#include +#include // for ceil() +#include + +#include +#include + +#include + +Q_LOGGING_CATEGORY(KWIN_BLUR, "kwin_effect_blur", QtWarningMsg) + +static void ensureResources() +{ + // Must initialize resources manually because the effect is a static lib. + Q_INIT_RESOURCE(blur); +} + +namespace KWin +{ + +static const QByteArray s_blurAtomName = QByteArrayLiteral("_KDE_NET_WM_BLUR_BEHIND_REGION"); + +BlurManagerInterface *BlurEffect::s_blurManager = nullptr; +QTimer *BlurEffect::s_blurManagerRemoveTimer = nullptr; + +ContrastManagerInterface *BlurEffect::s_contrastManager = nullptr; +QTimer *BlurEffect::s_contrastManagerRemoveTimer = nullptr; + +static QMatrix4x4 colorTransformMatrix(qreal saturation, qreal contrast) +{ + QMatrix4x4 saturationMatrix; + QMatrix4x4 contrastMatrix; + + if (!qFuzzyCompare(saturation, 1.0)) { + const qreal rval = (1.0 - saturation) * 0.2126; + const qreal gval = (1.0 - saturation) * 0.7152; + const qreal bval = (1.0 - saturation) * 0.0722; + + saturationMatrix = QMatrix4x4(rval + saturation, rval, rval, 0.0, + gval, gval + saturation, gval, 0.0, + bval, bval, bval + saturation, 0.0, + 0.0, 0.0, 0.0, 1.0); + } + + if (!qFuzzyCompare(contrast, 1.0)) { + const float transl = (1.0 - contrast) / 2.0; + + contrastMatrix = QMatrix4x4(contrast, 0.0, 0.0, 0.0, + 0.0, contrast, 0.0, 0.0, + 0.0, 0.0, contrast, 0.0, + transl, transl, transl, 1.0); + } + + return contrastMatrix * saturationMatrix; +} + +BlurEffect::BlurEffect() +{ + BlurConfig::instance(effects->config()); + ensureResources(); + + m_onscreenPass.shader = ShaderManager::instance()->generateShaderFromFile(ShaderTrait::MapTexture, + QStringLiteral(":/effects/blur/shaders/vertex.vert"), + QStringLiteral(":/effects/blur/shaders/onscreen.frag")); + if (!m_onscreenPass.shader) { + qCWarning(KWIN_BLUR) << "Failed to load onscreen pass shader"; + return; + } else { + m_onscreenPass.mvpMatrixLocation = m_onscreenPass.shader->uniformLocation("modelViewProjectionMatrix"); + m_onscreenPass.colorMatrixLocation = m_onscreenPass.shader->uniformLocation("colorMatrix"); + m_onscreenPass.offsetLocation = m_onscreenPass.shader->uniformLocation("offset"); + m_onscreenPass.halfpixelLocation = m_onscreenPass.shader->uniformLocation("halfpixel"); + } + + m_roundedOnscreenPass.shader = ShaderManager::instance()->generateShaderFromFile(ShaderTrait::MapTexture, + QStringLiteral(":/effects/blur/shaders/onscreen_rounded.vert"), + QStringLiteral(":/effects/blur/shaders/onscreen_rounded.frag")); + if (!m_roundedOnscreenPass.shader) { + qCWarning(KWIN_BLUR) << "Failed to load onscreen pass shader"; + return; + } else { + m_roundedOnscreenPass.mvpMatrixLocation = m_roundedOnscreenPass.shader->uniformLocation("modelViewProjectionMatrix"); + m_roundedOnscreenPass.colorMatrixLocation = m_roundedOnscreenPass.shader->uniformLocation("colorMatrix"); + m_roundedOnscreenPass.offsetLocation = m_roundedOnscreenPass.shader->uniformLocation("offset"); + m_roundedOnscreenPass.halfpixelLocation = m_roundedOnscreenPass.shader->uniformLocation("halfpixel"); + m_roundedOnscreenPass.boxLocation = m_roundedOnscreenPass.shader->uniformLocation("box"); + m_roundedOnscreenPass.cornerRadiusLocation = m_roundedOnscreenPass.shader->uniformLocation("cornerRadius"); + m_roundedOnscreenPass.opacityLocation = m_roundedOnscreenPass.shader->uniformLocation("opacity"); + } + + m_downsamplePass.shader = ShaderManager::instance()->generateShaderFromFile(ShaderTrait::MapTexture, + QStringLiteral(":/effects/blur/shaders/vertex.vert"), + QStringLiteral(":/effects/blur/shaders/downsample.frag")); + if (!m_downsamplePass.shader) { + qCWarning(KWIN_BLUR) << "Failed to load downsampling pass shader"; + return; + } else { + m_downsamplePass.mvpMatrixLocation = m_downsamplePass.shader->uniformLocation("modelViewProjectionMatrix"); + m_downsamplePass.offsetLocation = m_downsamplePass.shader->uniformLocation("offset"); + m_downsamplePass.halfpixelLocation = m_downsamplePass.shader->uniformLocation("halfpixel"); + } + + m_upsamplePass.shader = ShaderManager::instance()->generateShaderFromFile(ShaderTrait::MapTexture, + QStringLiteral(":/effects/blur/shaders/vertex.vert"), + QStringLiteral(":/effects/blur/shaders/upsample.frag")); + if (!m_upsamplePass.shader) { + qCWarning(KWIN_BLUR) << "Failed to load upsampling pass shader"; + return; + } else { + m_upsamplePass.mvpMatrixLocation = m_upsamplePass.shader->uniformLocation("modelViewProjectionMatrix"); + m_upsamplePass.offsetLocation = m_upsamplePass.shader->uniformLocation("offset"); + m_upsamplePass.halfpixelLocation = m_upsamplePass.shader->uniformLocation("halfpixel"); + } + + m_noisePass.shader = ShaderManager::instance()->generateShaderFromFile(ShaderTrait::MapTexture, + QStringLiteral(":/effects/blur/shaders/vertex.vert"), + QStringLiteral(":/effects/blur/shaders/noise.frag")); + if (!m_noisePass.shader) { + qCWarning(KWIN_BLUR) << "Failed to load noise pass shader"; + return; + } else { + m_noisePass.mvpMatrixLocation = m_noisePass.shader->uniformLocation("modelViewProjectionMatrix"); + m_noisePass.noiseTextureSizeLocation = m_noisePass.shader->uniformLocation("noiseTextureSize"); + } + + initBlurStrengthValues(); + reconfigure(ReconfigureAll); + +#if KWIN_BUILD_X11 + if (effects->xcbConnection()) { + net_wm_blur_region = effects->announceSupportProperty(s_blurAtomName, this); + } +#endif + + if (!s_blurManagerRemoveTimer) { + s_blurManagerRemoveTimer = new QTimer(QCoreApplication::instance()); + s_blurManagerRemoveTimer->setSingleShot(true); + s_blurManagerRemoveTimer->callOnTimeout([]() { + s_blurManager->remove(); + s_blurManager = nullptr; + }); + } + s_blurManagerRemoveTimer->stop(); + if (!s_blurManager) { + s_blurManager = new BlurManagerInterface(effects->waylandDisplay(), s_blurManagerRemoveTimer); + } + + if (!s_contrastManagerRemoveTimer) { + s_contrastManagerRemoveTimer = new QTimer(QCoreApplication::instance()); + s_contrastManagerRemoveTimer->setSingleShot(true); + s_contrastManagerRemoveTimer->callOnTimeout([]() { + s_contrastManager->remove(); + s_contrastManager = nullptr; + }); + } + s_contrastManagerRemoveTimer->stop(); + if (!s_contrastManager) { + s_contrastManager = new ContrastManagerInterface(effects->waylandDisplay(), s_contrastManagerRemoveTimer); + } + + connect(effects, &EffectsHandler::windowAdded, this, &BlurEffect::slotWindowAdded); + connect(effects, &EffectsHandler::windowDeleted, this, &BlurEffect::slotWindowDeleted); + connect(effects, &EffectsHandler::viewRemoved, this, &BlurEffect::slotViewRemoved); +#if KWIN_BUILD_X11 + connect(effects, &EffectsHandler::propertyNotify, this, &BlurEffect::slotPropertyNotify); + connect(effects, &EffectsHandler::xcbConnectionChanged, this, [this]() { + net_wm_blur_region = effects->announceSupportProperty(s_blurAtomName, this); + }); +#endif + + // Fetch the blur regions for all windows + const auto stackingOrder = effects->stackingOrder(); + for (EffectWindow *window : stackingOrder) { + slotWindowAdded(window); + } + + m_valid = true; +} + +BlurEffect::~BlurEffect() +{ + // When compositing is restarted, avoid removing the manager immediately. + if (s_blurManager) { + s_blurManagerRemoveTimer->start(1000); + } + + if (s_contrastManager) { + s_contrastManagerRemoveTimer->start(1000); + } +} + +void BlurEffect::initBlurStrengthValues() +{ + // This function creates an array of blur strength values that are evenly distributed + + // The range of the slider on the blur settings UI + int numOfBlurSteps = 15; + int remainingSteps = numOfBlurSteps; + + /* + * Explanation for these numbers: + * + * The texture blur amount depends on the downsampling iterations and the offset value. + * By changing the offset we can alter the blur amount without relying on further downsampling. + * But there is a minimum and maximum value of offset per downsample iteration before we + * get artifacts. + * + * The minOffset variable is the minimum offset value for an iteration before we + * get blocky artifacts because of the downsampling. + * + * The maxOffset value is the maximum offset value for an iteration before we + * get diagonal line artifacts because of the nature of the dual kawase blur algorithm. + * + * The expandSize value is the minimum value for an iteration before we reach the end + * of a texture in the shader and sample outside of the area that was copied into the + * texture from the screen. + */ + + // {minOffset, maxOffset, expandSize} + blurOffsets.append({1.0, 2.0, 10}); // Down sample size / 2 + blurOffsets.append({2.0, 3.0, 20}); // Down sample size / 4 + blurOffsets.append({2.0, 5.0, 50}); // Down sample size / 8 + blurOffsets.append({3.0, 8.0, 150}); // Down sample size / 16 + // blurOffsets.append({5.0, 10.0, 400}); // Down sample size / 32 + // blurOffsets.append({7.0, ?.0}); // Down sample size / 64 + + float offsetSum = 0; + + for (int i = 0; i < blurOffsets.size(); i++) { + offsetSum += blurOffsets[i].maxOffset - blurOffsets[i].minOffset; + } + + for (int i = 0; i < blurOffsets.size(); i++) { + int iterationNumber = std::ceil((blurOffsets[i].maxOffset - blurOffsets[i].minOffset) / offsetSum * numOfBlurSteps); + remainingSteps -= iterationNumber; + + if (remainingSteps < 0) { + iterationNumber += remainingSteps; + } + + float offsetDifference = blurOffsets[i].maxOffset - blurOffsets[i].minOffset; + + for (int j = 1; j <= iterationNumber; j++) { + // {iteration, offset} + blurStrengthValues.append({i + 1, blurOffsets[i].minOffset + (offsetDifference / iterationNumber) * j}); + } + } +} + +void BlurEffect::reconfigure(ReconfigureFlags flags) +{ + BlurConfig::self()->read(); + + int blurStrength = BlurConfig::blurStrength() - 1; + m_iterationCount = blurStrengthValues[blurStrength].iteration; + m_offset = blurStrengthValues[blurStrength].offset; + m_expandSize = blurOffsets[m_iterationCount - 1].expandSize; + m_noiseStrength = BlurConfig::noiseStrength(); + m_colorMatrix = colorTransformMatrix(BlurConfig::saturation() / 100.0, 1.0); + for (auto &[window, data] : m_windows) { + data.blurItem->setPixelsToExpandRepaintsBelowOpaqueRegions(m_expandSize); + } + + // Update all windows for the blur to take effect + effects->addRepaintFull(); +} + +void BlurEffect::updateBlurRegion(EffectWindow *w) +{ + std::optional content; + std::optional frame; + std::optional saturation; + std::optional contrast; + +#if KWIN_BUILD_X11 + if (net_wm_blur_region != XCB_ATOM_NONE) { + const QByteArray value = w->readProperty(net_wm_blur_region, XCB_ATOM_CARDINAL, 32); + Region region; + if (value.size() > 0 && !(value.size() % (4 * sizeof(uint32_t)))) { + const uint32_t *cardinals = reinterpret_cast(value.constData()); + for (unsigned int i = 0; i < value.size() / sizeof(uint32_t);) { + int x = cardinals[i++]; + int y = cardinals[i++]; + int w = cardinals[i++]; + int h = cardinals[i++]; + region += Xcb::fromXNative(Rect(x, y, w, h)).toRect(); + } + } + if (!value.isNull()) { + content = region; + } + } +#endif + + if (SurfaceInterface *surface = w->surface()) { + if (surface->blur()) { + content = surface->blur()->region(); + } + if (surface->contrast()) { + saturation = surface->contrast()->saturation(); + contrast = surface->contrast()->contrast(); + } + } + + if (auto internal = w->internalWindow()) { + const auto property = internal->property("kwin_blur"); + if (property.isValid()) { + content = property.value(); + } + } + + if (w->decorationHasAlpha() && decorationSupportsBlurBehind(w)) { + frame = decorationBlurRegion(w); + } + + if (content.has_value() || frame.has_value()) { + BlurEffectData &data = m_windows[w]; + data.content = content; + data.frame = frame; + if (saturation || contrast) { + data.colorMatrix = colorTransformMatrix(saturation.value_or(1.0), contrast.value_or(1.0)); + } else { + data.colorMatrix.reset(); + } + if (!data.blurItem) { + data.blurItem = std::make_unique(w->windowItem()); + } + data.blurItem->setPixelsToExpandRepaintsBelowOpaqueRegions(m_expandSize); + data.blurItem->setEffectBoundingRect(blurRegion(w).boundingRect()); + } else { + if (auto it = m_windows.find(w); it != m_windows.end()) { + effects->makeOpenGLContextCurrent(); + m_windows.erase(it); + } + } +} + +void BlurEffect::slotWindowAdded(EffectWindow *w) +{ + SurfaceInterface *surf = w->surface(); + + if (surf) { + windowBlurChangedConnections[w] = connect(surf, &SurfaceInterface::blurChanged, this, [this, w]() { + if (w) { + updateBlurRegion(w); + } + }); + windowContrastChangedConnections[w] = connect(surf, &SurfaceInterface::contrastChanged, this, [this, w]() { + if (w) { + updateBlurRegion(w); + } + }); + } + if (auto internal = w->internalWindow()) { + internal->installEventFilter(this); + } + + setupDecorationConnections(w); + connect(w, &EffectWindow::windowDecorationChanged, this, [this, w]() { + setupDecorationConnections(w); + updateBlurRegion(w); + }); + + updateBlurRegion(w); +} + +void BlurEffect::slotWindowDeleted(EffectWindow *w) +{ + if (auto it = m_windows.find(w); it != m_windows.end()) { + effects->makeOpenGLContextCurrent(); + m_windows.erase(it); + } + if (auto it = windowBlurChangedConnections.find(w); it != windowBlurChangedConnections.end()) { + disconnect(*it); + windowBlurChangedConnections.erase(it); + } + if (auto it = windowContrastChangedConnections.find(w); it != windowContrastChangedConnections.end()) { + disconnect(*it); + windowContrastChangedConnections.erase(it); + } +} + +void BlurEffect::slotViewRemoved(KWin::RenderView *view) +{ + for (auto &[window, data] : m_windows) { + if (auto it = data.render.find(view); it != data.render.end()) { + effects->makeOpenGLContextCurrent(); + data.render.erase(it); + } + } +} + +#if KWIN_BUILD_X11 +void BlurEffect::slotPropertyNotify(EffectWindow *w, long atom) +{ + if (w && atom == net_wm_blur_region && net_wm_blur_region != XCB_ATOM_NONE) { + updateBlurRegion(w); + } +} +#endif + +void BlurEffect::setupDecorationConnections(EffectWindow *w) +{ + if (!w->decoration()) { + return; + } + + connect(w->decoration(), &KDecoration3::Decoration::blurRegionChanged, this, [this, w]() { + updateBlurRegion(w); + }); +} + +bool BlurEffect::eventFilter(QObject *watched, QEvent *event) +{ + auto internal = qobject_cast(watched); + if (internal && event->type() == QEvent::DynamicPropertyChange) { + QDynamicPropertyChangeEvent *pe = static_cast(event); + if (pe->propertyName() == "kwin_blur") { + if (auto w = effects->findWindow(internal)) { + updateBlurRegion(w); + } + } + } + return false; +} + +bool BlurEffect::enabledByDefault() +{ + const auto context = effects->openglContext(); + if (!context || context->isSoftwareRenderer()) { + return false; + } + GLPlatform *gl = context->glPlatform(); + + if (gl->isIntel() && gl->chipClass() < SandyBridge) { + return false; + } + if (gl->isPanfrost() && gl->chipClass() <= MaliT8XX) { + return false; + } + // The blur effect works, but is painfully slow (FPS < 5) on Mali and VideoCore + if (gl->isLima() || gl->isVideoCore4() || gl->isVideoCore3D()) { + return false; + } + return true; +} + +bool BlurEffect::supported() +{ + return effects->isOpenGLCompositing(); +} + +bool BlurEffect::decorationSupportsBlurBehind(const EffectWindow *w) const +{ + return w->decoration() && !w->decoration()->blurRegion().isNull(); +} + +Region BlurEffect::decorationBlurRegion(const EffectWindow *w) const +{ + if (!decorationSupportsBlurBehind(w)) { + return Region(); + } + + Region decorationRegion = Region(Rect(w->decoration()->rect().toAlignedRect())) - w->contentsRect().toRect(); + //! we return only blurred regions that belong to decoration region + return decorationRegion.intersected(Region(w->decoration()->blurRegion())); +} + +Region BlurEffect::blurRegion(EffectWindow *w) const +{ + Region region; + + if (auto it = m_windows.find(w); it != m_windows.end()) { + const std::optional &content = it->second.content; + const std::optional &frame = it->second.frame; + if (content.has_value()) { + if (content->isEmpty()) { + // An empty region means that the blur effect should be enabled + // for the whole window. + region = Rect(w->contentsRect().toRect()); + } else { + region = content->translated(w->contentsRect().topLeft().toPoint()) & w->contentsRect().toRect(); + } + if (frame.has_value()) { + region += frame.value(); + } + } else if (frame.has_value()) { + region = frame.value(); + } + } + + return region; +} + +void BlurEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + m_currentView = data.view; + + effects->prePaintScreen(data, presentTime); +} + +bool BlurEffect::shouldBlur(const EffectWindow *w, int mask, const WindowPaintData &data) const +{ + if (effects->activeFullScreenEffect() && !w->data(WindowForceBlurRole).toBool()) { + return false; + } + + if (w->isDesktop()) { + return false; + } + + bool scaled = !qFuzzyCompare(data.xScale(), 1.0) && !qFuzzyCompare(data.yScale(), 1.0); + bool translated = data.xTranslation() || data.yTranslation(); + + if ((scaled || (translated || (mask & PAINT_WINDOW_TRANSFORMED))) && !w->data(WindowForceBlurRole).toBool()) { + return false; + } + + return true; +} + +void BlurEffect::drawWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) +{ + blur(renderTarget, viewport, w, mask, deviceRegion, data); + + // Draw the window over the blurred area + effects->drawWindow(renderTarget, viewport, w, mask, deviceRegion, data); +} + +GLTexture *BlurEffect::ensureNoiseTexture() +{ + if (m_noiseStrength == 0) { + return nullptr; + } + + const qreal scale = std::max(1.0, QGuiApplication::primaryScreen()->logicalDotsPerInch() / 96.0); + if (!m_noisePass.noiseTexture || m_noisePass.noiseTextureScale != scale || m_noisePass.noiseTextureStength != m_noiseStrength) { + // Init randomness based on time + std::srand((uint)QTime::currentTime().msec()); + + QImage noiseImage(QSize(256, 256), QImage::Format_Grayscale8); + + for (int y = 0; y < noiseImage.height(); y++) { + uint8_t *noiseImageLine = (uint8_t *)noiseImage.scanLine(y); + + for (int x = 0; x < noiseImage.width(); x++) { + noiseImageLine[x] = std::rand() % m_noiseStrength; + } + } + + noiseImage = noiseImage.scaled(noiseImage.size() * scale); + + m_noisePass.noiseTexture = GLTexture::upload(noiseImage); + if (!m_noisePass.noiseTexture) { + return nullptr; + } + m_noisePass.noiseTexture->setFilter(GL_NEAREST); + m_noisePass.noiseTexture->setWrapMode(GL_REPEAT); + m_noisePass.noiseTextureScale = scale; + m_noisePass.noiseTextureStength = m_noiseStrength; + } + + return m_noisePass.noiseTexture.get(); +} + +void BlurEffect::blur(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) +{ + auto it = m_windows.find(w); + if (it == m_windows.end()) { + return; + } + + BlurEffectData &blurInfo = it->second; + BlurRenderData &renderInfo = blurInfo.render[m_currentView]; + if (!shouldBlur(w, mask, data)) { + return; + } + + // Compute the effective blur shape. Note that if the window is transformed, so will be the blur shape. + Region blurShape = blurRegion(w).translated(w->pos().toPoint()); + if (data.xScale() != 1 || data.yScale() != 1) { + QPoint pt = blurShape.boundingRect().topLeft(); + Region scaledShape; + for (const Rect &r : blurShape.rects()) { + const QPointF topLeft(pt.x() + (r.x() - pt.x()) * data.xScale() + data.xTranslation(), + pt.y() + (r.y() - pt.y()) * data.yScale() + data.yTranslation()); + const QPoint bottomRight(std::floor(topLeft.x() + r.width() * data.xScale()) - 1, + std::floor(topLeft.y() + r.height() * data.yScale()) - 1); + scaledShape += QRect(QPoint(std::floor(topLeft.x()), std::floor(topLeft.y())), bottomRight); + } + blurShape = scaledShape; + } else if (data.xTranslation() || data.yTranslation()) { + blurShape.translate(std::round(data.xTranslation()), std::round(data.yTranslation())); + } + + const QRect backgroundRect = blurShape.boundingRect(); + const QRect scaledBackgroundRect = snapToPixelGrid(scaledRect(backgroundRect, viewport.scale())); + const QRect deviceBackgroundRect = snapToPixelGrid(viewport.mapToDeviceCoordinates(backgroundRect)); + const auto opacity = w->opacity() * data.opacity(); + + // Get the effective shape that will be actually blurred. It's possible that all of it will be clipped. + QList effectiveShape; + effectiveShape.reserve(blurShape.rects().size()); + if (deviceRegion != Region::infinite()) { + for (const Rect &clipRect : deviceRegion.rects()) { + const RectF deviceClipRect = clipRect.translated(-deviceBackgroundRect.topLeft()); + for (const Rect &shapeRect : blurShape.rects()) { + const RectF deviceShapeRect = shapeRect.translated(-backgroundRect.topLeft()).scaled(viewport.scale()).rounded(); + if (const QRectF intersected = deviceClipRect.intersected(deviceShapeRect); !intersected.isEmpty()) { + effectiveShape.append(intersected); + } + } + } + } else { + for (const Rect &rect : blurShape.rects()) { + effectiveShape.append(rect.translated(-backgroundRect.topLeft()).scaled(viewport.scale()).rounded()); + } + } + if (effectiveShape.isEmpty()) { + return; + } + + // Maybe reallocate offscreen render targets. Keep in mind that the first one contains + // original background behind the window, it's not blurred. + GLenum textureFormat = GL_RGBA8; + if (renderTarget.texture()) { + textureFormat = renderTarget.texture()->internalFormat(); + } + + if (renderInfo.framebuffers.size() != (m_iterationCount + 1) || renderInfo.textures[0]->size() != backgroundRect.size() || renderInfo.textures[0]->internalFormat() != textureFormat) { + renderInfo.framebuffers.clear(); + renderInfo.textures.clear(); + + glClearColor(0, 0, 0, 0); + for (size_t i = 0; i <= m_iterationCount; ++i) { + auto texture = GLTexture::allocate(textureFormat, backgroundRect.size() / (1 << i)); + if (!texture) { + qCWarning(KWIN_BLUR) << "Failed to allocate an offscreen texture"; + return; + } + texture->setFilter(GL_LINEAR); + texture->setWrapMode(GL_CLAMP_TO_EDGE); + + auto framebuffer = std::make_unique(texture.get()); + if (!framebuffer->valid()) { + qCWarning(KWIN_BLUR) << "Failed to create an offscreen framebuffer"; + return; + } + EglContext::currentContext()->pushFramebuffer(framebuffer.get()); + glClear(GL_COLOR_BUFFER_BIT); + EglContext::currentContext()->popFramebuffer(); + renderInfo.textures.push_back(std::move(texture)); + renderInfo.framebuffers.push_back(std::move(framebuffer)); + } + } + + // Fetch the pixels behind the shape that is going to be blurred. + const Region dirtyRegion = viewport.mapFromDeviceCoordinatesContained(deviceRegion) & backgroundRect; + for (const Rect &dirtyRect : dirtyRegion.rects()) { + renderInfo.framebuffers[0]->blitFromRenderTarget(renderTarget, viewport, dirtyRect, dirtyRect.translated(-backgroundRect.topLeft())); + } + + // Upload the geometry: the first 6 vertices are used when downsampling and upsampling offscreen, + // the remaining vertices are used when rendering on the screen. + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + vbo->setAttribLayout(std::span(GLVertexBuffer::GLVertex2DLayout), sizeof(GLVertex2D)); + + const int vertexCount = effectiveShape.size() * 6; + if (auto result = vbo->map(6 + vertexCount)) { + auto map = *result; + + size_t vboIndex = 0; + + // The geometry that will be blurred offscreen, in logical pixels. + { + const QRectF localRect = QRectF(0, 0, backgroundRect.width(), backgroundRect.height()); + + const float x0 = localRect.left(); + const float y0 = localRect.top(); + const float x1 = localRect.right(); + const float y1 = localRect.bottom(); + + const float u0 = x0 / backgroundRect.width(); + const float v0 = 1.0f - y0 / backgroundRect.height(); + const float u1 = x1 / backgroundRect.width(); + const float v1 = 1.0f - y1 / backgroundRect.height(); + + // first triangle + map[vboIndex++] = GLVertex2D{ + .position = QVector2D(x0, y0), + .texcoord = QVector2D(u0, v0), + }; + map[vboIndex++] = GLVertex2D{ + .position = QVector2D(x1, y1), + .texcoord = QVector2D(u1, v1), + }; + map[vboIndex++] = GLVertex2D{ + .position = QVector2D(x0, y1), + .texcoord = QVector2D(u0, v1), + }; + + // second triangle + map[vboIndex++] = GLVertex2D{ + .position = QVector2D(x0, y0), + .texcoord = QVector2D(u0, v0), + }; + map[vboIndex++] = GLVertex2D{ + .position = QVector2D(x1, y0), + .texcoord = QVector2D(u1, v0), + }; + map[vboIndex++] = GLVertex2D{ + .position = QVector2D(x1, y1), + .texcoord = QVector2D(u1, v1), + }; + } + + // The geometry that will be painted on screen, in device pixels. + for (const QRectF &rect : effectiveShape) { + const float x0 = rect.left(); + const float y0 = rect.top(); + const float x1 = rect.right(); + const float y1 = rect.bottom(); + + const float u0 = x0 / scaledBackgroundRect.width(); + const float v0 = 1.0f - y0 / scaledBackgroundRect.height(); + const float u1 = x1 / scaledBackgroundRect.width(); + const float v1 = 1.0f - y1 / scaledBackgroundRect.height(); + + // first triangle + map[vboIndex++] = GLVertex2D{ + .position = QVector2D(x0, y0), + .texcoord = QVector2D(u0, v0), + }; + map[vboIndex++] = GLVertex2D{ + .position = QVector2D(x1, y1), + .texcoord = QVector2D(u1, v1), + }; + map[vboIndex++] = GLVertex2D{ + .position = QVector2D(x0, y1), + .texcoord = QVector2D(u0, v1), + }; + + // second triangle + map[vboIndex++] = GLVertex2D{ + .position = QVector2D(x0, y0), + .texcoord = QVector2D(u0, v0), + }; + map[vboIndex++] = GLVertex2D{ + .position = QVector2D(x1, y0), + .texcoord = QVector2D(u1, v0), + }; + map[vboIndex++] = GLVertex2D{ + .position = QVector2D(x1, y1), + .texcoord = QVector2D(u1, v1), + }; + } + + vbo->unmap(); + } else { + qCWarning(KWIN_BLUR) << "Failed to map vertex buffer"; + return; + } + + vbo->bindArrays(); + + // The downsample pass of the dual Kawase algorithm: the background will be scaled down 50% every iteration. + { + ShaderManager::instance()->pushShader(m_downsamplePass.shader.get()); + + QMatrix4x4 projectionMatrix; + projectionMatrix.ortho(QRectF(0.0, 0.0, backgroundRect.width(), backgroundRect.height())); + + m_downsamplePass.shader->setUniform(m_downsamplePass.mvpMatrixLocation, projectionMatrix); + m_downsamplePass.shader->setUniform(m_downsamplePass.offsetLocation, float(m_offset)); + + for (size_t i = 1; i < renderInfo.framebuffers.size(); ++i) { + const auto &read = renderInfo.framebuffers[i - 1]; + const auto &draw = renderInfo.framebuffers[i]; + + const QVector2D halfpixel(0.5 / read->colorAttachment()->width(), + 0.5 / read->colorAttachment()->height()); + m_downsamplePass.shader->setUniform(m_downsamplePass.halfpixelLocation, halfpixel); + + read->colorAttachment()->bind(); + + GLFramebuffer::pushFramebuffer(draw.get()); + vbo->draw(GL_TRIANGLES, 0, 6); + } + + ShaderManager::instance()->popShader(); + } + + // The upsample pass of the dual Kawase algorithm: the background will be scaled up 200% every iteration. + { + ShaderManager::instance()->pushShader(m_upsamplePass.shader.get()); + + QMatrix4x4 projectionMatrix; + projectionMatrix.ortho(QRectF(0.0, 0.0, backgroundRect.width(), backgroundRect.height())); + + m_upsamplePass.shader->setUniform(m_upsamplePass.mvpMatrixLocation, projectionMatrix); + m_upsamplePass.shader->setUniform(m_upsamplePass.offsetLocation, float(m_offset)); + + for (size_t i = renderInfo.framebuffers.size() - 1; i > 1; --i) { + GLFramebuffer::popFramebuffer(); + const auto &read = renderInfo.framebuffers[i]; + + const QVector2D halfpixel(0.5 / read->colorAttachment()->width(), + 0.5 / read->colorAttachment()->height()); + m_upsamplePass.shader->setUniform(m_upsamplePass.halfpixelLocation, halfpixel); + + read->colorAttachment()->bind(); + + vbo->draw(GL_TRIANGLES, 0, 6); + } + + ShaderManager::instance()->popShader(); + } + + const QMatrix4x4 &colorMatrix = blurInfo.colorMatrix ? *blurInfo.colorMatrix : m_colorMatrix; + const float modulation = opacity * opacity; + + if (const BorderRadius cornerRadius = w->window()->borderRadius(); !cornerRadius.isNull()) { + ShaderManager::instance()->pushShader(m_roundedOnscreenPass.shader.get()); + + QMatrix4x4 projectionMatrix = viewport.projectionMatrix(); + projectionMatrix.translate(scaledBackgroundRect.x(), scaledBackgroundRect.y()); + + GLFramebuffer::popFramebuffer(); + const auto &read = renderInfo.framebuffers[1]; + + const QVector2D halfpixel(0.5 / read->colorAttachment()->width(), + 0.5 / read->colorAttachment()->height()); + + const QRectF transformedRect = QRectF{ + w->frameGeometry().x() + data.xTranslation(), + w->frameGeometry().y() + data.yTranslation(), + w->frameGeometry().width() * data.xScale(), + w->frameGeometry().height() * data.yScale(), + }; + const QRectF nativeBox = snapToPixelGridF(scaledRect(transformedRect, viewport.scale())) + .translated(-scaledBackgroundRect.topLeft()); + const BorderRadius nativeCornerRadius = cornerRadius.scaled(viewport.scale()).rounded(); + + m_roundedOnscreenPass.shader->setUniform(m_roundedOnscreenPass.mvpMatrixLocation, projectionMatrix); + m_roundedOnscreenPass.shader->setUniform(m_roundedOnscreenPass.colorMatrixLocation, colorMatrix); + m_roundedOnscreenPass.shader->setUniform(m_roundedOnscreenPass.halfpixelLocation, halfpixel); + m_roundedOnscreenPass.shader->setUniform(m_roundedOnscreenPass.offsetLocation, float(m_offset)); + m_roundedOnscreenPass.shader->setUniform(m_roundedOnscreenPass.boxLocation, QVector4D(nativeBox.x() + nativeBox.width() * 0.5, nativeBox.y() + nativeBox.height() * 0.5, nativeBox.width() * 0.5, nativeBox.height() * 0.5)); + m_roundedOnscreenPass.shader->setUniform(m_roundedOnscreenPass.cornerRadiusLocation, nativeCornerRadius.toVector()); + m_roundedOnscreenPass.shader->setUniform(m_roundedOnscreenPass.opacityLocation, modulation); + + read->colorAttachment()->bind(); + + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + vbo->draw(GL_TRIANGLES, 6, vertexCount); + + glDisable(GL_BLEND); + + ShaderManager::instance()->popShader(); + } else { + ShaderManager::instance()->pushShader(m_onscreenPass.shader.get()); + + QMatrix4x4 projectionMatrix = viewport.projectionMatrix(); + projectionMatrix.translate(scaledBackgroundRect.x(), scaledBackgroundRect.y()); + + GLFramebuffer::popFramebuffer(); + const auto &read = renderInfo.framebuffers[1]; + + const QVector2D halfpixel(0.5 / read->colorAttachment()->width(), + 0.5 / read->colorAttachment()->height()); + + m_onscreenPass.shader->setUniform(m_onscreenPass.mvpMatrixLocation, projectionMatrix); + m_onscreenPass.shader->setUniform(m_onscreenPass.colorMatrixLocation, colorMatrix); + m_onscreenPass.shader->setUniform(m_onscreenPass.halfpixelLocation, halfpixel); + m_onscreenPass.shader->setUniform(m_onscreenPass.offsetLocation, float(m_offset)); + + read->colorAttachment()->bind(); + + if (modulation < 1.0) { + glEnable(GL_BLEND); + glBlendColor(0, 0, 0, modulation); + glBlendFunc(GL_CONSTANT_ALPHA, GL_ONE_MINUS_CONSTANT_ALPHA); + } + + vbo->draw(GL_TRIANGLES, 6, vertexCount); + + if (modulation < 1.0) { + glDisable(GL_BLEND); + } + + ShaderManager::instance()->popShader(); + } + + if (m_noiseStrength > 0) { + // Apply an additive noise onto the blurred image. The noise is useful to mask banding + // artifacts, which often happens due to the smooth color transitions in the blurred image. + + glEnable(GL_BLEND); + if (opacity < 1.0) { + glBlendFunc(GL_CONSTANT_ALPHA, GL_ONE); + } else { + glBlendFunc(GL_ONE, GL_ONE); + } + + if (GLTexture *noiseTexture = ensureNoiseTexture()) { + ShaderManager::instance()->pushShader(m_noisePass.shader.get()); + + QMatrix4x4 projectionMatrix = viewport.projectionMatrix(); + projectionMatrix.translate(scaledBackgroundRect.x(), scaledBackgroundRect.y()); + + m_noisePass.shader->setUniform(m_noisePass.mvpMatrixLocation, projectionMatrix); + m_noisePass.shader->setUniform(m_noisePass.noiseTextureSizeLocation, QVector2D(noiseTexture->width(), noiseTexture->height())); + + noiseTexture->bind(); + + vbo->draw(GL_TRIANGLES, 6, vertexCount); + + ShaderManager::instance()->popShader(); + } + + glDisable(GL_BLEND); + } + + vbo->unbindArrays(); +} + +bool BlurEffect::isActive() const +{ + return m_valid && !effects->isScreenLocked(); +} + +bool BlurEffect::blocksDirectScanout() const +{ + return false; +} + +} // namespace KWin + +#include "moc_blur.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/blur/blur.h b/local/recipes/kde/kwin/source/src/plugins/blur/blur.h new file mode 100644 index 0000000000..a0c9716360 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blur/blur.h @@ -0,0 +1,198 @@ +/* + SPDX-FileCopyrightText: 2010 Fredrik Höglund + SPDX-FileCopyrightText: 2018 Alex Nemeth + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/effect.h" +#include "opengl/glutils.h" +#include "scene/item.h" + +#include + +#include + +namespace KWin +{ + +class BlurManagerInterface; +class ContrastManagerInterface; +class BackgroundEffectItem; + +struct BlurRenderData +{ + /// Temporary render targets needed for the Dual Kawase algorithm, the first texture + /// contains not blurred background behind the window, it's cached. + std::vector> textures; + std::vector> framebuffers; +}; + +struct BlurEffectData +{ + /// The region that should be blurred behind the window + std::optional content; + + /// The region that should be blurred behind the frame + std::optional frame; + + /** + * The render data per render view, as they can have different + * color spaces and even different windows on them + */ + std::unordered_map render; + + std::unique_ptr blurItem; + + /** + * Color transformation matrix (contrast, and saturation). + */ + std::optional colorMatrix; +}; + +class BlurEffect : public KWin::Effect +{ + Q_OBJECT + +public: + BlurEffect(); + ~BlurEffect() override; + + static bool supported(); + static bool enabledByDefault(); + + void reconfigure(ReconfigureFlags flags) override; + void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override; + void drawWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) override; + + bool provides(Feature feature) override; + bool isActive() const override; + + int requestedEffectChainPosition() const override + { + return 20; + } + + bool eventFilter(QObject *watched, QEvent *event) override; + + bool blocksDirectScanout() const override; + +public Q_SLOTS: + void slotWindowAdded(KWin::EffectWindow *w); + void slotWindowDeleted(KWin::EffectWindow *w); + void slotViewRemoved(KWin::RenderView *view); +#if KWIN_BUILD_X11 + void slotPropertyNotify(KWin::EffectWindow *w, long atom); +#endif + void setupDecorationConnections(EffectWindow *w); + +private: + void initBlurStrengthValues(); + Region blurRegion(EffectWindow *w) const; + Region decorationBlurRegion(const EffectWindow *w) const; + bool decorationSupportsBlurBehind(const EffectWindow *w) const; + bool shouldBlur(const EffectWindow *w, int mask, const WindowPaintData &data) const; + void updateBlurRegion(EffectWindow *w); + void blur(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data); + GLTexture *ensureNoiseTexture(); + +private: + struct + { + std::unique_ptr shader; + int mvpMatrixLocation; + int colorMatrixLocation; + int offsetLocation; + int halfpixelLocation; + } m_onscreenPass; + + struct + { + std::unique_ptr shader; + int mvpMatrixLocation; + int colorMatrixLocation; + int offsetLocation; + int halfpixelLocation; + int boxLocation; + int cornerRadiusLocation; + int opacityLocation; + } m_roundedOnscreenPass; + + struct + { + std::unique_ptr shader; + int mvpMatrixLocation; + int offsetLocation; + int halfpixelLocation; + } m_downsamplePass; + + struct + { + std::unique_ptr shader; + int mvpMatrixLocation; + int offsetLocation; + int halfpixelLocation; + } m_upsamplePass; + + struct + { + std::unique_ptr shader; + int mvpMatrixLocation; + int noiseTextureSizeLocation; + + std::unique_ptr noiseTexture; + qreal noiseTextureScale = 1.0; + int noiseTextureStength = 0; + } m_noisePass; + + bool m_valid = false; +#if KWIN_BUILD_X11 + long net_wm_blur_region = 0; +#endif + RenderView *m_currentView = nullptr; + + QMatrix4x4 m_colorMatrix; + size_t m_iterationCount; // number of times the texture will be downsized to half size + int m_offset; + int m_expandSize; + int m_noiseStrength; + + struct OffsetStruct + { + float minOffset; + float maxOffset; + int expandSize; + }; + + QList blurOffsets; + + struct BlurValuesStruct + { + int iteration; + float offset; + }; + + QList blurStrengthValues; + + QMap windowBlurChangedConnections; + QMap windowContrastChangedConnections; + std::unordered_map m_windows; + + static BlurManagerInterface *s_blurManager; + static QTimer *s_blurManagerRemoveTimer; + + static ContrastManagerInterface *s_contrastManager; + static QTimer *s_contrastManagerRemoveTimer; +}; + +inline bool BlurEffect::provides(Effect::Feature feature) +{ + if (feature == Blur) { + return true; + } + return KWin::Effect::provides(feature); +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/blur/blur.kcfg b/local/recipes/kde/kwin/source/src/plugins/blur/blur.kcfg new file mode 100644 index 0000000000..c8d827adca --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blur/blur.kcfg @@ -0,0 +1,18 @@ + + + + + + 15 + + + 5 + + + 150 + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/blur/blur.qrc b/local/recipes/kde/kwin/source/src/plugins/blur/blur.qrc new file mode 100644 index 0000000000..64420426b9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blur/blur.qrc @@ -0,0 +1,18 @@ + + + shaders/downsample.frag + shaders/downsample_core.frag + shaders/noise.frag + shaders/noise_core.frag + shaders/onscreen.frag + shaders/onscreen_core.frag + shaders/onscreen_rounded_core.frag + shaders/onscreen_rounded.frag + shaders/onscreen_rounded_core.vert + shaders/onscreen_rounded.vert + shaders/upsample.frag + shaders/upsample_core.frag + shaders/vertex.vert + shaders/vertex_core.vert + + diff --git a/local/recipes/kde/kwin/source/src/plugins/blur/blur_config.cpp b/local/recipes/kde/kwin/source/src/plugins/blur/blur_config.cpp new file mode 100644 index 0000000000..ff9a59e0c2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blur/blur_config.cpp @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: 2010 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "blur_config.h" + +#include "config-kwin.h" + +// KConfigSkeleton +#include "blurconfig.h" + +#include +#include + +K_PLUGIN_CLASS(KWin::BlurEffectConfig) + +namespace KWin +{ + +BlurEffectConfig::BlurEffectConfig(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) +{ + ui.setupUi(widget()); + BlurConfig::instance(KWIN_CONFIG); + addConfig(BlurConfig::self(), widget()); +} + +BlurEffectConfig::~BlurEffectConfig() +{ +} + +void BlurEffectConfig::save() +{ + KCModule::save(); + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("blur")); +} + +} // namespace KWin + +#include "blur_config.moc" + +#include "moc_blur_config.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/blur/blur_config.h b/local/recipes/kde/kwin/source/src/plugins/blur/blur_config.h new file mode 100644 index 0000000000..c14c8afe53 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blur/blur_config.h @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2010 Fredrik Höglund + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "ui_blur_config.h" +#include + +namespace KWin +{ + +class BlurEffectConfig : public KCModule +{ + Q_OBJECT + +public: + explicit BlurEffectConfig(QObject *parent, const KPluginMetaData &data); + ~BlurEffectConfig() override; + + void save() override; + +private: + ::Ui::BlurEffectConfig ui; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/blur/blur_config.ui b/local/recipes/kde/kwin/source/src/plugins/blur/blur_config.ui new file mode 100644 index 0000000000..65be75bb26 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blur/blur_config.ui @@ -0,0 +1,183 @@ + + + BlurEffectConfig + + + + 0 + 0 + 480 + 184 + + + + + + + Blur strength: + + + + + + + + + 1 + + + 15 + + + 1 + + + 1 + + + 10 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBelow + + + + + + + + + Light + + + + + + + Strong + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + + + + + Noise strength: + + + + + + + + + 14 + + + 5 + + + 5 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBelow + + + 1 + + + + + + + + + Light + + + + + + + Strong + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + + + + + Saturation: + + + + + + + + + 100 + + + 500 + + + 25 + + + 25 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksBelow + + + + + + + + + None + + + + + + + Strong + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/blur/blurconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/blur/blurconfig.kcfgc new file mode 100644 index 0000000000..4fb3fe58ed --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blur/blurconfig.kcfgc @@ -0,0 +1,5 @@ +File=blur.kcfg +ClassName=BlurConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/local/recipes/kde/kwin/source/src/plugins/blur/main.cpp b/local/recipes/kde/kwin/source/src/plugins/blur/main.cpp new file mode 100644 index 0000000000..b5370621cb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blur/main.cpp @@ -0,0 +1,20 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "blur.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED_ENABLED(BlurEffect, + "metadata.json.stripped", + return BlurEffect::supported(); + , + return BlurEffect::enabledByDefault();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/blur/metadata.json b/local/recipes/kde/kwin/source/src/plugins/blur/metadata.json new file mode 100644 index 0000000000..1f30c8345e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blur/metadata.json @@ -0,0 +1,105 @@ +{ + "KPlugin": { + "Category": "Appearance", + "Description": "Blurs the background behind semi-transparent windows", + "Description[ar]": "تغشي الخلفية خلف النوافذ شبه الشفافة", + "Description[az]": "Yarımşəffaf pəncərələrin altındakı fonu yayğınlaşdırmaq", + "Description[be]": "Размыццё фону за паўпразрыстымі вокнамі", + "Description[bg]": "Размива фона зад полупрозрачни прозорци", + "Description[ca@valencia]": "Difumina el fons de darrere de les finestres semitransparents", + "Description[ca]": "Difumina el fons de darrere de les finestres semitransparents", + "Description[cs]": "Rozostří pozadí poloprůhledných oken", + "Description[da]": "Slører baggrunden bag semigennemsigtige vinduer", + "Description[de]": "Verwischt den Hintergrund halbtransparenter Fenster", + "Description[en_GB]": "Blurs the background behind semi-transparent windows", + "Description[eo]": "Malklarigas la fonon malantaŭ duontravideblaj fenestroj", + "Description[es]": "Difumina el fondo detrás de las ventanas semitransparentes", + "Description[et]": "Poolläbipaistvate akende tausta hägustamine", + "Description[eu]": "Leiho erdi-gardenen atzeko planoa lausotzen du", + "Description[fi]": "Sumentaa taustan puoliksi läpikuultavien ikkunoiden takana", + "Description[fr]": "Rend flou l'arrière-plan sous les fenêtres semi-transparentes", + "Description[gl]": "Desenfoca o fondo tras das xanelas semitransparentes.", + "Description[he]": "מטשטש את הרקע מאחורי חלונות שקופים למחצה", + "Description[hu]": "Elmosódottá teszi a félig áttetsző ablakok hátterét", + "Description[ia]": "Obscura le fundo detra fenestras semi-transparente", + "Description[id]": "Memburamkan belakang latar-belakang jendela semi transparan", + "Description[is]": "Setur bakgrunn undir hálfgagnsæjum gluggum í móðu", + "Description[it]": "Sfoca lo sfondo dietro a finestre semitrasparenti", + "Description[ja]": "半透明なウィンドウの背景をぼかします", + "Description[ka]": "ნახევრადგამჭვირვალე ფანჯრებს მიღმა ფონის დაბინდვა", + "Description[ko]": "반투명 창의 뒷배경을 흐리게 합니다", + "Description[lt]": "Sulieja už pusiau permatomų langų esantį foną", + "Description[lv]": "Aizmiglo fonu aiz puscaurspīdīgiem logiem", + "Description[nb]": "Gjør bakgrunnen til halvgjennomsiktige vinduer uklar", + "Description[nl]": "Vervaagt de achtergrond van halftransparante vensters", + "Description[nn]": "Gjer bakgrunnen til halvgjennomsiktige vindauge uklar", + "Description[pl]": "Rozmywa tło za półprzezroczystymi oknami", + "Description[pt]": "Borra o fundo atrás das janelas semi-transparentes", + "Description[pt_BR]": "Borra o plano de fundo por trás das janelas semitransparentes", + "Description[ro]": "Încețoșează fundalul în spatele ferestrelor semi-transparente", + "Description[ru]": "Размывание фона под полупрозрачными окнами", + "Description[sa]": "अर्धपारदर्शकजालकस्य पृष्ठतः पृष्ठभूमिं धुन्धलं करोति", + "Description[sk]": "Rozmaže pozadie za polopriehľadnými oknami", + "Description[sl]": "Zabriše ozadje za polprosojnimi okni", + "Description[sv]": "Gör bakgrunden bakom halvgenomskinliga fönster suddig", + "Description[ta]": "ஒளிபுகும் சாளரங்களின் பின்புலத்தை மங்கலாக்கும்", + "Description[tr]": "Yarısaydam pencerelerin ardını bulanıklaştırır", + "Description[uk]": "Розмивання тла напівпрозорих вікон", + "Description[vi]": "Làm mờ phần nền phía sau các cửa sổ bán trong suốt", + "Description[zh_CN]": "对半透明窗口的背景进行虚化处理", + "Description[zh_TW]": "模糊半透明視窗的背景", + "EnabledByDefault": true, + "License": "GPL", + "Name": "Blur", + "Name[ar]": "الغشاوة", + "Name[az]": "Bulanıq", + "Name[be]": "Размыццё", + "Name[bg]": "Замъгляване", + "Name[ca@valencia]": "Difuminat", + "Name[ca]": "Difuminat", + "Name[cs]": "Rozostření", + "Name[da]": "Slør", + "Name[de]": "Verwischen", + "Name[en_GB]": "Blur", + "Name[eo]": "Malklarigi", + "Name[es]": "Desenfocar", + "Name[et]": "Hägu", + "Name[eu]": "Lausotu", + "Name[fi]": "Sumennus", + "Name[fr]": "Flou", + "Name[gl]": "Desenfocar", + "Name[he]": "טשטוש", + "Name[hu]": "Elmosás", + "Name[ia]": "Obscura (Blur)", + "Name[id]": "Buram", + "Name[is]": "Móða", + "Name[it]": "Sfocatura", + "Name[ja]": "ぼかし", + "Name[ka]": "ბუნდოვნება", + "Name[ko]": "흐리게", + "Name[lt]": "Suliejimas", + "Name[lv]": "Aizmiglot", + "Name[nb]": "Uklar", + "Name[nl]": "Vervagen", + "Name[nn]": "Uklar", + "Name[pl]": "Rozmycie", + "Name[pt]": "BlueFish", + "Name[pt_BR]": "Borrar", + "Name[ro]": "Încețoșare", + "Name[ru]": "Размытие", + "Name[sa]": "धुन्धला", + "Name[sk]": "Rozmazanie", + "Name[sl]": "Zabriši", + "Name[sv]": "Oskärpa", + "Name[ta]": "மங்கலாக்கு", + "Name[tr]": "Bulanıklaştır", + "Name[uk]": "Розмиття", + "Name[vi]": "Mờ", + "Name[zh_CN]": "窗口背景虚化", + "Name[zh_TW]": "模糊" + }, + "X-KDE-ConfigModule": "kwin_blur_config", + "org.kde.kwin.effect": { + "enabledByDefaultMethod": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/blur/shaders/downsample.frag b/local/recipes/kde/kwin/source/src/plugins/blur/shaders/downsample.frag new file mode 100644 index 0000000000..d83b7133b1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blur/shaders/downsample.frag @@ -0,0 +1,16 @@ +uniform sampler2D texUnit; +uniform float offset; +uniform vec2 halfpixel; + +varying vec2 uv; + +void main(void) +{ + vec4 sum = texture2D(texUnit, uv) * 4.0; + sum += texture2D(texUnit, uv - halfpixel.xy * offset); + sum += texture2D(texUnit, uv + halfpixel.xy * offset); + sum += texture2D(texUnit, uv + vec2(halfpixel.x, -halfpixel.y) * offset); + sum += texture2D(texUnit, uv - vec2(halfpixel.x, -halfpixel.y) * offset); + + gl_FragColor = sum / 8.0; +} diff --git a/local/recipes/kde/kwin/source/src/plugins/blur/shaders/downsample_core.frag b/local/recipes/kde/kwin/source/src/plugins/blur/shaders/downsample_core.frag new file mode 100644 index 0000000000..8f16f232a5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blur/shaders/downsample_core.frag @@ -0,0 +1,20 @@ +#version 140 + +uniform sampler2D texUnit; +uniform float offset; +uniform vec2 halfpixel; + +in vec2 uv; + +out vec4 fragColor; + +void main(void) +{ + vec4 sum = texture(texUnit, uv) * 4.0; + sum += texture(texUnit, uv - halfpixel.xy * offset); + sum += texture(texUnit, uv + halfpixel.xy * offset); + sum += texture(texUnit, uv + vec2(halfpixel.x, -halfpixel.y) * offset); + sum += texture(texUnit, uv - vec2(halfpixel.x, -halfpixel.y) * offset); + + fragColor = sum / 8.0; +} diff --git a/local/recipes/kde/kwin/source/src/plugins/blur/shaders/noise.frag b/local/recipes/kde/kwin/source/src/plugins/blur/shaders/noise.frag new file mode 100644 index 0000000000..cc4f5d4519 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blur/shaders/noise.frag @@ -0,0 +1,11 @@ +uniform sampler2D texUnit; +uniform vec2 noiseTextureSize; + +varying vec2 uv; + +void main(void) +{ + vec2 uvNoise = vec2(gl_FragCoord.xy / noiseTextureSize); + + gl_FragColor = vec4(texture2D(texUnit, uvNoise).rrr, 0); +} diff --git a/local/recipes/kde/kwin/source/src/plugins/blur/shaders/noise_core.frag b/local/recipes/kde/kwin/source/src/plugins/blur/shaders/noise_core.frag new file mode 100644 index 0000000000..03ae93e242 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blur/shaders/noise_core.frag @@ -0,0 +1,15 @@ +#version 140 + +uniform sampler2D texUnit; +uniform vec2 noiseTextureSize; + +in vec2 uv; + +out vec4 fragColor; + +void main(void) +{ + vec2 uvNoise = vec2(gl_FragCoord.xy / noiseTextureSize); + + fragColor = vec4(texture(texUnit, uvNoise).rrr, 0); +} diff --git a/local/recipes/kde/kwin/source/src/plugins/blur/shaders/upsample.frag b/local/recipes/kde/kwin/source/src/plugins/blur/shaders/upsample.frag new file mode 100644 index 0000000000..050a7c94c4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blur/shaders/upsample.frag @@ -0,0 +1,19 @@ +uniform sampler2D texUnit; +uniform float offset; +uniform vec2 halfpixel; + +varying vec2 uv; + +void main(void) +{ + vec4 sum = texture2D(texUnit, uv + vec2(-halfpixel.x * 2.0, 0.0) * offset); + sum += texture2D(texUnit, uv + vec2(-halfpixel.x, halfpixel.y) * offset) * 2.0; + sum += texture2D(texUnit, uv + vec2(0.0, halfpixel.y * 2.0) * offset); + sum += texture2D(texUnit, uv + vec2(halfpixel.x, halfpixel.y) * offset) * 2.0; + sum += texture2D(texUnit, uv + vec2(halfpixel.x * 2.0, 0.0) * offset); + sum += texture2D(texUnit, uv + vec2(halfpixel.x, -halfpixel.y) * offset) * 2.0; + sum += texture2D(texUnit, uv + vec2(0.0, -halfpixel.y * 2.0) * offset); + sum += texture2D(texUnit, uv + vec2(-halfpixel.x, -halfpixel.y) * offset) * 2.0; + + gl_FragColor = sum / 12.0; +} diff --git a/local/recipes/kde/kwin/source/src/plugins/blur/shaders/upsample_core.frag b/local/recipes/kde/kwin/source/src/plugins/blur/shaders/upsample_core.frag new file mode 100644 index 0000000000..3498fc2baa --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blur/shaders/upsample_core.frag @@ -0,0 +1,23 @@ +#version 140 + +uniform sampler2D texUnit; +uniform float offset; +uniform vec2 halfpixel; + +in vec2 uv; + +out vec4 fragColor; + +void main(void) +{ + vec4 sum = texture(texUnit, uv + vec2(-halfpixel.x * 2.0, 0.0) * offset); + sum += texture(texUnit, uv + vec2(-halfpixel.x, halfpixel.y) * offset) * 2.0; + sum += texture(texUnit, uv + vec2(0.0, halfpixel.y * 2.0) * offset); + sum += texture(texUnit, uv + vec2(halfpixel.x, halfpixel.y) * offset) * 2.0; + sum += texture(texUnit, uv + vec2(halfpixel.x * 2.0, 0.0) * offset); + sum += texture(texUnit, uv + vec2(halfpixel.x, -halfpixel.y) * offset) * 2.0; + sum += texture(texUnit, uv + vec2(0.0, -halfpixel.y * 2.0) * offset); + sum += texture(texUnit, uv + vec2(-halfpixel.x, -halfpixel.y) * offset) * 2.0; + + fragColor = sum / 12.0; +} diff --git a/local/recipes/kde/kwin/source/src/plugins/blur/shaders/vertex.vert b/local/recipes/kde/kwin/source/src/plugins/blur/shaders/vertex.vert new file mode 100644 index 0000000000..d26f713e76 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blur/shaders/vertex.vert @@ -0,0 +1,12 @@ +uniform mat4 modelViewProjectionMatrix; + +attribute vec2 position; +attribute vec2 texcoord; + +varying vec2 uv; + +void main(void) +{ + gl_Position = modelViewProjectionMatrix * vec4(position, 0.0, 1.0); + uv = texcoord; +} diff --git a/local/recipes/kde/kwin/source/src/plugins/blur/shaders/vertex_core.vert b/local/recipes/kde/kwin/source/src/plugins/blur/shaders/vertex_core.vert new file mode 100644 index 0000000000..2834a30d19 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/blur/shaders/vertex_core.vert @@ -0,0 +1,14 @@ +#version 140 + +uniform mat4 modelViewProjectionMatrix; + +in vec2 position; +in vec2 texcoord; + +out vec2 uv; + +void main(void) +{ + gl_Position = modelViewProjectionMatrix * vec4(position, 0.0, 1.0); + uv = texcoord; +} diff --git a/local/recipes/kde/kwin/source/src/plugins/bouncekeys/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/bouncekeys/CMakeLists.txt new file mode 100644 index 0000000000..6fea1ca7b6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/bouncekeys/CMakeLists.txt @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2023 Nicolas Fella +# SPDX-License-Identifier: BSD-3-Clause + +kcoreaddons_add_plugin(BounceKeysPlugin INSTALL_NAMESPACE "kwin/plugins") + +ecm_qt_declare_logging_category(BounceKeysPlugin + HEADER bouncekeys_debug.h + IDENTIFIER KWIN_BOUNCEKEYS + CATEGORY_NAME kwin_bouncekeys + DEFAULT_SEVERITY Warning +) + +target_sources(BounceKeysPlugin PRIVATE + main.cpp + bouncekeys.cpp +) +target_link_libraries(BounceKeysPlugin PRIVATE kwin KF6::WindowSystem) + diff --git a/local/recipes/kde/kwin/source/src/plugins/bouncekeys/bouncekeys.cpp b/local/recipes/kde/kwin/source/src/plugins/bouncekeys/bouncekeys.cpp new file mode 100644 index 0000000000..937ed2143a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/bouncekeys/bouncekeys.cpp @@ -0,0 +1,66 @@ +/* + SPDX-FileCopyrightText: 2023 Nicolas Fella + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "bouncekeys.h" +#include "keyboard_input.h" + +BounceKeysFilter::BounceKeysFilter() + : KWin::InputEventFilter(KWin::InputFilterOrder::BounceKeys) + , m_configWatcher(KConfigWatcher::create(KSharedConfig::openConfig("kaccessrc"))) +{ + const QLatin1String groupName("Keyboard"); + connect(m_configWatcher.get(), &KConfigWatcher::configChanged, this, [this, groupName](const KConfigGroup &group) { + if (group.name() == groupName) { + loadConfig(group); + } + }); + loadConfig(m_configWatcher->config()->group(groupName)); +} + +void BounceKeysFilter::loadConfig(const KConfigGroup &group) +{ + KWin::input()->uninstallInputEventFilter(this); + + if (group.readEntry("BounceKeys", false)) { + KWin::input()->installInputEventFilter(this); + + m_delay = std::chrono::milliseconds(group.readEntry("BounceKeysDelay", 500)); + } else { + m_lastEvent.clear(); + } +} + +bool BounceKeysFilter::keyboardKey(KWin::KeyboardKeyEvent *event) +{ + switch (event->state) { + case KWin::KeyboardKeyState::Pressed: + if (auto it = m_lastEvent.find(event->key); it == m_lastEvent.end()) { + // first time is always good + m_lastEvent[event->key].lastReceived = event->timestamp; + m_lastEvent[event->key].rejected = false; + return false; + } else { + auto last = it->lastReceived; + it->lastReceived = event->timestamp; + + it->rejected = event->timestamp - last < m_delay; + return it->rejected; + } + case KWin::KeyboardKeyState::Repeated: + if (auto it = m_lastEvent.find(event->key); it == m_lastEvent.end()) { + // should never happen normally, just let it through + return false; + } else { + return it->rejected; + } + case KWin::KeyboardKeyState::Released: + return false; + } + + Q_UNREACHABLE(); +} + +#include "moc_bouncekeys.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/bouncekeys/bouncekeys.h b/local/recipes/kde/kwin/source/src/plugins/bouncekeys/bouncekeys.h new file mode 100644 index 0000000000..ee093a12e2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/bouncekeys/bouncekeys.h @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2023 Nicolas Fella + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include "plugin.h" + +#include "input.h" +#include "input_event.h" + +class BounceKeysFilter : public KWin::Plugin, public KWin::InputEventFilter +{ + Q_OBJECT +public: + explicit BounceKeysFilter(); + + bool keyboardKey(KWin::KeyboardKeyEvent *event) override; + +private: + struct BounceKeyInfo + { + std::chrono::microseconds lastReceived; + bool rejected; + }; + + void loadConfig(const KConfigGroup &group); + + KConfigWatcher::Ptr m_configWatcher; + std::chrono::milliseconds m_delay; + QHash m_lastEvent; +}; diff --git a/local/recipes/kde/kwin/source/src/plugins/bouncekeys/main.cpp b/local/recipes/kde/kwin/source/src/plugins/bouncekeys/main.cpp new file mode 100644 index 0000000000..210ed6576b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/bouncekeys/main.cpp @@ -0,0 +1,23 @@ +/* + SPDX-FileCopyrightText: 2023 Nicolas Fella + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "plugin.h" + +#include "bouncekeys.h" + +class KWIN_EXPORT StickyKeysFactory : public KWin::PluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID PluginFactory_iid FILE "metadata.json") + Q_INTERFACES(KWin::PluginFactory) + +public: + std::unique_ptr create() const override + { + return std::make_unique(); + } +}; + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/bouncekeys/metadata.json b/local/recipes/kde/kwin/source/src/plugins/bouncekeys/metadata.json new file mode 100644 index 0000000000..aa304f4093 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/bouncekeys/metadata.json @@ -0,0 +1,5 @@ +{ + "KPlugin": { + "EnabledByDefault": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/buttonrebinds/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/buttonrebinds/CMakeLists.txt new file mode 100644 index 0000000000..5a8e00e451 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/buttonrebinds/CMakeLists.txt @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2022 David Redondo +# SPDX-License-Identifier: BSD-3-Clause + +kcoreaddons_add_plugin(buttonsrebind INSTALL_NAMESPACE "kwin/plugins") + +ecm_qt_declare_logging_category(buttonsrebind + HEADER buttonrebinds_debug.h + IDENTIFIER KWIN_BUTTONREBINDS + CATEGORY_NAME kwin_buttonrebinds + DEFAULT_SEVERITY Warning +) + +target_sources(buttonsrebind PRIVATE + main.cpp + buttonrebindsfilter.cpp +) +target_link_libraries(buttonsrebind PRIVATE kwin Qt::GuiPrivate XKB::XKB) diff --git a/local/recipes/kde/kwin/source/src/plugins/buttonrebinds/buttonrebindsfilter.cpp b/local/recipes/kde/kwin/source/src/plugins/buttonrebinds/buttonrebindsfilter.cpp new file mode 100644 index 0000000000..0142377b4c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/buttonrebinds/buttonrebindsfilter.cpp @@ -0,0 +1,612 @@ +/* + SPDX-FileCopyrightText: 2022 David Redondo + SPDX-FileCopyrightText: 2022 Harald Sitter + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "buttonrebindsfilter.h" +#include "buttonrebinds_debug.h" + +#include "cursor.h" +#include "input_event.h" +#include "keyboard_input.h" +#include "xkb.h" + +#include + +#include + +#include +#include +#include + +#include + +// Tells us that we are already in a binding event +class RebindScope +{ + static uint s_scopes; + +public: + RebindScope() + { + s_scopes++; + } + ~RebindScope() + { + Q_ASSERT(s_scopes > 0); + s_scopes--; + } + Q_DISABLE_COPY_MOVE(RebindScope) + static bool isRebinding() + { + return s_scopes > 0; + } +}; +uint RebindScope::s_scopes = 0; + +quint32 qHash(const Trigger &t) +{ + return qHash(t.device) * (t.button + 1); +} + +QString InputDevice::name() const +{ + return QStringLiteral("Button rebinding device"); +} + +void InputDevice::setEnabled(bool enabled) +{ +} + +bool InputDevice::isEnabled() const +{ + return true; +} + +bool InputDevice::isKeyboard() const +{ + return true; +} + +bool InputDevice::isLidSwitch() const +{ + return false; +} + +bool InputDevice::isPointer() const +{ + return true; +} + +bool InputDevice::isTabletModeSwitch() const +{ + return false; +} + +bool InputDevice::isTabletPad() const +{ + return true; +} + +bool InputDevice::isTabletTool() const +{ + return true; +} + +bool InputDevice::isTouch() const +{ + return false; +} + +bool InputDevice::isTouchpad() const +{ + return false; +} + +ButtonRebindsFilter::ButtonRebindsFilter() + : KWin::InputEventFilter(KWin::InputFilterOrder::ButtonRebind) + , m_configWatcher(KConfigWatcher::create(KSharedConfig::openConfig("kcminputrc"))) +{ + const QLatin1String groupName("ButtonRebinds"); + connect(m_configWatcher.get(), &KConfigWatcher::configChanged, this, [this, groupName](const KConfigGroup &group) { + // We want to get the top-most parent in the config file, since our ButtonRebinds configs tend to be very nested + auto parent = group.parent(); + while (parent.name() != "") { + if (parent.name() == groupName) { + loadConfig(parent); + return; + } + parent = parent.parent(); + } + }); + loadConfig(m_configWatcher->config()->group(groupName)); +} + +ButtonRebindsFilter::~ButtonRebindsFilter() +{ + if (m_inputDevice) { + KWin::input()->removeInputDevice(m_inputDevice.get()); + } +} + +void ButtonRebindsFilter::loadConfig(const KConfigGroup &group) +{ + Q_ASSERT(QLatin1String("ButtonRebinds") == group.name()); + if (m_inputDevice) { + KWin::input()->removeInputDevice(m_inputDevice.get()); + m_inputDevice.reset(); + } + KWin::input()->uninstallInputEventFilter(this); + for (auto &action : m_actions) { + action.clear(); + } + + bool foundActions = false; + + const auto mouseButtonEnum = QMetaEnum::fromType(); + const auto mouseGroup = group.group(QStringLiteral("Mouse")); + const auto mouseGroupKeys = mouseGroup.keyList(); + for (const QString &configKey : mouseGroupKeys) { + const int mappedButton = mouseButtonEnum.keyToValue(configKey.toLatin1()); + if (mappedButton != -1) { + const auto action = mouseGroup.readEntry(configKey, QStringList()); + insert(Pointer, {QString(), static_cast(mappedButton), 0}, action); + foundActions = true; + } + } + + const auto tabletsGroup = group.group(QStringLiteral("Tablet")); + const auto tablets = tabletsGroup.groupList(); + for (const auto &tabletName : tablets) { + const auto tabletGroup = tabletsGroup.group(tabletName); + const auto tabletButtons = tabletGroup.keyList(); + for (const auto &buttonName : tabletButtons) { + const auto entry = tabletGroup.readEntry(buttonName, QStringList()); + bool ok = false; + const uint button = buttonName.toUInt(&ok); + if (ok) { + foundActions = true; + insert(TabletPad, {tabletName, button, 0}, entry); + } + } + } + + const auto tabletToolsGroup = group.group(QStringLiteral("TabletTool")); + const auto tabletTools = tabletToolsGroup.groupList(); + for (const auto &tabletToolName : tabletTools) { + const auto toolGroup = tabletToolsGroup.group(tabletToolName); + const auto tabletToolButtons = toolGroup.keyList(); + for (const auto &buttonName : tabletToolButtons) { + const auto entry = toolGroup.readEntry(buttonName, QStringList()); + bool ok = false; + const uint button = buttonName.toUInt(&ok); + if (ok) { + foundActions = true; + insert(TabletToolButtonType, {tabletToolName, button, 0}, entry); + } + } + } + + const auto tabletDialsGroup = group.group(QStringLiteral("TabletDial")); + const auto tabletDials = tabletDialsGroup.groupList(); + for (const auto &tabletDialName : tabletDials) { + const auto dialGroup = tabletDialsGroup.group(tabletDialName); + const auto tabletDialButtons = dialGroup.keyList(); + for (const auto &buttonName : tabletDialButtons) { + const auto entry = dialGroup.readEntry(buttonName, QStringList()); + bool ok = false; + const uint button = buttonName.toUInt(&ok); + if (ok) { + foundActions = true; + insert(TabletDial, {tabletDialName, button, 0}, entry); + } + } + } + + const auto tabletRingsGroup = group.group(QStringLiteral("TabletRing")); + const auto tabletRings = tabletRingsGroup.groupList(); + for (const auto &tabletRingName : tabletRings) { + const auto ringModesGroup = tabletRingsGroup.group(tabletRingName); + for (const auto &modeGroupName : ringModesGroup.groupList()) { + const auto ringGroup = ringModesGroup.group(modeGroupName); + const auto tabletRingButtons = ringGroup.keyList(); + for (const auto &buttonName : tabletRingButtons) { + const auto entry = ringGroup.readEntry(buttonName, QStringList()); + bool buttonOk = false; + const uint button = buttonName.toUInt(&buttonOk); + bool modeOk = false; + const uint mode = modeGroupName.toUInt(&modeOk); + if (buttonOk && modeOk) { + foundActions = true; + insert(TabletRing, {tabletRingName, button, mode}, entry); + } + } + } + } + + if (foundActions) { + KWin::input()->installInputEventFilter(this); + + m_inputDevice = std::make_unique(); + KWin::input()->addInputDevice(m_inputDevice.get()); + } +} + +bool ButtonRebindsFilter::pointerButton(KWin::PointerButtonEvent *event) +{ + if (RebindScope::isRebinding()) { + return false; + } + + return send(Pointer, {{}, event->button, 0}, event->state == KWin::PointerButtonState::Pressed, 0, event->timestamp); +} + +bool ButtonRebindsFilter::tabletToolProximityEvent(KWin::TabletToolProximityEvent *event) +{ + if (RebindScope::isRebinding()) { + return false; + } + m_tabletCursorPos = event->position; + return false; +} + +bool ButtonRebindsFilter::tabletToolAxisEvent(KWin::TabletToolAxisEvent *event) +{ + if (RebindScope::isRebinding()) { + return false; + } + m_tabletCursorPos = event->position; + return false; +} + +bool ButtonRebindsFilter::tabletToolTipEvent(KWin::TabletToolTipEvent *event) +{ + if (RebindScope::isRebinding()) { + return false; + } + m_tabletCursorPos = event->position; + return false; +} + +bool ButtonRebindsFilter::tabletPadButtonEvent(KWin::TabletPadButtonEvent *event) +{ + if (RebindScope::isRebinding()) { + return false; + } + return send(TabletPad, {event->device->name(), event->button, 0}, event->pressed, 0, event->time); +} + +bool ButtonRebindsFilter::tabletToolButtonEvent(KWin::TabletToolButtonEvent *event) +{ + if (RebindScope::isRebinding()) { + return false; + } + m_tabletTool = event->tool; + return send(TabletToolButtonType, {event->device->name(), event->button, 0}, event->pressed, 0, event->time); +} + +bool ButtonRebindsFilter::tabletPadDialEvent(KWin::TabletPadDialEvent *event) +{ + if (RebindScope::isRebinding()) { + return false; + } + return send(TabletDial, {event->device->name(), static_cast(event->number), 0}, false, event->delta, event->time); +} + +bool ButtonRebindsFilter::tabletPadRingEvent(KWin::TabletPadRingEvent *event) +{ + if (RebindScope::isRebinding()) { + return false; + } + + // We only get the ring's position in degrees + // So we need to start when it's pressed, and stop when released (that's given as -1) + // And only emit events with the delta passes a certain threshold + if (event->position != -1) { + if (m_initialRingPosition == -1) { + m_initialRingPosition = event->position; + } + + // The maximum number of degrees a device could ever go from one position to the other. This is completely arbitrary. + // I would guarantee that most devices emit ring events in small, ~5 degree increments, but *we don't know* how big their increments are but it's safe to assume it's less than this. + constexpr int maximumIncrement = 180; + + qreal delta = m_initialRingPosition - event->position; + + // If the delta is something crazy, and could never be feasibly emitted by the device, then it's probably + // because we made a complete circle. + // For example, going from 0->355. That's a delta of 5 degrees, not -355 degrees! + if (abs(delta) > maximumIncrement) { + if (delta < 0) { + delta += 360; + } else { + delta -= 360; + } + } + + // Rings seem to have a minimum delta of 5 degrees, so we can assume that's one "unit". + const bool sent = send(TabletRing, {event->device->name(), static_cast(event->number), event->mode}, false, delta * 120, event->time); + // If accepted (that means the threshold is met) then we want to reset ourselves + if (sent) { + m_initialRingPosition = event->position; + } + return sent; + } else { + m_initialRingPosition = -1; + } + + return false; +} + +void ButtonRebindsFilter::insert(TriggerType type, const Trigger &trigger, const QStringList &entry) +{ + if (entry.empty()) { + qCWarning(KWIN_BUTTONREBINDS) << "Failed to rebind to" << entry; + return; + } + if (entry.first() == QLatin1String("Key")) { + if (entry.size() != 2) { + qCWarning(KWIN_BUTTONREBINDS) << "Invalid key" << entry; + return; + } + + const auto keys = QKeySequence::fromString(entry.at(1), QKeySequence::PortableText); + if (!keys.isEmpty()) { + m_actions.at(type).insert(trigger, keys); + } + } else if (entry.first() == QLatin1String("AxisKey")) { + if (entry.size() != 4) { + qCWarning(KWIN_BUTTONREBINDS) << "Invalid axis key" << entry; + return; + } + + const auto upKey = QKeySequence::fromString(entry.at(1), QKeySequence::PortableText); + const auto downKey = QKeySequence::fromString(entry.at(2), QKeySequence::PortableText); + const auto threshold = entry.at(3).toInt(); + + if (!upKey.isEmpty() && !downKey.isEmpty()) { + m_actions.at(type).insert(trigger, AxisKeybind{upKey, downKey, threshold}); + } + } else if (entry.first() == QLatin1String("MouseButton")) { + if (entry.size() < 2) { + qCWarning(KWIN_BUTTONREBINDS) << "Invalid mouse button" << entry; + return; + } + + bool ok = false; + MouseButton mb{entry[1].toUInt(&ok), {}}; + + // Last bit is the keyboard mods + if (entry.size() == 3) { + const auto keyboardModsRaw = entry.last().toInt(&ok); + mb.modifiers = Qt::KeyboardModifiers{keyboardModsRaw}; + } + + if (ok) { + m_actions.at(type).insert(trigger, mb); + } else { + qCWarning(KWIN_BUTTONREBINDS) << "Could not convert" << entry << "into a mouse button"; + } + } else if (entry.first() == QLatin1String("TabletToolButton")) { + if (entry.size() != 2) { + qCWarning(KWIN_BUTTONREBINDS) + << "Invalid tablet tool button" << entry; + return; + } + + bool ok = false; + const TabletToolButton tb{entry.last().toUInt(&ok)}; + if (ok) { + m_actions.at(type).insert(trigger, tb); + } else { + qCWarning(KWIN_BUTTONREBINDS) << "Could not convert" << entry << "into a mouse button"; + } + } else if (entry.first() == QLatin1String("Scroll")) { + m_actions.at(type).insert(trigger, ScrollWheel{}); + } else if (entry.first() == QLatin1String("Disabled")) { + m_actions.at(type).insert(trigger, DisabledButton{}); + } +} + +bool ButtonRebindsFilter::send(TriggerType type, const Trigger &trigger, bool pressed, double delta, std::chrono::microseconds timestamp) +{ + const auto &typeActions = m_actions.at(type); + if (typeActions.isEmpty()) { + return false; + } + + const auto &action = typeActions[trigger]; + if (const QKeySequence *seq = std::get_if(&action)) { + return sendKeySequence(*seq, pressed, timestamp); + } + if (const AxisKeybind *bind = std::get_if(&action)) { + if (std::abs(delta) < bind->threshold) { + return false; + } + + const auto sequence = delta > 0 ? bind->up : bind->down; + + bool sentKeyEvent = false; + sentKeyEvent |= sendKeySequence(sequence, true, timestamp); + sentKeyEvent |= sendKeySequence(sequence, false, timestamp); + return sentKeyEvent; + } + if (const auto mb = std::get_if(&action)) { + bool sentMouseEvent = false; + if (pressed && type != Pointer) { + sentMouseEvent |= sendMousePosition(m_tabletCursorPos, timestamp); + } + sendKeyModifiers(mb->modifiers, pressed, timestamp); + sentMouseEvent |= sendMouseButton(mb->button, pressed, timestamp); + if (sentMouseEvent) { + sendMouseFrame(); + } + return sentMouseEvent; + } + if (const auto tb = std::get_if(&action)) { + return sendTabletToolButton(tb->button, pressed, timestamp); + } + if (std::get_if(&action)) { + bool sentMouseEvent = false; + if (type != Pointer) { + sentMouseEvent |= sendMousePosition(m_tabletCursorPos, timestamp); + } + sentMouseEvent |= sendScrollWheel(delta, timestamp); + if (sentMouseEvent) { + sendMouseFrame(); + } + return sentMouseEvent; + } + if (std::get_if(&action)) { + // Intentional, we don't want to anything to anybody + return true; + } + return false; +} + +static constexpr std::array, 4> s_modifierKeyTable = { + std::pair(Qt::Key_Control, KEY_LEFTCTRL), + std::pair(Qt::Key_Alt, KEY_LEFTALT), + std::pair(Qt::Key_Shift, KEY_LEFTSHIFT), + std::pair(Qt::Key_Meta, KEY_LEFTMETA), +}; + +bool ButtonRebindsFilter::sendKeySequence(const QKeySequence &keys, bool pressed, std::chrono::microseconds time) +{ + if (keys.isEmpty()) { + return false; + } + + const auto &key = keys[0]; + auto sendKey = [this, pressed, time](xkb_keycode_t key) { + auto state = pressed ? KWin::KeyboardKeyState::Pressed : KWin::KeyboardKeyState::Released; + Q_EMIT m_inputDevice->keyChanged(key, state, time, m_inputDevice.get()); + }; + + // handle modifier-only keys + for (const auto &[keySymQt, keySymLinux] : s_modifierKeyTable) { + if (key == QKeyCombination::fromCombined(keySymQt)) { + RebindScope scope; + sendKey(keySymLinux); + return true; + } + } + + QList syms = KWin::Xkb::keysymsFromQtKey(keys[0]); + + // Use keysyms from the keypad if and only if KeypadModifier is set + syms.erase(std::remove_if(syms.begin(), syms.end(), [keys](int sym) { + bool onKeyPad = sym >= XKB_KEY_KP_Space && sym <= XKB_KEY_KP_Equal; + if (keys[0].keyboardModifiers() & Qt::KeypadModifier) { + return !onKeyPad; + } else { + return onKeyPad; + } + }), + syms.end()); + + if (syms.empty()) { + qCWarning(KWIN_BUTTONREBINDS) << "Could not convert" << keys << "to keysym"; + return false; + } + std::optional code; + // KKeyServer returns upper case syms, lower it to not confuse modifiers handling + for (int sym : syms) { + code = KWin::input()->keyboard()->xkb()->keycodeFromKeysym(sym); + if (code) { + break; + } + } + if (!code) { + qCWarning(KWIN_BUTTONREBINDS) << "Could not convert" << keys << "syms: " << syms << "to keycode"; + return false; + } + + RebindScope scope; + + if (key.keyboardModifiers() & Qt::ShiftModifier || code->level == 1) { + sendKey(KEY_LEFTSHIFT); + } + if (key.keyboardModifiers() & Qt::ControlModifier) { + sendKey(KEY_LEFTCTRL); + } + if (key.keyboardModifiers() & Qt::AltModifier) { + sendKey(KEY_LEFTALT); + } + if (key.keyboardModifiers() & Qt::MetaModifier) { + sendKey(KEY_LEFTMETA); + } + + sendKey(code->keyCode); + return true; +} + +bool ButtonRebindsFilter::sendKeyModifiers(const Qt::KeyboardModifiers &modifiers, bool pressed, std::chrono::microseconds time) +{ + if (modifiers == Qt::NoModifier) { + return false; + } + + auto sendKey = [this, pressed, time](xkb_keycode_t key) { + auto state = pressed ? KWin::KeyboardKeyState::Pressed : KWin::KeyboardKeyState::Released; + Q_EMIT m_inputDevice->keyChanged(key, state, time, m_inputDevice.get()); + }; + + if (modifiers.testFlag(Qt::ShiftModifier)) { + sendKey(KEY_LEFTSHIFT); + } + if (modifiers.testFlag(Qt::ControlModifier)) { + sendKey(KEY_LEFTCTRL); + } + if (modifiers.testFlag(Qt::AltModifier)) { + sendKey(KEY_LEFTALT); + } + if (modifiers.testFlag(Qt::MetaModifier)) { + sendKey(KEY_LEFTMETA); + } + + return true; +} + +bool ButtonRebindsFilter::sendMouseButton(quint32 button, bool pressed, std::chrono::microseconds time) +{ + RebindScope scope; + Q_EMIT m_inputDevice->pointerButtonChanged(button, KWin::PointerButtonState(pressed), time, m_inputDevice.get()); + return true; +} + +bool ButtonRebindsFilter::sendMousePosition(QPointF position, std::chrono::microseconds time) +{ + RebindScope scope; + Q_EMIT m_inputDevice->pointerMotionAbsolute(position, time, m_inputDevice.get()); + return true; +} + +bool ButtonRebindsFilter::sendMouseFrame() +{ + RebindScope scope; + Q_EMIT m_inputDevice->pointerFrame(m_inputDevice.get()); + return true; +} + +bool ButtonRebindsFilter::sendTabletToolButton(quint32 button, bool pressed, std::chrono::microseconds time) +{ + if (!m_tabletTool) { + return false; + } + RebindScope scope; + Q_EMIT m_inputDevice->tabletToolButtonEvent(button, pressed, m_tabletTool, time, m_inputDevice.get()); + return true; +} + +bool ButtonRebindsFilter::sendScrollWheel(double v120, std::chrono::microseconds time) +{ + RebindScope scope; + Q_EMIT m_inputDevice->pointerAxisChanged(KWin::PointerAxis::Vertical, v120 > 0 ? 15 : -15, v120, KWin::PointerAxisSource::Wheel, false, time, m_inputDevice.get()); + Q_EMIT m_inputDevice->pointerFrame(m_inputDevice.get()); + return true; +} + +#include "moc_buttonrebindsfilter.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/buttonrebinds/buttonrebindsfilter.h b/local/recipes/kde/kwin/source/src/plugins/buttonrebinds/buttonrebindsfilter.h new file mode 100644 index 0000000000..417f913114 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/buttonrebinds/buttonrebindsfilter.h @@ -0,0 +1,111 @@ +/* + SPDX-FileCopyrightText: 2022 David Redondo + SPDX-FileCopyrightText: 2022 Harald Sitter + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include "plugin.h" +#include +#include +#include + +#include "core/inputdevice.h" +#include "input.h" +#include "input_event.h" + +#include + +class InputDevice : public KWin::InputDevice +{ + QString name() const override; + + bool isEnabled() const override; + void setEnabled(bool enabled) override; + + bool isKeyboard() const override; + bool isPointer() const override; + bool isTouchpad() const override; + bool isTouch() const override; + bool isTabletTool() const override; + bool isTabletPad() const override; + bool isTabletModeSwitch() const override; + bool isLidSwitch() const override; +}; + +struct Trigger +{ + QString device; + uint button; + quint32 mode; + bool operator==(const Trigger &o) const = default; +}; + +class ButtonRebindsFilter : public KWin::Plugin, public KWin::InputEventFilter +{ + Q_OBJECT +public: + enum TriggerType { + Pointer, + TabletPad, + TabletToolButtonType, + TabletDial, + TabletRing, + LastType + }; + Q_ENUM(TriggerType) + using SingleKeybind = QKeySequence; + struct AxisKeybind + { + QKeySequence up; + QKeySequence down; + int threshold = 120; // multiples of 120, or how many "twists" of the dial or ring is needed to emit this event + }; + struct TabletToolButton + { + quint32 button; + }; + struct MouseButton + { + quint32 button; + Qt::KeyboardModifiers modifiers; + }; + struct DisabledButton + { + }; + struct ScrollWheel + { + }; + + explicit ButtonRebindsFilter(); + ~ButtonRebindsFilter() override; + bool pointerButton(KWin::PointerButtonEvent *event) override; + bool tabletToolProximityEvent(KWin::TabletToolProximityEvent *event) override; + bool tabletToolAxisEvent(KWin::TabletToolAxisEvent *event) override; + bool tabletToolTipEvent(KWin::TabletToolTipEvent *event) override; + bool tabletPadButtonEvent(KWin::TabletPadButtonEvent *event) override; + bool tabletToolButtonEvent(KWin::TabletToolButtonEvent *event) override; + bool tabletPadDialEvent(KWin::TabletPadDialEvent *event) override; + bool tabletPadRingEvent(KWin::TabletPadRingEvent *event) override; + +private: + void loadConfig(const KConfigGroup &group); + void insert(TriggerType type, const Trigger &trigger, const QStringList &action); + bool send(TriggerType type, const Trigger &trigger, bool pressed, double delta, std::chrono::microseconds timestamp); + bool sendKeySequence(const QKeySequence &sequence, bool pressed, std::chrono::microseconds time); + bool sendKeyModifiers(const Qt::KeyboardModifiers &modifiers, bool pressed, std::chrono::microseconds time); + bool sendMouseButton(quint32 button, bool pressed, std::chrono::microseconds time); + bool sendMousePosition(QPointF position, std::chrono::microseconds time); + bool sendMouseFrame(); + bool sendTabletToolButton(quint32 button, bool pressed, std::chrono::microseconds time); + bool sendScrollWheel(double v120, std::chrono::microseconds time); + + std::unique_ptr m_inputDevice; + std::array>, LastType> m_actions; + KConfigWatcher::Ptr m_configWatcher; + QPointer m_tabletTool; + QPointF m_cursorPos, m_tabletCursorPos; + qreal m_initialRingPosition = -1; +}; diff --git a/local/recipes/kde/kwin/source/src/plugins/buttonrebinds/main.cpp b/local/recipes/kde/kwin/source/src/plugins/buttonrebinds/main.cpp new file mode 100644 index 0000000000..cdfdce7a34 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/buttonrebinds/main.cpp @@ -0,0 +1,25 @@ +/* + SPDX-FileCopyrightText: 2022 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include +#include + +#include "buttonrebindsfilter.h" + +class KWIN_EXPORT ButtonRebindsFactory : public KWin::PluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID PluginFactory_iid FILE "metadata.json") + Q_INTERFACES(KWin::PluginFactory) + +public: + std::unique_ptr create() const override + { + return std::make_unique(); + } +}; + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/buttonrebinds/metadata.json b/local/recipes/kde/kwin/source/src/plugins/buttonrebinds/metadata.json new file mode 100644 index 0000000000..aa304f4093 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/buttonrebinds/metadata.json @@ -0,0 +1,5 @@ +{ + "KPlugin": { + "EnabledByDefault": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/CMakeLists.txt new file mode 100644 index 0000000000..1d76a66223 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/CMakeLists.txt @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2023 Fushan Wen +# SPDX-License-Identifier: BSD-3-Clause + +kwin_add_builtin_effect(colorblindnesscorrection) + +target_sources(colorblindnesscorrection PRIVATE + main.cpp + colorblindnesscorrection.cpp + colorblindnesscorrection.qrc +) + +kconfig_add_kcfg_files(colorblindnesscorrection + colorblindnesscorrectionconfig.kcfgc +) + +target_link_libraries(colorblindnesscorrection PRIVATE + kwin + KF6::ConfigGui +) diff --git a/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrection.cpp b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrection.cpp new file mode 100644 index 0000000000..e50aac5b60 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrection.cpp @@ -0,0 +1,192 @@ +/* + SPDX-FileCopyrightText: 2023 Fushan Wen + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "colorblindnesscorrection.h" + +#include "effect/effecthandler.h" +#include "opengl/glshader.h" + +#include "colorblindnesscorrectionconfig.h" + +Q_LOGGING_CATEGORY(KWIN_COLORBLINDNESS_CORRECTION, "kwin_effect_colorblindnesscorrection", QtWarningMsg) + +static void ensureResources() +{ + // Must initialize resources manually because the effect is a static lib. + Q_INIT_RESOURCE(colorblindnesscorrection); +} + +namespace KWin +{ + +ColorBlindnessCorrectionEffect::ColorBlindnessCorrectionEffect() + : OffscreenEffect() +{ + ColorBlindnessCorrectionSettings::instance(effects->config()); + m_mode = static_cast(ColorBlindnessCorrectionSettings::mode()); + m_intensity = std::clamp(ColorBlindnessCorrectionSettings::intensity(), 0.0f, 1.0f); + + loadData(); +} + +ColorBlindnessCorrectionEffect::~ColorBlindnessCorrectionEffect() +{ +} + +bool ColorBlindnessCorrectionEffect::supported() +{ + return effects->isOpenGLCompositing(); +} + +void ColorBlindnessCorrectionEffect::loadData() +{ + ensureResources(); + + QMatrix3x3 defectMatrix; + QString fragPath = QStringLiteral(":/effects/colorblindnesscorrection/shaders/colorblindnesscorrection.frag"); + + switch (m_mode) { + case Deuteranopia: + defectMatrix(0, 0) = 1.0; + defectMatrix(1, 0) = 0.494207; + defectMatrix(2, 0) = 0.0; + defectMatrix(0, 1) = 0.0; + defectMatrix(1, 1) = 0.0; + defectMatrix(2, 1) = 0.0; + defectMatrix(0, 2) = 0.0; + defectMatrix(1, 2) = 1.24827; + defectMatrix(2, 2) = 1.0; + break; + case Tritanopia: + defectMatrix(0, 0) = 1.0; + defectMatrix(1, 0) = 0.0; + defectMatrix(2, 0) = -0.395913; + defectMatrix(0, 1) = 0.0; + defectMatrix(1, 1) = 1.0; + defectMatrix(2, 1) = 0.801109; + defectMatrix(0, 2) = 0.0; + defectMatrix(1, 2) = 0.0; + defectMatrix(2, 2) = 0.0; + break; + case Monochrome: + fragPath = QStringLiteral(":/effects/colorblindnesscorrection/shaders/monochrome.frag"); + break; + case Protanopia: // Most common, use it as fallback + default: + defectMatrix(0, 0) = 0.0; + defectMatrix(1, 0) = 0.0; + defectMatrix(2, 0) = 0.0; + defectMatrix(0, 1) = 2.02344; + defectMatrix(1, 1) = 1.0; + defectMatrix(2, 1) = 0.0; + defectMatrix(0, 2) = -2.52581; + defectMatrix(1, 2) = 0.0; + defectMatrix(2, 2) = 1.0; + break; + } + + m_shader = ShaderManager::instance()->generateShaderFromFile(ShaderTrait::MapTexture, QString(), fragPath); + + if (!m_shader->isValid()) { + qCCritical(KWIN_COLORBLINDNESS_CORRECTION) << "Failed to load the shader!"; + return; + } + + ShaderBinder binder{m_shader.get()}; + + if (m_mode != Monochrome) { + // These uniforms aren't present in the monochrome shader, so we shouldn't set them there. + m_shader->setUniform("intensity", m_intensity); + m_shader->setUniform("defectMatrix", defectMatrix); + } + + for (const auto windows = effects->stackingOrder(); EffectWindow * w : windows) { + correctColor(w); + } + effects->addRepaintFull(); + + connect(effects, &EffectsHandler::windowDeleted, this, &ColorBlindnessCorrectionEffect::slotWindowDeleted); + connect(effects, &EffectsHandler::windowAdded, this, &ColorBlindnessCorrectionEffect::correctColor); +} + +void ColorBlindnessCorrectionEffect::drawWindow(const RenderTarget &renderTarget, + const RenderViewport &viewport, + EffectWindow *w, + int mask, + const Region &logicalRegion, + WindowPaintData &data) +{ + // The 'saturation' uniform is exported in the base function, so we have to modify its value beforehand. + if (m_mode == Monochrome) { + data.setSaturation(1.0f - m_intensity); + } + + OffscreenEffect::drawWindow(renderTarget, viewport, w, mask, logicalRegion, data); +} + +void ColorBlindnessCorrectionEffect::correctColor(KWin::EffectWindow *w) +{ + if (m_windows.contains(w)) { + return; + } + + redirect(w); + setShader(w, m_shader.get()); + m_windows.insert(w); +} + +void ColorBlindnessCorrectionEffect::slotWindowDeleted(EffectWindow *w) +{ + if (auto it = m_windows.find(w); it != m_windows.end()) { + m_windows.erase(it); + } +} + +bool ColorBlindnessCorrectionEffect::isActive() const +{ + return !m_windows.empty(); +} + +bool ColorBlindnessCorrectionEffect::provides(Feature f) +{ + return f == Contrast; +} + +void ColorBlindnessCorrectionEffect::reconfigure(ReconfigureFlags flags) +{ + if (flags != Effect::ReconfigureAll) { + return; + } + + ColorBlindnessCorrectionSettings::self()->read(); + const auto newMode = static_cast(ColorBlindnessCorrectionSettings::mode()); + const auto newIntensity = std::clamp(ColorBlindnessCorrectionSettings::intensity(), 0.0f, 1.0f); + if (m_mode == newMode && qFuzzyCompare(m_intensity, newIntensity)) { + return; + } + + m_mode = newMode; + m_intensity = newIntensity; + + disconnect(effects, &EffectsHandler::windowDeleted, this, &ColorBlindnessCorrectionEffect::slotWindowDeleted); + disconnect(effects, &EffectsHandler::windowAdded, this, &ColorBlindnessCorrectionEffect::correctColor); + + for (EffectWindow *w : m_windows) { + unredirect(w); + } + m_windows.clear(); + + loadData(); +} + +int ColorBlindnessCorrectionEffect::requestedEffectChainPosition() const +{ + return 98; +} + +} // namespace + +#include "moc_colorblindnesscorrection.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrection.h b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrection.h new file mode 100644 index 0000000000..cbc41972f4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrection.h @@ -0,0 +1,64 @@ +/* + SPDX-FileCopyrightText: 2023 Fushan Wen + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "effect/offscreeneffect.h" +#include "opengl/glshadermanager.h" + +namespace KWin +{ + +/** + * The color filter supports protanopia, deuteranopia, tritanopia, and monochrome. + */ +class ColorBlindnessCorrectionEffect : public OffscreenEffect +{ + Q_OBJECT + +public: + enum Mode { + Protanopia = 0, // m_windows; + std::unique_ptr m_shader; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrection.qrc b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrection.qrc new file mode 100644 index 0000000000..3a71671ea8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrection.qrc @@ -0,0 +1,8 @@ + + + shaders/colorblindnesscorrection.frag + shaders/colorblindnesscorrection_core.frag + shaders/monochrome.frag + shaders/monochrome_core.frag + + diff --git a/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrectionconfig.kcfg b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrectionconfig.kcfg new file mode 100644 index 0000000000..abc776ed9a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrectionconfig.kcfg @@ -0,0 +1,19 @@ + + + + + + + 0 + + + 1.0 + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrectionconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrectionconfig.kcfgc new file mode 100644 index 0000000000..1487d936ae --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/colorblindnesscorrectionconfig.kcfgc @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2023 Fushan Wen +# SPDX-License-Identifier: GPL-2.0-or-later + +File=colorblindnesscorrectionconfig.kcfg +ClassName=ColorBlindnessCorrectionSettings +NameSpace=KWin +Singleton=true \ No newline at end of file diff --git a/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/main.cpp b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/main.cpp new file mode 100644 index 0000000000..216f2a005a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/main.cpp @@ -0,0 +1,16 @@ +/* + SPDX-FileCopyrightText: 2023 Fushan Wen + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "colorblindnesscorrection.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(ColorBlindnessCorrectionEffect, "metadata.json.stripped", return ColorBlindnessCorrectionEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/metadata.json b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/metadata.json new file mode 100644 index 0000000000..2992c2aa56 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/metadata.json @@ -0,0 +1,95 @@ +{ + "KPlugin": { + "Category": "Accessibility", + "Description": "Enhances color perception for color blindness", + "Description[ar]": "تحسن استقبال الألوان للأشخاص ذوي عمى الألوان", + "Description[be]": "Паляпшэнне ўспрымання колераў пры праблемах з успрыманнем колераў", + "Description[bg]": "Подобряване на цветното възприемане при цветна слепота", + "Description[ca@valencia]": "Millora la percepció del color dels daltònics", + "Description[ca]": "Millora la percepció del color dels daltònics", + "Description[da]": "Forbedrer farveopfattelse for farveblindhed", + "Description[de]": "Verbessert die Farbwahrnehmung bei Farbenblindheit", + "Description[en_GB]": "Enhances colour perception for colour blindness", + "Description[eo]": "Plibonigas kolorpercepton por kolora blindeco", + "Description[es]": "Mejora la percepción del color para el daltonismo", + "Description[eu]": "Kolore itsutasunerako koloreen zorroztasuna areagotzen du", + "Description[fi]": "Tehostaa värien erottuvuutta värisokeille", + "Description[fr]": "Améliore la perception des couleurs pour le daltonisme", + "Description[gl]": "Mellora a percepción de cor para xente con daltonismo.", + "Description[he]": "משפר את תפיסת הצבע לעיוורון צבעים", + "Description[hu]": "Színérzékelés fokozása színtévesztőknek", + "Description[ia]": "Intensifica perceprion de color per cecitate de color", + "Description[id]": "Meningkatkan persepsi warna untuk buta warna", + "Description[is]": "Bætir litaskynjun fyrir litblinda", + "Description[it]": "Migliora la percezione del colore per il daltonismo", + "Description[ja]": "色彩を補正して色覚異常を持つユーザの使用体験を改善します", + "Description[ka]": "ფერების გაფართოება ფერებით ბრმა პიროვნებებისთვის", + "Description[ko]": "색각 이상 사용자를 위한 색 분별력 향상", + "Description[lt]": "Pagerina spalvinį aklumą turinčių žmonių spalvų nuovoką", + "Description[lv]": "Uzlabo krāsu uztveri krāsu akluma gadījumā", + "Description[nb]": "Forbedrer fargeoppfatning for fargeblinde", + "Description[nl]": "Perceptie van kleur verbeteren voor kleurenblindheid", + "Description[nn]": "Forbetra fargeattgjeving for fargeblinde", + "Description[pl]": "Polepsza rozpoznawanie barw dla ślepych na barwy", + "Description[pt_BR]": "Melhora a percepção de cores para daltonismo", + "Description[ro]": "Îmbunătățește percepția culorilor pentru daltonism", + "Description[ru]": "Улучшение восприятия цветов при цветовой слепоте", + "Description[sa]": "वर्णअन्धतायाः कृते वर्णबोधं वर्धयति", + "Description[sk]": "Zlepšuje vnímanie farieb pri farbosleposti", + "Description[sl]": "Izboljša dojemanje barv za barvno slepe", + "Description[sv]": "Förbättrar färguppfattningen för färgblindhet", + "Description[ta]": "நிறக்குருடு உடையவர்களுக்கு உதவும் வகையில் நிறங்களை மாற்றும்", + "Description[tr]": "Renk körlüğü aralığında renk algısını iyileştirir", + "Description[uk]": "Удосконалює сприйняття кольорів для випадків дальтонізму", + "Description[zh_CN]": "增强色盲视觉的颜色感知", + "Description[zh_TW]": "為色盲現象改善顏色辨識", + "EnabledByDefault": false, + "License": "GPL-2.0+", + "Name": "Colorblindness Correction", + "Name[ar]": "تصحيح عمى الألوان", + "Name[be]": "Карэкцыя дальтанізму", + "Name[bg]": "Корекция на цветна слепота", + "Name[ca@valencia]": "Correcció del daltonisme", + "Name[ca]": "Correcció del daltonisme", + "Name[cs]": "Oprava barvosleposti", + "Name[da]": "Korrektion for farveblindhed", + "Name[de]": "Korrektur bei Farbenblindheit", + "Name[en_GB]": "Colourblindness Correction", + "Name[eo]": "Korekto de Kolorblindeco", + "Name[es]": "Corrección para daltonismo", + "Name[eu]": "Kolore itsutasunerako zuzenketa", + "Name[fi]": "Värisokeuskorjaus", + "Name[fr]": "Correction du daltonisme", + "Name[gl]": "Corrección de daltonismo", + "Name[he]": "תיקון עיוורון צבעים", + "Name[hu]": "Színtévesztés-korrekció", + "Name[ia]": "Correction de cecitate de color", + "Name[id]": "Koreksi Buta Warna", + "Name[is]": "Leiðrétting fyrir litblindu", + "Name[it]": "Correzione per il daltonismo", + "Name[ja]": "色覚異常補正", + "Name[ka]": "ფერების სიბრმავის შესწორება", + "Name[ko]": "색각 이상 보정", + "Name[lt]": "Spalvinio aklumo korekcija", + "Name[lv]": "Krāsu akluma korekcija", + "Name[nb]": "Korrigering for fargeblindhet", + "Name[nl]": "Correctie voor kleurenblindheid", + "Name[nn]": "Korrigering for fargeblindskap", + "Name[pl]": "Poprawki dla ślepych na barwy", + "Name[pt_BR]": "Correção de daltonismo", + "Name[ro]": "Corecție pentru daltonism", + "Name[ru]": "Корректировка при цветовой слепоте", + "Name[sa]": "वर्णअन्धता सुधार", + "Name[sk]": "Korekcia farbosleposti", + "Name[sl]": "Popravek za barvno slepoto", + "Name[sv]": "Korrigering av färgblindhet", + "Name[ta]": "நிறக்குருடு திருத்தம்", + "Name[tr]": "Renk Körlüğü Düzeltmesi", + "Name[uk]": "Виправлення для дальтонізму", + "Name[zh_CN]": "色盲校正", + "Name[zh_TW]": "色盲修正" + }, + "org.kde.kwin.effect": { + "internal": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/metadata.json.license b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/metadata.json.license new file mode 100644 index 0000000000..66f78fc957 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/metadata.json.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: None +# SPDX-License-Identifier: CC0-1.0 diff --git a/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/shaders/README b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/shaders/README new file mode 100644 index 0000000000..19833ecc41 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/shaders/README @@ -0,0 +1,8 @@ +ColorBlindness correction shader with adjustable intensity. Can correct for: +* Protanopia (Greatly reduced reds) +* Deuteranopia (Greatly reduced greens) +* Tritanopia (Greatly reduced blues) + +The correction algorithm is taken from http://www.daltonize.org/search/label/Daltonize + +This shader is released under the CC0 license. Feel free to use, improve and change this shader and consider sharing the modified result. diff --git a/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/shaders/colorblindnesscorrection.frag b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/shaders/colorblindnesscorrection.frag new file mode 100644 index 0000000000..292247f572 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/shaders/colorblindnesscorrection.frag @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: None +// SPDX-License-Identifier: CC0-1.0 +#include "saturation.glsl" +#include "colormanagement.glsl" + +uniform sampler2D sampler; +uniform vec4 modulation; +uniform float intensity; +varying vec2 texcoord0; +uniform mat3 defectMatrix; + +const mat3 srgbToLMS = mat3( + 17.8824, 3.45565, 0.0299566, + 43.5161, 27.1554, 0.184309, + 4.11935, 3.86714, 1.46709 +); +const mat3 errorMat = mat3( + 0.0809444479, -0.0102485335, -0.000365296938, + -0.130504409, 0.0540193266, -0.00412161469, + 0.116721066, -0.113614708, 0.693511405 +); + +void main() +{ + vec4 tex = texture2D(sampler, texcoord0); + tex = sourceEncodingToNitsInDestinationColorspace(tex); + tex = adjustSaturation(tex); + + vec3 LMS = srgbToLMS * tex.rgb; + vec3 lms = defectMatrix * LMS; + vec3 error = errorMat * lms; + + vec3 diff = (tex.rgb - error) * vec3(intensity); + vec3 correction = vec3(0.0, + (diff.r * 0.7) + (diff.g * 1.0), + (diff.r * 0.7) + (diff.b * 1.0)); + + tex = (tex + vec4(correction, 0.0)) * modulation; + gl_FragColor = nitsToDestinationEncoding(tex); +} diff --git a/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/shaders/colorblindnesscorrection_core.frag b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/shaders/colorblindnesscorrection_core.frag new file mode 100644 index 0000000000..b6a6f7aa91 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/colorblindnesscorrection/shaders/colorblindnesscorrection_core.frag @@ -0,0 +1,42 @@ +#version 140 +// SPDX-FileCopyrightText: None +// SPDX-License-Identifier: CC0-1.0 +#include "saturation.glsl" +#include "colormanagement.glsl" + +uniform sampler2D sampler; +uniform vec4 modulation; +uniform float intensity; +in vec2 texcoord0; +out vec4 fragColor; +uniform mat3 defectMatrix; + +const mat3 srgbToLMS = mat3( + 17.8824, 3.45565, 0.0299566, + 43.5161, 27.1554, 0.184309, + 4.11935, 3.86714, 1.46709 +); +const mat3 errorMat = mat3( + 0.0809444479, -0.0102485335, -0.000365296938, + -0.130504409, 0.0540193266, -0.00412161469, + 0.116721066, -0.113614708, 0.693511405 +); + +void main() +{ + vec4 tex = texture2D(sampler, texcoord0); + tex = sourceEncodingToNitsInDestinationColorspace(tex); + tex = adjustSaturation(tex); + + vec3 LMS = srgbToLMS * tex.rgb; + vec3 lms = defectMatrix * LMS; + vec3 error = errorMat * lms; + + vec3 diff = (tex.rgb - error) * vec3(intensity); + vec3 correction = vec3(0.0, + (diff.r * 0.7) + (diff.g * 1.0), + (diff.r * 0.7) + (diff.b * 1.0)); + + tex = (tex + vec4(correction, 0.0)) * modulation; + gl_FragColor = nitsToDestinationEncoding(tex); +} diff --git a/local/recipes/kde/kwin/source/src/plugins/colorpicker/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/colorpicker/CMakeLists.txt new file mode 100644 index 0000000000..2f7d609633 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/colorpicker/CMakeLists.txt @@ -0,0 +1,17 @@ +####################################### +# Effect + +set(colorpicker_SOURCES + colorpicker.cpp + colorpickerlayer.cpp + main.cpp +) + +kwin_add_builtin_effect(colorpicker ${colorpicker_SOURCES}) +target_link_libraries(colorpicker PRIVATE + kwin + + KF6::I18n + + Qt::DBus +) diff --git a/local/recipes/kde/kwin/source/src/plugins/colorpicker/colorpicker.cpp b/local/recipes/kde/kwin/source/src/plugins/colorpicker/colorpicker.cpp new file mode 100644 index 0000000000..9054b66a5c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/colorpicker/colorpicker.cpp @@ -0,0 +1,176 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "colorpicker.h" +#include "colorpickerlayer.h" + +#include "compositor.h" +#include "core/output.h" +#include "core/pixelgrid.h" +#include "core/rendertarget.h" +#include "core/renderviewport.h" +#include "effect/effect.h" +#include "effect/effecthandler.h" +#include "opengl/eglbackend.h" +#include "opengl/glplatform.h" +#include "opengl/glutils.h" +#include "scene/item.h" +#include "scene/itemrenderer.h" +#include "scene/windowitem.h" +#include "scene/workspacescene.h" +#include "window.h" +#include "workspace.h" + +#include +#include +#include + +Q_DECLARE_METATYPE(QColor) + +QDBusArgument &operator<<(QDBusArgument &argument, const QColor &color) +{ + argument.beginStructure(); + argument << color.rgba(); + argument.endStructure(); + return argument; +} + +const QDBusArgument &operator>>(const QDBusArgument &argument, QColor &color) +{ + argument.beginStructure(); + QRgb rgba; + argument >> rgba; + argument.endStructure(); + color = QColor::fromRgba(rgba); + return argument; +} + +namespace KWin +{ + +bool ColorPickerEffect::supported() +{ + return effects->isOpenGLCompositing(); +} + +ColorPickerEffect::ColorPickerEffect() +{ + qDBusRegisterMetaType(); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/ColorPicker"), this, QDBusConnection::ExportScriptableContents); +} + +ColorPickerEffect::~ColorPickerEffect() +{ + setPicking(false); +} + +QColor ColorPickerEffect::pick() +{ + if (!calledFromDBus()) { + return QColor(); + } + if (m_picking) { + sendErrorReply(QDBusError::Failed, "Color picking is already in progress"); + return QColor(); + } + setPicking(true); + m_replyMessage = message(); + setDelayedReply(true); + showInfoMessage(); + effects->startInteractivePositionSelection( + [this](const QPointF &p) { + hideInfoMessage(); + if (p == QPointF(-1, -1)) { + // error condition + QDBusConnection::sessionBus().send(m_replyMessage.createErrorReply(QStringLiteral("org.kde.kwin.ColorPicker.Error.Cancelled"), "Color picking got cancelled")); + setPicking(false); + } else { + const auto turnOffPicking = qScopeGuard([this] { + setPicking(false); + }); + + const auto eglBackend = dynamic_cast(Compositor::self()->backend()); + if (!eglBackend) { + return; + } + const auto context = eglBackend->openglContext(); + if (!context || !context->makeCurrent()) { + return; + } + + const auto offscreenTexture = GLTexture::allocate(GL_RGB8, QSize(1, 1)); + if (!offscreenTexture) { + return; + } + const auto target = std::make_unique(offscreenTexture.get()); + if (!target->valid()) { + return; + } + + auto screen = effects->screenAt(p.toPoint()); + if (!screen) { + return; + } + + ColorPickerLayer layer(screen->backendOutput(), target.get()); + if (!layer.preparePresentationTest()) { + return; + } + const auto beginInfo = layer.beginFrame(); + if (!beginInfo) { + return; + } + SceneView sceneView(Compositor::self()->scene(), screen, nullptr, &layer); + auto cursorView = std::make_unique(&sceneView, Compositor::self()->scene()->cursorItem(), workspace()->outputs().front(), nullptr, nullptr); + cursorView->setExclusive(true); + const Rect pixelDamage = QRect(QPoint(), QSize(1, 1)); + sceneView.setViewport(QRectF(p, QSizeF(1, 1))); + sceneView.prePaint(); + sceneView.paint(beginInfo->renderTarget, QPoint(), pixelDamage); + sceneView.postPaint(); + if (!layer.endFrame(pixelDamage, pixelDamage, nullptr)) { + return; + } + + GLFramebuffer::pushFramebuffer(target.get()); + QImage snapshot = QImage(offscreenTexture->size(), QImage::Format_RGBA8888); + context->glReadnPixels(0, 0, snapshot.width(), snapshot.height(), GL_RGBA, GL_UNSIGNED_BYTE, snapshot.sizeInBytes(), static_cast(snapshot.bits())); + GLFramebuffer::popFramebuffer(); + + QDBusConnection::sessionBus().send(m_replyMessage.createReply(snapshot.pixelColor(0, 0))); + } + }); + return QColor(); +} + +void ColorPickerEffect::showInfoMessage() +{ + effects->showOnScreenMessage(i18n("Select a position for color picking with left click or enter.\nEscape or right click to cancel."), QStringLiteral("color-picker")); +} + +void ColorPickerEffect::hideInfoMessage() +{ + effects->hideOnScreenMessage(); +} + +void ColorPickerEffect::setPicking(bool picking) +{ + if (m_picking != picking) { + m_picking = picking; + Q_EMIT effects->colorPickerActiveChanged(); + } +} + +bool ColorPickerEffect::isActive() const +{ + return m_picking && !effects->isScreenLocked(); +} + +} // namespace + +#include "moc_colorpicker.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/colorpicker/colorpicker.h b/local/recipes/kde/kwin/source/src/plugins/colorpicker/colorpicker.h new file mode 100644 index 0000000000..c781076329 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/colorpicker/colorpicker.h @@ -0,0 +1,49 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "effect/effect.h" +#include +#include +#include +#include +#include + +namespace KWin +{ + +class ColorPickerEffect : public Effect, protected QDBusContext +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.ColorPicker") +public: + ColorPickerEffect(); + ~ColorPickerEffect() override; + bool isActive() const override; + + int requestedEffectChainPosition() const override + { + return 0; + } + + static bool supported(); + +public Q_SLOTS: + Q_SCRIPTABLE QColor pick(); + +private: + void showInfoMessage(); + void hideInfoMessage(); + void setPicking(bool picking); + + QDBusMessage m_replyMessage; + bool m_picking = false; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/colorpicker/main.cpp b/local/recipes/kde/kwin/source/src/plugins/colorpicker/main.cpp new file mode 100644 index 0000000000..3f7d7bd20f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/colorpicker/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "colorpicker.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(ColorPickerEffect, + "metadata.json.stripped", + return ColorPickerEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/colorpicker/metadata.json b/local/recipes/kde/kwin/source/src/plugins/colorpicker/metadata.json new file mode 100644 index 0000000000..69cda9229b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/colorpicker/metadata.json @@ -0,0 +1,104 @@ +{ + "KPlugin": { + "Category": "Accessibility", + "Description": "Supports picking a color", + "Description[ar]": "يوفر إمكانية انتقاء لون", + "Description[az]": "Rəng seçimi dəstəklənir", + "Description[be]": "Падтрымка выбару колеру", + "Description[bg]": "Поддържа избор на цвят", + "Description[ca@valencia]": "Permet seleccionar un color", + "Description[ca]": "Permet seleccionar un color", + "Description[cs]": "Podporuje výběr barev", + "Description[da]": "Understøtter valg af farve", + "Description[de]": "Unterstützt das Auswählen einer Farbe", + "Description[en_GB]": "Supports picking a colour", + "Description[eo]": "Subtenas elekton de koloro", + "Description[es]": "Permite seleccionar un color", + "Description[et]": "Võimaldab värvi valida", + "Description[eu]": "Kolore bat hartzea onartzen du", + "Description[fi]": "Antaa valita ruudulta värin", + "Description[fr]": "Permet de sélectionner une couleur", + "Description[gl]": "Permite seleccionar unha cor.", + "Description[he]": "תומך בבחירת צבע", + "Description[hu]": "Támogatja a színválasztást", + "Description[ia]": "Supporta seliger un color", + "Description[id]": "Mendukung penukilan warna", + "Description[is]": "Styður val á lit", + "Description[it]": "Supporta la selezione di un colore", + "Description[ja]": "カラーピッカーをサポートします", + "Description[ka]": "აქვს ფერის არჩევის მხარდაჭერა", + "Description[ko]": "색상 선택 지원", + "Description[lt]": "Palaiko spalvos parinkimą", + "Description[lv]": "Atbalsta krāsu atlasīšanu", + "Description[nb]": "Støtter valg av farge", + "Description[nl]": "Ondersteunt een kleur kiezen", + "Description[nn]": "Støttar veljing av farge", + "Description[pl]": "Obsługuje wybieranie koloru", + "Description[pt]": "Suporta a selecção de uma cor", + "Description[pt_BR]": "Suporta a escolha de uma cor", + "Description[ro]": "Permite alegerea unei culori", + "Description[ru]": "Выбор цвета на экране", + "Description[sa]": "एकं वर्णं चिन्वितुं समर्थयति", + "Description[sk]": "Podporuje výber farby", + "Description[sl]": "Podpira izbor barve", + "Description[sv]": "Stöder att hämta en färg", + "Description[ta]": "திரையிலுள்ள ஓர் நிறத்தை தேர்ந்தெடுக்க உதவும்", + "Description[tr]": "Bir renk seçmeyi destekler", + "Description[uk]": "Підтримка взяття кольору з екрана", + "Description[vi]": "Hỗ trợ nhặt màu", + "Description[zh_CN]": "拾色器专用辅助特效", + "Description[zh_TW]": "支援挑選顏色", + "EnabledByDefault": true, + "License": "GPL", + "Name": "Color Picker", + "Name[ar]": "منتق الألوان", + "Name[az]": "Rəng seçimi", + "Name[be]": "Сродак выбару колеру", + "Name[bg]": "Избиране на цвят", + "Name[ca@valencia]": "Selector de color", + "Name[ca]": "Selector de color", + "Name[cs]": "Kapátko", + "Name[da]": "Farvevælger", + "Name[de]": "Farbauswahl", + "Name[en_GB]": "Colour Picker", + "Name[eo]": "Kolor-Elektilo", + "Name[es]": "Selector de color", + "Name[et]": "Värvivalija", + "Name[eu]": "Kolore hautatzailea", + "Name[fi]": "Värivalinta", + "Name[fr]": "Sélecteur de couleurs", + "Name[gl]": "Selector de cor", + "Name[he]": "בורר צבעים", + "Name[hu]": "Színválasztó", + "Name[ia]": "Selectionator de color", + "Name[id]": "Pemilih Warna", + "Name[is]": "Litaplokkari", + "Name[it]": "Selettore del colore", + "Name[ja]": "カラーピッカー", + "Name[ka]": "ფერის ამრჩევი", + "Name[ko]": "색상 선택기", + "Name[lt]": "Spalvos parinkiklis", + "Name[lv]": "Krāsu atlasītājs", + "Name[nb]": "Fargevelger", + "Name[nl]": "Kleurenkiezer", + "Name[nn]": "Fargeveljar", + "Name[pl]": "Wybierak barwy", + "Name[pt]": "Selector de Cores", + "Name[pt_BR]": "Seletor de cores", + "Name[ro]": "Selector de culori", + "Name[ru]": "Выбор цвета", + "Name[sa]": "रङ्ग पिकर", + "Name[sk]": "Výber farby", + "Name[sl]": "Izbirnik barv", + "Name[sv]": "Färghämtare", + "Name[ta]": "வண்ண எடுப்பான்", + "Name[tr]": "Renk Seçicisi", + "Name[uk]": "Піпетка", + "Name[vi]": "Trình nhặt màu", + "Name[zh_CN]": "拾色器", + "Name[zh_TW]": "取色工具" + }, + "org.kde.kwin.effect": { + "internal": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/desktopchangeosd/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/desktopchangeosd/CMakeLists.txt new file mode 100644 index 0000000000..32a6233649 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/desktopchangeosd/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_script(desktopchangeosd package) diff --git a/local/recipes/kde/kwin/source/src/plugins/desktopchangeosd/package/contents/ui/main.qml b/local/recipes/kde/kwin/source/src/plugins/desktopchangeosd/package/contents/ui/main.qml new file mode 100644 index 0000000000..41195fdfa5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/desktopchangeosd/package/contents/ui/main.qml @@ -0,0 +1,24 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick +import org.kde.kwin + +Loader { + id: mainItemLoader + + Connections { + target: Workspace + function onCurrentDesktopChanged(previous) { + if (!mainItemLoader.item) { + mainItemLoader.source = "osd.qml"; + } + mainItemLoader.item.show(previous); + } + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/desktopchangeosd/package/contents/ui/osd.qml b/local/recipes/kde/kwin/source/src/plugins/desktopchangeosd/package/contents/ui/osd.qml new file mode 100644 index 0000000000..6a2f2f37ed --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/desktopchangeosd/package/contents/ui/osd.qml @@ -0,0 +1,286 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012, 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +import QtQuick +import QtQuick.Window +import org.kde.plasma.core as PlasmaCore +import org.kde.kirigami as Kirigami +import org.kde.ksvg as KSvg +import org.kde.kwin + +PlasmaCore.Window { + id: dialog + visible: false + flags: Qt.X11BypassWindowManagerHint | Qt.FramelessWindowHint + + width: mainItem.implicitWidth + leftPadding + rightPadding + height: mainItem.implicitHeight + topPadding + bottomPadding + + mainItem: Item { + function loadConfig() { + dialogItem.animationDuration = KWin.readConfig("PopupHideDelay", 1000); + if (KWin.readConfig("TextOnly", "false") == "true") { + dialogItem.showGrid = false; + } else { + dialogItem.showGrid = true; + } + } + + id: dialogItem + property int screenWidth: 0 + property int screenHeight: 0 + property int currentIndex: 0 + property int previousIndex: 0 + property int animationDuration: 1000 + property bool showGrid: true + + implicitWidth: dialogItem.showGrid ? view.itemWidth * view.columns : Math.ceil(textElement.implicitWidth) + implicitHeight: dialogItem.showGrid ? view.itemHeight * view.rows + textElement.implicitHeight : textElement.implicitHeight + + Kirigami.Heading { + id: textElement + anchors.top: dialogItem.showGrid ? parent.top : undefined + anchors.left: parent.left + anchors.right: parent.right + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.NoWrap + elide: Text.ElideRight + text: Workspace.currentDesktop.name + } + + Grid { + id: view + columns: 1 + rows: 1 + property int itemWidth: dialogItem.screenWidth * Math.min(0.8/columns, 0.1) + property int itemHeight: Math.min(itemWidth * (dialogItem.screenHeight / dialogItem.screenWidth), dialogItem.screenHeight * Math.min(0.8/rows, 0.1)) + anchors { + top: textElement.bottom + left: parent.left + right: parent.right + bottom: parent.bottom + } + visible: dialogItem.showGrid + Repeater { + id: repeater + model: Workspace.desktops + Item { + width: view.itemWidth + height: view.itemHeight + KSvg.FrameSvgItem { + anchors.fill: parent + imagePath: "widgets/pager" + prefix: "normal" + } + KSvg.FrameSvgItem { + id: activeElement + anchors.fill: parent + imagePath: "widgets/pager" + prefix: "active" + opacity: 0.0 + Behavior on opacity { + NumberAnimation { duration: dialogItem.animationDuration/2 } + } + } + Item { + id: arrowsContainer + anchors.fill: parent + Kirigami.Icon { + anchors.fill: parent + source: "go-up" + visible: false + } + Kirigami.Icon { + anchors.fill: parent + source: "go-down" + visible: { + if (dialogItem.currentIndex <= index) { + // don't show for target desktop + return false; + } + if (index < dialogItem.previousIndex) { + return false; + } + if (dialogItem.currentIndex < dialogItem.previousIndex) { + // we only go down if the new desktop is higher + return false; + } + if (Math.floor(dialogItem.currentIndex/view.columns) == Math.floor(index/view.columns)) { + // don't show icons in same row as target desktop + return false; + } + if (dialogItem.previousIndex % view.columns == index % view.columns) { + // show arrows for icons in same column as the previous desktop + return true; + } + return false; + } + } + Kirigami.Icon { + anchors.fill: parent + source: "go-up" + visible: { + if (dialogItem.currentIndex >= index) { + // don't show for target desktop + return false; + } + if (index > dialogItem.previousIndex) { + return false; + } + if (dialogItem.currentIndex > dialogItem.previousIndex) { + // we only go down if the new desktop is higher + return false; + } + if (Math.floor(dialogItem.currentIndex/view.columns) == Math.floor(index/view.columns)) { + // don't show icons in same row as target desktop + return false; + } + if (dialogItem.previousIndex % view.columns == index % view.columns) { + // show arrows for icons in same column as the previous desktop + return true; + } + return false; + } + } + Kirigami.Icon { + anchors.fill: parent + source: "go-next" + visible: { + if (dialogItem.currentIndex <= index) { + // we don't show for desktops not on the path + return false; + } + if (index < dialogItem.previousIndex) { + // we might have to show this icon in case we go up and to the right + if (Math.floor(dialogItem.currentIndex/view.columns) == Math.floor(index/view.columns)) { + // can only happen in same row + if (index % view.columns >= dialogItem.previousIndex % view.columns) { + // but only for items in the same column or after of the previous desktop + return true; + } + } + return false; + } + if (dialogItem.currentIndex < dialogItem.previousIndex) { + // we only go right if the new desktop is higher + return false; + } + if (Math.floor(dialogItem.currentIndex/view.columns) == Math.floor(index/view.columns)) { + // show icons in same row as target desktop + if (index % view.columns < dialogItem.previousIndex % view.columns) { + // but only for items in the same column or after of the previous desktop + return false; + } + return true; + } + return false; + } + } + Kirigami.Icon { + anchors.fill: parent + source: "go-previous" + visible: { + if (dialogItem.currentIndex >= index) { + // we don't show for desktops not on the path + return false; + } + if (index > dialogItem.previousIndex) { + // we might have to show this icon in case we go down and to the left + if (Math.floor(dialogItem.currentIndex/view.columns) == Math.floor(index/view.columns)) { + // can only happen in same row + if (index % view.columns <= dialogItem.previousIndex % view.columns) { + // but only for items in the same column or before the previous desktop + return true; + } + } + return false; + } + if (dialogItem.currentIndex > dialogItem.previousIndex) { + // we only go left if the new desktop is lower + return false; + } + if (Math.floor(dialogItem.currentIndex/view.columns) == Math.floor(index/view.columns)) { + // show icons in same row as target desktop + if (index % view.columns > dialogItem.previousIndex % view.columns) { + // but only for items in the same column or before of the previous desktop + return false; + } + return true; + } + return false; + } + } + } + states: [ + State { + name: "NORMAL" + when: index != dialogItem.currentIndex + PropertyChanges { + target: activeElement + opacity: 0.0 + } + }, + State { + name: "SELECTED" + when: index == dialogItem.currentIndex + PropertyChanges { + target: activeElement + opacity: 1.0 + } + } + ] + Component.onCompleted: { + view.state = (index == dialogItem.currentIndex) ? "SELECTED" : "NORMAL" + } + } + } + } + + Timer { + id: timer + repeat: false + interval: dialogItem.animationDuration + onTriggered: dialog.visible = false + } + + Connections { + target: Options + function onConfigChanged() { + dialogItem.loadConfig() + } + } + Component.onCompleted: { + view.columns = Workspace.desktopGridWidth; + view.rows = Workspace.desktopGridHeight; + dialogItem.loadConfig(); + } + } + + function show(previous) { + if (Workspace.isEffectActive("overview")) { + return; + } + dialogItem.previousIndex = Workspace.desktops.indexOf(previous); + dialogItem.currentIndex = Workspace.desktops.indexOf(Workspace.currentDesktop); + // screen geometry might have changed + var screen = Workspace.clientArea(KWin.FullScreenArea, Workspace.activeScreen, Workspace.currentDesktop); + dialogItem.screenWidth = screen.width; + dialogItem.screenHeight = screen.height; + if (dialogItem.showGrid) { + // non dependable properties might have changed + view.columns = Workspace.desktopGridWidth; + view.rows = Workspace.desktopGridHeight; + } + dialog.visible = true; + // position might have changed + dialog.x = screen.x + screen.width/2 - dialogItem.width/2; + dialog.y = screen.y + screen.height/2 - dialogItem.height/2; + // start the hide timer + timer.restart(); + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/desktopchangeosd/package/metadata.json b/local/recipes/kde/kwin/source/src/plugins/desktopchangeosd/package/metadata.json new file mode 100644 index 0000000000..1849f29cd2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/desktopchangeosd/package/metadata.json @@ -0,0 +1,153 @@ +{ + "KPackageStructure": "KWin/Script", + "KPlugin": { + "Authors": [ + { + "Email": "mgraesslin@kde.org", + "Name": "Martin Gräßlin", + "Name[ar]": "مارتن جراجلين", + "Name[be]": "Martin Gräßlin", + "Name[bg]": "Martin Gräßlin", + "Name[ca@valencia]": "Martin Gräßlin", + "Name[ca]": "Martin Gräßlin", + "Name[cs]": "Martin Gräßlin", + "Name[da]": "Martin Gräßlin", + "Name[de]": "Martin Gräßlin", + "Name[en_GB]": "Martin Gräßlin", + "Name[eo]": "Martin Gräßlin", + "Name[es]": "Martin Gräßlin", + "Name[et]": "Martin Gräßlin", + "Name[eu]": "Martin Gräßlin", + "Name[fi]": "Martin Gräßlin", + "Name[fr]": "Martin Gräßlin", + "Name[ga]": "Martin Gräßlin", + "Name[gl]": "Martin Gräßlin.", + "Name[he]": "מרטין גרייסלין", + "Name[hu]": "Martin Gräßlin", + "Name[ia]": "Martin Gräßlin", + "Name[id]": "Martin Gräßlin", + "Name[is]": "Martin Gräßlin", + "Name[it]": "Martin Gräßlin", + "Name[ja]": "Martin Gräßlin", + "Name[ka]": "მარტინ გრესსლინი", + "Name[ko]": "Martin Gräßlin", + "Name[lt]": "Martin Gräßlin", + "Name[lv]": "Martin Gräßlin", + "Name[nb]": "Martin Gräßlin", + "Name[nl]": "Martin Gräßlin", + "Name[nn]": "Martin Gräßlin", + "Name[pl]": "Martin Gräßlin", + "Name[pt]": "Martin Gräßlin", + "Name[pt_BR]": "Martin Gräßlin", + "Name[ro]": "Martin Gräßlin", + "Name[ru]": "Martin Gräßlin", + "Name[sa]": "मार्टिन् ग्रास्लिन्", + "Name[sk]": "Martin Gräßlin", + "Name[sl]": "Martin Gräßlin", + "Name[sv]": "Martin Gräßlin", + "Name[ta]": "மார்ட்டின் கிராஸ்லின்", + "Name[tr]": "Martin Gräßlin", + "Name[uk]": "Martin Gräßlin", + "Name[vi]": "Martin Gräßlin", + "Name[zh_CN]": "Martin Gräßlin", + "Name[zh_TW]": "Martin Gräßlin" + } + ], + "Description": "An on screen display indicating the desktop change", + "Description[ar]": "عرض تنبيه على الشاشة يشير إلى تغيير سطح المكتب", + "Description[be]": "На экране паказваецца змена працоўнага стала", + "Description[bg]": "Екранен надпис, показващ промяната на работния плот", + "Description[ca@valencia]": "Un missatge a la pantalla indicant el canvi d'escriptori", + "Description[ca]": "Un missatge a la pantalla indicant el canvi d'escriptori", + "Description[cs]": "Displej označující změnu plochy", + "Description[da]": "Et display på skærmen indikerer skrivebordsændringen", + "Description[de]": "Ein On-Screen-Display (OSD), das den Wechsel der Arbeitsfläche anzeigt", + "Description[en_GB]": "An on screen display indicating the desktop change", + "Description[eo]": "Ekrana ekrano indikanta la surtablan ŝanĝon", + "Description[es]": "Un visor sobre la pantalla para indicar el cambio de escritorio", + "Description[et]": "Töölaua muutumist näitab ekraanikuva", + "Description[eu]": "Mahaigain-aldaketa adierazten duen pantailako azalpena", + "Description[fi]": "Työpöytävaihdon osoittava ruutunäyttö", + "Description[fr]": "Une incrustation vidéo indiquant un changement de bureau", + "Description[gl]": "Un visor na pantalla que indica o cambio de escritorio.", + "Description[he]": "תצוגה צפה על המסך שמציינת ששולחן העבודה התחלף", + "Description[hu]": "Asztalváltást jelző képernyőkijelzés", + "Description[ia]": "Un monstrator sur le schermo indicante le modification de scriptorio", + "Description[id]": "Sebuah on screen display mengindikasikan perubahan desktop", + "Description[is]": "Yfirlagsmynd á skjá sem sýnir þegar skipt er á milli skjáborða", + "Description[it]": "Una visualizzazione sullo schermo che indica il cambio di desktop", + "Description[ja]": "デスクトップの変更をオンスクリーンディスプレイで通知します", + "Description[ka]": "სამუშაო მაგიდის ცვლილების ეკრანზე ჩვენება", + "Description[ko]": "바탕 화면 변경 상황을 표시하는 OSD", + "Description[lt]": "Ekrane pasirodantis pranešimas, nurodantis, kad buvo perjungtas darbalaukis", + "Description[lv]": "Virsekrāna logs, kurā parāda darbvirsmas nomaiņu", + "Description[nb]": "Bruk skjermmelding ved skrivebordsbytte", + "Description[nl]": "Een on screen display (OSD) die de wijziging van het bureaublad aangeeft", + "Description[nn]": "Bruk skjermmelding ved skrivebordbyte", + "Description[pl]": "Wyświetlacz na ekranie wskazujący zmianę pulpitu", + "Description[pt]": "Uma visualização no ecrã que indica a mudança de ecrã", + "Description[pt_BR]": "Uma exibição na tela indicando a alteração da área de trabalho", + "Description[ro]": "Afișaj pe ecran ce indică schimbarea biroului", + "Description[ru]": "Экранные уведомления об изменении рабочего стола", + "Description[sa]": "डेस्कटॉप् परिवर्तनं सूचयति पर्दायां प्रदर्शनम्", + "Description[sk]": "Zobrazenie na obrazovke indikujúce zmenu pracovnej plochy", + "Description[sl]": "Prikaz na zasloni, ki nakazuje spremembo namizja", + "Description[sv]": "En skärmvisning som anger skrivbordsändringen", + "Description[ta]": "பணிமேடையை மாற்றும்போது காட்டப்படுவது", + "Description[tr]": "Masaüstü değişikliğini belirten bir ekran üzeri görüntüsü", + "Description[uk]": "Екранна панель спостереження за зміною стільниць", + "Description[vi]": "Một ô hiện nổi để biểu thị thay đổi của bàn làm việc", + "Description[zh_CN]": "能够突出显示桌面内容变化的辅助显示功能", + "Description[zh_TW]": "表示桌面變更用的螢幕顯示(OSD)", + "Icon": "preferences-system-windows-script-test", + "Id": "desktopchangeosd", + "License": "GPL", + "Name": "Desktop Change OSD", + "Name[ar]": "تنبيه على الشاشة حين تغير سطح المكتب", + "Name[be]": "Паказ змены працоўнага стала", + "Name[bg]": "Екранен надпис за промяна на работния плот", + "Name[ca@valencia]": "OSD al canvi d'escriptori", + "Name[ca]": "OSD al canvi d'escriptori", + "Name[cs]": "OSD změny plochy", + "Name[da]": "Skrivebordsændring OSD", + "Name[de]": "Arbeitsflächenwechsel-OSD", + "Name[en_GB]": "Desktop Change OSD", + "Name[eo]": "Labortablo Ŝanĝi OSD", + "Name[es]": "OSD de cambio de escritorio", + "Name[et]": "Töölaua muutmise ekraaniesitus", + "Name[eu]": "Mahaigain-aldaketari buruzko OSD", + "Name[fi]": "Työpöytävaihdon ruutunäyttö", + "Name[fr]": "Incrustation vidéo pour le changement de bureau", + "Name[gl]": "OSD de cambio de escritorio", + "Name[he]": "חיווי צף על החלפת שולחן עבודה", + "Name[hu]": "Asztalváltás képernyőkijelzés", + "Name[ia]": "Modifica OSD de scriptorio", + "Name[id]": "OSD Perubahan Desktop", + "Name[is]": "Yfirlagsmynd fyrir skjáborðsskipti", + "Name[it]": "OSD di cambio desktop", + "Name[ja]": "デスクトップの変更 OSD", + "Name[ka]": "OSD სამუშაო მაგიდის ცვლილებისას", + "Name[ko]": "바탕 화면 변경 OSD", + "Name[lt]": "Pranešimas keičiant darbalaukį", + "Name[lv]": "Darbvirsmas izmaiņu virsekrāna logs", + "Name[nb]": "Skjermmelding ved skrivebordsbytte", + "Name[nl]": "OSD voor bureaubladwijziging", + "Name[nn]": "Skjermmelding ved skrivebordsbyte", + "Name[pl]": "OSD zmiany pulpitu", + "Name[pt]": "OSD de Mudança do Ecrã", + "Name[pt_BR]": "Alterar OSD da área de trabalho", + "Name[ro]": "OSD la schimbarea biroului", + "Name[ru]": "Экранные уведомления об изменении рабочего стола", + "Name[sa]": "डेस्कटॉप परिवर्तनं OSD", + "Name[sk]": "OSD zmeny plochy", + "Name[sl]": "Sprememba namizja OSD", + "Name[sv]": "Skärmvisning av skrivbordsändring", + "Name[ta]": "பணிமேடை மாற்ற காட்டி", + "Name[tr]": "OSD Masaüstü Değişimi", + "Name[uk]": "Панель зміни стільниць", + "Name[vi]": "Ô hiện nổi thay đổi bàn làm việc", + "Name[zh_CN]": "桌面变化突出显示", + "Name[zh_TW]": "桌面變更時的螢幕顯示" + }, + "X-Plasma-API": "declarativescript" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/dialogparent/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/dialogparent/CMakeLists.txt new file mode 100644 index 0000000000..6a1ad972a4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/dialogparent/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_scripted_effect(dialogparent package) diff --git a/local/recipes/kde/kwin/source/src/plugins/dialogparent/package/contents/code/main.js b/local/recipes/kde/kwin/source/src/plugins/dialogparent/package/contents/code/main.js new file mode 100644 index 0000000000..b6d20bd280 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/dialogparent/package/contents/code/main.js @@ -0,0 +1,158 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +/*global effect, effects, animate, cancel, set, animationTime, Effect, QEasingCurve */ +/*jslint continue: true */ +var dialogParentEffect = { + duration: animationTime(300), + windowAdded: function (window) { + "use strict"; + window.minimizedChanged.connect(() => { + if (window.minimized) { + dialogParentEffect.cancelAnimationInstant(window); + } else { + dialogParentEffect.restartAnimation(window); + } + }); + window.windowModalityChanged.connect(dialogParentEffect.windowModalityChanged); + window.windowDesktopsChanged.connect(dialogParentEffect.cancelAnimationInstant); + window.windowDesktopsChanged.connect(dialogParentEffect.restartAnimation); + + if (window.modal) { + dialogParentEffect.dialogGotModality(window); + } + }, + dialogGotModality: function (window) { + "use strict"; + var mainWindows = window.mainWindows(); + for (var i = 0; i < mainWindows.length; ++i) { + dialogParentEffect.startAnimation(mainWindows[i]); + } + }, + startAnimation: function (window) { + "use strict"; + if (window.visible === false) { + return; + } + if (window.dialogParentAnimation) { + if (redirect(window.dialogParentAnimation, Effect.Forward)) { + return; + } + cancel(window.dialogParentAnimation); + } + window.dialogParentAnimation = set({ + window: window, + duration: dialogParentEffect.duration, + keepAlive: false, + animations: [{ + type: Effect.Saturation, + to: 0.4 + }, { + type: Effect.Brightness, + to: 0.6 + }] + }); + }, + windowClosed: function (window) { + "use strict"; + if (window.modal) { + dialogParentEffect.dialogLostModality(window); + } + }, + dialogLostModality: function (window) { + "use strict"; + var mainWindows = window.mainWindows(); + for (var i = 0; i < mainWindows.length; ++i) { + dialogParentEffect.cancelAnimationSmooth(mainWindows[i]); + } + }, + cancelAnimationInstant: function (window) { + "use strict"; + if (window.dialogParentAnimation) { + cancel(window.dialogParentAnimation); + delete window.dialogParentAnimation; + } + }, + cancelAnimationSmooth: function (window) { + "use strict"; + if (!window.dialogParentAnimation) { + return; + } + if (redirect(window.dialogParentAnimation, Effect.Backward)) { + return; + } + cancel(window.dialogParentAnimation); + delete window.dialogParentEffect; + }, + desktopChanged: function () { + "use strict"; + // If there is an active full screen effect, then try smoothly dim/brighten + // the main windows. Keep in mind that in order for this to work properly, this + // effect has to come after the full screen effect in the effect chain, + // otherwise this slot will be invoked before the full screen effect can mark + // itself as a full screen effect. + if (dialogParentEffect.globallyInhibited()) { + return; + } + + const windows = effects.stackingOrder; + for (const window of windows) { + dialogParentEffect.cancelAnimationInstant(window); + dialogParentEffect.restartAnimation(window); + } + }, + windowModalityChanged: function (window) { + "use strict"; + dialogParentEffect.refreshWindowEffect(window); + }, + restartAnimation: function (window) { + "use strict"; + if (window === null || window.findModal() === null) { + return; + } + dialogParentEffect.startAnimation(window); + if (window.dialogParentAnimation) { + complete(window.dialogParentAnimation); + } + }, + activeFullScreenOrColorPickerChanged: function () { + "use strict"; + const windows = effects.stackingOrder; + for (const window of windows) { + dialogParentEffect.refreshWindowEffect(window); + } + }, + globallyInhibited: function () { + return effects.hasActiveFullScreenEffect || effects.colorPickerActive; + }, + refreshWindowEffect: function (window) { + if (dialogParentEffect.globallyInhibited() || !window.modal) { + dialogParentEffect.dialogLostModality(window); + } else { + dialogParentEffect.dialogGotModality(window); + } + }, + init: function () { + "use strict"; + var i, windows; + effects.windowAdded.connect(dialogParentEffect.windowAdded); + effects.windowClosed.connect(dialogParentEffect.windowClosed); + effects.desktopChanged.connect(dialogParentEffect.desktopChanged); + effects.activeFullScreenEffectChanged.connect( + dialogParentEffect.activeFullScreenOrColorPickerChanged); + effects.colorPickerActiveChanged.connect( + dialogParentEffect.activeFullScreenOrColorPickerChanged); + + windows = effects.stackingOrder; + for (i = 0; i < windows.length; i += 1) { + dialogParentEffect.windowAdded(windows[i]); + } + } +}; +dialogParentEffect.init(); diff --git a/local/recipes/kde/kwin/source/src/plugins/dialogparent/package/metadata.json b/local/recipes/kde/kwin/source/src/plugins/dialogparent/package/metadata.json new file mode 100644 index 0000000000..0345792950 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/dialogparent/package/metadata.json @@ -0,0 +1,156 @@ +{ + "KPackageStructure": "KWin/Effect", + "KPlugin": { + "Authors": [ + { + "Email": "rivolaks@hot.ee, mgraesslin@kde.org", + "Name": "Rivo Laks, Martin Gräßlin", + "Name[ar]": "ريفو لاكس، مارتن جراجلين", + "Name[be]": "Rivo Laks, Martin Gräßlin", + "Name[bg]": "Rivo Laks, Martin Gräßlin", + "Name[ca@valencia]": "Rivo Laks, Martin Gräßlin", + "Name[ca]": "Rivo Laks, Martin Gräßlin", + "Name[cs]": "Rivo Laks, Martin Gräßlin", + "Name[da]": "Rivo Laks, Martin Gräßlin", + "Name[de]": "Rivo Laks, Martin Gräßlin", + "Name[en_GB]": "Rivo Laks, Martin Gräßlin", + "Name[eo]": "Rivo Laks, Martin Gräßlin", + "Name[es]": "Rivo Laks, Martin Gräßlin", + "Name[et]": "Rivo Laks, Martin Gräßlin", + "Name[eu]": "Rivo Laks, Martin Gräßlin", + "Name[fi]": "Rivo Laks, Martin Gräßlin", + "Name[fr]": "Rivo Laks, Martin Gräßlin", + "Name[ga]": "Rivo Laks, Martin Gräßlin", + "Name[gl]": "Rivo Laks e Martin Gräßlin.", + "Name[he]": "ריבו לאקס, מרטין גרייסלין", + "Name[hu]": "Rivo Laks, Martin Gräßlin", + "Name[ia]": "Rivo Laks, Martin Gräßlin", + "Name[id]": "Rivo Laks, Martin Gräßlin", + "Name[is]": "Rivo Laks, Martin Gräßlin", + "Name[it]": "Rivo Laks, Martin Gräßlin", + "Name[ja]": "Rivo Laks, Martin Gräßlin", + "Name[ka]": "Rivo Laks, Martin Gräßlin", + "Name[ko]": "Rivo Laks, Martin Gräßlin", + "Name[lt]": "Rivo Laks, Martin Gräßlin", + "Name[lv]": "Rivo Laks, Martin Gräßlin", + "Name[nb]": "Rivo Laks, Martin Gräßlin", + "Name[nl]": "Rivo Laks, Martin Gräßlin", + "Name[nn]": "Rivo Laks, Martin Gräßlin", + "Name[pl]": "Rivo Laks, Martin Gräßlin", + "Name[pt]": "Rivo Laks, Martin Gräßlin", + "Name[pt_BR]": "Rivo Laks, Martin Gräßlin", + "Name[ro]": "Rivo Laks, Martin Gräßlin", + "Name[ru]": "Rivo Laks, Martin Gräßlin", + "Name[sa]": "Rivo Laks, मार्टिन Gräßlin", + "Name[sk]": "Rivo Laks, Martin Gräßlin", + "Name[sl]": "Rivo Laks, Martin Gräßlin", + "Name[sv]": "Rivo Laks, Martin Gräßlin", + "Name[ta]": "ரிவோ லாக்சு, மார்ட்டின் கிராஸ்லின்", + "Name[tr]": "Rivo Laks, Martin Gräßlin", + "Name[uk]": "Rivo Laks, Martin Gräßlin", + "Name[vi]": "Rivo Laks, Martin Gräßlin", + "Name[zh_CN]": "Rivo Laks, Martin Gräßlin", + "Name[zh_TW]": "Rivo Laks, Martin Gräßlin" + } + ], + "Category": "Focus", + "Description": "Darkens the parent window of the currently active dialog", + "Description[ar]": "تعتم النافذة المولدة للحوار النشط حالياً", + "Description[be]": "Зацямненне бацькоўскага акна актыўнага дыялогавага акна", + "Description[bg]": "Затъмняване на главния от активните прозорци", + "Description[ca@valencia]": "Enfosquix la finestra principal del diàleg actualment actiu", + "Description[ca]": "Enfosqueix la finestra principal del diàleg actualment actiu", + "Description[cs]": "Ztmaví okno nadřazené aktivnímu dialogu", + "Description[da]": "Gør forældrevinduet for den nuværende aktive dialog mørkere", + "Description[de]": "Dunkelt das Eltern-Fenster des aktiven Dialogs ab", + "Description[en_GB]": "Darkens the parent window of the currently active dialog", + "Description[eo]": "Malheligas la gepatran fenestron de la nun aktiva dialogo", + "Description[es]": "Oscurece la ventana padre del diálogo activo", + "Description[et]": "Tumendab aktiivse dialoogi eellasakna", + "Description[eu]": "Unean aktibo dagoen elkarrizketa-koadroaren guraso leihoa iluntzen du", + "Description[fi]": "Tummentaa aktiivisen kyselyikkunan emoikkunan", + "Description[fr]": "Assombrit la fenêtre parente de la boite de dialogue actuellement active.", + "Description[gl]": "Escurece a xanela pai do diálogo activo.", + "Description[he]": "מחשיך את חלון ההורה של החלונית שפעילה כרגע", + "Description[hu]": "Elsötétíti az aktív párbeszédablak szülőablakát", + "Description[ia]": "Il obscura le fenestra genitor del dialogo currentemente active", + "Description[id]": "Memetangkan jendela induk dari dialog yang saat ini aktif", + "Description[is]": "Dekkir yfirglugga (forvera) virka svargluggans", + "Description[it]": "Scurisce la finestra madre di quella attiva", + "Description[ja]": "アクティブなダイアログの親ウィンドウを暗くします", + "Description[ka]": "ამჟამად აქტიური დიალოგის მშობელი ფანჯრის გამუქება", + "Description[ko]": "현재 활성 대화 상자의 부모 창을 어둡게 합니다", + "Description[lt]": "Užtemdo viršesnį esamu metu aktyvaus dialogo langą", + "Description[lv]": "Aptumšo pašreizējā aktīvā loga galveno logu", + "Description[nb]": "Gjør overvinduet til det aktive dialogvinduet mørkere", + "Description[nl]": "Maakt het venster dat bij de ouder van de actieve dialoog hoort donkerder", + "Description[nn]": "Gjer foreldrevindauget til det aktive dialogvindauget mørkare", + "Description[pl]": "Przyciemnia nadrzędne okna obecnie aktywnego okna dialogowego", + "Description[pt]": "Escurece a janela-mãe da janela activa de momento", + "Description[pt_BR]": "Escurece a janela de origem da caixa de diálogo ativa no momento", + "Description[ro]": "Întunecă fereastra-părinte a dialogului activ", + "Description[ru]": "Затемнение основного окна при показе диалога", + "Description[sa]": "वर्तमानसक्रियसंवादस्य मातापितृविण्डो अन्धकारं करोति", + "Description[sk]": "Stmaví nadradené okno aktuálne aktívneho dialógu", + "Description[sl]": "Potemni nadrejeno okno trenutno dejavnega okna", + "Description[sv]": "Gör fönstret som äger den för närvarande aktiva dialogrutan mörkare", + "Description[ta]": "தற்போது செயலில் உள்ள துணை சாளரத்தின் தாய் சாளரத்தை இருளாக்கும்", + "Description[tr]": "Geçerli etkin iletişim kutusunun üst penceresini karartır", + "Description[uk]": "Затемнення батьківських вікон активних діалогових вікон", + "Description[vi]": "Làm tối cửa sổ cha của hộp thoại hiện đang hoạt động", + "Description[zh_CN]": "当前活动对话框的父窗口将会变暗", + "Description[zh_TW]": "將目前對話框的親視窗變暗", + "EnabledByDefault": true, + "Icon": "preferences-system-windows-effect-dialog-parent", + "Id": "dialogparent", + "License": "GPL", + "Name": "Dialog Parent", + "Name[ar]": "أم نافذة الحوار", + "Name[be]": "Бацькоўскае акно", + "Name[bg]": "Основен прозорец", + "Name[ca@valencia]": "Diàleg principal", + "Name[ca]": "Diàleg principal", + "Name[cs]": "Předek dialogu", + "Name[da]": "Dialogforælder", + "Name[de]": "Eltern-Fenster abdunkeln", + "Name[en_GB]": "Dialog Parent", + "Name[eo]": "Dialoga Gepatro", + "Name[es]": "Ventana padre del diálogo", + "Name[et]": "Dialoogi eellane", + "Name[eu]": "Guraso elkarrizketa-koadroa", + "Name[fi]": "Kyselyikkunan emoikkuna", + "Name[fr]": "Boîte de dialogue parente", + "Name[gl]": "Diálogo superior", + "Name[he]": "הורה חלונית", + "Name[hu]": "Párbeszédablak-szülő", + "Name[ia]": "Dialogo genitor", + "Name[id]": "Induk Dialog", + "Name[is]": "Yfirgluggi svarglugga", + "Name[it]": "Finestra madre", + "Name[ja]": "ダイアログの親", + "Name[ka]": "ფანჯრის მშობელი", + "Name[ko]": "대화 상자 부모", + "Name[lt]": "Viršesnis dialogo langas", + "Name[lv]": "Dialoglodziņa vecāks", + "Name[nb]": "Mørklegg overvindu", + "Name[nl]": "Dialoogeigenaar", + "Name[nn]": "Mørklegg foreldrevindauge", + "Name[pl]": "Rodzic okna dialogowego", + "Name[pt]": "Pai da Janela", + "Name[pt_BR]": "Diálogo de origem", + "Name[ro]": "Părinte dialog", + "Name[ru]": "Затемнение основного окна", + "Name[sa]": "संवाद अभिभावक", + "Name[sk]": "Nadradený dialóg", + "Name[sl]": "Nadrejeno pogovorno okno", + "Name[sv]": "Dialogrutors ägare", + "Name[ta]": "துணை சாளரத்தின் தாய்", + "Name[tr]": "Üst İletişim Kutusu", + "Name[uk]": "Батьківське вікно", + "Name[vi]": "Cha hộp thoại", + "Name[zh_CN]": "对话框父级窗口变暗", + "Name[zh_TW]": "對話框上層" + }, + "X-KDE-Ordering": 70, + "X-Plasma-API": "javascript" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/diminactive/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/diminactive/CMakeLists.txt new file mode 100644 index 0000000000..83f6e2eb59 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/diminactive/CMakeLists.txt @@ -0,0 +1,36 @@ +####################################### +# Effect + +set(diminactive_SOURCES + diminactive.cpp + main.cpp +) + +kconfig_add_kcfg_files(diminactive_SOURCES + diminactiveconfig.kcfgc +) + +kwin_add_builtin_effect(diminactive ${diminactive_SOURCES}) +target_link_libraries(diminactive PRIVATE + kwin + + KF6::ConfigGui +) + +####################################### +# Config +if (KWIN_BUILD_KCMS) + set(kwin_diminactive_config_SRCS diminactive_config.cpp) + ki18n_wrap_ui(kwin_diminactive_config_SRCS diminactive_config.ui) + kconfig_add_kcfg_files(kwin_diminactive_config_SRCS diminactiveconfig.kcfgc) + + kwin_add_effect_config(kwin_diminactive_config ${kwin_diminactive_config_SRCS}) + + target_link_libraries(kwin_diminactive_config + KF6::KCMUtils + KF6::CoreAddons + KF6::I18n + Qt::DBus + KWinEffectsInterface + ) +endif() diff --git a/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive.cpp b/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive.cpp new file mode 100644 index 0000000000..c29ee1aecf --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive.cpp @@ -0,0 +1,423 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +// own +#include "diminactive.h" +#include "effect/effecthandler.h" + +// KConfigSkeleton +#include "diminactiveconfig.h" + +using namespace std::chrono_literals; + +namespace KWin +{ + +/** + * Checks if two windows belong to the same window group + * + * One possible example of a window group is an app window and app + * preferences window(e.g. Dolphin window and Dolphin Preferences window). + * + * @param w1 The first window + * @param w2 The second window + * @returns @c true if both windows belong to the same window group, @c false otherwise + */ +static inline bool belongToSameGroup(const EffectWindow *w1, const EffectWindow *w2) +{ + return w1 && w2 && w1->group() && w1->group() == w2->group(); +} + +DimInactiveEffect::DimInactiveEffect() +{ + DimInactiveConfig::instance(effects->config()); + reconfigure(ReconfigureAll); + + connect(effects, &EffectsHandler::windowActivated, + this, &DimInactiveEffect::windowActivated); + connect(effects, &EffectsHandler::windowAdded, + this, &DimInactiveEffect::windowAdded); + connect(effects, &EffectsHandler::windowClosed, + this, &DimInactiveEffect::windowClosed); + connect(effects, &EffectsHandler::windowDeleted, + this, &DimInactiveEffect::windowDeleted); + connect(effects, &EffectsHandler::activeFullScreenEffectChanged, + this, &DimInactiveEffect::activeFullScreenEffectChanged); + + const auto windows = effects->stackingOrder(); + for (EffectWindow *window : windows) { + windowAdded(window); + } +} + +DimInactiveEffect::~DimInactiveEffect() +{ +} + +void DimInactiveEffect::reconfigure(ReconfigureFlags flags) +{ + DimInactiveConfig::self()->read(); + + // TODO: Use normalized strength param. + m_dimStrength = std::clamp(DimInactiveConfig::strength() / 100.0, 0.1, 0.9); + m_dimPanels = DimInactiveConfig::dimPanels(); + m_dimDesktop = DimInactiveConfig::dimDesktop(); + m_dimKeepAbove = DimInactiveConfig::dimKeepAbove(); + m_dimByGroup = DimInactiveConfig::dimByGroup(); + m_dimFullScreen = DimInactiveConfig::dimFullScreen(); + + updateActiveWindow(effects->activeWindow()); + + m_activeWindowGroup = (m_dimByGroup && m_activeWindow) + ? m_activeWindow->group() + : nullptr; + + m_fullScreenTransition.timeLine.setDuration( + std::chrono::milliseconds(static_cast(animationTime(250ms)))); + + effects->addRepaintFull(); +} + +void DimInactiveEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + if (m_fullScreenTransition.active) { + m_fullScreenTransition.timeLine.advance(presentTime); + } + + auto transitionIt = m_transitions.begin(); + while (transitionIt != m_transitions.end()) { + (*transitionIt).advance(presentTime); + ++transitionIt; + } + + effects->prePaintScreen(data, presentTime); +} + +void DimInactiveEffect::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) +{ + auto transitionIt = m_transitions.constFind(w); + if (transitionIt != m_transitions.constEnd()) { + const qreal transitionProgress = (*transitionIt).value(); + dimWindow(data, m_dimStrength * transitionProgress); + effects->paintWindow(renderTarget, viewport, w, mask, deviceRegion, data); + return; + } + + auto forceIt = m_forceDim.constFind(w); + if (forceIt != m_forceDim.constEnd()) { + const qreal forcedStrength = *forceIt; + dimWindow(data, forcedStrength); + effects->paintWindow(renderTarget, viewport, w, mask, deviceRegion, data); + return; + } + + if (canDimWindow(w)) { + dimWindow(data, m_dimStrength); + } + + effects->paintWindow(renderTarget, viewport, w, mask, deviceRegion, data); +} + +void DimInactiveEffect::postPaintScreen() +{ + if (m_fullScreenTransition.active) { + if (m_fullScreenTransition.timeLine.done()) { + m_fullScreenTransition.active = false; + } + effects->addRepaintFull(); + } + + auto transitionIt = m_transitions.begin(); + while (transitionIt != m_transitions.end()) { + EffectWindow *w = transitionIt.key(); + if ((*transitionIt).done()) { + transitionIt = m_transitions.erase(transitionIt); + } else { + ++transitionIt; + } + w->addRepaintFull(); + } + + effects->postPaintScreen(); +} + +void DimInactiveEffect::dimWindow(WindowPaintData &data, qreal strength) +{ + qreal dimFactor; + if (m_fullScreenTransition.active) { + dimFactor = 1.0 - m_fullScreenTransition.timeLine.value(); + } else if (effects->activeFullScreenEffect()) { + dimFactor = 0.0; + } else { + dimFactor = 1.0; + } + + data.multiplyBrightness(1.0 - strength * dimFactor); + data.multiplySaturation(1.0 - strength * dimFactor); +} + +bool DimInactiveEffect::canDimWindow(const EffectWindow *w) const +{ + if (m_activeWindow == w) { + return false; + } + + if (m_dimByGroup && belongToSameGroup(m_activeWindow, w)) { + return false; + } + + if (w->isDock() && !m_dimPanels) { + return false; + } + + if (w->isDesktop() && !m_dimDesktop) { + return false; + } + + if (w->keepAbove() && !m_dimKeepAbove) { + return false; + } + + if (w->isFullScreen() && !m_dimFullScreen) { + return false; + } + + if (w->isPopupWindow() || w->isInputMethod()) { + return false; + } + + if (w->isX11Client() && !w->isManaged()) { + return false; + } + + return w->isNormalWindow() + || w->isDialog() + || w->isUtility() + || w->isDock() + || w->isDesktop(); +} + +void DimInactiveEffect::scheduleInTransition(EffectWindow *w) +{ + TimeLine &timeLine = m_transitions[w]; + timeLine.setDuration( + std::chrono::milliseconds(static_cast(animationTime(160ms)))); + if (timeLine.done()) { + // If the Out animation is still active, then we're truncating + // duration of the timeline(from 250ms to 160ms). If the timeline + // is about to be finished with the old duration, then after + // changing duration it will be in the "done" state. Thus, we + // have to reset the timeline, otherwise it won't update progress. + timeLine.reset(); + } + timeLine.setDirection(TimeLine::Backward); + timeLine.setEasingCurve(QEasingCurve::InOutSine); +} + +void DimInactiveEffect::scheduleGroupInTransition(EffectWindow *w) +{ + if (!m_dimByGroup) { + scheduleInTransition(w); + return; + } + + if (!w->group()) { + scheduleInTransition(w); + return; + } + + const auto members = w->group()->members(); + for (EffectWindow *member : members) { + scheduleInTransition(member); + } +} + +void DimInactiveEffect::scheduleOutTransition(EffectWindow *w) +{ + TimeLine &timeLine = m_transitions[w]; + timeLine.setDuration( + std::chrono::milliseconds(static_cast(animationTime(250ms)))); + if (timeLine.done()) { + timeLine.reset(); + } + timeLine.setDirection(TimeLine::Forward); + timeLine.setEasingCurve(QEasingCurve::InOutSine); +} + +void DimInactiveEffect::scheduleGroupOutTransition(EffectWindow *w) +{ + if (!m_dimByGroup) { + scheduleOutTransition(w); + return; + } + + if (!w->group()) { + scheduleOutTransition(w); + return; + } + + const auto members = w->group()->members(); + for (EffectWindow *member : members) { + scheduleOutTransition(member); + } +} + +void DimInactiveEffect::scheduleRepaint(EffectWindow *w) +{ + if (!m_dimByGroup) { + w->addRepaintFull(); + return; + } + + if (!w->group()) { + w->addRepaintFull(); + return; + } + + const auto members = w->group()->members(); + for (EffectWindow *member : members) { + member->addRepaintFull(); + } +} + +void DimInactiveEffect::windowActivated(EffectWindow *w) +{ + if (!w) { + return; + } + + if (m_activeWindow == w) { + return; + } + + if (m_dimByGroup && belongToSameGroup(m_activeWindow, w)) { + m_activeWindow = w; + return; + } + + // WORKAROUND: Deleted windows do not belong to any of window groups. + // So, if one of windows in a window group is closed, the In transition + // will be false-triggered for the rest of the window group. In addition + // to the active window, keep track of active window group so we can + // tell whether "focus" moved from a closed window to some other window + // in a window group. + if (m_dimByGroup && w->group() && w->group() == m_activeWindowGroup) { + m_activeWindow = w; + return; + } + + EffectWindow *previousActiveWindow = m_activeWindow; + m_activeWindow = canDimWindow(w) ? w : nullptr; + + m_activeWindowGroup = (m_dimByGroup && m_activeWindow) + ? m_activeWindow->group() + : nullptr; + + if (previousActiveWindow) { + scheduleGroupOutTransition(previousActiveWindow); + scheduleRepaint(previousActiveWindow); + } + + if (m_activeWindow) { + scheduleGroupInTransition(m_activeWindow); + scheduleRepaint(m_activeWindow); + } +} + +void DimInactiveEffect::windowAdded(EffectWindow *w) +{ + connect(w, &EffectWindow::windowKeepAboveChanged, + this, &DimInactiveEffect::updateActiveWindow); + connect(w, &EffectWindow::windowFullScreenChanged, + this, &DimInactiveEffect::updateActiveWindow); +} + +void DimInactiveEffect::windowClosed(EffectWindow *w) +{ + // When a window is closed, we should force current dim strength that + // is applied to it to avoid flickering when some effect animates + // the disappearing of the window. If there is no such effect then + // it won't be dimmed. + qreal forcedStrength = 0.0; + bool shouldForceDim = false; + + auto transitionIt = m_transitions.find(w); + if (transitionIt != m_transitions.end()) { + forcedStrength = m_dimStrength * (*transitionIt).value(); + shouldForceDim = true; + m_transitions.erase(transitionIt); + } else if (m_activeWindow == w) { + forcedStrength = 0.0; + shouldForceDim = true; + } else if (m_dimByGroup && belongToSameGroup(m_activeWindow, w)) { + forcedStrength = 0.0; + shouldForceDim = true; + } else if (canDimWindow(w)) { + forcedStrength = m_dimStrength; + shouldForceDim = true; + } + + if (shouldForceDim) { + m_forceDim.insert(w, forcedStrength); + } + + if (m_activeWindow == w) { + m_activeWindow = nullptr; + } +} + +void DimInactiveEffect::windowDeleted(EffectWindow *w) +{ + m_forceDim.remove(w); + + // FIXME: Sometimes we can miss the window close signal because KWin + // can activate a window that is not ready for painting and the window + // gets destroyed immediately. So, we have to remove active transitions + // for that window here, otherwise we'll crash in postPaintScreen. + m_transitions.remove(w); + if (m_activeWindow == w) { + m_activeWindow = nullptr; + } +} + +void DimInactiveEffect::activeFullScreenEffectChanged() +{ + if (m_fullScreenTransition.timeLine.done()) { + m_fullScreenTransition.timeLine.reset(); + } + m_fullScreenTransition.timeLine.setDirection( + effects->activeFullScreenEffect() + ? TimeLine::Forward + : TimeLine::Backward); + m_fullScreenTransition.active = true; + + effects->addRepaintFull(); +} + +void DimInactiveEffect::updateActiveWindow(EffectWindow *w) +{ + if (effects->activeWindow() == nullptr) { + return; + } + + if (effects->activeWindow() != w) { + return; + } + + // Need to reset m_activeWindow because canDimWindow depends on it. + m_activeWindow = nullptr; + + m_activeWindow = canDimWindow(w) ? w : nullptr; +} + +} // namespace KWin + +#include "moc_diminactive.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive.h b/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive.h new file mode 100644 index 0000000000..df8ede4953 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive.h @@ -0,0 +1,131 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +// kwineffects +#include "effect/effect.h" +#include "effect/timeline.h" + +namespace KWin +{ + +class EffectWindowGroup; + +class DimInactiveEffect : public Effect +{ + Q_OBJECT + Q_PROPERTY(int dimStrength READ dimStrength) + Q_PROPERTY(bool dimPanels READ dimPanels) + Q_PROPERTY(bool dimDesktop READ dimDesktop) + Q_PROPERTY(bool dimKeepAbove READ dimKeepAbove) + Q_PROPERTY(bool dimByGroup READ dimByGroup) + Q_PROPERTY(bool dimFullScreen READ dimFullScreen) + +public: + DimInactiveEffect(); + ~DimInactiveEffect() override; + + void reconfigure(ReconfigureFlags flags) override; + + void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override; + void paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) override; + void postPaintScreen() override; + + int requestedEffectChainPosition() const override; + bool isActive() const override; + + int dimStrength() const; + bool dimPanels() const; + bool dimDesktop() const; + bool dimKeepAbove() const; + bool dimByGroup() const; + bool dimFullScreen() const; + +private Q_SLOTS: + void windowActivated(EffectWindow *w); + void windowAdded(EffectWindow *w); + void windowClosed(EffectWindow *w); + void windowDeleted(EffectWindow *w); + void activeFullScreenEffectChanged(); + + void updateActiveWindow(EffectWindow *w); + +private: + void dimWindow(WindowPaintData &data, qreal strength); + bool canDimWindow(const EffectWindow *w) const; + void scheduleInTransition(EffectWindow *w); + void scheduleGroupInTransition(EffectWindow *w); + void scheduleOutTransition(EffectWindow *w); + void scheduleGroupOutTransition(EffectWindow *w); + void scheduleRepaint(EffectWindow *w); + +private: + qreal m_dimStrength; + bool m_dimPanels; + bool m_dimDesktop; + bool m_dimKeepAbove; + bool m_dimByGroup; + bool m_dimFullScreen; + + EffectWindow *m_activeWindow = nullptr; + const EffectWindowGroup *m_activeWindowGroup; + QHash m_transitions; + QHash m_forceDim; + + struct + { + bool active = false; + TimeLine timeLine; + } m_fullScreenTransition; +}; + +inline int DimInactiveEffect::requestedEffectChainPosition() const +{ + return 50; +} + +inline bool DimInactiveEffect::isActive() const +{ + return true; +} + +inline int DimInactiveEffect::dimStrength() const +{ + return qRound(m_dimStrength * 100.0); +} + +inline bool DimInactiveEffect::dimPanels() const +{ + return m_dimPanels; +} + +inline bool DimInactiveEffect::dimDesktop() const +{ + return m_dimDesktop; +} + +inline bool DimInactiveEffect::dimKeepAbove() const +{ + return m_dimKeepAbove; +} + +inline bool DimInactiveEffect::dimByGroup() const +{ + return m_dimByGroup; +} + +inline bool DimInactiveEffect::dimFullScreen() const +{ + return m_dimFullScreen; +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive.kcfg b/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive.kcfg new file mode 100644 index 0000000000..b8493d82cb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive.kcfg @@ -0,0 +1,27 @@ + + + + + + 25 + + + false + + + false + + + false + + + true + + + true + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive_config.cpp b/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive_config.cpp new file mode 100644 index 0000000000..49f4d9784d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive_config.cpp @@ -0,0 +1,52 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "diminactive_config.h" + +#include "config-kwin.h" + +// KConfigSkeleton +#include "diminactiveconfig.h" + +#include + +#include + +K_PLUGIN_CLASS(KWin::DimInactiveEffectConfig) + +namespace KWin +{ + +DimInactiveEffectConfig::DimInactiveEffectConfig(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) +{ + m_ui.setupUi(widget()); + DimInactiveConfig::instance(KWIN_CONFIG); + addConfig(DimInactiveConfig::self(), widget()); +} + +DimInactiveEffectConfig::~DimInactiveEffectConfig() +{ +} + +void DimInactiveEffectConfig::save() +{ + KCModule::save(); + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("diminactive")); +} + +} // namespace KWin + +#include "diminactive_config.moc" + +#include "moc_diminactive_config.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive_config.h b/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive_config.h new file mode 100644 index 0000000000..a3ba10532b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive_config.h @@ -0,0 +1,34 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "ui_diminactive_config.h" + +namespace KWin +{ + +class DimInactiveEffectConfig : public KCModule +{ + Q_OBJECT + +public: + explicit DimInactiveEffectConfig(QObject *parent, const KPluginMetaData &data); + ~DimInactiveEffectConfig() override; + + void save() override; + +private: + ::Ui::DimInactiveEffectConfig m_ui; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive_config.ui b/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive_config.ui new file mode 100644 index 0000000000..46ca290b83 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactive_config.ui @@ -0,0 +1,86 @@ + + + DimInactiveEffectConfig + + + + 0 + 0 + 400 + 160 + + + + + + + Strength: + + + + + + + + 0 + 0 + + + + 10 + + + 90 + + + 5 + + + + + + + Dim: + + + + + + + Docks and panels + + + + + + + Desktop + + + + + + + Keep above windows + + + + + + + By window group + + + + + + + Fullscreen windows + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactiveconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactiveconfig.kcfgc new file mode 100644 index 0000000000..0e57d63f17 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/diminactive/diminactiveconfig.kcfgc @@ -0,0 +1,5 @@ +File=diminactive.kcfg +ClassName=DimInactiveConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/local/recipes/kde/kwin/source/src/plugins/diminactive/main.cpp b/local/recipes/kde/kwin/source/src/plugins/diminactive/main.cpp new file mode 100644 index 0000000000..2bf8705992 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/diminactive/main.cpp @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "diminactive.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY(DimInactiveEffect, + "metadata.json.stripped") + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/diminactive/metadata.json b/local/recipes/kde/kwin/source/src/plugins/diminactive/metadata.json new file mode 100644 index 0000000000..5c8bb3faab --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/diminactive/metadata.json @@ -0,0 +1,102 @@ +{ + "KPlugin": { + "Category": "Focus", + "Description": "Darken inactive windows", + "Description[ar]": "أعتِم النوافذ غير النشطة", + "Description[az]": "Qeyri-aktiv pəncərələri tutqunlaşdırır", + "Description[be]": "Зацямненне неактыўных акон", + "Description[bg]": "Затъмняване на неактивните прозорци", + "Description[ca@valencia]": "Enfosquix les finestres inactives", + "Description[ca]": "Enfosqueix les finestres inactives", + "Description[cs]": "Ztmavit neaktivní okna", + "Description[da]": "Gør inaktive vinduer mørkere", + "Description[de]": "Dunkelt inaktive Fenster ab", + "Description[en_GB]": "Darken inactive windows", + "Description[eo]": "Mallumigi neaktivajn fenestrojn", + "Description[es]": "Oscurece las ventanas inactivas", + "Description[et]": "Tumendab mitteaktiivsed aknad", + "Description[eu]": "Leiho ez-aktiboak iluntzea", + "Description[fi]": "Tummenna passiivisia ikkunoita", + "Description[fr]": "Assombrir les fenêtres inactives", + "Description[gl]": "Escurece as xanelas inactivas.", + "Description[he]": "החשכת חלונות שאינם פעילים", + "Description[hu]": "Elsötétíti az inaktív ablakokat", + "Description[ia]": "Fenestras inactive obscurate", + "Description[id]": "Menggelapkan jendela yang tidak aktif", + "Description[is]": "Dekkir óvirka glugga", + "Description[it]": "Scurisci le finestre inattive", + "Description[ja]": "非アクティブなウィンドウを暗くします", + "Description[ka]": "არააქტიური ფანჯრების ჩაბნელება", + "Description[ko]": "비활성 창을 어둡게 합니다", + "Description[lt]": "Užtemdyti pasyvius langus", + "Description[lv]": "Aptumšo neaktīvos logus", + "Description[nb]": "Gjør inaktive vinduer mørkere", + "Description[nl]": "Maakt inactieve vensters donkerder", + "Description[nn]": "Gjer inaktive vindauge mørkare", + "Description[pl]": "Przyciemnia nieaktywne okna", + "Description[pt]": "Escurecer as janelas inactivas", + "Description[pt_BR]": "Escurece as janelas inativas", + "Description[ro]": "Întunecă ferestrele inactive", + "Description[ru]": "Затемнение неактивных окон", + "Description[sa]": "निष्क्रिय खिडकयः अन्धकारं कुर्वन्तु", + "Description[sk]": "Stmaví neaktívne okná", + "Description[sl]": "Potemni nedejavna okna", + "Description[sv]": "Gör inaktiva fönster mörkare", + "Description[ta]": "செயலிலில்லாத சாளரங்களின் பிரகாசத்தை குறைக்கும்", + "Description[tr]": "Etkin olmayan pencereleri koyulaştır", + "Description[uk]": "Затемнення неактивних вікон", + "Description[vi]": "Làm tối các cửa sổ bất hoạt", + "Description[zh_CN]": "非活动的窗口将会变暗", + "Description[zh_TW]": "將非作用中視窗變暗", + "EnabledByDefault": false, + "License": "GPL", + "Name": "Dim Inactive", + "Name[ar]": "تعتيم الخامل", + "Name[az]": "Qeyri-aktivi qaraldın", + "Name[be]": "Зацямненне неактыўных", + "Name[bg]": "Затъмняване на неактивни прозорци", + "Name[ca@valencia]": "Enfosquix les inactives", + "Name[ca]": "Enfosqueix les inactives", + "Name[cs]": "Ztmavit neaktivní", + "Name[da]": "Dæmp inaktive", + "Name[de]": "Inaktive abdunkeln", + "Name[en_GB]": "Dim Inactive", + "Name[eo]": "Malklaro Neaktiva", + "Name[es]": "Oscurecer inactivas", + "Name[et]": "Tuhm mitteaktiivne", + "Name[eu]": "Ahuldu ez-aktiboa", + "Name[fi]": "Passiivisen himmennys", + "Name[fr]": "Réglage inactif", + "Name[gl]": "Escurecer as inactivas", + "Name[he]": "עמעום בלתי פעיל", + "Name[hu]": "Inaktív ablakok kiszürkítése", + "Name[ia]": "Dim Inactive", + "Name[id]": "Gelapkan Tak Aktif", + "Name[is]": "Dimma óvirka glugga", + "Name[it]": "Scurisci le inattive", + "Name[ja]": "非アクティブなウィンドウを暗くする", + "Name[ka]": "არააქტიურის ჩაბნელება", + "Name[ko]": "비활성 창 어둡게", + "Name[lt]": "Pasyvių langų pritemdymas", + "Name[lv]": "Aptumšot neaktīvo", + "Name[nb]": "Mørklegg inaktive vinduer", + "Name[nl]": "Inactief dimmen", + "Name[nn]": "Mørklegg inaktive vindauge", + "Name[pl]": "Przyciemnienie nieaktywnych", + "Name[pt]": "Escurecer as Inactivas", + "Name[pt_BR]": "Escurecer inativas", + "Name[ro]": "Întunecă inactive", + "Name[ru]": "Затемнение неактивных окон", + "Name[sa]": "मन्द निष्क्रिय", + "Name[sk]": "Stmaviť neaktívne", + "Name[sl]": "Zatemni nedejavne", + "Name[sv]": "Dämpa inaktiva", + "Name[ta]": "செயலிலில்லாதவற்றின் பிரகாசத்தை குறை", + "Name[tr]": "Etkin Olmayanları Karart", + "Name[uk]": "Затемнення неактивних", + "Name[vi]": "Làm tối bất hoạt", + "Name[zh_CN]": "非活动窗口变暗", + "Name[zh_TW]": "暗化非作用" + }, + "X-KDE-ConfigModule": "kwin_diminactive_config" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/dimscreen/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/dimscreen/CMakeLists.txt new file mode 100644 index 0000000000..7cdaf77e4a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/dimscreen/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_scripted_effect(dimscreen package) diff --git a/local/recipes/kde/kwin/source/src/plugins/dimscreen/package/contents/code/main.js b/local/recipes/kde/kwin/source/src/plugins/dimscreen/package/contents/code/main.js new file mode 100644 index 0000000000..7dd20e96de --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/dimscreen/package/contents/code/main.js @@ -0,0 +1,237 @@ +/* + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var authenticationAgents = [ + "kdesu kdesu", + "kdesudo kdesudo", + "pinentry pinentry", + "polkit-kde-authentication-agent-1 polkit-kde-authentication-agent-1", + "polkit-kde-manager polkit-kde-manager", + + // On Wayland, the resource name is filename of executable. It's empty for + // authentication agents because KWayland can't get their executable paths. + " org.kde.kdesu", + " org.kde.polkit-kde-authentication-agent-1" +]; + +function activeAuthenticationAgent() { + var activeWindow = effects.activeWindow; + if (!activeWindow) { + return null; + } + + if (authenticationAgents.indexOf(activeWindow.windowClass) == -1) { + return null; + } + + return activeWindow; +} + +var dimScreenEffect = { + loadConfig: function () { + dimScreenEffect.duration = animationTime(250); + dimScreenEffect.brightness = 0.67; + dimScreenEffect.saturation = 0.67; + }, + startAnimation: function (window) { + if (!window.visible) { + return; + } + if (window.popupWindow) { + return; + } + if (!window.managed) { + return; + } + if (window.dimAnimation) { + if (redirect(window.dimAnimation, Effect.Forward)) { + return; + } + cancel(window.dimAnimation); + } + window.dimAnimation = set({ + window: window, + curve: QEasingCurve.InOutSine, + duration: dimScreenEffect.duration, + keepAlive: false, + animations: [ + { + type: Effect.Saturation, + to: dimScreenEffect.saturation + }, + { + type: Effect.Brightness, + to: dimScreenEffect.brightness + } + ] + }); + }, + startAnimationSmooth: function (window) { + dimScreenEffect.startAnimation(window); + }, + startAnimationInstant: function (window) { + dimScreenEffect.startAnimation(window); + + if (window.dimAnimation) { + complete(window.dimAnimation); + } + }, + cancelAnimationSmooth: function (window) { + if (!window.dimAnimation) { + return; + } + if (redirect(window.dimAnimation, Effect.Backward)) { + return; + } + cancel(window.dimAnimation); + delete window.dimAnimation; + }, + cancelAnimationInstant: function (window) { + if (window.dimAnimation) { + cancel(window.dimAnimation); + delete window.dimAnimation; + } + }, + dimScreen: function (agent, animationFunc, cancelFunc) { + // Keep track of currently active authentication agent so we don't have + // to re-scan the stacking order in brightenScreen each time when some + // window is activated. + dimScreenEffect.authenticationAgent = agent; + + var windows = effects.stackingOrder; + for (var i = 0; i < windows.length; ++i) { + var window = windows[i]; + if (window == agent) { + // The agent might have been dimmed before (because there are + // several authentication agents on the screen), so we need to + // cancel previous animations for it if there are any. + cancelFunc(agent); + continue; + } + animationFunc(window); + } + }, + dimScreenSmooth: function (agent) { + dimScreenEffect.dimScreen( + agent, + dimScreenEffect.startAnimationSmooth, + dimScreenEffect.cancelAnimationSmooth + ); + }, + dimScreenInstant: function (agent) { + dimScreenEffect.dimScreen( + agent, + dimScreenEffect.startAnimationInstant, + dimScreenEffect.cancelAnimationInstant + ); + }, + brightenScreen: function (cancelFunc) { + if (!dimScreenEffect.authenticationAgent) { + return; + } + dimScreenEffect.authenticationAgent = null; + + var windows = effects.stackingOrder; + for (var i = 0; i < windows.length; ++i) { + cancelFunc(windows[i]); + } + }, + brightenScreenSmooth: function () { + dimScreenEffect.brightenScreen(dimScreenEffect.cancelAnimationSmooth); + }, + brightenScreenInstant: function () { + dimScreenEffect.brightenScreen(dimScreenEffect.cancelAnimationInstant); + }, + slotWindowActivated: function (window) { + if (!window) { + return; + } + if (authenticationAgents.indexOf(window.windowClass) != -1) { + dimScreenEffect.dimScreenSmooth(window); + } else { + dimScreenEffect.brightenScreenSmooth(); + } + }, + slotWindowAdded: function (window) { + window.minimizedChanged.connect(() => { + if (window.minimized) { + dimScreenEffect.cancelAnimationInstant(window); + } else { + dimScreenEffect.restartAnimation(window); + } + }); + + // Don't dim authentication agents that just opened. + var agent = activeAuthenticationAgent(); + if (agent == window) { + return; + } + + // If a window appeared while the screen is dimmed, dim the window too. + if (agent) { + dimScreenEffect.startAnimationInstant(window); + } + }, + slotActiveFullScreenEffectChanged: function () { + // If some full screen effect has been activated, for example the desktop + // cube effect, then brighten screen back. We need to do that because the + // full screen effect can dim windows on its own. + if (effects.hasActiveFullScreenEffect) { + dimScreenEffect.brightenScreenSmooth(); + return; + } + + // If user left the full screen effect, try to dim screen back. + var agent = activeAuthenticationAgent(); + if (agent) { + dimScreenEffect.dimScreenSmooth(agent); + } + }, + slotDesktopChanged: function () { + // If there is an active full screen effect, then try smoothly dim/brighten + // the screen. Keep in mind that in order for this to work properly, this + // effect has to come after the full screen effect in the effect chain, + // otherwise this slot will be invoked before the full screen effect can mark + // itself as a full screen effect. + if (effects.hasActiveFullScreenEffect) { + return; + } + + // Try to brighten windows on the previous virtual desktop. + dimScreenEffect.brightenScreenInstant(); + + // Try to dim windows on the current virtual desktop. + var agent = activeAuthenticationAgent(); + if (agent) { + dimScreenEffect.dimScreenInstant(agent); + } + }, + restartAnimation: function (window) { + if (activeAuthenticationAgent()) { + dimScreenEffect.startAnimationInstant(window); + } + }, + init: function () { + dimScreenEffect.loadConfig(); + + effect.configChanged.connect(dimScreenEffect.loadConfig); + effects.windowActivated.connect(dimScreenEffect.slotWindowActivated); + effects.windowAdded.connect(dimScreenEffect.slotWindowAdded); + effects.activeFullScreenEffectChanged.connect( + dimScreenEffect.slotActiveFullScreenEffectChanged); + effects.desktopChanged.connect(dimScreenEffect.slotDesktopChanged); + + for (const window of effects.stackingOrder) { + dimScreenEffect.slotWindowAdded(window); + } + } +}; + +dimScreenEffect.init(); diff --git a/local/recipes/kde/kwin/source/src/plugins/dimscreen/package/metadata.json b/local/recipes/kde/kwin/source/src/plugins/dimscreen/package/metadata.json new file mode 100644 index 0000000000..45e12739d0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/dimscreen/package/metadata.json @@ -0,0 +1,156 @@ +{ + "KPackageStructure": "KWin/Effect", + "KPlugin": { + "Authors": [ + { + "Email": "mgraesslin@kde.org, vlad.zahorodnii@kde.org", + "Name": "Martin Flöser, Vlad Zahorodnii", + "Name[ar]": "مارتن فلوسر، فلاد زاهورودني", + "Name[be]": "Martin Flöser, Vlad Zahorodnii", + "Name[bg]": "Martin Flöser, Vlad Zahorodnii", + "Name[ca@valencia]": "Martin Flöser, Vlad Zahorodnii", + "Name[ca]": "Martin Flöser, Vlad Zahorodnii", + "Name[cs]": "Martin Flöser, Vlad Zahorodnii", + "Name[da]": "Martin Flöser, Vlad Zahorodnii", + "Name[de]": "Martin Flöser, Vlad Zahorodnii", + "Name[en_GB]": "Martin Flöser, Vlad Zahorodnii", + "Name[eo]": "Martin Flöser, Vlad Zahorodnii", + "Name[es]": "Martin Flöser, Vlad Zahorodnii", + "Name[et]": "Martin Flöser, Vlad Zahorodnii", + "Name[eu]": "Martin Flöser, Vlad Zahorodnii", + "Name[fi]": "Martin Flöser, Vlad Zahorodnii", + "Name[fr]": "Martin Flöser, Vlad Zahorodnii", + "Name[ga]": "Martin Flöser, Vlad Zahorodnii", + "Name[gl]": "Martin Flöser e Vlad Zahorodnii.", + "Name[he]": "מרטין פלויסר, ולאד זוהורוני", + "Name[hu]": "Martin Flöser, Vlad Zahorodnii", + "Name[ia]": "Martin Flöser, Vlad Zahorodnii", + "Name[id]": "Martin Flöser, Vlad Zahorodnii", + "Name[is]": "Martin Flöser, Vlad Zahorodnii", + "Name[it]": "Martin Flöser, Vlad Zahorodnii", + "Name[ja]": "Martin Flöser, Vlad Zahorodnii", + "Name[ka]": "Vlad Zahorodnii", + "Name[ko]": "Martin Flöser, Vlad Zahorodnii", + "Name[lt]": "Martin Flöser, Vlad Zahorodnii", + "Name[lv]": "Martin Flöser, Vlad Zahorodnii", + "Name[nb]": "Martin Flöser, Vlad Zahorodnii", + "Name[nl]": "Martin Flöser, Vlad Zahorodnii", + "Name[nn]": "Martin Flöser, Vlad Zahorodnii", + "Name[pl]": "Martin Flöser, Vlad Zahorodnii", + "Name[pt]": "Martin Flöser, Vlad Zahorodnii", + "Name[pt_BR]": "Martin Flöser, Vlad Zahorodnii", + "Name[ro]": "Martin Flöser, Vlad Zahorodnii", + "Name[ru]": "Martin Flöser, Влад Загородний", + "Name[sa]": "मार्टिन Flöser, व्लाद Zahorodnii", + "Name[sk]": "Martin Flöser, Vlad Zahorodnii", + "Name[sl]": "Martin Flöser, Vlad Zahorodnii", + "Name[sv]": "Martin Flöser, Vlad Zahorodnii", + "Name[ta]": "மார்ட்டின் ஃபுலோசர், விலாட் ஜாஹொரிடுனி", + "Name[tr]": "Martin Flöser, Vlad Zahorodnii", + "Name[uk]": "Martin Flöser, Влад Завгородній", + "Name[vi]": "Martin Flöser, Vlad Zahorodnii", + "Name[zh_CN]": "Martin Flöser, Vlad Zahorodnii", + "Name[zh_TW]": "Martin Flöser, Vlad Zahorodnii" + } + ], + "Category": "Focus", + "Description": "Darkens the entire screen when requesting root privileges", + "Description[ar]": "يعتم كامل الشاشة عند طلب صلاحيات الجذر", + "Description[be]": "Зацямненне ўсяго экрана пры запытванні правоў суперкарыстальніка", + "Description[bg]": "Екранът затъмнява при необходимост от администраторски права ", + "Description[ca@valencia]": "Enfosquix tota la pantalla en demanar privilegis d'administrador", + "Description[ca]": "Enfosqueix tota la pantalla en demanar privilegis d'administrador", + "Description[cs]": "Ztmaví obrazovku, pokud jsou vyžadována oprávnění administrátora systému", + "Description[da]": "Gør hele skærmen mørkere, når der bedes om root-privilegier", + "Description[de]": "Dunkelt den gesamten Bildschirm ab, wenn nach dem Systemverwalter-Passwort gefragt wird", + "Description[en_GB]": "Darkens the entire screen when requesting root privileges", + "Description[eo]": "Mallumigas la tutan ekranon kiam oni petas radikajn privilegiojn", + "Description[es]": "Oscurece toda la pantalla cuando se solicitan privilegios de «root»", + "Description[et]": "Tumendab administraatori õiguste nõudmisel kogu ekraani", + "Description[eu]": "«root»aren pribilegioak eskatzean pantaila osoa iluntzen du", + "Description[fi]": "Tummentaa koko näytön pääkäyttäjäoikeuksia pyydettäessä", + "Description[fr]": "Assombrir la totalité de l'écran quand les privilèges d'administrateur sont nécessaires", + "Description[gl]": "Escurece toda a pantalla cando se piden os privilexios de root.", + "Description[he]": "מחשיך את כל המסך בעת הצגת בקשה להרשאות משתמש על (root)", + "Description[hu]": "Elsötétíti a teljes képernyőt root jogosultságok kérésekor", + "Description[ia]": "Il obscura le schermo integre quando il demanda privilegios de super-usator (root)", + "Description[id]": "Memetangkan seluruh layar ketika meminta hak akses root", + "Description[is]": "Dekkir allan skjáinn þegar beðið er um lykilorð kerfisstjóra", + "Description[it]": "Scurisce tutto lo schermo quando si richiedono i privilegi di root", + "Description[ja]": "root 権限が要求されるとスクリーン全体を暗くします", + "Description[ka]": "ადმინისტრირების პრივილეგიების მოთხოვნისას მთელი ეკრანის გამუქება", + "Description[ko]": "루트 권한이 필요할 때 화면을 어둡게 합니다", + "Description[lt]": "Pritemdo visą ekraną, kai prašoma root naudotojo teisių", + "Description[lv]": "Aptumšo visu ekrānu, pieprasot saknes lietotāja privilēģijas", + "Description[nb]": "Gjør hele skjermen mørkere ved forespørsel om rottilgang", + "Description[nl]": "Maakt het volledige scherm donker wanneer er om root-privileges wordt verzocht", + "Description[nn]": "Gjer heile skjermen mørkare når det vert spurt om rottilgang", + "Description[pl]": "Przyciemnia ekran przy żądaniu praw administratora", + "Description[pt]": "Escurece o ecrã todo ao pedir privilégios de administração", + "Description[pt_BR]": "Escurece toda a tela ao solicitar privilégios de root", + "Description[ro]": "Întunecă întregul ecran la cererea privilegiilor de root", + "Description[ru]": "Затемнение всего экрана при запросе привилегий суперпользователя", + "Description[sa]": "मूलविशेषाधिकारस्य अनुरोधं कुर्वन् सम्पूर्णं पटलं अन्धकारं करोति", + "Description[sk]": "Stmaví celú obrazovku pri žiadosti o oprávnenia správcu systému root", + "Description[sl]": "Potemni celoten naslov ob zahtevi za korenske pravice", + "Description[sv]": "Gör hela skärmen mörkare när administratörsrättigheter begärs", + "Description[ta]": "ரூட் உரிமையை கோரும்போது திரை முழுவதையும் இருளாக்கும்", + "Description[tr]": "Yönetici ayrıcalıkları isterken tüm ekranı karartır", + "Description[uk]": "Притлумлює кольори усього екрана, коли працюємо з доступом root", + "Description[vi]": "Làm tối toàn bộ màn hình khi yêu cầu đặc quyền gốc", + "Description[zh_CN]": "系统请求 root 权限时降低屏幕亮度", + "Description[zh_TW]": "請求 root 權限時將整個螢幕變暗", + "EnabledByDefault": true, + "Icon": "preferences-system-windows-effect-dimscreen", + "Id": "dimscreen", + "License": "GPL", + "Name": "Dim Screen for Administrator Mode", + "Name[ar]": "يعتم الشاشة لنمط المدير", + "Name[be]": "Зацямненне экрана для рэжыму адміністратара", + "Name[bg]": "Затъмняване на екрана в администраторски режим", + "Name[ca@valencia]": "Enfosquix la pantalla per al mode administrador", + "Name[ca]": "Enfosqueix la pantalla per al mode administrador", + "Name[cs]": "Ztmavit obrazovku v administrátorském režimu", + "Name[da]": "Dæmp skærm for adminstratortilstand", + "Name[de]": "Bildschirm für Systemverwaltungsmodus abdunkeln", + "Name[en_GB]": "Dim Screen for Administrator Mode", + "Name[eo]": "Malfortigi Ekranon por Administranto-Reĝimo", + "Name[es]": "Oscurecer la pantalla en el modo administrador", + "Name[et]": "Tuhm ekraan administraatori režiimis", + "Name[eu]": "Ahuldu pantaila administratzaile modurako", + "Name[fi]": "Himmennä näyttö pääkäyttäjätilassa", + "Name[fr]": "Régler l'écran en mode administrateur", + "Name[gl]": "Escurecer a pantalla no modo de administración", + "Name[he]": "עמעוך המסך למצב ניהול", + "Name[hu]": "Képernyősötétítés adminisztrátor módban", + "Name[ia]": "Schermo Dim pro modo de administrator", + "Name[id]": "Gelapkan Layar untuk Mode Administrator", + "Name[is]": "Dimma skjá fyrir kerfisstjórastillingu", + "Name[it]": "Oscuramento dello schermo in modalità amministrativa", + "Name[ja]": "管理者モードでスクリーンを暗くする", + "Name[ka]": "ეკრანის ჩაბნელება ადმინისტრატორის რეჟიმისთვის", + "Name[ko]": "관리자 모드에서 화면 어둡게 하기", + "Name[lt]": "Ekrano pritemdymas prieš įjungiant administratoriaus veikseną", + "Name[lv]": "Aptumšot ekrānu administratora režīmam", + "Name[nb]": "Mørklegg skjermen i administratormodus", + "Name[nl]": "Scherm dimmen voor administratormodus", + "Name[nn]": "Mørklegg skjermen i administratormodus", + "Name[pl]": "Przyciemnienie ekranu dla trybu administratora", + "Name[pt]": "Escurecer o Ecrã para o Modo de Administrador", + "Name[pt_BR]": "Escurecer tela para modo administrador", + "Name[ro]": "Întunecă ecranul pentru regimul de administrator", + "Name[ru]": "Затемнение экрана при административной задаче", + "Name[sa]": "प्रशासक मोडस्य कृते Dim Screen इति", + "Name[sk]": "Stmaviť obrazovku v administrátorskom režime", + "Name[sl]": "Potemni zaslon za skrbniški način", + "Name[sv]": "Dämpa skärmen vid administratörsläge", + "Name[ta]": "ரூட் பயன்முறைக்கு திரையை இருளாக்கு", + "Name[tr]": "Yönetici Kipi için Ekranı Karart", + "Name[uk]": "Притлумлення кольорів у режимі адміністратора", + "Name[vi]": "Làm tối màn hình cho chế độ quản trị viên", + "Name[zh_CN]": "请求管理员模式时降低屏幕亮度", + "Name[zh_TW]": "為管理員模式暗化螢幕" + }, + "X-KDE-Ordering": 60, + "X-Plasma-API": "javascript" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/eis/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/eis/CMakeLists.txt new file mode 100644 index 0000000000..2332d534bf --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eis/CMakeLists.txt @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2024 David Redondo +# SPDX-License-Identifier: BSD-3-Clause + +kcoreaddons_add_plugin(eis INSTALL_NAMESPACE "kwin/plugins") + +ecm_qt_declare_logging_category(eis + HEADER libeis_logging.h + IDENTIFIER KWIN_EIS + CATEGORY_NAME kwin_libeis + DEFAULT_SEVERITY Warning +) + +ecm_qt_declare_logging_category(eis + HEADER inputcapture_logging.h + IDENTIFIER KWIN_INPUTCAPTURE + CATEGORY_NAME kwin_inputcapture + DEFAULT_SEVERITY Warning +) + +target_sources(eis PRIVATE + main.cpp + eisdevice.cpp + eisbackend.cpp + eiscontext.cpp + eisplugin.cpp + eisinputcapture.cpp + eisinputcapturemanager.cpp + eisinputcapturefilter.cpp +) + +target_link_libraries(eis PRIVATE kwin KF6::I18n KF6::GlobalAccel KF6::ConfigGui Libeis::Libeis XKB::XKB) + +if (Libeis_VERSION VERSION_GREATER_EQUAL "1.5") + set(EIS_HAVE_GET_CLIENT_PID 1) +endif() +configure_file(config-eis.h.in config-eis.h) + diff --git a/local/recipes/kde/kwin/source/src/plugins/eis/eisbackend.cpp b/local/recipes/kde/kwin/source/src/plugins/eis/eisbackend.cpp new file mode 100644 index 0000000000..3df341fe1a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eis/eisbackend.cpp @@ -0,0 +1,198 @@ +/* + SPDX-FileCopyrightText: 2024 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "eisbackend.h" + +#include "eiscontext.h" +#include "eisdevice.h" +#include "libeis_logging.h" + +#include "core/output.h" +#include "input.h" +#include "keyboard_input.h" +#include "keyboard_layout.h" +#include "main_wayland.h" +#include "workspace.h" +#include "xkb.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include + +namespace KWin +{ + +#define typeName(T) \ + [] { \ + static_assert( \ + requires { typename T; }, "T is not a type"); \ + return #T; \ + }() + +EisBackend::EisBackend(QObject *parent) + : KWin::InputBackend(parent) + , m_serviceWatcher(new QDBusServiceWatcher(this)) +{ +#if HAVE_XWAYLAND_ENABLE_EI_PORTAL + if (options->xwaylandEisNoPrompt()) { + // Unfortunately there is no way to pass a connected socket fd to libei like WAYLAND_SOCKET + // in libwayland so we are resorting to this hack + // https://gitlab.freedesktop.org/libinput/libei/-/issues/63 + m_xWaylandContext = std::make_unique(this); + FileDescriptor fd(open(m_xWaylandContext->socketName.constData(), O_PATH | O_CLOEXEC)); + unlink(m_xWaylandContext->socketName.constData()); + if (QByteArray(kwinApp()->metaObject()->className()) == typeName(KWin::ApplicationWayland)) { + auto appWayland = static_cast(kwinApp()); + appWayland->addExtraXWaylandEnvrionmentVariable(QStringLiteral("LIBEI_SOCKET"), QStringLiteral("/proc/self/fd/%1").arg(fd.get())); + appWayland->passFdToXwayland(std::move(fd)); + } + } +#endif + + m_serviceWatcher->setConnection(QDBusConnection::sessionBus()); + m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration); + connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &service) { + std::erase_if(m_contexts, [&service](const std::unique_ptr &context) { + return context->dbusService == service; + }); + m_serviceWatcher->removeWatchedService(service); + }); +} + +EisBackend::~EisBackend() +{ +} + +void EisBackend::initialize() +{ + const QByteArray keyMap = input()->keyboard()->xkb()->keymapContents(); + if (!keyMap.isEmpty()) { + m_keymapFile = RamFile("eis keymap", keyMap.data(), keyMap.size(), RamFile::Flag::SealWrite); + } + connect(input()->keyboard()->keyboardLayout(), &KeyboardLayout::layoutsReconfigured, this, [this] { + const QByteArray keyMap = input()->keyboard()->xkb()->keymapContents(); + if (!keyMap.isEmpty()) { + m_keymapFile = RamFile("eis keymap", keyMap.data(), keyMap.size(), RamFile::Flag::SealWrite); + } else { + m_keymapFile = RamFile(); + } + for (const auto &context : m_contexts) { + context->updateKeymap(); + } + }); + + QDBusConnection::sessionBus().registerObject("/org/kde/KWin/EIS/RemoteDesktop", "org.kde.KWin.EIS.RemoteDesktop", this, QDBusConnection::ExportAllInvokables); +} + +void EisBackend::updateScreens() +{ + for (const auto &context : m_contexts) { + context->updateScreens(); + } +} + +QDBusUnixFileDescriptor EisBackend::connectToEIS(const int &capabilities, int &cookie) +{ + constexpr int keyboardPortal = 1; + constexpr int pointerPortal = 2; + constexpr int touchPortal = 4; + QFlags eisCapabilities; + if (capabilities & keyboardPortal) { + eisCapabilities |= EIS_DEVICE_CAP_KEYBOARD; + } + if (capabilities & pointerPortal) { + eisCapabilities |= EIS_DEVICE_CAP_POINTER; + eisCapabilities |= EIS_DEVICE_CAP_POINTER_ABSOLUTE; + eisCapabilities |= EIS_DEVICE_CAP_BUTTON; + eisCapabilities |= EIS_DEVICE_CAP_SCROLL; + } + if (capabilities & touchPortal) { + eisCapabilities |= EIS_DEVICE_CAP_TOUCH; + } + const QString dbusService = message().service(); + static int s_cookie = 0; + cookie = ++s_cookie; + m_contexts.push_back(std::make_unique(this, eisCapabilities, cookie, dbusService)); + m_serviceWatcher->addWatchedService(dbusService); + return QDBusUnixFileDescriptor(m_contexts.back()->addClient()); +} + +void EisBackend::disconnect(int cookie) +{ + auto it = std::ranges::find(m_contexts, cookie, [](const std::unique_ptr &context) { + return context->cookie; + }); + if (it != std::ranges::end(m_contexts)) { + m_contexts.erase(it); + } +} + +eis_device *createDevice(eis_seat *seat, const QByteArray &name) +{ + auto device = eis_seat_new_device(seat); + + auto client = eis_seat_get_client(seat); + const char *clientName = eis_client_get_name(client); + const QByteArray deviceName = clientName + (' ' + name); + eis_device_configure_name(device, deviceName); + return device; +} + +eis_device *EisBackend::createPointer(eis_seat *seat) +{ + auto device = createDevice(seat, "eis pointer"); + eis_device_configure_capability(device, EIS_DEVICE_CAP_POINTER); + eis_device_configure_capability(device, EIS_DEVICE_CAP_SCROLL); + eis_device_configure_capability(device, EIS_DEVICE_CAP_BUTTON); + return device; +} + +eis_device *EisBackend::createAbsoluteDevice(eis_seat *seat) +{ + auto device = createDevice(seat, "eis absolute device"); + auto eisDevice = device; + eis_device_configure_capability(eisDevice, EIS_DEVICE_CAP_POINTER_ABSOLUTE); + eis_device_configure_capability(eisDevice, EIS_DEVICE_CAP_SCROLL); + eis_device_configure_capability(eisDevice, EIS_DEVICE_CAP_BUTTON); + eis_device_configure_capability(eisDevice, EIS_DEVICE_CAP_TOUCH); + + const auto outputs = workspace()->outputs(); + for (const auto output : outputs) { + auto region = eis_device_new_region(eisDevice); + const QRect outputGeometry = output->geometry(); + eis_region_set_offset(region, outputGeometry.x(), outputGeometry.y()); + eis_region_set_size(region, outputGeometry.width(), outputGeometry.height()); + eis_region_set_physical_scale(region, output->scale()); + eis_region_add(region); + eis_region_unref(region); + }; + + return device; +} + +eis_device *EisBackend::createKeyboard(eis_seat *seat) +{ + auto device = createDevice(seat, "eis keyboard"); + eis_device_configure_capability(device, EIS_DEVICE_CAP_KEYBOARD); + + if (m_keymapFile.isValid()) { + auto keymap = eis_device_new_keymap(device, EIS_KEYMAP_TYPE_XKB, m_keymapFile.fd(), m_keymapFile.size()); + eis_keymap_add(keymap); + eis_keymap_unref(keymap); + } + + return device; +} +} diff --git a/local/recipes/kde/kwin/source/src/plugins/eis/eisbackend.h b/local/recipes/kde/kwin/source/src/plugins/eis/eisbackend.h new file mode 100644 index 0000000000..f04282e58c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eis/eisbackend.h @@ -0,0 +1,54 @@ +/* + SPDX-FileCopyrightText: 2024 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include "core/inputbackend.h" +#include "utils/ramfile.h" + +#include +#include + +#include + +extern "C" { +struct eis; +struct eis_device; +struct eis_seat; +} + +class QDBusServiceWatcher; + +namespace KWin +{ +class DbusEisContext; +class XWaylandEisContext; + +class EisBackend : public KWin::InputBackend, public QDBusContext +{ + Q_OBJECT +public: + explicit EisBackend(QObject *parent = nullptr); + ~EisBackend() override; + void initialize() override; + + void updateScreens() override; + + Q_INVOKABLE QDBusUnixFileDescriptor connectToEIS(const int &capabilities, int &cookie); + Q_INVOKABLE void disconnect(int cookie); + + eis_device *createKeyboard(eis_seat *seat); + eis_device *createPointer(eis_seat *seat); + eis_device *createAbsoluteDevice(eis_seat *seat); + +private: + QDBusServiceWatcher *m_serviceWatcher; + RamFile m_keymapFile; + std::unique_ptr m_xWaylandContext; + std::vector> m_contexts; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/plugins/eis/eiscontext.cpp b/local/recipes/kde/kwin/source/src/plugins/eis/eiscontext.cpp new file mode 100644 index 0000000000..9f1d79bcc5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eis/eiscontext.cpp @@ -0,0 +1,367 @@ +/* + SPDX-FileCopyrightText: 2024 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "eiscontext.h" +#include "config-eis.h" +#include "eisbackend.h" +#include "eisdevice.h" +#include "libeis_logging.h" +#include "main.h" + +#include + +namespace KWin +{ + +static void eis_log_handler(eis *eis, eis_log_priority priority, const char *message, eis_log_context *context) +{ + switch (priority) { + case EIS_LOG_PRIORITY_DEBUG: + qCDebug(KWIN_EIS) << "Libeis:" << message; + break; + case EIS_LOG_PRIORITY_INFO: + qCInfo(KWIN_EIS) << "Libeis:" << message; + break; + case EIS_LOG_PRIORITY_WARNING: + qCWarning(KWIN_EIS) << "Libeis:" << message; + break; + case EIS_LOG_PRIORITY_ERROR: + qCritical(KWIN_EIS) << "Libeis:" << message; + break; + } +} + +struct EisClient +{ +public: + EisClient(eis_client *client, eis_seat *seat) + : handle(client) + , seat(seat) + { + eis_seat_set_user_data(seat, this); + eis_client_set_user_data(client, this); + } + ~EisClient() + { + eis_seat_unref(seat); + eis_client_disconnect(handle); + } + eis_client *handle; + eis_seat *seat; + std::unique_ptr absoluteDevice; + std::unique_ptr pointer; + std::unique_ptr keyboard; +}; + +DbusEisContext::DbusEisContext(KWin::EisBackend *backend, QFlags allowedCapabilities, int cookie, const QString &dbusService) + : EisContext(backend, allowedCapabilities) + , cookie(cookie) + , dbusService(dbusService) +{ + eis_setup_backend_fd(m_eisContext); +} + +int DbusEisContext::addClient() +{ + return eis_backend_fd_add_client(m_eisContext); +} + +XWaylandEisContext::XWaylandEisContext(KWin::EisBackend *backend) + : EisContext(backend, {EIS_DEVICE_CAP_POINTER | EIS_DEVICE_CAP_POINTER_ABSOLUTE | EIS_DEVICE_CAP_KEYBOARD | EIS_DEVICE_CAP_TOUCH | EIS_DEVICE_CAP_SCROLL | EIS_DEVICE_CAP_BUTTON}) + , socketName(qgetenv("XDG_RUNTIME_DIR") + QByteArrayLiteral("/kwin-xwayland-eis-socket.") + QByteArray::number(getpid())) +{ + eis_setup_backend_socket(m_eisContext, socketName.constData()); +} + +bool XWaylandEisContext::allowConnection(eis_client *client) const +{ +#if EIS_HAVE_GET_CLIENT_PID + const auto pid = eis_backend_socket_get_client_pid(client); + if (kwinApp()->xwaylandPid() != pid) { + qCWarning(KWIN_EIS) << "Non-xwayland process" << pid << "trying to connect to Xwayland socket - disconnecting"; + return false; + } +#endif + return true; +} + +EisContext::EisContext(KWin::EisBackend *backend, QFlags allowedCapabilities) + : m_eisContext(eis_new(this)) + , m_backend(backend) + , m_allowedCapabilities(allowedCapabilities) + , m_socketNotifier(eis_get_fd(m_eisContext), QSocketNotifier::Read) +{ + eis_log_set_priority(m_eisContext, EIS_LOG_PRIORITY_DEBUG); + eis_log_set_handler(m_eisContext, eis_log_handler); + QObject::connect(&m_socketNotifier, &QSocketNotifier::activated, [this] { + handleEvents(); + }); +} + +EisContext::~EisContext() +{ + for (const auto &client : m_clients) { + if (client->absoluteDevice) { + Q_EMIT m_backend->deviceRemoved(client->absoluteDevice.get()); + } + if (client->pointer) { + Q_EMIT m_backend->deviceRemoved(client->pointer.get()); + } + if (client->keyboard) { + Q_EMIT m_backend->deviceRemoved(client->keyboard.get()); + } + } +} + +void EisContext::updateScreens() +{ + for (const auto &client : m_clients) { + if (client->absoluteDevice) { + client->absoluteDevice->changeDevice(m_backend->createAbsoluteDevice(client->seat)); + } + } +} + +void EisContext::updateKeymap() +{ + for (const auto &client : m_clients) { + if (client->keyboard) { + client->keyboard->changeDevice(m_backend->createKeyboard(client->seat)); + } + } +} + +static std::chrono::microseconds currentTime() +{ + return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()); +} + +void EisContext::handleEvents() +{ + eis_dispatch(m_eisContext); + + while (eis_event *const event = eis_get_event(m_eisContext)) { + auto eisDevice = eis_event_get_device(event); + auto device = eisDevice ? static_cast(eis_device_get_user_data(eisDevice)) : nullptr; + + if (eisDevice && !device) { + qCWarning(KWIN_EIS) << "Event for destroyed device - ignoring"; + eis_event_unref(event); + continue; + } + + switch (eis_event_get_type(event)) { + case EIS_EVENT_CLIENT_CONNECT: { + auto client = eis_event_get_client(event); + const char *clientName = eis_client_get_name(client); + if (!eis_client_is_sender(client)) { + qCDebug(KWIN_EIS) << "disconnecting receiving client" << clientName; + eis_client_disconnect(client); + break; + } + if (!allowConnection(client)) { + eis_client_disconnect(client); + break; + } + + eis_client_connect(client); + + auto seat = eis_client_new_seat(client, QByteArrayLiteral(" seat").prepend(clientName)); + constexpr std::array allCapabilities{EIS_DEVICE_CAP_POINTER, EIS_DEVICE_CAP_POINTER_ABSOLUTE, EIS_DEVICE_CAP_KEYBOARD, EIS_DEVICE_CAP_TOUCH, EIS_DEVICE_CAP_SCROLL, EIS_DEVICE_CAP_BUTTON}; + for (auto capability : allCapabilities) { + if (m_allowedCapabilities & capability) { + eis_seat_configure_capability(seat, capability); + } + } + + eis_seat_add(seat); + m_clients.emplace_back(std::make_unique(client, seat)); + qCDebug(KWIN_EIS) << "New eis client" << clientName; + break; + } + case EIS_EVENT_CLIENT_DISCONNECT: { + auto client = eis_event_get_client(event); + qCDebug(KWIN_EIS) << "Client disconnected" << eis_client_get_name(client); + if (auto seat = static_cast(eis_client_get_user_data(client))) { + m_clients.erase(std::ranges::find(m_clients, seat, &std::unique_ptr::get)); + } + break; + } + case EIS_EVENT_SEAT_BIND: { + auto seat = eis_event_get_seat(event); + auto clientSeat = static_cast(eis_seat_get_user_data(seat)); + qCDebug(KWIN_EIS) << "Client" << eis_client_get_name(eis_event_get_client(event)) << "bound to seat" << eis_seat_get_name(seat); + auto updateDevice = [event, this](std::unique_ptr &device, auto &&createFunc, bool shouldHave) { + if (shouldHave) { + if (!device) { + device = std::make_unique(std::invoke(createFunc, m_backend, (eis_event_get_seat(event)))); + device->setEnabled(true); + Q_EMIT m_backend->deviceAdded(device.get()); + } + } else if (device) { + Q_EMIT m_backend->deviceRemoved(device.get()); + device.reset(); + } + }; + updateDevice(clientSeat->absoluteDevice, &EisBackend::createAbsoluteDevice, eis_event_seat_has_capability(event, EIS_DEVICE_CAP_POINTER_ABSOLUTE) || eis_event_seat_has_capability(event, EIS_DEVICE_CAP_TOUCH)); + updateDevice(clientSeat->pointer, &EisBackend::createPointer, eis_event_seat_has_capability(event, EIS_DEVICE_CAP_POINTER)); + updateDevice(clientSeat->keyboard, &EisBackend::createKeyboard, eis_event_seat_has_capability(event, EIS_DEVICE_CAP_KEYBOARD)); + break; + } + case EIS_EVENT_DEVICE_CLOSED: { + qCDebug(KWIN_EIS) << "Device" << device->name() << "closed by client"; + Q_EMIT m_backend->deviceRemoved(device); + auto seat = static_cast(eis_seat_get_user_data(eis_device_get_seat(device->handle()))); + if (device == seat->absoluteDevice.get()) { + seat->absoluteDevice.reset(); + } else if (device == seat->keyboard.get()) { + seat->keyboard.reset(); + } else if (device == seat->pointer.get()) { + seat->pointer.reset(); + } + break; + } + case EIS_EVENT_FRAME: { + qCDebug(KWIN_EIS) << "Frame for device" << device->name(); + if (device->isTouch()) { + Q_EMIT device->touchFrame(device); + } + if (device->isPointer()) { + Q_EMIT device->pointerFrame(device); + } + break; + } + case EIS_EVENT_DEVICE_START_EMULATING: { + qCDebug(KWIN_EIS) << "Device" << device->name() << "starts emulating"; + break; + } + case EIS_EVENT_DEVICE_STOP_EMULATING: { + qCDebug(KWIN_EIS) << "Device" << device->name() << "stops emulating"; + break; + } + case EIS_EVENT_POINTER_MOTION: { + const double x = eis_event_pointer_get_dx(event); + const double y = eis_event_pointer_get_dy(event); + qCDebug(KWIN_EIS) << device->name() << "pointer motion" << x << y; + const QPointF delta(x, y); + Q_EMIT device->pointerMotion(delta, delta, currentTime(), device); + break; + } + case EIS_EVENT_POINTER_MOTION_ABSOLUTE: { + const double x = eis_event_pointer_get_absolute_x(event); + const double y = eis_event_pointer_get_absolute_y(event); + qCDebug(KWIN_EIS) << device->name() << "pointer motion absolute" << x << y; + Q_EMIT device->pointerMotionAbsolute({x, y}, currentTime(), device); + break; + } + case EIS_EVENT_BUTTON_BUTTON: { + const quint32 button = eis_event_button_get_button(event); + const bool press = eis_event_button_get_is_press(event); + qCDebug(KWIN_EIS) << device->name() << "button" << button << press; + if (press) { + if (device->pressedButtons.contains(button)) { + continue; + } + device->pressedButtons.insert(button); + } else { + if (!device->pressedButtons.remove(button)) { + continue; + } + } + Q_EMIT device->pointerButtonChanged(button, press ? PointerButtonState::Pressed : PointerButtonState::Released, currentTime(), device); + break; + } + case EIS_EVENT_SCROLL_DELTA: { + const auto x = eis_event_scroll_get_dx(event); + const auto y = eis_event_scroll_get_dy(event); + qCDebug(KWIN_EIS) << device->name() << "scroll" << x << y; + if (x != 0) { + Q_EMIT device->pointerAxisChanged(PointerAxis::Horizontal, x, 0, PointerAxisSource::Unknown, false, currentTime(), device); + } + if (y != 0) { + Q_EMIT device->pointerAxisChanged(PointerAxis::Vertical, y, 0, PointerAxisSource::Unknown, false, currentTime(), device); + } + break; + } + case EIS_EVENT_SCROLL_STOP: + case EIS_EVENT_SCROLL_CANCEL: { + if (eis_event_scroll_get_stop_x(event)) { + qCDebug(KWIN_EIS) << device->name() << "scroll x" << (eis_event_get_type(event) == EIS_EVENT_SCROLL_STOP ? "stop" : "cancel"); + Q_EMIT device->pointerAxisChanged(PointerAxis::Horizontal, 0, 0, PointerAxisSource::Unknown, false, currentTime(), device); + } + if (eis_event_scroll_get_stop_y(event)) { + qCDebug(KWIN_EIS) << device->name() << "scroll y" << (eis_event_get_type(event) == EIS_EVENT_SCROLL_STOP ? "stop" : "cancel"); + Q_EMIT device->pointerAxisChanged(PointerAxis::Vertical, 0, 0, PointerAxisSource::Unknown, false, currentTime(), device); + } + break; + } + case EIS_EVENT_SCROLL_DISCRETE: { + const double x = eis_event_scroll_get_discrete_dx(event); + const double y = eis_event_scroll_get_discrete_dy(event); + qCDebug(KWIN_EIS) << device->name() << "scroll discrete" << x << y; + // otherwise no scroll event + constexpr auto anglePer120Step = 15 / 120.0; + if (x != 0) { + Q_EMIT device->pointerAxisChanged(PointerAxis::Horizontal, x * anglePer120Step, x, PointerAxisSource::Unknown, false, currentTime(), device); + } + if (y != 0) { + Q_EMIT device->pointerAxisChanged(PointerAxis::Vertical, y * anglePer120Step, y, PointerAxisSource::Unknown, false, currentTime(), device); + } + break; + } + case EIS_EVENT_KEYBOARD_KEY: { + const quint32 key = eis_event_keyboard_get_key(event); + const bool press = eis_event_keyboard_get_key_is_press(event); + qCDebug(KWIN_EIS) << device->name() << "key" << key << press; + if (press) { + if (device->pressedKeys.contains(key)) { + continue; + } + device->pressedKeys.insert(key); + } else { + if (!device->pressedKeys.remove(key)) { + continue; + } + } + Q_EMIT device->keyChanged(key, press ? KeyboardKeyState::Pressed : KeyboardKeyState::Released, currentTime(), device); + break; + } + case EIS_EVENT_TOUCH_DOWN: { + const auto x = eis_event_touch_get_x(event); + const auto y = eis_event_touch_get_y(event); + const auto id = eis_event_touch_get_id(event); + qCDebug(KWIN_EIS) << device->name() << "touch down" << id << x << y; + device->activeTouches.push_back(id); + Q_EMIT device->touchDown(id, {x, y}, currentTime(), device); + break; + } + case EIS_EVENT_TOUCH_UP: { + const auto id = eis_event_touch_get_id(event); + qCDebug(KWIN_EIS) << device->name() << "touch up" << id; + std::erase(device->activeTouches, id); + if (eis_event_touch_get_is_cancel(event)) { + Q_EMIT device->touchCanceled(device); + break; + } + Q_EMIT device->touchUp(id, currentTime(), device); + break; + } + case EIS_EVENT_TOUCH_MOTION: { + const auto x = eis_event_touch_get_x(event); + const auto y = eis_event_touch_get_y(event); + const auto id = eis_event_touch_get_id(event); + qCDebug(KWIN_EIS) << device->name() << "touch move" << id << x << y; + Q_EMIT device->touchMotion(id, {x, y}, currentTime(), device); + break; + } + case EIS_EVENT_PONG: + case EIS_EVENT_SYNC: + break; + } + eis_event_unref(event); + } +} +} diff --git a/local/recipes/kde/kwin/source/src/plugins/eis/eiscontext.h b/local/recipes/kde/kwin/source/src/plugins/eis/eiscontext.h new file mode 100644 index 0000000000..59c2d49d47 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eis/eiscontext.h @@ -0,0 +1,69 @@ +/* + SPDX-FileCopyrightText: 2024 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include +#include +#include + +#include + +#include +#include + +namespace KWin +{ + +class EisBackend; +struct EisClient; + +class EisContext +{ +public: + EisContext(EisBackend *backend, QFlags allowedCapabilities); + virtual ~EisContext(); + + void updateScreens(); + void updateKeymap(); + + virtual bool allowConnection(eis_client *client) const + { + return true; + } + +protected: + eis *m_eisContext; + +private: + void handleEvents(); + + EisBackend *m_backend; + QFlags m_allowedCapabilities; + QSocketNotifier m_socketNotifier; + std::vector> m_clients; +}; + +class DbusEisContext : public EisContext +{ +public: + DbusEisContext(EisBackend *backend, QFlags allowedCapabilities, int cookie, const QString &dbusService); + + int addClient(); + + const int cookie; + const QString dbusService; +}; + +class XWaylandEisContext : public EisContext +{ +public: + XWaylandEisContext(EisBackend *backend); + bool allowConnection(eis_client *client) const override; + + const QByteArray socketName; +}; +} diff --git a/local/recipes/kde/kwin/source/src/plugins/eis/eisdevice.cpp b/local/recipes/kde/kwin/source/src/plugins/eis/eisdevice.cpp new file mode 100644 index 0000000000..aa58dba19f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eis/eisdevice.cpp @@ -0,0 +1,111 @@ +/* + SPDX-FileCopyrightText: 2024 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "eisdevice.h" + +#include + +namespace KWin +{ + +static std::chrono::microseconds currentTime() +{ + return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()); +} + +EisDevice::EisDevice(eis_device *device, QObject *parent) + : InputDevice(parent) + , m_device(device) +{ + eis_device_set_user_data(device, this); + eis_device_add(device); +} + +EisDevice::~EisDevice() +{ + for (const auto button : pressedButtons) { + Q_EMIT pointerButtonChanged(button, PointerButtonState::Released, currentTime(), this); + } + for (const auto key : pressedKeys) { + Q_EMIT keyChanged(key, KeyboardKeyState::Released, currentTime(), this); + } + if (!activeTouches.empty()) { + Q_EMIT touchCanceled(this); + } + eis_device_remove(m_device); + eis_device_unref(m_device); +} + +void EisDevice::changeDevice(eis_device *device) +{ + eis_device_set_user_data(m_device, nullptr); + eis_device_remove(m_device); + eis_device_unref(m_device); + m_device = device; + eis_device_set_user_data(device, this); + eis_device_add(device); + if (m_enabled) { + eis_device_resume(device); + } +} + +QString EisDevice::name() const +{ + return QString::fromUtf8(eis_device_get_name(m_device)); +} + +bool EisDevice::isEnabled() const +{ + return m_enabled; +} + +void EisDevice::setEnabled(bool enabled) +{ + m_enabled = enabled; + enabled ? eis_device_resume(m_device) : eis_device_pause(m_device); +} + +bool EisDevice::isKeyboard() const +{ + return eis_device_has_capability(m_device, EIS_DEVICE_CAP_KEYBOARD); +} + +bool EisDevice::isPointer() const +{ + return eis_device_has_capability(m_device, EIS_DEVICE_CAP_POINTER) || eis_device_has_capability(m_device, EIS_DEVICE_CAP_POINTER_ABSOLUTE); +} + +bool EisDevice::isTouchpad() const +{ + return false; +} + +bool EisDevice::isTouch() const +{ + return eis_device_has_capability(m_device, EIS_DEVICE_CAP_TOUCH); +} + +bool EisDevice::isTabletTool() const +{ + return false; +} + +bool EisDevice::isTabletPad() const +{ + return false; +} + +bool EisDevice::isTabletModeSwitch() const +{ + return false; +} + +bool EisDevice::isLidSwitch() const +{ + return false; +} + +} diff --git a/local/recipes/kde/kwin/source/src/plugins/eis/eisdevice.h b/local/recipes/kde/kwin/source/src/plugins/eis/eisdevice.h new file mode 100644 index 0000000000..154936782f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eis/eisdevice.h @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2024 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include "core/inputdevice.h" + +struct eis_device; + +namespace KWin +{ + +class EisDevice : public InputDevice +{ + Q_OBJECT +public: + explicit EisDevice(eis_device *device, QObject *parent = nullptr); + ~EisDevice() override; + + eis_device *handle() const + { + return m_device; + } + void changeDevice(eis_device *device); + + QSet pressedButtons; + QSet pressedKeys; + std::vector activeTouches; + + QString name() const override; + + bool isEnabled() const override; + void setEnabled(bool enabled) override; + + bool isKeyboard() const override; + bool isPointer() const override; + bool isTouchpad() const override; + bool isTouch() const override; + bool isTabletTool() const override; + bool isTabletPad() const override; + bool isTabletModeSwitch() const override; + bool isLidSwitch() const override; + +private: + eis_device *m_device; + bool m_enabled; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapture.cpp b/local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapture.cpp new file mode 100644 index 0000000000..9ef9d9b4fc --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapture.cpp @@ -0,0 +1,327 @@ +/* + SPDX-FileCopyrightText: 2024 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "eisinputcapture.h" + +#include "eisinputcapturemanager.h" +#include "inputcapture_logging.h" + +#include "core/output.h" +#include "input.h" +#include "input_event.h" +#include "input_event_spy.h" +#include "pointer_input.h" +#include "workspace.h" + +#include +#include + +namespace KWin +{ + +static void eis_log_handler(eis *eis, eis_log_priority priority, const char *message, eis_log_context *context) +{ + switch (priority) { + case EIS_LOG_PRIORITY_DEBUG: + qCDebug(KWIN_INPUTCAPTURE) << "Libeis:" << message; + break; + case EIS_LOG_PRIORITY_INFO: + qCInfo(KWIN_INPUTCAPTURE) << "Libeis:" << message; + break; + case EIS_LOG_PRIORITY_WARNING: + qCWarning(KWIN_INPUTCAPTURE) << "Libeis:" << message; + break; + case EIS_LOG_PRIORITY_ERROR: + qCritical(KWIN_INPUTCAPTURE) << "Libeis:" << message; + break; + } +} + +static eis_device *createDevice(eis_seat *seat, const QByteArray &name) +{ + auto device = eis_seat_new_device(seat); + + auto client = eis_seat_get_client(seat); + const char *clientName = eis_client_get_name(client); + const QByteArray deviceName = clientName + (' ' + name); + eis_device_configure_name(device, deviceName); + return device; +} + +static eis_device *createPointer(eis_seat *seat) +{ + auto device = createDevice(seat, "capture pointer"); + eis_device_configure_capability(device, EIS_DEVICE_CAP_POINTER); + eis_device_configure_capability(device, EIS_DEVICE_CAP_SCROLL); + eis_device_configure_capability(device, EIS_DEVICE_CAP_BUTTON); + eis_device_add(device); + eis_device_resume(device); + return device; +} + +static eis_device *createAbsoluteDevice(eis_seat *seat) +{ + auto device = createDevice(seat, "capture absolute device"); + auto eisDevice = device; + eis_device_configure_capability(eisDevice, EIS_DEVICE_CAP_POINTER_ABSOLUTE); + eis_device_configure_capability(eisDevice, EIS_DEVICE_CAP_SCROLL); + eis_device_configure_capability(eisDevice, EIS_DEVICE_CAP_BUTTON); + eis_device_configure_capability(eisDevice, EIS_DEVICE_CAP_TOUCH); + + const auto outputs = workspace()->outputs(); + for (const auto output : outputs) { + auto region = eis_device_new_region(eisDevice); + const QRect outputGeometry = output->geometry(); + eis_region_set_offset(region, outputGeometry.x(), outputGeometry.y()); + eis_region_set_size(region, outputGeometry.width(), outputGeometry.height()); + eis_region_set_physical_scale(region, output->scale()); + eis_region_add(region); + eis_region_unref(region); + }; + eis_device_add(device); + eis_device_resume(device); + return device; +} + +static eis_device *createKeyboard(eis_seat *seat, const RamFile &keymap) +{ + auto device = createDevice(seat, "capture keyboard"); + eis_device_configure_capability(device, EIS_DEVICE_CAP_KEYBOARD); + + if (keymap.isValid()) { + auto eisKeymap = eis_device_new_keymap(device, EIS_KEYMAP_TYPE_XKB, keymap.fd(), keymap.size()); + eis_keymap_add(eisKeymap); + eis_keymap_unref(eisKeymap); + } + + eis_device_add(device); + eis_device_resume(device); + return device; +} + +EisInputCapture::EisInputCapture(EisInputCaptureManager *manager, const QString &dbusService, QFlags allowedCapabilities) + : dbusService(dbusService) + , m_manager(manager) + , m_allowedCapabilities(allowedCapabilities) + , m_eis(eis_new(this)) + , m_socketNotifier(eis_get_fd(m_eis), QSocketNotifier::Read) +{ + eis_setup_backend_fd(m_eis); + eis_log_set_priority(m_eis, EIS_LOG_PRIORITY_DEBUG); + eis_log_set_handler(m_eis, eis_log_handler); + connect(&m_socketNotifier, &QSocketNotifier::activated, this, &EisInputCapture::handleEvents); + static int counter = 0; + m_dbusPath = QStringLiteral("/org/kde/KWin/EIS/InputCapture/%1").arg(++counter); + QDBusConnection::sessionBus().registerObject(m_dbusPath, "org.kde.KWin.EIS.InputCapture", this, QDBusConnection::ExportAllInvokables | QDBusConnection::ExportAllSignals); +} + +EisInputCapture::~EisInputCapture() +{ + if (m_absoluteDevice) { + eis_device_unref(m_absoluteDevice); + } + if (m_pointer) { + eis_device_unref(m_pointer); + } + if (m_keyboard) { + eis_device_unref(m_keyboard); + } + if (m_seat) { + eis_seat_unref(m_seat); + } + if (m_client) { + eis_client_disconnect(m_client); + } + eis_unref(m_eis); +} + +QString EisInputCapture::dbusPath() const +{ + return m_dbusPath; +} + +QList EisInputCapture::barriers() const +{ + return m_barriers; +} + +eis_device *EisInputCapture::pointer() const +{ + return m_pointer; +} + +eis_device *EisInputCapture::keyboard() const +{ + return m_keyboard; +} + +eis_device *EisInputCapture::absoluteDevice() const +{ + return m_absoluteDevice; +} + +void EisInputCapture::activate(const QPointF &position) +{ + Q_EMIT activated(++m_activationId, position); + if (m_pointer) { + eis_device_start_emulating(m_pointer, m_activationId); + } + if (m_keyboard) { + eis_device_start_emulating(m_keyboard, m_activationId); + } + if (m_absoluteDevice) { + eis_device_start_emulating(m_absoluteDevice, m_activationId); + } +} + +void EisInputCapture::deactivate() +{ + Q_EMIT deactivated(m_activationId); + if (m_pointer) { + eis_device_stop_emulating(m_pointer); + } + if (m_keyboard) { + eis_device_stop_emulating(m_keyboard); + } + if (m_absoluteDevice) { + eis_device_stop_emulating(m_absoluteDevice); + } +} + +void EisInputCapture::enable(const QList> &barriers) +{ + m_barriers.clear(); + m_barriers.reserve(barriers.size()); + for (const auto &[p1, p2] : barriers) { + if (p1.x() == p2.x()) { + m_barriers.push_back({.orientation = Qt::Vertical, .position = p1.x(), .start = p1.y(), .end = p2.y()}); + } else if (p1.y() == p2.y()) { + m_barriers.push_back({.orientation = Qt::Horizontal, .position = p1.y(), .start = p1.x(), .end = p2.x()}); + } + } +} + +void EisInputCapture::disable() +{ + if (m_manager->activeCapture() == this) { + deactivate(); + } + m_barriers.clear(); + Q_EMIT disabled(); +} + +void EisInputCapture::release(const QPointF &cursorPosition, bool applyPosition) +{ + if (m_manager->activeCapture() != this) { + return; + } + if (applyPosition) { + input()->pointer()->warp(cursorPosition); + } + deactivate(); +} + +QDBusUnixFileDescriptor EisInputCapture::connectToEIS() +{ + return QDBusUnixFileDescriptor(eis_backend_fd_add_client(m_eis)); +} + +void EisInputCapture::handleEvents() +{ + eis_dispatch(m_eis); + while (eis_event *const event = eis_get_event(m_eis)) { + switch (eis_event_get_type(event)) { + case EIS_EVENT_CLIENT_CONNECT: { + auto client = eis_event_get_client(event); + const char *clientName = eis_client_get_name(client); + if (eis_client_is_sender(client)) { + qCWarning(KWIN_INPUTCAPTURE) << "disconnecting receiving client" << clientName; + eis_client_disconnect(client); + break; + } + if (m_client) { + qCWarning(KWIN_INPUTCAPTURE) << "unexpected client connection" << clientName; + eis_client_disconnect(client); + break; + } + eis_client_connect(client); + + m_client = client; + m_seat = eis_client_new_seat(client, QByteArrayLiteral(" input capture seat").prepend(clientName)); + constexpr std::array allCapabilities{EIS_DEVICE_CAP_POINTER, EIS_DEVICE_CAP_POINTER_ABSOLUTE, EIS_DEVICE_CAP_KEYBOARD, EIS_DEVICE_CAP_TOUCH, EIS_DEVICE_CAP_SCROLL, EIS_DEVICE_CAP_BUTTON}; + for (auto capability : allCapabilities) { + if (m_allowedCapabilities & capability) { + eis_seat_configure_capability(m_seat, capability); + } + } + + eis_seat_add(m_seat); + qCDebug(KWIN_INPUTCAPTURE) << "New eis client" << clientName; + break; + } + case EIS_EVENT_CLIENT_DISCONNECT: { + auto client = eis_event_get_client(event); + if (client != m_client) { + break; + } + qCDebug(KWIN_INPUTCAPTURE) << "Client disconnected" << eis_client_get_name(client); + eis_seat_unref(std::exchange(m_seat, nullptr)); + eis_client_unref(std::exchange(m_client, nullptr)); + m_manager->removeInputCapture(QDBusObjectPath(m_dbusPath)); + return; + } + case EIS_EVENT_SEAT_BIND: { + auto seat = eis_event_get_seat(event); + qCDebug(KWIN_INPUTCAPTURE) << "Client" << eis_client_get_name(eis_event_get_client(event)) << "bound to seat" << eis_seat_get_name(seat); + if (eis_event_seat_has_capability(event, EIS_DEVICE_CAP_POINTER_ABSOLUTE) || eis_event_seat_has_capability(event, EIS_DEVICE_CAP_TOUCH)) { + if (!m_absoluteDevice) { + m_absoluteDevice = createAbsoluteDevice(seat); + } + } else if (m_absoluteDevice) { + eis_device_remove(m_absoluteDevice); + eis_device_unref(std::exchange(m_absoluteDevice, nullptr)); + } + if (eis_event_seat_has_capability(event, EIS_DEVICE_CAP_POINTER)) { + if (!m_pointer) { + m_pointer = createPointer(seat); + } + } else if (m_pointer) { + eis_device_remove(m_pointer); + eis_device_unref(std::exchange(m_pointer, nullptr)); + } + if (eis_event_seat_has_capability(event, EIS_DEVICE_CAP_KEYBOARD)) { + if (!m_keyboard) { + m_keyboard = createKeyboard(seat, m_manager->keyMap()); + } + } else if (m_keyboard) { + eis_device_remove(m_keyboard); + eis_device_unref(std::exchange(m_keyboard, nullptr)); + } + break; + } + case EIS_EVENT_DEVICE_CLOSED: { + auto device = eis_event_get_device(event); + qCDebug(KWIN_INPUTCAPTURE) << "Device" << eis_device_get_name(device) << "closed by client"; + if (device == m_pointer) { + m_pointer = nullptr; + } else if (device == m_keyboard) { + m_keyboard = nullptr; + } else if (device == m_absoluteDevice) { + m_absoluteDevice = nullptr; + } + eis_device_remove(device); + eis_device_unref(device); + break; + } + default: + qCDebug(KWIN_INPUTCAPTURE) << "Unexpected event" << eis_event_get_type(event); + break; + } + eis_event_unref(event); + } +} + +} diff --git a/local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapture.h b/local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapture.h new file mode 100644 index 0000000000..e9381f02de --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapture.h @@ -0,0 +1,72 @@ +/* + SPDX-FileCopyrightText: 2024 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include +#include +#include +#include + +#include "eisinputcapturemanager.h" +#include "input_event_spy.h" + +#include + +#include + +namespace KWin +{ +class BarrierSpy; + +class EisInputCapture : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.KWin.EIS.InputCapture") +public: + EisInputCapture(EisInputCaptureManager *manager, const QString &dbusService, QFlags allowedCapabilities); + ~EisInputCapture(); + + const QString dbusService; + + QList barriers() const; + QString dbusPath() const; + + eis_device *pointer() const; + eis_device *keyboard() const; + eis_device *absoluteDevice() const; + + void activate(const QPointF &position); + + Q_INVOKABLE QDBusUnixFileDescriptor connectToEIS(); + Q_INVOKABLE void enable(const QList> &barriers); + Q_INVOKABLE void disable(); + Q_INVOKABLE void release(const QPointF &cursorPosition, bool applyPosition); +Q_SIGNALS: + void disabled(); + void activated(uint activationId, const QPointF &cursorPosition); + void deactivated(uint activationId); + +private: + void handleEvents(); + void createDevice(); + void deactivate(); + + EisInputCaptureManager *m_manager; + QList m_barriers; + QString m_dbusPath; + QFlags m_allowedCapabilities; + eis *m_eis; + QSocketNotifier m_socketNotifier; + eis_client *m_client = nullptr; + eis_seat *m_seat = nullptr; + eis_device *m_pointer = nullptr; + eis_device *m_keyboard = nullptr; + eis_device *m_absoluteDevice = nullptr; + uint m_activationId = 0; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapturefilter.cpp b/local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapturefilter.cpp new file mode 100644 index 0000000000..8ffcc44f62 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapturefilter.cpp @@ -0,0 +1,246 @@ +/* + SPDX-FileCopyrightText: 2024 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "eisinputcapturefilter.h" + +#include "eisinputcapture.h" +#include "eisinputcapturemanager.h" + +#include "input_event.h" + +#include + +namespace KWin +{ +EisInputCaptureFilter::EisInputCaptureFilter(EisInputCaptureManager *manager) + : InputEventFilter(InputFilterOrder::EisInput) + , m_manager(manager) +{ +} + +void EisInputCaptureFilter::clearTouches() +{ + for (const auto touch : m_touches) { + eis_touch_unref(touch); + } + m_touches.clear(); +} + +bool EisInputCaptureFilter::pointerMotion(PointerMotionEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + if (const auto pointer = m_manager->activeCapture()->pointer()) { + eis_device_pointer_motion(pointer, event->delta.x(), event->delta.y()); + } + return true; +} + +bool EisInputCaptureFilter::pointerButton(PointerButtonEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + if (const auto pointer = m_manager->activeCapture()->pointer()) { + eis_device_button_button(pointer, event->nativeButton, event->state == PointerButtonState::Pressed); + } + return true; +} + +bool EisInputCaptureFilter::pointerFrame() +{ + if (!m_manager->activeCapture()) { + return false; + } + if (const auto pointer = m_manager->activeCapture()->pointer()) { + eis_device_frame(pointer, std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); + } + return true; +} + +bool EisInputCaptureFilter::pointerAxis(PointerAxisEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + if (const auto pointer = m_manager->activeCapture()->pointer()) { + if (event->delta) { + if (event->deltaV120) { + if (event->orientation == Qt::Horizontal) { + eis_device_scroll_discrete(pointer, event->deltaV120, 0); + } else { + eis_device_scroll_discrete(pointer, 0, event->deltaV120); + } + } else { + if (event->orientation == Qt::Horizontal) { + eis_device_scroll_delta(pointer, event->delta, 0); + } else { + eis_device_scroll_delta(pointer, 0, event->delta); + } + } + } else { + if (event->orientation == Qt::Horizontal) { + eis_device_scroll_stop(pointer, true, false); + } else { + eis_device_scroll_stop(pointer, false, true); + } + } + } + return true; +} + +bool EisInputCaptureFilter::keyboardKey(KeyboardKeyEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + if (const auto keyboard = m_manager->activeCapture()->keyboard()) { + eis_device_keyboard_key(keyboard, event->nativeScanCode, event->state != KeyboardKeyState::Released); + eis_device_frame(keyboard, std::chrono::duration_cast(event->timestamp).count()); + } + return true; +} + +bool EisInputCaptureFilter::touchDown(TouchDownEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + if (const auto abs = m_manager->activeCapture()->absoluteDevice()) { + auto touch = eis_device_touch_new(abs); + m_touches.insert(event->id, touch); + eis_touch_down(touch, event->pos.x(), event->pos.y()); + } + return true; +} + +bool EisInputCaptureFilter::touchMotion(TouchMotionEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + if (auto touch = m_touches.value(event->id)) { + eis_touch_motion(touch, event->pos.x(), event->pos.y()); + } + return true; +} + +bool EisInputCaptureFilter::touchUp(TouchUpEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + if (auto touch = m_touches.take(event->id)) { + eis_touch_up(touch); + eis_touch_unref(touch); + } + return true; +} +bool EisInputCaptureFilter::touchCancel() +{ + if (!m_manager->activeCapture()) { + return false; + } + for (const auto touch : m_touches) { + eis_touch_cancel(touch); + eis_touch_unref(touch); + } + m_touches.clear(); + return true; +} +bool EisInputCaptureFilter::touchFrame() +{ + if (!m_manager->activeCapture()) { + return false; + } + if (const auto abs = m_manager->activeCapture()->absoluteDevice()) { + eis_device_frame(abs, std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); + } + return true; +} + +bool EisInputCaptureFilter::pinchGestureBegin(PointerPinchGestureBeginEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} +bool EisInputCaptureFilter::pinchGestureUpdate(PointerPinchGestureUpdateEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} + +bool EisInputCaptureFilter::pinchGestureEnd(PointerPinchGestureEndEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} +bool EisInputCaptureFilter::pinchGestureCancelled(PointerPinchGestureCancelEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} + +bool EisInputCaptureFilter::swipeGestureBegin(PointerSwipeGestureBeginEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} +bool EisInputCaptureFilter::swipeGestureUpdate(PointerSwipeGestureUpdateEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} +bool EisInputCaptureFilter::swipeGestureEnd(PointerSwipeGestureEndEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} +bool EisInputCaptureFilter::swipeGestureCancelled(PointerSwipeGestureCancelEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} + +bool EisInputCaptureFilter::holdGestureBegin(PointerHoldGestureBeginEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} +bool EisInputCaptureFilter::holdGestureEnd(PointerHoldGestureEndEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} +bool EisInputCaptureFilter::holdGestureCancelled(PointerHoldGestureCancelEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} +} diff --git a/local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapturefilter.h b/local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapturefilter.h new file mode 100644 index 0000000000..dc655a4c47 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapturefilter.h @@ -0,0 +1,57 @@ +/* + SPDX-FileCopyrightText: 2024 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include "input.h" + +extern "C" { +struct eis_touch; +}; + +namespace KWin +{ +class EisInputCaptureManager; + +class EisInputCaptureFilter : public InputEventFilter +{ +public: + EisInputCaptureFilter(EisInputCaptureManager *m_manager); + + void clearTouches(); + + bool pointerMotion(PointerMotionEvent *event) override; + bool pointerButton(PointerButtonEvent *event) override; + bool pointerFrame() override; + bool pointerAxis(PointerAxisEvent *event) override; + + bool keyboardKey(KeyboardKeyEvent *event) override; + + bool touchDown(TouchDownEvent *event) override; + bool touchMotion(TouchMotionEvent *event) override; + bool touchUp(TouchUpEvent *event) override; + bool touchCancel() override; + bool touchFrame() override; + + bool pinchGestureBegin(PointerPinchGestureBeginEvent *event) override; + bool pinchGestureUpdate(PointerPinchGestureUpdateEvent *event) override; + bool pinchGestureEnd(PointerPinchGestureEndEvent *event) override; + bool pinchGestureCancelled(PointerPinchGestureCancelEvent *event) override; + + bool swipeGestureBegin(PointerSwipeGestureBeginEvent *event) override; + bool swipeGestureUpdate(PointerSwipeGestureUpdateEvent *event) override; + bool swipeGestureEnd(PointerSwipeGestureEndEvent *event) override; + bool swipeGestureCancelled(PointerSwipeGestureCancelEvent *event) override; + + bool holdGestureBegin(PointerHoldGestureBeginEvent *event) override; + bool holdGestureEnd(PointerHoldGestureEndEvent *event) override; + bool holdGestureCancelled(PointerHoldGestureCancelEvent *event) override; + +private: + EisInputCaptureManager *m_manager; + QHash m_touches; +}; +} diff --git a/local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapturemanager.cpp b/local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapturemanager.cpp new file mode 100644 index 0000000000..3d8211d387 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapturemanager.cpp @@ -0,0 +1,221 @@ +/* + SPDX-FileCopyrightText: 2024 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "eisinputcapturemanager.h" + +#include "eisinputcapture.h" +#include "eisinputcapturefilter.h" +#include "inputcapture_logging.h" + +#include "core/output.h" +#include "cursor.h" +#include "input_event.h" +#include "input_event_spy.h" +#include "keyboard_input.h" +#include "keyboard_layout.h" +#include "workspace.h" +#include "xkb.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include + +namespace KWin +{ + +constexpr QKeyCombination defaultDisableKeys{Qt::META | Qt::SHIFT, Qt::Key_Escape}; + +class BarrierSpy : public InputEventSpy +{ +public: + BarrierSpy(EisInputCaptureManager *manager) + : manager(manager) + { + } + void pointerMotion(KWin::PointerMotionEvent *event) override + { + if (manager->activeCapture()) { + return; + } + for (const auto &capture : manager->m_inputCaptures) { + for (const auto &barrier : capture->barriers()) { + // Detect the user trying to move out of the workArea and across the barrier: + // Both current and previous positions are on the barrier but there was an orthogonal delta + if (barrier.hitTest(event->position) && barrier.hitTest(previousPos) && ((barrier.orientation == Qt::Vertical && event->delta.x() != 0) || (barrier.orientation == Qt::Horizontal && event->delta.y() != 0))) { + qCDebug(KWIN_INPUTCAPTURE) << "Activating input capture, crossing" + << "barrier(" << barrier.orientation << barrier.position << "[" << barrier.start << "," << barrier.end << "])" + << "at" << event->position << "with" << event->delta; + manager->barrierHit(capture.get(), event->position + event->delta); + break; + } + } + } + previousPos = event->position; + } + void keyboardKey(KWin::KeyboardKeyEvent *event) override + { + if (!manager->activeCapture()) { + return; + } + if (event->state != KeyboardKeyState::Pressed) { + return; + } + // Even if the user removed all sequences for this, we use the default one to have an escape hatch + auto disableKeySequence = KGlobalAccel::self()->shortcut(manager->m_disableCaptureAction).value(0, defaultDisableKeys)[0]; + if (event->key == disableKeySequence.key() && event->modifiers == disableKeySequence.keyboardModifiers()) { + manager->activeCapture()->disable(); + } + } + +private: + QKeyCombination currentCombination; + EisInputCaptureManager *manager; + QPointF previousPos; +}; + +bool EisInputCaptureBarrier::hitTest(const QPointF &point) const +{ + if (orientation == Qt::Vertical) { + return point.x() == position && start <= point.y() && point.y() <= end; + } + return point.y() == position && start <= point.x() && point.x() <= end; +} + +EisInputCaptureManager::EisInputCaptureManager() + : m_serviceWatcher(new QDBusServiceWatcher(this)) + , m_barrierSpy(std::make_unique(this)) + , m_inputFilter(std::make_unique(this)) +{ + qDBusRegisterMetaType>(); + qDBusRegisterMetaType>>(); + + const auto keymap = input()->keyboard()->xkb()->keymapContents(); + if (!keymap.isEmpty()) { + m_keymapFile = RamFile("input capture keymap", keymap.data(), keymap.size(), RamFile::Flag::SealWrite); + } + connect(input()->keyboard()->keyboardLayout(), &KeyboardLayout::layoutChanged, this, [this] { + const auto keymap = input()->keyboard()->xkb()->keymapContents(); + if (!keymap.isEmpty()) { + m_keymapFile = RamFile("input capture keymap", keymap.data(), keymap.size(), RamFile::Flag::SealWrite); + } else { + m_keymapFile = RamFile(); + } + }); + + m_serviceWatcher->setConnection(QDBusConnection::sessionBus()); + m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration); + connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &service) { + if (m_activeCapture && m_activeCapture->dbusService == service) { + deactivate(); + } + std::erase_if(m_inputCaptures, [&service](const std::unique_ptr &capture) { + return capture->dbusService == service; + }); + m_serviceWatcher->removeWatchedService(service); + }); + + m_disableCaptureAction = new QAction(this); + m_disableCaptureAction->setProperty("componentName", QStringLiteral("kwin")); + m_disableCaptureAction->setObjectName(QStringLiteral("disableInputCapture")); + m_disableCaptureAction->setText(i18nc("@action shortcut", "Disable Active Input Capture")); + KGlobalAccel::setGlobalShortcut(m_disableCaptureAction, QKeySequence(defaultDisableKeys)); + + QDBusConnection::sessionBus().registerObject("/org/kde/KWin/EIS/InputCapture", "org.kde.KWin.EIS.InputCaptureManager", this, QDBusConnection::ExportAllInvokables | QDBusConnection::ExportAllSignals); +} + +EisInputCaptureManager::~EisInputCaptureManager() +{ + if (input()) { + input()->uninstallInputEventFilter(m_inputFilter.get()); + input()->uninstallInputEventSpy(m_barrierSpy.get()); + } +} + +const RamFile &EisInputCaptureManager::keyMap() const +{ + return m_keymapFile; +} + +void EisInputCaptureManager::removeInputCapture(const QDBusObjectPath &capture) +{ + auto it = std::ranges::find(m_inputCaptures, capture.path(), &EisInputCapture::dbusPath); + if (it == std::ranges::end(m_inputCaptures)) { + return; + } + if (m_activeCapture == it->get()) { + deactivate(); + } + m_inputCaptures.erase(it); + if (m_inputCaptures.empty()) { + input()->uninstallInputEventSpy(m_barrierSpy.get()); + } +} + +QDBusObjectPath EisInputCaptureManager::addInputCapture(uint capabilities) +{ + constexpr uint keyboardPortal = 1; + constexpr uint pointerPortal = 2; + constexpr uint touchPortal = 4; + QFlags eisCapabilities; + if (capabilities & keyboardPortal) { + eisCapabilities |= EIS_DEVICE_CAP_KEYBOARD; + } + if (capabilities & pointerPortal) { + eisCapabilities |= EIS_DEVICE_CAP_POINTER; + eisCapabilities |= EIS_DEVICE_CAP_POINTER_ABSOLUTE; + eisCapabilities |= EIS_DEVICE_CAP_BUTTON; + eisCapabilities |= EIS_DEVICE_CAP_SCROLL; + } + if (capabilities & touchPortal) { + eisCapabilities |= EIS_DEVICE_CAP_TOUCH; + } + + const QString dbusService = message().service(); + m_serviceWatcher->addWatchedService(dbusService); + + if (m_inputCaptures.empty()) { + input()->installInputEventSpy(m_barrierSpy.get()); + } + auto &capture = m_inputCaptures.emplace_back(std::make_unique(this, dbusService, eisCapabilities)); + connect(capture.get(), &EisInputCapture::deactivated, this, [this] { + deactivate(); + }); + return QDBusObjectPath(capture->dbusPath()); +} + +void EisInputCaptureManager::barrierHit(KWin::EisInputCapture *capture, const QPointF &position) +{ + if (m_activeCapture) { + return; + } + m_activeCapture = capture; + capture->activate(position); + input()->installInputEventFilter(m_inputFilter.get()); + // Even though the input events are filtered out the cursor is updated on screen which looks weird + Cursors::self()->hideCursor(); +} + +void EisInputCaptureManager::deactivate() +{ + m_activeCapture = nullptr; + m_inputFilter->clearTouches(); + input()->uninstallInputEventFilter(m_inputFilter.get()); + Cursors::self()->showCursor(); +} + +EisInputCapture *EisInputCaptureManager::activeCapture() +{ + return m_activeCapture; +} + +} diff --git a/local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapturemanager.h b/local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapturemanager.h new file mode 100644 index 0000000000..8fee41adc8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eis/eisinputcapturemanager.h @@ -0,0 +1,62 @@ +/* + SPDX-FileCopyrightText: 2024 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include "utils/ramfile.h" + +#include +#include +#include +#include + +#include + +class QAction; +class QDBusServiceWatcher; + +namespace KWin +{ +class BarrierSpy; +class EisInputCapture; +class EisInputCaptureFilter; + +struct EisInputCaptureBarrier +{ + const Qt::Orientation orientation; + const int position; + const int start; + const int end; + bool hitTest(const QPointF &point) const; +}; + +class EisInputCaptureManager : public QObject, public QDBusContext +{ + Q_OBJECT +public: + EisInputCaptureManager(); + ~EisInputCaptureManager(); + + Q_INVOKABLE QDBusObjectPath addInputCapture(uint capabilities); + Q_INVOKABLE void removeInputCapture(const QDBusObjectPath &capture); + + const RamFile &keyMap() const; + + void barrierHit(EisInputCapture *capture, const QPointF &position); + EisInputCapture *activeCapture(); + void deactivate(); + +private: + RamFile m_keymapFile; + QDBusServiceWatcher *m_serviceWatcher; + std::unique_ptr m_barrierSpy; + std::unique_ptr m_inputFilter; + std::vector> m_inputCaptures; + EisInputCapture *m_activeCapture = nullptr; + QAction *m_disableCaptureAction; + friend class BarrierSpy; +}; +} diff --git a/local/recipes/kde/kwin/source/src/plugins/eis/eisplugin.cpp b/local/recipes/kde/kwin/source/src/plugins/eis/eisplugin.cpp new file mode 100644 index 0000000000..d2e16f4731 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eis/eisplugin.cpp @@ -0,0 +1,23 @@ +/* + SPDX-FileCopyrightText: 2024 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "eisplugin.h" + +#include "eisbackend.h" +#include "eisinputcapturemanager.h" + +#include "input.h" + +EisPlugin::EisPlugin() + : Plugin() + , m_inputCapture(std::make_unique()) +{ + KWin::input()->addInputBackend(std::make_unique()); +} + +EisPlugin::~EisPlugin() +{ +} diff --git a/local/recipes/kde/kwin/source/src/plugins/eis/eisplugin.h b/local/recipes/kde/kwin/source/src/plugins/eis/eisplugin.h new file mode 100644 index 0000000000..b1b1ef936e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eis/eisplugin.h @@ -0,0 +1,25 @@ +/* + SPDX-FileCopyrightText: 2024 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include "plugin.h" + +namespace KWin +{ +class EisInputCaptureManager; +}; + +class EisPlugin : public KWin::Plugin +{ + Q_OBJECT +public: + EisPlugin(); + ~EisPlugin() override; + +private: + std::unique_ptr m_inputCapture; +}; diff --git a/local/recipes/kde/kwin/source/src/plugins/eis/main.cpp b/local/recipes/kde/kwin/source/src/plugins/eis/main.cpp new file mode 100644 index 0000000000..15ca422bbd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eis/main.cpp @@ -0,0 +1,24 @@ +/* + SPDX-FileCopyrightText: 2024 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "plugin.h" + +#include "eisplugin.h" + +class KWIN_EXPORT EisPluginFactory : public KWin::PluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID PluginFactory_iid FILE "metadata.json") + Q_INTERFACES(KWin::PluginFactory) + +public: + std::unique_ptr create() const override + { + return std::make_unique(); + } +}; + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/eis/metadata.json b/local/recipes/kde/kwin/source/src/plugins/eis/metadata.json new file mode 100644 index 0000000000..aa304f4093 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eis/metadata.json @@ -0,0 +1,5 @@ +{ + "KPlugin": { + "EnabledByDefault": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/eyeonscreen/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/eyeonscreen/CMakeLists.txt new file mode 100644 index 0000000000..e12019ed3b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eyeonscreen/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_scripted_effect(eyeonscreen package) diff --git a/local/recipes/kde/kwin/source/src/plugins/eyeonscreen/package/contents/code/main.js b/local/recipes/kde/kwin/source/src/plugins/eyeonscreen/package/contents/code/main.js new file mode 100644 index 0000000000..7758e6f2de --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eyeonscreen/package/contents/code/main.js @@ -0,0 +1,97 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Thomas Lübking + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +/*global effect, effects, animate, animationTime, Effect, QEasingCurve */ + +"use strict"; + +var eyeOnScreenEffect = { + duration: animationTime(250), + loadConfig: function () { + eyeOnScreenEffect.duration = animationTime(250); + }, + slurp: function (showing) { + var stackingOrder = effects.stackingOrder; + for (var i = 0; i < stackingOrder.length; ++i) { + var w = stackingOrder[i]; + if (!w.visible || !(showing || w.slurpedByEyeOnScreen)) { + continue; + } + const screenGeo = w.screen.geometry; + const center = { value1: screenGeo.x + screenGeo.width / 2, + value2: screenGeo.y + screenGeo.height / 2 }; + w.slurpedByEyeOnScreen = showing; + if (w.desktopWindow) { + // causes "seizures" because of opposing movements + // var zoom = showing ? 0.8 : 1.2; + var zoom = 0.8; + w.eyeOnScreenShowsDesktop = showing; + animate({ + window: w, + duration: 2*eyeOnScreenEffect.duration, // "*2 for "bumper" transition + animations: [{ + type: Effect.Scale, + curve: Effect.GaussianCurve, + to: zoom + }, { + type: Effect.Opacity, + curve: Effect.GaussianCurve, + to: 0.0 + }] + }); + } else { + if (showing) { + animate({ + window: w, + animations: [{ + type: Effect.Scale, + curve: QEasingCurve.InCubic, + duration: eyeOnScreenEffect.duration, + to: 0.0 + }, { + type: Effect.Position, + curve: QEasingCurve.InCubic, + duration: eyeOnScreenEffect.duration, + to: center + }, { + type: Effect.Opacity, + curve: QEasingCurve.InCubic, + duration: eyeOnScreenEffect.duration, + to: 0.0 + }] + }); + } else { + animate({ + window: w, + duration: eyeOnScreenEffect.duration, + delay: eyeOnScreenEffect.duration, + animations: [{ + type: Effect.Scale, + curve: QEasingCurve.OutCubic, + from: 0.0 + }, { + type: Effect.Position, + curve: QEasingCurve.OutCubic, + from: center + }, { + type: Effect.Opacity, + curve: QEasingCurve.OutCubic, + from: 0.0 + }] + }); + } + } + } + }, + init: function () { + eyeOnScreenEffect.loadConfig(); + effects.showingDesktopChanged.connect(eyeOnScreenEffect.slurp); + } +}; + +eyeOnScreenEffect.init(); diff --git a/local/recipes/kde/kwin/source/src/plugins/eyeonscreen/package/metadata.json b/local/recipes/kde/kwin/source/src/plugins/eyeonscreen/package/metadata.json new file mode 100644 index 0000000000..1873ea33e1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/eyeonscreen/package/metadata.json @@ -0,0 +1,157 @@ +{ + "KPackageStructure": "KWin/Effect", + "KPlugin": { + "Authors": [ + { + "Email": "thomas.luebking@gmail.com", + "Name": "Thomas Lübking", + "Name[ar]": "توماس لوبكينج", + "Name[be]": "Thomas Lübking", + "Name[bg]": "Thomas Lübking", + "Name[ca@valencia]": "Thomas Lübking", + "Name[ca]": "Thomas Lübking", + "Name[cs]": "Thomas Lübking", + "Name[da]": "Thomas Lübking", + "Name[de]": "Thomas Lübking", + "Name[en_GB]": "Thomas Lübking", + "Name[eo]": "Thomas Lübking", + "Name[es]": "Thomas Lübking", + "Name[et]": "Thomas Lübking", + "Name[eu]": "Thomas Lübking", + "Name[fi]": "Thomas Lübking", + "Name[fr]": "Thomas Lübking", + "Name[ga]": "Thomas Lübking", + "Name[gl]": "Thomas Lübking.", + "Name[he]": "תומס ליובקינג", + "Name[hu]": "Thomas Lübking", + "Name[ia]": "Thomas Lübking", + "Name[id]": "Thomas Lübking", + "Name[is]": "Thomas Lübking", + "Name[it]": "Thomas Lübking", + "Name[ja]": "Thomas Lübking", + "Name[ka]": "Thomas Lübking", + "Name[ko]": "Thomas Lübking", + "Name[lt]": "Thomas Lübking", + "Name[lv]": "Thomas Lübking", + "Name[nb]": "Thomas Lübking", + "Name[nl]": "Thomas Lübking", + "Name[nn]": "Thomas Lübking", + "Name[pl]": "Thomas Lübking", + "Name[pt]": "Thomas Lübking", + "Name[pt_BR]": "Thomas Lübking", + "Name[ro]": "Thomas Lübking", + "Name[ru]": "Thomas Lübking", + "Name[sa]": "थोमस लुब्किङ्ग्", + "Name[sk]": "Thomas Lübking", + "Name[sl]": "Thomas Lübking", + "Name[sv]": "Thomas Lübking", + "Name[ta]": "தாமஸ் லுபுகிங்", + "Name[tr]": "Thomas Lübking", + "Name[uk]": "Thomas Lübking", + "Name[vi]": "Thomas Lübking", + "Name[zh_CN]": "Thomas Lübking", + "Name[zh_TW]": "Thomas Lübking" + } + ], + "Category": "Show Desktop Animation", + "Description": "Suck windows into the desktop", + "Description[ar]": "تمتص النوافذ في سطح المكتب", + "Description[be]": "Эфект усмоктвання акон у цэнтр экрана", + "Description[bg]": "Поглъщане на прозорците в работния плот", + "Description[ca@valencia]": "Apega les finestres a l'escriptori", + "Description[ca]": "Enganxa les finestres a l'escriptori", + "Description[cs]": "Vtáhnout okna do pracovní plochy", + "Description[da]": "Sug vinduer ind i skrivebordet", + "Description[de]": "Fenster in die Arbeitsfläche saugen", + "Description[en_GB]": "Suck windows into the desktop", + "Description[eo]": "Suĉi fenestrojn en la labortablon", + "Description[es]": "Aspirar las ventanas en el escritorio", + "Description[et]": "Akende imemine töölauale", + "Description[eu]": "Mahaigainak leihoak xurgatzea", + "Description[fi]": "Imaise ikkunat työpöydälle", + "Description[fr]": "Aspirer les fenêtres dans le bureau", + "Description[gl]": "Chupar as xanelas ao escritorio.", + "Description[he]": "שאיבת חלונות לתוך שולחן העבודה", + "Description[hu]": "Az asztal beszippantja az ablakokat", + "Description[ia]": "Suger fenestras in le scriptorio", + "Description[id]": "Sedot jendela ke dalam desktop", + "Description[is]": "Soga glugga inn í skjáborðið", + "Description[it]": "Risucchia le finestre nel desktop", + "Description[ja]": "ウィンドウをデスクトップに吸い込みます", + "Description[ka]": "ფანჯრების სამუშაო მაგიდაში შეწოვა", + "Description[ko]": "창을 바탕 화면으로 흡수", + "Description[lt]": "Įtraukti langus į darbalaukį", + "Description[lv]": "Iesūc logus darbvirsmā", + "Description[nb]": "Sug vinduer inn i skrivebordet", + "Description[nl]": "Zuig vensters in het bureaublad", + "Description[nn]": "Sug vindauge inn i skrivebordet", + "Description[pl]": "Zasysa okna na pulpicie", + "Description[pt]": "Aspirar as janelas para o ecrã", + "Description[pt_BR]": "Sugar janelas para a área de trabalho", + "Description[ro]": "Suge ferestrele în birou", + "Description[ru]": "Втягивание окон в центр рабочего стола", + "Description[sa]": "डेस्कटॉप् मध्ये विण्डोस् चूषयन्तु", + "Description[sk]": "Vtiahnutie okien do pracovnej plochy", + "Description[sl]": "Posesajte okna na namizje", + "Description[sv]": "Sug in fönster i skrivbordet", + "Description[ta]": "பணிமேடைக்குள் சாளரங்கள் உறிஞ்சப்படுவதுபோல் அசைவூட்டும்", + "Description[tr]": "Pencereleri masaüstüne çek", + "Description[uk]": "Засмоктування вікон до стільниці", + "Description[vi]": "Hút các cửa sổ vào bàn làm việc", + "Description[zh_CN]": "按下显示桌面时所有窗口被内收到桌面中心消失", + "Description[zh_TW]": "將視窗吸進桌面", + "EnabledByDefault": false, + "Icon": "preferences-system-windows-effect-eyeonscreen", + "Id": "eyeonscreen", + "License": "GPL", + "Name": "Eye on Screen", + "Name[ar]": "عين على الشاشة", + "Name[be]": "Экраннае вока", + "Name[bg]": "Поглед върху екрана", + "Name[ca@valencia]": "Ull a la pantalla", + "Name[ca]": "Ull a la pantalla", + "Name[cs]": "Oko na obrazovce", + "Name[da]": "Øje på skærm", + "Name[de]": "Ansicht der Arbeitsfläche", + "Name[en_GB]": "Eye on Screen", + "Name[eo]": "Okulo sur Ekrano", + "Name[es]": "Ojo en la pantalla", + "Name[et]": "Eye on Screen", + "Name[eu]": "Begirada pantailan", + "Name[fi]": "Silmä näytöllä", + "Name[fr]": "Les yeux sur l'écran", + "Name[gl]": "Ollo na pantalla", + "Name[he]": "עין על המסך", + "Name[hu]": "A képernyő szeme", + "Name[ia]": "Eye On Screen (Oculo sur schermo)", + "Name[id]": "Mata di Layar", + "Name[is]": "Auga á skjá", + "Name[it]": "Occhio su schermo", + "Name[ja]": "スクリーンの目", + "Name[ka]": "სენსორული ეკრანი", + "Name[ko]": "화면 위의 눈", + "Name[lt]": "Akutė ekrane", + "Name[lv]": "Acs ekrānā", + "Name[nb]": "Øye på skjerm", + "Name[nl]": "Oog op scherm", + "Name[nn]": "Auge på skjerm", + "Name[pl]": "Oko na ekranie", + "Name[pt]": "Olho no Ecrã", + "Name[pt_BR]": "Olho na tela", + "Name[ro]": "Ochi pe ecran", + "Name[ru]": "Втягивание окон в центр экрана", + "Name[sa]": "पर्दायां नेत्रम्", + "Name[sk]": "Oko na obrazovke", + "Name[sl]": "Oko na zaslonu", + "Name[sv]": "Ögat på skärmen", + "Name[ta]": "திரையில் கண்", + "Name[tr]": "Göz Ekranda", + "Name[uk]": "Око на екрані", + "Name[vi]": "Theo dõi màn hình", + "Name[zh_CN]": "窗口内收", + "Name[zh_TW]": "螢幕之眼" + }, + "X-KDE-Ordering": 50, + "X-KWin-Exclusive-Category": "show-desktop", + "X-Plasma-API": "javascript" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/fade/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/fade/CMakeLists.txt new file mode 100644 index 0000000000..56d9d6d2c7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fade/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_scripted_effect(fade package) diff --git a/local/recipes/kde/kwin/source/src/plugins/fade/package/contents/code/main.js b/local/recipes/kde/kwin/source/src/plugins/fade/package/contents/code/main.js new file mode 100644 index 0000000000..ab6f74f6a6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fade/package/contents/code/main.js @@ -0,0 +1,120 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Philip Falkner + SPDX-FileCopyrightText: 2012 Martin Gräßlin + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +const blacklist = { + // The logout screen has to be animated only by the logout effect. + "ksmserver ksmserver": [], + "ksmserver-logout-greeter ksmserver-logout-greeter": [], + + // KDE Plasma splash screen has to be animated only by the login effect. + "ksplashqml ksplashqml": [], + + "spectacle org.kde.spectacle": ["region-editor"], +}; + +class FadeEffect { + constructor() { + effect.configChanged.connect(this.loadConfig.bind(this)); + effects.windowAdded.connect(this.onWindowAdded.bind(this)); + effects.windowClosed.connect(this.onWindowClosed.bind(this)); + effects.windowDataChanged.connect(this.onWindowDataChanged.bind(this)); + + this.loadConfig(); + } + + loadConfig() { + this.fadeInTime = animationTime(effect.readConfig("FadeInTime", 150)); + this.fadeOutTime = animationTime(effect.readConfig("FadeOutTime", 150)) * 4; + } + + static isFadeWindow(w) { + const blacklistedTags = blacklist[w.windowClass]; + if (blacklistedTags && (blacklistedTags.length === 0 || blacklistedTags.includes(w.tag))) { + return false; + } + if (w.popupWindow) { + return false; + } + if (!w.managed) { + return false; + } + if (!w.visible) { + return false; + } + if (w.outline) { + return false; + } + if (w.deleted && effect.isGrabbed(w, Effect.WindowClosedGrabRole)) { + return false; + } else if (!w.deleted && effect.isGrabbed(w, Effect.WindowAddedGrabRole)) { + return false; + } + return w.normalWindow || w.dialog; + } + + onWindowAdded(window) { + if (effects.hasActiveFullScreenEffect) { + return; + } + if (!FadeEffect.isFadeWindow(window)) { + return; + } + if (window.fadeOutAnimation) { + cancel(window.fadeOutAnimation); + delete window.fadeOutAnimation; + } + window.fadeInAnimation = effect.animate({ + window, + duration: this.fadeInTime, + type: Effect.Opacity, + to: 1.0, + from: 0.0 + }); + } + + onWindowClosed(window) { + if (effects.hasActiveFullScreenEffect) { + return; + } + if (window.skipsCloseAnimation || !FadeEffect.isFadeWindow(window)) { + return; + } + window.fadeOutAnimation = animate({ + window, + duration: this.fadeOutTime, + animations: [{ + type: Effect.Opacity, + curve: QEasingCurve.OutQuart, + to: 0.0 + }] + }); + } + + onWindowDataChanged(window, role) { + if (role == Effect.WindowAddedGrabRole) { + if (effect.isGrabbed(window, Effect.WindowAddedGrabRole)) { + if (window.fadeInAnimation) { + cancel(window.fadeInAnimation); + delete window.fadeInAnimation; + } + } + } else if (role == Effect.WindowClosedGrabRole) { + if (effect.isGrabbed(window, Effect.WindowClosedGrabRole)) { + if (window.fadeOutAnimation) { + cancel(window.fadeOutAnimation); + delete window.fadeOutAnimation; + } + } + } + } +} + +new FadeEffect(); diff --git a/local/recipes/kde/kwin/source/src/plugins/fade/package/contents/config/main.xml b/local/recipes/kde/kwin/source/src/plugins/fade/package/contents/config/main.xml new file mode 100644 index 0000000000..2e0373f77d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fade/package/contents/config/main.xml @@ -0,0 +1,20 @@ + + + + + + + true + + + 150 + + + 150 + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/fade/package/metadata.json b/local/recipes/kde/kwin/source/src/plugins/fade/package/metadata.json new file mode 100644 index 0000000000..4bb6919d22 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fade/package/metadata.json @@ -0,0 +1,144 @@ +{ + "KPackageStructure": "KWin/Effect", + "KPlugin": { + "Authors": [ + { + "Email": "philip.falkner@gmail.com, mgraesslin@kde.org", + "Name": "Philip Falkner, Martin Gräßlin", + "Name[ar]": "فيليب فالكنر، مارتن جراجلين", + "Name[be]": "Philip Falkner, Martin Gräßlin", + "Name[bg]": "Philip Falkner, Martin Gräßlin", + "Name[ca@valencia]": "Philip Falkner, Martin Gräßlin", + "Name[ca]": "Philip Falkner, Martin Gräßlin", + "Name[cs]": "Philip Falkner, Martin Gräßlin", + "Name[da]": "Philip Falkner, Martin Gräßlin", + "Name[de]": "Philip Falkner, Martin Gräßlin", + "Name[en_GB]": "Philip Falkner, Martin Gräßlin", + "Name[eo]": "Philip Falkner, Martin Gräßlin", + "Name[es]": "Philip Falkner, Martin Gräßlin", + "Name[et]": "Philip Falkner, Martin Gräßlin", + "Name[eu]": "Philip Falkner, Martin Gräßlin", + "Name[fi]": "Philip Falkner, Martin Gräßlin", + "Name[fr]": "Philip Falkner, Martin Gräßlin", + "Name[ga]": "Philip Falkner, Martin Gräßlin", + "Name[gl]": "Philip Falkner e Martin Gräßlin.", + "Name[he]": "פיליפ פולקנר, מרטין גרייסלין", + "Name[hu]": "Philip Falkner, Martin Gräßlin", + "Name[ia]": "Philip Falkner, Martin Gräßlin", + "Name[id]": "Philip Falkner, Martin Gräßlin", + "Name[is]": "Philip Falkner, Martin Gräßlin", + "Name[it]": "Philip Falkner, Martin Gräßlin", + "Name[ja]": "Philip Falkner, Martin Gräßlin", + "Name[ka]": "Philip Falkner, Martin Gräßlin", + "Name[ko]": "Philip Falkner, Martin Gräßlin", + "Name[lt]": "Philip Falkner, Martin Gräßlin", + "Name[lv]": "Philip Falkner, Martin Gräßlin", + "Name[nb]": "Philip Falkner, Martin Gräßlin", + "Name[nl]": "Philip Falkner, Martin Gräßlin", + "Name[nn]": "Philip Falkner, Martin Gräßlin", + "Name[pl]": "Philip Falkner, Martin Gräßlin", + "Name[pt]": "Philip Falkner, Martin Gräßlin", + "Name[pt_BR]": "Philip Falkner, Martin Gräßlin", + "Name[ro]": "Philip Falkner, Martin Gräßlin", + "Name[ru]": "Philip Falkner, Martin Gräßlin", + "Name[sa]": "फिलिप Falkner, मार्टिन Gräßlin", + "Name[sk]": "Philip Falkner, Martin Gräßlin", + "Name[sl]": "Philip Falkner, Martin Gräßlin", + "Name[sv]": "Philip Falkner, Martin Gräßlin", + "Name[ta]": "ஃபிலிப்பு ஃபால்குனர், மார்ட்டின் கிராஸ்லின்", + "Name[tr]": "Philip Falkner, Martin Gräßlin", + "Name[uk]": "Philip Falkner, Martin Gräßlin", + "Name[vi]": "Philip Falkner, Martin Gräßlin", + "Name[zh_CN]": "Philip Falkner, Martin Gräßlin", + "Name[zh_TW]": "Philip Falkner, Martin Gräßlin" + } + ], + "Category": "Window Open/Close Animation", + "Description": "Make windows fade in and out when they are shown or hidden", + "Description[ar]": "اجعل النوافذ تظهر وتتلاشى عند إظهاراها وإخفائها", + "Description[bg]": "Избледняване при показване и скриване на прозорците", + "Description[ca@valencia]": "Fa que les finestres s'encengen o s'apaguen quan es mostren o s'oculten", + "Description[ca]": "Fa que les finestres s'encenguin o s'apaguin quan es mostren o s'oculten", + "Description[de]": "Fenster beim Anzeigen/Verstecken ein- bzw. ausblenden", + "Description[es]": "Hacer que las ventanas se desvanezcan y reaparezcan al mostrarlas y al ocultarlas", + "Description[eu]": "Leihoak moteldu/koloreztatu haiek erakustean edo ezkutatzean", + "Description[fi]": "Häivyttää ikkunat pehmeästi näytölle tai näytöltä", + "Description[fr]": "Effectuer un fondu avant ou arrière des fenêtres lors de leurs affichages ou de leurs masquages", + "Description[he]": "לגרום לחלונות להתעמעם פנימה והחוצה כשהם מופיעים או מוסתרים", + "Description[hu]": "Az ablakok áttűnő módon lesznek elrejtve és megjelenítve", + "Description[ia]": "Face que fenestras pote pallidir intra e foras quando illos es monstrate o celate", + "Description[is]": "Láta glugga hverfa og birtast mjúklega þegar þeir eru birtir eða faldir", + "Description[it]": "Fai dissolvere e comparire gradualmente le finestre quando sono mostrate o nascoste", + "Description[ja]": "ウィンドウが表示/非表示時にフェードします", + "Description[ka]": "ფანჯრების მინავლება და გამოჩენა, როცა ისინი იმალება ან გამოჩნდებიან", + "Description[ko]": "창이 보여지거나 감춰질 때 페이드 인/아웃을 사용합니다", + "Description[lt]": "Padaryti, kad langai laipsniškai atsirastų ir išnyktų, kai yra rodomi ar slepiami", + "Description[lv]": "Likt logiem izgaist un lēnām parādīties, kad tos parādāt vai slēpjat.", + "Description[nl]": "Laat vensters vloeiend opkomen/vervagen als ze worden weergegeven of verborgen", + "Description[nn]": "Ton vindauge inn og ut når dei vert viste eller gøymde", + "Description[pl]": "Okna wyłaniają się przy otwieraniu i zanikają przy zamykaniu", + "Description[pt_BR]": "Faz com que as janelas transicionem suavemente e esmaeçam quando forem mostradas ou ocultadas", + "Description[ro]": "Face ferestrele să se estompeze când sunt arătate sau ascunse", + "Description[ru]": "Закрывающиеся окна будут становиться всё более прозрачными, а потом совсем исчезать", + "Description[sk]": "Plynulé objavovanie a miznutie okien pri ich zobrazení alebo skrytí", + "Description[sl]": "Naj okna zbledijo in se spet obarvajo, ko so prikazana ali skrita", + "Description[sv]": "Gör att fönster tonas när de visas eller döljs", + "Description[tr]": "Pencereler gösterilirken veya gizlenirken solarak geçiş yapmalarını sağla", + "Description[uk]": "Затемнення або освітлення при появі або зникненні вікон", + "Description[zh_CN]": "窗口显示/隐藏时呈现渐入渐出过渡动效", + "Description[zh_TW]": "讓視窗在顯示或隱藏的時候淡入或淡出", + "EnabledByDefault": false, + "Icon": "preferences-system-windows-effect-fade", + "Id": "fade", + "License": "GPL", + "Name": "Fade", + "Name[ar]": "التلاشي", + "Name[be]": "Зацямненне", + "Name[bg]": "Избледняване", + "Name[ca@valencia]": "Esvaïment", + "Name[ca]": "Esvaïment", + "Name[cs]": "Blednutí", + "Name[da]": "Fade", + "Name[de]": "Verblassen", + "Name[en_GB]": "Fade", + "Name[eo]": "Dissolvi", + "Name[es]": "Desvanecer", + "Name[et]": "Hääbumine", + "Name[eu]": "Koloregabetu", + "Name[fi]": "Häivytä", + "Name[fr]": "Atténuer", + "Name[gl]": "Esvaer", + "Name[he]": "הדעכה", + "Name[hu]": "Elhalványulás", + "Name[ia]": "Pallidi", + "Name[id]": "Lesap", + "Name[is]": "Deyfa", + "Name[it]": "Dissolvi", + "Name[ja]": "フェード", + "Name[ka]": "მინავლება", + "Name[ko]": "페이드", + "Name[lt]": "Laipsniškas išnykimas ir atsiradimas", + "Name[lv]": "Izgaist", + "Name[nb]": "Ton inn og ut", + "Name[nl]": "Vervagen", + "Name[nn]": "Ton inn og ut", + "Name[pl]": "Zanikanie", + "Name[pt]": "Desvanecer", + "Name[pt_BR]": "Esmaecer", + "Name[ro]": "Estompare", + "Name[ru]": "Растворение", + "Name[sa]": "म्लै", + "Name[sk]": "Blednutie", + "Name[sl]": "Preliv", + "Name[sv]": "Tona", + "Name[ta]": "மங்கசெய்", + "Name[tr]": "Soldur", + "Name[uk]": "Згасання", + "Name[vi]": "Làm mờ dần", + "Name[zh_CN]": "淡出淡入动画", + "Name[zh_TW]": "淡化" + }, + "X-KDE-Ordering": 60, + "X-KWin-Exclusive-Category": "toplevel-open-close-animation", + "X-Plasma-API": "javascript" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/fadedesktop/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/fadedesktop/CMakeLists.txt new file mode 100644 index 0000000000..6ff23e4e22 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fadedesktop/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_scripted_effect(fadedesktop package) diff --git a/local/recipes/kde/kwin/source/src/plugins/fadedesktop/package/contents/code/main.js b/local/recipes/kde/kwin/source/src/plugins/fadedesktop/package/contents/code/main.js new file mode 100644 index 0000000000..ce186b3a65 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fadedesktop/package/contents/code/main.js @@ -0,0 +1,124 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2012 Martin Gräßlin + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var fadeDesktopEffect = { + loadConfig: function () { + fadeDesktopEffect.duration = animationTime(effect.readConfig("AnimationDuration", 250)); + }, + fadeInWindow: function (window) { + if (window.fadeOutAnimation) { + if (redirect(window.fadeOutAnimation, Effect.Backward)) { + return; + } + cancel(window.fadeOutAnimation); + delete window.fadeOutAnimation; + } + if (window.fadeInAnimation) { + if (redirect(window.fadeInAnimation, Effect.Forward)) { + return; + } + cancel(window.fadeInAnimation); + } + window.fadeInAnimation = animate({ + window: window, + curve: QEasingCurve.Linear, + duration: fadeDesktopEffect.duration, + fullScreen: true, + keepAlive: false, + type: Effect.Opacity, + from: 0.0, + to: 1.0 + }); + }, + fadeOutWindow: function (window) { + if (window.fadeInAnimation) { + if (redirect(window.fadeInAnimation, Effect.Backward)) { + return; + } + cancel(window.fadeInAnimation); + delete window.fadeInAnimation; + } + if (window.fadeOutAnimation) { + if (redirect(window.fadeOutAnimation, Effect.Forward)) { + return; + } + cancel(window.fadeOutAnimation); + } + window.fadeOutAnimation = animate({ + window: window, + curve: QEasingCurve.Linear, + duration: fadeDesktopEffect.duration, + fullScreen: true, + keepAlive: false, + type: Effect.Opacity, + from: 1.0, + to: 0.0 + }); + }, + slotDesktopChanged: function (oldDesktop, newDesktop, movingWindow) { + if (effects.hasActiveFullScreenEffect && !effect.isActiveFullScreenEffect) { + return; + } + + var stackingOrder = effects.stackingOrder; + for (var i = 0; i < stackingOrder.length; ++i) { + var w = stackingOrder[i]; + + // Don't animate windows that have been moved to the current + // desktop, i.e. newDesktop. + if (w == movingWindow) { + continue; + } + + // If the window is not on the old and the new desktop or it's + // on both of them, then don't animate it. + var onOldDesktop = w.isOnDesktop(oldDesktop); + var onNewDesktop = w.isOnDesktop(newDesktop); + if (onOldDesktop == onNewDesktop) { + continue; + } + + if (w.minimized) { + continue; + } + + if (!w.isOnActivity(effects.currentActivity)){ + continue; + } + + if (onOldDesktop) { + fadeDesktopEffect.fadeOutWindow(w); + } else { + fadeDesktopEffect.fadeInWindow(w); + } + } + }, + slotIsActiveFullScreenEffectChanged: function () { + var isActiveFullScreen = effect.isActiveFullScreenEffect; + var stackingOrder = effects.stackingOrder; + for (var i = 0; i < stackingOrder.length; ++i) { + var w = stackingOrder[i]; + w.setData(Effect.WindowForceBlurRole, isActiveFullScreen); + w.setData(Effect.WindowForceBackgroundContrastRole, isActiveFullScreen); + } + }, + init: function () { + fadeDesktopEffect.loadConfig(); + effect.configChanged.connect(fadeDesktopEffect.loadConfig); + effect.isActiveFullScreenEffectChanged.connect( + fadeDesktopEffect.slotIsActiveFullScreenEffectChanged); + effects.desktopChanged.connect(fadeDesktopEffect.slotDesktopChanged); + } +}; + +fadeDesktopEffect.init(); diff --git a/local/recipes/kde/kwin/source/src/plugins/fadedesktop/package/metadata.json b/local/recipes/kde/kwin/source/src/plugins/fadedesktop/package/metadata.json new file mode 100644 index 0000000000..1fc326a955 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fadedesktop/package/metadata.json @@ -0,0 +1,146 @@ +{ + "KPackageStructure": "KWin/Effect", + "KPlugin": { + "Authors": [ + { + "Email": "lmurray@undefinedfire.com, mgraesslin@kde.org", + "Name": "Lucas Murray, Martin Gräßlin", + "Name[ar]": "لوكاس موراي، مارتن جراجلين", + "Name[be]": "Lucas Murray, Martin Gräßlin", + "Name[bg]": "Lucas Murray, Martin Gräßlin", + "Name[ca@valencia]": "Lucas Murray, Martin Gräßlin", + "Name[ca]": "Lucas Murray, Martin Gräßlin", + "Name[cs]": "Lucas Murray, Martin Gräßlin", + "Name[da]": "Lucas Murray, Martin Gräßlin", + "Name[de]": "Lucas Murray, Martin Gräßlin", + "Name[en_GB]": "Lucas Murray, Martin Gräßlin", + "Name[eo]": "Lucas Murray, Martin Gräßlin", + "Name[es]": "Lucas Murray, Martin Gräßlin", + "Name[et]": "Lucas Murray, Martin Gräßlin", + "Name[eu]": "Lucas Murray, Martin Gräßlin", + "Name[fi]": "Lucas Murray, Martin Gräßlin", + "Name[fr]": "Lucas Murray, Martin Gräßlin", + "Name[ga]": "Lucas Murray, Martin Gräßlin", + "Name[gl]": "Lucas Murray e Martin Gräßlin.", + "Name[he]": "ולקאס מוראי, מרטין גרייסלין", + "Name[hu]": "Lucas Murray, Martin Gräßlin", + "Name[ia]": "Lucas Murray, Martin Gräßlin", + "Name[id]": "Lucas Murray, Martin Gräßlin", + "Name[is]": "Lucas Murray, Martin Gräßlin", + "Name[it]": "Lucas Murray, Martin Gräßlin", + "Name[ja]": "Lucas Murray, Martin Gräßlin", + "Name[ka]": "Lucas Murray, Martin Gräßlin", + "Name[ko]": "Lucas Murray, Martin Gräßlin", + "Name[lt]": "Lucas Murray, Martin Gräßlin", + "Name[lv]": "Lucas Murray, Martin Gräßlin", + "Name[nb]": "Lucas Murray, Martin Gräßlin", + "Name[nl]": "Lucas Murray, Martin Gräßlin", + "Name[nn]": "Lucas Murray, Martin Gräßlin", + "Name[pl]": "Lucas Murray, Martin Gräßlin", + "Name[pt]": "Lucas Murray, Martin Gräßlin", + "Name[pt_BR]": "Lucas Murray, Martin Gräßlin", + "Name[ro]": "Lucas Murray, Martin Gräßlin", + "Name[ru]": "Lucas Murray, Martin Gräßlin", + "Name[sa]": "लुकास मरे, मार्टिन Gräßlin", + "Name[sk]": "Lucas Murray, Martin Gräßlin", + "Name[sl]": "Lucas Murray, Martin Gräßlin", + "Name[sv]": "Lucas Murray, Martin Gräßlin", + "Name[ta]": "லூக்கஸ் முரே, மார்ட்டின் கிராஸ்லின்", + "Name[tr]": "Lucas Murray, Martin Gräßlin", + "Name[uk]": "Lucas Murray, Martin Gräßlin", + "Name[vi]": "Lucas Murray, Martin Gräßlin", + "Name[zh_CN]": "Lucas Murray, Martin Gräßlin", + "Name[zh_TW]": "Lucas Murray, Martin Gräßlin" + } + ], + "Category": "Virtual Desktop Switching Animation", + "Description": "Fade between virtual desktops when switching", + "Description[ar]": "التلاشي بين أسطح المكتب الافتراضية عند التبديل", + "Description[bg]": "Избледняване при превключване между виртуалните работни плотове", + "Description[ca@valencia]": "Esvaïx en canviar entre els escriptoris virtuals", + "Description[ca]": "Esvaeix en commutar entre els escriptoris virtuals", + "Description[de]": "Wechseln der virtuellen Arbeitsfläche durch Aus- und Einblenden", + "Description[es]": "Desvanecer al cambiar de escritorio virtual", + "Description[eu]": "Koloregabetu alegiazko mahaigainen artean haien artean trukatzean", + "Description[fi]": "Häivytä virtuaalityöpöytä sitä vaihdettaessa", + "Description[fr]": "Effectuer un fondu entre les bureaux virtuels lors de basculement", + "Description[he]": "עמעום בין שולחנות עבודה וירטואליים בעת המעבר", + "Description[hu]": "Áttűnés virtuális asztalok váltásakor", + "Description[ia]": "Pallidi inter scriptorio virtuales quando il commuta", + "Description[is]": "Sýndarskjáborð hverfa og birtast mjúklega þegar skipt er á milli þeirra", + "Description[it]": "Passa da un desktop virtuale all'altro con una dissolvenza", + "Description[ja]": "仮想デスクトップ切り替え時にフェードします", + "Description[ka]": "მინავლება ვირტუალურ სამუშაო მაგიდებს შორის გადართვისას", + "Description[ko]": "가상 바탕 화면 사이를 전환할 때 페이드 효과를 사용합니다", + "Description[lt]": "Laipsniškai pereiti tarp virtualių darbalaukių, kai jie perjungiami", + "Description[lv]": "Izgaist starp virtuālām darbvirsmām to pārslēgšanas laikā", + "Description[nl]": "Laat virtuele bureaubladen vervagen en opkomen bij het wisselen", + "Description[nn]": "Ton inn og ut ved veksling mellom virtuelle skrivebord", + "Description[pl]": "Przenikanie przy przełączaniu pomiędzy pulpitami wirtualnymi", + "Description[pt_BR]": "Desvanecer entre áreas de trabalho virtuais ao alternar", + "Description[ro]": "Estompare între birourile virtuale la schimbare", + "Description[ru]": "Постепенная смена изображения при переключении на другой рабочий стол", + "Description[sk]": "Prelínanie medzi virtuálnymi pracovnými plochami pri prepínaní medzi nimi", + "Description[sl]": "Obledi med navideznimi namizji, med preklopom med njimi", + "Description[sv]": "Tona mellan virtuella skrivbord vid byte", + "Description[tr]": "Geçiş yaparken sanal masaüstlerini soldur", + "Description[uk]": "Затемнення під час перемикання між віртуальними стільницями", + "Description[zh_CN]": "在虚拟桌面间切换时呈现渐入渐出动效", + "Description[zh_TW]": "切換虛擬桌面時在它們之間進行淡化", + "EnabledByDefault": false, + "Icon": "preferences-system-windows-effect-fadedesktop", + "Id": "fadedesktop", + "License": "GPL", + "Name": "Fade", + "Name[ar]": "التلاشي", + "Name[be]": "Зацямненне", + "Name[bg]": "Избледняване", + "Name[ca@valencia]": "Esvaïment", + "Name[ca]": "Esvaïment", + "Name[cs]": "Blednutí", + "Name[da]": "Fade", + "Name[de]": "Verblassen", + "Name[en_GB]": "Fade", + "Name[eo]": "Dissolvi", + "Name[es]": "Desvanecer", + "Name[et]": "Hääbumine", + "Name[eu]": "Koloregabetu", + "Name[fi]": "Häivytä", + "Name[fr]": "Atténuer", + "Name[gl]": "Esvaer", + "Name[he]": "הדעכה", + "Name[hu]": "Elhalványulás", + "Name[ia]": "Pallidi", + "Name[id]": "Lesap", + "Name[is]": "Deyfa", + "Name[it]": "Dissolvi", + "Name[ja]": "フェード", + "Name[ka]": "მინავლება", + "Name[ko]": "페이드", + "Name[lt]": "Laipsniškas išnykimas ir atsiradimas", + "Name[lv]": "Izgaist", + "Name[nb]": "Ton inn og ut", + "Name[nl]": "Vervagen", + "Name[nn]": "Ton inn og ut", + "Name[pl]": "Zanikanie", + "Name[pt]": "Desvanecer", + "Name[pt_BR]": "Esmaecer", + "Name[ro]": "Estompare", + "Name[ru]": "Растворение", + "Name[sa]": "म्लै", + "Name[sk]": "Blednutie", + "Name[sl]": "Preliv", + "Name[sv]": "Tona", + "Name[ta]": "மங்கசெய்", + "Name[tr]": "Soldur", + "Name[uk]": "Згасання", + "Name[vi]": "Làm mờ dần", + "Name[zh_CN]": "淡出淡入动画", + "Name[zh_TW]": "淡化" + }, + "X-KDE-ConfigModule": "kcm_kwin4_genericscripted", + "X-KDE-Ordering": 50, + "X-KWin-Config-TranslationDomain": "kwin", + "X-KWin-Exclusive-Category": "desktop-animations", + "X-Plasma-API": "javascript" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/fadingpopups/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/fadingpopups/CMakeLists.txt new file mode 100644 index 0000000000..486808e18a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fadingpopups/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_scripted_effect(fadingpopups package) diff --git a/local/recipes/kde/kwin/source/src/plugins/fadingpopups/package/contents/code/main.js b/local/recipes/kde/kwin/source/src/plugins/fadingpopups/package/contents/code/main.js new file mode 100644 index 0000000000..1ebb8d89f6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fadingpopups/package/contents/code/main.js @@ -0,0 +1,141 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var blacklist = [ + // ignore black background behind lockscreen + "ksmserver ksmserver", + // The logout screen has to be animated only by the logout effect. + "ksmserver-logout-greeter ksmserver-logout-greeter", + // The lockscreen isn't a popup window + "kscreenlocker_greet kscreenlocker_greet", + // KDE Plasma splash screen has to be animated only by the login effect. + "ksplashqml ksplashqml", +]; + +function isPopupWindow(window) { + // If the window is blacklisted, don't animate it. + if (blacklist.indexOf(window.windowClass) != -1) { + return false; + } + + // Animate combo box popups, tooltips, popup menus, etc. + if (window.popupWindow) { + return true; + } + + // Maybe the outline deserves its own effect. + if (window.outline) { + return true; + } + + // Override-redirect windows are usually used for user interface + // concepts that are expected to be animated by this effect, e.g. + // popups that contain window thumbnails on X11, etc. + if (!window.managed) { + // Some utility windows can look like popup windows (e.g. the + // address bar dropdown in Firefox), but we don't want to fade + // them because the fade effect didn't do that. + if (window.utility) { + return false; + } + + return true; + } + + // Previously, there was a "monolithic" fade effect, which tried to + // animate almost every window that was shown or hidden. Then it was + // split into two effects: one that animates toplevel windows and + // this one. In addition to popups, this effect also animates some + // special windows(e.g. notifications) because the monolithic version + // was doing that. + if (window.splash || window.toolbar + || window.notification || window.onScreenDisplay + || window.criticalNotification + || window.appletPopup) { + return true; + } + + return false; +} + +var fadingPopupsEffect = { + loadConfig: function () { + fadingPopupsEffect.fadeInDuration = animationTime(150); + fadingPopupsEffect.fadeOutDuration = animationTime(150) * 4; + }, + slotWindowAdded: function (window) { + if (effects.hasActiveFullScreenEffect) { + return; + } + if (!isPopupWindow(window)) { + return; + } + if (!window.visible) { + return; + } + if (!effect.grab(window, Effect.WindowAddedGrabRole)) { + return; + } + window.fadeInAnimation = animate({ + window: window, + curve: QEasingCurve.Linear, + duration: fadingPopupsEffect.fadeInDuration, + type: Effect.Opacity, + from: 0.0, + to: 1.0 + }); + }, + slotWindowClosed: function (window) { + if (effects.hasActiveFullScreenEffect) { + return; + } + if (!isPopupWindow(window)) { + return; + } + if (!window.visible || window.skipsCloseAnimation) { + return; + } + if (!effect.grab(window, Effect.WindowClosedGrabRole)) { + return; + } + window.fadeOutAnimation = animate({ + window: window, + curve: QEasingCurve.OutQuart, + duration: fadingPopupsEffect.fadeOutDuration, + type: Effect.Opacity, + from: 1.0, + to: 0.0 + }); + }, + slotWindowDataChanged: function (window, role) { + if (role == Effect.WindowAddedGrabRole) { + if (window.fadeInAnimation && effect.isGrabbed(window, role)) { + cancel(window.fadeInAnimation); + delete window.fadeInAnimation; + } + } else if (role == Effect.WindowClosedGrabRole) { + if (window.fadeOutAnimation && effect.isGrabbed(window, role)) { + cancel(window.fadeOutAnimation); + delete window.fadeOutAnimation; + } + } + }, + init: function () { + fadingPopupsEffect.loadConfig(); + + effect.configChanged.connect(fadingPopupsEffect.loadConfig); + effects.windowAdded.connect(fadingPopupsEffect.slotWindowAdded); + effects.windowClosed.connect(fadingPopupsEffect.slotWindowClosed); + effects.windowDataChanged.connect(fadingPopupsEffect.slotWindowDataChanged); + } +}; + +fadingPopupsEffect.init(); diff --git a/local/recipes/kde/kwin/source/src/plugins/fadingpopups/package/metadata.json b/local/recipes/kde/kwin/source/src/plugins/fadingpopups/package/metadata.json new file mode 100644 index 0000000000..456ec69f32 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fadingpopups/package/metadata.json @@ -0,0 +1,131 @@ +{ + "KPackageStructure": "KWin/Effect", + "KPlugin": { + "Authors": [ + { + "Email": "vlad.zahorodnii@kde.org", + "Name": "Vlad Zahorodnii", + "Name[ar]": "فلاد زاهورودني", + "Name[az]": "Vlad Zahorodnii", + "Name[be]": "Vlad Zahorodnii", + "Name[bg]": "Vlad Zahorodnii", + "Name[ca@valencia]": "Vlad Zahorodnii", + "Name[ca]": "Vlad Zahorodnii", + "Name[cs]": "Vlad Zahorodnii", + "Name[da]": "Vlad Zahorodnii", + "Name[de]": "Vlad Zahorodnii", + "Name[en_GB]": "Vlad Zahorodnii", + "Name[eo]": "Vlad Zahorodnii", + "Name[es]": "Vlad Zahorodnii", + "Name[et]": "Vlad Zahorodnii", + "Name[eu]": "Vlad Zahorodnii", + "Name[fi]": "Vlad Zahorodnii", + "Name[fr]": "Vlad Zahorodnii", + "Name[ga]": "Vlad Zahorodnii", + "Name[gl]": "Vlad Zahorodnii.", + "Name[he]": "ולאד זהורודני", + "Name[hu]": "Vlad Zahorodnii", + "Name[ia]": "Vlad Zahorodnii", + "Name[id]": "Vlad Zahorodnii", + "Name[is]": "Vlad Zahorodnii", + "Name[it]": "Vlad Zahorodnii", + "Name[ja]": "Vlad Zahorodnii", + "Name[ka]": "Vlad Zahorodnii", + "Name[ko]": "Vlad Zahorodnii", + "Name[lt]": "Vlad Zahorodnii", + "Name[lv]": "Vlad Zahorodnii", + "Name[nb]": "Vlad Zahorodnii", + "Name[nl]": "Vlad Zahorodnii", + "Name[nn]": "Vlad Zahorodnii", + "Name[pl]": "Vlad Zahorodnii", + "Name[pt]": "Vlad Zahorodnii", + "Name[pt_BR]": "Vlad Zahorodnii", + "Name[ro]": "Vlad Zahorodnii", + "Name[ru]": "Влад Загородний", + "Name[sa]": "व्लाद ज़ाहोरोदनी", + "Name[sk]": "Vlad Zahorodnii", + "Name[sl]": "Vlad Zahorodnii", + "Name[sv]": "Vlad Zahorodnii", + "Name[ta]": "விலாட் ஜாஹொரிடுனி", + "Name[tr]": "Vlad Zahorodnii", + "Name[uk]": "Влад Загородній", + "Name[vi]": "Vlad Zahorodnii", + "Name[zh_CN]": "Vlad Zahorodnii", + "Name[zh_TW]": "Vlad Zahorodnii" + } + ], + "Category": "Appearance", + "Description": "Fade popups in and out", + "Description[ar]": "ظهور واختفاء النوافذ المنبثقة", + "Description[bg]": "Избледняване на изскачащи прозорци", + "Description[ca@valencia]": "Esvaïx les finestres emergents d'entrada i eixida", + "Description[ca]": "Esvaeix les finestres emergents d'entrada i sortida", + "Description[de]": "Aufklappfenster ein- und ausblenden", + "Description[es]": "Desvanecer la aparición y desaparición de ventanas emergentes", + "Description[eu]": "Moteldu gainerakorren sartu-irtena", + "Description[fi]": "Ponnahdusikkunoiden häivytys sisään ja ulos", + "Description[fr]": "Effectuer un fondu avant et arrière pour les menus contextuels", + "Description[he]": "עמעום חלוניות פנימה והחוצה", + "Description[hu]": "Áttűnő felugrók megjelenéskor és eltűnéskor", + "Description[ia]": "Pallidi popups in e foras (in e out)", + "Description[is]": "Sprettigluggar hverfa og birtast mjúklega", + "Description[it]": "Dissolvenza le finestre a comparsa in ingresso e in uscita", + "Description[ja]": "ポップアップをフェードする", + "Description[ka]": "მხტუნარების მინავლება შიგნით და გარეთ", + "Description[ko]": "팝업 페이드 인 및 아웃", + "Description[lt]": "Rodyti iškylančiuosius langus kaip laipsniškai atsirandančius ir išnykstančius", + "Description[lv]": "Izgaistoši uzlecošie logi", + "Description[nl]": "Pop-ups in- en uitvagen", + "Description[nn]": "Ton inn og ut sprettoppvindauge", + "Description[pl]": "Wyłanianie i zanikanie okien wysuwnych", + "Description[pt_BR]": "Transicionar suavemente e esmaecer mensagens", + "Description[ro]": "Estompează indiciile la arătare și ascundere", + "Description[ru]": "Изменение прозрачности всплывающих окон при их появлении и скрытии", + "Description[sk]": "Postupné zobrazovanie a skrývanie vyskakovacích okien", + "Description[sl]": "Zbledi in obarvaj pojavna okna", + "Description[sv]": "Tona meddelanderutor", + "Description[tr]": "Açılır pencereleri soldur ve belirginleştir", + "Description[uk]": "Розчинення та поява контекстних вікон", + "Description[zh_CN]": "气泡显隐渐变动效", + "Description[zh_TW]": "讓彈出視窗淡入淡出", + "EnabledByDefault": true, + "Icon": "preferences-system-windows-effect-fadingpopups", + "Id": "fadingpopups", + "License": "GPL", + "Name": "Fading popups", + "Name[ar]": "تتلاشى النوافذ المنبثقة", + "Name[bg]": "Избледняващи изскачащи прозорци", + "Name[ca@valencia]": "Missatges emergents esvaïts", + "Name[ca]": "Missatges emergents esvaïts", + "Name[de]": "Überblendete Aufklappfenster", + "Name[es]": "Desvanecer ventanas emergentes", + "Name[eu]": "Moteltzen diren gainerakorrak", + "Name[fi]": "Ponnahdusikkunoiden häivytys", + "Name[fr]": "Atténuation des infobulles", + "Name[he]": "חלונית צצות מתעמעות", + "Name[hu]": "Áttűnő felugrók", + "Name[ia]": "Popups dissolvente", + "Name[is]": "Dofnandi sprettigluggar", + "Name[it]": "Finestre a comparsa che si dissolvono", + "Name[ja]": "フェードするポップアップ", + "Name[ka]": "მინავლებადი მხტუნარები", + "Name[ko]": "페이드 팝업", + "Name[lt]": "Laipsniškai išnykstantys ir atsirandantys iškylantieji langai", + "Name[lv]": "Izgaistoši uzlecošie logi", + "Name[nl]": "Vervagende pop-ups", + "Name[nn]": "Inn- og uttoning av sprettoppvindauge", + "Name[pl]": "Zanikanie okien wysuwnych", + "Name[pt_BR]": "Mensagens esmaecidas", + "Name[ro]": "Indicii estompate", + "Name[ru]": "Растворяющиеся всплывающие окна", + "Name[sk]": "Miznúce vyskakovacie okná", + "Name[sl]": "Bledenje pojavnih oken", + "Name[sv]": "Borttonande meddelanderutor", + "Name[tr]": "Solan açılır pencereler", + "Name[uk]": "Згасання контекстних панелей", + "Name[zh_CN]": "气泡显隐渐变动效", + "Name[zh_TW]": "淡化彈出視窗" + }, + "X-KDE-Ordering": 60, + "X-Plasma-API": "javascript" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/fallapart/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/fallapart/CMakeLists.txt new file mode 100644 index 0000000000..bb1ae07c0b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fallapart/CMakeLists.txt @@ -0,0 +1,19 @@ +####################################### +# Effect + +# Source files +set(fallapart_SOURCES + fallapart.cpp + main.cpp +) + +kconfig_add_kcfg_files(fallapart_SOURCES + fallapartconfig.kcfgc +) + +kwin_add_builtin_effect(fallapart ${fallapart_SOURCES}) +target_link_libraries(fallapart PRIVATE + kwin + + KF6::ConfigGui +) diff --git a/local/recipes/kde/kwin/source/src/plugins/fallapart/fallapart.cpp b/local/recipes/kde/kwin/source/src/plugins/fallapart/fallapart.cpp new file mode 100644 index 0000000000..c96c376f42 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fallapart/fallapart.cpp @@ -0,0 +1,222 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "fallapart.h" +#include "effect/effecthandler.h" +// KConfigSkeleton +#include "fallapartconfig.h" + +#include + +#include + +using namespace std::chrono_literals; + +Q_LOGGING_CATEGORY(KWIN_FALLAPART, "kwin_effect_fallapart", QtWarningMsg) + +namespace KWin +{ + +bool FallApartEffect::supported() +{ + return OffscreenEffect::supported() && effects->animationsSupported(); +} + +FallApartEffect::FallApartEffect() +{ + FallApartConfig::instance(effects->config()); + reconfigure(ReconfigureAll); + connect(effects, &EffectsHandler::windowClosed, this, &FallApartEffect::slotWindowClosed); + connect(effects, &EffectsHandler::windowDataChanged, this, &FallApartEffect::slotWindowDataChanged); +} + +void FallApartEffect::reconfigure(ReconfigureFlags) +{ + FallApartConfig::self()->read(); + blockSize = FallApartConfig::blockSize(); +} + +void FallApartEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + if (!windows.isEmpty()) { + data.mask |= PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS; + } + effects->prePaintScreen(data, presentTime); +} + +void FallApartEffect::prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) +{ + auto animationIt = windows.find(w); + if (animationIt != windows.end() && isRealWindow(w)) { + int time = 0; + if (animationIt->lastPresentTime.count()) { + time = (presentTime - animationIt->lastPresentTime).count(); + } + animationIt->lastPresentTime = presentTime; + + animationIt->progress += time / animationTime(1s); + data.setTransformed(); + } + effects->prePaintWindow(view, w, data, presentTime); +} + +void FallApartEffect::apply(EffectWindow *w, int mask, WindowPaintData &data, WindowQuadList &quads) +{ + auto animationIt = windows.constFind(w); + if (animationIt != windows.constEnd() && isRealWindow(w)) { + QEasingCurve easing(QEasingCurve::InCubic); + const qreal t = easing.valueForProgress(animationIt->progress); + // Request the window to be divided into cells + quads = quads.makeGrid(blockSize); + int cnt = 0; + for (WindowQuad &quad : quads) { + // make fragments move in various directions, based on where + // they are (left pieces generally move to the left, etc.) + QPointF p1(quad[0].x(), quad[0].y()); + double xdiff = 0; + if (p1.x() < w->width() / 2) { + xdiff = -(w->width() / 2 - p1.x()) / w->width() * 100; + } + if (p1.x() > w->width() / 2) { + xdiff = (p1.x() - w->width() / 2) / w->width() * 100; + } + double ydiff = 0; + if (p1.y() < w->height() / 2) { + ydiff = -(w->height() / 2 - p1.y()) / w->height() * 100; + } + if (p1.y() > w->height() / 2) { + ydiff = (p1.y() - w->height() / 2) / w->height() * 100; + } + double modif = t * 64; + srandom(cnt); // change direction randomly but consistently + xdiff += (rand() % 21 - 10); + ydiff += (rand() % 21 - 10); + for (int j = 0; + j < 4; + ++j) { + quad[j].move(quad[j].x() + xdiff * modif, quad[j].y() + ydiff * modif); + } + // also make the fragments rotate around their center + QPointF center((quad[0].x() + quad[1].x() + quad[2].x() + quad[3].x()) / 4, + (quad[0].y() + quad[1].y() + quad[2].y() + quad[3].y()) / 4); + double adiff = (rand() % 720 - 360) / 360. * 2 * M_PI; // spin randomly + for (int j = 0; + j < 4; + ++j) { + double x = quad[j].x() - center.x(); + double y = quad[j].y() - center.y(); + double angle = atan2(y, x); + angle += animationIt->progress * adiff; + double dist = sqrt(x * x + y * y); + x = dist * cos(angle); + y = dist * sin(angle); + quad[j].move(center.x() + x, center.y() + y); + } + ++cnt; + } + data.multiplyOpacity(interpolate(1.0, 0.0, t)); + } +} + +void FallApartEffect::postPaintScreen() +{ + for (auto it = windows.begin(); it != windows.end();) { + if (it->progress < 1) { + ++it; + } else { + unredirect(it.key()); + it = windows.erase(it); + } + } + + effects->addRepaintFull(); + effects->postPaintScreen(); +} + +bool FallApartEffect::isRealWindow(EffectWindow *w) +{ + // TODO: isSpecialWindow is rather generic, maybe tell windowtypes separately? + /* + qCDebug(KWIN_FALLAPART) << "--" << w->caption() << "--------------------------------"; + qCDebug(KWIN_FALLAPART) << "Tooltip:" << w->isTooltip(); + qCDebug(KWIN_FALLAPART) << "Toolbar:" << w->isToolbar(); + qCDebug(KWIN_FALLAPART) << "Desktop:" << w->isDesktop(); + qCDebug(KWIN_FALLAPART) << "Special:" << w->isSpecialWindow(); + qCDebug(KWIN_FALLAPART) << "TopMenu:" << w->isTopMenu(); + qCDebug(KWIN_FALLAPART) << "Notific:" << w->isNotification(); + qCDebug(KWIN_FALLAPART) << "Splash:" << w->isSplash(); + qCDebug(KWIN_FALLAPART) << "Normal:" << w->isNormalWindow(); + */ + if (w->isPopupWindow()) { + return false; + } + if (w->isOutline()) { + return false; + } + if (w->isLockScreen()) { + return false; + } + if (w->isX11Client() && !w->isManaged()) { + return false; + } + if (!w->isNormalWindow()) { + return false; + } + return true; +} + +void FallApartEffect::slotWindowClosed(EffectWindow *c) +{ + if (effects->activeFullScreenEffect()) { + return; + } + if (!isRealWindow(c)) { + return; + } + if (!c->isVisible()) { + return; + } + const void *e = c->data(WindowClosedGrabRole).value(); + if (e && e != this) { + return; + } + c->setData(WindowClosedGrabRole, QVariant::fromValue(static_cast(this))); + + FallApartAnimation &animation = windows[c]; + animation.progress = 0; + animation.deletedRef = EffectWindowDeletedRef(c); + + redirect(c); +} + +void FallApartEffect::slotWindowDataChanged(EffectWindow *w, int role) +{ + if (role != WindowClosedGrabRole) { + return; + } + + if (w->data(role).value() == this) { + return; + } + + auto it = windows.find(w); + if (it != windows.end()) { + unredirect(it.key()); + windows.erase(it); + } +} + +bool FallApartEffect::isActive() const +{ + return !windows.isEmpty(); +} + +} // namespace + +#include "moc_fallapart.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/fallapart/fallapart.h b/local/recipes/kde/kwin/source/src/plugins/fallapart/fallapart.h new file mode 100644 index 0000000000..1be22ab2a0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fallapart/fallapart.h @@ -0,0 +1,63 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/effectwindow.h" +#include "effect/offscreeneffect.h" + +namespace KWin +{ + +struct FallApartAnimation +{ + EffectWindowDeletedRef deletedRef; + std::chrono::milliseconds lastPresentTime = std::chrono::milliseconds::zero(); + qreal progress = 0; +}; + +class FallApartEffect : public OffscreenEffect +{ + Q_OBJECT + Q_PROPERTY(int blockSize READ configuredBlockSize) +public: + FallApartEffect(); + void reconfigure(ReconfigureFlags) override; + void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override; + void prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) override; + void postPaintScreen() override; + bool isActive() const override; + + int requestedEffectChainPosition() const override + { + return 70; + } + + // for properties + int configuredBlockSize() const + { + return blockSize; + } + + static bool supported(); + +protected: + void apply(EffectWindow *w, int mask, WindowPaintData &data, WindowQuadList &quads) override; + +public Q_SLOTS: + void slotWindowClosed(KWin::EffectWindow *c); + void slotWindowDataChanged(KWin::EffectWindow *w, int role); + +private: + QHash windows; + bool isRealWindow(EffectWindow *w); + int blockSize; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/fallapart/fallapart.kcfg b/local/recipes/kde/kwin/source/src/plugins/fallapart/fallapart.kcfg new file mode 100644 index 0000000000..fbefa02107 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fallapart/fallapart.kcfg @@ -0,0 +1,14 @@ + + + + + + 40 + 1 + 100000 + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/fallapart/fallapartconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/fallapart/fallapartconfig.kcfgc new file mode 100644 index 0000000000..1735fb7e7f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fallapart/fallapartconfig.kcfgc @@ -0,0 +1,5 @@ +File=fallapart.kcfg +ClassName=FallApartConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/local/recipes/kde/kwin/source/src/plugins/fallapart/main.cpp b/local/recipes/kde/kwin/source/src/plugins/fallapart/main.cpp new file mode 100644 index 0000000000..2a0a033caa --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fallapart/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "fallapart.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(FallApartEffect, + "metadata.json.stripped", + return FallApartEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/fallapart/metadata.json b/local/recipes/kde/kwin/source/src/plugins/fallapart/metadata.json new file mode 100644 index 0000000000..e5e34b43e9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fallapart/metadata.json @@ -0,0 +1,101 @@ +{ + "KPlugin": { + "Category": "Appearance", + "Description": "Closed windows fall into pieces", + "Description[ar]": "النوافذ المغلقة تسقط قطعًا", + "Description[az]": "Pəncərəni hissələrə parçalayaraq bağlayır", + "Description[be]": "Пры закрыцці вокны развальваюцца на кавалкі", + "Description[bg]": "Затворените прозорци се разпадат на малки квадрати", + "Description[ca@valencia]": "Les finestres tancades es trenquen en peces", + "Description[ca]": "Les finestres tancades es trenquen en peces", + "Description[cs]": "Uzavřená okna se rozpadnou na kousky", + "Description[da]": "Lukkede vinduer falder i stykker", + "Description[de]": "Lässt geschlossene Fenster auseinander fallen", + "Description[en_GB]": "Closed windows fall into pieces", + "Description[eo]": "Fermitaj fenestroj falas en pecojn", + "Description[es]": "Las ventanas cerradas caen en pedazos", + "Description[et]": "Suletud aknad lagunevad tükkideks", + "Description[eu]": "Itxitako leihoak puskatu egiten dira", + "Description[fi]": "Suljetut ikkunat hajoavat palasiksi", + "Description[fr]": "Les fenêtres fermées tombent en morceaux", + "Description[gl]": "As xanelas pechadas rachan en anacos.", + "Description[he]": "חלונות שנסגרים מתרסקים לרסיסים", + "Description[hu]": "A bezárt ablakok darabokra hullanak", + "Description[ia]": "Fenestras claudite cade in pecias", + "Description[id]": "Jendela ditutup pecah berpuing-puing", + "Description[is]": "Gluggar detta í sundur þegar þeim er lokað", + "Description[it]": "Le finestre cadono a pezzi alla chiusura", + "Description[ja]": "閉じられたウィンドウがバラバラに砕け散ります", + "Description[ka]": "დახურული ფანჯარა ნაწილებად დაიშლება", + "Description[ko]": "닫힌 창이 조각난 채로 떨어지게 합니다", + "Description[lt]": "Užverti langai subyra į šukes", + "Description[lv]": "Aizvērti logi sabrūk gabaliņos", + "Description[nb]": "La vinduer gå i tusen biter når de lukkes", + "Description[nl]": "Gesloten vensters vallen uit elkaar", + "Description[nn]": "La vindauge gå i tusen når dei vert lukka", + "Description[pl]": "Rozsypuje okna na kawałki przy ich zamykaniu", + "Description[pt]": "As janelas fechadas partem-se aos bocados", + "Description[pt_BR]": "Janelas fechadas caem em pedaços", + "Description[ro]": "Ferestrele închise se sparg în bucăți", + "Description[ru]": "Распад закрывающихся окон на части", + "Description[sa]": "पिहिताः खिडकयः खण्डेषु पतन्ति", + "Description[sk]": "Zatvorené okná sa rozpadnú na časti", + "Description[sl]": "Zaprta okna se razletijo na koščke", + "Description[sv]": "Stängda fönster faller i bitar", + "Description[ta]": "மூடப்படும் சாளரங்கள் துண்டு துண்டாக நொறுங்கி மறையும்", + "Description[tr]": "Kapanan pencereler parçalara ayrılırlar", + "Description[uk]": "Розпадання вікон на шматочки під час закриття вікон", + "Description[vi]": "Cửa sổ bị đóng sẽ vỡ thành nhiều mảnh", + "Description[zh_CN]": "窗口关闭时绘制粉碎飞散动画", + "Description[zh_TW]": "關閉的視窗會呈現粉碎效果", + "EnabledByDefault": false, + "License": "GPL", + "Name": "Fall Apart", + "Name[ar]": "السقوط أجزاء", + "Name[az]": "Dağılma", + "Name[be]": "Развальванне", + "Name[bg]": "Разпадане", + "Name[ca@valencia]": "Trencament", + "Name[ca]": "Trencament", + "Name[cs]": "Rozpad", + "Name[da]": "Fald fra hinanen", + "Name[de]": "Auseinanderfallen", + "Name[en_GB]": "Fall Apart", + "Name[eo]": "Disfali", + "Name[es]": "Desmoronar", + "Name[et]": "Lagunemine", + "Name[eu]": "Puskatu", + "Name[fi]": "Hajotus", + "Name[fr]": "Tomber en morceaux", + "Name[gl]": "Rachar", + "Name[he]": "התפרקות", + "Name[hu]": "Szétbontás", + "Name[ia]": "Cade a pecias", + "Name[id]": "Ambyar", + "Name[is]": "Detta í sundur", + "Name[it]": "Cadi a pezzi", + "Name[ja]": "バラバラにする", + "Name[ka]": "დამსხვრევა", + "Name[ko]": "떨어지기", + "Name[lt]": "Subyrėjimas", + "Name[lv]": "Sabrukt", + "Name[nb]": "Gå i knas", + "Name[nl]": "Uit elkaar vallen", + "Name[nn]": "Gå i knas", + "Name[pl]": "Rozpadanie", + "Name[pt]": "Queda aos Bocados", + "Name[pt_BR]": "Despedaçar", + "Name[ro]": "Spargere", + "Name[ru]": "Распад", + "Name[sa]": "पततु विच्छेदः", + "Name[sk]": "Rozpadnúť", + "Name[sl]": "Razpad", + "Name[sv]": "Fall sönder", + "Name[ta]": "நொறுக்கு", + "Name[tr]": "Parçalara Ayır", + "Name[uk]": "Розпадання", + "Name[vi]": "Tan vỡ", + "Name[zh_CN]": "窗口粉碎动画", + "Name[zh_TW]": "粉碎" + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/frozenapp/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/frozenapp/CMakeLists.txt new file mode 100644 index 0000000000..8ec31946fe --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/frozenapp/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_scripted_effect(frozenapp package) diff --git a/local/recipes/kde/kwin/source/src/plugins/frozenapp/package/contents/code/main.js b/local/recipes/kde/kwin/source/src/plugins/frozenapp/package/contents/code/main.js new file mode 100644 index 0000000000..7ca4ac62dd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/frozenapp/package/contents/code/main.js @@ -0,0 +1,123 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var frozenAppEffect = { + inDuration: animationTime(1500), + outDuration: animationTime(250), + loadConfig: function () { + frozenAppEffect.inDuration = animationTime(1500); + frozenAppEffect.outDuration = animationTime(250); + }, + windowAdded: function (window) { + window.minimizedChanged.connect(() => { + if (window.minimized) { + frozenAppEffect.cancelAnimation(window); + } else { + frozenAppEffect.restartAnimation(window); + } + }); + window.windowUnresponsiveChanged.connect(frozenAppEffect.unresponsiveChanged); + window.windowDesktopsChanged.connect(frozenAppEffect.cancelAnimation); + window.windowDesktopsChanged.connect(frozenAppEffect.restartAnimation); + + if (window.unresponsive) { + frozenAppEffect.startAnimation(window, 1); + } + }, + windowBecameUnresponsive: function (window) { + if (window.unresponsiveAnimation) { + return; + } + frozenAppEffect.startAnimation(window, frozenAppEffect.inDuration); + }, + startAnimation: function (window, duration) { + if (!window.visible) { + return; + } + window.unresponsiveAnimation = set({ + window: window, + duration: duration, + animations: [{ + type: Effect.Saturation, + to: 0.1 + }, { + type: Effect.Brightness, + to: 1.5 + }] + }); + }, + windowClosed: function (window) { + frozenAppEffect.cancelAnimation(window); + if (!window.unresponsive) { + return; + } + frozenAppEffect.windowBecameResponsive(window); + }, + windowBecameResponsive: function (window) { + if (!window.unresponsiveAnimation) { + return; + } + cancel(window.unresponsiveAnimation); + window.unresponsiveAnimation = undefined; + + animate({ + window: window, + duration: frozenAppEffect.outDuration, + animations: [{ + type: Effect.Saturation, + from: 0.1, + to: 1.0 + }, { + type: Effect.Brightness, + from: 1.5, + to: 1.0 + }] + }); + }, + cancelAnimation: function (window) { + if (window.unresponsiveAnimation) { + cancel(window.unresponsiveAnimation); + window.unresponsiveAnimation = undefined; + } + }, + desktopChanged: function () { + var windows = effects.stackingOrder; + for (var i = 0, length = windows.length; i < length; ++i) { + var window = windows[i]; + frozenAppEffect.cancelAnimation(window); + frozenAppEffect.restartAnimation(window); + } + }, + unresponsiveChanged: function (window) { + if (window.unresponsive) { + frozenAppEffect.windowBecameUnresponsive(window); + } else { + frozenAppEffect.windowBecameResponsive(window); + } + }, + restartAnimation: function (window) { + if (!window || !window.unresponsive) { + return; + } + frozenAppEffect.startAnimation(window, 1); + }, + init: function () { + effects.windowAdded.connect(frozenAppEffect.windowAdded); + effects.windowClosed.connect(frozenAppEffect.windowClosed); + effects.desktopChanged.connect(frozenAppEffect.desktopChanged); + + var windows = effects.stackingOrder; + for (var i = 0, length = windows.length; i < length; ++i) { + frozenAppEffect.windowAdded(windows[i]); + } + } +}; +frozenAppEffect.init(); diff --git a/local/recipes/kde/kwin/source/src/plugins/frozenapp/package/metadata.json b/local/recipes/kde/kwin/source/src/plugins/frozenapp/package/metadata.json new file mode 100644 index 0000000000..5269566b99 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/frozenapp/package/metadata.json @@ -0,0 +1,158 @@ +{ + "KPackageStructure": "KWin/Effect", + "KPlugin": { + "Authors": [ + { + "Email": "kde@privat.broulik.de", + "Name": "Kai Uwe Broulik", + "Name[ar]": "كاي أووي بروتيك", + "Name[be]": "Kai Uwe Broulik", + "Name[bg]": "Kai Uwe Broulik", + "Name[ca@valencia]": "Kai Uwe Broulik", + "Name[ca]": "Kai Uwe Broulik", + "Name[cs]": "Kai Uwe Broulik", + "Name[da]": "Kai Uwe Broulik", + "Name[de]": "Kai Uwe Broulik", + "Name[en_GB]": "Kai Uwe Broulik", + "Name[eo]": "Kai Uwe Broulik", + "Name[es]": "Kai Uwe Broulik", + "Name[et]": "Kai Uwe Broulik", + "Name[eu]": "Kai Uwe Broulik", + "Name[fi]": "Kai Uwe Broulik", + "Name[fr]": "Kai Uwe Broulik", + "Name[ga]": "Kai Uwe Broulik", + "Name[gl]": "Kai Uwe Broulik.", + "Name[he]": "קאי אווה ברוליק", + "Name[hu]": "Kai Uwe Broulik", + "Name[ia]": "Kai Uwe Broulik", + "Name[id]": "Kai Uwe Broulik", + "Name[is]": "Kai Uwe Broulik", + "Name[it]": "Kai Uwe Broulik", + "Name[ja]": "Kai Uwe Broulik", + "Name[ka]": "კაი უვე ბროულიკი", + "Name[ko]": "Kai Uwe Broulik", + "Name[lt]": "Kai Uwe Broulik", + "Name[lv]": "Kai Uwe Broulik", + "Name[nb]": "Kai Uwe Broulik", + "Name[nl]": "Kai Uwe Broulik", + "Name[nn]": "Kai Uwe Broulik", + "Name[pl]": "Kai Uwe Broulik", + "Name[pt]": "Kai Uwe Broulik", + "Name[pt_BR]": "Kai Uwe Broulik", + "Name[ro]": "Kai Uwe Broulik", + "Name[ru]": "Kai Uwe Broulik", + "Name[sa]": "कै उवे ब्रौलिक", + "Name[sk]": "Kai Uwe Broulik", + "Name[sl]": "Kai Uwe Broulik", + "Name[sv]": "Kai Uwe Broulik", + "Name[ta]": "காய் ஊவே புரோலிக்", + "Name[tr]": "Kai Uwe Broulik", + "Name[uk]": "Kai Uwe Broulik", + "Name[vi]": "Kai Uwe Broulik", + "Name[zh_CN]": "Kai Uwe Broulik", + "Name[zh_TW]": "Kai Uwe Broulik" + } + ], + "Category": "Appearance", + "Description": "Desaturate windows of unresponsive (frozen) applications", + "Description[ar]": "أظلم نوافذ التطبيقات غير المستجيبة (متجمدة)", + "Description[be]": "Абясколерванне праграм, якія не адказваюць (працэсы якіх замарожаныя)", + "Description[bg]": "Избледняване на прозорците на нереагиращи приложения", + "Description[ca@valencia]": "Dessatura les finestres de les aplicacions que no responen (congelades)", + "Description[ca]": "Dessatura les finestres de les aplicacions que no responen (congelades)", + "Description[cs]": "Ztmaví okna nereagujících (zamrzlých) aplikací", + "Description[da]": "Afmæt vinduer af applikationer, der ikke svarer (er frosne)", + "Description[de]": "Verringert die Sättigung von nicht reagierenden, eingefrorenen Anwendungen", + "Description[en_GB]": "Desaturate windows of unresponsive (frozen) applications", + "Description[eo]": "Malsaturigi fenestrojn de nerespondemaj (frostigitaj) aplikaĵoj", + "Description[es]": "Desaturar ventanas de las aplicaciones que no responden (congeladas)", + "Description[et]": "Reageerimisvõimetute (hangunud) rakenduste akende muutmine kahvatuks", + "Description[eu]": "Erantzuten ez duten aplikazioen leihoak (izoztutakoak) desasetzea", + "Description[fi]": "Vähentää jähmettyneiden sovellusikkunoiden värikylläisyyttä", + "Description[fr]": "Dé-saturer les fenêtres des applications non réactives (gelées)", + "Description[gl]": "Reducir a saturación das xanelas de aplicacións que non responden (conxeladas).", + "Description[he]": "החלשת הרוויה של יישומים שאינם מגיבים (קפואים)", + "Description[hu]": "Nem válaszoló (lefagyott) alkalmazások ablakának elmosása.", + "Description[ia]": "Destura fenestras de applicationes non responsive (congelate)", + "Description[id]": "Desaturasikan jendela pada aplikasi yang tidak responsif (beku)", + "Description[is]": "Aflita glugga forrita sem svara ekki (eru frosin)", + "Description[it]": "Desatura le finestre delle applicazione che non rispondono (bloccate)", + "Description[ja]": "応答しない (フリーズした) アプリケーションのウィンドウの彩度を下げます", + "Description[ka]": "უსიცოცხლო (დაკიდებული) აპლიკაციის ფანჯრების გაუფერულება", + "Description[ko]": "응답 없는 앱 창을 무채색으로 전환", + "Description[lt]": "Nusodrina nereaguojančių (užstrigusių) programų langus", + "Description[lv]": "Nereaģējošus (sasalušus) logus padarīt melnbaltus", + "Description[nb]": "Fjern fargemetting av vinduer på program som ikke lengre reagerer", + "Description[nl]": "Verzadiging van vensters van niet responsieve (bevroren) toepassingen verminderen", + "Description[nn]": "Fjern fargemetting på vindauge på program som ikkje lenger reagerer", + "Description[pl]": "Odbarwia okna nieodpowiadających (zawieszonych) aplikacji", + "Description[pt]": "Reduzir a saturação das janelas bloqueadas (sem resposta)", + "Description[pt_BR]": "Desaturar janelas de aplicativos que não respondem (congelados)", + "Description[ro]": "Desaturează ferestrele aplicațiilor ce nu răspund (înghețate)", + "Description[ru]": "Обесцвечивание окон приложений, не отвечающих на запросы", + "Description[sa]": "अप्रतिसादितानां (जमेन) अनुप्रयोगानाम् विण्डोः विसंतृप्तं कुर्वन्तु", + "Description[sk]": "Desaturácia okien nereagujúcich (zmrazených) aplikácií", + "Description[sl]": "Zmanjša nasičenost neodzivnih (zamrznjenih) programov", + "Description[sv]": "Avmätta fönster för oemottagliga (frysta) program", + "Description[ta]": "பதிலளிக்காத செயலிகளின் சாளரங்களை நிறமில்லாமல் (கருப்புவெள்ளையாக) காட்டும்", + "Description[tr]": "Yanıt vermeyen (donmuş) uygulamaların pencerelerini soldur", + "Description[uk]": "Зменшення насиченості кольорів вікон програм, які не відповідають на запити (повисли)", + "Description[vi]": "Khử bão hoà các cửa sổ của các ứng dụng không hồi đáp (đơ)", + "Description[zh_CN]": "降低无响应/已卡死窗口的颜色饱和度", + "Description[zh_TW]": "降低無回應(凍結)應用程式視窗的飽和度", + "EnabledByDefault": true, + "Icon": "preferences-system-windows-effect-frozenapp", + "Id": "frozenapp", + "License": "GPL", + "Name": "Desaturate Unresponsive Applications", + "Name[ar]": "أظلم التطبيقات غير المستجيبة", + "Name[be]": "Абясколерванне праграм, якія не адказваюць", + "Name[bg]": "Избледняване на нереагиращи приложения", + "Name[ca@valencia]": "Dessatura les aplicacions que no responen", + "Name[ca]": "Dessatura les aplicacions que no responen", + "Name[cs]": "Ztmavit nereagující aplikace", + "Name[da]": "Afmæt applikationer, der ikke svarer", + "Name[de]": "Sättigung von nicht reagierenden Anwendungen verringern", + "Name[en_GB]": "Desaturate Unresponsive Applications", + "Name[eo]": "Malsaturi Nerespondemaj Aplikoj", + "Name[es]": "Desaturar las aplicaciones que no responden", + "Name[et]": "Reageerimisvõimetute rakenduste muutmine kahvatuks", + "Name[eu]": "Desasetu erantzuten ez duten aplikazioak", + "Name[fi]": "Vähennä värikylläisyyttä sovelluksista, jotka eivät vastaa", + "Name[fr]": "Dé-saturer les applications non réactives", + "Name[gl]": "Reducir a saturación das aplicacións que non responden", + "Name[he]": "החלשת הרוויה ליישומים בלתי פעילים", + "Name[hu]": "Nem válaszoló alkalmazások elhalványítása", + "Name[ia]": "Desatura applicationes non responsive", + "Name[id]": "Desaturasi Aplikasi yang Tidak Responsif", + "Name[is]": "Aflita forrit sem svara ekki", + "Name[it]": "Desatura le applicazioni che non rispondono", + "Name[ja]": "応答しないアプリケーションの彩度を下げる", + "Name[ka]": "უსიცოცხლო აპლიკაციების გაუფერულება", + "Name[ko]": "응답 없는 앱을 무채색으로 전환", + "Name[lt]": "Nusodrinti nereaguojančias programas", + "Name[lv]": "Nereaģējošās programmas padarīt melnbaltas", + "Name[nb]": "Fjern fargemetting på ikke-responsive program", + "Name[nl]": "Verzadiging van niet responsieve toepassingen verminderen", + "Name[nn]": "Fjern fargemetting på ikkje-responsive program", + "Name[pl]": "Odbarwienie nieodpowiadających aplikacji", + "Name[pt]": "Reduzir a Saturação das Aplicações Bloqueadas", + "Name[pt_BR]": "Reduzir saturação de aplicativos que não respondem", + "Name[ro]": "Desaturează aplicațiile ce nu răspund", + "Name[ru]": "Обесцвечивание зависших приложений", + "Name[sa]": "अप्रतिसादित अनुप्रयोगों को असंतृप्त करें", + "Name[sk]": "Desaturovať neodpovedajúce aplikácie", + "Name[sl]": "Zmanjšaj nasičenost neodzivnih programov", + "Name[sv]": "Avmätta oemottagliga program", + "Name[ta]": "பதிலளிக்காத செயலிகளை கருப்புவெள்ளையாக்கு", + "Name[tr]": "Yanıt Vermeyen Uygulamaları Solgunlaştır", + "Name[uk]": "Зненасичення вікон, які не відповідають на запити", + "Name[vi]": "Khử bão hoà các ứng dụng không hồi đáp", + "Name[zh_CN]": "无响应窗口灰化", + "Name[zh_TW]": "降低無回應應用程式的飽和度" + }, + "X-KDE-ConfigModule": "kcm_kwin4_genericscripted", + "X-KDE-Ordering": 60, + "X-KWin-Config-TranslationDomain": "kwin", + "X-Plasma-API": "javascript" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/fullscreen/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/fullscreen/CMakeLists.txt new file mode 100644 index 0000000000..495c31fb97 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fullscreen/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_scripted_effect(fullscreen package) diff --git a/local/recipes/kde/kwin/source/src/plugins/fullscreen/package/contents/code/main.js b/local/recipes/kde/kwin/source/src/plugins/fullscreen/package/contents/code/main.js new file mode 100644 index 0000000000..ed3b958149 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fullscreen/package/contents/code/main.js @@ -0,0 +1,128 @@ +/* + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +class FullScreenEffect { + constructor() { + effect.configChanged.connect(this.loadConfig.bind(this)); + effect.animationEnded.connect(this.restoreForceBlurState.bind(this)); + + effects.windowAdded.connect(this.manage.bind(this)); + for (const window of effects.stackingOrder) { + this.manage(window); + } + + this.loadConfig(); + } + + loadConfig() { + this.duration = animationTime(250); + } + + manage(window) { + window.windowFrameGeometryChanged.connect( + this.onWindowFrameGeometryChanged.bind(this)); + window.windowFullScreenChanged.connect( + this.onWindowFullScreenChanged.bind(this)); + } + + onWindowFullScreenChanged(window) { + if (!window.visible || !window.oldGeometry) { + return; + } + window.setData(Effect.WindowForceBlurRole, true); + let oldGeometry = window.oldGeometry; + const newGeometry = window.geometry; + if (oldGeometry.width == newGeometry.width && oldGeometry.height == newGeometry.height) + oldGeometry = window.olderGeometry; + window.olderGeometry = Object.assign({}, window.oldGeometry); + window.oldGeometry = Object.assign({}, newGeometry); + + let couldRetarget = false; + if (window.fullScreenAnimation1) { + if (window.fullScreenAnimation1[0]) { + couldRetarget = retarget(window.fullScreenAnimation1[0], { + value1: newGeometry.width, + value2: newGeometry.height + }, this.duration); + } + if (window.fullScreenAnimation1[1]) { + couldRetarget = retarget(window.fullScreenAnimation1[1], { + value1: newGeometry.x + newGeometry.width / 2, + value2: newGeometry.y + newGeometry.height / 2 + }, this.duration); + } + } + if (!couldRetarget) { + if (window.fullScreenAnimation1) { + cancel(window.fullScreenAnimation1); + delete window.fullScreenAnimation1; + } + window.fullScreenAnimation1 = animate({ + window: window, + duration: this.duration, + animations: [{ + type: Effect.Size, + to: { + value1: newGeometry.width, + value2: newGeometry.height + }, + from: { + value1: oldGeometry.width, + value2: oldGeometry.height + }, + curve: QEasingCurve.OutCubic + }, { + type: Effect.Position, + to: { + value1: newGeometry.x + newGeometry.width / 2, + value2: newGeometry.y + newGeometry.height / 2 + }, + from: { + value1: oldGeometry.x + oldGeometry.width / 2, + value2: oldGeometry.y + oldGeometry.height / 2 + }, + curve: QEasingCurve.OutCubic + }] + }); + } + if (!window.resize) { + window.fullScreenAnimation2 =animate({ + window: window, + duration: this.duration, + animations: [{ + type: Effect.CrossFadePrevious, + to: 1.0, + from: 0.0, + curve: QEasingCurve.OutCubic + }] + }); + } + } + + restoreForceBlurState(window) { + window.setData(Effect.WindowForceBlurRole, null); + } + + onWindowFrameGeometryChanged(window, oldGeometry) { + if (window.fullScreenAnimation1) { + if (window.geometry.width != window.oldGeometry.width || + window.geometry.height != window.oldGeometry.height) { + if (window.fullScreenAnimation2) { + cancel(window.fullScreenAnimation2); + delete window.fullScreenAnimation2; + } + } + } + window.oldGeometry = Object.assign({}, window.geometry); + window.olderGeometry = Object.assign({}, oldGeometry); + } +} + +new FullScreenEffect(); diff --git a/local/recipes/kde/kwin/source/src/plugins/fullscreen/package/metadata.json b/local/recipes/kde/kwin/source/src/plugins/fullscreen/package/metadata.json new file mode 100644 index 0000000000..94d6b243cf --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/fullscreen/package/metadata.json @@ -0,0 +1,131 @@ +{ + "KPackageStructure": "KWin/Effect", + "KPlugin": { + "Authors": [ + { + "Email": "mgraesslin@kde.org", + "Name": "Martin Gräßlin", + "Name[ar]": "مارتن جراجلين", + "Name[be]": "Martin Gräßlin", + "Name[bg]": "Martin Gräßlin", + "Name[ca@valencia]": "Martin Gräßlin", + "Name[ca]": "Martin Gräßlin", + "Name[cs]": "Martin Gräßlin", + "Name[da]": "Martin Gräßlin", + "Name[de]": "Martin Gräßlin", + "Name[en_GB]": "Martin Gräßlin", + "Name[eo]": "Martin Gräßlin", + "Name[es]": "Martin Gräßlin", + "Name[et]": "Martin Gräßlin", + "Name[eu]": "Martin Gräßlin", + "Name[fi]": "Martin Gräßlin", + "Name[fr]": "Martin Gräßlin", + "Name[ga]": "Martin Gräßlin", + "Name[gl]": "Martin Gräßlin.", + "Name[he]": "מרטין גרייסלין", + "Name[hu]": "Martin Gräßlin", + "Name[ia]": "Martin Gräßlin", + "Name[id]": "Martin Gräßlin", + "Name[is]": "Martin Gräßlin", + "Name[it]": "Martin Gräßlin", + "Name[ja]": "Martin Gräßlin", + "Name[ka]": "მარტინ გრესსლინი", + "Name[ko]": "Martin Gräßlin", + "Name[lt]": "Martin Gräßlin", + "Name[lv]": "Martin Gräßlin", + "Name[nb]": "Martin Gräßlin", + "Name[nl]": "Martin Gräßlin", + "Name[nn]": "Martin Gräßlin", + "Name[pl]": "Martin Gräßlin", + "Name[pt]": "Martin Gräßlin", + "Name[pt_BR]": "Martin Gräßlin", + "Name[ro]": "Martin Gräßlin", + "Name[ru]": "Martin Gräßlin", + "Name[sa]": "मार्टिन् ग्रास्लिन्", + "Name[sk]": "Martin Gräßlin", + "Name[sl]": "Martin Gräßlin", + "Name[sv]": "Martin Gräßlin", + "Name[ta]": "மார்ட்டின் கிராஸ்லின்", + "Name[tr]": "Martin Gräßlin", + "Name[uk]": "Martin Gräßlin", + "Name[vi]": "Martin Gräßlin", + "Name[zh_CN]": "Martin Gräßlin", + "Name[zh_TW]": "Martin Gräßlin" + } + ], + "Category": "Appearance", + "Description": "Stretch windows going to and leaving full screen mode", + "Description[ar]": "تمديد النوافذ للانتقال إلى وضع ملء الشاشة والخروج منه", + "Description[bg]": "Разтягане за прозорците при влизане и излизане от режим на цял екран", + "Description[ca@valencia]": "Estira les finestres quan entren o abandonen el mode de pantalla completa", + "Description[ca]": "Estira les finestres quan entren o abandonen el mode de pantalla completa", + "Description[de]": "Fenster beim Umschalten des Vollbildmodus strecken", + "Description[es]": "Estirar las ventanas que entran o salen del modo a pantalla completa", + "Description[eu]": "Luzatu, pantaila-osoko modutik/modura aldatzen diren leihoak", + "Description[fi]": "Venytä ikkunoita siirryttäessä koko näytölle tai poistuttaessa siltä", + "Description[fr]": "Étirer la fenêtre apparaissant et sortir du mode plein écran", + "Description[he]": "למתוח את החלונות בכניסה וביציאה ממצב מסך מלא", + "Description[hu]": "A teljes képernyős módba belépő és azt elhagyó ablakok nyújtása", + "Description[ia]": "Extende fenestras quando va a e abandona modo de schermo plen", + "Description[is]": "Teygja glugga þegar þeir fylla skjá eða hætta að fylla skjá", + "Description[it]": "Allunga le finestre passando alla modalità a schermo intero e uscendo da essa", + "Description[ja]": "フルスクリーンモード開始時/終了時にウィンドウをストレッチします", + "Description[ka]": "ფანჯრების გაწელვა სრულ ეკრანზე გასვლისას და სრული ეკრანიდან გამოსვლისას", + "Description[ko]": "창이 전체 화면 모드로 진입하거나 벗어날 때 잡아 당깁니다", + "Description[lt]": "Ištempti langus, kurių viso ekrano veiksena įjungiama ar išjungiama", + "Description[lv]": "Staipīt logus, gan ieejat un izejat no pilnekrāna režīma", + "Description[nl]": "Vensters uitrekken die gaan naar volledig scherm en deze modus verlaten", + "Description[nn]": "Strekk vindauge som går til eller frå fullskjermsmodus", + "Description[pl]": "Rozciąganie okien wchodzących i opuszczających tryb pełnoekranowy", + "Description[pt_BR]": "Alongar janela indo para o modo de tela cheia e saindo dele", + "Description[ro]": "Întinde ferestrele ce intră sau ies din regim de ecran complet", + "Description[ru]": "Растягивание окон при переходе в полноэкранный режим и выходе из него", + "Description[sk]": "Animácia pre prechod a opustenie okna z režimu celej obrazovky", + "Description[sl]": "Raztegni okna, ki gredo v celozaslonski način in ga zapuščajo", + "Description[sv]": "Sträck ut fönster som går till och lämnar fullskärmsläge", + "Description[tr]": "Tam ekran kipine giren veya çıkan pencereleri ger", + "Description[uk]": "Розтягування вікон під час входу до повноекранного режиму і виходу з нього", + "Description[zh_CN]": "窗口进入/退出全屏模式时呈现拉伸动效", + "Description[zh_TW]": "拉伸正在進入或離開全螢幕的視窗", + "EnabledByDefault": true, + "Icon": "preferences-system-windows-effect-fullscreen", + "Id": "fullscreen", + "License": "GPL", + "Name": "Stretch", + "Name[ar]": "التمديد", + "Name[bg]": "Разтягане", + "Name[ca@valencia]": "Estira", + "Name[ca]": "Estira", + "Name[de]": "Strecken", + "Name[es]": "Estirar", + "Name[eu]": "Luzatu", + "Name[fi]": "Venytä", + "Name[fr]": "Étirer", + "Name[he]": "מתיחה", + "Name[hu]": "Nyújtás", + "Name[ia]": "A extension", + "Name[is]": "Teygja", + "Name[it]": "Allunga", + "Name[ja]": "ストレッチ", + "Name[ka]": "გაწელვა", + "Name[ko]": "늘이기", + "Name[lt]": "Ištempti", + "Name[lv]": "Izstiept", + "Name[nl]": "Uitrekken", + "Name[nn]": "Strekk", + "Name[pl]": "Rozciąganie", + "Name[pt_BR]": "Alongar", + "Name[ro]": "Întinde", + "Name[ru]": "Растягивание", + "Name[sk]": "Natiahnuť", + "Name[sl]": "Raztegni", + "Name[sv]": "Sträck", + "Name[tr]": "Ger", + "Name[uk]": "Розтягнути", + "Name[zh_CN]": "拉伸动效", + "Name[zh_TW]": "拉伸" + }, + "X-KDE-Ordering": 60, + "X-KWin-Exclusive-Category": "fullscreen", + "X-Plasma-API": "javascript" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/glide/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/glide/CMakeLists.txt new file mode 100644 index 0000000000..0df339d16d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/glide/CMakeLists.txt @@ -0,0 +1,36 @@ +####################################### +# Effect + +set(glide_SOURCES + glide.cpp + main.cpp +) + +kconfig_add_kcfg_files(glide_SOURCES + glideconfig.kcfgc +) + +kwin_add_builtin_effect(glide ${glide_SOURCES}) +target_link_libraries(glide PRIVATE + kwin + + KF6::ConfigGui +) + +####################################### +# Config +if (KWIN_BUILD_KCMS) + set(kwin_glide_config_SRCS glide_config.cpp) + ki18n_wrap_ui(kwin_glide_config_SRCS glide_config.ui) + kconfig_add_kcfg_files(kwin_glide_config_SRCS glideconfig.kcfgc) + + kwin_add_effect_config(kwin_glide_config ${kwin_glide_config_SRCS}) + + target_link_libraries(kwin_glide_config + KF6::KCMUtils + KF6::CoreAddons + KF6::I18n + Qt::DBus + KWinEffectsInterface + ) +endif() diff --git a/local/recipes/kde/kwin/source/src/plugins/glide/glide.cpp b/local/recipes/kde/kwin/source/src/plugins/glide/glide.cpp new file mode 100644 index 0000000000..4f92b4effb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/glide/glide.cpp @@ -0,0 +1,308 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Philip Falkner + SPDX-FileCopyrightText: 2009 Martin Gräßlin + SPDX-FileCopyrightText: 2010 Alexandre Pereira + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +// own +#include "glide.h" + +// KConfigSkeleton +#include "glideconfig.h" + +#include "core/rendertarget.h" +#include "core/renderviewport.h" +#include "effect/effecthandler.h" +#include "scene/windowitem.h" + +// Qt +#include +#include +#include + +#include + +using namespace std::chrono_literals; + +namespace KWin +{ + +static const QSet s_blacklist{ + QStringLiteral("ksmserver ksmserver"), + QStringLiteral("ksmserver-logout-greeter ksmserver-logout-greeter"), + QStringLiteral("ksplashqml ksplashqml"), +}; + +GlideEffect::GlideEffect() +{ + GlideConfig::instance(effects->config()); + reconfigure(ReconfigureAll); + + connect(effects, &EffectsHandler::windowAdded, this, &GlideEffect::windowAdded); + connect(effects, &EffectsHandler::windowClosed, this, &GlideEffect::windowClosed); + connect(effects, &EffectsHandler::windowDataChanged, this, &GlideEffect::windowDataChanged); +} + +GlideEffect::~GlideEffect() = default; + +void GlideEffect::reconfigure(ReconfigureFlags flags) +{ + GlideConfig::self()->read(); + m_duration = std::chrono::milliseconds(animationTime(160ms)); + + m_inParams.edge = static_cast(GlideConfig::inRotationEdge()); + m_inParams.angle.from = GlideConfig::inRotationAngle(); + m_inParams.angle.to = 0.0; + m_inParams.distance.from = GlideConfig::inDistance(); + m_inParams.distance.to = 0.0; + m_inParams.opacity.from = GlideConfig::inOpacity(); + m_inParams.opacity.to = 1.0; + + m_outParams.edge = static_cast(GlideConfig::outRotationEdge()); + m_outParams.angle.from = 0.0; + m_outParams.angle.to = GlideConfig::outRotationAngle(); + m_outParams.distance.from = 0.0; + m_outParams.distance.to = GlideConfig::outDistance(); + m_outParams.opacity.from = 1.0; + m_outParams.opacity.to = GlideConfig::outOpacity(); +} + +void GlideEffect::prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) +{ + auto animationIt = m_animations.find(w); + if (animationIt != m_animations.end()) { + animationIt->second.timeLine.advance(presentTime); + data.setTransformed(); + } + + effects->prePaintWindow(view, w, data, presentTime); +} + +void GlideEffect::apply(EffectWindow *window, int mask, WindowPaintData &data, WindowQuadList &quads) +{ + auto animationIt = m_animations.find(window); + if (animationIt == m_animations.end()) { + return; + } + + const GlideParams params = window->isDeleted() ? m_outParams : m_inParams; + const qreal t = animationIt->second.timeLine.value(); + + const QRectF rect = window->expandedGeometry().translated(-window->pos()); + const float fovY = std::tan(qDegreesToRadians(60.0f) / 2); + const float aspect = rect.width() / rect.height(); + const float zNear = 0.1f; + const float zFar = 100.0f; + + const float yMax = zNear * fovY; + const float yMin = -yMax; + const float xMin = yMin * aspect; + const float xMax = yMax * aspect; + + const float scaleFactor = 1.1 * fovY / yMax; + + QMatrix4x4 matrix; + matrix.viewport(rect); + matrix.frustum(xMin, xMax, yMax, yMin, zNear, zFar); + matrix.translate(xMin * scaleFactor, yMax * scaleFactor, -1.1); + matrix.scale((xMax - xMin) * scaleFactor / rect.width(), -(yMax - yMin) * scaleFactor / rect.height(), 0.001); + matrix.translate(-rect.x(), -rect.y()); + + const qreal angle = interpolate(params.angle.from, params.angle.to, t); + const qreal distance = interpolate(params.distance.from, params.distance.to, t); + switch (params.edge) { + case RotationEdge::Right: + matrix.translate(window->width(), window->height() / 2, -distance); + matrix.rotate(-angle, 0, 1, 0); + matrix.translate(-window->width(), -window->height() / 2); + break; + + case RotationEdge::Bottom: + matrix.translate(window->width() / 2, window->height(), -distance); + matrix.rotate(angle, 1, 0, 0); + matrix.translate(-window->width() / 2, -window->height()); + break; + + case RotationEdge::Left: + matrix.translate(0, window->height() / 2, -distance); + matrix.rotate(angle, 0, 1, 0); + matrix.translate(0, -window->height() / 2); + break; + + case RotationEdge::Top: + default: + matrix.translate(window->width() / 2, 0, -distance); + matrix.rotate(-angle, 1, 0, 0); + matrix.translate(-window->width() / 2, 0); + break; + } + + quads = quads.makeRegularGrid(20, 20); + for (WindowQuad &quad : quads) { + for (int i = 0; i < 4; ++i) { + const QPointF transformed = matrix.map(QPointF(quad[i].x(), quad[i].y())); + quad[i].setX(transformed.x()); + quad[i].setY(transformed.y()); + } + } + + data.multiplyOpacity(interpolate(params.opacity.from, params.opacity.to, t)); +} + +void GlideEffect::postPaintScreen() +{ + for (auto animationIt = m_animations.begin(); animationIt != m_animations.end();) { + EffectWindow *w = animationIt->first; + w->addRepaintFull(); + + if (animationIt->second.timeLine.done()) { + unredirect(animationIt->first); + animationIt = m_animations.erase(animationIt); + } else { + ++animationIt; + } + } + + effects->postPaintScreen(); +} + +bool GlideEffect::isActive() const +{ + return !m_animations.empty(); +} + +bool GlideEffect::supported() +{ + return effects->isOpenGLCompositing() + && effects->animationsSupported(); +} + +void GlideEffect::windowAdded(EffectWindow *w) +{ + if (effects->activeFullScreenEffect()) { + return; + } + + if (!isGlideWindow(w)) { + return; + } + + if (!w->isVisible()) { + return; + } + + const void *addGrab = w->data(WindowAddedGrabRole).value(); + if (addGrab) { + return; + } + + GlideAnimation &animation = m_animations[w]; + animation.timeLine.reset(); + animation.timeLine.setDirection(TimeLine::Forward); + animation.timeLine.setDuration(m_duration); + animation.timeLine.setEasingCurve(QEasingCurve::InCurve); + animation.effect = ItemEffect(w->windowItem()); + + redirect(w); + effects->addRepaintFull(); +} + +void GlideEffect::windowClosed(EffectWindow *w) +{ + const void *closeGrab = w->data(WindowClosedGrabRole).value(); + if (closeGrab) { + return; + } + if (effects->activeFullScreenEffect() || !isGlideWindow(w) || !w->isVisible() || w->skipsCloseAnimation()) { + const auto it = m_animations.find(w); + if (it != m_animations.end()) { + unredirect(w); + m_animations.erase(it); + } + return; + } + + GlideAnimation &animation = m_animations[w]; + animation.deletedRef = EffectWindowDeletedRef(w); + animation.timeLine.reset(); + animation.timeLine.setDirection(TimeLine::Forward); + animation.timeLine.setDuration(m_duration); + animation.timeLine.setEasingCurve(QEasingCurve::OutCurve); + + redirect(w); + effects->addRepaintFull(); +} + +void GlideEffect::windowDataChanged(EffectWindow *w, int role) +{ + if (role != WindowAddedGrabRole && role != WindowClosedGrabRole) { + return; + } + + auto animationIt = m_animations.find(w); + if (animationIt != m_animations.end()) { + unredirect(animationIt->first); + m_animations.erase(animationIt); + } +} + +bool GlideEffect::isGlideWindow(EffectWindow *w) const +{ + // We don't want to animate most of plasmashell's windows, yet, some + // of them we want to, for example, Task Manager Settings window. + // The problem is that all those window share single window class. + // So, the only way to decide whether a window should be animated is + // to use a heuristic: if a window has decoration, then it's most + // likely a dialog or a settings window so we have to animate it. + if (w->windowClass() == QLatin1String("plasmashell plasmashell") + || w->windowClass() == QLatin1String("plasmashell org.kde.plasmashell")) { + return w->hasDecoration(); + } + + if (w->windowClass() == QLatin1String("spectacle org.kde.spectacle") + && w->tag() == QLatin1String("region-editor")) { + return false; + } + + if (s_blacklist.contains(w->windowClass())) { + return false; + } + + if (w->hasDecoration()) { + return true; + } + + // Don't animate combobox popups, tooltips, popup menus, etc. + if (w->isPopupWindow()) { + return false; + } + + // Don't animate the outline and the screenlocker as it looks bad. + if (w->isLockScreen() || w->isOutline()) { + return false; + } + + // Override-redirect windows are usually used for user interface + // concepts that are not expected to be animated by this effect. + if (w->isX11Client() && !w->isManaged()) { + return false; + } + + return w->isNormalWindow() + || w->isDialog(); +} + +bool GlideEffect::blocksDirectScanout() const +{ + return false; +} + +} // namespace KWin + +#include "moc_glide.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/glide/glide.h b/local/recipes/kde/kwin/source/src/plugins/glide/glide.h new file mode 100644 index 0000000000..ff7fc4f3e1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/glide/glide.h @@ -0,0 +1,155 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Philip Falkner + SPDX-FileCopyrightText: 2009 Martin Gräßlin + SPDX-FileCopyrightText: 2010 Alexandre Pereira + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +// kwineffects +#include "effect/effectwindow.h" +#include "effect/offscreeneffect.h" +#include "effect/timeline.h" +#include "scene/item.h" + +#include + +namespace KWin +{ + +struct GlideAnimation +{ + EffectWindowDeletedRef deletedRef; + TimeLine timeLine; + ItemEffect effect; +}; + +class GlideEffect : public OffscreenEffect +{ + Q_OBJECT + Q_PROPERTY(int duration READ duration) + Q_PROPERTY(RotationEdge inRotationEdge READ inRotationEdge) + Q_PROPERTY(qreal inRotationAngle READ inRotationAngle) + Q_PROPERTY(qreal inDistance READ inDistance) + Q_PROPERTY(qreal inOpacity READ inOpacity) + Q_PROPERTY(RotationEdge outRotationEdge READ outRotationEdge) + Q_PROPERTY(qreal outRotationAngle READ outRotationAngle) + Q_PROPERTY(qreal outDistance READ outDistance) + Q_PROPERTY(qreal outOpacity READ outOpacity) + +public: + GlideEffect(); + ~GlideEffect() override; + + void reconfigure(ReconfigureFlags flags) override; + void prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) override; + void postPaintScreen() override; + bool isActive() const override; + int requestedEffectChainPosition() const override; + + static bool supported(); + + enum RotationEdge { + Top = 0, + Right = 1, + Bottom = 2, + Left = 3 + }; + Q_ENUM(RotationEdge) + + int duration() const; + RotationEdge inRotationEdge() const; + qreal inRotationAngle() const; + qreal inDistance() const; + qreal inOpacity() const; + RotationEdge outRotationEdge() const; + qreal outRotationAngle() const; + qreal outDistance() const; + qreal outOpacity() const; + bool blocksDirectScanout() const override; + +protected: + void apply(EffectWindow *window, int mask, WindowPaintData &data, WindowQuadList &quads) override; + +private Q_SLOTS: + void windowAdded(EffectWindow *w); + void windowClosed(EffectWindow *w); + void windowDataChanged(EffectWindow *w, int role); + +private: + bool isGlideWindow(EffectWindow *w) const; + + std::chrono::milliseconds m_duration; + std::unordered_map m_animations; + + struct GlideParams + { + RotationEdge edge; + struct + { + qreal from; + qreal to; + } angle, distance, opacity; + }; + + GlideParams m_inParams; + GlideParams m_outParams; +}; + +inline int GlideEffect::requestedEffectChainPosition() const +{ + return 50; +} + +inline int GlideEffect::duration() const +{ + return m_duration.count(); +} + +inline GlideEffect::RotationEdge GlideEffect::inRotationEdge() const +{ + return m_inParams.edge; +} + +inline qreal GlideEffect::inRotationAngle() const +{ + return m_inParams.angle.from; +} + +inline qreal GlideEffect::inDistance() const +{ + return m_inParams.distance.from; +} + +inline qreal GlideEffect::inOpacity() const +{ + return m_inParams.opacity.from; +} + +inline GlideEffect::RotationEdge GlideEffect::outRotationEdge() const +{ + return m_outParams.edge; +} + +inline qreal GlideEffect::outRotationAngle() const +{ + return m_outParams.angle.to; +} + +inline qreal GlideEffect::outDistance() const +{ + return m_outParams.distance.to; +} + +inline qreal GlideEffect::outOpacity() const +{ + return m_outParams.opacity.to; +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/glide/glide.kcfg b/local/recipes/kde/kwin/source/src/plugins/glide/glide.kcfg new file mode 100644 index 0000000000..d90520ca56 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/glide/glide.kcfg @@ -0,0 +1,40 @@ + + + + + + 0 + + + 0 + + + 3.0 + + + 30.0 + + + 0.4 + 0.0 + 1.0 + + + 2 + + + 3.0 + + + 30.0 + + + 0.0 + 0.0 + 1.0 + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/glide/glide_config.cpp b/local/recipes/kde/kwin/source/src/plugins/glide/glide_config.cpp new file mode 100644 index 0000000000..83dc4b9e1e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/glide/glide_config.cpp @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: 2010 Fredrik Höglund + SPDX-FileCopyrightText: 2010 Alexandre Pereira + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "glide_config.h" + +#include "config-kwin.h" + +// KConfigSkeleton +#include "glideconfig.h" + +#include +#include + +K_PLUGIN_CLASS(KWin::GlideEffectConfig) + +namespace KWin +{ + +GlideEffectConfig::GlideEffectConfig(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) +{ + ui.setupUi(widget()); + GlideConfig::instance(KWIN_CONFIG); + addConfig(GlideConfig::self(), widget()); +} + +GlideEffectConfig::~GlideEffectConfig() +{ +} + +void GlideEffectConfig::save() +{ + KCModule::save(); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("glide")); +} + +} // namespace KWin + +#include "glide_config.moc" + +#include "moc_glide_config.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/glide/glide_config.h b/local/recipes/kde/kwin/source/src/plugins/glide/glide_config.h new file mode 100644 index 0000000000..056ee18a53 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/glide/glide_config.h @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2010 Fredrik Höglund + SPDX-FileCopyrightText: 2010 Alexandre Pereira + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "ui_glide_config.h" +#include + +namespace KWin +{ + +class GlideEffectConfig : public KCModule +{ + Q_OBJECT + +public: + explicit GlideEffectConfig(QObject *parent, const KPluginMetaData &data); + ~GlideEffectConfig() override; + + void save() override; + +private: + ::Ui::GlideEffectConfig ui; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/glide/glide_config.ui b/local/recipes/kde/kwin/source/src/plugins/glide/glide_config.ui new file mode 100644 index 0000000000..f5bdef3056 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/glide/glide_config.ui @@ -0,0 +1,260 @@ + + + GlideEffectConfig + + + + 0 + 0 + 440 + 375 + + + + + + + + + Duration: + + + + + + + + 0 + 0 + + + + Default + + + milliseconds + + + 9999 + + + 5 + + + + + + + + + Window Open Animation + + + + + + Rotation edge: + + + + + + + + 0 + 0 + + + + + Top + + + + + Right + + + + + Bottom + + + + + Left + + + + + + + + Rotation angle: + + + + + + + + 0 + 0 + + + + + + + -360 + + + 360 + + + + + + + Distance: + + + + + + + + 0 + 0 + + + + -5000 + + + 5000 + + + 5 + + + + + + + + + + Window Close Animation + + + + + + Rotation edge: + + + + + + + + 0 + 0 + + + + + Top + + + + + Right + + + + + Bottom + + + + + Left + + + + + + + + Rotation angle: + + + + + + + Distance: + + + + + + + + 0 + 0 + + + + + + + -360 + + + 360 + + + + + + + + 0 + 0 + + + + -5000 + + + 5000 + + + 5 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/glide/glideconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/glide/glideconfig.kcfgc new file mode 100644 index 0000000000..3266f2c58d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/glide/glideconfig.kcfgc @@ -0,0 +1,5 @@ +File=glide.kcfg +ClassName=GlideConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/local/recipes/kde/kwin/source/src/plugins/glide/main.cpp b/local/recipes/kde/kwin/source/src/plugins/glide/main.cpp new file mode 100644 index 0000000000..9e37921528 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/glide/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "glide.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(GlideEffect, + "metadata.json.stripped", + return GlideEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/glide/metadata.json b/local/recipes/kde/kwin/source/src/plugins/glide/metadata.json new file mode 100644 index 0000000000..9c34e7ecab --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/glide/metadata.json @@ -0,0 +1,103 @@ +{ + "KPlugin": { + "Category": "Window Open/Close Animation", + "Description": "Glide windows as they appear or disappear", + "Description[ar]": "تنزلق النوافذ حال ظهورها أو اختفائها", + "Description[az]": "Pəncərələrin süzülərək açılması və ya bağlanması", + "Description[be]": "Плаўнасць пры з'яўленні або знікненні акон", + "Description[bg]": "Плъзгане на прозорците, докато се появяват или изчезват", + "Description[ca@valencia]": "Les finestres llisquen en aparéixer o desaparéixer", + "Description[ca]": "Les finestres llisquen en aparèixer o desaparèixer", + "Description[cs]": "Klouzat okny při jejich objevení nebo zmizení", + "Description[da]": "Glid vinduer, når de vises eller forsvinder", + "Description[de]": "Fenster beim Erscheinen oder Verschwinden gleiten lassen", + "Description[en_GB]": "Glide windows as they appear or disappear", + "Description[eo]": "Gliti fenestrojn kiel ili aperas aŭ malaperas", + "Description[es]": "Deslizar las ventanas a medida que aparecen o desaparecen", + "Description[et]": "Akende liuglemine ilmumisel või kadumisel", + "Description[eu]": "Irristatu leihoak haiek agertzean edo desagertzean", + "Description[fi]": "Liu’uta ilmestyviä tai katoavia ikkunoita", + "Description[fr]": "Faire glisser les fenêtres lorsqu'elles apparaissent ou disparaissent", + "Description[gl]": "Desprazar as xanelas cando aparecen ou desaparecen.", + "Description[he]": "הגלשת חלונות כשהם מופיעים או נעלמים", + "Description[hu]": "Be- és kicsúsztatja az ablakokat ahogy azok megjelennek vagy eltűnnek", + "Description[ia]": "Glissa fenestras quando illes appare e disappare", + "Description[id]": "Glide Jendela sebagaimana ia muncul atau menghilang", + "Description[is]": "Renna gluggum fram og aftur þegar þeir birtast eða hverfa", + "Description[it]": "Planata delle finestre quando appaiono o scompaiono", + "Description[ja]": "出現/消失するウィンドウがグライドします", + "Description[ka]": "ფანჯრების სრიალი მათი გაჩენის ან გაქრბისას", + "Description[ko]": "창이 나타나거나 사라질 때 미끄러짐 효과 사용하기", + "Description[lt]": "Padaryti, kad langai sklandytų jiems atsirandant ir išnykstant", + "Description[lv]": "Slidināt logus, tiem parādoties vai pazūdot", + "Description[nb]": "Gli inn/ut vinduer", + "Description[nl]": "Vensters laten glijden als ze verschijnen of verdwijnen", + "Description[nn]": "Glid inn/ut vindauge", + "Description[pl]": "Okna obracają się na zawiasie przy otwieraniu lub zamykaniu", + "Description[pt]": "Deslizar as janelas à medida que aparecem e desaparecem", + "Description[pt_BR]": "Deslizar as janelas à medida que aparecem ou desaparecem", + "Description[ro]": "Ferestrele se scurg când apar sau dispar", + "Description[ru]": "Эффект «глиссирования» окон при появлении и исчезновении", + "Description[sa]": "यथा यथा दृश्यन्ते अथवा अन्तर्धानं भवन्ति तथा तथा खिडकयः स्लाइड् कुर्वन्तु", + "Description[sk]": "Kĺzať okná ako sa objavujú alebo miznú", + "Description[sl]": "Okna drsijo, ko se pojavijo ali izginejo", + "Description[sv]": "Glid fönster när de visas eller försvinner", + "Description[tr]": "Pencereleri görünürken veya kaybolurken süzülmelerini sağla", + "Description[uk]": "Ковзання вікон при появі або зникненні", + "Description[vi]": "Lướt các cửa sổ khi chúng xuất hiện hoặc biến mất", + "Description[zh_CN]": "窗口显示/隐藏时呈现滑翔过渡动效", + "Description[zh_TW]": "讓視窗出現與消失時使用滑行效果", + "EnabledByDefault": false, + "License": "GPL", + "Name": "Glide", + "Name[ar]": "الطيران", + "Name[az]": "Sürüşmə", + "Name[be]": "Плаўнасць", + "Name[bg]": "Плъзгане", + "Name[ca@valencia]": "Lliscament", + "Name[ca]": "Lliscament", + "Name[cs]": "Klouzání", + "Name[da]": "Glid", + "Name[de]": "Gleiten", + "Name[en_GB]": "Glide", + "Name[eo]": "Gliti", + "Name[es]": "Planear", + "Name[et]": "Liuglemine", + "Name[eu]": "Irristatu", + "Name[fi]": "Ikkunaliuku", + "Name[fr]": "Glisser", + "Name[gl]": "Desprazar", + "Name[he]": "גלישה", + "Name[hu]": "Csúsztatás", + "Name[ia]": "Glissa", + "Name[id]": "Glide", + "Name[is]": "Renningur", + "Name[it]": "Plana", + "Name[ja]": "グライド", + "Name[ka]": "ლივლივი", + "Name[ko]": "글라이드", + "Name[lt]": "Sklandymas", + "Name[lv]": "Ieslīdēt", + "Name[nb]": "Gli", + "Name[nl]": "Schuiven", + "Name[nn]": "Skliding", + "Name[pl]": "Obracanie na zawiasie", + "Name[pt]": "Deslizar", + "Name[pt_BR]": "Deslizar", + "Name[ro]": "Scurgere", + "Name[ru]": "Скольжение", + "Name[sa]": "ग्लाइड्", + "Name[sk]": "Kĺzať", + "Name[sl]": "Drsanje", + "Name[sv]": "Glid", + "Name[tr]": "Süzül", + "Name[uk]": "Плин", + "Name[vi]": "Lướt", + "Name[zh_CN]": "滑翔动效", + "Name[zh_TW]": "滑行" + }, + "X-KDE-ConfigModule": "kwin_glide_config", + "org.kde.kwin.effect": { + "exclusiveGroup": "toplevel-open-close-animation" + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/hidecursor/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/hidecursor/CMakeLists.txt new file mode 100644 index 0000000000..cf5747c109 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/hidecursor/CMakeLists.txt @@ -0,0 +1,34 @@ +kwin_add_builtin_effect(hidecursor) + +target_sources(hidecursor PRIVATE + hidecursor.cpp + main.cpp +) + +kconfig_add_kcfg_files(hidecursor + hidecursorconfig.kcfgc +) + +target_link_libraries(hidecursor PRIVATE + kwin + + KF6::ConfigGui +) + +####################################### +# Config +if (KWIN_BUILD_KCMS) + set(kwin_hidecursor_config_SRCS hidecursor_config.cpp) + ki18n_wrap_ui(kwin_hidecursor_config_SRCS hidecursor_config.ui) + kconfig_add_kcfg_files(kwin_hidecursor_config_SRCS hidecursorconfig.kcfgc) + + kwin_add_effect_config(kwin_hidecursor_config ${kwin_hidecursor_config_SRCS}) + + target_link_libraries(kwin_hidecursor_config + KF6::KCMUtils + KF6::CoreAddons + KF6::I18n + KF6::XmlGui + KWinEffectsInterface + ) +endif() diff --git a/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor.cpp b/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor.cpp new file mode 100644 index 0000000000..4c521203fd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor.cpp @@ -0,0 +1,121 @@ +/* + SPDX-FileCopyrightText: 2024 Jin Liu + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "hidecursor.h" +#include "cursor.h" +#include "effect/effecthandler.h" +#include "hidecursorconfig.h" +#include "input_event.h" + +namespace KWin +{ + +HideCursorEffect::HideCursorEffect() + : m_cursor(Cursors::self()->mouse()) +{ + input()->installInputEventSpy(this); + + m_inactivityTimer.setSingleShot(true); + connect(&m_inactivityTimer, &QTimer::timeout, this, [this]() { + hideCursor(); + }); + + HideCursorConfig::instance(effects->config()); + reconfigure(ReconfigureAll); +} + +HideCursorEffect::~HideCursorEffect() +{ + showCursor(); +} + +void HideCursorEffect::reconfigure(ReconfigureFlags flags) +{ + HideCursorConfig::self()->read(); + m_inactivityDuration = HideCursorConfig::inactivityDuration() * 1000; + m_hideOnTyping = HideCursorConfig::hideOnTyping(); + + m_inactivityTimer.stop(); + showCursor(); + if (m_inactivityDuration > 0) { + m_inactivityTimer.start(m_inactivityDuration); + } +} + +bool HideCursorEffect::isActive() const +{ + return false; +} + +void HideCursorEffect::activity() +{ + showCursor(); + if (m_inactivityDuration > 0) { + m_inactivityTimer.start(m_inactivityDuration); + } +} + +void HideCursorEffect::pointerMotion(PointerMotionEvent *event) +{ + activity(); +} + +void HideCursorEffect::pointerButton(PointerButtonEvent *event) +{ + activity(); +} + +void HideCursorEffect::pointerAxis(PointerAxisEvent *event) +{ + activity(); +} + +void HideCursorEffect::tabletToolProximityEvent(TabletToolProximityEvent *event) +{ + activity(); +} + +void HideCursorEffect::tabletToolAxisEvent(TabletToolAxisEvent *event) +{ + activity(); +} + +void HideCursorEffect::tabletToolTipEvent(TabletToolTipEvent *event) +{ + activity(); +} + +void HideCursorEffect::keyboardKey(KeyboardKeyEvent *event) +{ + // All functional keys have a Qt key code greater than 0x01000000 + // https://doc.qt.io/qt-6/qt.html#Key-enum + // We don't want to hide the cursor when the user presses a functional key, since they are + // usually interleaved with mouse movements. + if (m_hideOnTyping && !m_cursorHidden && event->state == KeyboardKeyState::Pressed && event->key < 0x01000000 + && (event->modifiers == Qt::NoModifier || event->modifiers == Qt::ShiftModifier)) { + hideCursor(); + } +} + +void HideCursorEffect::showCursor() +{ + if (m_cursorHidden) { + effects->showCursor(); + m_cursorHidden = false; + } +} + +void HideCursorEffect::hideCursor() +{ + if (!m_cursorHidden) { + effects->hideCursor(); + m_cursorHidden = true; + } +} + +} // namespace KWin + +#include "moc_hidecursor.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor.h b/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor.h new file mode 100644 index 0000000000..7a6740f0b2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor.h @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2024 Jin Liu + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/effect.h" +#include "input_event_spy.h" + +#include + +namespace KWin +{ + +class Cursor; + +class HideCursorEffect : public Effect, public InputEventSpy +{ + Q_OBJECT + +public: + HideCursorEffect(); + ~HideCursorEffect() override; + + void reconfigure(ReconfigureFlags flags) override; + bool isActive() const override; + + void pointerMotion(PointerMotionEvent *event) override; + void pointerButton(PointerButtonEvent *event) override; + void pointerAxis(PointerAxisEvent *event) override; + void keyboardKey(KeyboardKeyEvent *event) override; + void tabletToolProximityEvent(TabletToolProximityEvent *event) override; + void tabletToolAxisEvent(TabletToolAxisEvent *event) override; + void tabletToolTipEvent(TabletToolTipEvent *event) override; + +private: + void showCursor(); + void hideCursor(); + + void activity(); + + int m_inactivityDuration; + bool m_hideOnTyping; + + Cursor *m_cursor; + bool m_cursorHidden = false; + QTimer m_inactivityTimer; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor_config.cpp b/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor_config.cpp new file mode 100644 index 0000000000..a1094cf360 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor_config.cpp @@ -0,0 +1,81 @@ +/* + SPDX-FileCopyrightText: 2024 Jin Liu + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "hidecursor_config.h" + +#include + +// KConfigSkeleton +#include "hidecursorconfig.h" + +#include +#include + +K_PLUGIN_CLASS(KWin::HideCursorEffectConfig) + +namespace KWin +{ + +InactivityDurationComboBox::InactivityDurationComboBox(QWidget *parent) + : QComboBox(parent) +{ + connect(this, &QComboBox::currentIndexChanged, this, &InactivityDurationComboBox::durationChanged); +} + +uint InactivityDurationComboBox::duration() const +{ + return currentData().toUInt(); +} + +void InactivityDurationComboBox::setDuration(uint duration) +{ + const int index = findData(duration); + if (index == -1) { + qWarning() << "Unknown duration:" << duration; + } else { + setCurrentIndex(index); + } +} + +HideCursorEffectConfig::HideCursorEffectConfig(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) +{ + HideCursorConfig::instance(KWIN_CONFIG); + m_ui.setupUi(widget()); + + auto inactivityDurationComboBox = new InactivityDurationComboBox(widget()); + inactivityDurationComboBox->setObjectName(QStringLiteral("kcfg_InactivityDuration")); + inactivityDurationComboBox->setProperty("kcfg_property", QStringLiteral("duration")); + m_ui.formLayout->setWidget(0, QFormLayout::FieldRole, inactivityDurationComboBox); + + const QList choices{0, 1, 5, 10, 15, 30, 60}; + for (const uint &choice : choices) { + if (choice == 0) { + inactivityDurationComboBox->addItem(i18nc("@item:inmenu never hide cursor on inactivity", "Never"), choice); + } else { + inactivityDurationComboBox->addItem(i18np("%1 second", "%1 seconds", choice), choice); + } + } + if (const uint configuredInactivityDuration = HideCursorConfig::inactivityDuration(); !choices.contains(configuredInactivityDuration)) { + inactivityDurationComboBox->addItem(i18np("%1 second", "%1 seconds", configuredInactivityDuration), configuredInactivityDuration); + } + + addConfig(HideCursorConfig::self(), widget()); +} + +void HideCursorEffectConfig::save() +{ + KCModule::save(); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("hidecursor")); +} + +} // namespace + +#include "hidecursor_config.moc" + +#include "moc_hidecursor_config.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor_config.h b/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor_config.h new file mode 100644 index 0000000000..9bf158330b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor_config.h @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2024 Jin Liu + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +#include "ui_hidecursor_config.h" + +class KActionCollection; + +namespace KWin +{ + +class InactivityDurationComboBox : public QComboBox +{ + Q_OBJECT + Q_PROPERTY(uint duration READ duration WRITE setDuration NOTIFY durationChanged) + +public: + explicit InactivityDurationComboBox(QWidget *parent = nullptr); + + uint duration() const; + void setDuration(uint duration); + +Q_SIGNALS: + void durationChanged(); +}; + +class HideCursorEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit HideCursorEffectConfig(QObject *parent, const KPluginMetaData &data); + +public Q_SLOTS: + void save() override; + +private: + Ui::HideCursorEffectConfig m_ui; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor_config.ui b/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor_config.ui new file mode 100644 index 0000000000..fcb871fb2f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursor_config.ui @@ -0,0 +1,32 @@ + + + HideCursorEffectConfig + + + + 0 + 0 + 251 + 70 + + + + + + + Hide cursor on inactivity: + + + + + + + Hide cursor on typing + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursorconfig.kcfg b/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursorconfig.kcfg new file mode 100644 index 0000000000..bcac2936e1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursorconfig.kcfg @@ -0,0 +1,15 @@ + + + + + + 0 + + + true + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursorconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursorconfig.kcfgc new file mode 100644 index 0000000000..d18632335b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/hidecursor/hidecursorconfig.kcfgc @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2024 Jin Liu +# +# SPDX-License-Identifier: GPL-2.0-or-later + +File=hidecursorconfig.kcfg +ClassName=HideCursorConfig +NameSpace=KWin +Singleton=true diff --git a/local/recipes/kde/kwin/source/src/plugins/hidecursor/main.cpp b/local/recipes/kde/kwin/source/src/plugins/hidecursor/main.cpp new file mode 100644 index 0000000000..9ad94079c5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/hidecursor/main.cpp @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2024 Jin Liu + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "hidecursor.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY(HideCursorEffect, + "metadata.json.stripped") + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/hidecursor/metadata.json b/local/recipes/kde/kwin/source/src/plugins/hidecursor/metadata.json new file mode 100644 index 0000000000..6776425886 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/hidecursor/metadata.json @@ -0,0 +1,93 @@ +{ + "KPlugin": { + "Category": "Accessibility", + "Description": "Hide mouse cursor on inactivity or keyboard input", + "Description[ar]": "إخفاء مؤشر الفأرة عند عدم النشاط أو كتابة بلوحة المفاتيح", + "Description[be]": "Хаваць курсор мышы пры бяздзейнасці або ўводзе з клавіятуры", + "Description[bg]": "Скриване показалеца на мишката при бездействие или при въвеждане от клавиатурата", + "Description[ca@valencia]": "Oculta el cursor del ratolí per inactivitat o per entrada de teclat", + "Description[ca]": "Oculta el cursor del ratolí per inactivitat o per entrada de teclat", + "Description[da]": "Skjul markøren ved inaktivitet eller tastaturinput", + "Description[de]": "Mauszeiger bei Inaktivität oder Tastatureingabe ausblenden", + "Description[en_GB]": "Hide mouse cursor on inactivity or keyboard input", + "Description[eo]": "Kaŝi muskursoron je neaktiveco aŭ klavara enigo", + "Description[es]": "Ocultar el cursor del ratón cuando no haya actividad o al usar el teclado", + "Description[eu]": "Ezkutatu saguaren kurtsorea jarduerarik ez dagoenean edo teklatuaren sarrera dagoenean", + "Description[fi]": "Piilota hiiriosoitin passiivisena tai näppäiltäessä", + "Description[fr]": "Masquer le pointeur de la souris en l'absence d'activités ou d'évènements de clavier", + "Description[gl]": "Agochar o cursor en caso de inactividade ou entrada do teclado.", + "Description[he]": "להסתיר את סמן העכבר בהעדר פעילות או קלט מקלדת", + "Description[hu]": "Egérmutató elrejtése inaktivitás vagy billentyűzet-bevitel esetén", + "Description[ia]": "Cela cursor de mus sur inactivitate o ingresso de claviero", + "Description[id]": "Sembunyikan kursor mouse saat tidak aktif atau input keyboard", + "Description[is]": "Fela músarbendil við aðgerðaleysi eða innslátt", + "Description[it]": "Nascondi il puntatore del mouse in caso di inattività o inserimento da tastiera", + "Description[ja]": "非アクティブまたはキーボード入力時にマウスカーソルを非表示にする", + "Description[ka]": "თაგუნას კურსორის დამალვა კლავიატურიდან შეყვანისას ან უქმეობისას", + "Description[ko]": "활동 중단이나 키보드 입력 시 마우스 숨기기", + "Description[lt]": "Slėpti pelės žymeklį neveiklumo metu ar rašant klaviatūra", + "Description[lv]": "Slēpt peles kursoru dīkstāves laikā un lietojot tastatūru", + "Description[nb]": "Skjul pekeren ved inaktivitet eller skriving", + "Description[nl]": "Muisaanwijzer verbergen bij inactiviteit of invoer van het toetsenbord", + "Description[nn]": "Gøym peikaren ved inaktivitet eller skriving", + "Description[pl]": "Ukryj wskaźnik myszy przy bezruchu lub wpisaniu z klawiatury", + "Description[pt_BR]": "Ocultar o cursor do mouse em caso de inatividade ou entrada do teclado", + "Description[ro]": "Ascunde cursorul șoricelului la inactivitate sau introducerea de la tastatură", + "Description[ru]": "Сокрытие курсора мыши при простое или вводе с клавиатуры", + "Description[sa]": "निष्क्रियतायां वा कीबोर्डनिवेशे वा मूषकस्य कर्सरं गोपयन्तु", + "Description[sk]": "Skryť kurzor myši pri nečinnosti alebo vstupe z klávesnice", + "Description[sl]": "Skrij miškino kazalko pri nedejavnosti ali vnosu s tipkovnice", + "Description[sv]": "Dölj muspekaren vid inaktivitet eller tangentbordsinmatning", + "Description[ta]": "ஒன்றும் செய்யாதபோதோ தட்டச்சிடும்போதோ சுட்டிக்குறியை மறைப்பது", + "Description[tr]": "Hareketsizlikte veya klavye girişinde fare imlecini gizle", + "Description[uk]": "Приховати вказівник миші при бездіяльності або введенні з клавіатури", + "Description[zh_CN]": "闲置或者键盘输入时隐藏鼠标光标", + "Description[zh_TW]": "在沒有動作或是有鍵盤輸入時隱藏滑鼠游標", + "EnabledByDefault": false, + "License": "GPL", + "Name": "Hide Cursor", + "Name[ar]": "إخفاء المؤشر", + "Name[be]": "Хаваць курсор", + "Name[bg]": "Скриване показалеца на мишката", + "Name[ca@valencia]": "Oculta el cursor", + "Name[ca]": "Oculta el cursor", + "Name[cs]": "Skrýt kurzor", + "Name[da]": "Skjul markør", + "Name[de]": "Zeiger ausblenden", + "Name[en_GB]": "Hide Cursor", + "Name[eo]": "Kaŝi Kursoron", + "Name[es]": "Ocultar el cursor", + "Name[eu]": "Ezkutatu kurtsorea", + "Name[fi]": "Piilota osoitin", + "Name[fr]": "Masquer le pointeur", + "Name[gl]": "Agochar o cursor", + "Name[he]": "הסתרת סמן", + "Name[hu]": "Kurzor elrejtése", + "Name[ia]": "Cela Cursor", + "Name[id]": "Sembunyikan Kursor", + "Name[is]": "Fela bendil", + "Name[it]": "Nascondi il puntatore", + "Name[ja]": "カーソルを隠す", + "Name[ka]": "კურსორის დამალვა", + "Name[ko]": "커서 숨기기", + "Name[lt]": "Slėpti žymeklį", + "Name[lv]": "Paslēpt kursoru", + "Name[nb]": "Skjul pekeren", + "Name[nl]": "Cursor verbergen", + "Name[nn]": "Gøym peikaren", + "Name[pl]": "Ukryj wskaźnik myszy", + "Name[pt_BR]": "Esconder cursor", + "Name[ro]": "Ascunde cursorul", + "Name[ru]": "Сокрытие курсора", + "Name[sa]": "कर्सरं गोपयतु", + "Name[sk]": "Zatriasť kurzorom", + "Name[sl]": "Skrij kazalko", + "Name[sv]": "Dölj markör", + "Name[ta]": "சுட்டிக்குறியை மறை", + "Name[tr]": "İmleci Gizle", + "Name[uk]": "Приховувати вказівник", + "Name[zh_CN]": "隐藏光标", + "Name[zh_TW]": "隱藏游標" + }, + "X-KDE-ConfigModule": "kwin_hidecursor_config" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/highlightwindow/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/highlightwindow/CMakeLists.txt new file mode 100644 index 0000000000..11cadfa3cb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/highlightwindow/CMakeLists.txt @@ -0,0 +1,15 @@ +####################################### +# Effect + +# Source files +set(highlightwindow_SOURCES + highlightwindow.cpp + main.cpp +) + +kwin_add_builtin_effect(highlightwindow ${highlightwindow_SOURCES}) +target_link_libraries(highlightwindow PRIVATE + kwin + + Qt::DBus +) diff --git a/local/recipes/kde/kwin/source/src/plugins/highlightwindow/highlightwindow.cpp b/local/recipes/kde/kwin/source/src/plugins/highlightwindow/highlightwindow.cpp new file mode 100644 index 0000000000..63f000c705 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/highlightwindow/highlightwindow.cpp @@ -0,0 +1,206 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2021 David Redondo + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "highlightwindow.h" +#include "effect/effecthandler.h" + +#include + +using namespace std::chrono_literals; + +Q_LOGGING_CATEGORY(KWIN_HIGHLIGHTWINDOW, "kwin_effect_highlightwindow", QtWarningMsg) + +namespace KWin +{ + +HighlightWindowEffect::HighlightWindowEffect() + : m_easingCurve(QEasingCurve::Linear) + , m_fadeDuration(animationTime(150ms)) +{ + connect(effects, &EffectsHandler::windowAdded, this, &HighlightWindowEffect::slotWindowAdded); + connect(effects, &EffectsHandler::windowDeleted, this, &HighlightWindowEffect::slotWindowDeleted); + + QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/kde/KWin/HighlightWindow"), + QStringLiteral("org.kde.KWin.HighlightWindow"), + this, + QDBusConnection::ExportScriptableContents); + QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.KWin.HighlightWindow")); +} + +HighlightWindowEffect::~HighlightWindowEffect() +{ + QDBusConnection::sessionBus().unregisterService(QStringLiteral("org.kde.KWin.HighlightWindow")); +} + +static bool isInitiallyHidden(EffectWindow *w) +{ + // Is the window initially hidden until it is highlighted? + return w->isMinimized() || !w->isOnCurrentDesktop(); +} + +static bool isHighlightWindow(EffectWindow *window) +{ + return window->isNormalWindow() || window->isDialog(); +} + +void HighlightWindowEffect::highlightWindows(const QStringList &windows) +{ + QList effectWindows; + effectWindows.reserve(windows.count()); + for (const auto &window : windows) { + if (auto effectWindow = effects->findWindow(QUuid(window)); effectWindow) { + effectWindows.append(effectWindow); + } else if (auto effectWindow = effects->findWindow(window.toLong()); effectWindow) { + effectWindows.append(effectWindow); + } + } + highlightWindows(effectWindows); +} + +void HighlightWindowEffect::slotWindowAdded(EffectWindow *w) +{ + if (!m_highlightedWindows.isEmpty()) { + if (isHighlightWindow(w)) { + const quint64 animationId = startGhostAnimation(w); // this window is not currently highlighted + complete(animationId); + } + } +} + +void HighlightWindowEffect::slotWindowDeleted(EffectWindow *w) +{ + m_animations.remove(w); + m_highlightedWindows.removeOne(w); +} + +void HighlightWindowEffect::prepareHighlighting() +{ + const QList windows = effects->stackingOrder(); + for (EffectWindow *window : windows) { + if (window->isDeleted() || !isHighlightWindow(window)) { + continue; + } + + if (isHighlighted(window)) { + startHighlightAnimation(window); + } else if (window->isVisible()) { + startGhostAnimation(window); + } else { + startRevertAnimation(window); + } + } +} + +void HighlightWindowEffect::finishHighlighting() +{ + const QList windows = effects->stackingOrder(); + for (EffectWindow *window : windows) { + if (window->isDeleted() || !isHighlightWindow(window)) { + continue; + } + + startRevertAnimation(window); + } + + // Sanity check, ideally, this should never happen. + if (!m_animations.isEmpty()) { + for (quint64 &animationId : m_animations) { + cancel(animationId); + } + m_animations.clear(); + } + + m_highlightedWindows.clear(); +} + +void HighlightWindowEffect::highlightWindows(const QList &windows) +{ + if (windows.isEmpty()) { + finishHighlighting(); + return; + } + + m_highlightedWindows = windows; + prepareHighlighting(); +} + +quint64 HighlightWindowEffect::startGhostAnimation(EffectWindow *window) +{ + quint64 &animationId = m_animations[window]; + if (animationId) { + retarget(animationId, FPx2(m_ghostOpacity, m_ghostOpacity), m_fadeDuration); + } else { + const qreal startOpacity = isInitiallyHidden(window) ? 0 : 1; + animationId = set(window, Opacity, 0, m_fadeDuration, FPx2(m_ghostOpacity, m_ghostOpacity), + m_easingCurve, 0, FPx2(startOpacity, startOpacity), false, false); + } + return animationId; +} + +quint64 HighlightWindowEffect::startHighlightAnimation(EffectWindow *window) +{ + quint64 &animationId = m_animations[window]; + if (animationId) { + retarget(animationId, FPx2(1.0, 1.0), m_fadeDuration); + } else { + const qreal startOpacity = isInitiallyHidden(window) ? 0 : 1; + animationId = set(window, Opacity, 0, m_fadeDuration, FPx2(1.0, 1.0), + m_easingCurve, 0, FPx2(startOpacity, startOpacity), false, false); + } + return animationId; +} + +void HighlightWindowEffect::startRevertAnimation(EffectWindow *window) +{ + const quint64 animationId = m_animations.take(window); + if (animationId) { + const qreal startOpacity = isHighlighted(window) ? 1 : m_ghostOpacity; + const qreal endOpacity = isInitiallyHidden(window) ? 0 : 1; + animate(window, Opacity, 0, m_fadeDuration, FPx2(endOpacity, endOpacity), + m_easingCurve, 0, FPx2(startOpacity, startOpacity), false, false); + cancel(animationId); + } +} + +bool HighlightWindowEffect::isHighlighted(EffectWindow *window) const +{ + return m_highlightedWindows.contains(window); +} + +bool HighlightWindowEffect::provides(Feature feature) +{ + switch (feature) { + case HighlightWindows: + return true; + default: + return false; + } +} + +bool HighlightWindowEffect::perform(Feature feature, const QVariantList &arguments) +{ + if (feature != HighlightWindows) { + return false; + } + if (arguments.size() != 1) { + return false; + } + highlightWindows(arguments.first().value>()); + return true; +} + +void HighlightWindowEffect::reconfigure(ReconfigureFlags flags) +{ + m_fadeDuration = animationTime(150ms); +} + +} // namespace + +#include "moc_highlightwindow.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/highlightwindow/highlightwindow.h b/local/recipes/kde/kwin/source/src/plugins/highlightwindow/highlightwindow.h new file mode 100644 index 0000000000..3208827c76 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/highlightwindow/highlightwindow.h @@ -0,0 +1,57 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/animationeffect.h" + +namespace KWin +{ + +class HighlightWindowEffect : public AnimationEffect +{ + Q_OBJECT + +public: + HighlightWindowEffect(); + ~HighlightWindowEffect() override; + + int requestedEffectChainPosition() const override + { + return 70; + } + + bool provides(Feature feature) override; + bool perform(Feature feature, const QVariantList &arguments) override; + void reconfigure(ReconfigureFlags flags) override; + Q_SCRIPTABLE void highlightWindows(const QStringList &windows); + +public Q_SLOTS: + void slotWindowAdded(KWin::EffectWindow *w); + void slotWindowDeleted(KWin::EffectWindow *w); + +private: + quint64 startGhostAnimation(EffectWindow *window); + quint64 startHighlightAnimation(EffectWindow *window); + void startRevertAnimation(EffectWindow *window); + + bool isHighlighted(EffectWindow *window) const; + + void prepareHighlighting(); + void finishHighlighting(); + void highlightWindows(const QList &windows); + + QList m_highlightedWindows; + QHash m_animations; + QEasingCurve m_easingCurve; + int m_fadeDuration; + float m_ghostOpacity = 0; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/highlightwindow/main.cpp b/local/recipes/kde/kwin/source/src/plugins/highlightwindow/main.cpp new file mode 100644 index 0000000000..39e92628ba --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/highlightwindow/main.cpp @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "highlightwindow.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY(HighlightWindowEffect, + "metadata.json.stripped") + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/highlightwindow/metadata.json b/local/recipes/kde/kwin/source/src/plugins/highlightwindow/metadata.json new file mode 100644 index 0000000000..b1dc0b3502 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/highlightwindow/metadata.json @@ -0,0 +1,104 @@ +{ + "KPlugin": { + "Category": "Appearance", + "Description": "Highlight the appropriate window when hovering over taskbar entries", + "Description[ar]": "إبراز النافذة المناسبة عند المرور فوق مُدخلات شريط المهام", + "Description[az]": "Tapşıqıq paneli üzərində hərəkət edərkən müvadiq pəncərənin işıqlanması", + "Description[be]": "Падсвятленне адпаведнага акна пры навядзенні курсора на яго запіс на панэлі задач", + "Description[bg]": "Осветяване на съответния прозорец при задържане на курсора на мишката над записите в лентата на задачите", + "Description[ca@valencia]": "Ressalta la finestra apropiada en passar el cursor sobre les entrades de la barra de tasques", + "Description[ca]": "Ressalta la finestra apropiada en passar el cursor sobre les entrades de la barra de tasques", + "Description[cs]": "Zvýraznit příslušné okno při přejezdu nad jeho položku v pruhu úloh", + "Description[da]": "Fremhæv det passende vindue, når musen er over dens opgavelinjeindgang", + "Description[de]": "Hebt das dazugehörige Fenster hervor, wenn sich der Mauszeiger über dem Fensterleisten-Eintrag befindet", + "Description[en_GB]": "Highlight the appropriate window when hovering over taskbar entries", + "Description[eo]": "Emfazu la taŭgan fenestron kiam ŝvebas super taskobretaj eniroj", + "Description[es]": "Resaltar la ventana correspondiente al pasar el ratón sobre las entradas de la barra de tareas", + "Description[et]": "Akende esiletõstmine, kui kursor on nende tegumiriba kirje kohal", + "Description[eu]": "Nabarmendu dagokion leihoa sagua ataza-barraren sarreren gainetik igarotzean", + "Description[fi]": "Korosta vastaavaa ikkunaa leijuttaessa tehtäväpalkkikohteiden yllä", + "Description[fr]": "Mettre en valeur la fenêtre appropriée lors du survol des entrées de la barre de tâches", + "Description[gl]": "Realza a xanela axeitada ao pasar o rato sobre a barra de tarefas.", + "Description[he]": "הדגשת החלון המתאים בעת ריחוף מעל רשומות בסרגל המשימות", + "Description[hu]": "Ha az egérmutató egy ablakbejegyzés fölé kerül a feladatsávon, a megfelelő ablak kiemelést kap", + "Description[ia]": "Evidentia le appropriate fenestra quando il es suspendite super entratas de barra de carga", + "Description[id]": "Soroti jendela yang sesuai ketika kursor berada di entri bilah-tugas", + "Description[is]": "Auðkenna viðeigandi glugga þegar sveimað er yfir tákni á verkstiku", + "Description[it]": "Evidenzia la finestra appropriata quando passi sugli elementi della barra delle applicazioni", + "Description[ja]": "タスクバーのエントリがホバーされたときウィンドウを強調表示します", + "Description[ka]": "ამოცანების პანელზე თაგუნას კურსორის გადატარებისას შესაბამისი ფანჯრის გამოკვეთა", + "Description[ko]": "작업 표시줄의 창 항목을 지나다닐 때 해당하는 창을 강조합니다", + "Description[lt]": "Paryškinti atitinkamą langą, kai užvedama pelė virš užduočių juostos įrašų", + "Description[lv]": "Iekrāso attiecīgo logu, peli liekot pāri uzdevumu joslas vienumiem", + "Description[nb]": "Fremhev vinduene når pekeren holdes over ikon på oppgavelinja", + "Description[nl]": "Laat het betreffende venster oplichten als de muisaanwijzer over taakbalkitems beweegt", + "Description[nn]": "Framhev vindauga når peikaren vert halden over ikon på oppgåvelinja", + "Description[pl]": "Podświetla odpowiednie okno po najechaniu na wpis paska zadań", + "Description[pt]": "Realçar a janela apropriada ao passar o cursor sobre os itens das tarefas", + "Description[pt_BR]": "Realça a janela apropriada ao passar o ponteiro do mouse sobre os itens da barra de tarefas", + "Description[ro]": "Evidențiază fereastra potrivită la planarea peste înregistrările barei de sarcini", + "Description[ru]": "Подсветка окна при наведении и соответствующем элементе в панели задач", + "Description[sa]": "कार्यपट्टिकाप्रविष्टीनां उपरि भ्रमन् समुचितं विण्डो प्रकाशयन्तु", + "Description[sk]": "Zvýrazní príslušné okno pri prechode kurzorom nad položkami v paneli úloh", + "Description[sl]": "Poudari ustrezno okno, ko je kazalec miške nad vnosi opravilne vrstice", + "Description[sv]": "Markera motsvarande fönster när pekaren hålls över poster i aktivitetsfältet", + "Description[ta]": "பணிப்பட்டை பதிவுகளின்மீது சுட்டியை வைக்கும்போது உரிய சாளரத்தை முன்னிலைப்படுத்தும்", + "Description[tr]": "Görev çubuğu girdilere arasında gezinirken uygun pencereleri vurgula", + "Description[uk]": "Підсвічування відповідного вікна у відповідь на наведення вказівника миші на елементи панелі задач", + "Description[vi]": "Tô sáng cửa sổ thích hợp khi đảo chuột trên các mục thanh tác vụ", + "Description[zh_CN]": "鼠标悬停在任务栏项上时突出显示对应的窗口", + "Description[zh_TW]": "滑鼠移至工作列項目上時突顯其對應的視窗", + "EnabledByDefault": true, + "License": "GPL", + "Name": "Highlight Window", + "Name[ar]": "إبراز النوافذ", + "Name[az]": "Pəncərəni vurğulamaq", + "Name[be]": "Падсвятленне акон", + "Name[bg]": "Осветяване на прозореца", + "Name[ca@valencia]": "Ressaltat de finestra", + "Name[ca]": "Ressaltat de finestra", + "Name[cs]": "Zvýraznit okno", + "Name[da]": "Fremhæv vindue", + "Name[de]": "Fenster hervorheben", + "Name[en_GB]": "Highlight Window", + "Name[eo]": "Marku Fenestron", + "Name[es]": "Resaltar ventana", + "Name[et]": "Akna esiletõstmine", + "Name[eu]": "Nabarmendu leihoa", + "Name[fi]": "Ikkunan korostus", + "Name[fr]": "Mettre en valeur une fenêtre", + "Name[gl]": "Realzar a xanela", + "Name[he]": "הדגשת חלון", + "Name[hu]": "Ablakkiemelés", + "Name[ia]": "Evidentia fenestra", + "Name[id]": "Sorot Jendela", + "Name[is]": "Auðkenna glugga", + "Name[it]": "Evidenzia le finestre", + "Name[ja]": "ウィンドウを強調表示", + "Name[ka]": "ფანჯრის გამოკვეთა", + "Name[ko]": "창 강조하기", + "Name[lt]": "Lango paryškinimas", + "Name[lv]": "Iekrāsot logu", + "Name[nb]": "Fremhev vindu", + "Name[nl]": "Venster op laten lichten", + "Name[nn]": "Framhev vindauge", + "Name[pl]": "Podświetlanie okien", + "Name[pt]": "Realce da Janela", + "Name[pt_BR]": "Janela realçada", + "Name[ro]": "Evidențiere fereastră", + "Name[ru]": "Подсвечивать окно", + "Name[sa]": "विण्डो हाइलाइट् कुर्वन्तु", + "Name[sk]": "Zvýrazniť okno", + "Name[sl]": "Poudari okno", + "Name[sv]": "Markera fönster", + "Name[ta]": "சாளரத்தை முன்னிலைப்படுத்து", + "Name[tr]": "Pencereyi Vurgula", + "Name[uk]": "Підсвічування вікна", + "Name[vi]": "Tô sáng cửa sổ", + "Name[zh_CN]": "突出显示窗口", + "Name[zh_TW]": "突顯視窗" + }, + "org.kde.kwin.effect": { + "internal": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/idletime/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/idletime/CMakeLists.txt new file mode 100644 index 0000000000..30e965a431 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/idletime/CMakeLists.txt @@ -0,0 +1,10 @@ +set(idletime_plugin_SRCS + poller.cpp +) + +add_library(KF6IdleTimeKWinPlugin OBJECT ${idletime_plugin_SRCS}) +target_compile_definitions(KF6IdleTimeKWinPlugin PRIVATE QT_STATICPLUGIN) +target_link_libraries(KF6IdleTimeKWinPlugin + KF6::IdleTime + kwin +) diff --git a/local/recipes/kde/kwin/source/src/plugins/idletime/kwin.json b/local/recipes/kde/kwin/source/src/plugins/idletime/kwin.json new file mode 100644 index 0000000000..aaf6fd00dd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/idletime/kwin.json @@ -0,0 +1,3 @@ +{ + "platforms": ["wayland-org.kde.kwin.qpa"] +} diff --git a/local/recipes/kde/kwin/source/src/plugins/idletime/poller.cpp b/local/recipes/kde/kwin/source/src/plugins/idletime/poller.cpp new file mode 100644 index 0000000000..4785820192 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/idletime/poller.cpp @@ -0,0 +1,91 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "poller.h" +#include "idledetector.h" +#include "input.h" + +namespace KWin +{ + +KWinIdleTimePoller::KWinIdleTimePoller(QObject *parent) + : KAbstractIdleTimePoller(parent) +{ +} + +bool KWinIdleTimePoller::isAvailable() +{ + return true; +} + +bool KWinIdleTimePoller::setUpPoller() +{ + return true; +} + +void KWinIdleTimePoller::unloadPoller() +{ +} + +void KWinIdleTimePoller::addTimeout(int nextTimeout) +{ + if (m_timeouts.contains(nextTimeout)) { + return; + } + + auto detector = new IdleDetector(std::chrono::milliseconds(nextTimeout), IdleDetector::OperatingMode::FollowsInhibitors, this); + m_timeouts.insert(nextTimeout, detector); + connect(detector, &IdleDetector::idle, this, [this, nextTimeout] { + Q_EMIT timeoutReached(nextTimeout); + }); + connect(detector, &IdleDetector::resumed, this, &KWinIdleTimePoller::resumingFromIdle); +} + +void KWinIdleTimePoller::removeTimeout(int nextTimeout) +{ + delete m_timeouts.take(nextTimeout); +} + +QList< int > KWinIdleTimePoller::timeouts() const +{ + return m_timeouts.keys(); +} + +void KWinIdleTimePoller::catchIdleEvent() +{ + if (m_catchResumeTimeout) { + // already setup + return; + } + m_catchResumeTimeout = new IdleDetector(std::chrono::milliseconds::zero(), IdleDetector::OperatingMode::FollowsInhibitors, this); + connect(m_catchResumeTimeout, &IdleDetector::resumed, this, [this]() { + m_catchResumeTimeout->deleteLater(); + m_catchResumeTimeout = nullptr; + Q_EMIT resumingFromIdle(); + }); +} + +void KWinIdleTimePoller::stopCatchingIdleEvents() +{ + delete m_catchResumeTimeout; + m_catchResumeTimeout = nullptr; +} + +int KWinIdleTimePoller::forcePollRequest() +{ + return 0; +} + +void KWinIdleTimePoller::simulateUserActivity() +{ + input()->simulateUserActivity(); +} + +} // namespace KWin + +#include "moc_poller.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/idletime/poller.h b/local/recipes/kde/kwin/source/src/plugins/idletime/poller.h new file mode 100644 index 0000000000..2846f17875 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/idletime/poller.h @@ -0,0 +1,47 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include +#include + +namespace KWin +{ + +class IdleDetector; + +class KWinIdleTimePoller : public KAbstractIdleTimePoller +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID KAbstractIdleTimePoller_iid FILE "kwin.json") + Q_INTERFACES(KAbstractIdleTimePoller) + +public: + KWinIdleTimePoller(QObject *parent = nullptr); + + bool isAvailable() override; + bool setUpPoller() override; + void unloadPoller() override; + +public Q_SLOTS: + void addTimeout(int nextTimeout) override; + void removeTimeout(int nextTimeout) override; + QList timeouts() const override; + int forcePollRequest() override; + void catchIdleEvent() override; + void stopCatchingIdleEvents() override; + void simulateUserActivity() override; + +private: + IdleDetector *m_catchResumeTimeout = nullptr; + QHash m_timeouts; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/plugins/invert/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/invert/CMakeLists.txt new file mode 100644 index 0000000000..55c6dba50a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/invert/CMakeLists.txt @@ -0,0 +1,16 @@ +####################################### +# Effect + +set(invert_SOURCES + invert.cpp + invert.qrc + main.cpp +) + +kwin_add_builtin_effect(invert ${invert_SOURCES}) +target_link_libraries(invert PRIVATE + kwin + + KF6::GlobalAccel + KF6::I18n +) diff --git a/local/recipes/kde/kwin/source/src/plugins/invert/invert.cpp b/local/recipes/kde/kwin/source/src/plugins/invert/invert.cpp new file mode 100644 index 0000000000..fdba10c68b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/invert/invert.cpp @@ -0,0 +1,167 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + SPDX-FileCopyrightText: 2008 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "invert.h" + +#include "effect/effecthandler.h" +#include "opengl/glplatform.h" +#include "opengl/glutils.h" +#include +#include +#include +#include +#include + +#include + +Q_LOGGING_CATEGORY(KWIN_INVERT, "kwin_effect_invert", QtWarningMsg) + +static void ensureResources() +{ + // Must initialize resources manually because the effect is a static lib. + Q_INIT_RESOURCE(invert); +} + +namespace KWin +{ + +InvertEffect::InvertEffect() + : m_inited(false) + , m_valid(true) + , m_shader(nullptr) + , m_allWindows(false) +{ + QAction *a = new QAction(this); + a->setAutoRepeat(false); + a->setObjectName(QStringLiteral("Invert")); + a->setText(i18n("Toggle Invert Effect")); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << (Qt::CTRL | Qt::META | Qt::Key_I)); + KGlobalAccel::self()->setShortcut(a, QList() << (Qt::CTRL | Qt::META | Qt::Key_I)); + connect(a, &QAction::triggered, this, &InvertEffect::toggleScreenInversion); + + QAction *b = new QAction(this); + b->setAutoRepeat(false); + b->setObjectName(QStringLiteral("InvertWindow")); + b->setText(i18n("Toggle Invert Effect on Window")); + KGlobalAccel::self()->setDefaultShortcut(b, QList() << (Qt::CTRL | Qt::META | Qt::Key_U)); + KGlobalAccel::self()->setShortcut(b, QList() << (Qt::CTRL | Qt::META | Qt::Key_U)); + connect(b, &QAction::triggered, this, &InvertEffect::toggleWindow); + + QAction *c = new QAction(this); + c->setObjectName(QStringLiteral("Invert Screen Colors")); + c->setText(i18n("Invert Screen Colors")); + KGlobalAccel::self()->setDefaultShortcut(c, QList()); + KGlobalAccel::self()->setShortcut(c, QList()); + connect(c, &QAction::triggered, this, &InvertEffect::toggleScreenInversion); + + connect(effects, &EffectsHandler::windowAdded, this, &InvertEffect::slotWindowAdded); + connect(effects, &EffectsHandler::windowClosed, this, &InvertEffect::slotWindowClosed); +} + +InvertEffect::~InvertEffect() = default; + +bool InvertEffect::supported() +{ + return effects->compositingType() == OpenGLCompositing; +} + +bool InvertEffect::isInvertable(EffectWindow *window) const +{ + return m_allWindows != m_windows.contains(window); +} + +void InvertEffect::invert(EffectWindow *window) +{ + if (m_valid && !m_inited) { + m_valid = loadData(); + } + + redirect(window); + setShader(window, m_shader.get()); +} + +void InvertEffect::uninvert(EffectWindow *window) +{ + unredirect(window); +} + +bool InvertEffect::loadData() +{ + ensureResources(); + m_inited = true; + + m_shader = ShaderManager::instance()->generateShaderFromFile(ShaderTrait::MapTexture, QString(), QStringLiteral(":/effects/invert/shaders/invert.frag")); + if (!m_shader->isValid()) { + qCCritical(KWIN_INVERT) << "The shader failed to load!"; + return false; + } + + return true; +} + +void InvertEffect::slotWindowAdded(KWin::EffectWindow *w) +{ + if (isInvertable(w)) { + invert(w); + } +} + +void InvertEffect::slotWindowClosed(EffectWindow *w) +{ + m_windows.removeOne(w); +} + +void InvertEffect::toggleScreenInversion() +{ + m_allWindows = !m_allWindows; + + const auto windows = effects->stackingOrder(); + for (EffectWindow *window : windows) { + if (isInvertable(window)) { + invert(window); + } else { + uninvert(window); + } + } + + effects->addRepaintFull(); +} + +void InvertEffect::toggleWindow() +{ + if (!effects->activeWindow()) { + return; + } + if (!m_windows.contains(effects->activeWindow())) { + m_windows.append(effects->activeWindow()); + } else { + m_windows.removeOne(effects->activeWindow()); + } + if (isInvertable(effects->activeWindow())) { + invert(effects->activeWindow()); + } else { + uninvert(effects->activeWindow()); + } + effects->activeWindow()->addRepaintFull(); +} + +bool InvertEffect::isActive() const +{ + return m_valid && (m_allWindows || !m_windows.isEmpty()); +} + +bool InvertEffect::provides(Feature f) +{ + return f == ScreenInversion; +} + +} // namespace + +#include "moc_invert.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/invert/invert.h b/local/recipes/kde/kwin/source/src/plugins/invert/invert.h new file mode 100644 index 0000000000..72fa8cab2a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/invert/invert.h @@ -0,0 +1,63 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + SPDX-FileCopyrightText: 2008 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/offscreeneffect.h" + +namespace KWin +{ + +class GLShader; + +/** + * Inverts desktop's colors + */ +class InvertEffect : public OffscreenEffect +{ + Q_OBJECT +public: + InvertEffect(); + ~InvertEffect() override; + + bool isActive() const override; + bool provides(Feature) override; + int requestedEffectChainPosition() const override; + + static bool supported(); + +public Q_SLOTS: + void toggleScreenInversion(); + void toggleWindow(); + + void slotWindowAdded(KWin::EffectWindow *w); + void slotWindowClosed(KWin::EffectWindow *w); + +protected: + bool loadData(); + +private: + bool isInvertable(EffectWindow *window) const; + void invert(EffectWindow *window); + void uninvert(EffectWindow *window); + + bool m_inited; + bool m_valid; + std::unique_ptr m_shader; + bool m_allWindows; + QList m_windows; +}; + +inline int InvertEffect::requestedEffectChainPosition() const +{ + return 99; +} + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/invert/invert.qrc b/local/recipes/kde/kwin/source/src/plugins/invert/invert.qrc new file mode 100644 index 0000000000..e55aef4845 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/invert/invert.qrc @@ -0,0 +1,6 @@ + + + shaders/invert.frag + shaders/invert_core.frag + + diff --git a/local/recipes/kde/kwin/source/src/plugins/invert/main.cpp b/local/recipes/kde/kwin/source/src/plugins/invert/main.cpp new file mode 100644 index 0000000000..0c119dbc42 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/invert/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "invert.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(InvertEffect, + "metadata.json.stripped", + return InvertEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/invert/metadata.json b/local/recipes/kde/kwin/source/src/plugins/invert/metadata.json new file mode 100644 index 0000000000..964457dbe1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/invert/metadata.json @@ -0,0 +1,95 @@ +{ + "KPlugin": { + "Category": "Accessibility", + "Description": "Inverts the color of the desktop and windows; activated with a keyboard shortcut", + "Description[ar]": "يعكس ألوان سطح المكتب والنوافذ، تفعّل باختصار لوحة المفاتيح", + "Description[bg]": "Обръщане на цветовете като негатив на работния плот и прозорците, активирано с клавишна комбинация", + "Description[ca@valencia]": "Invertix el color de l'escriptori i les finestres; activat amb una drecera de teclat", + "Description[ca]": "Inverteix el color de l'escriptori i les finestres; activat amb una drecera de teclat", + "Description[da]": "Invertér farven af skrivebordet og vinduerne; aktiveret med en tastaturgenvej", + "Description[de]": "Invertiert die Farben der Arbeitsfläche und Fenster; mit einem Tastatur-Kurzbefehl ausgelöst", + "Description[en_GB]": "Inverts the colour of the desktop and windows; activated with a keyboard shortcut", + "Description[eo]": "Inversigas la koloron de la labortablo kaj fenestroj; aktivigita per klavara ŝparvojo", + "Description[es]": "Invierte el color del escritorio y de las ventanas; se activa con un atajo de teclado", + "Description[eu]": "Mahaigaineko eta leihoetako koloreak alderantzikatzen ditu; teklatuko lasterbide batekin aktibatua", + "Description[fi]": "Kääntää työpöydän ja ikkunoiden värit; käynnistyy pikanäppäimellä", + "Description[fr]": "Inverse la couleur du bureau et des fenêtres. Activé grâce à un raccourci clavier.", + "Description[gl]": "Inverte a cor do escritorio e das xanelas; actívase cun atallo de teclado.", + "Description[he]": "הופך את צבע שולחן העבודה והחלונות, מופעל דרך צירוף מקשי מקלדת", + "Description[hu]": "Invertálja az asztal és az ablakok színét; gyorsbillentyűvel aktiválható", + "Description[ia]": "Il inverte le color del scriptorio e del fenestras; activate con un via breve de claviero", + "Description[is]": "Snýr við litum á skjáborðinu og gluggunum; virkjað með flýtilykli", + "Description[it]": "Inverte i colori del desktop e delle finestre; attivato con una scorciatoia da tastiera", + "Description[ka]": "სამუშაო მაგიდისა და ფანჯრების ფერის ინვერსია. აქტიურდება კლავიატურის მალსახმობით", + "Description[ko]": "바탕 화면과 창의 색 반전, 키보드 단축키로 활성화", + "Description[lt]": "Invertuoja darbalaukio ir langų spalvas; aktyvinama naudojant sparčiuosius klavišus", + "Description[lv]": "Invertē darbvirsmas un logu krāsas; ieslēdz ar tastatūras saīsni", + "Description[nb]": "Snur fargene på skrivebordet og vinduer – slått av/på med hurtigtast", + "Description[nl]": "Draait de kleuren van het bureaublad en de vensters om; geactiveerd met een sneltoets", + "Description[nn]": "Snu fargane på skrivebordet og vindauge – slått på/av med snøggtast", + "Description[pl]": "Odwraca kolory pulpitu i okien; uruchamiane skrótem klawiszowym", + "Description[pt_BR]": "Inverte as cores da área de trabalho e das janelas; ativado com um atalho de teclado", + "Description[ro]": "Inversează culorile biroului și ferestrelor; activat cu o scurtătură de taste", + "Description[ru]": "Инверсия цвета рабочего стола и окон; активируется нажатием комбинации клавиш", + "Description[sa]": "डेस्कटॉपस्य विण्डोजस्य च वर्णं विपर्ययति; कीबोर्ड-शॉर्टकट्-सहितं सक्रियम्", + "Description[sk]": "Invertuje farbu plochy a okien", + "Description[sl]": "Preobrne barvo namizja in oken; aktivirano z bližnjico tipkovnice", + "Description[sv]": "Invertera skrivbordets och fönstrens färg, aktiveras med en snabbtangent", + "Description[tr]": "Masaüstünün ve pencerelerin rengini tersine çevir; klavye kısayoluyla etkinleştirilir", + "Description[uk]": "Обернення кольорів стільниці і вікон; активується клавіатурним скороченням", + "Description[zh_CN]": "反相显示桌面和窗口的颜色;使用键盘快捷键激活", + "Description[zh_TW]": "反轉桌面和視窗的顏色——需要用鍵盤快捷鍵觸發", + "EnabledByDefault": false, + "License": "GPL", + "Name": "Invert", + "Name[ar]": "العكس", + "Name[az]": "Tərsinə çevirmək", + "Name[be]": "Інвертаваць", + "Name[bg]": "Негатив", + "Name[ca@valencia]": "Invertix", + "Name[ca]": "Inverteix", + "Name[cs]": "Invertovat", + "Name[da]": "Invertér", + "Name[de]": "Invertieren", + "Name[en_GB]": "Invert", + "Name[eo]": "Inverti", + "Name[es]": "Invertir", + "Name[et]": "Negatiiv", + "Name[eu]": "Alderantzikatu", + "Name[fi]": "Käänteiset värit", + "Name[fr]": "Inverser", + "Name[gl]": "Inverter", + "Name[he]": "היפוך", + "Name[hu]": "Invertálás", + "Name[ia]": "Inverte", + "Name[id]": "Kebalikan", + "Name[is]": "Umsnúa", + "Name[it]": "Inverti", + "Name[ja]": "反転", + "Name[ka]": "ინვერსია", + "Name[ko]": "반전", + "Name[lt]": "Invertavimas", + "Name[lv]": "Invertēt", + "Name[nb]": "Snu fargene", + "Name[nl]": "Inverteren", + "Name[nn]": "Snu fargane", + "Name[pl]": "Odwróć", + "Name[pt]": "Inverter", + "Name[pt_BR]": "Inverter", + "Name[ro]": "Inversare", + "Name[ru]": "Инверсия", + "Name[sa]": "विपर्ययम्", + "Name[sk]": "Invertovať", + "Name[sl]": "Preobrni", + "Name[sv]": "Invertera", + "Name[ta]": "புரட்டு", + "Name[tr]": "Ters Çevir", + "Name[uk]": "Інверсія", + "Name[vi]": "Đảo", + "Name[zh_CN]": "颜色反相", + "Name[zh_TW]": "反轉" + }, + "org.kde.kwin.effect": { + "internal": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/invert/shaders/invert.frag b/local/recipes/kde/kwin/source/src/plugins/invert/shaders/invert.frag new file mode 100644 index 0000000000..997e1b89e9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/invert/shaders/invert.frag @@ -0,0 +1,24 @@ +#include "colormanagement.glsl" +#include "saturation.glsl" + +uniform sampler2D sampler; +uniform vec4 modulation; + +varying vec2 texcoord0; + +void main() +{ + vec4 tex = texture2D(sampler, texcoord0); + tex = sourceEncodingToNitsInDestinationColorspace(tex); + tex = adjustSaturation(tex); + + // to preserve perceptual contrast, apply the inversion in gamma 2.2 space + tex = nitsToEncoding(tex, gamma22_EOTF, 0.0, destinationReferenceLuminance); + tex.rgb /= max(0.001, tex.a); + tex.rgb = vec3(1.0) - tex.rgb; + tex *= modulation; + tex.rgb *= tex.a; + tex = encodingToNits(tex, gamma22_EOTF, 0.0, destinationReferenceLuminance); + + gl_FragColor = nitsToDestinationEncoding(tex); +} diff --git a/local/recipes/kde/kwin/source/src/plugins/invert/shaders/invert_core.frag b/local/recipes/kde/kwin/source/src/plugins/invert/shaders/invert_core.frag new file mode 100644 index 0000000000..e5e862ed32 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/invert/shaders/invert_core.frag @@ -0,0 +1,28 @@ +#version 140 + +#include "colormanagement.glsl" +#include "saturation.glsl" + +uniform sampler2D sampler; +uniform vec4 modulation; + +in vec2 texcoord0; + +out vec4 fragColor; + +void main() +{ + vec4 tex = texture(sampler, texcoord0); + tex = sourceEncodingToNitsInDestinationColorspace(tex); + tex = adjustSaturation(tex); + + // to preserve perceptual contrast, apply the inversion in gamma 2.2 space + tex = nitsToEncoding(tex, gamma22_EOTF, 0.0, destinationReferenceLuminance); + tex.rgb /= max(0.001, tex.a); + tex.rgb = vec3(1.0) - tex.rgb; + tex *= modulation; + tex.rgb *= tex.a; + tex = encodingToNits(tex, gamma22_EOTF, 0.0, destinationReferenceLuminance); + + fragColor = nitsToDestinationEncoding(tex); +} diff --git a/local/recipes/kde/kwin/source/src/plugins/keynotification/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/keynotification/CMakeLists.txt new file mode 100644 index 0000000000..3009936db7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/keynotification/CMakeLists.txt @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2024 Nicolas Fella +# SPDX-License-Identifier: BSD-3-Clause + +kcoreaddons_add_plugin(KeyNotificationPlugin INSTALL_NAMESPACE "kwin/plugins") + +target_compile_definitions(KeyNotificationPlugin PRIVATE + -DTRANSLATION_DOMAIN=\"kwin\" +) + +target_sources(KeyNotificationPlugin PRIVATE + main.cpp + keynotification.cpp +) +target_link_libraries(KeyNotificationPlugin PRIVATE kwin KF6::Notifications KF6::I18n XKB::XKB) + diff --git a/local/recipes/kde/kwin/source/src/plugins/keynotification/keynotification.cpp b/local/recipes/kde/kwin/source/src/plugins/keynotification/keynotification.cpp new file mode 100644 index 0000000000..e61f5e4c44 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/keynotification/keynotification.cpp @@ -0,0 +1,127 @@ +/* + SPDX-FileCopyrightText: 2024 Nicolas Fella + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "keynotification.h" +#include "effect/effecthandler.h" +#include "keyboard_input.h" +#include "xkb.h" + +#include +#include + +namespace KWin +{ +KeyNotificationPlugin::KeyNotificationPlugin() + : m_configWatcher(KConfigWatcher::create(KSharedConfig::openConfig("kaccessrc"))) +{ + const QLatin1String groupName("Keyboard"); + connect(m_configWatcher.get(), &KConfigWatcher::configChanged, this, [this, groupName](const KConfigGroup &group) { + if (group.name() == groupName) { + loadConfig(group); + } + }); + loadConfig(m_configWatcher->config()->group(groupName)); + + connect(input()->keyboard(), &KeyboardInputRedirection::ledsChanged, this, + &KeyNotificationPlugin::ledsChanged); + + connect(input()->keyboard()->xkb(), &Xkb::modifierStateChanged, this, &KeyNotificationPlugin::modifiersChanged); +} + +void KeyNotificationPlugin::ledsChanged(LEDs leds) +{ + if (m_useBellWhenLocksChange) { + if (auto effect = effects->provides(Effect::SystemBell)) { + effect->perform(Effect::SystemBell, {}); + } + } + + if (m_enabled) { + if (!m_currentLEDs.testFlag(LED::CapsLock) && leds.testFlag(LED::CapsLock)) { + sendNotification("lockkey-locked", i18n("The Caps Lock key has been activated")); + } + + if (m_currentLEDs.testFlag(LED::CapsLock) && !leds.testFlag(LED::CapsLock)) { + sendNotification("lockkey-unlocked", i18n("The Caps Lock key is now inactive")); + } + + if (!m_currentLEDs.testFlag(LED::NumLock) && leds.testFlag(LED::NumLock)) { + sendNotification("lockkey-locked", i18n("The Num Lock key has been activated")); + } + + if (m_currentLEDs.testFlag(LED::NumLock) && !leds.testFlag(LED::NumLock)) { + sendNotification("lockkey-unlocked", i18n("The Num Lock key is now inactive")); + } + + if (!m_currentLEDs.testFlag(LED::ScrollLock) && leds.testFlag(LED::ScrollLock)) { + sendNotification("lockkey-locked", i18n("The Scroll Lock key has been activated")); + } + + if (m_currentLEDs.testFlag(LED::ScrollLock) && !leds.testFlag(LED::ScrollLock)) { + sendNotification("lockkey-unlocked", i18n("The Scroll Lock key is now inactive")); + } + } + + m_currentLEDs = leds; +} + +void KeyNotificationPlugin::modifiersChanged() +{ + Qt::KeyboardModifiers mods = input()->keyboard()->xkb()->modifiers(); + + if (m_enabled) { + if (!m_currentModifiers.testFlag(Qt::ShiftModifier) && mods.testFlag(Qt::ShiftModifier)) { + sendNotification("modifierkey-latched", i18n("The Shift key is now active.")); + } + + if (m_currentModifiers.testFlag(Qt::ShiftModifier) && !mods.testFlag(Qt::ShiftModifier)) { + sendNotification("modifierkey-unlatched", i18n("The Shift key is now inactive.")); + } + + if (!m_currentModifiers.testFlag(Qt::ControlModifier) && mods.testFlag(Qt::ControlModifier)) { + sendNotification("modifierkey-latched", i18n("The Control key is now active.")); + } + + if (m_currentModifiers.testFlag(Qt::ControlModifier) && !mods.testFlag(Qt::ControlModifier)) { + sendNotification("modifierkey-unlatched", i18n("The Control key is now inactive.")); + } + + if (!m_currentModifiers.testFlag(Qt::AltModifier) && mods.testFlag(Qt::AltModifier)) { + sendNotification("modifierkey-latched", i18n("The Alt key is now active.")); + } + + if (m_currentModifiers.testFlag(Qt::AltModifier) && !mods.testFlag(Qt::AltModifier)) { + sendNotification("modifierkey-unlatched", i18n("The Alt key is now inactive.")); + } + + if (!m_currentModifiers.testFlag(Qt::MetaModifier) && mods.testFlag(Qt::MetaModifier)) { + sendNotification("modifierkey-latched", i18n("The Meta key is now active.")); + } + + if (m_currentModifiers.testFlag(Qt::MetaModifier) && !mods.testFlag(Qt::MetaModifier)) { + sendNotification("modifierkey-unlatched", i18n("The Meta key is now inactive.")); + } + } + + m_currentModifiers = input()->keyboard()->xkb()->modifiers(); +} + +void KeyNotificationPlugin::sendNotification(const QString &eventId, const QString &text) +{ + KNotification *notification = new KNotification(eventId); + notification->setComponentName(QStringLiteral("kaccess")); + notification->setText(text); + notification->sendEvent(); +} + +void KeyNotificationPlugin::loadConfig(const KConfigGroup &group) +{ + m_enabled = group.readEntry("kNotifyModifiers", false); + m_useBellWhenLocksChange = group.readEntry("ToggleKeysBeep", false); +} +} + +#include "moc_keynotification.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/keynotification/keynotification.h b/local/recipes/kde/kwin/source/src/plugins/keynotification/keynotification.h new file mode 100644 index 0000000000..a4da0084aa --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/keynotification/keynotification.h @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2024 Nicolas Fella + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include "effect/globals.h" +#include "plugin.h" + +#include +#include + +namespace KWin +{ +class KeyNotificationPlugin : public KWin::Plugin +{ + Q_OBJECT +public: + explicit KeyNotificationPlugin(); + +private: + void loadConfig(const KConfigGroup &group); + void ledsChanged(KWin::LEDs leds); + void modifiersChanged(); + void sendNotification(const QString &eventId, const QString &text); + + KConfigWatcher::Ptr m_configWatcher; + bool m_enabled = false; + KWin::LEDs m_currentLEDs; + Qt::KeyboardModifiers m_currentModifiers; + bool m_useBellWhenLocksChange = false; +}; +} diff --git a/local/recipes/kde/kwin/source/src/plugins/keynotification/main.cpp b/local/recipes/kde/kwin/source/src/plugins/keynotification/main.cpp new file mode 100644 index 0000000000..ce5d87523d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/keynotification/main.cpp @@ -0,0 +1,24 @@ +/* + SPDX-FileCopyrightText: 2024 Nicolas Fella + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "plugin.h" + +#include "keynotification.h" + +class KWIN_EXPORT KeyNotificationFactory : public KWin::PluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID PluginFactory_iid FILE "metadata.json") + Q_INTERFACES(KWin::PluginFactory) + +public: + std::unique_ptr create() const override + { + return std::make_unique(); + } +}; + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/keynotification/metadata.json b/local/recipes/kde/kwin/source/src/plugins/keynotification/metadata.json new file mode 100644 index 0000000000..aa304f4093 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/keynotification/metadata.json @@ -0,0 +1,5 @@ +{ + "KPlugin": { + "EnabledByDefault": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/kglobalaccel/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/kglobalaccel/CMakeLists.txt new file mode 100644 index 0000000000..6b1e53af8a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kglobalaccel/CMakeLists.txt @@ -0,0 +1,3 @@ +add_library(KF6GlobalAccelKWinPlugin OBJECT kglobalaccel_plugin.cpp) +target_compile_definitions(KF6GlobalAccelKWinPlugin PRIVATE QT_STATICPLUGIN) +target_link_libraries(KF6GlobalAccelKWinPlugin K::KGlobalAccelD kwin) diff --git a/local/recipes/kde/kwin/source/src/plugins/kglobalaccel/kglobalaccel_plugin.cpp b/local/recipes/kde/kwin/source/src/plugins/kglobalaccel/kglobalaccel_plugin.cpp new file mode 100644 index 0000000000..c3fbb910ca --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kglobalaccel/kglobalaccel_plugin.cpp @@ -0,0 +1,56 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "kglobalaccel_plugin.h" + +#include "input.h" + +#include + +KGlobalAccelImpl::KGlobalAccelImpl(QObject *parent) + : KGlobalAccelInterface(parent) +{ +} + +KGlobalAccelImpl::~KGlobalAccelImpl() = default; + +bool KGlobalAccelImpl::grabKey(int key, bool grab) +{ + return true; +} + +bool KGlobalAccelImpl::checkKeyPressed(int keyQt, KWin::KeyboardKeyState state) +{ + switch (state) { + case KWin::KeyboardKeyState::Pressed: + return keyEvent(keyQt, ShortcutKeyState::Pressed); + case KWin::KeyboardKeyState::Repeated: + return keyEvent(keyQt, ShortcutKeyState::Repeated); + case KWin::KeyboardKeyState::Released: + return keyEvent(keyQt, ShortcutKeyState::Released); + } + + return false; +} + +bool KGlobalAccelImpl::checkPointerPressed(Qt::MouseButtons buttons) +{ + return pointerPressed(buttons); +} + +bool KGlobalAccelImpl::checkAxisTriggered(int axis) +{ + return axisTriggered(axis); +} + +void KGlobalAccelImpl::cancelModiferOnlySequence() +{ + resetModifierOnlyState(); +} + +#include "moc_kglobalaccel_plugin.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/kglobalaccel/kglobalaccel_plugin.h b/local/recipes/kde/kwin/source/src/plugins/kglobalaccel/kglobalaccel_plugin.h new file mode 100644 index 0000000000..7db80ba296 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kglobalaccel/kglobalaccel_plugin.h @@ -0,0 +1,34 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "core/inputdevice.h" + +#include + +#include + +class KGlobalAccelImpl : public KGlobalAccelInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID KGlobalAccelInterface_iid FILE "kwin.json") + Q_INTERFACES(KGlobalAccelInterface) + +public: + KGlobalAccelImpl(QObject *parent = nullptr); + ~KGlobalAccelImpl() override; + + bool grabKey(int key, bool grab) override; + +public Q_SLOTS: + bool checkKeyPressed(int keyQt, KWin::KeyboardKeyState state); + bool checkPointerPressed(Qt::MouseButtons buttons); + bool checkAxisTriggered(int axis); + void cancelModiferOnlySequence(); +}; diff --git a/local/recipes/kde/kwin/source/src/plugins/kglobalaccel/kwin.json b/local/recipes/kde/kwin/source/src/plugins/kglobalaccel/kwin.json new file mode 100644 index 0000000000..0a2fc35aca --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kglobalaccel/kwin.json @@ -0,0 +1,3 @@ +{ + "platforms": ["org.kde.kwin"] +} diff --git a/local/recipes/kde/kwin/source/src/plugins/kpackage/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/kpackage/CMakeLists.txt new file mode 100644 index 0000000000..0766021382 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kpackage/CMakeLists.txt @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2023 Alexander Lohnau +# SPDX-License-Identifier: BSD-2-Clause + +function(kwin_add_kpackage_structure dir file) + kcoreaddons_add_plugin(kwin_${dir} SOURCES ${file} INSTALL_NAMESPACE kf6/packagestructure) + target_link_libraries(kwin_${dir} KF6::Package) +endfunction() + +kwin_add_kpackage_structure(aurorae aurorae/aurorae.cpp) +kwin_add_kpackage_structure(decoration decoration/decoration.cpp) +kwin_add_kpackage_structure(effect effect/effect.cpp) +kwin_add_kpackage_structure(scripts scripts/scripts.cpp) +kwin_add_kpackage_structure(windowswitcher windowswitcher/windowswitcher.cpp) diff --git a/local/recipes/kde/kwin/source/src/plugins/kpackage/aurorae/aurorae.cpp b/local/recipes/kde/kwin/source/src/plugins/kpackage/aurorae/aurorae.cpp new file mode 100644 index 0000000000..0cbcde8f58 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kpackage/aurorae/aurorae.cpp @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2017 Demitrius Belai + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +class AuroraePackage : public KPackage::PackageStructure +{ + Q_OBJECT +public: + using KPackage::PackageStructure::PackageStructure; + void initPackage(KPackage::Package *package) override + { + package->setContentsPrefixPaths(QStringList()); + package->setDefaultPackageRoot(QStringLiteral("aurorae/themes/")); + + package->addFileDefinition("decoration", QStringLiteral("decoration.svgz")); + package->setRequired("decoration", true); + + package->addFileDefinition("close", QStringLiteral("close.svgz")); + package->addFileDefinition("minimize", QStringLiteral("minimize.svgz")); + package->addFileDefinition("maximize", QStringLiteral("maximize.svgz")); + package->addFileDefinition("restore", QStringLiteral("restore.svgz")); + package->addFileDefinition("alldesktops", QStringLiteral("alldesktops.svgz")); + package->addFileDefinition("keepabove", QStringLiteral("keepabove.svgz")); + package->addFileDefinition("keepbelow", QStringLiteral("keepbelow.svgz")); + package->addFileDefinition("shade", QStringLiteral("shade.svgz")); + package->addFileDefinition("help", QStringLiteral("help.svgz")); + package->addFileDefinition("configrc", QStringLiteral("configrc")); + package->setDefaultMimeTypes(QStringList{QStringLiteral("image/svg+xml-compressed")}); + } + + void pathChanged(KPackage::Package *package) override + { + if (package->path().isEmpty()) { + return; + } + + const QString configrc = package->metadata().pluginId() + "rc"; + package->addFileDefinition("configrc", configrc); + } +}; + +K_PLUGIN_CLASS_WITH_JSON(AuroraePackage, "aurorae.json") + +#include "aurorae.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/kpackage/aurorae/aurorae.json b/local/recipes/kde/kwin/source/src/plugins/kpackage/aurorae/aurorae.json new file mode 100644 index 0000000000..6d6803090c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kpackage/aurorae/aurorae.json @@ -0,0 +1,3 @@ +{ + "KPackageStructure": "KWin/Aurorae" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/kpackage/decoration/decoration.cpp b/local/recipes/kde/kwin/source/src/plugins/kpackage/decoration/decoration.cpp new file mode 100644 index 0000000000..20d997bd9e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kpackage/decoration/decoration.cpp @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2017 Demitrius Belai + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +class DecorationPackage : public KPackage::PackageStructure +{ + Q_OBJECT +public: + using KPackage::PackageStructure::PackageStructure; + void initPackage(KPackage::Package *package) override + { + package->setDefaultPackageRoot(QStringLiteral("kwin/decorations/")); + + package->addDirectoryDefinition("config", QStringLiteral("config")); + package->setMimeTypes("config", QStringList{QStringLiteral("text/xml")}); + + package->addDirectoryDefinition("ui", QStringLiteral("ui")); + + package->addDirectoryDefinition("code", QStringLiteral("code")); + + package->addFileDefinition("mainscript", QStringLiteral("code/main.qml")); + package->setRequired("mainscript", true); + + package->setMimeTypes("decoration", QStringList{QStringLiteral("text/plain")}); + } +}; + +K_PLUGIN_CLASS_WITH_JSON(DecorationPackage, "decoration.json") + +#include "decoration.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/kpackage/decoration/decoration.json b/local/recipes/kde/kwin/source/src/plugins/kpackage/decoration/decoration.json new file mode 100644 index 0000000000..82f12bfd49 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kpackage/decoration/decoration.json @@ -0,0 +1,3 @@ +{ + "KPackageStructure": "KWin/Decoration" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/kpackage/effect/effect.cpp b/local/recipes/kde/kwin/source/src/plugins/kpackage/effect/effect.cpp new file mode 100644 index 0000000000..f86d35556b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kpackage/effect/effect.cpp @@ -0,0 +1,50 @@ +/* + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +class EffectPackageStructure : public KPackage::PackageStructure +{ + Q_OBJECT +public: + using KPackage::PackageStructure::PackageStructure; + void initPackage(KPackage::Package *package) override + { + package->setDefaultPackageRoot(QStringLiteral("kwin/effects/")); + + package->addDirectoryDefinition("code", QStringLiteral("code")); + package->setMimeTypes("code", QStringList{QStringLiteral("text/plain")}); + + package->addDirectoryDefinition("ui", QStringLiteral("ui")); + package->setMimeTypes("ui", QStringList{QStringLiteral("text/plain")}); + + package->addFileDefinition("config", QStringLiteral("config/main.xml")); + package->setMimeTypes("config", QStringList{QStringLiteral("text/xml")}); + + package->addFileDefinition("configui", QStringLiteral("ui/config.ui")); + package->setMimeTypes("configui", QStringList{QStringLiteral("text/xml")}); + } + + void pathChanged(KPackage::Package *package) override + { + if (!package->metadata().isValid()) { + return; + } + + const QString api = package->metadata().value(QStringLiteral("X-Plasma-API")); + if (api == QLatin1StringView("javascript")) { + package->addFileDefinition("mainscript", QStringLiteral("code/main.js")); + package->setRequired("mainscript", true); + } else if (api == QLatin1StringView("declarativescript")) { + package->addFileDefinition("mainscript", QStringLiteral("ui/main.qml")); + package->setRequired("mainscript", true); + } + } +}; + +K_PLUGIN_CLASS_WITH_JSON(EffectPackageStructure, "effect.json") + +#include "effect.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/kpackage/effect/effect.json b/local/recipes/kde/kwin/source/src/plugins/kpackage/effect/effect.json new file mode 100644 index 0000000000..e27279a6c0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kpackage/effect/effect.json @@ -0,0 +1,3 @@ +{ + "KPackageStructure": "KWin/Effect" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/kpackage/scripts/scripts.cpp b/local/recipes/kde/kwin/source/src/plugins/kpackage/scripts/scripts.cpp new file mode 100644 index 0000000000..bee57910ed --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kpackage/scripts/scripts.cpp @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: 2017 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +class ScriptsPackage : public KPackage::PackageStructure +{ + Q_OBJECT +public: + using KPackage::PackageStructure::PackageStructure; + void initPackage(KPackage::Package *package) override + { + package->setDefaultPackageRoot(QStringLiteral("kwin/scripts/")); + + package->addDirectoryDefinition("config", QStringLiteral("config")); + package->setMimeTypes("config", QStringList{QStringLiteral("text/xml")}); + + package->addDirectoryDefinition("ui", QStringLiteral("ui")); + + package->addDirectoryDefinition("code", QStringLiteral("code")); + + package->setMimeTypes("scripts", QStringList{QStringLiteral("text/plain")}); + } + + void pathChanged(KPackage::Package *package) override + { + if (!package->metadata().isValid()) { + return; + } + + const QString api = package->metadata().value(QStringLiteral("X-Plasma-API")); + if (api == QStringLiteral("javascript")) { + package->addFileDefinition("mainscript", QStringLiteral("code/main.js")); + package->setRequired("mainscript", true); + } else if (api == QStringLiteral("declarativescript")) { + package->addFileDefinition("mainscript", QStringLiteral("ui/main.qml")); + package->setRequired("mainscript", true); + } + } +}; + +K_PLUGIN_CLASS_WITH_JSON(ScriptsPackage, "scripts.json") + +#include "scripts.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/kpackage/scripts/scripts.json b/local/recipes/kde/kwin/source/src/plugins/kpackage/scripts/scripts.json new file mode 100644 index 0000000000..503281db6a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kpackage/scripts/scripts.json @@ -0,0 +1,3 @@ +{ + "KPackageStructure": "KWin/Script" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/kpackage/windowswitcher/windowswitcher.cpp b/local/recipes/kde/kwin/source/src/plugins/kpackage/windowswitcher/windowswitcher.cpp new file mode 100644 index 0000000000..c0a58ba61e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kpackage/windowswitcher/windowswitcher.cpp @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2017 Marco Martin + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include + +class SwitcherPackage : public KPackage::PackageStructure +{ + Q_OBJECT +public: + using KPackage::PackageStructure::PackageStructure; + void initPackage(KPackage::Package *package) override + { + package->setDefaultPackageRoot(QStringLiteral("kwin/tabbox/")); + + package->addDirectoryDefinition("config", QStringLiteral("config")); + package->setMimeTypes("config", QStringList{QStringLiteral("text/xml")}); + + package->addDirectoryDefinition("ui", QStringLiteral("ui")); + + package->addDirectoryDefinition("code", QStringLiteral("code")); + + package->addFileDefinition("mainscript", QStringLiteral("ui/main.qml")); + package->setRequired("mainscript", true); + + package->setMimeTypes("windowswitcher", QStringList(QStringLiteral("text/plain"))); + } +}; + +K_PLUGIN_CLASS_WITH_JSON(SwitcherPackage, "windowswitcher.json") + +#include "windowswitcher.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/kpackage/windowswitcher/windowswitcher.json b/local/recipes/kde/kwin/source/src/plugins/kpackage/windowswitcher/windowswitcher.json new file mode 100644 index 0000000000..9b92ab98e5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kpackage/windowswitcher/windowswitcher.json @@ -0,0 +1,3 @@ +{ + "KPackageStructure": "KWin/WindowSwitcher" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/krunner-integration/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/krunner-integration/CMakeLists.txt new file mode 100644 index 0000000000..1cf474c3b9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/krunner-integration/CMakeLists.txt @@ -0,0 +1,11 @@ +set(krunnerintegration_SOURCES + main.cpp + windowsrunnerinterface.cpp +) + +qt_add_dbus_adaptor(krunnerintegration_SOURCES org.kde.krunner1.xml windowsrunnerinterface.h KWin::WindowsRunner) + +kcoreaddons_add_plugin(krunnerintegration SOURCES ${krunnerintegration_SOURCES} INSTALL_NAMESPACE "kwin/plugins") +target_link_libraries(krunnerintegration kwin KF6::I18n) + +install(FILES kwin-runner-windows.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins) diff --git a/local/recipes/kde/kwin/source/src/plugins/krunner-integration/dbusutils_p.h b/local/recipes/kde/kwin/source/src/plugins/krunner-integration/dbusutils_p.h new file mode 100644 index 0000000000..2e02d13d5d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/krunner-integration/dbusutils_p.h @@ -0,0 +1,134 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 David Edmundson + SPDX-FileCopyrightText: 2018 Laurent Montel + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +const qreal HighestCategoryRelevance = 100; // KRunner::QueryMatch::CategoryRelevance::Highest +const qreal LowCategoryRelevance = 30; + +struct RemoteMatch +{ + // sssuda{sv} + QString id; + QString text; + QString iconName; + int categoryRelevance = HighestCategoryRelevance; + qreal relevance = 0; + QVariantMap properties; +}; + +typedef QList RemoteMatches; + +struct RemoteAction +{ + QString id; + QString text; + QString iconName; +}; + +typedef QList RemoteActions; + +struct RemoteImage +{ + // iiibiiay (matching notification spec image-data attribute) + int width = 0; + int height = 0; + int rowStride = 0; + bool hasAlpha = false; + int bitsPerSample = 0; + int channels = 0; + QByteArray data; +}; + +inline QDBusArgument &operator<<(QDBusArgument &argument, const RemoteMatch &match) +{ + argument.beginStructure(); + argument << match.id; + argument << match.text; + argument << match.iconName; + argument << match.categoryRelevance; + argument << match.relevance; + argument << match.properties; + argument.endStructure(); + return argument; +} + +inline const QDBusArgument &operator>>(const QDBusArgument &argument, RemoteMatch &match) +{ + argument.beginStructure(); + argument >> match.id; + argument >> match.text; + argument >> match.iconName; + argument >> match.categoryRelevance; + argument >> match.relevance; + argument >> match.properties; + argument.endStructure(); + + return argument; +} + +inline QDBusArgument &operator<<(QDBusArgument &argument, const RemoteAction &action) +{ + argument.beginStructure(); + argument << action.id; + argument << action.text; + argument << action.iconName; + argument.endStructure(); + return argument; +} + +inline const QDBusArgument &operator>>(const QDBusArgument &argument, RemoteAction &action) +{ + argument.beginStructure(); + argument >> action.id; + argument >> action.text; + argument >> action.iconName; + argument.endStructure(); + return argument; +} + +inline QDBusArgument &operator<<(QDBusArgument &argument, const RemoteImage &image) +{ + argument.beginStructure(); + argument << image.width; + argument << image.height; + argument << image.rowStride; + argument << image.hasAlpha; + argument << image.bitsPerSample; + argument << image.channels; + argument << image.data; + argument.endStructure(); + return argument; +} + +inline const QDBusArgument &operator>>(const QDBusArgument &argument, RemoteImage &image) +{ + argument.beginStructure(); + argument >> image.width; + argument >> image.height; + argument >> image.rowStride; + argument >> image.hasAlpha; + argument >> image.bitsPerSample; + argument >> image.channels; + argument >> image.data; + argument.endStructure(); + return argument; +} + +Q_DECLARE_METATYPE(RemoteMatch) +Q_DECLARE_METATYPE(RemoteMatches) +Q_DECLARE_METATYPE(RemoteAction) +Q_DECLARE_METATYPE(RemoteActions) +Q_DECLARE_METATYPE(RemoteImage) diff --git a/local/recipes/kde/kwin/source/src/plugins/krunner-integration/kwin-runner-windows.desktop b/local/recipes/kde/kwin/source/src/plugins/krunner-integration/kwin-runner-windows.desktop new file mode 100644 index 0000000000..58b421d5d7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/krunner-integration/kwin-runner-windows.desktop @@ -0,0 +1,152 @@ +[Desktop Entry] +# ctxt: plasma runner +Name=Windows +Name[ar]=النوافذ +Name[az]=Pəncərələr +Name[be]=Вокны +Name[bg]=Прозорци +Name[ca]=Finestres +Name[ca@valencia]=Finestres +Name[cs]=Okna +Name[da]=Vinduer +Name[de]=Fenster +Name[el]=Παράθυρα +Name[en_GB]=Windows +Name[eo]=Vindozo +Name[es]=Ventanas +Name[eu]=Leihoak +Name[fi]=Ikkunat +Name[fr]=Fenêtres +Name[gl]=Xanelas +Name[he]=חלונות +Name[hu]=Ablakok +Name[ia]=Fenestras +Name[id]=Jendela +Name[is]=Gluggar +Name[it]=Finestre +Name[ja]=ウィンドウ +Name[ka]=ფანჯრები +Name[ko]=창 +Name[lt]=Langai +Name[lv]=Logi +Name[nb]=Vinduer +Name[nl]=Vensters +Name[nn]=Vindauge +Name[pl]=Okna +Name[pt]=Janelas +Name[pt_BR]=Janelas +Name[ro]=Ferestre +Name[ru]=Окна +Name[sa]=विण्डोज +Name[sk]=Okná +Name[sl]=Okna +Name[sv]=Fönster +Name[ta]=சாளரங்கள் +Name[th]=หน้าต่าง +Name[tr]=Pencereler +Name[uk]=Вікна +Name[vi]=Cửa sổ +Name[zh_CN]=窗口 +Name[zh_TW]=視窗 +Comment=List windows and desktops and switch them +Comment[ar]=يسرد كل النوافذ وأسطح المكتب ويبدل بينهن +Comment[az]=Pəncərələrin, İş Masalarının siyahısı və onlar arası keçid +Comment[be]=Спіс акон і працоўных сталоў і пераключэнне паміж імі +Comment[bg]=Показва списък на прозорци и работни плотове и превключва между тях +Comment[ca]=Llista finestres i escriptoris, i canvia entre ells +Comment[ca@valencia]=Llista finestres i escriptoris, i canvia entre ells +Comment[cs]=Seznam oken a ploch k přepínání +Comment[da]=Oplist vinduer og skriveborde og skift mellem dem +Comment[de]=Listet Fenster und Arbeitsflächen auf und wechselt zwischen ihnen +Comment[en_GB]=List windows and desktops and switch them +Comment[eo]=Listigi fenestrojn kaj labortablojn kaj ŝalti ilin +Comment[es]=Listar ventanas y escritorios y cambiar entre ellos +Comment[eu]=Leihoak eta mahaigainak zerrendatzea eta haien artean aldatzea +Comment[fi]=Luettele ikkunat ja työpöydät ja vaihda niihin +Comment[fr]=Lister les fenêtres et les bureaux pour pouvoir passer de l'un à l'autre. +Comment[gl]=Lista as xanelas e escritorios e salta entre eles. +Comment[he]=הצגת חלונות ושולחנות עבודה ואפשרות לעבור אליהם +Comment[hu]=Ablakok és asztalok listázása, és váltás azokra +Comment[ia]=Lista fenestras e scriptorios e commuta los +Comment[id]=Daftar jendela dan desktop dan beralih diantarnya +Comment[is]=Sýna lista yfir glugga og skjáborð og skipta á milli þeirra +Comment[it]=Elenca e seleziona le finestre e i desktop +Comment[ja]=ウィンドウとデスクトップをリストして切り替え +Comment[ka]=ფანჯრებისა და სამუშაო მაგიდების სია და მათ შორის გადართვა +Comment[ko]=창 및 바탕 화면 목록을 보여 주고 전환합니다 +Comment[lt]=Išvardyti langus ir darbalaukius bei perjungti į juos +Comment[lv]=Uzskaita logus un darbvirsmas un tās pārslēdz +Comment[nb]=Vis vinduer/skrivebord, og bytt mellom de +Comment[nl]=Toon vensters en bureaubladen en schakel ze om +Comment[nn]=Vis vindauge/skrivebord, og byt mellom dei +Comment[pl]=Wypisuje okna i pulpity oraz przełącza pomiędzy nimi +Comment[pt]=Listar as janelas e ecrãs e mudar entre elas +Comment[pt_BR]=Lista as janelas e áreas de trabalho e troca entre elas +Comment[ro]=Enumeră ferestre și birouri și le schimbă +Comment[ru]=Просмотр списка и переключение между окнами и рабочими столами +Comment[sa]=विण्डोज, डेस्कटॉप् च सूचीकृत्य तान् स्विच् कुर्वन्तु +Comment[sk]=Zobraziť okná a plochy a prepnúť ich +Comment[sl]=Prikaži seznam oken in namizij in preklopi med njimi +Comment[sv]=Lista fönster och skrivbord, och byt mellan dem +Comment[ta]=சாளரங்களையும் பணிமேடைகளையும் காட்டி அவற்றுக்கிடையே தாவும் +Comment[tr]=Pencereleri ve masaüstlerini listeleyin ve aralarında geçiş yapın +Comment[uk]=Показує список вікон і стільниць і перемикає їх +Comment[vi]=Liệt kê các cửa sổ và bàn làm việc và chuyển giữa chúng +Comment[zh_CN]=列出所有窗口和桌面,并可供切换 +Comment[zh_TW]=列出視窗與桌面並切換至它們 +Type=Service +Icon=preferences-system-windows +X-KDE-PluginInfo-Author=Martin Gräßlin +X-KDE-PluginInfo-Email=kde@martin-graesslin.com +X-KDE-PluginInfo-Name=windows +X-KDE-PluginInfo-Version=1.0 +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=true +X-Plasma-API=DBus +X-Plasma-DBusRunner-Service=org.kde.KWin +X-Plasma-DBusRunner-Path=/WindowsRunner +X-Plasma-Request-Actions-Once=true +X-Plasma-Runner-Syntaxes=:q:,:q:,:q:,window,desktop +X-Plasma-Runner-Syntax-Descriptions=Finds windows whose name or window application match :q:. It is possible to interact with the windows by using one of the following keywords: activate\\, close\\, min(imize)\\, max(imize)\\, fullscreen\\, shade\\, keep above and keep below.,Finds windows which are on desktop named :q:,Switch to desktop named :q:,Lists all windows and allows to activate them.,Lists all other desktops and allows to switch to them. +X-Plasma-Runner-Syntax-Descriptions[ar]=يعثر على النوافذ أو تطبيق النافذة التي تطابق :q:. من الممكن أن تتفاعل مع النوافذ باستخدام الكلمات المفتاحية التالية: activate\\, close\\, min(imize)\\, max(imize)\\, fullscreen\\, shade\\, اجعلها في الأعلى واجعلها في الأسفل, يعثر على النوافذ في سطح المكتب المسمى :q:, بدل بين سطح المكتب المسمى :q:, يسرد كل النوافذ ويسمح بتنشيطها, يسرد كل أسطح المكتب ويسمح بالتبديل بينها. +X-Plasma-Runner-Syntax-Descriptions[be]=Знаходзіць вокны, чые назвы або вокны праграмы адпавядаюць :q:. Можна ўзаемадзейнічаць з вокнамі з дапамогай наступных ключавых слоў: activate\\, close\\, min(imize)\\, max(imize)\\, fullscreen\\, shade\\, keep above і keep below. Знаходзіць вокны, якія знаходзяцца на працоўным стале :q:, можа пераключыцца на працоўны стол :q:, пералічвае ўсе вокны і дазваляе іх актываваць, пералічвае ўсе іншыя працоўныя сталы і дазваляе пераключацца на іх. +X-Plasma-Runner-Syntax-Descriptions[bg]=Намира прозорци, чието име или приложение за прозорци съвпада с :q:. Възможно е да се взаимодейства с прозорците, като се използва една от следните ключови думи: activate\\, close\\, min(imize)\\, max(imize)\\, fullscreen\\, shade\\, keep above и keep below. Намира прозорци, които са на работния плот с име :q:,Превключва на работния плот с име :q:,Изброява всички прозорци и позволява да ги активирате,Изброява всички десктопи и позволява да се премине към тях. +X-Plasma-Runner-Syntax-Descriptions[ca]=Cerca finestres el nom o l'aplicació de les quals coincideixi amb :q:. És possible interactuar amb les finestres utilitzant una de les següents paraules clau: activa\\, tanca\\, min(imitza)\\, max(imitza)\\, pantalla completa\\, plega\\, mantén a sobre i mantén a sota. Cerca les finestres que estan a l'escriptori anomenat :q:. Canvia a l'escriptori anomenat :q:. Llista totes les finestres i permet activar-les. Llista tots els altres escriptoris i permet canviar-hi. +X-Plasma-Runner-Syntax-Descriptions[ca@valencia]=Busca finestres el nom o l'aplicació de les quals coincidisca amb :q:. És possible interactuar amb les finestres utilitzant una de les paraules clau següents: activa\\, tanca\\, min(imitza)\\, max(imitza)\\, pantalla completa\\, plega\\, mantín damunt i mantín davall. Busca les finestres que estan a l'escriptori anomenat :q:. Canvia a l'escriptori anomenat :q:. Llista totes les finestres i permet activar-les. Llista tots els altres escriptoris i permet canviar-hi. +X-Plasma-Runner-Syntax-Descriptions[cs]=Najde okna, jejichž název nebo okno aplikace odpovídá :q:. S okny je možné manipulovat pomocí jednoho z následujících klíčových slov: aktivovat\\, zavřít\\, min(imalizovat)\\, max(imalizovat)\\, celá obrazovka\\, zastínit\\, držet nad a držet pod.,Najde okna, která jsou na ploše jménem :q:,Přepne na plochu jménem :q:,Vypíše všechna okna a umožní jejich aktivaci.,Vypíše všechny ostatní plochy a umožní na ně přepnout. +X-Plasma-Runner-Syntax-Descriptions[da]=Finder vinduer hvis navn eller vindueapplikationer matcher :q:. Det er muligt at interagere med vinduer ved at bruge følgende nøgleord: activate\\ , close\\, min(imize)\\, max(imize)\\, fulscreen\\, shade\\, keep above and below., Finder vinduer, der er på skrivebordet og hedder :1:,Skift til skriveborde, der hedder :q:, Vis alle vinduer i en liste, og tillader at aktiverer dem, Viser alle andre skriveborde i en liste, og tillader at skifte til dem. +X-Plasma-Runner-Syntax-Descriptions[de]=Findet Fenster, deren Namen oder Fensteranwendung mit :q: übereinstimmt. Es ist möglich, mit den Fenstern mittels eines der folgender Schlüsselwörter zu interagieren: aktivieren\\, schließen\\, min(imieren)\\, max(imieren)\\, Vollbild\\, Fensterheber\\, im Vordergrund halten und im Hintergrund halten.,Findet Fenster auf der Arbeitsfläche mit dem Namen :q:,Wechselt zu der Arbeitsfläche mit dem Namen :q:,Listet alle Fenster auf und ermöglicht deren Aktivierung.,Listet alle anderen Arbeitsflächen auf und ermöglicht, zu ihnen zu wechseln. +X-Plasma-Runner-Syntax-Descriptions[en_GB]=Finds windows whose name or window application match :q:. It is possible to interact with the windows by using one of the following keywords: activate\\, close\\, min(imise)\\, max(imise)\\, fullscreen\\, shade\\, keep above and keep below.,Finds windows which are on desktop named :q:,Switch to desktop named :q:,Lists all windows and allows to activate them.,Lists all other desktops and allows to switch to them. +X-Plasma-Runner-Syntax-Descriptions[eo]=Trovas fenestrojn kies nomo aŭ fenestra aplikaĵo kongruas kun :q:. Eblas interagi kun la fenestroj uzante unu el la sekvaj ŝlosilvortoj: aktivigi\\, fermi\\, min(imize)\\, max(imize)\\, plenekrana\\ \, ombro\\, tenu supre kaj konservu malsupre.,Trovas fenestrojn kiuj estas sur labortablo nomita :q:,Almu al labortablo nomita :q:,Listigas ĉiujn fenestrojn kaj permesas aktivigi ilin.,Listigas ĉiujn aliajn labortablojn kaj permesas por ŝanĝi al ili. +X-Plasma-Runner-Syntax-Descriptions[es]=Encuentra ventanas cuyo nombre o aplicación coincidan con :q:. Se puede interactuar con las ventanas usando alguna de las siguientes palabras clave: activate\\, close\\, nin(imize)\\, max(imize)\\, fullscreen\\, shade\\, keep above y keep below.,Encuentra ventanas que están en el escritorio llamado :q:.,Cambia al escritorio llamado :q:.,Lista todas las ventanas y permite activarlas.,Lista todos los escritorios y permite cambiar a ellos. +X-Plasma-Runner-Syntax-Descriptions[eu]=Haien izena edo leihoko aplikazioa :q:(e)rekin bat datozen leihoak aurkitzen ditu. Leihoekin elkarreragin daiteke ondoko gako-hitzetako bat erabiliz: aktibatu\\, itxi\\, min(imizatu)\\, max(imizatu)\\, pantaila-betea\\, bildu\\, mantendu gainean eta mantendu azpian.,:q: izeneko mahaigainean dauden leihoak aurkitzen ditu,aldatu :q: izeneko mahaigainera,Leiho guztiak zerrendatu eta haiek aktibatzeko aukera ematen du,Beste mahaigain guztiak zerrendatu eta haietara aldatzeko aukera ematen du. +X-Plasma-Runner-Syntax-Descriptions[fi]=Etsii ikkunoita, joiden nimi tai sovellus vastaa hakua :q:. Ikkunoihin voi vaikuttaa seuraavin avainsanoin: activate\\, close\\, min(imize)\\, max(imize)\\, fullscreen\\, shade\\, keep above ja keep below.,Etsii ikkunoita, jotka ovat työpöydällä nimeltä :q:,Vaihda työpöydälle nimeltä :q:,Luettelee kaikki ikkunat ja mahdollistaa niiden aktivoimisen.,Luettelee kaikki toiset työpöydät ja mahdollistaa niille vaihtamisen. +X-Plasma-Runner-Syntax-Descriptions[fr]=Trouve les fenêtres dont le nom ou l'application correspondent à :q:. Il est possible d'interagir avec les fenêtres en utilisant l'un des mots clés suivants : activate, close, min(imize), max(imize), fullscreen, shade, keep above et keep below. Trouve les fenêtres étant sur le bureau nommé :q:. Bascule vers le bureau nommé :q:. Liste toutes les fenêtres et permet de les activer. Liste tous les autres bureaux et permet de naviguer entre eux. +X-Plasma-Runner-Syntax-Descriptions[gl]=Atopa xanelas cun nome ou aplicación de xanela que coincide con :q:. Pódese interactuar coas xanelas usando unha das seguintes palabras clave: activate (activar)\\, close (pechar)\\, min(imize) (minimizar)\\, max(imize) (maximizar)\\, fullscreen (pantalla completa)\\, shade (ensombrecer)\\, keep above (manter por riba) e keep below (manter por baixo).,Atopa xanelas que están no escritorio chamado :q:.,Cambia ao escritorio chamado :q:.,Lista todas as xanelas e permite activalas.,Lista o resto de escritorios e permite cambiar a eles. +X-Plasma-Runner-Syntax-Descriptions[he]=מאתר חלון שהשם או יישום החלון עונה על :q:. אפשר לתפעל את החלונות בעזרת אחת ממילות המפתח הבאות: activate\\‎,‏ close\\‎,‏ min(imize)\\‎,‏ max(imize)\\‎,‏ fullscreen\\‎,‏ shade\\‎, לרחף מעל או לצלול מתחת.,מאתר חלונות שבשולחן עבודה בשם :q:, מעבר לשולחן עבודה בשם :q:,הצגת כל החלונות ומתן אפשרות להפעיל אותם.,להציג את כל שאר שולחנות העבודה ומתן אפשרות לעבור אליהם. +X-Plasma-Runner-Syntax-Descriptions[hu]=Megkeresi azokat az ablakokat, amelyeknek a neve vagy az alkalmazása illeszkedik a :q: kifejezésre. Lehetséges interakcióba lépni az ablakokkal a következő kulcsszavak egyikének használatával: aktiválás\\, bezárás\\, min(imalizálás)\\, max(imalizálás)\\, teljes képernyő\\, felgördítés\\, maradjon felül és maradjon alul.,Megkeresi azokat az ablakokat, amik a :q: nevű assztalon vannak,Vált a :q: nevű asztalra,Listázza az összes ablakot és lehetővé teszi az aktiválásukat.,Listázza az összes többi asztalt és lehetővé teszi a váltást azokra. +X-Plasma-Runner-Syntax-Descriptions[ia]=Trova fenestras cuje nomine o application de fenestra corresponde a whose name or window :q:. Illo es possibile pro interager con le fenestras per usar un del claves sequente: activa\\, claude\\, min(imisa)\\, max(imisa)\\, schermoplen\\, umbra\\, mantene in alto e mantena a basso., Trova fenestras qui es sur scriptorio nominate:q:,Passa a Scriptorio nominate :q:,Lista omne fenestras e permitte activar los.,Lista omne alter scriptorios e permitte commutar a illos. +X-Plasma-Runner-Syntax-Descriptions[id]=Menemukan jendela yang nama atau aplikasi jendelanya cocok dengan :q:. Anda dapat berinteraksi dengan jendela dengan menggunakan salah satu kata kunci berikut: aktifkan\\, tutup\\, min(imalkan)\\, maks(imalkan)\\, layar penuh\\, teduh\\, tetap di atas dan tetap di bawah, Menemukan jendela yang ada di desktop bernama :q:, Beralih ke desktop bernama :q:, Mencantumkan semua jendela dan memungkinkan untuk mengaktifkannya, Mencantumkan semua desktop lain dan memungkinkan untuk beralih ke desktop tersebut. +X-Plasma-Runner-Syntax-Descriptions[is]=Finnur glugga þar sem heiti eða forritsgluggi samsvarar :q:. Það er hægt að vinna með gluggana með því að nota eftirfarandi lykilorð: activate\\, close\\, min(imize)\\, max(imize)\\, fullscreen\\, shade\\, keep above og keep below.,Finnur glugga sem eru á skjáborði sem heitir :q:,Skiptir yfir á skjáborð sem heitir :q:,Telur upp alla glugga og gerir kleift að virkja þá.,Telur upp önnur skjáborð og gerir kleift að skipta yfir á þau. +X-Plasma-Runner-Syntax-Descriptions[it]=Trova le finestre il cui nome o applicazione della finestra corrisponde a :q:. È possibile interagire con le finestre utilizzando una delle seguenti parole chiave: attiva\\, chiudi\\, min(imizza)\\, mas(simizza)\\, schermo interno\\, arrotola\\, mantieni sopra e mantieni sotto.,Trova le finestre che si trovano sul desktop con nome :q:,Passa al desktop con nome :q:,Elenca tutte le finestre e consente di attivarle.,Elenca tutti gli altri desktop e consente di passarvi. +X-Plasma-Runner-Syntax-Descriptions[ka]=ეძებს ფანჯარას სრული სახელით ან :q:-ზე დამთხვეული აპლიკაციით. ასევე შესაძლებელია ამ ფანჯრებთან ინტერაქცია შემდეგ ბრძანებებით: activate\\, close\\, min(imize)\\, max(imize)\\, fullscreen\\, shade\\, keep above და keep below. ეძებს ფანჯრებს, რომლებიც მდებარეობენ სამუშაო მაგიდაზე სახელით :q:, გადართვა სამუშაო მაგიდაზე სახელით :q:, ჩამოთვლის ყველა ფანჯარას და საშუალებას მოგცემთ, გაააქტიუროთ ისინი, ჩამოთვლის სხვა სამუშაო მაგიდებს და საშუალებას გაძლევთ, გადაერთოთ მათზე. +X-Plasma-Runner-Syntax-Descriptions[ko]=이름이나 창을 연 앱이 :q:와 일치하는 창을 찾습니다. 활성화\\, 닫기\\, 최소화\\, 최대화\\, 전체 화면\\, 말아 올리기\\, 항상 위\\, 항상 아래\\ 키워드를 사용하여 해당 창을 제어할 수도 있습니다. 가상 바탕 화면 :q:에 있는 창을 찾거나, :q: 가상 바탕 화면으로 전환하거나, 모든 창의 목록을 표시하고 해당 창을 활성화하거나, 모든 가상 바탕 화면의 목록을 표시하고 전환할 수 있습니다. +X-Plasma-Runner-Syntax-Descriptions[lt]=Suranda langus, kurių pavadinimas ar lango pavadinimas atitinka :q:. Galima sąveikauti su langais naudojant vieną iš šių raktažodžių: aktyvuoti\\, užverti\\, suskleisti\\, išskleisti\\, visas ekranas\\, pridengti\\, laikyti aukščiau ir laikyti žemiau.,Suranda langus, kurie yra darbalaukyje ir, kurių pavadinimas :q:,Perjungti į darbalaukį, pavadinimu, :q:,Išvardija visus langus ir leidžia juos aktyvuoti.,Išvardija visus kitus darbalaukius ir leidžia į juos perjungti. +X-Plasma-Runner-Syntax-Descriptions[lv]=Atrod logus, kuru nosaukumi vai loga programma atbilst: :q:. Ar šiem logiem ir iespējams mijiedarboties, izmantojot kādu no šiem atslēgas vārdiem: aktivizēt\\, aizvērt\\, min(imizēt)\\, maks(imizēt)\\, pilnekrāns\\, saritināt\\, paturēt virspusē un paturēt apakšā.,Atrod logus, kas darbvirsmā ir nosaukti :q:,Pārslēdz darbvirsmu, kas saucas :q:,Uzskaita visus logus un ļauj tos aktivizēt., Uzskaita visas pārējās darbvirsmas un ļauj uz tām pārslēgties. +X-Plasma-Runner-Syntax-Descriptions[nb]=Finn vinduer med navn eller programnavn i samsvar med :q:. Du kan påvirke vinduene via disse nøkkelordene: activate\\, close\\, min(imize)\\, max(imize)\\, fullscreen\\, shade\\, keep above og keep below.,Finn vinduer på skrivebord med navn :q:,Bytt til skrivebord med navn :q:,Vis oversikt over alle vinduer, med mulighet for å aktivere de. Viser oversikt over andre skrivebord og lar deg bytte mellom de. +X-Plasma-Runner-Syntax-Descriptions[nl]=Zoekt vensters waarvan de naam of venstertoepassing overeenkomt met :q:. Het is mogelijk om te werken met de vensters door een van de volgende trefwoorden te gebruiken: activeer\\, sluiten\\, min(imaliseren)\\, max(imaliseren)\\, volledig scherm\\, oprollen\\, boven houden en onder houden.,Zoek vensters die op het bureaublad zijn genaamd :q:,Schakel om naar bureaublad genaamd :q:,Maak een lijst met alle vensters en sta toe ze te activeren.,Maak een lijst met alle andere bureaubladen en sta toe naar ze over te schakelen. +X-Plasma-Runner-Syntax-Descriptions[nn]=Finn vindauge med namn eller programnamn i samsvar med :q:. Du kan påverka vindauga via desse nøkkelorda: activate\\, close\\, min(imize)\\, max(imize)\\, fullscreen\\, shade\\, keep above og keep below.,Finn vindauge på skrivebord med namn :q:,Byt til skrivebord med namn :q:,Vis oversikt over alle vindauge, med moglegheit for å aktivera dei.,Viser oversikt oer andre skrivebord og lèt deg veksla mellom dei. +X-Plasma-Runner-Syntax-Descriptions[pl]=Znajduje okna, których nazwa lub okno aplikacji odpowiada :q:. Można oddziaływać na okno poprzez użyci jednego z następujących słów kluczowych: activate\\, close\\, min(imize)\\, max(imize)\\, fullscreen\\, shade\\, keep above oraz keep below., Znajduje okna, które na pulpicie nazywają się :q:,Przełącza na pulpit o nazwie :q:,Wymienia wszystkie okna i umożliwia ich uaktywnienie.,Wymienia wszystkie inne pulpity i umożliwia przełączenie do nich. +X-Plasma-Runner-Syntax-Descriptions[pt]=Descobre as janelas cujo nome ou aplicação da janela corresponde a :q:. É possível interagir com as janelas, usando uma das seguintes palavras-chave: activar\\, fechar\\, min(imizar)\\, max(imizar)\\, ecrã completo\\, enrolar\\, manter acima e manter abaixo.,Descobre as janelas que estão no ecrã chamado :q:,Muda para o ecrã chamado :q:,Apresenta todas as janelas e permite activá-las.,Apresenta todos os outros ecrãs e permite mudar para eles. +X-Plasma-Runner-Syntax-Descriptions[pt_BR]=Localiza janelas cujo nome ou aplicativo de janela corresponda a :q:. É possível interagir com as janelas usando uma das seguintes palavras-chave: activate\\, close\\, min(imize)\\, max(imize)\\, fullscreen\\, shade\\, manter acima e manter abaixo.,Encontrar janelas que estão na área de trabalho chamada :q:,Mudar para a área de trabalho chamada :q:,Lista todas as janelas e permite ativá-las.,Lista todas as outras áreas de trabalho e permite para mudar para elas. +X-Plasma-Runner-Syntax-Descriptions[ro]=Găsește ferestre a căror denumire sau fereastră se potrivește cu :q:. Se poate interacționa cu ferestrele folosind unul dintre cuvintele-cheie următoare: activate\\, close\\, min(imize)\\, max(imize)\\, fullscreen\\, shade\\, keep above și keep below.,Găsește ferestre ce se află pe biroul numit :q:,Trece la biroul numit :q:,Enumeră toate ferestrele și permite activarea acestora.,Enumeră toate celelalte birouri și permite trecerea la acestea. +X-Plasma-Runner-Syntax-Descriptions[ru]=Поиск окон, название приложения или окна которых соответствует :q:. Для управления окнами возможно использовать одно или ключевых слов: активировать\\, закрыть\\, свернуть\\, распахнуть\\, во весь экран, развернуть из заголовка\\, поддерживать поверх других и поддерживать на заднем плане. Поиск окон, расположенном на рабочем столе с именем :q:. Переключение на рабочий стол с именем :q:. Вывод списка окон и переключение в эти окна. Вывод списка всех остальных рабочих столов и переключение в эти рабочие столы. +X-Plasma-Runner-Syntax-Descriptions[sa]=येषां नाम वा विण्डो अनुप्रयोगः :q: इत्यनेन सह मेलति इति विण्डो अन्वेषयति । निम्नलिखित कीवर्डमध्ये एकस्य उपयोगेन विण्डोज इत्यनेन सह अन्तरक्रिया कर्तुं शक्यते: activate\\, close\\, min(imize)\\, max(imize)\\, fullscreen\\, shade\\, keep above and keep below.,विण्डोस् अन्वेषयति ये :q: इति नामकं डेस्कटॉप् मध्ये सन्ति, :q: इति नामकं डेस्कटॉप् मध्ये स्विच् करोति,सर्वविण्डोस् सूचीकृत्य तान् सक्रियं कर्तुं अनुमतिं ददाति।,अन्ये सर्वान् डेस्कटॉप् सूचीबद्धं करोति तथा तेषु स्विच् कर्तुं अनुमतिं ददाति। +X-Plasma-Runner-Syntax-Descriptions[sk]=Vyhľadá okná, ktorých názov alebo aplikácia okna zodpovedajú :q:. Interakcia s oknami je možná pomocou jedného z nasledujúcich kľúčových slov: activate\\, close\\, min(imize)\\, max(imize)\\, fullscreen\\, shade\\, keep above a keep below.,Vyhľadá okná, ktoré sú na ploche s názvom :q:,Prepnutie na plochu s názvom :q:,Vypíše zoznam všetkých okien a umožní ich aktiváciu.,Vypíše zoznam všetkých ostatných plôch a umožní na ne prepnúť. +X-Plasma-Runner-Syntax-Descriptions[sl]=Poišče okna, pri katerih ime ali okenska aplikacija ujema z :q: . Možna je interakcija z okni z uporabo ene od naslednjih ključnih besed: activate\\, close\\, min(imize)\\, max(imize)\\, fullscreen\\, shade\\, keep above and keep below. Poišče okna, ki so na namizju z imenom :q:,Preklopite na namizje z imenom :q:, naredi seznam vseh oken in omogoča, da jih aktivirate.,Naredi seznam vseh drugih namizij in omogoča prehod nanje. +X-Plasma-Runner-Syntax-Descriptions[sv]=Hittar fönster vars namn eller fönsterprogram motsvarar :q:. Det är möjligt att påverka fönstren genom att använda ett av följande nyckelord: aktivera\\, stäng\\, min(imera)\\, max(imera)\\, fullskärm\\, skugga\\, behåll ovanför och behåll under. Söker efter fönster som finns på skrivbordet vit namn :q:. Byt till skrivbordet vid namn :q:. Listar alla fönster och gör det möjligt att aktivera den. Listar alla andra skrivbord och gör det möjligt att byta till dem. +X-Plasma-Runner-Syntax-Descriptions[tr]=Adı veya uygulaması :q: ile eşleşen pencereleri bulur. Aşağıdaki anahtar sözcüklerden birini kullanarak pencerelerle etkileşmek olanaklıdır: etkinleştir\\, kapat\\, küçült\\, büyüt\\, tam ekran\\, panjuru kapat\\, üzerinde tut ve altında tut.,:q: adlı masaüstünde bulunan pencereleri bulur, :q: adlı masaüstüne geçer,tüm pencereleri listeler ve etkinleştirmeye izin verir.,tüm diğer masaüstlerini listeler ve onlara geçmeye için verir. +X-Plasma-Runner-Syntax-Descriptions[uk]=Знаходить вікна, чия назва або програма, яку пов'язано із вікном, відповідають запиту :q:. Можна взаємодіяти із вікнами за допомогою одного з таких ключових слів: activate\\, close\\, min(imize)\\, max(imize)\\, fullscreen\\, shade\\, keep above та keep below.,Знаходить вікна, які розташовано на стільниці із назвою :q:,Перемикає на стільницю із назвою :q:,Виводить список усіх вікон і надає змогу активувати їх.,Показує список усіх інших стільниць і надає змогу перемкнутися на них. +X-Plasma-Runner-Syntax-Descriptions[vi]=Tìm các cửa sổ mà tên hoặc ứng dụng khớp với :q:. Có thể tương tác với các cửa sổ bằng cách dùng một trong các từ khoá: activate\\, close\\, min(imize)\\, max(imize)\\, fullscreen\\, shade\\, keep above và keep below.,Tìm các cửa sổ ở bàn làm việc có tên :q:,Chuyển sang bàn làm việc có tên :q:,Liệt kê tất cả các cửa sổ và cho phép kích hoạt chúng.,Liệt kê tất cả các bàn làm việc khác và cho phép chuyển sang chúng. +X-Plasma-Runner-Syntax-Descriptions[zh_CN]=查找名称或者窗口应用程序匹配 :q: 的窗口。可以使用下列关键词之一来与窗口交互:activate\\, close\\, min(imize)\\, max(imize)\\, fullscreen\\, shade\\, keep above 和 keep below;查找位于桌面上名称为 :q: 的窗口;切换到名称为 :q: 的桌面;列出所有窗口并允许激活它们;列出所有其他桌面并允许切换到它们。 +X-Plasma-Runner-Syntax-Descriptions[zh_TW]=尋找名稱或所屬的應用程式符合 :q: 的視窗。可以使用以下關鍵字來控制視窗:activate(設為作用中)、close(關閉)、min 或 minimize(最小化)、max 或 maximize(最大化)、fullscreen(全螢幕)、shade(收起)、keep above(維持在最上方)、以及 keep below(維持在最下方)。,尋找在名為 :q: 的桌面的視窗,切換到名為 :q: 的桌面,列出所有視窗並允許將它們設為作用中。,列出所有其他的桌面並允許將切換到它們。 diff --git a/local/recipes/kde/kwin/source/src/plugins/krunner-integration/main.cpp b/local/recipes/kde/kwin/source/src/plugins/krunner-integration/main.cpp new file mode 100644 index 0000000000..750d2c4ed5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/krunner-integration/main.cpp @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "windowsrunnerinterface.h" + +#include + +using namespace KWin; + +class KWIN_EXPORT KRunnerIntegrationFactory : public PluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID PluginFactory_iid FILE "metadata.json") + Q_INTERFACES(KWin::PluginFactory) + +public: + explicit KRunnerIntegrationFactory() = default; + + std::unique_ptr create() const override; +}; + +std::unique_ptr KRunnerIntegrationFactory::create() const +{ + return std::make_unique(); +} + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/krunner-integration/metadata.json b/local/recipes/kde/kwin/source/src/plugins/krunner-integration/metadata.json new file mode 100644 index 0000000000..aa304f4093 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/krunner-integration/metadata.json @@ -0,0 +1,5 @@ +{ + "KPlugin": { + "EnabledByDefault": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/krunner-integration/org.kde.krunner1.xml b/local/recipes/kde/kwin/source/src/plugins/krunner-integration/org.kde.krunner1.xml new file mode 100644 index 0000000000..b9d39a1281 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/krunner-integration/org.kde.krunner1.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/krunner-integration/windowsrunnerinterface.cpp b/local/recipes/kde/kwin/source/src/plugins/krunner-integration/windowsrunnerinterface.cpp new file mode 100644 index 0000000000..cb03060507 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/krunner-integration/windowsrunnerinterface.cpp @@ -0,0 +1,349 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + SPDX-FileCopyrightText: 2020 Benjamin Port + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "windowsrunnerinterface.h" + +#include "virtualdesktops.h" +#include "window.h" +#include "workspace.h" + +#include "krunner1adaptor.h" +#include + +namespace KWin +{ + +WindowsRunner::WindowsRunner() +{ + new Krunner1Adaptor(this); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/WindowsRunner"), this); + QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.KWin")); +} + +RemoteMatches WindowsRunner::Match(const QString &searchTerm) +{ + RemoteMatches matches; + + QString term = searchTerm; + WindowsRunnerAction action = ActivateAction; + if (QString keyword = i18nc("Note this is a KRunner keyword", "activate"); term.endsWith(keyword, Qt::CaseInsensitive)) { + action = ActivateAction; + term = term.left(term.lastIndexOf(keyword) - 1); + } else if (QString keyword = i18nc("Note this is a KRunner keyword", "close"); term.endsWith(keyword, Qt::CaseInsensitive)) { + action = CloseAction; + term = term.left(term.lastIndexOf(keyword) - 1); + } else if (QString keyword = i18nc("Note this is a KRunner keyword", "min"); term.endsWith(keyword, Qt::CaseInsensitive)) { + action = MinimizeAction; + term = term.left(term.lastIndexOf(keyword) - 1); + } else if (QString keyword = i18nc("Note this is a KRunner keyword", "minimize"); term.endsWith(keyword, Qt::CaseInsensitive)) { + action = MinimizeAction; + term = term.left(term.lastIndexOf(keyword) - 1); + } else if (QString keyword = i18nc("Note this is a KRunner keyword", "max"); term.endsWith(keyword, Qt::CaseInsensitive)) { + action = MaximizeAction; + term = term.left(term.lastIndexOf(keyword) - 1); + } else if (term.endsWith(i18nc("Note this is a KRunner keyword", "maximize"), Qt::CaseInsensitive)) { + action = MaximizeAction; + term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "maximize")) - 1); + } else if (QString keyword = i18nc("Note this is a KRunner keyword", "fullscreen"); term.endsWith(keyword, Qt::CaseInsensitive)) { + action = FullscreenAction; + term = term.left(term.lastIndexOf(keyword) - 1); + } else if (QString keyword = i18nc("Note this is a KRunner keyword", "shade"); term.endsWith(keyword, Qt::CaseInsensitive)) { + action = ShadeAction; + term = term.left(term.lastIndexOf(keyword) - 1); + } else if (QString keyword = i18nc("Note this is a KRunner keyword", "keep above"); term.endsWith(keyword, Qt::CaseInsensitive)) { + action = KeepAboveAction; + term = term.left(term.lastIndexOf(keyword) - 1); + } else if (QString keyword = i18nc("Note this is a KRunner keyword", "keep below"); term.endsWith(keyword, Qt::CaseInsensitive)) { + action = KeepBelowAction; + term = term.left(term.lastIndexOf(keyword) - 1); + } + + // keyword match: when term starts with "window" we list all windows + // the list can be restricted to windows matching a given name, class, role or desktop + if (term.startsWith(i18nc("Note this is a KRunner keyword", "window"), Qt::CaseInsensitive)) { + const QStringList keywords = term.split(QLatin1Char(' ')); + QString windowName; + QString windowAppName; + VirtualDesktop *targetDesktop = nullptr; + QVariant desktopId; + for (const QString &keyword : keywords) { + if (keyword.endsWith(QLatin1Char('='))) { + continue; + } + if (keyword.startsWith(i18nc("Note this is a KRunner keyword", "name") + QLatin1Char('='), Qt::CaseInsensitive)) { + windowName = keyword.split(QLatin1Char('='))[1]; + } else if (keyword.startsWith(i18nc("Note this is a KRunner keyword", "appname") + QLatin1Char('='), Qt::CaseInsensitive)) { + windowAppName = keyword.split(QLatin1Char('='))[1]; + } else if (keyword.startsWith(i18nc("Note this is a KRunner keyword", "desktop") + QLatin1Char('='), Qt::CaseInsensitive)) { + desktopId = keyword.split(QLatin1Char('='))[1]; + for (const auto desktop : VirtualDesktopManager::self()->desktops()) { + if (desktop->name().contains(desktopId.toString(), Qt::CaseInsensitive) || desktop->x11DesktopNumber() == desktopId.toUInt()) { + targetDesktop = desktop; + } + } + } else { + // not a keyword - use as name if name is unused, but another option is set + if (windowName.isEmpty() && !keyword.contains(QLatin1Char('=')) && (!windowAppName.isEmpty() || targetDesktop)) { + windowName = keyword; + } + } + } + + for (const Window *window : Workspace::self()->windows()) { + if (window->isUnmanaged()) { + continue; + } + if (!window->isNormalWindow()) { + continue; + } + const QString appName = window->resourceClass(); + const QString name = window->caption(); + if (!windowName.isEmpty() && !name.startsWith(windowName, Qt::CaseInsensitive)) { + continue; + } + if (!windowAppName.isEmpty() && !appName.contains(windowAppName, Qt::CaseInsensitive)) { + continue; + } + + if (targetDesktop && !window->desktops().contains(targetDesktop) && !window->isOnAllDesktops()) { + continue; + } + // check for windows when no keywords were used + // check the name and app name for containing the query without the keyword + if (windowName.isEmpty() && windowAppName.isEmpty() && !targetDesktop) { + const QString &test = term.mid(keywords[0].length() + 1); + if (!name.contains(test, Qt::CaseInsensitive) && !appName.contains(test, Qt::CaseInsensitive)) { + continue; + } + } + // blacklisted everything else: we have a match + if (actionSupported(window, action)) { + matches << windowsMatch(window, action); + } + } + + if (!matches.isEmpty()) { + // the window keyword found matches - do not process other syntax possibilities + return matches; + } + } + + bool desktopAdded = false; + // check for desktop keyword + if (term.startsWith(i18nc("Note this is a KRunner keyword", "desktop"), Qt::CaseInsensitive)) { + const QStringList parts = term.split(QLatin1Char(' ')); + if (parts.size() == 1) { + // only keyword - list all desktops + for (auto desktop : VirtualDesktopManager::self()->desktops()) { + matches << desktopMatch(desktop); + desktopAdded = true; + } + } + } + + // check for matching desktops by name + for (const Window *window : Workspace::self()->windows()) { + if (window->isUnmanaged()) { + continue; + } + if (!window->isNormalWindow()) { + continue; + } + const QString appName = window->resourceClass(); + const QString name = window->caption(); + if (name.startsWith(term, Qt::CaseInsensitive) || appName.startsWith(term, Qt::CaseInsensitive)) { + matches << windowsMatch(window, action, 0.8, HighestCategoryRelevance); + } else if ((name.contains(term, Qt::CaseInsensitive) || appName.contains(term, Qt::CaseInsensitive)) && actionSupported(window, action)) { + matches << windowsMatch(window, action, 0.7, LowCategoryRelevance); + } + } + + for (auto *desktop : VirtualDesktopManager::self()->desktops()) { + if (desktop->name().contains(term, Qt::CaseInsensitive)) { + if (!desktopAdded && desktop != VirtualDesktopManager::self()->currentDesktop()) { + matches << desktopMatch(desktop, ActivateDesktopAction, 0.8); + } + // search for windows on desktop and list them with less relevance + for (const Window *window : Workspace::self()->windows()) { + if (window->isUnmanaged()) { + continue; + } + if (!window->isNormalWindow()) { + continue; + } + if ((window->desktops().contains(desktop) || window->isOnAllDesktops()) && actionSupported(window, action)) { + matches << windowsMatch(window, action, 0.5, LowCategoryRelevance); + } + } + } + } + + return matches; +} + +void WindowsRunner::Run(const QString &id, const QString &actionId) +{ + // Split id to get actionId and realId. We don't use actionId because our actions list is not constant + const QStringList parts = id.split(QLatin1Char('_')); + if (parts.size() != 2) { + return; + } + + const auto action = WindowsRunnerAction(parts[0].toInt()); + const auto objectId = parts[1]; + + if (action == ActivateDesktopAction) { + QByteArray desktopId = objectId.toLocal8Bit(); + auto desktop = VirtualDesktopManager::self()->desktopForId(desktopId); + if (!desktop) { + return; + } + VirtualDesktopManager::self()->setCurrent(desktop); + return; + } + + const auto window = workspace()->findWindow(QUuid::fromString(objectId)); + if (!window || !window->isClient()) { + return; + } + + switch (action) { + case ActivateAction: + workspace()->activateWindow(window); + break; + case CloseAction: + window->closeWindow(); + break; + case MinimizeAction: + window->setMinimized(!window->isMinimized()); + break; + case MaximizeAction: + window->setMaximize(window->maximizeMode() == MaximizeRestore, window->maximizeMode() == MaximizeRestore); + break; + case FullscreenAction: + window->setFullScreen(!window->isFullScreen()); + break; + case ShadeAction: + break; + case KeepAboveAction: + window->setKeepAbove(!window->keepAbove()); + break; + case KeepBelowAction: + window->setKeepBelow(!window->keepBelow()); + break; + case ActivateDesktopAction: + Q_UNREACHABLE(); + } +} + +RemoteMatch WindowsRunner::desktopMatch(const VirtualDesktop *desktop, const WindowsRunnerAction action, qreal relevance) const +{ + RemoteMatch match; + match.id = QString::number(action) + QLatin1Char('_') + desktop->id(); + match.categoryRelevance = HighestCategoryRelevance; + match.iconName = QStringLiteral("user-desktop"); + match.text = desktop->name(); + match.relevance = relevance; + match.properties.insert(QStringLiteral("subtext"), i18n("Switch to desktop %1", desktop->name())); + return match; +} + +RemoteMatch WindowsRunner::windowsMatch(const Window *window, const WindowsRunnerAction action, qreal relevance, qreal categoryRelevance) const +{ + RemoteMatch match; + match.id = QString::number((int)action) + QLatin1Char('_') + window->internalId().toString(); + match.text = window->caption(); + match.iconName = window->icon().name(); + match.relevance = relevance; + match.categoryRelevance = categoryRelevance; + + const QList desktops = window->desktops(); + bool allDesktops = window->isOnAllDesktops(); + + const VirtualDesktop *targetDesktop = VirtualDesktopManager::self()->currentDesktop(); + // Show on current desktop unless window is only attached to other desktop, in this case show on the first attached desktop + if (!allDesktops && !window->isOnCurrentDesktop() && !desktops.isEmpty()) { + targetDesktop = desktops.first(); + } + + // When there is no icon name, send a pixmap along instead + if (match.iconName.isEmpty()) { + QImage convertedImage = window->icon().pixmap(QSize(64, 64)).toImage().convertToFormat(QImage::Format_RGBA8888); + RemoteImage remoteImage{ + convertedImage.width(), + convertedImage.height(), + static_cast(convertedImage.bytesPerLine()), + true, // hasAlpha + 8, // bitsPerSample + 4, // channels + QByteArray(reinterpret_cast(convertedImage.constBits()), convertedImage.sizeInBytes())}; + match.properties.insert(QStringLiteral("icon-data"), QVariant::fromValue(remoteImage)); + } + + const QString desktopName = targetDesktop->name(); + switch (action) { + case CloseAction: + match.properties[QStringLiteral("subtext")] = i18n("Close running window on %1", desktopName); + break; + case MinimizeAction: + match.properties[QStringLiteral("subtext")] = i18n("(Un)minimize running window on %1", desktopName); + break; + case MaximizeAction: + match.properties[QStringLiteral("subtext")] = i18n("Maximize/restore running window on %1", desktopName); + break; + case FullscreenAction: + match.properties[QStringLiteral("subtext")] = i18n("Toggle fullscreen for running window on %1", desktopName); + break; + case ShadeAction: + match.properties[QStringLiteral("subtext")] = i18n("(Un)shade running window on %1", desktopName); + break; + case KeepAboveAction: + match.properties[QStringLiteral("subtext")] = i18n("Toggle keep above for running window on %1", desktopName); + break; + case KeepBelowAction: + match.properties[QStringLiteral("subtext")] = i18n("Toggle keep below running window on %1", desktopName); + break; + case ActivateAction: + default: + match.properties[QStringLiteral("subtext")] = i18n("Activate running window on %1", desktopName); + break; + } + return match; +} + +bool WindowsRunner::actionSupported(const Window *window, const WindowsRunnerAction action) const +{ + switch (action) { + case CloseAction: + return window->isCloseable(); + case MinimizeAction: + return window->isMinimizable(); + case MaximizeAction: + return window->isMaximizable(); + case ShadeAction: + return false; + case FullscreenAction: + return window->isFullScreenable(); + case KeepAboveAction: + case KeepBelowAction: + case ActivateAction: + default: + return true; + } +} + +} + +#include "moc_windowsrunnerinterface.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/krunner-integration/windowsrunnerinterface.h b/local/recipes/kde/kwin/source/src/plugins/krunner-integration/windowsrunnerinterface.h new file mode 100644 index 0000000000..336b966750 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/krunner-integration/windowsrunnerinterface.h @@ -0,0 +1,60 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + SPDX-FileCopyrightText: 2020 Benjamin Port + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "dbusutils_p.h" +#include "plugin.h" + +#include +#include +#include +#include +#include + +namespace KWin +{ +class VirtualDesktop; +class Window; + +class WindowsRunner : public Plugin, protected QDBusContext +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.KWin.WindowsRunner") +public: + explicit WindowsRunner(); + + RemoteActions Actions() + { + return {}; + } + RemoteMatches Match(const QString &searchTerm); + void Run(const QString &id, const QString &actionId); + +private: + enum WindowsRunnerAction { + // Windows related actions + ActivateAction, + CloseAction, + MinimizeAction, + MaximizeAction, + FullscreenAction, + ShadeAction, + KeepAboveAction, + KeepBelowAction, + // Desktop related actions + ActivateDesktopAction, + }; + + RemoteMatch desktopMatch(const VirtualDesktop *desktop, const WindowsRunnerAction action = ActivateDesktopAction, qreal relevance = 1.0) const; + RemoteMatch windowsMatch(const Window *window, const WindowsRunnerAction action = ActivateAction, qreal relevance = 1.0, qreal categoryRelevance = HighestCategoryRelevance) const; + bool actionSupported(const Window *window, const WindowsRunnerAction action) const; +}; +} diff --git a/local/recipes/kde/kwin/source/src/plugins/kscreen/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/kscreen/CMakeLists.txt new file mode 100644 index 0000000000..4662f23fbf --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kscreen/CMakeLists.txt @@ -0,0 +1,16 @@ +####################################### +# Effect + +# Source files +set(kscreen_SOURCES + kscreen.cpp + main.cpp +) + +kconfig_add_kcfg_files(kscreen_SOURCES kscreenconfig.kcfgc) +kwin_add_builtin_effect(kscreen ${kscreen_SOURCES}) +target_link_libraries(kscreen PRIVATE + kwin + + KF6::ConfigGui +) diff --git a/local/recipes/kde/kwin/source/src/plugins/kscreen/kscreen.cpp b/local/recipes/kde/kwin/source/src/plugins/kscreen/kscreen.cpp new file mode 100644 index 0000000000..c1d823ac04 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kscreen/kscreen.cpp @@ -0,0 +1,168 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +// own +#include "kscreen.h" +#include "core/output.h" +#include "effect/effecthandler.h" +#include "workspace.h" +// KConfigSkeleton +#include "kscreenconfig.h" + +using namespace std::chrono_literals; + +namespace KWin +{ + +KscreenEffect::KscreenEffect() + : Effect() +{ + KscreenConfig::instance(effects->config()); + reconfigure(ReconfigureAll); + + connect(workspace(), &Workspace::dpmsStateChanged, this, &KscreenEffect::dpmsChanged); + connect(effects, &EffectsHandler::screenRemoved, this, [this](LogicalOutput *screen) { + m_states.remove(screen); + }); +} + +void KscreenEffect::dpmsChanged(std::chrono::milliseconds animationTime) +{ + switch (workspace()->dpmsState()) { + case Workspace::DpmsState::On: { + const auto screens = effects->screens(); + for (LogicalOutput *screen : screens) { + auto &state = m_states[screen]; + state.m_timeLine.setDuration(animationTime); + setState(state, StateFadingIn); + } + break; + } + case Workspace::DpmsState::AboutToTurnOff: { + const auto screens = effects->screens(); + for (LogicalOutput *screen : screens) { + auto &state = m_states[screen]; + state.m_timeLine.setDuration(animationTime); + setState(state, StateFadingOut); + } + break; + } + case Workspace::DpmsState::Off: + break; + } +} + +void KscreenEffect::reconfigure(ReconfigureFlags flags) +{ + KscreenConfig::self()->read(); +} + +void KscreenEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + if (isScreenActive(data.screen)) { + auto &state = m_states[data.screen]; + m_currentScreen = data.screen; + + if (state.m_state == StateFadingIn || state.m_state == StateFadingOut) { + state.m_timeLine.advance(presentTime); + if (state.m_timeLine.done()) { + switchState(state); + if (state.m_state == StateNormal) { + m_states.remove(data.screen); + } + } + } + } + + effects->prePaintScreen(data, presentTime); +} + +void KscreenEffect::postPaintScreen() +{ + if (isScreenActive(m_currentScreen)) { + auto &state = m_states[m_currentScreen]; + if (state.m_state == StateFadingIn || state.m_state == StateFadingOut) { + effects->addRepaintFull(); + } + } + m_currentScreen = nullptr; + effects->postPaintScreen(); +} + +void KscreenEffect::prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) +{ + auto screen = w->screen(); + if (isScreenActive(screen)) { + auto &state = m_states[screen]; + if (state.m_state != StateNormal) { + data.setTranslucent(); + } + } + effects->prePaintWindow(view, w, data, presentTime); +} + +void KscreenEffect::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) +{ + auto screen = w->screen(); + if (isScreenActive(screen)) { + auto &state = m_states[screen]; + // fade to black and fully opaque + switch (state.m_state) { + case StateFadingOut: + data.setOpacity(data.opacity() + (1.0 - data.opacity()) * state.m_timeLine.value()); + data.multiplyBrightness(1.0 - state.m_timeLine.value()); + break; + case StateFadedOut: + data.multiplyOpacity(0.0); + data.multiplyBrightness(0.0); + break; + case StateFadingIn: + data.setOpacity(data.opacity() + (1.0 - data.opacity()) * (1.0 - state.m_timeLine.value())); + data.multiplyBrightness(state.m_timeLine.value()); + break; + default: + // no adjustment + break; + } + } + effects->paintWindow(renderTarget, viewport, w, mask, deviceRegion, data); +} + +void KscreenEffect::setState(ScreenState &state, FadeOutState newState) +{ + if (state.m_state == newState) { + return; + } + + state.m_state = newState; + state.m_timeLine.reset(); + effects->addRepaintFull(); +} + +void KscreenEffect::switchState(ScreenState &state) +{ + if (state.m_state == StateFadingOut) { + state.m_state = StateFadedOut; + } else if (state.m_state == StateFadingIn) { + state.m_state = StateNormal; + } +} + +bool KscreenEffect::isActive() const +{ + return !m_states.isEmpty(); +} + +bool KscreenEffect::isScreenActive(LogicalOutput *screen) const +{ + return m_states.contains(screen); +} + +} // namespace KWin + +#include "moc_kscreen.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/kscreen/kscreen.h b/local/recipes/kde/kwin/source/src/plugins/kscreen/kscreen.h new file mode 100644 index 0000000000..b9425fc242 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kscreen/kscreen.h @@ -0,0 +1,60 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "effect/effect.h" +#include "effect/timeline.h" + +namespace KWin +{ + +class KscreenEffect : public Effect +{ + Q_OBJECT + +public: + KscreenEffect(); + + void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override; + void postPaintScreen() override; + void prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) override; + void paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) override; + + void reconfigure(ReconfigureFlags flags) override; + bool isActive() const override; + + int requestedEffectChainPosition() const override + { + return 99; + } + +private: + enum FadeOutState { + StateNormal, + StateFadingOut, + StateFadedOut, + StateFadingIn, + LastState + }; + struct ScreenState + { + TimeLine m_timeLine; + FadeOutState m_state = StateNormal; + }; + + void switchState(ScreenState &state); + void setState(ScreenState &state, FadeOutState newState); + void dpmsChanged(std::chrono::milliseconds animationTime); + bool isScreenActive(LogicalOutput *screen) const; + + QHash m_states; + LogicalOutput *m_currentScreen = nullptr; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/kscreen/kscreen.kcfg b/local/recipes/kde/kwin/source/src/plugins/kscreen/kscreen.kcfg new file mode 100644 index 0000000000..69442f11a3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kscreen/kscreen.kcfg @@ -0,0 +1,12 @@ + + + + + + 0 + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/kscreen/kscreenconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/kscreen/kscreenconfig.kcfgc new file mode 100644 index 0000000000..c2b8bbbe61 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kscreen/kscreenconfig.kcfgc @@ -0,0 +1,5 @@ +File=kscreen.kcfg +ClassName=KscreenConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/local/recipes/kde/kwin/source/src/plugins/kscreen/main.cpp b/local/recipes/kde/kwin/source/src/plugins/kscreen/main.cpp new file mode 100644 index 0000000000..1b55ba804c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kscreen/main.cpp @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "kscreen.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY(KscreenEffect, + "metadata.json.stripped") + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/kscreen/metadata.json b/local/recipes/kde/kwin/source/src/plugins/kscreen/metadata.json new file mode 100644 index 0000000000..34ad7162b0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/kscreen/metadata.json @@ -0,0 +1,106 @@ +{ + "KPlugin": { + "Category": "Appearance", + "Description": "Helper Effect for KScreen", + "Description[ar]": "تأثير مساعدة لـشاشة.ك", + "Description[az]": "KScreen üşün köməkçi effekti", + "Description[be]": "Дапаможны эфект для KScreen", + "Description[bg]": "Помощен ефект за KScreen", + "Description[ca@valencia]": "Efecte auxiliar per a KScreen", + "Description[ca]": "Efecte auxiliar per al KScreen", + "Description[cs]": "Pomocný efekt pro KScreen", + "Description[da]": "Hjælper-effekt for KSkærm", + "Description[de]": "Hilfseffekt für KScreen", + "Description[en_GB]": "Helper Effect for KScreen", + "Description[eo]": "Helpa Efekto por KScreen", + "Description[es]": "Efecto auxiliar para KScreen", + "Description[et]": "KScreeni abiefekt", + "Description[eu]": "KScreen-en efektu laguntzailea", + "Description[fi]": "Avustustehoste KScreenille", + "Description[fr]": "Effet d'assistance pour KScreen", + "Description[gl]": "Efecto auxiliar para KScreen.", + "Description[he]": "אפקט סיוע ל־KScreen", + "Description[hu]": "Segédeffektus a KScreenhez", + "Description[ia]": "Effecto de adjutante pro KScreen", + "Description[id]": "Efek Penunjang untuk KScreen", + "Description[is]": "Hjálparáhrif fyrir KScreen", + "Description[it]": "Effetto di assistenza per KScreen", + "Description[ja]": "KScreen のためのヘルパー効果", + "Description[ka]": "დამხმარე ეფექტი KScreen -ისთვის", + "Description[ko]": "KScreen 도우미 효과", + "Description[lt]": "KScreen pagelbiklio efektas", + "Description[lv]": "„KScreen“ palīga efekts", + "Description[nb]": "Hjelpeeffekt for KScreen", + "Description[nl]": "Effect van hulp voor KScreen", + "Description[nn]": "Hjelpareffekt for KScreen", + "Description[pl]": "Efekt pomocniczy dla KEkranu", + "Description[pt]": "Efeito Auxiliar do KScreen", + "Description[pt_BR]": "Efeito auxiliar do KScreen", + "Description[ro]": "Efect ajutător pentru KScreen", + "Description[ru]": "Вспомогательный эффект для KScreen", + "Description[sa]": "KScreen कृते सहायकः प्रभावः", + "Description[sk]": "Pomocný efekt pre KScreen", + "Description[sl]": "Učinek pomočnika za KScreen", + "Description[sv]": "Hjälpeffekt för Kskärm", + "Description[ta]": "கேஸ்கிரீனுக்கான உதவி அசைவூட்டம்", + "Description[tr]": "KScreen için Yardımcı Etkisi", + "Description[uk]": "Допоміжний ефект KScreen", + "Description[vi]": "Hiệu ứng trợ giúp cho KScreen", + "Description[zh_CN]": "KScreen 的辅助特效", + "Description[zh_TW]": "KScreen 輔助效果", + "EnabledByDefault": true, + "License": "GPL", + "Name": "Kscreen", + "Name[ar]": "شاشة.ك", + "Name[ast]": "Kscreen", + "Name[az]": "Kscreen", + "Name[be]": "Kscreen", + "Name[bg]": "Kscreen", + "Name[ca@valencia]": "Kscreen", + "Name[ca]": "Kscreen", + "Name[cs]": "Kscreen", + "Name[da]": "Kskærm", + "Name[de]": "KScreen", + "Name[en_GB]": "Kscreen", + "Name[eo]": "Kscreen", + "Name[es]": "Kscreen", + "Name[et]": "Kscreen", + "Name[eu]": "Kscreen", + "Name[fi]": "KScreen", + "Name[fr]": "KScreen", + "Name[ga]": "Kscreen", + "Name[gl]": "Kscreen", + "Name[he]": "Kscreen", + "Name[hu]": "Kscreen", + "Name[ia]": "Kscreen", + "Name[id]": "KScreen", + "Name[is]": "Kscreen", + "Name[it]": "Kscreen", + "Name[ja]": "Kscreen", + "Name[ka]": "Kscreen", + "Name[ko]": "Kscreen", + "Name[lt]": "Kscreen", + "Name[lv]": "Kscreen", + "Name[nb]": "KScreen", + "Name[nl]": "Kscreen", + "Name[nn]": "KScreen", + "Name[pl]": "KEkran", + "Name[pt]": "Kscreen", + "Name[pt_BR]": "KScreen", + "Name[ro]": "Kscreen", + "Name[ru]": "Kscreen", + "Name[sa]": "Kscreen", + "Name[sk]": "Kscreen", + "Name[sl]": "Kscreen", + "Name[sv]": "Kskärm", + "Name[ta]": "கேஸ்கிரீன்", + "Name[tr]": "Kscreen", + "Name[uk]": "Kscreen", + "Name[vi]": "Kscreen", + "Name[zh_CN]": "Kscreen 辅助特效", + "Name[zh_TW]": "KScreen" + }, + "org.kde.kwin.effect": { + "internal": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/login/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/login/CMakeLists.txt new file mode 100644 index 0000000000..dfb39f2d25 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/login/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_scripted_effect(login package) diff --git a/local/recipes/kde/kwin/source/src/plugins/login/package/contents/code/main.js b/local/recipes/kde/kwin/source/src/plugins/login/package/contents/code/main.js new file mode 100644 index 0000000000..e3ad0b76c0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/login/package/contents/code/main.js @@ -0,0 +1,69 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var loginEffect = { + duration: animationTime(500), + isFadeToBlack: false, + loadConfig: function () { + loginEffect.isFadeToBlack = effect.readConfig("FadeToBlack", false); + }, + isLoginSplash: function (window) { + return window.windowClass === "ksplashqml ksplashqml"; + }, + fadeOut: function (window) { + animate({ + window: window, + duration: loginEffect.duration, + type: Effect.Opacity, + from: 1.0, + to: 0.0 + }); + }, + fadeToBlack: function (window) { + animate({ + window: window, + duration: loginEffect.duration / 2, + animations: [{ + type: Effect.Brightness, + from: 1.0, + to: 0.0 + }, { + type: Effect.Opacity, + from: 1.0, + to: 0.0, + delay: loginEffect.duration / 2 + }, { + // TODO: is there a better way to keep brightness constant? + type: Effect.Brightness, + from: 0.0, + to: 0.0, + delay: loginEffect.duration / 2 + }] + }); + }, + closed: function (window) { + if (!loginEffect.isLoginSplash(window)) { + return; + } + if (loginEffect.isFadeToBlack === true) { + loginEffect.fadeToBlack(window); + } else { + loginEffect.fadeOut(window); + } + }, + init: function () { + effect.configChanged.connect(loginEffect.loadConfig); + effects.windowClosed.connect(loginEffect.closed); + loginEffect.loadConfig(); + } +}; +loginEffect.init(); diff --git a/local/recipes/kde/kwin/source/src/plugins/login/package/contents/config/main.xml b/local/recipes/kde/kwin/source/src/plugins/login/package/contents/config/main.xml new file mode 100644 index 0000000000..626a7f074e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/login/package/contents/config/main.xml @@ -0,0 +1,12 @@ + + + + + + false + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/login/package/contents/ui/config.ui b/local/recipes/kde/kwin/source/src/plugins/login/package/contents/ui/config.ui new file mode 100644 index 0000000000..07cb5f1cb8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/login/package/contents/ui/config.ui @@ -0,0 +1,38 @@ + + + KWin::LoginEffectConfigForm + + + + 0 + 0 + 400 + 160 + + + + + + + Fade to black (fullscreen splash screens only) + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/login/package/metadata.json b/local/recipes/kde/kwin/source/src/plugins/login/package/metadata.json new file mode 100644 index 0000000000..7c284ae4f4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/login/package/metadata.json @@ -0,0 +1,145 @@ +{ + "KPackageStructure": "KWin/Effect", + "KPlugin": { + "Authors": [ + { + "Email": "l.lunak@kde.org, kde@privat.broulik.de, mgraesslin@kde.org", + "Name": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[ar]": "لوبوس لوناك، كاي أووي بروتيك، مارتن جراجلين", + "Name[be]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[bg]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[ca@valencia]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[ca]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[cs]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[da]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[de]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[en_GB]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[eo]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[es]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[et]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[eu]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[fi]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[fr]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[ga]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[gl]": "Lubos Lunak, Kai Uwe Broulik e Martin Gräßlin.", + "Name[he]": "לובוש לוניאק, קאי אווה ברוליקה, מרטין גרייסלין", + "Name[hu]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[ia]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[id]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[is]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[it]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[ja]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[ka]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[ko]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[lt]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[lv]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[nb]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[nl]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[nn]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[pl]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[pt]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[pt_BR]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[ro]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[ru]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[sa]": "Lubos Lunak, Kai Uwe Broulik, मार्टिन Gräßlin", + "Name[sk]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[sl]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[sv]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[ta]": "லூபோசு லுனாக்கு, காய் ஊவே புரோலிக்கு, மார்ட்டின் கிராஸ்லின்", + "Name[tr]": "Luboš Luňák, Kai Uwe Broulik, Martin Gräßlin", + "Name[uk]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[vi]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[zh_CN]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin", + "Name[zh_TW]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin" + } + ], + "Category": "Appearance", + "Description": "Fade to the desktop when logging in", + "Description[ar]": "التلاشي إلى سطح المكتب عند الولوج", + "Description[bg]": "Плавен преход към работния плот при влизане", + "Description[ca@valencia]": "Esvaïment a l'escriptori en connectar-se", + "Description[ca]": "Esvaïment a l'escriptori en connectar-se", + "Description[de]": "Die Arbeitsfläche nach der Anmeldung einblenden", + "Description[es]": "Desvanecer al mostrar el escritorio al iniciar sesión", + "Description[eu]": "Moteldu mahaigainerantz saioa hastean", + "Description[fi]": "Häivytä työpöydälle kirjauduttaessa", + "Description[fr]": "Effectuer un effet de fondu du bureau lors de la connexion", + "Description[he]": "לעמעם לתוך שולחן העבודה עם הכניסה למערכת", + "Description[hu]": "Áttűnés az asztalra bejelentkezéskor", + "Description[ia]": "Pallidi al scriptorio quando accedente in identification", + "Description[is]": "Láta skjáborð birtast mjúklega við innskráningu", + "Description[it]": "Dissolvenza sul desktop all'accesso", + "Description[ja]": "ログイン時にデスクトップをフェード", + "Description[ka]": "შესვლისას სამუშაო მაგიდაზე რბილი გადასვლა", + "Description[ko]": "로그인할 때 바탕 화면으로 페이드합니다", + "Description[lt]": "Prisijungiant prie paskyros laipsniškai pereiti į darbalaukį", + "Description[lv]": "Izgaist uz darbvirsmu, kad ierakstāties", + "Description[nl]": "Vervagen naar het opkomende bureaublad tijdens bij aanmelden", + "Description[nn]": "Ton inn skrivebordet ved innlogging", + "Description[pl]": "Rozjaśnianie do pulpitu podczas logowania", + "Description[pt_BR]": "Transição suave para a área de trabalho ao iniciar a sessão", + "Description[ro]": "Estompează spre birou la autentificare", + "Description[ru]": "Постепенное появление рабочего стола при входе в систему", + "Description[sk]": "Plynulé prelínanie na plochu pri prihlasovaní", + "Description[sl]": "Obledi na namizju ob prijavi", + "Description[sv]": "Tona till skrivbordet vid inloggning", + "Description[tr]": "Oturum açarken masaüstüne solarak geçiş yap", + "Description[uk]": "Висвітлення стільниці під час входу", + "Description[zh_CN]": "登录时平滑地过渡到桌面", + "Description[zh_TW]": "登入時淡入桌面", + "EnabledByDefault": true, + "Icon": "preferences-system-windows-effect-login", + "Id": "login", + "License": "GPL", + "Name": "Login", + "Name[ar]": "الولوج", + "Name[be]": "Увайсці", + "Name[bg]": "Вход", + "Name[ca@valencia]": "Inici de sessió", + "Name[ca]": "Inici de sessió", + "Name[cs]": "Přihlášení", + "Name[da]": "Log ind", + "Name[de]": "Anmeldung", + "Name[en_GB]": "Login", + "Name[eo]": "Ensaluti", + "Name[es]": "Inicio de sesión", + "Name[et]": "Sisselogimine", + "Name[eu]": "Saio-hastea", + "Name[fi]": "Kirjaudu", + "Name[fr]": "Se connecter", + "Name[gl]": "Acceso", + "Name[he]": "כניסה", + "Name[hu]": "Bejelentkezés", + "Name[ia]": "Accesso de identification", + "Name[id]": "Login", + "Name[is]": "Innskráning", + "Name[it]": "Accedi", + "Name[ja]": "ログイン", + "Name[ka]": "შესვლა", + "Name[ko]": "로그인", + "Name[lt]": "Prisijungimas", + "Name[lv]": "Ierakstīšanās", + "Name[nb]": "Innlogging", + "Name[nl]": "Aanmelden", + "Name[nn]": "Innlogging", + "Name[pl]": "Logowanie", + "Name[pt]": "Arranque", + "Name[pt_BR]": "Início de sessão", + "Name[ro]": "Autentificare", + "Name[ru]": "Вход в систему", + "Name[sa]": "प्रवेशः", + "Name[sk]": "Prihlásenie", + "Name[sl]": "Prijava", + "Name[sv]": "Inloggning", + "Name[ta]": "நுழைவு", + "Name[tr]": "Oturumu Aç", + "Name[uk]": "Вхід", + "Name[vi]": "Đăng nhập", + "Name[zh_CN]": "登录渐变动画", + "Name[zh_TW]": "登入" + }, + "X-KDE-ConfigModule": "kcm_kwin4_genericscripted", + "X-KDE-Ordering": 40, + "X-KWin-Config-TranslationDomain": "kwin", + "X-Plasma-API": "javascript" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/logout/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/logout/CMakeLists.txt new file mode 100644 index 0000000000..cedb238bd1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/logout/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_scripted_effect(logout package) diff --git a/local/recipes/kde/kwin/source/src/plugins/logout/package/contents/code/main.js b/local/recipes/kde/kwin/source/src/plugins/logout/package/contents/code/main.js new file mode 100644 index 0000000000..9dc5b37034 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/logout/package/contents/code/main.js @@ -0,0 +1,61 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2013 Martin Gräßlin + SPDX-FileCopyrightText: 2017 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var logoutEffect = { + isLogoutWindow: function (window) { + if (window.windowClass === "ksmserver-logout-greeter ksmserver-logout-greeter") { + return true; + } + return false; + }, + opened: function (window) { + if (!logoutEffect.isLogoutWindow(window)) { + return; + } + // If the Out animation is still active, kill it. + if (window.outAnimation !== undefined) { + cancel(window.outAnimation); + delete window.outAnimation; + } + window.inAnimation = animate({ + window: window, + duration: animationTime(800), + type: Effect.Opacity, + from: 0.0, + to: 1.0 + }); + }, + closed: function (window) { + if (!logoutEffect.isLogoutWindow(window)) { + return; + } + // If the In animation is still active, kill it. + if (window.inAnimation !== undefined) { + cancel(window.inAnimation); + delete window.inAnimation; + } + window.outAnimation = animate({ + window: window, + duration: animationTime(400), + type: Effect.Opacity, + from: 1.0, + to: 0.0 + }); + }, + init: function () { + effects.windowAdded.connect(logoutEffect.opened); + effects.windowClosed.connect(logoutEffect.closed); + } +}; +logoutEffect.init(); + diff --git a/local/recipes/kde/kwin/source/src/plugins/logout/package/metadata.json b/local/recipes/kde/kwin/source/src/plugins/logout/package/metadata.json new file mode 100644 index 0000000000..0c03da39b2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/logout/package/metadata.json @@ -0,0 +1,144 @@ +{ + "KPackageStructure": "KWin/Effect", + "KPlugin": { + "Authors": [ + { + "Email": "l.lunak@kde.org, kde@privat.broulik.de, mgraesslin@kde.org, mart@kde.org", + "Name": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[ar]": "لوبوس لوناك، كاي أووي بروتيك، مارتن جراجلين، ماركو مارتن", + "Name[be]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[bg]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[ca@valencia]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[ca]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[cs]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[da]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[de]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[en_GB]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[eo]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[es]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[et]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[eu]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[fi]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[fr]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[ga]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[gl]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin e Marco Martin.", + "Name[he]": "לובוש לוניאק, קאי אווה ברוליקה, מרטין גרייסלין, מרקו מרטין", + "Name[hu]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[ia]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[id]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[is]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[it]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[ja]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[ka]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[ko]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[lt]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[lv]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[nb]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[nl]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[nn]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[pl]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[pt]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[pt_BR]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[ro]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[ru]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[sa]": "Lubos Lunak, Kai Uwe Broulik, मार्टिन Gräßlin, मार्को मार्टिन", + "Name[sk]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[sl]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[sv]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[ta]": "லூபோசு லுனாக்கு, காய் ஊவே புரோலிக்கு, மார்ட்டின் கிராஸ்லின், மார்க்கோ மார்ட்டின்", + "Name[tr]": "Luboš Luňák, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[uk]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[vi]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[zh_CN]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin", + "Name[zh_TW]": "Lubos Lunak, Kai Uwe Broulik, Martin Gräßlin, Marco Martin" + } + ], + "Category": "Appearance", + "Description": "Fade to the logout screen", + "Description[ar]": "تلاشي إلى شاشة الخروج", + "Description[bg]": "Плавен преход към екрана за излизане в системата", + "Description[ca@valencia]": "Esvaïment a la pantalla d'eixida", + "Description[ca]": "Esvaïment a la pantalla de sortida", + "Description[de]": "Den Abmeldebildschrim einblenden", + "Description[es]": "Desvanecer al mostrar la pantalla de cierre de sesión", + "Description[eu]": "Moteldu saioa ixteko pantailarantz", + "Description[fi]": "Häivytä uloskirjautumisnäytölle", + "Description[fr]": "Effectuer un effet de fondu vers l'écran de déconnexion", + "Description[he]": "עמעום למסך היציאה", + "Description[hu]": "Áttűnés a kijelentkező képernyőre", + "Description[ia]": "Pallidi al schermo de abandono (logout)", + "Description[is]": "Láta útskráningarskjá birtast mjúklega", + "Description[it]": "Dissolvenza alla schermata di chiusura della sessione", + "Description[ja]": "ログアウト画面にフェード", + "Description[ka]": "მინავლება გასვლის ეკრანამდე", + "Description[ko]": "로그아웃 화면으로 페이드합니다", + "Description[lt]": "Laipsniškai pereiti į atsijungimo langą", + "Description[lv]": "Izgaist uz izrakstīšanās ekrānu", + "Description[nl]": "Naar het afmeldscherm vervagen", + "Description[nn]": "Ton ut til utloggingsbiletet", + "Description[pl]": "Zanikanie do ekranu wylogowywania", + "Description[pt_BR]": "Esmaecer para a tela de encerramento da sessão", + "Description[ro]": "Estompează spre ecranul de ieșire", + "Description[ru]": "Постепенное появление экрана завершения работы", + "Description[sk]": "Plynulé prelínanie na obrazovku odhlásenia", + "Description[sl]": "Obledi na odjavni zaslon", + "Description[sv]": "Tona till utloggningsskärmen", + "Description[tr]": "Oturumu kapatırken masaüstünden solarak çık", + "Description[uk]": "Затемнення до вікна виходу з системи", + "Description[zh_CN]": "注销时平滑地过渡到注销屏幕", + "Description[zh_TW]": "淡入至登出畫面", + "EnabledByDefault": true, + "Icon": "preferences-system-windows-effect-logout", + "Id": "logout", + "License": "GPL", + "Name": "Logout", + "Name[ar]": "الخروج", + "Name[be]": "Выйсці", + "Name[bg]": "Излизане от сесия", + "Name[ca@valencia]": "Eixida", + "Name[ca]": "Sortida", + "Name[cs]": "Odhlášení", + "Name[da]": "Log ud", + "Name[de]": "Abmeldung", + "Name[en_GB]": "Logout", + "Name[eo]": "Elsaluti", + "Name[es]": "Cierre de sesión", + "Name[et]": "Väljalogimine", + "Name[eu]": "Saio-ixtea", + "Name[fi]": "Kirjaudu ulos", + "Name[fr]": "Se déconnecter", + "Name[ga]": "Logáil amach", + "Name[gl]": "Saída", + "Name[he]": "יציאה", + "Name[hu]": "Kijelentkezés", + "Name[ia]": "Claude session", + "Name[id]": "Logout", + "Name[is]": "Útskráning", + "Name[it]": "Chiudi sessione", + "Name[ja]": "ログアウト", + "Name[ka]": "გასვლა", + "Name[ko]": "로그아웃", + "Name[lt]": "Atsijungimas", + "Name[lv]": "Izrakstīšanās", + "Name[nb]": "Utlogging", + "Name[nl]": "Afmelden", + "Name[nn]": "Utlogging", + "Name[pl]": "Wylogowywanie", + "Name[pt]": "Encerrar", + "Name[pt_BR]": "Encerrar sessão", + "Name[ro]": "Ieșire din sistem", + "Name[ru]": "Завершение сеанса", + "Name[sa]": "लॉगआउट", + "Name[sk]": "Odhlásenie", + "Name[sl]": "Odjava", + "Name[sv]": "Utloggning", + "Name[ta]": "வெளியேற்றம்", + "Name[tr]": "Oturumu Kapat", + "Name[uk]": "Вихід", + "Name[vi]": "Đăng xuất", + "Name[zh_CN]": "注销过渡动画", + "Name[zh_TW]": "登出" + }, + "X-KDE-Ordering": 40, + "X-Plasma-API": "javascript" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/magiclamp/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/magiclamp/CMakeLists.txt new file mode 100644 index 0000000000..1846444c9f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/magiclamp/CMakeLists.txt @@ -0,0 +1,36 @@ +####################################### +# Effect + +set(magiclamp_SOURCES + magiclamp.cpp + main.cpp +) + +kconfig_add_kcfg_files(magiclamp_SOURCES + magiclampconfig.kcfgc +) + +kwin_add_builtin_effect(magiclamp ${magiclamp_SOURCES}) +target_link_libraries(magiclamp PRIVATE + kwin + + KF6::ConfigGui +) + +####################################### +# Config +if (KWIN_BUILD_KCMS) + set(kwin_magiclamp_config_SRCS magiclamp_config.cpp) + ki18n_wrap_ui(kwin_magiclamp_config_SRCS magiclamp_config.ui) + kconfig_add_kcfg_files(kwin_magiclamp_config_SRCS magiclampconfig.kcfgc) + + kwin_add_effect_config(kwin_magiclamp_config ${kwin_magiclamp_config_SRCS}) + + target_link_libraries(kwin_magiclamp_config + KF6::KCMUtils + KF6::CoreAddons + KF6::I18n + Qt::DBus + KWinEffectsInterface + ) +endif() diff --git a/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp.cpp b/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp.cpp new file mode 100644 index 0000000000..6f1ccaf91a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp.cpp @@ -0,0 +1,440 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +// based on minimize animation by Rivo Laks + +#include "magiclamp.h" +#include "effect/effecthandler.h" +// KConfigSkeleton +#include "magiclampconfig.h" + +using namespace std::chrono_literals; + +namespace KWin +{ + +MagicLampEffect::MagicLampEffect() +{ + MagicLampConfig::instance(effects->config()); + reconfigure(ReconfigureAll); + connect(effects, &EffectsHandler::windowAdded, this, &MagicLampEffect::slotWindowAdded); + connect(effects, &EffectsHandler::windowDeleted, this, &MagicLampEffect::slotWindowDeleted); + + const auto windows = effects->stackingOrder(); + for (EffectWindow *window : windows) { + slotWindowAdded(window); + } + + setVertexSnappingMode(RenderGeometry::VertexSnappingMode::None); +} + +bool MagicLampEffect::supported() +{ + return OffscreenEffect::supported() && effects->animationsSupported(); +} + +void MagicLampEffect::reconfigure(ReconfigureFlags) +{ + MagicLampConfig::self()->read(); + + // TODO: Rename animationDuration to duration so we can use + // animationTime(250). + const std::chrono::milliseconds d = MagicLampConfig::animationDuration() != 0 + ? std::chrono::milliseconds(MagicLampConfig::animationDuration()) + : 250ms; + m_duration = std::chrono::milliseconds(static_cast(animationTime(d))); +} + +void MagicLampEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + // We need to mark the screen windows as transformed. Otherwise the + // whole screen won't be repainted, resulting in artefacts. + data.mask |= PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS; + + effects->prePaintScreen(data, presentTime); +} + +void MagicLampEffect::prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) +{ + // Schedule window for transformation if the animation is still in + // progress + auto animationIt = m_animations.find(w); + if (animationIt != m_animations.end()) { + (*animationIt).timeLine.advance(presentTime); + data.setTransformed(); + } + + effects->prePaintWindow(view, w, data, presentTime); +} + +void MagicLampEffect::apply(EffectWindow *w, int mask, WindowPaintData &data, WindowQuadList &quads) +{ + auto animationIt = m_animations.constFind(w); + if (animationIt != m_animations.constEnd()) { + // 0 = not minimized, 1 = fully minimized + const qreal progress = (*animationIt).timeLine.value(); + + QRect geo = w->frameGeometry().toRect(); + QRect icon = w->iconGeometry().toRect(); + IconPosition position = Top; + // If there's no icon geometry, minimize to the center of the screen + if (!icon.isValid()) { + QRect extG = geo; + QPoint pt = cursorPos().toPoint(); + // focussing inside the window is no good, leads to ugly artefacts, find nearest border + if (extG.contains(pt)) { + const int d[2][2] = {{pt.x() - extG.x(), extG.right() - pt.x()}, + {pt.y() - extG.y(), extG.bottom() - pt.y()}}; + int di = d[1][0]; + position = Top; + if (d[0][0] < di) { + di = d[0][0]; + position = Left; + } + if (d[1][1] < di) { + di = d[1][1]; + position = Bottom; + } + if (d[0][1] < di) { + position = Right; + } + switch (position) { + case Top: + pt.setY(extG.y()); + break; + case Left: + pt.setX(extG.x()); + break; + case Bottom: + pt.setY(extG.bottom()); + break; + case Right: + pt.setX(extG.right()); + break; + } + } else { + if (pt.y() < geo.y()) { + position = Top; + } else if (pt.x() < geo.x()) { + position = Left; + } else if (pt.y() > geo.bottom()) { + position = Bottom; + } else if (pt.x() > geo.right()) { + position = Right; + } + } + icon = QRect(pt, QSize(0, 0)); + } else { + // Assumption: there is a panel containing the icon position + EffectWindow *panel = nullptr; + const auto stackingOrder = effects->stackingOrder(); + for (EffectWindow *window : stackingOrder) { + if (!window->isDock()) { + continue; + } + // we have to use intersects as there seems to be a Plasma bug + // the published icon geometry might be bigger than the panel + if (window->frameGeometry().intersects(icon)) { + panel = window; + break; + } + } + if (panel) { + // Assumption: width of horizontal panel is greater than its height and vice versa + const QRectF windowScreen = effects->clientArea(ScreenArea, w); + + if (panel->width() >= panel->height()) { + // horizontal panel + if (icon.center().y() <= windowScreen.center().y()) { + position = Top; + } else { + position = Bottom; + } + } else { + // vertical panel + if (icon.center().x() <= windowScreen.center().x()) { + position = Left; + } else { + position = Right; + } + } + + // If the panel is hidden, move the icon offscreen so the animation looks correct. + if (panel->isHidden()) { + const QRectF panelScreen = effects->clientArea(ScreenArea, panel); + switch (position) { + case Bottom: + icon.moveTop(panelScreen.y() + panelScreen.height()); + break; + case Top: + icon.moveTop(panelScreen.y() - icon.height()); + break; + case Left: + icon.moveLeft(panelScreen.x() - icon.width()); + break; + case Right: + icon.moveLeft(panelScreen.x() + panelScreen.width()); + break; + } + } + } else { + // we did not find a panel, so it might be autohidden + QRectF iconScreen = effects->clientArea(ScreenArea, icon.topLeft(), effects->currentDesktop()); + // as the icon geometry could be overlap a screen edge we use an intersection + QRectF rect = iconScreen.intersected(icon); + // here we need a different assumption: icon geometry borders one screen edge + // this assumption might be wrong for e.g. task applet being the only applet in panel + // in this case the icon borders two screen edges + // there might be a wrong animation, but not distorted + if (rect.x() == iconScreen.x()) { + position = Left; + } else if (rect.x() + rect.width() == iconScreen.x() + iconScreen.width()) { + position = Right; + } else if (rect.y() == iconScreen.y()) { + position = Top; + } else { + position = Bottom; + } + } + } + + quads = quads.makeGrid(40); + float quadFactor; // defines how fast a quad is vertically moved: y coordinates near to window top are slowed down + // it is used as quadFactor^3/windowHeight^3 + // quadFactor is the y position of the quad but is changed towards becoming the window height + // by that the factor becomes 1 and has no influence any more + float offset[2] = {0, 0}; // how far has a quad to be moved? Distance between icon and window multiplied by the progress and by the quadFactor + float p_progress[2] = {0, 0}; // the factor which defines how far the x values have to be changed + // factor is the current moved y value diveded by the distance between icon and window + WindowQuad lastQuad; + lastQuad[0].setX(-1); + lastQuad[0].setY(-1); + lastQuad[1].setX(-1); + lastQuad[1].setY(-1); + lastQuad[2].setX(-1); + lastQuad[2].setY(-1); + + if (position == Bottom) { + const float height_cube = float(geo.height()) * float(geo.height()) * float(geo.height()); + const float maxY = icon.y() - geo.y(); + + for (WindowQuad &quad : quads) { + + if (quad[0].y() != lastQuad[0].y() || quad[2].y() != lastQuad[2].y()) { + quadFactor = quad[0].y() + (geo.height() - quad[0].y()) * progress; + offset[0] = (icon.y() + quad[0].y() - geo.y()) * progress * ((quadFactor * quadFactor * quadFactor) / height_cube); + quadFactor = quad[2].y() + (geo.height() - quad[2].y()) * progress; + offset[1] = (icon.y() + quad[2].y() - geo.y()) * progress * ((quadFactor * quadFactor * quadFactor) / height_cube); + p_progress[1] = std::min(offset[1] / (icon.y() - geo.y() - float(quad[2].y())), 1.0f); + p_progress[0] = std::min(offset[0] / (icon.y() - geo.y() - float(quad[0].y())), 1.0f); + } else { + lastQuad = quad; + } + + p_progress[0] = std::abs(p_progress[0]); + p_progress[1] = std::abs(p_progress[1]); + + // x values are moved towards the center of the icon + quad[0].setX((icon.x() + icon.width() * (quad[0].x() / geo.width()) - (quad[0].x() + geo.x())) * p_progress[0] + quad[0].x()); + quad[1].setX((icon.x() + icon.width() * (quad[1].x() / geo.width()) - (quad[1].x() + geo.x())) * p_progress[0] + quad[1].x()); + quad[2].setX((icon.x() + icon.width() * (quad[2].x() / geo.width()) - (quad[2].x() + geo.x())) * p_progress[1] + quad[2].x()); + quad[3].setX((icon.x() + icon.width() * (quad[3].x() / geo.width()) - (quad[3].x() + geo.x())) * p_progress[1] + quad[3].x()); + + quad[0].setY(std::min(maxY, float(quad[0].y()) + offset[0])); + quad[1].setY(std::min(maxY, float(quad[1].y()) + offset[0])); + quad[2].setY(std::min(maxY, float(quad[2].y()) + offset[1])); + quad[3].setY(std::min(maxY, float(quad[3].y()) + offset[1])); + } + } else if (position == Top) { + const float height_cube = float(geo.height()) * float(geo.height()) * float(geo.height()); + const float minY = icon.y() + icon.height() - geo.y(); + + for (WindowQuad &quad : quads) { + + if (quad[0].y() != lastQuad[0].y() || quad[2].y() != lastQuad[2].y()) { + quadFactor = geo.height() - quad[0].y() + (quad[0].y()) * progress; + offset[0] = (geo.y() - icon.height() + geo.height() + quad[0].y() - icon.y()) * progress * ((quadFactor * quadFactor * quadFactor) / height_cube); + quadFactor = geo.height() - quad[2].y() + (quad[2].y()) * progress; + offset[1] = (geo.y() - icon.height() + geo.height() + quad[2].y() - icon.y()) * progress * ((quadFactor * quadFactor * quadFactor) / height_cube); + p_progress[0] = std::min(offset[0] / (geo.y() - icon.height() + geo.height() - icon.y() - float(geo.height() - quad[0].y())), 1.0f); + p_progress[1] = std::min(offset[1] / (geo.y() - icon.height() + geo.height() - icon.y() - float(geo.height() - quad[2].y())), 1.0f); + } else { + lastQuad = quad; + } + + offset[0] = -offset[0]; + offset[1] = -offset[1]; + + p_progress[0] = std::abs(p_progress[0]); + p_progress[1] = std::abs(p_progress[1]); + + // x values are moved towards the center of the icon + quad[0].setX((icon.x() + icon.width() * (quad[0].x() / geo.width()) - (quad[0].x() + geo.x())) * p_progress[0] + quad[0].x()); + quad[1].setX((icon.x() + icon.width() * (quad[1].x() / geo.width()) - (quad[1].x() + geo.x())) * p_progress[0] + quad[1].x()); + quad[2].setX((icon.x() + icon.width() * (quad[2].x() / geo.width()) - (quad[2].x() + geo.x())) * p_progress[1] + quad[2].x()); + quad[3].setX((icon.x() + icon.width() * (quad[3].x() / geo.width()) - (quad[3].x() + geo.x())) * p_progress[1] + quad[3].x()); + + quad[0].setY(std::max(minY, float(quad[0].y()) + offset[0])); + quad[1].setY(std::max(minY, float(quad[1].y()) + offset[0])); + quad[2].setY(std::max(minY, float(quad[2].y()) + offset[1])); + quad[3].setY(std::max(minY, float(quad[3].y()) + offset[1])); + } + } else if (position == Left) { + const float width_cube = float(geo.width()) * float(geo.width()) * float(geo.width()); + const float minX = icon.x() + icon.width() - geo.x(); + + for (WindowQuad &quad : quads) { + + if (quad[0].x() != lastQuad[0].x() || quad[1].x() != lastQuad[1].x()) { + quadFactor = geo.width() - quad[0].x() + (quad[0].x()) * progress; + offset[0] = (geo.x() - icon.width() + geo.width() + quad[0].x() - icon.x()) * progress * ((quadFactor * quadFactor * quadFactor) / width_cube); + quadFactor = geo.width() - quad[1].x() + (quad[1].x()) * progress; + offset[1] = (geo.x() - icon.width() + geo.width() + quad[1].x() - icon.x()) * progress * ((quadFactor * quadFactor * quadFactor) / width_cube); + p_progress[0] = std::min(offset[0] / (geo.x() - icon.width() + geo.width() - icon.x() - float(geo.width() - quad[0].x())), 1.0f); + p_progress[1] = std::min(offset[1] / (geo.x() - icon.width() + geo.width() - icon.x() - float(geo.width() - quad[1].x())), 1.0f); + } else { + lastQuad = quad; + } + + offset[0] = -offset[0]; + offset[1] = -offset[1]; + + p_progress[0] = std::abs(p_progress[0]); + p_progress[1] = std::abs(p_progress[1]); + + // y values are moved towards the center of the icon + quad[0].setY((icon.y() + icon.height() * (quad[0].y() / geo.height()) - (quad[0].y() + geo.y())) * p_progress[0] + quad[0].y()); + quad[1].setY((icon.y() + icon.height() * (quad[1].y() / geo.height()) - (quad[1].y() + geo.y())) * p_progress[1] + quad[1].y()); + quad[2].setY((icon.y() + icon.height() * (quad[2].y() / geo.height()) - (quad[2].y() + geo.y())) * p_progress[1] + quad[2].y()); + quad[3].setY((icon.y() + icon.height() * (quad[3].y() / geo.height()) - (quad[3].y() + geo.y())) * p_progress[0] + quad[3].y()); + + quad[0].setX(std::max(minX, float(quad[0].x()) + offset[0])); + quad[1].setX(std::max(minX, float(quad[1].x()) + offset[1])); + quad[2].setX(std::max(minX, float(quad[2].x()) + offset[1])); + quad[3].setX(std::max(minX, float(quad[3].x()) + offset[0])); + } + } else if (position == Right) { + const float width_cube = float(geo.width()) * float(geo.width()) * float(geo.width()); + const float maxX = icon.x() - geo.x(); + + for (WindowQuad &quad : quads) { + + if (quad[0].x() != lastQuad[0].x() || quad[1].x() != lastQuad[1].x()) { + quadFactor = quad[0].x() + (geo.width() - quad[0].x()) * progress; + offset[0] = (icon.x() + quad[0].x() - geo.x()) * progress * ((quadFactor * quadFactor * quadFactor) / width_cube); + quadFactor = quad[1].x() + (geo.width() - quad[1].x()) * progress; + offset[1] = (icon.x() + quad[1].x() - geo.x()) * progress * ((quadFactor * quadFactor * quadFactor) / width_cube); + p_progress[0] = std::min(offset[0] / (icon.x() - geo.x() - float(quad[0].x())), 1.0f); + p_progress[1] = std::min(offset[1] / (icon.x() - geo.x() - float(quad[1].x())), 1.0f); + } else { + lastQuad = quad; + } + + p_progress[0] = std::abs(p_progress[0]); + p_progress[1] = std::abs(p_progress[1]); + + // y values are moved towards the center of the icon + quad[0].setY((icon.y() + icon.height() * (quad[0].y() / geo.height()) - (quad[0].y() + geo.y())) * p_progress[0] + quad[0].y()); + quad[1].setY((icon.y() + icon.height() * (quad[1].y() / geo.height()) - (quad[1].y() + geo.y())) * p_progress[1] + quad[1].y()); + quad[2].setY((icon.y() + icon.height() * (quad[2].y() / geo.height()) - (quad[2].y() + geo.y())) * p_progress[1] + quad[2].y()); + quad[3].setY((icon.y() + icon.height() * (quad[3].y() / geo.height()) - (quad[3].y() + geo.y())) * p_progress[0] + quad[3].y()); + + quad[0].setX(std::min(maxX, float(quad[0].x()) + offset[0])); + quad[1].setX(std::min(maxX, float(quad[1].x()) + offset[1])); + quad[2].setX(std::min(maxX, float(quad[2].x()) + offset[1])); + quad[3].setX(std::min(maxX, float(quad[3].x()) + offset[0])); + } + } + } +} + +void MagicLampEffect::postPaintScreen() +{ + auto animationIt = m_animations.begin(); + while (animationIt != m_animations.end()) { + if ((*animationIt).timeLine.done()) { + unredirect(animationIt.key()); + animationIt = m_animations.erase(animationIt); + } else { + ++animationIt; + } + } + + effects->addRepaintFull(); + + // Call the next effect. + effects->postPaintScreen(); +} + +void MagicLampEffect::slotWindowAdded(EffectWindow *w) +{ + connect(w, &EffectWindow::minimizedChanged, this, [this, w]() { + if (w->isMinimized()) { + slotWindowMinimized(w); + } else { + slotWindowUnminimized(w); + } + }); +} + +void MagicLampEffect::slotWindowDeleted(EffectWindow *w) +{ + m_animations.remove(w); +} + +void MagicLampEffect::slotWindowMinimized(EffectWindow *w) +{ + if (effects->activeFullScreenEffect()) { + return; + } + + MagicLampAnimation &animation = m_animations[w]; + + if (animation.timeLine.running()) { + animation.timeLine.toggleDirection(); + } else { + animation.visibleRef = EffectWindowVisibleRef(w, EffectWindow::PAINT_DISABLED_BY_MINIMIZE); + animation.timeLine.setDirection(TimeLine::Forward); + animation.timeLine.setDuration(m_duration); + animation.timeLine.setEasingCurve(QEasingCurve::Linear); + } + + redirect(w); + effects->addRepaintFull(); +} + +void MagicLampEffect::slotWindowUnminimized(EffectWindow *w) +{ + if (effects->activeFullScreenEffect()) { + return; + } + + MagicLampAnimation &animation = m_animations[w]; + + if (animation.timeLine.running()) { + animation.timeLine.toggleDirection(); + } else { + animation.visibleRef = EffectWindowVisibleRef(w, EffectWindow::PAINT_DISABLED_BY_MINIMIZE); + animation.timeLine.setDirection(TimeLine::Backward); + animation.timeLine.setDuration(m_duration); + animation.timeLine.setEasingCurve(QEasingCurve::Linear); + } + + redirect(w); + effects->addRepaintFull(); +} + +bool MagicLampEffect::isActive() const +{ + return !m_animations.isEmpty(); +} + +} // namespace + +#include "moc_magiclamp.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp.h b/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp.h new file mode 100644 index 0000000000..38136e29af --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp.h @@ -0,0 +1,66 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/effectwindow.h" +#include "effect/offscreeneffect.h" +#include "effect/timeline.h" + +namespace KWin +{ + +struct MagicLampAnimation +{ + EffectWindowVisibleRef visibleRef; + TimeLine timeLine; +}; + +class MagicLampEffect : public OffscreenEffect +{ + Q_OBJECT + +public: + MagicLampEffect(); + + void reconfigure(ReconfigureFlags) override; + void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override; + void prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) override; + void postPaintScreen() override; + bool isActive() const override; + + int requestedEffectChainPosition() const override + { + return 50; + } + + static bool supported(); + +protected: + void apply(EffectWindow *window, int mask, WindowPaintData &data, WindowQuadList &quads) override; + +public Q_SLOTS: + void slotWindowAdded(KWin::EffectWindow *w); + void slotWindowDeleted(KWin::EffectWindow *w); + void slotWindowMinimized(KWin::EffectWindow *w); + void slotWindowUnminimized(KWin::EffectWindow *w); + +private: + std::chrono::milliseconds m_duration; + QHash m_animations; + + enum IconPosition { + Top, + Bottom, + Left, + Right + }; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp.kcfg b/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp.kcfg new file mode 100644 index 0000000000..6ae7a5af73 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp.kcfg @@ -0,0 +1,12 @@ + + + + + + 0 + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp_config.cpp b/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp_config.cpp new file mode 100644 index 0000000000..1704bc2f33 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp_config.cpp @@ -0,0 +1,50 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "magiclamp_config.h" + +#include "config-kwin.h" + +// KConfigSkeleton +#include "magiclampconfig.h" + +#include + +#include +#include + +#include + +K_PLUGIN_CLASS(KWin::MagicLampEffectConfig) + +namespace KWin +{ + +MagicLampEffectConfig::MagicLampEffectConfig(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) +{ + m_ui.setupUi(widget()); + + MagicLampConfig::instance(KWIN_CONFIG); + addConfig(MagicLampConfig::self(), widget()); +} + +void MagicLampEffectConfig::save() +{ + KCModule::save(); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("magiclamp")); +} + +} // namespace + +#include "magiclamp_config.moc" + +#include "moc_magiclamp_config.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp_config.h b/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp_config.h new file mode 100644 index 0000000000..4c0f1c49b3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp_config.h @@ -0,0 +1,31 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "ui_magiclamp_config.h" + +namespace KWin +{ +class MagicLampEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit MagicLampEffectConfig(QObject *parent, const KPluginMetaData &data); + +public Q_SLOTS: + void save() override; + +private: + Ui::MagicLampEffectConfigForm m_ui; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp_config.ui b/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp_config.ui new file mode 100644 index 0000000000..838dcb1faa --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclamp_config.ui @@ -0,0 +1,53 @@ + + + KWin::MagicLampEffectConfigForm + + + + 0 + 0 + 400 + 300 + + + + + + + Animation duration: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_AnimationDuration + + + + + + + + 0 + 0 + + + + Default + + + milliseconds + + + 5000 + + + 10 + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclampconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclampconfig.kcfgc new file mode 100644 index 0000000000..fcea95d05c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/magiclamp/magiclampconfig.kcfgc @@ -0,0 +1,5 @@ +File=magiclamp.kcfg +ClassName=MagicLampConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/local/recipes/kde/kwin/source/src/plugins/magiclamp/main.cpp b/local/recipes/kde/kwin/source/src/plugins/magiclamp/main.cpp new file mode 100644 index 0000000000..67c2a3c30f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/magiclamp/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "magiclamp.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(MagicLampEffect, + "metadata.json.stripped", + return MagicLampEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/magiclamp/metadata.json b/local/recipes/kde/kwin/source/src/plugins/magiclamp/metadata.json new file mode 100644 index 0000000000..8e4affa04e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/magiclamp/metadata.json @@ -0,0 +1,105 @@ +{ + "KPlugin": { + "Category": "Appearance", + "Description": "Simulate a magic lamp when minimizing windows", + "Description[ar]": "حاكِ مصباحًا سحريًّا عند تصغير النوافذ", + "Description[az]": "Pəncərələri sehirli lampa effekti ilə yığır", + "Description[be]": "Сімуляцыя чароўнай лямпы пры згортванні акон", + "Description[bg]": "Анимиране като поява от магическа лампа при минимизиране на прозорците", + "Description[ca@valencia]": "Simula una làmpada màgica en minimitzar les finestres", + "Description[ca]": "Simula una làmpada màgica en minimitzar les finestres", + "Description[cs]": "Simuluje magickou lampu při minimalizaci oken", + "Description[da]": "Simulér en magisk lampe når vinduer minimeres", + "Description[de]": "Simuliert zum Minimieren von Fenstern den Effekt einer Wunderlampe", + "Description[en_GB]": "Simulate a magic lamp when minimising windows", + "Description[eo]": "Simuli magian lampon dum minimumigo de fenestroj", + "Description[es]": "Simular una lámpara mágica al minimizar las ventanas", + "Description[et]": "Imelambi simuleerimine akende minimeerimisel", + "Description[eu]": "Lanpara magikoarena egin leihoak ikonotzean", + "Description[fi]": "Jäljittelee taikalamppua ikkunoita pienennettäessä", + "Description[fr]": "Simuler une lampe magique lors de la réduction des fenêtres", + "Description[gl]": "Simula unha lámpada máxica ao minimizar as xanelas.", + "Description[he]": "הדמיית מנורת קסם בעת מזעור חלונות", + "Description[hu]": "Az ablakok minimalizálása varázslámpás animáció kíséretében történik", + "Description[ia]": "Simula un lampa magic quando minimisa fenestras", + "Description[id]": "Simulasikan lampu ajaib ketika meminimalkan jendela", + "Description[is]": "Líkja eftir töfralampa þegar gluggar eru faldir", + "Description[it]": "Simula una lampada magica nella minimizzazione delle finestre", + "Description[ja]": "ウィンドウがランプの魔人のように最小化します", + "Description[ka]": "ფანჯრების ჩაკეცვისას მაგიური ნათურის სიმულაცია", + "Description[ko]": "창을 최소화할 때 요술 램프에 빨려들어가는 애니메이션을 보여줍니다", + "Description[lt]": "Suskleidžiant langus vaizduoti stebuklingą lempą", + "Description[lv]": "Simulē maģisko lampu, minimizējot logus", + "Description[nb]": "Minimer vinduer ned i en magisk lampe", + "Description[nl]": "Simuleert een magische lamp tijdens het minimaliseren van vensters", + "Description[nn]": "Minimer vindauge ned i ein magisk lampe", + "Description[pl]": "Okna wchodzą do lampy przy minimalizowaniu i wychodzą przy powrocie z niej", + "Description[pt]": "Simula uma lâmpada mágica ao minimizar as janelas", + "Description[pt_BR]": "Simula uma lâmpada mágica ao minimizar janelas", + "Description[ro]": "Simulează o lampă fermecată la minimizarea ferestrelor", + "Description[ru]": "Эффект волшебной лампы при сворачивании окон", + "Description[sa]": "खिडकीनां न्यूनीकरणे जादुदीपस्य अनुकरणं कुर्वन्तु", + "Description[sk]": "Simuluje efekt magickej lampy pri minimalizovaní okien", + "Description[sl]": "Simulirajte čurobno svetilko pri strnjevanju oken", + "Description[sv]": "Simulera en magisk lanterna vid minimering av fönster", + "Description[ta]": "சாளரங்களை ஒதுக்கும்போது, மாய விளக்குக்குள் அவை உறிஞ்சப்படுவதுபோல் அசைவூட்டும்", + "Description[tr]": "Pencereleri küçültürken sihirli bir lambaya öykün", + "Description[uk]": "Імітація ефекту чарівної лампи під час мінімізації вікон", + "Description[vi]": "Mô phỏng một cây đèn thần khi thu nhỏ các cửa sổ", + "Description[zh_CN]": "窗口最小化时绘制仿照神灯的过渡动画", + "Description[zh_TW]": "最小化視窗時模擬魔術燈", + "EnabledByDefault": false, + "License": "GPL", + "Name": "Magic Lamp", + "Name[ar]": "المصباح السحري", + "Name[az]": "Sehirli Lampa", + "Name[be]": "Чароўная лямпа", + "Name[bg]": "Вълшебна лампа", + "Name[ca@valencia]": "Làmpada màgica", + "Name[ca]": "Làmpada màgica", + "Name[cs]": "Magická lampa", + "Name[da]": "Magisk lampe", + "Name[de]": "Wunderlampe", + "Name[en_GB]": "Magic Lamp", + "Name[eo]": "Magia Lampo", + "Name[es]": "Lámpara mágica", + "Name[et]": "Imelamp", + "Name[eu]": "Lanpara magikoa", + "Name[fi]": "Taikalamppu", + "Name[fr]": "Lampe magique", + "Name[gl]": "Lámpada máxica", + "Name[he]": "מנורת קסם", + "Name[hu]": "Varázslámpa", + "Name[ia]": "Lampa Magic", + "Name[id]": "Lampu Ajaib", + "Name[is]": "Töfralampi", + "Name[it]": "Lampada magica", + "Name[ja]": "魔法のランプ", + "Name[ka]": "ჯადოსნური ნათურა", + "Name[ko]": "요술 램프", + "Name[lt]": "Stebuklinga lempa", + "Name[lv]": "Maģiskā lampa", + "Name[nb]": "Magisk lampe", + "Name[nl]": "Magische lamp", + "Name[nn]": "Magisk lampe", + "Name[pl]": "Magiczna lampa", + "Name[pt]": "Lâmpada Mágica", + "Name[pt_BR]": "Lâmpada mágica", + "Name[ro]": "Lampă fermecată", + "Name[ru]": "Волшебная лампа", + "Name[sa]": "जादू दीपक", + "Name[sk]": "Magická lampa", + "Name[sl]": "Čarobna svetilka", + "Name[sv]": "Magisk lanterna", + "Name[ta]": "மாய விளக்கு", + "Name[tr]": "Sihirli Lamba", + "Name[uk]": "Чарівна лампа", + "Name[vi]": "Đèn thần", + "Name[zh_CN]": "最小化过渡动画 (神灯)", + "Name[zh_TW]": "魔術燈" + }, + "X-KDE-ConfigModule": "kwin_magiclamp_config", + "org.kde.kwin.effect": { + "exclusiveGroup": "minimize" + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/magnifier/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/magnifier/CMakeLists.txt new file mode 100644 index 0000000000..921dd8c3ff --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/magnifier/CMakeLists.txt @@ -0,0 +1,20 @@ +####################################### +# Effect + +set(magnifier_SOURCES + magnifier.cpp + main.cpp +) + +kconfig_add_kcfg_files(magnifier_SOURCES + magnifierconfig.kcfgc +) + +kwin_add_builtin_effect(magnifier ${magnifier_SOURCES}) +target_link_libraries(magnifier PRIVATE + kwin + + KF6::ConfigGui + KF6::GlobalAccel + KF6::I18n +) diff --git a/local/recipes/kde/kwin/source/src/plugins/magnifier/magnifier.cpp b/local/recipes/kde/kwin/source/src/plugins/magnifier/magnifier.cpp new file mode 100644 index 0000000000..b592b41b66 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/magnifier/magnifier.cpp @@ -0,0 +1,353 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + SPDX-FileCopyrightText: 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "magnifier.h" +// KConfigSkeleton +#include "magnifierconfig.h" + +#include +#include + +#include + +#include "core/renderviewport.h" +#include "effect/effecthandler.h" +#include "opengl/eglcontext.h" +#include "opengl/glutils.h" +#include "utils/keys.h" +#include + +using namespace std::chrono_literals; + +namespace KWin +{ + +const int FRAME_WIDTH = 5; + +MagnifierEffect::MagnifierEffect() + : m_zoom(1) + , m_targetZoom(1) + , m_lastPresentTime(std::chrono::milliseconds::zero()) +{ + m_configurationTimer = std::make_unique(); + m_configurationTimer->setSingleShot(true); + m_configurationTimer->setInterval(1s); + connect(m_configurationTimer.get(), &QTimer::timeout, this, &MagnifierEffect::saveInitialZoom); + + MagnifierConfig::instance(effects->config()); + QAction *a; + a = KStandardActions::zoomIn(this, &MagnifierEffect::zoomIn, this); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << (Qt::META | Qt::Key_Plus) << (Qt::META | Qt::Key_Equal)); + KGlobalAccel::self()->setShortcut(a, QList() << (Qt::META | Qt::Key_Plus) << (Qt::META | Qt::Key_Equal)); + + a = KStandardActions::zoomOut(this, &MagnifierEffect::zoomOut, this); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << (Qt::META | Qt::Key_Minus)); + KGlobalAccel::self()->setShortcut(a, QList() << (Qt::META | Qt::Key_Minus)); + + a = KStandardActions::actualSize(this, &MagnifierEffect::toggle, this); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << (Qt::META | Qt::Key_0)); + KGlobalAccel::self()->setShortcut(a, QList() << (Qt::META | Qt::Key_0)); + + m_touchpadAction = std::make_unique(); + connect(m_touchpadAction.get(), &QAction::triggered, this, [this]() { + const double threshold = 1.15; + if (m_targetZoom < threshold) { + setTargetZoom(1.0); // zoomTo + } + m_lastPinchProgress = 0; + }); + effects->registerTouchpadPinchShortcut(PinchDirection::Expanding, 3, m_touchpadAction.get(), [this](qreal progress) { + const qreal delta = progress - m_lastPinchProgress; + m_lastPinchProgress = progress; + realtimeZoom(delta); + }); + effects->registerTouchpadPinchShortcut(PinchDirection::Contracting, 3, m_touchpadAction.get(), [this](qreal progress) { + const qreal delta = progress - m_lastPinchProgress; + m_lastPinchProgress = progress; + realtimeZoom(-delta); + }); + + connect(effects, &EffectsHandler::mouseChanged, this, &MagnifierEffect::slotMouseChanged); + connect(effects, &EffectsHandler::windowAdded, this, &MagnifierEffect::slotWindowAdded); + + const auto windows = effects->stackingOrder(); + for (EffectWindow *window : windows) { + slotWindowAdded(window); + } + + reconfigure(ReconfigureAll); + + const double initialZoom = MagnifierConfig::initialZoom(); + if (initialZoom > 1.0) { + setTargetZoom(initialZoom); + } +} + +MagnifierEffect::~MagnifierEffect() +{ + // Save the zoom value. + saveInitialZoom(); +} + +bool MagnifierEffect::supported() +{ + return effects->openglContext() && effects->openglContext()->supportsBlits(); +} + +void MagnifierEffect::reconfigure(ReconfigureFlags) +{ + MagnifierConfig::self()->read(); + + const QRect oldVisibleArea = visibleArea(); + + int width, height; + width = MagnifierConfig::width(); + height = MagnifierConfig::height(); + m_magnifierSize = QSize(width, height); + m_zoomFactor = MagnifierConfig::zoomFactor(); + + const Qt::KeyboardModifiers pointerAxisModifiers = stringToKeyboardModifiers(MagnifierConfig::pointerAxisGestureModifiers()); + if (m_axisModifiers != pointerAxisModifiers) { + m_zoomInAxisAction.reset(); + m_zoomOutAxisAction.reset(); + m_axisModifiers = pointerAxisModifiers; + + if (pointerAxisModifiers) { + m_zoomInAxisAction = std::make_unique(); + connect(m_zoomInAxisAction.get(), &QAction::triggered, this, &MagnifierEffect::zoomIn); + effects->registerAxisShortcut(pointerAxisModifiers, PointerAxisUp, m_zoomInAxisAction.get()); + + m_zoomOutAxisAction = std::make_unique(); + connect(m_zoomOutAxisAction.get(), &QAction::triggered, this, &MagnifierEffect::zoomOut); + effects->registerAxisShortcut(pointerAxisModifiers, PointerAxisDown, m_zoomOutAxisAction.get()); + } + } + + if (m_zoom > 1.0) { + effects->addRepaint(oldVisibleArea.united(visibleArea())); + } +} + +void MagnifierEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + const int time = m_lastPresentTime.count() ? (presentTime - m_lastPresentTime).count() : 0; + + if (m_zoom != m_targetZoom) { + double diff = time / animationTime(500ms); + if (m_targetZoom > m_zoom) { + m_zoom = std::min(m_zoom * std::max(1 + diff, 1.2), m_targetZoom); + } else { + m_zoom = std::max(m_zoom * std::min(1 - diff, 0.8), m_targetZoom); + } + } + if (m_zoom == 1.0) { + // zoom ended - delete FBO and texture + m_fbo.reset(); + m_texture.reset(); + } else if (!m_texture || m_texture->size() != m_magnifierSize) { + if (auto texture = GLTexture::allocate(GL_RGBA16F, m_magnifierSize)) { + texture->setWrapMode(GL_CLAMP_TO_EDGE); + texture->setFilter(GL_LINEAR); + + if (auto fbo = std::make_unique(texture.get()); fbo->valid()) { + m_texture = std::move(texture); + m_fbo = std::move(fbo); + } + } + } + + if (m_zoom != m_targetZoom) { + m_lastPresentTime = presentTime; + } else { + m_lastPresentTime = std::chrono::milliseconds::zero(); + } + + effects->prePaintScreen(data, presentTime); + if (m_zoom != 1.0) { + data.paint += visibleArea(); + } +} + +void MagnifierEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) +{ + effects->paintScreen(renderTarget, viewport, mask, deviceRegion, screen); // paint normal screen + if (m_zoom != 1.0 && m_fbo) { + // get the right area from the current rendered screen + const QRect area = magnifierArea(); + const QPointF cursor = cursorPos(); + const auto scale = viewport.scale(); + + QRectF srcArea(cursor.x() - (double)area.width() / (m_zoom * 2), + cursor.y() - (double)area.height() / (m_zoom * 2), + (double)area.width() / m_zoom, (double)area.height() / m_zoom); + if (effects->isOpenGLCompositing()) { + m_fbo->blitFromRenderTarget(renderTarget, viewport, srcArea.toRect(), QRect(QPoint(), m_fbo->size())); + // paint magnifier + auto s = ShaderManager::instance()->pushShader(ShaderTrait::MapTexture); + QMatrix4x4 mvp = viewport.projectionMatrix(); + mvp.translate(area.x() * scale, area.y() * scale); + s->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, mvp); + m_texture->render(area.size() * scale); + ShaderManager::instance()->popShader(); + + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + + QRectF areaF = scaledRect(area, scale); + const QRectF frame = scaledRect(area.adjusted(-FRAME_WIDTH, -FRAME_WIDTH, FRAME_WIDTH, FRAME_WIDTH), scale); + QList verts; + verts.reserve(4 * 6 * 2); + // top frame + verts.push_back(QVector2D(frame.right(), frame.top())); + verts.push_back(QVector2D(frame.left(), frame.top())); + verts.push_back(QVector2D(frame.left(), areaF.top())); + verts.push_back(QVector2D(frame.left(), areaF.top())); + verts.push_back(QVector2D(frame.right(), areaF.top())); + verts.push_back(QVector2D(frame.right(), frame.top())); + // left frame + verts.push_back(QVector2D(areaF.left(), frame.top())); + verts.push_back(QVector2D(frame.left(), frame.top())); + verts.push_back(QVector2D(frame.left(), frame.bottom())); + verts.push_back(QVector2D(frame.left(), frame.bottom())); + verts.push_back(QVector2D(areaF.left(), frame.bottom())); + verts.push_back(QVector2D(areaF.left(), frame.top())); + // right frame + verts.push_back(QVector2D(frame.right(), frame.top())); + verts.push_back(QVector2D(areaF.right(), frame.top())); + verts.push_back(QVector2D(areaF.right(), frame.bottom())); + verts.push_back(QVector2D(areaF.right(), frame.bottom())); + verts.push_back(QVector2D(frame.right(), frame.bottom())); + verts.push_back(QVector2D(frame.right(), frame.top())); + // bottom frame + verts.push_back(QVector2D(frame.right(), areaF.bottom())); + verts.push_back(QVector2D(frame.left(), areaF.bottom())); + verts.push_back(QVector2D(frame.left(), frame.bottom())); + verts.push_back(QVector2D(frame.left(), frame.bottom())); + verts.push_back(QVector2D(frame.right(), frame.bottom())); + verts.push_back(QVector2D(frame.right(), areaF.bottom())); + vbo->setVertices(verts); + + ShaderBinder binder(ShaderTrait::UniformColor); + binder.shader()->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, viewport.projectionMatrix()); + binder.shader()->setUniform(GLShader::ColorUniform::Color, QColor(0, 0, 0)); + vbo->render(GL_TRIANGLES); + } + } +} + +void MagnifierEffect::postPaintScreen() +{ + if (m_zoom != m_targetZoom) { + effects->addRepaint(visibleArea()); + } + effects->postPaintScreen(); +} + +QRect MagnifierEffect::magnifierArea(QPointF pos) const +{ + return QRect(pos.x() - m_magnifierSize.width() / 2, pos.y() - m_magnifierSize.height() / 2, + m_magnifierSize.width(), m_magnifierSize.height()); +} + +QRect MagnifierEffect::visibleArea(QPointF pos) const +{ + return magnifierArea(pos).adjusted(-FRAME_WIDTH, -FRAME_WIDTH, FRAME_WIDTH, FRAME_WIDTH); +} + +void MagnifierEffect::zoomIn() +{ + setTargetZoom(m_targetZoom * m_zoomFactor); +} + +void MagnifierEffect::zoomOut() +{ + setTargetZoom(m_targetZoom / m_zoomFactor); +} + +void MagnifierEffect::toggle() +{ + if (m_zoom == 1.0) { + if (m_targetZoom == 1.0) { + setTargetZoom(2.0); + } + } else { + setTargetZoom(1.0); + } +} + +void MagnifierEffect::slotMouseChanged(const QPointF &pos, const QPointF &old, + Qt::MouseButtons, Qt::MouseButtons, Qt::KeyboardModifiers, Qt::KeyboardModifiers) +{ + if (pos != old && m_zoom != 1) { + // need full repaint as we might lose some change events on fast mouse movements + // see Bug 187658 + effects->addRepaintFull(); + } +} + +void MagnifierEffect::slotWindowAdded(EffectWindow *w) +{ + connect(w, &EffectWindow::windowDamaged, this, &MagnifierEffect::slotWindowDamaged); +} + +void MagnifierEffect::slotWindowDamaged() +{ + if (isActive()) { + effects->addRepaint(magnifierArea()); + } +} + +bool MagnifierEffect::isActive() const +{ + return m_zoom != 1.0 || m_zoom != m_targetZoom; +} + +QSize MagnifierEffect::magnifierSize() const +{ + return m_magnifierSize; +} + +qreal MagnifierEffect::targetZoom() const +{ + return m_targetZoom; +} + +void MagnifierEffect::saveInitialZoom() +{ + MagnifierConfig::setInitialZoom(m_targetZoom); + MagnifierConfig::self()->save(); +} + +void MagnifierEffect::setTargetZoom(double zoomFactor) +{ + const double effectiveTargetZoom = std::clamp(zoomFactor, 1.0, 100.0); + if (m_targetZoom == effectiveTargetZoom) { + return; + } + + m_targetZoom = effectiveTargetZoom; + effects->addRepaint(visibleArea()); + m_configurationTimer->start(); +} + +void MagnifierEffect::realtimeZoom(double delta) +{ + // for the change speed to feel roughly linear, + // we have to increase the delta at higher zoom levels + delta *= m_targetZoom / 2; + setTargetZoom(m_targetZoom + delta); + // skip the animation, we want this to be real time + m_zoom = m_targetZoom; +} + +} // namespace + +#include "moc_magnifier.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/magnifier/magnifier.h b/local/recipes/kde/kwin/source/src/plugins/magnifier/magnifier.h new file mode 100644 index 0000000000..2b1bcfaf30 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/magnifier/magnifier.h @@ -0,0 +1,76 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/effect.h" + +#include + +class QAction; + +namespace KWin +{ + +class GLFramebuffer; +class GLTexture; + +class MagnifierEffect : public Effect +{ + Q_OBJECT + Q_PROPERTY(QSize magnifierSize READ magnifierSize) + Q_PROPERTY(qreal targetZoom READ targetZoom) +public: + MagnifierEffect(); + ~MagnifierEffect() override; + void reconfigure(ReconfigureFlags) override; + void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override; + void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) override; + void postPaintScreen() override; + bool isActive() const override; + static bool supported(); + + // for properties + QSize magnifierSize() const; + qreal targetZoom() const; +private Q_SLOTS: + void saveInitialZoom(); + void zoomIn(); + void zoomOut(); + void toggle(); + void slotMouseChanged(const QPointF &pos, const QPointF &old, + Qt::MouseButtons buttons, Qt::MouseButtons oldbuttons, + Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers); + void slotWindowAdded(EffectWindow *w); + void slotWindowDamaged(); + +private: + QRect magnifierArea(QPointF pos = cursorPos()) const; + QRect visibleArea(QPointF pos = cursorPos()) const; + void setTargetZoom(double zoomFactor); + void realtimeZoom(double delta); + + std::unique_ptr m_configurationTimer; + double m_zoom; + double m_targetZoom; + double m_zoomFactor; + double m_pixelGridZoom; + std::unique_ptr m_zoomInAxisAction; + std::unique_ptr m_zoomOutAxisAction; + Qt::KeyboardModifiers m_axisModifiers; + std::unique_ptr m_touchpadAction; + double m_lastPinchProgress = 0; + std::chrono::milliseconds m_lastPresentTime; + QSize m_magnifierSize; + std::unique_ptr m_texture; + std::unique_ptr m_fbo; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/magnifier/magnifier.kcfg b/local/recipes/kde/kwin/source/src/plugins/magnifier/magnifier.kcfg new file mode 100644 index 0000000000..91bdac5743 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/magnifier/magnifier.kcfg @@ -0,0 +1,28 @@ + + + + + + 200 + + + 200 + + + + + + + 1.2 + + + 1.0 + + + Meta+Control + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/magnifier/magnifierconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/magnifier/magnifierconfig.kcfgc new file mode 100644 index 0000000000..6286bc8113 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/magnifier/magnifierconfig.kcfgc @@ -0,0 +1,5 @@ +File=magnifier.kcfg +ClassName=MagnifierConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/local/recipes/kde/kwin/source/src/plugins/magnifier/main.cpp b/local/recipes/kde/kwin/source/src/plugins/magnifier/main.cpp new file mode 100644 index 0000000000..862236f9cc --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/magnifier/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "magnifier.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(MagnifierEffect, + "metadata.json.stripped", + return MagnifierEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/magnifier/metadata.json b/local/recipes/kde/kwin/source/src/plugins/magnifier/metadata.json new file mode 100644 index 0000000000..b282c793c8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/magnifier/metadata.json @@ -0,0 +1,96 @@ +{ + "KPlugin": { + "Category": "Accessibility", + "Description": "Magnify the section of the screen that is near the mouse cursor; activated with a keyboard shortcut", + "Description[ar]": "كبّر قسم الشاشة القريب من مؤشّر الفأرة، تفعّل باختصار لوحة المفاتيح", + "Description[bg]": "Увеличаване на секцията на екрана, която е близо до курсора на мишката, активирано с клавишна комбинация", + "Description[ca@valencia]": "Amplia la secció de la pantalla que està prop del cursor del ratolí; activat amb una drecera de teclat", + "Description[ca]": "Amplia la secció de la pantalla que està prop del cursor del ratolí; activat amb una drecera de teclat", + "Description[da]": "Forstør sektionen af skærmen, der er nær markøren; aktiveret med en tastaturgenvej", + "Description[de]": "Vergrößert den Arbeitsflächenbereich unter dem Mauszeiger; mit einem Tastatur-Kurzbefehl ausgelöst", + "Description[en_GB]": "Magnify the section of the screen that is near the mouse cursor; activated with a keyboard shortcut", + "Description[eo]": "Pligrandigi la sekcion de la ekrano, kiu estas proksime de la muskursoro; aktivigita per klavara ŝparvojo", + "Description[es]": "Ampliar la sección de la pantalla que está cerca del cursor del ratón; se activa con un atajo de teclado", + "Description[eu]": "Handiagotu sagu-kurtsorearen ondoko pantailaren zatia; teklatuko lasterbide batekin aktibatua", + "Description[fi]": "Suurenna hiiriosoittimen läheinen näytön alue; käynnistyy pikanäppäimellä", + "Description[fr]": "Agrandir la partie de l'écran proche du pointeur de la souris. Activé grâce à un raccourci clavier.", + "Description[gl]": "Amplía a parte da pantalla que está preto do rato; actívase cun atallo de teclado.", + "Description[he]": "הגדלת החלק במסך שקרוב לסמן העכבר, מופעל דרך צירוף מקשי מקלדת", + "Description[hu]": "Felnagyítja az egérmutató alatti területet; gyorsbillentyűvel aktiválható", + "Description[ia]": "Aggrandi le section del schermo que es vicin al cursor del mus; activate con un via breve de claviero", + "Description[is]": "Stækka þann hluta af skjánum sem er nálægt músarbendlinum; virkjað með flýtilykli", + "Description[it]": "Ingrandisci la sezione dello schermo nei pressi del puntatore del mouse; attivato con una scorciatoia da tastiera", + "Description[ka]": "თაგუნას კურსორის ახლომდებარე ეკრანის მონაკვეთის გადიდება. აქტიურდება კლავიატურის მალსახმობით", + "Description[ko]": "마우스 커서 근처에 있는 화면을 확대, 키보드 단축키로 활성화", + "Description[lt]": "Padidinti ekrano sekciją, kuri yra šalia pelės rodyklės; aktyvinama naudojant sparčiuosius klavišus", + "Description[lv]": "Pietuvina ekrāna daļu, kas atrodas pie peles kursora; ieslēdz ar tastatūras saīsni", + "Description[nb]": "Forstørr delen av skjermen som er under musepekeren – slått av/på med hurtigtast", + "Description[nl]": "Vergroot het deel van het scherm dat zich onder de muis bevindt; geactiveerd met een sneltoets", + "Description[nn]": "Forstørr delen av skjermen som er under musepeikaren – slått på/av med snøggtast", + "Description[pl]": "Powiększa obszar ekranu znajdujący się pod wskaźnikiem myszy; uruchamiane skrótem klawiszowym", + "Description[pt_BR]": "Amplia a seção da tela próxima ao ponteiro do mouse; ativado com um atalho de teclado", + "Description[ro]": "Mărește secțiunea ecranului de lângă cursorul șoricelului; activat cu o scurtătură de taste", + "Description[ru]": "Увеличение части экрана у курсора мыши; активируется нажатием комбинации клавиш", + "Description[sa]": "मूषकस्य कर्सरस्य समीपे स्थितस्य स्क्रीनस्य खण्डं वर्धयन्तु; कीबोर्ड-शॉर्टकट्-सहितं सक्रियम्", + "Description[sk]": "Zväčší časť obrazovky, ktorá je okolo kurzora myši", + "Description[sl]": "Povečajte del zaslona, ki je blizu miškinega kazalca; aktivirano z bližnjico tipkovnice", + "Description[sv]": "Förstorar den del av skärmen som är nära muspekaren, aktiveras med en snabbtangent", + "Description[tr]": "Ekranın fare imlecinin yakınında olan kısımlarını büyüt; klavye kısayoluyla etkinleştirilir", + "Description[uk]": "Збільшення частини екрана поруч з вказівником миші; активується клавіатурним скороченням", + "Description[zh_CN]": "放大鼠标光标附近的屏幕区域;使用键盘快捷键激活", + "Description[zh_TW]": "放大在滑鼠游標附近的部分——需要用鍵盤快捷鍵觸發", + "EnabledByDefault": false, + "License": "GPL", + "Name": "Magnifier", + "Name[ar]": "المكبّر", + "Name[az]": "Böyüdücü", + "Name[be]": "Лупа", + "Name[bg]": "Лупа", + "Name[ca@valencia]": "Lupa", + "Name[ca]": "Lupa", + "Name[cs]": "Lupa", + "Name[da]": "Forstørrelsesglas", + "Name[de]": "Lupe", + "Name[en_GB]": "Magnifier", + "Name[eo]": "Lupeo", + "Name[es]": "Lupa", + "Name[et]": "Suurendaja", + "Name[eu]": "Lupa", + "Name[fi]": "Suurennuslasi", + "Name[fr]": "Loupe", + "Name[gl]": "Lupa", + "Name[he]": "זכוכית מגדלת", + "Name[hu]": "Nagyító", + "Name[ia]": "Aggranditor", + "Name[id]": "Kaca Pembesar", + "Name[is]": "Stækkunargler", + "Name[it]": "Lente d'ingrandimento", + "Name[ja]": "拡大鏡", + "Name[ka]": "გამადიდებელი", + "Name[ko]": "돋보기", + "Name[lt]": "Didinamasis stiklas", + "Name[lv]": "Lupa", + "Name[nb]": "Forstørr skjermdel", + "Name[nl]": "Vergrootglas", + "Name[nn]": "Forstørr skjermdel", + "Name[pl]": "Powiększenie", + "Name[pt]": "Lupa", + "Name[pt_BR]": "Lupa", + "Name[ro]": "Lupă", + "Name[ru]": "Лупа", + "Name[sa]": "आवर्धक", + "Name[sk]": "Lupa", + "Name[sl]": "Povečevalo", + "Name[sv]": "Förstoringsglas", + "Name[ta]": "உருப்பெருக்கி", + "Name[tr]": "Büyüteç", + "Name[uk]": "Лупа", + "Name[vi]": "Trình phóng đại", + "Name[zh_CN]": "放大镜 (方形)", + "Name[zh_TW]": "放大鏡" + }, + "org.kde.kwin.effect": { + "exclusiveGroup": "magnifiers", + "internal": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/maximize/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/maximize/CMakeLists.txt new file mode 100644 index 0000000000..e90b868831 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/maximize/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_scripted_effect(maximize package) diff --git a/local/recipes/kde/kwin/source/src/plugins/maximize/package/contents/code/main.js b/local/recipes/kde/kwin/source/src/plugins/maximize/package/contents/code/main.js new file mode 100644 index 0000000000..ba58a68a44 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/maximize/package/contents/code/main.js @@ -0,0 +1,127 @@ +/* + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +class MaximizeEffect { + constructor() { + effect.configChanged.connect(this.loadConfig.bind(this)); + effect.animationEnded.connect(this.restoreForceBlurState.bind(this)); + + effects.windowAdded.connect(this.manage.bind(this)); + for (const window of effects.stackingOrder) { + this.manage(window); + } + + this.loadConfig(); + } + + loadConfig() { + this.duration = animationTime(250); + } + + manage(window) { + window.windowFrameGeometryChanged.connect(this.onWindowFrameGeometryChanged.bind(this)); + window.windowMaximizedStateChanged.connect(this.onWindowMaximizedStateChanged.bind(this)); + window.windowMaximizedStateAboutToChange.connect(this.onWindowMaximizedStateAboutToChange.bind(this)); + } + + onWindowMaximizedStateAboutToChange(window) { + if (!window.visible) { + return; + } + + window.oldGeometry = Object.assign({}, window.geometry); + + if (window.maximizeAnimation1) { + cancel(window.maximizeAnimation1); + delete window.maximizeAnimation1; + } + let couldRetarget = false; + if (window.maximizeAnimation2) { + couldRetarget = retarget(window.maximizeAnimation2, 1.0, this.duration); + } + if (!couldRetarget) { + window.maximizeAnimation2 = animate({ + window: window, + duration: this.duration, + animations: [{ + type: Effect.CrossFadePrevious, + to: 1.0, + from: 0.0, + curve: QEasingCurve.OutCubic + }] + }); + } + } + + onWindowMaximizedStateChanged(window) { + if (!window.visible || !window.oldGeometry) { + return; + } + window.setData(Effect.WindowForceBlurRole, true); + const oldGeometry = window.oldGeometry; + const newGeometry = window.geometry; + window.maximizeAnimation1 = animate({ + window: window, + duration: this.duration, + animations: [{ + type: Effect.Size, + to: { + value1: newGeometry.width, + value2: newGeometry.height + }, + from: { + value1: oldGeometry.width, + value2: oldGeometry.height + }, + curve: QEasingCurve.OutCubic + }, { + type: Effect.Translation, + to: { + value1: 0, + value2: 0 + }, + from: { + value1: oldGeometry.x - newGeometry.x - (newGeometry.width / 2 - oldGeometry.width / 2), + value2: oldGeometry.y - newGeometry.y - (newGeometry.height / 2 - oldGeometry.height / 2) + }, + curve: QEasingCurve.OutCubic + }] + }); + + // Make sure all animations end on the same frame to prevent WindowForceBlurRole from being removed too early. + retarget(window.maximizeAnimation2, 1.0, this.duration); + } + + restoreForceBlurState(window) { + window.setData(Effect.WindowForceBlurRole, null); + } + + onWindowFrameGeometryChanged(window, oldGeometry) { + if (!window.maximizeAnimation1 || + // Check only dimension changes. + (window.geometry.width == oldGeometry.width && window.geometry.height == oldGeometry.height) || + // Check only if last dimension isn't equal to dimension from which effect was started (window.oldGeometry). + (window.oldGeometry.width == oldGeometry.width && window.oldGeometry.height == oldGeometry.height) + ) { + return; + } + + // Cancel animation if window got resized halfway through it. + cancel(window.maximizeAnimation1); + delete window.maximizeAnimation1; + + if (window.maximizeAnimation2) { + cancel(window.maximizeAnimation2); + delete window.maximizeAnimation2; + } + } +} + +new MaximizeEffect(); diff --git a/local/recipes/kde/kwin/source/src/plugins/maximize/package/metadata.json b/local/recipes/kde/kwin/source/src/plugins/maximize/package/metadata.json new file mode 100644 index 0000000000..6abe3c417b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/maximize/package/metadata.json @@ -0,0 +1,131 @@ +{ + "KPackageStructure": "KWin/Effect", + "KPlugin": { + "Authors": [ + { + "Email": "mgraesslin@kde.org", + "Name": "Martin Gräßlin", + "Name[ar]": "مارتن جراجلين", + "Name[be]": "Martin Gräßlin", + "Name[bg]": "Martin Gräßlin", + "Name[ca@valencia]": "Martin Gräßlin", + "Name[ca]": "Martin Gräßlin", + "Name[cs]": "Martin Gräßlin", + "Name[da]": "Martin Gräßlin", + "Name[de]": "Martin Gräßlin", + "Name[en_GB]": "Martin Gräßlin", + "Name[eo]": "Martin Gräßlin", + "Name[es]": "Martin Gräßlin", + "Name[et]": "Martin Gräßlin", + "Name[eu]": "Martin Gräßlin", + "Name[fi]": "Martin Gräßlin", + "Name[fr]": "Martin Gräßlin", + "Name[ga]": "Martin Gräßlin", + "Name[gl]": "Martin Gräßlin.", + "Name[he]": "מרטין גרייסלין", + "Name[hu]": "Martin Gräßlin", + "Name[ia]": "Martin Gräßlin", + "Name[id]": "Martin Gräßlin", + "Name[is]": "Martin Gräßlin", + "Name[it]": "Martin Gräßlin", + "Name[ja]": "Martin Gräßlin", + "Name[ka]": "მარტინ გრესსლინი", + "Name[ko]": "Martin Gräßlin", + "Name[lt]": "Martin Gräßlin", + "Name[lv]": "Martin Gräßlin", + "Name[nb]": "Martin Gräßlin", + "Name[nl]": "Martin Gräßlin", + "Name[nn]": "Martin Gräßlin", + "Name[pl]": "Martin Gräßlin", + "Name[pt]": "Martin Gräßlin", + "Name[pt_BR]": "Martin Gräßlin", + "Name[ro]": "Martin Gräßlin", + "Name[ru]": "Martin Gräßlin", + "Name[sa]": "मार्टिन् ग्रास्लिन्", + "Name[sk]": "Martin Gräßlin", + "Name[sl]": "Martin Gräßlin", + "Name[sv]": "Martin Gräßlin", + "Name[ta]": "மார்ட்டின் கிராஸ்லின்", + "Name[tr]": "Martin Gräßlin", + "Name[uk]": "Martin Gräßlin", + "Name[vi]": "Martin Gräßlin", + "Name[zh_CN]": "Martin Gräßlin", + "Name[zh_TW]": "Martin Gräßlin" + } + ], + "Category": "Appearance", + "Description": "Stretch windows when they are maximized or restored", + "Description[ar]": "توسيع النوافذ عند تكبيرها أو استعادتها", + "Description[bg]": "Разтягане на прозорците, когато са максимизирани или възстановени", + "Description[ca@valencia]": "Estira les finestres quan es maximitzen o minimitzen", + "Description[ca]": "Estira les finestres quan es maximitzin o minimitzin", + "Description[de]": "Fenster beim Maximieren und Wiederherstellen strecken", + "Description[es]": "Estirar las ventanas cuando se maximizan o se restauran", + "Description[eu]": "Luzatu leihoak maximizatzen edo lehengoratzen direnean", + "Description[fi]": "Venytä suurennettavia tai palautettavia ikkunoita", + "Description[fr]": "Étirer les fenêtres lors de leur maximisation ou minimisation", + "Description[he]": "למתוח חלונות כשהם מוגדלים או משוחזרים", + "Description[hu]": "Kinyújtja az ablakokat maximalizáláskor vagy visszaállításkor", + "Description[ia]": "Extende fenestras quando illos es maximisate o restabilite", + "Description[is]": "Teygja glugga þegar þeir eru fullstækkaðir eða endurheimtir", + "Description[it]": "Allunga le finestre quando sono massimizzate o ripristinate", + "Description[ja]": "最大化/最小化時にウィンドウをストレッチします", + "Description[ka]": "ფანჯრების გაწელვა მათი ჩაკეცვის ან ამოკეცვის დროს", + "Description[ko]": "창을 최대화하거나 복원할 때 잡아 당깁니다", + "Description[lt]": "Ištempti langus, kai jie išskleidžiami ar atkuriami", + "Description[lv]": "Izstiept logus, kad tos maksimizējat vai atjaunojat", + "Description[nl]": "Vensters uitrekken wanneer ze geminimaliseerd worden of hersteld", + "Description[nn]": "Strekk vindauge når dei vert maksimerte eller gjenoppretta", + "Description[pl]": "Okna są rozciągane przy ich maksymalizacji lub podniesieniu", + "Description[pt_BR]": "Alongar as janelas quando elas forem maximizadas ou restauradas", + "Description[ro]": "Întinde ferestrele când sunt maximizate sau restabilite", + "Description[ru]": "Растягивание окна при распахивании или разворачивании", + "Description[sk]": "Stlačenie okien, keď sú minimalizované", + "Description[sl]": "Raztegni okna, ko so razprta ali obnovljena", + "Description[sv]": "Sträck ut fönster när de maximeras eller återställs", + "Description[tr]": "Pencereler ekranı kapladığında veya eski hallerine döndüğünde onları ger", + "Description[uk]": "Розтягування вікон при максимізації або відновленні", + "Description[zh_CN]": "最大化或恢复窗口时显示窗口拉伸动效", + "Description[zh_TW]": "最大化或復原視窗時拉伸它們", + "EnabledByDefault": true, + "Icon": "preferences-system-windows-effect-maximize", + "Id": "maximize", + "License": "GPL", + "Name": "Stretch", + "Name[ar]": "التمديد", + "Name[bg]": "Разтягане", + "Name[ca@valencia]": "Estira", + "Name[ca]": "Estira", + "Name[de]": "Strecken", + "Name[es]": "Estirar", + "Name[eu]": "Luzatu", + "Name[fi]": "Venytä", + "Name[fr]": "Étirer", + "Name[he]": "מתיחה", + "Name[hu]": "Nyújtás", + "Name[ia]": "A extension", + "Name[is]": "Teygja", + "Name[it]": "Allunga", + "Name[ja]": "ストレッチ", + "Name[ka]": "გაწელვა", + "Name[ko]": "늘이기", + "Name[lt]": "Ištempti", + "Name[lv]": "Izstiept", + "Name[nl]": "Uitrekken", + "Name[nn]": "Strekk", + "Name[pl]": "Rozciąganie", + "Name[pt_BR]": "Alongar", + "Name[ro]": "Întinde", + "Name[ru]": "Растягивание", + "Name[sk]": "Natiahnuť", + "Name[sl]": "Raztegni", + "Name[sv]": "Sträck", + "Name[tr]": "Ger", + "Name[uk]": "Розтягнути", + "Name[zh_CN]": "拉伸动效", + "Name[zh_TW]": "拉伸" + }, + "X-KDE-Ordering": 60, + "X-KWin-Exclusive-Category": "maximize", + "X-Plasma-API": "javascript" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/minimizeall/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/minimizeall/CMakeLists.txt new file mode 100644 index 0000000000..8d7f9a2de3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/minimizeall/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_script(minimizeall package) diff --git a/local/recipes/kde/kwin/source/src/plugins/minimizeall/package/contents/code/main.js b/local/recipes/kde/kwin/source/src/plugins/minimizeall/package/contents/code/main.js new file mode 100644 index 0000000000..b232fc01a4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/minimizeall/package/contents/code/main.js @@ -0,0 +1,116 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2014 Thomas Lübking + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +var registeredBorders = []; + +function isRelevant(window) { + return window.minimizable && + (!window.desktops.length || window.desktops.includes(workspace.currentDesktop)) && + (!window.activities.length || window.activities.includes(workspace.currentActivity)); +} + +function minimizeWindows(windows) { + let minimize = false; + var relevantWindows = []; + for (const window of windows) { + if (!isRelevant(window)) { + continue; + } + if (!window.minimized) { + minimize = true; + } + relevantWindows.push(window); + } + // Try to preserve topmost window by sorting windows. + relevantWindows.sort((a, b) => { + if (a.active) { + return 1; + } else if (b.active) { + return -1; + } + return a.stackingOrder - b.stackingOrder; + }); + + for (const window of relevantWindows) { + var wasMinimizedByScript = window.minimizedByScript; + delete window.minimizedByScript; + + if (minimize) { + if (window.minimized) { + continue; + } + window.minimized = true; + window.minimizedByScript = true; + } else { + if (!wasMinimizedByScript) { + continue; + } + window.minimized = false; + } + } + return [relevantWindows, minimize]; +} + +function minimizeWindowsRestoreFocus(windows) { + const [relevantWindows, minimize] = minimizeWindows(windows); + if (!minimize) { + for (let i = relevantWindows.length - 1; i >= 0; i--) { + const window = relevantWindows[i]; + if (window === workspace.activeWindow) { + break; + } + if (!workspace.windowList().includes(window)) { + continue; + } + workspace.activeWindow = window; + break; + } + } +} + +function minimizeAllWindows() { + minimizeWindowsRestoreFocus(workspace.windowList()); +} + +function minimizeAllWindowsActiveScreen() { + minimizeWindowsRestoreFocus(workspace.windowList().filter(window => window.output === workspace.activeScreen)); +} + +function minimizeAllOthers() { + minimizeWindows(workspace.windowList().filter(window => !window.active)); +} + +function minimizeAllOthersActiveScreen() { + minimizeWindows(workspace.windowList().filter(window => !window.active && window.output === workspace.activeScreen)); +} + +function init() { + for (var i in registeredBorders) { + unregisterScreenEdge(registeredBorders[i]); + } + + registeredBorders = []; + + var borders = readConfig("BorderActivate", "").toString().split(","); + for (var i in borders) { + var border = parseInt(borders[i]); + if (isFinite(border)) { + registeredBorders.push(border); + registerScreenEdge(border, minimizeAllWindows); + } + } +} + +options.configChanged.connect(init); + +registerShortcut("MinimizeAll", "Minimize all windows", "Meta+Shift+D", minimizeAllWindows); +registerShortcut("MinimizeAllActiveScreen", "Minimize all windows in active screen", "", minimizeAllWindowsActiveScreen); +registerShortcut("minimizeAllOthers", "Minimize all other windows", "Meta+Shift+O", minimizeAllOthers); +registerShortcut("minimizeAllOthersActiveScreen", "Minimize all other windows in active screen", "", minimizeAllOthersActiveScreen); +init(); diff --git a/local/recipes/kde/kwin/source/src/plugins/minimizeall/package/metadata.json b/local/recipes/kde/kwin/source/src/plugins/minimizeall/package/metadata.json new file mode 100644 index 0000000000..8e5c8b832f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/minimizeall/package/metadata.json @@ -0,0 +1,155 @@ +{ + "KPackageStructure": "KWin/Script", + "KPlugin": { + "Authors": [ + { + "Email": "thomas.luebking@gmail.com", + "Name": "Thomas Lübking", + "Name[ar]": "توماس لوبكينج", + "Name[be]": "Thomas Lübking", + "Name[bg]": "Thomas Lübking", + "Name[ca@valencia]": "Thomas Lübking", + "Name[ca]": "Thomas Lübking", + "Name[cs]": "Thomas Lübking", + "Name[da]": "Thomas Lübking", + "Name[de]": "Thomas Lübking", + "Name[en_GB]": "Thomas Lübking", + "Name[eo]": "Thomas Lübking", + "Name[es]": "Thomas Lübking", + "Name[et]": "Thomas Lübking", + "Name[eu]": "Thomas Lübking", + "Name[fi]": "Thomas Lübking", + "Name[fr]": "Thomas Lübking", + "Name[ga]": "Thomas Lübking", + "Name[gl]": "Thomas Lübking.", + "Name[he]": "תומס ליובקינג", + "Name[hu]": "Thomas Lübking", + "Name[ia]": "Thomas Lübking", + "Name[id]": "Thomas Lübking", + "Name[is]": "Thomas Lübking", + "Name[it]": "Thomas Lübking", + "Name[ja]": "Thomas Lübking", + "Name[ka]": "Thomas Lübking", + "Name[ko]": "Thomas Lübking", + "Name[lt]": "Thomas Lübking", + "Name[lv]": "Thomas Lübking", + "Name[nb]": "Thomas Lübking", + "Name[nl]": "Thomas Lübking", + "Name[nn]": "Thomas Lübking", + "Name[pl]": "Thomas Lübking", + "Name[pt]": "Thomas Lübking", + "Name[pt_BR]": "Thomas Lübking", + "Name[ro]": "Thomas Lübking", + "Name[ru]": "Thomas Lübking", + "Name[sa]": "थोमस लुब्किङ्ग्", + "Name[sk]": "Thomas Lübking", + "Name[sl]": "Thomas Lübking", + "Name[sv]": "Thomas Lübking", + "Name[ta]": "தாமஸ் லுபுகிங்", + "Name[tr]": "Thomas Lübking", + "Name[uk]": "Thomas Lübking", + "Name[vi]": "Thomas Lübking", + "Name[zh_CN]": "Thomas Lübking", + "Name[zh_TW]": "Thomas Lübking" + } + ], + "Description": "Adds a shortcut to minimize and restore all windows", + "Description[ar]": "يضيف اختصارًا لتصغير جميع النوافذ واستعادتها", + "Description[be]": "Дадае спалучэнне клавіш для згортвання і разгортвання ўсіх акон", + "Description[bg]": "Добавя клавишна комбинация за минимизиране и възстановяване на всички прозорци", + "Description[ca@valencia]": "Afig una drecera per a minimitzar i restaurar totes les finestres", + "Description[ca]": "Afegeix una drecera per a minimitzar i restaurar totes les finestres", + "Description[cs]": "Přidá zkratku k minimalizaci a obnovení všech oken.", + "Description[da]": "Tilføjer en genvej for at minimere og gendanne alle vinduer", + "Description[de]": "Fügt einen Kurzbefehl hinzu, um alle Fenster zu minimieren oder wieder anzuzeigen", + "Description[en_GB]": "Adds a shortcut to minimise and restore all windows", + "Description[eo]": "Aldonas ŝparvojon por minimumigi kaj restarigi ĉiujn fenestrojn", + "Description[es]": "Añade un atajo de teclado para minimizar y restaurar todas las ventanas", + "Description[et]": "Kiirklahvi lisamine kõigi akende minimeerimiseks või sel moel minimeeritud akende suuruse taastamiseks", + "Description[eu]": "Lasterbide bat eransten du leiho guztiak minimizatzeko eta lehengoratzeko", + "Description[fi]": "Lisää pikanäppäimen kaikkien ikkunoiden pienentämiseksi ja palauttamiseksi", + "Description[fr]": "Ajoute un raccourci pour minimiser et restaurer toutes les fenêtres", + "Description[gl]": "Engade un atallo para minimizar e restaurar todas as xanelas.", + "Description[he]": "מוסיף קיצור דרך למזעור ושחזור כל החלונות", + "Description[hu]": "Gyorsbillentyű hozzáadása az összes ablak minimalizálásához és visszaállításához", + "Description[ia]": "Adde un via breve pe minimisar e restabilir omne fenestras", + "Description[id]": "Menambahkan sebuah pintasan untuk minimalkan dan pulihkan semua jendela", + "Description[is]": "Bætir við flýtilykli til að fela eða endurheimta alla glugga", + "Description[it]": "Aggiunge una scorciatoia per minimizzare e ripristinare tutte le finestre", + "Description[ja]": "すべてのウィンドウを最小化/復元するショートカットを追加します", + "Description[ka]": "ყველა ფანჯრის ჩაკეცვისა და ამოკეცვის მალსახმობის დამატება", + "Description[ko]": "모든 창을 최소화하고 복원하는 단축키를 추가합니다", + "Description[lt]": "Prideda spartųjį klavišą, skirtą suskleisit ir atkurti visus langus", + "Description[lv]": "Pievieno saīsni visu logu minimizēšanai un atjaunošanai", + "Description[nb]": "Legg til snarveier for å minimere og gjenopprette alle vinduer", + "Description[nl]": "Voegt een sneltoets toe om alle vensters te minimaliseren en te herstellen", + "Description[nn]": "Legg til snarvegar for å minimera og gjenoppretta alle vindauge", + "Description[pl]": "Dodaje skrót do zminimalizowania i przywracania wszystkich okien", + "Description[pt]": "Adicionar um atalho para minimizar e repor todas as janelas", + "Description[pt_BR]": "Adiciona um atalho para minimizar e restaurar todas as janelas", + "Description[ro]": "Adaugă o scurtătură pentru a minimiza și restabili toate ferestrele", + "Description[ru]": "Добавляет глобальную комбинацию клавиш для сворачивания и восстановления всех окон", + "Description[sa]": "सर्वाणि विण्डोस् न्यूनीकर्तुं पुनःस्थापयितुं च शॉर्टकट् योजयति", + "Description[sk]": "Pridá skratku na minimalizovanie a obnovenie všetkých okien", + "Description[sl]": "Doda bližnjico za pomanjšanje in obnovitev vseh oken", + "Description[sv]": "Lägger till en genväg för att minimera och återställa alla fönster", + "Description[ta]": "அனைத்து சாளரங்களை ஒதுக்கவும் மீட்டமைக்கவும் உதவும் சுருக்குவழியை சேர்க்கும்", + "Description[tr]": "Tüm pencereleri simge durumuna küçültmek veya eski hallerine dönmeleri için bir kısayol ekler", + "Description[uk]": "Додає скорочення мінімізації усіх вікон або скасування такої мінімізації для усіх вікон", + "Description[vi]": "Thêm một phím tắt để thu nhỏ và khôi phục tất cả các cửa sổ", + "Description[zh_CN]": "添加最小化/恢复全部窗口的快捷方式", + "Description[zh_TW]": "新增捷徑來最小化或回復所有視窗", + "Icon": "preferences-system-windows-script-test", + "Id": "minimizeall", + "License": "GPL", + "Name": "MinimizeAll", + "Name[ar]": "صغّر الكل", + "Name[be]": "Згортванне ўсіх акон", + "Name[bg]": "Минимизиране всички", + "Name[ca@valencia]": "MinimitzaTot", + "Name[ca]": "MinimitzaTot", + "Name[cs]": "MinimalizovatVše", + "Name[da]": "MinimérAlle", + "Name[de]": "Alle minimieren", + "Name[en_GB]": "MinimiseAll", + "Name[eo]": "Minimumigi Ĉion", + "Name[es]": "Minimizar todo", + "Name[et]": "Kõigi minimeerimine", + "Name[eu]": "Minimizatu denak", + "Name[fi]": "PienennäKaikki", + "Name[fr]": "Tout minimiser", + "Name[gl]": "Minimizalo todo", + "Name[he]": "מזעור הכול", + "Name[hu]": "Összes minimalizálása", + "Name[ia]": "MaximizaOmne", + "Name[id]": "Minimalkan Semua", + "Name[is]": "Fela allt", + "Name[it]": "Minimizza tutto", + "Name[ja]": "すべて最小化", + "Name[ka]": "ყველას ჩაკეცვა", + "Name[ko]": "모두최소화", + "Name[lt]": "Suskleisti visus", + "Name[lv]": "Maksimizēt visu", + "Name[nb]": "Minimer alle", + "Name[nl]": "Alles-minimaliseren", + "Name[nn]": "Minimer alle", + "Name[pl]": "Minimalizuj wszystko", + "Name[pt]": "Maximizar Tudo", + "Name[pt_BR]": "Minimizar tudo", + "Name[ro]": "MinimizeazăTot", + "Name[ru]": "Сворачивание всех окон", + "Name[sa]": "MinimizeAll इति", + "Name[sk]": "Minimalizovať všetko", + "Name[sl]": "Pomanjšaj vse", + "Name[sv]": "Minimera alla", + "Name[ta]": "அனைத்தையும் ஒதுக்கு", + "Name[tr]": "Tümünü Simge Durumuna Küçült", + "Name[uk]": "Мінімізувати усі", + "Name[vi]": "Thu nhỏ tất cả", + "Name[zh_CN]": "最小化全部", + "Name[zh_TW]": "全部最小化" + }, + "X-KDE-ConfigModule": "kcm_kwin4_genericscripted", + "X-KWin-Border-Activate": "true", + "X-Plasma-API": "javascript" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/mouseclick/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/mouseclick/CMakeLists.txt new file mode 100644 index 0000000000..875764baf6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mouseclick/CMakeLists.txt @@ -0,0 +1,40 @@ +####################################### +# Effect + +set(mouseclick_SOURCES + main.cpp + mouseclick.cpp +) + +kconfig_add_kcfg_files(mouseclick_SOURCES + mouseclickconfig.kcfgc +) + +kwin_add_builtin_effect(mouseclick ${mouseclick_SOURCES}) +target_link_libraries(mouseclick PRIVATE + kwin + + KF6::ConfigGui + KF6::GlobalAccel + KF6::I18n +) + +########################## +## configuration dialog +########################## +if (KWIN_BUILD_KCMS) + set(kwin_mouseclick_config_SRCS mouseclick_config.cpp) + ki18n_wrap_ui(kwin_mouseclick_config_SRCS mouseclick_config.ui) + kconfig_add_kcfg_files(kwin_mouseclick_config_SRCS mouseclickconfig.kcfgc) + + kwin_add_effect_config(kwin_mouseclick_config ${kwin_mouseclick_config_SRCS}) + + target_link_libraries(kwin_mouseclick_config + KF6::KCMUtils + KF6::CoreAddons + KF6::GlobalAccel + KF6::I18n + KF6::XmlGui + KWinEffectsInterface + ) +endif() diff --git a/local/recipes/kde/kwin/source/src/plugins/mouseclick/main.cpp b/local/recipes/kde/kwin/source/src/plugins/mouseclick/main.cpp new file mode 100644 index 0000000000..1cac776c3f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mouseclick/main.cpp @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "mouseclick.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY(MouseClickEffect, + "metadata.json.stripped") + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/mouseclick/metadata.json b/local/recipes/kde/kwin/source/src/plugins/mouseclick/metadata.json new file mode 100644 index 0000000000..73d91f1c3f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mouseclick/metadata.json @@ -0,0 +1,94 @@ +{ + "KPlugin": { + "Category": "Accessibility", + "Description": "Creates an animation whenever a mouse button is clicked. This is useful for screenrecordings/presentations. Activated with a keyboard shortcut", + "Description[ar]": "ينشئ حركة عندما يُنقَر زر الفأرة. هذا مفيد لتسجيلات الشاشة/العروض التقديمية. تفعّل باختصار لوحة المفاتيح", + "Description[bg]": "Създава анимация при всяко щракване на бутон на мишката. Това е полезно за екранни записи/презентации. Активирано с клавишна комбинация", + "Description[ca@valencia]": "Crea una animació quan es clica amb un botó del ratolí. Açò és útil per a gravar la pantalla o en presentacions. Activat amb una drecera de teclat", + "Description[ca]": "Crea una animació quan es fa clic amb un botó del ratolí. Això és útil per a enregistrar la pantalla o en presentacions. Activat amb una drecera de teclat", + "Description[da]": "Opretter en animation når der klikkes på en museknap. Dette er brugbart for skærmoptagelser/præsentationer. Aktiveret med en tastaturgenvej", + "Description[de]": "Erzeugt bei jedem Mausklick eine Animation. Dies ist sinnvoll für Bildschirmaufnahmen oder Präsentationen; mit einem Tastatur-Kurzbefehl ausgelöst", + "Description[en_GB]": "Creates an animation whenever a mouse button is clicked. This is useful for screenrecordings/presentations. Activated with a keyboard shortcut", + "Description[eo]": "Kreas animacion kiam ajn musbutono estas klakita. Ĉi tio utilas por ekranregistradoj/prezentoj. Aktivigita per klavara ŝparvojo", + "Description[es]": "Crea una animación cada vez que se pulsa un botón del ratón. Resulta útil durante la grabación de la pantalla y las presentaciones. Se activa con un atajo de teclado.", + "Description[eu]": "Animazio bat sortzen du saguaren botoi bati klik egiterakoan. Erabilgarria da pantaila-grabaketarako/aurkezpenetarako. Teklatuko lasterbide batekin aktibatua", + "Description[fi]": "Näyttää animoinnin aina hiiripainiketta napsautettaessa. Avuksi näytöntallennuksissa ja esityksissä. Käynnistyy pikanäppäimellä", + "Description[fr]": "Crée une animation associée à un clic avec le bouton de la souris. Ceci est utile pour les enregistrements d'écran ou les présentations. Activé grâce à un raccourci clavier.", + "Description[gl]": "Crea unha animación cando se preme o botón do rato. Isto é útil nas presentacións e nas gravacións da pantalla. Actívase cun atallo de teclado.", + "Description[he]": "יוצר הנפשה עם כל לחיצה על סמן העכבר. שימושי להסרטות מסך/מצגות. מופעל דרך צירוף מקשי מקלדת", + "Description[hu]": "Animációt hoz létre, amikor az egérgombbal kattintanak. Ez hasznos a képernyő felvételekor vagy bemutatónál. Gyorsbillentyűvel aktiválható", + "Description[ia]": "Crea un animation quando un button de mus es pressate. Isto es util pro registrationes de schermo/presentationes. Activate con un via breve de claviero", + "Description[is]": "Býr til hreyfiáhrif þegar smellt er með músarhnappi. Þetta er gagnlegt í skjáupptöku eða kynningum. Virkjað með flýtilykli", + "Description[it]": "Crea un'animazione nel momento in cui viene premuto un pulsante del mouse. Ciò è utile per la registrazione dello schermo o per le presentazioni. Attivato con una scorciatoia da tastiera", + "Description[ja]": "マウスボタンがクリックされたときアニメーションを表示します。画面録画やプレゼンテーションの際に便利な機能です。ショートカットで起動します", + "Description[ka]": "ქმნის ანიმაციას, როცა თაგუნას ღილაკს დააწკაპუნებთ. სასარგებლოა ეკრანის ჩაწერისას/პრეზენტაციებისას. აქტიურდება კლავიატურის მალსახმობით", + "Description[ko]": "마우스 단추를 클릭했을 때 애니메이션을 표시합니다. 화면 녹화/프레젠테이션에서 유용합니다. 키보드 단축키로 활성화합니다", + "Description[lt]": "Rodo animaciją kas kartą, kai yra paspaudžiamas pelės mygtukas. Tai naudinga filmuojant ekraną ar rodant pateiktis. Aktyvinama naudojant sparčiuosius klavišus", + "Description[lv]": "Izveido animāciju, kad noklikšķina peles pogu. Noderīgi ekrāna ierakstiem/prezentācijām. Aktivizē ar tastatūras saīsni", + "Description[nb]": "Lager en animasjon hver gang en museknapp trykkes ned. Dette er nyttig for skjermopptak/presentasjoner – slått av/på med hurtigtast", + "Description[nl]": "Maakt een animatie wanneer op een muisknop wordt geklikt. Dit is nuttig voor schermopnamen/presentaties. Geactiveerd met een sneltoets", + "Description[nn]": "Lagar ein animasjon kvar gong ein museknapp vert trykt ned. Dette er nyttig for skjermopptak/presentasjonar. Slått på/av med snøggtast.", + "Description[pl]": "Tworzy animację przy każdym kliknięciu klawiszem myszy. Jest to użyteczne przy nagrywaniu ekranu/prezentacjach. Włączane skrótem klawiszowym", + "Description[pt_BR]": "Cria uma animação cada vez que o botão do mouse é clicado. Isso é útil para apresentações/gravações de tela; ativado com um atalho de teclado", + "Description[ro]": "Creează o animație ori de câte ori e apăsat un buton al șoricelului. Util pentru înregistrări de ecran și prezentări. Activat cu o scurtătură de taste", + "Description[ru]": "Создание анимации при каждом щелчке мышью. Это удобно для записи видео с экрана и для презентаций. Активируется нажатием комбинации клавиш", + "Description[sa]": "यदा कदापि मूषकस्य बटन् क्लिक् भवति तदा एनिमेशनं निर्माति । एतत् स्क्रीनरेकर्डिङ्ग्/प्रस्तुतिषु उपयोगी भवति । कीबोर्ड-शॉर्टकट्-सहितं सक्रियम्", + "Description[sk]": "Vytvorí animáciu vždy, keď sa klikne tlačidlom myši. Toto je užitočné pre prezentácie a záznamy obrazoviek", + "Description[sl]": "Ustvari animacijo, ko kliknete miškin gumb. To je koristno za posnetke zaslona/predstavitve. Aktivirano z bližnjico tipkovnice", + "Description[sv]": "Skapar en animering så fort en musknapp klickas. Det är användbart för skärminspelningar och presentationer. Aktiveras med en snabbtangent", + "Description[tr]": "Ne zaman bir fare düğmesi tıklansa bir canlandırma oluşturur. Bu ekran kaydı ve sunumlar için yararlıdır; klavye kısayoluyla etkinleştirilir", + "Description[uk]": "Створення анімацій у відповідь на клацання кнопкою миші. Корисне для записів демонстрацій та презентацій. Активується клавіатурним скороченням", + "Description[zh_CN]": "鼠标按键按下时显示动效,在进行录屏演示时非常有用;使用键盘快捷键激活", + "Description[zh_TW]": "點擊滑鼠按鍵時顯示一個動畫。這在螢幕錄影或投影片播放時很有用。——需要用鍵盤快捷鍵觸發", + "EnabledByDefault": false, + "License": "GPL", + "Name": "Mouse Click Animation", + "Name[ar]": "حركة نقرة الفأرة", + "Name[az]": "Siçan kliki animasiyası", + "Name[be]": "Анімацыя пстрыкання мышшу", + "Name[bg]": "Анимация на щракванията с мишка", + "Name[ca@valencia]": "Animació quan es clique amb el ratolí", + "Name[ca]": "Animació en fer clic amb el ratolí", + "Name[cs]": "Animace kliknutí myši", + "Name[da]": "Animation for museklik", + "Name[de]": "Animation für Mausklicks", + "Name[en_GB]": "Mouse Click Animation", + "Name[eo]": "Musklaka Animacio", + "Name[es]": "Animación del clic del ratón", + "Name[et]": "Hiireklõpsu animeerimine", + "Name[eu]": "Sagu-klikaren animazioa", + "Name[fi]": "Hiiren napsautuksen animointi", + "Name[fr]": "Animation du clic de la souris", + "Name[gl]": "Animación ao premer o rato", + "Name[he]": "הנפשת לחיצת עכבר", + "Name[hu]": "Egérkattintási animáció", + "Name[ia]": "Animation de click de mus", + "Name[id]": "Animasi Klik Mouse", + "Name[is]": "Hreyfiáhrif við músarsmell", + "Name[it]": "Animazione del clic del mouse", + "Name[ja]": "マウスクリックのアニメーション", + "Name[ka]": "თაგუნას წკაპის ანიმაცია", + "Name[ko]": "마우스 클릭 애니메이션", + "Name[lt]": "Pelės spustelėjimo animacija", + "Name[lv]": "Peles klikšķa animācija", + "Name[nb]": "Museklikkanimasjon", + "Name[nl]": "Animatie van muisklik", + "Name[nn]": "Museklikkanimasjon", + "Name[pl]": "Animacja kliknięcia myszą", + "Name[pt]": "Animação do Botão do Rato", + "Name[pt_BR]": "Animação de clique do mouse", + "Name[ro]": "Animație la clic de maus", + "Name[ru]": "Анимация щелчка мышью", + "Name[sa]": "माउस क्लिक् एनिमेशन", + "Name[sk]": "Animácia kliknutia myšou", + "Name[sl]": "Animacija klika z miško", + "Name[sv]": "Animering av musklick", + "Name[ta]": "சுட்டி கிளிக் அசைவூட்டம்", + "Name[tr]": "Fare Tıklama Canlandırması", + "Name[uk]": "Анімація за клацанням миші", + "Name[vi]": "Hiệu ứng động khi bấm chuột", + "Name[zh_CN]": "鼠标点击动效", + "Name[zh_TW]": "滑鼠點擊動畫" + }, + "X-KDE-ConfigModule": "kwin_mouseclick_config" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick.cpp b/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick.cpp new file mode 100644 index 0000000000..724e6daf57 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick.cpp @@ -0,0 +1,421 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Filip Wieladek + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "mouseclick.h" +// KConfigSkeleton +#include "mouseclickconfig.h" + +#include "core/inputdevice.h" +#include "core/rendertarget.h" +#include "core/renderviewport.h" +#include "effect/effecthandler.h" +#include "input_event.h" + +#include + +#include +#include + +#include + +#include + +namespace KWin +{ + +MouseClickEffect::MouseClickEffect() +{ + MouseClickConfig::instance(effects->config()); + m_enabled = false; + + QAction *a = new QAction(this); + a->setObjectName(QStringLiteral("ToggleMouseClick")); + a->setText(i18n("Toggle Mouse Click Animation")); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << (Qt::META | Qt::Key_Asterisk)); + KGlobalAccel::self()->setShortcut(a, QList() << (Qt::META | Qt::Key_Asterisk)); + connect(a, &QAction::triggered, this, &MouseClickEffect::toggleEnabled); + + reconfigure(ReconfigureAll); + + m_buttons[0] = std::make_unique(i18nc("Left mouse button", "Left"), Qt::LeftButton); + m_buttons[1] = std::make_unique(i18nc("Middle mouse button", "Middle"), Qt::MiddleButton); + m_buttons[2] = std::make_unique(i18nc("Right mouse button", "Right"), Qt::RightButton); +} + +MouseClickEffect::~MouseClickEffect() +{ +} + +void MouseClickEffect::reconfigure(ReconfigureFlags) +{ + MouseClickConfig::self()->read(); + m_colors[0] = MouseClickConfig::color1(); + m_colors[1] = MouseClickConfig::color2(); + m_colors[2] = MouseClickConfig::color3(); + m_lineWidth = MouseClickConfig::lineWidth(); + m_ringLife = MouseClickConfig::ringLife(); + m_ringMaxSize = MouseClickConfig::ringSize(); + m_ringCount = MouseClickConfig::ringCount(); + m_showText = MouseClickConfig::showText(); + m_font = MouseClickConfig::font(); +} + +void MouseClickEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + const int time = m_lastPresentTime.count() ? (presentTime - m_lastPresentTime).count() : 0; + + for (auto &click : m_clicks) { + click->m_time += time; + } + + for (int i = 0; i < BUTTON_COUNT; ++i) { + if (m_buttons[i]->m_isPressed) { + m_buttons[i]->m_time += time; + } + } + + while (m_clicks.size() > 0) { + if (m_clicks.front()->m_time <= m_ringLife) { + break; + } + m_clicks.pop_front(); + } + + if (isActive()) { + m_lastPresentTime = presentTime; + } else { + m_lastPresentTime = std::chrono::milliseconds::zero(); + } + + effects->prePaintScreen(data, presentTime); +} + +void MouseClickEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) +{ + effects->paintScreen(renderTarget, viewport, mask, deviceRegion, screen); + + if (effects->isOpenGLCompositing()) { + paintScreenSetupGl(renderTarget, viewport.projectionMatrix()); + } + for (const auto &click : m_clicks) { + for (int i = 0; i < m_ringCount; ++i) { + float alpha = computeAlpha(click.get(), i); + float size = computeRadius(click.get(), i); + if (size > 0 && alpha > 0) { + QColor color = m_colors[click->m_button]; + color.setAlphaF(alpha); + drawCircle(viewport, color, click->m_pos.x(), click->m_pos.y(), size); + } + } + + if (m_showText && click->m_frame) { + float frameAlpha = (click->m_time * 2.0f - m_ringLife) / m_ringLife; + frameAlpha = frameAlpha < 0 ? 1 : -(frameAlpha * frameAlpha) + 1; + click->m_frame->render(renderTarget, viewport, Region::infinite(), frameAlpha, frameAlpha); + } + } + for (const auto &tool : std::as_const(m_tabletTools)) { + const int step = m_ringMaxSize * (1. - tool.m_pressure); + for (qreal size = m_ringMaxSize; size > 0; size -= step) { + drawCircle(viewport, tool.m_color, tool.m_globalPosition.x(), tool.m_globalPosition.y(), size); + } + } + if (effects->isOpenGLCompositing()) { + paintScreenFinishGl(); + } +} + +void MouseClickEffect::postPaintScreen() +{ + effects->postPaintScreen(); + repaint(); +} + +float MouseClickEffect::computeRadius(const MouseClickMouseEvent *click, int ring) +{ + float ringDistance = m_ringLife / (m_ringCount * 3); + if (click->m_press) { + return ((click->m_time - ringDistance * ring) / m_ringLife) * m_ringMaxSize; + } + return ((m_ringLife - click->m_time - ringDistance * ring) / m_ringLife) * m_ringMaxSize; +} + +float MouseClickEffect::computeAlpha(const MouseClickMouseEvent *click, int ring) +{ + float ringDistance = m_ringLife / (m_ringCount * 3); + return (m_ringLife - (float)click->m_time - ringDistance * (ring)) / m_ringLife; +} + +void MouseClickEffect::slotMouseChanged(const QPointF &pos, const QPointF &, + Qt::MouseButtons buttons, Qt::MouseButtons oldButtons, + Qt::KeyboardModifiers, Qt::KeyboardModifiers) +{ + if (buttons == oldButtons) { + return; + } + + std::unique_ptr m; + int i = BUTTON_COUNT; + while (--i >= 0) { + MouseButton *b = m_buttons[i].get(); + if (isPressed(b->m_button, buttons, oldButtons)) { + m = std::make_unique(i, pos.toPoint(), 0, createEffectFrame(pos.toPoint(), b->m_labelDown), true); + break; + } else if (isReleased(b->m_button, buttons, oldButtons) && (!b->m_isPressed || b->m_time > m_ringLife)) { + // we might miss a press, thus also check !b->m_isPressed, bug #314762 + m = std::make_unique(i, pos.toPoint(), 0, createEffectFrame(pos.toPoint(), b->m_labelUp), false); + break; + } + b->setPressed(b->m_button & buttons); + } + + if (m) { + m_clicks.push_back(std::move(m)); + } + repaint(); +} + +std::unique_ptr MouseClickEffect::createEffectFrame(const QPoint &pos, const QString &text) +{ + if (!m_showText) { + return nullptr; + } + QPoint point(pos.x() + m_ringMaxSize, pos.y()); + std::unique_ptr frame = std::make_unique(EffectFrameStyled, false, point, Qt::AlignLeft); + frame->setFont(m_font); + frame->setText(text); + return frame; +} + +void MouseClickEffect::repaint() +{ + if (m_clicks.size() > 0) { + Region dirtyRegion; + const int radius = m_ringMaxSize + m_lineWidth; + for (auto &click : m_clicks) { + dirtyRegion += QRect(click->m_pos.x() - radius, click->m_pos.y() - radius, 2 * radius, 2 * radius); + if (click->m_frame) { + dirtyRegion += click->m_frame->geometry(); + } + } + effects->addRepaint(dirtyRegion); + } + if (!m_tabletTools.isEmpty()) { + Region dirtyRegion; + const int radius = m_ringMaxSize + m_lineWidth; + for (const auto &event : std::as_const(m_tabletTools)) { + dirtyRegion += QRect(event.m_globalPosition.x() - radius, event.m_globalPosition.y() - radius, 2 * radius, 2 * radius); + } + effects->addRepaint(dirtyRegion); + } +} + +bool MouseClickEffect::isReleased(Qt::MouseButtons button, Qt::MouseButtons buttons, Qt::MouseButtons oldButtons) +{ + return !(button & buttons) && (button & oldButtons); +} + +bool MouseClickEffect::isPressed(Qt::MouseButtons button, Qt::MouseButtons buttons, Qt::MouseButtons oldButtons) +{ + return (button & buttons) && !(button & oldButtons); +} + +void MouseClickEffect::toggleEnabled() +{ + m_enabled = !m_enabled; + + if (m_enabled) { + connect(effects, &EffectsHandler::mouseChanged, this, &MouseClickEffect::slotMouseChanged); + } else { + disconnect(effects, &EffectsHandler::mouseChanged, this, &MouseClickEffect::slotMouseChanged); + } + + m_clicks.clear(); + m_tabletTools.clear(); + + for (int i = 0; i < BUTTON_COUNT; ++i) { + m_buttons[i]->m_time = 0; + m_buttons[i]->m_isPressed = false; + } +} + +bool MouseClickEffect::isActive() const +{ + return m_enabled && (m_clicks.size() != 0 || !m_tabletTools.isEmpty()); +} + +void MouseClickEffect::drawCircle(const RenderViewport &viewport, const QColor &color, float cx, float cy, float r) +{ + if (effects->isOpenGLCompositing()) { + drawCircleGl(viewport, color, cx, cy, r); + } else if (effects->compositingType() == QPainterCompositing) { + drawCircleQPainter(color, cx, cy, r); + } +} + +void MouseClickEffect::drawCircleGl(const RenderViewport &viewport, const QColor &color, float cx, float cy, float r) +{ + static const int num_segments = 80; + static const float theta = 2 * 3.1415926 / float(num_segments); + static const float c = cosf(theta); // precalculate the sine and cosine + static const float s = sinf(theta); + const float scale = viewport.scale(); + float t; + + float x = r; // we start at angle = 0 + float y = 0; + + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + QList verts; + verts.reserve(num_segments * 2); + + for (int ii = 0; ii < num_segments; ++ii) { + verts.push_back(QVector2D((x + cx) * scale, (y + cy) * scale)); // output vertex + // apply the rotation matrix + t = x; + x = c * x - s * y; + y = s * t + c * y; + } + vbo->setVertices(verts); + ShaderManager::instance()->getBoundShader()->setUniform(GLShader::ColorUniform::Color, color); + vbo->render(GL_LINE_LOOP); +} + +void MouseClickEffect::drawCircleQPainter(const QColor &color, float cx, float cy, float r) +{ + QPainter *painter = effects->scenePainter(); + painter->save(); + painter->setPen(color); + painter->drawArc(cx - r, cy - r, r * 2, r * 2, 0, 5760); + painter->restore(); +} + +void MouseClickEffect::paintScreenSetupGl(const RenderTarget &renderTarget, const QMatrix4x4 &projectionMatrix) +{ + GLShader *shader = ShaderManager::instance()->pushShader(ShaderTrait::UniformColor | ShaderTrait::TransformColorspace); + shader->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, projectionMatrix); + shader->setColorspaceUniforms(ColorDescription::sRGB, renderTarget.colorDescription(), RenderingIntent::Perceptual); + + glLineWidth(m_lineWidth); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); +} + +void MouseClickEffect::paintScreenFinishGl() +{ + glDisable(GL_BLEND); + + ShaderManager::instance()->popShader(); +} + +TabletToolEvent &MouseClickEffect::getOrCreateTabletPoint(InputDeviceTabletTool *tool) +{ + auto &point = m_tabletTools[tool]; + if (!point.m_color.isValid()) { + switch (tool->type()) { + case InputDeviceTabletTool::Type::Finger: + case InputDeviceTabletTool::Type::Pen: + case InputDeviceTabletTool::Pencil: + case InputDeviceTabletTool::Brush: + case InputDeviceTabletTool::Airbrush: + case InputDeviceTabletTool::Lens: + case InputDeviceTabletTool::Totem: + point.m_color = MouseClickConfig::color1(); + break; + case InputDeviceTabletTool::Type::Eraser: + point.m_color = MouseClickConfig::color2(); + break; + case KWin::InputDeviceTabletTool::Type::Mouse: + point.m_color = MouseClickConfig::color3(); + break; + break; + } + } + return point; +} + +bool MouseClickEffect::tabletToolProximity(TabletToolProximityEvent *event) +{ + if (event->type == TabletToolProximityEvent::LeaveProximity) { + m_tabletTools.remove(event->tool); + } + return false; +} + +bool MouseClickEffect::tabletToolAxis(TabletToolAxisEvent *event) +{ + auto &point = getOrCreateTabletPoint(event->tool); + point.m_globalPosition = event->position; + point.m_pressure = event->pressure; + return false; +} + +bool MouseClickEffect::tabletToolTip(TabletToolTipEvent *event) +{ + auto &point = getOrCreateTabletPoint(event->tool); + point.m_pressed = event->type == TabletToolTipEvent::Press; + point.m_pressure = event->pressure; + point.m_globalPosition = event->position; + return false; +} + +QColor MouseClickEffect::color1() const +{ + return m_colors[0]; +} + +QColor MouseClickEffect::color2() const +{ + return m_colors[1]; +} + +QColor MouseClickEffect::color3() const +{ + return m_colors[2]; +} + +qreal MouseClickEffect::lineWidth() const +{ + return m_lineWidth; +} + +int MouseClickEffect::ringLife() const +{ + return m_ringLife; +} + +int MouseClickEffect::ringSize() const +{ + return m_ringMaxSize; +} + +int MouseClickEffect::ringCount() const +{ + return m_ringCount; +} + +bool MouseClickEffect::isShowText() const +{ + return m_showText; +} + +QFont MouseClickEffect::font() const +{ + return m_font; +} + +bool MouseClickEffect::isEnabled() const +{ + return m_enabled; +} + +} // namespace + +#include "moc_mouseclick.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick.h b/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick.h new file mode 100644 index 0000000000..04cc2f110a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick.h @@ -0,0 +1,165 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Filip Wieladek + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/effect.h" +#include "effect/effectframe.h" +#include "opengl/glutils.h" +#include +#include +#include +#include + +namespace KWin +{ + +#define BUTTON_COUNT 3 + +class EffectFrame; +class InputDeviceTabletTool; + +class MouseClickMouseEvent +{ +public: + MouseClickMouseEvent(int button, QPoint point, int time, std::unique_ptr &&frame, bool press) + : m_button(button) + , m_pos(point) + , m_time(time) + , m_frame(std::move(frame)) + , m_press(press){}; + + int m_button; + QPoint m_pos; + int m_time; + std::unique_ptr m_frame; + bool m_press; +}; + +class TabletToolEvent +{ +public: + QPointF m_globalPosition; + bool m_pressed = false; + qreal m_pressure = 0; + QColor m_color; +}; + +class MouseButton +{ +public: + MouseButton(QString label, Qt::MouseButtons button) + : m_labelUp(label) + , m_labelDown(label) + , m_button(button) + , m_isPressed(false) + , m_time(0) + { + m_labelDown.append(i18n("↓")); + m_labelUp.append(i18n("↑")); + }; + + inline void setPressed(bool pressed) + { + if (m_isPressed != pressed) { + m_isPressed = pressed; + if (pressed) { + m_time = 0; + } + } + } + + QString m_labelUp; + QString m_labelDown; + Qt::MouseButtons m_button; + bool m_isPressed; + int m_time; +}; + +class MouseClickEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(QColor color1 READ color1) + Q_PROPERTY(QColor color2 READ color2) + Q_PROPERTY(QColor color3 READ color3) + Q_PROPERTY(qreal lineWidth READ lineWidth) + Q_PROPERTY(int ringLife READ ringLife) + Q_PROPERTY(int ringSize READ ringSize) + Q_PROPERTY(int ringCount READ ringCount) + Q_PROPERTY(bool showText READ isShowText) + Q_PROPERTY(QFont font READ font) + Q_PROPERTY(bool enabled READ isEnabled) +public: + MouseClickEffect(); + ~MouseClickEffect() override; + void reconfigure(ReconfigureFlags) override; + void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override; + void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) override; + void postPaintScreen() override; + bool isActive() const override; + + // for properties + QColor color1() const; + QColor color2() const; + QColor color3() const; + qreal lineWidth() const; + int ringLife() const; + int ringSize() const; + int ringCount() const; + bool isShowText() const; + QFont font() const; + bool isEnabled() const; + + bool tabletToolProximity(TabletToolProximityEvent *event) override; + bool tabletToolAxis(TabletToolAxisEvent *event) override; + bool tabletToolTip(TabletToolTipEvent *event) override; + +private Q_SLOTS: + void toggleEnabled(); + void slotMouseChanged(const QPointF &pos, const QPointF &old, + Qt::MouseButtons buttons, Qt::MouseButtons oldbuttons, + Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers); + +private: + std::unique_ptr createEffectFrame(const QPoint &pos, const QString &text); + inline void drawCircle(const RenderViewport &viewport, const QColor &color, float cx, float cy, float r); + + inline bool isReleased(Qt::MouseButtons button, Qt::MouseButtons buttons, Qt::MouseButtons oldButtons); + inline bool isPressed(Qt::MouseButtons button, Qt::MouseButtons buttons, Qt::MouseButtons oldButtons); + + inline float computeRadius(const MouseClickMouseEvent *click, int ring); + inline float computeAlpha(const MouseClickMouseEvent *click, int ring); + + void repaint(); + + void drawCircleGl(const RenderViewport &viewport, const QColor &color, float cx, float cy, float r); + void drawCircleQPainter(const QColor &color, float cx, float cy, float r); + void paintScreenSetupGl(const RenderTarget &renderTarget, const QMatrix4x4 &projectionMatrix); + void paintScreenFinishGl(); + + TabletToolEvent &getOrCreateTabletPoint(InputDeviceTabletTool *tool); + + QColor m_colors[BUTTON_COUNT]; + int m_ringCount; + float m_lineWidth; + float m_ringLife; + float m_ringMaxSize; + bool m_showText; + QFont m_font; + std::chrono::milliseconds m_lastPresentTime = std::chrono::milliseconds::zero(); + + std::deque> m_clicks; + std::unique_ptr m_buttons[BUTTON_COUNT]; + QHash m_tabletTools; + + bool m_enabled; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick.kcfg b/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick.kcfg new file mode 100644 index 0000000000..d67c39187b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick.kcfg @@ -0,0 +1,34 @@ + + + + + + QColor(Qt::red) + + + QColor(Qt::green) + + + QColor(Qt::blue) + + + 1.0 + + + 300 + + + 20 + + + 2 + + + true + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick_config.cpp b/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick_config.cpp new file mode 100644 index 0000000000..e6438338ba --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick_config.cpp @@ -0,0 +1,68 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Filip Wieladek + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "mouseclick_config.h" + +#include "config-kwin.h" + +// KConfigSkeleton +#include "mouseclickconfig.h" +#include + +#include + +#include +#include +#include +#include + +#include + +K_PLUGIN_CLASS(KWin::MouseClickEffectConfig) + +namespace KWin +{ + +MouseClickEffectConfig::MouseClickEffectConfig(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) +{ + m_ui.setupUi(widget()); + + connect(m_ui.editor, &KShortcutsEditor::keyChange, this, &KCModule::markAsChanged); + + // Shortcut config. The shortcut belongs to the component "kwin"! + m_actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + m_actionCollection->setComponentDisplayName(i18n("KWin")); + + QAction *a = m_actionCollection->addAction(QStringLiteral("ToggleMouseClick")); + a->setText(i18n("Toggle Mouse Click Animation")); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << (Qt::META | Qt::Key_Asterisk)); + KGlobalAccel::self()->setShortcut(a, QList() << (Qt::META | Qt::Key_Asterisk)); + + m_ui.editor->addCollection(m_actionCollection); + + MouseClickConfig::instance(KWIN_CONFIG); + addConfig(MouseClickConfig::self(), widget()); +} + +void MouseClickEffectConfig::save() +{ + KCModule::save(); + m_ui.editor->save(); // undo() will restore to this state from now on + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("mouseclick")); +} + +} // namespace + +#include "mouseclick_config.moc" + +#include "moc_mouseclick_config.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick_config.h b/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick_config.h new file mode 100644 index 0000000000..6fe11ef411 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick_config.h @@ -0,0 +1,34 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Filip Wieladek + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "ui_mouseclick_config.h" + +class KActionCollection; + +namespace KWin +{ + +class MouseClickEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit MouseClickEffectConfig(QObject *parent, const KPluginMetaData &data); + + void save() override; + +private: + Ui::MouseClickEffectConfigForm m_ui; + KActionCollection *m_actionCollection; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick_config.ui b/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick_config.ui new file mode 100644 index 0000000000..28d8e16898 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclick_config.ui @@ -0,0 +1,282 @@ + + + KWin::MouseClickEffectConfigForm + + + + 0 + 0 + 335 + 378 + + + + + + + 0 + + + + Basic Settings + + + + + + + 0 + 0 + + + + + + + + Left Mouse Button Color: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Color1 + + + + + + + Middle Mouse Button Color: + + + kcfg_Color2 + + + + + + + + 0 + 0 + + + + + + + + Right Mouse Button Color: + + + kcfg_Color3 + + + + + + + + 0 + 0 + + + + + + + + + Advanced Settings + + + + + + Rings + + + + + + Line Width: + + + kcfg_LineWidth + + + + + + + + 0 + 0 + + + + pixel + + + + + + + + 0 + 0 + + + + msec + + + 50 + + + 5000 + + + + + + + Ring Duration: + + + kcfg_RingLife + + + + + + + Ring Radius: + + + kcfg_RingSize + + + + + + + + 0 + 0 + + + + pixel + + + 1 + + + 1000 + + + + + + + Ring Count: + + + kcfg_RingCount + + + + + + + + 0 + 0 + + + + 1 + + + + + + + + + + Text + + + + + + Font: + + + + + + + + + + + + + + + + + Show Text: + + + kcfg_ShowText + + + + + + + + + + + + + + + 0 + 0 + + + + KShortcutsEditor::GlobalAction + + + + + + + + KColorCombo + QComboBox +
kcolorcombo.h
+
+ + KFontRequester + QWidget +
kfontrequester.h
+
+ + KShortcutsEditor + QWidget +
KShortcutsEditor
+ 1 +
+
+ + +
diff --git a/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclickconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclickconfig.kcfgc new file mode 100644 index 0000000000..cdfae15782 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mouseclick/mouseclickconfig.kcfgc @@ -0,0 +1,5 @@ +File=mouseclick.kcfg +ClassName=MouseClickConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/local/recipes/kde/kwin/source/src/plugins/mousemark/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/mousemark/CMakeLists.txt new file mode 100644 index 0000000000..e067781317 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mousemark/CMakeLists.txt @@ -0,0 +1,46 @@ +####################################### +# Effect + +set(mousemark_SOURCES + main.cpp + mousemark.cpp +) + +kconfig_add_kcfg_files(mousemark_SOURCES + mousemarkconfig.kcfgc +) + +kwin_add_builtin_effect(mousemark ${mousemark_SOURCES}) +target_link_libraries(mousemark PRIVATE + kwin + + KF6::ConfigGui + KF6::GlobalAccel + KF6::I18n +) + +ecm_qt_declare_logging_category(mousemark + HEADER mousemarklogging.h + IDENTIFIER KWIN_MOUSEMARK + CATEGORY_NAME kwin_effect_mousemark + DEFAULT_SEVERITY Warning +) + +####################################### +# Config +if (KWIN_BUILD_KCMS) + set(kwin_mousemark_config_SRCS mousemark_config.cpp) + ki18n_wrap_ui(kwin_mousemark_config_SRCS mousemark_config.ui) + kconfig_add_kcfg_files(kwin_mousemark_config_SRCS mousemarkconfig.kcfgc) + + kwin_add_effect_config(kwin_mousemark_config ${kwin_mousemark_config_SRCS}) + + target_link_libraries(kwin_mousemark_config + KF6::KCMUtils + KF6::CoreAddons + KF6::GlobalAccel + KF6::I18n + KF6::XmlGui + KWinEffectsInterface + ) +endif() diff --git a/local/recipes/kde/kwin/source/src/plugins/mousemark/main.cpp b/local/recipes/kde/kwin/source/src/plugins/mousemark/main.cpp new file mode 100644 index 0000000000..020b81995c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mousemark/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + SPDX-FileCopyrightText: 2023 Andrew Shark + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "mousemark.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY(MouseMarkEffect, + "metadata.json.stripped") + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/mousemark/metadata.json b/local/recipes/kde/kwin/source/src/plugins/mousemark/metadata.json new file mode 100644 index 0000000000..cb9428d5c7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mousemark/metadata.json @@ -0,0 +1,95 @@ +{ + "KPlugin": { + "Category": "Appearance", + "Description": "Allows you to draw lines on the desktop; activated with a keyboard shortcut", + "Description[ar]": "يسمح لك برسم خطوط على سطح المكتب، تفعّل باختصار لوحة المفاتيح", + "Description[bg]": "Позволява ви да чертаете линии на работния плот, активирано с клавишна комбинация", + "Description[ca@valencia]": "Permet dibuixar línies en l'escriptori; activat amb una drecera de teclat", + "Description[ca]": "Permet dibuixar línies a l'escriptori; activat amb una drecera de teclat", + "Description[da]": "Tillader dig at tegne linjer på skrivebordet; aktiveret med en tastaturgenvej", + "Description[de]": "Lässt Sie mit der Maus Linien auf die Arbeitsfläche zeichnen; mit einem Tastatur-Kurzbefehl ausgelöst", + "Description[en_GB]": "Allows you to draw lines on the desktop; activated with a keyboard shortcut", + "Description[eo]": "Permesas al vi desegni liniojn sur la labortablo; aktivigita per klavara ŝparvojo", + "Description[es]": "Le permite dibujar líneas en el escritorio; se activa con un atajo de teclado", + "Description[eu]": "Mahaigainean marrak marrazteko aukera ematen dizu; ; teklatuko lasterbide batekin aktibatua", + "Description[fi]": "Antaa piirtää viivoja työpöydälle; käynnistyy pikanäppäimellä", + "Description[fr]": "Vous permet de tracer des lignes sur le bureau. Activé grâce à un raccourci clavier.", + "Description[gl]": "Permítelle debuxar riscos no escritorio; actívase cun atallo de teclado.", + "Description[he]": "מאפשר לך לצייר קווים על גבי שולחן העבודה, מופעל דרך צירוף מקשי מקלדת", + "Description[hu]": "Lehetővé teszi vonalak rajzolását az asztalon; gyorsbillentyűvel aktiválható", + "Description[ia]": "Permitte te designar lineas super le scriptorio; activate con un via breve de claviero", + "Description[is]": "Gerir þér kleift að teikna línur á skjáborðið; virkjað með flýtilykli", + "Description[it]": "Consente di tracciare linee sul desktop; attivato con una scorciatoia da tastiera", + "Description[ja]": "デスクトップに線を描くことができます。ショートカットで起動します", + "Description[ka]": "სამუშაო მაგიდაზე ხაზების დახატვის უფლება. აქტიურდება კლავიატურის მალსახმობით", + "Description[ko]": "바탕 화면에 선 그리기, 키보드 단축키로 활성화", + "Description[lt]": "Leidžia piešti linijas ant darbalaukio; aktyvinama naudojant sparčiuosius klavišus", + "Description[lv]": "Ļauj darbvirsmā zīmēt līnijas; ieslēdz ar tastatūras saīsni", + "Description[nb]": "lar deg tegne linjer på skjermen – slått av/på med hurtigtast", + "Description[nl]": "Laat u lijnen tekenen op het bureaublad; geactiveerd met een sneltoets", + "Description[nn]": "La deg teikna linjer på skjermen – slått på/av med snøggtast", + "Description[pl]": "Umożliwia rysowanie linii na pulpicie; uruchamiane skrótem klawiszowym", + "Description[pt_BR]": "Permite-lhe desenhar linhas na sua área de trabalho; ativado com um atalho de teclado", + "Description[ro]": "Vă permite să desenați linii pe birou; activat cu o scurtătură de taste", + "Description[ru]": "Рисование на экране курсором мыши; активируется нажатием комбинации клавиш", + "Description[sa]": "डेस्कटॉप् मध्ये रेखाः आकर्षितुं शक्नोति; कीबोर्ड-शॉर्टकट्-सहितं सक्रियम्", + "Description[sk]": "Umožňuje myšou kresliť čiary na ploche", + "Description[sl]": "Omogoča risanje črt na namizju; aktivirano z bližnjico tipkovnice", + "Description[sv]": "Låter dig rita linjer på skrivbordet, aktiveras med en snabbtangent", + "Description[tr]": "Masaüstünde çizgiler çizmenize izin verir; klavye kısayoluyla etkinleştirilir", + "Description[uk]": "Можливість малювання ліній на стільниці; активується клавіатурним скороченням", + "Description[zh_CN]": "用于在桌面上绘制线条;使用键盘快捷键激活", + "Description[zh_TW]": "讓您能在桌面上畫線——需要用鍵盤快捷鍵觸發", + "EnabledByDefault": false, + "License": "GPL", + "Name": "Mouse Mark", + "Name[ar]": "علامة الفأرة", + "Name[ast]": "Marca del mur", + "Name[az]": "Siçanla rəsmçəkmə", + "Name[be]": "Пазначэнне мышшу", + "Name[bg]": "Маркиране с мишката", + "Name[ca@valencia]": "Marca amb el ratolí", + "Name[ca]": "Marca amb el ratolí", + "Name[cs]": "Značkovač", + "Name[da]": "Musemærke", + "Name[de]": "Mausspur", + "Name[en_GB]": "Mouse Mark", + "Name[eo]": "Muso Marko", + "Name[es]": "Marcar con el ratón", + "Name[et]": "Hiirejälg", + "Name[eu]": "Saguaren arrastoa", + "Name[fi]": "Hiiren jäljet", + "Name[fr]": "Tracé à la souris", + "Name[gl]": "Marca do rato", + "Name[he]": "סימון עכבר", + "Name[hu]": "Egérnyom", + "Name[ia]": "Marca de mus", + "Name[id]": "Coretan Mouse", + "Name[is]": "Músarmerki", + "Name[it]": "Pennarello", + "Name[ja]": "マウスマーク", + "Name[ka]": "თაგუნას ნიშანი", + "Name[ko]": "마우스 자취", + "Name[lt]": "Žymėjimas pele", + "Name[lv]": "Atzīmēšana ar peli", + "Name[nb]": "Muselinjer", + "Name[nl]": "Muismarkering", + "Name[nn]": "Muselinjer", + "Name[pl]": "Znacznik myszy", + "Name[pt]": "Marcação com o Rato", + "Name[pt_BR]": "Anotar com o mouse", + "Name[ro]": "Urme de maus", + "Name[ru]": "Рисование мышью", + "Name[sa]": "मूषकचिह्न", + "Name[sk]": "Stopa myši", + "Name[sl]": "Oznaka miške", + "Name[sv]": "Markera med musen", + "Name[ta]": "சுட்டியை குறி", + "Name[tr]": "Fare İmi", + "Name[uk]": "Позначки мишкою", + "Name[vi]": "Vết chuột", + "Name[zh_CN]": "鼠标屏幕画线", + "Name[zh_TW]": "滑鼠標記" + }, + "X-KDE-ConfigModule": "kwin_mousemark_config" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark.cpp b/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark.cpp new file mode 100644 index 0000000000..558e5d07ab --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark.cpp @@ -0,0 +1,275 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + SPDX-FileCopyrightText: 2023 Andrew Shark + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "mousemark.h" +#include "mousemarklogging.h" + +// KConfigSkeleton +#include "mousemarkconfig.h" + +#include "core/rendertarget.h" +#include "core/renderviewport.h" +#include "effect/effecthandler.h" +#include "opengl/glplatform.h" +#include +#include +#include + +#include + +#include + +namespace KWin +{ + +static consteval QPoint nullPoint() +{ + return QPoint(-1, -1); +} + +MouseMarkEffect::MouseMarkEffect() +{ + MouseMarkConfig::instance(effects->config()); + QAction *a = new QAction(this); + a->setObjectName(QStringLiteral("ClearMouseMarks")); + a->setText(i18n("Clear All Mouse Marks")); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << (Qt::SHIFT | Qt::META | Qt::Key_F11)); + KGlobalAccel::self()->setShortcut(a, QList() << (Qt::SHIFT | Qt::META | Qt::Key_F11)); + connect(a, &QAction::triggered, this, &MouseMarkEffect::clear); + a = new QAction(this); + a->setObjectName(QStringLiteral("ClearLastMouseMark")); + a->setText(i18n("Clear Last Mouse Mark")); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << (Qt::SHIFT | Qt::META | Qt::Key_F12)); + KGlobalAccel::self()->setShortcut(a, QList() << (Qt::SHIFT | Qt::META | Qt::Key_F12)); + connect(a, &QAction::triggered, this, &MouseMarkEffect::clearLast); + + connect(effects, &EffectsHandler::mouseChanged, this, &MouseMarkEffect::slotMouseChanged); + connect(effects, &EffectsHandler::screenLockingChanged, this, &MouseMarkEffect::screenLockingChanged); + reconfigure(ReconfigureAll); + arrow_tail = nullPoint(); +} + +MouseMarkEffect::~MouseMarkEffect() +{ +} + +static int width_2 = 1; +void MouseMarkEffect::reconfigure(ReconfigureFlags) +{ + m_freedraw_modifiers = Qt::KeyboardModifiers(); + m_arrowdraw_modifiers = Qt::KeyboardModifiers(); + MouseMarkConfig::self()->read(); + width = MouseMarkConfig::lineWidth(); + width_2 = width / 2; + color = MouseMarkConfig::color(); + color.setAlphaF(1.0); + if (MouseMarkConfig::freedrawshift()) { + m_freedraw_modifiers |= Qt::ShiftModifier; + } + if (MouseMarkConfig::freedrawalt()) { + m_freedraw_modifiers |= Qt::AltModifier; + } + if (MouseMarkConfig::freedrawcontrol()) { + m_freedraw_modifiers |= Qt::ControlModifier; + } + if (MouseMarkConfig::freedrawmeta()) { + m_freedraw_modifiers |= Qt::MetaModifier; + } + + if (MouseMarkConfig::arrowdrawshift()) { + m_arrowdraw_modifiers |= Qt::ShiftModifier; + } + if (MouseMarkConfig::arrowdrawalt()) { + m_arrowdraw_modifiers |= Qt::AltModifier; + } + if (MouseMarkConfig::arrowdrawcontrol()) { + m_arrowdraw_modifiers |= Qt::ControlModifier; + } + if (MouseMarkConfig::arrowdrawmeta()) { + m_arrowdraw_modifiers |= Qt::MetaModifier; + } +} + +void MouseMarkEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) +{ + effects->paintScreen(renderTarget, viewport, mask, deviceRegion, screen); // paint normal screen + if (marks.isEmpty() && drawing.isEmpty()) { + return; + } + if (const auto context = effects->openglContext()) { + if (!context->isOpenGLES()) { + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glEnable(GL_LINE_SMOOTH); + glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); + } + glLineWidth(width); + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + const auto scale = viewport.scale(); + ShaderBinder binder(ShaderTrait::UniformColor | ShaderTrait::TransformColorspace); + binder.shader()->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, viewport.projectionMatrix()); + binder.shader()->setColorspaceUniforms(ColorDescription::sRGB, renderTarget.colorDescription(), RenderingIntent::Perceptual); + binder.shader()->setUniform(GLShader::ColorUniform::Color, color); + QList verts; + for (const Mark &mark : std::as_const(marks)) { + verts.clear(); + verts.reserve(mark.size()); + for (const QPointF &p : std::as_const(mark)) { + verts.push_back(QVector2D(p.x() * scale, p.y() * scale)); + } + vbo->setVertices(verts); + vbo->render(GL_LINE_STRIP); + } + if (!drawing.isEmpty()) { + verts.clear(); + verts.reserve(drawing.size()); + for (const QPointF &p : std::as_const(drawing)) { + verts.push_back(QVector2D(p.x() * scale, p.y() * scale)); + } + vbo->setVertices(verts); + vbo->render(GL_LINE_STRIP); + } + glLineWidth(1.0); + if (!context->isOpenGLES()) { + glDisable(GL_LINE_SMOOTH); + glDisable(GL_BLEND); + } + } else if (effects->compositingType() == QPainterCompositing) { + QPainter *painter = effects->scenePainter(); + painter->save(); + QPen pen(color); + pen.setWidth(width); + painter->setPen(pen); + for (const Mark &mark : std::as_const(marks)) { + drawMark(painter, mark); + } + drawMark(painter, drawing); + painter->restore(); + } +} + +void MouseMarkEffect::drawMark(QPainter *painter, const Mark &mark) +{ + if (mark.count() <= 1) { + return; + } + for (int i = 0; i < mark.count() - 1; ++i) { + painter->drawLine(mark[i], mark[i + 1]); + } +} + +void MouseMarkEffect::slotMouseChanged(const QPointF &pos, const QPointF &, + Qt::MouseButtons, Qt::MouseButtons, + Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers) +{ + if (effects->isScreenLocked()) { + return; + } + qCDebug(KWIN_MOUSEMARK) << "MouseChanged" << pos; + if (modifiers == m_arrowdraw_modifiers && m_arrowdraw_modifiers != Qt::NoModifier) { // start/finish arrow + if (arrow_tail != nullPoint()) { + if (drawing.length() != 0) { + clearLast(); // clear our arrow with tail at previous position + } + drawing = createArrow(pos, arrow_tail); + effects->addRepaintFull(); + return; + } else { + if (drawing.length() > 0) { // has unfinished freedraw right before arrowdraw + marks.append(drawing); + drawing.clear(); + } + arrow_tail = pos; + } + } else if (modifiers == m_freedraw_modifiers && m_freedraw_modifiers != Qt::NoModifier ) { // activated + if (arrow_tail != nullPoint()) { + arrow_tail = nullPoint(); // for the case when user started freedraw right after arrowdraw + marks.append(drawing); + drawing.clear(); + } + if (drawing.isEmpty()) { + drawing.append(pos); + } + if (drawing.last() == pos) { + return; + } + QPointF pos2 = drawing.last(); + drawing.append(pos); + QRect repaint = QRect(std::min(pos.x(), pos2.x()), std::min(pos.y(), pos2.y()), + std::max(pos.x(), pos2.x()), std::max(pos.y(), pos2.y())); + repaint.adjust(-width, -width, width, width); + effects->addRepaint(repaint); + } else { // neither freedraw, nor arrowdraw modifiers pressed, but mouse moved + if (drawing.length() > 1) { + marks.append(drawing); + } + drawing.clear(); + arrow_tail = nullPoint(); + } +} + +void MouseMarkEffect::clear() +{ + arrow_tail = nullPoint(); + drawing.clear(); + marks.clear(); + effects->addRepaintFull(); +} + +void MouseMarkEffect::clearLast() +{ + if (drawing.length() > 1) { // just pressing a modifiers already create a drawing with 1 point (so not visible), treat it as non-existent + drawing.clear(); + effects->addRepaintFull(); + } else if (!marks.isEmpty()) { + marks.pop_back(); + effects->addRepaintFull(); + } +} + +MouseMarkEffect::Mark MouseMarkEffect::createArrow(QPointF arrow_head, QPointF arrow_tail) +{ + Mark ret; + double angle = atan2((double)(arrow_tail.y() - arrow_head.y()), (double)(arrow_tail.x() - arrow_head.x())); + // Arrow is made of connected lines. We make it's last point at tail, so freedraw can begin from the tail + ret += arrow_head; + ret += arrow_head + QPoint(50 * cos(angle + M_PI / 6), + 50 * sin(angle + M_PI / 6)); // right one + ret += arrow_head; + ret += arrow_head + QPoint(50 * cos(angle - M_PI / 6), + 50 * sin(angle - M_PI / 6)); // left one + ret += arrow_head; + ret += arrow_tail; + return ret; +} + +void MouseMarkEffect::screenLockingChanged(bool locked) +{ + if (!marks.isEmpty() || !drawing.isEmpty()) { + effects->addRepaintFull(); + } +} + +bool MouseMarkEffect::isActive() const +{ + return (!marks.isEmpty() || !drawing.isEmpty()) && !effects->isScreenLocked(); +} + +int MouseMarkEffect::requestedEffectChainPosition() const +{ + return 10; +} + +} // namespace + +#include "moc_mousemark.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark.h b/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark.h new file mode 100644 index 0000000000..eeeca49e6c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark.h @@ -0,0 +1,68 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2023 Andrew Shark + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/effect.h" +#include "opengl/glutils.h" + +namespace KWin +{ + +class MouseMarkEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(int width READ configuredWidth) + Q_PROPERTY(QColor color READ configuredColor) + Q_PROPERTY(Qt::KeyboardModifiers modifiers READ freedraw_modifiers) +public: + MouseMarkEffect(); + ~MouseMarkEffect() override; + void reconfigure(ReconfigureFlags) override; + void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) override; + bool isActive() const override; + int requestedEffectChainPosition() const override; + + // for properties + int configuredWidth() const + { + return width; + } + QColor configuredColor() const + { + return color; + } + Qt::KeyboardModifiers freedraw_modifiers() const + { + return m_freedraw_modifiers; + } +private Q_SLOTS: + void clear(); + void clearLast(); + void slotMouseChanged(const QPointF &pos, const QPointF &old, + Qt::MouseButtons buttons, Qt::MouseButtons oldbuttons, + Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers); + void screenLockingChanged(bool locked); + +private: + typedef QList Mark; + void drawMark(QPainter *painter, const Mark &mark); + static Mark createArrow(QPointF arrow_head, QPointF arrow_tail); + QList marks; + Mark drawing; + QPointF arrow_tail; + int width; + QColor color; + Qt::KeyboardModifiers m_freedraw_modifiers; + Qt::KeyboardModifiers m_arrowdraw_modifiers; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark.kcfg b/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark.kcfg new file mode 100644 index 0000000000..cc61ff2b87 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark.kcfg @@ -0,0 +1,40 @@ + + + + + + 3 + + + 255,0,0 + + + true + + + false + + + false + + + true + + + + true + + + true + + + false + + + true + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark_config.cpp b/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark_config.cpp new file mode 100644 index 0000000000..22761b1d85 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark_config.cpp @@ -0,0 +1,95 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + SPDX-FileCopyrightText: 2023 Andrew Shark + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "mousemark_config.h" + +#include "config-kwin.h" + +// KConfigSkeleton +#include "mousemarkconfig.h" +#include + +#include + +#include +#include +#include +#include + +#include +#include + +K_PLUGIN_CLASS(KWin::MouseMarkEffectConfig) + +namespace KWin +{ + +MouseMarkEffectConfig::MouseMarkEffectConfig(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) +{ + m_ui.setupUi(widget()); + + MouseMarkConfig::instance(KWIN_CONFIG); + addConfig(MouseMarkConfig::self(), widget()); + + // Shortcut config. The shortcut belongs to the component "kwin"! + m_actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + m_actionCollection->setComponentDisplayName(i18n("KWin")); + + QAction *a = m_actionCollection->addAction(QStringLiteral("ClearMouseMarks")); + a->setText(i18n("Clear Mouse Marks")); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << (Qt::SHIFT | Qt::META | Qt::Key_F11)); + KGlobalAccel::self()->setShortcut(a, QList() << (Qt::SHIFT | Qt::META | Qt::Key_F11)); + + a = m_actionCollection->addAction(QStringLiteral("ClearLastMouseMark")); + a->setText(i18n("Clear Last Mouse Mark")); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << (Qt::SHIFT | Qt::META | Qt::Key_F12)); + KGlobalAccel::self()->setShortcut(a, QList() << (Qt::SHIFT | Qt::META | Qt::Key_F12)); + + m_ui.editor->addCollection(m_actionCollection); + connect(m_ui.editor, &KShortcutsEditor::keyChange, this, &MouseMarkEffectConfig::markAsChanged); + + connect(m_ui.kcfg_LineWidth, qOverload(&QSpinBox::valueChanged), this, [this]() { + updateSpinBoxSuffix(); + }); +} + +void MouseMarkEffectConfig::load() +{ + KCModule::load(); + + updateSpinBoxSuffix(); +} + +void MouseMarkEffectConfig::save() +{ + qDebug() << "Saving config of MouseMark"; + KCModule::save(); + + m_actionCollection->writeSettings(); + m_ui.editor->save(); // undo() will restore to this state from now on + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("mousemark")); +} + +void MouseMarkEffectConfig::updateSpinBoxSuffix() +{ + m_ui.kcfg_LineWidth->setSuffix(i18ncp("Suffix", " pixel", " pixels", m_ui.kcfg_LineWidth->value())); +} + +} // namespace + +#include "mousemark_config.moc" + +#include "moc_mousemark_config.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark_config.h b/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark_config.h new file mode 100644 index 0000000000..768aba2a1f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark_config.h @@ -0,0 +1,38 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + SPDX-FileCopyrightText: 2023 Andrew Shark + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "ui_mousemark_config.h" + +class KActionCollection; + +namespace KWin +{ + +class MouseMarkEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit MouseMarkEffectConfig(QObject *parent, const KPluginMetaData &data); + + void load() override; + void save() override; + +private: + void updateSpinBoxSuffix(); + + Ui::MouseMarkEffectConfigForm m_ui; + KActionCollection *m_actionCollection; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark_config.ui b/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark_config.ui new file mode 100644 index 0000000000..9c300a9baf --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemark_config.ui @@ -0,0 +1,242 @@ + + + KWin::MouseMarkEffectConfigForm + + + + 0 + 0 + 279 + 178 + + + + + + + Appearance + + + + + + Wid&th: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_LineWidth + + + + + + + &Color: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Color + + + + + + + + 0 + 0 + + + + + + + + 1 + + + 10 + + + 3 + + + + + + + + + + KShortcutsEditor::GlobalAction + + + + + + + Draw with the mouse by holding modifier keys and moving the mouse + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Free draw modifier keys: + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Alt + + + + + + + Ctrl + + + + + + + Shift + + + + + + + Meta + + + + + + + + + + + Arrow draw modifier keys: + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Alt + + + + + + + Ctrl + + + + + + + Shift + + + + + + + Meta + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + KColorCombo + QComboBox +
kcolorcombo.h
+
+ + KShortcutsEditor + QWidget +
kshortcutseditor.h
+ 1 +
+
+ + +
diff --git a/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemarkconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemarkconfig.kcfgc new file mode 100644 index 0000000000..299221dd2a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/mousemark/mousemarkconfig.kcfgc @@ -0,0 +1,5 @@ +File=mousemark.kcfg +ClassName=MouseMarkConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/local/recipes/kde/kwin/source/src/plugins/nightlight/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/nightlight/CMakeLists.txt new file mode 100644 index 0000000000..41a5047ea1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/nightlight/CMakeLists.txt @@ -0,0 +1,36 @@ +kcoreaddons_add_plugin(nightlight INSTALL_NAMESPACE "kwin/plugins") +target_sources(nightlight PRIVATE + nightlightdbusinterface.cpp + nightlightmanager.cpp + main.cpp +) + +ecm_qt_declare_logging_category(nightlight + HEADER nightlightlogging.h + IDENTIFIER KWIN_NIGHTLIGHT + CATEGORY_NAME kwin_nightlight + DEFAULT_SEVERITY Critical +) + +kconfig_add_kcfg_files(nightlight nightlightsettings.kcfgc) + +kconfig_target_kcfg_file(nightlight + FILE nightlightstate.kcfg + CLASS_NAME NightLightState + MUTATORS +) + +set(nightlight_xml_SOURCES) +qt_add_dbus_adaptor(nightlight_xml_SOURCES org.kde.KWin.NightLight.xml nightlightdbusinterface.h KWin::NightLightDBusInterface) +target_sources(nightlight PRIVATE ${nightlight_xml_SOURCES}) + +target_link_libraries(nightlight + kwin + KNightTime + KF6::ConfigGui + KF6::GlobalAccel + KF6::I18n +) + +install(FILES nightlightsettings.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) +install(FILES org.kde.KWin.NightLight.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR}) diff --git a/local/recipes/kde/kwin/source/src/plugins/nightlight/constants.h b/local/recipes/kde/kwin/source/src/plugins/nightlight/constants.h new file mode 100644 index 0000000000..e32933084c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/nightlight/constants.h @@ -0,0 +1,22 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Roman Gilg + SPDX-FileCopyrightText: 2024 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +namespace KWin +{ + +static const int MSC_DAY = 86400000; +static const int MIN_TEMPERATURE = 1000; +static const int DEFAULT_DAY_TEMPERATURE = 6500; +static const int DEFAULT_NIGHT_TEMPERATURE = 4500; +static const int DEFAULT_TRANSITION_DURATION = 1800000; /* 30 minutes */ +static const int MIN_TRANSITION_DURATION = 60000; + +} diff --git a/local/recipes/kde/kwin/source/src/plugins/nightlight/main.cpp b/local/recipes/kde/kwin/source/src/plugins/nightlight/main.cpp new file mode 100644 index 0000000000..9b3c293bde --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/nightlight/main.cpp @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "nightlightmanager.h" + +#include + +using namespace KWin; + +class KWIN_EXPORT NightLightManagerFactory : public PluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID PluginFactory_iid FILE "metadata.json") + Q_INTERFACES(KWin::PluginFactory) + +public: + explicit NightLightManagerFactory() = default; + + std::unique_ptr create() const override; +}; + +std::unique_ptr NightLightManagerFactory::create() const +{ + return std::make_unique(); +} + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/nightlight/metadata.json b/local/recipes/kde/kwin/source/src/plugins/nightlight/metadata.json new file mode 100644 index 0000000000..aa304f4093 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/nightlight/metadata.json @@ -0,0 +1,5 @@ +{ + "KPlugin": { + "EnabledByDefault": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightdbusinterface.cpp b/local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightdbusinterface.cpp new file mode 100644 index 0000000000..b292712569 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightdbusinterface.cpp @@ -0,0 +1,237 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Roman Gilg + SPDX-FileCopyrightText: 2024 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "nightlightdbusinterface.h" +#include "nightlightadaptor.h" +#include "nightlightmanager.h" + +#include + +namespace KWin +{ + +static void announceChangedProperties(const QVariantMap &properties) +{ + QDBusMessage message = QDBusMessage::createSignal( + QStringLiteral("/org/kde/KWin/NightLight"), + QStringLiteral("org.freedesktop.DBus.Properties"), + QStringLiteral("PropertiesChanged") + ); + + message.setArguments({ + QStringLiteral("org.kde.KWin.NightLight"), + properties, + QStringList(), // invalidated_properties + }); + + QDBusConnection::sessionBus().send(message); +} + +NightLightDBusInterface::NightLightDBusInterface(NightLightManager *parent) + : QObject(parent) + , m_manager(parent) + , m_inhibitorWatcher(new QDBusServiceWatcher(this)) +{ + m_inhibitorWatcher->setConnection(QDBusConnection::sessionBus()); + m_inhibitorWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration); + connect(m_inhibitorWatcher, &QDBusServiceWatcher::serviceUnregistered, + this, &NightLightDBusInterface::removeInhibitorService); + + connect(m_manager, &NightLightManager::inhibitedChanged, this, [this] { + announceChangedProperties({ + {QStringLiteral("inhibited"), isInhibited()}, + }); + }); + + connect(m_manager, &NightLightManager::enabledChanged, this, [this] { + announceChangedProperties({ + {QStringLiteral("enabled"), isEnabled()}, + }); + }); + + connect(m_manager, &NightLightManager::runningChanged, this, [this] { + announceChangedProperties({ + {QStringLiteral("running"), isRunning()}, + }); + }); + + connect(m_manager, &NightLightManager::currentTemperatureChanged, this, [this] { + announceChangedProperties({ + {QStringLiteral("currentTemperature"), currentTemperature()}, + }); + }); + + connect(m_manager, &NightLightManager::targetTemperatureChanged, this, [this] { + announceChangedProperties({ + {QStringLiteral("targetTemperature"), targetTemperature()}, + }); + }); + + connect(m_manager, &NightLightManager::modeChanged, this, [this] { + announceChangedProperties({ + {QStringLiteral("mode"), mode()}, + }); + }); + + connect(m_manager, &NightLightManager::daylightChanged, this, [this] { + announceChangedProperties({ + {QStringLiteral("daylight"), daylight()}, + }); + }); + + connect(m_manager, &NightLightManager::previousTransitionTimingsChanged, this, [this] { + announceChangedProperties({ + {QStringLiteral("previousTransitionDateTime"), previousTransitionDateTime()}, + {QStringLiteral("previousTransitionDuration"), previousTransitionDuration()}, + }); + }); + + connect(m_manager, &NightLightManager::scheduledTransitionTimingsChanged, this, [this] { + announceChangedProperties({ + {QStringLiteral("scheduledTransitionDateTime"), scheduledTransitionDateTime()}, + {QStringLiteral("scheduledTransitionDuration"), scheduledTransitionDuration()}, + }); + }); + + new NightLightAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/kde/KWin/NightLight"), this); + QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.KWin.NightLight")); +} + +NightLightDBusInterface::~NightLightDBusInterface() +{ + QDBusConnection::sessionBus().unregisterService(QStringLiteral("org.kde.KWin.NightLight")); +} + +bool NightLightDBusInterface::isInhibited() const +{ + return m_manager->isInhibited(); +} + +bool NightLightDBusInterface::isEnabled() const +{ + return m_manager->isEnabled(); +} + +bool NightLightDBusInterface::isRunning() const +{ + return m_manager->isRunning(); +} + +bool NightLightDBusInterface::isAvailable() const +{ + return true; // TODO: Night color should register its own dbus service instead. +} + +quint32 NightLightDBusInterface::currentTemperature() const +{ + return m_manager->currentTemperature(); +} + +quint32 NightLightDBusInterface::targetTemperature() const +{ + return m_manager->targetTemperature(); +} + +quint32 NightLightDBusInterface::mode() const +{ + return m_manager->mode(); +} + +bool NightLightDBusInterface::daylight() const +{ + return m_manager->daylight(); +} + +quint64 NightLightDBusInterface::previousTransitionDateTime() const +{ + const QDateTime dateTime = m_manager->previousTransitionDateTime(); + if (dateTime.isValid()) { + return quint64(dateTime.toSecsSinceEpoch()); + } + return 0; +} + +quint32 NightLightDBusInterface::previousTransitionDuration() const +{ + return quint32(m_manager->previousTransitionDuration()); +} + +quint64 NightLightDBusInterface::scheduledTransitionDateTime() const +{ + const QDateTime dateTime = m_manager->scheduledTransitionDateTime(); + if (dateTime.isValid()) { + return quint64(dateTime.toSecsSinceEpoch()); + } + return 0; +} + +quint32 NightLightDBusInterface::scheduledTransitionDuration() const +{ + return quint32(m_manager->scheduledTransitionDuration()); +} + +uint NightLightDBusInterface::inhibit() +{ + const QString serviceName = QDBusContext::message().service(); + + if (!m_inhibitors.contains(serviceName)) { + m_inhibitorWatcher->addWatchedService(serviceName); + } + + m_inhibitors.insert(serviceName, ++m_lastInhibitionCookie); + + m_manager->inhibit(); + + return m_lastInhibitionCookie; +} + +void NightLightDBusInterface::uninhibit(uint cookie) +{ + const QString serviceName = QDBusContext::message().service(); + + uninhibit(serviceName, cookie); +} + +void NightLightDBusInterface::uninhibit(const QString &serviceName, uint cookie) +{ + const int removedCount = m_inhibitors.remove(serviceName, cookie); + if (!removedCount) { + return; + } + + if (!m_inhibitors.contains(serviceName)) { + m_inhibitorWatcher->removeWatchedService(serviceName); + } + + m_manager->uninhibit(); +} + +void NightLightDBusInterface::removeInhibitorService(const QString &serviceName) +{ + const auto cookies = m_inhibitors.values(serviceName); + for (const uint &cookie : cookies) { + uninhibit(serviceName, cookie); + } +} + +void NightLightDBusInterface::preview(uint previewTemp) +{ + m_manager->preview(previewTemp); +} + +void NightLightDBusInterface::stopPreview() +{ + m_manager->stopPreview(); +} + +} + +#include "moc_nightlightdbusinterface.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightdbusinterface.h b/local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightdbusinterface.h new file mode 100644 index 0000000000..40da7d4270 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightdbusinterface.h @@ -0,0 +1,90 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Roman Gilg + SPDX-FileCopyrightText: 2024 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include + +namespace KWin +{ + +class NightLightManager; + +class NightLightDBusInterface : public QObject, public QDBusContext +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.KWin.NightLight") + Q_PROPERTY(bool inhibited READ isInhibited) + Q_PROPERTY(bool enabled READ isEnabled) + Q_PROPERTY(bool running READ isRunning) + Q_PROPERTY(bool available READ isAvailable) + Q_PROPERTY(quint32 currentTemperature READ currentTemperature) + Q_PROPERTY(quint32 targetTemperature READ targetTemperature) + Q_PROPERTY(quint32 mode READ mode) + Q_PROPERTY(bool daylight READ daylight) + Q_PROPERTY(quint64 previousTransitionDateTime READ previousTransitionDateTime) + Q_PROPERTY(quint32 previousTransitionDuration READ previousTransitionDuration) + Q_PROPERTY(quint64 scheduledTransitionDateTime READ scheduledTransitionDateTime) + Q_PROPERTY(quint32 scheduledTransitionDuration READ scheduledTransitionDuration) + +public: + explicit NightLightDBusInterface(NightLightManager *parent); + ~NightLightDBusInterface() override; + + bool isInhibited() const; + bool isEnabled() const; + bool isRunning() const; + bool isAvailable() const; + quint32 currentTemperature() const; + quint32 targetTemperature() const; + quint32 mode() const; + bool daylight() const; + quint64 previousTransitionDateTime() const; + quint32 previousTransitionDuration() const; + quint64 scheduledTransitionDateTime() const; + quint32 scheduledTransitionDuration() const; + +public Q_SLOTS: + /** + * @brief Temporarily blocks Night Light. + * @since 5.18 + */ + uint inhibit(); + /** + * @brief Cancels the previous call to inhibit(). + * @since 5.18 + */ + void uninhibit(uint cookie); + /** + * @brief Previews a given temperature for a short time (15s). + * @since 5.25 + */ + void preview(uint temperature); + /** + * @brief Stops an ongoing preview. + * @since 5.25 + */ + void stopPreview(); + +private Q_SLOTS: + void removeInhibitorService(const QString &serviceName); + +private: + void uninhibit(const QString &serviceName, uint cookie); + + NightLightManager *m_manager; + QDBusServiceWatcher *m_inhibitorWatcher; + QMultiHash m_inhibitors; + uint m_lastInhibitionCookie = 0; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightmanager.cpp b/local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightmanager.cpp new file mode 100644 index 0000000000..d8616c92a3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightmanager.cpp @@ -0,0 +1,581 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Roman Gilg + SPDX-FileCopyrightText: 2024 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "nightlightmanager.h" +#include "3rdparty/colortemperature.h" +#include "core/backendoutput.h" +#include "core/outputbackend.h" +#include "core/session.h" +#include "main.h" +#include "nightlightdbusinterface.h" +#include "nightlightlogging.h" +#include "nightlightsettings.h" +#include "nightlightstate.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +static const int QUICK_ADJUST_DURATION = 2000; +static const int TEMPERATURE_STEP = 50; + +NightLightManager::NightLightManager() +{ + NightLightSettings::instance(kwinApp()->config()); + + m_iface = new NightLightDBusInterface(this); + m_skewNotifier = new KSystemClockSkewNotifier(this); + connect(m_skewNotifier, &KSystemClockSkewNotifier::skewed, this, &NightLightManager::resetAllTimers); + + // Display a message when Night Light is (un)inhibited. + connect(this, &NightLightManager::inhibitedChanged, this, [this] { + const QString iconName = isInhibited() + ? QStringLiteral("redshift-status-off") + : m_daylight && m_targetTemperature != DEFAULT_DAY_TEMPERATURE ? QStringLiteral("redshift-status-day") + : QStringLiteral("redshift-status-on"); + + const QString text = isInhibited() + ? i18nc("Night Light was temporarily disabled", "Night Light Suspended") + : i18nc("Night Light was reenabled from temporary suspension", "Night Light Resumed"); + + QDBusMessage message = QDBusMessage::createMethodCall( + QStringLiteral("org.kde.plasmashell"), + QStringLiteral("/org/kde/osdService"), + QStringLiteral("org.kde.osdService"), + QStringLiteral("showText")); + message.setArguments({iconName, text}); + + QDBusConnection::sessionBus().asyncCall(message); + }); + + m_configWatcher = KConfigWatcher::create(kwinApp()->config()); + connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, &NightLightManager::reconfigure); + + // we may always read in the current config + readConfig(); + + QAction *toggleAction = new QAction(this); + toggleAction->setProperty("componentName", QStringLiteral("kwin")); + toggleAction->setObjectName(QStringLiteral("Toggle Night Color")); + toggleAction->setText(i18nc("Temporarily disable/reenable Night Light", "Suspend/Resume Night Light")); + KGlobalAccel::setGlobalShortcut(toggleAction, QList()); + connect(toggleAction, &QAction::triggered, this, &NightLightManager::toggle); + + connect(kwinApp()->outputBackend(), &OutputBackend::outputAdded, this, &NightLightManager::hardReset); + + connect(kwinApp()->session(), &Session::activeChanged, this, [this](bool active) { + if (active) { + hardReset(); + } else { + cancelAllTimers(); + } + }); + connect(kwinApp()->session(), &Session::awoke, this, &NightLightManager::hardReset); + + hardReset(); +} + +NightLightManager::~NightLightManager() +{ +} + +void NightLightManager::hardReset() +{ + cancelAllTimers(); + + updateTransitionTimings(QDateTime::currentDateTime()); + updateTargetTemperature(); + + if (isEnabled() && !isInhibited()) { + setRunning(true); + commitGammaRamps(currentTargetTemperature()); + } + resetAllTimers(); +} + +void NightLightManager::reconfigure() +{ + cancelAllTimers(); + readConfig(); + resetAllTimers(); +} + +void NightLightManager::toggle() +{ + m_isGloballyInhibited = !m_isGloballyInhibited; + m_isGloballyInhibited ? inhibit() : uninhibit(); +} + +bool NightLightManager::isInhibited() const +{ + return m_inhibitReferenceCount; +} + +void NightLightManager::inhibit() +{ + m_inhibitReferenceCount++; + + if (m_inhibitReferenceCount == 1) { + resetAllTimers(); + Q_EMIT inhibitedChanged(); + } +} + +void NightLightManager::uninhibit() +{ + m_inhibitReferenceCount--; + + if (!m_inhibitReferenceCount) { + resetAllTimers(); + Q_EMIT inhibitedChanged(); + } +} + +bool NightLightManager::isEnabled() const +{ + return m_active; +} + +bool NightLightManager::isRunning() const +{ + return m_running; +} + +int NightLightManager::currentTemperature() const +{ + return m_currentTemperature; +} + +int NightLightManager::targetTemperature() const +{ + return m_targetTemperature; +} + +NightLightMode NightLightManager::mode() const +{ + return m_mode; +} + +QDateTime NightLightManager::previousTransitionDateTime() const +{ + return m_prev.first; +} + +qint64 NightLightManager::previousTransitionDuration() const +{ + return m_prev.first.msecsTo(m_prev.second); +} + +QDateTime NightLightManager::scheduledTransitionDateTime() const +{ + return m_next.first; +} + +qint64 NightLightManager::scheduledTransitionDuration() const +{ + return m_next.first.msecsTo(m_next.second); +} + +void NightLightManager::readConfig() +{ + NightLightSettings *settings = NightLightSettings::self(); + settings->load(); + + setEnabled(settings->active()); + + const NightLightMode mode = settings->mode(); + switch (settings->mode()) { + case NightLightMode::Constant: + case NightLightMode::DarkLight: + setMode(mode); + break; + default: + // Fallback for invalid setting values. + setMode(NightLightMode::DarkLight); + break; + } + + if (m_active && m_mode == NightLightMode::DarkLight) { + if (!m_stateConfig) { + m_stateConfig = std::make_unique(); + } + if (!m_darkLightScheduler) { + m_darkLightScheduler = std::make_unique(m_stateConfig->state()); + connect(m_darkLightScheduler.get(), &KDarkLightScheduleProvider::scheduleChanged, this, [this]() { + m_stateConfig->setState(m_darkLightScheduler->state()); + m_stateConfig->save(); + resetAllTimers(); + }); + } + } else { + m_stateConfig.reset(); + m_darkLightScheduler.reset(); + } + + m_dayTargetTemperature = std::clamp(settings->dayTemperature(), MIN_TEMPERATURE, DEFAULT_DAY_TEMPERATURE); + m_nightTargetTemperature = std::clamp(settings->nightTemperature(), MIN_TEMPERATURE, DEFAULT_DAY_TEMPERATURE); +} + +void NightLightManager::resetAllTimers() +{ + cancelAllTimers(); + setRunning(isEnabled() && !isInhibited()); + // we do this also for active being false in order to reset the temperature back to the day value + updateTransitionTimings(QDateTime::currentDateTime()); + updateTargetTemperature(); + resetQuickAdjustTimer(currentTargetTemperature()); +} + +void NightLightManager::cancelAllTimers() +{ + m_slowUpdateStartTimer.reset(); + m_slowUpdateTimer.reset(); + m_quickAdjustTimer.reset(); +} + +void NightLightManager::resetQuickAdjustTimer(int targetTemperature) +{ + int tempDiff = std::abs(targetTemperature - m_currentTemperature); + // allow tolerance of one TEMPERATURE_STEP to compensate if a slow update is coincidental + if (tempDiff > TEMPERATURE_STEP) { + cancelAllTimers(); + m_quickAdjustTimer = std::make_unique(); + m_quickAdjustTimer->setSingleShot(false); + connect(m_quickAdjustTimer.get(), &QTimer::timeout, this, [this, targetTemperature]() { + quickAdjust(targetTemperature); + }); + + int interval = (QUICK_ADJUST_DURATION / (m_previewTimer ? 8 : 1)) / (tempDiff / TEMPERATURE_STEP); + if (interval == 0) { + interval = 1; + } + m_quickAdjustTimer->start(interval); + } else { + resetSlowUpdateTimers(); + } +} + +void NightLightManager::quickAdjust(int targetTemperature) +{ + if (!m_quickAdjustTimer) { + return; + } + + int nextTemperature; + if (m_currentTemperature < targetTemperature) { + nextTemperature = std::min(m_currentTemperature + TEMPERATURE_STEP, targetTemperature); + } else { + nextTemperature = std::max(m_currentTemperature - TEMPERATURE_STEP, targetTemperature); + } + commitGammaRamps(nextTemperature); + + if (nextTemperature == targetTemperature) { + // stop timer, we reached the target temp + m_quickAdjustTimer.reset(); + resetSlowUpdateTimers(); + } +} + +void NightLightManager::resetSlowUpdateTimers() +{ + m_slowUpdateStartTimer.reset(); + + if (!m_running || m_quickAdjustTimer) { + // only reenable the slow update start timer when quick adjust is not active anymore + return; + } + + // If a preview is currently running, do not reset the update timer as it + // would change the temperature back to the normal value. + if (m_previewTimer) { + return; + } + + // There is no need for starting the slow update timer. Screen color temperature + // will be constant all the time now. + if (m_mode == NightLightMode::Constant) { + return; + } + + const QDateTime dateTime = QDateTime::currentDateTime(); + updateTransitionTimings(dateTime); + updateTargetTemperature(); + + const int diff = dateTime.msecsTo(m_next.first); + if (diff <= 0) { + qCCritical(KWIN_NIGHTLIGHT) << "Error in time calculation. Deactivating Night Light."; + return; + } + m_slowUpdateStartTimer = std::make_unique(); + m_slowUpdateStartTimer->setSingleShot(true); + connect(m_slowUpdateStartTimer.get(), &QTimer::timeout, this, &NightLightManager::resetSlowUpdateTimers); + m_slowUpdateStartTimer->start(diff); + + // start the current slow update + m_slowUpdateTimer.reset(); + + if (m_currentTemperature == m_targetTemperature) { + return; + } + + if (dateTime < m_prev.second) { + m_slowUpdateTimer = std::make_unique(); + m_slowUpdateTimer->setSingleShot(false); + connect(m_slowUpdateTimer.get(), &QTimer::timeout, this, [this]() { + slowUpdate(m_targetTemperature); + }); + + // calculate interval such as temperature is changed by TEMPERATURE_STEP K per timer timeout + int interval = dateTime.msecsTo(m_prev.second) * TEMPERATURE_STEP / std::abs(m_targetTemperature - m_currentTemperature); + if (interval == 0) { + interval = 1; + } + m_slowUpdateTimer->start(interval); + } else { + commitGammaRamps(m_targetTemperature); + } +} + +void NightLightManager::slowUpdate(int targetTemperature) +{ + if (!m_slowUpdateTimer) { + return; + } + int nextTemperature; + if (m_currentTemperature < targetTemperature) { + nextTemperature = std::min(m_currentTemperature + TEMPERATURE_STEP, targetTemperature); + } else { + nextTemperature = std::max(m_currentTemperature - TEMPERATURE_STEP, targetTemperature); + } + commitGammaRamps(nextTemperature); + if (nextTemperature == targetTemperature) { + // stop timer, we reached the target temp + m_slowUpdateTimer.reset(); + } +} + +void NightLightManager::preview(uint previewTemp) +{ + previewTemp = std::clamp(previewTemp, MIN_TEMPERATURE, DEFAULT_DAY_TEMPERATURE); + resetQuickAdjustTimer((int)previewTemp); + if (m_previewTimer) { + m_previewTimer.reset(); + } + m_previewTimer = std::make_unique(); + m_previewTimer->setSingleShot(true); + connect(m_previewTimer.get(), &QTimer::timeout, this, &NightLightManager::stopPreview); + m_previewTimer->start(15000); + + QDBusMessage message = QDBusMessage::createMethodCall( + QStringLiteral("org.kde.plasmashell"), + QStringLiteral("/org/kde/osdService"), + QStringLiteral("org.kde.osdService"), + QStringLiteral("showText")); + message.setArguments( + {QStringLiteral("redshift-status-on"), + i18n("Color Temperature Preview")}); + QDBusConnection::sessionBus().asyncCall(message); +} + +void NightLightManager::stopPreview() +{ + if (m_previewTimer) { + m_previewTimer.reset(); + updateTransitionTimings(QDateTime::currentDateTime()); + updateTargetTemperature(); + resetQuickAdjustTimer(currentTargetTemperature()); + } +} + +void NightLightManager::updateTargetTemperature() +{ + const int targetTemperature = mode() != NightLightMode::Constant && daylight() ? m_dayTargetTemperature : m_nightTargetTemperature; + + if (m_targetTemperature == targetTemperature) { + return; + } + + m_targetTemperature = targetTemperature; + + Q_EMIT targetTemperatureChanged(); +} + +void NightLightManager::updateTransitionTimings(const QDateTime &dateTime) +{ + const auto oldPrev = m_prev; + const auto oldNext = m_next; + + if (!m_active) { + setDaylight(true); + m_next = DateTimes(); + m_prev = DateTimes(); + } else { + switch (m_mode) { + case NightLightMode::Constant: { + setDaylight(false); + m_next = DateTimes(); + m_prev = DateTimes(); + break; + } + + case NightLightMode::DarkLight: { + const auto previousTransition = m_darkLightScheduler->schedule().previousTransition(dateTime); + const auto nextTransition = m_darkLightScheduler->schedule().nextTransition(dateTime); + + bool passedMorning = false; + bool passedEvening = false; + + switch (previousTransition->test(dateTime)) { + case KDarkLightTransition::Upcoming: + break; + case KDarkLightTransition::InProgress: + case KDarkLightTransition::Passed: + if (previousTransition->type() == KDarkLightTransition::Morning) { + passedMorning = true; + } else { + passedEvening = true; + } + } + + switch (nextTransition->test(dateTime)) { + case KDarkLightTransition::Upcoming: + break; + case KDarkLightTransition::InProgress: + case KDarkLightTransition::Passed: + if (nextTransition->type() == KDarkLightTransition::Morning) { + passedMorning = true; + } else { + passedEvening = true; + } + } + + setDaylight(passedMorning && !passedEvening); + m_prev = DateTimes(previousTransition->startDateTime(), previousTransition->endDateTime()); + m_next = DateTimes(nextTransition->startDateTime(), nextTransition->endDateTime()); + break; + } + } + } + + if (oldPrev != m_prev) { + Q_EMIT previousTransitionTimingsChanged(); + } + if (oldNext != m_next) { + Q_EMIT scheduledTransitionTimingsChanged(); + } +} + +bool NightLightManager::daylight() const +{ + return m_daylight; +} + +int NightLightManager::currentTargetTemperature() const +{ + if (!m_running) { + return DEFAULT_DAY_TEMPERATURE; + } + + if (m_mode == NightLightMode::Constant) { + return m_nightTargetTemperature; + } + + const QDateTime dateTime = QDateTime::currentDateTime(); + + auto f = [this, dateTime](int target1, int target2) -> int { + if (dateTime <= m_prev.first) { + return target1; + } + if (dateTime >= m_prev.second) { + return target2; + } + + const double progress = double(m_prev.first.msecsTo(dateTime)) / m_prev.first.msecsTo(m_prev.second); + return std::lerp(target1, target2, progress); + }; + + if (daylight()) { + return f(m_nightTargetTemperature, m_dayTargetTemperature); + } else { + return f(m_dayTargetTemperature, m_nightTargetTemperature); + } +} + +void NightLightManager::commitGammaRamps(int temperature) +{ + // TODO this list should ideally be filtered by workspace + const QList outputs = kwinApp()->outputBackend()->outputs(); + const QVector3D rgbFactors = sampleColorTemperature(temperature); + for (BackendOutput *output : outputs) { + output->setChannelFactors(rgbFactors); + } + + setCurrentTemperature(temperature); +} + +void NightLightManager::setEnabled(bool enabled) +{ + if (m_active == enabled) { + return; + } + m_active = enabled; + m_skewNotifier->setActive(enabled); + Q_EMIT enabledChanged(); +} + +void NightLightManager::setRunning(bool running) +{ + if (m_running == running) { + return; + } + m_running = running; + Q_EMIT runningChanged(); +} + +void NightLightManager::setCurrentTemperature(int temperature) +{ + if (m_currentTemperature == temperature) { + return; + } + m_currentTemperature = temperature; + Q_EMIT currentTemperatureChanged(); +} + +void NightLightManager::setMode(NightLightMode mode) +{ + if (m_mode == mode) { + return; + } + m_mode = mode; + Q_EMIT modeChanged(); +} + +void NightLightManager::setDaylight(bool daylight) +{ + if (m_daylight == daylight) { + return; + } + m_daylight = daylight; + Q_EMIT daylightChanged(); +} + +} // namespace KWin + +#include "moc_nightlightmanager.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightmanager.h b/local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightmanager.h new file mode 100644 index 0000000000..0b347403cb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightmanager.h @@ -0,0 +1,292 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Roman Gilg + SPDX-FileCopyrightText: 2024 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "constants.h" +#include "plugin.h" + +#include + +#include +#include +#include + +class KDarkLightScheduleProvider; +class KSystemClockSkewNotifier; +class NightLightState; +class QTimer; + +namespace KWin +{ + +class NightLightDBusInterface; + +typedef QPair DateTimes; + +/** + * This enum type is used to specify operation mode of the night light manager. + */ +enum NightLightMode { + /** + * Color temperature is constant throughout the day. + */ + Constant, + /** + * The color temperature is adjusted based on time of day. + */ + DarkLight, +}; + +/** + * The night light manager is a blue light filter similar to Redshift. + * + * There are four modes this manager can operate in: Automatic, Location, Timings, + * and Constant. Both Automatic and Location modes derive screen color temperature + * from the current position of the Sun, the only difference between two is how + * coordinates of the user are specified. If the user is located near the North or + * South pole, we can't compute correct position of the Sun, that's why we need + * Timings and Constant mode. + * + * With the Timings mode, screen color temperature is computed based on the clock + * time. The user needs to specify timings of the sunset and sunrise as well the + * transition time. + * + * With the Constant mode, screen color temperature is always constant. + */ +class KWIN_EXPORT NightLightManager : public Plugin +{ + Q_OBJECT + +public: + explicit NightLightManager(); + ~NightLightManager() override; + + /** + * Toggles the active state of the filter. + * + * A quick transition will be started if the difference between current screen + * color temperature and target screen color temperature is too large. Target + * temperature is defined in context of the new active state. + * + * If the filter becomes inactive after calling this method, the target color + * temperature is 6500 K. + * + * If the filter becomes active after calling this method, the target screen + * color temperature is defined by the current operation mode. + * + * Note that this method is a no-op if the underlying platform doesn't support + * adjusting gamma ramps. + */ + void toggle(); + + /** + * Returns @c true if the night light manager is blocked; otherwise @c false. + */ + bool isInhibited() const; + + /** + * Temporarily blocks the night light manager. + * + * After calling this method, the screen color temperature will be reverted + * back to 6500C. When you're done, call uninhibit() method. + */ + void inhibit(); + + /** + * Attempts to unblock the night light manager. + */ + void uninhibit(); + + /** + * Returns @c true if Night Light is enabled; otherwise @c false. + */ + bool isEnabled() const; + + /** + * Returns @c true if Night Light is currently running; otherwise @c false. + */ + bool isRunning() const; + + /** + * Returns the current screen color temperature. + */ + int currentTemperature() const; + + /** + * Returns the target screen color temperature. + */ + int targetTemperature() const; + + /** + * Returns the mode in which Night Light is operating. + */ + NightLightMode mode() const; + + /** + * Returns whether Night Light is currently on day time. + */ + bool daylight() const; + + /** + * Returns the datetime that specifies when the previous screen color temperature transition + * had started. Notice that when Night Light operates in the Constant mode, the returned date + * time object is not valid. + */ + QDateTime previousTransitionDateTime() const; + + /** + * Returns the duration of the previous screen color temperature transition, in milliseconds. + */ + qint64 previousTransitionDuration() const; + + /** + * Returns the datetime that specifies when the next screen color temperature transition will + * start. Notice that when Night Light operates in the Constant mode, the returned date time + * object is not valid. + */ + QDateTime scheduledTransitionDateTime() const; + + /** + * Returns the duration of the next screen color temperature transition, in milliseconds. + */ + qint64 scheduledTransitionDuration() const; + + /** + * Applies new night light settings. + */ + void reconfigure(); + + /** + * Previews a given temperature for a short time (15s). + */ + void preview(uint previewTemperature); + + /** + * Stops an ongoing preview. + * Has no effect if there is currently no preview. + */ + void stopPreview(); + +public Q_SLOTS: + void quickAdjust(int targetTemperature); + +Q_SIGNALS: + /** + * Emitted whenever the night light manager is blocked or unblocked. + */ + void inhibitedChanged(); + + /** + * Emitted whenever the night light manager is enabled or disabled. + */ + void enabledChanged(); + + /** + * Emitted whenever the night light manager starts or stops running. + */ + void runningChanged(); + + /** + * Emitted whenever the current screen color temperature has changed. + */ + void currentTemperatureChanged(); + + /** + * Emitted whenever the target screen color temperature has changed. + */ + void targetTemperatureChanged(); + + /** + * Emitted whenever the operation mode has changed. + */ + void modeChanged(); + + /** + * Emitted whenever night light has switched between day and night time. + */ + void daylightChanged(); + + /** + * Emitted whenever the timings of the previous color temperature transition have changed. + */ + void previousTransitionTimingsChanged(); + + /** + * Emitted whenever the timings of the next color temperature transition have changed. + */ + void scheduledTransitionTimingsChanged(); + +private: + void readConfig(); + void hardReset(); + void slowUpdate(int targetTemperature); + void resetAllTimers(); + int currentTargetTemperature() const; + void cancelAllTimers(); + /** + * Quick shift on manual change to current target Temperature + */ + void resetQuickAdjustTimer(int targetTemperature); + /** + * Slow shift to daytime target Temperature + */ + void resetSlowUpdateTimers(); + + void updateTargetTemperature(); + void updateTransitionTimings(const QDateTime &dateTime); + + void commitGammaRamps(int temperature); + + void setEnabled(bool enabled); + void setRunning(bool running); + void setCurrentTemperature(int temperature); + void setMode(NightLightMode mode); + void setDaylight(bool daylight); + + NightLightDBusInterface *m_iface; + KSystemClockSkewNotifier *m_skewNotifier; + + std::unique_ptr m_stateConfig; + std::unique_ptr m_darkLightScheduler; + + // Specifies whether Night Light is enabled. + bool m_active = false; + + // Specifies whether Night Light is currently running. + bool m_running = false; + + // Specifies whether Night Light is inhibited globally. + bool m_isGloballyInhibited = false; + + NightLightMode m_mode = NightLightMode::DarkLight; + + // the previous and next sunrise/sunset intervals - in UTC time + DateTimes m_prev = DateTimes(); + DateTimes m_next = DateTimes(); + + // whether it is currently day or night + bool m_daylight = true; + + std::unique_ptr m_slowUpdateStartTimer; + std::unique_ptr m_slowUpdateTimer; + std::unique_ptr m_quickAdjustTimer; + std::unique_ptr m_previewTimer; + + int m_currentTemperature = DEFAULT_DAY_TEMPERATURE; + int m_targetTemperature = DEFAULT_DAY_TEMPERATURE; + int m_dayTargetTemperature = DEFAULT_DAY_TEMPERATURE; + int m_nightTargetTemperature = DEFAULT_NIGHT_TEMPERATURE; + + int m_inhibitReferenceCount = 0; + KConfigWatcher::Ptr m_configWatcher; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightsettings.kcfg b/local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightsettings.kcfg new file mode 100644 index 0000000000..a99b262f6b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightsettings.kcfg @@ -0,0 +1,25 @@ + + + + + + false + + + + + + + NightLightMode::DarkLight + + + 6500 + + + 4500 + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightsettings.kcfgc b/local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightsettings.kcfgc new file mode 100644 index 0000000000..92f06d89fe --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/nightlight/nightlightsettings.kcfgc @@ -0,0 +1,8 @@ +File=nightlightsettings.kcfg +NameSpace=KWin +ClassName=NightLightSettings +Singleton=true +Mutators=true +# nightlightmanager.h is needed for NightLightMode +IncludeFiles=nightlightmanager.h +UseEnumTypes=true diff --git a/local/recipes/kde/kwin/source/src/plugins/nightlight/org.kde.KWin.NightLight.xml b/local/recipes/kde/kwin/source/src/plugins/nightlight/org.kde.KWin.NightLight.xml new file mode 100644 index 0000000000..cac27218f2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/nightlight/org.kde.KWin.NightLight.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/outputlocator/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/outputlocator/CMakeLists.txt new file mode 100644 index 0000000000..f1e3192960 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/outputlocator/CMakeLists.txt @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2022 David Redondo +# +# SPDX-License-Identifier: BSD-3-Clause + + +kwin_add_builtin_effect(outputlocator main.cpp outputlocator.cpp) + +ecm_add_qml_module(outputlocator + URI org.kde.kwin.outputlocator + QML_FILES + qml/OutputLabel.qml + QT_NO_PLUGIN +) + +target_link_libraries(outputlocator PRIVATE + kwin + Qt::DBus + Qt::Quick + KF6::I18n +) diff --git a/local/recipes/kde/kwin/source/src/plugins/outputlocator/main.cpp b/local/recipes/kde/kwin/source/src/plugins/outputlocator/main.cpp new file mode 100644 index 0000000000..90918a1efd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/outputlocator/main.cpp @@ -0,0 +1,14 @@ +/* + SPDX-FileCopyrightText: 2022 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "outputlocator.h" + +namespace KWin +{ +KWIN_EFFECT_FACTORY(OutputLocatorEffect, "metadata.json.stripped"); +} + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/outputlocator/metadata.json b/local/recipes/kde/kwin/source/src/plugins/outputlocator/metadata.json new file mode 100644 index 0000000000..8a177c63a0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/outputlocator/metadata.json @@ -0,0 +1,147 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "kde@david-redondo.de", + "Name": "David Redondo", + "Name[ar]": "ديفيد ريدوندو", + "Name[az]": "David Redondo", + "Name[be]": "David Redondo", + "Name[bg]": "David Redondo", + "Name[ca@valencia]": "David Redondo", + "Name[ca]": "David Redondo", + "Name[cs]": "David Redondo", + "Name[da]": "David Redondo", + "Name[de]": "David Redondo", + "Name[en_GB]": "David Redondo", + "Name[eo]": "David Redondo", + "Name[es]": "David Redondo", + "Name[et]": "David Redondo", + "Name[eu]": "David Redondo", + "Name[fi]": "David Redondo", + "Name[fr]": "David Redondo", + "Name[ga]": "David Redondo", + "Name[gl]": "David Redondo", + "Name[he]": "דיויד רדונדו", + "Name[hu]": "David Redondo", + "Name[ia]": "David Redondo", + "Name[id]": "David Redondo", + "Name[is]": "David Redondo", + "Name[it]": "David Redondo", + "Name[ja]": "David Redondo", + "Name[ka]": "David Redondo", + "Name[ko]": "David Redondo", + "Name[lt]": "David Redondo", + "Name[lv]": "David Redondo", + "Name[nb]": "David Redondo", + "Name[nl]": "David Redondo", + "Name[nn]": "David Redondo", + "Name[pl]": "David Redondo", + "Name[pt]": "David Redondo", + "Name[pt_BR]": "David Redondo", + "Name[ro]": "David Redondo", + "Name[ru]": "David Redondo", + "Name[sa]": "डेविड् रेडोन्डो", + "Name[sk]": "David Redondo", + "Name[sl]": "David Redondo", + "Name[sv]": "David Redondo", + "Name[ta]": "டேவிட் ரிடொண்டோ", + "Name[tr]": "David Redondo", + "Name[uk]": "David Redondo", + "Name[vi]": "David Redondo", + "Name[zh_CN]": "David Redondo", + "Name[zh_TW]": "David Redondo" + } + ], + "Category": "Appearance", + "Description": "Show screen names", + "Description[ar]": "أظهر أسماء الشاشات", + "Description[be]": "Паказваць назвы экранаў", + "Description[bg]": "Показване на имената на екраните", + "Description[ca@valencia]": "Mostra el nom de les pantalles", + "Description[ca]": "Mostra els noms de les pantalles", + "Description[cs]": "Zobrazit názvy obrazovek:", + "Description[da]": "Vis skærmnavne", + "Description[de]": "Bildschirmnamen anzeigen", + "Description[en_GB]": "Show screen names", + "Description[eo]": "Montri ekranajn nomojn", + "Description[es]": "Mostrar nombres de las pantallas", + "Description[eu]": "Erakutsi pantailen izenak", + "Description[fi]": "Näytä näyttöjen nimet", + "Description[fr]": "Afficher le nom des écrans", + "Description[gl]": "Amosar os nomes das pantallas.", + "Description[he]": "הצגת שמות מסכים", + "Description[hu]": "Képernyőnevek megjelenítése", + "Description[ia]": "Monstra nomines de schermo", + "Description[id]": "Menampilkan nama-nama layar", + "Description[is]": "Sýna skjáheiti", + "Description[it]": "Mostra i nomi degli schermi", + "Description[ja]": "スクリーンの名前を表示します", + "Description[ka]": "ეკრანის სახელების ჩვენება", + "Description[ko]": "화면 이름 표시", + "Description[lt]": "Rodyti ekranų pavadinimus", + "Description[lv]": "Rādīt ekrānu nosaukumus", + "Description[nb]": "Vis skjermnavn", + "Description[nl]": "Schermnamen tonen", + "Description[nn]": "Vis skjermnamn", + "Description[pl]": "Pokaż nazwy ekranów", + "Description[pt_BR]": "Mostra os nomes das telas", + "Description[ro]": "Arată denumirile ecranelor", + "Description[ru]": "Просмотр названий экранов", + "Description[sa]": "स्क्रीननामानि दर्शयतु", + "Description[sk]": "Okraje obrazovky", + "Description[sl]": "Prikaži imena zaslonov", + "Description[sv]": "Visa skärmnamn", + "Description[ta]": "திரைகளின் பெயர்களைக் காட்டும்", + "Description[tr]": "Ekran adlarını göster", + "Description[uk]": "Показувати назви екранів", + "Description[zh_CN]": "显示屏幕名称", + "Description[zh_TW]": "顯示螢幕名稱", + "EnabledByDefault": true, + "License": "GPL", + "Name": "Output Locator", + "Name[ar]": "محدد مكان الخرج", + "Name[be]": "Лакатар вываду", + "Name[bg]": "Локализатор на изхода", + "Name[ca@valencia]": "Localitzador d'eixides", + "Name[ca]": "Localitzador de sortides", + "Name[da]": "Udgangslokalisør ", + "Name[de]": "Ausgabefinder", + "Name[en_GB]": "Output Locator", + "Name[eo]": "Elig-Lokilo", + "Name[es]": "Localizador de salidas", + "Name[eu]": "Irteera kokatzailea", + "Name[fi]": "Ulostulopaikannin", + "Name[fr]": "Positionnement de la sortie", + "Name[gl]": "Localizador de saída", + "Name[he]": "מאתר פלט", + "Name[hu]": "Kimenetkereső", + "Name[ia]": "Locator de Exito", + "Name[is]": "Úttaksvísir", + "Name[it]": "Localizzatore uscita", + "Name[ja]": "出力ロケータ", + "Name[ka]": "გამოტანის მომძებნი", + "Name[ko]": "출력 식별기", + "Name[lt]": "Išvedimo vietos nustatymas", + "Name[lv]": "Izvades meklētājs", + "Name[nb]": "Utdata-markering", + "Name[nl]": "Locatie bepalen van uitvoer", + "Name[nn]": "Uteining-markering", + "Name[pl]": "Namierzanie wyjścia", + "Name[pt_BR]": "Localizador de saída", + "Name[ro]": "Găsește afișaje", + "Name[ru]": "Определение устройства вывода", + "Name[sa]": "आउटपुट लोकेटर", + "Name[sk]": "Lokátor výstupu", + "Name[sl]": "Lokator izhoda", + "Name[sv]": "Lokalisering", + "Name[ta]": "திரை கண்டுபிடிப்பி", + "Name[tr]": "Çıktı Bulucusu", + "Name[uk]": "Локатор виведення", + "Name[zh_CN]": "输出定位器", + "Name[zh_TW]": "輸出位置檢測器" + }, + "org.kde.kwin.effect": { + "internal": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/outputlocator/outputlocator.cpp b/local/recipes/kde/kwin/source/src/plugins/outputlocator/outputlocator.cpp new file mode 100644 index 0000000000..1f1ac0ec44 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/outputlocator/outputlocator.cpp @@ -0,0 +1,117 @@ +/* + SPDX-FileCopyrightText: 2022 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "outputlocator.h" +#include "core/output.h" +#include "effect/effecthandler.h" +#include "effect/offscreenquickview.h" + +#include + +#include + +#include +#include + +namespace KWin +{ + +static QString outputName(const LogicalOutput *screen) +{ + const auto screens = effects->screens(); + const bool shouldShowSerialNumber = std::any_of(screens.cbegin(), screens.cend(), [screen](const LogicalOutput *other) { + return other != screen && other->manufacturer() == screen->manufacturer() && other->model() == screen->model(); + }); + const bool shouldShowConnector = shouldShowSerialNumber && std::any_of(screens.cbegin(), screens.cend(), [screen](const LogicalOutput *other) { + return other != screen && other->serialNumber() == screen->serialNumber(); + }); + + QStringList parts; + if (!screen->manufacturer().isEmpty()) { + parts.append(screen->manufacturer()); + } + + if (!screen->model().isEmpty()) { + parts.append(screen->model()); + } + + if (shouldShowSerialNumber && !screen->serialNumber().isEmpty()) { + parts.append(screen->serialNumber()); + } + + if (shouldShowConnector) { + parts.append(screen->name()); + } + + if (parts.isEmpty()) { + return i18nc("@label", "Unknown"); + } else { + return parts.join(QLatin1Char(' ')); + } +} + +OutputLocatorEffect::OutputLocatorEffect(QObject *parent) + : Effect(parent) +{ + QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/kde/KWin/Effect/OutputLocator1"), + QStringLiteral("org.kde.KWin.Effect.OutputLocator1"), + this, + QDBusConnection::ExportAllSlots); + connect(&m_showTimer, &QTimer::timeout, this, &OutputLocatorEffect::hide); +} + +bool OutputLocatorEffect::isActive() const +{ + return m_showTimer.isActive(); +} + +void OutputLocatorEffect::show() +{ + if (isActive()) { + m_showTimer.start(std::chrono::milliseconds(2500)); + return; + } + + const auto screens = effects->screens(); + for (const auto screen : screens) { + auto scene = new OffscreenQuickScene(); + scene->loadFromModule(QStringLiteral("org.kde.kwin.outputlocator"), QStringLiteral("OutputLabel"), {{QStringLiteral("outputName"), outputName(screen)}, {QStringLiteral("resolution"), screen->pixelSize()}, {QStringLiteral("scale"), screen->scale()}}); + QRectF geometry(0, 0, scene->rootItem()->implicitWidth(), scene->rootItem()->implicitHeight()); + geometry.moveCenter(screen->geometry().center()); + scene->setGeometry(geometry.toRect()); + connect(scene, &OffscreenQuickView::repaintNeeded, this, [scene] { + effects->addRepaint(scene->geometry()); + }); + m_scenesByScreens[screen].reset(scene); + } + + m_showTimer.start(std::chrono::milliseconds(2500)); +} + +void OutputLocatorEffect::hide() +{ + m_showTimer.stop(); + + Region repaintRegion; + for (const auto &[screen, scene] : m_scenesByScreens) { + repaintRegion += scene->geometry(); + } + + m_scenesByScreens.clear(); + effects->addRepaint(repaintRegion); +} + +void OutputLocatorEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) +{ + effects->paintScreen(renderTarget, viewport, mask, deviceRegion, screen); + + if (auto it = m_scenesByScreens.find(screen); it != m_scenesByScreens.end()) { + effects->renderOffscreenQuickView(renderTarget, viewport, it->second.get()); + } +} +} + +#include "moc_outputlocator.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/outputlocator/outputlocator.h b/local/recipes/kde/kwin/source/src/plugins/outputlocator/outputlocator.h new file mode 100644 index 0000000000..601df46f60 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/outputlocator/outputlocator.h @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2022 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include "effect/effect.h" + +#include + +#include + +namespace KWin +{ +class OffscreenQuickScene; + +class OutputLocatorEffect : public KWin::Effect +{ + Q_OBJECT + +public: + explicit OutputLocatorEffect(QObject *parent = nullptr); + void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) override; + bool isActive() const override; + +public Q_SLOTS: + void show(); + void hide(); + +private: + QTimer m_showTimer; + std::unordered_map> m_scenesByScreens; +}; +} diff --git a/local/recipes/kde/kwin/source/src/plugins/outputlocator/qml/OutputLabel.qml b/local/recipes/kde/kwin/source/src/plugins/outputlocator/qml/OutputLabel.qml new file mode 100644 index 0000000000..62debd2444 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/outputlocator/qml/OutputLabel.qml @@ -0,0 +1,47 @@ +/* + SPDX-FileCopyrightText: 2012 Dan Vratil + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +import QtQuick +import org.kde.plasma.components as PlasmaComponents3 +import org.kde.kirigami as Kirigami + +Rectangle { + id: root; + + property string outputName; + property size resolution; + property double scale; + + color: Kirigami.Theme.backgroundColor + border { + color: Kirigami.Theme.textColor + width: 2 + } + + implicitWidth: childrenRect.width + 2 * childrenRect.x + implicitHeight: childrenRect.height + 2 * childrenRect.y + + PlasmaComponents3.Label { + id: displayName + x: Kirigami.Units.largeSpacing * 2 + y: Kirigami.Units.largeSpacing + font.pointSize: Kirigami.Theme.defaultFont.pointSize * 3 + text: root.outputName; + wrapMode: Text.WordWrap; + horizontalAlignment: Text.AlignHCenter; + } + + PlasmaComponents3.Label { + id: modeLabel; + anchors { + horizontalCenter: displayName.horizontalCenter + top: displayName.bottom + } + text: resolution.width + "x" + resolution.height + + (root.scale !== 1 ? "@" + Math.round(root.scale * 100.0) + "%": "") + horizontalAlignment: Text.AlignHCenter; + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/overview/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/overview/CMakeLists.txt new file mode 100644 index 0000000000..de224fccef --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/overview/CMakeLists.txt @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2021 Vlad Zahorodnii +# +# SPDX-License-Identifier: BSD-3-Clause + +if (KWIN_BUILD_KCMS) + add_subdirectory(kcm) +endif() + +set(overview_SOURCES + main.cpp + overvieweffect.cpp +) + +kconfig_add_kcfg_files(overview_SOURCES + overviewconfig.kcfgc +) + +kwin_add_builtin_effect(overview ${overview_SOURCES}) + +qt6_add_qml_module(overview + NO_PLUGIN + URI org.kde.kwin.overview + QML_FILES + qml/DesktopBar.qml + qml/DesktopView.qml + qml/Main.qml +) + +target_link_libraries(overview PRIVATE + kwin + + KF6::ConfigGui + KF6::GlobalAccel + KF6::I18n + + Qt::Quick +) diff --git a/local/recipes/kde/kwin/source/src/plugins/overview/kcm/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/overview/kcm/CMakeLists.txt new file mode 100644 index 0000000000..2a3439f029 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/overview/kcm/CMakeLists.txt @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2021 Vlad Zahorodnii +# +# SPDX-License-Identifier: BSD-3-Clause + +set(kwin_overview_config_SOURCES overvieweffectkcm.cpp) +ki18n_wrap_ui(kwin_overview_config_SOURCES overvieweffectkcm.ui) +kconfig_add_kcfg_files(kwin_overview_config_SOURCES ../overviewconfig.kcfgc) + +kwin_add_effect_config(kwin_overview_config ${kwin_overview_config_SOURCES}) +target_link_libraries(kwin_overview_config + KF6::KCMUtils + KF6::CoreAddons + KF6::GlobalAccel + KF6::I18n + KF6::XmlGui + KWinEffectsInterface +) diff --git a/local/recipes/kde/kwin/source/src/plugins/overview/kcm/overvieweffectkcm.cpp b/local/recipes/kde/kwin/source/src/plugins/overview/kcm/overvieweffectkcm.cpp new file mode 100644 index 0000000000..053b250daf --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/overview/kcm/overvieweffectkcm.cpp @@ -0,0 +1,90 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "overvieweffectkcm.h" + +#include "config-kwin.h" + +#include "overviewconfig.h" + +#include + +#include +#include +#include +#include + +#include + +K_PLUGIN_CLASS(KWin::OverviewEffectConfig) + +namespace KWin +{ + +OverviewEffectConfig::OverviewEffectConfig(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) +{ + ui.setupUi(widget()); + OverviewConfig::instance(KWIN_CONFIG); + addConfig(OverviewConfig::self(), widget()); + + auto actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + + actionCollection->setComponentDisplayName(i18n("KWin")); + actionCollection->setConfigGroup(QStringLiteral("Overview")); + actionCollection->setConfigGlobal(true); + + QAction *cycleAction = actionCollection->addAction(QStringLiteral("Cycle Overview")); + cycleAction->setText(i18nc("@action Overview and Grid View are the name of KWin effects", "Cycle through Overview and Grid View")); + cycleAction->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(cycleAction, {}); + KGlobalAccel::self()->setShortcut(cycleAction, {}); + + QAction *reverseCycleAction = actionCollection->addAction(QStringLiteral("Cycle Overview Opposite")); + reverseCycleAction->setText(i18nc("@action Grid View and Overview are the name of KWin effects", "Cycle through Grid View and Overview")); + reverseCycleAction->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(reverseCycleAction, {}); + KGlobalAccel::self()->setShortcut(reverseCycleAction, {}); + + const QKeySequence defaultOverviewShortcut = Qt::META | Qt::Key_W; + QAction *overviewAction = actionCollection->addAction(QStringLiteral("Overview")); + overviewAction->setText(i18nc("@action Overview is the name of a KWin effect", "Toggle Overview")); + overviewAction->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(overviewAction, {defaultOverviewShortcut}); + KGlobalAccel::self()->setShortcut(overviewAction, {defaultOverviewShortcut}); + + const QKeySequence defaultGridShortcut = Qt::META | Qt::Key_G; + QAction *gridAction = actionCollection->addAction(QStringLiteral("Grid View")); + gridAction->setText(i18nc("@action Grid View is the name of a KWin effect", "Toggle Grid View")); + gridAction->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(gridAction, {defaultGridShortcut}); + KGlobalAccel::self()->setShortcut(gridAction, {defaultGridShortcut}); + + ui.shortcutsEditor->addCollection(actionCollection); + connect(ui.shortcutsEditor, &KShortcutsEditor::keyChange, this, &KCModule::markAsChanged); +} + +void OverviewEffectConfig::save() +{ + KCModule::save(); + ui.shortcutsEditor->save(); + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("overview")); +} + +void OverviewEffectConfig::defaults() +{ + ui.shortcutsEditor->allDefault(); + KCModule::defaults(); +} + +} // namespace KWin + +#include "overvieweffectkcm.moc" + +#include "moc_overvieweffectkcm.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/overview/kcm/overvieweffectkcm.h b/local/recipes/kde/kwin/source/src/plugins/overview/kcm/overvieweffectkcm.h new file mode 100644 index 0000000000..3420e1b99b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/overview/kcm/overvieweffectkcm.h @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "ui_overvieweffectkcm.h" + +namespace KWin +{ + +class OverviewEffectConfig : public KCModule +{ + Q_OBJECT + +public: + explicit OverviewEffectConfig(QObject *parent, const KPluginMetaData &data); + +public Q_SLOTS: + void save() override; + void defaults() override; + +private: + ::Ui::OverviewEffectConfig ui; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/overview/kcm/overvieweffectkcm.ui b/local/recipes/kde/kwin/source/src/plugins/overview/kcm/overvieweffectkcm.ui new file mode 100644 index 0000000000..8a0410e7a1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/overview/kcm/overvieweffectkcm.ui @@ -0,0 +1,86 @@ + + + + OverviewEffectConfig + + + + 0 + 0 + 455 + 201 + + + + + + + Ignore minimized windows: + + + + + + + + + + + + + + Organize windows in the Grid View: + + + + + + + + + + + + + + Search results include filtered windows: + + + + + + + + + + + + + + + 0 + 0 + + + + KShortcutsEditor::GlobalAction + + + + + + + + KShortcutsEditor + QWidget +
kshortcutseditor.h
+ 1 +
+
+ + +
diff --git a/local/recipes/kde/kwin/source/src/plugins/overview/main.cpp b/local/recipes/kde/kwin/source/src/plugins/overview/main.cpp new file mode 100644 index 0000000000..bef23bdcea --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/overview/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "overvieweffect.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(OverviewEffect, + "metadata.json.stripped", + return OverviewEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/overview/metadata.json b/local/recipes/kde/kwin/source/src/plugins/overview/metadata.json new file mode 100644 index 0000000000..e6cb3e0d12 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/overview/metadata.json @@ -0,0 +1,94 @@ +{ + "KPlugin": { + "Category": "Window Management", + "Description": "Allows you to overview virtual desktops and windows; activated with a keyboard shortcut", + "Description[ar]": "يسمح لك بإلقاء نظرة عامة على النوافذ وأسطح المكتب الافتراضية، تفعّل باختصار لوحة المفاتيح", + "Description[bg]": "Позволява ви да преглеждате работните плотове и прозорци, активирано с клавишна комбинация", + "Description[ca@valencia]": "Permet una vista general dels escriptoris virtuals i les finestres; activat amb una drecera de teclat", + "Description[ca]": "Permet una vista general dels escriptoris virtuals i les finestres; activat amb una drecera de teclat", + "Description[da]": "Tillader dig at få overblik over virtuelle skriveborde og vinduer; aktiveret med en tastaturgenvej", + "Description[de]": "Übersicht über virtuelle Arbeitsflächen und Fenster erlangen; mit einem Tastatur-Kurzbefehl ausgelöst", + "Description[en_GB]": "Allows you to overview virtual desktops and windows; activated with a keyboard shortcut", + "Description[eo]": "Ebligas al vi superrigardi virtualajn labortablojn kaj fenestrojn; aktivigita per klavara ŝparvojo", + "Description[es]": "Le permite mostrar una vista general de los escritorios virtuales y de las ventanas; se activa con un atajo de teclado", + "Description[eu]": "Alegiazko mahaigain eta leihoen ikuspegi orokor bat izaten uzten dizu; teklatuko lasterbide batekin aktibatua", + "Description[fi]": "Näyttää yleiskuvan virtuaalityöpöydistä ikkunoineen; käynnistyy pikanäppäimellä", + "Description[fr]": "Permet d'avoir un aperçu des bureaux virtuels et des fenêtres. Activé grâce à un raccourci clavier.", + "Description[gl]": "Permítelle obter unha vista xeral dos escritorios virtuais e das xanelas; actívase cun atallo de teclado.", + "Description[he]": "מאפשר לך לסקור את כל שולחנות העבודה הווירטואליים והחלונות, מופעל דרך צירוף מקשי מקלדת", + "Description[hu]": "Lehetővé teszi a virtuális asztalok és ablakok áttekintését; gyorsbillentyűvel aktiválható", + "Description[ia]": "Permitte te a supervider scriptorios virtual e fenestre; activate con un via breve de claviero", + "Description[is]": "Opnar yfirlit yfir sýndarskjáborð og glugga; virkjað með flýtilykli", + "Description[it]": "Consente di visualizzare una panoramica di desktop e finestre virtuali; attivato con una scorciatoia da tastiera", + "Description[ja]": "仮想デスクトップとウィンドウを一覧表示します。ショートカットで起動します", + "Description[ka]": "ვირტუალური სამუშაო მაგიდებისა და ფანჯრების გადახედვის დაშვება. აქტიურდება კლავიატურის მალსახმობით", + "Description[ko]": "가상 바탕 화면과 창 한눈에 보기, 키보드 단축키로 활성화", + "Description[lt]": "Leidžia apžvelgti langus ir virtualius darbalaukius; aktyvinama naudojant sparčiuosius klavišus", + "Description[lv]": "Ļauj pārskatīt virtuālās darbvirsmas un logus; ieslēdz ar tastatūras saīsni", + "Description[nb]": "Lar deg se oversikt over virtuelle skrivebord og vinduer – slått av/på med hurtigtast", + "Description[nl]": "Biedt u een overzicht van virtuele bureaubladen en vensters; geactiveerd met een sneltoets", + "Description[pl]": "Umożliwia przeglądanie wirtualnych pulpitów i okien; uruchamiane skrótem klawiszowym", + "Description[pt_BR]": "Permite-lhe ter uma visão geral das áreas de trabalho virtuais e janelas; ativado com um atalho de teclado", + "Description[ro]": "Arată o privire generală asupra birourilor virtuale și ferestrelor; activat cu o scurtătură de taste", + "Description[ru]": "Режим обзора виртуальных столов и окон; активируется нажатием комбинации клавиш", + "Description[sa]": "वर्चुअल् डेस्कटॉप्स् तथा विण्डोस् इत्येतयोः अवलोकनं कर्तुं शक्नोति; कीबोर्ड-शॉर्टकट्-सहितं सक्रियम्", + "Description[sk]": "Umožňuje vám zobraziť prehľad virtuálnych plôch a okien", + "Description[sl]": "Omogoča pregled navideznih namizij in oken; aktivirano z bližnjico tipkovnice", + "Description[sv]": "Ger dig en översikt av virtuella skrivbord och fönster, aktiveras med en snabbtangent", + "Description[tr]": "Sanal masaüstlerini ve pencerelerin genel bir görünümünü sağlar; klavye kısayoluyla etkinleştirilir", + "Description[uk]": "Надає вам змогу оглядати віртуальні стільниці і вікна; активується клавіатурним скороченням", + "Description[zh_CN]": "用于显示虚拟桌面和窗口的概览;使用键盘快捷键激活", + "Description[zh_TW]": "讓您一覽虛擬桌面與視窗的總覽——需要用鍵盤快捷鍵觸發", + "EnabledByDefault": true, + "License": "GPL", + "Name": "Overview", + "Name[ar]": "النظرة العامة", + "Name[ast]": "Vista xeneral", + "Name[az]": "Ümumi Baxış", + "Name[be]": "Агляд", + "Name[bg]": "Общ преглед", + "Name[ca@valencia]": "Vista general", + "Name[ca]": "Vista general", + "Name[cs]": "Přehled", + "Name[da]": "Overblik", + "Name[de]": "Übersicht", + "Name[en_GB]": "Overview", + "Name[eo]": "Superrigardo", + "Name[es]": "Vista general", + "Name[et]": "Ülevaade", + "Name[eu]": "Ikuspegi orokorra", + "Name[fi]": "Yleiskuva", + "Name[fr]": "Aperçu", + "Name[gl]": "Vista xeral", + "Name[he]": "סקירה", + "Name[hu]": "Áttekintés", + "Name[ia]": "Vista de insimul", + "Name[id]": "Ikhtisar", + "Name[is]": "Yfirlitsskjár", + "Name[it]": "Panoramica", + "Name[ja]": "オーバービュー", + "Name[ka]": "მიმოხილვა", + "Name[ko]": "한눈에 보기", + "Name[lt]": "Apžvalga", + "Name[lv]": "Pārskats", + "Name[nb]": "Oversikt", + "Name[nl]": "Overzicht", + "Name[nn]": "Oversikt", + "Name[pl]": "Przegląd", + "Name[pt]": "Visão Geral", + "Name[pt_BR]": "Visão geral", + "Name[ro]": "Privire generală", + "Name[ru]": "Обзор", + "Name[sa]": "अवलोकनम्", + "Name[sk]": "Prehľad", + "Name[sl]": "Pregled", + "Name[sv]": "Översikt", + "Name[ta]": "மேலோட்டம்", + "Name[tr]": "Genel Görünüm", + "Name[uk]": "Огляд", + "Name[vi]": "Tổng quan", + "Name[zh_CN]": "桌面概览", + "Name[zh_TW]": "總覽" + }, + "X-KDE-ConfigModule": "kwin_overview_config" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/overview/overviewconfig.kcfg b/local/recipes/kde/kwin/source/src/plugins/overview/overviewconfig.kcfg new file mode 100644 index 0000000000..ab450a85bc --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/overview/overviewconfig.kcfg @@ -0,0 +1,30 @@ + + + + + + + false + + + true + + + true + + + + ElectricTopLeft + + + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/overview/overviewconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/overview/overviewconfig.kcfgc new file mode 100644 index 0000000000..5688e91d06 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/overview/overviewconfig.kcfgc @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2021 Vlad Zahorodnii +# +# SPDX-License-Identifier: CC0-1.0 + +File=overviewconfig.kcfg +ClassName=OverviewConfig +NameSpace=KWin +Singleton=true +Mutators=true +IncludeFiles=\"effect/globals.h\" diff --git a/local/recipes/kde/kwin/source/src/plugins/overview/overvieweffect.cpp b/local/recipes/kde/kwin/source/src/plugins/overview/overvieweffect.cpp new file mode 100644 index 0000000000..4310fdcb4c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/overview/overvieweffect.cpp @@ -0,0 +1,344 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "overvieweffect.h" +#include "effect/effecthandler.h" +#include "overviewconfig.h" + +#include +#include + +#include +#include +#include +#include + +using namespace std::chrono_literals; + +namespace KWin +{ + +OverviewEffect::OverviewEffect() + // manages the transition between inactive -> overview + : m_overviewState(new EffectTogglableState(this)) + // manages the transition between overview -> grid + , m_transitionState(new EffectTogglableState(this)) + // manages the transition between inactive -> overview + , m_gridState(new EffectTogglableState(this)) + , m_border(new EffectTogglableTouchBorder(m_overviewState)) + , m_gridBorder(new EffectTogglableTouchBorder(m_gridState)) + , m_shutdownTimer(new QTimer(this)) +{ + auto gesture = new EffectTogglableGesture(m_overviewState); + gesture->addTouchpadSwipeGesture(SwipeDirection::Up, 4); + gesture->addTouchscreenSwipeGesture(SwipeDirection::Up, 3); + + auto transitionGesture = new EffectTogglableGesture(m_transitionState); + transitionGesture->addTouchpadSwipeGesture(SwipeDirection::Up, 4); + transitionGesture->addTouchscreenSwipeGesture(SwipeDirection::Up, 3); + m_transitionState->stop(); + + auto gridGesture = new EffectTogglableGesture(m_gridState); + gridGesture->addTouchpadSwipeGesture(SwipeDirection::Down, 4); + gridGesture->addTouchscreenSwipeGesture(SwipeDirection::Down, 3); + + connect(m_overviewState, &EffectTogglableState::inProgressChanged, this, &OverviewEffect::overviewGestureInProgressChanged); + connect(m_overviewState, &EffectTogglableState::partialActivationFactorChanged, this, &OverviewEffect::overviewPartialActivationFactorChanged); + + connect(m_overviewState, &EffectTogglableState::statusChanged, this, [this](EffectTogglableState::Status status) { + if (status == EffectTogglableState::Status::Activating || status == EffectTogglableState::Status::Active) { + m_searchText = QString(); + setRunning(true); + m_gridState->stop(); + } + if (status == EffectTogglableState::Status::Active) { + m_transitionState->deactivate(); + } + if (status == EffectTogglableState::Status::Inactive || status == EffectTogglableState::Status::Deactivating) { + m_transitionState->stop(); + } + if (status == EffectTogglableState::Status::Inactive) { + m_gridState->deactivate(); + deactivate(); + } + }); + + connect(m_transitionState, &EffectTogglableState::statusChanged, this, [this](EffectTogglableState::Status status) { + if (status == EffectTogglableState::Status::Activating || status == EffectTogglableState::Status::Active) { + m_overviewState->stop(); + } + if (status == EffectTogglableState::Status::Inactive) { + m_overviewState->activate(); + } + if (status == EffectTogglableState::Status::Active) { + m_gridState->activate(); + } + if (status == EffectTogglableState::Status::Inactive || status == EffectTogglableState::Status::Deactivating) { + m_gridState->stop(); + } + }); + + connect(m_gridState, &EffectTogglableState::statusChanged, this, [this](EffectTogglableState::Status status) { + if (status == EffectTogglableState::Status::Activating || status == EffectTogglableState::Status::Active) { + m_searchText = QString(); + setRunning(true); + m_overviewState->stop(); + } + if (status == EffectTogglableState::Status::Inactive) { + m_overviewState->deactivate(); + deactivate(); + } + if (status == EffectTogglableState::Status::Active) { + m_transitionState->activate(); + } + if (status == EffectTogglableState::Status::Inactive || status == EffectTogglableState::Status::Deactivating) { + m_transitionState->stop(); + } + }); + + connect(m_transitionState, &EffectTogglableState::inProgressChanged, this, &OverviewEffect::transitionGestureInProgressChanged); + connect(m_transitionState, &EffectTogglableState::partialActivationFactorChanged, this, &OverviewEffect::transitionPartialActivationFactorChanged); + + connect(m_gridState, &EffectTogglableState::inProgressChanged, this, &OverviewEffect::gridGestureInProgressChanged); + connect(m_gridState, &EffectTogglableState::partialActivationFactorChanged, this, &OverviewEffect::gridPartialActivationFactorChanged); + + connect(effects, &EffectsHandler::desktopChanging, this, [this](VirtualDesktop *old, QPointF desktopOffset, EffectWindow *with) { + m_desktopOffset = desktopOffset; + Q_EMIT desktopOffsetChanged(); + }); + connect(effects, &EffectsHandler::desktopChanged, this, [this](VirtualDesktop *old, VirtualDesktop *current, EffectWindow *with) { + m_desktopOffset = QPointF(0, 0); + Q_EMIT desktopOffsetChanged(); + }); + connect(effects, &EffectsHandler::desktopChangingCancelled, this, [this]() { + m_desktopOffset = QPointF(0, 0); + Q_EMIT desktopOffsetChanged(); + }); + + m_shutdownTimer->setSingleShot(true); + connect(m_shutdownTimer, &QTimer::timeout, this, &OverviewEffect::realDeactivate); + + auto cycleAction = new QAction(this); + connect(cycleAction, &QAction::triggered, this, &OverviewEffect::cycle); + cycleAction->setObjectName(QStringLiteral("Cycle Overview")); + cycleAction->setText(i18nc("@action Grid View and Overview are the name of KWin effects", "Cycle through Overview and Grid View")); + KGlobalAccel::self()->setDefaultShortcut(cycleAction, {}); + KGlobalAccel::self()->setShortcut(cycleAction, {}); + m_cycleShortcut = KGlobalAccel::self()->shortcut(cycleAction); + + auto reverseCycleAction = new QAction(this); + connect(reverseCycleAction, &QAction::triggered, this, &OverviewEffect::reverseCycle); + reverseCycleAction->setObjectName(QStringLiteral("Cycle Overview Opposite")); + reverseCycleAction->setText(i18nc("@action Grid View and Overview are the name of KWin effects", "Cycle through Grid View and Overview")); + KGlobalAccel::self()->setDefaultShortcut(reverseCycleAction, {}); + KGlobalAccel::self()->setShortcut(reverseCycleAction, {}); + m_reverseCycleShortcut = KGlobalAccel::self()->shortcut(reverseCycleAction); + + const QKeySequence defaultOverviewShortcut = Qt::META | Qt::Key_W; + auto overviewAction = m_overviewState->toggleAction(); + overviewAction->setObjectName(QStringLiteral("Overview")); + overviewAction->setText(i18nc("@action Overview is the name of a Kwin effect", "Toggle Overview")); + overviewAction->setAutoRepeat(false); + KGlobalAccel::self()->setDefaultShortcut(overviewAction, {defaultOverviewShortcut}); + KGlobalAccel::self()->setShortcut(overviewAction, {defaultOverviewShortcut}); + m_overviewShortcut = KGlobalAccel::self()->shortcut(overviewAction); + + const QKeySequence defaultGridShortcut = Qt::META | Qt::Key_G; + auto gridAction = m_gridState->toggleAction(); + gridAction->setObjectName(QStringLiteral("Grid View")); + gridAction->setText(i18nc("@action Grid view is the name of a Kwin effect", "Toggle Grid View")); + gridAction->setAutoRepeat(false); + KGlobalAccel::self()->setDefaultShortcut(gridAction, {defaultGridShortcut}); + KGlobalAccel::self()->setShortcut(gridAction, {defaultGridShortcut}); + m_gridShortcut = KGlobalAccel::self()->shortcut(gridAction); + + connect(effects, &EffectsHandler::screenAboutToLock, this, &OverviewEffect::realDeactivate); + + OverviewConfig::instance(effects->config()); + reconfigure(ReconfigureAll); + + auto delegate = new QQmlComponent(effects->qmlEngine(), this); + connect(delegate, &QQmlComponent::statusChanged, this, [delegate]() { + if (delegate->isError()) { + qWarning() << "Failed to load overview:" << delegate->errorString(); + } + }); + delegate->loadFromModule(QStringLiteral("org.kde.kwin.overview"), QStringLiteral("Main"), QQmlComponent::Asynchronous); + setDelegate(delegate); +} + +OverviewEffect::~OverviewEffect() +{ +} + +void OverviewEffect::reconfigure(ReconfigureFlags) +{ + OverviewConfig::self()->read(); + setAnimationDuration(animationTime(300ms)); + setFilterWindows(OverviewConfig::filterWindows()); + + for (const ElectricBorder &border : std::as_const(m_borderActivate)) { + effects->unreserveElectricBorder(border, this); + } + for (const ElectricBorder &border : std::as_const(m_gridBorderActivate)) { + effects->unreserveElectricBorder(border, this); + } + + m_borderActivate.clear(); + m_gridBorderActivate.clear(); + + const QList activateBorders = OverviewConfig::borderActivate(); + for (const int &border : activateBorders) { + m_borderActivate.append(ElectricBorder(border)); + effects->reserveElectricBorder(ElectricBorder(border), this); + } + + const QList gridActivateBorders = OverviewConfig::gridBorderActivate(); + for (const int &border : gridActivateBorders) { + m_gridBorderActivate.append(ElectricBorder(border)); + effects->reserveElectricBorder(ElectricBorder(border), this); + } + + m_border->setBorders(OverviewConfig::touchBorderActivate()); + m_gridBorder->setBorders(OverviewConfig::gridTouchBorderActivate()); +} + +int OverviewEffect::animationDuration() const +{ + return m_animationDuration; +} + +void OverviewEffect::setAnimationDuration(int duration) +{ + if (m_animationDuration != duration) { + m_animationDuration = duration; + Q_EMIT animationDurationChanged(); + } +} + +bool OverviewEffect::filterWindows() const +{ + return m_filterWindows; +} + +void OverviewEffect::setFilterWindows(bool filterWindows) +{ + if (m_filterWindows != filterWindows) { + m_filterWindows = filterWindows; + Q_EMIT filterWindowsChanged(); + } +} + +qreal OverviewEffect::overviewPartialActivationFactor() const +{ + return m_overviewState->partialActivationFactor(); +} + +bool OverviewEffect::overviewGestureInProgress() const +{ + return m_overviewState->inProgress(); +} + +qreal OverviewEffect::transitionPartialActivationFactor() const +{ + return m_transitionState->partialActivationFactor(); +} + +bool OverviewEffect::transitionGestureInProgress() const +{ + return m_transitionState->inProgress(); +} + +qreal OverviewEffect::gridPartialActivationFactor() const +{ + return m_gridState->partialActivationFactor(); +} + +bool OverviewEffect::gridGestureInProgress() const +{ + return m_gridState->inProgress(); +} + +QPointF OverviewEffect::desktopOffset() const +{ + return m_desktopOffset; +} + +bool OverviewEffect::ignoreMinimized() const +{ + return OverviewConfig::ignoreMinimized(); +} + +bool OverviewEffect::organizedGrid() const +{ + return OverviewConfig::organizedGrid(); +} + +int OverviewEffect::requestedEffectChainPosition() const +{ + return 70; +} + +bool OverviewEffect::borderActivated(ElectricBorder border) +{ + if (m_borderActivate.contains(border)) { + m_overviewState->toggle(); + return true; + } else if (m_gridBorderActivate.contains(border)) { + m_gridState->toggle(); + return true; + } + return false; +} + +void OverviewEffect::activate() +{ + if (effects->isScreenLocked()) { + return; + } + + m_overviewState->activate(); +} + +void OverviewEffect::deactivate() +{ + const auto screens = effects->screens(); + m_shutdownTimer->start(animationDuration()); + m_overviewState->deactivate(); +} + +void OverviewEffect::realDeactivate() +{ + if (m_overviewState->status() == EffectTogglableState::Status::Inactive) { + setRunning(false); + } +} + +void OverviewEffect::cycle() +{ + if (m_overviewState->status() == EffectTogglableState::Status::Inactive) { + m_overviewState->activate(); + } else if (m_transitionState->status() == EffectTogglableState::Status::Inactive) { + m_transitionState->activate(); + } else if (m_gridState->status() == EffectTogglableState::Status::Active) { + m_overviewState->deactivate(); + } +} + +void OverviewEffect::reverseCycle() +{ + if (m_overviewState->status() == EffectTogglableState::Status::Active) { + m_overviewState->deactivate(); + } else if (m_transitionState->status() == EffectTogglableState::Status::Active) { + m_transitionState->deactivate(); + } else if (m_gridState->status() == EffectTogglableState::Status::Inactive) { + m_gridState->activate(); + } +} + +} // namespace KWin + +#include "moc_overvieweffect.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/overview/overvieweffect.h b/local/recipes/kde/kwin/source/src/plugins/overview/overvieweffect.h new file mode 100644 index 0000000000..99fbf7719b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/overview/overvieweffect.h @@ -0,0 +1,101 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/effecttogglablestate.h" +#include "effect/quickeffect.h" + +namespace KWin +{ + +class VirtualDesktop; + +class OverviewEffect : public QuickSceneEffect +{ + Q_OBJECT + Q_PROPERTY(int animationDuration READ animationDuration NOTIFY animationDurationChanged) + Q_PROPERTY(bool ignoreMinimized READ ignoreMinimized NOTIFY ignoreMinimizedChanged) + Q_PROPERTY(bool filterWindows READ filterWindows NOTIFY filterWindowsChanged) + Q_PROPERTY(bool organizedGrid READ organizedGrid NOTIFY organizedGridChanged) + Q_PROPERTY(qreal overviewPartialActivationFactor READ overviewPartialActivationFactor NOTIFY overviewPartialActivationFactorChanged) + // More efficient from a property binding pov rather than checking if partialActivationFactor is strictly between 0 and 1 + Q_PROPERTY(bool overviewGestureInProgress READ overviewGestureInProgress NOTIFY overviewGestureInProgressChanged) + Q_PROPERTY(qreal transitionPartialActivationFactor READ transitionPartialActivationFactor NOTIFY transitionPartialActivationFactorChanged) + Q_PROPERTY(bool transitionGestureInProgress READ transitionGestureInProgress NOTIFY transitionGestureInProgressChanged) + Q_PROPERTY(qreal gridPartialActivationFactor READ gridPartialActivationFactor NOTIFY gridPartialActivationFactorChanged) + Q_PROPERTY(bool gridGestureInProgress READ gridGestureInProgress NOTIFY gridGestureInProgressChanged) + Q_PROPERTY(QPointF desktopOffset READ desktopOffset NOTIFY desktopOffsetChanged) + Q_PROPERTY(QString searchText MEMBER m_searchText NOTIFY searchTextChanged) + +public: + OverviewEffect(); + ~OverviewEffect() override; + + bool ignoreMinimized() const; + bool organizedGrid() const; + + bool filterWindows() const; + void setFilterWindows(bool filterWindows); + + int animationDuration() const; + void setAnimationDuration(int duration); + + qreal overviewPartialActivationFactor() const; + bool overviewGestureInProgress() const; + qreal transitionPartialActivationFactor() const; + bool transitionGestureInProgress() const; + qreal gridPartialActivationFactor() const; + bool gridGestureInProgress() const; + QPointF desktopOffset() const; + + int requestedEffectChainPosition() const override; + bool borderActivated(ElectricBorder border) override; + void reconfigure(ReconfigureFlags flags) override; + +Q_SIGNALS: + void animationDurationChanged(); + void overviewPartialActivationFactorChanged(); + void overviewGestureInProgressChanged(); + void transitionPartialActivationFactorChanged(); + void transitionGestureInProgressChanged(); + void gridPartialActivationFactorChanged(); + void gridGestureInProgressChanged(); + void ignoreMinimizedChanged(); + void filterWindowsChanged(); + void organizedGridChanged(); + void desktopOffsetChanged(); + void searchTextChanged(); + +public Q_SLOTS: + void activate(); + void deactivate(); + +private: + void realDeactivate(); + void cycle(); + void reverseCycle(); + + EffectTogglableState *const m_overviewState; + EffectTogglableState *const m_transitionState; + EffectTogglableState *const m_gridState; + EffectTogglableTouchBorder *const m_border; + EffectTogglableTouchBorder *const m_gridBorder; + + QTimer *m_shutdownTimer; + QList m_cycleShortcut; + QList m_reverseCycleShortcut; + QList m_overviewShortcut; + QList m_gridShortcut; + QList m_borderActivate; + QList m_gridBorderActivate; + QString m_searchText; + QPointF m_desktopOffset; + bool m_filterWindows = true; + int m_animationDuration = 400; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/overview/qml/DesktopBar.qml b/local/recipes/kde/kwin/source/src/plugins/overview/qml/DesktopBar.qml new file mode 100644 index 0000000000..b36c693ed8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/overview/qml/DesktopBar.qml @@ -0,0 +1,294 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + SPDX-FileCopyrightText: 2022 ivan tkachenko + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import org.kde.kirigami as Kirigami +import org.kde.kwin as KWinComponents +import org.kde.kwin.private.effects +import org.kde.plasma.components as PC3 + +Item { + id: bar + + readonly property real desktopHeight: Kirigami.Units.gridUnit * 5 + readonly property real desktopWidth: desktopHeight * targetScreen.geometry.width / targetScreen.geometry.height + readonly property real columnHeight: desktopHeight + Kirigami.Units.gridUnit + readonly property real columnWidth: desktopWidth + Kirigami.Units.gridUnit + readonly property int desktopCount: desktopRepeater.count + + property bool verticalDesktopBar + property QtObject windowModel + property alias desktopModel: desktopRepeater.model + property QtObject selectedDesktop: null + property var heap + + implicitHeight: columnHeight + 2 * Kirigami.Units.smallSpacing + implicitWidth: columnWidth + 2 * Kirigami.Units.smallSpacing + + Flickable { + anchors.fill: parent + leftMargin: Math.max((width - contentWidth) / 2, 0) + topMargin: Math.max((height - contentHeight) / 2, 0) + contentWidth: contentItem.childrenRect.width + contentHeight: contentItem.childrenRect.height + interactive: contentWidth > width + clip: true + flickableDirection: Flickable.HorizontalFlick + + Grid { + spacing: Kirigami.Units.largeSpacing + columns: verticalDesktopBar ? 1 : desktopCount + 1 + + Repeater { + id: desktopRepeater + + Column { + id: delegate + activeFocusOnTab: true + width: bar.desktopWidth + height: bar.columnHeight + spacing: Kirigami.Units.smallSpacing + + required property QtObject desktop + required property int index + + Keys.onEnterPressed: activate(); + Keys.onReturnPressed: activate(); + Keys.onSpacePressed: activate(); + Keys.onDeletePressed: remove(); + + Keys.onPressed: { + if (event.key === Qt.Key_F2) { + event.accepted = true; + label.startEditing(); + } + } + + Keys.onLeftPressed: nextItemInFocusChain(LayoutMirroring.enabled).forceActiveFocus(Qt.BacktabFocusReason); + Keys.onRightPressed: nextItemInFocusChain(!LayoutMirroring.enabled).forceActiveFocus(Qt.TabFocusReason); + + function activate() { + if (KWinComponents.Workspace.currentDesktop === delegate.desktop) { + effect.deactivate() + } else { + KWinComponents.Workspace.currentDesktop = delegate.desktop; + } + } + + function remove() { + if (desktopRepeater.count === 1) { + return; + } + if (delegate.activeFocus) { + if (delegate.index === 0) { + desktopRepeater.itemAt(1).forceActiveFocus(); + } else { + desktopRepeater.itemAt(delegate.index - 1).forceActiveFocus(); + } + } + bar.desktopModel.remove(delegate.index); + } + + Item { + width: bar.desktopWidth + height: bar.desktopHeight + + scale: thumbnailHover.hovered ? 1.03 : 1 + + Behavior on scale { + NumberAnimation { + duration: Kirigami.Units.shortDuration + } + } + + HoverHandler { + id: thumbnailHover + } + + DesktopView { + id: thumbnail + + width: targetScreen.geometry.width + height: targetScreen.geometry.height + windowModel: bar.windowModel + desktop: delegate.desktop + scale: bar.desktopHeight / targetScreen.geometry.height + transformOrigin: Item.TopLeft + + + layer.textureSize: Qt.size(bar.desktopWidth, bar.desktopHeight) + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + anchors.centerIn: parent + width: thumbnail.width + height: thumbnail.height + // Using 5% of width since that's constant even under scaling: + radius: width / 20 + } + } + } + + Rectangle { + readonly property bool active: (delegate.activeFocus || dropArea.containsDrag || mouseArea.containsPress || bar.selectedDesktop === delegate.desktop) + anchors.fill: parent + radius: Kirigami.Units.cornerRadius + color: "transparent" + border.width: active ? 2 : 1 + border.color: active ? Kirigami.Theme.highlightColor : Kirigami.Theme.textColor + opacity: dropArea.containsDrag || !active ? 0.5 : 1.0 + } + + MouseArea { + id: mouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton + onClicked: mouse => { + mouse.accepted = true; + delegate.activate(); + } + } + + Loader { + LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft + active: !bar.heap.dragActive && (hoverHandler.hovered || Kirigami.Settings.tabletMode || Kirigami.Settings.hasTransientTouchInput) && desktopCount > 1 + anchors.right: parent.right + anchors.top: parent.top + sourceComponent: PC3.Button { + text: i18nd("kwin", "Delete Virtual Desktop") + icon.name: "delete" + display: PC3.AbstractButton.IconOnly + + PC3.ToolTip.text: text + PC3.ToolTip.visible: hovered + PC3.ToolTip.delay: Kirigami.Units.toolTipDelay + + onClicked: delegate.remove() + } + } + + DropArea { + id: dropArea + anchors.fill: parent + onEntered: (drag) => { + drag.accepted = true; + } + onDropped: drop => { + drop.accepted = true; + // dragging a KWin::Window + if (drag.source.desktops.length === 0 || drag.source.desktops.indexOf(delegate.desktop) !== -1) { + drop.action = Qt.IgnoreAction; + return; + } + drag.source.desktops = [delegate.desktop]; + } + } + } + + Item { + id: label + width: bar.desktopWidth + height: Kirigami.Units.gridUnit + state: "normal" + + PC3.Label { + anchors.fill: parent + elide: Text.ElideRight + text: delegate.desktop.name + textFormat: Text.PlainText + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + visible: label.state === "normal" + } + + MouseArea { + anchors.fill: parent + onPressed: mouse => { + mouse.accepted = true; + label.startEditing(); + } + } + + Loader { + active: label.state === "editing" + anchors.fill: parent + sourceComponent: PC3.TextField { + topPadding: 0 + bottomPadding: 0 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: delegate.desktop.name + onEditingFinished: { + delegate.desktop.name = text; + label.stopEditing(); + } + Keys.onEscapePressed: label.stopEditing(); + Component.onCompleted: forceActiveFocus(); + } + } + + states: [ + State { + name: "normal" + }, + State { + name: "editing" + } + ] + + function startEditing() { + state = "editing"; + } + function stopEditing() { + state = "normal"; + } + } + + HoverHandler { + id: hoverHandler + } + } + } + + PC3.Button { + width: bar.desktopWidth + height: bar.desktopHeight + + text: i18nd("kwin", "Add Virtual Desktop") + icon.name: "list-add" + display: PC3.AbstractButton.IconOnly + opacity: hovered ? 1 : 0.75 + + PC3.ToolTip.text: text + PC3.ToolTip.visible: hovered + PC3. ToolTip.delay: Kirigami.Units.toolTipDelay + Accessible.name: text + + Keys.onReturnPressed: action.trigger() + Keys.onEnterPressed: action.trigger() + + Keys.onLeftPressed: nextItemInFocusChain(LayoutMirroring.enabled).forceActiveFocus(Qt.BacktabFocusReason); + Keys.onRightPressed: nextItemInFocusChain(!LayoutMirroring.enabled).forceActiveFocus(Qt.TabFocusReason); + + onClicked: desktopModel.create(desktopModel.rowCount()) + + DropArea { + anchors.fill: parent + onEntered: drag => { + drag.accepted = desktopModel.rowCount() < 20 + } + onDropped: drag => { + drag.source.desktops = [desktopModel.create(desktopModel.rowCount())]; + } + } + } + } + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/overview/qml/DesktopView.qml b/local/recipes/kde/kwin/source/src/plugins/overview/qml/DesktopView.qml new file mode 100644 index 0000000000..bf5a5f0092 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/overview/qml/DesktopView.qml @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick +import org.kde.kwin as KWinComponents + +Item { + id: desktopView + + required property QtObject windowModel + required property QtObject desktop + + Repeater { + model: KWinComponents.WindowFilterModel { + activity: KWinComponents.Workspace.currentActivity + desktop: desktopView.desktop + screenName: targetScreen.name + windowModel: desktopView.windowModel + } + + KWinComponents.WindowThumbnail { + wId: model.window.internalId + x: model.window.x - targetScreen.geometry.x + y: model.window.y - targetScreen.geometry.y + width: model.window.width + height: model.window.height + z: model.window.stackingOrder + visible: !model.window.minimized + } + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/private/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/private/CMakeLists.txt new file mode 100644 index 0000000000..5bce4311f0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/private/CMakeLists.txt @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2021 Vlad Zahorodnii +# +# SPDX-License-Identifier: BSD-3-Clause + +ecm_add_qml_module(effectsplugin URI "org.kde.kwin.private.effects") + +ecm_target_qml_sources(effectsplugin + SOURCES + qml/WindowHeap.qml + qml/WindowHeapDelegate.qml +) + +target_sources(effectsplugin PRIVATE + expoarea.cpp + expolayout.cpp + plugin.cpp +) + +target_link_libraries(effectsplugin PRIVATE + kwin + Qt6::Quick + Qt6::Qml + KF6::I18n +) + +ecm_finalize_qml_module(effectsplugin DESTINATION ${KDE_INSTALL_QMLDIR}) + diff --git a/local/recipes/kde/kwin/source/src/plugins/private/expoarea.cpp b/local/recipes/kde/kwin/source/src/plugins/private/expoarea.cpp new file mode 100644 index 0000000000..be06788927 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/private/expoarea.cpp @@ -0,0 +1,87 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "expoarea.h" +#include "virtualdesktops.h" +#include "workspace.h" + +namespace KWin +{ + +ExpoArea::ExpoArea(QObject *parent) + : QObject(parent) +{ +} + +qreal ExpoArea::x() const +{ + return m_rect.x(); +} + +qreal ExpoArea::y() const +{ + return m_rect.y(); +} + +qreal ExpoArea::width() const +{ + return m_rect.width(); +} + +qreal ExpoArea::height() const +{ + return m_rect.height(); +} + +LogicalOutput *ExpoArea::screen() const +{ + return m_screen; +} + +void ExpoArea::setScreen(LogicalOutput *screen) +{ + if (m_screen != screen) { + if (m_screen) { + disconnect(m_screen, &LogicalOutput::geometryChanged, this, &ExpoArea::update); + } + m_screen = screen; + if (m_screen) { + connect(m_screen, &LogicalOutput::geometryChanged, this, &ExpoArea::update); + } + update(); + Q_EMIT screenChanged(); + } +} + +void ExpoArea::update() +{ + if (!m_screen) { + return; + } + const QRectF oldRect = m_rect; + + m_rect = workspace()->clientArea(MaximizeArea, m_screen, VirtualDesktopManager::self()->currentDesktop()); + + // Map the area to the output local coordinates. + m_rect.translate(-m_screen->geometry().topLeft()); + + if (oldRect.x() != m_rect.x()) { + Q_EMIT xChanged(); + } + if (oldRect.y() != m_rect.y()) { + Q_EMIT yChanged(); + } + if (oldRect.width() != m_rect.width()) { + Q_EMIT widthChanged(); + } + if (oldRect.height() != m_rect.height()) { + Q_EMIT heightChanged(); + } +} + +} // namespace KWin + +#include "moc_expoarea.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/private/expoarea.h b/local/recipes/kde/kwin/source/src/plugins/private/expoarea.h new file mode 100644 index 0000000000..778bcb673d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/private/expoarea.h @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/output.h" + +namespace KWin +{ + +class ExpoArea : public QObject +{ + Q_OBJECT + Q_PROPERTY(KWin::LogicalOutput *screen READ screen WRITE setScreen NOTIFY screenChanged) + Q_PROPERTY(qreal x READ x NOTIFY xChanged) + Q_PROPERTY(qreal y READ y NOTIFY yChanged) + Q_PROPERTY(qreal width READ width NOTIFY widthChanged) + Q_PROPERTY(qreal height READ height NOTIFY heightChanged) + +public: + explicit ExpoArea(QObject *parent = nullptr); + + LogicalOutput *screen() const; + void setScreen(LogicalOutput *screen); + + qreal x() const; + qreal y() const; + qreal width() const; + qreal height() const; + +Q_SIGNALS: + void screenChanged(); + void xChanged(); + void yChanged(); + void widthChanged(); + void heightChanged(); + +private: + void update(); + + QRectF m_rect; + LogicalOutput *m_screen = nullptr; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/private/expolayout.cpp b/local/recipes/kde/kwin/source/src/plugins/private/expolayout.cpp new file mode 100644 index 0000000000..257bf4e5f4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/private/expolayout.cpp @@ -0,0 +1,849 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + SPDX-FileCopyrightText: 2024 Yifan Zhu + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "expolayout.h" + +#include +#include +#include +#include + + +ExpoCell::ExpoCell(QQuickItem *parent) + : QQuickItem(parent) +{ + connect(this, &ExpoCell::visibleChanged, this, [this]() { + if (m_contentItem) { + m_contentItem->setVisible(isVisible()); + } + }); + + // This only works for a static visual tree hierarchy. + // TODO: Make it work with reparenting or warn if any parent in the tree changes? + QQuickItem *ancestor = this; + while (ancestor) { + connect(ancestor, &QQuickItem::xChanged, this, &ExpoCell::polish); + connect(ancestor, &QQuickItem::yChanged, this, &ExpoCell::polish); + connect(ancestor, &QQuickItem::widthChanged, this, &ExpoCell::polish); + connect(ancestor, &QQuickItem::heightChanged, this, &ExpoCell::polish); + ancestor = ancestor->parentItem(); + } +} + +ExpoCell::~ExpoCell() +{ + setLayout(nullptr); +} + +void ExpoCell::componentComplete() +{ + QQuickItem::componentComplete(); + + QQmlProperty xProperty(this, "Kirigami.ScenePosition.x", qmlContext(this)); + xProperty.connectNotifySignal(this, SLOT(updateContentItemGeometry())); + QQmlProperty yProperty(this, "Kirigami.ScenePosition.y", qmlContext(this)); + yProperty.connectNotifySignal(this, SLOT(updateContentItemGeometry())); + + updateContentItemGeometry(); +} + +ExpoLayout *ExpoCell::layout() const +{ + return m_layout; +} + +void ExpoCell::setLayout(ExpoLayout *layout) +{ + if (m_layout == layout) { + return; + } + if (m_layout) { + m_layout->removeCell(this); + } + m_layout = layout; + if (m_layout && m_shouldLayout) { + m_layout->addCell(this); + } + updateContentItemGeometry(); + Q_EMIT layoutChanged(); +} + +bool ExpoCell::shouldLayout() const +{ + return m_shouldLayout; +} + +void ExpoCell::setShouldLayout(bool shouldLayout) +{ + if (shouldLayout == m_shouldLayout) { + return; + } + + m_shouldLayout = shouldLayout; + + if (m_layout) { + if (m_shouldLayout) { + m_layout->addCell(this); + } else { + m_layout->removeCell(this); + } + } + + Q_EMIT shouldLayoutChanged(); +} + +QQuickItem *ExpoCell::contentItem() const +{ + return m_contentItem; +} + +void ExpoCell::setContentItem(QQuickItem *item) +{ + if (m_contentItem == item) { + return; + } + + if (m_contentItem) { + disconnect(m_contentItem, &QQuickItem::parentChanged, this, &ExpoCell::updateContentItemGeometry); + } + + m_contentItem = item; + + connect(m_contentItem, &QQuickItem::parentChanged, this, &ExpoCell::updateContentItemGeometry); + + if (m_contentItem) { + m_contentItem->setVisible(isVisible()); + } + + updateContentItemGeometry(); + Q_EMIT contentItemChanged(); +} + +qreal ExpoCell::partialActivationFactor() const +{ + return m_partialActivationFactor; +} + +void ExpoCell::setPartialActivationFactor(qreal factor) +{ + if (m_partialActivationFactor == factor) { + return; + } + + m_partialActivationFactor = factor; + // Since this is an animation controller we want it to have immediate effect + updateContentItemGeometry(); + + Q_EMIT partialActivationFactorChanged(); +} + +void ExpoCell::updateLayout() +{ + if (m_layout) { + m_layout->polish(); + } +} + +qreal ExpoCell::offsetX() const +{ + return m_offsetX; +} + +void ExpoCell::setOffsetX(qreal x) +{ + if (m_offsetX != x) { + m_offsetX = x; + updateContentItemGeometry(); + Q_EMIT offsetXChanged(); + } +} + +qreal ExpoCell::offsetY() const +{ + return m_offsetY; +} + +void ExpoCell::setOffsetY(qreal y) +{ + if (m_offsetY != y) { + m_offsetY = y; + updateContentItemGeometry(); + Q_EMIT offsetYChanged(); + } +} + +qreal ExpoCell::naturalX() const +{ + return m_naturalX; +} + +void ExpoCell::setNaturalX(qreal x) +{ + if (m_naturalX != x) { + m_naturalX = x; + updateLayout(); + Q_EMIT naturalXChanged(); + } +} + +qreal ExpoCell::naturalY() const +{ + return m_naturalY; +} + +void ExpoCell::setNaturalY(qreal y) +{ + if (m_naturalY != y) { + m_naturalY = y; + updateLayout(); + Q_EMIT naturalYChanged(); + } +} + +qreal ExpoCell::naturalWidth() const +{ + return m_naturalWidth; +} + +void ExpoCell::setNaturalWidth(qreal width) +{ + if (m_naturalWidth != width) { + m_naturalWidth = width; + updateLayout(); + Q_EMIT naturalWidthChanged(); + } +} + +qreal ExpoCell::naturalHeight() const +{ + return m_naturalHeight; +} + +void ExpoCell::setNaturalHeight(qreal height) +{ + if (m_naturalHeight != height) { + m_naturalHeight = height; + updateLayout(); + Q_EMIT naturalHeightChanged(); + } +} + +QRectF ExpoCell::naturalRect() const +{ + return QRectF(m_naturalX, m_naturalY, m_naturalWidth, m_naturalHeight); +} + +QMarginsF ExpoCell::margins() const +{ + return m_margins; +} + +QString ExpoCell::persistentKey() const +{ + return m_persistentKey; +} + +void ExpoCell::setPersistentKey(const QString &key) +{ + if (m_persistentKey != key) { + m_persistentKey = key; + updateLayout(); + Q_EMIT persistentKeyChanged(); + } +} + +qreal ExpoCell::bottomMargin() const +{ + return m_margins.bottom(); +} + +void ExpoCell::setBottomMargin(qreal margin) +{ + if (m_margins.bottom() != margin) { + m_margins.setBottom(margin); + update(); + Q_EMIT bottomMarginChanged(); + } +} + +void ExpoCell::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + updateContentItemGeometry(); + QQuickItem::geometryChange(newGeometry, oldGeometry); +} + +void ExpoCell::updateContentItemGeometry() +{ + if (!m_contentItem) { + return; + } + + QRectF rect = mapRectToItem(m_contentItem->parentItem(), boundingRect()); + + rect = { + rect.x() * m_partialActivationFactor + (m_naturalX + m_offsetX) * (1.0 - m_partialActivationFactor), + rect.y() * m_partialActivationFactor + (m_naturalY + m_offsetY) * (1.0 - m_partialActivationFactor), + rect.width() * m_partialActivationFactor + m_naturalWidth * (1.0 - m_partialActivationFactor), + rect.height() * m_partialActivationFactor + m_naturalHeight * (1.0 - m_partialActivationFactor)}; + + m_contentItem->setX(rect.x()); + m_contentItem->setY(rect.y()); + m_contentItem->setSize(rect.size()); +} + +ExpoLayout::ExpoLayout(QQuickItem *parent) + : QQuickItem(parent) +{ +} + +ExpoLayout::PlacementMode ExpoLayout::placementMode() const +{ + return m_placementMode; +} + +void ExpoLayout::setPlacementMode(PlacementMode mode) +{ + if (m_placementMode != mode) { + m_placementMode = mode; + polish(); + Q_EMIT placementModeChanged(); + } +} + +bool ExpoLayout::isReady() const +{ + return m_ready; +} + +void ExpoLayout::setReady() +{ + if (!m_ready) { + m_ready = true; + Q_EMIT readyChanged(); + } +} + +void ExpoLayout::forceLayout() +{ + updatePolish(); +} + +void ExpoLayout::updateCellsMapping() +{ + for (ExpoCell *cell : m_cells) { + cell->polish(); + } +} + +void ExpoLayout::addCell(ExpoCell *cell) +{ + Q_ASSERT(!m_cells.contains(cell)); + m_cells.append(cell); + polish(); +} + +void ExpoLayout::removeCell(ExpoCell *cell) +{ + m_cells.removeOne(cell); + polish(); +} + +void ExpoLayout::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + if (newGeometry.size() != oldGeometry.size()) { + polish(); + } + QQuickItem::geometryChange(newGeometry, oldGeometry); +} + +// Move and scale rect to fit inside area +static void moveToFit(QRectF &rect, const QRectF &area) +{ + qreal scale = std::min(area.width() / rect.width(), area.height() / rect.height()); + rect.setWidth(rect.width() * scale); + rect.setHeight(rect.height() * scale); + rect.moveCenter(area.center()); +} + +void ExpoLayout::updatePolish() +{ + if (m_cells.isEmpty()) { + setReady(); + return; + } + + QRectF area = QRectF(0, 0, width(), height()); + + std::sort(m_cells.begin(), m_cells.end(), + [](const ExpoCell *a, const ExpoCell *b) { + return a->persistentKey() < b->persistentKey(); + }); + + // Estimate the scale factor we need to apply by simple heuristics + qreal totalArea = 0; + qreal availableArea = area.width() * area.height(); + for (ExpoCell *cell : std::as_const(m_cells)) { + totalArea += cell->naturalWidth() * cell->naturalHeight(); + } + qreal scale = std::sqrt(availableArea / totalArea) * 0.7; // conservative estimate + scale = std::clamp(scale, 0.1, 10.0); // don't go crazy + + QList windowSizes; + for (ExpoCell *cell : std::as_const(m_cells)) { + const QMarginsF &margins = cell->margins(); + const QMarginsF scaledMargins(margins.left() / scale, margins.top() / scale, margins.right() / scale, margins.bottom() / scale); + windowSizes.emplace_back(cell->naturalRect().marginsAdded(scaledMargins)); + } + auto windowLayouts = ExpoLayout::layout(area, windowSizes); + for (int i = 0; i < windowLayouts.size(); ++i) { + ExpoCell *cell = m_cells[i]; + QRectF target = windowLayouts[i]; + + QRectF adjustedTarget = target.marginsRemoved(cell->margins()); + if (adjustedTarget.isValid()) { + target = adjustedTarget; // Borders + } + + QRectF rect = cell->naturalRect(); + moveToFit(rect, target); + if (m_ready) { + // Use setProperty so the QML side can animate with Behavior + cell->setProperty("x", rect.x()); + cell->setProperty("y", rect.y()); + cell->setProperty("width", rect.width()); + cell->setProperty("height", rect.height()); + } else { + cell->setX(rect.x()); + cell->setY(rect.y()); + cell->setWidth(rect.width()); + cell->setHeight(rect.height()); + } + } + setReady(); +} + +Layer::Layer(qreal maxWidth, const QList &windowSizes, const QList &windowIds, size_t startPos, size_t endPos) + : maxWidth(maxWidth) + , maxHeight(windowSizes[windowIds[endPos - 1]].height()) + , ids(windowIds.begin() + startPos, windowIds.begin() + endPos) +{ + remainingWidth = maxWidth; + for (auto id = ids.begin(); id != ids.end(); ++id) { + remainingWidth -= windowSizes[*id].width(); + } +} + +qreal Layer::width() const +{ + return maxWidth - remainingWidth; +} + +LayeredPacking::LayeredPacking(qreal maxWidth, const QList &windowSizes, const QList &ids, const QList &layerStartPos) + : maxWidth(maxWidth) + , width(0) + , height(0) +{ + for (int i = 1; i < layerStartPos.size(); ++i) { + layers.emplace_back(maxWidth, windowSizes, ids, layerStartPos[i - 1], layerStartPos[i]); + width = std::max(width, layers.back().width()); + height += layers.back().maxHeight; + } +} + +/** + * @brief Check if @param candidate can be ignored in the future because either @param alternativeSmall or @param alternativeBig is at least as good as @param candidate for layerStart. + * + * More formally, returns false if and only if there exists a k with @param alternativeBig < k <= @param length + * such that leastWeightCandidate( @param candidate, k ) < leastWeightCandidate( @param alternativeSmall, k ) and leastWeightCandidate( @param candidate, k ) < leastWeightCandidate( + * @param alternativeBig, k ). + * + * The input must satisfy @param alternativeSmall < @param candidate < @param alternativeBig + * + * The run time of the algorithm is O(log length). + * + * The Bridge algorithm from Hirschberg, Daniel S., and Lawrence L. + * Larmore. "The least weight subsequence problem." SIAM Journal on + * Computing 16.4 (1987): 628-638 + * + * @param length The length of the sequence. + * @param leastWeightCandidate leastWeightCandidate(i, j) is the weight of arranging the first j windows, + * if we use the optimal arrangement of the first i windows, and the last layest consists of windows [i, j) + */ +static bool isDominated(size_t candidate, size_t alternativeSmall, size_t alternativeBig, size_t length, std::function leastWeightCandidate) +{ + Q_ASSERT(alternativeSmall < candidate && candidate < alternativeBig); + if (alternativeBig == length) { + return true; + } + + // We assumed that the weight function is concave, i.e., for all i <= j < k <= l, + // weight(i,k) + weight(j,l) <= weight(i,l) + weight(j,k) + // This implies the following about leastWeightCandidate: + // For all i <= j < k <= l + // - If leastWeightCandidate(i, l) <= leastWeightCandidate(j, l), then leastWeightCandidate(i, k) <= leastWeightCandidate(j, k) + // - If leastWeightCandidate(j, k) <= leastWeightCandidate(i, k), then leastWeightCandidate(j, l) <= leastWeightCandidate(i, l) + // + // In particular, this implies that the set of ks such that + // leastWeightCandidate(candidate, k) < leastWeightCandidate(alternativeSmall, k) + // is a (possibly empty) interval [k1, length] for some k1. + // This is because if for some k, + // leastWeightCandidate(alternativeSmall, k) <= leastWeightCandidate(candidate, k), + // then for all candidate < k' <= k, + // leastWeightCandidate(alternativeSmall, k') <= leastWeightCandidate(candidate, k') + // + // Similarly, the set of ks such that + // leastWeightCandidate(candidate, k) < leastWeightCandidate(alternativeBig, k) + // is a (possibly empty) interval [alternativeBig + 1, k2] for some k2. + // This is because if for some k, + // leastWeightCandidate(alternativeBig, k) <= leastWeightCandidate(candidate, k), + // then for all k' >= k + // leastWeightCandidate(alternativeBig, k') <= leastWeightCandidate(candidate, k') + // + // Hence, to check if a k exists in both intervals, we can use binary search to find the smallest k1 such that + // leastWeightCandidate(candidate, k1) < leastWeightCandidate(alternativeSmall, k1). + // If such a k1 exists, it suffices to check if + // leastWeightCandidate(alternativeBig, k1) <= leastWeightCandidate(candidate, k1). + if (leastWeightCandidate(alternativeSmall, length) <= leastWeightCandidate(candidate, length)) { + return true; + } + // Now we know that leastWeightCandidate(candidate, length) < leastWeightCandidate(alternativeSmall, length) + // i.e, the first interval is non-empty + // Our candidate k1 is in the interval (low, high] (inclusive on high) + size_t low = alternativeBig; + size_t high = length; + while (high - low >= 2) { + size_t mid = (low + high) / 2; + if (leastWeightCandidate(alternativeSmall, mid) <= leastWeightCandidate(candidate, mid)) { + low = mid; + } else { + high = mid; + } + } + return (leastWeightCandidate(alternativeBig, high) <= leastWeightCandidate(candidate, high)); +} + +/** + * @brief Returns the layerStartPos for a good packing of the windows using the Basic algorithm + * from Hirschberg, Daniel S., and Lawrence L. Larmore. "The least weight subsequence problem." + * SIAM Journal on Computing 16.4 (1987): 628-638. + * + * The Basic algorithm solves the Least Weight Subsequence Problem (LWS) for + * concave weight functions. + * + * The LWS problem on the interval [a,b] is defined as follows: + * Given a weight function weight(i,j) for all i,j in [a,b], find a subsequence + * of [a,b], i.e. a sequence of strictly monotonically increasing indices + * i_0 < i_2 < ... < i_t, such that the total weight, + * sum_{k=1}^t weight(i_{k-1}, i_k), is minimized. + * + * A weight function is concave if for all i <= j < k <= l, the following holds: + * weight(i,k) + weight(j,l) <= weight(i,l) + weight(j,k) + * + * The run time of the algorithm is O(n log n). + * + * Modified from the version in the paper to fix some bugs. + * + * @param idealWidth The target width of each layer. All widths of windows *MUST* be smaller than idealWidth. + * @param length The length of the sequence. Solves the LWS problem on the interval [0, length]. (n in paper) + * @param cumWidths cumWidths[i] is the sum of widths of windows 0, 1, ..., i - 1 + * + * @return QList The subsequence (starting at 0 and ending at length) + * that minimizes the total weight. The ith element is the index of the first window in layer i. + * Always starts with 0 and ends with ids.size(). + */ +static QList getLayerStartPos(qreal maxWidth, qreal idealWidth, const size_t length, const QList &cumWidths) +{ + // weight(start, end) is the penalty of placing all windows in the range [start, end) in the same layer. + // The following form only works when the maximum width of a window is less than or equal to idealWidth. + // + // The weight function is designed such that + // 1. The weight function is concave (see definition in Basic algorithm) + // 2. It scales like (width - idealWidth) ^ 2 for width < idealWidth + // 3. Exceeding maxWidth is guaranteed to be worse than any other solution + // + // 1. holds as long as weight(i, j) = f(cumWidths[j] - cumWidths[i]) for some convex function f + // 3. is guaranteed by making the penalty of exceeding maxWidth at least + // cumWidths.size(), which strictly upper bounds the total weight of placing + // each window in its own layer + // + auto weight = [maxWidth, idealWidth, &cumWidths](size_t start, size_t end) { + qreal width = cumWidths[end] - cumWidths[start]; + if (width < idealWidth) { + return (width - idealWidth) * (width - idealWidth) / idealWidth / idealWidth; + } else { + qreal penaltyFactor = cumWidths.size(); + return penaltyFactor * (width - idealWidth) * (width - idealWidth) / (maxWidth - idealWidth) / (maxWidth - idealWidth); + } + }; + + // layerStart[j] is where the last layer should start, if there were only the first j windows. + // I.e., layerStart[5]=3 means that the last layer should start at window 3 if there were only the first 10 windows + // (bestLeft in paper) + QList layerStart(length + 1); + // leastWeight[i] is the least weight of any subsequence starting at 0 and ending at i (f in paper) + QList leastWeight(length + 1); + // layerStartcandidates contains all current candidates for layerStart[currentIndex] (d in paper) + std::deque layerStartCandidates; + + leastWeight[0] = 0; + + // leastWeightCandidate(lastRowStartPos, num) is a candidate value for leastWeight[num]. + // It is the weight for arranging the first num windows, assuming optimal arrangement of + // the first lastRowStartPos windows, and a last layer consisting of windows [lastRowStartPos, num) + // (g in paper) + auto leastWeightCandidate = [&leastWeight, &weight](size_t lastRowStartPos, size_t num) { + return leastWeight[lastRowStartPos] + weight(lastRowStartPos, num); + }; + layerStartCandidates.push_back(0); + for (size_t currentIndex = 1; currentIndex < length; ++currentIndex) { // currentIndex is m in paper + leastWeight[currentIndex] = leastWeightCandidate(layerStartCandidates.front(), currentIndex); + layerStart[currentIndex] = layerStartCandidates.front(); + + // Modification of algorithm in paper; + // needed so that layerStartCandidates.front can be correctly removed when layerStartCandidates.size() == 1 + layerStartCandidates.push_back(currentIndex); + + // Remove candidates from the front if they are dominated by the second candidate + // Dominate means that the second candidate is at least as good as the first candidate for layerStart + while (layerStartCandidates.size() >= 2 && leastWeightCandidate(layerStartCandidates[1], currentIndex + 1) <= leastWeightCandidate(layerStartCandidates[0], currentIndex + 1)) { + layerStartCandidates.pop_front(); + } + layerStartCandidates.pop_back(); // Modification of algorithm in paper + + // Remove candidates from the back if they are dominated by either the second to last candidate, or currentIndex + while (layerStartCandidates.size() >= 2 && isDominated(layerStartCandidates.back(), layerStartCandidates[layerStartCandidates.size() - 2], currentIndex, length, leastWeightCandidate)) { + layerStartCandidates.pop_back(); + } + + // Modification of algorithm in paper; we need at least one candidate in layerStartCandidates + if (layerStartCandidates.empty()) { + layerStartCandidates.push_back(currentIndex); + continue; + } + + // Add currentIndex to layerStartCandidates if it is not dominated by the last candidate + if (leastWeightCandidate(currentIndex, length) < leastWeightCandidate(layerStartCandidates.back(), length)) { + layerStartCandidates.push_back(currentIndex); + } + } + + // recover the solution using layerStart + leastWeight[length] = leastWeightCandidate(layerStartCandidates.front(), length); + layerStart[length] = layerStartCandidates.front(); + + QList layerStartPosReversed; + layerStartPosReversed.push_back(length); + size_t currentIndex = length; + + while (currentIndex > 0) { + currentIndex = layerStart[currentIndex]; + layerStartPosReversed.push_back(currentIndex); + } + + return QList(layerStartPosReversed.rbegin(), layerStartPosReversed.rend()); +} + +// Reflection about the line y = x +static QMarginsF reflect(const QMarginsF &margins) +{ + return QMarginsF(margins.top(), margins.right(), margins.bottom(), margins.left()); +} +static QRectF reflect(const QRectF &rect) +{ + return QRectF(rect.y(), rect.x(), rect.height(), rect.width()); +} +static QPointF reflect(const QPointF &point) +{ + return point.transposed(); +} +template +static QList reflect(const QList &v) +{ + QList result; + result.reserve(v.size()); + for (const auto &x : v) { + result.emplace_back(reflect(x)); + } + return result; +} + +QList ExpoLayout::layout(const QRectF &area, const QList &windowSizes) +{ + const qreal shortSide = std::min(area.width(), area.height()); + const QMarginsF margins(shortSide * m_relativeMarginLeft, + shortSide * m_relativeMarginTop, + shortSide * m_relativeMarginRight, + shortSide * m_relativeMarginBottom); + const qreal minLength = m_relativeMinLength * shortSide; + const QRectF minSize = QRectF(0, 0, minLength, minLength); + + QList centers; + for (const QRectF &windowSize : windowSizes) { + centers.push_back(windowSize.center()); + } + + // windows bigger than 4x the area are considered ill-behaved and their sizes are clipped + const auto adjustedSizes = adjustSizes(minSize, QRectF(0, 0, 4 * area.width(), 4 * area.height()), margins, windowSizes); + + if (placementMode() == PlacementMode::Rows) { + LayeredPacking bestPacking = findGoodPacking(area, adjustedSizes, centers, m_idealWidthRatio, m_searchTolerance); + return refineAndApplyPacking(area, margins, bestPacking, adjustedSizes, centers); + } else { + QList adjustedSizesReflected(reflect(adjustedSizes)); + QList centersReflected(reflect(centers)); + + LayeredPacking bestPacking = findGoodPacking(area.transposed(), adjustedSizesReflected, centersReflected, m_idealWidthRatio, m_searchTolerance); + return reflect(refineAndApplyPacking(area.transposed(), reflect(margins), bestPacking, adjustedSizesReflected, centersReflected)); + } +} + +QList ExpoLayout::adjustSizes(const QRectF &minSize, const QRectF &maxSize, const QMarginsF &margins, const QList &windowSizes) +{ + QList adjustedSizes; + for (QRectF windowSize : windowSizes) { + windowSize.setWidth(std::clamp(windowSize.width(), minSize.width(), maxSize.width())); + windowSize.setHeight(std::clamp(windowSize.height(), minSize.height(), maxSize.height())); + windowSize += margins; + adjustedSizes.emplace_back(windowSize); + } + return adjustedSizes; +} + +LayeredPacking +ExpoLayout::findGoodPacking(const QRectF &area, const QList &windowSizes, const QList ¢ers, qreal idealWidthRatio, qreal tol) +{ + QList> windowSizesWithIds; + + for (int i = 0; i < windowSizes.size(); ++i) { + windowSizesWithIds.emplace_back(i, windowSizes[i], centers[i]); + } + + // Sorting by height ensures that windows in same layer (row) have similar heights + std::stable_sort(windowSizesWithIds.begin(), windowSizesWithIds.end(), [](const auto &a, const auto &b) { + // in case of same height, sort by y to minimize vertical movement + return std::tuple(std::get<1>(a).height(), std::get<2>(a).y()) + < std::tuple(std::get<1>(b).height(), std::get<2>(b).y()); + }); + + QList ids; // ids of windows in sorted order + QList cumWidths; // cumWidths[i] is the sum of widths of windows 0, 1, ..., i - 1 + + // Minimum and maximum strip widths to use in the binary search. + // Strips should be at least as wide as the widest window, and at most as + // wide as the sum of all window widths. + qreal stripWidthMin = 0; + qreal stripWidthMax = 0; + + cumWidths.push_back(0); + for (const auto &windowSizeWithId : windowSizesWithIds) { + ids.push_back(std::get<0>(windowSizeWithId)); + qreal width = std::get<1>(windowSizeWithId).width(); + cumWidths.push_back(cumWidths.back() + width); + + stripWidthMin = std::max(stripWidthMin, width); + stripWidthMax += width; + } + stripWidthMin /= idealWidthRatio; + stripWidthMax /= idealWidthRatio; + + qreal targetRatio = area.height() / area.width(); + + auto findPacking = [&windowSizes, &ids, &cumWidths, idealWidthRatio](qreal stripWidth) { + QList layerStartPos = getLayerStartPos(stripWidth, stripWidth * idealWidthRatio, ids.size(), cumWidths); + LayeredPacking result(stripWidth, windowSizes, ids, layerStartPos); + Q_ASSERT(result.width <= stripWidth); + return result; + }; + + // the placement with the minimum strip width corresponds with a big aspect + // ratio (ratioHigh), and the placement with the maximum strip width + // corresponds with a small aspect ratio (ratioLow) + + LayeredPacking placementWidthMin = findPacking(stripWidthMin); + qreal ratioHigh = placementWidthMin.height / placementWidthMin.width; + + if (ratioHigh <= targetRatio) { + return placementWidthMin; + } + + LayeredPacking placementWidthMax = findPacking(stripWidthMax); + qreal ratioLow = placementWidthMax.height / placementWidthMax.width; + + if (ratioLow >= targetRatio) { + return placementWidthMax; + } + + while (stripWidthMax / stripWidthMin > 1 + tol) { + qreal stripWidthMid = std::sqrt(stripWidthMin * stripWidthMax); + LayeredPacking placementMid = findPacking(stripWidthMid); + qreal ratioMid = placementMid.height / placementMid.width; + + if (ratioMid > targetRatio) { + stripWidthMin = stripWidthMid; + placementWidthMin = placementMid; + ratioHigh = ratioMid; + } else { + // small optimization: use the actual strip width + stripWidthMax = placementMid.width; + placementWidthMax = placementMid; + ratioLow = ratioMid; + } + } + + // how much we need to scale the placement to fit + qreal scaleWidthMin = std::min(area.width() / placementWidthMin.width, area.height() / placementWidthMin.height); + qreal scaleWidthMax = std::min(area.width() / placementWidthMax.width, area.height() / placementWidthMax.height); + + if (scaleWidthMin > scaleWidthMax) { + return placementWidthMin; + } else { + return placementWidthMax; + } +} + +QList ExpoLayout::refineAndApplyPacking(const QRectF &area, const QMarginsF &margins, const LayeredPacking &packing, const QList &windowSizes, const QList ¢ers) +{ + // Scale packing to fit area + qreal scale = std::min(area.width() / packing.width, area.height() / packing.height); + scale = std::min(scale, m_maxScale); + + const QMarginsF scaledMargins = QMarginsF(margins.left() * scale, margins.top() * scale, + margins.right() * scale, margins.bottom() * scale); + + // The maximum gap in additional to margins to leave between windows + qreal maxGapY = m_maxGapRatio * (scaledMargins.top() + scaledMargins.bottom()); + qreal maxGapX = m_maxGapRatio * (scaledMargins.left() + scaledMargins.right()); + + // center align y + qreal extraY = area.height() - packing.height * scale; + qreal gapY = std::min(maxGapY, extraY / (packing.layers.size() + 1)); + qreal y = area.y() + (extraY - gapY * (packing.layers.size() - 1)) / 2; + + QList finalWindowLayouts(windowSizes); + // smaller windows "float" to the top + for (const auto &layer : packing.layers) { + qreal extraX = area.width() - layer.width() * scale; + qreal gapX = std::min(maxGapX, extraX / (layer.ids.size() + 1)); + qreal x = area.x() + (extraX - gapX * (layer.ids.size() - 1)) / 2; + + QList ids(layer.ids); + std::stable_sort(ids.begin(), ids.end(), [¢ers](size_t a, size_t b) { + return centers[a].x() < centers[b].x(); // minimize horizontal movement + }); + for (auto id : std::as_const(ids)) { + QRectF &windowLayout = finalWindowLayouts[id]; + qreal newY = y + (layer.maxHeight - windowLayout.height()) * scale / 2; // center align y + windowLayout = QRectF(x, newY, windowLayout.width() * scale, windowLayout.height() * scale); + x += windowLayout.width() + gapX; + windowLayout -= scaledMargins; + } + y += layer.maxHeight * scale + gapY; + } + return finalWindowLayouts; +} + +#include "moc_expolayout.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/private/expolayout.h b/local/recipes/kde/kwin/source/src/plugins/private/expolayout.h new file mode 100644 index 0000000000..99f6957b72 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/private/expolayout.h @@ -0,0 +1,348 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + SPDX-FileCopyrightText: 2024 Yifan Zhu + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +#include + +class ExpoCell; +struct Layer; +struct LayeredPacking; + +/** + * @brief Adapts the algorithm from [0] to layout the windows intelligently. + * + * Design goals: + * - use screen space efficiently, given diverse geometries of windows + * - be aesthetically pleasing + * - and minimize movement of windows from initial positions + * + * More concretely, the algorithm produces a layered layout, where each layer, + * or strip, is a row or column. The algorithm tries to ensure that different + * strips have similar widths, and uses binary search to find a packing with + * similar aspect ratio to the layout area. Within each strip, the algorithm + * tries to minimize horizontal movement (for rows) or vertical movement (for + * columns) of the windows. + * + * [0] Hirschberg, Daniel S., and Lawrence L. Larmore. "The least weight + * subsequence problem." SIAM Journal on Computing 16.4 (1987): 628-638. + */ +class ExpoLayout : public QQuickItem +{ + Q_OBJECT + // Place windows in rows or columns. + Q_PROPERTY(PlacementMode placementMode READ placementMode WRITE setPlacementMode NOTIFY placementModeChanged) + Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged) + /** + * Stop binary search when the two candidate strip widths are within tol (as a fraction of the larger strip width). + * Default is 0.2. + */ + Q_PROPERTY(qreal searchTolerance MEMBER m_searchTolerance NOTIFY searchToleranceChanged) + /** + * The ideal sum of window widths in a strip (including added margins), as a fraction of the strip width. *MUST* be strictly less than 1. + * Default is 0.8. + */ + Q_PROPERTY(qreal idealWidthRatio MEMBER m_idealWidthRatio NOTIFY idealWidthRatioChanged) + /** + * Left margin size, as a ratio of the short side of layout area. Default is 0.07. + * The margins are added to each window before layout. + */ + Q_PROPERTY(qreal relativeMarginLeft MEMBER m_relativeMarginLeft NOTIFY relativeMarginLeftChanged) + /** + * Right margin size, as a ratio of the short side of layout area. Default is 0.07. + * The margins are added to each window before layout. + */ + Q_PROPERTY(qreal relativeMarginRight MEMBER m_relativeMarginRight NOTIFY relativeMarginRightChanged) + /** + * Top margin size, as a ratio of the short side of layout area. Default is 0.07. + * The margins are added to each window before layout. + */ + Q_PROPERTY(qreal relativeMarginTop MEMBER m_relativeMarginTop NOTIFY relativeMarginTopChanged) + /** + * Bottom margin size, as a ratio of the short side of layout area. Default is 0.07. + * The margins are added to each window before layout. + */ + Q_PROPERTY(qreal relativeMarginBottom MEMBER m_relativeMarginBottom NOTIFY relativeMarginBottomChanged) + /** + * Minimal length of windows, as a ratio of the short side of layout area. + * Smaller windows will be resized to this. Default is 0.15. + */ + Q_PROPERTY(qreal relativeMinLength MEMBER m_relativeMinLength NOTIFY relativeMinLengthChanged) + /** + * Maximum additional gap between windows, as a ratio of normal spacing (2*margin). Default is 1.5. + */ + Q_PROPERTY(qreal maxGapRatio MEMBER m_maxGapRatio NOTIFY maxGapRatioChanged) + /** + * Maximum scale applied to windows, *after* the minimum length is enforced. Default is 1.0. + */ + Q_PROPERTY(qreal maxScale MEMBER m_maxScale NOTIFY maxScaleChanged) + +public: + enum PlacementMode : uint { + Rows, + Columns, + }; + Q_ENUM(PlacementMode) + + explicit ExpoLayout(QQuickItem *parent = nullptr); + + PlacementMode placementMode() const; + void setPlacementMode(PlacementMode mode); + + void addCell(ExpoCell *cell); + void removeCell(ExpoCell *cell); + + bool isReady() const; + void setReady(); + + Q_INVOKABLE void forceLayout(); + Q_INVOKABLE void updateCellsMapping(); + +protected: + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; + void updatePolish() override; + + /** + * @brief Layout the windows with @param windowSizes into @param area. + * + * This is the main entry point for the layout algorithm. + */ + QList layout(const QRectF &area, const QList &windowSizes); + + /** + * @brief First clip @param windowSizes to be between @param minSize and + * @param maxSize. Then add @param margins to each window size, and @return + * the adjusted window sizes. + */ + QList adjustSizes(const QRectF &minSize, const QRectF &maxSize, const QMarginsF &margins, const QList &windowSizes); + + /** + * @brief Use binary search to find a good packing of the @param windowSizes + * into @param area such that the resulting packing has similar aspect ratio + * (height/width) to @param area. + * + * The binary search is performed on the logarithm of the width of the + * possible packings, and the search is terminated when the width of the + * packing is within @param tol of the ideal width. + * + * We try to find a packing such that the total widths of windows in each + * layer are close to @param idealWidthRatio times the maximum width of the + * packing. + * + * In the case of identical window heights, we also try to minimize vertical + * movement based on the @param centers of the windows. + * + * Run time is O(n log n log log (totalWidth / maxWidth)) + * Since we clip the window size, this is just O(n log n log log n) + */ + LayeredPacking + findGoodPacking(const QRectF &area, const QList &windowSizes, const QList ¢ers, qreal idealWidthRatio, qreal tol); + + /** + * @brief LogicalOutput the final window layouts from the packing. + * + * Geven @param windowSizes, scale @param packing to fit @param area, + * remove previously added @param margins, add padding and align, + * and @return the final layout. + * In each layer, sort the windows by x coordinates of the @param centers. + */ + QList refineAndApplyPacking(const QRectF &area, const QMarginsF &margins, const LayeredPacking &packing, const QList &windowSizes, const QList ¢ers); + +Q_SIGNALS: + void placementModeChanged(); + void readyChanged(); + void searchToleranceChanged(); + void idealWidthRatioChanged(); + void relativeMarginLeftChanged(); + void relativeMarginRightChanged(); + void relativeMarginTopChanged(); + void relativeMarginBottomChanged(); + void relativeMinLengthChanged(); + void maxGapRatioChanged(); + void maxScaleChanged(); + +private: + QList m_cells; + PlacementMode m_placementMode = Rows; + bool m_ready = false; + + qreal m_searchTolerance = 0.2; + qreal m_idealWidthRatio = 0.8; + qreal m_relativeMarginLeft = 0.07; + qreal m_relativeMarginRight = 0.07; + qreal m_relativeMarginTop = 0.07; + qreal m_relativeMarginBottom = 0.07; + qreal m_relativeMinLength = 0.15; + qreal m_maxGapRatio = 1.5; + qreal m_maxScale = 1.0; +}; + +class ExpoCell : public QQuickItem +{ + Q_OBJECT + Q_PROPERTY(ExpoLayout *layout READ layout WRITE setLayout NOTIFY layoutChanged) + Q_PROPERTY(QQuickItem *contentItem READ contentItem WRITE setContentItem NOTIFY contentItemChanged) + Q_PROPERTY(qreal partialActivationFactor READ partialActivationFactor WRITE setPartialActivationFactor NOTIFY partialActivationFactorChanged) + Q_PROPERTY(bool shouldLayout READ shouldLayout WRITE setShouldLayout NOTIFY shouldLayoutChanged) + Q_PROPERTY(qreal offsetX READ offsetX WRITE setOffsetX NOTIFY offsetXChanged) + Q_PROPERTY(qreal offsetY READ offsetY WRITE setOffsetY NOTIFY offsetYChanged) + Q_PROPERTY(qreal naturalX READ naturalX WRITE setNaturalX NOTIFY naturalXChanged) + Q_PROPERTY(qreal naturalY READ naturalY WRITE setNaturalY NOTIFY naturalYChanged) + Q_PROPERTY(qreal naturalWidth READ naturalWidth WRITE setNaturalWidth NOTIFY naturalWidthChanged) + Q_PROPERTY(qreal naturalHeight READ naturalHeight WRITE setNaturalHeight NOTIFY naturalHeightChanged) + Q_PROPERTY(QString persistentKey READ persistentKey WRITE setPersistentKey NOTIFY persistentKeyChanged) + Q_PROPERTY(qreal bottomMargin READ bottomMargin WRITE setBottomMargin NOTIFY bottomMarginChanged) + +public: + explicit ExpoCell(QQuickItem *parent = nullptr); + ~ExpoCell() override; + + void componentComplete() override; + + ExpoLayout *layout() const; + void setLayout(ExpoLayout *layout); + + bool shouldLayout() const; + void setShouldLayout(bool layout); + + QQuickItem *contentItem() const; + void setContentItem(QQuickItem *item); + + qreal partialActivationFactor() const; + void setPartialActivationFactor(qreal factor); + + qreal offsetX() const; + void setOffsetX(qreal x); + + qreal offsetY() const; + void setOffsetY(qreal y); + + qreal naturalX() const; + void setNaturalX(qreal x); + + qreal naturalY() const; + void setNaturalY(qreal y); + + qreal naturalWidth() const; + void setNaturalWidth(qreal width); + + qreal naturalHeight() const; + void setNaturalHeight(qreal height); + + QRectF naturalRect() const; + QMarginsF margins() const; + + QString persistentKey() const; + void setPersistentKey(const QString &key); + + qreal bottomMargin() const; + void setBottomMargin(qreal margin); + +protected: + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; + +Q_SIGNALS: + void layoutChanged(); + void shouldLayoutChanged(); + void contentItemChanged(); + void partialActivationFactorChanged(); + void offsetXChanged(); + void offsetYChanged(); + void naturalXChanged(); + void naturalYChanged(); + void naturalWidthChanged(); + void naturalHeightChanged(); + void persistentKeyChanged(); + void bottomMarginChanged(); + +private Q_SLOTS: + void updateContentItemGeometry(); + +private: + void updateLayout(); + + QString m_persistentKey; + qreal m_offsetX = 0; + qreal m_offsetY = 0; + qreal m_naturalX = 0; + qreal m_naturalY = 0; + qreal m_naturalWidth = 0; + qreal m_naturalHeight = 0; + QMarginsF m_margins; + QPointer m_layout; + QPointer m_contentItem; + qreal m_partialActivationFactor = 1.0; + bool m_shouldLayout = true; +}; + +/** + * @brief Each Layer is a horizontal strip of windows with a maximum width and + * height. + */ +struct Layer +{ + qreal maxWidth; + qreal maxHeight; + /** + * @brief The remaining width available to new windows in this layer. + * width() + remainingWidth() == maxWidth + */ + qreal remainingWidth; + + /** + * @brief The indices of windows in this layer. + */ + QList ids; + + /** + * @brief Initializes a new layer with the given maximum width and populates + * it with the given windows. + * + * @param maxWidth The maximum width of the layer. + * @param windowSizes The sizes of all the windows. Must be sorted in + * ascending order by height. + * @param windowIds Ids of the windows. + * @param startPos windowIds[startPos] is the first window in this layer. + * @param endPos windowIds[endPos-1] is the last window in this layer. + */ + Layer(qreal maxWidth, const QList &windowSizes, const QList &windowIds, size_t startPos, size_t endPos); + + /** + * @brief The total width of all the windows in this layer. + * + */ + qreal width() const; +}; + +/** + * @brief A LayeredPacking is a packing of windows into layers, which are + * horizontal strips of windows. + */ +struct LayeredPacking +{ + qreal maxWidth; + qreal width; + qreal height; + QList layers; + + /** + * @brief Construct a new LayeredPacking object from a list of windows + * sorted by height in descending order. + * + * @param maxWidth The maximum width of the packing. + * @param windowSizes must be sorted by height in ascending order + * @param ids Ids of the windows + * @param layerStartPos Array of indices into ids that indicate the start + * of a new layer. Must start with 0 and end with ids.size(). + */ + LayeredPacking(qreal maxWidth, const QList &windowSizes, const QList &ids, const QList &layerStartPos); +}; diff --git a/local/recipes/kde/kwin/source/src/plugins/private/plugin.cpp b/local/recipes/kde/kwin/source/src/plugins/private/plugin.cpp new file mode 100644 index 0000000000..014954363b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/private/plugin.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "plugin.h" +#include "expoarea.h" +#include "expolayout.h" + +void EffectKitExtensionPlugin::registerTypes(const char *uri) +{ + qmlRegisterType(uri, 1, 0, "ExpoArea"); + qmlRegisterType(uri, 1, 0, "ExpoLayout"); + qmlRegisterType(uri, 1, 0, "ExpoCell"); +} + +#include "moc_plugin.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/private/plugin.h b/local/recipes/kde/kwin/source/src/plugins/private/plugin.h new file mode 100644 index 0000000000..ad42b61a71 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/private/plugin.h @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +class EffectKitExtensionPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") + +public: + void registerTypes(const char *uri) override; +}; diff --git a/local/recipes/kde/kwin/source/src/plugins/private/qml/WindowHeap.qml b/local/recipes/kde/kwin/source/src/plugins/private/qml/WindowHeap.qml new file mode 100644 index 0000000000..427ce60ee1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/private/qml/WindowHeap.qml @@ -0,0 +1,389 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + SPDX-FileCopyrightText: 2022 ivan tkachenko + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick +import QtQuick.Window +import org.kde.kirigami as Kirigami +import org.kde.kwin as KWinComponents +import org.kde.kwin.private.effects + +FocusScope { + id: heap + + enum Direction { + Left, + Right, + Up, + Down + } + + property alias model: windowsInstantiator.model + property alias delegate: windowsInstantiator.delegate + readonly property alias count: windowsInstantiator.count + readonly property bool activeEmpty: { + var children = expoLayout.visibleChildren; + for (var i = 0; i < children.length; i++) { + var child = children[i]; + if (child instanceof WindowHeapDelegate && !child.activeHidden) { + return false; + } + } + return true; + } + + property alias layout: expoLayout + property int selectedIndex: -1 + property int animationDuration: Kirigami.Units.longDuration + property bool animationEnabled: false + property bool absolutePositioning: true + property real padding: 0 + // Either a string "activeClass" or a list internalIds of windows + property var showOnly: [] + + required property bool organized + readonly property bool effectiveOrganized: expoLayout.ready && organized + property bool dragActive: false + + signal activated() + + function activateIndex(index) { + KWinComponents.Workspace.activeWindow = windowsInstantiator.objectAt(index).window; + activated(); + } + + property var dndManagerStore: ({}) + + function saveDND(key: int, rect: rect) { + dndManagerStore[key] = rect; + } + function containsDND(key: int): bool { + return key in dndManagerStore; + } + function restoreDND(key: int): rect { + return dndManagerStore[key]; + } + function deleteDND(key: int) { + delete dndManagerStore[key]; + } + + KWinComponents.WindowThumbnail { + id: otherScreenThumbnail + z: 2 + property KWinComponents.WindowThumbnail cloneOf + visible: false + client: cloneOf ? cloneOf.client : null + width: cloneOf ? cloneOf.width : 0 + height: cloneOf ? cloneOf.height : 0 + onCloneOfChanged: { + if (!cloneOf) { + visible = false; + } + } + } + + Connections { + target: effect + function onItemDraggedOutOfScreen(item, screens) { + let found = false; + + // don't put a proxy for item's own screen + if (screens.length === 0 || item.screen === targetScreen) { + otherScreenThumbnail.visible = false; + return; + } + + for (let i in screens) { + if (targetScreen === screens[i]) { + found = true; + const heapRelativePos = heap.mapFromGlobal(item.mapToGlobal(0, 0)); + otherScreenThumbnail.cloneOf = item + otherScreenThumbnail.x = heapRelativePos.x; + otherScreenThumbnail.y = heapRelativePos.y; + if (item.Drag.active) { + otherScreenThumbnail.visible = true; + } + } + } + + if (!found) { + otherScreenThumbnail.visible = false; + } + } + function onItemDroppedOutOfScreen(pos, item, screen) { + if (screen === targetScreen) { + // To actually move we need a screen number rather than an EffectScreen + KWinComponents.Workspace.sendClientToScreen(item.client, KWinComponents.Workspace.screenAt(pos)); + } + } + } + + ExpoLayout { + id: expoLayout + + anchors.fill: parent + anchors.margins: heap.padding + + placementMode: width >= height ? ExpoLayout.Rows : ExpoLayout.Columns + + Instantiator { + id: windowsInstantiator + + asynchronous: true + + delegate: WindowHeapDelegate { + windowHeap: heap + layout: expoLayout + } + + onObjectRemoved: (index, object) => { + // undo explicitly set parent in objectAdded so it can be + // removed from the scene immediately + object.parent = null + } + + onObjectAdded: (index, object) => { + object.parent = expoLayout + var key = object.window.internalId; + if (heap.containsDND(key)) { + expoLayout.forceLayout(); + var oldGlobalRect = heap.restoreDND(key); + object.restoreDND(oldGlobalRect); + heap.deleteDND(key); + } else if (heap.effectiveOrganized) { + // New window has opened in the middle of a running effect. + // Make sure it is positioned before enabling its animations. + expoLayout.forceLayout(); + } + object.animationEnabled = true; + } + } + } + + function findFirstItem() { + for (let candidateIndex = 0; candidateIndex < windowsInstantiator.count; ++candidateIndex) { + const candidateItem = windowsInstantiator.objectAt(candidateIndex); + if (!candidateItem.activeHidden) { + return candidateIndex; + } + } + return -1; + } + + function findNextItem(selectedIndex, direction) { + if (selectedIndex === -1) { + return findFirstItem(); + } + + const selectedItem = windowsInstantiator.objectAt(selectedIndex); + let nextIndex = -1; + + switch (direction) { + case WindowHeap.Direction.Left: + for (let candidateIndex = 0; candidateIndex < windowsInstantiator.count; ++candidateIndex) { + const candidateItem = windowsInstantiator.objectAt(candidateIndex); + if (candidateItem.activeHidden) { + continue; + } + + if (candidateItem.y + candidateItem.height <= selectedItem.y) { + continue; + } else if (selectedItem.y + selectedItem.height <= candidateItem.y) { + continue; + } + + if (candidateItem.x + candidateItem.width < selectedItem.x + selectedItem.width) { + if (nextIndex === -1) { + nextIndex = candidateIndex; + } else { + const nextItem = windowsInstantiator.objectAt(nextIndex); + if (candidateItem.x + candidateItem.width > nextItem.x + nextItem.width) { + nextIndex = candidateIndex; + } + } + } + } + break; + case WindowHeap.Direction.Right: + for (let candidateIndex = 0; candidateIndex < windowsInstantiator.count; ++candidateIndex) { + const candidateItem = windowsInstantiator.objectAt(candidateIndex); + if (candidateItem.activeHidden) { + continue; + } + + if (candidateItem.y + candidateItem.height <= selectedItem.y) { + continue; + } else if (selectedItem.y + selectedItem.height <= candidateItem.y) { + continue; + } + + if (selectedItem.x < candidateItem.x) { + if (nextIndex === -1) { + nextIndex = candidateIndex; + } else { + const nextItem = windowsInstantiator.objectAt(nextIndex); + if (nextIndex === -1 || candidateItem.x < nextItem.x) { + nextIndex = candidateIndex; + } + } + } + } + break; + case WindowHeap.Direction.Up: + for (let candidateIndex = 0; candidateIndex < windowsInstantiator.count; ++candidateIndex) { + const candidateItem = windowsInstantiator.objectAt(candidateIndex); + if (candidateItem.activeHidden) { + continue; + } + + if (candidateItem.x + candidateItem.width <= selectedItem.x) { + continue; + } else if (selectedItem.x + selectedItem.width <= candidateItem.x) { + continue; + } + + if (candidateItem.y + candidateItem.height < selectedItem.y + selectedItem.height) { + if (nextIndex === -1) { + nextIndex = candidateIndex; + } else { + const nextItem = windowsInstantiator.objectAt(nextIndex); + if (nextItem.y + nextItem.height < candidateItem.y + candidateItem.height) { + nextIndex = candidateIndex; + } + } + } + } + break; + case WindowHeap.Direction.Down: + for (let candidateIndex = 0; candidateIndex < windowsInstantiator.count; ++candidateIndex) { + const candidateItem = windowsInstantiator.objectAt(candidateIndex); + if (candidateItem.activeHidden) { + continue; + } + + if (candidateItem.x + candidateItem.width <= selectedItem.x) { + continue; + } else if (selectedItem.x + selectedItem.width <= candidateItem.x) { + continue; + } + + if (selectedItem.y < candidateItem.y) { + if (nextIndex === -1) { + nextIndex = candidateIndex; + } else { + const nextItem = windowsInstantiator.objectAt(nextIndex); + if (candidateItem.y < nextItem.y) { + nextIndex = candidateIndex; + } + } + } + } + break; + } + + return nextIndex; + } + + function resetSelected() { + selectedIndex = -1; + } + + function selectNextItem(direction) { + const nextIndex = findNextItem(selectedIndex, direction); + if (nextIndex !== -1) { + selectedIndex = nextIndex; + return true; + } + return false; + } + + function selectLastItem(direction) { + let last = selectedIndex; + while (true) { + const next = findNextItem(last, direction); + if (next === -1) { + break; + } else { + last = next; + } + } + if (last !== -1) { + selectedIndex = last; + return true; + } + return false; + } + + + Keys.onPressed: event => { + let handled = false; + switch (event.key) { + case Qt.Key_Up: + handled = selectNextItem(WindowHeap.Direction.Up); + heap.focus = true; + break; + case Qt.Key_Down: + handled = selectNextItem(WindowHeap.Direction.Down); + heap.focus = true; + break; + case Qt.Key_Left: + handled = selectNextItem(WindowHeap.Direction.Left); + heap.focus = true; + break; + case Qt.Key_Right: + handled = selectNextItem(WindowHeap.Direction.Right); + heap.focus = true; + break; + case Qt.Key_Home: + handled = selectLastItem(WindowHeap.Direction.Left); + heap.focus = true; + break; + case Qt.Key_End: + handled = selectLastItem(WindowHeap.Direction.Right); + heap.focus = true; + break; + case Qt.Key_PageUp: + handled = selectLastItem(WindowHeap.Direction.Up); + heap.focus = true; + break; + case Qt.Key_PageDown: + handled = selectLastItem(WindowHeap.Direction.Down); + heap.focus = true; + break; + case Qt.Key_Space: + if (!heap.focus) { + break; + } + case Qt.Key_Return: + handled = false; + let selectedItem = null; + if (selectedIndex !== -1) { + selectedItem = windowsInstantiator.objectAt(selectedIndex); + } else { + // If the window heap has only one visible window, activate it. + for (let i = 0; i < windowsInstantiator.count; ++i) { + const candidateItem = windowsInstantiator.objectAt(i); + if (candidateItem.activeHidden) { + continue; + } else if (selectedItem) { + selectedItem = null; + break; + } + selectedItem = candidateItem; + } + } + if (selectedItem) { + handled = true; + KWinComponents.Workspace.activeWindow = selectedItem.window; + activated(); + } + break; + default: + return; + } + event.accepted = handled; + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/private/qml/WindowHeapDelegate.qml b/local/recipes/kde/kwin/source/src/plugins/private/qml/WindowHeapDelegate.qml new file mode 100644 index 0000000000..100cd40b04 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/private/qml/WindowHeapDelegate.qml @@ -0,0 +1,392 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + SPDX-FileCopyrightText: 2022 ivan tkachenko + SPDX-FileCopyrightText: 2024 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick +import QtQuick.Window +import Qt5Compat.GraphicalEffects +import org.kde.kirigami as Kirigami +import org.kde.kwin as KWinComponents +import org.kde.kwin.private.effects +import org.kde.plasma.components as PC3 +import org.kde.plasma.extras as PlasmaExtras +import org.kde.ksvg as KSvg + +ExpoCell { + id: thumb + + required property QtObject window + required property int index + required property Item windowHeap + + readonly property bool selected: windowHeap.selectedIndex === index + property bool gestureInProgress: effect.gestureInProgress + // Where the internal contentItem will be parented to + property Item contentItemParent: this + + // no desktops is a special value which means "All Desktops" + readonly property bool presentOnCurrentDesktop: !window.desktops.length || window.desktops.indexOf(KWinComponents.Workspace.currentDesktop) !== -1 + readonly property bool initialHidden: window.minimized + readonly property bool activeHidden: { + if (window.skipSwitcher) { + return true; + } else if (windowHeap.showOnly === "activeClass") { + if (!KWinComponents.Workspace.activeWindow) { + return true; + } else { + return KWinComponents.Workspace.activeWindow.resourceName !== window.resourceName; + } + } else { + return windowHeap.showOnly.length !== 0 + && windowHeap.showOnly.indexOf(window.internalId) === -1; + } + } + + // Show a close button on this thumbnail + property bool closeButtonVisible: true + // Show a text label under this thumbnail + property bool windowTitleVisible: true + + // Same as for window heap + property bool animationEnabled: false + + //scale up and down the whole thumbnail without affecting layouting + property real targetScale: 1.0 + + property DragManager activeDragHandler: dragHandler + + // Swipe down gesture by touch, in some effects will close the window + readonly property alias downGestureProgress: touchDragHandler.downGestureProgress + signal downGestureTriggered() + + property bool isReady: width !== 0 && height !== 0 + + function restoreDND(oldGlobalRect: rect) { + thumbSource.restoreDND(oldGlobalRect); + } + + layout: windowHeap.layout + shouldLayout: !thumb.activeHidden + partialActivationFactor: effect.partialActivationFactor + naturalX: thumb.window.x - thumb.window.output.geometry.x + naturalY: thumb.window.y - thumb.window.output.geometry.y + naturalWidth: thumb.window.width + naturalHeight: thumb.window.height + persistentKey: thumb.window.internalId + bottomMargin: icon.height / 4 + (caption.visible ? caption.height + Kirigami.Units.smallSpacing : 0) + Kirigami.Units.largeSpacing + + Behavior on x { + enabled: thumb.isReady + NumberAnimation { + duration: thumb.windowHeap.animationDuration + easing.type: Easing.InOutCubic + } + } + Behavior on y { + enabled: thumb.isReady + NumberAnimation { + duration: thumb.windowHeap.animationDuration + easing.type: Easing.InOutCubic + } + } + Behavior on width { + enabled: thumb.isReady + NumberAnimation { + duration: thumb.windowHeap.animationDuration + easing.type: Easing.InOutCubic + } + } + Behavior on height { + enabled: thumb.isReady + NumberAnimation { + duration: thumb.windowHeap.animationDuration + easing.type: Easing.InOutCubic + } + } + + contentItem: Item { + id: mainContent + parent: contentItemParent + visible: opacity > 0 && (!activeHidden || !initialHidden) + opacity: (1 - downGestureProgress) * (initialHidden ? partialActivationFactor : 1) + z: (activeDragHandler.active || returnAnimation.running) ? 1000 + : thumb.window.stackingOrder * (presentOnCurrentDesktop ? 1 : 0.001) + + KWinComponents.WindowThumbnail { + id: thumbSource + wId: thumb.window.internalId + scale: targetScale + width: mainContent.width + height: mainContent.height + + Binding on width { + value: mainContent.width + when: !returnAnimation.active + } + Binding on height { + value: mainContent.height + when: !returnAnimation.active + } + + Drag.proposedAction: Qt.MoveAction + Drag.supportedActions: Qt.MoveAction + Drag.source: thumb.window + Drag.hotSpot: Qt.point( + thumb.activeDragHandler.centroid.pressPosition.x, + thumb.activeDragHandler.centroid.pressPosition.y) + Drag.keys: ["kwin-window"] + + onXChanged: effect.checkItemDraggedOutOfScreen(thumbSource) + onYChanged: effect.checkItemDraggedOutOfScreen(thumbSource) + + function saveDND() { + const oldGlobalRect = mapToItem(null, 0, 0, width, height); + thumb.windowHeap.saveDND(thumb.window.internalId, oldGlobalRect); + } + function restoreDND(oldGlobalRect: rect) { + const newGlobalRect = mapFromItem(null, oldGlobalRect); + // We need proper mapping for the heap geometry because they are positioned with + // translation transformations + const heapRect = thumb.windowHeap.mapToItem(null, Qt.size(thumb.windowHeap.width, thumb.windowHeap.height)); + // Disable bindings + returnAnimation.active = true; + x = newGlobalRect.x; + y = newGlobalRect.y; + width = newGlobalRect.width; + height = newGlobalRect.height; + + returnAnimation.restart(); + + // If we dropped on another desktop, don't make the window fly off the screen + if ((oldGlobalRect.x < heapRect.x && heapRect.x + heapRect.width < oldGlobalRect.x + oldGlobalRect.width) || + (oldGlobalRect.y < heapRect.y && heapRect.y + heapRect.height < oldGlobalRect.y + oldGlobalRect.height)) { + returnAnimation.complete(); + } + } + function deleteDND() { + thumb.windowHeap.deleteDND(thumb.window.internalId); + } + + // Not using FrameSvg hover element intentionally for stylistic reasons + Rectangle { + border.width: 6 + border.color: Kirigami.Theme.highlightColor + anchors.fill: parent + anchors.margins: -border.width + radius: Kirigami.Units.cornerRadius + color: "transparent" + visible: !thumb.windowHeap.dragActive && (hoverHandler.hovered || (thumb.selected && Window.window.activeFocusItem)) && windowHeap.effectiveOrganized + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + cursorShape: thumb.activeDragHandler.active ? Qt.ClosedHandCursor : Qt.ArrowCursor + } + ParallelAnimation { + id: returnAnimation + property bool active: false + onRunningChanged: active = running + NumberAnimation { + target: thumbSource + properties: "x,y" + to: 0 + duration: thumb.windowHeap.animationDuration + easing.type: Easing.InOutCubic + } + NumberAnimation { + target: thumbSource + property: "width" + to: mainContent.width + duration: thumb.windowHeap.animationDuration + easing.type: Easing.InOutCubic + } + NumberAnimation { + target: thumbSource + property: "height" + to: mainContent.height + duration: thumb.windowHeap.animationDuration + easing.type: Easing.InOutCubic + } + NumberAnimation { + target: thumbSource + property: "scale" + to: 1 + duration: thumb.windowHeap.animationDuration + easing.type: Easing.InOutCubic + } + } + } + + PC3.Label { + anchors.fill: thumbSource + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: i18nd("kwin", "Drag Down To Close") + visible: !thumb.activeHidden && touchDragHandler.active + background: Rectangle { + anchors.centerIn: parent + height: parent.contentHeight + Kirigami.Units.smallSpacing + width: parent.contentWidth + Kirigami.Units.smallSpacing + color: Kirigami.Theme.backgroundColor + radius: Kirigami.Units.cornerRadius + } + } + + Kirigami.Icon { + id: icon + width: Kirigami.Units.iconSizes.large + height: Kirigami.Units.iconSizes.large + opacity: partialActivationFactor + scale: Math.min(1.0, mainContent.width / Math.max(0.01, thumb.width)) + source: thumb.window.icon + anchors.horizontalCenter: thumbSource.horizontalCenter + anchors.verticalCenter: thumbSource.bottom + anchors.verticalCenterOffset: -Math.round(height / 4) * scale + visible: !thumb.activeHidden && !activeDragHandler.active && !returnAnimation.running + PC3.Label { + id: caption + visible: thumb.window.caption.length > 0 && thumb.windowTitleVisible + width: thumb.width + maximumLineCount: 1 + anchors.top: parent.bottom + anchors.topMargin: Kirigami.Units.smallSpacing + anchors.horizontalCenter: parent.horizontalCenter + elide: Text.ElideRight + text: thumb.window.caption + color: Kirigami.Theme.textColor + textFormat: Text.PlainText + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + background: Rectangle { + anchors.centerIn: parent + height: parent.contentHeight + Kirigami.Units.smallSpacing + width: parent.contentWidth + Kirigami.Units.smallSpacing + color: Kirigami.Theme.backgroundColor + radius: Kirigami.Units.cornerRadius + } + } + } + + HoverHandler { + id: hoverHandler + onHoveredChanged: if (hovered !== selected) { + thumb.windowHeap.resetSelected(); + } + } + + TapHandler { + acceptedButtons: Qt.LeftButton + onTapped: { + KWinComponents.Workspace.activeWindow = thumb.window; + thumb.windowHeap.activated(); + } + onPressedChanged: { + if (pressed) { + thumbSource.Drag.active = true; + } else if (!thumb.activeDragHandler.active) { + thumbSource.Drag.active = false; + } + } + } + + component DragManager : DragHandler { + target: thumbSource + grabPermissions: PointerHandler.CanTakeOverFromAnything + // This does not work when moving pointer fast and pressing along the way + // See also QTBUG-105903, QTBUG-105904 + // enabled: thumb.state !== "active-normal" + + onActiveChanged: { + thumb.windowHeap.dragActive = active; + if (active) { + thumb.activeDragHandler = this; + } else { + thumbSource.saveDND(); + returnAnimation.restart(); + + var action = thumbSource.Drag.drop(); + if (action === Qt.MoveAction) { + // This whole component is in the process of being destroyed due to drop onto + // another virtual desktop (not another screen). + if (typeof thumbSource !== "undefined") { + // Except the case when it was dropped on the same desktop which it's already on, so let's return to normal state anyway. + thumbSource.deleteDND(); + } + return; + } + + var globalPos = targetScreen.mapToGlobal(centroid.scenePosition); + effect.checkItemDroppedOutOfScreen(globalPos, thumbSource); + + if (typeof thumbSource !== "undefined") { + // else, return to normal without reparenting + thumbSource.deleteDND(); + } + } + } + } + + DragManager { + id: dragHandler + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.Stylus + } + + DragManager { + id: touchDragHandler + acceptedDevices: PointerDevice.TouchScreen + readonly property double downGestureProgress: { + if (!active) { + return 0.0; + } + + const startDistance = thumb.windowHeap.Kirigami.ScenePosition.y + thumb.windowHeap.height - centroid.scenePressPosition.y; + const localPosition = thumb.windowHeap.Kirigami.ScenePosition.y + thumb.windowHeap.height - centroid.scenePosition.y; + return 1 - Math.min(localPosition/startDistance, 1); + } + + onActiveChanged: { + if (!active) { + if (downGestureProgress > 0.6) { + thumb.downGestureTriggered(); + } + } + } + } + + Loader { + id: closeButton + LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft + + anchors { + right: thumbSource.right + top: thumbSource.top + margins: Kirigami.Units.smallSpacing + } + active: thumb.closeButtonVisible && (hoverHandler.hovered || Kirigami.Settings.tabletMode || Kirigami.Settings.hasTransientTouchInput) && thumb.window.closeable && !thumb.activeDragHandler.active && !returnAnimation.running + + sourceComponent: PC3.Button { + text: i18ndc("kwin", "@info:tooltip as in: 'close this window'", "Close window") + icon.name: "window-close" + display: PC3.AbstractButton.IconOnly + + PC3.ToolTip.text: text + PC3.ToolTip.visible: hovered && display === PC3.AbstractButton.IconOnly + PC3.ToolTip.delay: Kirigami.Units.toolTipDelay + Accessible.name: text + + onClicked: thumb.window.closeWindow(); + } + } + + Component.onDestruction: { + if (selected) { + windowHeap.resetSelected(); + } + } + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/qpa/CMakeLists.txt new file mode 100644 index 0000000000..dc96a5adf2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/CMakeLists.txt @@ -0,0 +1,30 @@ +add_library(KWinQpaPlugin OBJECT) +target_sources(KWinQpaPlugin PRIVATE + backingstore.cpp + clipboard.cpp + eglhelpers.cpp + eglplatformcontext.cpp + integration.cpp + main.cpp + offscreensurface.cpp + platformcursor.cpp + screen.cpp + swapchain.cpp + window.cpp +) + +ecm_qt_declare_logging_category(KWinQpaPlugin + HEADER logging.h + IDENTIFIER KWIN_QPA + CATEGORY_NAME kwin_qpa_plugin + DEFAULT_SEVERITY Critical +) + +target_compile_definitions(KWinQpaPlugin PRIVATE QT_STATICPLUGIN) + +target_link_libraries(KWinQpaPlugin PRIVATE + Qt::Concurrent + Qt::CorePrivate + Qt::GuiPrivate + kwin +) diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/backingstore.cpp b/local/recipes/kde/kwin/source/src/plugins/qpa/backingstore.cpp new file mode 100644 index 0000000000..9a9643882e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/backingstore.cpp @@ -0,0 +1,112 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "backingstore.h" +#include "core/graphicsbuffer.h" +#include "core/graphicsbufferview.h" +#include "internalwindow.h" +#include "logging.h" +#include "swapchain.h" +#include "window.h" + +#include +#include + +namespace KWin +{ +namespace QPA +{ + +BackingStore::BackingStore(QWindow *window) + : QPlatformBackingStore(window) +{ +} + +QPaintDevice *BackingStore::paintDevice() +{ + return m_bufferView->image(); +} + +void BackingStore::resize(const QSize &size, const QRegion &staticContents) +{ + QPlatformWindow *platformWindow = static_cast(window()->handle()); + platformWindow->invalidateSurface(); +} + +void BackingStore::beginPaint(const QRegion ®ion) +{ + Window *platformWindow = static_cast(window()->handle()); + Swapchain *swapchain = platformWindow->swapchain(nullptr, {{DRM_FORMAT_ARGB8888, {DRM_FORMAT_MOD_LINEAR}}}); + if (!swapchain) { + qCCritical(KWIN_QPA, "Failed to create a swapchain for the backing store!"); + return; + } + + const auto oldBuffer = m_buffer; + m_buffer = swapchain->acquire(); + if (!m_buffer) { + qCCritical(KWIN_QPA, "Failed to acquire a graphics buffer for the backing store"); + return; + } + + m_bufferView = std::make_unique(m_buffer, GraphicsBuffer::Read | GraphicsBuffer::Write); + if (m_bufferView->isNull()) { + qCCritical(KWIN_QPA) << "Failed to map a graphics buffer for the backing store"; + return; + } + + if (oldBuffer && oldBuffer != m_buffer && oldBuffer->size() == m_buffer->size()) { + const GraphicsBufferView oldView(oldBuffer, GraphicsBuffer::Read); + std::memcpy(m_bufferView->image()->bits(), oldView.image()->constBits(), oldView.image()->sizeInBytes()); + } + + QImage *image = m_bufferView->image(); + image->setDevicePixelRatio(platformWindow->devicePixelRatio()); + + if (image->hasAlphaChannel()) { + QPainter p(image); + p.setCompositionMode(QPainter::CompositionMode_Source); + const QColor blank = Qt::transparent; + for (const QRect &rect : region) { + p.fillRect(rect, blank); + } + } +} + +void BackingStore::endPaint() +{ + m_bufferView.reset(); +} + +void BackingStore::flush(QWindow *window, const QRegion ®ion, const QPoint &offset) +{ + Window *platformWindow = static_cast(window->handle()); + InternalWindow *internalWindow = platformWindow->internalWindow(); + if (!internalWindow) { + return; + } + + const qreal scale = platformWindow->devicePixelRatio(); + const QRect bufferRect(QPoint(0, 0), m_buffer->size()); + Region bufferDamage; + for (const QRect &rect : region) { + bufferDamage += Rect(rect) + .scaled(scale) + .roundedOut() + .intersected(bufferRect); + } + + internalWindow->present(InternalWindowFrame{ + .buffer = m_buffer, + .bufferDamage = bufferDamage, + }); +} + +} +} diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/backingstore.h b/local/recipes/kde/kwin/source/src/plugins/qpa/backingstore.h new file mode 100644 index 0000000000..2152c6437f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/backingstore.h @@ -0,0 +1,42 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include + +#include + +namespace KWin +{ + +class GraphicsBuffer; +class GraphicsBufferView; + +namespace QPA +{ + +class BackingStore : public QPlatformBackingStore +{ +public: + explicit BackingStore(QWindow *window); + + QPaintDevice *paintDevice() override; + void flush(QWindow *window, const QRegion ®ion, const QPoint &offset) override; + void resize(const QSize &size, const QRegion &staticContents) override; + void beginPaint(const QRegion ®ion) override; + void endPaint() override; + +private: + QPointer m_buffer; + std::unique_ptr m_bufferView; +}; + +} +} diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/clipboard.cpp b/local/recipes/kde/kwin/source/src/plugins/qpa/clipboard.cpp new file mode 100644 index 0000000000..56839983bd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/clipboard.cpp @@ -0,0 +1,207 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "plugins/qpa/clipboard.h" +#include "utils/filedescriptor.h" +#include "utils/pipe.h" +#include "wayland/display.h" +#include "wayland/seat.h" +#include "wayland_server.h" + +#include + +#include +#include + +namespace KWin::QPA +{ + +ClipboardDataSource::ClipboardDataSource(QMimeData *mimeData, QObject *parent) + : AbstractDataSource(parent) + , m_mimeData(mimeData) +{ +} + +QMimeData *ClipboardDataSource::mimeData() const +{ + return m_mimeData; +} + +static void writeData(FileDescriptor fd, const QByteArray &buffer) +{ + size_t remainingSize = buffer.size(); + + pollfd pfds[1]; + pfds[0].fd = fd.get(); + pfds[0].events = POLLOUT; + + while (true) { + const int ready = poll(pfds, 1, 5000); + if (ready < 0) { + if (errno != EINTR) { + return; + } + } else if (ready == 0) { + return; + } else if (!(pfds[0].revents & POLLOUT)) { + return; + } else { + const char *chunk = buffer.constData() + (buffer.size() - remainingSize); + const ssize_t n = write(fd.get(), chunk, remainingSize); + + if (n < 0) { + return; + } else if (n == 0) { + return; + } else { + remainingSize -= n; + if (remainingSize == 0) { + return; + } + } + } + } +} + +void ClipboardDataSource::requestData(const QString &mimeType, FileDescriptor fd) +{ + const QByteArray data = m_mimeData->data(mimeType); + QThreadPool::globalInstance()->start([data, pipe = std::move(fd)]() mutable { + writeData(std::move(pipe), data); + }); +} + +void ClipboardDataSource::cancel() +{ +} + +QStringList ClipboardDataSource::mimeTypes() const +{ + return m_mimeData->formats(); +} + +ClipboardMimeData::ClipboardMimeData(AbstractDataSource *dataSource) + : m_dataSource(dataSource) +{ +} + +static QVariant readData(FileDescriptor fd) +{ + pollfd pfd; + pfd.fd = fd.get(); + pfd.events = POLLIN; + + QByteArray buffer; + while (true) { + const int ready = poll(&pfd, 1, 1000); + if (ready < 0) { + if (errno != EINTR) { + return QVariant(); + } + } else if (ready == 0) { + return QVariant(); + } else { + char chunk[4096]; + const ssize_t n = read(fd.get(), chunk, sizeof chunk); + + if (n < 0) { + return QVariant(); + } else if (n == 0) { + return buffer; + } else if (n > 0) { + buffer.append(chunk, n); + } + } + } +} + +QVariant ClipboardMimeData::retrieveData(const QString &mimeType, QMetaType preferredType) const +{ + std::optional pipe = Pipe::create(O_CLOEXEC); + if (!pipe) { + return QVariant(); + } + + m_dataSource->requestData(mimeType, std::move(pipe->writeEndpoint)); + + waylandServer()->display()->flush(); + return readData(std::move(pipe->readEndpoint)); +} + +Clipboard::Clipboard() +{ +} + +void Clipboard::initialize() +{ + connect(waylandServer()->seat(), &SeatInterface::selectionChanged, this, [this](AbstractDataSource *selection) { + if (selection && m_ownSelection.get() != selection) { + m_externalMimeData = std::make_unique(selection); + } else { + m_externalMimeData.reset(); + } + emitChanged(QClipboard::Clipboard); + }); +} + +QMimeData *Clipboard::mimeData(QClipboard::Mode mode) +{ + switch (mode) { + case QClipboard::Clipboard: + if (m_ownSelection) { + if (waylandServer()->seat()->selection() == m_ownSelection.get()) { + return m_ownSelection->mimeData(); + } + } + return m_externalMimeData ? m_externalMimeData.get() : &m_emptyData; + default: + return &m_emptyData; + } +} + +void Clipboard::setMimeData(QMimeData *data, QClipboard::Mode mode) +{ + static const QString plain = QStringLiteral("text/plain"); + static const QString utf8 = QStringLiteral("text/plain;charset=utf-8"); + + if (data && data->hasFormat(plain) && !data->hasFormat(utf8)) { + data->setData(utf8, data->data(plain)); + } + + switch (mode) { + case QClipboard::Clipboard: + if (data) { + auto oldSelection = std::move(m_ownSelection); + m_ownSelection = std::make_unique(data); + waylandServer()->seat()->setSelection(m_ownSelection.get(), waylandServer()->display()->nextSerial()); + } else { + if (waylandServer()->seat()->selection() == m_ownSelection.get()) { + waylandServer()->seat()->setSelection(nullptr, waylandServer()->display()->nextSerial()); + } + m_ownSelection.reset(); + } + break; + default: + break; + } +} + +bool Clipboard::supportsMode(QClipboard::Mode mode) const +{ + return mode == QClipboard::Clipboard; +} + +bool Clipboard::ownsMode(QClipboard::Mode mode) const +{ + switch (mode) { + case QClipboard::Clipboard: + return m_ownSelection && waylandServer()->seat()->selection() == m_ownSelection.get(); + default: + return false; + } +} + +} diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/clipboard.h b/local/recipes/kde/kwin/source/src/plugins/qpa/clipboard.h new file mode 100644 index 0000000000..7d76eddf9a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/clipboard.h @@ -0,0 +1,69 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "wayland/abstract_data_source.h" + +#include + +#include + +namespace KWin::QPA +{ + +class ClipboardDataSource : public AbstractDataSource +{ + Q_OBJECT + +public: + explicit ClipboardDataSource(QMimeData *mimeData, QObject *parent = nullptr); + + QMimeData *mimeData() const; + + void requestData(const QString &mimeType, FileDescriptor fd) override; + void cancel() override; + QStringList mimeTypes() const override; + +private: + QMimeData *m_mimeData; +}; + +class ClipboardMimeData : public QMimeData +{ + Q_OBJECT + +public: + explicit ClipboardMimeData(AbstractDataSource *dataSource); + +protected: + QVariant retrieveData(const QString &mimetype, QMetaType preferredType) const override; + +private: + AbstractDataSource *m_dataSource; +}; + +class Clipboard : public QObject, public QPlatformClipboard +{ + Q_OBJECT + +public: + Clipboard(); + + void initialize(); + + QMimeData *mimeData(QClipboard::Mode mode = QClipboard::Clipboard) override; + void setMimeData(QMimeData *data, QClipboard::Mode mode = QClipboard::Clipboard) override; + bool supportsMode(QClipboard::Mode mode) const override; + bool ownsMode(QClipboard::Mode mode) const override; + +private: + QMimeData m_emptyData; + std::unique_ptr m_externalMimeData; + std::unique_ptr m_ownSelection; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/eglhelpers.cpp b/local/recipes/kde/kwin/source/src/plugins/qpa/eglhelpers.cpp new file mode 100644 index 0000000000..6cb210e08e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/eglhelpers.cpp @@ -0,0 +1,126 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Flöser + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "eglhelpers.h" +#include "opengl/egldisplay.h" +#include "opengl/eglutils_p.h" + +#include + +#include + +namespace KWin +{ +namespace QPA +{ + +bool isOpenGLES() +{ + if (qstrcmp(qgetenv("KWIN_COMPOSE"), "O2ES") == 0) { + return true; + } + + return QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES; +} + +EGLConfig configFromFormat(EglDisplay *display, const QSurfaceFormat &surfaceFormat, EGLint surfaceType) +{ + // std::max as these values are initialized to -1 by default. + const EGLint redSize = std::max(surfaceFormat.redBufferSize(), 0); + const EGLint greenSize = std::max(surfaceFormat.greenBufferSize(), 0); + const EGLint blueSize = std::max(surfaceFormat.blueBufferSize(), 0); + const EGLint alphaSize = std::max(surfaceFormat.alphaBufferSize(), 0); + const EGLint depthSize = std::max(surfaceFormat.depthBufferSize(), 0); + const EGLint stencilSize = std::max(surfaceFormat.stencilBufferSize(), 0); + + const EGLint renderableType = isOpenGLES() ? EGL_OPENGL_ES2_BIT : EGL_OPENGL_BIT; + + // Not setting samples as QtQuick doesn't need it. + const QList attributes{ + EGL_SURFACE_TYPE, surfaceType, + EGL_RED_SIZE, redSize, + EGL_GREEN_SIZE, greenSize, + EGL_BLUE_SIZE, blueSize, + EGL_ALPHA_SIZE, alphaSize, + EGL_DEPTH_SIZE, depthSize, + EGL_STENCIL_SIZE, stencilSize, + EGL_RENDERABLE_TYPE, renderableType, + EGL_NONE}; + + EGLint configCount; + if (!eglChooseConfig(display->handle(), attributes.data(), nullptr, 0, &configCount)) { + qCWarning(KWIN_QPA) << "eglChooseConfig failed:" << getEglErrorString(); + return EGL_NO_CONFIG_KHR; + } + if (configCount == 0) { + qCWarning(KWIN_QPA, "eglChooseConfig did not return any configs"); + return EGL_NO_CONFIG_KHR; + } + + QList configs(configCount); + if (!eglChooseConfig(display->handle(), attributes.data(), configs.data(), configCount, &configCount)) { + qCWarning(KWIN_QPA) << "eglChooseConfig failed:" << getEglErrorString(); + return EGL_NO_CONFIG_KHR; + } + if (configCount != configs.size()) { + qCWarning(KWIN_QPA, "eglChooseConfig did not return requested configs"); + return EGL_NO_CONFIG_KHR; + } + + for (const EGLConfig &config : std::as_const(configs)) { + EGLint redConfig, greenConfig, blueConfig, alphaConfig; + eglGetConfigAttrib(display->handle(), config, EGL_RED_SIZE, &redConfig); + eglGetConfigAttrib(display->handle(), config, EGL_GREEN_SIZE, &greenConfig); + eglGetConfigAttrib(display->handle(), config, EGL_BLUE_SIZE, &blueConfig); + eglGetConfigAttrib(display->handle(), config, EGL_ALPHA_SIZE, &alphaConfig); + + if ((redSize == 0 || redSize == redConfig) && (greenSize == 0 || greenSize == greenConfig) && (blueSize == 0 || blueSize == blueConfig) && (alphaSize == 0 || alphaSize == alphaConfig)) { + return config; + } + } + + // Return first config as a fallback. + return configs[0]; +} + +QSurfaceFormat formatFromConfig(EglDisplay *display, EGLConfig config) +{ + int redSize = 0; + int blueSize = 0; + int greenSize = 0; + int alphaSize = 0; + int stencilSize = 0; + int depthSize = 0; + int sampleCount = 0; + + eglGetConfigAttrib(display->handle(), config, EGL_RED_SIZE, &redSize); + eglGetConfigAttrib(display->handle(), config, EGL_GREEN_SIZE, &greenSize); + eglGetConfigAttrib(display->handle(), config, EGL_BLUE_SIZE, &blueSize); + eglGetConfigAttrib(display->handle(), config, EGL_ALPHA_SIZE, &alphaSize); + eglGetConfigAttrib(display->handle(), config, EGL_STENCIL_SIZE, &stencilSize); + eglGetConfigAttrib(display->handle(), config, EGL_DEPTH_SIZE, &depthSize); + eglGetConfigAttrib(display->handle(), config, EGL_SAMPLES, &sampleCount); + + QSurfaceFormat format; + format.setRedBufferSize(redSize); + format.setGreenBufferSize(greenSize); + format.setBlueBufferSize(blueSize); + format.setAlphaBufferSize(alphaSize); + format.setStencilBufferSize(stencilSize); + format.setDepthBufferSize(depthSize); + format.setSamples(sampleCount); + format.setRenderableType(isOpenGLES() ? QSurfaceFormat::OpenGLES : QSurfaceFormat::OpenGL); + format.setStereo(false); + + return format; +} + +} // namespace QPA +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/eglhelpers.h b/local/recipes/kde/kwin/source/src/plugins/qpa/eglhelpers.h new file mode 100644 index 0000000000..086de1ad50 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/eglhelpers.h @@ -0,0 +1,30 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +namespace KWin +{ + +class EglDisplay; + +namespace QPA +{ + +bool isOpenGLES(); + +EGLConfig configFromFormat(EglDisplay *display, const QSurfaceFormat &surfaceFormat, EGLint surfaceType = 0); +QSurfaceFormat formatFromConfig(EglDisplay *display, EGLConfig config); + +} // namespace QPA +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/eglplatformcontext.cpp b/local/recipes/kde/kwin/source/src/plugins/qpa/eglplatformcontext.cpp new file mode 100644 index 0000000000..dd593bb560 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/eglplatformcontext.cpp @@ -0,0 +1,268 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "eglplatformcontext.h" +#include "core/outputbackend.h" +#include "eglhelpers.h" +#include "internalwindow.h" +#include "offscreensurface.h" +#include "opengl/eglcontext.h" +#include "opengl/egldisplay.h" +#include "opengl/eglutils_p.h" +#include "opengl/glutils.h" +#include "swapchain.h" +#include "window.h" + +#include "logging.h" + +namespace KWin +{ +namespace QPA +{ + +EGLRenderTarget::EGLRenderTarget(GraphicsBuffer *buffer, std::unique_ptr fbo, std::shared_ptr texture) + : buffer(buffer) + , fbo(std::move(fbo)) + , texture(std::move(texture)) +{ +} + +EGLRenderTarget::~EGLRenderTarget() +{ + fbo.reset(); + texture.reset(); +} + +EGLPlatformContext::EGLPlatformContext(QOpenGLContext *context, EglDisplay *display) + : m_eglDisplay(display) +{ + create(context->format(), kwinApp()->outputBackend()->sceneEglGlobalShareContext()); +} + +EGLPlatformContext::~EGLPlatformContext() +{ + if (!m_eglContext) { + return; + } + if (!m_renderTargets.empty() || !m_zombieRenderTargets.empty()) { + m_eglContext->makeCurrent(); + m_renderTargets.clear(); + m_zombieRenderTargets.clear(); + } +} + +bool EGLPlatformContext::makeCurrent(QPlatformSurface *surface) +{ + if (!m_eglContext) { + return false; + } + const bool ok = m_eglContext->makeCurrent(); + if (!ok) { + qCWarning(KWIN_QPA) << "eglMakeCurrent failed:" << getEglErrorString(); + return false; + } + if (m_eglContext->checkGraphicsResetStatus() != GL_NO_ERROR) { + m_renderTargets.clear(); + m_zombieRenderTargets.clear(); + m_eglContext.reset(); + return false; + } + + m_zombieRenderTargets.clear(); + + if (surface->surface()->surfaceClass() == QSurface::Window) { + Window *window = static_cast(surface); + Swapchain *swapchain = window->swapchain(m_eglContext, m_eglDisplay->nonExternalOnlySupportedDrmFormats()); + if (!swapchain) { + return false; + } + + GraphicsBuffer *buffer = swapchain->acquire(); + if (!buffer) { + return false; + } + + auto it = m_renderTargets.find(buffer); + if (it != m_renderTargets.end()) { + m_current = it->second; + } else { + std::shared_ptr texture = m_eglContext->importDmaBufAsTexture(*buffer->dmabufAttributes()); + if (!texture) { + return false; + } + + std::unique_ptr fbo = std::make_unique(texture.get(), GLFramebuffer::CombinedDepthStencil); + if (!fbo->valid()) { + return false; + } + + auto target = std::make_shared(buffer, std::move(fbo), std::move(texture)); + m_renderTargets[buffer] = target; + QObject::connect(buffer, &QObject::destroyed, this, [this, buffer]() { + if (auto it = m_renderTargets.find(buffer); it != m_renderTargets.end()) { + m_zombieRenderTargets.push_back(std::move(it->second)); + m_renderTargets.erase(it); + } + }); + + m_current = target; + } + + glBindFramebuffer(GL_FRAMEBUFFER, m_current->fbo->handle()); + } + + return true; +} + +void EGLPlatformContext::doneCurrent() +{ + if (m_eglContext) { + m_eglContext->doneCurrent(); + } +} + +bool EGLPlatformContext::isValid() const +{ + return m_eglContext != nullptr && !m_markedInvalid; +} + +bool EGLPlatformContext::isSharing() const +{ + return false; +} + +QSurfaceFormat EGLPlatformContext::format() const +{ + return m_format; +} + +QFunctionPointer EGLPlatformContext::getProcAddress(const char *procName) +{ + return eglGetProcAddress(procName); +} + +void EGLPlatformContext::swapBuffers(QPlatformSurface *surface) +{ + if (surface->surface()->surfaceClass() == QSurface::Window) { + Window *window = static_cast(surface); + InternalWindow *internalWindow = window->internalWindow(); + if (!internalWindow) { + return; + } + + glFlush(); // We need to flush pending rendering commands manually + + internalWindow->present(InternalWindowFrame{ + .buffer = m_current->buffer, + .bufferDamage = Rect(QPoint(0, 0), m_current->buffer->size()), + .bufferTransform = OutputTransform::FlipY, + }); + + m_current.reset(); + } +} + +GLuint EGLPlatformContext::defaultFramebufferObject(QPlatformSurface *surface) const +{ + if (surface->surface()->surfaceClass() == QSurface::Window) { + if (m_current) { + return m_current->fbo->handle(); + } + qCDebug(KWIN_QPA) << "No default framebuffer object for internal window"; + } + + return 0; +} + +void EGLPlatformContext::create(const QSurfaceFormat &format, ::EGLContext shareContext) +{ + if (!eglBindAPI(isOpenGLES() ? EGL_OPENGL_ES_API : EGL_OPENGL_API)) { + qCWarning(KWIN_QPA) << "eglBindAPI failed:" << getEglErrorString(); + return; + } + + m_config = configFromFormat(m_eglDisplay, format); + if (m_config == EGL_NO_CONFIG_KHR) { + qCWarning(KWIN_QPA) << "Could not find suitable EGLConfig for" << format; + return; + } + + m_format = formatFromConfig(m_eglDisplay, m_config); + m_eglContext = EglContext::create(m_eglDisplay, m_config, shareContext); + if (!m_eglContext) { + qCWarning(KWIN_QPA) << "Failed to create EGL context"; + return; + } + updateFormatFromContext(); +} + +void EGLPlatformContext::updateFormatFromContext() +{ + m_eglContext->makeCurrent(); + + const char *version = reinterpret_cast(glGetString(GL_VERSION)); + int major, minor; + if (parseOpenGLVersion(version, major, minor)) { + m_format.setMajorVersion(major); + m_format.setMinorVersion(minor); + } else { + qCWarning(KWIN_QPA) << "Unrecognized OpenGL version:" << version; + } + + GLint value; + + if (m_format.version() >= qMakePair(3, 0)) { + glGetIntegerv(GL_CONTEXT_FLAGS, &value); + if (!(value & GL_CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT)) { + m_format.setOption(QSurfaceFormat::DeprecatedFunctions); + } + if (value & GL_CONTEXT_FLAG_DEBUG_BIT) { + m_format.setOption(QSurfaceFormat::DebugContext); + } + } else { + m_format.setOption(QSurfaceFormat::DeprecatedFunctions); + } + + if (m_format.version() >= qMakePair(3, 2)) { + glGetIntegerv(GL_CONTEXT_PROFILE_MASK, &value); + if (value & GL_CONTEXT_CORE_PROFILE_BIT) { + m_format.setProfile(QSurfaceFormat::CoreProfile); + } else if (value & GL_CONTEXT_COMPATIBILITY_PROFILE_BIT) { + m_format.setProfile(QSurfaceFormat::CompatibilityProfile); + } else { + m_format.setProfile(QSurfaceFormat::NoProfile); + } + } else { + m_format.setProfile(QSurfaceFormat::NoProfile); + } +} + +EGLContext EGLPlatformContext::nativeContext() const +{ + return m_eglContext->handle(); +}; + +EGLConfig EGLPlatformContext::config() const +{ + return m_eglContext->config(); +} + +EGLDisplay EGLPlatformContext::display() const +{ + return m_eglContext->displayObject()->handle(); +} + +void EGLPlatformContext::invalidateContext() +{ + m_markedInvalid = true; +} + +} // namespace QPA +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/eglplatformcontext.h b/local/recipes/kde/kwin/source/src/plugins/qpa/eglplatformcontext.h new file mode 100644 index 0000000000..8ac4eba6df --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/eglplatformcontext.h @@ -0,0 +1,80 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +#include + +namespace KWin +{ + +class GLFramebuffer; +class GLTexture; +class GraphicsBuffer; +class EglDisplay; +class EglContext; + +namespace QPA +{ + +class Window; + +class EGLRenderTarget +{ +public: + EGLRenderTarget(GraphicsBuffer *buffer, std::unique_ptr fbo, std::shared_ptr texture); + ~EGLRenderTarget(); + + GraphicsBuffer *buffer; + std::unique_ptr fbo; + std::shared_ptr texture; +}; + +class EGLPlatformContext : public QObject, public QPlatformOpenGLContext, public QNativeInterface::QEGLContext +{ + Q_OBJECT + +public: + EGLPlatformContext(QOpenGLContext *context, EglDisplay *display); + ~EGLPlatformContext() override; + + bool makeCurrent(QPlatformSurface *surface) override; + void doneCurrent() override; + QSurfaceFormat format() const override; + bool isValid() const override; + bool isSharing() const override; + GLuint defaultFramebufferObject(QPlatformSurface *surface) const override; + QFunctionPointer getProcAddress(const char *procName) override; + void swapBuffers(QPlatformSurface *surface) override; + EGLContext nativeContext() const override; + EGLConfig config() const override; + EGLDisplay display() const override; + void invalidateContext() override; + +private: + void create(const QSurfaceFormat &format, ::EGLContext shareContext); + void updateFormatFromContext(); + + EglDisplay *const m_eglDisplay; + QSurfaceFormat m_format; + EGLConfig m_config = EGL_NO_CONFIG_KHR; + std::shared_ptr m_eglContext; + std::unordered_map> m_renderTargets; + std::vector> m_zombieRenderTargets; + std::shared_ptr m_current; + bool m_markedInvalid = false; +}; + +} // namespace QPA +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/integration.cpp b/local/recipes/kde/kwin/source/src/plugins/qpa/integration.cpp new file mode 100644 index 0000000000..2d85afbd47 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/integration.cpp @@ -0,0 +1,255 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "integration.h" +#include "backingstore.h" +#include "clipboard.h" +#include "eglplatformcontext.h" +#include "inputmethod.h" +#include "internalinputmethodcontext.h" +#include "logging.h" +#include "offscreensurface.h" +#include "screen.h" +#include "wayland_server.h" +#include "window.h" + +#include "core/output.h" +#include "core/outputbackend.h" +#include "main.h" +#include "workspace.h" + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#if __has_include() +#include +#else +#include +#endif +#include + +#if !defined(QT_NO_ACCESSIBILITY_ATSPI_BRIDGE) +#include +#endif + +namespace KWin +{ + +namespace QPA +{ + +Integration::Integration() + : QObject() + , QPlatformIntegration() + , m_fontDb(new QGenericUnixFontDatabase()) + , m_nativeInterface(new QPlatformNativeInterface()) + , m_services(new QDesktopUnixServices()) + , m_clipboard(new Clipboard()) +{ + QWindowSystemInterface::setSynchronousWindowSystemEvents(true); + QWindowSystemInterfacePrivate::TabletEvent::setPlatformSynthesizesMouse(false); +} + +Integration::~Integration() +{ + for (QPlatformScreen *platformScreen : std::as_const(m_screens)) { + QWindowSystemInterface::handleScreenRemoved(platformScreen); + } + if (m_dummyScreen) { + QWindowSystemInterface::handleScreenRemoved(m_dummyScreen); + } +} + +QHash Integration::screens() const +{ + return m_screens; +} + +bool Integration::hasCapability(Capability cap) const +{ + switch (cap) { + case ThreadedPixmaps: + case OpenGL: + case MultipleWindows: + case NonFullScreenWindows: +#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 2) + case OffscreenSurface: +#endif + return true; + case ThreadedOpenGL: + case BufferQueueingOpenGL: + case RasterGLSurface: + return false; + default: + return QPlatformIntegration::hasCapability(cap); + } +} + +void Integration::initialize() +{ + // This method is called from QGuiApplication's constructor, before kwinApp is built + QTimer::singleShot(0, this, [this] { + // The QPA is initialized before the workspace is created. + if (workspace()) { + handleWorkspaceCreated(); + } else { + connect(kwinApp(), &Application::workspaceCreated, this, &Integration::handleWorkspaceCreated); + } + }); + + QPlatformIntegration::initialize(); + + m_dummyScreen = new PlaceholderScreen(); + QWindowSystemInterface::handleScreenAdded(m_dummyScreen); +} + +QAbstractEventDispatcher *Integration::createEventDispatcher() const +{ + return new QUnixEventDispatcherQPA; +} + +QPlatformBackingStore *Integration::createPlatformBackingStore(QWindow *window) const +{ + return new BackingStore(window); +} + +QPlatformWindow *Integration::createPlatformWindow(QWindow *window) const +{ + return new Window(window); +} + +QPlatformOffscreenSurface *Integration::createPlatformOffscreenSurface(QOffscreenSurface *surface) const +{ + return new KWin::QPA::OffscreenSurface(surface); +} + +QPlatformFontDatabase *Integration::fontDatabase() const +{ + return m_fontDb.get(); +} + +QPlatformTheme *Integration::createPlatformTheme(const QString &name) const +{ + return QGenericUnixTheme::createUnixTheme(name); +} + +QStringList Integration::themeNames() const +{ + if (qEnvironmentVariableIsSet("KDE_FULL_SESSION")) { + return QStringList({QStringLiteral("kde")}); + } + return QStringList({QLatin1String(QGenericUnixTheme::name)}); +} + +QPlatformOpenGLContext *Integration::createPlatformOpenGLContext(QOpenGLContext *context) const +{ + if (!kwinApp()->outputBackend()) { + return nullptr; + } + if (kwinApp()->outputBackend()->sceneEglGlobalShareContext() == EGL_NO_CONTEXT) { + qCWarning(KWIN_QPA) << "Attempting to create a QOpenGLContext before the scene is initialized"; + return nullptr; + } + const auto eglDisplay = kwinApp()->outputBackend()->sceneEglDisplayObject(); + if (eglDisplay) { + EGLPlatformContext *platformContext = new EGLPlatformContext(context, eglDisplay); + return platformContext; + } + return nullptr; +} + +QPlatformAccessibility *Integration::accessibility() const +{ + if (!m_accessibility) { +#if !defined(QT_NO_ACCESSIBILITY_ATSPI_BRIDGE) + m_accessibility.reset(new QSpiAccessibleBridge()); +#endif + } + return m_accessibility.get(); +} + +void Integration::handleWorkspaceCreated() +{ + connect(workspace(), &Workspace::outputAdded, + this, &Integration::handleOutputEnabled); + connect(workspace(), &Workspace::outputRemoved, + this, &Integration::handleOutputDisabled); + + const QList outputs = workspace()->outputs(); + for (LogicalOutput *output : outputs) { + handleOutputEnabled(output); + } + + m_clipboard->initialize(); +} + +void Integration::handleOutputEnabled(LogicalOutput *output) +{ + Screen *platformScreen = new Screen(output, this); + QWindowSystemInterface::handleScreenAdded(platformScreen); + m_screens.insert(output, platformScreen); + + if (m_dummyScreen) { + QWindowSystemInterface::handleScreenRemoved(m_dummyScreen); + m_dummyScreen = nullptr; + } +} + +void Integration::handleOutputDisabled(LogicalOutput *output) +{ + Screen *platformScreen = m_screens.take(output); + if (!platformScreen) { + qCWarning(KWIN_QPA) << "Unknown output" << output; + return; + } + + if (m_screens.isEmpty()) { + m_dummyScreen = new PlaceholderScreen(); + QWindowSystemInterface::handleScreenAdded(m_dummyScreen); + } + + QWindowSystemInterface::handleScreenRemoved(platformScreen); +} + +QPlatformNativeInterface *Integration::nativeInterface() const +{ + return m_nativeInterface.get(); +} + +QPlatformInputContext *Integration::inputContext() const +{ + if (!kwinApp()->inputMethod()) { // for some unit tests + return nullptr; + } + return kwinApp()->inputMethod()->internalContext(); +} + +QPlatformServices *Integration::services() const +{ + return m_services.get(); +} + +QPlatformClipboard *Integration::clipboard() const +{ + return m_clipboard.get(); +} + +} +} + +#include "moc_integration.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/integration.h b/local/recipes/kde/kwin/source/src/plugins/qpa/integration.h new file mode 100644 index 0000000000..6d849a02b5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/integration.h @@ -0,0 +1,71 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include + +#include +#include +#include + +namespace KWin +{ + +class LogicalOutput; + +namespace QPA +{ + +class Clipboard; +class Screen; + +class Integration : public QObject, public QPlatformIntegration +{ + Q_OBJECT +public: + explicit Integration(); + ~Integration() override; + + bool hasCapability(Capability cap) const override; + QPlatformWindow *createPlatformWindow(QWindow *window) const override; + QPlatformOffscreenSurface *createPlatformOffscreenSurface(QOffscreenSurface *surface) const override; + QPlatformBackingStore *createPlatformBackingStore(QWindow *window) const override; + QAbstractEventDispatcher *createEventDispatcher() const override; + QPlatformFontDatabase *fontDatabase() const override; + QStringList themeNames() const override; + QPlatformTheme *createPlatformTheme(const QString &name) const override; + QPlatformOpenGLContext *createPlatformOpenGLContext(QOpenGLContext *context) const override; + QPlatformAccessibility *accessibility() const override; + QPlatformNativeInterface *nativeInterface() const override; + QPlatformInputContext *inputContext() const override; + + QPlatformServices *services() const override; + QPlatformClipboard *clipboard() const override; + void initialize() override; + + QHash screens() const; + +private Q_SLOTS: + void handleOutputEnabled(LogicalOutput *output); + void handleOutputDisabled(LogicalOutput *output); + void handleWorkspaceCreated(); + +private: + std::unique_ptr m_fontDb; + mutable std::unique_ptr m_accessibility; + std::unique_ptr m_nativeInterface; + QPlatformPlaceholderScreen *m_dummyScreen = nullptr; + QHash m_screens; + std::unique_ptr m_services; + std::unique_ptr m_clipboard; +}; + +} +} diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/kwin.json b/local/recipes/kde/kwin/source/src/plugins/qpa/kwin.json new file mode 100644 index 0000000000..d820f2a1d4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/kwin.json @@ -0,0 +1,3 @@ +{ + "Keys": [ "wayland-org.kde.kwin.qpa" ] +} diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/main.cpp b/local/recipes/kde/kwin/source/src/plugins/qpa/main.cpp new file mode 100644 index 0000000000..1aa3382d8f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/main.cpp @@ -0,0 +1,36 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "integration.h" +#include + +#include + +class KWinIntegrationPlugin : public QPlatformIntegrationPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID QPlatformIntegrationFactoryInterface_iid FILE "kwin.json") +public: + using QPlatformIntegrationPlugin::create; + QPlatformIntegration *create(const QString &system, const QStringList ¶mList) override; +}; + +QPlatformIntegration *KWinIntegrationPlugin::create(const QString &system, const QStringList ¶mList) +{ + if (!QCoreApplication::applicationFilePath().endsWith(QLatin1String("kwin_wayland")) && !qEnvironmentVariableIsSet("KWIN_FORCE_OWN_QPA")) { + // Not KWin + return nullptr; + } + if (system.compare(QLatin1String("wayland-org.kde.kwin.qpa"), Qt::CaseInsensitive) == 0) { + // create our integration + return new KWin::QPA::Integration; + } + return nullptr; +} + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/offscreensurface.cpp b/local/recipes/kde/kwin/source/src/plugins/qpa/offscreensurface.cpp new file mode 100644 index 0000000000..9e84e911d2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/offscreensurface.cpp @@ -0,0 +1,39 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "offscreensurface.h" +#include "core/outputbackend.h" +#include "eglhelpers.h" +#include "opengl/egldisplay.h" + +#include + +namespace KWin +{ +namespace QPA +{ + +OffscreenSurface::OffscreenSurface(QOffscreenSurface *surface) + : QPlatformOffscreenSurface(surface) + , m_format(surface->requestedFormat()) +{ +} + +QSurfaceFormat OffscreenSurface::format() const +{ + return m_format; +} + +bool OffscreenSurface::isValid() const +{ + return true; +} + +} // namespace QPA +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/offscreensurface.h b/local/recipes/kde/kwin/source/src/plugins/qpa/offscreensurface.h new file mode 100644 index 0000000000..9e27fc9dbe --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/offscreensurface.h @@ -0,0 +1,37 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +namespace KWin +{ + +class EglDisplay; + +namespace QPA +{ + +class OffscreenSurface : public QPlatformOffscreenSurface +{ +public: + explicit OffscreenSurface(QOffscreenSurface *surface); + + QSurfaceFormat format() const override; + bool isValid() const override; + +private: + QSurfaceFormat m_format; +}; + +} // namespace QPA +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/platformcursor.cpp b/local/recipes/kde/kwin/source/src/plugins/qpa/platformcursor.cpp new file mode 100644 index 0000000000..d74d882144 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/platformcursor.cpp @@ -0,0 +1,40 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "platformcursor.h" +#include "pointer_input.h" + +namespace KWin +{ +namespace QPA +{ + +PlatformCursor::PlatformCursor() + : QPlatformCursor() +{ +} + +PlatformCursor::~PlatformCursor() = default; + +QPoint PlatformCursor::pos() const +{ + return input()->pointer()->pos().toPoint(); +} + +void PlatformCursor::setPos(const QPoint &pos) +{ + input()->pointer()->warp(pos); +} + +void PlatformCursor::changeCursor(QCursor *windowCursor, QWindow *window) +{ + // TODO: implement +} + +} +} diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/platformcursor.h b/local/recipes/kde/kwin/source/src/plugins/qpa/platformcursor.h new file mode 100644 index 0000000000..5d442aac38 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/platformcursor.h @@ -0,0 +1,29 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include + +namespace KWin +{ +namespace QPA +{ + +class PlatformCursor : public QPlatformCursor +{ +public: + PlatformCursor(); + ~PlatformCursor() override; + QPoint pos() const override; + void setPos(const QPoint &pos) override; + void changeCursor(QCursor *windowCursor, QWindow *window) override; +}; + +} +} diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/screen.cpp b/local/recipes/kde/kwin/source/src/plugins/qpa/screen.cpp new file mode 100644 index 0000000000..df00d6ee98 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/screen.cpp @@ -0,0 +1,122 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "screen.h" +#include "core/output.h" +#include "integration.h" +#include "logging.h" +#include "platformcursor.h" + +#include + +namespace KWin +{ +namespace QPA +{ + +static int forcedDpi() +{ + return qEnvironmentVariableIsSet("QT_WAYLAND_FORCE_DPI") ? qEnvironmentVariableIntValue("QT_WAYLAND_FORCE_DPI") : -1; +} + +Screen::Screen(LogicalOutput *output, Integration *integration) + : m_output(output) + , m_cursor(new PlatformCursor) + , m_integration(integration) +{ + connect(output, &LogicalOutput::geometryChanged, this, &Screen::handleGeometryChanged); +} + +Screen::~Screen() = default; + +QList Screen::virtualSiblings() const +{ + const auto screens = m_integration->screens(); + + QList siblings; + siblings.reserve(siblings.size()); + + for (Screen *screen : screens) { + siblings << screen; + } + + return siblings; +} + +int Screen::depth() const +{ + return 32; +} + +QImage::Format Screen::format() const +{ + return QImage::Format_ARGB32_Premultiplied; +} + +QRect Screen::geometry() const +{ + if (Q_UNLIKELY(!m_output)) { + qCCritical(KWIN_QPA) << "Attempting to get the geometry of a destroyed output"; + return QRect(); + } + return m_output->geometry(); +} + +QSizeF Screen::physicalSize() const +{ + if (Q_UNLIKELY(!m_output)) { + qCCritical(KWIN_QPA) << "Attempting to get the physical size of a destroyed output"; + return QSizeF(); + } + return m_output->physicalSize(); +} + +QPlatformCursor *Screen::cursor() const +{ + return m_cursor.get(); +} + +QDpi Screen::logicalDpi() const +{ + const int dpi = forcedDpi(); + return dpi > 0 ? QDpi(dpi, dpi) : QDpi(96, 96); +} + +qreal Screen::devicePixelRatio() const +{ + if (Q_UNLIKELY(!m_output)) { + qCCritical(KWIN_QPA) << "Attempting to get the scale factor of a destroyed output"; + return 1; + } + return m_output->scale(); +} + +QString Screen::name() const +{ + if (Q_UNLIKELY(!m_output)) { + qCCritical(KWIN_QPA) << "Attempting to get the name of a destroyed output"; + return QString(); + } + return m_output->name(); +} + +void Screen::handleGeometryChanged() +{ + QWindowSystemInterface::handleScreenGeometryChange(screen(), geometry(), geometry()); +} + +QDpi PlaceholderScreen::logicalDpi() const +{ + const int dpi = forcedDpi(); + return dpi > 0 ? QDpi(dpi, dpi) : QDpi(96, 96); +} + +} +} + +#include "moc_screen.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/screen.h b/local/recipes/kde/kwin/source/src/plugins/qpa/screen.h new file mode 100644 index 0000000000..5981d6e842 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/screen.h @@ -0,0 +1,58 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include + +#include + +namespace KWin +{ +class LogicalOutput; + +namespace QPA +{ +class Integration; +class PlatformCursor; + +class Screen : public QObject, public QPlatformScreen +{ + Q_OBJECT + +public: + Screen(LogicalOutput *output, Integration *integration); + ~Screen() override; + + QString name() const override; + QRect geometry() const override; + int depth() const override; + QImage::Format format() const override; + QSizeF physicalSize() const override; + QPlatformCursor *cursor() const override; + QDpi logicalDpi() const override; + qreal devicePixelRatio() const override; + QList virtualSiblings() const override; + +private Q_SLOTS: + void handleGeometryChanged(); + +private: + QPointer m_output; + std::unique_ptr m_cursor; + Integration *m_integration; +}; + +class PlaceholderScreen : public QPlatformPlaceholderScreen +{ +public: + QDpi logicalDpi() const override; +}; + +} +} diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/swapchain.cpp b/local/recipes/kde/kwin/source/src/plugins/qpa/swapchain.cpp new file mode 100644 index 0000000000..001e3081c0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/swapchain.cpp @@ -0,0 +1,60 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "plugins/qpa/swapchain.h" + +namespace KWin +{ +namespace QPA +{ + +Swapchain::Swapchain(GraphicsBufferAllocator *allocator, const GraphicsBufferOptions &options, GraphicsBuffer *initialBuffer) + : m_allocator(allocator) + , m_allocationOptions(options) +{ + m_buffers.push_back(initialBuffer); +} + +Swapchain::~Swapchain() +{ + for (GraphicsBuffer *buffer : std::as_const(m_buffers)) { + buffer->drop(); + } +} + +QSize Swapchain::size() const +{ + return m_allocationOptions.size; +} + +GraphicsBuffer *Swapchain::acquire() +{ + for (GraphicsBuffer *buffer : std::as_const(m_buffers)) { + if (!buffer->isReferenced()) { + return buffer; + } + } + + GraphicsBuffer *buffer = m_allocator->allocate(m_allocationOptions); + if (!buffer) { + return nullptr; + } + + m_buffers.append(buffer); + return buffer; +} + +uint32_t Swapchain::format() const +{ + return m_allocationOptions.format; +} + +QList Swapchain::modifiers() const +{ + return m_allocationOptions.modifiers; +} +} +} diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/swapchain.h b/local/recipes/kde/kwin/source/src/plugins/qpa/swapchain.h new file mode 100644 index 0000000000..c5824814b7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/swapchain.h @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/graphicsbuffer.h" +#include "core/graphicsbufferallocator.h" + +namespace KWin +{ + +namespace QPA +{ + +class Swapchain +{ +public: + Swapchain(GraphicsBufferAllocator *allocator, const GraphicsBufferOptions &options, GraphicsBuffer *initialBuffer); + ~Swapchain(); + + QSize size() const; + + GraphicsBuffer *acquire(); + uint32_t format() const; + QList modifiers() const; + +private: + GraphicsBufferAllocator *m_allocator; + GraphicsBufferOptions m_allocationOptions; + QList m_buffers; +}; + +} +} diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/window.cpp b/local/recipes/kde/kwin/source/src/plugins/qpa/window.cpp new file mode 100644 index 0000000000..bde5d969ca --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/window.cpp @@ -0,0 +1,176 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +// this needs to be on top, epoxy has an error if you include it after GL/gl.h, +// which Qt does include +#include "utils/drm_format_helper.h" + +#include "compositor.h" +#include "core/drmdevice.h" +#include "core/renderbackend.h" +#include "core/shmgraphicsbufferallocator.h" +#include "internalwindow.h" +#include "swapchain.h" +#include "window.h" + +#include + +#include +#include + +namespace KWin +{ +namespace QPA +{ +static quint32 s_windowId = 0; + +Window::Window(QWindow *window) + : QPlatformWindow(window) + , m_windowId(++s_windowId) + , m_scale(kwinApp()->devicePixelRatio()) +{ + Q_ASSERT(!window->property("_KWIN_WINDOW_IS_OFFSCREEN").toBool()); +} + +Window::~Window() +{ + unmap(); +} + +Swapchain *Window::swapchain(const std::shared_ptr &context, const QHash> &formats) +{ + const QSize nativeSize = geometry().size() * devicePixelRatio(); + const bool software = window()->surfaceType() == QSurface::RasterSurface; // RasterGLSurface is unsupported by us + if (!m_swapchain || m_swapchain->size() != nativeSize + || !formats.contains(m_swapchain->format()) + || m_swapchain->modifiers() != formats[m_swapchain->format()] + || (!software && m_eglContext.lock() != context)) { + + GraphicsBufferAllocator *allocator; + if (software) { + static ShmGraphicsBufferAllocator shmAllocator; + allocator = &shmAllocator; + } else { + allocator = Compositor::self()->backend()->drmDevice()->allocator(); + } + + for (auto it = formats.begin(); it != formats.end(); it++) { + if (auto info = FormatInfo::get(it.key()); info && info->bitsPerColor == 8 && info->alphaBits == 8) { + const auto options = GraphicsBufferOptions{ + .size = nativeSize, + .format = it.key(), + .modifiers = it.value(), + .software = software, + }; + auto buffer = allocator->allocate(options); + if (!buffer) { + continue; + } + m_swapchain = std::make_unique(allocator, options, buffer); + m_eglContext = context; + break; + } + } + } + return m_swapchain.get(); +} + +void Window::invalidateSurface() +{ + m_swapchain.reset(); +} + +void Window::setVisible(bool visible) +{ + if (visible) { + map(); + } else { + unmap(); + } +} + +QSurfaceFormat Window::format() const +{ + return m_format; +} + +void Window::requestActivateWindow() +{ + QWindowSystemInterface::handleFocusWindowChanged(window()); +} + +void Window::raise() +{ + // Left blank intentionally to suppress warnings in QPlatformWindow::raise(). +} + +void Window::lower() +{ + // Left blank intentionally to suppress warnings in QPlatformWindow::lower(). +} + +void Window::setGeometry(const QRect &rect) +{ + if (m_handle) { + m_handle->moveResize(rect); + return; + } + + QWindowSystemInterface::handleGeometryChange(window(), rect); +} + +WId Window::winId() const +{ + return m_windowId; +} + +qreal Window::devicePixelRatio() const +{ + return m_scale; +} + +bool Window::isExposed() const +{ + return m_exposed; +} + +InternalWindow *Window::internalWindow() const +{ + return m_handle; +} + +void Window::map() +{ + if (m_handle) { + return; + } + + m_handle = new InternalWindow(window()); + + m_exposed = true; + QWindowSystemInterface::handleExposeEvent(window(), QRect(QPoint(), geometry().size())); +} + +void Window::unmap() +{ + if (!m_handle) { + return; + } + + m_handle->destroyWindow(); + m_handle = nullptr; + + invalidateSurface(); + + m_exposed = false; + QWindowSystemInterface::handleExposeEvent(window(), QRect()); +} + +} +} diff --git a/local/recipes/kde/kwin/source/src/plugins/qpa/window.h b/local/recipes/kde/kwin/source/src/plugins/qpa/window.h new file mode 100644 index 0000000000..3030ea80c5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/qpa/window.h @@ -0,0 +1,60 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Martin Gräßlin + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include + +namespace KWin +{ + +class InternalWindow; +class EglContext; + +namespace QPA +{ + +class Swapchain; + +class Window : public QPlatformWindow +{ +public: + explicit Window(QWindow *window); + ~Window() override; + + void invalidateSurface() override; + QSurfaceFormat format() const override; + void setVisible(bool visible) override; + void setGeometry(const QRect &rect) override; + WId winId() const override; + qreal devicePixelRatio() const override; + void requestActivateWindow() override; + void raise() override; + void lower() override; + bool isExposed() const override; + + InternalWindow *internalWindow() const; + Swapchain *swapchain(const std::shared_ptr &context, const QHash> &formats); + +private: + void map(); + void unmap(); + + QSurfaceFormat m_format; + QPointer m_handle; + std::unique_ptr m_swapchain; + std::weak_ptr m_eglContext; + quint32 m_windowId; + qreal m_scale = 1; + bool m_exposed = false; +}; + +} +} diff --git a/local/recipes/kde/kwin/source/src/plugins/scale/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/scale/CMakeLists.txt new file mode 100644 index 0000000000..aa29c8037c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/scale/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_scripted_effect(scale package) diff --git a/local/recipes/kde/kwin/source/src/plugins/scale/package/contents/code/main.js b/local/recipes/kde/kwin/source/src/plugins/scale/package/contents/code/main.js new file mode 100644 index 0000000000..d52eeeca0a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/scale/package/contents/code/main.js @@ -0,0 +1,175 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018, 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +const blacklist = { + // The logout screen has to be animated only by the logout effect. + "ksmserver ksmserver": [], + "ksmserver-logout-greeter ksmserver-logout-greeter": [], + + // KDE Plasma splash screen has to be animated only by the login effect. + "ksplashqml ksplashqml": [], + + "spectacle org.kde.spectacle": ["region-editor"], +}; + +class ScaleEffect { + constructor() { + effect.configChanged.connect(this.loadConfig.bind(this)); + effect.animationEnded.connect(this.cleanupForcedRoles.bind(this)); + effects.windowAdded.connect(this.slotWindowAdded.bind(this)); + effects.windowClosed.connect(this.slotWindowClosed.bind(this)); + effects.windowDataChanged.connect(this.slotWindowDataChanged.bind(this)); + + this.loadConfig(); + } + + loadConfig() { + const defaultDuration = 200; + const duration = effect.readConfig("Duration", defaultDuration) || defaultDuration; + this.duration = animationTime(duration); + this.inScale = effect.readConfig("InScale", 0.8); + this.outScale = effect.readConfig("OutScale", 0.8); + } + + static isScaleWindow(window) { + // We don't want to animate most of plasmashell's windows, yet, some + // of them we want to, for example, Task Manager Settings window. + // The problem is that all those window share single window class. + // So, the only way to decide whether a window should be animated is + // to use a heuristic: if a window has decoration, then it's most + // likely a dialog or a settings window so we have to animate it. + if (window.windowClass == "plasmashell plasmashell" + || window.windowClass == "plasmashell org.kde.plasmashell") { + return window.hasDecoration; + } + + const blacklistedTags = blacklist[window.windowClass]; + if (blacklistedTags && (blacklistedTags.length === 0 || blacklistedTags.includes(window.tag))) { + return false; + } + + if (window.hasDecoration) { + return true; + } + + // Don't animate combobox popups, tooltips, popup menus, etc. + if (window.popupWindow) { + return false; + } + + // Don't animate the outline and the screenlocker as it looks bad. + if (window.lockScreen || window.outline) { + return false; + } + + // Override-redirect windows are usually used for user interface + // concepts that are not expected to be animated by this effect. + if (!window.managed) { + return false; + } + + return window.normalWindow || window.dialog; + } + + setupForcedRoles(window) { + window.setData(Effect.WindowForceBackgroundContrastRole, true); + window.setData(Effect.WindowForceBlurRole, true); + } + + cleanupForcedRoles(window) { + window.setData(Effect.WindowForceBackgroundContrastRole, null); + window.setData(Effect.WindowForceBlurRole, null); + } + + slotWindowAdded(window) { + if (effects.hasActiveFullScreenEffect) { + return; + } + if (!ScaleEffect.isScaleWindow(window)) { + return; + } + if (!window.visible) { + return; + } + if (effect.isGrabbed(window, Effect.WindowAddedGrabRole)) { + return; + } + this.setupForcedRoles(window); + window.scaleInAnimation = animate({ + window: window, + curve: QEasingCurve.OutCubic, + duration: this.duration, + animations: [ + { + type: Effect.Scale, + from: this.inScale + }, + { + type: Effect.Opacity, + from: 0 + } + ] + }); + } + + slotWindowClosed(window) { + if (effects.hasActiveFullScreenEffect) { + return; + } + if (!ScaleEffect.isScaleWindow(window)) { + return; + } + if (!window.visible || window.skipsCloseAnimation) { + return; + } + if (effect.isGrabbed(window, Effect.WindowClosedGrabRole)) { + return; + } + if (window.scaleInAnimation) { + cancel(window.scaleInAnimation); + delete window.scaleInAnimation; + } + this.setupForcedRoles(window); + window.scaleOutAnimation = animate({ + window: window, + curve: QEasingCurve.InCubic, + duration: this.duration, + animations: [ + { + type: Effect.Scale, + to: this.outScale + }, + { + type: Effect.Opacity, + to: 0 + } + ] + }); + } + + slotWindowDataChanged(window, role) { + if (role == Effect.WindowAddedGrabRole) { + if (window.scaleInAnimation && effect.isGrabbed(window, role)) { + cancel(window.scaleInAnimation); + delete window.scaleInAnimation; + this.cleanupForcedRoles(window); + } + } else if (role == Effect.WindowClosedGrabRole) { + if (window.scaleOutAnimation && effect.isGrabbed(window, role)) { + cancel(window.scaleOutAnimation); + delete window.scaleOutAnimation; + this.cleanupForcedRoles(window); + } + } + } +} + +new ScaleEffect(); diff --git a/local/recipes/kde/kwin/source/src/plugins/scale/package/contents/config/main.xml b/local/recipes/kde/kwin/source/src/plugins/scale/package/contents/config/main.xml new file mode 100644 index 0000000000..20ec056080 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/scale/package/contents/config/main.xml @@ -0,0 +1,18 @@ + + + + + + 0 + + + 0.8 + + + 0.8 + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/scale/package/contents/ui/config.ui b/local/recipes/kde/kwin/source/src/plugins/scale/package/contents/ui/config.ui new file mode 100644 index 0000000000..97b43b57f5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/scale/package/contents/ui/config.ui @@ -0,0 +1,93 @@ + + + ScaleEffectConfig + + + + 0 + 0 + 455 + 177 + + + + + + + Duration: + + + + + + + + 0 + 0 + + + + Default + + + milliseconds + + + 9999 + + + 5 + + + + + + + Window open scale: + + + + + + + Window close scale: + + + + + + + + 0 + 0 + + + + 9.990000000000000 + + + 0.050000000000000 + + + + + + + + 0 + 0 + + + + 9.990000000000000 + + + 0.050000000000000 + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/scale/package/metadata.json b/local/recipes/kde/kwin/source/src/plugins/scale/package/metadata.json new file mode 100644 index 0000000000..0dd68ce519 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/scale/package/metadata.json @@ -0,0 +1,147 @@ +{ + "KPackageStructure": "KWin/Effect", + "KPlugin": { + "Authors": [ + { + "Email": "vlad.zahorodnii@kde.org", + "Name": "Vlad Zahorodnii", + "Name[ar]": "فلاد زاهورودني", + "Name[az]": "Vlad Zahorodnii", + "Name[be]": "Vlad Zahorodnii", + "Name[bg]": "Vlad Zahorodnii", + "Name[ca@valencia]": "Vlad Zahorodnii", + "Name[ca]": "Vlad Zahorodnii", + "Name[cs]": "Vlad Zahorodnii", + "Name[da]": "Vlad Zahorodnii", + "Name[de]": "Vlad Zahorodnii", + "Name[en_GB]": "Vlad Zahorodnii", + "Name[eo]": "Vlad Zahorodnii", + "Name[es]": "Vlad Zahorodnii", + "Name[et]": "Vlad Zahorodnii", + "Name[eu]": "Vlad Zahorodnii", + "Name[fi]": "Vlad Zahorodnii", + "Name[fr]": "Vlad Zahorodnii", + "Name[ga]": "Vlad Zahorodnii", + "Name[gl]": "Vlad Zahorodnii.", + "Name[he]": "ולאד זהורודני", + "Name[hu]": "Vlad Zahorodnii", + "Name[ia]": "Vlad Zahorodnii", + "Name[id]": "Vlad Zahorodnii", + "Name[is]": "Vlad Zahorodnii", + "Name[it]": "Vlad Zahorodnii", + "Name[ja]": "Vlad Zahorodnii", + "Name[ka]": "Vlad Zahorodnii", + "Name[ko]": "Vlad Zahorodnii", + "Name[lt]": "Vlad Zahorodnii", + "Name[lv]": "Vlad Zahorodnii", + "Name[nb]": "Vlad Zahorodnii", + "Name[nl]": "Vlad Zahorodnii", + "Name[nn]": "Vlad Zahorodnii", + "Name[pl]": "Vlad Zahorodnii", + "Name[pt]": "Vlad Zahorodnii", + "Name[pt_BR]": "Vlad Zahorodnii", + "Name[ro]": "Vlad Zahorodnii", + "Name[ru]": "Влад Загородний", + "Name[sa]": "व्लाद ज़ाहोरोदनी", + "Name[sk]": "Vlad Zahorodnii", + "Name[sl]": "Vlad Zahorodnii", + "Name[sv]": "Vlad Zahorodnii", + "Name[ta]": "விலாட் ஜாஹொரிடுனி", + "Name[tr]": "Vlad Zahorodnii", + "Name[uk]": "Влад Загородній", + "Name[vi]": "Vlad Zahorodnii", + "Name[zh_CN]": "Vlad Zahorodnii", + "Name[zh_TW]": "Vlad Zahorodnii" + } + ], + "Category": "Window Open/Close Animation", + "Description": "Make windows scale in and out when they are shown or hidden", + "Description[ar]": "اجعل النوافذ تكبر وتصغر عند إظهاراها وإخفائها", + "Description[bg]": "Мащабиране на прозорците при показване или скриване", + "Description[ca@valencia]": "Fa que les finestres s'engrandisquen o empetitisquen quan es mostren o s'oculten", + "Description[ca]": "Fa que les finestres s'engrandeixin o empetiteixin quan es mostren o s'oculten", + "Description[de]": "Fenster beim Ein- oder Ausblenden skalieren", + "Description[es]": "Hacer que las ventanas se escalen al mostrarlas y al ocultarlas", + "Description[eu]": "Leihoak handitu eta txikitu haiek erakutsi edo ezkutatzean", + "Description[fi]": "Skaalaa ponnahdusikkunat näytölle tai näytöltä", + "Description[fr]": "Faire une mise l'échelle en avant ou en arrière des fenêtres lors de leurs affichages ou de leurs masquages", + "Description[he]": "הפיכת הגדלת והקטנת קנה מידה של חלונות כשהם מוצגים או מוסתרים", + "Description[hu]": "Az ablakok méretezett módon lesznek elrejtve és megjelenítve", + "Description[ia]": "Face que fenestras scala intra e foras quando illos es monstrate o celate", + "Description[is]": "Láta glugga stækka og minnka inn og út þegar þeir eru birtir eða faldir", + "Description[it]": "Ridimensiona le finestre dolcemente quando sono mostrate o nascoste", + "Description[ja]": "ウィンドウが表示/非表示時に拡大/縮小します", + "Description[ka]": "ფანჯრების რბილად გადიდება/დაპატარავება მათი ჩვენება/დამალვისას", + "Description[ko]": "창이 보여지거나 감춰질 때 크기 조정 효과를 사용합니다", + "Description[lt]": "Padaryti, kad langai keistų dydį, kai yra rodomi ar slepiami", + "Description[lv]": "Likt logiem samazināties vai palielināties, kad tie parādās vai pazūd", + "Description[nl]": "Vensters kleiner en groter schalen wanneer ze worden getoond of verborgen", + "Description[nn]": "Skaler vindauge inn og ut når dei vert viste eller gøymde", + "Description[pl]": "Okna pomniejszają się przy otwieraniu i powiększają przy zamykaniu", + "Description[pt_BR]": "Faz com que as janelas aumentem e diminuam quando forem mostradas ou ocultadas", + "Description[ro]": "Face ferestrele să se scaleze când sunt arătate sau ascunse", + "Description[ru]": "Увеличение или уменьшение окон при их появлении и скрытии", + "Description[sk]": "Plynulé zväčšovanie a zmenšovanie okien pri ich zobrazení alebo skrytí", + "Description[sl]": "Naj se okna povečajo in zmanjšajo, ko so prikazana ali skrita", + "Description[sv]": "Gör att fönster skalas in eller ut när de visas eller döljs", + "Description[tr]": "Pencereler gösterilirken veya gizlenirken onları ölçekle", + "Description[uk]": "Масштабування вікон при появі або приховуванні", + "Description[zh_CN]": "窗口显示/隐藏时呈现等比例缩放过渡动效", + "Description[zh_TW]": "讓視窗在顯示或隱藏的時候以比例縮放方式出現或消失", + "EnabledByDefault": true, + "Icon": "preferences-system-windows-effect-scale", + "Id": "scale", + "License": "GPL", + "Name": "Scale", + "Name[ar]": "تغيير الحجم", + "Name[be]": "Маштабаванне", + "Name[bg]": "Мащабиране", + "Name[ca@valencia]": "Escala", + "Name[ca]": "Escala", + "Name[cs]": "Měřítko", + "Name[da]": "Skala", + "Name[de]": "Skalieren", + "Name[en_GB]": "Scale", + "Name[eo]": "Skalo", + "Name[es]": "Escalar", + "Name[et]": "Skaleerimine", + "Name[eu]": "Eskalatu", + "Name[fi]": "Skaalaus", + "Name[fr]": "Échelle", + "Name[gl]": "Escala", + "Name[he]": "קנה מידה", + "Name[hu]": "Méretezés", + "Name[ia]": "Scala", + "Name[id]": "Skala", + "Name[is]": "Kvarði", + "Name[it]": "Scala", + "Name[ja]": "スケール", + "Name[ka]": "მასშტაბირება", + "Name[ko]": "크기 조정", + "Name[lt]": "Mastelis", + "Name[lv]": "Mērogot", + "Name[nb]": "Skalering", + "Name[nl]": "Schaal", + "Name[nn]": "Skalering", + "Name[pl]": "Skalowanie", + "Name[pt]": "Escala", + "Name[pt_BR]": "Escalonar", + "Name[ro]": "Scalare", + "Name[ru]": "Масштабирование", + "Name[sa]": "मापन", + "Name[sk]": "Meniť mierku", + "Name[sl]": "Spremeni velikost", + "Name[sv]": "Skala", + "Name[ta]": "அளவுமாற்று", + "Name[tr]": "Ölçeklendir", + "Name[uk]": "Масштабування", + "Name[vi]": "Đổi cỡ", + "Name[zh_CN]": "按比例缩放", + "Name[zh_TW]": "縮放" + }, + "X-KDE-ConfigModule": "kcm_kwin4_genericscripted", + "X-KDE-Ordering": 60, + "X-KWin-Config-TranslationDomain": "kwin", + "X-KWin-Exclusive-Category": "toplevel-open-close-animation", + "X-Plasma-API": "javascript" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/screencast/CMakeLists.txt new file mode 100644 index 0000000000..4dd5262d78 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/CMakeLists.txt @@ -0,0 +1,23 @@ +kcoreaddons_add_plugin(screencast INSTALL_NAMESPACE "kwin/plugins") +target_sources(screencast PRIVATE + filteredsceneview.cpp + main.cpp + outputscreencastsource.cpp + pipewirecore.cpp + regionscreencastsource.cpp + screencastbuffer.cpp + screencastlayer.cpp + screencastmanager.cpp + screencastsource.cpp + screencaststream.cpp + windowscreencastsource.cpp +) + +ecm_qt_declare_logging_category(screencast + HEADER kwinscreencast_logging.h + IDENTIFIER KWIN_SCREENCAST + CATEGORY_NAME kwin_screencast + DEFAULT_SEVERITY Warning +) + +target_link_libraries(screencast kwin KF6::I18n PkgConfig::PipeWire Libdrm::Libdrm) diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/main.cpp b/local/recipes/kde/kwin/source/src/plugins/screencast/main.cpp new file mode 100644 index 0000000000..86ee486b8f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/main.cpp @@ -0,0 +1,30 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "screencastmanager.h" + +#include + +using namespace KWin; + +class KWIN_EXPORT ScreencastManagerFactory : public PluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID PluginFactory_iid FILE "metadata.json") + Q_INTERFACES(KWin::PluginFactory) + +public: + explicit ScreencastManagerFactory() = default; + + std::unique_ptr create() const override; +}; + +std::unique_ptr ScreencastManagerFactory::create() const +{ + return std::make_unique(); +} + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/metadata.json b/local/recipes/kde/kwin/source/src/plugins/screencast/metadata.json new file mode 100644 index 0000000000..aa304f4093 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/metadata.json @@ -0,0 +1,5 @@ +{ + "KPlugin": { + "EnabledByDefault": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/outputscreencastsource.cpp b/local/recipes/kde/kwin/source/src/plugins/screencast/outputscreencastsource.cpp new file mode 100644 index 0000000000..0fd74bf3b8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/outputscreencastsource.cpp @@ -0,0 +1,173 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "outputscreencastsource.h" +#include "filteredsceneview.h" +#include "screencastlayer.h" +#include "screencastutils.h" + +#include "compositor.h" +#include "core/output.h" +#include "core/renderbackend.h" +#include "core/renderloop.h" +#include "cursor.h" +#include "opengl/eglbackend.h" +#include "opengl/egldisplay.h" +#include "opengl/glframebuffer.h" +#include "opengl/gltexture.h" +#include "scene/workspacescene.h" +#include "workspace.h" + +#include + +namespace KWin +{ + +OutputScreenCastSource::OutputScreenCastSource(LogicalOutput *output, std::optional pidToHide) + : ScreenCastSource() + , m_output(output) + , m_pidToHide(pidToHide) +{ + connect(workspace(), &Workspace::outputRemoved, this, [this](LogicalOutput *output) { + if (m_output == output) { + Q_EMIT closed(); + } + }); + connect(Compositor::self(), &Compositor::aboutToToggleCompositing, this, [this]() { + Q_EMIT closed(); + }); +} + +OutputScreenCastSource::~OutputScreenCastSource() +{ + pause(); +} + +quint32 OutputScreenCastSource::drmFormat() const +{ + return DRM_FORMAT_ARGB8888; +} + +QSize OutputScreenCastSource::textureSize() const +{ + return m_output->pixelSize(); +} + +qreal OutputScreenCastSource::devicePixelRatio() const +{ + return m_output->scale(); +} + +void OutputScreenCastSource::setRenderCursor(bool enable) +{ + m_renderCursor = enable; + if (m_cursorView) { + m_cursorView->setExclusive(!enable); + } +} + +Region OutputScreenCastSource::render(QImage *target, const Region &bufferRepair) +{ + auto texture = GLTexture::allocate(GL_RGBA8, target->size()); + if (!texture) { + return Region{}; + } + GLFramebuffer buffer(texture.get()); + const Region ret = render(&buffer, Region::infinite()); + grabTexture(texture.get(), target); + return ret; +} + +Region OutputScreenCastSource::render(GLFramebuffer *target, const Region &bufferRepair) +{ + m_layer->setFramebuffer(target, bufferRepair & Rect(QPoint(), target->size())); + if (!m_layer->preparePresentationTest()) { + return Region{}; + } + const auto beginInfo = m_layer->beginFrame(); + if (!beginInfo) { + return Region{}; + } + m_sceneView->prePaint(); + const auto bufferDamage = (m_layer->deviceRepaints() | m_sceneView->collectDamage()) & Rect(QPoint(), target->size()); + const auto repaints = beginInfo->repaint | bufferDamage; + m_layer->resetRepaints(); + m_sceneView->paint(beginInfo->renderTarget, QPoint(), repaints); + m_sceneView->postPaint(); + if (!m_layer->endFrame(repaints, bufferDamage, nullptr)) { + return Region{}; + } + return bufferDamage; +} + +std::chrono::nanoseconds OutputScreenCastSource::clock() const +{ + return m_output->backendOutput()->renderLoop()->lastPresentationTimestamp(); +} + +uint OutputScreenCastSource::refreshRate() const +{ + return m_output->refreshRate(); +} + +void OutputScreenCastSource::resume() +{ + if (m_active) { + return; + } + + m_layer = std::make_unique(m_output, static_cast(Compositor::self()->backend())->openglContext()->displayObject()->nonExternalOnlySupportedDrmFormats()); + + m_sceneView = std::make_unique(Compositor::self()->scene(), m_output, m_layer.get(), m_pidToHide); + m_sceneView->setViewport(m_output->geometryF()); + m_sceneView->setScale(m_output->scale()); + connect(m_output, &LogicalOutput::changed, m_sceneView.get(), [this]() { + m_sceneView->setViewport(m_output->geometryF()); + m_sceneView->setScale(m_output->scale()); + }); + + m_cursorView = std::make_unique(m_sceneView.get(), Compositor::self()->scene()->cursorItem(), m_output, nullptr, nullptr); + m_cursorView->setExclusive(!m_renderCursor); + + connect(m_layer.get(), &OutputLayer::repaintScheduled, this, &OutputScreenCastSource::frame); + Q_EMIT frame(); + + m_active = true; +} + +void OutputScreenCastSource::pause() +{ + if (!m_active) { + return; + } + + disconnect(m_layer.get(), &OutputLayer::repaintScheduled, this, &OutputScreenCastSource::frame); + + m_cursorView.reset(); + m_sceneView.reset(); + m_layer.reset(); + + m_active = false; +} + +bool OutputScreenCastSource::includesCursor(Cursor *cursor) const +{ + return cursor->isOnOutput(m_output); +} + +QPointF OutputScreenCastSource::mapFromGlobal(const QPointF &point) const +{ + return m_output->mapFromGlobal(point); +} + +RectF OutputScreenCastSource::mapFromGlobal(const RectF &rect) const +{ + return m_output->mapFromGlobal(rect); +} + +} // namespace KWin + +#include "moc_outputscreencastsource.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/outputscreencastsource.h b/local/recipes/kde/kwin/source/src/plugins/screencast/outputscreencastsource.h new file mode 100644 index 0000000000..7c3142b179 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/outputscreencastsource.h @@ -0,0 +1,57 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "screencastsource.h" + +#include + +namespace KWin +{ + +class FilteredSceneView; +class ItemTreeView; +class LogicalOutput; +class ScreencastLayer; + +class OutputScreenCastSource : public ScreenCastSource +{ + Q_OBJECT + +public: + explicit OutputScreenCastSource(LogicalOutput *output, std::optional pidToHide); + ~OutputScreenCastSource() override; + + uint refreshRate() const override; + QSize textureSize() const override; + qreal devicePixelRatio() const override; + quint32 drmFormat() const override; + + void setRenderCursor(bool enable) override; + Region render(GLFramebuffer *target, const Region &bufferRepair) override; + Region render(QImage *target, const Region &bufferRepair) override; + std::chrono::nanoseconds clock() const override; + + void resume() override; + void pause() override; + + bool includesCursor(Cursor *cursor) const override; + + QPointF mapFromGlobal(const QPointF &point) const override; + RectF mapFromGlobal(const RectF &rect) const override; + +private: + QPointer m_output; + std::optional m_pidToHide; + std::unique_ptr m_layer; + std::unique_ptr m_sceneView; + std::unique_ptr m_cursorView; + bool m_active = false; + bool m_renderCursor = false; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/pipewirecore.cpp b/local/recipes/kde/kwin/source/src/plugins/screencast/pipewirecore.cpp new file mode 100644 index 0000000000..94d77594f7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/pipewirecore.cpp @@ -0,0 +1,108 @@ +/* + SPDX-FileCopyrightText: 2018-2020 Red Hat Inc + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + SPDX-FileContributor: Jan Grulich + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "pipewirecore.h" +#include "kwinscreencast_logging.h" +#include + +#include + +#include + +namespace KWin +{ + +PipeWireCore::PipeWireCore() +{ + pw_init(nullptr, nullptr); + pwCoreEvents.version = PW_VERSION_CORE_EVENTS; + pwCoreEvents.error = &PipeWireCore::onCoreError; +} + +PipeWireCore::~PipeWireCore() +{ + if (pwMainLoop) { + pw_loop_leave(pwMainLoop); + } + + if (pwCore) { + pw_core_disconnect(pwCore); + } + + if (pwContext) { + pw_context_destroy(pwContext); + } + + if (pwMainLoop) { + pw_loop_destroy(pwMainLoop); + } + + pw_deinit(); +} + +void PipeWireCore::onCoreError(void *data, uint32_t id, int seq, int res, const char *message) +{ + qCWarning(KWIN_SCREENCAST) << "PipeWire remote error: " << message; + if (id == PW_ID_CORE && res == -EPIPE) { + PipeWireCore *pw = static_cast(data); + pw->m_valid = false; + Q_EMIT pw->pipewireFailed(QString::fromUtf8(message)); + } +} + +bool PipeWireCore::init() +{ + pwMainLoop = pw_loop_new(nullptr); + if (!pwMainLoop) { + qCWarning(KWIN_SCREENCAST, "Failed to create PipeWire loop: %s", strerror(errno)); + m_error = i18n("Failed to start main PipeWire loop"); + return false; + } + pw_loop_enter(pwMainLoop); + + QSocketNotifier *notifier = new QSocketNotifier(pw_loop_get_fd(pwMainLoop), QSocketNotifier::Read, this); + connect(notifier, &QSocketNotifier::activated, this, [this] { + int result = pw_loop_iterate(pwMainLoop, 0); + if (result < 0) { + qCWarning(KWIN_SCREENCAST) << "pipewire_loop_iterate failed: " << result; + } + }); + + pwContext = pw_context_new(pwMainLoop, nullptr, 0); + if (!pwContext) { + qCWarning(KWIN_SCREENCAST) << "Failed to create PipeWire context"; + m_error = i18n("Failed to create PipeWire context"); + return false; + } + + pwCore = pw_context_connect(pwContext, nullptr, 0); + if (!pwCore) { + qCWarning(KWIN_SCREENCAST) << "Failed to connect PipeWire context"; + m_error = i18n("Failed to connect PipeWire context"); + return false; + } + + if (pw_loop_iterate(pwMainLoop, 0) < 0) { + qCWarning(KWIN_SCREENCAST) << "Failed to start main PipeWire loop"; + m_error = i18n("Failed to start main PipeWire loop"); + return false; + } + + pw_core_add_listener(pwCore, &coreListener, &pwCoreEvents, this); + m_valid = true; + return true; +} + +bool PipeWireCore::isValid() const +{ + return m_valid; +} + +} // namespace KWin + +#include "moc_pipewirecore.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/pipewirecore.h b/local/recipes/kde/kwin/source/src/plugins/screencast/pipewirecore.h new file mode 100644 index 0000000000..f66dc0edb2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/pipewirecore.h @@ -0,0 +1,50 @@ +/* + SPDX-FileCopyrightText: 2018-2020 Red Hat Inc + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + SPDX-FileContributor: Jan Grulich + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include + +#include +#include + +namespace KWin +{ + +class PipeWireCore : public QObject +{ + Q_OBJECT +public: + PipeWireCore(); + + static void onCoreError(void *data, uint32_t id, int seq, int res, const char *message); + + ~PipeWireCore(); + + bool init(); + bool isValid() const; + + static std::shared_ptr self(); + + struct pw_core *pwCore = nullptr; + struct pw_context *pwContext = nullptr; + struct pw_loop *pwMainLoop = nullptr; + spa_hook coreListener; + QString m_error; + + pw_core_events pwCoreEvents = {}; + +Q_SIGNALS: + void pipewireFailed(const QString &message); + +private: + bool m_valid = false; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/regionscreencastsource.cpp b/local/recipes/kde/kwin/source/src/plugins/screencast/regionscreencastsource.cpp new file mode 100644 index 0000000000..8e0ff9c803 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/regionscreencastsource.cpp @@ -0,0 +1,183 @@ +/* + SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "regionscreencastsource.h" +#include "filteredsceneview.h" +#include "screencastlayer.h" +#include "screencastutils.h" + +#include "compositor.h" +#include "core/output.h" +#include "cursor.h" +#include "opengl/eglbackend.h" +#include "opengl/glframebuffer.h" +#include "opengl/gltexture.h" +#include "scene/workspacescene.h" +#include "workspace.h" + +#include + +namespace KWin +{ + +RegionScreenCastSource::RegionScreenCastSource(const Rect ®ion, qreal scale, std::optional pidToHide) + : ScreenCastSource() + , m_region(region) + , m_scale(scale) + , m_pidToHide(pidToHide) +{ + Q_ASSERT(m_region.isValid()); + Q_ASSERT(m_scale > 0); + + // TODO once the layer doesn't depend on the output anymore, remove this? + connect(workspace(), &Workspace::outputsChanged, this, &RegionScreenCastSource::close); + connect(Compositor::self(), &Compositor::aboutToToggleCompositing, this, &RegionScreenCastSource::close); +} + +RegionScreenCastSource::~RegionScreenCastSource() +{ + pause(); +} + +QSize RegionScreenCastSource::textureSize() const +{ + return m_region.size() * m_scale; +} + +qreal RegionScreenCastSource::devicePixelRatio() const +{ + return m_scale; +} + +quint32 RegionScreenCastSource::drmFormat() const +{ + return DRM_FORMAT_ARGB8888; +} + +std::chrono::nanoseconds RegionScreenCastSource::clock() const +{ + return m_last; +} + +void RegionScreenCastSource::setRenderCursor(bool enable) +{ + m_renderCursor = enable; + if (m_cursorView) { + m_cursorView->setExclusive(!enable); + } +} + +Region RegionScreenCastSource::render(GLFramebuffer *target, const Region &bufferRepair) +{ + m_last = std::chrono::steady_clock::now().time_since_epoch(); + m_layer->setFramebuffer(target, bufferRepair & Rect(QPoint(), target->size())); + if (!m_layer->preparePresentationTest()) { + return Region{}; + } + const auto beginInfo = m_layer->beginFrame(); + if (!beginInfo) { + return Region{}; + } + m_sceneView->prePaint(); + const auto bufferDamage = (m_layer->deviceRepaints() | m_sceneView->collectDamage()) & Rect(QPoint(), target->size()); + const auto repaints = beginInfo->repaint | bufferDamage; + m_layer->resetRepaints(); + m_sceneView->paint(beginInfo->renderTarget, QPoint(), repaints); + m_sceneView->postPaint(); + if (!m_layer->endFrame(repaints, bufferDamage, nullptr)) { + return Region{}; + } + return bufferDamage; +} + +Region RegionScreenCastSource::render(QImage *target, const Region &bufferRepair) +{ + auto texture = GLTexture::allocate(GL_RGBA8, target->size()); + if (!texture) { + return Region{}; + } + GLFramebuffer buffer(texture.get()); + const Region ret = render(&buffer, Region::infinite()); + grabTexture(texture.get(), target); + return ret; +} + +uint RegionScreenCastSource::refreshRate() const +{ + uint ret = 0; + const auto allOutputs = workspace()->outputs(); + for (auto output : allOutputs) { + if (output->geometry().intersects(m_region)) { + ret = std::max(ret, output->refreshRate()); + } + } + return ret; +} + +void RegionScreenCastSource::close() +{ + if (!m_closed) { + m_closed = true; + Q_EMIT closed(); + } +} + +void RegionScreenCastSource::pause() +{ + if (!m_active) { + return; + } + + m_active = false; + disconnect(m_layer.get(), &OutputLayer::repaintScheduled, this, &RegionScreenCastSource::frame); + + m_cursorView.reset(); + m_sceneView.reset(); + m_layer.reset(); +} + +void RegionScreenCastSource::resume() +{ + if (m_active) { + return; + } + + m_layer = std::make_unique(workspace()->outputs().front(), static_cast(Compositor::self()->backend())->openglContext()->displayObject()->nonExternalOnlySupportedDrmFormats()); + + m_sceneView = std::make_unique(Compositor::self()->scene(), workspace()->outputs().front(), m_layer.get(), m_pidToHide); + m_sceneView->setViewport(m_region); + m_sceneView->setScale(m_scale); + + m_cursorView = std::make_unique(m_sceneView.get(), Compositor::self()->scene()->cursorItem(), workspace()->outputs().front(), nullptr, nullptr); + m_cursorView->setExclusive(!m_renderCursor); + + m_active = true; + connect(m_layer.get(), &OutputLayer::repaintScheduled, this, &RegionScreenCastSource::frame); + Q_EMIT frame(); +} + +bool RegionScreenCastSource::includesCursor(Cursor *cursor) const +{ + if (Cursors::self()->isCursorHidden()) { + return false; + } + + return cursor->geometry().intersects(m_region); +} + +QPointF RegionScreenCastSource::mapFromGlobal(const QPointF &point) const +{ + return point - m_region.topLeft(); +} + +RectF RegionScreenCastSource::mapFromGlobal(const RectF &rect) const +{ + return rect.translated(-m_region.topLeft()); +} + +} // namespace KWin + +#include "moc_regionscreencastsource.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/regionscreencastsource.h b/local/recipes/kde/kwin/source/src/plugins/screencast/regionscreencastsource.h new file mode 100644 index 0000000000..b9fd361e6d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/regionscreencastsource.h @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2022 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/rect.h" +#include "screencastsource.h" + +namespace KWin +{ + +class FilteredSceneView; +class ItemTreeView; +class RegionScreenCastSource; +class ScreencastLayer; + +class RegionScreenCastSource : public ScreenCastSource +{ + Q_OBJECT + +public: + explicit RegionScreenCastSource(const Rect ®ion, qreal scale, std::optional pidToHide); + ~RegionScreenCastSource() override; + + quint32 drmFormat() const override; + QSize textureSize() const override; + qreal devicePixelRatio() const override; + uint refreshRate() const override; + + void setRenderCursor(bool enable) override; + Region render(GLFramebuffer *target, const Region &bufferRepair) override; + Region render(QImage *target, const Region &bufferRepair) override; + std::chrono::nanoseconds clock() const override; + + void close(); + void pause() override; + void resume() override; + + bool includesCursor(Cursor *cursor) const override; + + QPointF mapFromGlobal(const QPointF &point) const override; + RectF mapFromGlobal(const RectF &rect) const override; + +private: + const Rect m_region; + const qreal m_scale; + const std::optional m_pidToHide; + std::chrono::nanoseconds m_last{0}; + bool m_closed = false; + bool m_active = false; + bool m_renderCursor = false; + + std::unique_ptr m_layer; + std::unique_ptr m_sceneView; + std::unique_ptr m_cursorView; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/screencastbuffer.cpp b/local/recipes/kde/kwin/source/src/plugins/screencast/screencastbuffer.cpp new file mode 100644 index 0000000000..78a10d4956 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/screencastbuffer.cpp @@ -0,0 +1,149 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "screencastbuffer.h" +#include "compositor.h" +#include "core/drmdevice.h" +#include "core/shmgraphicsbufferallocator.h" +#include "opengl/eglbackend.h" +#include "opengl/glframebuffer.h" + +namespace KWin +{ + +ScreenCastBuffer::ScreenCastBuffer(GraphicsBuffer *buffer) + : m_buffer(buffer) +{ +} + +ScreenCastBuffer::~ScreenCastBuffer() +{ + m_buffer->drop(); +} + +DmaBufScreenCastBuffer::DmaBufScreenCastBuffer(GraphicsBuffer *buffer, std::shared_ptr &&texture, std::unique_ptr &&framebuffer, std::unique_ptr &&synctimeline) + : ScreenCastBuffer(buffer) + , texture(std::move(texture)) + , framebuffer(std::move(framebuffer)) + , synctimeline(std::move(synctimeline)) +{ +} + +DmaBufScreenCastBuffer *DmaBufScreenCastBuffer::create(pw_buffer *pwBuffer, const GraphicsBufferOptions &options) +{ + EglBackend *backend = dynamic_cast(Compositor::self()->backend()); + if (!backend || !backend->drmDevice()) { + return nullptr; + } + + GraphicsBuffer *buffer = backend->drmDevice()->allocator()->allocate(options); + if (!buffer) { + return nullptr; + } + + const DmaBufAttributes *attrs = buffer->dmabufAttributes(); + if (!attrs) { + buffer->drop(); + return nullptr; + } + + const void *syncTimelineMeta = spa_buffer_find_meta_data(pwBuffer->buffer, SPA_META_SyncTimeline, sizeof(spa_meta_sync_timeline)); + if (pwBuffer->buffer->n_datas != uint32_t(attrs->planeCount + (syncTimelineMeta ? 2 : 0))) { + buffer->drop(); + return nullptr; + } + + backend->openglContext()->makeCurrent(); + + auto texture = backend->importDmaBufAsTexture(*attrs); + if (!texture) { + buffer->drop(); + return nullptr; + } + + auto framebuffer = std::make_unique(texture.get()); + if (!framebuffer->valid()) { + buffer->drop(); + return nullptr; + } + + struct spa_data *spaData = pwBuffer->buffer->datas; + for (int i = 0; i < attrs->planeCount; ++i) { + spaData[i].type = SPA_DATA_DmaBuf; + spaData[i].flags = SPA_DATA_FLAG_READWRITE; + spaData[i].mapoffset = 0; + spaData[i].maxsize = i == 0 ? attrs->pitch[i] * attrs->height : 0; // TODO: dmabufs don't have a well defined size, it should be zero but some clients check the size to see if the buffer is valid + spaData[i].fd = attrs->fd[i].get(); + spaData[i].data = nullptr; + spaData[i].chunk->offset = attrs->offset[i]; + spaData[i].chunk->size = spaData[i].maxsize; + spaData[i].chunk->stride = attrs->pitch[i]; + spaData[i].chunk->flags = SPA_CHUNK_FLAG_NONE; + }; + + std::unique_ptr synctimeline; + if (syncTimelineMeta) { + synctimeline = std::make_unique(backend->drmDevice()->fileDescriptor()); + const FileDescriptor &syncobjfd = synctimeline->fileDescriptor(); + if (!syncobjfd.isValid()) { + buffer->drop(); + return nullptr; + } + + // Signal the first timeline point, so the very first recording can proceed. + synctimeline->signal(0); + + spa_data &acquireData = spaData[attrs->planeCount]; + acquireData.type = SPA_DATA_SyncObj; + acquireData.flags = SPA_DATA_FLAG_READABLE; + acquireData.fd = syncobjfd.get(); + + spa_data &releaseData = spaData[attrs->planeCount + 1]; + releaseData.type = SPA_DATA_SyncObj; + releaseData.flags = SPA_DATA_FLAG_READABLE; + releaseData.fd = syncobjfd.get(); + } + + return new DmaBufScreenCastBuffer(buffer, std::move(texture), std::move(framebuffer), std::move(synctimeline)); +} + +MemFdScreenCastBuffer::MemFdScreenCastBuffer(GraphicsBuffer *buffer, GraphicsBufferView &&view) + : ScreenCastBuffer(buffer) + , view(std::move(view)) +{ +} + +MemFdScreenCastBuffer *MemFdScreenCastBuffer::create(pw_buffer *pwBuffer, const GraphicsBufferOptions &options) +{ + GraphicsBuffer *buffer = ShmGraphicsBufferAllocator().allocate(options); + if (!buffer) { + return nullptr; + } + + GraphicsBufferView view(buffer, GraphicsBuffer::Read | GraphicsBuffer::Write); + if (view.isNull()) { + buffer->drop(); + return nullptr; + } + + const ShmAttributes *attributes = buffer->shmAttributes(); + + struct spa_data *spaData = pwBuffer->buffer->datas; + spaData->type = SPA_DATA_MemFd; + spaData->flags = SPA_DATA_FLAG_READWRITE; + spaData->mapoffset = 0; + spaData->maxsize = attributes->stride * attributes->size.height(); + spaData->fd = attributes->fd.get(); + spaData->data = nullptr; + spaData->chunk->offset = 0; + spaData->chunk->size = spaData->maxsize; + spaData->chunk->stride = attributes->stride; + spaData->chunk->flags = SPA_CHUNK_FLAG_NONE; + + return new MemFdScreenCastBuffer(buffer, std::move(view)); +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/screencastbuffer.h b/local/recipes/kde/kwin/source/src/plugins/screencast/screencastbuffer.h new file mode 100644 index 0000000000..8b63c9eaac --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/screencastbuffer.h @@ -0,0 +1,58 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/graphicsbufferview.h" +#include "core/syncobjtimeline.h" + +#include + +namespace KWin +{ + +class GLFramebuffer; +class GLTexture; +class GraphicsBuffer; +struct GraphicsBufferOptions; + +class ScreenCastBuffer +{ +public: + explicit ScreenCastBuffer(GraphicsBuffer *buffer); + virtual ~ScreenCastBuffer(); + + int m_age = 0; + +private: + GraphicsBuffer *m_buffer; +}; + +class DmaBufScreenCastBuffer : public ScreenCastBuffer +{ +public: + static DmaBufScreenCastBuffer *create(pw_buffer *pwBuffer, const GraphicsBufferOptions &options); + + std::shared_ptr texture; + std::unique_ptr framebuffer; + std::unique_ptr synctimeline; + +private: + DmaBufScreenCastBuffer(GraphicsBuffer *buffer, std::shared_ptr &&texture, std::unique_ptr &&framebuffer, std::unique_ptr &&synctimeline); +}; + +class MemFdScreenCastBuffer : public ScreenCastBuffer +{ +public: + static MemFdScreenCastBuffer *create(pw_buffer *pwBuffer, const GraphicsBufferOptions &options); + + GraphicsBufferView view; + +private: + MemFdScreenCastBuffer(GraphicsBuffer *buffer, GraphicsBufferView &&view); +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/screencastmanager.cpp b/local/recipes/kde/kwin/source/src/plugins/screencast/screencastmanager.cpp new file mode 100644 index 0000000000..d30c9136c3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/screencastmanager.cpp @@ -0,0 +1,204 @@ +/* + SPDX-FileCopyrightText: 2018-2020 Red Hat Inc + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + SPDX-FileContributor: Jan Grulich + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "screencastmanager.h" +#include "compositor.h" +#include "core/backendoutput.h" +#include "core/output.h" +#include "core/outputbackend.h" +#include "core/renderbackend.h" +#include "outputscreencastsource.h" +#include "pipewirecore.h" +#include "regionscreencastsource.h" +#include "screencaststream.h" +#include "wayland/clientconnection.h" +#include "wayland/display.h" +#include "wayland/output.h" +#include "wayland_server.h" +#include "window.h" +#include "windowscreencastsource.h" +#include "workspace.h" + +#include + +namespace KWin +{ + +ScreencastManager::ScreencastManager() + : m_screencast(new ScreencastV1Interface(waylandServer()->display(), this)) +{ + getPipewireConnection(); + + connect(m_screencast, &ScreencastV1Interface::windowScreencastRequested, this, &ScreencastManager::streamWindow); + connect(m_screencast, &ScreencastV1Interface::outputScreencastRequested, this, &ScreencastManager::streamWaylandOutput); + connect(m_screencast, &ScreencastV1Interface::virtualOutputScreencastRequested, this, &ScreencastManager::streamVirtualOutput); + connect(m_screencast, &ScreencastV1Interface::regionScreencastRequested, this, &ScreencastManager::streamRegion); +} + +static bool isSupportedCompositingType() +{ + if (auto backend = Compositor::self()->backend()) { + return backend->compositingType() == OpenGLCompositing; + } + return false; +} + +void ScreencastManager::streamWindow(ScreencastStreamV1Interface *waylandStream, + const QString &winid, + ScreencastV1Interface::CursorMode mode) +{ + if (!isSupportedCompositingType()) { + waylandStream->sendFailed(i18n("Unsupported compositing type")); + return; + } + + auto window = Workspace::self()->findWindow(QUuid(winid)); + if (!window) { + waylandStream->sendFailed(i18n("Could not find window id %1", winid)); + return; + } + + auto stream = new ScreenCastStream(new WindowScreenCastSource(window), getPipewireConnection(), this); + stream->setObjectName(window->desktopFileName()); + stream->setCursorMode(mode); + + integrateStreams(waylandStream, stream); +} + +void ScreencastManager::streamVirtualOutput(ScreencastStreamV1Interface *stream, + const QString &name, + const QString &description, + const QSize &size, + double scale, + ScreencastV1Interface::CursorMode mode) +{ + if (!isSupportedCompositingType()) { + stream->sendFailed(i18n("Unsupported compositing type")); + return; + } + + auto output = kwinApp()->outputBackend()->createVirtualOutput(name, description, size, scale); + streamOutput(stream, workspace()->findOutput(output), mode); + connect(stream, &ScreencastStreamV1Interface::finished, output, [output] { + kwinApp()->outputBackend()->removeVirtualOutput(output); + }); +} + +void ScreencastManager::streamWaylandOutput(ScreencastStreamV1Interface *waylandStream, + OutputInterface *output, + ScreencastV1Interface::CursorMode mode) +{ + if (!isSupportedCompositingType()) { + waylandStream->sendFailed(i18n("Unsupported compositing type")); + return; + } + + streamOutput(waylandStream, output->handle(), mode); +} + +static std::optional getPid(ScreencastStreamV1Interface *waylandStream) +{ + if (waylandStream->connection()->executablePath().contains("xdg-desktop-portal-kde")) { + // HACK to avoid the portal's windows being hidden + return std::nullopt; + } + return waylandStream->connection()->processId(); +} + +void ScreencastManager::streamOutput(ScreencastStreamV1Interface *waylandStream, + LogicalOutput *streamOutput, + ScreencastV1Interface::CursorMode mode) +{ + if (!streamOutput) { + waylandStream->sendFailed(i18n("Could not find output")); + return; + } + + auto stream = new ScreenCastStream(new OutputScreenCastSource(streamOutput, getPid(waylandStream)), getPipewireConnection(), this); + stream->setObjectName(streamOutput->name()); + stream->setCursorMode(mode); + + integrateStreams(waylandStream, stream); +} + +static QString rectToString(const Rect &rect) +{ + return QStringLiteral("%1,%2 %3x%4").arg(rect.x()).arg(rect.y()).arg(rect.width()).arg(rect.height()); +} + +static qreal devicePixelRatioForRegion(const Rect ®ion) +{ + qreal devicePixelRatio = 1.0; + + const auto outputs = workspace()->outputs(); + for (const LogicalOutput *output : outputs) { + if (output->geometry().intersects(region)) { + devicePixelRatio = std::max(devicePixelRatio, output->scale()); + } + } + + return devicePixelRatio; +} + +void ScreencastManager::streamRegion(ScreencastStreamV1Interface *waylandStream, const Rect &geometry, qreal scale, ScreencastV1Interface::CursorMode mode) +{ + if (!isSupportedCompositingType()) { + waylandStream->sendFailed(i18n("Unsupported compositing type")); + return; + } + + if (!geometry.isValid()) { + waylandStream->sendFailed(i18n("Invalid region")); + return; + } + + if (scale == 0) { + scale = devicePixelRatioForRegion(geometry); + } + + auto source = new RegionScreenCastSource(geometry, scale, getPid(waylandStream)); + auto stream = new ScreenCastStream(source, getPipewireConnection(), this); + stream->setObjectName(rectToString(geometry)); + stream->setCursorMode(mode); + + integrateStreams(waylandStream, stream); +} + +void ScreencastManager::integrateStreams(ScreencastStreamV1Interface *waylandStream, ScreenCastStream *stream) +{ + connect(waylandStream, &ScreencastStreamV1Interface::finished, stream, &ScreenCastStream::close); + connect(stream, &ScreenCastStream::closed, waylandStream, [stream, waylandStream] { + waylandStream->sendClosed(); + stream->deleteLater(); + }); + connect(stream, &ScreenCastStream::ready, stream, [waylandStream](uint nodeid) { + waylandStream->sendCreated(nodeid); + }); + if (!stream->init()) { + waylandStream->sendFailed(stream->error()); + delete stream; + } +} + +std::shared_ptr ScreencastManager::getPipewireConnection() +{ + if (m_pipewireConnectionCache && m_pipewireConnectionCache->isValid()) { + return m_pipewireConnectionCache; + } else { + std::shared_ptr pipeWireCore = std::make_shared(); + if (pipeWireCore->init()) { + m_pipewireConnectionCache = pipeWireCore; + } + // return a valid object even if init fails + return pipeWireCore; + } +} + +} // namespace KWin + +#include "moc_screencastmanager.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/screencastmanager.h b/local/recipes/kde/kwin/source/src/plugins/screencast/screencastmanager.h new file mode 100644 index 0000000000..509c738ffb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/screencastmanager.h @@ -0,0 +1,56 @@ +/* + SPDX-FileCopyrightText: 2018-2020 Red Hat Inc + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + SPDX-FileContributor: Jan Grulich + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "plugin.h" + +#include "wayland/screencast_v1.h" + +namespace KWin +{ +class LogicalOutput; +class ScreenCastStream; +class PipeWireCore; + +class ScreencastManager : public Plugin +{ + Q_OBJECT + +public: + explicit ScreencastManager(); + +private: + void streamWindow(ScreencastStreamV1Interface *stream, + const QString &winid, + ScreencastV1Interface::CursorMode mode); + void streamWaylandOutput(ScreencastStreamV1Interface *stream, + OutputInterface *output, + ScreencastV1Interface::CursorMode mode); + void + streamOutput(ScreencastStreamV1Interface *stream, LogicalOutput *output, ScreencastV1Interface::CursorMode mode); + void streamVirtualOutput(ScreencastStreamV1Interface *stream, + const QString &name, + const QString &description, + const QSize &size, + double scale, + ScreencastV1Interface::CursorMode mode); + void streamRegion(ScreencastStreamV1Interface *stream, + const Rect &geometry, + qreal scale, + ScreencastV1Interface::CursorMode mode); + + void integrateStreams(ScreencastStreamV1Interface *waylandStream, ScreenCastStream *stream); + + std::shared_ptr getPipewireConnection(); + + ScreencastV1Interface *m_screencast; + std::shared_ptr m_pipewireConnectionCache; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/screencastsource.cpp b/local/recipes/kde/kwin/source/src/plugins/screencast/screencastsource.cpp new file mode 100644 index 0000000000..df6bcb1f5d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/screencastsource.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "screencastsource.h" + +namespace KWin +{ + +ScreenCastSource::ScreenCastSource() +{ +} + +} // namespace KWin + +#include "moc_screencastsource.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/screencastsource.h b/local/recipes/kde/kwin/source/src/plugins/screencast/screencastsource.h new file mode 100644 index 0000000000..c15f81259b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/screencastsource.h @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +class QImage; + +namespace KWin +{ + +class Cursor; +class GLFramebuffer; +class GLTexture; +class RectF; +class Region; + +class ScreenCastSource : public QObject +{ + Q_OBJECT + +public: + explicit ScreenCastSource(); + + virtual uint refreshRate() const = 0; + virtual quint32 drmFormat() const = 0; + virtual QSize textureSize() const = 0; + virtual qreal devicePixelRatio() const = 0; + + virtual void setRenderCursor(bool enable) = 0; + virtual Region render(GLFramebuffer *target, const Region &bufferRepair) = 0; + virtual Region render(QImage *target, const Region &bufferRepair) = 0; + virtual std::chrono::nanoseconds clock() const = 0; + + virtual void resume() = 0; + virtual void pause() = 0; + + virtual bool includesCursor(Cursor *cursor) const = 0; + + virtual QPointF mapFromGlobal(const QPointF &point) const = 0; + virtual RectF mapFromGlobal(const RectF &rect) const = 0; + +Q_SIGNALS: + void frame(); + void closed(); +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/screencaststream.cpp b/local/recipes/kde/kwin/source/src/plugins/screencast/screencaststream.cpp new file mode 100644 index 0000000000..87584fc34f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/screencaststream.cpp @@ -0,0 +1,909 @@ +/* + SPDX-FileCopyrightText: 2018-2020 Red Hat Inc + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + SPDX-FileContributor: Jan Grulich + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "screencaststream.h" +#include "compositor.h" +#include "core/drmdevice.h" +#include "core/graphicsbufferallocator.h" +#include "core/renderbackend.h" +#include "cursor.h" +#include "kwinscreencast_logging.h" +#include "main.h" +#include "opengl/eglbackend.h" +#include "opengl/eglnativefence.h" +#include "opengl/glplatform.h" +#include "opengl/gltexture.h" +#include "pipewirecore.h" +#include "scene/workspacescene.h" +#include "screencastbuffer.h" +#include "screencastsource.h" +#include "utils/drm_format_helper.h" + +#include + +#include +#include + +#include +#include +#include + +namespace KWin +{ + +static const struct +{ + uint32_t drmFormat; + spa_video_format spaFormat; +} supportedFormats[] = { + { + .drmFormat = DRM_FORMAT_ARGB8888, + .spaFormat = SPA_VIDEO_FORMAT_BGRA, + }, + { + .drmFormat = DRM_FORMAT_XRGB8888, + .spaFormat = SPA_VIDEO_FORMAT_BGRx, + }, + { + .drmFormat = DRM_FORMAT_RGBA8888, + .spaFormat = SPA_VIDEO_FORMAT_ABGR, + }, + { + .drmFormat = DRM_FORMAT_RGBX8888, + .spaFormat = SPA_VIDEO_FORMAT_xBGR, + }, + { + .drmFormat = DRM_FORMAT_ABGR8888, + .spaFormat = SPA_VIDEO_FORMAT_RGBA, + }, + { + .drmFormat = DRM_FORMAT_XBGR8888, + .spaFormat = SPA_VIDEO_FORMAT_RGBx, + }, + { + .drmFormat = DRM_FORMAT_BGRA8888, + .spaFormat = SPA_VIDEO_FORMAT_ARGB, + }, + { + .drmFormat = DRM_FORMAT_BGRX8888, + .spaFormat = SPA_VIDEO_FORMAT_xRGB, + }, + { + .drmFormat = DRM_FORMAT_NV12, + .spaFormat = SPA_VIDEO_FORMAT_NV12, + }, + { + .drmFormat = DRM_FORMAT_RGB888, + .spaFormat = SPA_VIDEO_FORMAT_BGR, + }, + { + .drmFormat = DRM_FORMAT_BGR888, + .spaFormat = SPA_VIDEO_FORMAT_RGB, + }, +}; + +static spa_video_format drmFormatToSpaVideoFormat(quint32 drmFormat) +{ + for (const auto &info : supportedFormats) { + if (info.drmFormat == drmFormat) { + return info.spaFormat; + } + } + + qCDebug(KWIN_SCREENCAST) << "cannot convert drm format to spa format:" << drmFormat; + return SPA_VIDEO_FORMAT_UNKNOWN; +} + +static uint32_t spaVideoFormatToDrmFormat(spa_video_format spaFormat) +{ + for (const auto &info : supportedFormats) { + if (info.spaFormat == spaFormat) { + return info.drmFormat; + } + } + + qCDebug(KWIN_SCREENCAST) << "cannot convert spa format to drm format:" << spaFormat; + return DRM_FORMAT_INVALID; +} + +void ScreenCastStream::onStreamStateChanged(pw_stream_state old, pw_stream_state state, const char *error_message) +{ + qCDebug(KWIN_SCREENCAST) << objectName() << "state changed" << pw_stream_state_as_string(old) << " -> " << pw_stream_state_as_string(state) << error_message; + if (m_closed) { + return; + } + + switch (state) { + case PW_STREAM_STATE_ERROR: + qCWarning(KWIN_SCREENCAST) << objectName() << "Stream error: " << error_message; + break; + case PW_STREAM_STATE_PAUSED: + if (nodeId() == 0 && m_pwStream) { + m_pwNodeId = pw_stream_get_node_id(m_pwStream); + Q_EMIT ready(nodeId()); + } + m_pendingFrame.stop(); + m_pendingContents = Contents(); + m_source->pause(); + break; + case PW_STREAM_STATE_STREAMING: + m_lastSent.reset(); + m_source->resume(); + break; + case PW_STREAM_STATE_CONNECTING: + break; + case PW_STREAM_STATE_UNCONNECTED: + close(); + break; + } +} + +#define CURSOR_BPP 4 +#define CURSOR_META_SIZE(w, h) (sizeof(struct spa_meta_cursor) + sizeof(struct spa_meta_bitmap) + w * h * CURSOR_BPP) +static const int videoDamageRegionCount = 16; + +void ScreenCastStream::newStreamParams() +{ + qCDebug(KWIN_SCREENCAST) << objectName() << "announcing stream params. with dmabuf:" << m_dmabufParams.has_value(); + const int buffertypes = m_dmabufParams ? (1 << SPA_DATA_DmaBuf) : (1 << SPA_DATA_MemFd); + const int bpp = m_videoFormat.format == SPA_VIDEO_FORMAT_RGB || m_videoFormat.format == SPA_VIDEO_FORMAT_BGR ? 3 : 4; + const int stride = SPA_ROUND_UP_N(m_resolution.width() * bpp, 4); + + struct spa_pod_dynamic_builder pod_builder; + struct spa_pod_frame f; + spa_pod_dynamic_builder_init(&pod_builder, nullptr, 0, 1024); + + QVarLengthArray params; + + // Buffer parameters for explicit sync. It requires two extra blocks to hold acquire and + // release syncobjs. + if (m_dmabufParams && m_dmabufParams->supportsSyncObj) { + spa_pod_builder_push_object(&pod_builder.b, &f, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers); + spa_pod_builder_add(&pod_builder.b, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(3, 2, 4), + SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(buffertypes), + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(m_dmabufParams->planeCount + 2), 0); + spa_pod_builder_prop(&pod_builder.b, SPA_PARAM_BUFFERS_metaType, SPA_POD_PROP_FLAG_MANDATORY); + spa_pod_builder_int(&pod_builder.b, 1 << SPA_META_SyncTimeline); + params.append((spa_pod *)spa_pod_builder_pop(&pod_builder.b, &f)); + } + + // Fallback buffer parameters for DmaBuf with implicit sync or MemFd + spa_pod_builder_push_object(&pod_builder.b, &f, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers); + spa_pod_builder_add(&pod_builder.b, + SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(3, 2, 4), + SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(buffertypes), 0); + if (!m_dmabufParams) { + spa_pod_builder_add(&pod_builder.b, + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(stride * m_resolution.height()), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(stride), + SPA_PARAM_BUFFERS_align, SPA_POD_Int(16), 0); + } else { + spa_pod_builder_add(&pod_builder.b, + SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(m_dmabufParams->planeCount), 0); + } + params.append((spa_pod *)spa_pod_builder_pop(&pod_builder.b, &f)); + + // Metadata parameters + params.append( + (spa_pod *)spa_pod_builder_add_object(&pod_builder.b, + SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Cursor), + SPA_PARAM_META_size, SPA_POD_Int(CURSOR_META_SIZE(m_cursor.bitmapSize.width(), m_cursor.bitmapSize.height())))); + params.append( + (spa_pod *)spa_pod_builder_add_object(&pod_builder.b, + SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoDamage), + SPA_PARAM_META_size, SPA_POD_CHOICE_RANGE_Int(sizeof(struct spa_meta_region) * videoDamageRegionCount, sizeof(struct spa_meta_region) * 1, sizeof(struct spa_meta_region) * videoDamageRegionCount))); + params.append( + (spa_pod *)spa_pod_builder_add_object(&pod_builder.b, + SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)))); + if (m_dmabufParams && m_dmabufParams->supportsSyncObj) { + params.append( + (spa_pod *)spa_pod_builder_add_object(&pod_builder.b, + SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_SyncTimeline), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_sync_timeline)))); + } + + pw_stream_update_params(m_pwStream, params.data(), params.count()); +} + +void ScreenCastStream::onStreamParamChanged(uint32_t id, const struct spa_pod *format) +{ + if (m_closed) { + return; + } + + if (!format || id != SPA_PARAM_Format) { + qCDebug(KWIN_SCREENCAST) << objectName() << "stream param request ignored, id:" << id << "and with format:" << (format != nullptr); + return; + } + + spa_format_video_raw_parse(format, &m_videoFormat); + auto modifierProperty = spa_pod_find_prop(format, nullptr, SPA_FORMAT_VIDEO_modifier); + if (modifierProperty) { + const uint32_t valueCount = SPA_POD_CHOICE_N_VALUES(&modifierProperty->value); + const uint64_t *values = (uint64_t *)SPA_POD_CHOICE_VALUES(&modifierProperty->value); // values[0] is the preferred choice + + // Note that we don't need to search for duplicates in values[1...] but we do that anyway as a + // sanity check because the DRM modifier negotiation logic can be easily tripped by bad input. + QList receivedModifiers; + receivedModifiers.reserve(valueCount); + for (uint32_t i = 0; i < valueCount; ++i) { + if (!receivedModifiers.contains(values[i])) { + receivedModifiers.append(values[i]); + } + } + + if (!m_dmabufParams || m_dmabufParams->width != m_resolution.width() || m_dmabufParams->height != m_resolution.height() || !receivedModifiers.contains(m_dmabufParams->modifier)) { + // DRM_MOD_INVALID should be used as a last option. Do not just remove it it's the only + // item on the list + if (receivedModifiers.count() > 1) { + receivedModifiers.removeAll(DRM_FORMAT_MOD_INVALID); + } + m_dmabufParams = testCreateDmaBuf(m_resolution, m_drmFormat, receivedModifiers); + + // In case we fail to use any modifier from the list of offered ones, remove these + // from our all future offerings, otherwise there will be no indication that it cannot + // be used and clients can go for it over and over + if (!m_dmabufParams.has_value()) { + for (uint64_t modifier : receivedModifiers) { + m_modifiers.removeAll(modifier); + } + } + + qCDebug(KWIN_SCREENCAST) << objectName() << "Stream dmabuf modifiers received, offering our best suited modifier" << m_dmabufParams.has_value(); + char buffer[2048]; + auto params = buildFormats(m_dmabufParams.has_value(), buffer); + pw_stream_update_params(m_pwStream, params.data(), params.count()); + return; + } + } else { + m_dmabufParams.reset(); + } + + qCDebug(KWIN_SCREENCAST) << objectName() << "Stream format found, defining buffers"; + newStreamParams(); +} + +void ScreenCastStream::onStreamAddBuffer(pw_buffer *pwBuffer) +{ + if (m_closed) { + return; + } + + struct spa_data *spa_data = pwBuffer->buffer->datas; + if (spa_data[0].type & (1 << SPA_DATA_DmaBuf)) { + if (auto dmabuf = DmaBufScreenCastBuffer::create(pwBuffer, GraphicsBufferOptions{ + .size = QSize(m_videoFormat.size.width, m_videoFormat.size.height), + .format = spaVideoFormatToDrmFormat(m_videoFormat.format), + .modifiers = {m_videoFormat.modifier}, + })) { + pwBuffer->user_data = dmabuf; + m_allBuffers.push_back(dmabuf); + return; + } + } + + if (spa_data[0].type & (1 << SPA_DATA_MemFd)) { + if (auto memfd = MemFdScreenCastBuffer::create(pwBuffer, GraphicsBufferOptions{ + .size = QSize(m_videoFormat.size.width, m_videoFormat.size.height), + .format = spaVideoFormatToDrmFormat(m_videoFormat.format), + .software = true, + })) { + pwBuffer->user_data = memfd; + m_allBuffers.push_back(memfd); + return; + } + } +} + +void ScreenCastStream::onStreamRemoveBuffer(pw_buffer *pwBuffer) +{ + if (ScreenCastBuffer *buffer = static_cast(pwBuffer->user_data)) { + delete buffer; + pwBuffer->user_data = nullptr; + m_allBuffers.removeOne(buffer); + } + + m_dequeuedBuffers.removeOne(pwBuffer); +} + +ScreenCastStream::ScreenCastStream(ScreenCastSource *source, std::shared_ptr pwCore, QObject *parent) + : QObject(parent) + , m_pwCore(pwCore) + , m_source(source) + , m_resolution(source->textureSize()) +{ + connect(source, &ScreenCastSource::frame, this, [this]() { + scheduleRecord(Content::Video); + }); + connect(source, &ScreenCastSource::closed, this, &ScreenCastStream::close); + + m_pwStreamEvents.version = PW_VERSION_STREAM_EVENTS; + m_pwStreamEvents.add_buffer = [](void *data, struct pw_buffer *buffer) { + auto _this = static_cast(data); + _this->onStreamAddBuffer(buffer); + }; + m_pwStreamEvents.remove_buffer = [](void *data, struct pw_buffer *buffer) { + auto _this = static_cast(data); + _this->onStreamRemoveBuffer(buffer); + }; + m_pwStreamEvents.state_changed = [](void *data, pw_stream_state old, pw_stream_state state, const char *error_message) { + auto _this = static_cast(data); + _this->onStreamStateChanged(old, state, error_message); + }; + m_pwStreamEvents.param_changed = [](void *data, uint32_t id, const struct spa_pod *param) { + auto _this = static_cast(data); + _this->onStreamParamChanged(id, param); + }; + + m_pendingFrame.setSingleShot(true); + connect(&m_pendingFrame, &QTimer::timeout, this, [this] { + record(m_pendingContents); + m_pendingContents = Contents(); + }); +} + +ScreenCastStream::~ScreenCastStream() +{ + m_closed = true; + + if (m_pwStream) { + pw_stream_destroy(m_pwStream); + } +} + +bool ScreenCastStream::init() +{ + if (!m_pwCore->m_error.isEmpty()) { + m_error = m_pwCore->m_error; + return false; + } + + EglBackend *backend = qobject_cast(Compositor::self()->backend()); + if (!backend) { + m_error = i18n("OpenGL compositing is required for screencasting"); + return false; + } + + connect(m_pwCore.get(), &PipeWireCore::pipewireFailed, this, &ScreenCastStream::coreFailed); + + if (!createStream()) { + qCWarning(KWIN_SCREENCAST) << objectName() << "Failed to create PipeWire stream"; + m_error = i18n("Failed to create PipeWire stream"); + return false; + } + + return true; +} + +uint ScreenCastStream::framerate() +{ + if (m_pwStream) { + return m_videoFormat.max_framerate.num / m_videoFormat.max_framerate.denom; + } + + return 0; +} + +uint ScreenCastStream::nodeId() +{ + return m_pwNodeId; +} + +bool ScreenCastStream::createStream() +{ + const QByteArray objname = "kwin-screencast-" + objectName().toUtf8(); + m_pwStream = pw_stream_new(m_pwCore->pwCore, objname, nullptr); + + const auto supported = Compositor::self()->backend()->supportedFormats(); + auto itModifiers = supported.constFind(m_source->drmFormat()); + + // If the offered format is not available for dmabuf, prefer converting to another one than resorting to memfd + if (itModifiers == supported.constEnd() && !supported.isEmpty()) { + itModifiers = supported.constFind(DRM_FORMAT_ARGB8888); + if (itModifiers != supported.constEnd()) { + m_drmFormat = itModifiers.key(); + } + } + + if (itModifiers == supported.constEnd()) { + m_drmFormat = m_source->drmFormat(); + m_modifiers = {DRM_FORMAT_MOD_INVALID}; + } else { + m_drmFormat = itModifiers.key(); + m_modifiers = *itModifiers; + } + m_hasDmaBuf = testCreateDmaBuf(m_resolution, m_drmFormat, m_modifiers).has_value(); + + char buffer[2048]; + QList params = buildFormats(false, buffer); + + pw_stream_add_listener(m_pwStream, &m_streamListener, &m_pwStreamEvents, this); + auto flags = pw_stream_flags(PW_STREAM_FLAG_DRIVER | PW_STREAM_FLAG_ALLOC_BUFFERS); + + if (pw_stream_connect(m_pwStream, PW_DIRECTION_OUTPUT, SPA_ID_INVALID, flags, params.data(), params.count()) != 0) { + qCWarning(KWIN_SCREENCAST) << objectName() << "Could not connect to stream"; + pw_stream_destroy(m_pwStream); + m_pwStream = nullptr; + return false; + } + + switch (m_cursor.mode) { + case ScreencastV1Interface::Hidden: + break; + case ScreencastV1Interface::Embedded: + case ScreencastV1Interface::Metadata: + m_cursor.changedConnection = connect(Cursors::self(), &Cursors::currentCursorChanged, this, &ScreenCastStream::invalidateCursor); + m_cursor.positionChangedConnection = connect(Cursors::self(), &Cursors::positionChanged, this, [this] { + scheduleRecord(Content::Cursor); + }); + break; + } + + qCDebug(KWIN_SCREENCAST) << objectName() << "stream created, drm format:" << FormatInfo::drmFormatName(m_drmFormat) << "with DMA-BUF:" << m_hasDmaBuf; + return true; +} +void ScreenCastStream::coreFailed(const QString &errorMessage) +{ + m_error = errorMessage; + close(); +} + +void ScreenCastStream::close() +{ + if (m_closed) { + return; + } + + m_closed = true; + m_pendingFrame.stop(); + + disconnect(m_cursor.changedConnection); + m_cursor.changedConnection = {}; + disconnect(m_cursor.positionChangedConnection); + m_cursor.positionChangedConnection = {}; + + m_source.reset(); + + Q_EMIT closed(); +} + +void ScreenCastStream::scheduleRecord(Contents contents) +{ + Q_ASSERT(!m_closed); + + const char *error = ""; + auto state = pw_stream_get_state(m_pwStream, &error); + if (state != PW_STREAM_STATE_STREAMING) { + if (error) { + qCWarning(KWIN_SCREENCAST) << objectName() << "Failed to record frame: stream is not active" << error; + } + return; + } + + if (contents == Content::Cursor) { + if (!m_cursor.visible && !m_source->includesCursor(Cursors::self()->currentCursor())) { + return; + } + } + + m_pendingContents |= contents; + + if (m_pendingFrame.isActive()) { + return; + } + std::chrono::milliseconds waitInterval{0}; + if (m_videoFormat.max_framerate.num != 0 && m_lastSent.has_value()) { + const auto now = std::chrono::steady_clock::now(); + const auto frameInterval = std::chrono::milliseconds(1000 * m_videoFormat.max_framerate.denom / m_videoFormat.max_framerate.num); + const auto lastSentAgo = std::chrono::duration_cast(now - m_lastSent.value()); + if (lastSentAgo < frameInterval) { + waitInterval = frameInterval - lastSentAgo; + } + } + m_pendingFrame.start(waitInterval); +} + +pw_buffer *ScreenCastStream::dequeueBuffer() +{ + const auto isBufferUsable = [](pw_buffer *pwBuffer) { + const spa_buffer *spaBuffer = pwBuffer->buffer; + const spa_data *spaData = spaBuffer->datas; + + if (spaData[0].type != SPA_DATA_DmaBuf) { + return true; + } + + auto dmabuf = static_cast(pwBuffer->user_data); + if (dmabuf && dmabuf->synctimeline) { + spa_meta_sync_timeline *synctmeta = + static_cast(spa_buffer_find_meta_data(spaBuffer, + SPA_META_SyncTimeline, + sizeof(spa_meta_sync_timeline))); + return dmabuf->synctimeline->isMaterialized(synctmeta->release_point); + } + + return true; + }; + + // First, search the list of already dequeued buffers + auto foundBuffer = std::find_if(m_dequeuedBuffers.begin(), m_dequeuedBuffers.end(), isBufferUsable); + if (foundBuffer != m_dequeuedBuffers.end()) { + pw_buffer *pwBuffer = *foundBuffer; + m_dequeuedBuffers.erase(foundBuffer); + return pwBuffer; + } + + // If we do not have a usable dequeued buffer, fetch a new one from the stream + pw_buffer *pwBuffer = pw_stream_dequeue_buffer(m_pwStream); + if (!pwBuffer) { + return nullptr; + } + + if (!pwBuffer->user_data) { + qCWarning(KWIN_SCREENCAST) << objectName() << "Received stream buffer that does not contain user data"; + corruptHeader(pwBuffer->buffer); + pw_stream_queue_buffer(m_pwStream, pwBuffer); + return nullptr; + } + + if (!isBufferUsable(pwBuffer)) { + m_dequeuedBuffers.append(pwBuffer); + return nullptr; + } + + return pwBuffer; +} + +void ScreenCastStream::record(Contents contents) +{ + EglBackend *backend = qobject_cast(Compositor::self()->backend()); + if (!backend) { + return; + } + + struct pw_buffer *pwBuffer = dequeueBuffer(); + if (!pwBuffer) { + return; + } + + struct spa_buffer *spa_buffer = pwBuffer->buffer; + struct spa_data *spa_data = spa_buffer->datas; + + ScreenCastBuffer *buffer = static_cast(pwBuffer->user_data); + + Contents effectiveContents = contents; + switch (m_cursor.mode) { + case ScreencastV1Interface::Hidden: + m_source->setRenderCursor(false); + break; + case ScreencastV1Interface::Metadata: + effectiveContents |= Content::Cursor; + m_source->setRenderCursor(false); + if (effectiveContents & Content::Cursor) { + addCursorMetadata(spa_buffer, Cursors::self()->currentCursor()); + } + break; + case ScreencastV1Interface::Embedded: + effectiveContents |= Content::Cursor | Content::Video; + m_source->setRenderCursor(m_source->includesCursor(Cursors::self()->currentCursor())); + break; + } + + EglContext *context = backend->openglContext(); + context->makeCurrent(); + + spa_meta_sync_timeline *synctmeta = nullptr; + + Region damage; + if (effectiveContents & Content::Video) { + if (auto memfd = dynamic_cast(buffer)) { + damage = m_source->render(memfd->view.image(), m_damageJournal.accumulate(memfd->m_age, Region::infinite())); + bumpBufferAge(memfd); + } else if (auto dmabuf = dynamic_cast(buffer)) { + if (dmabuf->synctimeline) { + synctmeta = static_cast(spa_buffer_find_meta_data(spa_buffer, + SPA_META_SyncTimeline, + sizeof(spa_meta_sync_timeline))); + FileDescriptor syncFileFd = dmabuf->synctimeline->exportSyncFile(synctmeta->release_point); + EGLNativeFence fence = EGLNativeFence::importFence(backend->eglDisplayObject(), std::move(syncFileFd)); + if (fence.waitSync() != EGL_TRUE) { + qCWarning(KWIN_SCREENCAST) << objectName() << "Failed to wait on a fence, recording may be corrupted"; + } + } + + damage = m_source->render(dmabuf->framebuffer.get(), m_damageJournal.accumulate(dmabuf->m_age, Region::infinite())); + bumpBufferAge(dmabuf); + } + m_damageJournal.add(damage); + } + + if (spa_data[0].type == SPA_DATA_DmaBuf) { + if (synctmeta) { + EGLNativeFence fence(backend->eglDisplayObject()); + + synctmeta->acquire_point = synctmeta->release_point + 1; + synctmeta->release_point = synctmeta->acquire_point + 1; + + auto dmabuf = static_cast(buffer); + dmabuf->synctimeline->moveInto(synctmeta->acquire_point, fence.takeFileDescriptor()); + } else { + // Implicit sync is broken on Nvidia and with llvmpipe + if (context->glPlatform()->isNvidia() || context->isSoftwareRenderer()) { + glFinish(); + } else { + glFlush(); + } + } + } + + addDamage(spa_buffer, damage); + addHeader(spa_buffer); + + if (effectiveContents & Content::Video) { + spa_data->chunk->flags = SPA_CHUNK_FLAG_NONE; + } else { + // in pipewire terms, corrupted means "do not look at the frame contents" and here they're empty. + spa_data->chunk->flags = SPA_CHUNK_FLAG_CORRUPTED; + } + + pw_stream_queue_buffer(m_pwStream, pwBuffer); + m_lastSent = std::chrono::steady_clock::now(); + + resize(m_source->textureSize()); +} + +void ScreenCastStream::bumpBufferAge(ScreenCastBuffer *renderedBuffer) +{ + for (ScreenCastBuffer *buffer : std::as_const(m_allBuffers)) { + if (buffer == renderedBuffer) { + buffer->m_age = 1; + } else if (buffer->m_age > 0) { + buffer->m_age++; + } + } +} + +void ScreenCastStream::resize(const QSize &resolution) +{ + if (m_resolution == resolution) { + return; + } + m_resolution = resolution; + + char buffer[2048]; + auto params = buildFormats(false, buffer); + pw_stream_update_params(m_pwStream, params.data(), params.count()); +} + +void ScreenCastStream::addHeader(spa_buffer *spaBuffer) +{ + spa_meta_header *spaHeader = (spa_meta_header *)spa_buffer_find_meta_data(spaBuffer, SPA_META_Header, sizeof(spa_meta_header)); + if (spaHeader) { + spaHeader->flags = 0; + spaHeader->dts_offset = 0; + spaHeader->seq = m_sequential++; + spaHeader->pts = m_source->clock().count(); + } +} + +void ScreenCastStream::corruptHeader(spa_buffer *spaBuffer) +{ + spa_meta_header *spaHeader = (spa_meta_header *)spa_buffer_find_meta_data(spaBuffer, SPA_META_Header, sizeof(spa_meta_header)); + if (spaHeader) { + spaHeader->flags = SPA_META_HEADER_FLAG_CORRUPTED; + } +} + +void ScreenCastStream::addDamage(spa_buffer *spaBuffer, const Region &damagedRegion) +{ + if (spa_meta *vdMeta = spa_buffer_find_meta(spaBuffer, SPA_META_VideoDamage)) { + struct spa_meta_region *r = (spa_meta_region *)spa_meta_first(vdMeta); + + // If there's too many rectangles, we just send the bounding rect + if (damagedRegion.rects().size() > videoDamageRegionCount - 1) { + if (spa_meta_check(r, vdMeta)) { + auto rect = damagedRegion.boundingRect(); + r->region = SPA_REGION(rect.x(), rect.y(), quint32(rect.width()), quint32(rect.height())); + r++; + } + } else { + for (const Rect &rect : damagedRegion.rects()) { + if (spa_meta_check(r, vdMeta)) { + r->region = SPA_REGION(rect.x(), rect.y(), quint32(rect.width()), quint32(rect.height())); + r++; + } + } + } + + if (spa_meta_check(r, vdMeta)) { + r->region = SPA_REGION(0, 0, 0, 0); + } + } +} + +void ScreenCastStream::invalidateCursor() +{ + m_cursor.invalid = true; +} + +QList ScreenCastStream::buildFormats(bool fixate, char buffer[2048]) +{ + const auto dmabufFormat = drmFormatToSpaVideoFormat(m_drmFormat); + const auto shmFormat = drmFormatToSpaVideoFormat(DRM_FORMAT_ARGB8888); + + spa_pod_builder podBuilder = SPA_POD_BUILDER_INIT(buffer, 2048); + spa_fraction defFramerate = SPA_FRACTION(0, 1); + spa_fraction minFramerate = SPA_FRACTION(0, 1); + spa_fraction maxFramerate = SPA_FRACTION(m_source->refreshRate() / 1000, 1); + + spa_rectangle resolution = SPA_RECTANGLE(uint32_t(m_resolution.width()), uint32_t(m_resolution.height())); + + QList params; + if (m_hasDmaBuf) { + if (fixate) { + params.append(buildFormat(&podBuilder, dmabufFormat, &resolution, &defFramerate, &minFramerate, &maxFramerate, {m_dmabufParams->modifier}, SPA_POD_PROP_FLAG_MANDATORY)); + } + params.append(buildFormat(&podBuilder, dmabufFormat, &resolution, &defFramerate, &minFramerate, &maxFramerate, m_modifiers, SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE)); + } + params.append(buildFormat(&podBuilder, shmFormat, &resolution, &defFramerate, &minFramerate, &maxFramerate, {}, 0)); + return params; +} + +spa_pod *ScreenCastStream::buildFormat(struct spa_pod_builder *b, enum spa_video_format format, struct spa_rectangle *resolution, + struct spa_fraction *defaultFramerate, struct spa_fraction *minFramerate, struct spa_fraction *maxFramerate, + const QList &modifiers, quint32 modifiersFlags) +{ + struct spa_pod_frame f[2]; + spa_pod_builder_push_object(b, &f[0], SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + spa_pod_builder_add(b, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), 0); + spa_pod_builder_add(b, SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), 0); + spa_pod_builder_add(b, SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(resolution), 0); + spa_pod_builder_add(b, SPA_FORMAT_VIDEO_framerate, SPA_POD_Fraction(defaultFramerate), 0); + spa_pod_builder_add(b, SPA_FORMAT_VIDEO_maxFramerate, + SPA_POD_CHOICE_RANGE_Fraction( + SPA_POD_Fraction(maxFramerate), + SPA_POD_Fraction(minFramerate), + SPA_POD_Fraction(maxFramerate)), + 0); + + if (format == SPA_VIDEO_FORMAT_BGRA) { + /* announce equivalent format without alpha */ + spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(3, format, format, SPA_VIDEO_FORMAT_BGRx), 0); + } else if (format == SPA_VIDEO_FORMAT_RGBA) { + /* announce equivalent format without alpha */ + spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(3, format, format, SPA_VIDEO_FORMAT_RGBx), 0); + } else { + spa_pod_builder_add(b, SPA_FORMAT_VIDEO_format, SPA_POD_Id(format), 0); + } + + if (!modifiers.isEmpty()) { + spa_pod_builder_prop(b, SPA_FORMAT_VIDEO_modifier, modifiersFlags); + spa_pod_builder_push_choice(b, &f[1], SPA_CHOICE_Enum, 0); + + int c = 0; + for (auto modifier : modifiers) { + spa_pod_builder_long(b, modifier); + if (c++ == 0) { + spa_pod_builder_long(b, modifier); + } + } + spa_pod_builder_pop(b, &f[1]); + } + return (spa_pod *)spa_pod_builder_pop(b, &f[0]); +} + +void ScreenCastStream::addCursorMetadata(spa_buffer *spaBuffer, Cursor *cursor) +{ + if (!cursor) { + return; + } + + auto spaMetaCursor = (spa_meta_cursor *)spa_buffer_find_meta_data(spaBuffer, SPA_META_Cursor, sizeof(spa_meta_cursor)); + if (!spaMetaCursor) { + return; + } + + if (!m_source->includesCursor(cursor)) { + spaMetaCursor->id = 0; + m_cursor.visible = false; + return; + } + m_cursor.visible = true; + + const qreal scale = m_source->devicePixelRatio(); + const auto position = m_source->mapFromGlobal(cursor->pos()) * scale; + + spaMetaCursor->id = 1; + spaMetaCursor->position.x = position.x(); + spaMetaCursor->position.y = position.y(); + spaMetaCursor->hotspot.x = cursor->hotspot().x() * scale; + spaMetaCursor->hotspot.y = cursor->hotspot().y() * scale; + spaMetaCursor->bitmap_offset = 0; + + if (!m_cursor.invalid) { + return; + } + + m_cursor.invalid = false; + spaMetaCursor->bitmap_offset = sizeof(struct spa_meta_cursor); + + const QSize targetSize = (cursor->rect().size() * scale).toSize(); + + struct spa_meta_bitmap *spaMetaBitmap = SPA_MEMBER(spaMetaCursor, + spaMetaCursor->bitmap_offset, + struct spa_meta_bitmap); + spaMetaBitmap->format = SPA_VIDEO_FORMAT_RGBA; + spaMetaBitmap->offset = sizeof(struct spa_meta_bitmap); + spaMetaBitmap->size.width = std::min(m_cursor.bitmapSize.width(), targetSize.width()); + spaMetaBitmap->size.height = std::min(m_cursor.bitmapSize.height(), targetSize.height()); + spaMetaBitmap->stride = spaMetaBitmap->size.width * 4; + + uint8_t *bitmap_data = SPA_MEMBER(spaMetaBitmap, spaMetaBitmap->offset, uint8_t); + QImage dest(bitmap_data, + spaMetaBitmap->size.width, + spaMetaBitmap->size.height, + spaMetaBitmap->stride, + QImage::Format_RGBA8888_Premultiplied); + dest.fill(Qt::transparent); + + const QImage image = kwinApp()->cursorImage().image(); + if (!image.isNull()) { + QPainter painter(&dest); + painter.drawImage(Rect({0, 0}, targetSize), image); + } +} + +void ScreenCastStream::setCursorMode(ScreencastV1Interface::CursorMode mode) +{ + m_cursor.mode = mode; +} + +std::optional ScreenCastStream::testCreateDmaBuf(const QSize &size, quint32 format, const QList &modifiers) +{ + EglBackend *backend = qobject_cast(Compositor::self()->backend()); + if (!backend) { + return std::nullopt; + } + + GraphicsBuffer *buffer = backend->drmDevice()->allocator()->allocate(GraphicsBufferOptions{ + .size = size, + .format = format, + .modifiers = modifiers, + }); + if (!buffer) { + return std::nullopt; + } + auto drop = qScopeGuard([&buffer]() { + buffer->drop(); + }); + + const DmaBufAttributes *attrs = buffer->dmabufAttributes(); + if (!attrs) { + return std::nullopt; + } + + return ScreenCastDmaBufTextureParams{ + .planeCount = attrs->planeCount, + .width = attrs->width, + .height = attrs->height, + .format = attrs->format, + .modifier = attrs->modifier, + .supportsSyncObj = backend->drmDevice()->supportsSyncObjTimelines(), + }; +} + +} // namespace KWin + +#include "moc_screencaststream.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/screencaststream.h b/local/recipes/kde/kwin/source/src/plugins/screencast/screencaststream.h new file mode 100644 index 0000000000..9518b58a0d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/screencaststream.h @@ -0,0 +1,147 @@ +/* + SPDX-FileCopyrightText: 2018-2020 Red Hat Inc + SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez + SPDX-FileContributor: Jan Grulich + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "utils/damagejournal.h" +#include "wayland/screencast_v1.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace KWin +{ + +class Cursor; +class PipeWireCore; +class Region; +class ScreenCastBuffer; +class ScreenCastSource; + +struct ScreenCastDmaBufTextureParams +{ + int planeCount = 0; + int width = 0; + int height = 0; + uint32_t format = 0; + uint64_t modifier = 0; + bool supportsSyncObj = false; +}; + +class KWIN_EXPORT ScreenCastStream : public QObject +{ + Q_OBJECT +public: + explicit ScreenCastStream(ScreenCastSource *source, std::shared_ptr pwCore, QObject *parent); + ~ScreenCastStream(); + + enum class Content { + None, + Video = 0x1, + Cursor = 0x2, + }; + Q_FLAG(Content) + Q_DECLARE_FLAGS(Contents, Content) + + bool init(); + uint framerate(); + uint nodeId(); + QString error() const + { + return m_error; + } + + void close(); + + void scheduleRecord(Contents contents = Content::Video); + + void setCursorMode(ScreencastV1Interface::CursorMode mode); + +public Q_SLOTS: + void invalidateCursor(); + +Q_SIGNALS: + void ready(quint32 nodeId); + void closed(); + +private: + void onStreamParamChanged(uint32_t id, const struct spa_pod *format); + void onStreamStateChanged(pw_stream_state old, pw_stream_state state, const char *error_message); + void onStreamAddBuffer(pw_buffer *buffer); + void onStreamRemoveBuffer(pw_buffer *buffer); + + bool createStream(); + QList buildFormats(bool fixate, char buffer[2048]); + void updateParams(); + void resize(const QSize &resolution); + void coreFailed(const QString &errorMessage); + void addCursorMetadata(spa_buffer *spaBuffer, Cursor *cursor); + void addHeader(spa_buffer *spaBuffer); + void corruptHeader(spa_buffer *spaBuffer); + void addDamage(spa_buffer *spaBuffer, const Region &damagedRegion); + void newStreamParams(); + spa_pod *buildFormat(struct spa_pod_builder *b, enum spa_video_format format, struct spa_rectangle *resolution, + struct spa_fraction *defaultFramerate, struct spa_fraction *minFramerate, struct spa_fraction *maxFramerate, + const QList &modifiers, quint32 modifiersFlags); + pw_buffer *dequeueBuffer(); + void record(Contents contents); + void bumpBufferAge(ScreenCastBuffer *renderedBuffer); + + std::optional testCreateDmaBuf(const QSize &size, quint32 format, const QList &modifiers); + + std::shared_ptr m_pwCore; + std::unique_ptr m_source; + struct pw_stream *m_pwStream = nullptr; + spa_hook m_streamListener; + pw_stream_events m_pwStreamEvents = {}; + + uint32_t m_pwNodeId = 0; + + QSize m_resolution; + bool m_closed = false; + + spa_video_info_raw m_videoFormat; + QString m_error; + QList m_modifiers; + std::optional m_dmabufParams; // when fixated + + struct + { + ScreencastV1Interface::CursorMode mode = ScreencastV1Interface::Hidden; + const QSize bitmapSize = QSize(256, 256); + bool visible = false; + bool invalid = true; + QMetaObject::Connection changedConnection = QMetaObject::Connection(); + QMetaObject::Connection positionChangedConnection = QMetaObject::Connection(); + } m_cursor; + + quint64 m_sequential = 0; + bool m_hasDmaBuf = false; + quint32 m_drmFormat = 0; + + std::optional m_lastSent; + QTimer m_pendingFrame; + Contents m_pendingContents = Content::None; + QList m_dequeuedBuffers; + + QList m_allBuffers; + DamageJournal m_damageJournal; +}; + +} // namespace KWin + +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::ScreenCastStream::Contents) diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/screencastutils.h b/local/recipes/kde/kwin/source/src/plugins/screencast/screencastutils.h new file mode 100644 index 0000000000..0d83508d83 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/screencastutils.h @@ -0,0 +1,111 @@ +/* + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "opengl/glplatform.h" +#include "opengl/gltexture.h" +#include "opengl/glutils.h" + +namespace KWin +{ + +// in-place vertical mirroring +static void mirrorVertically(uchar *data, int height, int stride) +{ + const int halfHeight = height / 2; + std::vector temp(stride); + for (int y = 0; y < halfHeight; ++y) { + auto cur = &data[y * stride], dest = &data[(height - y - 1) * stride]; + memcpy(temp.data(), cur, stride); + memcpy(cur, dest, stride); + memcpy(dest, temp.data(), stride); + } +} + +static GLenum closestGLType(QImage::Format format) +{ + switch (format) { + case QImage::Format_ARGB32: + case QImage::Format_ARGB32_Premultiplied: + case QImage::Format_RGB32: + return GL_BGRA; + default: + qDebug() << "unknown format" << format; + return GL_RGBA; + } +} + +static void doGrabTexture(GLTexture *texture, QImage *target) +{ + if (texture->size() != target->size()) { + return; + } + + const auto context = EglContext::currentContext(); + const QSize size = texture->size(); + const bool invertNeeded = context->isOpenGLES() ^ (texture->contentTransform() != OutputTransform::FlipY); + const bool invertNeededAndSupported = invertNeeded && context->supportsPackInvert(); + GLboolean prev; + if (invertNeededAndSupported) { + glGetBooleanv(GL_PACK_INVERT_MESA, &prev); + glPixelStorei(GL_PACK_INVERT_MESA, GL_TRUE); + } + + texture->bind(); + // BUG: The nvidia driver fails to glGetTexImage + // Drop driver() == DriverNVidia some time after that's fixed + if (context->isOpenGLES() || context->glPlatform()->driver() == Driver_NVidia) { + GLFramebuffer fbo(texture); + GLFramebuffer::pushFramebuffer(&fbo); + context->glReadnPixels(0, 0, size.width(), size.height(), closestGLType(target->format()), GL_UNSIGNED_BYTE, target->sizeInBytes(), target->bits()); + GLFramebuffer::popFramebuffer(); + } else { + context->glGetnTexImage(texture->target(), 0, closestGLType(target->format()), GL_UNSIGNED_BYTE, target->sizeInBytes(), target->bits()); + } + + if (invertNeededAndSupported) { + if (!prev) { + glPixelStorei(GL_PACK_INVERT_MESA, prev); + } + } else if (invertNeeded) { + mirrorVertically(static_cast(target->bits()), size.height(), target->bytesPerLine()); + } +} + +static void grabTexture(GLTexture *texture, QImage *target) +{ + const OutputTransform contentTransform = texture->contentTransform(); + if (contentTransform == OutputTransform::Normal || contentTransform == OutputTransform::FlipY) { + doGrabTexture(texture, target); + } else { + const QSize size = contentTransform.map(texture->size()); + const auto backingTexture = GLTexture::allocate(GL_RGBA8, size); + if (!backingTexture) { + return; + } + backingTexture->setContentTransform(OutputTransform::FlipY); + + ShaderBinder shaderBinder(ShaderTrait::MapTexture); + QMatrix4x4 projectionMatrix; + projectionMatrix.scale(1, -1); + projectionMatrix.ortho(QRect(QPoint(), size)); + shaderBinder.shader()->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, projectionMatrix); + + GLFramebuffer fbo(backingTexture.get()); + GLFramebuffer::pushFramebuffer(&fbo); + texture->render(size); + GLFramebuffer::popFramebuffer(); + doGrabTexture(backingTexture.get(), target); + } +} + +static inline Region scaleRegion(const Region ®ion, qreal scale, const Rect &bounds) +{ + return region.scaledAndRoundedOut(scale) & bounds; +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/windowscreencastsource.cpp b/local/recipes/kde/kwin/source/src/plugins/screencast/windowscreencastsource.cpp new file mode 100644 index 0000000000..59708970ea --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/windowscreencastsource.cpp @@ -0,0 +1,215 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + SPDX-FileCopyrightText: 2025 David Redondo + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "windowscreencastsource.h" +#include "screencastutils.h" + +#include "compositor.h" +#include "core/backendoutput.h" +#include "core/renderloop.h" +#include "core/rendertarget.h" +#include "core/renderviewport.h" +#include "effect/effect.h" +#include "input.h" +#include "opengl/glframebuffer.h" +#include "opengl/gltexture.h" +#include "scene/itemrenderer.h" +#include "scene/windowitem.h" +#include "scene/workspacescene.h" +#include "workspace.h" + +#include + +namespace KWin +{ + +WindowScreenCastSource::WindowScreenCastSource(Window *window) + : ScreenCastSource() +{ + add(window); + + connect(workspace(), &Workspace::windowAdded, this, [this](Window *window) { + if (window->isPopupWindow() && m_windows.contains(window->transientFor())) { + add(window); + if (m_active) { + Q_EMIT frame(); + } + } + }); + connect(Compositor::self(), &Compositor::aboutToToggleCompositing, this, [this]() { + Q_EMIT closed(); + }); +} + +WindowScreenCastSource::~WindowScreenCastSource() +{ + pause(); +} + +void WindowScreenCastSource::add(Window *window) +{ + m_windows.push_back(window); + + connect(window, &Window::closed, this, [this, window] { + m_windows.removeOne(window); + if (m_active) { + unwatch(window); + Q_EMIT frame(); + } + if (m_windows.empty()) { + Q_EMIT closed(); + } + }); + + if (m_active) { + watch(window); + } + + for (const auto child : window->transients()) { + if (child->isPopupWindow()) { + add(child); + } + } +} + +void WindowScreenCastSource::watch(Window *window) +{ + window->refOffscreenRendering(); + connect(window, &Window::damaged, this, &WindowScreenCastSource::frame); +} + +void WindowScreenCastSource::unwatch(Window *window) +{ + window->unrefOffscreenRendering(); + disconnect(window, &Window::damaged, this, &WindowScreenCastSource::frame); +} + +quint32 WindowScreenCastSource::drmFormat() const +{ + return DRM_FORMAT_ARGB8888; +} + +QSize WindowScreenCastSource::textureSize() const +{ + return (boundingRect().size() * m_windows[0]->targetScale()).toSize(); +} + +qreal WindowScreenCastSource::devicePixelRatio() const +{ + return m_windows[0]->targetScale(); +} + +void WindowScreenCastSource::setRenderCursor(bool enable) +{ + m_renderCursor = enable; +} + +Region WindowScreenCastSource::render(QImage *target, const Region &bufferDamage) +{ + const auto offscreenTexture = GLTexture::allocate(GL_RGBA8, target->size()); + if (!offscreenTexture) { + return Region{}; + } + offscreenTexture->setContentTransform(OutputTransform::FlipY); + + GLFramebuffer offscreenTarget(offscreenTexture.get()); + render(&offscreenTarget, Region::infinite()); + grabTexture(offscreenTexture.get(), target); + return Rect(QPoint(), target->size()); +} + +Region WindowScreenCastSource::render(GLFramebuffer *target, const Region &bufferDamage) +{ + RenderTarget renderTarget(target); + RenderViewport viewport(boundingRect(), devicePixelRatio(), renderTarget, QPoint()); + + WorkspaceScene *scene = Compositor::self()->scene(); + + scene->renderer()->beginFrame(renderTarget, viewport); + glClearColor(0.0, 0.0, 0.0, 0.0); + glClear(GL_COLOR_BUFFER_BIT); + for (const auto &window : m_windows) { + scene->renderer()->renderItem(renderTarget, viewport, window->windowItem(), Scene::PAINT_WINDOW_TRANSFORMED, Region::infinite(), WindowPaintData{}, {}, {}); + } + if (m_renderCursor && scene->cursorItem()->isVisible()) { + scene->renderer()->renderItem(renderTarget, viewport, scene->cursorItem(), 0, Region::infinite(), WindowPaintData{}, {}, {}); + } + scene->renderer()->endFrame(); + return Rect(QPoint(), target->size()); +} + +std::chrono::nanoseconds WindowScreenCastSource::clock() const +{ + return m_windows[0]->output()->backendOutput()->renderLoop()->lastPresentationTimestamp(); +} + +uint WindowScreenCastSource::refreshRate() const +{ + return m_windows[0]->output()->refreshRate(); +} + +void WindowScreenCastSource::pause() +{ + if (!m_active) { + return; + } + + for (const auto &window : std::as_const(m_windows)) { + unwatch(window); + } + m_active = false; +} + +void WindowScreenCastSource::resume() +{ + if (m_active) { + return; + } + + for (const auto &window : std::as_const(m_windows)) { + watch(window); + } + Q_EMIT frame(); + + m_active = true; +} + +bool WindowScreenCastSource::includesCursor(Cursor *cursor) const +{ + if (Cursors::self()->isCursorHidden()) { + return false; + } + + if (!boundingRect().intersects(cursor->geometry())) { + return false; + } + + return m_windows.contains(input()->findToplevel(cursor->pos())); +} + +QPointF WindowScreenCastSource::mapFromGlobal(const QPointF &point) const +{ + return point - boundingRect().topLeft(); +} + +RectF WindowScreenCastSource::mapFromGlobal(const RectF &rect) const +{ + return rect.translated(-boundingRect().topLeft()); +} + +RectF WindowScreenCastSource::boundingRect() const +{ + RectF boundingRect; + for (const auto &window : m_windows) { + boundingRect = boundingRect.united(window->frameGeometry()); + } + return boundingRect; +} + +} // namespace KWin + +#include "moc_windowscreencastsource.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/screencast/windowscreencastsource.h b/local/recipes/kde/kwin/source/src/plugins/screencast/windowscreencastsource.h new file mode 100644 index 0000000000..80edd86ac8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screencast/windowscreencastsource.h @@ -0,0 +1,55 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "screencastsource.h" +#include "window.h" + +#include +#include + +namespace KWin +{ + +class WindowScreenCastSource : public ScreenCastSource +{ + Q_OBJECT + +public: + explicit WindowScreenCastSource(Window *window); + ~WindowScreenCastSource() override; + + quint32 drmFormat() const override; + QSize textureSize() const override; + qreal devicePixelRatio() const override; + uint refreshRate() const override; + + void setRenderCursor(bool enable) override; + Region render(GLFramebuffer *target, const Region &bufferDamage) override; + Region render(QImage *target, const Region &bufferDamage) override; + std::chrono::nanoseconds clock() const override; + + void resume() override; + void pause() override; + + bool includesCursor(Cursor *cursor) const override; + + QPointF mapFromGlobal(const QPointF &point) const override; + RectF mapFromGlobal(const RectF &rect) const override; + +private: + void add(Window *window); + void watch(Window *window); + void unwatch(Window *window); + RectF boundingRect() const; + + QList m_windows; + bool m_active = false; + bool m_renderCursor = false; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/screenedge/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/screenedge/CMakeLists.txt new file mode 100644 index 0000000000..91880e65c7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screenedge/CMakeLists.txt @@ -0,0 +1,15 @@ +####################################### +# Effect + +# Source files +set(screenedge_SOURCES + main.cpp + screenedgeeffect.cpp +) + +kwin_add_builtin_effect(screenedge ${screenedge_SOURCES}) +target_link_libraries(screenedge PRIVATE + kwin + + KF6::ConfigCore +) diff --git a/local/recipes/kde/kwin/source/src/plugins/screenedge/main.cpp b/local/recipes/kde/kwin/source/src/plugins/screenedge/main.cpp new file mode 100644 index 0000000000..ab453d907a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screenedge/main.cpp @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "screenedgeeffect.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY(ScreenEdgeEffect, + "metadata.json.stripped") + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/screenedge/metadata.json b/local/recipes/kde/kwin/source/src/plugins/screenedge/metadata.json new file mode 100644 index 0000000000..ea3440e799 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screenedge/metadata.json @@ -0,0 +1,86 @@ +{ + "KPlugin": { + "Category": "Appearance", + "Description": "Highlights screen edges and corners that trigger actions when touched as the cursor approaches them", + "Description[ar]": "يبرز حواف الشاشة والزوايا الساخنة التي ستحدث إجراءات عند لمسها عند اقتراب المؤشر منها", + "Description[bg]": "Маркира краищата и ъглите на екрана,които задействат действия при докосване,когато курсорът ги приближи", + "Description[ca@valencia]": "Ressalta les vores de la pantalla i els cantons que activen accions quan es toquen si el cursor s'hi aproxima", + "Description[ca]": "Ressalta les vores de la pantalla i les cantonades que activen accions quan es toquen si el cursor s'hi aproxima", + "Description[da]": "Fremhæver skærmkanter og -hjørner, der udløser handlinger ved berøring, når musen nærmer sig.", + "Description[de]": "Hervorheben von Bildschirmrändern und -ecken, die bei Berührung Aktionen auslösen, wenn der Zeiger sich ihnen nähert", + "Description[en_GB]": "Highlights screen edges and corners that trigger actions when touched as the cursor approaches them", + "Description[eo]": "Elstarigas ekranajn randojn kaj angulojn, kiuj ekigas agojn kiam tuŝitaj dum kiam la kursoro alproksimiĝas al ili", + "Description[es]": "Resalta los bordes y las esquinas de la pantalla que desencadenan acciones cuando se tocan al aproximar el cursor", + "Description[eu]": "Pantailaren ertzak eta kurtsorea hurbiltzean ukitzen dituenean ekitzak eragiten dituzten izkinak nabarmentzen ditu", + "Description[fi]": "Korostaa näytön reunoja ja nurkkia, jotka käynnistävät toiminnon osoittimen lähestyessä niitä", + "Description[fr]": "Mettre en valeur les bords des écrans et les coins actifs qui déclenchent des actions lorsqu'ils sont touchés lorsque le pointeur s'approche d'eux", + "Description[gl]": "Cando se achegue o cursor, realzar os bordos da pantalla e as esquinas que disparan accións ao tocalas.", + "Description[he]": "הדגשת קצוות המסך שמזניקות פעולות כשנוגעים בהן כשהסמן מגיע אליהן", + "Description[hu]": "Kiemeli a képernyő azon éleit és sarkait, amelyek elindítanak egy műveletet, ha a kurzor megközelíti azokat", + "Description[ia]": "Evidentia margines e angulos de schermo que discatena actiones quando toccate quando le cursor se apporixima lor", + "Description[is]": "Lýsir upp skjájaðra og horn sem virkja aðgerðir þegar þau eru snert um leið", + "Description[it]": "Evidenzia i bordi e gli angoli dello schermo che attivano le azioni quando vengono toccati mentre il cursore si avvicina ad essi", + "Description[ja]": "画面の端や角にカーソルが近づくと、タッチしたときに発動するアクションをハイライトする", + "Description[ka]": "გამოკვეთს ეკრანის წიბოებს და კუთხეებს, რომლებიც ატრიგერებენ ქმედებას შეხებისას, როცა მათ კურსორი უახლოვდება", + "Description[ko]": "커서가 화면 경계와 모서리에 다가갈 때 동작을 트리거하는 경우 강조 표시", + "Description[lt]": "Artinant žymeklį paryškina ekrano kraštus ir kampus, kurie sukelia veiksmus", + "Description[lv]": "Tuvinot kursoru, izceļ ekrāna malas un stūrus, kas izsauc darbības.", + "Description[nb]": "Fremhever når pekeren kommer nær skjermkanter og hjørne som utløser handlinger ved kontakt", + "Description[nl]": "Accentueert schermranden en hoeken die acties starten bij aanraken als de cursor ze benaderd", + "Description[nn]": "Framhevar når peikaren kjem nær skjermkantar og hjørne som utløyser handlingar ved kontakt", + "Description[pl]": "Podświetla krawędzie ekranu oraz gorące narożniki, które wyzwalają działania, gdy dotknięte wskaźnikiem myszy", + "Description[pt_BR]": "Destaca as bordas e cantos da tela que acionam ações quando tocados conforme o cursor se aproxima deles", + "Description[ro]": "Evidențiază marginile și colțurile ecranului care declanșează acțiuni când sunt atinse la apropierea cursorului", + "Description[ru]": "Выделение краёв и углов экрана, которые активируют действия, когда их касается подведённый курсор", + "Description[sa]": "कर्सरस्य समीपं गच्छन् स्पृष्टे सति क्रियाः प्रवर्तयन्ति इति स्क्रीन-धाराः कोणाः च प्रकाशयति", + "Description[sk]": "Zvýrazní okraje a rohy obrazovky, ktoré spúšťajú akcie pri dotyku, keď sa k nim priblíži kurzor", + "Description[sl]": "Osvetli robove in vogale zaslona, ​​ki ob dotiku sprožijo dejanja, ko se jim približa kazalka", + "Description[sv]": "Markerar skärmkanter och hörn som utlöser åtgärder om de berörs när markören närmar sig", + "Description[tr]": "İmleç onlara yaklaştıkça dokunulduğunda eylemleri tetikleyen ekran kenarlarını ve köşeleri vurgular", + "Description[uk]": "Підсвічує краї та кути екрана, які запускають дії при торканні, якщо вказівник наближається до них", + "Description[zh_CN]": "光标靠近触碰时会触发操作的屏幕边缘和四角时高亮显示它们", + "Description[zh_TW]": "游標接近時突顯螢幕邊緣或有觸發動作的角落", + "EnabledByDefault": true, + "License": "GPL", + "Name": "Highlight Screen Edges and Hot Corners", + "Name[ar]": "إبراز حواف الشاشة والزوايا الساخنة", + "Name[bg]": "Осветява края на екрана при приближаване и активните ъгли", + "Name[ca@valencia]": "Ressalta les vores de la pantalla i els cantons actius", + "Name[ca]": "Ressalta les vores de la pantalla i les cantonades actives", + "Name[cs]": "Zvýraznit okraje obrazovky a aktivní rohy", + "Name[da]": "Fremhæver skærmkanter og varme hjørner", + "Name[de]": "Bildschirmränder und aktive Ecken", + "Name[en_GB]": "Highlight Screen Edges and Hot Corners", + "Name[eo]": "Elstarigi Ekranrandojn kaj Ag-Angulojn", + "Name[es]": "Resaltar los bordes y las esquinas activas de la pantalla", + "Name[eu]": "Nabarmendu pantailaren ertzak eta izkina beroak", + "Name[fi]": "Korostaa näytön ”kuumat” reunat ja nurkat niitä lähestyttäessä", + "Name[fr]": "Mettre en valeur les bords des écrans et les coins actifs", + "Name[gl]": "Realzar os bordos da pantalla e as esquinas con accións", + "Name[he]": "הדגשת קצוות המסך והפינות החמות", + "Name[hu]": "Képernyőszélek és sarkok kiemelése", + "Name[ia]": "Evidentia un margines de schermo e angulos calide", + "Name[is]": "Lýsa upp skjájaðra og virk horn", + "Name[it]": "Evidenzia i bordi dello schermo e gli angoli attivi", + "Name[ja]": "画面の端とホットコーナーを強調表示する", + "Name[ka]": "ეკრანის წიბოებისა და აქტიური კუთხეები გამოკვეთა", + "Name[ko]": "화면 경계와 선택된 모서리 강조", + "Name[lt]": "Paryškinti ekrano kraštus ir karštuosius kampus", + "Name[lv]": "Izcelt ekrāna malas un aktīvos stūrus", + "Name[nb]": "Fremhev skjermkanter og handlingsutløsende hjørner", + "Name[nl]": "Accentueert schermranden en actieve hoeken", + "Name[nn]": "Framhev skjermkantar og handlingsutløysande hjørne", + "Name[pl]": "Podświetla krawędzie ekranu oraz gorące narożniki", + "Name[pt_BR]": "Destacar bordas da tela e cantos ativos", + "Name[ro]": "Evidențiază muchiile ecranului și colțurile fierbinți", + "Name[ru]": "Выделение краёв и углов экрана", + "Name[sa]": "स्क्रीन एज्स् तथा हॉट् कॉर्नर् हाइलाइट् कुर्वन्तु", + "Name[sk]": "Zvýrazní okraj obrazovky pri priblížení", + "Name[sl]": "Poudari robove zaslona in vroče vogale", + "Name[sv]": "Markerar skärmkanter och aktiva hörn", + "Name[tr]": "Ekran Kenarlarını ve Sıcak Köşeleri Vurgula", + "Name[uk]": "Підсвічування країв і активних кутів екрана", + "Name[zh_CN]": "高亮显示屏幕边缘和四角", + "Name[zh_TW]": "突顯螢幕邊緣和角落" + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/screenedge/screenedgeeffect.cpp b/local/recipes/kde/kwin/source/src/plugins/screenedge/screenedgeeffect.cpp new file mode 100644 index 0000000000..f035d5a869 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screenedge/screenedgeeffect.cpp @@ -0,0 +1,213 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "screenedgeeffect.h" +// KWin +#include "effect/effecthandler.h" +#include "scene/imageitem.h" +#include "scene/itemrenderer.h" +#include "scene/workspacescene.h" +// KDE +#include +#include +#include +// Qt +#include +#include +#include + +namespace KWin +{ + +ScreenEdgeEffect::ScreenEdgeEffect() + : Effect() + , m_cleanupTimer(new QTimer(this)) +{ + connect(effects, &EffectsHandler::screenEdgeApproaching, this, &ScreenEdgeEffect::edgeApproaching); + m_cleanupTimer->setInterval(5000); + m_cleanupTimer->setSingleShot(true); + connect(m_cleanupTimer, &QTimer::timeout, this, &ScreenEdgeEffect::cleanup); + connect(effects, &EffectsHandler::screenLockingChanged, this, [this](bool locked) { + if (locked) { + cleanup(); + } + }); +} + +ScreenEdgeEffect::~ScreenEdgeEffect() +{ + cleanup(); +} + +void ScreenEdgeEffect::ensureGlowSvg() +{ + if (!m_glow) { + m_glow = new KSvg::Svg(this); + m_glow->imageSet()->setBasePath(QStringLiteral("plasma/desktoptheme")); + + const QString groupName = QStringLiteral("Theme"); + KSharedConfig::Ptr config = KSharedConfig::openConfig(QStringLiteral("plasmarc")); + KConfigGroup cg = KConfigGroup(config, groupName); + m_glow->imageSet()->setImageSetName(cg.readEntry("name", QStringLiteral("default"))); + + m_configWatcher = KConfigWatcher::create(config); + + connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) { + if (group.name() != QStringLiteral("Theme") || !names.contains(QStringLiteral("name"))) { + return; + } + m_glow->imageSet()->setImageSetName(group.readEntry("name", QStringLiteral("default"))); + }); + + m_glow->setImagePath(QStringLiteral("widgets/glowbar")); + } +} + +void ScreenEdgeEffect::cleanup() +{ + m_glowItems.clear(); +} + +void ScreenEdgeEffect::edgeApproaching(ElectricBorder border, qreal factor, const QRect &geometry) +{ + auto it = m_glowItems.find(border); + if (it != m_glowItems.end()) { + ImageItem *glowItem = it->second.get(); + glowItem->setOpacity(factor); + if (glowItem->position() != geometry.topLeft() || glowItem->size() != geometry.size()) { + glowItem->setImage(glowImage(border, geometry.size())); + glowItem->setPosition(geometry.topLeft()); + glowItem->setSize(geometry.size()); + } + if (factor == 0.0) { + m_cleanupTimer->start(); + } else { + m_cleanupTimer->stop(); + } + } else if (factor != 0.0) { + std::unique_ptr glow = createGlowItem(border, factor, geometry); + if (glow) { + m_glowItems[border] = std::move(glow); + } + } +} + +std::unique_ptr ScreenEdgeEffect::createGlowItem(ElectricBorder border, qreal factor, const QRect &geometry) +{ + const QImage image = glowImage(border, geometry.size()); + if (image.isNull()) { + return nullptr; + } + + WorkspaceScene *scene = effects->scene(); + + std::unique_ptr imageItem = scene->renderer()->createImageItem(scene->overlayItem()); + imageItem->setImage(image); + imageItem->setPosition(geometry.topLeft()); + imageItem->setSize(geometry.size()); + imageItem->setOpacity(factor); + + return imageItem; +} + +QImage ScreenEdgeEffect::glowImage(ElectricBorder border, const QSize &size) +{ + if (border == ElectricLeft || border == ElectricRight || border == ElectricTop || border == ElectricBottom) { + return edgeGlowImage(border, size); + } else { + return cornerGlowImage(border); + } +} + +QImage ScreenEdgeEffect::cornerGlowImage(ElectricBorder border) +{ + ensureGlowSvg(); + + switch (border) { + case ElectricTopLeft: + return m_glow->pixmap(QStringLiteral("bottomright")).toImage(); + case ElectricTopRight: + return m_glow->pixmap(QStringLiteral("bottomleft")).toImage(); + case ElectricBottomRight: + return m_glow->pixmap(QStringLiteral("topleft")).toImage(); + case ElectricBottomLeft: + return m_glow->pixmap(QStringLiteral("topright")).toImage(); + default: + return QImage{}; + } +} + +QImage ScreenEdgeEffect::edgeGlowImage(ElectricBorder border, const QSize &size) +{ + ensureGlowSvg(); + + const bool stretchBorder = m_glow->hasElement(QStringLiteral("hint-stretch-borders")); + + QPoint pixmapPosition(0, 0); + QPixmap l, r, c; + switch (border) { + case ElectricTop: + l = m_glow->pixmap(QStringLiteral("bottomleft")); + r = m_glow->pixmap(QStringLiteral("bottomright")); + c = m_glow->pixmap(QStringLiteral("bottom")); + break; + case ElectricBottom: + l = m_glow->pixmap(QStringLiteral("topleft")); + r = m_glow->pixmap(QStringLiteral("topright")); + c = m_glow->pixmap(QStringLiteral("top")); + pixmapPosition = QPoint(0, size.height() - c.height()); + break; + case ElectricLeft: + l = m_glow->pixmap(QStringLiteral("topright")); + r = m_glow->pixmap(QStringLiteral("bottomright")); + c = m_glow->pixmap(QStringLiteral("right")); + break; + case ElectricRight: + l = m_glow->pixmap(QStringLiteral("topleft")); + r = m_glow->pixmap(QStringLiteral("bottomleft")); + c = m_glow->pixmap(QStringLiteral("left")); + pixmapPosition = QPoint(size.width() - c.width(), 0); + break; + default: + return QImage{}; + } + QPixmap image(size); + image.fill(Qt::transparent); + QPainter p; + p.begin(&image); + if (border == ElectricBottom || border == ElectricTop) { + p.drawPixmap(pixmapPosition, l); + const QRect cRect(l.width(), pixmapPosition.y(), size.width() - l.width() - r.width(), c.height()); + if (stretchBorder) { + p.drawPixmap(cRect, c); + } else { + p.drawTiledPixmap(cRect, c); + } + p.drawPixmap(QPoint(size.width() - r.width(), pixmapPosition.y()), r); + } else { + p.drawPixmap(pixmapPosition, l); + const QRect cRect(pixmapPosition.x(), l.height(), c.width(), size.height() - l.height() - r.height()); + if (stretchBorder) { + p.drawPixmap(cRect, c); + } else { + p.drawTiledPixmap(cRect, c); + } + p.drawPixmap(QPoint(pixmapPosition.x(), size.height() - r.height()), r); + } + p.end(); + return image.toImage(); +} + +bool ScreenEdgeEffect::isActive() const +{ + return !m_glowItems.empty() && !effects->isScreenLocked(); +} + +} // namespace + +#include "moc_screenedgeeffect.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/screenedge/screenedgeeffect.h b/local/recipes/kde/kwin/source/src/plugins/screenedge/screenedgeeffect.h new file mode 100644 index 0000000000..09b538c4ff --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screenedge/screenedgeeffect.h @@ -0,0 +1,55 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "effect/effect.h" + +#include + +class QTimer; +namespace KSvg +{ +class Svg; +} + +namespace KWin +{ + +class ImageItem; + +class ScreenEdgeEffect : public Effect +{ + Q_OBJECT +public: + ScreenEdgeEffect(); + ~ScreenEdgeEffect() override; + bool isActive() const override; + + int requestedEffectChainPosition() const override + { + return 10; + } + +private Q_SLOTS: + void edgeApproaching(ElectricBorder border, qreal factor, const QRect &geometry); + void cleanup(); + +private: + void ensureGlowSvg(); + QImage cornerGlowImage(ElectricBorder border); + QImage edgeGlowImage(ElectricBorder border, const QSize &size); + QImage glowImage(ElectricBorder border, const QSize &size); + std::unique_ptr createGlowItem(ElectricBorder border, qreal factor, const QRect &geometry); + + KConfigWatcher::Ptr m_configWatcher; + KSvg::Svg *m_glow = nullptr; + std::unordered_map> m_glowItems; + QTimer *m_cleanupTimer; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/screenshot/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/screenshot/CMakeLists.txt new file mode 100644 index 0000000000..eeda807fa1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screenshot/CMakeLists.txt @@ -0,0 +1,25 @@ +set(screenshot_SOURCES + main.cpp + screenshot.cpp + screenshotdbusinterface2.cpp + screenshotlayer.cpp +) + +qt_add_dbus_adaptor(screenshot_SOURCES org.kde.KWin.ScreenShot2.xml screenshotdbusinterface2.h KWin::ScreenShotDBusInterface2) + +kcoreaddons_add_plugin(screenshot SOURCES ${screenshot_SOURCES} INSTALL_NAMESPACE "kwin/plugins") +target_link_libraries(screenshot PRIVATE + kwin + + KF6::Service + KF6::I18n + + Qt::DBus +) + +ecm_qt_declare_logging_category(screenshot + HEADER screenshotlogging.h + IDENTIFIER KWIN_SCREENSHOT + CATEGORY_NAME kwin_screenshot + DEFAULT_SEVERITY Warning +) diff --git a/local/recipes/kde/kwin/source/src/plugins/screenshot/main.cpp b/local/recipes/kde/kwin/source/src/plugins/screenshot/main.cpp new file mode 100644 index 0000000000..adb824d805 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screenshot/main.cpp @@ -0,0 +1,32 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "screenshot.h" + +#include + +namespace KWin +{ + +class KWIN_EXPORT ScreenshotManagerFactory : public PluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID PluginFactory_iid FILE "metadata.json") + Q_INTERFACES(KWin::PluginFactory) +public: + explicit ScreenshotManagerFactory() = default; + + std::unique_ptr create() const override; +}; + +std::unique_ptr ScreenshotManagerFactory::create() const +{ + return std::make_unique(); +} + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/screenshot/metadata.json b/local/recipes/kde/kwin/source/src/plugins/screenshot/metadata.json new file mode 100644 index 0000000000..aa304f4093 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screenshot/metadata.json @@ -0,0 +1,5 @@ +{ + "KPlugin": { + "EnabledByDefault": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/screenshot/org.kde.KWin.ScreenShot2.xml b/local/recipes/kde/kwin/source/src/plugins/screenshot/org.kde.KWin.ScreenShot2.xml new file mode 100644 index 0000000000..39ee14573d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screenshot/org.kde.KWin.ScreenShot2.xml @@ -0,0 +1,373 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/screenshot/screenshot.cpp b/local/recipes/kde/kwin/source/src/plugins/screenshot/screenshot.cpp new file mode 100644 index 0000000000..60b88b7d5e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screenshot/screenshot.cpp @@ -0,0 +1,280 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Martin Gräßlin + SPDX-FileCopyrightText: 2010 Nokia Corporation and /or its subsidiary(-ies) + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "screenshot.h" +#include "screenshotdbusinterface2.h" + +#include "compositor.h" +#include "core/output.h" +#include "core/pixelgrid.h" +#include "core/rendertarget.h" +#include "core/renderviewport.h" +#include "effect/effect.h" +#include "opengl/eglbackend.h" +#include "opengl/glplatform.h" +#include "opengl/glutils.h" +#include "scene/decorationitem.h" +#include "scene/item.h" +#include "scene/itemrenderer.h" +#include "scene/shadowitem.h" +#include "scene/surfaceitem.h" +#include "scene/windowitem.h" +#include "scene/workspacescene.h" +#include "screenshotlayer.h" +#include "window.h" +#include "workspace.h" + +#include + +namespace KWin +{ + +static void convertFromGLImage(QImage &img, int w, int h, const OutputTransform &renderTargetTransformation) +{ + // from QtOpenGL/qgl.cpp + // SPDX-FileCopyrightText: 2010 Nokia Corporation and /or its subsidiary(-ies) + // see https://github.com/qt/qtbase/blob/dev/src/opengl/qgl.cpp + if (QSysInfo::ByteOrder == QSysInfo::BigEndian) { + // OpenGL gives RGBA; Qt wants ARGB + uint *p = reinterpret_cast(img.bits()); + uint *end = p + w * h; + while (p < end) { + uint a = *p << 24; + *p = (*p >> 8) | a; + p++; + } + } else { + // OpenGL gives ABGR (i.e. RGBA backwards); Qt wants ARGB + for (int y = 0; y < h; y++) { + uint *q = reinterpret_cast(img.scanLine(y)); + for (int x = 0; x < w; ++x) { + const uint pixel = *q; + *q = ((pixel << 16) & 0xff0000) | ((pixel >> 16) & 0xff) + | (pixel & 0xff00ff00); + + q++; + } + } + } + + QMatrix4x4 matrix; + // apply render target transformation + matrix *= renderTargetTransformation.inverted().toMatrix(); + // OpenGL textures are flipped vs QImage + matrix.scale(1, -1); + img = img.transformed(matrix.toTransform()); +} + +ScreenShotManager::ScreenShotManager() + : m_dbusInterface2(new ScreenShotDBusInterface2(this)) +{ +} + +ScreenShotManager::~ScreenShotManager() +{ +} + +// TODO share code with the screencast plugin? + +std::optional ScreenShotManager::takeScreenShot(LogicalOutput *screen, ScreenShotFlags flags, std::optional pidToHide) +{ + const auto eglBackend = dynamic_cast(Compositor::self()->backend()); + if (!eglBackend) { + return std::nullopt; + } + const auto context = eglBackend->openglContext(); + if (!context || !context->makeCurrent()) { + return std::nullopt; + } + + qreal scale = 1.0; + if (flags & ScreenShotNativeResolution) { + scale = screen->scale(); + } + const QSize nativeSize = (screen->geometryF().size() * scale).toSize(); + + const auto offscreenTexture = GLTexture::allocate(GL_RGBA8, nativeSize); + if (!offscreenTexture) { + return std::nullopt; + } + offscreenTexture->setFilter(GL_LINEAR); + offscreenTexture->setWrapMode(GL_CLAMP_TO_EDGE); + const auto target = std::make_unique(offscreenTexture.get()); + if (!target->valid()) { + return std::nullopt; + } + + ScreenshotLayer layer(screen, target.get()); + if (!layer.preparePresentationTest()) { + return std::nullopt; + } + const auto beginInfo = layer.beginFrame(); + if (!beginInfo) { + return std::nullopt; + } + SceneView sceneView(Compositor::self()->scene(), screen, nullptr, &layer); + std::unique_ptr cursorView; + if (!(flags & ScreenShotIncludeCursor)) { + cursorView = std::make_unique(&sceneView, Compositor::self()->scene()->cursorItem(), workspace()->outputs().front(), nullptr, nullptr); + cursorView->setExclusive(true); + } + if (pidToHide.has_value()) { + sceneView.addWindowFilter([pid = *pidToHide](Window *window) { + return window->pid() == pid; + }); + } + const Rect fullDamage = Rect(QPoint(), target->size()); + sceneView.setViewport(screen->geometryF()); + sceneView.setScale(scale); + sceneView.prePaint(); + sceneView.paint(beginInfo->renderTarget, QPoint(), fullDamage); + sceneView.postPaint(); + if (!layer.endFrame(fullDamage, fullDamage, nullptr)) { + return std::nullopt; + } + + GLFramebuffer::pushFramebuffer(target.get()); + QImage snapshot = QImage(offscreenTexture->size(), QImage::Format_ARGB32_Premultiplied); + context->glReadnPixels(0, 0, snapshot.width(), snapshot.height(), GL_RGBA, GL_UNSIGNED_BYTE, snapshot.sizeInBytes(), static_cast(snapshot.bits())); + convertFromGLImage(snapshot, snapshot.width(), snapshot.height(), OutputTransform::Normal); + GLFramebuffer::popFramebuffer(); + + snapshot.setDevicePixelRatio(scale); + return snapshot; +} + +std::optional ScreenShotManager::takeScreenShot(const Rect &area, ScreenShotFlags flags, std::optional pidToHide) +{ + const auto eglBackend = dynamic_cast(Compositor::self()->backend()); + if (!eglBackend) { + return std::nullopt; + } + const auto context = eglBackend->openglContext(); + if (!context || !context->makeCurrent()) { + return std::nullopt; + } + + qreal scale = 1.0; + if (flags & ScreenShotNativeResolution) { + const auto outputs = workspace()->outputs(); + for (LogicalOutput *output : outputs) { + scale = std::max(scale, output->scale()); + } + } + const QSize nativeSize = area.size() * scale; + + const auto offscreenTexture = GLTexture::allocate(GL_RGBA8, nativeSize); + if (!offscreenTexture) { + return std::nullopt; + } + offscreenTexture->setFilter(GL_LINEAR); + offscreenTexture->setWrapMode(GL_CLAMP_TO_EDGE); + const auto target = std::make_unique(offscreenTexture.get()); + if (!target->valid()) { + return std::nullopt; + } + + ScreenshotLayer layer(workspace()->outputs().front(), target.get()); + if (!layer.preparePresentationTest()) { + return std::nullopt; + } + const auto beginInfo = layer.beginFrame(); + if (!beginInfo) { + return std::nullopt; + } + SceneView sceneView(Compositor::self()->scene(), workspace()->outputs().front(), nullptr, &layer); + std::unique_ptr cursorView; + if (!(flags & ScreenShotIncludeCursor)) { + cursorView = std::make_unique(&sceneView, Compositor::self()->scene()->cursorItem(), workspace()->outputs().front(), nullptr, nullptr); + cursorView->setExclusive(true); + } + if (pidToHide.has_value()) { + sceneView.addWindowFilter([pid = *pidToHide](Window *window) { + return window->pid() == pid; + }); + } + const Rect fullDamage = Rect(QPoint(), target->size()); + sceneView.setViewport(area); + sceneView.setScale(scale); + sceneView.prePaint(); + sceneView.paint(beginInfo->renderTarget, QPoint(), fullDamage); + sceneView.postPaint(); + if (!layer.endFrame(fullDamage, fullDamage, nullptr)) { + return std::nullopt; + } + + GLFramebuffer::pushFramebuffer(target.get()); + QImage snapshot = QImage(offscreenTexture->size(), QImage::Format_ARGB32_Premultiplied); + context->glReadnPixels(0, 0, snapshot.width(), snapshot.height(), GL_RGBA, GL_UNSIGNED_BYTE, snapshot.sizeInBytes(), static_cast(snapshot.bits())); + convertFromGLImage(snapshot, snapshot.width(), snapshot.height(), OutputTransform::Normal); + GLFramebuffer::popFramebuffer(); + + snapshot.setDevicePixelRatio(scale); + return snapshot; +} + +std::optional ScreenShotManager::takeScreenShot(Window *window, ScreenShotFlags flags) +{ + const auto eglBackend = dynamic_cast(Compositor::self()->backend()); + if (!eglBackend) { + return std::nullopt; + } + const auto context = eglBackend->openglContext(); + if (!context || !context->makeCurrent()) { + return std::nullopt; + } + + const qreal scale = window->targetScale(); + RectF geometry = window->visibleGeometry(); + if (window->windowItem()->decorationItem() && !(flags & ScreenShotIncludeDecoration)) { + geometry = window->clientGeometry(); + } else if (!(flags & ScreenShotIncludeShadow)) { + geometry = window->frameGeometry(); + } + const QSize nativeSize = (geometry.size() * scale).toSize(); + const auto offscreenTexture = GLTexture::allocate(GL_RGBA8, nativeSize); + if (!offscreenTexture) { + return std::nullopt; + } + + GLFramebuffer offscreenTarget(offscreenTexture.get()); + + RenderTarget renderTarget(&offscreenTarget); + RenderViewport viewport(geometry, scale, renderTarget, QPoint()); + + WorkspaceScene *scene = Compositor::self()->scene(); + + scene->renderer()->beginFrame(renderTarget, viewport); + glClearColor(0.0, 0.0, 0.0, 0.0); + glClear(GL_COLOR_BUFFER_BIT); + scene->renderer()->renderItem(renderTarget, viewport, window->windowItem(), Scene::PAINT_WINDOW_TRANSFORMED, Region::infinite(), WindowPaintData{}, [flags, w = window->windowItem()](Item *item) { + const bool deco = flags & ScreenShotFlag::ScreenShotIncludeDecoration; + const bool shadow = deco && (flags & ScreenShotFlag::ScreenShotIncludeShadow); + return (!deco && item == w->decorationItem()) + || (!shadow && item == w->shadowItem()); + }, {}); + if ((flags & ScreenShotFlag::ScreenShotIncludeCursor) && scene->cursorItem()->isVisible()) { + scene->renderer()->renderItem(renderTarget, viewport, scene->cursorItem(), 0, Region::infinite(), WindowPaintData{}, {}, {}); + } + scene->renderer()->endFrame(); + + GLFramebuffer::pushFramebuffer(&offscreenTarget); + QImage snapshot = QImage(offscreenTexture->size(), QImage::Format_ARGB32_Premultiplied); + context->glReadnPixels(0, 0, snapshot.width(), snapshot.height(), GL_RGBA, GL_UNSIGNED_BYTE, snapshot.sizeInBytes(), static_cast(snapshot.bits())); + convertFromGLImage(snapshot, snapshot.width(), snapshot.height(), OutputTransform::Normal); + GLFramebuffer::popFramebuffer(); + + snapshot.setDevicePixelRatio(scale); + return snapshot; +} + +} // namespace KWin + +#include "moc_screenshot.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/screenshot/screenshot.h b/local/recipes/kde/kwin/source/src/plugins/screenshot/screenshot.h new file mode 100644 index 0000000000..9f7b175555 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screenshot/screenshot.h @@ -0,0 +1,54 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Martin Gräßlin + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "plugin.h" + +namespace KWin +{ + +/** + * This enum type is used to specify how a screenshot needs to be taken. + */ +enum ScreenShotFlag { + ScreenShotIncludeDecoration = 0x1, ///< Include window titlebar and borders + ScreenShotIncludeCursor = 0x2, ///< Include the cursor + ScreenShotNativeResolution = 0x4, ///< Take the screenshot at the native resolution + ScreenShotIncludeShadow = 0x8, ///< Include the window shadow +}; +Q_DECLARE_FLAGS(ScreenShotFlags, ScreenShotFlag) + +class LogicalOutput; +class Rect; +class ScreenShotDBusInterface2; +class Window; + +/** + * The ScreenShotManager provides a convenient way to capture the contents of a given window, + * screen or an area in the global coordinates. + */ +class ScreenShotManager : public Plugin +{ +public: + ScreenShotManager(); + ~ScreenShotManager() override; + + std::optional takeScreenShot(LogicalOutput *screen, ScreenShotFlags flags, std::optional pidToHide); + std::optional takeScreenShot(const Rect &area, ScreenShotFlags flags, std::optional pidToHide); + std::optional takeScreenShot(Window *window, ScreenShotFlags flags = {}); + +private: + std::unique_ptr m_dbusInterface2; +}; + +} // namespace KWin + +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::ScreenShotFlags) diff --git a/local/recipes/kde/kwin/source/src/plugins/screenshot/screenshotdbusinterface2.cpp b/local/recipes/kde/kwin/source/src/plugins/screenshot/screenshotdbusinterface2.cpp new file mode 100644 index 0000000000..51dde76f8c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screenshot/screenshotdbusinterface2.cpp @@ -0,0 +1,513 @@ +/* + SPDX-FileCopyrightText: 2010 Martin Gräßlin + SPDX-FileCopyrightText: 2021 Méven Car + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "screenshotdbusinterface2.h" +#include "core/output.h" +#include "effect/effecthandler.h" +#include "screenshot2adaptor.h" +#include "screenshotlogging.h" +#include "utils/filedescriptor.h" +#include "utils/serviceutils.h" +#include "window.h" +#include "workspace.h" + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace KWin +{ + +class ScreenShotWriter2 : public QRunnable +{ +public: + ScreenShotWriter2(FileDescriptor &&fileDescriptor, const QImage &image) + : m_fileDescriptor(std::move(fileDescriptor)) + , m_image(image) + { + } + + void run() override + { + const int flags = fcntl(m_fileDescriptor.get(), F_GETFL, 0); + if (flags == -1) { + qCWarning(KWIN_SCREENSHOT) << "failed to get screenshot fd flags:" << strerror(errno); + return; + } + if (!(flags & O_NONBLOCK)) { + if (fcntl(m_fileDescriptor.get(), F_SETFL, flags | O_NONBLOCK) == -1) { + qCWarning(KWIN_SCREENSHOT) << "failed to make screenshot fd non blocking:" << strerror(errno); + return; + } + } + + QFile file; + if (!file.open(m_fileDescriptor.get(), QIODevice::WriteOnly)) { + qCWarning(KWIN_SCREENSHOT) << Q_FUNC_INFO << "failed to open pipe:" << file.errorString(); + return; + } + + const QByteArrayView buffer(m_image.constBits(), m_image.sizeInBytes()); + qint64 remainingSize = buffer.size(); + + pollfd pfds[1]; + pfds[0].fd = m_fileDescriptor.get(); + pfds[0].events = POLLOUT; + + while (true) { + const int ready = poll(pfds, 1, 60000); + if (ready < 0) { + if (errno != EINTR) { + qCWarning(KWIN_SCREENSHOT) << Q_FUNC_INFO << "poll() failed:" << strerror(errno); + return; + } + } else if (ready == 0) { + qCWarning(KWIN_SCREENSHOT) << Q_FUNC_INFO << "timed out writing to pipe"; + return; + } else if (!(pfds[0].revents & POLLOUT)) { + qCWarning(KWIN_SCREENSHOT) << Q_FUNC_INFO << "pipe is broken"; + return; + } else { + const char *chunk = buffer.constData() + (buffer.size() - remainingSize); + const qint64 writtenCount = file.write(chunk, remainingSize); + + if (writtenCount < 0) { + qCWarning(KWIN_SCREENSHOT) << Q_FUNC_INFO << "write() failed:" << file.errorString(); + return; + } + + remainingSize -= writtenCount; + if (writtenCount == 0 || remainingSize == 0) { + return; + } + } + } + } + +protected: + FileDescriptor m_fileDescriptor; + QImage m_image; +}; + +static ScreenShotFlags screenShotFlagsFromOptions(const QVariantMap &options) +{ + ScreenShotFlags flags = ScreenShotFlags(); + + const QVariant includeDecoration = options.value(QStringLiteral("include-decoration")); + if (includeDecoration.toBool()) { + flags |= ScreenShotIncludeDecoration; + } + + const QVariant includeShadow = options.value(QStringLiteral("include-shadow"), true); + if (includeShadow.toBool()) { + flags |= ScreenShotIncludeShadow; + } + + const QVariant includeCursor = options.value(QStringLiteral("include-cursor")); + if (includeCursor.toBool()) { + flags |= ScreenShotIncludeCursor; + } + + const QVariant nativeResolution = options.value(QStringLiteral("native-resolution")); + if (nativeResolution.toBool()) { + flags |= ScreenShotNativeResolution; + } + + return flags; +} + +static std::optional pidToHide(std::optional callerPid, const QVariantMap &options) +{ + if (!callerPid.has_value()) { + return std::nullopt; + } + const bool hideCallerWindows = options.value(QStringLiteral("hide-caller-windows"), true).toBool(); + if (!hideCallerWindows) { + return std::nullopt; + } + return callerPid; +} + +static const QString s_dbusServiceName = QStringLiteral("org.kde.KWin.ScreenShot2"); +static const QString s_dbusInterface = QStringLiteral("org.kde.KWin.ScreenShot2"); +static const QString s_dbusObjectPath = QStringLiteral("/org/kde/KWin/ScreenShot2"); + +static const QString s_errorNotAuthorized = QStringLiteral("org.kde.KWin.ScreenShot2.Error.NoAuthorized"); +static const QString s_errorNotAuthorizedMessage = QStringLiteral("The process is not authorized to take a screenshot"); +static const QString s_errorCancelled = QStringLiteral("org.kde.KWin.ScreenShot2.Error.Cancelled"); +static const QString s_errorCancelledMessage = QStringLiteral("Screenshot got cancelled"); +static const QString s_errorInvalidWindow = QStringLiteral("org.kde.KWin.ScreenShot2.Error.InvalidWindow"); +static const QString s_errorInvalidWindowMessage = QStringLiteral("Invalid window requested"); +static const QString s_errorNoActiveWindow = QStringLiteral("org.kde.KWin.ScreenShot2.Error.NoActiveWindow"); +static const QString s_errorNoActiveWindowMessage = QStringLiteral("No active window"); +static const QString s_errorInvalidArea = QStringLiteral("org.kde.KWin.ScreenShot2.Error.InvalidArea"); +static const QString s_errorInvalidAreaMessage = QStringLiteral("Invalid area requested"); +static const QString s_errorInvalidScreen = QStringLiteral("org.kde.KWin.ScreenShot2.Error.InvalidScreen"); +static const QString s_errorInvalidScreenMessage = QStringLiteral("Invalid screen requested"); +static const QString s_errorFileDescriptor = QStringLiteral("org.kde.KWin.ScreenShot2.Error.FileDescriptor"); +static const QString s_errorFileDescriptorMessage = QStringLiteral("No valid file descriptor"); + +class ScreenShotSinkPipe2 : public QObject +{ + Q_OBJECT + +public: + ScreenShotSinkPipe2(int fileDescriptor, QDBusMessage replyMessage); + + void cancel(); + void flush(const QImage &image, const QVariantMap &attributes); + +private: + QDBusMessage m_replyMessage; + FileDescriptor m_fileDescriptor; +}; + +ScreenShotSinkPipe2::ScreenShotSinkPipe2(int fileDescriptor, QDBusMessage replyMessage) + : m_replyMessage(replyMessage) + , m_fileDescriptor(fileDescriptor) +{ +} + +void ScreenShotSinkPipe2::cancel() +{ + QDBusConnection::sessionBus().send(m_replyMessage.createErrorReply(s_errorCancelled, + s_errorCancelledMessage)); +} + +void ScreenShotSinkPipe2::flush(const QImage &image, const QVariantMap &attributes) +{ + if (!m_fileDescriptor.isValid()) { + return; + } + + // Note that the type of the data stored in the vardict matters. Be careful. + QVariantMap results = attributes; + results.insert(QStringLiteral("type"), QStringLiteral("raw")); + results.insert(QStringLiteral("format"), quint32(image.format())); + results.insert(QStringLiteral("width"), quint32(image.width())); + results.insert(QStringLiteral("height"), quint32(image.height())); + results.insert(QStringLiteral("stride"), quint32(image.bytesPerLine())); + results.insert(QStringLiteral("scale"), double(image.devicePixelRatio())); + QDBusConnection::sessionBus().send(m_replyMessage.createReply(results)); + + auto writer = new ScreenShotWriter2(std::move(m_fileDescriptor), image); + writer->setAutoDelete(true); + QThreadPool::globalInstance()->start(writer); +} + +ScreenShotDBusInterface2::ScreenShotDBusInterface2(ScreenShotManager *manager) + : QObject(manager) + , m_effect(manager) +{ + new ScreenShot2Adaptor(this); + + QDBusConnection::sessionBus().registerObject(s_dbusObjectPath, this); + QDBusConnection::sessionBus().registerService(s_dbusServiceName); +} + +ScreenShotDBusInterface2::~ScreenShotDBusInterface2() +{ + QDBusConnection::sessionBus().unregisterService(s_dbusServiceName); + QDBusConnection::sessionBus().unregisterObject(s_dbusObjectPath); +} + +int ScreenShotDBusInterface2::version() const +{ + return 5; +} + +std::optional ScreenShotDBusInterface2::determineCallerPid() const +{ + if (!calledFromDBus()) { + return std::nullopt; + } + const QDBusReply reply = connection().interface()->servicePid(message().service()); + if (reply.isValid()) { + return reply.value(); + } else { + return std::nullopt; + } +} + +bool ScreenShotDBusInterface2::checkPermissions(std::optional pid) const +{ + static bool permissionCheckDisabled = qEnvironmentVariableIntValue("KWIN_SCREENSHOT_NO_PERMISSION_CHECKS") == 1; + if (permissionCheckDisabled) { + return true; + } + if (!pid.has_value()) { + return false; + } + const auto interfaces = KWin::fetchRestrictedDBusInterfacesFromPid(*pid); + if (!interfaces.contains(s_dbusInterface)) { + sendErrorReply(s_errorNotAuthorized, s_errorNotAuthorizedMessage); + return false; + } + return true; +} + +QVariantMap ScreenShotDBusInterface2::CaptureActiveWindow(const QVariantMap &options, + QDBusUnixFileDescriptor pipe) +{ + if (!checkPermissions(determineCallerPid())) { + return QVariantMap(); + } + + Window *window = workspace()->activeWindow(); + if (!window) { + sendErrorReply(s_errorNoActiveWindow, s_errorNoActiveWindowMessage); + return QVariantMap(); + } + + const int fileDescriptor = fcntl(pipe.fileDescriptor(), F_DUPFD_CLOEXEC, 0); + if (fileDescriptor == -1) { + sendErrorReply(s_errorFileDescriptor, s_errorFileDescriptorMessage); + return QVariantMap(); + } + + takeScreenShot(window, screenShotFlagsFromOptions(options), + new ScreenShotSinkPipe2(fileDescriptor, message())); + + setDelayedReply(true); + return QVariantMap(); +} + +QVariantMap ScreenShotDBusInterface2::CaptureWindow(const QString &handle, + const QVariantMap &options, + QDBusUnixFileDescriptor pipe) +{ + if (!checkPermissions(determineCallerPid())) { + return QVariantMap(); + } + + Window *window = workspace()->findWindow(QUuid(handle)); + if (!window) { + sendErrorReply(s_errorInvalidWindow, s_errorInvalidWindowMessage); + return QVariantMap(); + } + + const int fileDescriptor = fcntl(pipe.fileDescriptor(), F_DUPFD_CLOEXEC, 0); + if (fileDescriptor == -1) { + sendErrorReply(s_errorFileDescriptor, s_errorFileDescriptorMessage); + return QVariantMap(); + } + + takeScreenShot(window, screenShotFlagsFromOptions(options), + new ScreenShotSinkPipe2(fileDescriptor, message())); + + setDelayedReply(true); + return QVariantMap(); +} + +QVariantMap ScreenShotDBusInterface2::CaptureArea(int x, int y, int width, int height, + const QVariantMap &options, + QDBusUnixFileDescriptor pipe) +{ + const auto pid = determineCallerPid(); + if (!checkPermissions(pid)) { + return QVariantMap(); + } + + const Rect area(x, y, width, height); + if (area.isEmpty()) { + sendErrorReply(s_errorInvalidArea, s_errorInvalidAreaMessage); + return QVariantMap(); + } + + const int fileDescriptor = fcntl(pipe.fileDescriptor(), F_DUPFD_CLOEXEC, 0); + if (fileDescriptor == -1) { + sendErrorReply(s_errorFileDescriptor, s_errorFileDescriptorMessage); + return QVariantMap(); + } + + takeScreenShot(area, screenShotFlagsFromOptions(options), + new ScreenShotSinkPipe2(fileDescriptor, message()), pidToHide(pid, options)); + + setDelayedReply(true); + return QVariantMap(); +} + +QVariantMap ScreenShotDBusInterface2::CaptureScreen(const QString &name, + const QVariantMap &options, + QDBusUnixFileDescriptor pipe) +{ + const auto pid = determineCallerPid(); + if (!checkPermissions(pid)) { + return QVariantMap(); + } + + LogicalOutput *screen = workspace()->findOutput(name); + if (!screen) { + sendErrorReply(s_errorInvalidScreen, s_errorInvalidScreenMessage); + return QVariantMap(); + } + + const int fileDescriptor = fcntl(pipe.fileDescriptor(), F_DUPFD_CLOEXEC, 0); + if (fileDescriptor == -1) { + sendErrorReply(s_errorFileDescriptor, s_errorFileDescriptorMessage); + return QVariantMap(); + } + + takeScreenShot(screen, screenShotFlagsFromOptions(options), + new ScreenShotSinkPipe2(fileDescriptor, message()), pidToHide(pid, options)); + + setDelayedReply(true); + return QVariantMap(); +} + +QVariantMap ScreenShotDBusInterface2::CaptureActiveScreen(const QVariantMap &options, + QDBusUnixFileDescriptor pipe) +{ + const auto pid = determineCallerPid(); + if (!checkPermissions(pid)) { + return QVariantMap(); + } + + LogicalOutput *screen = workspace()->activeOutput(); + if (!screen) { + sendErrorReply(s_errorInvalidScreen, s_errorInvalidScreenMessage); + return QVariantMap(); + } + + const int fileDescriptor = fcntl(pipe.fileDescriptor(), F_DUPFD_CLOEXEC, 0); + if (fileDescriptor == -1) { + sendErrorReply(s_errorFileDescriptor, s_errorFileDescriptorMessage); + return QVariantMap(); + } + + takeScreenShot(screen, screenShotFlagsFromOptions(options), + new ScreenShotSinkPipe2(fileDescriptor, message()), pidToHide(pid, options)); + + setDelayedReply(true); + return QVariantMap(); +} + +QVariantMap ScreenShotDBusInterface2::CaptureInteractive(uint kind, + const QVariantMap &options, + QDBusUnixFileDescriptor pipe) +{ + const auto pid = determineCallerPid(); + const int fileDescriptor = fcntl(pipe.fileDescriptor(), F_DUPFD_CLOEXEC, 0); + if (fileDescriptor == -1) { + sendErrorReply(s_errorFileDescriptor, s_errorFileDescriptorMessage); + return QVariantMap(); + } + + const QDBusMessage replyMessage = message(); + + if (kind == 0) { + kwinApp()->startInteractiveWindowSelection([=, this](Window *window) { + effects->hideOnScreenMessage(EffectsHandler::OnScreenMessageHideFlag::SkipsCloseAnimation); + + if (!window) { + close(fileDescriptor); + + QDBusConnection bus = QDBusConnection::sessionBus(); + bus.send(replyMessage.createErrorReply(s_errorCancelled, s_errorCancelledMessage)); + } else { + takeScreenShot(window, screenShotFlagsFromOptions(options), + new ScreenShotSinkPipe2(fileDescriptor, replyMessage)); + } + }); + effects->showOnScreenMessage(i18n("Select window to screen shot with left click or enter.\n" + "Escape or right click to cancel."), + QStringLiteral("spectacle")); + } else { + kwinApp()->startInteractivePositionSelection([=, this](const QPointF &point) { + effects->hideOnScreenMessage(EffectsHandler::OnScreenMessageHideFlag::SkipsCloseAnimation); + + if (point == QPoint(-1, -1)) { + close(fileDescriptor); + + QDBusConnection bus = QDBusConnection::sessionBus(); + bus.send(replyMessage.createErrorReply(s_errorCancelled, s_errorCancelledMessage)); + } else { + LogicalOutput *screen = effects->screenAt(point.toPoint()); + takeScreenShot(screen, screenShotFlagsFromOptions(options), + new ScreenShotSinkPipe2(fileDescriptor, replyMessage), pidToHide(pid, options)); + } + }); + effects->showOnScreenMessage(i18n("Create screen shot with left click or enter.\n" + "Escape or right click to cancel."), + QStringLiteral("spectacle")); + } + + setDelayedReply(true); + return QVariantMap(); +} + +QVariantMap ScreenShotDBusInterface2::CaptureWorkspace(const QVariantMap &options, QDBusUnixFileDescriptor pipe) +{ + const auto pid = determineCallerPid(); + if (!checkPermissions(pid)) { + return QVariantMap(); + } + + const int fileDescriptor = fcntl(pipe.fileDescriptor(), F_DUPFD_CLOEXEC, 0); + if (fileDescriptor == -1) { + sendErrorReply(s_errorFileDescriptor, s_errorFileDescriptorMessage); + return QVariantMap(); + } + + takeScreenShot(effects->virtualScreenGeometry(), screenShotFlagsFromOptions(options), + new ScreenShotSinkPipe2(fileDescriptor, message()), pidToHide(pid, options)); + + setDelayedReply(true); + return QVariantMap(); +} + +void ScreenShotDBusInterface2::takeScreenShot(LogicalOutput *screen, ScreenShotFlags flags, + ScreenShotSinkPipe2 *sink, std::optional pid) +{ + if (const auto result = m_effect->takeScreenShot(screen, flags, pid)) { + sink->flush(*result, QVariantMap{ + {QStringLiteral("screen"), screen->name()}, + }); + } else { + sink->cancel(); + } + sink->deleteLater(); +} + +void ScreenShotDBusInterface2::takeScreenShot(const Rect &area, ScreenShotFlags flags, + ScreenShotSinkPipe2 *sink, std::optional pid) +{ + if (const auto result = m_effect->takeScreenShot(area, flags, pid)) { + sink->flush(*result, {}); + } else { + sink->cancel(); + } + sink->deleteLater(); +} + +void ScreenShotDBusInterface2::takeScreenShot(Window *window, ScreenShotFlags flags, + ScreenShotSinkPipe2 *sink) +{ + if (const auto result = m_effect->takeScreenShot(window, flags)) { + sink->flush(*result, QVariantMap{ + {QStringLiteral("windowId"), window->internalId().toString()}, + }); + } else { + sink->cancel(); + } + sink->deleteLater(); +} + +} // namespace KWin + +#include "screenshotdbusinterface2.moc" + +#include "moc_screenshotdbusinterface2.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/screenshot/screenshotdbusinterface2.h b/local/recipes/kde/kwin/source/src/plugins/screenshot/screenshotdbusinterface2.h new file mode 100644 index 0000000000..7e30b0a8d2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screenshot/screenshotdbusinterface2.h @@ -0,0 +1,70 @@ +/* + SPDX-FileCopyrightText: 2010 Martin Gräßlin + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "screenshot.h" + +#include +#include +#include +#include + +namespace KWin +{ + +class Rect; +class ScreenShotManager; +class ScreenShotSinkPipe2; +class ScreenShotSource2; + +/** + * The ScreenshotDBusInterface2 class provides a d-bus api to take screenshots. This implements + * the org.kde.KWin.ScreenShot2 interface. + * + * An application that requests a screenshot must have "org.kde.KWin.ScreenShot2" listed in its + * X-KDE-DBUS-Restricted-Interfaces desktop file field. + */ +class ScreenShotDBusInterface2 : public QObject, public QDBusContext +{ + Q_OBJECT + Q_PROPERTY(int Version READ version CONSTANT) + +public: + explicit ScreenShotDBusInterface2(ScreenShotManager *effect); + ~ScreenShotDBusInterface2() override; + + int version() const; + +public Q_SLOTS: + QVariantMap CaptureWindow(const QString &handle, const QVariantMap &options, + QDBusUnixFileDescriptor pipe); + QVariantMap CaptureActiveWindow(const QVariantMap &options, + QDBusUnixFileDescriptor pipe); + QVariantMap CaptureArea(int x, int y, int width, int height, + const QVariantMap &options, + QDBusUnixFileDescriptor pipe); + QVariantMap CaptureScreen(const QString &name, const QVariantMap &options, + QDBusUnixFileDescriptor pipe); + QVariantMap CaptureActiveScreen(const QVariantMap &options, + QDBusUnixFileDescriptor pipe); + QVariantMap CaptureInteractive(uint kind, const QVariantMap &options, + QDBusUnixFileDescriptor pipe); + QVariantMap CaptureWorkspace(const QVariantMap &options, + QDBusUnixFileDescriptor pipe); + +private: + void takeScreenShot(LogicalOutput *screen, ScreenShotFlags flags, ScreenShotSinkPipe2 *sink, std::optional pid); + void takeScreenShot(const Rect &area, ScreenShotFlags flags, ScreenShotSinkPipe2 *sink, std::optional pid); + void takeScreenShot(Window *window, ScreenShotFlags flags, ScreenShotSinkPipe2 *sink); + std::optional determineCallerPid() const; + bool checkPermissions(std::optional pid) const; + + ScreenShotManager *m_effect; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/screentransform/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/screentransform/CMakeLists.txt new file mode 100644 index 0000000000..0a27a61558 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screentransform/CMakeLists.txt @@ -0,0 +1,13 @@ +####################################### +# Effect + +set(screentransform_SOURCES + main.cpp + screentransform.cpp + screentransform.qrc +) + +kwin_add_builtin_effect(screentransform ${screentransform_SOURCES}) +target_link_libraries(screentransform PRIVATE + kwin +) diff --git a/local/recipes/kde/kwin/source/src/plugins/screentransform/main.cpp b/local/recipes/kde/kwin/source/src/plugins/screentransform/main.cpp new file mode 100644 index 0000000000..1e8a2ba976 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screentransform/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "screentransform.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(ScreenTransformEffect, + "metadata.json.stripped", + return ScreenTransformEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/screentransform/metadata.json b/local/recipes/kde/kwin/source/src/plugins/screentransform/metadata.json new file mode 100644 index 0000000000..209b792ae3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screentransform/metadata.json @@ -0,0 +1,104 @@ +{ + "KPlugin": { + "Category": "Appearance", + "Description": "Animates display transformations", + "Description[ar]": "يحرك تحورات العرض", + "Description[az]": "Ekranın dəyişməsi animasiyası", + "Description[be]": "Анімацыя трансфармавання дысплэя", + "Description[bg]": "Анимира трансформациите на дисплея", + "Description[ca@valencia]": "Anima les transformacions de pantalla", + "Description[ca]": "Anima les transformacions de pantalla", + "Description[cs]": "Animuje transformace zobrazení", + "Description[da]": "Animerer skærmtransformationer", + "Description[de]": "Animiert Anzeigeänderungen", + "Description[en_GB]": "Animates display transformations", + "Description[eo]": "Animas ekranajn transformojn", + "Description[es]": "Anima las transformaciones de la pantalla", + "Description[et]": "Kuvateisenduste animeerimine", + "Description[eu]": "Pantaila aldaketak animatzen ditu", + "Description[fi]": "Animoi näyttömuunnokset", + "Description[fr]": "Anime les transformations d'affichage", + "Description[gl]": "Anima as transformacións da pantalla.", + "Description[he]": "מנפיש התמרות תצוגה", + "Description[hu]": "Animálja a megjelenítési transzformációkat", + "Description[ia]": "Anima transformationes de monstrator ", + "Description[id]": "Animasikan transformasi tampilan display", + "Description[is]": "Hreyfiáhrif fyrir ummyndanir", + "Description[it]": "Anima le trasformazioni dello schermo", + "Description[ja]": "ディスプレイの変形をアニメートします", + "Description[ka]": "ეკრანის გარდაქმნების ანიმაცია", + "Description[ko]": "디스플레이 변형 애니메이션", + "Description[lt]": "Animuoja ekrano perkeitimus", + "Description[lv]": "Displeja transformāciju animācija", + "Description[nb]": "Animer utseendetransformasjoner", + "Description[nl]": "Animeert transformaties op scherm", + "Description[nn]": "Animer utsjånadstransformeringar", + "Description[pl]": "Animuje przekształcenia wyświetlacza", + "Description[pt]": "Anima as transformações do ecrã", + "Description[pt_BR]": "Anima transformações de visualização", + "Description[ro]": "Animează transformările afișajului", + "Description[ru]": "Анимация преобразований рабочего стола", + "Description[sa]": "प्रदर्शनं परिवर्तनं सजीवं करोति", + "Description[sk]": "Animuje zobrazenie transformácií", + "Description[sl]": "Animiraj preoblikovanja zaslona", + "Description[sv]": "Animerar skärmtransformeringar", + "Description[ta]": "காட்சிக்கருவி மாற்றங்களை அசைவூட்டும்", + "Description[tr]": "Görüntü dönüşümlerini canlandırır", + "Description[uk]": "Анімація перетворень показу", + "Description[vi]": "Tạo hiệu ứng động cho các biến đổi hiển thị", + "Description[zh_CN]": "动态显示变形", + "Description[zh_TW]": "為顯示設定變更使用動畫效果", + "EnabledByDefault": true, + "License": "GPL", + "Name": "Transform", + "Name[ar]": "التّحويل", + "Name[az]": "Dönüşüm", + "Name[be]": "Трансфармаваць", + "Name[bg]": "Трансформиране", + "Name[ca@valencia]": "Transforma", + "Name[ca]": "Transforma", + "Name[cs]": "Transformovat", + "Name[da]": "Transformér", + "Name[de]": "Transformieren", + "Name[en_GB]": "Transform", + "Name[eo]": "Transformi", + "Name[es]": "Transformar", + "Name[et]": "Teisendamine", + "Name[eu]": "Eraldatu", + "Name[fi]": "Muunnos", + "Name[fr]": "Transformer", + "Name[gl]": "Transformar", + "Name[he]": "התמרה", + "Name[hu]": "Átalakítás", + "Name[ia]": "Transforma", + "Name[id]": "Transformasi", + "Name[is]": "Ummynda", + "Name[it]": "Trasforma", + "Name[ja]": "変形", + "Name[ka]": "ტრასფორმირება", + "Name[ko]": "변형", + "Name[lt]": "Transformavimas", + "Name[lv]": "Transformēšana", + "Name[nb]": "Transformer", + "Name[nl]": "Transformeren", + "Name[nn]": "Transformer", + "Name[pl]": "Przekształcenie", + "Name[pt]": "Transformar", + "Name[pt_BR]": "Transformar", + "Name[ro]": "Transformare", + "Name[ru]": "Преобразование", + "Name[sa]": "परिवर्तनं कुरुत", + "Name[sk]": "Transformovať", + "Name[sl]": "Preoblikuj", + "Name[sv]": "Transformera", + "Name[ta]": "உருமாற்றம்", + "Name[tr]": "Dönüştür", + "Name[uk]": "Перетворення", + "Name[vi]": "Biến đổi", + "Name[zh_CN]": "变形", + "Name[zh_TW]": "顯示設定變更" + }, + "org.kde.kwin.effect": { + "internal": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/screentransform/screentransform.cpp b/local/recipes/kde/kwin/source/src/plugins/screentransform/screentransform.cpp new file mode 100644 index 0000000000..40253c1c3d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screentransform/screentransform.cpp @@ -0,0 +1,277 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +// own +#include "screentransform.h" +#include "core/outputconfiguration.h" +#include "core/outputlayer.h" +#include "core/rendertarget.h" +#include "core/renderviewport.h" +#include "effect/effecthandler.h" +#include "opengl/glutils.h" +#include "scene/workspacescene.h" + +#include + +using namespace std::chrono_literals; + +static void ensureResources() +{ + // Must initialize resources manually because the effect is a static lib. + Q_INIT_RESOURCE(screentransform); +} + +namespace KWin +{ + +ScreenTransformEffect::ScreenTransformEffect() + : Effect() +{ + // Make sure that shaders in /effects/screentransform/shaders/* are loaded. + ensureResources(); + + m_shader = ShaderManager::instance()->generateShaderFromFile( + ShaderTrait::MapTexture, + QStringLiteral(":/effects/screentransform/shaders/crossfade.vert"), + QStringLiteral(":/effects/screentransform/shaders/crossfade.frag")); + + m_modelViewProjectioMatrixLocation = m_shader->uniformLocation("modelViewProjectionMatrix"); + m_blendFactorLocation = m_shader->uniformLocation("blendFactor"); + m_previousTextureLocation = m_shader->uniformLocation("previousTexture"); + m_currentTextureLocation = m_shader->uniformLocation("currentTexture"); + + const QList screens = effects->screens(); + for (auto screen : screens) { + addScreen(screen); + } + connect(effects, &EffectsHandler::screenAdded, this, &ScreenTransformEffect::addScreen); + connect(effects, &EffectsHandler::screenRemoved, this, &ScreenTransformEffect::removeScreen); +} + +ScreenTransformEffect::~ScreenTransformEffect() = default; + +bool ScreenTransformEffect::supported() +{ + return effects->compositingType() == OpenGLCompositing && effects->animationsSupported(); +} + +qreal transformAngle(OutputTransform current, OutputTransform old) +{ + auto ensureShort = [](int angle) { + return angle > 180 ? angle - 360 : angle < -180 ? angle + 360 + : angle; + }; + // % 4 to ignore flipped cases (for now) + return ensureShort((int(current.kind()) % 4 - int(old.kind()) % 4) * 90); +} + +void ScreenTransformEffect::addScreen(LogicalOutput *screen) +{ + connect(screen, &LogicalOutput::aboutToChange, this, [this, screen](OutputChangeSet *changeSet) { + const OutputTransform transform = changeSet->transform.value_or(screen->transform()); + if (screen->transform() == transform) { + return; + } + + // Avoid including this effect while capturing previous screen state. + m_capturing = true; + auto resetCapturing = qScopeGuard([this]() { + m_capturing = false; + }); + + effects->makeOpenGLContextCurrent(); + auto texture = GLTexture::allocate(GL_RGBA16F, screen->pixelSize()); + if (!texture) { + m_states.remove(screen); + return; + } + auto &state = m_states[screen]; + state.m_oldTransform = screen->transform(); + state.m_oldGeometry = screen->geometry(); + state.m_timeLine.setDuration(std::chrono::milliseconds(long(animationTime(250ms)))); + state.m_timeLine.setEasingCurve(QEasingCurve::InOutCubic); + state.m_angle = transformAngle(changeSet->transform.value(), state.m_oldTransform); + state.m_prev.texture = std::move(texture); + state.m_prev.framebuffer = std::make_unique(state.m_prev.texture.get()); + RenderTarget renderTarget(state.m_prev.framebuffer.get(), screen->blendingColor()); + + Scene *scene = effects->scene(); + SceneView delegate(scene, screen, nullptr, nullptr); + delegate.setViewport(screen->geometryF()); + delegate.setScale(screen->scale()); + scene->prePaint(&delegate); + scene->paint(renderTarget, QPoint(), screen->geometry()); + scene->postPaint(); + }); +} + +void ScreenTransformEffect::removeScreen(LogicalOutput *screen) +{ + screen->disconnect(this); + if (auto it = m_states.find(screen); it != m_states.end()) { + effects->makeOpenGLContextCurrent(); + m_states.erase(it); + } +} + +void ScreenTransformEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + m_currentView = data.view; + auto it = m_states.find(data.screen); + if (it != m_states.end()) { + it->m_timeLine.advance(presentTime); + if (it->m_timeLine.done()) { + m_states.remove(data.screen); + } + } + + effects->prePaintScreen(data, presentTime); +} + +static GLVertexBuffer *texturedRectVbo(const QRectF &geometry, qreal scale) +{ + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + vbo->setAttribLayout(std::span(GLVertexBuffer::GLVertex2DLayout), sizeof(GLVertex2D)); + + const auto opt = vbo->map(6); + if (!opt) { + return nullptr; + } + const auto map = *opt; + + auto deviceGeometry = scaledRect(geometry, scale); + + // first triangle + map[0] = GLVertex2D{ + .position = QVector2D(deviceGeometry.left(), deviceGeometry.top()), + .texcoord = QVector2D(0.0, 1.0), + }; + map[1] = GLVertex2D{ + .position = QVector2D(deviceGeometry.right(), deviceGeometry.bottom()), + .texcoord = QVector2D(1.0, 0.0), + }; + map[2] = GLVertex2D{ + .position = QVector2D(deviceGeometry.left(), deviceGeometry.bottom()), + .texcoord = QVector2D(0.0, 0.0), + }; + + // second triangle + map[3] = GLVertex2D{ + .position = QVector2D(deviceGeometry.left(), deviceGeometry.top()), + .texcoord = QVector2D(0.0, 1.0), + }; + map[4] = GLVertex2D{ + .position = QVector2D(deviceGeometry.right(), deviceGeometry.top()), + .texcoord = QVector2D(1.0, 1.0), + }; + map[5] = GLVertex2D{ + .position = QVector2D(deviceGeometry.right(), deviceGeometry.bottom()), + .texcoord = QVector2D(1.0, 0.0), + }; + + vbo->unmap(); + return vbo; +} + +static qreal lerp(qreal a, qreal b, qreal t) +{ + return (1 - t) * a + t * b; +} + +static QRectF lerp(const QRectF &a, const QRectF &b, qreal t) +{ + QRectF ret; + ret.setWidth(lerp(a.width(), b.width(), t)); + ret.setHeight(lerp(a.height(), b.height(), t)); + ret.moveCenter(b.center()); + return ret; +} + +void ScreenTransformEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) +{ + auto it = m_states.find(screen); + if (it == m_states.end() || m_currentView->backendOutput() != screen->backendOutput()) { + effects->paintScreen(renderTarget, viewport, mask, deviceRegion, screen); + return; + } + + // Render the screen in an offscreen texture. + const QSize nativeSize = screen->geometry().size() * screen->scale(); + if (!it->m_current.texture || it->m_current.texture->size() != nativeSize + || it->m_current.texture->internalFormat() != renderTarget.texture()->internalFormat()) { + it->m_current.texture = GLTexture::allocate(renderTarget.texture()->internalFormat(), nativeSize); + if (!it->m_current.texture) { + m_states.remove(screen); + return; + } + it->m_current.framebuffer = std::make_unique(it->m_current.texture.get()); + } + + RenderTarget fboRenderTarget(it->m_current.framebuffer.get(), renderTarget.colorDescription()); + RenderViewport fboViewport(viewport.renderRect(), viewport.scale(), fboRenderTarget, QPoint()); + + GLFramebuffer::pushFramebuffer(it->m_current.framebuffer.get()); + effects->paintScreen(fboRenderTarget, fboViewport, mask, deviceRegion, screen); + GLFramebuffer::popFramebuffer(); + + const qreal blendFactor = it->m_timeLine.value(); + const RectF screenRect = screen->geometry(); + const qreal angle = it->m_angle * (1 - blendFactor); + + const auto scale = viewport.scale(); + + // Projection matrix + rotate transform. + const QVector3D transformOrigin(screenRect.center()); + QMatrix4x4 modelViewProjectionMatrix(viewport.projectionMatrix()); + modelViewProjectionMatrix.translate(transformOrigin * scale); + modelViewProjectionMatrix.rotate(angle, 0, 0, 1); + modelViewProjectionMatrix.translate(-transformOrigin * scale); + + glActiveTexture(GL_TEXTURE1); + it->m_prev.texture->bind(); + glActiveTexture(GL_TEXTURE0); + it->m_current.texture->bind(); + + // Clear the background. + glClearColor(0, 0, 0, 0); + glClear(GL_COLOR_BUFFER_BIT); + + GLVertexBuffer *vbo = texturedRectVbo(lerp(it->m_oldGeometry, screenRect, blendFactor), scale); + if (!vbo) { + return; + } + + ShaderManager *sm = ShaderManager::instance(); + sm->pushShader(m_shader.get()); + m_shader->setUniform(m_modelViewProjectioMatrixLocation, modelViewProjectionMatrix); + m_shader->setUniform(m_blendFactorLocation, float(blendFactor)); + m_shader->setUniform(m_currentTextureLocation, 0); + m_shader->setUniform(m_previousTextureLocation, 1); + + vbo->bindArrays(); + vbo->draw(GL_TRIANGLES, 0, 6); + vbo->unbindArrays(); + sm->popShader(); + + glActiveTexture(GL_TEXTURE1); + it->m_prev.texture->unbind(); + glActiveTexture(GL_TEXTURE0); + it->m_current.texture->unbind(); + + effects->addRepaintFull(); +} + +bool ScreenTransformEffect::isActive() const +{ + return !m_states.isEmpty() && !m_capturing; +} + +} // namespace KWin + +#include "moc_screentransform.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/screentransform/screentransform.h b/local/recipes/kde/kwin/source/src/plugins/screentransform/screentransform.h new file mode 100644 index 0000000000..5e4b4247cc --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screentransform/screentransform.h @@ -0,0 +1,71 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "core/output.h" +#include "effect/effect.h" +#include "effect/timeline.h" + +namespace KWin +{ +class GLFramebuffer; +class GLShader; +class GLTexture; + +class ScreenTransformEffect : public Effect +{ + Q_OBJECT + +public: + ScreenTransformEffect(); + ~ScreenTransformEffect() override; + + void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override; + void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) override; + + bool isActive() const override; + + int requestedEffectChainPosition() const override + { + return 99; + } + static bool supported(); + +private: + struct Snapshot + { + std::shared_ptr texture; + std::shared_ptr framebuffer; + }; + + struct ScreenState + { + TimeLine m_timeLine; + Snapshot m_prev; + Snapshot m_current; + QRect m_oldGeometry; + OutputTransform m_oldTransform; + qreal m_angle = 0; + }; + + void addScreen(LogicalOutput *screen); + void removeScreen(LogicalOutput *screen); + + QHash m_states; + + std::unique_ptr m_shader; + int m_previousTextureLocation = -1; + int m_currentTextureLocation = -1; + int m_modelViewProjectioMatrixLocation = -1; + int m_blendFactorLocation = -1; + bool m_capturing = false; + RenderView *m_currentView = nullptr; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/screentransform/screentransform.qrc b/local/recipes/kde/kwin/source/src/plugins/screentransform/screentransform.qrc new file mode 100644 index 0000000000..4b52a40959 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screentransform/screentransform.qrc @@ -0,0 +1,9 @@ + + + + shaders/crossfade.frag + shaders/crossfade_core.frag + shaders/crossfade.vert + shaders/crossfade_core.vert + + diff --git a/local/recipes/kde/kwin/source/src/plugins/screentransform/shaders/crossfade.frag b/local/recipes/kde/kwin/source/src/plugins/screentransform/shaders/crossfade.frag new file mode 100644 index 0000000000..3861b04358 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screentransform/shaders/crossfade.frag @@ -0,0 +1,12 @@ +uniform sampler2D previousTexture; +uniform sampler2D currentTexture; +uniform float blendFactor; + +varying vec2 texcoord0; + +void main() +{ + vec4 previous = texture2D(previousTexture, texcoord0); + vec4 current = texture2D(currentTexture, texcoord0); + gl_FragColor = mix(previous, current, blendFactor); +} diff --git a/local/recipes/kde/kwin/source/src/plugins/screentransform/shaders/crossfade.vert b/local/recipes/kde/kwin/source/src/plugins/screentransform/shaders/crossfade.vert new file mode 100644 index 0000000000..9bc7e761a6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screentransform/shaders/crossfade.vert @@ -0,0 +1,12 @@ +uniform mat4 modelViewProjectionMatrix; + +attribute vec2 position; +attribute vec2 texcoord; + +varying vec2 texcoord0; + +void main() +{ + gl_Position = modelViewProjectionMatrix * vec4(position, 0.0, 1.0); + texcoord0 = texcoord; +} diff --git a/local/recipes/kde/kwin/source/src/plugins/screentransform/shaders/crossfade_core.frag b/local/recipes/kde/kwin/source/src/plugins/screentransform/shaders/crossfade_core.frag new file mode 100644 index 0000000000..7ed60cfecf --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screentransform/shaders/crossfade_core.frag @@ -0,0 +1,16 @@ +#version 140 + +uniform sampler2D previousTexture; +uniform sampler2D currentTexture; +uniform float blendFactor; + +in vec2 texcoord0; + +out vec4 fragColor; + +void main() +{ + vec4 previous = texture(previousTexture, texcoord0); + vec4 current = texture(currentTexture, texcoord0); + fragColor = mix(previous, current, blendFactor); +} diff --git a/local/recipes/kde/kwin/source/src/plugins/screentransform/shaders/crossfade_core.vert b/local/recipes/kde/kwin/source/src/plugins/screentransform/shaders/crossfade_core.vert new file mode 100644 index 0000000000..f70884b666 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/screentransform/shaders/crossfade_core.vert @@ -0,0 +1,14 @@ +#version 140 + +uniform mat4 modelViewProjectionMatrix; + +in vec2 position; +in vec2 texcoord; + +out vec2 texcoord0; + +void main() +{ + gl_Position = modelViewProjectionMatrix * vec4(position, 0.0, 1.0); + texcoord0 = texcoord; +} diff --git a/local/recipes/kde/kwin/source/src/plugins/sessionquit/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/sessionquit/CMakeLists.txt new file mode 100644 index 0000000000..5849ac8ae2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/sessionquit/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_scripted_effect(sessionquit package) diff --git a/local/recipes/kde/kwin/source/src/plugins/sessionquit/package/contents/code/main.js b/local/recipes/kde/kwin/source/src/plugins/sessionquit/package/contents/code/main.js new file mode 100644 index 0000000000..6b73fd90b4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/sessionquit/package/contents/code/main.js @@ -0,0 +1,32 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2019 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var quitEffect = { + closed: function (window) { + if (!window.desktopWindow || effects.sessionState != Globals.Quitting) { + return; + } + if (!effect.grab(window, Effect.WindowClosedGrabRole)) { + return; + } + window.outAnimation = animate({ + window: window, + duration: 30 * 1000, // 30 seconds should be long enough for any shutdown + type: Effect.Generic, // do nothing, just hold a reference + from: 0.0, + to: 0.0 + }); + }, + init: function () { + effects.windowClosed.connect(quitEffect.closed); + } +}; +quitEffect.init(); diff --git a/local/recipes/kde/kwin/source/src/plugins/sessionquit/package/metadata.json b/local/recipes/kde/kwin/source/src/plugins/sessionquit/package/metadata.json new file mode 100644 index 0000000000..49c4f537a4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/sessionquit/package/metadata.json @@ -0,0 +1,155 @@ +{ + "KPackageStructure": "KWin/Effect", + "KPlugin": { + "Authors": [ + { + "Email": "davidedmundson@kde.org", + "Name": "David Edmundson", + "Name[ar]": "ديفيد إدموندسون", + "Name[be]": "David Edmundson", + "Name[bg]": "David Edmundson", + "Name[ca@valencia]": "David Edmundson", + "Name[ca]": "David Edmundson", + "Name[cs]": "David Edmundson", + "Name[da]": "David Edmundson", + "Name[de]": "David Edmundson", + "Name[en_GB]": "David Edmundson", + "Name[eo]": "David Edmundson", + "Name[es]": "David Edmundson", + "Name[et]": "David Edmundson", + "Name[eu]": "David Edmundson", + "Name[fi]": "David Edmundson", + "Name[fr]": "David Edmundson", + "Name[ga]": "David Edmundson", + "Name[gl]": "David Edmundson", + "Name[he]": "דיויד אדמונדסון", + "Name[hu]": "David Edmundson", + "Name[ia]": "David Edmundson", + "Name[id]": "David Edmundson", + "Name[is]": "David Edmundson", + "Name[it]": "David Edmundson", + "Name[ja]": "David Edmundson", + "Name[ka]": "დავიდ ედმუნდსონი", + "Name[ko]": "David Edmundson", + "Name[lt]": "David Edmundson", + "Name[lv]": "David Edmundson", + "Name[nb]": "David Edmundson", + "Name[nl]": "David Edmundson", + "Name[nn]": "David Edmundson", + "Name[pl]": "David Edmundson", + "Name[pt]": "David Edmundson", + "Name[pt_BR]": "David Edmundson", + "Name[ro]": "David Edmundson", + "Name[ru]": "David Edmundson", + "Name[sa]": "डेविड् एडमण्ड्सनः", + "Name[sk]": "David Edmundson", + "Name[sl]": "David Edmundson", + "Name[sv]": "David Edmundson", + "Name[ta]": "டேவிட் எட்மண்டுஸன்", + "Name[tr]": "David Edmundson", + "Name[uk]": "David Edmundson", + "Name[vi]": "David Edmundson", + "Name[zh_CN]": "David Edmundson", + "Name[zh_TW]": "David Edmundson" + } + ], + "Category": "Appearance", + "Description": "Keep the desktop background alive during logout until the end", + "Description[ar]": "حافظ على خلفية سطح المكتب حتى نهاية عملية الخروج", + "Description[be]": "Захаванне фону працоўнага стала падчас выхаду", + "Description[bg]": "Показване на фона на работния плот до края на излизането от сесия", + "Description[ca@valencia]": "Manté viu el fons de l'escriptori durant l'eixida de la sessió fins al final", + "Description[ca]": "Manté viu el fons de l'escriptori durant la sortida de la sessió fins al final", + "Description[da]": "Holder skrivebordsbaggrunden i live under log ud helt til slut", + "Description[de]": "Beim Abmelden den Hintergrund der Arbeitsfläche bis zum Schluss behalten", + "Description[en_GB]": "Keep the desktop background alive during logout until the end", + "Description[eo]": "Konservu la labortablan fonon dum elsaluto ĝis la fino", + "Description[es]": "Mantener vivo el fondo del escritorio hasta el final durante el cierre de sesión", + "Description[et]": "Töölaua taust hoitakse väljalogimisel alles kuni lõpuni", + "Description[eu]": "Eutsi mahaigaineko atzeko-planoa bizirik saio-ixten bukaera arte", + "Description[fi]": "Pitää ulos kirjauduttaessa työpöydän elävänä loppuun asti", + "Description[fr]": "Conserver l'arrière-plan du bureau actif jusqu'à la fin de la déconnexion", + "Description[gl]": "Manter vivo o fondo de escritorio durante a saída ata o final.", + "Description[he]": "משאיר את טפט שולחן העבודה חי במהלך יציאה עד הסוף", + "Description[hu]": "Az asztal hátterének megtartása a kijelentkezés végéig", + "Description[ia]": "Mantene le fundo de criptorio vive durante le clausura de session usque le fin", + "Description[id]": "Jaga latar belakang desktop tetap nyala selama keluar log sampai selesai", + "Description[is]": "Halda skjáborðsbakgrunninum lifandi í útskráningu alveg til enda", + "Description[it]": "Mantieni attivo lo sfondo del desktop durante la chiusura della sessione fino alla fine", + "Description[ja]": "ログアウト時最後までデスクトップの背景を表示し続けます", + "Description[ka]": "სამუშაო მაგიდის ფონური სურათის ცოცხლად დატოვება გასვლისას, ბოლო წამამდე", + "Description[ko]": "로그아웃이 끝날 때까지 데스크톱 배경 그림 유지", + "Description[lt]": "Palikti iki pat galo atsijungimo metu darbalaukio foną reaguojantį", + "Description[lv]": "Izrakstoties darbvirsmas fonu līdz pēdējam brīdim padarīt aktīvu", + "Description[nb]": "Hold skrivebordsbakgrunnen i live helt til utlogginga fullføres", + "Description[nl]": "De achtergrond van het bureaublad tot het einde levend houden gedurende afmelden", + "Description[nn]": "Hald skrivebordsbakgrunnen i live heilt til utlogginga er fullført", + "Description[pl]": "Utrzymuj tło pulpitu podczas wylogowania, aż do końca", + "Description[pt]": "Manter activo o fundo do ecrã durante o encerramento até ao fim", + "Description[pt_BR]": "Manter o plano de fundo da área de trabalho ativo durante o logout até o final", + "Description[ro]": "Păstrează viu fundalul biroului în timpul ieșirii din sesiune până la capăt", + "Description[ru]": "Сохранение фона рабочего стола до окончания завершения сеанса", + "Description[sa]": "अन्त्यपर्यन्तं लॉगआउट्-काले डेस्कटॉप्-पृष्ठभूमिं जीवितं कुर्वन्तु", + "Description[sk]": "Ponechanie pozadia pracovnej plochy počas odhlásenia až do konca", + "Description[sl]": "Ohranite ozadje namizja dejavno med odjavo vse do konca", + "Description[sv]": "Behåll skrivbordets bakgrund levande under utloggning till slutet", + "Description[ta]": "வெளியேறும்போது கடைசிவரை பணிமேடையின் பின்புல படத்தை வைத்திருக்கும்", + "Description[tr]": "Oturum kapatma sırasında masaüstü arka planını sonuna kadar canlı tut", + "Description[uk]": "Не вимикати тло стільниці до кінця процедури виходу із облікового запису", + "Description[vi]": "Giữ nền bàn làm việc hiện cho đến cuối quá trình đăng xuất", + "Description[zh_CN]": "注销时继续显示桌面背景,直到完全退出会话", + "Description[zh_TW]": "直到登出完成前皆保留桌面背景圖片", + "EnabledByDefault": true, + "Icon": "preferences-system-windows-effect-logout", + "Id": "sessionquit", + "License": "GPL", + "Name": "Session Quit", + "Name[ar]": "انتهاء الجلسة", + "Name[be]": "Выхад з сеанса", + "Name[bg]": "Изход от сесията", + "Name[ca@valencia]": "Eixida de la sessió", + "Name[ca]": "Sortida de la sessió", + "Name[cs]": "Sezení bylo ukončeno", + "Name[da]": "Session afsluttet", + "Name[de]": "Sitzungsende", + "Name[en_GB]": "Session Quit", + "Name[eo]": "Session Quit", + "Name[es]": "Salir de la sesión", + "Name[et]": "Seansist väljumine", + "Name[eu]": "Saiotik irtetea", + "Name[fi]": "Istunnon lopetus", + "Name[fr]": "Fermeture de la session", + "Name[gl]": "Saída de sesión", + "Name[he]": "יציאה מהפעלה", + "Name[hu]": "Kilépés a munkamenetből", + "Name[ia]": "Abandona session", + "Name[is]": "Hætta setu", + "Name[it]": "Chiusura sessione", + "Name[ja]": "セッション退出", + "Name[ka]": "სესიიდან გასვლა", + "Name[ko]": "세션 종료", + "Name[lt]": "Išėjimas iš seanso", + "Name[lv]": "Iziešana no sesijas", + "Name[nb]": "Avslutt økt", + "Name[nl]": "Afsluiten van sessie", + "Name[nn]": "Avslutt økt", + "Name[pl]": "Opuszczenie sesji", + "Name[pt]": "Sair da Sessão", + "Name[pt_BR]": "Saída da sessão", + "Name[ro]": "Părăsirea sesiunii", + "Name[ru]": "Завершение сеанса", + "Name[sa]": "सत्रं त्यजतु", + "Name[sk]": "Ukončenie sedenia", + "Name[sl]": "Zaključek seje", + "Name[sv]": "Avsluta session", + "Name[ta]": "அமர்வு வெளியேற்றம்", + "Name[tr]": "Oturum Çıkışı", + "Name[uk]": "Вихід з сеансу", + "Name[vi]": "Thoát phiên", + "Name[zh_CN]": "会话退出优化", + "Name[zh_TW]": "離開工作階段" + }, + "X-KDE-Ordering": 40, + "X-KWin-Internal": "true", + "X-Plasma-API": "javascript" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/shakecursor/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/shakecursor/CMakeLists.txt new file mode 100644 index 0000000000..95caa22d02 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/shakecursor/CMakeLists.txt @@ -0,0 +1,17 @@ +kwin_add_builtin_effect(shakecursor) + +target_sources(shakecursor PRIVATE + main.cpp + shakecursor.cpp + shakedetector.cpp +) + +kconfig_add_kcfg_files(shakecursor + shakecursorconfig.kcfgc +) + +target_link_libraries(shakecursor PRIVATE + kwin + + KF6::ConfigGui +) diff --git a/local/recipes/kde/kwin/source/src/plugins/shakecursor/main.cpp b/local/recipes/kde/kwin/source/src/plugins/shakecursor/main.cpp new file mode 100644 index 0000000000..b0df83ce7c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/shakecursor/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "plugins/shakecursor/shakecursor.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(ShakeCursorEffect, + "metadata.json.stripped", + return ShakeCursorEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/shakecursor/metadata.json b/local/recipes/kde/kwin/source/src/plugins/shakecursor/metadata.json new file mode 100644 index 0000000000..53f3a3fd01 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/shakecursor/metadata.json @@ -0,0 +1,95 @@ +{ + "KPlugin": { + "Category": "Accessibility", + "Description": "Makes the cursor larger when the pointer is quickly moved back and forth", + "Description[ar]": "يجعل المؤشر أكبر عند تحريك المؤشر بسرعة للخلف وللأمام", + "Description[be]": "Робіць курсор большым, калі паказальнік хутка перамяшчаецца наперад і назад", + "Description[bg]": "Уголемява курсора, когато курсорът се мести бързо напред и назад", + "Description[ca@valencia]": "Fa el cursor més gran quan el punter es mou ràpidament cap avant i cap arrere", + "Description[ca]": "Fa el cursor més gran quan el punter es mou ràpidament cap endavant i cap enrere", + "Description[da]": "Gør markøren større når den hurtigt bevæges frem og tilbage", + "Description[de]": "Vergrößert den Zeiger, wenn er schnell hin und her bewegt wird", + "Description[en_GB]": "Makes the cursor larger when the pointer is quickly moved back and forth", + "Description[eo]": "Pligrandigas la kursoron kiam la montrilo estas rapide movita tien kaj reen", + "Description[es]": "Agranda el cursor cuando el puntero se mueve rápidamente adelante y atrás", + "Description[eu]": "Kurtsorea handitzen du erakuslea aurrera eta atzera azkar mugitzen denean", + "Description[fi]": "Suurentaa kohdistinta liikutettaessa osoitinta nopeasti edestakaisin", + "Description[fr]": "Agrandit le curseur lorsque le pointeur est rapidement déplacé d'avant en arrière", + "Description[gl]": "Aumenta o cursor ao movelo rapidamente dun lado a outro.", + "Description[he]": "מגדיל את הסמן בעת הזזת המצביע הלוך ושוב במהירות", + "Description[hu]": "Nagyobbá teszi a kurzort, amikor a mutatót gyorsan oda-vissza mozgatják", + "Description[ia]": "Face le cursor plus grande quando le punctator es rapidemente movite retro e avante", + "Description[id]": "Buat kursor lebih besar ketika pointer digerakkan maju mundur secara cepat", + "Description[is]": "Stækkar bendilinn þegar hann er færður snöggt fram og til baka", + "Description[it]": "Ingrandisce il cursore quando il puntatore viene spostato rapidamente avanti e indietro", + "Description[ja]": "ポインタが素早く動かされたときにカーソルを拡大します", + "Description[ka]": "კურსორის გადიდება, როცა მას წინ და უკან სწრაფად გააქანებთ", + "Description[ko]": "포인터를 빠르게 흔들 때 커서 확대", + "Description[lt]": "Padaro pelės žymeklį didesnį, kai rodyklė judinama pirmyn ir atgal", + "Description[lv]": "Padara kursoru lielāku, to ātri kustinot uz priekšu un atpakaļ", + "Description[nb]": "Gjør musepekeren større når du rister den frem og tilbake.", + "Description[nl]": "Maakt de cursor groter wanneer de aanwijzer snel heen en weer wordt bewogen", + "Description[nn]": "Gjer musepeikaren større når du ristar han att og fram", + "Description[pl]": "Powiększ wskaźnik, gdy przemieści się on szybko z jednego miejsca na drugie", + "Description[pt_BR]": "Aumenta o tamanho do cursor quando o ponteiro é movido rapidamente para frente e para trás", + "Description[ro]": "Mărește cursorul când indicatorul e mișcat rapid înainte și înapoi", + "Description[ru]": "Увеличение размера курсора при его быстром перемещении вперёд и назад", + "Description[sa]": "यदा सूचकः शीघ्रं अग्रे पश्चात् च गच्छति तदा कर्सरं बृहत्तरं करोति", + "Description[sk]": "Zväčší kurzor pri rýchlom pohybe ukazovateľa tam a späť", + "Description[sl]": "Poveča kazalec, kadar se kazalec hitro premika naprej in nazaj", + "Description[sv]": "Gör markören större när pekaren snabbt flyttas fram och tillbaka", + "Description[ta]": "சுட்டியை வேகமாக ஆட்டும்போது சுட்டிக்குறியைப் பெரிதாக்கும்", + "Description[tr]": "İşaretçi tez ileri geri hareket ettirildiğinde imleci büyütür", + "Description[uk]": "Робити курсор більшим, коли вказівник швидко рухається туди-сюди", + "Description[zh_CN]": "快速往返移动指针后加大显示光标", + "Description[zh_TW]": "快速來回移動游標時讓游標變大", + "EnabledByDefault": true, + "License": "GPL", + "Name": "Shake Cursor", + "Name[ar]": "اهتزاز المؤشر", + "Name[be]": "Страсянуць курсор", + "Name[bg]": "Разклащане на курсора", + "Name[ca@valencia]": "Sacsa el cursor", + "Name[ca]": "Sacseja el cursor", + "Name[cs]": "Zatřást kurzorem", + "Name[da]": "Ryst markør", + "Name[de]": "Zeiger schütteln", + "Name[en_GB]": "Shake Cursor", + "Name[eo]": "Skui Kursoron", + "Name[es]": "Sacudir el cursor", + "Name[eu]": "Astindu kurtsorea", + "Name[fi]": "Ravista kohdistinta", + "Name[fr]": "Agiter le pointeur", + "Name[gl]": "Axitar o cursor", + "Name[he]": "ניעור סמן", + "Name[hu]": "Kurzor rázása", + "Name[ia]": "Agita Cursor", + "Name[id]": "Guncang Kursor", + "Name[is]": "Hrista bendil", + "Name[it]": "Scuoti il cursore", + "Name[ja]": "カーソルを前後に動かす", + "Name[ka]": "კურსორის ჯანჯღარი", + "Name[ko]": "커서 흔들기", + "Name[lt]": "Pakratyti pelės žymeklį", + "Name[lv]": "Pakratīt kursoru", + "Name[nb]": "Rist på pekeren", + "Name[nl]": "Cursor schudden", + "Name[nn]": "Rist på peikaren", + "Name[pl]": "Trzęsący się wskaźnik", + "Name[pt_BR]": "Agitar cursor", + "Name[ro]": "Scutură cursorul", + "Name[ru]": "Увеличение курсора", + "Name[sa]": "शेक कर्सर", + "Name[sk]": "Zatriasť kurzorom", + "Name[sl]": "Stresaj kazalko", + "Name[sv]": "Skaka markör", + "Name[ta]": "சுட்டிக்குறியை ஆட்டு", + "Name[tr]": "İmleci Salla", + "Name[uk]": "Бовтанка курсора", + "Name[zh_CN]": "抖动后放大光标", + "Name[zh_TW]": "甩動游標" + }, + "org.kde.kwin.effect": { + "internal": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/shakecursor/shakecursor.cpp b/local/recipes/kde/kwin/source/src/plugins/shakecursor/shakecursor.cpp new file mode 100644 index 0000000000..a531f10be5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/shakecursor/shakecursor.cpp @@ -0,0 +1,162 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "plugins/shakecursor/shakecursor.h" +#include "cursor.h" +#include "cursorsource.h" +#include "effect/effecthandler.h" +#include "input_event.h" +#include "plugins/shakecursor/shakecursorconfig.h" +#include "pointer_input.h" +#include "scene/imageitem.h" +#include "scene/itemrenderer.h" +#include "scene/workspacescene.h" + +namespace KWin +{ + +ShakeCursorItem::ShakeCursorItem(const CursorTheme &theme, Item *parent) + : Item(parent) +{ + m_source = std::make_unique(); + m_source->setTheme(theme); + m_source->setShape(Qt::ArrowCursor); + + refresh(); + connect(m_source.get(), &CursorSource::changed, this, &ShakeCursorItem::refresh); +} + +void ShakeCursorItem::refresh() +{ + if (!m_imageItem) { + m_imageItem = scene()->renderer()->createImageItem(this); + } + m_imageItem->setImage(m_source->image()); + m_imageItem->setPosition(-m_source->hotspot()); + m_imageItem->setSize(m_source->image().deviceIndependentSize()); +} + +ShakeCursorEffect::ShakeCursorEffect() + : m_cursor(Cursors::self()->mouse()) +{ + input()->installInputEventSpy(this); + + m_deflateTimer.setSingleShot(true); + connect(&m_deflateTimer, &QTimer::timeout, this, &ShakeCursorEffect::deflate); + + connect(&m_scaleAnimation, &QVariantAnimation::valueChanged, this, [this]() { + magnify(m_scaleAnimation.currentValue().toReal()); + }); + + ShakeCursorConfig::instance(effects->config()); + reconfigure(ReconfigureAll); +} + +ShakeCursorEffect::~ShakeCursorEffect() +{ + magnify(1.0); +} + +bool ShakeCursorEffect::supported() +{ + return effects->isOpenGLCompositing(); +} + +bool ShakeCursorEffect::isActive() const +{ + return m_currentMagnification != 1.0; +} + +void ShakeCursorEffect::reconfigure(ReconfigureFlags flags) +{ + ShakeCursorConfig::self()->read(); + + m_shakeDetector.setInterval(ShakeCursorConfig::timeInterval()); + m_shakeDetector.setSensitivity(ShakeCursorConfig::sensitivity()); +} + +void ShakeCursorEffect::inflate() +{ + qreal magnification; + if (m_targetMagnification == 1.0) { + magnification = ShakeCursorConfig::magnification(); + } else { + magnification = m_targetMagnification + ShakeCursorConfig::overMagnification(); + } + + animateTo(magnification); +} + +void ShakeCursorEffect::deflate() +{ + animateTo(1.0); +} + +void ShakeCursorEffect::animateTo(qreal magnification) +{ + if (m_targetMagnification != magnification) { + m_scaleAnimation.stop(); + + m_scaleAnimation.setStartValue(m_currentMagnification); + m_scaleAnimation.setEndValue(magnification); + m_scaleAnimation.setDuration(200); // ignore animation speed, it's not an animation from user perspective + m_scaleAnimation.setEasingCurve(QEasingCurve::InOutCubic); + m_scaleAnimation.start(); + + m_targetMagnification = magnification; + } +} + +void ShakeCursorEffect::pointerMotion(PointerMotionEvent *event) +{ + if (event->buttons != Qt::NoButton || event->warp) { + m_shakeDetector.reset(); + return; + } + + if (input()->pointer()->isConstrained()) { + return; + } + + if (m_shakeDetector.update(event)) { + inflate(); + m_deflateTimer.start(2000); + } +} + +void ShakeCursorEffect::magnify(qreal magnification) +{ + if (magnification == 1.0) { + m_currentMagnification = 1.0; + if (m_cursorItem) { + m_cursorItem.reset(); + effects->showCursor(); + } + } else { + m_currentMagnification = magnification; + + if (!m_cursorItem) { + effects->hideCursor(); + + const qreal maxScale = ShakeCursorConfig::magnification() + 8 * ShakeCursorConfig::overMagnification(); + const CursorTheme originalTheme = input()->pointer()->cursorTheme(); + if (m_cursorTheme.name() != originalTheme.name() || m_cursorTheme.size() != originalTheme.size() || m_cursorTheme.devicePixelRatio() != maxScale) { + m_cursorTheme = CursorTheme(originalTheme.name(), originalTheme.size(), maxScale); + } + + m_cursorItem = std::make_unique(m_cursorTheme, effects->scene()->overlayItem()); + m_cursorItem->setPosition(m_cursor->pos()); + connect(m_cursor, &Cursor::posChanged, m_cursorItem.get(), [this]() { + m_cursorItem->setPosition(m_cursor->pos()); + }); + } + m_cursorItem->setTransform(QTransform::fromScale(magnification, magnification)); + } +} + +} // namespace KWin + +#include "moc_shakecursor.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/shakecursor/shakecursor.h b/local/recipes/kde/kwin/source/src/plugins/shakecursor/shakecursor.h new file mode 100644 index 0000000000..994c590e49 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/shakecursor/shakecursor.h @@ -0,0 +1,71 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/effect.h" +#include "input_event_spy.h" +#include "plugins/shakecursor/shakedetector.h" +#include "scene/cursoritem.h" +#include "utils/cursortheme.h" + +#include +#include + +namespace KWin +{ + +class Cursor; +class CursorItem; +class ShapeCursorSource; + +class ShakeCursorItem : public Item +{ + Q_OBJECT + +public: + ShakeCursorItem(const CursorTheme &theme, Item *parent); + +private: + void refresh(); + + std::unique_ptr m_imageItem; + std::unique_ptr m_source; +}; + +class ShakeCursorEffect : public Effect, public InputEventSpy +{ + Q_OBJECT + +public: + ShakeCursorEffect(); + ~ShakeCursorEffect() override; + + static bool supported(); + + bool isActive() const override; + void reconfigure(ReconfigureFlags flags) override; + void pointerMotion(PointerMotionEvent *event) override; + +private: + void magnify(qreal magnification); + + void inflate(); + void deflate(); + void animateTo(qreal magnification); + + QTimer m_deflateTimer; + QVariantAnimation m_scaleAnimation; + ShakeDetector m_shakeDetector; + + Cursor *m_cursor; + std::unique_ptr m_cursorItem; + CursorTheme m_cursorTheme; + qreal m_targetMagnification = 1.0; + qreal m_currentMagnification = 1.0; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/shakecursor/shakecursorconfig.kcfg b/local/recipes/kde/kwin/source/src/plugins/shakecursor/shakecursorconfig.kcfg new file mode 100644 index 0000000000..fb355248cf --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/shakecursor/shakecursorconfig.kcfg @@ -0,0 +1,21 @@ + + + + + + 1000 + + + 4 + + + 3 + + + 1 + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/shakecursor/shakecursorconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/shakecursor/shakecursorconfig.kcfgc new file mode 100644 index 0000000000..76b2117589 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/shakecursor/shakecursorconfig.kcfgc @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2021 Vlad Zahorodnii +# +# SPDX-License-Identifier: CC0-1.0 + +File=shakecursorconfig.kcfg +ClassName=ShakeCursorConfig +NameSpace=KWin +Singleton=true diff --git a/local/recipes/kde/kwin/source/src/plugins/shakecursor/shakedetector.cpp b/local/recipes/kde/kwin/source/src/plugins/shakecursor/shakedetector.cpp new file mode 100644 index 0000000000..76d8de1cca --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/shakecursor/shakedetector.cpp @@ -0,0 +1,116 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "shakedetector.h" +#include "input_event.h" + +#include + +namespace KWin +{ + +ShakeDetector::ShakeDetector() +{ +} + +quint64 ShakeDetector::interval() const +{ + return m_interval.count(); +} + +void ShakeDetector::setInterval(quint64 interval) +{ + m_interval = std::chrono::milliseconds(interval); +} + +qreal ShakeDetector::sensitivity() const +{ + return m_sensitivity; +} + +void ShakeDetector::setSensitivity(qreal sensitivity) +{ + m_sensitivity = sensitivity; +} + +static inline bool sameSign(qreal a, qreal b) +{ + constexpr qreal tolerance = 1; + // movements less than tolerance count as movements in any direction + return (a >= -tolerance && b >= -tolerance) || (a <= tolerance && b <= tolerance); +} + +void ShakeDetector::reset() +{ + m_history.clear(); +} + +bool ShakeDetector::update(PointerMotionEvent *event) +{ + // Prune the old entries in the history. + auto it = m_history.begin(); + for (; it != m_history.end(); ++it) { + if (event->timestamp - it->timestamp < m_interval) { + break; + } + } + if (it != m_history.begin()) { + m_history.erase(m_history.begin(), it); + } + + if (m_history.size() >= 2) { + HistoryItem &last = m_history[m_history.size() - 1]; + const HistoryItem &prev = m_history[m_history.size() - 2]; + if (sameSign(last.position.x() - prev.position.x(), event->position.x() - last.position.x()) && sameSign(last.position.y() - prev.position.y(), event->position.y() - last.position.y())) { + last = HistoryItem{ + .position = event->position, + .timestamp = event->timestamp, + }; + return false; + } + } + + m_history.emplace_back(HistoryItem{ + .position = event->position, + .timestamp = event->timestamp, + }); + + qreal left = m_history[0].position.x(); + qreal top = m_history[0].position.y(); + qreal right = m_history[0].position.x(); + qreal bottom = m_history[0].position.y(); + qreal distance = 0; + + for (size_t i = 1; i < m_history.size(); ++i) { + // Compute the length of the mouse path. + const qreal deltaX = m_history[i].position.x() - m_history[i - 1].position.x(); + const qreal deltaY = m_history[i].position.y() - m_history[i - 1].position.y(); + distance += std::sqrt(deltaX * deltaX + deltaY * deltaY); + + // Compute the bounds of the mouse path. + left = std::min(left, m_history[i].position.x()); + top = std::min(top, m_history[i].position.y()); + right = std::max(right, m_history[i].position.x()); + bottom = std::max(bottom, m_history[i].position.y()); + } + + const qreal boundsWidth = right - left; + const qreal boundsHeight = bottom - top; + const qreal diagonal = std::sqrt(boundsWidth * boundsWidth + boundsHeight * boundsHeight); + if (diagonal < 100) { + return false; + } + + const qreal shakeFactor = distance / diagonal; + if (shakeFactor > m_sensitivity) { + m_history.clear(); + return true; + } + + return false; +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/shakecursor/shakedetector.h b/local/recipes/kde/kwin/source/src/plugins/shakecursor/shakedetector.h new file mode 100644 index 0000000000..038899fd39 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/shakecursor/shakedetector.h @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include +#include + +namespace KWin +{ + +struct PointerMotionEvent; + +/** + * The ShakeDetector type provides a way to detect pointer shake gestures. + * + * Shake gestures are detected by comparing the length of the trail of the cursor within past N milliseconds + * with the length of the diagonal of the bounding rectangle of the trail. If the trail is longer + * than the diagonal by certain preconfigured factor, it's assumed that the user shook the pointer. + */ +class ShakeDetector +{ +public: + ShakeDetector(); + + void reset(); + bool update(PointerMotionEvent *event); + + quint64 interval() const; + void setInterval(quint64 interval); + + qreal sensitivity() const; + void setSensitivity(qreal sensitivity); + +private: + struct HistoryItem + { + QPointF position; + std::chrono::microseconds timestamp; + }; + + std::deque m_history; + std::chrono::milliseconds m_interval = std::chrono::milliseconds(1000); + qreal m_sensitivity = 4; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/sheet/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/sheet/CMakeLists.txt new file mode 100644 index 0000000000..1074730f51 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/sheet/CMakeLists.txt @@ -0,0 +1,16 @@ +####################################### +# Effect + +# Source files +set(sheet_SOURCES + main.cpp + sheet.cpp +) +kconfig_add_kcfg_files(sheet_SOURCES sheetconfig.kcfgc) + +kwin_add_builtin_effect(sheet ${sheet_SOURCES}) +target_link_libraries(sheet PRIVATE + kwin + + KF6::ConfigGui +) diff --git a/local/recipes/kde/kwin/source/src/plugins/sheet/main.cpp b/local/recipes/kde/kwin/source/src/plugins/sheet/main.cpp new file mode 100644 index 0000000000..a71c29e23c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/sheet/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "sheet.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(SheetEffect, + "metadata.json.stripped", + return SheetEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/sheet/metadata.json b/local/recipes/kde/kwin/source/src/plugins/sheet/metadata.json new file mode 100644 index 0000000000..24b581521a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/sheet/metadata.json @@ -0,0 +1,100 @@ +{ + "KPlugin": { + "Category": "Appearance", + "Description": "Make modal dialogs smoothly fly in and out when they are shown or hidden", + "Description[ar]": "اجعل الحوارات الـ... تطير داخلةً وخارجةً عند إظهارها أو إخفائها", + "Description[az]": "Modal pəncərələr rəvan şəkildə uçuşla göstərilir və ya gizlədilir", + "Description[be]": "Мадальныя дыялогавыя вокны плаўна з'яўляюцца з бацькоўскіх акон і знікаюць у іх, калі яны паказваюцца або хаваюцца", + "Description[bg]": "Създаване на модални диалогови прозорци, които плавно се прелистват, когато се показват или скриват", + "Description[ca@valencia]": "Fa que els diàlegs modals entren o isquen volant quan es mostren o s'oculten", + "Description[ca]": "Fa que els diàlegs modals entrin o surtin volant quan es mostren o s'oculten", + "Description[cs]": "Nechat modální dialogy plynule zmizet/objevit se, pokud jsou zobrazeny resp. skryty", + "Description[da]": "Få modale dialoger til jævnt at flyve ind og ud når de vises eller skjules", + "Description[de]": "Lässt modale Dialogfenster langsam herein- bzw. herausschweben", + "Description[en_GB]": "Make modal dialogues smoothly fly in and out when they are shown or hidden", + "Description[eo]": "Fari modalajn dialogojn glate enflugi kaj eksteren kiam ili estas montritaj aŭ kaŝitaj", + "Description[es]": "Hace que los diálogos modales vuelen suavemente hacia adentro y hacia afuera cuando se muestran u ocultan", + "Description[et]": "Paneb modaalsed dialoogid sujuvalt peale või ära kerima, kui need nähtavale tuuakse või peidetakse", + "Description[eu]": "Egin elkarrizketa-koadro modalak leunki hegan sartu edo irteten daitezen haiek erakutsi edo ezkutatzean", + "Description[fi]": "Lennättää ponnahdusikkunat ruudulle ja ruudulta", + "Description[fr]": "Fait voler progressivement en avant ou en arrière les boites de dialogue modales lors de leurs affichages ou masquages", + "Description[gl]": "Fai entrar e saír voando os diálogos modais cando se amosan ou agochan.", + "Description[he]": "לגרום לחלוניות חוסמות להתעופף פנימה והחוצה כשהן מופיעות או מוסתרות", + "Description[hu]": "Az ablakok folyamatosan áttűnő módon lesznek elrejtve és megjelenítve", + "Description[ia]": "Face que le dialogos modal pote volar dulcemente intra e foras quando illes es monstrate o celate", + "Description[id]": "Buat secara halus sosok dialog terbang ketika ia ditampilkan atau disembunyikan", + "Description[is]": "Láta svarglugga fljúga mjúklega inn og út þegar þeir eru birtir eða faldir", + "Description[it]": "Fai planare le finestre modali quando appaiono e scompaiono", + "Description[ja]": "ダイアログが表示/非表示時に飛んで行ったり飛んで来たりします", + "Description[ka]": "მოდალური დიალოგების შეფრენა/გამოფრენა მათი ჩვენება/დამალვისას", + "Description[ko]": "모달 대화 상자를 보이거나 숨길 때 부드럽게 날아다니도록 합니다", + "Description[lt]": "Padaro, kad modaliniai dialogai glotniai įslinktų ir išslinktų, kai yra rodomi ar slepiami", + "Description[lv]": "Liek modālajiem dialogiem vienmērīgi atlidot un aizlidot, kad tie tiek parādīti vai paslēpti", + "Description[nb]": "Brett undervinduer inn og ut av skjermen", + "Description[nl]": "Laat modale dialogen vloeiend in/uitvliegen als ze worden getoond of verborgen", + "Description[nn]": "Brett undervindauge inn og ut av skjermen", + "Description[pl]": "Okna dialogowe są gładko wyciągane przy otwieraniu i wsuwane przy zamykaniu", + "Description[pt]": "Faz com que as janelas modais voem suavemente quando aparecem ou desaparecem", + "Description[pt_BR]": "Faz as janelas voarem suavemente quando são exibidas ou ocultadas", + "Description[ro]": "Face dialogurile modale să zboare lin când sunt arătate sau ascunse", + "Description[ru]": "Плавное появление и скрытие модальных диалогов", + "Description[sa]": "मोडल संवादाः दर्शिते वा गुप्ते वा सुचारुतया अन्तः बहिः च उड्डीयन्ते इति कुर्वन्तु", + "Description[sk]": "Modálne dialógy sa plynule objavia/zmiznú pri ich zobrazení alebo skrytí", + "Description[sl]": "Modalna okna gladko priletijo in odletijo, kadar se prikažejo ali skrijejo", + "Description[sv]": "Gör att dialogrutor mjukt flyger in eller ut när de visas eller döljs", + "Description[ta]": "துணை சாளரங்கள் காட்டப்படும்போது அவை பறந்து வருவதுபோல் அசைவூட்டும்", + "Description[tr]": "Kipsel iletişim kutuları gösterilirken veya gizlenirken pürüzsüzce uçmalarını sağlar", + "Description[uk]": "Плавне влітання або відлітання вікон під час їх появи або приховування", + "Description[vi]": "Làm các hộp thoại chế độ bay vào và ra một cách êm dịu khi chúng hiện ra và biến mất", + "Description[zh_CN]": "对话框显示/隐藏时绘制过渡动画", + "Description[zh_TW]": "讓情境對話框在顯示或隱藏的時候平滑地飛入或飛出", + "EnabledByDefault": false, + "License": "GPL", + "Name": "Sheet", + "Name[ar]": "الورقة", + "Name[az]": "Vərəq", + "Name[be]": "Табліца", + "Name[bg]": "Лист", + "Name[ca@valencia]": "Full", + "Name[ca]": "Full", + "Name[cs]": "List", + "Name[da]": "Ark", + "Name[de]": "Schweben", + "Name[en_GB]": "Sheet", + "Name[eo]": "Folio", + "Name[es]": "Hoja", + "Name[et]": "Leht", + "Name[eu]": "Orria", + "Name[fi]": "Arkki", + "Name[fr]": "Feuille", + "Name[gl]": "Folla", + "Name[he]": "גיליון", + "Name[hu]": "Fólia", + "Name[ia]": "Folio", + "Name[id]": "Lembar", + "Name[is]": "Blað", + "Name[it]": "Foglio", + "Name[ja]": "シート", + "Name[ka]": "ფურცელი", + "Name[ko]": "시트", + "Name[lt]": "Lakštas", + "Name[lv]": "Loksne", + "Name[nb]": "Ark", + "Name[nl]": "Werkblad", + "Name[nn]": "Ark", + "Name[pl]": "Arkusz", + "Name[pt]": "Folha", + "Name[pt_BR]": "Folha", + "Name[ro]": "Foaie", + "Name[ru]": "Лист", + "Name[sa]": "आस्तरण", + "Name[sk]": "List", + "Name[sl]": "List", + "Name[sv]": "Blad", + "Name[tr]": "Sayfa", + "Name[uk]": "Аркуш", + "Name[vi]": "Tấm mỏng", + "Name[zh_CN]": "对话框显隐过渡", + "Name[zh_TW]": "薄紙" + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/sheet/sheet.cpp b/local/recipes/kde/kwin/source/src/plugins/sheet/sheet.cpp new file mode 100644 index 0000000000..4a0ada6342 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/sheet/sheet.cpp @@ -0,0 +1,212 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Philip Falkner + SPDX-FileCopyrightText: 2009 Martin Gräßlin + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +// own +#include "sheet.h" + +// KConfigSkeleton +#include "sheetconfig.h" + +#include "core/renderviewport.h" +#include "effect/effecthandler.h" + +// Qt +#include +#include + +using namespace std::chrono_literals; + +namespace KWin +{ + +SheetEffect::SheetEffect() +{ + SheetConfig::instance(effects->config()); + reconfigure(ReconfigureAll); + + connect(effects, &EffectsHandler::windowAdded, this, &SheetEffect::slotWindowAdded); + connect(effects, &EffectsHandler::windowClosed, this, &SheetEffect::slotWindowClosed); +} + +void SheetEffect::reconfigure(ReconfigureFlags flags) +{ + SheetConfig::self()->read(); + + // TODO: Rename AnimationTime config key to Duration. + const double d = animationTime(SheetConfig::animationTime() != 0 + ? std::chrono::milliseconds(SheetConfig::animationTime()) + : 300ms); + m_duration = std::chrono::milliseconds(int(d)); +} + +void SheetEffect::prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) +{ + auto animationIt = m_animations.find(w); + if (animationIt != m_animations.end()) { + (*animationIt).timeLine.advance(presentTime); + data.setTransformed(); + } + + effects->prePaintWindow(view, w, data, presentTime); +} + +void SheetEffect::apply(EffectWindow *window, int mask, WindowPaintData &data, WindowQuadList &quads) +{ + auto animationIt = m_animations.constFind(window); + if (animationIt == m_animations.constEnd()) { + return; + } + + const qreal t = (*animationIt).timeLine.value(); + + const QRectF rect = window->expandedGeometry().translated(-window->pos()); + const float fovY = std::tan(qDegreesToRadians(60.0f) / 2); + const float aspect = rect.width() / rect.height(); + const float zNear = 0.1f; + const float zFar = 100.0f; + + const float yMax = zNear * fovY; + const float yMin = -yMax; + const float xMin = yMin * aspect; + const float xMax = yMax * aspect; + + const float scaleFactor = 1.1 * fovY / yMax; + + QMatrix4x4 matrix; + matrix.viewport(rect); + matrix.frustum(xMin, xMax, yMax, yMin, zNear, zFar); + matrix.translate(xMin * scaleFactor, yMax * scaleFactor, -1.1); + matrix.scale((xMax - xMin) * scaleFactor / rect.width(), -(yMax - yMin) * scaleFactor / rect.height(), 0.001); + matrix.translate(-rect.x(), -rect.y()); + + matrix.scale(1.0, t, t); + matrix.translate(0.0, -interpolate(window->y() - (*animationIt).parentY, 0.0, t)); + + matrix.translate(window->width() / 2, 0); + matrix.rotate(interpolate(60.0, 0.0, t), 1, 0, 0); + matrix.translate(-window->width() / 2, 0); + + for (WindowQuad &quad : quads) { + for (int i = 0; i < 4; ++i) { + const QPointF transformed = matrix.map(QPointF(quad[i].x(), quad[i].y())); + quad[i].setX(transformed.x()); + quad[i].setY(transformed.y()); + } + } + + data.multiplyOpacity(t); +} + +void SheetEffect::postPaintScreen() +{ + for (auto animationIt = m_animations.begin(); animationIt != m_animations.end();) { + EffectWindow *w = animationIt.key(); + w->addRepaintFull(); + if ((*animationIt).timeLine.done()) { + unredirect(w); + animationIt = m_animations.erase(animationIt); + } else { + ++animationIt; + } + } + + if (m_animations.isEmpty()) { + effects->addRepaintFull(); + } + + effects->postPaintScreen(); +} + +bool SheetEffect::isActive() const +{ + return !m_animations.isEmpty(); +} + +bool SheetEffect::supported() +{ + return effects->isOpenGLCompositing() + && effects->animationsSupported(); +} + +void SheetEffect::slotWindowAdded(EffectWindow *w) +{ + if (effects->activeFullScreenEffect()) { + return; + } + + if (!isSheetWindow(w)) { + return; + } + + Animation &animation = m_animations[w]; + animation.deletedRef = EffectWindowDeletedRef(w); + animation.parentY = 0; + animation.timeLine.reset(); + animation.timeLine.setDuration(m_duration); + animation.timeLine.setDirection(TimeLine::Forward); + animation.timeLine.setEasingCurve(QEasingCurve::Linear); + + const auto windows = effects->stackingOrder(); + auto parentIt = std::find_if(windows.constBegin(), windows.constEnd(), + [w](EffectWindow *p) { + return p->findModal() == w; + }); + if (parentIt != windows.constEnd()) { + animation.parentY = (*parentIt)->y(); + } + + w->setData(WindowAddedGrabRole, QVariant::fromValue(static_cast(this))); + + redirect(w); + w->addRepaintFull(); +} + +void SheetEffect::slotWindowClosed(EffectWindow *w) +{ + if (effects->activeFullScreenEffect()) { + return; + } + + if (!isSheetWindow(w) || w->skipsCloseAnimation()) { + return; + } + + Animation &animation = m_animations[w]; + animation.deletedRef = EffectWindowDeletedRef(w); + animation.timeLine.reset(); + animation.parentY = 0; + animation.timeLine.setDuration(m_duration); + animation.timeLine.setDirection(TimeLine::Backward); + animation.timeLine.setEasingCurve(QEasingCurve::Linear); + + const auto windows = effects->stackingOrder(); + auto parentIt = std::find_if(windows.constBegin(), windows.constEnd(), + [w](EffectWindow *p) { + return p->findModal() == w; + }); + if (parentIt != windows.constEnd()) { + animation.parentY = (*parentIt)->y(); + } + + w->setData(WindowClosedGrabRole, QVariant::fromValue(static_cast(this))); + + redirect(w); + w->addRepaintFull(); +} + +bool SheetEffect::isSheetWindow(EffectWindow *w) const +{ + return w->isModal(); +} + +} // namespace KWin + +#include "moc_sheet.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/sheet/sheet.h b/local/recipes/kde/kwin/source/src/plugins/sheet/sheet.h new file mode 100644 index 0000000000..aae23cbaa4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/sheet/sheet.h @@ -0,0 +1,75 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Philip Falkner + SPDX-FileCopyrightText: 2009 Martin Gräßlin + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +// kwineffects +#include "effect/effectwindow.h" +#include "effect/offscreeneffect.h" +#include "effect/timeline.h" + +namespace KWin +{ + +class SheetEffect : public OffscreenEffect +{ + Q_OBJECT + Q_PROPERTY(int duration READ duration) + +public: + SheetEffect(); + + void reconfigure(ReconfigureFlags flags) override; + + void prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) override; + void postPaintScreen() override; + + bool isActive() const override; + int requestedEffectChainPosition() const override; + + static bool supported(); + + int duration() const; + +protected: + void apply(EffectWindow *window, int mask, WindowPaintData &data, WindowQuadList &quads) override; + +private Q_SLOTS: + void slotWindowAdded(EffectWindow *w); + void slotWindowClosed(EffectWindow *w); + +private: + bool isSheetWindow(EffectWindow *w) const; + +private: + std::chrono::milliseconds m_duration; + + struct Animation + { + EffectWindowDeletedRef deletedRef; + TimeLine timeLine; + int parentY; + }; + + QHash m_animations; +}; + +inline int SheetEffect::requestedEffectChainPosition() const +{ + return 60; +} + +inline int SheetEffect::duration() const +{ + return m_duration.count(); +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/sheet/sheet.kcfg b/local/recipes/kde/kwin/source/src/plugins/sheet/sheet.kcfg new file mode 100644 index 0000000000..fc70ed4adb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/sheet/sheet.kcfg @@ -0,0 +1,12 @@ + + + + + + 0 + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/sheet/sheetconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/sheet/sheetconfig.kcfgc new file mode 100644 index 0000000000..239872e0bb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/sheet/sheetconfig.kcfgc @@ -0,0 +1,5 @@ +File=sheet.kcfg +ClassName=SheetConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/local/recipes/kde/kwin/source/src/plugins/showcompositing/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/showcompositing/CMakeLists.txt new file mode 100644 index 0000000000..03bc597453 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/showcompositing/CMakeLists.txt @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: 2022 Arjen Hiemstra +# SPDX-FileCopyrightText: 2024 Xaver Hugl +# +# SPDX-License-Identifier: BSD-3-Clause + +set(showcompositing_SOURCES + main.cpp + showcompositing.cpp +) + +kwin_add_builtin_effect(showcompositing ${showcompositing_SOURCES}) + +ecm_add_qml_module(showcompositing + URI org.kde.kwin.showcompositing + QML_FILES + qml/Main.qml + QT_NO_PLUGIN +) + +target_link_libraries(showcompositing PRIVATE + kwin + KF6::I18n + Qt::Quick +) diff --git a/local/recipes/kde/kwin/source/src/plugins/showcompositing/main.cpp b/local/recipes/kde/kwin/source/src/plugins/showcompositing/main.cpp new file mode 100644 index 0000000000..184928a01d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/showcompositing/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "showcompositing.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(ShowCompositingEffect, + "metadata.json.stripped", + return ShowCompositingEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/showcompositing/metadata.json b/local/recipes/kde/kwin/source/src/plugins/showcompositing/metadata.json new file mode 100644 index 0000000000..6c54506551 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/showcompositing/metadata.json @@ -0,0 +1,94 @@ +{ + "KPlugin": { + "Category": "Tools", + "Description": "Display if compositing is active", + "Description[ar]": "يعرض إذا ما كان التركيب نشطًا", + "Description[be]": "Індыкацыя актыўнасці кампазітынгу", + "Description[bg]": "Показване, ако композирането е активно", + "Description[ca@valencia]": "Mostra si la composició està activa", + "Description[ca]": "Mostra si la composició està activa", + "Description[cs]": "Zobrazit pokud je kompozice aktivní", + "Description[da]": "Vis hvis compositing er aktiv", + "Description[de]": "Anzeige, wenn Compositing aktiv ist", + "Description[en_GB]": "Display if compositing is active", + "Description[eo]": "Montri se kompostado estas aktiva", + "Description[es]": "Mostrar si la composición está activa", + "Description[eu]": "Azaldu konposatzea aktibo dagoen", + "Description[fi]": "Näytä, onko koostaminen käytössä", + "Description[fr]": "Afficher l'état d'activité de la composition", + "Description[gl]": "Indicar se a composición está activada.", + "Description[he]": "להציג אם ניהול חלונאי פעיל", + "Description[hu]": "Megjelenítés, ha a kompozitálás aktív", + "Description[ia]": "Mostra si le composition es active", + "Description[id]": "Tampilkan jika pengomposisian telah aktif", + "Description[is]": "Birta ef skjásamsetning er virk", + "Description[it]": "Visualizza se la composizione è attiva", + "Description[ja]": "コンポジタがアクティブかどうかを表示", + "Description[ka]": "ჩვენება, თუ კომპოზიცია ჩართულია", + "Description[ko]": "컴포지팅 활성화 여부 표시", + "Description[lt]": "Rodyti, ar komponavimas yra aktyvus", + "Description[lv]": "Parādīt, vai kompozitēšanas ir aktīva", + "Description[nb]": "Vis hvis sammensetting er påslått", + "Description[nl]": "Tonen indien samenstellen actief is", + "Description[nn]": "Vis viss samansetjing er påslått", + "Description[pl]": "Pokaż czy komponowanie jest włączone", + "Description[pt_BR]": "Exibir se a composição está ativa", + "Description[ro]": "Arată dacă compoziționarea e activă", + "Description[ru]": "Индикатор активности графических эффектов", + "Description[sa]": "यदि कम्पोजिटिङ्ग् सक्रियम् अस्ति तर्हि प्रदर्शयतु", + "Description[sk]": "Zobraziť, či je kompozícia aktívna", + "Description[sl]": "Prikaži, če je upravljalnik skladnje aktiven", + "Description[sv]": "Visa om sammansättning är aktiv", + "Description[tr]": "Bileşikleştirme etkinse görüntüle", + "Description[uk]": "Показувати, чи активною є композиція", + "Description[zh_CN]": "在显示特效合成处于活动状态时显示", + "Description[zh_TW]": "顯示合成器是否已啟用", + "EnabledByDefault": false, + "License": "GPL", + "Name": "Show Compositing", + "Name[ar]": "إظهار التركيب", + "Name[be]": "Паказ кампазітынгу", + "Name[bg]": "Показване на визуални ефекти", + "Name[ca@valencia]": "Mostra la composició", + "Name[ca]": "Mostra la composició", + "Name[cs]": "Zobrazit kompozici", + "Name[da]": "Vis compositing", + "Name[de]": "Compositing anzeigen", + "Name[en_GB]": "Show Compositing", + "Name[eo]": "Kompostisto", + "Name[es]": "Mostrar composición", + "Name[eu]": "Erakutsi konposatzea", + "Name[fi]": "Näytä koostamistila", + "Name[fr]": "Afficher le compositeur", + "Name[gl]": "Amosar a composición", + "Name[he]": "הצגת ניהול חלונאי", + "Name[hu]": "Kompozitálás megjelenítése", + "Name[ia]": "Monstra Composition", + "Name[id]": "Tampilkan Pengomposisian", + "Name[is]": "Sýna skjásamsetningu", + "Name[it]": "Mostra composizione", + "Name[ja]": "コンポジタを表示", + "Name[ka]": "კომპოზიციის ჩვენება", + "Name[ko]": "컴포지팅 표시", + "Name[lt]": "Rodyti komponavimą", + "Name[lv]": "Rādīt kompozīciju", + "Name[nb]": "Vis sammensetting", + "Name[nl]": "Opstellen tonen", + "Name[nn]": "Vis samansetjing", + "Name[pl]": "Pokaż komponowanie", + "Name[pt_BR]": "Mostrar composição", + "Name[ro]": "Arată compoziționarea", + "Name[ru]": "Индикация графических эффектов", + "Name[sa]": "रचनां दर्शयतु", + "Name[sk]": "Kompozítor", + "Name[sl]": "Prikaži Upravljalnik skladnje", + "Name[sv]": "Visa sammansättning", + "Name[tr]": "Bileşikleştirmeyi Göster", + "Name[uk]": "Показувати композицію", + "Name[zh_CN]": "显示特效合成活动指示器", + "Name[zh_TW]": "顯示合成器狀態" + }, + "org.kde.kwin.effect": { + "internal": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/showcompositing/showcompositing.cpp b/local/recipes/kde/kwin/source/src/plugins/showcompositing/showcompositing.cpp new file mode 100644 index 0000000000..77aec7e793 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/showcompositing/showcompositing.cpp @@ -0,0 +1,55 @@ +/* + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + SPDX-FileCopyrightText: 2022 Arjen Hiemstra + SPDX-FileCopyrightText: 2024 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "showcompositing.h" +#include "core/output.h" +#include "core/renderviewport.h" +#include "effect/effecthandler.h" + +namespace KWin +{ + +ShowCompositingEffect::ShowCompositingEffect() +{ +} + +ShowCompositingEffect::~ShowCompositingEffect() = default; + +void ShowCompositingEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + effects->prePaintScreen(data, presentTime); + if (!m_scene) { + m_scene = std::make_unique(); + m_scene->loadFromModule(QStringLiteral("org.kde.kwin.showcompositing"), QStringLiteral("Main"), {}); + } +} + +void ShowCompositingEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) +{ + effects->paintScreen(renderTarget, viewport, mask, deviceRegion, screen); + const auto rect = viewport.renderRect(); + m_scene->setGeometry(QRect(rect.x() + rect.width() - 300, rect.y(), 300, 150)); + effects->renderOffscreenQuickView(renderTarget, viewport, m_scene.get()); +} + +bool ShowCompositingEffect::supported() +{ + return effects->isOpenGLCompositing(); +} + +bool ShowCompositingEffect::blocksDirectScanout() const +{ + // this is intentionally wrong, as we want direct scanout to change the image + // with this effect + return false; +} + +} // namespace KWin + +#include "moc_showcompositing.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/showcompositing/showcompositing.h b/local/recipes/kde/kwin/source/src/plugins/showcompositing/showcompositing.h new file mode 100644 index 0000000000..db5ca8489e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/showcompositing/showcompositing.h @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + SPDX-FileCopyrightText: 2022 Arjen Hiemstra + SPDX-FileCopyrightText: 2024 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/effect.h" +#include "effect/offscreenquickview.h" + +namespace KWin +{ + +class ShowCompositingEffect : public Effect +{ + Q_OBJECT +public: + ShowCompositingEffect(); + ~ShowCompositingEffect() override; + + void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override; + void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) override; + bool blocksDirectScanout() const override; + + static bool supported(); + +private: + std::unique_ptr m_scene; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/showfps/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/showfps/CMakeLists.txt new file mode 100644 index 0000000000..c268447ceb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/showfps/CMakeLists.txt @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2022 Arjen Hiemstra +# +# SPDX-License-Identifier: BSD-3-Clause + +set(showfps_SOURCES + main.cpp + showfpseffect.cpp +) + +kwin_add_builtin_effect(showfps ${showfps_SOURCES}) + +ecm_add_qml_module(showfps + URI "org.kde.kwin.showfps" + QML_FILES + qml/Main.qml + qml/Fallback.qml + QT_NO_PLUGIN +) + +target_link_libraries(showfps PRIVATE + kwin + + KF6::I18n + + Qt::Quick + ) diff --git a/local/recipes/kde/kwin/source/src/plugins/showfps/main.cpp b/local/recipes/kde/kwin/source/src/plugins/showfps/main.cpp new file mode 100644 index 0000000000..83711a955d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/showfps/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "showfpseffect.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(ShowFpsEffect, + "metadata.json.stripped", + return ShowFpsEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/showfps/metadata.json b/local/recipes/kde/kwin/source/src/plugins/showfps/metadata.json new file mode 100644 index 0000000000..e5f1952e63 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/showfps/metadata.json @@ -0,0 +1,104 @@ +{ + "KPlugin": { + "Category": "Tools", + "Description": "Display KWin's performance in the corner of the screen", + "Description[ar]": "اعرض أداء نوافذ.ك في زاوية من زوايا الشاشة", + "Description[az]": "Ekranın küncündə qrafikanın effektlərin saniyədəki kadrlarının sayını göstərir", + "Description[be]": "Паказ прадукцыйнасці KWin у куце экрана", + "Description[bg]": "Показване на FPS (кадри в секунда) и производителността на KWin в ъгъла на екрана", + "Description[ca@valencia]": "Mostra el rendiment de KWin en el cantó de la pantalla", + "Description[ca]": "Mostra el rendiment del KWin a la cantonada de la pantalla", + "Description[cs]": "Zobrazuje výkon KWin v rohu obrazovky", + "Description[da]": "Vis KWins ydeevne i skærmhjørnet", + "Description[de]": "Zeigt die Leistung von KWin in der Bildschirmecke an", + "Description[en_GB]": "Display KWin's performance in the corner of the screen", + "Description[eo]": "Montri la agadon de KWin en la angulo de la ekrano", + "Description[es]": "Mostrar el rendimiento de KWin en una esquina de la pantalla", + "Description[et]": "KWin'i jõudluse näitamine ekraani servas", + "Description[eu]": "Erakutsi KWin-en performantzia pantailaren izkinan", + "Description[fi]": "Näyttää KWinin suorituskyvyn näytön kulmassa", + "Description[fr]": "Afficher les performances de KWin dans le coin de l'écran", + "Description[gl]": "Amosa o rendemento de KWin nun canto da pantalla.", + "Description[he]": "הצגת הביצועים של KWin בפינת המסך", + "Description[hu]": "A KWin teljesítményének kijelzése a képernyő sarkában", + "Description[ia]": "Monstra le rendimento de KWin in le angulo del schermo", + "Description[id]": "Menampilkan kinerja KWin di pojok layar", + "Description[is]": "Sýna afköst KWin í horni skjásins", + "Description[it]": "Visualizza le prestazioni di KWin in un angolo dello schermo", + "Description[ja]": "KWin のパフォーマンスをスクリーンの角に表示します", + "Description[ka]": "KWin-ის წარმადობის ჩვენება ეკრანის კუთხეში", + "Description[ko]": "KWin의 성능을 화면 구석에 표시합니다", + "Description[lt]": "Rodyti ekrano kampe KWin našumą", + "Description[lv]": "Ekrāna stūrī parādīt „KWin“ veiktspēju", + "Description[nb]": "Vis KWin-ytelsen i et hjørne av skjermen", + "Description[nl]": "Toont de prestaties van KWin in de hoek van het scherm", + "Description[nn]": "Vis KWin-ytinga i eit hjørne av skjermen", + "Description[pl]": "Wyświetla wydajność KWin w narożniku ekranu", + "Description[pt]": "Mostrar a performance do KWin no canto do ecrã", + "Description[pt_BR]": "Mostra o desempenho do KWin no canto da tela", + "Description[ro]": "Afișează performanța KWin în colțul ecranului", + "Description[ru]": "Индикатор производительности эффектов в углу экрана", + "Description[sa]": "स्क्रीनस्य कोणे KWin इत्यस्य प्रदर्शनं प्रदर्शयन्तु", + "Description[sk]": "Zobrazí výkon KWin v rohu obrazovky", + "Description[sl]": "Prikažite odzivnost Kwin v kotu zaslona", + "Description[sv]": "Visa prestanda för Kwin i hörnet av skärmen", + "Description[ta]": "திரையின் ஓர் ஓரத்தில் கேவின்னின் செயல்திறனை காட்டும்", + "Description[tr]": "KWin başarımını pencerenin bir köşesinde görüntüle", + "Description[uk]": "Показ параметра швидкодії KWin (частоти кадрів) у куті екрана", + "Description[vi]": "Hiện hiệu suất hoạt động của KWin ở góc màn hình", + "Description[zh_CN]": "在屏幕一角显示 KWin 的每秒帧数图表", + "Description[zh_TW]": "在螢幕角落顯示 KWin 的效能資訊", + "EnabledByDefault": false, + "License": "GPL", + "Name": "Show FPS", + "Name[ar]": "أظهر عدد الإطارات بالثانية", + "Name[az]": "FPS göstərilsin", + "Name[be]": "Паказваць FPS", + "Name[bg]": "Показване на FPS", + "Name[ca@valencia]": "Mostra els FPS", + "Name[ca]": "Mostra els FPS", + "Name[cs]": "Zobrazit FPS", + "Name[da]": "Vis FPS", + "Name[de]": "Bilder pro Sekunde anzeigen", + "Name[en_GB]": "Show FPS", + "Name[eo]": "Montri FPS", + "Name[es]": "Mostrar FPS", + "Name[et]": "FPS-i näitamine", + "Name[eu]": "Erakutsi FPS", + "Name[fi]": "FPS-näyttö", + "Name[fr]": "Afficher le nombre de trames par seconde", + "Name[gl]": "Amosar os FPS", + "Name[he]": "הצגת תמוניות לשנייה", + "Name[hu]": "Képkockaszámláló", + "Name[ia]": "Monstra FPS", + "Name[id]": "Tampilkan FPS", + "Name[is]": "Sýna ramma á sekúndu", + "Name[it]": "Mostra fotogrammi al secondo", + "Name[ja]": "FPS を表示", + "Name[ka]": "კადრი/წმ-ის ჩვენება", + "Name[ko]": "FPS 표시", + "Name[lt]": "Rodyti kadr./sek.", + "Name[lv]": "Rādīt kadru nomaiņas ātrumu sekundē", + "Name[nb]": "Vis tallet på bilde per sekund", + "Name[nl]": "FPS tonen", + "Name[nn]": "Vis talet på bilete per sekund", + "Name[pl]": "Pokaż liczbę klatek na sekundę", + "Name[pt]": "Mostrar as IPS", + "Name[pt_BR]": "Mostrar FPS", + "Name[ro]": "Arată CPS", + "Name[ru]": "График производительности", + "Name[sa]": "FPS दर्शयतु", + "Name[sk]": "Zobraziť FPS", + "Name[sl]": "Pokaži št. sličic na sekundo", + "Name[sv]": "Visa ramar/s", + "Name[ta]": "FPS-ஐ காட்டு", + "Name[tr]": "FPS’yi Göster", + "Name[uk]": "Показ частоти кадрів", + "Name[vi]": "Hiện FPS", + "Name[zh_CN]": "显示每秒帧数", + "Name[zh_TW]": "顯示 FPS" + }, + "org.kde.kwin.effect": { + "internal": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/showfps/showfpseffect.cpp b/local/recipes/kde/kwin/source/src/plugins/showfps/showfpseffect.cpp new file mode 100644 index 0000000000..f5afc750bc --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/showfps/showfpseffect.cpp @@ -0,0 +1,130 @@ +/* + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + SPDX-FileCopyrightText: 2022 Arjen Hiemstra + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "showfpseffect.h" +#include "core/output.h" +#include "core/renderviewport.h" +#include "effect/effecthandler.h" + +#include + +namespace KWin +{ + +ShowFpsEffect::ShowFpsEffect() +{ +} + +ShowFpsEffect::~ShowFpsEffect() = default; + +int ShowFpsEffect::fps() const +{ + return m_fps; +} + +int ShowFpsEffect::maximumFps() const +{ + return m_maximumFps; +} + +int ShowFpsEffect::paintDuration() const +{ + return m_paintDuration; +} + +int ShowFpsEffect::paintAmount() const +{ + return m_paintAmount; +} + +QColor ShowFpsEffect::paintColor() const +{ + auto normalizedDuration = std::min(1.0, m_paintDuration / 100.0); + return QColor::fromHsvF(0.3 - (0.3 * normalizedDuration), 1.0, 1.0); +} + +void ShowFpsEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + effects->prePaintScreen(data, presentTime); + + m_newFps += 1; + + m_paintDurationTimer.restart(); + m_paintAmount = 0; + + // detect highest monitor refresh rate + uint32_t maximumFps = 0; + const auto screens = effects->screens(); + for (auto screen : screens) { + maximumFps = std::max(screen->refreshRate(), maximumFps); + } + maximumFps /= 1000; // Convert from mHz to Hz. + + if (maximumFps != m_maximumFps) { + m_maximumFps = maximumFps; + Q_EMIT maximumFpsChanged(); + } + + if (!m_scene) { + m_scene = std::make_unique(); + m_scene->loadFromModule(QStringLiteral("org.kde.kwin.showfps"), QStringLiteral("Main"), {{QStringLiteral("effect"), QVariant::fromValue(this)}}); + if (!m_scene->rootItem()) { + // main-fallback.qml has less dependencies than main.qml, so it should work on any system where kwin compiles + m_scene->loadFromModule(QStringLiteral("org.kde.kwin.showfps"), QStringLiteral("Fallback"), {{QStringLiteral("effect"), QVariant::fromValue(this)}}); + } + } +} + +void ShowFpsEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) +{ + effects->paintScreen(renderTarget, viewport, mask, deviceRegion, screen); + + auto now = std::chrono::steady_clock::now(); + if ((now - m_lastFpsTime) >= std::chrono::milliseconds(1000)) { + m_fps = m_newFps; + m_newFps = 0; + m_lastFpsTime = now; + Q_EMIT fpsChanged(); + } + + const auto rect = viewport.renderRect(); + m_scene->setGeometry(QRect(rect.x() + rect.width() - 300, rect.y(), 300, 150)); + effects->renderOffscreenQuickView(renderTarget, viewport, m_scene.get()); +} + +void ShowFpsEffect::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) +{ + effects->paintWindow(renderTarget, viewport, w, mask, deviceRegion, data); + + // Take intersection of region and actual window's rect, minus the fps area + // (since we keep repainting it) and count the pixels. + Region repaintRegion = deviceRegion & viewport.mapToDeviceCoordinatesAligned(w->frameGeometry()); + repaintRegion -= viewport.mapToDeviceCoordinatesAligned(Rect(m_scene->geometry())); + for (const Rect &rect : repaintRegion.rects()) { + m_paintAmount += rect.width() * rect.height(); + } +} + +void ShowFpsEffect::postPaintScreen() +{ + effects->postPaintScreen(); + + m_paintDuration = m_paintDurationTimer.elapsed(); + Q_EMIT paintChanged(); + + effects->addRepaint(m_scene->geometry()); +} + +bool ShowFpsEffect::supported() +{ + return effects->isOpenGLCompositing(); +} + +} // namespace KWin + +#include "moc_showfpseffect.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/showfps/showfpseffect.h b/local/recipes/kde/kwin/source/src/plugins/showfps/showfpseffect.h new file mode 100644 index 0000000000..f4f4ad4592 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/showfps/showfpseffect.h @@ -0,0 +1,64 @@ +/* + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + SPDX-FileCopyrightText: 2022 Arjen Hiemstra + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/effect.h" +#include "effect/offscreenquickview.h" + +#include + +namespace KWin +{ + +class ShowFpsEffect : public Effect +{ + Q_OBJECT + Q_PROPERTY(int fps READ fps NOTIFY fpsChanged) + Q_PROPERTY(int maximumFps READ maximumFps NOTIFY maximumFpsChanged) + Q_PROPERTY(int paintDuration READ paintDuration NOTIFY paintChanged) + Q_PROPERTY(int paintAmount READ paintAmount NOTIFY paintChanged) + Q_PROPERTY(QColor paintColor READ paintColor NOTIFY paintChanged) + +public: + ShowFpsEffect(); + ~ShowFpsEffect() override; + + int fps() const; + int maximumFps() const; + int paintDuration() const; + int paintAmount() const; + QColor paintColor() const; + + void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override; + void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) override; + void paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, KWin::WindowPaintData &data) override; + void postPaintScreen() override; + + static bool supported(); + +Q_SIGNALS: + void fpsChanged(); + void maximumFpsChanged(); + void paintChanged(); + +private: + std::unique_ptr m_scene; + + uint32_t m_maximumFps = 0; + + int m_fps = 0; + int m_newFps = 0; + std::chrono::steady_clock::time_point m_lastFpsTime; + + int m_paintDuration = 0; + int m_paintAmount = 0; + QElapsedTimer m_paintDurationTimer; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/showpaint/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/showpaint/CMakeLists.txt new file mode 100644 index 0000000000..d0bf1fb78a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/showpaint/CMakeLists.txt @@ -0,0 +1,12 @@ +####################################### +# Effect + +set(showpaint_SOURCES + main.cpp + showpaint.cpp +) + +kwin_add_builtin_effect(showpaint ${showpaint_SOURCES}) +target_link_libraries(showpaint PRIVATE + kwin +) diff --git a/local/recipes/kde/kwin/source/src/plugins/showpaint/main.cpp b/local/recipes/kde/kwin/source/src/plugins/showpaint/main.cpp new file mode 100644 index 0000000000..cb6bddfad8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/showpaint/main.cpp @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "showpaint.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY(ShowPaintEffect, + "metadata.json.stripped") + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/showpaint/metadata.json b/local/recipes/kde/kwin/source/src/plugins/showpaint/metadata.json new file mode 100644 index 0000000000..f322393857 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/showpaint/metadata.json @@ -0,0 +1,95 @@ +{ + "KPlugin": { + "Category": "Tools", + "Description": "Highlight areas of the desktop that have been recently updated; activated with a keyboard shortcut", + "Description[ar]": "أبرِز مناطق سطح المكتب التي حُدِّثت مؤخّرًا، تفعّل باختصار لوحة المفاتيح", + "Description[bg]": "Осветяване на областите на работния плот, които са наскоро актуализирани, активирано с клавишна комбинация", + "Description[ca@valencia]": "Ressalta les àrees de l'escriptori que s'han actualitzat darrerament; activat amb una drecera de teclat", + "Description[ca]": "Ressalta les àrees de l'escriptori que s'han actualitzat darrerament; activat amb una drecera de teclat", + "Description[da]": "Fremhæver områder af skrivebordet, der for nyligt blev opdateret; aktiveret med en tastaturgenvej", + "Description[de]": "Hebt die Bereiche der Arbeitsfläche farbig hervor, die kürzlich aktualisiert worden sind; mit einem Tastatur-Kurzbefehl ausgelöst", + "Description[en_GB]": "Highlight areas of the desktop that have been recently updated; activated with a keyboard shortcut", + "Description[eo]": "Marki areojn de la labortablo kiuj estis lastatempe ĝisdatigitaj; aktivigita per klavara ŝparvojo", + "Description[es]": "Resaltar áreas del escritorio que se han actualizado recientemente; se activa con un atajo de teclado", + "Description[eu]": "Nabarmendu berriki eguneratu diren mahaigaineko eremuak; teklatuko lasterbide batekin aktibatua", + "Description[fi]": "Korosta äskettäin päivittyneet työpöydän alueet; käynnistyy pikanäppäimellä", + "Description[fr]": "Mettre en valeur les zones de bureau ayant été récemment mises à jour. Activé grâce à un raccourci clavier.", + "Description[gl]": "Realza áreas do escritorio que se actualizaron recentemente; actívase cun atallo de teclado.", + "Description[he]": "הדגשת אזורי שולחן העבודה האחרונים שעודכנו, מופעל דרך צירוף מקשי מקלדת", + "Description[hu]": "Kiemeli az asztalon az éppen felfrissített területet; gyorsbillentyűvel aktiválható", + "Description[ia]": "Evidentia areas del scriptorio que ha essite actualisate recentemente; activate co un via breve de claviero", + "Description[is]": "Auðkenna svæði á skjáborðinu sem hafa verið nýlega uppfærð; virkjað með flýtilykli", + "Description[it]": "Evidenzia aree del desktop che sono state aggiornate di recente; attivato con una scorciatoia da tastiera", + "Description[ka]": "სამუშაო ადგილების ახალხანს განახლებული ადგილების გამოკვეთა. აქტიურდება კლავიატურის მალსახმობით", + "Description[ko]": "최근에 다시 그려진 바탕 화면의 영역을 표시, 키보드 단축키로 활성화", + "Description[lt]": "Paryškinti darbalaukio sritis, kurios buvo paskiausiai atnaujintos; aktyvinama naudojant sparčiuosius klavišus", + "Description[lv]": "Iekrāsot darbvirsmas zonas, kas nesen ir mainījušās; ieslēdz ar tastatūras saīsni", + "Description[nb]": "Fremhever deler av skrivebordet som nylig ble oppdatert – slått av/på med hurtigtast", + "Description[nl]": "Accentueert gebieden van het bureaublad die recentelijk zijn geactualiseerd; geactiveerd met een sneltoets", + "Description[nn]": "Marker nyleg oppdaterte område på skjermflata (starta med snøggtast)", + "Description[pl]": "Podświetla obszary pulpitu, które zostały ostatnio uaktualnione; uruchamiane skrótem klawiszowym", + "Description[pt_BR]": "Realça as áreas da área de trabalho que foram recentemente atualizadas; ativado com um atalho de teclado", + "Description[ro]": "Evidențiază zonele biroului ce au fost actualizate recent; activat cu o scurtătură de taste", + "Description[ru]": "Подсветка обновляемых областей экрана; активируется нажатием комбинации клавиш", + "Description[sa]": "डेस्कटॉप् इत्यस्य क्षेत्राणि प्रकाशयन्तु ये अद्यतनतया अद्यतनं कृतवन्तः; कीबोर्ड-शॉर्टकट्-सहितं सक्रियम्", + "Description[sk]": "Zvýrazní oblasti plochy, ktoré boli práve aktualizované", + "Description[sl]": "Poudari območja namizja, ki so bili pred kratkim posodobljeni; aktivirano z bližnjico tipkovnice", + "Description[sv]": "Markera områden på skärmen som nyligen har uppdaterats, aktiveras med en snabbtangent", + "Description[tr]": "Masaüstünün yeni güncellenen alanlarını vurgula; klavye kısayoluyla etkinleştirilir", + "Description[uk]": "Підсвічує області екрана, у яких спостерігалося оновлення; активується клавіатурним скороченням", + "Description[zh_CN]": "高亮显示最近更新的桌面区域;使用键盘快捷键激活", + "Description[zh_TW]": "突顯桌面上剛被更新的區域——需要用鍵盤快捷鍵觸發", + "EnabledByDefault": false, + "License": "GPL", + "Name": "Show Paint", + "Name[ar]": "أظهر الطلاء", + "Name[az]": "Yenilənən sahə", + "Name[be]": "Паказ абнаўлення", + "Name[bg]": "Оцветено показване", + "Name[ca@valencia]": "Mostra el pintat", + "Name[ca]": "Mostra el pintat", + "Name[cs]": "Zobrazit kreslení", + "Name[da]": "Vis maling", + "Name[de]": "Zeichnungsbereiche hervorheben", + "Name[en_GB]": "Show Paint", + "Name[eo]": "Montri Farbon", + "Name[es]": "Mostrar pintado", + "Name[et]": "Joonistatud alade näitamine", + "Name[eu]": "Erakutsi pintura", + "Name[fi]": "Näytä näytönpiirto", + "Name[fr]": "Afficher la peinture", + "Name[gl]": "Amosar o pintado", + "Name[he]": "הצגת צביעה", + "Name[hu]": "Rajzkiemelés", + "Name[ia]": "Monstra pictura", + "Name[id]": "Tampilkan Lukisan", + "Name[is]": "Sýna litun", + "Name[it]": "Mostra il ridisegno", + "Name[ja]": "描画を表示", + "Name[ka]": "საღებავის ჩვენება", + "Name[ko]": "그리기 영역 표시", + "Name[lt]": "Rodyti piešimą", + "Name[lv]": "Rādīt zīmēšanu", + "Name[nb]": "Vis opptegning", + "Name[nl]": "Intekening tonen", + "Name[nn]": "Vis oppteikning", + "Name[pl]": "Pokaż rysowane", + "Name[pt]": "Mostrar a Pintura", + "Name[pt_BR]": "Mostrar pintura", + "Name[ro]": "Arată vopseaua", + "Name[ru]": "Подсветка отрисовки", + "Name[sa]": "रङ्गं दर्शयतु", + "Name[sk]": "Zobraziť kresbu", + "Name[sl]": "Pokaži barvo", + "Name[sv]": "Visa uppritning", + "Name[ta]": "திரைப்புதுப்பிபுகளைக் காட்டு", + "Name[tr]": "Boyamayı Göster", + "Name[uk]": "Показ малювання", + "Name[vi]": "Hiện sơn", + "Name[zh_CN]": "突出显示画面更新区域", + "Name[zh_TW]": "顯示塗繪" + }, + "org.kde.kwin.effect": { + "internal": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/showpaint/showpaint.cpp b/local/recipes/kde/kwin/source/src/plugins/showpaint/showpaint.cpp new file mode 100644 index 0000000000..5ee713ed26 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/showpaint/showpaint.cpp @@ -0,0 +1,96 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2010 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "showpaint.h" + +#include "core/pixelgrid.h" +#include "core/rendertarget.h" +#include "core/renderviewport.h" +#include "effect/effecthandler.h" +#include "opengl/glutils.h" + +#include +#include + +namespace KWin +{ + +static const qreal s_alpha = 0.2; +static const QList s_colors{ + Qt::red, + Qt::green, + Qt::blue, + Qt::cyan, + Qt::magenta, + Qt::yellow, + Qt::gray}; + +ShowPaintEffect::ShowPaintEffect() = default; + +void ShowPaintEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) +{ + m_painted = Region(); + effects->paintScreen(renderTarget, viewport, mask, deviceRegion, screen); + if (effects->isOpenGLCompositing()) { + paintGL(renderTarget, viewport); + } else if (effects->compositingType() == QPainterCompositing) { + paintQPainter(viewport); + } + if (++m_colorIndex == s_colors.count()) { + m_colorIndex = 0; + } +} + +void ShowPaintEffect::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) +{ + m_painted += deviceRegion; + effects->paintWindow(renderTarget, viewport, w, mask, deviceRegion, data); +} + +void ShowPaintEffect::paintGL(const RenderTarget &renderTarget, const RenderViewport &viewport) +{ + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + ShaderBinder binder(ShaderTrait::UniformColor | ShaderTrait::TransformColorspace); + binder.shader()->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, viewport.projectionMatrix()); + binder.shader()->setColorspaceUniforms(ColorDescription::sRGB, renderTarget.colorDescription(), RenderingIntent::Perceptual); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + QColor color = s_colors[m_colorIndex]; + color.setAlphaF(s_alpha); + binder.shader()->setUniform(GLShader::ColorUniform::Color, color); + QList verts; + verts.reserve(m_painted.rects().size() * 12); + for (const Rect &deviceRect : m_painted.rects()) { + const auto r = deviceRect.translated(viewport.scaledRenderRect().topLeft()); + verts.push_back(QVector2D(r.x() + r.width(), r.y())); + verts.push_back(QVector2D(r.x(), r.y())); + verts.push_back(QVector2D(r.x(), r.y() + r.height())); + verts.push_back(QVector2D(r.x(), r.y() + r.height())); + verts.push_back(QVector2D(r.x() + r.width(), r.y() + r.height())); + verts.push_back(QVector2D(r.x() + r.width(), r.y())); + } + vbo->setVertices(verts); + vbo->render(GL_TRIANGLES); + glDisable(GL_BLEND); +} + +void ShowPaintEffect::paintQPainter(const RenderViewport &viewport) +{ + QColor color = s_colors[m_colorIndex]; + color.setAlphaF(s_alpha); + for (const Rect &deviceRect : m_painted.rects()) { + effects->scenePainter()->fillRect(viewport.mapFromDeviceCoordinates(deviceRect), color); + } +} + +} // namespace KWin + +#include "moc_showpaint.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/showpaint/showpaint.h b/local/recipes/kde/kwin/source/src/plugins/showpaint/showpaint.h new file mode 100644 index 0000000000..20218b2609 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/showpaint/showpaint.h @@ -0,0 +1,35 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/effect.h" + +namespace KWin +{ + +class ShowPaintEffect : public Effect +{ + Q_OBJECT + +public: + ShowPaintEffect(); + + void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) override; + void paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceGeometry, WindowPaintData &data) override; + +private: + void paintGL(const RenderTarget &renderTarget, const RenderViewport &viewport); + void paintQPainter(const RenderViewport &viewport); + + Region m_painted; // what's painted in one pass + int m_colorIndex = 0; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/slide/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/slide/CMakeLists.txt new file mode 100644 index 0000000000..646cb5d906 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slide/CMakeLists.txt @@ -0,0 +1,37 @@ +####################################### +# Effect + +set(slide_SOURCES + main.cpp + slide.cpp + springmotion.cpp +) + +kconfig_add_kcfg_files(slide_SOURCES + slideconfig.kcfgc +) + +kwin_add_builtin_effect(slide ${slide_SOURCES}) +target_link_libraries(slide PRIVATE + kwin + + KF6::ConfigGui +) + +####################################### +# Config +if (KWIN_BUILD_KCMS) + set(kwin_slide_config_SRCS slide_config.cpp) + ki18n_wrap_ui(kwin_slide_config_SRCS slide_config.ui) + kconfig_add_kcfg_files(kwin_slide_config_SRCS slideconfig.kcfgc) + + kwin_add_effect_config(kwin_slide_config ${kwin_slide_config_SRCS}) + + target_link_libraries(kwin_slide_config + KF6::KCMUtils + KF6::CoreAddons + KF6::I18n + Qt::DBus + KWinEffectsInterface + ) +endif() diff --git a/local/recipes/kde/kwin/source/src/plugins/slide/main.cpp b/local/recipes/kde/kwin/source/src/plugins/slide/main.cpp new file mode 100644 index 0000000000..4fb67ae7fd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slide/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "slide.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(SlideEffect, + "metadata.json.stripped", + return SlideEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/slide/metadata.json b/local/recipes/kde/kwin/source/src/plugins/slide/metadata.json new file mode 100644 index 0000000000..341ad6b160 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slide/metadata.json @@ -0,0 +1,90 @@ +{ + "KPlugin": { + "Category": "Virtual Desktop Switching Animation", + "Description": "Slide between virtual desktops when switching", + "Description[ar]": "التنقل بين أسطح المكتب الافتراضية مثل الشرائح عند التبديل", + "Description[bg]": "Плъзгане при превключване между работните плотове", + "Description[ca@valencia]": "Llisca entre els escriptoris virtuals quan es canvien", + "Description[ca]": "Llisca entre els escriptoris virtuals quan es commuten", + "Description[de]": "Wechseln der virtuellen Arbeitsfläche durch Gleiten", + "Description[es]": "Deslizar los escritorios virtuales al cambiar entre ellos", + "Description[eu]": "Aldatzean, alegiazko mahaigainen artean irristatu", + "Description[fi]": "Liu’uta virtuaalityöpöytää sitä vaihdettaessa", + "Description[fr]": "Faire un effet de diaporama entre les bureaux virtuels lors d'un basculement", + "Description[he]": "החלקה בין שולחנות עבודה וירטואליים בעת המעבר", + "Description[hu]": "Csúszás virtuális asztalok váltásakor", + "Description[ia]": "Glissa inter scriptorio virtuales quando il commuta inter los", + "Description[is]": "Renna á milli sýndarskjáborða þegar skipt er á milli þeirra", + "Description[it]": "Scorri tra i desktop virtuali durante il passaggio", + "Description[ja]": "仮想デスクトップ切り替え時にスライドします", + "Description[ka]": "სრიალი ვირტუალურ სამუშაო მაგიდებს შორის გადართვისას", + "Description[ko]": "가상 바탕 화면 사이를 전환할 때 슬라이딩 효과를 사용합니다", + "Description[lt]": "Slinkti tarp virtualių darbalaukių, kai jie perjungiami", + "Description[lv]": "Slīdēt starp virtuālajām darbvirsmām, starp tām pārslēdzoties", + "Description[nl]": "Virtuele bureaubladen verschuiven bij het wisselen", + "Description[nn]": "Ton inn og ut ved veksling mellom virtuelle skrivebord", + "Description[pl]": "Prześlizgiwanie przy przełączaniu pomiędzy pulpitami wirtualnymi", + "Description[pt_BR]": "Deslizar entre áreas de trabalho virtuais ao alternar", + "Description[ro]": "Alunecă între birourile virtuale la schimbare", + "Description[ru]": "Скольжение изображения при переключении на другой рабочий стол", + "Description[sk]": "Prelínanie medzi virtuálnymi pracovnými plochami pri prepínaní medzi nimi", + "Description[sl]": "Drsaj med virtualnimi namizji med preklapljanjem", + "Description[sv]": "Skjut mellan virtuella skrivbord vid byte", + "Description[tr]": "Sanal masaüstleri arasında kayarak geçiş yap", + "Description[uk]": "Ковзання під час перемикання між віртуальними стільницями", + "Description[zh_CN]": "在虚拟桌面间切换时呈现滑动动效", + "Description[zh_TW]": "用滑動方式切換虛擬桌面", + "EnabledByDefault": true, + "License": "GPL", + "Name": "Slide", + "Name[ar]": "الشريحة", + "Name[az]": "Slayd", + "Name[be]": "Ссоўванне", + "Name[bg]": "Плъзгане", + "Name[ca@valencia]": "Lliscament", + "Name[ca]": "Lliscament", + "Name[cs]": "Sklouznutí", + "Name[da]": "Glid", + "Name[de]": "Gleiten", + "Name[en_GB]": "Slide", + "Name[eo]": "Gliti", + "Name[es]": "Deslizar", + "Name[et]": "Liuglemine", + "Name[eu]": "Irristatu", + "Name[fi]": "Työpöytäliuku", + "Name[fr]": "Diapositive", + "Name[gl]": "Esvarar", + "Name[he]": "החלקה", + "Name[hu]": "Csúsztatott váltás", + "Name[ia]": "Glissa", + "Name[id]": "Geser", + "Name[is]": "Renna", + "Name[it]": "Scivola", + "Name[ja]": "スライド", + "Name[ka]": "სლაიდი", + "Name[ko]": "슬라이드", + "Name[lt]": "Slinkimas", + "Name[lv]": "Slidināt", + "Name[nb]": "Skli", + "Name[nl]": "Dia", + "Name[nn]": "Skliding", + "Name[pl]": "Slajd", + "Name[pt]": "Deslizar", + "Name[pt_BR]": "Deslizar", + "Name[ro]": "Alunecă", + "Name[ru]": "Скольжение", + "Name[sa]": "च्यु", + "Name[sk]": "Posúvať", + "Name[sl]": "Drsaj", + "Name[sv]": "Skjut", + "Name[tr]": "Kaydır", + "Name[uk]": "Ковзання", + "Name[vi]": "Trượt", + "Name[zh_CN]": "滑动", + "Name[zh_TW]": "滑動" + }, + "X-KDE-ConfigModule": "kwin_slide_config", + "org.kde.kwin.effect": { + "exclusiveGroup": "desktop-animations" + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/slide/slide.cpp b/local/recipes/kde/kwin/source/src/plugins/slide/slide.cpp new file mode 100644 index 0000000000..9ee9066536 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slide/slide.cpp @@ -0,0 +1,555 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2008 Lucas Murray + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +// own +#include "slide.h" +#include "core/output.h" +#include "core/renderviewport.h" +#include "effect/effecthandler.h" + +// KConfigSkeleton +#include "slideconfig.h" + +#include + +namespace KWin +{ + +SlideEffect::SlideEffect() +{ + SlideConfig::instance(effects->config()); + reconfigure(ReconfigureAll); + + connect(effects, &EffectsHandler::desktopChanged, + this, &SlideEffect::desktopChanged); + connect(effects, &EffectsHandler::desktopChanging, + this, &SlideEffect::desktopChanging); + connect(effects, QOverload<>::of(&EffectsHandler::desktopChangingCancelled), + this, &SlideEffect::desktopChangingCancelled); + connect(effects, &EffectsHandler::windowAdded, + this, &SlideEffect::windowAdded); + connect(effects, &EffectsHandler::windowDeleted, + this, &SlideEffect::windowDeleted); + connect(effects, &EffectsHandler::desktopAdded, + this, &SlideEffect::finishedSwitching); + connect(effects, &EffectsHandler::desktopRemoved, + this, &SlideEffect::finishedSwitching); + connect(effects, &EffectsHandler::screenAdded, + this, &SlideEffect::finishedSwitching); + connect(effects, &EffectsHandler::screenRemoved, + this, &SlideEffect::finishedSwitching); + connect(effects, &EffectsHandler::currentActivityAboutToChange, this, [this]() { + m_switchingActivity = true; + }); + connect(effects, &EffectsHandler::currentActivityChanged, this, [this]() { + m_switchingActivity = false; + }); +} + +SlideEffect::~SlideEffect() +{ + finishedSwitching(); +} + +bool SlideEffect::supported() +{ + return effects->animationsSupported(); +} + +void SlideEffect::reconfigure(ReconfigureFlags) +{ + SlideConfig::self()->read(); + + const qreal springConstant = 300.0 / effects->animationTimeFactor(); + const qreal dampingRatio = 1.1; + + m_motionX = SpringMotion(springConstant, dampingRatio); + m_motionY = SpringMotion(springConstant, dampingRatio); + + m_hGap = SlideConfig::horizontalGap(); + m_vGap = SlideConfig::verticalGap(); + m_slideBackground = SlideConfig::slideBackground(); +} + +inline Region buildClipRegion(const QPoint &pos, int w, int h) +{ + const QSize screenSize = effects->virtualScreenSize(); + Region r = Rect(pos, screenSize); + if (effects->optionRollOverDesktops()) { + r += (r & QRect(-w, 0, w, h)).translated(w, 0); // W + r += (r & QRect(w, 0, w, h)).translated(-w, 0); // E + + r += (r & QRect(0, -h, w, h)).translated(0, h); // N + r += (r & QRect(0, h, w, h)).translated(0, -h); // S + + r += (r & QRect(-w, -h, w, h)).translated(w, h); // NW + r += (r & QRect(w, -h, w, h)).translated(-w, h); // NE + r += (r & QRect(w, h, w, h)).translated(-w, -h); // SE + r += (r & QRect(-w, h, w, h)).translated(w, -h); // SW + } + return r; +} + +void SlideEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + const QList desktops = effects->desktops(); + const int w = effects->desktopGridWidth(); + const int h = effects->desktopGridHeight(); + + switch (m_state) { + case State::Inactive: + Q_UNREACHABLE(); + + case State::ActiveAnimation: { + std::chrono::milliseconds timeDelta = std::chrono::milliseconds::zero(); + if (m_lastPresentTime.count()) { + timeDelta = presentTime - m_lastPresentTime; + } + m_lastPresentTime = presentTime; + + m_motionX.advance(timeDelta); + m_motionY.advance(timeDelta); + + const QSize virtualSpaceSize = effects->virtualScreenSize(); + m_paintCtx.position = QPointF(m_motionX.position() / virtualSpaceSize.width(), m_motionY.position() / virtualSpaceSize.height()); + break; + } + case State::ActiveGesture: + m_paintCtx.position = m_gesturePos; + break; + } + + // Clipping + m_paintCtx.visibleDesktops.clear(); + m_paintCtx.visibleDesktops.reserve(4); // 4 - maximum number of visible desktops + bool includedX = false, includedY = false; + for (VirtualDesktop *desktop : desktops) { + const QPoint coords = effects->desktopGridCoords(desktop); + if (coords.x() % w == (int)(m_paintCtx.position.x()) % w) { + includedX = true; + } else if (coords.x() % w == ((int)(m_paintCtx.position.x()) + 1) % w) { + includedX = true; + } + if (coords.y() % h == (int)(m_paintCtx.position.y()) % h) { + includedY = true; + } else if (coords.y() % h == ((int)(m_paintCtx.position.y()) + 1) % h) { + includedY = true; + } + + if (includedX && includedY) { + m_paintCtx.visibleDesktops << desktop; + } + } + + data.mask |= PAINT_SCREEN_TRANSFORMED | PAINT_SCREEN_BACKGROUND_FIRST; + + effects->prePaintScreen(data, presentTime); +} + +void SlideEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) +{ + m_paintCtx.wrap = effects->optionRollOverDesktops(); + effects->paintScreen(renderTarget, viewport, mask, deviceRegion, screen); +} + +QPoint SlideEffect::getDrawCoords(QPointF pos, LogicalOutput *screen) +{ + QPoint c = QPoint(); + c.setX(pos.x() * (screen->geometry().width() + m_hGap)); + c.setY(pos.y() * (screen->geometry().height() + m_vGap)); + return c; +} + +/** + * Decide whether given window @p w should be transformed/translated. + * @returns @c true if given window @p w should be transformed, otherwise @c false + */ +bool SlideEffect::isTranslated(const EffectWindow *w) const +{ + if (w->isOnAllDesktops()) { + if (w->isDesktop()) { + return m_slideBackground; + } + return false; + } else if (w == m_movingWindow) { + return false; + } + return true; +} + +/** + * Will a window be painted during this frame? + */ +bool SlideEffect::willBePainted(const EffectWindow *w) const +{ + if (w->isOnAllDesktops()) { + return true; + } + if (w == m_movingWindow) { + return true; + } + for (VirtualDesktop *desktop : std::as_const(m_paintCtx.visibleDesktops)) { + if (w->isOnDesktop(desktop)) { + return true; + } + } + return false; +} + +void SlideEffect::prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) +{ + data.setTransformed(); + effects->prePaintWindow(view, w, data, presentTime); +} + +void SlideEffect::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceGeometry, WindowPaintData &data) +{ + if (!willBePainted(w)) { + return; + } + + if (!isTranslated(w)) { + effects->paintWindow(renderTarget, viewport, w, mask, deviceGeometry, data); + return; + } + + const int gridWidth = effects->desktopGridWidth(); + const int gridHeight = effects->desktopGridHeight(); + + QPointF drawPosition = forcePositivePosition(m_paintCtx.position); + drawPosition = m_paintCtx.wrap ? constrainToDrawableRange(drawPosition) : drawPosition; + + // If we're wrapping, draw the desktop in the second position. + const bool wrappingX = drawPosition.x() > gridWidth - 1; + const bool wrappingY = drawPosition.y() > gridHeight - 1; + + const auto screens = effects->screens(); + + for (VirtualDesktop *desktop : std::as_const(m_paintCtx.visibleDesktops)) { + if (!w->isOnDesktop(desktop)) { + continue; + } + QPointF desktopTranslation = QPointF(effects->desktopGridCoords(desktop)) - drawPosition; + // Decide if that first desktop should be drawn at 0 or the higher position used for wrapping. + if (effects->desktopGridCoords(desktop).x() == 0 && wrappingX) { + desktopTranslation = QPointF(desktopTranslation.x() + gridWidth, desktopTranslation.y()); + } + if (effects->desktopGridCoords(desktop).y() == 0 && wrappingY) { + desktopTranslation = QPointF(desktopTranslation.x(), desktopTranslation.y() + gridHeight); + } + + for (LogicalOutput *screen : screens) { + QPoint drawTranslation = getDrawCoords(desktopTranslation, screen); + data += drawTranslation; + + const Rect screenArea = screen->geometry(); + const Rect logicalDamage = screenArea.translated(drawTranslation).intersected(screenArea); + + effects->paintWindow( + renderTarget, viewport, w, mask, + // Only paint the region that intersects the current screen and desktop. + deviceGeometry.intersected(viewport.mapToDeviceCoordinatesAligned(logicalDamage)), + data); + + // Undo the translation for the next screen. I know, it hurts me too. + data += QPoint(-drawTranslation.x(), -drawTranslation.y()); + } + } +} + +void SlideEffect::postPaintScreen() +{ + if (m_state == State::ActiveAnimation && !m_motionX.isMoving() && !m_motionY.isMoving()) { + finishedSwitching(); + } + + effects->addRepaintFull(); + effects->postPaintScreen(); +} + +/* + * Negative desktop positions aren't allowed. + */ +QPointF SlideEffect::forcePositivePosition(QPointF p) const +{ + if (p.x() < 0) { + p.setX(p.x() + std::ceil(-p.x() / effects->desktopGridWidth()) * effects->desktopGridWidth()); + } + if (p.y() < 0) { + p.setY(p.y() + std::ceil(-p.y() / effects->desktopGridHeight()) * effects->desktopGridHeight()); + } + return p; +} + +bool SlideEffect::shouldElevate(const EffectWindow *w) const +{ + // Static docks(i.e. this effect doesn't slide docks) should be elevated + // so they can properly animate themselves when an user enters or leaves + // a virtual desktop with a window in fullscreen mode. + return w->isDock(); +} + +/* + * This function is called when the desktop changes. + * Called AFTER the gesture is released. + * Sets up animation to round off to the new current desktop. + */ +void SlideEffect::startAnimation(const QPointF &oldPos, VirtualDesktop *current, EffectWindow *movingWindow) +{ + if (m_state == State::Inactive) { + prepareSwitching(); + } + + m_state = State::ActiveAnimation; + m_movingWindow = movingWindow; + + m_startPos = oldPos; + m_endPos = effects->desktopGridCoords(current); + if (effects->optionRollOverDesktops()) { + optimizePath(); + } + + const QSize virtualSpaceSize = effects->virtualScreenSize(); + m_motionX.setAnchor(m_endPos.x() * virtualSpaceSize.width()); + m_motionX.setPosition(m_startPos.x() * virtualSpaceSize.width()); + m_motionY.setAnchor(m_endPos.y() * virtualSpaceSize.height()); + m_motionY.setPosition(m_startPos.y() * virtualSpaceSize.height()); + m_lastPresentTime = std::chrono::milliseconds::zero(); + + effects->setActiveFullScreenEffect(this); + effects->addRepaintFull(); +} + +void SlideEffect::prepareSwitching() +{ + const auto windows = effects->stackingOrder(); + m_windowData.reserve(windows.count()); + + for (EffectWindow *w : windows) { + m_windowData[w] = WindowData{ + .visibilityRef = EffectWindowVisibleRef(w, EffectWindow::PAINT_DISABLED_BY_DESKTOP), + }; + + if (shouldElevate(w)) { + effects->setElevatedWindow(w, true); + m_elevatedWindows << w; + } + w->setData(WindowForceBackgroundContrastRole, QVariant(true)); + w->setData(WindowForceBlurRole, QVariant(true)); + } +} + +void SlideEffect::finishedSwitching() +{ + if (m_state == State::Inactive) { + return; + } + const QList windows = effects->stackingOrder(); + for (EffectWindow *w : windows) { + w->setData(WindowForceBackgroundContrastRole, QVariant()); + w->setData(WindowForceBlurRole, QVariant()); + } + + for (EffectWindow *w : std::as_const(m_elevatedWindows)) { + effects->setElevatedWindow(w, false); + } + m_elevatedWindows.clear(); + + m_windowData.clear(); + m_movingWindow = nullptr; + m_state = State::Inactive; + effects->setActiveFullScreenEffect(nullptr); +} + +void SlideEffect::desktopChanged(VirtualDesktop *old, VirtualDesktop *current, EffectWindow *with) +{ + if (m_switchingActivity || (effects->hasActiveFullScreenEffect() && effects->activeFullScreenEffect() != this)) { + return; + } + + QPointF previousPos; + switch (m_state) { + case State::Inactive: + previousPos = effects->desktopGridCoords(old); + break; + + case State::ActiveAnimation: { + const QSize virtualSpaceSize = effects->virtualScreenSize(); + previousPos = QPointF(m_motionX.position() / virtualSpaceSize.width(), m_motionY.position() / virtualSpaceSize.height()); + break; + } + + case State::ActiveGesture: + previousPos = m_gesturePos; + break; + } + + startAnimation(previousPos, current, with); +} + +void SlideEffect::desktopChanging(VirtualDesktop *old, QPointF desktopOffset, EffectWindow *with) +{ + if (effects->hasActiveFullScreenEffect() && effects->activeFullScreenEffect() != this) { + return; + } + + if (m_state == State::Inactive) { + prepareSwitching(); + } + + m_state = State::ActiveGesture; + m_movingWindow = with; + + // Find desktop position based on animationDelta + QPoint gridPos = effects->desktopGridCoords(old); + m_gesturePos.setX(gridPos.x() + desktopOffset.x()); + m_gesturePos.setY(gridPos.y() + desktopOffset.y()); + + if (effects->optionRollOverDesktops()) { + m_gesturePos = forcePositivePosition(m_gesturePos); + } else { + m_gesturePos = moveInsideDesktopGrid(m_gesturePos); + } + + effects->setActiveFullScreenEffect(this); + effects->addRepaintFull(); +} + +void SlideEffect::desktopChangingCancelled() +{ + // If the fingers have been lifted and the current desktop didn't change, start animation + // to move back to the original virtual desktop. + if (effects->activeFullScreenEffect() == this) { + startAnimation(m_gesturePos, effects->currentDesktop(), nullptr); + } +} + +QPointF SlideEffect::moveInsideDesktopGrid(QPointF p) +{ + if (p.x() < 0) { + p.setX(0); + } + if (p.y() < 0) { + p.setY(0); + } + if (p.x() > effects->desktopGridWidth() - 1) { + p.setX(effects->desktopGridWidth() - 1); + } + if (p.y() > effects->desktopGridHeight() - 1) { + p.setY(effects->desktopGridHeight() - 1); + } + return p; +} + +void SlideEffect::windowAdded(EffectWindow *w) +{ + if (m_state == State::Inactive) { + return; + } + if (shouldElevate(w)) { + effects->setElevatedWindow(w, true); + m_elevatedWindows << w; + } + w->setData(WindowForceBackgroundContrastRole, QVariant(true)); + w->setData(WindowForceBlurRole, QVariant(true)); + + m_windowData[w] = WindowData{ + .visibilityRef = EffectWindowVisibleRef(w, EffectWindow::PAINT_DISABLED_BY_DESKTOP), + }; +} + +void SlideEffect::windowDeleted(EffectWindow *w) +{ + if (m_state == State::Inactive) { + return; + } + if (w == m_movingWindow) { + m_movingWindow = nullptr; + } + m_elevatedWindows.removeAll(w); + m_windowData.remove(w); +} + +/* + * Find the fastest path between two desktops. + * This function decides when it's better to wrap around the grid or not. + * Only call if wrapping is enabled. + */ +void SlideEffect::optimizePath() +{ + int w = effects->desktopGridWidth(); + int h = effects->desktopGridHeight(); + + // Keep coordinates as low as possible + if (m_startPos.x() >= w && m_endPos.x() >= w) { + m_startPos.setX(fmod(m_startPos.x(), w)); + m_endPos.setX(fmod(m_endPos.x(), w)); + } + if (m_startPos.y() >= h && m_endPos.y() >= h) { + m_startPos.setY(fmod(m_startPos.y(), h)); + m_endPos.setY(fmod(m_endPos.y(), h)); + } + + // Is there is a shorter possible route? + // If the x distance to be traveled is more than half the grid width, it's faster to wrap. + // To avoid negative coordinates, take the lower coordinate and raise. + if (std::abs((m_startPos.x() - m_endPos.x())) > w / 2.0) { + if (m_startPos.x() < m_endPos.x()) { + while (m_startPos.x() < m_endPos.x()) { + m_startPos.setX(m_startPos.x() + w); + } + } else { + while (m_endPos.x() < m_startPos.x()) { + m_endPos.setX(m_endPos.x() + w); + } + } + // Keep coordinates as low as possible + if (m_startPos.x() >= w && m_endPos.x() >= w) { + m_startPos.setX(fmod(m_startPos.x(), w)); + m_endPos.setX(fmod(m_endPos.x(), w)); + } + } + + // Same for y + if (std::abs((m_endPos.y() - m_startPos.y())) > (double)h / (double)2) { + if (m_startPos.y() < m_endPos.y()) { + while (m_startPos.y() < m_endPos.y()) { + m_startPos.setY(m_startPos.y() + h); + } + } else { + while (m_endPos.y() < m_startPos.y()) { + m_endPos.setY(m_endPos.y() + h); + } + } + // Keep coordinates as low as possible + if (m_startPos.y() >= h && m_endPos.y() >= h) { + m_startPos.setY(fmod(m_startPos.y(), h)); + m_endPos.setY(fmod(m_endPos.y(), h)); + } + } +} + +/* + * Takes the point and uses modulus to keep draw position within [0, desktopGridWidth] + * The render loop will draw the first desktop (0) after the last one (at position desktopGridWidth) for the wrap animation. + * This function finds the true fastest path, regardless of which direction the animation is already going; + * I was a little upset about this limitation until I realized that MacOS can't even wrap desktops :) + */ +QPointF SlideEffect::constrainToDrawableRange(QPointF p) +{ + p.setX(fmod(p.x(), effects->desktopGridWidth())); + p.setY(fmod(p.y(), effects->desktopGridHeight())); + return p; +} + +} // namespace KWin + +#include "moc_slide.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/slide/slide.h b/local/recipes/kde/kwin/source/src/plugins/slide/slide.h new file mode 100644 index 0000000000..ca184a0a53 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slide/slide.h @@ -0,0 +1,163 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2008 Lucas Murray + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +// kwineffects +#include "effect/effect.h" +#include "effect/effectwindow.h" +#include "plugins/slide/springmotion.h" + +namespace KWin +{ + +/* + * How it Works: + * + * This effect doesn't change the current desktop, only receives changes from the VirtualDesktopManager. + * The only visually apparent inputs are desktopChanged() and desktopChanging(). + * + * When responding to desktopChanging(), the draw position is only affected by what's received from there. + * After desktopChanging() is done, or without desktopChanging() having been called at all, desktopChanged() is called. + * The desktopChanged() function configures the m_startPos and m_endPos for the animation, and the duration. + * + * m_currentPosition and everything else not labeled "drawCoordinate" uses desktops as a unit. + * Exmp: 1.2 means the desktop at index 1 shifted over by .2 desktops. + * All coords must be positive. + * + * For the wrapping effect, the render loop has to handle desktop coordinates larger than the total grid's width. + * 1. It uses modulus to keep the desktop coords in the range [0, gridWidth]. + * 2. It will draw the desktop at index 0 at index gridWidth if it has to. + * I will not draw any thing farther outside the range than that. + * + * I've put an explanation of all the important private vars down at the bottom. + * + * Good luck :) + */ + +class SlideEffect : public Effect +{ + Q_OBJECT + Q_PROPERTY(int horizontalGap READ horizontalGap) + Q_PROPERTY(int verticalGap READ verticalGap) + Q_PROPERTY(bool slideBackground READ slideBackground) + +public: + SlideEffect(); + ~SlideEffect() override; + + void reconfigure(ReconfigureFlags) override; + + void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override; + void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) override; + void postPaintScreen() override; + + void prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) override; + void paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceGeometry, WindowPaintData &data) override; + + bool isActive() const override; + int requestedEffectChainPosition() const override; + + static bool supported(); + + int horizontalGap() const; + int verticalGap() const; + bool slideBackground() const; + +private Q_SLOTS: + void desktopChanged(VirtualDesktop *old, VirtualDesktop *current, EffectWindow *with); + void desktopChanging(VirtualDesktop *old, QPointF desktopOffset, EffectWindow *with); + void desktopChangingCancelled(); + void windowAdded(EffectWindow *w); + void windowDeleted(EffectWindow *w); + +private: + QPoint getDrawCoords(QPointF pos, LogicalOutput *screen); + bool isTranslated(const EffectWindow *w) const; + bool willBePainted(const EffectWindow *w) const; + bool shouldElevate(const EffectWindow *w) const; + QPointF moveInsideDesktopGrid(QPointF p); + QPointF constrainToDrawableRange(QPointF p); + QPointF forcePositivePosition(QPointF p) const; + void optimizePath(); // Find the best path to target desktop + + void startAnimation(const QPointF &oldPos, VirtualDesktop *current, EffectWindow *movingWindow = nullptr); + void prepareSwitching(); + void finishedSwitching(); + +private: + int m_hGap; + int m_vGap; + bool m_slideBackground; + + enum class State { + Inactive, + ActiveAnimation, + ActiveGesture, + }; + + State m_state = State::Inactive; + SpringMotion m_motionX; + SpringMotion m_motionY; + + // When the desktop isn't desktopChanging(), these two variables are used to control the animation path. + // They use desktops as a unit. + QPointF m_startPos; + QPointF m_endPos; + + QPointF m_gesturePos; + + EffectWindow *m_movingWindow = nullptr; + std::chrono::milliseconds m_lastPresentTime = std::chrono::milliseconds::zero(); + + struct + { + QPointF position; + bool wrap; + QList visibleDesktops; + } m_paintCtx; + + struct WindowData + { + EffectWindowVisibleRef visibilityRef; + }; + + QList m_elevatedWindows; + QHash m_windowData; + bool m_switchingActivity = false; +}; + +inline int SlideEffect::horizontalGap() const +{ + return m_hGap; +} + +inline int SlideEffect::verticalGap() const +{ + return m_vGap; +} + +inline bool SlideEffect::slideBackground() const +{ + return m_slideBackground; +} + +inline bool SlideEffect::isActive() const +{ + return m_state != State::Inactive; +} + +inline int SlideEffect::requestedEffectChainPosition() const +{ + return 50; +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/slide/slide.kcfg b/local/recipes/kde/kwin/source/src/plugins/slide/slide.kcfg new file mode 100644 index 0000000000..3bf4ae84fd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slide/slide.kcfg @@ -0,0 +1,19 @@ + + + + + + + 45 + + + 20 + + + true + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/slide/slide_config.cpp b/local/recipes/kde/kwin/source/src/plugins/slide/slide_config.cpp new file mode 100644 index 0000000000..3dc7c49d68 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slide/slide_config.cpp @@ -0,0 +1,51 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017, 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "slide_config.h" + +#include "config-kwin.h" + +// KConfigSkeleton +#include "slideconfig.h" + +#include + +#include + +K_PLUGIN_CLASS(KWin::SlideEffectConfig) + +namespace KWin +{ + +SlideEffectConfig::SlideEffectConfig(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) +{ + m_ui.setupUi(widget()); + SlideConfig::instance(KWIN_CONFIG); + addConfig(SlideConfig::self(), widget()); +} + +SlideEffectConfig::~SlideEffectConfig() +{ +} + +void SlideEffectConfig::save() +{ + KCModule::save(); + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("slide")); +} + +} // namespace KWin + +#include "slide_config.moc" + +#include "moc_slide_config.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/slide/slide_config.h b/local/recipes/kde/kwin/source/src/plugins/slide/slide_config.h new file mode 100644 index 0000000000..8538d445fe --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slide/slide_config.h @@ -0,0 +1,32 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017, 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "ui_slide_config.h" +#include + +namespace KWin +{ + +class SlideEffectConfig : public KCModule +{ + Q_OBJECT + +public: + explicit SlideEffectConfig(QObject *parent, const KPluginMetaData &data); + ~SlideEffectConfig() override; + + void save() override; + +private: + ::Ui::SlideEffectConfig m_ui; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/slide/slide_config.ui b/local/recipes/kde/kwin/source/src/plugins/slide/slide_config.ui new file mode 100644 index 0000000000..bfde3204c6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slide/slide_config.ui @@ -0,0 +1,93 @@ + + + SlideEffectConfig + + + + 0 + 0 + 400 + 250 + + + + + + + Gap between desktops + + + + + + Horizontal: + + + + + + + + 0 + 0 + + + + 1000 + + + 5 + + + + + + + Vertical: + + + + + + + + 0 + 0 + + + + 1000 + + + 5 + + + + + + + + + + Slide desktop background + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/slide/slideconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/slide/slideconfig.kcfgc new file mode 100644 index 0000000000..680f393cbc --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slide/slideconfig.kcfgc @@ -0,0 +1,5 @@ +File=slide.kcfg +ClassName=SlideConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/local/recipes/kde/kwin/source/src/plugins/slide/springmotion.cpp b/local/recipes/kde/kwin/source/src/plugins/slide/springmotion.cpp new file mode 100644 index 0000000000..5795484a54 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slide/springmotion.cpp @@ -0,0 +1,164 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "springmotion.h" + +#include + +namespace KWin +{ + +static qreal lerp(qreal a, qreal b, qreal t) +{ + return a * (1 - t) + b * t; +} + +SpringMotion::SpringMotion() + : SpringMotion(200.0, 1.1) +{ +} + +SpringMotion::SpringMotion(qreal springConstant, qreal dampingRatio) + : m_prev({0, 0}) + , m_next({0, 0}) + , m_t(1.0) + , m_timestep(1.0 / 100.0) + , m_anchor(0) + , m_springConstant(springConstant) + , m_dampingRatio(dampingRatio) + , m_dampingCoefficient(2 * std::sqrt(m_springConstant) * m_dampingRatio) + , m_epsilon(1.0) +{ +} + +bool SpringMotion::isMoving() const +{ + return std::fabs(position() - anchor()) > m_epsilon || std::fabs(velocity()) > m_epsilon; +} + +qreal SpringMotion::springConstant() const +{ + return m_springConstant; +} + +qreal SpringMotion::dampingRatio() const +{ + return m_dampingRatio; +} + +qreal SpringMotion::velocity() const +{ + return lerp(m_prev.velocity, m_next.velocity, m_t); +} + +void SpringMotion::setVelocity(qreal velocity) +{ + m_next = State{ + .position = position(), + .velocity = velocity, + }; + m_t = 1.0; +} + +qreal SpringMotion::position() const +{ + return lerp(m_prev.position, m_next.position, m_t); +} + +void SpringMotion::setPosition(qreal position) +{ + m_next = State{ + .position = position, + .velocity = velocity(), + }; + m_t = 1.0; +} + +qreal SpringMotion::epsilon() const +{ + return m_epsilon; +} + +void SpringMotion::setEpsilon(qreal epsilon) +{ + m_epsilon = epsilon; +} + +qreal SpringMotion::anchor() const +{ + return m_anchor; +} + +void SpringMotion::setAnchor(qreal anchor) +{ + m_anchor = anchor; +} + +SpringMotion::Slope SpringMotion::evaluate(const State &state, qreal dt, const Slope &slope) +{ + const State next{ + .position = state.position + slope.dp * dt, + .velocity = state.velocity + slope.dv * dt, + }; + + // The math here follows from the mass-spring-damper model equation. + const qreal springForce = (m_anchor - next.position) * m_springConstant; + const qreal dampingForce = -next.velocity * m_dampingCoefficient; + const qreal acceleration = springForce + dampingForce; + + return Slope{ + .dp = state.velocity, + .dv = acceleration, + }; +} + +SpringMotion::State SpringMotion::integrate(const State &state, qreal dt) +{ + // Use Runge-Kutta method (RK4) to integrate the mass-spring-damper equation. + const Slope initial{ + .dp = 0, + .dv = 0, + }; + const Slope k1 = evaluate(state, 0.0, initial); + const Slope k2 = evaluate(state, 0.5 * dt, k1); + const Slope k3 = evaluate(state, 0.5 * dt, k2); + const Slope k4 = evaluate(state, dt, k3); + + const qreal dpdt = 1.0 / 6.0 * (k1.dp + 2 * k2.dp + 2 * k3.dp + k4.dp); + const qreal dvdt = 1.0 / 6.0 * (k1.dv + 2 * k2.dv + 2 * k3.dv + k4.dv); + + return State{ + .position = state.position + dpdt * dt, + .velocity = state.velocity + dvdt * dt, + }; +} + +void SpringMotion::advance(std::chrono::milliseconds delta) +{ + if (!isMoving()) { + return; + } + + // If m_springConstant is infinite, we have an animation time factor of zero. + // As such, we should advance to the target immediately. + if (std::isinf(m_springConstant)) { + m_next = State{ + .position = m_anchor, + .velocity = 0.0, + }; + return; + } + + // If the delta interval is not multiple of m_timestep precisely, the previous and + // the next samples will be linearly interpolated to get current position and velocity. + const qreal steps = (delta.count() / 1000.0) / m_timestep; + for (m_t += steps; m_t > 1.0; m_t -= 1.0) { + m_prev = m_next; + m_next = integrate(m_next, m_timestep); + } +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/slide/springmotion.h b/local/recipes/kde/kwin/source/src/plugins/slide/springmotion.h new file mode 100644 index 0000000000..cd580a6fde --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slide/springmotion.h @@ -0,0 +1,103 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +namespace KWin +{ + +/** + * The SpringMotion class simulates the motion of a spring along one dimension using the + * mass-spring-damper model. The sping constant parameter controls the acceleration of the + * spring. The damping ratio controls the oscillation of the spring. + */ +class SpringMotion +{ +public: + SpringMotion(); + SpringMotion(qreal springConstant, qreal dampingRatio); + + /** + * Advance the simulation by the given @a delta milliseconds. + */ + void advance(std::chrono::milliseconds delta); + bool isMoving() const; + + /** + * Returns the current velocity. + */ + qreal velocity() const; + void setVelocity(qreal velocity); + + /** + * Returns the current position. + */ + qreal position() const; + void setPosition(qreal position); + + /** + * Returns the anchor position. It's the position that the spring is pulled towards. + */ + qreal anchor() const; + void setAnchor(qreal anchor); + + /** + * Returns the spring constant. It controls the acceleration of the spring. + */ + qreal springConstant() const; + + /** + * Returns the damping ratio. It controls the oscillation of the spring. Potential values: + * + * - 0 or undamped: the spring will oscillate indefinitely + * - less than 1 or underdamped: the mass tends to overshoot its starting position, but with + * every oscillation some energy is dissipated and the oscillation dies away + * - 1 or critically damped: the mass will fail to overshoot and make a single oscillation + * - greater than 1 or overdamped: the mass slowly returns to the anchor position without + * overshooting + */ + qreal dampingRatio() const; + + /** + * If the distance of the mass between two consecutive simulations is smaller than the epsilon + * value, consider that the mass has stopped moving. + */ + qreal epsilon() const; + void setEpsilon(qreal epsilon); + +private: + struct State + { + qreal position; + qreal velocity; + }; + + struct Slope + { + qreal dp; + qreal dv; + }; + + State integrate(const State &state, qreal dt); + Slope evaluate(const State &state, qreal dt, const Slope &slope); + + State m_prev; + State m_next; + qreal m_t; + qreal m_timestep; + + qreal m_anchor; + qreal m_springConstant; + qreal m_dampingRatio; + qreal m_dampingCoefficient; + qreal m_epsilon; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/slideback/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/slideback/CMakeLists.txt new file mode 100644 index 0000000000..152e28fa96 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slideback/CMakeLists.txt @@ -0,0 +1,12 @@ +####################################### +# Effect + +kwin_add_builtin_effect(slideback) +target_sources(slideback PRIVATE + main.cpp + motionmanager.cpp + slideback.cpp +) +target_link_libraries(slideback PRIVATE + kwin +) diff --git a/local/recipes/kde/kwin/source/src/plugins/slideback/main.cpp b/local/recipes/kde/kwin/source/src/plugins/slideback/main.cpp new file mode 100644 index 0000000000..1bb19f1771 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slideback/main.cpp @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "slideback.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY(SlideBackEffect, + "metadata.json.stripped") + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/slideback/metadata.json b/local/recipes/kde/kwin/source/src/plugins/slideback/metadata.json new file mode 100644 index 0000000000..cfed24a3a0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slideback/metadata.json @@ -0,0 +1,99 @@ +{ + "KPlugin": { + "Category": "Focus", + "Description": "Slide back windows when another window is raised", + "Description[ar]": "مرر النوافذ للخلف عند صعود نافذة أخرى", + "Description[az]": "Yeni pəncərə qaldırıldığında digər pəncərə arxaya sürüşür", + "Description[be]": "Пры ўзніманні іншага акна акно ссоўваецца назад", + "Description[bg]": "Плъзгане на прозорците назад, когато се повдигне друг прозорец", + "Description[ca@valencia]": "Llisca arrere les finestres quan s'eleva una altra finestra", + "Description[ca]": "Llisca enrere les finestres quan s'eleva una altra finestra", + "Description[cs]": "Zasunout okna, pokud ztratí zaměření", + "Description[da]": "Glid vinduer tilbage når et andet vindue rejses", + "Description[de]": "Fenster nach hinten gleiten, wenn ein anderes Fenster aktiviert wird", + "Description[en_GB]": "Slide back windows when another window is raised", + "Description[eo]": "Glitigi malantaŭen fenestrojn kiam alia fenestro estas levita", + "Description[es]": "Deslizar hacia atrás las ventanas cuando otra ventana pasa al primer plano", + "Description[et]": "Akende tagasiliuglemine uue akna esiletoomisel", + "Description[eu]": "Irristatu atzerantz beste leiho bat altxatzen denean", + "Description[fi]": "Liu’uttaa ikkunoita taaksepäin toisen ikkunan noustessa", + "Description[fr]": "Faire glisser les fenêtres vers l'arrière lors de l'apparition d'une autre fenêtre", + "Description[gl]": "Despraza cara atrás as xanelas cando outra se eleva.", + "Description[he]": "החלקת חלוניות אחורה כשחלון אחר מוגבה", + "Description[hu]": "Ablakok visszacsúsztatása másik ablak előtérbe hozásakor", + "Description[ia]": "Glissa retro fenestras quando un altere fenestra es elevate", + "Description[id]": "Menggeser mundur jendela ketika jendela yang lain dinaikkan", + "Description[is]": "Renna gluggum aftur þegar annar gluggi er opnaður", + "Description[it]": "Fai scivolare indietro le altre finestre quando un'altra finestra viene portata in primo piano", + "Description[ja]": "ウィンドウが前面に移動されたとき他のウィンドウを背面へスライドします", + "Description[ka]": "სხვა ფანჯრების აწევისას ფანჯრების უკან გასრიალებ", + "Description[ko]": "다른 창을 올렸을 때 창 뒤로 밀기", + "Description[lt]": "Slinkti langus į antrąjį planą, kai yra iškeliamas kitas langas", + "Description[lv]": "Aizslidināt aizmugurē logu, kad tiek aktivizēts cits logs", + "Description[nb]": "Skli tilbake vinduer som mister fokus", + "Description[nl]": "Schuif vensters terug wanneer een ander venster omhoog komt", + "Description[nn]": "Skli tilbake vindauge som mistar fokus", + "Description[pl]": "Przesuwa inne okna do tyłu przy przywoływaniu danego okna", + "Description[pt]": "Deslizar as janelas para trás quando é elevada outra janela", + "Description[pt_BR]": "Desliza as janelas para trás quando outra for elevada", + "Description[ro]": "Alunecă ferestrele înapoi când altă fereastră e ridicată", + "Description[ru]": "При подъёме окна другие окна соскальзывают назад", + "Description[sa]": "अन्यं खिडकं यदा उत्थापितं भवति तदा विण्डोः पृष्ठतः स्लाइड् कुर्वन्तु", + "Description[sk]": "Zasunie okná pri zdvihnutí iného okna", + "Description[sl]": "Podrsa okna nazaj, ko se dvigne drugo okno", + "Description[sv]": "Låt fönster skjutas tillbaka när ett annat fönster höjs", + "Description[tr]": "Başka bir pencere yükseltildiğinde pencereleri geri kaydır", + "Description[uk]": "Ковзання вікон на задній план під час підняття інших вікон", + "Description[vi]": "Trượt lùi cửa sổ khi một cửa sổ khác được nâng lên", + "Description[zh_CN]": "激活一个窗口时呈现其他窗口的后滑动效", + "Description[zh_TW]": "在另一個視窗被移到上層時把視窗滑到後面去", + "EnabledByDefault": false, + "License": "GPL", + "Name": "Slide Back", + "Name[ar]": "التمرير الخلفي", + "Name[az]": "Arxasına sürüşdürmək", + "Name[be]": "Ссоўване назад", + "Name[bg]": "Плъзгане назад", + "Name[ca@valencia]": "Llisca cap arrere", + "Name[ca]": "Llisca cap enrere", + "Name[cs]": "Zasunout", + "Name[da]": "Glid tilbage", + "Name[de]": "Nach hinten gleiten", + "Name[en_GB]": "Slide Back", + "Name[eo]": "Gliti Reen", + "Name[es]": "Deslizar hacia atrás", + "Name[et]": "Tagasiliuglemine", + "Name[eu]": "Irristatu atzerantz", + "Name[fi]": "Liuku taaksepäin", + "Name[fr]": "Glisser vers l'arrière", + "Name[gl]": "Botar para atrás", + "Name[he]": "החלקה אחורה", + "Name[hu]": "Visszacsúszó ablakok", + "Name[ia]": "Glissa de retro", + "Name[id]": "Geser Mundur", + "Name[is]": "Renna aftur", + "Name[it]": "Scivola indietro", + "Name[ja]": "後ろにスライド", + "Name[ka]": "უკან გადახვევა", + "Name[ko]": "뒤로 미끄러짐", + "Name[lt]": "Slinkimas į antrąjį planą", + "Name[lv]": "Aizlidināt aizmugurē", + "Name[nb]": "Skli tilbake", + "Name[nl]": "Naar achter bewegen", + "Name[nn]": "Skli tilbake", + "Name[pl]": "Przesuwanie w tył", + "Name[pt]": "Deslizar para Trás", + "Name[pt_BR]": "Deslizar para trás", + "Name[ro]": "Alunecă înapoi", + "Name[ru]": "Соскальзывание", + "Name[sa]": "स्लाइड बैक", + "Name[sk]": "Zasunúť", + "Name[sl]": "Drsaj nazaj", + "Name[sv]": "Skjut tillbaka", + "Name[tr]": "Geri Kaydır", + "Name[uk]": "Зниження", + "Name[vi]": "Trượt lùi", + "Name[zh_CN]": "窗口后滑动效", + "Name[zh_TW]": "滑動到後面" + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/slideback/motionmanager.cpp b/local/recipes/kde/kwin/source/src/plugins/slideback/motionmanager.cpp new file mode 100644 index 0000000000..773fd9d6c0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slideback/motionmanager.cpp @@ -0,0 +1,284 @@ +/* + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "plugins/slideback/motionmanager.h" + +namespace KWin +{ + +/*************************************************************** + Motion1D +***************************************************************/ + +Motion1D::Motion1D(double initial, double strength, double smoothness) + : Motion(initial, strength, smoothness) +{ +} + +Motion1D::Motion1D(const Motion1D &other) + : Motion(other) +{ +} + +Motion1D::~Motion1D() +{ +} + +/*************************************************************** + Motion2D +***************************************************************/ + +Motion2D::Motion2D(QPointF initial, double strength, double smoothness) + : Motion(initial, strength, smoothness) +{ +} + +Motion2D::Motion2D(const Motion2D &other) + : Motion(other) +{ +} + +Motion2D::~Motion2D() +{ +} + +/*************************************************************** + WindowMotionManager +***************************************************************/ + +WindowMotionManager::WindowMotionManager(bool useGlobalAnimationModifier) + : m_useGlobalAnimationModifier(useGlobalAnimationModifier) + +{ + // TODO: Allow developer to modify motion attributes +} // TODO: What happens when the window moves by an external force? + +WindowMotionManager::~WindowMotionManager() +{ +} + +void WindowMotionManager::manage(EffectWindow *w) +{ + if (m_managedWindows.contains(w)) { + return; + } + + double strength = 0.12; + double smoothness = 2.5; + if (m_useGlobalAnimationModifier && effects->animationTimeFactor()) { + // If the factor is == 0 then we just skip the calculation completely + strength = 0.12 / effects->animationTimeFactor(); + smoothness = effects->animationTimeFactor() * 2.5; + } + + WindowMotion &motion = m_managedWindows[w]; + motion.translation.setStrength(strength); + motion.translation.setSmoothness(smoothness); + motion.scale.setStrength(strength * 1.33); + motion.scale.setSmoothness(smoothness / 2.0); + + motion.translation.setValue(w->pos()); + motion.scale.setValue(QPointF(1.0, 1.0)); +} + +void WindowMotionManager::unmanage(EffectWindow *w) +{ + m_movingWindowsSet.remove(w); + m_managedWindows.remove(w); +} + +void WindowMotionManager::unmanageAll() +{ + m_managedWindows.clear(); + m_movingWindowsSet.clear(); +} + +void WindowMotionManager::calculate(int time) +{ + if (!effects->animationTimeFactor()) { + // Just skip it completely if the user wants no animation + m_movingWindowsSet.clear(); + QHash::iterator it = m_managedWindows.begin(); + for (; it != m_managedWindows.end(); ++it) { + WindowMotion *motion = &it.value(); + motion->translation.finish(); + motion->scale.finish(); + } + } + + QHash::iterator it = m_managedWindows.begin(); + for (; it != m_managedWindows.end(); ++it) { + WindowMotion *motion = &it.value(); + int stopped = 0; + + // TODO: What happens when distance() == 0 but we are still moving fast? + // TODO: Motion needs to be calculated from the window's center + + Motion2D *trans = &motion->translation; + if (trans->distance().isNull()) { + ++stopped; + } else { + // Still moving + trans->calculate(time); + const short fx = trans->target().x() <= trans->startValue().x() ? -1 : 1; + const short fy = trans->target().y() <= trans->startValue().y() ? -1 : 1; + if (trans->distance().x() * fx / 0.5 < 1.0 && trans->velocity().x() * fx / 0.2 < 1.0 + && trans->distance().y() * fy / 0.5 < 1.0 && trans->velocity().y() * fy / 0.2 < 1.0) { + // Hide tiny oscillations + motion->translation.finish(); + ++stopped; + } + } + + Motion2D *scale = &motion->scale; + if (scale->distance().isNull()) { + ++stopped; + } else { + // Still scaling + scale->calculate(time); + const short fx = scale->target().x() < 1.0 ? -1 : 1; + const short fy = scale->target().y() < 1.0 ? -1 : 1; + if (scale->distance().x() * fx / 0.001 < 1.0 && scale->velocity().x() * fx / 0.05 < 1.0 + && scale->distance().y() * fy / 0.001 < 1.0 && scale->velocity().y() * fy / 0.05 < 1.0) { + // Hide tiny oscillations + motion->scale.finish(); + ++stopped; + } + } + + // We just finished this window's motion + if (stopped == 2) { + m_movingWindowsSet.remove(it.key()); + } + } +} + +void WindowMotionManager::reset() +{ + QHash::iterator it = m_managedWindows.begin(); + for (; it != m_managedWindows.end(); ++it) { + WindowMotion *motion = &it.value(); + EffectWindow *window = it.key(); + motion->translation.setTarget(window->pos()); + motion->translation.finish(); + motion->scale.setTarget(QPointF(1.0, 1.0)); + motion->scale.finish(); + } +} + +void WindowMotionManager::reset(EffectWindow *w) +{ + QHash::iterator it = m_managedWindows.find(w); + if (it == m_managedWindows.end()) { + return; + } + + WindowMotion *motion = &it.value(); + motion->translation.setTarget(w->pos()); + motion->translation.finish(); + motion->scale.setTarget(QPointF(1.0, 1.0)); + motion->scale.finish(); +} + +void WindowMotionManager::apply(EffectWindow *w, WindowPaintData &data) +{ + QHash::iterator it = m_managedWindows.find(w); + if (it == m_managedWindows.end()) { + return; + } + + // TODO: Take into account existing scale so that we can work with multiple managers (E.g. Present windows + grid) + WindowMotion *motion = &it.value(); + data += (motion->translation.value() - QPointF(w->x(), w->y())); + data *= QVector2D(motion->scale.value()); +} + +void WindowMotionManager::moveWindow(EffectWindow *w, QPoint target, double scale, double yScale) +{ + QHash::iterator it = m_managedWindows.find(w); + Q_ASSERT(it != m_managedWindows.end()); // Notify the effect author that they did something wrong + + WindowMotion *motion = &it.value(); + + if (yScale == 0.0) { + yScale = scale; + } + QPointF scalePoint(scale, yScale); + + if (motion->translation.value() == target && motion->scale.value() == scalePoint) { + return; // Window already at that position + } + + motion->translation.setTarget(target); + motion->scale.setTarget(scalePoint); + + m_movingWindowsSet << w; +} + +QRectF WindowMotionManager::transformedGeometry(EffectWindow *w) const +{ + QHash::const_iterator it = m_managedWindows.constFind(w); + if (it == m_managedWindows.end()) { + return w->frameGeometry(); + } + + const WindowMotion *motion = &it.value(); + QRectF geometry(w->frameGeometry()); + + // TODO: Take into account existing scale so that we can work with multiple managers (E.g. Present windows + grid) + geometry.moveTo(motion->translation.value()); + geometry.setWidth(geometry.width() * motion->scale.value().x()); + geometry.setHeight(geometry.height() * motion->scale.value().y()); + + return geometry; +} + +void WindowMotionManager::setTransformedGeometry(EffectWindow *w, const QRectF &geometry) +{ + QHash::iterator it = m_managedWindows.find(w); + if (it == m_managedWindows.end()) { + return; + } + WindowMotion *motion = &it.value(); + motion->translation.setValue(geometry.topLeft()); + motion->scale.setValue(QPointF(geometry.width() / qreal(w->width()), geometry.height() / qreal(w->height()))); +} + +QRectF WindowMotionManager::targetGeometry(EffectWindow *w) const +{ + QHash::const_iterator it = m_managedWindows.constFind(w); + if (it == m_managedWindows.end()) { + return w->frameGeometry(); + } + + const WindowMotion *motion = &it.value(); + QRectF geometry(w->frameGeometry()); + + // TODO: Take into account existing scale so that we can work with multiple managers (E.g. Present windows + grid) + geometry.moveTo(motion->translation.target()); + geometry.setWidth(geometry.width() * motion->scale.target().x()); + geometry.setHeight(geometry.height() * motion->scale.target().y()); + + return geometry; +} + +EffectWindow *WindowMotionManager::windowAtPoint(QPoint point, bool useStackingOrder) const +{ + // TODO: Stacking order uses EffectsHandler::stackingOrder() then filters by m_managedWindows + QHash::ConstIterator it = m_managedWindows.constBegin(); + while (it != m_managedWindows.constEnd()) { + if (RectF(transformedGeometry(it.key())).contains(point)) { + return it.key(); + } + ++it; + } + + return nullptr; +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/slideback/motionmanager.h b/local/recipes/kde/kwin/source/src/plugins/slideback/motionmanager.h new file mode 100644 index 0000000000..e1bef1d417 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slideback/motionmanager.h @@ -0,0 +1,361 @@ +/* + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + SPDX-FileCopyrightText: 2010, 2011 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/effecthandler.h" + +namespace KWin +{ + +/** + * @internal + */ +template +class KWIN_EXPORT Motion +{ +public: + /** + * Creates a new motion object. "Strength" is the amount of + * acceleration that is applied to the object when the target + * changes and "smoothness" relates to how fast the object + * can change its direction and speed. + */ + explicit Motion(T initial, double strength, double smoothness); + /** + * Creates an exact copy of another motion object, including + * position, target and velocity. + */ + Motion(const Motion &other); + ~Motion(); + + inline T value() const + { + return m_value; + } + inline void setValue(const T value) + { + m_value = value; + } + inline T target() const + { + return m_target; + } + inline void setTarget(const T target) + { + m_start = m_value; + m_target = target; + } + inline T velocity() const + { + return m_velocity; + } + inline void setVelocity(const T velocity) + { + m_velocity = velocity; + } + + inline double strength() const + { + return m_strength; + } + inline void setStrength(const double strength) + { + m_strength = strength; + } + inline double smoothness() const + { + return m_smoothness; + } + inline void setSmoothness(const double smoothness) + { + m_smoothness = smoothness; + } + inline T startValue() + { + return m_start; + } + + /** + * The distance between the current position and the target. + */ + inline T distance() const + { + return m_target - m_value; + } + + /** + * Calculates the new position if not at the target. Called + * once per frame only. + */ + void calculate(const int msec); + /** + * Place the object on top of the target immediately, + * bypassing all movement calculation. + */ + void finish(); + +private: + T m_value; + T m_start; + T m_target; + T m_velocity; + double m_strength; + double m_smoothness; +}; + +/** + * @short A single 1D motion dynamics object. + * + * This class represents a single object that can be moved around a + * 1D space. Although it can be used directly by itself it is + * recommended to use a motion manager instead. + */ +class KWIN_EXPORT Motion1D : public Motion +{ +public: + explicit Motion1D(double initial = 0.0, double strength = 0.08, double smoothness = 4.0); + Motion1D(const Motion1D &other); + ~Motion1D(); +}; + +/** + * @short A single 2D motion dynamics object. + * + * This class represents a single object that can be moved around a + * 2D space. Although it can be used directly by itself it is + * recommended to use a motion manager instead. + */ +class KWIN_EXPORT Motion2D : public Motion +{ +public: + explicit Motion2D(QPointF initial = QPointF(), double strength = 0.08, double smoothness = 4.0); + Motion2D(const Motion2D &other); + ~Motion2D(); +}; + +/** + * @short Helper class for motion dynamics in KWin effects. + * + * This motion manager class is intended to help KWin effect authors + * move windows across the screen smoothly and naturally. Once + * windows are registered by the manager the effect can issue move + * commands with the moveWindow() methods. The position of any + * managed window can be determined in realtime by the + * transformedGeometry() method. As the manager knows if any windows + * are moving at any given time it can also be used as a notifier as + * to see whether the effect is active or not. + */ +class KWIN_EXPORT WindowMotionManager +{ +public: + /** + * Creates a new window manager object. + */ + explicit WindowMotionManager(bool useGlobalAnimationModifier = true); + ~WindowMotionManager(); + + /** + * Register a window for managing. + */ + void manage(EffectWindow *w); + /** + * Register a list of windows for managing. + */ + inline void manage(const QList &list) + { + for (int i = 0; i < list.size(); i++) { + manage(list.at(i)); + } + } + /** + * Deregister a window. All transformations applied to the + * window will be permanently removed and cannot be recovered. + */ + void unmanage(EffectWindow *w); + /** + * Deregister all windows, returning the manager to its + * originally initiated state. + */ + void unmanageAll(); + /** + * Determine the new positions for windows that have not + * reached their target. Called once per frame, usually in + * prePaintScreen(). Remember to set the + * Effect::PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS flag. + */ + void calculate(int time); + /** + * Modify a registered window's paint data to make it appear + * at its real location on the screen. Usually called in + * paintWindow(). Remember to flag the window as having been + * transformed in prePaintWindow() by calling + * WindowPrePaintData::setTransformed() + */ + void apply(EffectWindow *w, WindowPaintData &data); + /** + * Set all motion targets and values back to where the + * windows were before transformations. The same as + * unmanaging then remanaging all windows. + */ + void reset(); + /** + * Resets the motion target and current value of a single + * window. + */ + void reset(EffectWindow *w); + + /** + * Ask the manager to move the window to the target position + * with the specified scale. If `yScale` is not provided or + * set to 0.0, `scale` will be used as the scale in the + * vertical direction as well as in the horizontal direction. + */ + void moveWindow(EffectWindow *w, QPoint target, double scale = 1.0, double yScale = 0.0); + /** + * This is an overloaded method, provided for convenience. + * + * Ask the manager to move the window to the target rectangle. + * Automatically determines scale. + */ + inline void moveWindow(EffectWindow *w, QRect target) + { + // TODO: Scale might be slightly different in the comparison due to rounding + moveWindow(w, target.topLeft(), + target.width() / double(w->width()), target.height() / double(w->height())); + } + + /** + * Retrieve the current transformed geometry of a registered + * window. + */ + QRectF transformedGeometry(EffectWindow *w) const; + /** + * Sets the current transformed geometry of a registered window to the given geometry. + * @see transformedGeometry + * @since 4.5 + */ + void setTransformedGeometry(EffectWindow *w, const QRectF &geometry); + /** + * Retrieve the current target geometry of a registered + * window. + */ + QRectF targetGeometry(EffectWindow *w) const; + /** + * Return the window that has its transformed geometry under + * the specified point. It is recommended to use the stacking + * order as it's what the user sees, but it is slightly + * slower to process. + */ + EffectWindow *windowAtPoint(QPoint point, bool useStackingOrder = true) const; + + /** + * Return a list of all currently registered windows. + */ + inline QList managedWindows() const + { + return m_managedWindows.keys(); + } + /** + * Returns whether or not a specified window is being managed + * by this manager object. + */ + inline bool isManaging(EffectWindow *w) const + { + return m_managedWindows.contains(w); + } + /** + * Returns whether or not this manager object is actually + * managing any windows or not. + */ + inline bool managingWindows() const + { + return !m_managedWindows.empty(); + } + /** + * Returns whether all windows have reached their targets yet + * or not. Can be used to see if an effect should be + * processed and displayed or not. + */ + inline bool areWindowsMoving() const + { + return !m_movingWindowsSet.isEmpty(); + } + /** + * Returns whether a window has reached its targets yet + * or not. + */ + inline bool isWindowMoving(EffectWindow *w) const + { + return m_movingWindowsSet.contains(w); + } + +private: + bool m_useGlobalAnimationModifier; + struct WindowMotion + { + // TODO: Rotation, etc? + Motion2D translation; // Absolute position + Motion2D scale; // xScale and yScale + }; + QHash m_managedWindows; + QSet m_movingWindowsSet; +}; + +template +Motion::Motion(T initial, double strength, double smoothness) + : m_value(initial) + , m_start(initial) + , m_target(initial) + , m_velocity() + , m_strength(strength) + , m_smoothness(smoothness) +{ +} + +template +Motion::Motion(const Motion &other) + : m_value(other.value()) + , m_start(other.target()) + , m_target(other.target()) + , m_velocity(other.velocity()) + , m_strength(other.strength()) + , m_smoothness(other.smoothness()) +{ +} + +template +Motion::~Motion() +{ +} + +template +void Motion::calculate(const int msec) +{ + if (m_value == m_target && m_velocity == T()) { // At target and not moving + return; + } + + // Poor man's time independent calculation + int steps = std::max(1, msec / 5); + for (int i = 0; i < steps; i++) { + T diff = m_target - m_value; + T strength = diff * m_strength; + m_velocity = (m_smoothness * m_velocity + strength) / (m_smoothness + 1.0); + m_value += m_velocity; + } +} + +template +void Motion::finish() +{ + m_value = m_target; + m_velocity = T(); +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/slideback/slideback.cpp b/local/recipes/kde/kwin/source/src/plugins/slideback/slideback.cpp new file mode 100644 index 0000000000..0e05163cfb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slideback/slideback.cpp @@ -0,0 +1,353 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Michael Zanetti + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "slideback.h" +#include "core/renderviewport.h" +#include "effect/effecthandler.h" + +namespace KWin +{ + +SlideBackEffect::SlideBackEffect() +{ + m_tabboxActive = 0; + m_justMapped = m_upmostWindow = nullptr; + connect(effects, &EffectsHandler::windowAdded, this, &SlideBackEffect::slotWindowAdded); + connect(effects, &EffectsHandler::windowDeleted, this, &SlideBackEffect::slotWindowDeleted); + connect(effects, &EffectsHandler::tabBoxAdded, this, &SlideBackEffect::slotTabBoxAdded); + connect(effects, &EffectsHandler::stackingOrderChanged, this, &SlideBackEffect::slotStackingOrderChanged); + connect(effects, &EffectsHandler::tabBoxClosed, this, &SlideBackEffect::slotTabBoxClosed); + + const auto windows = effects->stackingOrder(); + for (EffectWindow *window : windows) { + slotWindowAdded(window); + } +} + +void SlideBackEffect::slotStackingOrderChanged() +{ + if (effects->activeFullScreenEffect() || m_tabboxActive) { + oldStackingOrder = effects->stackingOrder(); + usableOldStackingOrder = usableWindows(oldStackingOrder); + return; + } + + QList newStackingOrder = effects->stackingOrder(), + usableNewStackingOrder = usableWindows(newStackingOrder); + if (usableNewStackingOrder == usableOldStackingOrder || usableNewStackingOrder.isEmpty()) { + oldStackingOrder = newStackingOrder; + usableOldStackingOrder = usableNewStackingOrder; + return; + } + + m_upmostWindow = usableNewStackingOrder.last(); + + if (m_upmostWindow == m_justMapped) { // a window was added, got on top, stacking changed. Nothing impressive + m_justMapped = nullptr; + } else if (!usableOldStackingOrder.isEmpty() && m_upmostWindow != usableOldStackingOrder.last()) { + windowRaised(m_upmostWindow); + } + + oldStackingOrder = newStackingOrder; + usableOldStackingOrder = usableNewStackingOrder; +} + +void SlideBackEffect::windowRaised(EffectWindow *w) +{ + // Determine all windows on top of the activated one + bool currentFound = false; + for (EffectWindow *tmp : std::as_const(oldStackingOrder)) { + if (!currentFound) { + if (tmp == w) { + currentFound = true; + } + } else { + if (isWindowUsable(tmp) && tmp->isOnCurrentDesktop() && w->isOnCurrentDesktop() + && tmp->isOnCurrentActivity() && w->isOnCurrentActivity()) { + // Do we have to move it? + if (intersects(w, tmp->frameGeometry().toRect())) { + QRect slideRect; + slideRect = getSlideDestination(getModalGroupGeometry(w), tmp->frameGeometry().toRect()); + effects->setElevatedWindow(tmp, true); + elevatedList.append(tmp); + motionManager.manage(tmp); + motionManager.moveWindow(tmp, slideRect); + destinationList.insert(tmp, slideRect); + coveringWindows.append(tmp); + } else { + // Does it intersect with a moved (elevated) window and do we have to elevate it too? + for (EffectWindow *elevatedWindow : std::as_const(elevatedList)) { + if (tmp->frameGeometry().intersects(elevatedWindow->frameGeometry())) { + effects->setElevatedWindow(tmp, true); + elevatedList.append(tmp); + break; + } + } + } + } + if (tmp->isDock() || tmp->keepAbove()) { + effects->setElevatedWindow(tmp, true); + elevatedList.append(tmp); + } + } + } + // If a window is minimized it could happen that the panels stay elevated without any windows sliding. + // clear all elevation settings + if (!motionManager.managingWindows()) { + for (EffectWindow *tmp : std::as_const(elevatedList)) { + effects->setElevatedWindow(tmp, false); + } + } +} + +QRect SlideBackEffect::getSlideDestination(const QRect &windowUnderGeometry, const QRect &windowOverGeometry) +{ + // Determine the shortest way: + int leftSlide = windowUnderGeometry.left() - windowOverGeometry.right() - 20; + int rightSlide = windowUnderGeometry.right() - windowOverGeometry.left() + 20; + int upSlide = windowUnderGeometry.top() - windowOverGeometry.bottom() - 20; + int downSlide = windowUnderGeometry.bottom() - windowOverGeometry.top() + 20; + + int horizSlide = leftSlide; + if (std::abs(horizSlide) > std::abs(rightSlide)) { + horizSlide = rightSlide; + } + int vertSlide = upSlide; + if (std::abs(vertSlide) > std::abs(downSlide)) { + vertSlide = downSlide; + } + + QRect slideRect = windowOverGeometry; + if (std::abs(horizSlide) < std::abs(vertSlide)) { + slideRect.moveLeft(slideRect.x() + horizSlide); + } else { + slideRect.moveTop(slideRect.y() + vertSlide); + } + return slideRect; +} + +void SlideBackEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + int time = 0; + if (m_lastPresentTime.count()) { + time = (presentTime - m_lastPresentTime).count(); + } + m_lastPresentTime = presentTime; + + if (motionManager.managingWindows()) { + motionManager.calculate(time); + data.mask |= Effect::PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS; + } + + const QList windows = effects->stackingOrder(); + for (auto *w : windows) { + w->setData(WindowForceBlurRole, QVariant(true)); + } + + effects->prePaintScreen(data, presentTime); +} + +void SlideBackEffect::postPaintScreen() +{ + const auto managedWindows = motionManager.managedWindows(); + for (EffectWindow *w : managedWindows) { + if (destinationList.contains(w)) { + if (!motionManager.isWindowMoving(w)) { // has window reached its destination? + // If we are still intersecting with the upmostWindow it is moving. slide to somewhere else + // restore the stacking order of all windows not intersecting any more except panels + if (coveringWindows.contains(w)) { + QList tmpList; + for (EffectWindow *tmp : std::as_const(elevatedList)) { + QRect elevatedGeometry = tmp->frameGeometry().toRect(); + if (motionManager.isManaging(tmp)) { + elevatedGeometry = motionManager.transformedGeometry(tmp).toAlignedRect(); + } + if (m_upmostWindow && !tmp->isDock() && !tmp->keepAbove() && m_upmostWindow->frameGeometry().intersects(elevatedGeometry)) { + QRect newDestination; + newDestination = getSlideDestination(getModalGroupGeometry(m_upmostWindow), elevatedGeometry); + if (!motionManager.isManaging(tmp)) { + motionManager.manage(tmp); + } + motionManager.moveWindow(tmp, newDestination); + destinationList[tmp] = newDestination; + } else { + if (!tmp->isDock()) { + bool keepElevated = false; + for (EffectWindow *elevatedWindow : std::as_const(tmpList)) { + if (tmp->frameGeometry().intersects(elevatedWindow->frameGeometry())) { + keepElevated = true; + } + } + if (!keepElevated) { + effects->setElevatedWindow(tmp, false); + elevatedList.removeAll(tmp); + } + } + } + tmpList.append(tmp); + } + } else { + // Move the window back where it belongs + motionManager.moveWindow(w, w->frameGeometry().toRect()); + destinationList.remove(w); + } + } + } else { + // is window back at its original position? + if (!motionManager.isWindowMoving(w)) { + motionManager.unmanage(w); + effects->addRepaintFull(); + } + } + if (coveringWindows.contains(w)) { + // It could happen that there is no aciveWindow() here if the user clicks the close-button on an inactive window. + // Just skip... the window will be removed in windowDeleted() later + if (m_upmostWindow && !intersects(m_upmostWindow, motionManager.transformedGeometry(w).toAlignedRect())) { + coveringWindows.removeAll(w); + if (coveringWindows.isEmpty()) { + // Restore correct stacking order + for (EffectWindow *tmp : std::as_const(elevatedList)) { + effects->setElevatedWindow(tmp, false); + } + elevatedList.clear(); + } + } + } + } + + if (motionManager.areWindowsMoving()) { + effects->addRepaintFull(); + } + + for (auto &w : effects->stackingOrder()) { + w->setData(WindowForceBlurRole, QVariant()); + } + + if (!isActive()) { + m_lastPresentTime = std::chrono::milliseconds::zero(); + } + + effects->postPaintScreen(); +} + +void SlideBackEffect::prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) +{ + if (motionManager.isManaging(w)) { + data.setTransformed(); + } + + effects->prePaintWindow(view, w, data, presentTime); +} + +void SlideBackEffect::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceGeometry, WindowPaintData &data) +{ + if (motionManager.isManaging(w)) { + motionManager.apply(w, data); + } + Region effectiveRegion = deviceGeometry; + for (const Region &r : std::as_const(clippedRegions)) { + effectiveRegion = effectiveRegion.intersected(viewport.mapToDeviceCoordinatesAligned(r)); + } + effects->paintWindow(renderTarget, viewport, w, mask, effectiveRegion, data); + clippedRegions.clear(); +} + +void SlideBackEffect::slotWindowDeleted(EffectWindow *w) +{ + if (w == m_upmostWindow) { + m_upmostWindow = nullptr; + } + if (w == m_justMapped) { + m_justMapped = nullptr; + } + usableOldStackingOrder.removeAll(w); + oldStackingOrder.removeAll(w); + coveringWindows.removeAll(w); + elevatedList.removeAll(w); + if (motionManager.isManaging(w)) { + motionManager.unmanage(w); + } +} + +void SlideBackEffect::slotWindowAdded(EffectWindow *w) +{ + m_justMapped = w; + + connect(w, &EffectWindow::minimizedChanged, this, [this, w]() { + if (!w->isMinimized()) { + slotWindowUnminimized(w); + } + }); +} + +void SlideBackEffect::slotWindowUnminimized(EffectWindow *w) +{ + // SlideBack should not be triggered on an unminimized window. For this we need to store the last unminimized window. + m_justMapped = w; + // the stackingOrderChanged() signal came before the window turned an effect window + // usually this is no problem as the change shall not be caught anyway, but + // the window may have changed its stack position, bug #353745 + slotStackingOrderChanged(); +} + +void SlideBackEffect::slotTabBoxAdded() +{ + ++m_tabboxActive; +} + +void SlideBackEffect::slotTabBoxClosed() +{ + m_tabboxActive = std::max(m_tabboxActive - 1, 0); +} + +bool SlideBackEffect::isWindowUsable(EffectWindow *w) +{ + return w && (w->isNormalWindow() || w->isDialog()) && !w->keepAbove() && !w->isDeleted() && !w->isMinimized(); +} + +bool SlideBackEffect::intersects(EffectWindow *windowUnder, const QRect &windowOverGeometry) +{ + QRect windowUnderGeometry = getModalGroupGeometry(windowUnder); + return windowUnderGeometry.intersects(windowOverGeometry); +} + +QList SlideBackEffect::usableWindows(const QList &allWindows) +{ + QList retList; + auto isWindowVisible = [](const EffectWindow *window) { + return window && effects->virtualScreenGeometry().intersects(window->frameGeometry().toAlignedRect()); + }; + for (EffectWindow *tmp : std::as_const(allWindows)) { + if (isWindowUsable(tmp) && isWindowVisible(tmp)) { + retList.append(tmp); + } + } + return retList; +} + +QRect SlideBackEffect::getModalGroupGeometry(EffectWindow *w) +{ + QRect modalGroupGeometry = w->frameGeometry().toRect(); + if (w->isModal()) { + const auto mainWindows = w->mainWindows(); + for (EffectWindow *modalWindow : mainWindows) { + modalGroupGeometry = modalGroupGeometry.united(getModalGroupGeometry(modalWindow)); + } + } + return modalGroupGeometry; +} + +bool SlideBackEffect::isActive() const +{ + return motionManager.managingWindows(); +} + +} // Namespace + +#include "moc_slideback.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/slideback/slideback.h b/local/recipes/kde/kwin/source/src/plugins/slideback/slideback.h new file mode 100644 index 0000000000..c7df9cf983 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slideback/slideback.h @@ -0,0 +1,66 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Michael Zanetti + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +// Include with base class for effects. +#include "effect/effect.h" +#include "plugins/slideback/motionmanager.h" + +namespace KWin +{ + +class SlideBackEffect + : public Effect +{ + Q_OBJECT +public: + SlideBackEffect(); + + void prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) override; + void paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceGeometry, WindowPaintData &data) override; + + void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override; + void postPaintScreen() override; + bool isActive() const override; + + int requestedEffectChainPosition() const override + { + return 50; + } + +public Q_SLOTS: + void slotWindowAdded(KWin::EffectWindow *w); + void slotWindowDeleted(KWin::EffectWindow *w); + void slotWindowUnminimized(KWin::EffectWindow *w); + void slotStackingOrderChanged(); + void slotTabBoxAdded(); + void slotTabBoxClosed(); + +private: + WindowMotionManager motionManager; + QList usableOldStackingOrder; + QList oldStackingOrder; + QList coveringWindows; + QList elevatedList; + EffectWindow *m_justMapped, *m_upmostWindow; + QHash destinationList; + int m_tabboxActive; + QList clippedRegions; + std::chrono::milliseconds m_lastPresentTime = std::chrono::milliseconds::zero(); + + QRect getSlideDestination(const QRect &windowUnderGeometry, const QRect &windowOverGeometry); + bool isWindowUsable(EffectWindow *w); + bool intersects(EffectWindow *windowUnder, const QRect &windowOverGeometry); + QList usableWindows(const QList &allWindows); + QRect getModalGroupGeometry(EffectWindow *w); + void windowRaised(EffectWindow *w); +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/slidingpopups/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/slidingpopups/CMakeLists.txt new file mode 100644 index 0000000000..cc787278eb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slidingpopups/CMakeLists.txt @@ -0,0 +1,19 @@ +####################################### +# Effect + +# Source files +set(slidingpopups_SOURCES + main.cpp + slidingpopups.cpp +) + +kconfig_add_kcfg_files(slidingpopups_SOURCES + slidingpopupsconfig.kcfgc +) + +kwin_add_builtin_effect(slidingpopups ${slidingpopups_SOURCES}) +target_link_libraries(slidingpopups PRIVATE + kwin + + KF6::ConfigGui +) diff --git a/local/recipes/kde/kwin/source/src/plugins/slidingpopups/main.cpp b/local/recipes/kde/kwin/source/src/plugins/slidingpopups/main.cpp new file mode 100644 index 0000000000..f992843770 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slidingpopups/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "slidingpopups.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(SlidingPopupsEffect, + "metadata.json.stripped", + return SlidingPopupsEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/slidingpopups/metadata.json b/local/recipes/kde/kwin/source/src/plugins/slidingpopups/metadata.json new file mode 100644 index 0000000000..503a964c37 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slidingpopups/metadata.json @@ -0,0 +1,75 @@ +{ + "KPlugin": { + "Category": "Appearance", + "Description": "Slide Plasma popups in and out", + "Description[ar]": "حرك النوافذ المنبثقة للبلازما للداخل والخارج", + "Description[bg]": "Плъзгане на изскачащите прозорци на Plasma", + "Description[ca@valencia]": "Llisca les finestres emergents de Plasma d'entrada i eixida", + "Description[ca]": "Llisca les finestres emergents del Plasma d'entrada i sortida", + "Description[de]": "Plasma-Aufklappfenster ein- und ausgleiten lassen", + "Description[es]": "Deslizar las ventanas emergentes de Plasma al mostrarlas y ocultarlas", + "Description[eu]": "Plasma gainerakorrak barrura eta kanpora irristatu", + "Description[fi]": "Liu’uta Plasma-ponnahdusikkunat sisään ja ulos", + "Description[fr]": "Faire un effet de diaporama avant et arrière pour les menus contextuels", + "Description[he]": "החלקת חלוניות של פלזמה פנימה והחוצה", + "Description[hu]": "Csúszás Plasma felugrók megjelenésekor és eltűnésekor", + "Description[ia]": "Glissa popups de Plasma in e out (in e foras)", + "Description[is]": "Renna Plasma-sprettigluggum inn og út", + "Description[it]": "Mostra e nascondi le finestre a comparsa di Plasma con scorrimento", + "Description[ja]": "Plasma のポップアップをスライド", + "Description[ka]": "Plasma-ის მხტუნარების სრიალი შიგნით და გარეთ", + "Description[ko]": "Plasma 팝업을 슬라이드 인 및 아웃", + "Description[lt]": "Padaryti, kad Plasma iškylantieji langai įslinktų ir išslinktų", + "Description[lv]": "Slidināt „Plasma“ uzlecošos logus", + "Description[nl]": "Plasma pop-ups in- en uitschuiven", + "Description[nn]": "Skli Plasma-sprettoppvindauge inn og ut", + "Description[pl]": "Wyłanianie i zanikanie okien wysuwnych Plazmy", + "Description[pt_BR]": "Deslizar mensagens do Plasma", + "Description[ro]": "Alunecă indiciile contextuale Plasma", + "Description[ru]": "Скольжение всплывающих окон Plasma при их появлении и скрытии", + "Description[sk]": "Vysúvanie a zasúvanie vyskakovacích okien Plasma", + "Description[sl]": "Drsaj pojavna okna Plasme", + "Description[sv]": "Skjut Plasma meddelanderutor in och ut", + "Description[tr]": "Plasma açılır pencerelerini içeri ve dışarı kaydır", + "Description[uk]": "Виковзування за заковзування контекстних вікон Плазми", + "Description[zh_CN]": "滑动显示/隐藏 Plasma 弹窗", + "Description[zh_TW]": "讓 Plasma 彈出視窗滑進滑出", + "EnabledByDefault": true, + "License": "GPL", + "Name": "Sliding popups", + "Name[ar]": "المنبثقات المنزلقة", + "Name[bg]": "Плъзгащи се изскачащи прозорци", + "Name[ca@valencia]": "Missatges emergents desplaçant-se", + "Name[ca]": "Missatges emergents desplaçant-se", + "Name[cs]": "Klouzající vyskakovací okna", + "Name[de]": "Hereingleitende Aufklappfenster", + "Name[es]": "Deslizar ventanas emergentes", + "Name[eu]": "Gainerakor irristagarriak", + "Name[fi]": "Ponnahdusikkunaliuku", + "Name[fr]": "Annotations glissantes", + "Name[he]": "חלוניות צצות מחליקות", + "Name[hu]": "Csúszó felugrók", + "Name[ia]": "Popups glissante", + "Name[id]": "Sliding popups", + "Name[is]": "Renna sprettigluggum", + "Name[it]": "Finestre a comparsa che scivolano", + "Name[ja]": "スライドするポップアップ", + "Name[ka]": "მცურავი მხტუნარები", + "Name[ko]": "미끄러지는 팝업", + "Name[lt]": "Slenkantys iškylantieji langai", + "Name[lv]": "Slīdoši uzlecošie logi", + "Name[nl]": "Schuivende pop-ups", + "Name[nn]": "Glidande sprettoppvindauge", + "Name[pl]": "Wysuwne okna", + "Name[pt_BR]": "Mensagens deslizantes", + "Name[ro]": "Indicii lunecoase", + "Name[ru]": "Скольжение всплывающих окон", + "Name[sk]": "Vyskakovacie okná", + "Name[sl]": "Drsna pojavna okna", + "Name[sv]": "Skjutande meddelanderutor", + "Name[tr]": "Kayan açılır pencereler", + "Name[uk]": "Ковзні контекстні вікна", + "Name[zh_CN]": "气泡滑动动效", + "Name[zh_TW]": "滑動的彈出視窗" + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/slidingpopups/slidingpopups.cpp b/local/recipes/kde/kwin/source/src/plugins/slidingpopups/slidingpopups.cpp new file mode 100644 index 0000000000..6dacda9bac --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slidingpopups/slidingpopups.cpp @@ -0,0 +1,535 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Marco Martin notmart @gmail.com + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "slidingpopups.h" +#include "slidingpopupsconfig.h" + +#include "core/renderviewport.h" +#include "effect/effecthandler.h" +#include "scene/windowitem.h" +#include "wayland/display.h" +#include "wayland/slide.h" +#include "wayland/surface.h" + +#include +#include +#include +#include + +#include + +Q_DECLARE_METATYPE(KWindowEffects::SlideFromLocation) + +using namespace std::chrono_literals; + +namespace KWin +{ + +SlideManagerInterface *SlidingPopupsEffect::s_slideManager = nullptr; +QTimer *SlidingPopupsEffect::s_slideManagerRemoveTimer = nullptr; + +SlidingPopupsEffect::SlidingPopupsEffect() +{ + SlidingPopupsConfig::instance(effects->config()); + + if (!s_slideManagerRemoveTimer) { + s_slideManagerRemoveTimer = new QTimer(QCoreApplication::instance()); + s_slideManagerRemoveTimer->setSingleShot(true); + s_slideManagerRemoveTimer->callOnTimeout([]() { + s_slideManager->remove(); + s_slideManager = nullptr; + }); + } + s_slideManagerRemoveTimer->stop(); + if (!s_slideManager) { + s_slideManager = new SlideManagerInterface(effects->waylandDisplay(), s_slideManagerRemoveTimer); + } + + m_slideLength = QFontMetrics(QGuiApplication::font()).height() * 8; + + connect(effects, &EffectsHandler::windowAdded, this, &SlidingPopupsEffect::slotWindowAdded); + connect(effects, &EffectsHandler::windowClosed, this, &SlidingPopupsEffect::slotWindowClosed); + connect(effects, &EffectsHandler::windowDeleted, this, &SlidingPopupsEffect::slotWindowDeleted); + connect(effects, &EffectsHandler::desktopChanged, + this, &SlidingPopupsEffect::stopAnimations); + connect(effects, &EffectsHandler::activeFullScreenEffectChanged, + this, &SlidingPopupsEffect::stopAnimations); + connect(effects, &EffectsHandler::screenLockingChanged, + this, &SlidingPopupsEffect::stopAnimations); + + reconfigure(ReconfigureAll); + + const QList windows = effects->stackingOrder(); + for (EffectWindow *window : windows) { + setupSlideData(window); + } +} + +SlidingPopupsEffect::~SlidingPopupsEffect() +{ + // When compositing is restarted, avoid removing the manager immediately. + if (s_slideManager) { + s_slideManagerRemoveTimer->start(1000); + } + + // Cancel animations here while both m_animations and m_animationsData are still valid. + // slotWindowDeleted may access m_animationsData when an animation is removed. + m_animations.clear(); + m_animationsData.clear(); +} + +bool SlidingPopupsEffect::supported() +{ + return effects->animationsSupported(); +} + +void SlidingPopupsEffect::reconfigure(ReconfigureFlags flags) +{ + SlidingPopupsConfig::self()->read(); + // Keep these durations in sync with the value of Kirigami.Units.longDuration + m_slideInDuration = std::chrono::milliseconds( + static_cast(animationTime(SlidingPopupsConfig::slideInTime() != 0 ? std::chrono::milliseconds(SlidingPopupsConfig::slideInTime()) : 200ms))); + m_slideOutDuration = std::chrono::milliseconds( + static_cast(animationTime(SlidingPopupsConfig::slideOutTime() != 0 ? std::chrono::milliseconds(SlidingPopupsConfig::slideOutTime()) : 200ms))); + + for (auto &[window, animation] : m_animations) { + animation.timeLine.setDuration(animation.kind == AnimationKind::In ? m_slideInDuration : m_slideOutDuration); + } + + auto dataIt = m_animationsData.begin(); + while (dataIt != m_animationsData.end()) { + (*dataIt).slideInDuration = m_slideInDuration; + (*dataIt).slideOutDuration = m_slideOutDuration; + ++dataIt; + } +} + +void SlidingPopupsEffect::prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) +{ + auto animationIt = m_animations.find(w); + if (animationIt == m_animations.end()) { + effects->prePaintWindow(view, w, data, presentTime); + return; + } + + animationIt->second.timeLine.advance(presentTime); + data.setTransformed(); + + effects->prePaintWindow(view, w, data, presentTime); +} + +void SlidingPopupsEffect::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceGeometry, WindowPaintData &data) +{ + auto animationIt = m_animations.find(w); + if (animationIt == m_animations.end()) { + effects->paintWindow(renderTarget, viewport, w, mask, deviceGeometry, data); + return; + } + + const AnimationData &animData = m_animationsData[w]; + const qreal slideLength = (animData.slideLength > 0) ? animData.slideLength : m_slideLength; + + const QRectF geo = w->expandedGeometry(); + const qreal t = animationIt->second.timeLine.value(); + + Region effectiveRegion = deviceGeometry; + switch (animData.location) { + case Location::Left: + if (slideLength < geo.width()) { + data.multiplyOpacity(t); + } + data.translate(-interpolate(std::min(geo.width(), slideLength), 0.0, t)); + break; + case Location::Top: + if (slideLength < geo.height()) { + data.multiplyOpacity(t); + } + data.translate(0.0, -interpolate(std::min(geo.height(), slideLength), 0.0, t)); + break; + case Location::Right: + if (slideLength < geo.width()) { + data.multiplyOpacity(t); + } + data.translate(interpolate(std::min(geo.width(), slideLength), 0.0, t)); + break; + case Location::Bottom: + default: + if (slideLength < geo.height()) { + data.multiplyOpacity(t); + } + data.translate(0.0, interpolate(std::min(geo.height(), slideLength), 0.0, t)); + } + + effectiveRegion &= viewport.mapToDeviceCoordinatesAligned(damagedLogicalArea(w, animData)); + + effects->paintWindow(renderTarget, viewport, w, mask, effectiveRegion, data); +} + +void SlidingPopupsEffect::postPaintScreen() +{ + for (auto animationIt = m_animations.begin(); animationIt != m_animations.end();) { + EffectWindow *w = animationIt->first; + const AnimationData &animData = m_animationsData[w]; + effects->addRepaint(damagedLogicalArea(w, animData)); + + if (animationIt->second.timeLine.done()) { + if (!w->isDeleted()) { + w->setData(WindowForceBackgroundContrastRole, QVariant()); + w->setData(WindowForceBlurRole, QVariant()); + } + animationIt = m_animations.erase(animationIt); + } else { + ++animationIt; + } + } + + effects->postPaintScreen(); +} + +void SlidingPopupsEffect::setupSlideData(EffectWindow *w) +{ + connect(w, &EffectWindow::windowHiddenChanged, this, &SlidingPopupsEffect::slotWindowHiddenChanged); + + if (effects->inputPanel() == w) { + setupInputPanelSlide(); + } else if (auto surf = w->surface()) { + slotWaylandSlideOnShowChanged(w); + connect(surf, &SurfaceInterface::slideOnShowHideChanged, this, [this, surf] { + slotWaylandSlideOnShowChanged(effects->findWindow(surf)); + }); + } + + if (auto internal = w->internalWindow()) { + internal->installEventFilter(this); + setupInternalWindowSlide(w); + } +} + +void SlidingPopupsEffect::slotWindowAdded(EffectWindow *w) +{ + setupSlideData(w); + if (!w->isHidden()) { + slideIn(w); + } +} + +void SlidingPopupsEffect::slotWindowClosed(EffectWindow *w) +{ + if (!w->isHidden()) { + slideOut(w); + } +} + +void SlidingPopupsEffect::slotWindowDeleted(EffectWindow *w) +{ + m_animationsData.remove(w); +} + +void SlidingPopupsEffect::slotWindowHiddenChanged(EffectWindow *w) +{ + if (w->isHidden()) { + slideOut(w); + } else { + slideIn(w); + } +} + +void SlidingPopupsEffect::setupAnimData(EffectWindow *w) +{ + const QRectF screenRect = effects->clientArea(FullScreenArea, w->screen(), effects->currentDesktop()); + const QRectF windowGeo = w->frameGeometry(); + AnimationData &animData = m_animationsData[w]; + + if (animData.offset == -1) { + switch (animData.location) { + case Location::Left: + animData.offset = std::max(windowGeo.left() - screenRect.left(), 0); + break; + case Location::Top: + animData.offset = std::max(windowGeo.top() - screenRect.top(), 0); + break; + case Location::Right: + animData.offset = std::max(screenRect.right() - windowGeo.right(), 0); + break; + case Location::Bottom: + default: + animData.offset = std::max(screenRect.bottom() - windowGeo.bottom(), 0); + break; + } + } + // sanitize + switch (animData.location) { + case Location::Left: + animData.offset = std::max(0, animData.offset); + break; + case Location::Top: + animData.offset = std::max(0, animData.offset); + break; + case Location::Right: + animData.offset = std::max(0, animData.offset); + break; + case Location::Bottom: + default: + animData.offset = std::max(0, animData.offset); + break; + } + + animData.slideInDuration = (animData.slideInDuration.count() != 0) + ? animData.slideInDuration + : m_slideInDuration; + + animData.slideOutDuration = (animData.slideOutDuration.count() != 0) + ? animData.slideOutDuration + : m_slideOutDuration; + + // Grab the window, so other windowClosed effects will ignore it + w->setData(WindowClosedGrabRole, QVariant::fromValue(static_cast(this))); +} + +void SlidingPopupsEffect::slotWaylandSlideOnShowChanged(EffectWindow *w) +{ + if (!w) { + return; + } + + SurfaceInterface *surf = w->surface(); + if (!surf) { + return; + } + + if (surf->slideOnShowHide()) { + AnimationData &animData = m_animationsData[w]; + + animData.offset = surf->slideOnShowHide()->offset(); + + switch (surf->slideOnShowHide()->location()) { + case SlideInterface::Location::Top: + animData.location = Location::Top; + break; + case SlideInterface::Location::Left: + animData.location = Location::Left; + break; + case SlideInterface::Location::Right: + animData.location = Location::Right; + break; + case SlideInterface::Location::Bottom: + default: + animData.location = Location::Bottom; + break; + } + animData.slideLength = 0; + animData.slideInDuration = m_slideInDuration; + animData.slideOutDuration = m_slideOutDuration; + + setupAnimData(w); + } +} + +void SlidingPopupsEffect::setupInternalWindowSlide(EffectWindow *w) +{ + if (!w) { + return; + } + auto internal = w->internalWindow(); + if (!internal) { + return; + } + const QVariant slideProperty = internal->property("kwin_slide"); + if (!slideProperty.isValid()) { + return; + } + Location location; + switch (slideProperty.value()) { + case KWindowEffects::BottomEdge: + location = Location::Bottom; + break; + case KWindowEffects::TopEdge: + location = Location::Top; + break; + case KWindowEffects::RightEdge: + location = Location::Right; + break; + case KWindowEffects::LeftEdge: + location = Location::Left; + break; + default: + return; + } + AnimationData &animData = m_animationsData[w]; + animData.location = location; + bool intOk = false; + animData.offset = internal->property("kwin_slide_offset").toInt(&intOk); + if (!intOk) { + animData.offset = -1; + } + animData.slideLength = 0; + animData.slideInDuration = m_slideInDuration; + animData.slideOutDuration = m_slideOutDuration; + + setupAnimData(w); +} + +void SlidingPopupsEffect::setupInputPanelSlide() +{ + auto w = effects->inputPanel(); + + if (!w || effects->isInputPanelOverlay()) { + return; + } + + AnimationData &animData = m_animationsData[w]; + animData.location = Location::Bottom; + animData.offset = 0; + animData.slideLength = 0; + animData.slideInDuration = m_slideInDuration; + animData.slideOutDuration = m_slideOutDuration; + + setupAnimData(w); +} + +QRectF SlidingPopupsEffect::damagedLogicalArea(EffectWindow *w, const AnimationData animData) +{ + const QRectF screenRect = effects->clientArea(FullScreenArea, w->screen(), effects->currentDesktop()); + qreal splitPoint = 0; + const QRectF geo = w->expandedGeometry(); + + switch (animData.location) { + case Location::Left: + splitPoint = geo.width() - (geo.x() + geo.width() - screenRect.x() - animData.offset); + return QRectF(geo.x() + splitPoint, geo.y(), geo.width() - splitPoint, geo.height()); + case Location::Top: + splitPoint = geo.height() - (geo.y() + geo.height() - screenRect.y() - animData.offset); + return QRectF(geo.x(), geo.y() + splitPoint, geo.width(), geo.height() - splitPoint); + case Location::Right: + splitPoint = screenRect.x() + screenRect.width() - geo.x() - animData.offset; + return QRectF(geo.x(), geo.y(), splitPoint, geo.height()); + break; + case Location::Bottom: + default: + splitPoint = screenRect.y() + screenRect.height() - geo.y() - animData.offset; + return QRectF(geo.x(), geo.y(), geo.width(), splitPoint); + break; + } +} + +bool SlidingPopupsEffect::eventFilter(QObject *watched, QEvent *event) +{ + auto internal = qobject_cast(watched); + if (internal && event->type() == QEvent::DynamicPropertyChange) { + QDynamicPropertyChangeEvent *pe = static_cast(event); + if (pe->propertyName() == "kwin_slide" || pe->propertyName() == "kwin_slide_offset") { + if (auto w = effects->findWindow(internal)) { + setupInternalWindowSlide(w); + } + } + } + return false; +} + +void SlidingPopupsEffect::slideIn(EffectWindow *w) +{ + if (effects->activeFullScreenEffect()) { + return; + } + + if (!w->isVisible()) { + return; + } + + auto dataIt = m_animationsData.constFind(w); + if (dataIt == m_animationsData.constEnd()) { + return; + } + + Animation &animation = m_animations[w]; + animation.kind = AnimationKind::In; + animation.timeLine.setDirection(TimeLine::Forward); + animation.timeLine.setDuration((*dataIt).slideInDuration); + animation.timeLine.setEasingCurve(QEasingCurve::OutCubic); + animation.windowEffect = ItemEffect(w->windowItem()); + + // If the opposite animation (Out) was active and it had shorter duration, + // at this point, the timeline can end up in the "done" state. Thus, we have + // to reset it. + if (animation.timeLine.done()) { + animation.timeLine.reset(); + } + + w->setData(WindowAddedGrabRole, QVariant::fromValue(static_cast(this))); + w->setData(WindowForceBackgroundContrastRole, QVariant(true)); + w->setData(WindowForceBlurRole, QVariant(true)); + + w->addRepaintFull(); +} + +void SlidingPopupsEffect::slideOut(EffectWindow *w) +{ + if (effects->activeFullScreenEffect()) { + return; + } + + if (!w->isVisible()) { + return; + } + + auto dataIt = m_animationsData.constFind(w); + if (dataIt == m_animationsData.constEnd()) { + return; + } + + Animation &animation = m_animations[w]; + animation.deletedRef = EffectWindowDeletedRef(w); + animation.visibleRef = EffectWindowVisibleRef(w, EffectWindow::PAINT_DISABLED); + animation.kind = AnimationKind::Out; + animation.timeLine.setDirection(TimeLine::Backward); + animation.timeLine.setDuration((*dataIt).slideOutDuration); + // this is effectively InCubic because the direction is reversed + animation.timeLine.setEasingCurve(QEasingCurve::OutCubic); + animation.windowEffect = ItemEffect(w->windowItem()); + + // If the opposite animation (In) was active and it had shorter duration, + // at this point, the timeline can end up in the "done" state. Thus, we have + // to reset it. + if (animation.timeLine.done()) { + animation.timeLine.reset(); + } + + w->setData(WindowClosedGrabRole, QVariant::fromValue(static_cast(this))); + w->setData(WindowForceBackgroundContrastRole, QVariant(true)); + w->setData(WindowForceBlurRole, QVariant(true)); + + w->addRepaintFull(); +} + +void SlidingPopupsEffect::stopAnimations() +{ + for (const auto &[window, animation] : m_animations) { + if (!window->isDeleted()) { + window->setData(WindowForceBackgroundContrastRole, QVariant()); + window->setData(WindowForceBlurRole, QVariant()); + } + } + + m_animations.clear(); +} + +bool SlidingPopupsEffect::isActive() const +{ + return !m_animations.empty(); +} + +bool SlidingPopupsEffect::blocksDirectScanout() const +{ + return false; +} + +} // namespace + +#include "moc_slidingpopups.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/slidingpopups/slidingpopups.h b/local/recipes/kde/kwin/source/src/plugins/slidingpopups/slidingpopups.h new file mode 100644 index 0000000000..e3325c7c46 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slidingpopups/slidingpopups.h @@ -0,0 +1,121 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2009 Marco Martin notmart @gmail.com + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/effect.h" +#include "effect/effectwindow.h" +#include "effect/timeline.h" +#include "scene/item.h" + +namespace KWin +{ + +class SlideManagerInterface; + +class SlidingPopupsEffect : public Effect +{ + Q_OBJECT + Q_PROPERTY(int slideInDuration READ slideInDuration) + Q_PROPERTY(int slideOutDuration READ slideOutDuration) + +public: + SlidingPopupsEffect(); + ~SlidingPopupsEffect() override; + + void prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) override; + void paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceGeometry, WindowPaintData &data) override; + void reconfigure(ReconfigureFlags flags) override; + bool isActive() const override; + void postPaintScreen() override; + + int requestedEffectChainPosition() const override + { + return 40; + } + + static bool supported(); + + int slideInDuration() const; + int slideOutDuration() const; + + bool eventFilter(QObject *watched, QEvent *event) override; + bool blocksDirectScanout() const override; + +private Q_SLOTS: + void slotWindowAdded(EffectWindow *w); + void slotWindowClosed(EffectWindow *w); + void slotWindowDeleted(EffectWindow *w); + void slotWaylandSlideOnShowChanged(EffectWindow *w); + void slotWindowHiddenChanged(EffectWindow *w); + + void slideIn(EffectWindow *w); + void slideOut(EffectWindow *w); + void stopAnimations(); + +private: + void setupAnimData(EffectWindow *w); + void setupInternalWindowSlide(EffectWindow *w); + void setupSlideData(EffectWindow *w); + void setupInputPanelSlide(); + + static SlideManagerInterface *s_slideManager; + static QTimer *s_slideManagerRemoveTimer; + + int m_slideLength; + std::chrono::milliseconds m_slideInDuration; + std::chrono::milliseconds m_slideOutDuration; + + enum class AnimationKind { + In, + Out + }; + + struct Animation + { + EffectWindowDeletedRef deletedRef; + EffectWindowVisibleRef visibleRef; + AnimationKind kind; + TimeLine timeLine; + ItemEffect windowEffect; + }; + std::unordered_map m_animations; + + enum class Location { + Left, + Top, + Right, + Bottom + }; + + struct AnimationData + { + int offset; + Location location; + std::chrono::milliseconds slideInDuration; + std::chrono::milliseconds slideOutDuration; + int slideLength; + }; + QHash m_animationsData; + + QRectF damagedLogicalArea(EffectWindow *w, const AnimationData animData); +}; + +inline int SlidingPopupsEffect::slideInDuration() const +{ + return m_slideInDuration.count(); +} + +inline int SlidingPopupsEffect::slideOutDuration() const +{ + return m_slideOutDuration.count(); +} + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/slidingpopups/slidingpopups.kcfg b/local/recipes/kde/kwin/source/src/plugins/slidingpopups/slidingpopups.kcfg new file mode 100644 index 0000000000..ae9c6e436f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slidingpopups/slidingpopups.kcfg @@ -0,0 +1,17 @@ + + + + + + + 0 + + + 0 + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/slidingpopups/slidingpopupsconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/slidingpopups/slidingpopupsconfig.kcfgc new file mode 100644 index 0000000000..6a8c915f51 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/slidingpopups/slidingpopupsconfig.kcfgc @@ -0,0 +1,5 @@ +File=slidingpopups.kcfg +ClassName=SlidingPopupsConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/local/recipes/kde/kwin/source/src/plugins/squash/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/squash/CMakeLists.txt new file mode 100644 index 0000000000..7368422aad --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/squash/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_scripted_effect(squash package) diff --git a/local/recipes/kde/kwin/source/src/plugins/squash/package/contents/code/main.js b/local/recipes/kde/kwin/source/src/plugins/squash/package/contents/code/main.js new file mode 100644 index 0000000000..32b4ac1581 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/squash/package/contents/code/main.js @@ -0,0 +1,181 @@ +/* + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +"use strict"; + +var squashEffect = { + duration: animationTime(250), + loadConfig: function () { + squashEffect.duration = animationTime(250); + }, + slotWindowMinimized: function (window) { + if (effects.hasActiveFullScreenEffect) { + return; + } + + window.setData(Effect.WindowForceBlurRole, true); + + // If the window doesn't have an icon in the task manager, + // don't animate it. + var iconRect = window.iconGeometry; + if (iconRect.width == 0 || iconRect.height == 0) { + return; + } + + if (window.unminimizeAnimation) { + if (redirect(window.unminimizeAnimation, Effect.Backward)) { + return; + } + cancel(window.unminimizeAnimation); + delete window.unminimizeAnimation; + } + + if (window.minimizeAnimation) { + if (redirect(window.minimizeAnimation, Effect.Forward)) { + return; + } + cancel(window.minimizeAnimation); + } + + var windowRect = window.geometry; + + window.setData(Effect.WindowForceBlurRole, true); + + window.minimizeAnimation = animate({ + window: window, + curve: QEasingCurve.InCubic, + duration: squashEffect.duration, + keepAlive: false, + animations: [ + { + type: Effect.Size, + from: { + value1: windowRect.width, + value2: windowRect.height + }, + to: { + value1: iconRect.width, + value2: iconRect.height + } + }, + { + type: Effect.Translation, + from: { + value1: 0.0, + value2: 0.0 + }, + to: { + value1: iconRect.x - windowRect.x - + (windowRect.width - iconRect.width) / 2, + value2: iconRect.y - windowRect.y - + (windowRect.height - iconRect.height) / 2, + } + }, + { + type: Effect.Opacity, + from: 1.0, + to: 0.0 + } + ] + }); + }, + slotWindowUnminimized: function (window) { + if (effects.hasActiveFullScreenEffect) { + return; + } + + window.setData(Effect.WindowForceBlurRole, true); + + // If the window doesn't have an icon in the task manager, + // don't animate it. + var iconRect = window.iconGeometry; + if (iconRect.width == 0 || iconRect.height == 0) { + return; + } + + if (window.minimizeAnimation) { + if (redirect(window.minimizeAnimation, Effect.Backward)) { + return; + } + cancel(window.minimizeAnimation); + delete window.minimizeAnimation; + } + + if (window.unminimizeAnimation) { + if (redirect(window.unminimizeAnimation, Effect.Forward)) { + return; + } + cancel(window.unminimizeAnimation); + } + + var windowRect = window.geometry; + + window.setData(Effect.WindowForceBlurRole, true); + + window.unminimizeAnimation = animate({ + window: window, + curve: QEasingCurve.OutCubic, + duration: squashEffect.duration, + keepAlive: false, + animations: [ + { + type: Effect.Size, + from: { + value1: iconRect.width, + value2: iconRect.height + }, + to: { + value1: windowRect.width, + value2: windowRect.height + } + }, + { + type: Effect.Translation, + from: { + value1: iconRect.x - windowRect.x - + (windowRect.width - iconRect.width) / 2, + value2: iconRect.y - windowRect.y - + (windowRect.height - iconRect.height) / 2, + }, + to: { + value1: 0.0, + value2: 0.0 + } + }, + { + type: Effect.Opacity, + from: 0.0, + to: 1.0 + } + ] + }); + }, + slotWindowAdded: function (window) { + window.minimizedChanged.connect(() => { + if (window.minimized) { + squashEffect.slotWindowMinimized(window); + } else { + squashEffect.slotWindowUnminimized(window); + } + }); + }, + restoreForceBlurState: function(window) { + window.setData(Effect.WindowForceBlurRole, null); + }, + init: function () { + effect.configChanged.connect(squashEffect.loadConfig); + effect.animationEnded.connect(this.restoreForceBlurState.bind(this)); + + effects.windowAdded.connect(squashEffect.slotWindowAdded); + for (const window of effects.stackingOrder) { + squashEffect.slotWindowAdded(window); + } + } +}; + +squashEffect.init(); diff --git a/local/recipes/kde/kwin/source/src/plugins/squash/package/metadata.json b/local/recipes/kde/kwin/source/src/plugins/squash/package/metadata.json new file mode 100644 index 0000000000..5dd1de12bf --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/squash/package/metadata.json @@ -0,0 +1,155 @@ +{ + "KPackageStructure": "KWin/Effect", + "KPlugin": { + "Authors": [ + { + "Email": "rivolaks@hot.ee, vlad.zahorodnii@kde.org", + "Name": "Rivo Laks, Vlad Zahorodnii", + "Name[ar]": "ريفو لاكس، فلاد زاهورودني", + "Name[be]": "Rivo Laks, Vlad Zahorodnii", + "Name[bg]": "Rivo Laks, Vlad Zahorodnii", + "Name[ca@valencia]": "Rivo Laks, Vlad Zahorodnii", + "Name[ca]": "Rivo Laks, Vlad Zahorodnii", + "Name[cs]": "Rivo Laks, Vlad Zahorodnii", + "Name[da]": "Rivo Laks, Vlad Zahorodnii", + "Name[de]": "Rivo Laks, Vlad Zahorodnii", + "Name[en_GB]": "Rivo Laks, Vlad Zahorodnii", + "Name[eo]": "Rivo Laks, Vlad Zahorodnii", + "Name[es]": "Rivo Laks, Vlad Zahorodnii", + "Name[et]": "Rivo Laks, Vlad Zahorodnii", + "Name[eu]": "Rivo Laks, Vlad Zahorodnii", + "Name[fi]": "Rivo Laks, Vlad Zahorodnii", + "Name[fr]": "Rivo Laks, Vlad Zahorodnii", + "Name[ga]": "Rivo Laks, Vlad Zahorodnii", + "Name[gl]": "Rivo Laks e Vlad Zahorodnii.", + "Name[he]": "ריבו לאקס, ולאד זוהורוני", + "Name[hu]": "Rivo Laks, Vlad Zahorodnii", + "Name[ia]": "Rivo Laks, Vlad Zahorodnii", + "Name[id]": "Rivo Laks, Vlad Zahorodnii", + "Name[is]": "Rivo Laks, Vlad Zahorodnii", + "Name[it]": "Rivo Laks, Vlad Zahorodnii", + "Name[ja]": "Rivo Laks, Vlad Zahorodnii", + "Name[ka]": "Vlad Zahorodnii", + "Name[ko]": "Rivo Laks, Vlad Zahorodnii", + "Name[lt]": "Rivo Laks, Vlad Zahorodnii", + "Name[lv]": "Rivo Laks, Vlad Zahorodnii", + "Name[nb]": "Rivo Laks, Vlad Zahorodnii", + "Name[nl]": "Rivo Laks, Vlad Zahorodnii", + "Name[nn]": "Rivo Laks, Vlad Zahorodnii", + "Name[pl]": "Rivo Laks, Vlad Zahorodnii", + "Name[pt]": "Rivo Laks, Vlad Zahorodnii", + "Name[pt_BR]": "Rivo Laks, Vlad Zahorodnii", + "Name[ro]": "Rivo Laks, Vlad Zahorodnii", + "Name[ru]": "Rivo Laks, Влад Загородний", + "Name[sa]": "रिवो लक्स, व्लाद ज़ाहोरोडनी", + "Name[sk]": "Rivo Laks, Vlad Zahorodnii", + "Name[sl]": "Rivo Laks, Vlad Zahorodnii", + "Name[sv]": "Rivo Laks, Vlad Zahorodnii", + "Name[ta]": "ரிவோ லாக்சு, விலாட் ஜாஹொரிடுனி", + "Name[tr]": "Rivo Laks, Vlad Zahorodnii", + "Name[uk]": "Rivo Laks, Влад Завгородній", + "Name[vi]": "Rivo Laks, Vlad Zahorodnii", + "Name[zh_CN]": "Rivo Laks, Vlad Zahorodnii", + "Name[zh_TW]": "Rivo Laks, Vlad Zahorodnii" + } + ], + "Category": "Appearance", + "Description": "Squash windows when they are minimized", + "Description[ar]": "تنكمش النوافذ عند تصغيرها", + "Description[be]": "Сціскаць вокны, калі яны згорнутыя", + "Description[bg]": "Свиване на прозорците при минимизиране", + "Description[ca@valencia]": "Amuntega les finestres quan estan minimitzades", + "Description[ca]": "Amuntega les finestres quan estan minimitzades", + "Description[da]": "Knus vinduer når de minimeres", + "Description[de]": "Quetscht Fenster beim Minimieren zusammen", + "Description[en_GB]": "Squash windows when they are minimised", + "Description[eo]": "Squash fenestroj kiam ili estas minimumigitaj", + "Description[es]": "Aplastar las ventanas cuando se minimizan", + "Description[et]": "Minimeeritud akende taas üleshüpitamine", + "Description[eu]": "Zanpatu leihoak haiek ikonotzen direnean", + "Description[fi]": "Litistä pienennettävät ikkunat", + "Description[fr]": "Déformer les fenêtres lors de leur minimisation", + "Description[gl]": "Xuntar as xanelas cando estean minimizadas.", + "Description[he]": "למעוך את החלונות כשהם ממוזערים", + "Description[hu]": "Összevonja az ablakokat, amikor minimalizálódnak", + "Description[ia]": "Deforma fenestras durante que illes es minimisate", + "Description[id]": "Menyesakkan jendela ketika ia diminimalkan", + "Description[is]": "Kremja glugga þegar þeir eru faldir", + "Description[it]": "Schiaccia le finestre quando vengono minimizzate", + "Description[ja]": "最小化されたときウィンドウが縮小します", + "Description[ka]": "ფანჯრების შეჭყლეტა ჩაკეცვისა დროს", + "Description[ko]": "창을 최소화할 때 압축시킵니다", + "Description[lt]": "Sutraiškyti langus, kai jie suskleidžiami", + "Description[lv]": "Saspiest logus, kad tos minimizē", + "Description[nb]": "Skvis vinduer når de minimeres", + "Description[nl]": "Krimp vensters wanneer ze geminimaliseerd zijn", + "Description[nn]": "Skvis vindauge når dei vert minimerte", + "Description[pl]": "Ściąga okna przy ich minimalizacji", + "Description[pt]": "Deformar as janelas quando são minimizadas", + "Description[pt_BR]": "Esmaga as janelas quando elas estiverem minimizadas", + "Description[ro]": "Strivește ferestrele când sunt minimizate", + "Description[ru]": "Сжатие окна при сворачивании", + "Description[sa]": "यदा न्यूनीकृताः भवन्ति तदा खिडकयः स्क्वैश कुर्वन्तु", + "Description[sk]": "Stlačenie okien, keď sú minimalizované", + "Description[sl]": "Skrči okna, ko so pomanjšana", + "Description[sv]": "Kläm fönster när de minimeras", + "Description[ta]": "சாளரங்களை ஒதுக்கும்போது அவற்றை நசுக்குவது போல் அசைவூட்டும்", + "Description[tr]": "Pencereler simge durumuna küçültüldüğünde onları tıkıştır", + "Description[uk]": "Складує вікна, якщо їх мінімізовано", + "Description[vi]": "Ép nhỏ cửa sổ khi thu nhỏ", + "Description[zh_CN]": "窗口最小化时绘制收缩过渡动画", + "Description[zh_TW]": "最小化視窗時擠壓它們", + "EnabledByDefault": true, + "Icon": "preferences-system-windows-effect-squash", + "Id": "squash", + "License": "GPL", + "Name": "Squash", + "Name[ar]": "الانكماش", + "Name[be]": "Сцісканне", + "Name[bg]": "Свиване", + "Name[ca@valencia]": "Amuntega", + "Name[ca]": "Amuntega", + "Name[da]": "Squash", + "Name[de]": "Quetschen", + "Name[en_GB]": "Squash", + "Name[eo]": "Dispremi", + "Name[es]": "Aplastar", + "Name[et]": "Üleshüpe", + "Name[eu]": "Zanpatu", + "Name[fi]": "Litistys", + "Name[fr]": "Compresser", + "Name[gl]": "Xuntar", + "Name[he]": "מעיכה", + "Name[hu]": "Összevonás", + "Name[ia]": "Squash", + "Name[id]": "Sesakkan", + "Name[is]": "Kremja", + "Name[it]": "Schiaccia", + "Name[ja]": "縮小", + "Name[ka]": "დაჭმუჭნვა", + "Name[ko]": "압축", + "Name[lt]": "Sutraiškymas", + "Name[lv]": "Saspiest", + "Name[nb]": "Skvis", + "Name[nl]": "Krimpen", + "Name[nn]": "Skvis", + "Name[pl]": "Ściąganie", + "Name[pt]": "Esmagar", + "Name[pt_BR]": "Achatar", + "Name[ro]": "Strivire", + "Name[ru]": "Сжатие", + "Name[sa]": "स्क्वैशः", + "Name[sk]": "Stlačiť", + "Name[sl]": "Strni", + "Name[sv]": "Kläm", + "Name[ta]": "நசுக்கு", + "Name[tr]": "Tıkıştır", + "Name[uk]": "Складування", + "Name[vi]": "Ép nhỏ", + "Name[zh_CN]": "最小化过渡动画 (收缩)", + "Name[zh_TW]": "壓縮" + }, + "X-KDE-Ordering": 60, + "X-KWin-Exclusive-Category": "minimize", + "X-Plasma-API": "javascript" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/startupfeedback/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/startupfeedback/CMakeLists.txt new file mode 100644 index 0000000000..78667d40e7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/startupfeedback/CMakeLists.txt @@ -0,0 +1,17 @@ +####################################### +# Effect + +# Source files +set(startupfeedback_SOURCES + main.cpp + startupfeedback.cpp + startupfeedback.qrc +) + +kwin_add_builtin_effect(startupfeedback ${startupfeedback_SOURCES}) +target_link_libraries(startupfeedback PRIVATE + kwin + + Qt::DBus + Qt::Widgets +) diff --git a/local/recipes/kde/kwin/source/src/plugins/startupfeedback/main.cpp b/local/recipes/kde/kwin/source/src/plugins/startupfeedback/main.cpp new file mode 100644 index 0000000000..169d9eeed4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/startupfeedback/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "startupfeedback.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(StartupFeedbackEffect, + "metadata.json.stripped", + return StartupFeedbackEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/startupfeedback/metadata.json b/local/recipes/kde/kwin/source/src/plugins/startupfeedback/metadata.json new file mode 100644 index 0000000000..19acbcc800 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/startupfeedback/metadata.json @@ -0,0 +1,104 @@ +{ + "KPlugin": { + "Category": "Appearance", + "Description": "Helper effect for startup feedback", + "Description[ar]": "تأثير مساعد لمؤشر بدء التشغيل", + "Description[az]": "İstifadəçi rəyinin başlanğıcı üçün yardımçı effekt", + "Description[be]": "Дапаможны эфект для водгукаў пры запуску", + "Description[bg]": "Помощен ефект за обратна връзка при стартиране", + "Description[ca@valencia]": "Efecte auxiliar per a la retroacció en iniciar", + "Description[ca]": "Efecte auxiliar per a la retroacció en iniciar", + "Description[cs]": "Pomocný efekt pro Odezvu při spouštění", + "Description[da]": "Hjælper-effekt for opstartsfeedback", + "Description[de]": "Hilfseffekt für Programmstartanzeige", + "Description[en_GB]": "Helper effect for startup feedback", + "Description[eo]": "Helpema efiko por komencaj sugestoj", + "Description[es]": "Efecto auxiliar para la notificación de lanzamiento", + "Description[et]": "Käivitamise tagasiside abiefekt", + "Description[eu]": "Abiatze berrelikaduraren efektu laguntzailea", + "Description[fi]": "Käynnistyspalautteen aputehoste", + "Description[fr]": "Effet d'assistance du témoin de démarrage ", + "Description[gl]": "Efecto auxiliar para notificar o lanzamento de programas.", + "Description[he]": "אפקט סיוע למשוב התחלה", + "Description[hu]": "Segédeffektus alkalmazásindításhoz", + "Description[ia]": "Effecto de adjutante pro retorno ab lanceamento", + "Description[id]": "Efek penunjang untuk feedback pemulaian", + "Description[is]": "Hjálparáhrif fyrir endurgjöf við ræsingu", + "Description[it]": "Effetto di assistenza per il segnale di avvio", + "Description[ja]": "起動フィードバックのためのヘルパー効果", + "Description[ka]": "გაშვების უკუკავშირის დამხმარე ეფექტი", + "Description[ko]": "실행 피드백 효과", + "Description[lt]": "Pagelbiklio efektas skirtas paleidimo grįžtamajam ryšiui", + "Description[lv]": "Palaišanas ziņošanas palīgefekts", + "Description[nb]": "Hjelpeeffekt for oppstartsvisning", + "Description[nl]": "Effect van hulp bij terugkoppeling van opstarten", + "Description[nn]": "Hjelpareffekt for oppstartsvising", + "Description[pl]": "Efekt pomocniczy dla wrażeń przy starcie", + "Description[pt]": "Efeito auxiliar da reacção ao arranque", + "Description[pt_BR]": "Assistente de efeito sugestões e críticas da inicialização", + "Description[ro]": "Efect ajutător pentru reacție la lansare", + "Description[ru]": "Вспомогательный эффект для отклика запуска приложений", + "Description[sa]": "स्टार्टअप प्रतिक्रियायाः कृते सहायकप्रभावः", + "Description[sk]": "Pomocný efekt pre odozvu pri spustení", + "Description[sl]": "Učinek pomočnika pri odzivu zagona", + "Description[sv]": "Hjälpeffekt för gensvar vid programstart", + "Description[ta]": "தொடக்க பின்னூட்டத்தை காட்ட உதவும் அசைவூட்டம்", + "Description[tr]": "Başlangıç geri bildirimi için yardımcı efekti", + "Description[uk]": "Допоміжний ефект перегляду повідомлень, що надсилаються програмою під час запуску", + "Description[vi]": "Hiệu ứng trợ giúp cho phản hồi khởi động", + "Description[zh_CN]": "程序启动动效反馈辅助效果", + "Description[zh_TW]": "啟動回饋輔助效果", + "EnabledByDefault": true, + "License": "GPL", + "Name": "Startup Feedback", + "Name[ar]": "مؤشر البدء", + "Name[az]": "İstifadəçi rəyinin başlanğıcı", + "Name[be]": "Водгук пры запуску", + "Name[bg]": "Обратна връзка при стартиране", + "Name[ca@valencia]": "Retroacció en iniciar", + "Name[ca]": "Retroacció en iniciar", + "Name[cs]": "Odezva při spouštění", + "Name[da]": "Feedback ved opstart", + "Name[de]": "Programmstartanzeige", + "Name[en_GB]": "Startup Feedback", + "Name[eo]": "Komenca Reago", + "Name[es]": "Notificación de lanzamiento", + "Name[et]": "Käivitamise tagasiside", + "Name[eu]": "Abiatze berrelikadura", + "Name[fi]": "Käynnistyspalaute", + "Name[fr]": "Témoin de démarrage", + "Name[gl]": "Notificación de lanzamento", + "Name[he]": "משוב פתיחה", + "Name[hu]": "Alkalmazásindítási effektus", + "Name[ia]": "Retorno ab lanceamento", + "Name[id]": "Feedback Pemulaian", + "Name[is]": "Endurgjöf í ræsingu", + "Name[it]": "Segnale di avvio", + "Name[ja]": "起動フィードバック", + "Name[ka]": "გაშვების უკუკავშირი", + "Name[ko]": "실행 피드백", + "Name[lt]": "Paleidimo grįžtamasis ryšys", + "Name[lv]": "Palaišanas ziņošana", + "Name[nb]": "Oppstartsmelding", + "Name[nl]": "Terugkoppeling van opstarten", + "Name[nn]": "Oppstartsmelding", + "Name[pl]": "Wrażenia przy uruchamianiu", + "Name[pt]": "Reacção ao Arranque", + "Name[pt_BR]": "Críticas e sugestões da inicialização", + "Name[ro]": "Reacție la lansare", + "Name[ru]": "Отклик запуска приложений", + "Name[sa]": "प्रारम्भ प्रतिक्रिया", + "Name[sk]": "Odozva pri spustení", + "Name[sl]": "Odziv zagona", + "Name[sv]": "Gensvar vid programstart", + "Name[ta]": "தொடக்க பின்னூட்டம்", + "Name[tr]": "Başlangıç Geri Bildirimi", + "Name[uk]": "Повідомлення під час запуску", + "Name[vi]": "Phản hồi khởi động", + "Name[zh_CN]": "程序启动动效", + "Name[zh_TW]": "啟動回饋" + }, + "org.kde.kwin.effect": { + "internal": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/startupfeedback/shaders/blinking-startup.frag b/local/recipes/kde/kwin/source/src/plugins/startupfeedback/shaders/blinking-startup.frag new file mode 100644 index 0000000000..c022a9d3d7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/startupfeedback/shaders/blinking-startup.frag @@ -0,0 +1,16 @@ +#include "colormanagement.glsl" + +uniform sampler2D sampler; +uniform vec4 geometryColor; + +varying vec2 texcoord0; + +void main() +{ + vec4 tex = texture2D(sampler, texcoord0); + if (tex.a != 1.0) { + tex = geometryColor; + } + tex = sourceEncodingToNitsInDestinationColorspace(tex); + gl_FragColor = nitsToDestinationEncoding(tex); +} diff --git a/local/recipes/kde/kwin/source/src/plugins/startupfeedback/shaders/blinking-startup_core.frag b/local/recipes/kde/kwin/source/src/plugins/startupfeedback/shaders/blinking-startup_core.frag new file mode 100644 index 0000000000..7201e15b65 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/startupfeedback/shaders/blinking-startup_core.frag @@ -0,0 +1,20 @@ +#version 140 + +#include "colormanagement.glsl" + +uniform sampler2D sampler; +uniform vec4 geometryColor; + +in vec2 texcoord0; + +out vec4 fragColor; + +void main() +{ + vec4 tex = texture(sampler, texcoord0); + if (tex.a != 1.0) { + tex = geometryColor; + } + tex = sourceEncodingToNitsInDestinationColorspace(tex); + fragColor = nitsToDestinationEncoding(tex); +} diff --git a/local/recipes/kde/kwin/source/src/plugins/startupfeedback/startupfeedback.cpp b/local/recipes/kde/kwin/source/src/plugins/startupfeedback/startupfeedback.cpp new file mode 100644 index 0000000000..fd9b57b6a0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/startupfeedback/startupfeedback.cpp @@ -0,0 +1,482 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Martin Gräßlin + SPDX-FileCopyrightText: 2020 David Redondo + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "startupfeedback.h" +// Qt +#include +#include +#include +#include +#include +#include +#include +#include +#include +// KDE +#include +#if KWIN_BUILD_X11 +#include +#endif +#include +#include +// KWin +#include "core/output.h" +#include "core/pixelgrid.h" +#include "core/rendertarget.h" +#include "core/renderviewport.h" +#include "cursor.h" +#include "cursorsource.h" +#include "effect/effecthandler.h" +#include "opengl/glutils.h" + +// based on StartupId in KRunner by Lubos Lunak +// SPDX-FileCopyrightText: 2001 Lubos Lunak + +Q_LOGGING_CATEGORY(KWIN_STARTUPFEEDBACK, "kwin_effect_startupfeedback", QtWarningMsg) + +static void ensureResources() +{ + // Must initialize resources manually because the effect is a static lib. + Q_INIT_RESOURCE(startupfeedback); +} + +namespace KWin +{ + +// number of key frames for bouncing animation +static const int BOUNCE_FRAMES = 20; +// duration between two key frames in msec +static const int BOUNCE_FRAME_DURATION = 30; +// duration of one bounce animation +static const int BOUNCE_DURATION = BOUNCE_FRAME_DURATION * BOUNCE_FRAMES; +// number of key frames for blinking animation +static const int BLINKING_FRAMES = 5; +// duration between two key frames in msec +static const int BLINKING_FRAME_DURATION = 100; +// duration of one blinking animation +static const int BLINKING_DURATION = BLINKING_FRAME_DURATION * BLINKING_FRAMES; +// const int color_to_pixmap[] = { 0, 1, 2, 3, 2, 1 }; +static const int FRAME_TO_BOUNCE_YOFFSET[] = { + -5, -1, 2, 5, 8, 10, 12, 13, 15, 15, 15, 15, 14, 12, 10, 8, 5, 2, -1, -5}; +static const QSize BOUNCE_SIZES[] = { + QSize(16, 16), QSize(14, 18), QSize(12, 20), QSize(18, 14), QSize(20, 12)}; +static const int FRAME_TO_BOUNCE_TEXTURE[] = { + 0, 0, 0, 1, 2, 2, 1, 0, 3, 4, 4, 3, 0, 1, 2, 2, 1, 0, 0, 0}; +static const int FRAME_TO_BLINKING_COLOR[] = { + 0, 1, 2, 3, 2, 1}; +static const QColor BLINKING_COLORS[] = { + Qt::black, Qt::darkGray, Qt::lightGray, Qt::white, Qt::white}; +static const int s_startupDefaultTimeout = 5; + +StartupFeedbackEffect::StartupFeedbackEffect() + : m_bounceSizesRatio(1.0) +#if KWIN_BUILD_X11 + , m_startupInfo(new KStartupInfo(KStartupInfo::CleanOnCantDetect, this)) + , m_selection(nullptr) +#endif + , m_active(false) + , m_frame(0) + , m_progress(0) + , m_lastPresentTime(std::chrono::milliseconds::zero()) + , m_type(BouncingFeedback) + , m_cursorSize(24) + , m_configWatcher(KConfigWatcher::create(KSharedConfig::openConfig("klaunchrc", KConfig::NoGlobals))) + , m_splashVisible(false) +{ +#if KWIN_BUILD_X11 + // TODO: move somewhere that is x11-specific + if (KWindowSystem::isPlatformX11()) { + m_selection = new KSelectionOwner("_KDE_STARTUP_FEEDBACK", effects->xcbConnection(), effects->x11RootWindow(), this); + m_selection->claim(true); + } + connect(m_startupInfo, &KStartupInfo::gotNewStartup, this, [](const KStartupInfoId &id, const KStartupInfoData &data) { + const auto icon = QIcon::fromTheme(data.findIcon(), QIcon::fromTheme(QStringLiteral("system-run"))); + Q_EMIT effects->startupAdded(id.id(), icon); + }); + connect(m_startupInfo, &KStartupInfo::gotRemoveStartup, this, [](const KStartupInfoId &id, const KStartupInfoData &data) { + Q_EMIT effects->startupRemoved(id.id()); + }); + connect(m_startupInfo, &KStartupInfo::gotStartupChange, this, [](const KStartupInfoId &id, const KStartupInfoData &data) { + const auto icon = QIcon::fromTheme(data.findIcon(), QIcon::fromTheme(QStringLiteral("system-run"))); + Q_EMIT effects->startupChanged(id.id(), icon); + }); +#endif + + connect(effects, &EffectsHandler::startupAdded, this, &StartupFeedbackEffect::gotNewStartup); + connect(effects, &EffectsHandler::startupRemoved, this, &StartupFeedbackEffect::gotRemoveStartup); + connect(effects, &EffectsHandler::startupChanged, this, &StartupFeedbackEffect::gotStartupChange); + + connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this]() { + reconfigure(ReconfigureAll); + }); + reconfigure(ReconfigureAll); + + m_splashVisible = QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.KSplash")); + auto serviceWatcher = new QDBusServiceWatcher(QStringLiteral("org.kde.KSplash"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this); + connect(serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, [this] { + m_splashVisible = true; + stop(); + }); + connect(serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this] { + m_splashVisible = false; + gotRemoveStartup({}); // Start the next feedback + }); +} + +StartupFeedbackEffect::~StartupFeedbackEffect() +{ +} + +bool StartupFeedbackEffect::supported() +{ + return effects->isOpenGLCompositing(); +} + +void StartupFeedbackEffect::reconfigure(Effect::ReconfigureFlags flags) +{ + KConfigGroup c = m_configWatcher->config()->group(QStringLiteral("FeedbackStyle")); + const bool busyCursor = c.readEntry("BusyCursor", true); + + c = m_configWatcher->config()->group(QStringLiteral("BusyCursorSettings")); + m_timeout = std::chrono::seconds(c.readEntry("Timeout", s_startupDefaultTimeout)); +#if KWIN_BUILD_X11 + m_startupInfo->setTimeout(m_timeout.count()); +#endif + const bool busyBlinking = c.readEntry("Blinking", false); + const bool busyBouncing = c.readEntry("Bouncing", true); + if (!busyCursor) { + m_type = NoFeedback; + } else if (busyBouncing) { + m_type = BouncingFeedback; + } else if (busyBlinking) { + m_type = BlinkingFeedback; + if (effects->compositingType() == OpenGLCompositing) { + ensureResources(); + m_blinkingShader = ShaderManager::instance()->generateShaderFromFile(ShaderTrait::MapTexture, QString(), QStringLiteral(":/effects/startupfeedback/shaders/blinking-startup.frag")); + if (m_blinkingShader->isValid()) { + qCDebug(KWIN_STARTUPFEEDBACK) << "Blinking Shader is valid"; + } else { + qCDebug(KWIN_STARTUPFEEDBACK) << "Blinking Shader is not valid"; + } + } + } else { + m_type = PassiveFeedback; + } + if (m_active) { + stop(); + start(m_startups[m_currentStartup]); + } +} + +void StartupFeedbackEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + int time = 0; + if (m_lastPresentTime.count()) { + time = (presentTime - m_lastPresentTime).count(); + } + m_lastPresentTime = presentTime; + + if (m_active && effects->isCursorHidden()) { + stop(); + } + if (m_active) { + // need the unclipped version + switch (m_type) { + case BouncingFeedback: + m_progress = (m_progress + time) % BOUNCE_DURATION; + m_frame = qRound((qreal)m_progress / (qreal)BOUNCE_FRAME_DURATION) % BOUNCE_FRAMES; + m_currentGeometry = feedbackRect(); // bounce alters geometry with m_frame + data.paint = data.paint.united(m_currentGeometry); + break; + case BlinkingFeedback: + m_progress = (m_progress + time) % BLINKING_DURATION; + m_frame = qRound((qreal)m_progress / (qreal)BLINKING_FRAME_DURATION) % BLINKING_FRAMES; + break; + default: + break; // nothing + } + } + effects->prePaintScreen(data, presentTime); +} + +void StartupFeedbackEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) +{ + effects->paintScreen(renderTarget, viewport, mask, deviceRegion, screen); + if (m_active) { + GLTexture *texture; + switch (m_type) { + case BouncingFeedback: + texture = m_bouncingTextures[FRAME_TO_BOUNCE_TEXTURE[m_frame]].get(); + break; + case BlinkingFeedback: // fall through + case PassiveFeedback: + texture = m_texture.get(); + break; + default: + return; // safety + } + if (!texture) { + return; + } + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + GLShader *shader = nullptr; + if (m_type == BlinkingFeedback && m_blinkingShader && m_blinkingShader->isValid()) { + const QColor &blinkingColor = BLINKING_COLORS[FRAME_TO_BLINKING_COLOR[m_frame]]; + ShaderManager::instance()->pushShader(m_blinkingShader.get()); + shader = m_blinkingShader.get(); + m_blinkingShader->setUniform(GLShader::ColorUniform::Color, blinkingColor); + } else { + shader = ShaderManager::instance()->pushShader(ShaderTrait::MapTexture | ShaderTrait::TransformColorspace); + } + const QRectF pixelGeometry = snapToPixelGridF(scaledRect(m_currentGeometry, viewport.scale())); + QMatrix4x4 mvp = viewport.projectionMatrix(); + mvp.translate(pixelGeometry.x(), pixelGeometry.y()); + shader->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, mvp); + shader->setColorspaceUniforms(ColorDescription::sRGB, renderTarget.colorDescription(), RenderingIntent::Perceptual); + texture->render(pixelGeometry.size()); + ShaderManager::instance()->popShader(); + glDisable(GL_BLEND); + } +} + +void StartupFeedbackEffect::postPaintScreen() +{ + if (m_active) { + m_dirtyRect = m_currentGeometry; // ensure the now dirty region is cleaned on the next pass + if (m_type == BlinkingFeedback || m_type == BouncingFeedback) { + effects->addRepaint(m_dirtyRect); // we also have to trigger a repaint + } + } + effects->postPaintScreen(); +} + +void StartupFeedbackEffect::slotMouseChanged(const QPointF &pos, const QPointF &oldpos, Qt::MouseButtons buttons, + Qt::MouseButtons oldbuttons, Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers) +{ + if (m_active) { + m_dirtyRect |= m_currentGeometry; + m_currentGeometry = feedbackRect(); + m_dirtyRect |= m_currentGeometry; + effects->addRepaint(m_dirtyRect); + } +} + +void StartupFeedbackEffect::gotNewStartup(const QString &id, const QIcon &icon) +{ + if (Cursors::self()->isCursorHidden()) { + return; + } + + const Cursor *mouse = Cursors::self()->mouse(); + if (mouse->source() && mouse->source()->isBlank()) { + return; + } + + Startup &startup = m_startups[id]; + startup.icon = icon; + + startup.expiredTimer = std::make_unique(); + // Stop the animation if the startup doesn't finish within reasonable interval. + connect(startup.expiredTimer.get(), &QTimer::timeout, this, [this, id]() { + gotRemoveStartup(id); + }); + startup.expiredTimer->setSingleShot(true); + startup.expiredTimer->start(m_timeout); + + m_currentStartup = id; + start(startup); +} + +void StartupFeedbackEffect::gotRemoveStartup(const QString &id) +{ + if (!m_startups.remove(id)) { + return; + } + if (m_startups.isEmpty()) { + m_currentStartup.clear(); + stop(); + return; + } + m_currentStartup = m_startups.begin().key(); + start(m_startups[m_currentStartup]); +} + +void StartupFeedbackEffect::gotStartupChange(const QString &id, const QIcon &icon) +{ + if (m_currentStartup == id) { + Startup ¤tStartup = m_startups[m_currentStartup]; + if (!icon.isNull() && icon.name() != currentStartup.icon.name()) { + currentStartup.icon = icon; + start(currentStartup); + } + } +} + +void StartupFeedbackEffect::start(const Startup &startup) +{ + if (m_type == NoFeedback || m_splashVisible || effects->isCursorHidden()) { + return; + } + + const LogicalOutput *output = effects->screenAt(effects->cursorPos().toPoint()); + if (!output) { + return; + } + if (!m_active) { + connect(effects, &EffectsHandler::mouseChanged, this, &StartupFeedbackEffect::slotMouseChanged); + } + m_active = true; + + // read details about the mouse-cursor theme define per default + KConfigGroup mousecfg(effects->inputConfig(), QStringLiteral("Mouse")); + m_cursorSize = mousecfg.readEntry("cursorSize", 24); + + int iconSize = m_cursorSize / 1.5; + if (!iconSize) { + iconSize = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize); + } + // get ratio for bouncing cursor so we don't need to manually calculate the sizes for each icon size + if (m_type == BouncingFeedback) { + m_bounceSizesRatio = iconSize / 16.0; + } + + const QPixmap iconPixmap = startup.icon.pixmap(QSize(iconSize, iconSize), output->scale()); + prepareTextures(iconPixmap); + m_dirtyRect = m_currentGeometry = feedbackRect(); + effects->addRepaint(m_dirtyRect); +} + +void StartupFeedbackEffect::stop() +{ + if (!m_active) { + return; + } + disconnect(effects, &EffectsHandler::mouseChanged, this, &StartupFeedbackEffect::slotMouseChanged); + m_active = false; + m_lastPresentTime = std::chrono::milliseconds::zero(); + effects->makeOpenGLContextCurrent(); + switch (m_type) { + case BouncingFeedback: + for (int i = 0; i < 5; ++i) { + m_bouncingTextures[i].reset(); + } + break; + case BlinkingFeedback: + case PassiveFeedback: + m_texture.reset(); + break; + case NoFeedback: + return; // don't want the full repaint + default: + break; // impossible + } + effects->addRepaintFull(); +} + +void StartupFeedbackEffect::prepareTextures(const QPixmap &pix) +{ + effects->makeOpenGLContextCurrent(); + switch (m_type) { + case BouncingFeedback: + for (int i = 0; i < 5; ++i) { + m_bouncingTextures[i] = GLTexture::upload(scalePixmap(pix, BOUNCE_SIZES[i])); + if (!m_bouncingTextures[i]) { + return; + } + m_bouncingTextures[i]->setFilter(GL_LINEAR); + m_bouncingTextures[i]->setWrapMode(GL_CLAMP_TO_EDGE); + } + break; + case BlinkingFeedback: + case PassiveFeedback: + m_texture = GLTexture::upload(pix); + if (!m_texture) { + return; + } + m_texture->setFilter(GL_LINEAR); + m_texture->setWrapMode(GL_CLAMP_TO_EDGE); + break; + default: + // for safety + stop(); + break; + } +} + +QImage StartupFeedbackEffect::scalePixmap(const QPixmap &pm, const QSize &size) const +{ + const qreal devicePixelRatio = pm.devicePixelRatioF(); + const QSize &adjustedSize = size * m_bounceSizesRatio; + + QImage result(feedbackIconSize() * devicePixelRatio, QImage::Format_ARGB32_Premultiplied); + result.fill(Qt::transparent); + result.setDevicePixelRatio(devicePixelRatio); + + QPainter p(&result); + p.setRenderHint(QPainter::SmoothPixmapTransform); + p.setCompositionMode(QPainter::CompositionMode_Source); + p.drawPixmap(QRectF((20 * m_bounceSizesRatio - adjustedSize.width()) / 2, + (20 * m_bounceSizesRatio - adjustedSize.height()) / 2, + adjustedSize.width(), + adjustedSize.height()), + pm, + pm.rect()); + return result; +} + +QSize StartupFeedbackEffect::feedbackIconSize() const +{ + return QSize(20, 20) * m_bounceSizesRatio; +} + +QRect StartupFeedbackEffect::feedbackRect() const +{ + int xDiff; + if (m_cursorSize <= 16) { + xDiff = 8 + 7; + } else if (m_cursorSize <= 32) { + xDiff = 16 + 7; + } else if (m_cursorSize <= 48) { + xDiff = 24 + 7; + } else { + xDiff = 32 + 7; + } + int yDiff = xDiff; + GLTexture *texture = nullptr; + int yOffset = 0; + switch (m_type) { + case BouncingFeedback: + texture = m_bouncingTextures[FRAME_TO_BOUNCE_TEXTURE[m_frame]].get(); + yOffset = FRAME_TO_BOUNCE_YOFFSET[m_frame] * m_bounceSizesRatio; + break; + case BlinkingFeedback: // fall through + case PassiveFeedback: + texture = m_texture.get(); + break; + default: + // nothing + break; + } + const QPoint cursorPos = effects->cursorPos().toPoint() + QPoint(xDiff, yDiff + yOffset); + QRect rect; + if (texture) { + rect = QRect(cursorPos, feedbackIconSize()); + } + return rect; +} + +bool StartupFeedbackEffect::isActive() const +{ + return m_active; +} + +} // namespace + +#include "moc_startupfeedback.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/startupfeedback/startupfeedback.h b/local/recipes/kde/kwin/source/src/plugins/startupfeedback/startupfeedback.h new file mode 100644 index 0000000000..c5a92afa2b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/startupfeedback/startupfeedback.h @@ -0,0 +1,104 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once +#include "effect/effect.h" +#include + +#if KWIN_BUILD_X11 +#include +#endif +#include +#include + +#include + +class KSelectionOwner; +namespace KWin +{ + +class GLShader; +class GLTexture; + +class StartupFeedbackEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(int type READ type) +public: + StartupFeedbackEffect(); + ~StartupFeedbackEffect() override; + + void reconfigure(ReconfigureFlags flags) override; + void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override; + void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) override; + void postPaintScreen() override; + bool isActive() const override; + + int requestedEffectChainPosition() const override + { + return 90; + } + + int type() const + { + return int(m_type); + } + + static bool supported(); + +private Q_SLOTS: + void gotNewStartup(const QString &id, const QIcon &icon); + void gotRemoveStartup(const QString &id); + void gotStartupChange(const QString &id, const QIcon &icon); + void slotMouseChanged(const QPointF &pos, const QPointF &oldpos, Qt::MouseButtons buttons, Qt::MouseButtons oldbuttons, Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers); + +private: + enum FeedbackType { + NoFeedback, + BouncingFeedback, + BlinkingFeedback, + PassiveFeedback + }; + + struct Startup + { + QIcon icon; + std::shared_ptr expiredTimer; + }; + + void start(const Startup &startup); + void stop(); + QImage scalePixmap(const QPixmap &pm, const QSize &size) const; + void prepareTextures(const QPixmap &pix); + QRect feedbackRect() const; + QSize feedbackIconSize() const; + + qreal m_bounceSizesRatio; +#if KWIN_BUILD_X11 + KStartupInfo *m_startupInfo; + KSelectionOwner *m_selection; +#endif + QString m_currentStartup; + QMap m_startups; + bool m_active; + int m_frame; + int m_progress; + std::chrono::milliseconds m_lastPresentTime; + std::unique_ptr m_bouncingTextures[5]; + std::unique_ptr m_texture; // for passive and blinking + FeedbackType m_type; + QRect m_currentGeometry, m_dirtyRect; + std::unique_ptr m_blinkingShader; + int m_cursorSize; + KConfigWatcher::Ptr m_configWatcher; + bool m_splashVisible; + std::chrono::seconds m_timeout; +}; +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/startupfeedback/startupfeedback.qrc b/local/recipes/kde/kwin/source/src/plugins/startupfeedback/startupfeedback.qrc new file mode 100644 index 0000000000..175186d892 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/startupfeedback/startupfeedback.qrc @@ -0,0 +1,7 @@ + + + shaders/blinking-startup.frag + shaders/blinking-startup_core.frag + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/stickykeys/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/stickykeys/CMakeLists.txt new file mode 100644 index 0000000000..b99b5d04ab --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/stickykeys/CMakeLists.txt @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2022 Nicolas Fella +# SPDX-License-Identifier: BSD-3-Clause + +kcoreaddons_add_plugin(StickyKeysPlugin INSTALL_NAMESPACE "kwin/plugins") + +ecm_qt_declare_logging_category(StickyKeysPlugin + HEADER stickykeys_debug.h + IDENTIFIER KWIN_STICKYKEYS + CATEGORY_NAME kwin_stickykeys + DEFAULT_SEVERITY Warning +) + +target_sources(StickyKeysPlugin PRIVATE + main.cpp + stickykeys.cpp +) +target_link_libraries(StickyKeysPlugin PRIVATE kwin KF6::WindowSystem KF6::I18n XKB::XKB) + +if (KWIN_BUILD_NOTIFICATIONS) + target_link_libraries(StickyKeysPlugin PRIVATE KF6::Notifications) +endif() diff --git a/local/recipes/kde/kwin/source/src/plugins/stickykeys/main.cpp b/local/recipes/kde/kwin/source/src/plugins/stickykeys/main.cpp new file mode 100644 index 0000000000..84d5bd6602 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/stickykeys/main.cpp @@ -0,0 +1,24 @@ +/* + SPDX-FileCopyrightText: 2022 Nicolas Fella + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include + +#include "stickykeys.h" + +class KWIN_EXPORT StickyKeysFactory : public KWin::PluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID PluginFactory_iid FILE "metadata.json") + Q_INTERFACES(KWin::PluginFactory) + +public: + std::unique_ptr create() const override + { + return std::make_unique(); + } +}; + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/stickykeys/metadata.json b/local/recipes/kde/kwin/source/src/plugins/stickykeys/metadata.json new file mode 100644 index 0000000000..aa304f4093 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/stickykeys/metadata.json @@ -0,0 +1,5 @@ +{ + "KPlugin": { + "EnabledByDefault": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/stickykeys/stickykeys.cpp b/local/recipes/kde/kwin/source/src/plugins/stickykeys/stickykeys.cpp new file mode 100644 index 0000000000..f8c218016b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/stickykeys/stickykeys.cpp @@ -0,0 +1,227 @@ +/* + SPDX-FileCopyrightText: 2022 Nicolas Fella + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "stickykeys.h" +#include "effect/effecthandler.h" +#include "keyboard_input.h" +#include "xkb.h" + +#include + +#include +#if KWIN_BUILD_NOTIFICATIONS +#include +#endif + +struct Modifier +{ + Qt::Key key; + KLazyLocalizedString lockedText; +}; + +static const std::array modifiers = { + Modifier{Qt::Key_Shift, kli18n("The Shift key has been locked and is now active for all of the following keypresses.")}, + Modifier{Qt::Key_Control, kli18n("The Control key has been locked and is now active for all of the following keypresses.")}, + Modifier{Qt::Key_Alt, kli18n("The Alt key has been locked and is now active for all of the following keypresses.")}, + Modifier{Qt::Key_Meta, kli18n("The Meta key has been locked and is now active for all of the following keypresses.")}, + Modifier{Qt::Key_AltGr, kli18n("The AltGr key has been locked and is now active for all of the following keypresses.")}, +}; + +StickyKeysFilter::StickyKeysFilter() + : KWin::InputEventFilter(KWin::InputFilterOrder::StickyKeys) + , m_configWatcher(KConfigWatcher::create(KSharedConfig::openConfig("kaccessrc"))) +{ + const QLatin1String groupName("Keyboard"); + connect(m_configWatcher.get(), &KConfigWatcher::configChanged, this, [this, groupName](const KConfigGroup &group) { + if (group.name() == groupName) { + loadConfig(group); + } + }); + loadConfig(m_configWatcher->config()->group(groupName)); + + for (int mod : std::as_const(m_modifiers)) { + m_keyStates[mod] = None; + } +} + +KWin::Xkb::Modifier keyToModifier(Qt::Key key) +{ + if (key == Qt::Key_Shift) { + return KWin::Xkb::Shift; + } else if (key == Qt::Key_Alt) { + return KWin::Xkb::Mod1; + } else if (key == Qt::Key_Control) { + return KWin::Xkb::Control; + } else if (key == Qt::Key_AltGr) { + return KWin::Xkb::Mod5; + } else if (key == Qt::Key_Meta) { + return KWin::Xkb::Mod4; + } + + return KWin::Xkb::NoModifier; +} + +void StickyKeysFilter::loadConfig(const KConfigGroup &group) +{ + KWin::input()->uninstallInputEventFilter(this); + + m_lockKeys = group.readEntry("StickyKeysLatch", true); + m_showNotificationForLockedKeys = group.readEntry("kNotifyModifiers", false); + m_disableOnTwoKeys = group.readEntry("StickyKeysAutoOff", false); + m_ringBell = group.readEntry("StickyKeysBeep", false); + + bool changed = false; + if (!m_lockKeys) { + // locking keys is deactivated, unlock all locked keys + for (auto it = m_keyStates.keyValueBegin(); it != m_keyStates.keyValueEnd(); ++it) { + if (it->second == KeyState::Locked) { + it->second = KeyState::None; + KWin::input()->keyboard()->xkb()->setModifierLocked(keyToModifier(static_cast(it->first)), false); + changed = true; + } + } + } + + if (group.readEntry("StickyKeys", false)) { + KWin::input()->installInputEventFilter(this); + } else { + // sticky keys are deactivated, unlatch all latched/locked keys + for (auto it = m_keyStates.keyValueBegin(); it != m_keyStates.keyValueEnd(); ++it) { + if (it->second != KeyState::None) { + it->second = KeyState::None; + KWin::input()->keyboard()->xkb()->setModifierLatched(keyToModifier(static_cast(it->first)), false); + changed = true; + } + } + } + if (changed) { + KWin::input()->keyboard()->xkb()->forwardModifiers(); + Q_EMIT KWin::input()->keyboard()->xkb()->modifierStateChanged(); + } +} + +bool StickyKeysFilter::keyboardKey(KWin::KeyboardKeyEvent *event) +{ + const bool pressed = event->state == KWin::KeyboardKeyState::Repeated || event->state == KWin::KeyboardKeyState::Pressed; + bool changed = false; + + if (m_modifiers.contains(event->key)) { + if (pressed) { + m_pressedModifiers << event->key; + } else { + m_pressedModifiers.remove(event->key); + } + + auto keyState = m_keyStates.find(event->key); + + if (m_ringBell && !pressed) { + if (auto effect = KWin::effects->provides(KWin::Effect::SystemBell)) { + effect->perform(KWin::Effect::SystemBell, {}); + } + } + + if (keyState != m_keyStates.end()) { + if (pressed) { + // An unlatched modifier was pressed, latch it + if (keyState.value() == None) { + keyState.value() = Latched; + KWin::input()->keyboard()->xkb()->setModifierLatched(keyToModifier(static_cast(event->key)), true); + changed = true; + } + // A latched modifier was pressed, lock it + else if (keyState.value() == Latched && m_lockKeys) { + keyState.value() = Locked; + KWin::input()->keyboard()->xkb()->setModifierLatched(keyToModifier(static_cast(event->key)), false); + KWin::input()->keyboard()->xkb()->setModifierLocked(keyToModifier(static_cast(event->key)), true); + changed = true; + + if (m_showNotificationForLockedKeys) { +#if KWIN_BUILD_NOTIFICATIONS + KNotification *noti = new KNotification("modifierkey-locked"); + noti->setComponentName("kaccess"); + + for (const auto mod : modifiers) { + if (mod.key == event->key) { + noti->setText(mod.lockedText.toString()); + break; + } + } + + noti->sendEvent(); +#endif + } + } + // A locked modifier was pressed, unlock it + else if (keyState.value() == Locked && m_lockKeys) { + keyState.value() = None; + KWin::input()->keyboard()->xkb()->setModifierLocked(keyToModifier(static_cast(event->key)), false); + changed = true; + } + } + } + } else if (pressed) { + if (!m_pressedModifiers.isEmpty() && m_disableOnTwoKeys) { + disableStickyKeys(); + changed = true; + } + + // a non-modifier key was pressed, unlatch all unlocked modifiers + for (auto it = m_keyStates.keyValueBegin(); it != m_keyStates.keyValueEnd(); ++it) { + + if (it->second == Locked) { + continue; + } + + it->second = KeyState::None; + + KWin::input()->keyboard()->xkb()->setModifierLatched(keyToModifier(static_cast(it->first)), false); + changed = true; + } + } + if (changed) { + Q_EMIT KWin::input()->keyboard()->xkb()->modifierStateChanged(); + } + + return false; +} + +void StickyKeysFilter::disableStickyKeys() +{ + for (auto it = m_keyStates.keyValueBegin(); it != m_keyStates.keyValueEnd(); ++it) { + it->second = KeyState::None; + KWin::input()->keyboard()->xkb()->setModifierLatched(keyToModifier(static_cast(it->first)), false); + KWin::input()->keyboard()->xkb()->setModifierLocked(keyToModifier(static_cast(it->first)), false); + } + + KWin::input()->uninstallInputEventFilter(this); +} + +bool StickyKeysFilter::pointerButton(KWin::PointerButtonEvent *event) +{ + if (event->state == KWin::PointerButtonState::Released) { + // unlatch all unlocked modifiers + for (auto it = m_keyStates.keyValueBegin(); it != m_keyStates.keyValueEnd(); ++it) { + + if (it->second == Locked) { + continue; + } + + it->second = KeyState::None; + + KWin::input()->keyboard()->xkb()->setModifierLatched(keyToModifier(static_cast(it->first)), false); + + // We need to delay the modifier update until the client received the mouse event, otherwise + // the updated modifiers arrive before the mouse event and e.g. Ctrl+Click won't work + QTimer::singleShot(0, this, [] { + KWin::input()->keyboard()->xkb()->forwardModifiers(); + }); + } + } + + return false; +} + +#include "moc_stickykeys.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/stickykeys/stickykeys.h b/local/recipes/kde/kwin/source/src/plugins/stickykeys/stickykeys.h new file mode 100644 index 0000000000..f76a7ebfce --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/stickykeys/stickykeys.h @@ -0,0 +1,41 @@ +/* + SPDX-FileCopyrightText: 2022 Nicolas Fella + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include "plugin.h" + +#include "input.h" +#include "input_event.h" + +class StickyKeysFilter : public KWin::Plugin, public KWin::InputEventFilter +{ + Q_OBJECT +public: + explicit StickyKeysFilter(); + + bool keyboardKey(KWin::KeyboardKeyEvent *event) override; + bool pointerButton(KWin::PointerButtonEvent *event) override; + + enum KeyState { + None, + Latched, + Locked, + }; + +private: + void loadConfig(const KConfigGroup &group); + void disableStickyKeys(); + + KConfigWatcher::Ptr m_configWatcher; + QMap m_keyStates; + QList m_modifiers = {Qt::Key_Shift, Qt::Key_Control, Qt::Key_Alt, Qt::Key_AltGr, Qt::Key_Meta}; + bool m_lockKeys = false; + bool m_showNotificationForLockedKeys = false; + bool m_disableOnTwoKeys = false; + QSet m_pressedModifiers; + bool m_ringBell = false; +}; diff --git a/local/recipes/kde/kwin/source/src/plugins/strip-effect-metadata.py b/local/recipes/kde/kwin/source/src/plugins/strip-effect-metadata.py new file mode 100755 index 0000000000..6981aa4c53 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/strip-effect-metadata.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2021 Vlad Zahorodnii +# SPDX-FileCopyrightText: 2022 Alex Richardson +# SPDX-License-Identifier: GPL-2.0-or-later +# +# This little helper strips unnecessary information from builtin effect metadata files to +# reduce the size of kwin executables and json parsing runtime overhead. + +import argparse +import json + +def main(): + parser = argparse.ArgumentParser(prog="kwin-strip-effect-metadata") + parser.add_argument("--source", help="input file", required=True) + parser.add_argument("--output", help="output file", required=True) + args = parser.parse_args() + stripped_json = dict(KPlugin=dict()) + with open(args.source, "r") as src: + original_json = json.load(src) + stripped_json["KPlugin"]["EnabledByDefault"] = original_json["KPlugin"]["EnabledByDefault"] + + with open(args.output, "w") as dst: + json.dump(stripped_json, dst) + + +if __name__ == "__main__": + main() diff --git a/local/recipes/kde/kwin/source/src/plugins/synchronizeskipswitcher/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/synchronizeskipswitcher/CMakeLists.txt new file mode 100644 index 0000000000..a5d280a6d8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/synchronizeskipswitcher/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_script(synchronizeskipswitcher package) diff --git a/local/recipes/kde/kwin/source/src/plugins/synchronizeskipswitcher/package/contents/code/main.js b/local/recipes/kde/kwin/source/src/plugins/synchronizeskipswitcher/package/contents/code/main.js new file mode 100644 index 0000000000..50424ab5e0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/synchronizeskipswitcher/package/contents/code/main.js @@ -0,0 +1,22 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +function setupConnection(window) { + window.skipSwitcher = window.skipTaskbar; + window.skipTaskbarChanged.connect(() => { + window.skipSwitcher = window.skipTaskbar; + }); +} + +workspace.windowAdded.connect(setupConnection); +// connect all existing clients +var clients = workspace.windowList(); +for (var i=0; i + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "systembell.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(SystemBellEffect, + "metadata.json.stripped", + return SystemBellEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/systembell/metadata.json b/local/recipes/kde/kwin/source/src/plugins/systembell/metadata.json new file mode 100644 index 0000000000..03a9aa291a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/systembell/metadata.json @@ -0,0 +1,94 @@ +{ + "KPlugin": { + "Category": "Accessibility", + "Description": "Provides the bell", + "Description[ar]": "يحدث صوت جرسا", + "Description[be]": "Забеспячэнне сістэмнага гукавога апавяшчэння", + "Description[bg]": "Предоставя системна сигнализация", + "Description[ca@valencia]": "Proporciona el timbre", + "Description[ca]": "Proporciona el timbre", + "Description[cs]": "Poskytuje zvonek", + "Description[da]": "Leverer klokken", + "Description[de]": "Stellt den Signalton bereit", + "Description[en_GB]": "Provides the bell", + "Description[eo]": "Provizas la pepon", + "Description[es]": "Proporciona el timbre del sistema", + "Description[eu]": "Txirrina hornitzen du", + "Description[fi]": "Tarjoaa äänimerkin", + "Description[fr]": "Fournit la cloche", + "Description[gl]": "Fornece a badalada.", + "Description[he]": "מספק את הפעמון", + "Description[hu]": "A csengőt biztosítja", + "Description[ia]": "Forni le campana", + "Description[is]": "Virkjar bjölluna", + "Description[it]": "Fornisce la campanella", + "Description[ja]": "ベルを鳴らす", + "Description[ka]": "მოგაწოდებთ ზარს", + "Description[ko]": "벨을 제공함", + "Description[lt]": "Pateikia skambutį", + "Description[lv]": "Nodrošina zvanu", + "Description[nb]": "Gir tilgang til systembjella", + "Description[nl]": "Levert de bel", + "Description[nn]": "Gjev tilgang til systembjølla", + "Description[pl]": "Dostarcza dzwonek", + "Description[pt_BR]": "Fornece a campainha", + "Description[ro]": "Furnizează clopoțelul", + "Description[ru]": "Предоставление сигнала", + "Description[sa]": "घण्टां प्रदाति", + "Description[sk]": "Poskytuje zvonček", + "Description[sl]": "Zagotavlja zvonec", + "Description[sv]": "Tillhandahåller alarmet", + "Description[ta]": "கணினி மணியை வழங்கும்", + "Description[tr]": "Zili çalar", + "Description[uk]": "Надає сигнал", + "Description[zh_CN]": "提供系统提示", + "Description[zh_TW]": "提供系統響鈴", + "EnabledByDefault": true, + "License": "GPL", + "Name": "System Bell", + "Name[ar]": "جرس النظام", + "Name[be]": "Сістэмнае гукавое апавяшчэнне", + "Name[bg]": "Системна сигнализация", + "Name[ca@valencia]": "Timbre del sistema", + "Name[ca]": "Timbre del sistema", + "Name[cs]": "Systémový zvonek", + "Name[da]": "Systemklokke", + "Name[de]": "Signalton des Systems", + "Name[en_GB]": "System Bell", + "Name[eo]": "Sistempepo", + "Name[es]": "Timbre del sistema", + "Name[eu]": "Sistemaren txirrina", + "Name[fi]": "Järjestelmän äänimerkki", + "Name[fr]": "Cloche du système", + "Name[gl]": "Badalada do sistema", + "Name[he]": "פעמון מערכת", + "Name[hu]": "Rendszercsengő", + "Name[ia]": "Campana de systema", + "Name[is]": "Kerfisbjalla", + "Name[it]": "Campanella di sistema", + "Name[ja]": "システムベル", + "Name[ka]": "სისტემური ზარი", + "Name[ko]": "시스템 벨", + "Name[lt]": "Sistemos skambutis", + "Name[lv]": "Sistēmas zvans", + "Name[nb]": "Systembjelle", + "Name[nl]": "Systeembel", + "Name[nn]": "Systembjølle", + "Name[pl]": "Dzwonek systemowy", + "Name[pt_BR]": "Campainha do sistema", + "Name[ro]": "Clopoțel de sistem", + "Name[ru]": "Системный сигнал", + "Name[sa]": "प्रणाली घण्टी", + "Name[sk]": "Systémový zvonček", + "Name[sl]": "Sistemski zvonec", + "Name[sv]": "Systemalarm", + "Name[ta]": "கணினி மணி", + "Name[tr]": "Sistem Zili", + "Name[uk]": "Системний дзвінок", + "Name[zh_CN]": "系统提示", + "Name[zh_TW]": "系統響鈴" + }, + "org.kde.kwin.effect": { + "internal": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/systembell/shaders/color.frag b/local/recipes/kde/kwin/source/src/plugins/systembell/shaders/color.frag new file mode 100644 index 0000000000..54664a157e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/systembell/shaders/color.frag @@ -0,0 +1,6 @@ +uniform vec4 geometryColor; + +void main() +{ + gl_FragColor = geometryColor; +} diff --git a/local/recipes/kde/kwin/source/src/plugins/systembell/shaders/color_core.frag b/local/recipes/kde/kwin/source/src/plugins/systembell/shaders/color_core.frag new file mode 100644 index 0000000000..54664a157e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/systembell/shaders/color_core.frag @@ -0,0 +1,6 @@ +uniform vec4 geometryColor; + +void main() +{ + gl_FragColor = geometryColor; +} diff --git a/local/recipes/kde/kwin/source/src/plugins/systembell/shaders/invert.frag b/local/recipes/kde/kwin/source/src/plugins/systembell/shaders/invert.frag new file mode 100644 index 0000000000..997e1b89e9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/systembell/shaders/invert.frag @@ -0,0 +1,24 @@ +#include "colormanagement.glsl" +#include "saturation.glsl" + +uniform sampler2D sampler; +uniform vec4 modulation; + +varying vec2 texcoord0; + +void main() +{ + vec4 tex = texture2D(sampler, texcoord0); + tex = sourceEncodingToNitsInDestinationColorspace(tex); + tex = adjustSaturation(tex); + + // to preserve perceptual contrast, apply the inversion in gamma 2.2 space + tex = nitsToEncoding(tex, gamma22_EOTF, 0.0, destinationReferenceLuminance); + tex.rgb /= max(0.001, tex.a); + tex.rgb = vec3(1.0) - tex.rgb; + tex *= modulation; + tex.rgb *= tex.a; + tex = encodingToNits(tex, gamma22_EOTF, 0.0, destinationReferenceLuminance); + + gl_FragColor = nitsToDestinationEncoding(tex); +} diff --git a/local/recipes/kde/kwin/source/src/plugins/systembell/shaders/invert_core.frag b/local/recipes/kde/kwin/source/src/plugins/systembell/shaders/invert_core.frag new file mode 100644 index 0000000000..e5e862ed32 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/systembell/shaders/invert_core.frag @@ -0,0 +1,28 @@ +#version 140 + +#include "colormanagement.glsl" +#include "saturation.glsl" + +uniform sampler2D sampler; +uniform vec4 modulation; + +in vec2 texcoord0; + +out vec4 fragColor; + +void main() +{ + vec4 tex = texture(sampler, texcoord0); + tex = sourceEncodingToNitsInDestinationColorspace(tex); + tex = adjustSaturation(tex); + + // to preserve perceptual contrast, apply the inversion in gamma 2.2 space + tex = nitsToEncoding(tex, gamma22_EOTF, 0.0, destinationReferenceLuminance); + tex.rgb /= max(0.001, tex.a); + tex.rgb = vec3(1.0) - tex.rgb; + tex *= modulation; + tex.rgb *= tex.a; + tex = encodingToNits(tex, gamma22_EOTF, 0.0, destinationReferenceLuminance); + + fragColor = nitsToDestinationEncoding(tex); +} diff --git a/local/recipes/kde/kwin/source/src/plugins/systembell/systembell.cpp b/local/recipes/kde/kwin/source/src/plugins/systembell/systembell.cpp new file mode 100644 index 0000000000..3861d69df8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/systembell/systembell.cpp @@ -0,0 +1,310 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2024 Nicolas Fella + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "systembell.h" + +#include "effect/effecthandler.h" +#include "opengl/glshader.h" +#include "opengl/glshadermanager.h" +#include "wayland/surface.h" +#include "wayland/xdgsystembell_v1.h" +#include "wayland_server.h" +#include "window.h" + +#include +#include +#include +#include + +#include + +#include + +using namespace std::literals; + +Q_LOGGING_CATEGORY(KWIN_SYSTEMBELL, "kwin_effect_systembell", QtWarningMsg) + +static void ensureResources() +{ + // Must initialize resources manually because the effect is a static lib. + Q_INIT_RESOURCE(systembell); +} + +namespace KWin +{ + +QTimer *SystemBellEffect::s_systemBellRemoveTimer = nullptr; +XdgSystemBellV1Interface *SystemBellEffect::s_systemBell = nullptr; + +SystemBellEffect::SystemBellEffect() + : m_configWatcher(KConfigWatcher::create(KSharedConfig::openConfig("kaccessrc"))) + , m_kdeglobals(QStringLiteral("kdeglobals")) +{ + QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/kde/KWin/Effect/SystemBell1"), + QStringLiteral("org.kde.KWin.Effect.SystemBell1"), + this, + QDBusConnection::ExportAllSlots); + + connect(effects, &EffectsHandler::windowClosed, this, &SystemBellEffect::slotWindowClosed); + + const QLatin1String groupName("Bell"); + connect(m_configWatcher.get(), &KConfigWatcher::configChanged, this, [this, groupName](const KConfigGroup &group) { + if (group.name() == groupName) { + m_bellConfig = group; + reconfigure(ReconfigureAll); + } + }); + m_bellConfig = m_configWatcher->config()->group(groupName); + reconfigure(ReconfigureAll); + + int ret = ca_context_create(&m_caContext); + if (ret != CA_SUCCESS) { + qCWarning(KWIN_SYSTEMBELL) << "Failed to initialize canberra context for audio notification:" << ca_strerror(ret); + m_caContext = nullptr; + } else { + ret = ca_context_change_props(m_caContext, + CA_PROP_APPLICATION_NAME, + qApp->applicationDisplayName().toUtf8().constData(), + CA_PROP_APPLICATION_ID, + qApp->desktopFileName().toUtf8().constData(), + nullptr); + if (ret != CA_SUCCESS) { + qCWarning(KWIN_SYSTEMBELL) << "Failed to set application properties on canberra context for audio notification:" << ca_strerror(ret); + } + } + + if (!s_systemBellRemoveTimer) { + s_systemBellRemoveTimer = new QTimer(QCoreApplication::instance()); + s_systemBellRemoveTimer->setSingleShot(true); + s_systemBellRemoveTimer->callOnTimeout([]() { + s_systemBell->remove(); + s_systemBell = nullptr; + }); + } + s_systemBellRemoveTimer->stop(); + if (!s_systemBell) { + s_systemBell = new XdgSystemBellV1Interface(waylandServer()->display(), s_systemBellRemoveTimer); + connect(s_systemBell, &XdgSystemBellV1Interface::ringSurface, this, [this](SurfaceInterface *surface) { + triggerWindow(effects->findWindow(surface)); + }); + connect(s_systemBell, &XdgSystemBellV1Interface::ring, this, [this](ClientConnection *client) { + if (effects->activeWindow()) { + if (effects->activeWindow()->surface() && effects->activeWindow()->surface()->client() == client) { + triggerWindow(effects->activeWindow()); + } + } + }); + } + + m_audioThrottleTimer.setInterval(100ms); + m_audioThrottleTimer.setSingleShot(true); + + // The Web Content Accessibility Guidelines (WCAG) recommend that any + // element that flashes in the screen must have a maximum period of + // 3Hz to avoid the risk of Photosensitivity Seizures. + // 3Hz is 333ms, double that to account for the window un-inverting, and round up + m_visualThrottleTimer.setInterval(700ms); + m_visualThrottleTimer.setSingleShot(true); +} + +SystemBellEffect::~SystemBellEffect() +{ + if (m_caContext) { + ca_context_destroy(m_caContext); + } + // When compositing is restarted, avoid removing the system bell immediately + if (s_systemBell) { + s_systemBellRemoveTimer->start(1000); + } +} + +void SystemBellEffect::reconfigure(ReconfigureFlags flags) +{ + m_inited = false; + m_color = m_bellConfig.readEntry("VisibleBellColor", QColor(Qt::red)); + m_mode = m_bellConfig.readEntry("VisibleBellInvert", false) ? Invert : Color; + m_duration = m_bellConfig.readEntry("VisibleBellPause", 500); + m_audibleBell = m_bellConfig.readEntry("SystemBell", true); + m_customBell = m_bellConfig.readEntry("ArtsBell", false); + m_customBellFile = m_bellConfig.readEntry("ArtsBellFile", QString()); + m_visibleBell = m_bellConfig.readEntry("VisibleBell", false); +} + +bool SystemBellEffect::supported() +{ + return effects->compositingType() == OpenGLCompositing; +} + +void SystemBellEffect::flash(EffectWindow *window) +{ + if (m_valid && !m_inited) { + m_valid = loadData(); + } + + redirect(window); + setShader(window, m_shader.get()); +} + +void SystemBellEffect::unflash(EffectWindow *window) +{ + unredirect(window); +} + +bool SystemBellEffect::loadData() +{ + ensureResources(); + m_inited = true; + + if (m_visibleBell) { + if (m_mode == Invert) { + m_shader = ShaderManager::instance()->generateShaderFromFile(ShaderTrait::MapTexture, QString(), QStringLiteral(":/effects/systembell/shaders/invert.frag")); + } else { + m_shader = ShaderManager::instance()->generateShaderFromFile(ShaderTrait::MapTexture, QString(), QStringLiteral(":/effects/systembell/shaders/color.frag")); + ShaderBinder binder(m_shader.get()); + m_shader->setUniform(GLShader::ColorUniform::Color, m_color); + } + + if (!m_shader->isValid()) { + qCCritical(KWIN_SYSTEMBELL) << "The shader failed to load!"; + return false; + } + } + + return true; +} + +void SystemBellEffect::slotWindowClosed(EffectWindow *w) +{ + m_windows.removeOne(w); +} + +void SystemBellEffect::triggerScreen() +{ + if (m_allWindows) { + return; + } + + if (m_audibleBell) { + playAudibleBell(); + } + + if (m_visibleBell) { + m_allWindows = true; + + if (m_visualThrottleTimer.isActive()) { + return; + } + + m_visualThrottleTimer.start(); + + const auto windows = effects->stackingOrder(); + for (EffectWindow *window : windows) { + flash(window); + } + + QTimer::singleShot(m_duration, this, [this] { + const auto windows = effects->stackingOrder(); + for (EffectWindow *window : windows) { + unflash(window); + } + m_allWindows = false; + effects->addRepaintFull(); + }); + + effects->addRepaintFull(); + } +} + +void SystemBellEffect::triggerWindow() +{ + triggerWindow(effects->activeWindow()); +} + +void SystemBellEffect::triggerWindow(EffectWindow *window) +{ + if (!window || m_windows.contains(window)) { + return; + } + + if (m_audibleBell) { + playAudibleBell(); + } + + if (m_visibleBell) { + if (m_visualThrottleTimer.isActive()) { + return; + } + + m_visualThrottleTimer.start(); + + m_windows.append(window); + flash(window); + + QTimer::singleShot(m_duration, this, [this, window] { + // window may be closed by now + if (m_windows.contains(window)) { + unflash(window); + m_windows.removeOne(window); + window->addRepaintFull(); + } + }); + + window->addRepaintFull(); + } +} + +bool SystemBellEffect::isActive() const +{ + return m_valid && (m_allWindows || !m_windows.isEmpty()); +} + +bool SystemBellEffect::provides(Feature f) +{ + return f == SystemBell; +} + +bool SystemBellEffect::perform(Feature feature, const QVariantList &arguments) +{ + triggerScreen(); + return true; +} + +void SystemBellEffect::playAudibleBell() +{ + if (m_audioThrottleTimer.isActive()) { + return; + } + + m_audioThrottleTimer.start(); + + if (m_customBell) { + ca_context_play(m_caContext, + 0, + CA_PROP_MEDIA_FILENAME, + QFile::encodeName(QUrl(m_customBellFile).toLocalFile()).constData(), + CA_PROP_MEDIA_ROLE, + "event", + nullptr); + } else { + const QString themeName = m_kdeglobals.group(QStringLiteral("Sounds")).readEntry("Theme", QStringLiteral("ocean")); + ca_context_play(m_caContext, + 0, + CA_PROP_EVENT_ID, + "bell", + CA_PROP_MEDIA_ROLE, + "event", + CA_PROP_CANBERRA_XDG_THEME_NAME, + themeName.toUtf8().constData(), + nullptr); + } +} + +} // namespace + +#include "moc_systembell.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/systembell/systembell.h b/local/recipes/kde/kwin/source/src/plugins/systembell/systembell.h new file mode 100644 index 0000000000..089e53f4a1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/systembell/systembell.h @@ -0,0 +1,92 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2024 Nicolas Fella + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/offscreeneffect.h" + +#include +#include + +#include + +class ca_context; + +namespace KWin +{ + +class GLShader; +class XdgSystemBellV1Interface; + +class SystemBellEffect : public OffscreenEffect +{ + Q_OBJECT +public: + SystemBellEffect(); + ~SystemBellEffect() override; + + bool isActive() const override; + int requestedEffectChainPosition() const override; + bool provides(Feature f) override; + bool perform(Feature feature, const QVariantList &arguments) override; + void reconfigure(ReconfigureFlags flags) override; + + static bool supported(); + +public Q_SLOTS: + void triggerScreen(); + void triggerWindow(); + +private Q_SLOTS: + void slotWindowClosed(KWin::EffectWindow *w); + +protected: + bool loadData(); + +private: + enum Mode { + Invert, + Color, + }; + + void triggerWindow(EffectWindow *window); + void flash(EffectWindow *window); + void unflash(EffectWindow *window); + void loadConfig(const KConfigGroup &group); + void playAudibleBell(); + + bool m_inited = false; + bool m_valid = true; + std::unique_ptr m_shader; + bool m_allWindows = false; + QList m_windows; + QColor m_color; + int m_duration; + Mode m_mode; + ca_context *m_caContext = nullptr; + bool m_visibleBell = false; + bool m_audibleBell = false; + bool m_customBell = false; + QString m_customBellFile; + KConfigWatcher::Ptr m_configWatcher; + KConfig m_kdeglobals; + KConfigGroup m_bellConfig; + + static QTimer *s_systemBellRemoveTimer; + QTimer m_audioThrottleTimer; + QTimer m_visualThrottleTimer; + static XdgSystemBellV1Interface *s_systemBell; +}; + +inline int SystemBellEffect::requestedEffectChainPosition() const +{ + return 99; +} + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/systembell/systembell.qrc b/local/recipes/kde/kwin/source/src/plugins/systembell/systembell.qrc new file mode 100644 index 0000000000..a2ac0706c3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/systembell/systembell.qrc @@ -0,0 +1,8 @@ + + + shaders/color.frag + shaders/color_core.frag + shaders/invert.frag + shaders/invert_core.frag + + diff --git a/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/CMakeLists.txt new file mode 100644 index 0000000000..e545ccdbb6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/CMakeLists.txt @@ -0,0 +1,39 @@ +####################################### +# Effect + +set(thumbnailaside_SOURCES + main.cpp + thumbnailaside.cpp +) + +kconfig_add_kcfg_files(thumbnailaside_SOURCES + thumbnailasideconfig.kcfgc +) + +kwin_add_builtin_effect(thumbnailaside ${thumbnailaside_SOURCES}) +target_link_libraries(thumbnailaside PRIVATE + kwin + + KF6::ConfigGui + KF6::GlobalAccel + KF6::I18n +) + +####################################### +# Config +if (KWIN_BUILD_KCMS) + set(kwin_thumbnailaside_config_SRCS thumbnailaside_config.cpp) + ki18n_wrap_ui(kwin_thumbnailaside_config_SRCS thumbnailaside_config.ui) + kconfig_add_kcfg_files(kwin_thumbnailaside_config_SRCS thumbnailasideconfig.kcfgc) + + kwin_add_effect_config(kwin_thumbnailaside_config ${kwin_thumbnailaside_config_SRCS}) + + target_link_libraries(kwin_thumbnailaside_config + KF6::KCMUtils + KF6::CoreAddons + KF6::GlobalAccel + KF6::I18n + KF6::XmlGui + KWinEffectsInterface + ) +endif() diff --git a/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/main.cpp b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/main.cpp new file mode 100644 index 0000000000..c073db769e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/main.cpp @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "thumbnailaside.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY(ThumbnailAsideEffect, + "metadata.json.stripped") + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/metadata.json b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/metadata.json new file mode 100644 index 0000000000..db0bd4c88f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/metadata.json @@ -0,0 +1,94 @@ +{ + "KPlugin": { + "Category": "Appearance", + "Description": "Display window thumbnails on the edge of the screen; activated with a keyboard shortcut", + "Description[ar]": "اعرض مصغّرات النوافذ في حافة الشاشة، تفعّل باختصار لوحة المفاتيح", + "Description[bg]": "Показване на миниатюри на прозореца в края на екрана, активирано с клавишна комбинация", + "Description[ca@valencia]": "Mostra les miniatures de les finestres a la vora de la pantalla; activat amb una drecera de teclat", + "Description[ca]": "Mostra les miniatures de les finestres a la vora de la pantalla; activat amb una drecera de teclat", + "Description[da]": "Viser vinduethumbnails på kanten af skærmen; aktiveret med en tastaturgenvej", + "Description[de]": "Zeigt Vorschaubilder von Fenstern an einem Bildschirmrand an; mit einem Tastatur-Kurzbefehl ausgelöst", + "Description[en_GB]": "Display window thumbnails on the edge of the screen; activated with a keyboard shortcut", + "Description[eo]": "Montri fenestrajn bildetojn ĉe la rando de la ekrano; aktivigita per klavara ŝparvojo", + "Description[es]": "Mostrar miniaturas de las ventanas en el borde de la pantalla; se activa con un atajo de teclado", + "Description[eu]": "Bistaratu leihoen koadro txikiak pantailaren ertzean; teklatuko lasterbide batekin aktibatua", + "Description[fi]": "Näytä näytön reunalla ikkunoiden pienoiskuvat; käynnistyy pikanäppäimellä", + "Description[fr]": "Afficher les vignettes de fenêtres sur le bord de l'écran. Activé grâce à un raccourci clavier.", + "Description[gl]": "Amosa miniaturas das xanelas na beira da pantalla; actívase cun atallo de teclado.", + "Description[he]": "הצגת תמונות חלונות ממוזערות בקצה המסך, מופעל דרך צירוף מקשי מקלדת", + "Description[hu]": "A képernyő szélénél megjelennek az ablakok kicsinyített képei; gyorsbillentyűvel aktiválható", + "Description[ia]": "Monstra miniaturas de fenestra al margine del schermo; activate con un via breve de claviero", + "Description[is]": "Sýna gluggasmámyndir við brún skjásins; virkjað með flýtilykli", + "Description[it]": "Visualizza anteprime delle finestre sul bordo delle schermo; attivato con una scorciatoia da tastiera", + "Description[ja]": "スクリーンの角にウィンドウのサムネイルを表示します。ショートカットで起動します", + "Description[ka]": "ფანჯრის მინიატურების ჩვენება ეკრანის კიდეზე. აქტიურდება კლავიატურის მალსახმობით", + "Description[ko]": "화면 모서리에 창의 섬네일을 표시, 키보드 단축키로 활성화", + "Description[lt]": "Rodyti ekrano krašte pasirinkto lango miniatiūrą; aktyvinama naudojant sparčiuosius klavišus", + "Description[lv]": "Ekrāna malā parāda logu sīktēlus; ieslēdz ar tastatūras saīsni", + "Description[nb]": "Vis miniatyrbilder for vinduer på kanten av skjermen – slått av/på med hurtigtast", + "Description[nl]": "Toont vensterminiaturen langs de rand van het scherm; geactiveerd met een sneltoets", + "Description[nn]": "Vis miniatyrbilete av vindauge ved kanten av skjermen (vert starta med snøggtast)", + "Description[pl]": "Wyświetla miniatury okien na krawędzi ekranu; uruchamiane skrótem klawiszowym", + "Description[pt_BR]": "Mostra as miniaturas das janelas no canto da tela; ativado com um atalho de teclado", + "Description[ro]": "Afișează miniaturile ferestrelor la marginea ecranului; activat cu o scurtătură de taste", + "Description[ru]": "Отображение миниатюр окон на краю экрана; активируется нажатием комбинации клавиш", + "Description[sa]": "स्क्रीनस्य धारायाम् विण्डो लघुचित्रं प्रदर्शयन्तु; कीबोर्ड-शॉर्टकट्-सहितं सक्रियम्", + "Description[sk]": "Zobrazí náhľady okien na okraji obrazovky", + "Description[sl]": "Prikažite predogledne sličice oken na robu zaslona; aktivirano z bližnjico tipkovnice", + "Description[sv]": "Visa miniatyrbilder av fönster vid skärmkanten, aktiveras med en snabbtangent", + "Description[tr]": "Pencere küçük görsellerini ekranın kenarında görüntüle; klavye kısayoluyla etkinleştirilir", + "Description[uk]": "Показ мініатюр вікон при наведенні на край екрана; активується клавіатурним скороченням", + "Description[zh_CN]": "在屏幕边缘显示窗口缩略图;使用键盘快捷键激活", + "Description[zh_TW]": "在螢幕邊緣顯示視窗縮圖——需要用鍵盤快捷鍵觸發", + "EnabledByDefault": false, + "License": "GPL", + "Name": "Thumbnail Aside", + "Name[ar]": "مصغرات على الجانب", + "Name[az]": "Yandakı miniatür pəncərəsi", + "Name[be]": "Бакавыя мініяцюры", + "Name[bg]": "Миниатюра встрани", + "Name[ca@valencia]": "Miniatures al costat", + "Name[ca]": "Miniatures al costat", + "Name[cs]": "Postranní miniatura", + "Name[da]": "Thumbnail til siden", + "Name[de]": "Seitliche Vorschaubilder", + "Name[en_GB]": "Thumbnail Aside", + "Name[eo]": "Bildeto Flanken", + "Name[es]": "Miniaturas a un lado", + "Name[et]": "Pisipildid kõrval", + "Name[eu]": "Koadro txikiak alboan", + "Name[fi]": "Esikatselukuva vieressä", + "Name[fr]": "Vignettes sur le côté", + "Name[gl]": "Miniatura a un lado", + "Name[he]": "תמונה ממוזערת בצד", + "Name[hu]": "Ablakbetekintő oldal", + "Name[ia]": "Miniatura a parte", + "Name[id]": "Thumbnail Aside", + "Name[is]": "Smámyndir til hliðar", + "Name[it]": "Miniature a fianco", + "Name[ja]": "サムネイルをわきに", + "Name[ka]": "მინიატურა გვერდითვე", + "Name[ko]": "옆쪽에 섬네일", + "Name[lt]": "Šoninė miniatiūra", + "Name[lv]": "Sīktēli malā", + "Name[nb]": "Miniatyrbilde ved skjermkanten", + "Name[nl]": "Miniatuur ernaast", + "Name[nn]": "Miniatyrbilete ved skjermkanten", + "Name[pl]": "Miniatura z boku", + "Name[pt]": "Miniaturas Lado-a-Lado", + "Name[pt_BR]": "Miniatura de lado", + "Name[ro]": "Miniatură lateral", + "Name[ru]": "Миниатюры окон сбоку", + "Name[sa]": "लघुचित्र एकपार्श्वे", + "Name[sk]": "Bočný náhľad", + "Name[sl]": "Predogledno sličico na stran", + "Name[sv]": "Miniatyrbild vid sidan om", + "Name[ta]": "ஓரத்தில் சிறுபடம்", + "Name[tr]": "Küçük Görsel Kenarda", + "Name[uk]": "Мініатюри збоку", + "Name[vi]": "Hình nhỏ ở cạnh", + "Name[zh_CN]": "缩略图置边", + "Name[zh_TW]": "在一旁的縮圖" + }, + "X-KDE-ConfigModule": "kwin_thumbnailaside_config" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside.cpp b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside.cpp new file mode 100644 index 0000000000..fb21971414 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside.cpp @@ -0,0 +1,209 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "thumbnailaside.h" +#include "core/renderviewport.h" +#include "effect/effecthandler.h" +// KConfigSkeleton +#include "thumbnailasideconfig.h" + +#include +#include + +#include +#include + +namespace KWin +{ + +ThumbnailAsideEffect::ThumbnailAsideEffect() +{ + ThumbnailAsideConfig::instance(effects->config()); + QAction *a = new QAction(this); + a->setObjectName(QStringLiteral("ToggleCurrentThumbnail")); + a->setText(i18n("Toggle Thumbnail for Current Window")); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << (Qt::META | Qt::CTRL | Qt::Key_T)); + KGlobalAccel::self()->setShortcut(a, QList() << (Qt::META | Qt::CTRL | Qt::Key_T)); + connect(a, &QAction::triggered, this, &ThumbnailAsideEffect::toggleCurrentThumbnail); + + connect(effects, &EffectsHandler::windowAdded, this, &ThumbnailAsideEffect::slotWindowAdded); + connect(effects, &EffectsHandler::windowClosed, this, &ThumbnailAsideEffect::slotWindowClosed); + connect(effects, &EffectsHandler::screenLockingChanged, this, &ThumbnailAsideEffect::repaintAll); + + const auto windows = effects->stackingOrder(); + for (EffectWindow *window : windows) { + slotWindowAdded(window); + } + + reconfigure(ReconfigureAll); +} + +void ThumbnailAsideEffect::reconfigure(ReconfigureFlags) +{ + ThumbnailAsideConfig::self()->read(); + maxwidth = ThumbnailAsideConfig::maxWidth(); + spacing = ThumbnailAsideConfig::spacing(); + opacity = ThumbnailAsideConfig::opacity() / 100.0; + screen = ThumbnailAsideConfig::screen(); // Xinerama screen TODO add gui option + arrange(); +} + +void ThumbnailAsideEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) +{ + painted = Region(); + effects->paintScreen(renderTarget, viewport, mask, deviceRegion, screen); + + for (const Data &d : std::as_const(windows)) { + if (painted.intersects(viewport.mapToDeviceCoordinatesAligned(d.rect))) { + WindowPaintData data; + data.multiplyOpacity(opacity); + Rect region; + setPositionTransformations(data, region, d.window, d.rect, Qt::KeepAspectRatio); + effects->drawWindow(renderTarget, viewport, d.window, PAINT_WINDOW_OPAQUE | PAINT_WINDOW_TRANSLUCENT | PAINT_WINDOW_TRANSFORMED, + viewport.mapToDeviceCoordinatesAligned(region), data); + } + } +} + +void ThumbnailAsideEffect::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceGeometry, WindowPaintData &data) +{ + effects->paintWindow(renderTarget, viewport, w, mask, deviceGeometry, data); + painted += deviceGeometry; +} + +void ThumbnailAsideEffect::slotWindowDamaged(EffectWindow *w) +{ + for (const Data &d : std::as_const(windows)) { + if (d.window == w) { + effects->addRepaint(d.rect); + } + } +} + +void ThumbnailAsideEffect::slotWindowFrameGeometryChanged(EffectWindow *w, const QRectF &old) +{ + for (const Data &d : std::as_const(windows)) { + if (d.window == w) { + if (w->size() == old.size()) { + effects->addRepaint(d.rect); + } else { + arrange(); + } + return; + } + } +} + +void ThumbnailAsideEffect::slotWindowAdded(EffectWindow *w) +{ + connect(w, &EffectWindow::windowFrameGeometryChanged, this, &ThumbnailAsideEffect::slotWindowFrameGeometryChanged); + connect(w, &EffectWindow::windowDamaged, this, &ThumbnailAsideEffect::slotWindowDamaged); +} + +void ThumbnailAsideEffect::slotWindowClosed(EffectWindow *w) +{ + removeThumbnail(w); +} + +void ThumbnailAsideEffect::toggleCurrentThumbnail() +{ + EffectWindow *active = effects->activeWindow(); + if (active == nullptr) { + return; + } + if (windows.contains(active)) { + removeThumbnail(active); + } else { + addThumbnail(active); + } +} + +void ThumbnailAsideEffect::addThumbnail(EffectWindow *w) +{ + repaintAll(); // repaint old areas + Data d; + d.window = w; + d.index = windows.count(); + windows[w] = d; + arrange(); +} + +void ThumbnailAsideEffect::removeThumbnail(EffectWindow *w) +{ + if (!windows.contains(w)) { + return; + } + repaintAll(); // repaint old areas + int index = windows[w].index; + windows.remove(w); + for (QHash::Iterator it = windows.begin(); + it != windows.end(); + ++it) { + Data &d = *it; + if (d.index > index) { + --d.index; + } + } + arrange(); +} + +void ThumbnailAsideEffect::arrange() +{ + if (windows.size() == 0) { + return; + } + int height = 0; + QList pos(windows.size()); + qreal mwidth = 0; + for (const Data &d : std::as_const(windows)) { + height += d.window->height(); + mwidth = std::max(mwidth, d.window->width()); + pos[d.index] = d.window->height(); + } + LogicalOutput *effectiveScreen = effects->findScreen(screen); + if (!effectiveScreen) { + effectiveScreen = effects->activeScreen(); + } + QRectF area = effects->clientArea(MaximizeArea, effectiveScreen, effects->currentDesktop()); + double scale = area.height() / double(height); + scale = std::min(scale, maxwidth / double(mwidth)); // don't be wider than maxwidth pixels + int add = 0; + for (int i = 0; + i < windows.size(); + ++i) { + pos[i] = int(pos[i] * scale); + pos[i] += spacing + add; // compute offset of each item + add = pos[i]; + } + for (QHash::Iterator it = windows.begin(); + it != windows.end(); + ++it) { + Data &d = *it; + int width = int(d.window->width() * scale); + d.rect = QRect(area.right() - width, area.bottom() - pos[d.index], width, int(d.window->height() * scale)); + } + repaintAll(); +} + +void ThumbnailAsideEffect::repaintAll() +{ + for (const Data &d : std::as_const(windows)) { + effects->addRepaint(d.rect); + } +} + +bool ThumbnailAsideEffect::isActive() const +{ + return !windows.isEmpty() && !effects->isScreenLocked(); +} + +} // namespace + +#include "moc_thumbnailaside.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside.h b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside.h new file mode 100644 index 0000000000..624d36da7a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside.h @@ -0,0 +1,85 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Lubos Lunak + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* + + Testing of painting a window more than once. + +*/ + +#pragma once + +#include "effect/effect.h" + +#include + +namespace KWin +{ + +class ThumbnailAsideEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(int maxWidth READ configuredMaxWidth) + Q_PROPERTY(int spacing READ configuredSpacing) + Q_PROPERTY(qreal opacity READ configuredOpacity) + Q_PROPERTY(int screen READ configuredScreen) +public: + ThumbnailAsideEffect(); + void reconfigure(ReconfigureFlags) override; + void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) override; + void paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) override; + + // for properties + int configuredMaxWidth() const + { + return maxwidth; + } + int configuredSpacing() const + { + return spacing; + } + qreal configuredOpacity() const + { + return opacity; + } + int configuredScreen() const + { + return screen; + } + bool isActive() const override; + +private Q_SLOTS: + void toggleCurrentThumbnail(); + void slotWindowAdded(KWin::EffectWindow *w); + void slotWindowClosed(KWin::EffectWindow *w); + void slotWindowFrameGeometryChanged(KWin::EffectWindow *w, const QRectF &old); + void slotWindowDamaged(KWin::EffectWindow *w); + void repaintAll(); + +private: + void addThumbnail(EffectWindow *w); + void removeThumbnail(EffectWindow *w); + void arrange(); + struct Data + { + EffectWindow *window; // the same like the key in the hash (makes code simpler) + int index; + Rect rect; + }; + QHash windows; + int maxwidth; + int spacing; + double opacity; + int screen; + Region painted; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside.kcfg b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside.kcfg new file mode 100644 index 0000000000..7771e416f3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside.kcfg @@ -0,0 +1,21 @@ + + + + + + 200 + + + 10 + + + 50 + + + -1 + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside_config.cpp b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside_config.cpp new file mode 100644 index 0000000000..8ab88f5e08 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside_config.cpp @@ -0,0 +1,72 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "thumbnailaside_config.h" + +#include "config-kwin.h" + +// KConfigSkeleton +#include "thumbnailasideconfig.h" +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include + +K_PLUGIN_CLASS(KWin::ThumbnailAsideEffectConfig) + +namespace KWin +{ +ThumbnailAsideEffectConfig::ThumbnailAsideEffectConfig(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) +{ + m_ui.setupUi(widget()); + + connect(m_ui.editor, &KShortcutsEditor::keyChange, this, &KCModule::markAsChanged); + + ThumbnailAsideConfig::instance(KWIN_CONFIG); + addConfig(ThumbnailAsideConfig::self(), widget()); + + // Shortcut config. The shortcut belongs to the component "kwin"! + m_actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + + m_actionCollection->setComponentDisplayName(i18n("KWin")); + m_actionCollection->setConfigGroup(QStringLiteral("ThumbnailAside")); + m_actionCollection->setConfigGlobal(true); + + QAction *a = m_actionCollection->addAction(QStringLiteral("ToggleCurrentThumbnail")); + a->setText(i18n("Toggle Thumbnail for Current Window")); + a->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << (Qt::META | Qt::CTRL | Qt::Key_T)); + KGlobalAccel::self()->setShortcut(a, QList() << (Qt::META | Qt::CTRL | Qt::Key_T)); + + m_ui.editor->addCollection(m_actionCollection); +} + +void ThumbnailAsideEffectConfig::save() +{ + KCModule::save(); + m_ui.editor->save(); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("thumbnailaside")); +} + +} // namespace + +#include "thumbnailaside_config.moc" + +#include "moc_thumbnailaside_config.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside_config.h b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside_config.h new file mode 100644 index 0000000000..481d873693 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside_config.h @@ -0,0 +1,33 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Christian Nitschkowski + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "ui_thumbnailaside_config.h" + +class KActionCollection; + +namespace KWin +{ +class ThumbnailAsideEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit ThumbnailAsideEffectConfig(QObject *parent, const KPluginMetaData &data); + + void save() override; + +private: + Ui::ThumbnailAsideEffectConfigForm m_ui; + KActionCollection *m_actionCollection; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside_config.ui b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside_config.ui new file mode 100644 index 0000000000..439805c135 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailaside_config.ui @@ -0,0 +1,138 @@ + + + KWin::ThumbnailAsideEffectConfigForm + + + + 0 + 0 + 400 + 300 + + + + + + + Appearance + + + + + + Maximum &width: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_MaxWidth + + + + + + + &Spacing: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Spacing + + + + + + + + 0 + 0 + + + + pixels + + + 30 + + + 10 + + + + + + + &Opacity: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Opacity + + + + + + + + 0 + 0 + + + + % + + + 100 + + + 50 + + + + + + + + 0 + 0 + + + + pixels + + + 9999 + + + 200 + + + + + + + + + + KShortcutsEditor::GlobalAction + + + + + + + + KShortcutsEditor + QWidget +
KShortcutsEditor
+ 1 +
+
+ + +
diff --git a/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailasideconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailasideconfig.kcfgc new file mode 100644 index 0000000000..21e6ca75c6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/thumbnailaside/thumbnailasideconfig.kcfgc @@ -0,0 +1,5 @@ +File=thumbnailaside.kcfg +ClassName=ThumbnailAsideConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/local/recipes/kde/kwin/source/src/plugins/tileseditor/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/tileseditor/CMakeLists.txt new file mode 100644 index 0000000000..39d19e7d4d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/tileseditor/CMakeLists.txt @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: 2022 Marco Martin +# +# SPDX-License-Identifier: BSD-3-Clause + +if (KWIN_BUILD_KCMS) + add_subdirectory(kcm) +endif() + +set(tileseditor_SOURCES + main.cpp + tileseditoreffect.cpp +) + +kwin_add_builtin_effect(tileseditor ${tileseditor_SOURCES}) + +ecm_add_qml_module(tileseditor + URI org.kde.kwin.tileseditor + RESOURCES qml/layouts.svg + QML_FILES + qml/Main.qml + qml/ResizeCorner.qml + qml/ResizeHandle.qml + qml/TileDelegate.qml + QT_NO_PLUGIN +) + +target_link_libraries(tileseditor PRIVATE + kwin + + KF6::ConfigGui + KF6::GlobalAccel + KF6::I18n + + Qt::Quick +) diff --git a/local/recipes/kde/kwin/source/src/plugins/tileseditor/kcm/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/tileseditor/kcm/CMakeLists.txt new file mode 100644 index 0000000000..bfbebdf7fe --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/tileseditor/kcm/CMakeLists.txt @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2022 Marco Martin +# +# SPDX-License-Identifier: BSD-3-Clause + +set(kwin_tileseditor_config_SOURCES tileseditoreffectkcm.cpp) +ki18n_wrap_ui(kwin_tileseditor_config_SOURCES tileseditoreffectkcm.ui) + +kwin_add_effect_config(kwin_tileseditor_config ${kwin_tileseditor_config_SOURCES}) +target_link_libraries(kwin_tileseditor_config + KF6::KCMUtils + KF6::CoreAddons + KF6::GlobalAccel + KF6::I18n + KF6::XmlGui + KWinEffectsInterface +) diff --git a/local/recipes/kde/kwin/source/src/plugins/tileseditor/kcm/tileseditoreffectkcm.cpp b/local/recipes/kde/kwin/source/src/plugins/tileseditor/kcm/tileseditoreffectkcm.cpp new file mode 100644 index 0000000000..91974b8eaf --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/tileseditor/kcm/tileseditoreffectkcm.cpp @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2022 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "tileseditoreffectkcm.h" + +#include "config-kwin.h" + +#include + +#include +#include +#include +#include + +#include + +K_PLUGIN_CLASS(KWin::TilesEditorEffectConfig) + +namespace KWin +{ + +TilesEditorEffectConfig::TilesEditorEffectConfig(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) +{ + ui.setupUi(widget()); + + auto actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + + actionCollection->setComponentDisplayName(i18n("KWin")); + actionCollection->setConfigGroup(QStringLiteral("tileseditor")); + actionCollection->setConfigGlobal(true); + + const QKeySequence defaultToggleShortcut = Qt::META | Qt::Key_T; + QAction *toggleAction = actionCollection->addAction(QStringLiteral("Edit Tiles")); + toggleAction->setText(i18n("Toggle Tiles Editor")); + toggleAction->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(toggleAction, {defaultToggleShortcut}); + KGlobalAccel::self()->setShortcut(toggleAction, {defaultToggleShortcut}); + + ui.shortcutsEditor->addCollection(actionCollection); + connect(ui.shortcutsEditor, &KShortcutsEditor::keyChange, this, &KCModule::markAsChanged); +} + +void TilesEditorEffectConfig::save() +{ + KCModule::save(); + ui.shortcutsEditor->save(); + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("tileseditor")); +} + +void TilesEditorEffectConfig::defaults() +{ + ui.shortcutsEditor->allDefault(); + KCModule::defaults(); +} + +} // namespace KWin + +#include "tileseditoreffectkcm.moc" + +#include "moc_tileseditoreffectkcm.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/tileseditor/kcm/tileseditoreffectkcm.h b/local/recipes/kde/kwin/source/src/plugins/tileseditor/kcm/tileseditoreffectkcm.h new file mode 100644 index 0000000000..fea0dccbe9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/tileseditor/kcm/tileseditoreffectkcm.h @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2022 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "ui_tileseditoreffectkcm.h" + +namespace KWin +{ + +class TilesEditorEffectConfig : public KCModule +{ + Q_OBJECT + +public: + explicit TilesEditorEffectConfig(QObject *parent, const KPluginMetaData &data); + +public Q_SLOTS: + void save() override; + void defaults() override; + +private: + ::Ui::TilesEditorEffectConfig ui; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/tileseditor/kcm/tileseditoreffectkcm.ui b/local/recipes/kde/kwin/source/src/plugins/tileseditor/kcm/tileseditoreffectkcm.ui new file mode 100644 index 0000000000..d9eae24b34 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/tileseditor/kcm/tileseditoreffectkcm.ui @@ -0,0 +1,36 @@ + + + TilesEditorEffectConfig + + + + 0 + 0 + 455 + 177 + + + + + + + + 0 + 0 + + + + + + + + + KShortcutsEditor + QWidget +
kshortcutseditor.h
+ 1 +
+
+ + +
diff --git a/local/recipes/kde/kwin/source/src/plugins/tileseditor/main.cpp b/local/recipes/kde/kwin/source/src/plugins/tileseditor/main.cpp new file mode 100644 index 0000000000..a96ad1e442 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/tileseditor/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2022 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "tileseditoreffect.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(TilesEditorEffect, + "metadata.json.stripped", + return TilesEditorEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/tileseditor/metadata.json b/local/recipes/kde/kwin/source/src/plugins/tileseditor/metadata.json new file mode 100644 index 0000000000..e79e7ad269 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/tileseditor/metadata.json @@ -0,0 +1,97 @@ +{ + "KPlugin": { + "Category": "Window Management", + "Description": "Editor for the tiling areas", + "Description[ar]": "محرر لترتيب المساحات", + "Description[be]": "Рэдактар абласцей кампанавання", + "Description[bg]": "Редактиране на зоните на плочковидно разпределяне", + "Description[ca@valencia]": "Editor per a les àrees de mosaic", + "Description[ca]": "Editor per a les àrees de mosaic", + "Description[cs]": "Editor pro oblasti dlaždicování", + "Description[da]": "Editor for tiling-områder", + "Description[de]": "Editor für die kachelartige Anordnung von Fenstern", + "Description[en_GB]": "Editor for the tiling areas", + "Description[eo]": "Redaktilo por la kahelaroj", + "Description[es]": "Editor para las áreas de mosaico", + "Description[eu]": "Eremu lauzatuen editorea", + "Description[fi]": "Ruutupaneelien muokkain", + "Description[fr]": "Éditeur pour les zones de recouvrement", + "Description[gl]": "Editor das zonas de teselas.", + "Description[he]": "עורך לאזורים המרוצפים", + "Description[hu]": "Szerkesztő a mozaik területekhez", + "Description[ia]": "Editor per le areas de tiling (tegular)", + "Description[is]": "Ritill fyrir reitaröðuð svæði", + "Description[it]": "Editor per le aree di affiancamento", + "Description[ja]": "タイリング機能を設定するためのエディタ", + "Description[ka]": "მოზაიკის რედაქტორი", + "Description[ko]": "타일링 영역 편집기", + "Description[lt]": "Išklojimo sričių redaktorius", + "Description[lv]": "Flīzēšanas zonu redaktors", + "Description[nb]": "Rediger flisområde", + "Description[nl]": "Bewerker voor de gebieden waar tegels worden geplaatst", + "Description[nn]": "Rediger flisområde", + "Description[pl]": "Edytor dla obszarów kafelkowanych", + "Description[pt]": "Editor das áreas com padrões", + "Description[pt_BR]": "Editor para as áreas de janelas lado a lado", + "Description[ro]": "Redactor pentru zonele mozaicului", + "Description[ru]": "Редактирование областей окон приложений в мозаичном режиме рабочего стола", + "Description[sa]": "टाइलिंग् क्षेत्राणां कृते सम्पादकः", + "Description[sk]": "Editor pre oblasti dlaždíc", + "Description[sl]": "Urejevalnik za področja s sličicami", + "Description[sv]": "Editor för rutområdena", + "Description[tr]": "Döşeme alanları için düzenleyicisi", + "Description[uk]": "Редактор для мозаїчних областей", + "Description[vi]": "Trình biên tập các vùng lát", + "Description[zh_CN]": "磁贴区域编辑器", + "Description[zh_TW]": "平鋪方塊區域的編輯器", + "EnabledByDefault": true, + "License": "GPL", + "Name": "Tiling Editor", + "Name[ar]": "محرر الترتيب", + "Name[be]": "Рэдактар кампанавання", + "Name[bg]": "Редактор на разпределяне", + "Name[ca@valencia]": "Editor de mosaic", + "Name[ca]": "Editor de mosaic", + "Name[cs]": "Editor dlaždic", + "Name[da]": "Tiling editor", + "Name[de]": "Tiling-Editor", + "Name[en_GB]": "Tiling Editor", + "Name[eo]": "Kahela Redaktilo", + "Name[es]": "Editor de mosaico", + "Name[eu]": "Lauzadura editorea", + "Name[fi]": "Paneelimuokkain", + "Name[fr]": "Éditeur pour la gestion des recouvrements", + "Name[gl]": "Editor de teselas", + "Name[he]": "עורך ריצוף", + "Name[hu]": "Mozaikszerkesztő", + "Name[ia]": "Editor de tiling (tegular)", + "Name[is]": "Reitaritill", + "Name[it]": "Editor di affiancamento", + "Name[ja]": "タイリング設定", + "Name[ka]": "მოზაიკის რედაქტორი", + "Name[ko]": "타일 편집기", + "Name[lt]": "Išklojimo redaktorius", + "Name[lv]": "Flīzēšanas redaktors", + "Name[nb]": "Rediger flisutforming", + "Name[nl]": "Bewerker voor tegels plaatsen", + "Name[nn]": "Rediger flisutforming", + "Name[pl]": "Edytor kafelków", + "Name[pt]": "Editor de Padrões", + "Name[pt_BR]": "Editor de janelas lado a lado", + "Name[ro]": "Redactor de mozaic", + "Name[ru]": "Редактор областей мозаичного режима", + "Name[sa]": "टाइलिंग सम्पादक", + "Name[sk]": "Editor dlaždíc", + "Name[sl]": "Urejevalnik sličic", + "Name[sv]": "Ruteditor", + "Name[tr]": "Döşeme Düzenleyicisi", + "Name[uk]": "Редактор мозаїк", + "Name[vi]": "Trình biên tập lát", + "Name[zh_CN]": "磁贴编辑器", + "Name[zh_TW]": "平鋪方塊編輯器" + }, + "X-KDE-ConfigModule": "kwin_tileseditor_config", + "org.kde.kwin.effect": { + "internal": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/tileseditor/qml/ResizeCorner.qml b/local/recipes/kde/kwin/source/src/plugins/tileseditor/qml/ResizeCorner.qml new file mode 100644 index 0000000000..f2d7e9efa7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/tileseditor/qml/ResizeCorner.qml @@ -0,0 +1,91 @@ +/* + SPDX-FileCopyrightText: 2022 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick +import Qt5Compat.GraphicalEffects +import QtQuick.Layouts +import org.kde.kwin as KWinComponents +import org.kde.kwin.private.effects +import org.kde.kirigami as Kirigami + +Rectangle { + id: handle + + required property QtObject tile + + required property int corner + + z: 2 + + implicitWidth: Kirigami.Units.gridUnit + implicitHeight: Kirigami.Units.gridUnit + + radius: Kirigami.Units.cornerRadius + color: Kirigami.Theme.highlightColor + opacity: hoverHandler.hovered || dragHandler.active ? 0.4 : 0 + visible: tile && + (tile.layoutDirection === KWinComponents.Tile.Floating || + (corner === Qt.TopLeftCorner || corner === Qt.BottomLeftCorner ? tile.relativeGeometry.x > 0 : true) && + (corner === Qt.TopLeftCorner || corner === Qt.TopRightCorner ? tile.relativeGeometry.y > 0 : true) && + (corner === Qt.TopRightCorner || corner === Qt.BottomRightCorner ? tile.relativeGeometry.x + tile.relativeGeometry.width < 1 : true) && + (corner === Qt.BottomLeftCorner || corner === Qt.BottomRightCorner ? tile.relativeGeometry.y + tile.relativeGeometry.height < 1 : true)) + + HoverHandler { + id: hoverHandler + grabPermissions: PointerHandler.TakeOverForbidden | PointerHandler.CanTakeOverFromAnything + cursorShape: { + switch (handle.corner) { + case Qt.TopLeftCorner: + return Qt.SizeFDiagCursor; + case Qt.TopRightCorner: + return Qt.SizeBDiagCursor; + case Qt.BottomLeftCorner: + return Qt.SizeBDiagCursor; + case Qt.BottomRightCorner: + return Qt.SizeFDiagCursor; + } + } + } + + DragHandler { + id: dragHandler + target: null + property point oldPoint: Qt.point(0, 0) + property point dragPoint: centroid.scenePosition + dragThreshold: 0 + grabPermissions: PointerHandler.TakeOverForbidden | PointerHandler.CanTakeOverFromAnything + onActiveChanged: { + if (active) { + oldPoint = dragPoint; + } + } + onDragPointChanged: { + if (!active) { + return; + } + switch (handle.corner) { + case Qt.TopLeftCorner: + tile.resizeByPixels(dragPoint.x - oldPoint.x, Qt.LeftEdge); + tile.resizeByPixels(dragPoint.y - oldPoint.y, Qt.TopEdge); + break; + case Qt.TopRightCorner: + tile.resizeByPixels(dragPoint.x - oldPoint.x, Qt.RightEdge); + tile.resizeByPixels(dragPoint.y - oldPoint.y, Qt.TopEdge); + break; + case Qt.BottomLeftCorner: + tile.resizeByPixels(dragPoint.x - oldPoint.x, Qt.LeftEdge); + tile.resizeByPixels(dragPoint.y - oldPoint.y, Qt.BottomEdge); + break; + case Qt.BottomRightCorner: + tile.resizeByPixels(dragPoint.x - oldPoint.x, Qt.RightEdge); + tile.resizeByPixels(dragPoint.y - oldPoint.y, Qt.BottomEdge); + break; + } + + oldPoint = dragPoint; + } + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/tileseditor/qml/ResizeHandle.qml b/local/recipes/kde/kwin/source/src/plugins/tileseditor/qml/ResizeHandle.qml new file mode 100644 index 0000000000..199bbdd1d1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/tileseditor/qml/ResizeHandle.qml @@ -0,0 +1,67 @@ +/* + SPDX-FileCopyrightText: 2022 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick +import org.kde.kwin as KWinComponents +import org.kde.kwin.private.effects +import org.kde.kirigami as Kirigami + +Rectangle { + id: handle + + required property QtObject tile + + required property int edge + readonly property int orientation: edge === Qt.LeftEdge || edge === Qt.RightEdge ? Qt.Horizontal : Qt.Vertical + readonly property bool valid: tile !== null && tile.parent !== null && (tile.layoutDirection === KWinComponents.Tile.Floating + || (orientation === Qt.Horizontal && tile.parent.layoutDirection === KWinComponents.Tile.Horizontal) + || (orientation === Qt.Vertical && tile.parent.layoutDirection === KWinComponents.Tile.Vertical)) + + z: 2 + + implicitWidth: Kirigami.Units.smallSpacing * 2 + implicitHeight: Kirigami.Units.smallSpacing * 2 + + radius: Kirigami.Units.cornerRadius + color: Kirigami.Theme.highlightColor + opacity: hoverHandler.hovered || dragHandler.active ? 0.4 : 0 + visible: valid && (tile.layoutDirection === KWinComponents.Tile.Floating || tile.positionInLayout > 0) + + HoverHandler { + id: hoverHandler + cursorShape: orientation == Qt.Horizontal ? Qt.SizeHorCursor : Qt.SizeVerCursor + } + + DragHandler { + id: dragHandler + target: null + property point oldPoint: Qt.point(0, 0) + property point dragPoint: centroid.scenePosition + dragThreshold: 0 + onActiveChanged: { + if (active) { + oldPoint = dragPoint; + } + } + onDragPointChanged: { + if (!active) { + return; + } + switch (handle.edge) { + case Qt.LeftEdge: + case Qt.RightEdge: + tile.resizeByPixels(dragPoint.x - oldPoint.x, handle.edge); + break; + case Qt.TopEdge: + case Qt.BottomEdge: + tile.resizeByPixels(dragPoint.y - oldPoint.y, handle.edge); + break; + } + + oldPoint = dragPoint; + } + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/tileseditor/qml/TileDelegate.qml b/local/recipes/kde/kwin/source/src/plugins/tileseditor/qml/TileDelegate.qml new file mode 100644 index 0000000000..01913579ec --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/tileseditor/qml/TileDelegate.qml @@ -0,0 +1,214 @@ +/* + SPDX-FileCopyrightText: 2022 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick +import QtQuick.Layouts +import org.kde.kwin as KWinComponents +import org.kde.kwin.private.effects +import org.kde.kirigami as Kirigami +import org.kde.plasma.components as PlasmaComponents + +Item { + id: delegate + required property KWinComponents.Tile tile + + x: Math.round(tile.absoluteGeometryInScreen.x) + y: Math.round(tile.absoluteGeometryInScreen.y) + z: focus ? 1000 : 0 + //onZChanged: print(delegate + " "+z) + width: Math.round(tile.absoluteGeometryInScreen.width) + height: Math.round(tile.absoluteGeometryInScreen.height) + Connections { + target: tile + function onTilesChanged() { + if (tile.layoutDirection === KWinComponents.Tile.Floating && tile.tiles.length === 0) { + tile.layoutDirection = tile.parent.layoutDirection; + } + } + } + ResizeHandle { + anchors { + horizontalCenter: parent.left + top: parent.top + bottom: parent.bottom + } + tile: delegate.tile + edge: Qt.LeftEdge + } + ResizeHandle { + anchors { + verticalCenter: parent.top + left: parent.left + right: parent.right + } + tile: delegate.tile + edge: Qt.TopEdge + } + ResizeHandle { + anchors { + horizontalCenter: parent.right + top: parent.top + bottom: parent.bottom + } + tile: delegate.tile + edge: Qt.RightEdge + visible: tile.layoutDirection === KWinComponents.Tile.Floating + } + ResizeHandle { + anchors { + verticalCenter: parent.bottom + left: parent.left + right: parent.right + } + tile: delegate.tile + edge: Qt.BottomEdge + visible: tile.layoutDirection === KWinComponents.Tile.Floating + } + + ResizeCorner { + anchors { + top: parent.top + left: parent.left + } + tile: delegate.tile + corner: Qt.TopLeftCorner + } + ResizeCorner { + anchors { + top: parent.top + right: parent.right + } + tile: delegate.tile + corner: Qt.TopRightCorner + } + ResizeCorner { + anchors { + left: parent.left + bottom: parent.bottom + } + tile: delegate.tile + corner: Qt.BottomLeftCorner + } + ResizeCorner { + anchors { + right: parent.right + bottom: parent.bottom + } + tile: delegate.tile + corner: Qt.BottomRightCorner + } + + Item { + anchors.fill: parent + + Rectangle { + anchors { + fill: parent + margins: Kirigami.Units.smallSpacing + } + visible: tile.tiles.length === 0 + radius: Kirigami.Units.cornerRadius + opacity: tile.layoutDirection === KWinComponents.Tile.Floating ? 0.6 : 0.3 + color: tile.layoutDirection === KWinComponents.Tile.Floating ? Kirigami.Theme.backgroundColor : "transparent" + border.color: Kirigami.Theme.textColor + Rectangle { + anchors { + fill: parent + margins: 1 + } + radius: Kirigami.Units.cornerRadius + color: "transparent" + border.color: Kirigami.Theme.backgroundColor + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + propagateComposedEvents: true + property point lastPos + onClicked: { + delegate.focus = true; + if (tile.layoutDirection !== KWinComponents.Tile.Floating) { + mouse.accepted = false; + } + } + onPressed: { + lastPos = mapToItem(null, mouse.x, mouse.y); + } + onPositionChanged: { + if (!pressed || tile.layoutDirection !== KWinComponents.Tile.Floating) { + return; + } + let pos = mapToItem(null, mouse.x, mouse.y); + let deltaPoint = Qt.point(pos.x - lastPos.x, pos.y - lastPos.y); + deltaPoint.x = Math.max(deltaPoint.x, tile.parent.absoluteGeometryInScreen.x - tile.absoluteGeometryInScreen.x); + deltaPoint.x = Math.min(deltaPoint.x, (tile.parent.absoluteGeometryInScreen.x + tile.parent.absoluteGeometryInScreen.width) - (tile.absoluteGeometryInScreen.x + tile.absoluteGeometryInScreen.width)); + + deltaPoint.y = Math.max(deltaPoint.y, tile.parent.absoluteGeometryInScreen.y - tile.absoluteGeometryInScreen.y); + deltaPoint.y = Math.min(deltaPoint.y, (tile.parent.absoluteGeometryInScreen.y + tile.parent.absoluteGeometryInScreen.height) - (tile.absoluteGeometryInScreen.y + tile.absoluteGeometryInScreen.height)); + + tile.moveByPixels(deltaPoint); + lastPos = pos; + } + } + } + GridLayout { + anchors.centerIn: parent + visible: tile.tiles.length === 0 + readonly property bool compact: delegate.width < Kirigami.Units.gridUnit * 10 || delegate.height < splitButton.implicitHeight * visibleChildren.length + rowSpacing * visibleChildren.length + Kirigami.Units.gridUnit + rows: compact ? 1 : -1 + columns: compact ? -1 : 1 + PlasmaComponents.Button { + id: splitButton + Layout.fillWidth: true + icon.name: "view-split-left-right" + text: i18nd("kwin","Split Left/Right") + display: parent.compact ? PlasmaComponents.Button.IconOnly : PlasmaComponents.Button.TextBesideIcon + onClicked: tile.split(KWinComponents.Tile.Horizontal) + } + PlasmaComponents.Button { + Layout.fillWidth: true + icon.name: "view-split-top-bottom" + text: i18nd("kwin","Split Top/Bottom") + display: parent.compact ? PlasmaComponents.Button.IconOnly : PlasmaComponents.Button.TextBesideIcon + onClicked: tile.split(KWinComponents.Tile.Vertical) + } + PlasmaComponents.Button { + Layout.fillWidth: true + visible: tile.layoutDirection !== KWinComponents.Tile.Floating + icon.name: "window-duplicate" + text: i18nd("kwin","Add Floating Tile") + display: parent.compact ? PlasmaComponents.Button.IconOnly : PlasmaComponents.Button.TextBesideIcon + onClicked: tile.split(KWinComponents.Tile.Floating) + } + PlasmaComponents.Button { + id: deleteButton + visible: tile.canBeRemoved + Layout.fillWidth: true + icon.name: "edit-delete" + text: i18nd("kwin","Delete") + display: parent.compact ? PlasmaComponents.Button.IconOnly : PlasmaComponents.Button.TextBesideIcon + onClicked: { + tile.remove(); + } + } + } + } + TapHandler { + enabled: tile.layoutDirection === KWinComponents.Tile.Floating && tile.isLayout + onTapped: effect.deactivate(effect.animationDuration); + } + PlasmaComponents.Button { + anchors { + right: parent.right + bottom: parent.bottom + margins: Kirigami.Units.smallSpacing + } + visible: tile.layoutDirection === KWinComponents.Tile.Floating && tile.isLayout + icon.name: "window-duplicate" + text: i18nd("kwin","Add Floating Tile") + onClicked: tile.split(KWinComponents.Tile.Floating) + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/tileseditor/qml/layouts.svg b/local/recipes/kde/kwin/source/src/plugins/tileseditor/qml/layouts.svg new file mode 100644 index 0000000000..e1797dc316 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/tileseditor/qml/layouts.svg @@ -0,0 +1,133 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/tileseditor/tileseditoreffect.cpp b/local/recipes/kde/kwin/source/src/plugins/tileseditor/tileseditoreffect.cpp new file mode 100644 index 0000000000..8d24657f6e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/tileseditor/tileseditoreffect.cpp @@ -0,0 +1,120 @@ +/* + SPDX-FileCopyrightText: 2022 Marco Martin + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "tileseditoreffect.h" +#include "effect/effecthandler.h" + +#include +#include +#include + +#include +#include + +using namespace std::chrono_literals; + +namespace KWin +{ + +TilesEditorEffect::TilesEditorEffect() + : m_shutdownTimer(std::make_unique()) +{ + m_shutdownTimer->setSingleShot(true); + connect(m_shutdownTimer.get(), &QTimer::timeout, this, &TilesEditorEffect::realDeactivate); + connect(effects, &EffectsHandler::screenAboutToLock, this, &TilesEditorEffect::realDeactivate); + + const QKeySequence defaultToggleShortcut = Qt::META | Qt::Key_T; + m_toggleAction = std::make_unique(); + connect(m_toggleAction.get(), &QAction::triggered, this, &TilesEditorEffect::toggle); + m_toggleAction->setObjectName(QStringLiteral("Edit Tiles")); + m_toggleAction->setText(i18n("Toggle Tiles Editor")); + m_toggleAction->setAutoRepeat(false); + KGlobalAccel::self()->setDefaultShortcut(m_toggleAction.get(), {defaultToggleShortcut}); + KGlobalAccel::self()->setShortcut(m_toggleAction.get(), {defaultToggleShortcut}); + m_toggleShortcut = KGlobalAccel::self()->shortcut(m_toggleAction.get()); + + loadFromModule(QStringLiteral("org.kde.kwin/tileseditor"), QStringLiteral("Main")); +} + +TilesEditorEffect::~TilesEditorEffect() +{ +} + +QVariantMap TilesEditorEffect::initialProperties(LogicalOutput *screen) +{ + return QVariantMap{ + {QStringLiteral("effect"), QVariant::fromValue(this)}, + {QStringLiteral("targetScreen"), QVariant::fromValue(screen)}, + }; +} + +void TilesEditorEffect::reconfigure(ReconfigureFlags) +{ + setAnimationDuration(animationTime(200ms)); +} + +void TilesEditorEffect::toggle() +{ + if (!isRunning()) { + activate(); + } else { + deactivate(0); + } +} + +void TilesEditorEffect::activate() +{ + setRunning(true); +} + +void TilesEditorEffect::deactivate(int timeout) +{ + const auto screens = effects->screens(); + for (const auto screen : screens) { + if (QuickSceneView *view = viewForScreen(screen)) { + QMetaObject::invokeMethod(view->rootItem(), "stop"); + } + } + + m_shutdownTimer->start(timeout); +} + +void TilesEditorEffect::realDeactivate() +{ + setRunning(false); +} + +int TilesEditorEffect::animationDuration() const +{ + return m_animationDuration; +} + +void TilesEditorEffect::setAnimationDuration(int duration) +{ + if (m_animationDuration != duration) { + m_animationDuration = duration; + Q_EMIT animationDurationChanged(); + } +} + +int TilesEditorEffect::requestedEffectChainPosition() const +{ + return 70; +} + +void TilesEditorEffect::grabbedKeyboardEvent(QKeyEvent *keyEvent) +{ + if (m_toggleShortcut.contains(keyEvent->key() | keyEvent->modifiers())) { + if (keyEvent->type() == QEvent::KeyPress) { + toggle(); + } + return; + } + QuickSceneEffect::grabbedKeyboardEvent(keyEvent); +} + +} // namespace KWin + +#include "moc_tileseditoreffect.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/tileseditor/tileseditoreffect.h b/local/recipes/kde/kwin/source/src/plugins/tileseditor/tileseditoreffect.h new file mode 100644 index 0000000000..26e4687682 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/tileseditor/tileseditoreffect.h @@ -0,0 +1,53 @@ +/* + SPDX-FileCopyrightText: 2022 Marco Martin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/quickeffect.h" + +#include + +namespace KWin +{ + +class TilesEditorEffect : public QuickSceneEffect +{ + Q_OBJECT + Q_PROPERTY(int animationDuration READ animationDuration NOTIFY animationDurationChanged) + +public: + TilesEditorEffect(); + ~TilesEditorEffect() override; + + void reconfigure(ReconfigureFlags) override; + + int animationDuration() const; + void setAnimationDuration(int duration); + + int requestedEffectChainPosition() const override; + void grabbedKeyboardEvent(QKeyEvent *keyEvent) override; + +public Q_SLOTS: + void toggle(); + void activate(); + void deactivate(int timeout); + +Q_SIGNALS: + void animationDurationChanged(); + +protected: + QVariantMap initialProperties(LogicalOutput *screen) override; + +private: + void realDeactivate(); + + std::unique_ptr m_shutdownTimer; + std::unique_ptr m_toggleAction; + QList m_toggleShortcut; + int m_animationDuration = 200; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/touchpoints/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/touchpoints/CMakeLists.txt new file mode 100644 index 0000000000..9fe1a28505 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/touchpoints/CMakeLists.txt @@ -0,0 +1,14 @@ +####################################### +# Effect + +set(touchpoints_SOURCES + main.cpp + touchpoints.cpp +) + +kwin_add_builtin_effect(touchpoints ${touchpoints_SOURCES}) +target_link_libraries(touchpoints PRIVATE + kwin + + KF6::GlobalAccel +) diff --git a/local/recipes/kde/kwin/source/src/plugins/touchpoints/main.cpp b/local/recipes/kde/kwin/source/src/plugins/touchpoints/main.cpp new file mode 100644 index 0000000000..8478c68ca3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/touchpoints/main.cpp @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "touchpoints.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY(TouchPointsEffect, + "metadata.json.stripped") + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/touchpoints/metadata.json b/local/recipes/kde/kwin/source/src/plugins/touchpoints/metadata.json new file mode 100644 index 0000000000..19c0e512c7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/touchpoints/metadata.json @@ -0,0 +1,101 @@ +{ + "KPlugin": { + "Category": "Appearance", + "Description": "Visualize touch points", + "Description[ar]": "تصور نقاط اللمس", + "Description[az]": "Ekranda toxunan nöqtələri canlandırır", + "Description[be]": "Візуалізацыя пунктаў дакранання", + "Description[bg]": "Визуализиране на точките на допир", + "Description[ca@valencia]": "Visualitza els punts de contacte", + "Description[ca]": "Visualitza els punts de contacte", + "Description[cs]": "Vizualizovat dotekové body", + "Description[da]": "Visualisér touchområder", + "Description[de]": "Berührungspunkte anzeigen", + "Description[en_GB]": "Visualise touch points", + "Description[eo]": "Vidigi tuŝpunktojn", + "Description[es]": "Visualizar los puntos de contacto", + "Description[et]": "Puutepunktide visualiseerimine", + "Description[eu]": "Bistaratu ukimen puntuak", + "Description[fi]": "Korosta kosketuspisteet", + "Description[fr]": "Afficher les points tactiles", + "Description[gl]": "Visualizar os puntos de toque.", + "Description[he]": "המחשת נקודות מגע", + "Description[hu]": "Vizualizálja az érintési pontokat", + "Description[ia]": "Visualisa punctos de contacto", + "Description[id]": "Memvisualkan titik sentuh", + "Description[is]": "Gera snertipunkta sýnilega", + "Description[it]": "Visualizza i punti di tocco", + "Description[ja]": "タッチしたポイントを視覚化します", + "Description[ka]": "შეხების წერტილების ვიზუალიზაცია", + "Description[ko]": "터치 지점 표시", + "Description[lt]": "Vizualizuoti prilietimo taškus", + "Description[lv]": "Vizualizē kontaktpunktus", + "Description[nb]": "Visualiser kontaktpunkt", + "Description[nl]": "Aanraakpunten visualiseren", + "Description[nn]": "Visualiser kontaktpunkt", + "Description[pl]": "Uwidacznia punkty dotyku", + "Description[pt]": "Visualizar os pontos de toque", + "Description[pt_BR]": "Visualizar pontos de toque", + "Description[ro]": "Vizualizează punctele de atingere", + "Description[ru]": "Визуализация событий касания экрана", + "Description[sa]": "स्पर्शबिन्दून् कल्पयन्तु", + "Description[sk]": "Vizualizovať dotykové body", + "Description[sl]": "Vizualizirajte točke dotika", + "Description[sv]": "Åskådliggör beröringspunkter", + "Description[ta]": "தொடப்படும் இடங்களை காட்டும்", + "Description[tr]": "Dokunma noktalarını görselleştir", + "Description[uk]": "Візуалізувати точки дотику", + "Description[vi]": "Trực quan hoá các điểm chạm", + "Description[zh_CN]": "高亮显示在触控屏幕上的触摸点", + "Description[zh_TW]": "視覺化觸控點", + "EnabledByDefault": false, + "License": "GPL", + "Name": "Touch Points", + "Name[ar]": "نقاط اللمس", + "Name[az]": "Toxunma nöqtələri", + "Name[be]": "Пункты дакранання", + "Name[bg]": "Допирни точки", + "Name[ca@valencia]": "Punts de contacte", + "Name[ca]": "Punts de contacte", + "Name[cs]": "Dotekové body", + "Name[da]": "Berøringspunkter", + "Name[de]": "Berührungspunkte", + "Name[en_GB]": "Touch Points", + "Name[eo]": "Tuŝpunktoj", + "Name[es]": "Puntos de contacto", + "Name[et]": "Puutepunktid", + "Name[eu]": "Ukimen puntuak", + "Name[fi]": "Kosketuspisteet", + "Name[fr]": "Points tactiles", + "Name[gl]": "Puntos de toque", + "Name[he]": "נקודות מגע", + "Name[hu]": "Érintési pontok", + "Name[ia]": "Punctos de contacto", + "Name[id]": "Titik Sentuh", + "Name[is]": "Snertipunktar", + "Name[it]": "Punti di tocco", + "Name[ja]": "タッチポイント", + "Name[ka]": "შეხების წერტილები", + "Name[ko]": "터치 지점", + "Name[lt]": "Prilietimo taškai", + "Name[lv]": "Kontaktpunkti", + "Name[nb]": "Kontaktpunkt", + "Name[nl]": "Aanraakpunten", + "Name[nn]": "Kontaktpunkt", + "Name[pl]": "Punkty dotyku", + "Name[pt]": "Pontos de Toque", + "Name[pt_BR]": "Pontos de toque", + "Name[ro]": "Puncte de atingere", + "Name[ru]": "Точки прикосновения", + "Name[sa]": "स्पर्शबिन्दवः", + "Name[sk]": "Dotykové body", + "Name[sl]": "Točke dotika", + "Name[sv]": "Beröringspunkter", + "Name[ta]": "தொடும் இடங்கள்", + "Name[tr]": "Dokunma Noktaları", + "Name[uk]": "Точки дотику", + "Name[vi]": "Điểm chạm", + "Name[zh_CN]": "触摸点高亮", + "Name[zh_TW]": "觸控點" + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/touchpoints/touchpoints.cpp b/local/recipes/kde/kwin/source/src/plugins/touchpoints/touchpoints.cpp new file mode 100644 index 0000000000..85f1602bcd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/touchpoints/touchpoints.cpp @@ -0,0 +1,251 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Filip Wieladek + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "touchpoints.h" + +#include "core/rendertarget.h" +#include "core/renderviewport.h" +#include "effect/effecthandler.h" +#include "opengl/glutils.h" +#include + +#include +#include + +#include + +#include + +namespace KWin +{ + +TouchPointsEffect::TouchPointsEffect() + : Effect() +{ +} + +TouchPointsEffect::~TouchPointsEffect() = default; + +static const Qt::GlobalColor s_colors[] = { + Qt::blue, + Qt::red, + Qt::green, + Qt::cyan, + Qt::magenta, + Qt::yellow, + Qt::gray, + Qt::darkBlue, + Qt::darkRed, + Qt::darkGreen}; + +Qt::GlobalColor TouchPointsEffect::colorForId(quint32 id) +{ + auto it = m_colors.constFind(id); + if (it != m_colors.constEnd()) { + return it.value(); + } + static int s_colorIndex = -1; + s_colorIndex = (s_colorIndex + 1) % 10; + m_colors.insert(id, s_colors[s_colorIndex]); + return s_colors[s_colorIndex]; +} + +bool TouchPointsEffect::touchDown(qint32 id, const QPointF &pos, std::chrono::microseconds time) +{ + TouchPoint point; + point.pos = pos; + point.press = true; + point.color = colorForId(id); + m_points << point; + m_latestPositions.insert(id, pos); + repaint(); + return false; +} + +bool TouchPointsEffect::touchMotion(qint32 id, const QPointF &pos, std::chrono::microseconds time) +{ + TouchPoint point; + point.pos = pos; + point.press = true; + point.color = colorForId(id); + m_points << point; + m_latestPositions.insert(id, pos); + repaint(); + return false; +} + +bool TouchPointsEffect::touchUp(qint32 id, std::chrono::microseconds time) +{ + auto it = m_latestPositions.constFind(id); + if (it != m_latestPositions.constEnd()) { + TouchPoint point; + point.pos = it.value(); + point.press = false; + point.color = colorForId(id); + m_points << point; + } + return false; +} + +void TouchPointsEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + int time = 0; + if (m_lastPresentTime.count()) { + time = (presentTime - m_lastPresentTime).count(); + } + + auto it = m_points.begin(); + while (it != m_points.end()) { + it->time += time; + if (it->time > m_ringLife) { + it = m_points.erase(it); + } else { + it++; + } + } + + if (m_points.isEmpty()) { + m_lastPresentTime = std::chrono::milliseconds::zero(); + } else { + m_lastPresentTime = presentTime; + } + + effects->prePaintScreen(data, presentTime); +} + +void TouchPointsEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) +{ + effects->paintScreen(renderTarget, viewport, mask, deviceRegion, screen); + + if (effects->isOpenGLCompositing()) { + paintScreenSetupGl(renderTarget, viewport.projectionMatrix()); + } + for (auto it = m_points.constBegin(), end = m_points.constEnd(); it != end; ++it) { + for (int i = 0; i < m_ringCount; ++i) { + float alpha = computeAlpha(it->time, i); + float size = computeRadius(it->time, it->press, i); + if (size > 0 && alpha > 0) { + QColor color = it->color; + color.setAlphaF(alpha); + drawCircle(viewport, color, it->pos.x(), it->pos.y(), size); + } + } + } + if (effects->isOpenGLCompositing()) { + paintScreenFinishGl(); + } +} + +void TouchPointsEffect::postPaintScreen() +{ + effects->postPaintScreen(); + repaint(); +} + +float TouchPointsEffect::computeRadius(int time, bool press, int ring) +{ + float ringDistance = m_ringLife / (m_ringCount * 3); + if (press) { + return ((time - ringDistance * ring) / m_ringLife) * m_ringMaxSize; + } + return ((m_ringLife - time - ringDistance * ring) / m_ringLife) * m_ringMaxSize; +} + +float TouchPointsEffect::computeAlpha(int time, int ring) +{ + float ringDistance = m_ringLife / (m_ringCount * 3); + return (m_ringLife - (float)time - ringDistance * (ring)) / m_ringLife; +} + +void TouchPointsEffect::repaint() +{ + if (!m_points.isEmpty()) { + Region dirtyRegion; + const int radius = m_ringMaxSize + m_lineWidth; + for (auto it = m_points.constBegin(), end = m_points.constEnd(); it != end; ++it) { + dirtyRegion += Rect(it->pos.x() - radius, it->pos.y() - radius, 2 * radius, 2 * radius); + } + effects->addRepaint(dirtyRegion); + } +} + +bool TouchPointsEffect::isActive() const +{ + return !m_points.isEmpty(); +} + +void TouchPointsEffect::drawCircle(const RenderViewport &viewport, const QColor &color, float cx, float cy, float r) +{ + if (effects->isOpenGLCompositing()) { + drawCircleGl(viewport, color, cx, cy, r); + } else if (effects->compositingType() == QPainterCompositing) { + drawCircleQPainter(color, cx, cy, r); + } +} + +void TouchPointsEffect::drawCircleGl(const RenderViewport &viewport, const QColor &color, float cx, float cy, float r) +{ + static const int num_segments = 80; + static const float theta = 2 * 3.1415926 / float(num_segments); + static const float c = cosf(theta); // precalculate the sine and cosine + static const float s = sinf(theta); + const auto scale = viewport.scale(); + float t; + + float x = r; // we start at angle = 0 + float y = 0; + + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + ShaderManager::instance()->getBoundShader()->setUniform(GLShader::ColorUniform::Color, color); + QList verts; + verts.reserve(num_segments); + + for (int ii = 0; ii < num_segments; ++ii) { + verts.push_back(QVector2D((x + cx) * scale, (y + cy) * scale)); + // apply the rotation matrix + t = x; + x = c * x - s * y; + y = s * t + c * y; + } + vbo->setVertices(verts); + vbo->render(GL_LINE_LOOP); +} + +void TouchPointsEffect::drawCircleQPainter(const QColor &color, float cx, float cy, float r) +{ + QPainter *painter = effects->scenePainter(); + painter->save(); + painter->setPen(color); + painter->drawArc(cx - r, cy - r, r * 2, r * 2, 0, 5760); + painter->restore(); +} + +void TouchPointsEffect::paintScreenSetupGl(const RenderTarget &renderTarget, const QMatrix4x4 &projectionMatrix) +{ + GLShader *shader = ShaderManager::instance()->pushShader(ShaderTrait::UniformColor | ShaderTrait::TransformColorspace); + shader->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, projectionMatrix); + shader->setColorspaceUniforms(ColorDescription::sRGB, renderTarget.colorDescription(), RenderingIntent::Perceptual); + + glLineWidth(m_lineWidth); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); +} + +void TouchPointsEffect::paintScreenFinishGl() +{ + glDisable(GL_BLEND); + + ShaderManager::instance()->popShader(); +} + +} // namespace + +#include "moc_touchpoints.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/touchpoints/touchpoints.h b/local/recipes/kde/kwin/source/src/plugins/touchpoints/touchpoints.h new file mode 100644 index 0000000000..92d35af362 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/touchpoints/touchpoints.h @@ -0,0 +1,87 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Filip Wieladek + SPDX-FileCopyrightText: 2016 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/effect.h" + +namespace KWin +{ + +class TouchPointsEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(qreal lineWidth READ lineWidth) + Q_PROPERTY(int ringLife READ ringLife) + Q_PROPERTY(int ringSize READ ringSize) + Q_PROPERTY(int ringCount READ ringCount) +public: + TouchPointsEffect(); + ~TouchPointsEffect() override; + void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override; + void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) override; + void postPaintScreen() override; + bool isActive() const override; + bool touchDown(qint32 id, const QPointF &pos, std::chrono::microseconds time) override; + bool touchMotion(qint32 id, const QPointF &pos, std::chrono::microseconds time) override; + bool touchUp(qint32 id, std::chrono::microseconds time) override; + + // for properties + qreal lineWidth() const + { + return m_lineWidth; + } + int ringLife() const + { + return m_ringLife; + } + int ringSize() const + { + return m_ringMaxSize; + } + int ringCount() const + { + return m_ringCount; + } + +private: + inline void drawCircle(const RenderViewport &viewport, const QColor &color, float cx, float cy, float r); + + void repaint(); + + float computeAlpha(int time, int ring); + float computeRadius(int time, bool press, int ring); + void drawCircleGl(const RenderViewport &viewport, const QColor &color, float cx, float cy, float r); + void drawCircleQPainter(const QColor &color, float cx, float cy, float r); + void paintScreenSetupGl(const RenderTarget &renderTarget, const QMatrix4x4 &projectionMatrix); + void paintScreenFinishGl(); + + Qt::GlobalColor colorForId(quint32 id); + + int m_ringCount = 2; + float m_lineWidth = 1.0; + int m_ringLife = 300; + float m_ringMaxSize = 20.0; + + struct TouchPoint + { + QPointF pos; + int time = 0; + bool press; + QColor color; + }; + QList m_points; + QHash m_latestPositions; + QHash m_colors; + std::chrono::milliseconds m_lastPresentTime = std::chrono::milliseconds::zero(); +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/trackmouse/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/trackmouse/CMakeLists.txt new file mode 100644 index 0000000000..a5e4f9c848 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/trackmouse/CMakeLists.txt @@ -0,0 +1,41 @@ +####################################### +# Effect +# Data files +install(FILES data/tm_inner.png data/tm_outer.png DESTINATION ${KDE_INSTALL_DATADIR}/kwin-wayland) + +set(trackmouse_SOURCES + main.cpp + trackmouse.cpp +) + +kconfig_add_kcfg_files(trackmouse_SOURCES + trackmouseconfig.kcfgc +) + +kwin_add_builtin_effect(trackmouse ${trackmouse_SOURCES}) +target_link_libraries(trackmouse PRIVATE + kwin + + KF6::ConfigGui + KF6::GlobalAccel + KF6::I18n +) + +####################################### +# Config +if (KWIN_BUILD_KCMS) + set(kwin_trackmouse_config_SRCS trackmouse_config.cpp) + ki18n_wrap_ui(kwin_trackmouse_config_SRCS trackmouse_config.ui) + kconfig_add_kcfg_files(kwin_trackmouse_config_SRCS trackmouseconfig.kcfgc) + + kwin_add_effect_config(kwin_trackmouse_config ${kwin_trackmouse_config_SRCS}) + + target_link_libraries(kwin_trackmouse_config + KF6::KCMUtils + KF6::CoreAddons + KF6::GlobalAccel + KF6::I18n + KF6::XmlGui + KWinEffectsInterface + ) +endif() diff --git a/local/recipes/kde/kwin/source/src/plugins/trackmouse/data/tm_inner.png b/local/recipes/kde/kwin/source/src/plugins/trackmouse/data/tm_inner.png new file mode 100644 index 0000000000..42d8739f13 Binary files /dev/null and b/local/recipes/kde/kwin/source/src/plugins/trackmouse/data/tm_inner.png differ diff --git a/local/recipes/kde/kwin/source/src/plugins/trackmouse/data/tm_outer.png b/local/recipes/kde/kwin/source/src/plugins/trackmouse/data/tm_outer.png new file mode 100644 index 0000000000..de0625bf2b Binary files /dev/null and b/local/recipes/kde/kwin/source/src/plugins/trackmouse/data/tm_outer.png differ diff --git a/local/recipes/kde/kwin/source/src/plugins/trackmouse/main.cpp b/local/recipes/kde/kwin/source/src/plugins/trackmouse/main.cpp new file mode 100644 index 0000000000..cff45ce868 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/trackmouse/main.cpp @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "trackmouse.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY(TrackMouseEffect, + "metadata.json.stripped") + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/trackmouse/metadata.json b/local/recipes/kde/kwin/source/src/plugins/trackmouse/metadata.json new file mode 100644 index 0000000000..8fb610d597 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/trackmouse/metadata.json @@ -0,0 +1,94 @@ +{ + "KPlugin": { + "Category": "Accessibility", + "Description": "Display a mouse cursor locating effect; activated with a keyboard shortcut", + "Description[ar]": "اعرض تأثير تحديد موقع مؤشر الفأرة عند التنشيط، تفعّل باختصار لوحة المفاتيح", + "Description[bg]": "Показва ефект на локализиране на показалеца на мишката, активиран с клавишна комбинация", + "Description[ca@valencia]": "Mostra un efecte de posició del cursor del ratolí; activat amb una drecera de teclat", + "Description[ca]": "Mostra un efecte de posició del cursor del ratolí; activat amb una drecera de teclat", + "Description[da]": "Viser en markør lokaliseringseffekt når aktiveret; aktiveret med en tastaturgenvej", + "Description[de]": "Hebt bei Bedarf die Position des Mauszeigers hervor; mit einem Tastatur-Kurzbefehl ausgelöst", + "Description[en_GB]": "Display a mouse cursor locating effect; activated with a keyboard shortcut", + "Description[eo]": "Montri muskursor-lokalizan efikon; aktivigita per klavara ŝparvojo", + "Description[es]": "Muestra un efecto de localización del cursor del ratón; se activa con un atajo de teclado", + "Description[eu]": "Bistaratu saguaren kurtsorea kokatzeko efektu bat; teklatuko lasterbide batekin aktibatua", + "Description[fi]": "Näytä hiiriosoittimen jäljitystehoste; käynnistyy pikanäppäimellä", + "Description[fr]": "Afficher un effet indiquant la position du pointeur de la souris. Activé grâce à un raccourci clavier.", + "Description[gl]": "Mostra un efecto de atopar o cursor do rato; actívase cun atallo de teclado.", + "Description[he]": "הצגת אפקט איתור סמן עכבר, מופעל דרך צירוף מקשי מקלדת", + "Description[hu]": "Megjelenít egy effektust az egérkurzor megtalálásához; gyorsbillentyűvel aktiválható", + "Description[ia]": "Monstra un effecto de location de mus; activate con un via breve de claviero", + "Description[is]": "Sýna staðsetningu músarbendils sjónrænt; virkjað með flýtilykli", + "Description[it]": "Visualizza un effetto di posizionamento del puntatore del mouse quando è attivato; attivato con una scorciatoia da tastiera", + "Description[ja]": "マウスカーソルの場所を示すエフェクトを表示します。ショートカットで起動します", + "Description[ka]": "აქტივაციისას თაგუნას კურსორის მდებარეობის ეფექტის ჩვენება. აქტიურდება კლავიატურის მალსახმობით", + "Description[ko]": "활성화되었을 때 마우스 커서의 위치를 표시, 키보드 단축키로 활성화", + "Description[lt]": "Rodyti pelės žymeklio vietos nustatymo efektą; aktyvinama naudojant sparčiuosius klavišus", + "Description[lv]": "Aktivizējot parāda peles kursora atrašanas efektu; aktivizē ar tastatūras saīsni", + "Description[nb]": "Vis hvor musepekeren er – slått av/på med hurtigtast", + "Description[nl]": "Toont het effect van de positie van de muisaanwijzer; geactiveerd met een sneltoets", + "Description[nn]": "Vis kor musepeikaren er – slått på/av med snøggtast", + "Description[pl]": "Wyświetla efekt wskazujący położenie wskaźnika myszy; uruchamiane skrótem klawiszowym", + "Description[pt_BR]": "Mostra um efeito de localização do ponteiro do mouse; ativado com um atalho de teclado", + "Description[ro]": "Afișează un efect de găsire a cursorului; se activează cu o scurtătură de taste", + "Description[ru]": "Положение курсора мыши на экране; активируется нажатием комбинации клавиш", + "Description[sa]": "मूषकस्य कर्सरस्थाननिर्धारणप्रभावं प्रदर्शयन्तु; कीबोर्ड-शॉर्टकट्-सहितं सक्रियम्", + "Description[sk]": "Zobrazí pri aktivácii efekt zvýrazňujúci pozíciu kurzoru myši", + "Description[sl]": "Prikaži učinek lociranja kazalca miške; aktivirano z bližnjico tipkovnice", + "Description[sv]": "Visa en lokaliseringseffekt för muspekaren om aktiverad, aktiveras med en snabbtangent", + "Description[tr]": "Etkinleştirildiğinde bir fare imleci bulma efekti görüntüle; klavye kısayoluyla etkinleştirilir", + "Description[uk]": "Показ наближення вказівника миші; активується клавіатурним скороченням", + "Description[zh_CN]": "显示用于定位鼠标光标位置的特效;使用键盘快捷键激活", + "Description[zh_TW]": "強調顯示滑鼠游標讓它比較容易被找到——需要用鍵盤快捷鍵觸發", + "EnabledByDefault": false, + "License": "GPL", + "Name": "Track Mouse", + "Name[ar]": "تتبع الفارة", + "Name[az]": "Kursorun izi", + "Name[be]": "Адсочванне мышы", + "Name[bg]": "Проследяване на показалеца", + "Name[ca@valencia]": "Seguiment del ratolí", + "Name[ca]": "Seguiment del ratolí", + "Name[cs]": "Sledování myši", + "Name[da]": "Spor mus", + "Name[de]": "Maus-Position finden", + "Name[en_GB]": "Track Mouse", + "Name[eo]": "Spurmuso", + "Name[es]": "Seguimiento del ratón", + "Name[et]": "Hiire jälgimine", + "Name[eu]": "Saguaren jarraipena", + "Name[fi]": "Hiiren jäljitys", + "Name[fr]": "Suivi de la souris", + "Name[gl]": "Seguir o rato", + "Name[he]": "מעקב אחר עכבר", + "Name[hu]": "Egérkövetés", + "Name[ia]": "Tracia mus", + "Name[id]": "Lacak mouse", + "Name[is]": "Fylgjast með mús", + "Name[it]": "Trova il mouse", + "Name[ja]": "マウスを追跡", + "Name[ka]": "თაგუნას დევნება", + "Name[ko]": "마우스 추적", + "Name[lt]": "Pelės sekimas", + "Name[lv]": "Sekot pelei", + "Name[nb]": "Følg musepekeren", + "Name[nl]": "Muis volgen", + "Name[nn]": "Følg musepeikaren", + "Name[pl]": "Śledzenie myszy", + "Name[pt]": "Seguir o Rato", + "Name[pt_BR]": "Seguir o mouse", + "Name[ro]": "Urmărește mausul", + "Name[ru]": "Поиск курсора мыши на экране", + "Name[sa]": "ट्रैक माउस", + "Name[sk]": "Sledovať myš", + "Name[sl]": "Sledi miški", + "Name[sv]": "Följ musen", + "Name[ta]": "சுட்டியை கண்டுபிடி", + "Name[tr]": "Fareyi İzle", + "Name[uk]": "Сліди мишки", + "Name[vi]": "Theo dõi chuột", + "Name[zh_CN]": "鼠标定位", + "Name[zh_TW]": "滑鼠追蹤" + }, + "X-KDE-ConfigModule": "kwin_trackmouse_config" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse.cpp b/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse.cpp new file mode 100644 index 0000000000..49604f5c10 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse.cpp @@ -0,0 +1,201 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2010 Jorge Mata + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "plugins/trackmouse/trackmouse.h" +#include "effect/effecthandler.h" +#include "plugins/trackmouse/trackmouseconfig.h" +#include "scene/imageitem.h" +#include "scene/itemrenderer.h" +#include "scene/workspacescene.h" + +#include +#include + +#include +#include + +namespace KWin +{ + +RotatingArcsItem::RotatingArcsItem(Item *parentItem) + : Item(parentItem) +{ + const QString f[2] = {QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kwin-wayland/tm_outer.png")), + QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kwin-wayland/tm_inner.png"))}; + if (f[0].isEmpty() || f[1].isEmpty()) { + return; + } + + const QImage outerImage(f[0]); + m_outerArcItem = scene()->renderer()->createImageItem(this); + m_outerArcItem->setImage(outerImage); + m_outerArcItem->setSize(outerImage.size()); + m_outerArcItem->setPosition(QPointF(-outerImage.width() / 2, -outerImage.height() / 2)); + + const QImage innerImage(f[1]); + m_innerArcItem = scene()->renderer()->createImageItem(this); + m_innerArcItem->setImage(innerImage); + m_innerArcItem->setSize(innerImage.size()); + m_innerArcItem->setPosition(QPointF(-innerImage.width() / 2, -innerImage.height() / 2)); +} + +void RotatingArcsItem::rotate(qreal angle) +{ + m_outerArcItem->setTransform( + QTransform() + .translate(m_outerArcItem->size().width() / 2, m_outerArcItem->size().height() / 2) + .rotate(angle) + .translate(-m_outerArcItem->size().width() / 2, -m_outerArcItem->size().height() / 2)); + + m_innerArcItem->setTransform( + QTransform() + .translate(m_innerArcItem->size().width() / 2, m_innerArcItem->size().height() / 2) + .rotate(-2 * angle) + .translate(-m_innerArcItem->size().width() / 2, -m_innerArcItem->size().height() / 2)); +} + +TrackMouseEffect::TrackMouseEffect() +{ + TrackMouseConfig::instance(effects->config()); + + QAction *action = new QAction(this); + action->setObjectName(QStringLiteral("TrackMouse")); + action->setText(i18n("Track mouse")); + KGlobalAccel::self()->setDefaultShortcut(action, QList()); + KGlobalAccel::self()->setShortcut(action, QList()); + connect(action, &QAction::triggered, this, &TrackMouseEffect::toggle); + + reconfigure(ReconfigureAll); +} + +TrackMouseEffect::~TrackMouseEffect() +{ +} + +void TrackMouseEffect::reconfigure(ReconfigureFlags) +{ + const bool needMouseEventsOld = needMouseEvents(); + m_modifiers = Qt::KeyboardModifiers(); + TrackMouseConfig::self()->read(); + if (TrackMouseConfig::shift()) { + m_modifiers |= Qt::ShiftModifier; + } + if (TrackMouseConfig::alt()) { + m_modifiers |= Qt::AltModifier; + } + if (TrackMouseConfig::control()) { + m_modifiers |= Qt::ControlModifier; + } + if (TrackMouseConfig::meta()) { + m_modifiers |= Qt::MetaModifier; + } + const bool needMouseEventsNew = needMouseEvents(); + if (needMouseEventsNew && !needMouseEventsOld) { + connect(effects, &EffectsHandler::mouseChanged, this, &TrackMouseEffect::slotMouseChanged); + } else if (!needMouseEventsNew && needMouseEventsOld) { + disconnect(effects, &EffectsHandler::mouseChanged, this, &TrackMouseEffect::slotMouseChanged); + } +} + +void TrackMouseEffect::postPaintScreen() +{ + QTime t = QTime::currentTime(); + m_angle = ((t.second() % 4) * 90.0) + (t.msec() / 1000.0 * 90.0); + m_rotatingArcsItem->rotate(m_angle); + + effects->postPaintScreen(); +} + +void TrackMouseEffect::toggle() +{ + const bool needMouseEventsOld = needMouseEvents(); + switch (m_state) { + case State::ActivatedByModifiers: + m_state = State::ActivatedByShortcut; + break; + + case State::ActivatedByShortcut: + m_state = State::Inactive; + break; + + case State::Inactive: + m_state = State::ActivatedByShortcut; + break; + + default: + Q_UNREACHABLE(); + break; + } + + const bool needMouseEventsNew = needMouseEvents(); + if (needMouseEventsNew && !needMouseEventsOld) { + connect(effects, &EffectsHandler::mouseChanged, this, &TrackMouseEffect::slotMouseChanged); + } else if (!needMouseEventsNew && needMouseEventsOld) { + disconnect(effects, &EffectsHandler::mouseChanged, this, &TrackMouseEffect::slotMouseChanged); + } + if (m_state == State::Inactive) { + m_rotatingArcsItem.reset(); + } else { + if (!m_rotatingArcsItem) { + m_rotatingArcsItem = std::make_unique(effects->scene()->overlayItem()); + } + m_rotatingArcsItem->setPosition(effects->cursorPos()); + } +} + +void TrackMouseEffect::slotMouseChanged(const QPointF &, const QPointF &, + Qt::MouseButtons, Qt::MouseButtons, + Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers) +{ + switch (m_state) { + case State::ActivatedByModifiers: + if (modifiers != m_modifiers) { + m_state = State::Inactive; + } + break; + + case State::ActivatedByShortcut: + break; + + case State::Inactive: + if (modifiers == m_modifiers) { + m_state = State::ActivatedByModifiers; + } + break; + + default: + Q_UNREACHABLE(); + break; + } + + if (m_state == State::Inactive) { + m_rotatingArcsItem.reset(); + } else { + if (!m_rotatingArcsItem) { + m_rotatingArcsItem = std::make_unique(effects->scene()->overlayItem()); + } + m_rotatingArcsItem->setPosition(effects->cursorPos()); + } +} + +bool TrackMouseEffect::isActive() const +{ + return m_state != State::Inactive; +} + +bool TrackMouseEffect::needMouseEvents() const +{ + return m_state != State::Inactive || m_modifiers; +} + +} // namespace + +#include "moc_trackmouse.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse.h b/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse.h new file mode 100644 index 0000000000..7b6d5685a9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse.h @@ -0,0 +1,74 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2010 Jorge Mata + SPDX-FileCopyrightText: 2018 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/effect.h" +#include "scene/item.h" + +namespace KWin +{ + +class ImageItem; + +class RotatingArcsItem : public Item +{ + Q_OBJECT + +public: + explicit RotatingArcsItem(Item *parentItem); + + void rotate(qreal angle); + +private: + std::unique_ptr m_outerArcItem; + std::unique_ptr m_innerArcItem; +}; + +class TrackMouseEffect : public Effect +{ + Q_OBJECT + Q_PROPERTY(Qt::KeyboardModifiers modifiers READ modifiers) +public: + TrackMouseEffect(); + ~TrackMouseEffect() override; + + void postPaintScreen() override; + void reconfigure(ReconfigureFlags) override; + bool isActive() const override; + + // for properties + Qt::KeyboardModifiers modifiers() const + { + return m_modifiers; + } +private Q_SLOTS: + void toggle(); + void slotMouseChanged(const QPointF &pos, const QPointF &old, + Qt::MouseButtons buttons, Qt::MouseButtons oldbuttons, + Qt::KeyboardModifiers modifiers, Qt::KeyboardModifiers oldmodifiers); + +private: + enum class State { + ActivatedByModifiers, + ActivatedByShortcut, + Inactive + }; + + bool needMouseEvents() const; + + State m_state = State::Inactive; + float m_angle = 0; + Qt::KeyboardModifiers m_modifiers; + std::unique_ptr m_rotatingArcsItem; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse.kcfg b/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse.kcfg new file mode 100644 index 0000000000..3795657318 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse.kcfg @@ -0,0 +1,21 @@ + + + + + + true + + + true + + + false + + + false + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse_config.cpp b/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse_config.cpp new file mode 100644 index 0000000000..aa761cbaa3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse_config.cpp @@ -0,0 +1,103 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + SPDX-FileCopyrightText: 2010 Jorge Mata + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "trackmouse_config.h" + +#include "config-kwin.h" + +// KConfigSkeleton +#include "trackmouseconfig.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include + +K_PLUGIN_CLASS(KWin::TrackMouseEffectConfig) + +namespace KWin +{ + +static const QString s_toggleTrackMouseActionName = QStringLiteral("TrackMouse"); + +TrackMouseEffectConfig::TrackMouseEffectConfig(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) +{ + TrackMouseConfig::instance(KWIN_CONFIG); + m_ui.setupUi(widget()); + + addConfig(TrackMouseConfig::self(), widget()); + + m_actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + m_actionCollection->setComponentDisplayName(i18n("KWin")); + m_actionCollection->setConfigGroup(QStringLiteral("TrackMouse")); + m_actionCollection->setConfigGlobal(true); + + QAction *a = m_actionCollection->addAction(s_toggleTrackMouseActionName); + a->setText(i18n("Track mouse")); + a->setProperty("isConfigurationAction", true); + + KGlobalAccel::self()->setDefaultShortcut(a, QList()); + KGlobalAccel::self()->setShortcut(a, QList()); + + connect(m_ui.shortcut, &KKeySequenceWidget::keySequenceChanged, + this, &TrackMouseEffectConfig::shortcutChanged); +} + +TrackMouseEffectConfig::~TrackMouseEffectConfig() +{ +} + +void TrackMouseEffectConfig::load() +{ + KCModule::load(); + + if (QAction *a = m_actionCollection->action(s_toggleTrackMouseActionName)) { + auto shortcuts = KGlobalAccel::self()->shortcut(a); + if (!shortcuts.isEmpty()) { + m_ui.shortcut->setKeySequence(shortcuts.first()); + } + } +} + +void TrackMouseEffectConfig::save() +{ + KCModule::save(); + m_actionCollection->writeSettings(); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("trackmouse")); +} + +void TrackMouseEffectConfig::defaults() +{ + KCModule::defaults(); + m_ui.shortcut->clearKeySequence(); +} + +void TrackMouseEffectConfig::shortcutChanged(const QKeySequence &seq) +{ + if (QAction *a = m_actionCollection->action(QStringLiteral("TrackMouse"))) { + KGlobalAccel::self()->setShortcut(a, QList() << seq, KGlobalAccel::NoAutoloading); + } + setNeedsSave(true); +} + +} // namespace + +#include "trackmouse_config.moc" + +#include "moc_trackmouse_config.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse_config.h b/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse_config.h new file mode 100644 index 0000000000..4ba1d95b58 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse_config.h @@ -0,0 +1,40 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2007 Rivo Laks + SPDX-FileCopyrightText: 2010 Jorge Mata + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "ui_trackmouse_config.h" + +class KActionCollection; + +namespace KWin +{ +class TrackMouseEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit TrackMouseEffectConfig(QObject *parent, const KPluginMetaData &data); + ~TrackMouseEffectConfig() override; + +public Q_SLOTS: + void save() override; + void load() override; + void defaults() override; +private Q_SLOTS: + void shortcutChanged(const QKeySequence &seq); + +private: + Ui::TrackMouseEffectConfigForm m_ui; + KActionCollection *m_actionCollection; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse_config.ui b/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse_config.ui new file mode 100644 index 0000000000..dff595d26e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouse_config.ui @@ -0,0 +1,104 @@ + + + KWin::TrackMouseEffectConfigForm + + + + 0 + 0 + 345 + 112 + + + + + QFormLayout::FieldsStayAtSizeHint + + + + + + 75 + true + + + + Trigger effect with: + + + + + + + Keyboard shortcut: + + + + + + + + + + Modifier keys: + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Alt + + + + + + + Ctrl + + + + + + + Shift + + + + + + + Meta + + + + + + + + + + + KKeySequenceWidget + QWidget +
kkeysequencewidget.h
+
+
+ + +
diff --git a/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouseconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouseconfig.kcfgc new file mode 100644 index 0000000000..3aa4117496 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/trackmouse/trackmouseconfig.kcfgc @@ -0,0 +1,5 @@ +File=trackmouse.kcfg +ClassName=TrackMouseConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/local/recipes/kde/kwin/source/src/plugins/translucency/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/translucency/CMakeLists.txt new file mode 100644 index 0000000000..b86fad6dff --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/translucency/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_scripted_effect(translucency package) diff --git a/local/recipes/kde/kwin/source/src/plugins/translucency/package/contents/code/main.js b/local/recipes/kde/kwin/source/src/plugins/translucency/package/contents/code/main.js new file mode 100644 index 0000000000..a743597e5b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/translucency/package/contents/code/main.js @@ -0,0 +1,237 @@ +/* + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +/*global effect, effects, animate, cancel, set, animationTime, Effect, QEasingCurve */ + +"use strict"; + +var translucencyEffect = { + activeWindow: effects.activeWindow, + settings: { + duration: animationTime(250), + moveresize: 100, + dialogs: 100, + inactive: 100, + comboboxpopups: 100, + menus: 100, + dropdownmenus: 100, + popupmenus: 100, + tornoffmenus: 100 + }, + loadConfig: function () { + var i, individualMenu, windows; + // TODO: add animation duration + translucencyEffect.settings.moveresize = effect.readConfig("MoveResize", 80); + translucencyEffect.settings.dialogs = effect.readConfig("Dialogs", 100); + translucencyEffect.settings.inactive = effect.readConfig("Inactive", 100); + translucencyEffect.settings.comboboxpopups = effect.readConfig("ComboboxPopups", 100); + translucencyEffect.settings.menus = effect.readConfig("Menus", 100); + individualMenu = effect.readConfig("IndividualMenuConfig", false); + if (individualMenu === true) { + translucencyEffect.settings.dropdownmenus = effect.readConfig("DropdownMenus", 100); + translucencyEffect.settings.popupmenus = effect.readConfig("PopupMenus", 100); + translucencyEffect.settings.tornoffmenus = effect.readConfig("TornOffMenus", 100); + } else { + translucencyEffect.settings.dropdownmenus = translucencyEffect.settings.menus; + translucencyEffect.settings.popupmenus = translucencyEffect.settings.menus; + translucencyEffect.settings.tornoffmenus = translucencyEffect.settings.menus; + } + + windows = effects.stackingOrder; + for (i = 0; i < windows.length; i += 1) { + // stop all existing animations + translucencyEffect.cancelAnimations(windows[i]); + // schedule new animations based on new settings + translucencyEffect.startAnimation(windows[i]); + if (windows[i] !== effects.activeWindow) { + translucencyEffect.inactive.animate(windows[i]); + } + } + }, + /** + * @brief Starts the set animations depending on window type + * + */ + startAnimation: function (window) { + var checkWindow = function (window, value) { + if (value !== 100) { + var ids = set({ + window: window, + duration: 1, + animations: [{ + type: Effect.Opacity, + from: value / 100.0, + to: value / 100.0 + }] + }); + if (window.translucencyWindowTypeAnimation !== undefined) { + cancel(window.translucencyWindowTypeAnimation); + } + window.translucencyWindowTypeAnimation = ids; + } + }; + if (window.desktopWindow === true || window.dock === true || window.visible === false) { + return; + } + if (window.dialog === true) { + checkWindow(window, translucencyEffect.settings.dialogs); + } else if (window.dropdownMenu === true) { + checkWindow(window, translucencyEffect.settings.dropdownmenus); + } else if (window.popupMenu === true) { + checkWindow(window, translucencyEffect.settings.popupmenus); + } else if (window.comboBox === true) { + checkWindow(window, translucencyEffect.settings.comboboxpopups); + } else if (window.menu === true) { + checkWindow(window, translucencyEffect.settings.tornoffmenus); + } + }, + /** + * @brief Cancels all animations for window type and inactive window + * + */ + cancelAnimations: function (window) { + if (window.translucencyWindowTypeAnimation !== undefined) { + cancel(window.translucencyWindowTypeAnimation); + window.translucencyWindowTypeAnimation = undefined; + } + if (window.translucencyInactiveAnimation !== undefined) { + cancel(window.translucencyInactiveAnimation); + window.translucencyInactiveAnimation = undefined; + } + if (window.translucencyMoveResizeAnimations !== undefined) { + cancel(window.translucencyMoveResizeAnimations); + window.translucencyMoveResizeAnimations = undefined; + } + }, + moveResize: { + start: function (window) { + var ids; + if (translucencyEffect.settings.moveresize === 100) { + return; + } + ids = set({ + window: window, + duration: translucencyEffect.settings.duration, + animations: [{ + type: Effect.Opacity, + to: translucencyEffect.settings.moveresize / 100.0 + }] + }); + window.translucencyMoveResizeAnimations = ids; + }, + finish: function (window) { + if (window.translucencyMoveResizeAnimations !== undefined) { + // start revert animation + animate({ + window: window, + duration: translucencyEffect.settings.duration, + animations: [{ + type: Effect.Opacity, + from: translucencyEffect.settings.moveresize / 100.0 + }] + }); + // and cancel previous animation + cancel(window.translucencyMoveResizeAnimations); + window.translucencyMoveResizeAnimations = undefined; + } + } + }, + inactive: { + activated: function (window) { + if (translucencyEffect.settings.inactive === 100) { + return; + } + translucencyEffect.inactive.animate(translucencyEffect.activeWindow); + translucencyEffect.activeWindow = window; + if (window === null) { + return; + } + if (window.translucencyInactiveAnimation !== undefined) { + // start revert animation + animate({ + window: window, + duration: translucencyEffect.settings.duration, + animations: [{ + type: Effect.Opacity, + from: translucencyEffect.settings.inactive / 100.0 + }] + }); + // and cancel previous animation + cancel(window.translucencyInactiveAnimation); + window.translucencyInactiveAnimation = undefined; + } + }, + animate: function (window) { + var ids; + if (translucencyEffect.settings.inactive === 100) { + return; + } + if (window === null) { + return; + } + if (window === effects.activeWindow || + window.popup === true || + window.managed === false || + window.desktopWindow === true || + window.dock === true || + window.visible === false || + window.deleted === true) { + return; + } + ids = set({ + window: window, + duration: translucencyEffect.settings.duration, + animations: [{ + type: Effect.Opacity, + to: translucencyEffect.settings.inactive / 100.0 + }] + }); + window.translucencyInactiveAnimation = ids; + } + }, + desktopChanged: function () { + var i, windows; + windows = effects.stackingOrder; + for (i = 0; i < windows.length; i += 1) { + translucencyEffect.cancelAnimations(windows[i]); + translucencyEffect.startAnimation(windows[i]); + if (windows[i] !== effects.activeWindow) { + translucencyEffect.inactive.animate(windows[i]); + } + } + }, + manage: function (window) { + window.windowDesktopsChanged.connect(translucencyEffect.cancelAnimations); + window.windowDesktopsChanged.connect(translucencyEffect.startAnimation); + window.windowStartUserMovedResized.connect(translucencyEffect.moveResize.start); + window.windowFinishUserMovedResized.connect(translucencyEffect.moveResize.finish); + + window.minimizedChanged.connect(() => { + if (window.minimized) { + translucencyEffect.cancelAnimations(window); + } else { + translucencyEffect.startAnimation(window); + translucencyEffect.inactive.animate(window); + } + }); + }, + init: function () { + effect.configChanged.connect(translucencyEffect.loadConfig); + effects.windowAdded.connect(translucencyEffect.manage); + effects.windowAdded.connect(translucencyEffect.startAnimation); + effects.windowClosed.connect(translucencyEffect.cancelAnimations); + effects.windowActivated.connect(translucencyEffect.inactive.activated); + effects.desktopChanged.connect(translucencyEffect.desktopChanged); + + for (const window of effects.stackingOrder) { + translucencyEffect.manage(window); + } + + translucencyEffect.loadConfig(); + } +}; +translucencyEffect.init(); diff --git a/local/recipes/kde/kwin/source/src/plugins/translucency/package/contents/config/main.xml b/local/recipes/kde/kwin/source/src/plugins/translucency/package/contents/config/main.xml new file mode 100644 index 0000000000..31124425e2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/translucency/package/contents/config/main.xml @@ -0,0 +1,36 @@ + + + + + + 80 + + + 100 + + + 100 + + + 100 + + + 100 + + + false + + + 100 + + + 100 + + + 100 + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/translucency/package/contents/ui/config.ui b/local/recipes/kde/kwin/source/src/plugins/translucency/package/contents/ui/config.ui new file mode 100644 index 0000000000..30d517e3d4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/translucency/package/contents/ui/config.ui @@ -0,0 +1,473 @@ + + + KWin::TranslucencyEffectConfigForm + + + + 0 + 0 + 643 + 269 + + + + Translucency + + + + + + General Translucency Settings + + + + + + 10 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 10 + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + 0 + 0 + + + + Combobox popups: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_ComboboxPopups + + + + + + + 10 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 10 + + + + + + + 10 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 10 + + + + + + + + 0 + 0 + + + + Opaque + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + Dialogs: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Dialogs + + + + + + + + 0 + 0 + + + + Transparent + + + + + + + 10 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 10 + + + + + + + + 0 + 0 + + + + Menus: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Menus + + + + + + + + 0 + 0 + + + + Moving windows: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_MoveResize + + + + + + + + 0 + 0 + + + + Inactive windows: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Inactive + + + + + + + + 170 + 0 + + + + 10 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 10 + + + + + + + + + + Set menu translucency independently + + + true + + + false + + + + + + + 0 + 0 + + + + Dropdown menus: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_DropdownMenus + + + + + + + + 170 + 0 + + + + 10 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 10 + + + + + + + + 0 + 0 + + + + Popup menus: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_PopupMenus + + + + + + + 10 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 10 + + + + + + + + 0 + 0 + + + + Torn-off menus: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_TornOffMenus + + + + + + + 10 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 10 + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + 0 + 0 + + + + Transparent + + + + + + + + 0 + 0 + + + + Opaque + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + kcfg_Inactive + kcfg_MoveResize + kcfg_Dialogs + kcfg_ComboboxPopups + kcfg_Menus + kcfg_IndividualMenuConfig + kcfg_DropdownMenus + kcfg_PopupMenus + kcfg_TornOffMenus + + + + + kcfg_IndividualMenuConfig + toggled(bool) + kcfg_Menus + setDisabled(bool) + + + 109 + 316 + + + 212 + 220 + + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/translucency/package/metadata.json b/local/recipes/kde/kwin/source/src/plugins/translucency/package/metadata.json new file mode 100644 index 0000000000..a6a83c5416 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/translucency/package/metadata.json @@ -0,0 +1,155 @@ +{ + "KPackageStructure": "KWin/Effect", + "KPlugin": { + "Authors": [ + { + "Email": "l.lunak@kde.org, mgraesslin@kde.org", + "Name": "Luboš Luňák, Martin Gräßlin", + "Name[ar]": "لوبوس لوناك، مارتن جراجلين", + "Name[be]": "Luboš Luňák, Martin Gräßlin", + "Name[bg]": "Luboš Luňák, Martin Gräßlin", + "Name[ca@valencia]": "Luboš Luňák, Martin Gräßlin", + "Name[ca]": "Luboš Luňák, Martin Gräßlin", + "Name[cs]": "Luboš Luňák, Martin Gräßlin", + "Name[da]": "Luboš Luňák, Martin Gräßlin", + "Name[de]": "Luboš Luňák, Martin Gräßlin", + "Name[en_GB]": "Luboš Luňák, Martin Gräßlin", + "Name[eo]": "Luboš Luňák, Martin Gräßlin", + "Name[es]": "Luboš Luňák, Martin Gräßlin", + "Name[et]": "Luboš Luňák, Martin Gräßlin", + "Name[eu]": "Luboš Luňák, Martin Gräßlin", + "Name[fi]": "Luboš Luňák, Martin Gräßlin", + "Name[fr]": "Luboš Luňák, Martin Gräßlin", + "Name[ga]": "Luboš Luňák, Martin Gräßlin", + "Name[gl]": "Luboš Luňák e Martin Gräßlin.", + "Name[he]": "לובוש לוניאק", + "Name[hu]": "Luboš Luňák, Martin Gräßlin", + "Name[ia]": "Luboš Luňák, Martin Gräßlin", + "Name[id]": "Luboš Luňák, Martin Gräßlin", + "Name[is]": "Luboš Luňák, Martin Gräßlin", + "Name[it]": "Luboš Luňák, Martin Gräßlin", + "Name[ja]": "Luboš Luňák, Martin Gräßlin", + "Name[ka]": "Luboš Luňák, Martin Gräßlin", + "Name[ko]": "Luboš Luňák, Martin Gräßlin", + "Name[lt]": "Luboš Luňák, Martin Gräßlin", + "Name[lv]": "Luboš Luňák, Martin Gräßlin", + "Name[nb]": "Luboš Luňák, Martin Gräßlin", + "Name[nl]": "Luboš Luňák, Martin Gräßlin", + "Name[nn]": "Luboš Luňák, Martin Gräßlin", + "Name[pl]": "Luboš Luňák, Martin Gräßlin", + "Name[pt]": "Luboš Luňák, Martin Gräßlin", + "Name[pt_BR]": "Luboš Luňák, Martin Gräßlin", + "Name[ro]": "Luboš Luňák, Martin Gräßlin", + "Name[ru]": "Luboš Luňák, Martin Gräßlin", + "Name[sa]": "Luboš Luňák, मार्टिन Gräßlin", + "Name[sk]": "Luboš Luňák, Martin Gräßlin", + "Name[sl]": "Luboš Luňák, Martin Gräßlin", + "Name[sv]": "Luboš Luňák, Martin Gräßlin", + "Name[ta]": "லூபொசு லுனாக்கு, மார்ட்டின் கிராஸ்லின்", + "Name[tr]": "Luboš Luňák, Martin Gräßlin", + "Name[uk]": "Luboš Luňák, Martin Gräßlin", + "Name[vi]": "Luboš Luňák, Martin Gräßlin", + "Name[zh_CN]": "Luboš Luňák, Martin Gräßlin", + "Name[zh_TW]": "Luboš Luňák, Martin Gräßlin" + } + ], + "Category": "Appearance", + "Description": "Make windows translucent under different conditions", + "Description[ar]": "يجعل النوافذ شبه شفافة في شروط مختلفة", + "Description[be]": "Забеспячэнне празрыстасці акон", + "Description[bg]": "Прозрачност на прозорците при определени условия", + "Description[ca@valencia]": "Torna translúcides les finestres en diverses condicions", + "Description[ca]": "Torna translúcides les finestres en diverses condicions", + "Description[cs]": "Zobrazuje okna různě průhledná", + "Description[da]": "Gør alle vinduer gennemsigtige under forskellige forhold", + "Description[de]": "Lässt Fenster unter festgelegten Bedingungen transparent erscheinen", + "Description[en_GB]": "Make windows translucent under different conditions", + "Description[eo]": "Igi fenestrojn travideblaj sub malsamaj kondiĉoj", + "Description[es]": "Hacer que las ventanas sean transparentes bajo distintas condiciones", + "Description[et]": "Akende muutmine läbipaistvaks teatavatel tingimustel", + "Description[eu]": "Leihoak zeharrargitsu bihurtzen ditu baldintzen arabera", + "Description[fi]": "Muuttaa ikkunat läpikuultaviksi eri olosuhteissa", + "Description[fr]": "Rendre les fenêtres translucides sous différentes conditions", + "Description[gl]": "Fai translúcidas as xanelas en distintos casos.", + "Description[he]": "להפוך את החלונות לשקופים למחצה לפי כל מיני תנאים שונים", + "Description[hu]": "Az ablakok átlátszóvá tétele bizonyos feltételek mellett", + "Description[ia]": "On face fenestra translucente sub conditiones differente", + "Description[is]": "Gerir glugga gegnsæja við ólíkar kringumstæður", + "Description[it]": "Rende le finestre translucide in certe condizioni", + "Description[ja]": "さまざまな状況でウィンドウを半透明にします", + "Description[ka]": "სხვადასხვა პირობებისას ფანჯრის გამჭვირვალობა", + "Description[ko]": "다른 조건 하에서 창을 투명하게 만듭니다", + "Description[lt]": "Paversti langus dalinai permatomais, kai yra įvykdomos įvairios sąlygos", + "Description[lv]": "Dažādos apstākļos logus padara caurspīdīgus", + "Description[nb]": "Gjør vinduer i visse tilfeller gjennomsiktige", + "Description[nl]": "Maakt vensters doorschijnend onder andere condities", + "Description[nn]": "Gjer vindauge i visse tilfelle gjennomsiktige", + "Description[pl]": "Okna są prześwitujące w zależności od różnych warunków", + "Description[pt]": "Tornar as janelas translúcidas em diferentes condições", + "Description[pt_BR]": "Torna as janelas translúcidas em diferentes condições", + "Description[ro]": "Face ferestrele translucide în diferite condiții", + "Description[ru]": "Использование полупрозрачности окон при разных событиях", + "Description[sa]": "भिन्न-भिन्न-स्थितौ खिडकयः अर्धपारदर्शिकाः कुर्वन्तु", + "Description[sk]": "Vytvorenie priesvitných okien za rôznych podmienok", + "Description[sl]": "Naredite okna prosojna pod različnimi pogoji", + "Description[sv]": "Gör fönster halvgenomskinliga enligt olika villkor", + "Description[ta]": "சில சூழ்நிலைகளில் சாளரங்களின் ஒளிபுகுமையை கூட்டும்", + "Description[tr]": "Değişik koşullar altında pencereleri yarısaydam yap", + "Description[uk]": "Додавання прозорості до вікон за різних умов", + "Description[vi]": "Làm cửa sổ trong mờ trong các điều kiện khác nhau", + "Description[zh_CN]": "窗口在某些条件下呈现透明效果", + "Description[zh_TW]": "在不同的情況下讓視窗變半透明", + "Icon": "preferences-system-windows-effect-translucency", + "Id": "translucency", + "License": "GPL", + "Name": "Translucency", + "Name[ar]": "شبه الشفافية", + "Name[be]": "Празрыстасць", + "Name[bg]": "Прозрачност", + "Name[ca@valencia]": "Translucidesa", + "Name[ca]": "Translucidesa", + "Name[cs]": "Průhlednost", + "Name[da]": "Gennemsigtighed", + "Name[de]": "Transparenz", + "Name[en_GB]": "Translucency", + "Name[eo]": "Travidebleco", + "Name[es]": "Transparencia", + "Name[et]": "Läbipaistvus", + "Name[eu]": "Zeharrargitsua", + "Name[fi]": "Läpikuultavuus", + "Name[fr]": "Translucidité", + "Name[gl]": "Translucidez", + "Name[he]": "שקיפות למחצה", + "Name[hu]": "Áttetszőség", + "Name[ia]": "Translucentia", + "Name[is]": "Gagnsæi", + "Name[it]": "Translucenza", + "Name[ja]": "半透明性", + "Name[ka]": "გამჭვირვალობა", + "Name[ko]": "반투명", + "Name[lt]": "Dalinis permatomumas", + "Name[lv]": "Caurspīdība", + "Name[nb]": "Gjennomsiktighet", + "Name[nl]": "Transparantie", + "Name[nn]": "Gjennomsikt", + "Name[pl]": "Prześwitywanie", + "Name[pt]": "Translucidez", + "Name[pt_BR]": "Transparência", + "Name[ro]": "Transluciditate", + "Name[ru]": "Полупрозрачность", + "Name[sa]": "पारदर्शिता", + "Name[sk]": "Priesvitnosť", + "Name[sl]": "Prosojnost", + "Name[sv]": "Genomlysning", + "Name[ta]": "ஒளிபுகுமை", + "Name[tr]": "Yarısaydamlık", + "Name[uk]": "Прозорість", + "Name[vi]": "Độ trong mờ", + "Name[zh_CN]": "窗口透明度", + "Name[zh_TW]": "半透明" + }, + "X-KDE-ConfigModule": "kcm_kwin4_genericscripted", + "X-KDE-Ordering": 50, + "X-KWin-Config-TranslationDomain": "kwin", + "X-Plasma-API": "javascript" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/videowall/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/videowall/CMakeLists.txt new file mode 100644 index 0000000000..83c9855008 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/videowall/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_script(videowall package) diff --git a/local/recipes/kde/kwin/source/src/plugins/videowall/package/contents/code/main.js b/local/recipes/kde/kwin/source/src/plugins/videowall/package/contents/code/main.js new file mode 100644 index 0000000000..d25daac62b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/videowall/package/contents/code/main.js @@ -0,0 +1,38 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +var applyTo = readConfig("ApplyTo", true); +var whitelist = readConfig("Whitelist", "vlc, xv, vdpau, smplayer, dragon, xine, ffplay, mplayer").toString().toLowerCase().split(","); +for (i = 0; i < whitelist.length; ++i) + whitelist[i] = whitelist[i].trim(); + +var ignore = readConfig("Ignore", false); +var blacklist = readConfig("Blacklist", "").toString().toLowerCase().split(","); +for (i = 0; i < blacklist.length; ++i) + blacklist[i] = blacklist[i].trim(); + + +function isVideoPlayer(client) { + if (applyTo == true && whitelist.indexOf(client.resourceClass.toString()) < 0) + return false; // required whitelist match failed + if (ignore == true && blacklist.indexOf(client.resourceClass.toString()) > -1) + return false; // required blacklist match hit + return true; +} + +function setup(window) { + window.fullScreenChanged.connect(() => { + if (window.fullScreen && isVideoPlayer(window)) { + window.frameGeometry = workspace.clientArea(KWin.FullArea, window); + } + }); +} + +workspace.windowAdded.connect(setup); +workspace.windowList().forEach(setup); diff --git a/local/recipes/kde/kwin/source/src/plugins/videowall/package/contents/config/main.xml b/local/recipes/kde/kwin/source/src/plugins/videowall/package/contents/config/main.xml new file mode 100644 index 0000000000..7baa80d459 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/videowall/package/contents/config/main.xml @@ -0,0 +1,22 @@ + + + + + + true + + + vlc, xv, vdpau, smplayer, dragon, xine, ffplay, mplayer + + + false + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/videowall/package/contents/ui/config.ui b/local/recipes/kde/kwin/source/src/plugins/videowall/package/contents/ui/config.ui new file mode 100644 index 0000000000..53543cdfcb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/videowall/package/contents/ui/config.ui @@ -0,0 +1,148 @@ + + + KWin::VideoWallConfigForm + + + + 0 + 0 + 334 + 59 + + + + Video Wall + + + + + + Apply to + + + true + + + + + + + + + vlc, xv, vdpau, smplayer, dragon, xine, ffplay + + + Comma separated list of window classes + + + + + + + false + + + All + + + + + + + + + Ignore + + + + + + + + + false + + + Comma separated list of window classes + + + + + + + None + + + + + + + + + + + kcfg_ApplyTo + toggled(bool) + kcfg_Whitelist + setVisible(bool) + + + 41 + 9 + + + 143 + 13 + + + + + kcfg_ApplyTo + toggled(bool) + applyLabel + setHidden(bool) + + + 28 + 15 + + + 330 + 20 + + + + + kcfg_Ignore + toggled(bool) + kcfg_Blacklist + setVisible(bool) + + + 33 + 44 + + + 111 + 45 + + + + + kcfg_Ignore + toggled(bool) + ignoreLabel + setHidden(bool) + + + 51 + 33 + + + 327 + 42 + + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/videowall/package/metadata.json b/local/recipes/kde/kwin/source/src/plugins/videowall/package/metadata.json new file mode 100644 index 0000000000..c9a6311a46 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/videowall/package/metadata.json @@ -0,0 +1,152 @@ +{ + "KPackageStructure": "KWin/Script", + "KPlugin": { + "Authors": [ + { + "Email": "mgraesslin@kde.org", + "Name": "Martin Gräßlin", + "Name[ar]": "مارتن جراجلين", + "Name[be]": "Martin Gräßlin", + "Name[bg]": "Martin Gräßlin", + "Name[ca@valencia]": "Martin Gräßlin", + "Name[ca]": "Martin Gräßlin", + "Name[cs]": "Martin Gräßlin", + "Name[da]": "Martin Gräßlin", + "Name[de]": "Martin Gräßlin", + "Name[en_GB]": "Martin Gräßlin", + "Name[eo]": "Martin Gräßlin", + "Name[es]": "Martin Gräßlin", + "Name[et]": "Martin Gräßlin", + "Name[eu]": "Martin Gräßlin", + "Name[fi]": "Martin Gräßlin", + "Name[fr]": "Martin Gräßlin", + "Name[ga]": "Martin Gräßlin", + "Name[gl]": "Martin Gräßlin.", + "Name[he]": "מרטין גרייסלין", + "Name[hu]": "Martin Gräßlin", + "Name[ia]": "Martin Gräßlin", + "Name[id]": "Martin Gräßlin", + "Name[is]": "Martin Gräßlin", + "Name[it]": "Martin Gräßlin", + "Name[ja]": "Martin Gräßlin", + "Name[ka]": "მარტინ გრესსლინი", + "Name[ko]": "Martin Gräßlin", + "Name[lt]": "Martin Gräßlin", + "Name[lv]": "Martin Gräßlin", + "Name[nb]": "Martin Gräßlin", + "Name[nl]": "Martin Gräßlin", + "Name[nn]": "Martin Gräßlin", + "Name[pl]": "Martin Gräßlin", + "Name[pt]": "Martin Gräßlin", + "Name[pt_BR]": "Martin Gräßlin", + "Name[ro]": "Martin Gräßlin", + "Name[ru]": "Martin Gräßlin", + "Name[sa]": "मार्टिन् ग्रास्लिन्", + "Name[sk]": "Martin Gräßlin", + "Name[sl]": "Martin Gräßlin", + "Name[sv]": "Martin Gräßlin", + "Name[ta]": "மார்ட்டின் கிராஸ்லின்", + "Name[tr]": "Martin Gräßlin", + "Name[uk]": "Martin Gräßlin", + "Name[vi]": "Martin Gräßlin", + "Name[zh_CN]": "Martin Gräßlin", + "Name[zh_TW]": "Martin Gräßlin" + } + ], + "Description": "Spans fullscreen video player over all attached screens to create a Video Wall", + "Description[ar]": "يمتد مشغل الفيديو بملء الشاشة على جميع الشاشات المرفقة لإنشاء حائط فيديو", + "Description[be]": "Размяркоўвае поўнаэкранны відэаплэер на ўсе падлучаныя экраны, каб стварыць відэасцяну", + "Description[bg]": "Разпределяне на видео предаването на всички прикачени екрани, за да се създаде видео стена", + "Description[ca@valencia]": "Expandix el reproductor de vídeo a pantalla completa per a totes les pantalles adjuntades per a crear un mur de vídeo", + "Description[ca]": "Expandeix el reproductor de vídeo a pantalla completa per a totes les pantalles adjuntades per a crear un mur de vídeo", + "Description[cs]": "Roztáhne přehrávání videa přes všechny plochy pro vytvoření video zdi", + "Description[da]": "Spænder fuldskærmsvinduer over alle tilsluttede skærme for at lave en videovæg", + "Description[de]": "Erweitert einen Videospieler im Vollbildmodus über alle Bildschirme, um eine Video-Wand zu erzeugen", + "Description[en_GB]": "Spans fullscreen video player over all attached screens to create a Video Wall", + "Description[eo]": "Enhavas plenekranan videoludilon super ĉiuj kunigitaj ekranoj por krei Videomuron", + "Description[es]": "Amplía el reproductor de video en pantalla completa a todas las pantallas conectadas para crear un muro de vídeo", + "Description[et]": "Täisekraan-videomängija laiendamine kõigile ühendatud ekraanile videoseina loomiseks", + "Description[eu]": "Bideo-horma bat sortzeko, leiho-osoko bideo jotzailea atxikitako pantaila guzietara hedatzen du", + "Description[fi]": "Luo videoseinän ulottamalla koko näytön videon kaikkiin kytkettyihin näyttöihin", + "Description[fr]": "Affiche le lecteur vidéo en plein écran sur tous les écrans connectés pour créer un mur d'images", + "Description[gl]": "Estende un reprodutor de vídeo a pantalla completa por todas as pantallas anexas para crear unha parede de vídeo.", + "Description[he]": "פורש נגן וידאו במסך מלא כל פני כל המסכים המצורפים כדי ליצור מסך סרטונים", + "Description[hu]": "Teljes képernyős videólejátszó kiterjesztése az összes csatlakoztatott kijelzőre, létrehozva egy videófalat", + "Description[ia]": "Extende jocator de video de schermo plen super omne schermos attachate pro crea un Muro de Video", + "Description[is]": "Láta myndskeið sem fylla skjá ná yfir alla tengda skjái til að mynda einn myndvegg", + "Description[it]": "Distribuisce il lettore video a schermo intero su tutti gli schermi per creare una parete video", + "Description[ja]": "フルスクリーンの動画プレーヤーを接続されたすべてのスクリーンに広げて一つの画面を作ります", + "Description[ka]": "მთლიან ეკრანზე დაკვრის შემთხვევაში ვიდეოდამკვრელის ყველა მიერთებულ ეკრანზე გაწელვა და ამით ვიდეოკედლის შექმნა", + "Description[ko]": "전체 화면 동영상 재생기를 모든 화면에 걸쳐서 동영상 벽 만들기", + "Description[lt]": "Ištempia viso ekrano vaizdo leistuvę taip, kad per visus prijungtus ekranus būtų matoma vaizdo siena", + "Description[lv]": "Pilnekrāna video atskaņotāju izpleš pāri visiem pievienotajiem ekrāniem, izveidojot video sienu", + "Description[nb]": "Filmfremsyning i fullskjermsmodus over alle tilgjengelige skjermer", + "Description[nl]": "Verdeelt de videospeler in modus volledig scherm over alle aangesloten schermen om een Videomuur te creëren", + "Description[nn]": "Filmframsyning i fullskjermsmodus over alle tilgjengelege skjermar", + "Description[pl]": "Rozciąga pełnoekranowy odtwarzacz filmów na wszystkie podłączone ekrany, tworząc ścianę wideo", + "Description[pt]": "Expande o leitor de vídeo de ecrã completo para todos os ecrãs ligados, de forma a formar um mural de vídeo", + "Description[pt_BR]": "Estende o player de vídeo em tela cheia para todas as telas anexadas criando um mural de vídeo", + "Description[ro]": "Întinde un lector video în ecran complet peste toate ecranele atașate pentru a crea un perete video", + "Description[ru]": "Для создания видеостены вывод видеопроигрывателя распределяется по всем подключённым экранам", + "Description[sa]": "Video Wall निर्मातुं सर्वेषु संलग्नस्क्रीन् उपरि पूर्णपर्दे विडियो प्लेयरं व्याप्नोति", + "Description[sk]": "Rozšíri prehrávač videa na celú obrazovku na všetky pripojené obrazovky a vytvorí tak veľkoplošnú projekčnú plochu", + "Description[sl]": "Raztegne celozaslonski video predvajalnik na vseh pripete zaslone, da ustvarite video steno", + "Description[sv]": "Utökar en videospelare med fullskärm över alla anslutna bildskärmar för att skapa en videovägg", + "Description[ta]": "முழுத்திரை நிகழ்படத்தை பல திரைகளில் பரப்பி ஒரே திரையை போல் காட்டும்", + "Description[tr]": "Tam ekran video oynatıcılarını tüm bağlı ekranlara genişleterek bir tür Video Duvarı oluşturur", + "Description[uk]": "Розподіл зображення відеопрогравача у повноекранному режимі на всі екрани з метою створення відеостіни", + "Description[vi]": "Trải trình phát phim toàn màn hình ra tất cả các màn hình đang được gắn vào để tạo nên một màn hình ghép", + "Description[zh_CN]": "视频播放器在全屏时将显示在全部已连接的屏幕上,组成视频墙", + "Description[zh_TW]": "將全螢幕的影片播放器擴展到所有的螢幕,建立一個影片牆", + "Icon": "preferences-system-windows-script-test", + "Id": "videowall", + "License": "GPL", + "Name": "Video Wall", + "Name[ar]": "حائط الفيديو", + "Name[be]": "Відэасцяна", + "Name[bg]": "Видео стена", + "Name[ca@valencia]": "Mur de vídeo", + "Name[ca]": "Mur de vídeo", + "Name[cs]": "Video stěna", + "Name[da]": "Videovæg", + "Name[de]": "Video-Wand", + "Name[en_GB]": "Video Wall", + "Name[eo]": "Video Muro", + "Name[es]": "Video wall", + "Name[et]": "Videosein", + "Name[eu]": "Bideo-horma", + "Name[fi]": "Videoseinä", + "Name[fr]": "Mur de vidéos", + "Name[gl]": "Parede de vídeo", + "Name[he]": "קיר סרטון", + "Name[hu]": "Videófal", + "Name[ia]": "Muro de Video", + "Name[is]": "Myndveggur", + "Name[it]": "Parete video", + "Name[ja]": "ビデオウォール", + "Name[ka]": "ვიდეო კედელი", + "Name[ko]": "동영상 벽", + "Name[lt]": "Vaizdo siena", + "Name[lv]": "Video siena", + "Name[nb]": "Videovegg", + "Name[nl]": "Videomuur", + "Name[nn]": "Videovegg", + "Name[pl]": "Ściana wideo", + "Name[pt]": "Painel de Vídeo", + "Name[pt_BR]": "Parede de vídeo", + "Name[ro]": "Perete video", + "Name[ru]": "Видеостена", + "Name[sa]": "विडियो वाल", + "Name[sk]": "Veľkoplošná projekčná plocha", + "Name[sl]": "Video stena", + "Name[sv]": "Videovägg", + "Name[ta]": "நிகழ்பட சுவர்", + "Name[tr]": "Video Duvarı", + "Name[uk]": "Відеостіна", + "Name[vi]": "Màn hình ghép", + "Name[zh_CN]": "视频墙", + "Name[zh_TW]": "影片牆" + }, + "X-KDE-ConfigModule": "kwin/effects/configs/kcm_kwin4_genericscripted", + "X-Plasma-API": "javascript" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/windowaperture/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/windowaperture/CMakeLists.txt new file mode 100644 index 0000000000..4fad6944e8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowaperture/CMakeLists.txt @@ -0,0 +1 @@ +kwin_add_scripted_effect(windowaperture package) diff --git a/local/recipes/kde/kwin/source/src/plugins/windowaperture/package/contents/code/main.js b/local/recipes/kde/kwin/source/src/plugins/windowaperture/package/contents/code/main.js new file mode 100644 index 0000000000..ed34dd4a72 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowaperture/package/contents/code/main.js @@ -0,0 +1,234 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2015 Thomas Lübking + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +/*global effect, effects, animate, animationTime, Effect, QEasingCurve */ + +"use strict"; + +var badBadWindowsEffect = { + duration: animationTime(250), + showingDesktop: false, + loadConfig: function () { + badBadWindowsEffect.duration = animationTime(250); + }, + setShowingDesktop: function (showing) { + badBadWindowsEffect.showingDesktop = showing; + }, + offToCorners: function (showing, frozenTime) { + if (typeof frozenTime === "undefined") { + frozenTime = -1; + } + var stackingOrder = effects.stackingOrder; + var screenGeo = effects.virtualScreenGeometry; + var xOffset = screenGeo.width / 16; + var yOffset = screenGeo.height / 16; + if (showing) { + var closestWindows = [ undefined, undefined, undefined, undefined ]; + var movedWindowsCount = 0; + for (var i = 0; i < stackingOrder.length; ++i) { + var w = stackingOrder[i]; + + if (!w.hiddenByShowDesktop) { + continue; + } + + // ignore invisible windows and such that do not have to be restored + if (!w.visible) { + if (w.offToCornerId) { + // if it was visible when the effect was activated delete its animation data + cancel(w.offToCornerId); + delete w.offToCornerId; + delete w.apertureCorner; + } + continue; + } + + // Don't touch docks + if (w.dock) { + continue; + } + + // calculate the corner distances + var geo = w.geometry; + var dl = geo.x + geo.width - screenGeo.x; + var dr = screenGeo.x + screenGeo.width - geo.x; + var dt = geo.y + geo.height - screenGeo.y; + var db = screenGeo.y + screenGeo.height - geo.y; + w.apertureDistances = [ dl + dt, dr + dt, dr + db, dl + db ]; + movedWindowsCount += 1; + + // if this window is the closest one to any corner, set it as preferred there + var nearest = 0; + for (var j = 1; j < 4; ++j) { + if (w.apertureDistances[j] < w.apertureDistances[nearest] || + (w.apertureDistances[j] == w.apertureDistances[nearest] && closestWindows[j] === undefined)) { + nearest = j; + } + } + if (closestWindows[nearest] === undefined || + closestWindows[nearest].apertureDistances[nearest] > w.apertureDistances[nearest]) + closestWindows[nearest] = w; + } + + // second pass, select corners + + // 1st off, move the nearest windows to their nearest corners + // this will ensure that if there's only on window in the lower right + // it won't be moved out to the upper left + var movedWindowsDec = [ 0, 0, 0, 0 ]; + for (var i = 0; i < 4; ++i) { + if (closestWindows[i] === undefined) + continue; + closestWindows[i].apertureCorner = i; + delete closestWindows[i].apertureDistances; + movedWindowsDec[i] = 1; + } + + // 2nd, distribute the remainders according to their preferences + // this doesn't exactly have heapsort performance ;-) + movedWindowsCount = Math.floor((movedWindowsCount + 3) / 4); + for (var i = 0; i < 4; ++i) { + for (var j = 0; j < movedWindowsCount - movedWindowsDec[i]; ++j) { + var bestWindow = undefined; + for (var k = 0; k < stackingOrder.length; ++k) { + if (stackingOrder[k].apertureDistances === undefined) + continue; + if (bestWindow === undefined || + stackingOrder[k].apertureDistances[i] < bestWindow.apertureDistances[i]) + bestWindow = stackingOrder[k]; + } + if (bestWindow === undefined) + break; + bestWindow.apertureCorner = i; + delete bestWindow.apertureDistances; + } + } + + } + + // actually re/move windows from/to assigned corners + for (var i = 0; i < stackingOrder.length; ++i) { + var w = stackingOrder[i]; + if (w.apertureCorner === undefined && w.offToCornerId === undefined) + continue; + + if (w.dock) { + continue; + } + + var anchor, tx, ty; + var geo = w.geometry; + if (w.apertureCorner == 1 || w.apertureCorner == 2) { + tx = screenGeo.x + screenGeo.width - xOffset; + anchor = Effect.Left; + } else { + tx = xOffset; + anchor = Effect.Right; + } + if (w.apertureCorner > 1) { + ty = screenGeo.y + screenGeo.height - yOffset; + anchor |= Effect.Top; + } else { + ty = yOffset; + anchor |= Effect.Bottom; + } + + if (showing) { + if (!w.offToCornerId || !freezeInTime(w.offToCornerId, frozenTime)) { + + w.offToCornerId = set({ + window: w, + duration: badBadWindowsEffect.duration, + curve: QEasingCurve.InOutCubic, + animations: [{ + type: Effect.Position, + targetAnchor: anchor, + to: { value1: tx, value2: ty }, + frozenTime: frozenTime + },{ + type: Effect.Opacity, + to: 0.0, + frozenTime: frozenTime + }] + }); + } + } else { + // Reset if the window has become invisible in the meantime + if (!w.visible) { + cancel(w.offToCornerId); + delete w.offToCornerId; + delete w.apertureCorner; + // This if the window was invisible and has become visible in the meantime + } else if (!w.offToCornerId || !redirect(w.offToCornerId, Effect.Backward) || !freezeInTime(w.offToCornerId, frozenTime)) { + animate({ + window: w, + duration: badBadWindowsEffect.duration, + curve: QEasingCurve.InOutCubic, + animations: [{ + type: Effect.Position, + sourceAnchor: anchor, + gesture: true, + from: { value1: tx, value2: ty } + },{ + type: Effect.Opacity, + from: 0.0 + }] + }); + } + } + } + }, + animationEnded: function (w, a, meta) { + // After the animation that closes the effect, reset all the parameters + if (!badBadWindowsEffect.showingDesktop) { + cancel(w.offToCornerId); + delete w.offToCornerId; + delete w.apertureCorner; + } + }, + realtimeScreenEdgeCallback: function (border, deltaProgress, effectScreen) { + if (!deltaProgress || !effectScreen) { + badBadWindowsEffect.offToCorners(badBadWindowsEffect.showingDesktop, -1); + return; + } + let time = 0; + + switch (border) { + case KWin.ElectricTop: + case KWin.ElectricBottom: + time = Math.min(1, (Math.abs(deltaProgress.height) / (effectScreen.geometry.height / 2))) * badBadWindowsEffect.duration; + break; + case KWin.ElectricLeft: + case KWin.ElectricRight: + time = Math.min(1, (Math.abs(deltaProgress.width) / (effectScreen.geometry.width / 2))) * badBadWindowsEffect.duration; + break; + default: + return; + } + if (badBadWindowsEffect.showingDesktop) { + time = badBadWindowsEffect.duration - time; + } + + badBadWindowsEffect.offToCorners(true, time) + }, + init: function () { + badBadWindowsEffect.loadConfig(); + effects.showingDesktopChanged.connect(badBadWindowsEffect.setShowingDesktop); + effects.showingDesktopChanged.connect(badBadWindowsEffect.offToCorners); + effect.animationEnded.connect(badBadWindowsEffect.animationEnded); + + let edges = effect.touchEdgesForAction("show-desktop"); + + for (let i in edges) { + let edge = parseInt(edges[i]); + registerRealtimeScreenEdge(edge, badBadWindowsEffect.realtimeScreenEdgeCallback); + } + } +}; + +badBadWindowsEffect.init(); diff --git a/local/recipes/kde/kwin/source/src/plugins/windowaperture/package/metadata.json b/local/recipes/kde/kwin/source/src/plugins/windowaperture/package/metadata.json new file mode 100644 index 0000000000..29b7a1ce7e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowaperture/package/metadata.json @@ -0,0 +1,155 @@ +{ + "KPackageStructure": "KWin/Effect", + "KPlugin": { + "Authors": [ + { + "Email": "thomas.luebking@gmail.com", + "Name": "Thomas Lübking", + "Name[ar]": "توماس لوبكينج", + "Name[be]": "Thomas Lübking", + "Name[bg]": "Thomas Lübking", + "Name[ca@valencia]": "Thomas Lübking", + "Name[ca]": "Thomas Lübking", + "Name[cs]": "Thomas Lübking", + "Name[da]": "Thomas Lübking", + "Name[de]": "Thomas Lübking", + "Name[en_GB]": "Thomas Lübking", + "Name[eo]": "Thomas Lübking", + "Name[es]": "Thomas Lübking", + "Name[et]": "Thomas Lübking", + "Name[eu]": "Thomas Lübking", + "Name[fi]": "Thomas Lübking", + "Name[fr]": "Thomas Lübking", + "Name[ga]": "Thomas Lübking", + "Name[gl]": "Thomas Lübking.", + "Name[he]": "תומס ליובקינג", + "Name[hu]": "Thomas Lübking", + "Name[ia]": "Thomas Lübking", + "Name[id]": "Thomas Lübking", + "Name[is]": "Thomas Lübking", + "Name[it]": "Thomas Lübking", + "Name[ja]": "Thomas Lübking", + "Name[ka]": "Thomas Lübking", + "Name[ko]": "Thomas Lübking", + "Name[lt]": "Thomas Lübking", + "Name[lv]": "Thomas Lübking", + "Name[nb]": "Thomas Lübking", + "Name[nl]": "Thomas Lübking", + "Name[nn]": "Thomas Lübking", + "Name[pl]": "Thomas Lübking", + "Name[pt]": "Thomas Lübking", + "Name[pt_BR]": "Thomas Lübking", + "Name[ro]": "Thomas Lübking", + "Name[ru]": "Thomas Lübking", + "Name[sa]": "थोमस लुब्किङ्ग्", + "Name[sk]": "Thomas Lübking", + "Name[sl]": "Thomas Lübking", + "Name[sv]": "Thomas Lübking", + "Name[ta]": "தாமஸ் லுபுகிங்", + "Name[tr]": "Thomas Lübking", + "Name[uk]": "Thomas Lübking", + "Name[vi]": "Thomas Lübking", + "Name[zh_CN]": "Thomas Lübking", + "Name[zh_TW]": "Thomas Lübking" + } + ], + "Category": "Show Desktop Animation", + "Description": "Move windows into screen corners", + "Description[ar]": "تحرك النوافذ إلى زوايا الشاشة", + "Description[be]": "Перамяшчэнне акон у куты экрана", + "Description[bg]": "Преместване на прозорците в ъглите на екрана", + "Description[ca@valencia]": "Mou les finestres cap als cantons de la pantalla", + "Description[ca]": "Mou les finestres cap a les cantonades de la pantalla", + "Description[cs]": "Přesunout okna do rohů obrazovky", + "Description[da]": "Flyt vinduer til skærmhjørner", + "Description[de]": "Fenster in Bildschirmecken schieben", + "Description[en_GB]": "Move windows into screen corners", + "Description[eo]": "Movi fenestrojn en ekranangulojn", + "Description[es]": "Mover las ventanas a las esquinas de la pantalla", + "Description[et]": "Akende liigutamine ekraani nurkadesse", + "Description[eu]": "Eraman leihoak pantailako bazterretara", + "Description[fi]": "Siirrä ikkunat näytön kulmiin", + "Description[fr]": "Déplacer les fenêtres vers les coins de l'écran", + "Description[gl]": "Mover as xanelas ás esquinas de pantalla.", + "Description[he]": "העברת חלונות לפינות המסך", + "Description[hu]": "Ablakok áthelyezése a képernyő sarkaiba", + "Description[ia]": "Move fenestra in angulos de schermo", + "Description[is]": "Færa glugga í hornin á skjánum", + "Description[it]": "Sposta le finestre negli angoli dello schermo", + "Description[ja]": "ウィンドウを画面の角に移動します", + "Description[ka]": "ფანჯრების ეკრანის კუთხეებში გადატანა", + "Description[ko]": "창을 화면 꼭짓점으로 이동", + "Description[lt]": "Perkelti langus į ekrano kampus", + "Description[lv]": "Pārvieto logus ekrāna stūros", + "Description[nb]": "Flytt vinduer til skjermhjørner", + "Description[nl]": "Verplaats vensters in de hoeken van het scherm", + "Description[nn]": "Flytt vindauge til skjermhjørne", + "Description[pl]": "Rozsuwa okna w narożniki ekranu", + "Description[pt]": "Mover as janelas para os cantos do ecrã", + "Description[pt_BR]": "Move as janelas para os cantos da tela", + "Description[ro]": "Mută ferestrele în colțurile ecranului", + "Description[ru]": "Перемещение окон в углы экрана", + "Description[sa]": "विण्डोजः स्क्रीनकोणेषु स्थानान्तरयन्तु", + "Description[sk]": "Presun okien do rohov obrazovky", + "Description[sl]": "Premaknite okna v kote zaslona", + "Description[sv]": "Flytta fönster till skärmhörn", + "Description[ta]": "சாளரங்களை திரையின் மூலைகளுக்க நகர்த்தும்", + "Description[tr]": "Pencereleri ekran kenarlarına taşı", + "Description[uk]": "Пересування вікон до кутів екрана", + "Description[vi]": "Di chuyển các cửa sổ đến các góc màn hình", + "Description[zh_CN]": "按下显示桌面时所有窗口被外推到桌面之外消失", + "Description[zh_TW]": "將視窗移至螢幕角落", + "EnabledByDefault": true, + "Icon": "preferences-system-windows-effect-windowaperture", + "Id": "windowaperture", + "License": "GPL", + "Name": "Window Aperture", + "Name[ar]": "إزاحة النوافذ", + "Name[be]": "Аконная дыяфрагма", + "Name[bg]": "Апертура на прозорци", + "Name[ca@valencia]": "Obriu la finestra", + "Name[ca]": "Obertura de la finestra", + "Name[cs]": "Mřížka oken", + "Name[da]": "Vinduesapertur", + "Name[de]": "Fensteröffnung", + "Name[en_GB]": "Window Aperture", + "Name[eo]": "Fenestra Aperturo", + "Name[es]": "Apertura de ventanas", + "Name[et]": "Aknaava", + "Name[eu]": "Leihoaren irekiera", + "Name[fi]": "Ikkuna-aukko", + "Name[fr]": "Ouverture de fenêtre", + "Name[gl]": "Apertura das xanelas", + "Name[he]": "צמצם חלון", + "Name[hu]": "Ablakrekesz", + "Name[ia]": "Aperturas de fenestra", + "Name[is]": "Gluggaljósop", + "Name[it]": "Apertura delle finestre", + "Name[ja]": "ウィンドウアパーチャー", + "Name[ka]": "ფანჯრების წესები", + "Name[ko]": "조리개 모양 배치", + "Name[lt]": "Langų anga", + "Name[lv]": "Loga apertūra", + "Name[nb]": "Vindusflukt", + "Name[nl]": "Vensteropening", + "Name[nn]": "Vindaugsflukt", + "Name[pl]": "Przesłona okna", + "Name[pt]": "Abertura da Janela", + "Name[pt_BR]": "Abertura da janela", + "Name[ro]": "Diafragmă fereastră", + "Name[ru]": "Разбрасывание окон в стороны", + "Name[sa]": "खिडकी एपर्चर", + "Name[sk]": "Clona okien", + "Name[sl]": "Zaslonka oken", + "Name[sv]": "Fönsteröppning", + "Name[ta]": "ஓரத்தில் சாளரங்கள்", + "Name[tr]": "Pencere Açıklığı", + "Name[uk]": "Апертура вікна", + "Name[vi]": "Độ mở cửa sổ", + "Name[zh_CN]": "窗口外推", + "Name[zh_TW]": "視窗光圈" + }, + "X-KDE-Ordering": 50, + "X-KWin-Exclusive-Category": "show-desktop", + "X-Plasma-API": "javascript" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/windowsystem/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/windowsystem/CMakeLists.txt new file mode 100644 index 0000000000..e765798751 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowsystem/CMakeLists.txt @@ -0,0 +1,10 @@ +set(kwindowsystem_plugin_SRCS + plugin.cpp + windoweffects.cpp + windowshadow.cpp + windowsystem.cpp +) + +add_library(KF6WindowSystemKWinPlugin OBJECT ${kwindowsystem_plugin_SRCS}) +target_compile_definitions(KF6WindowSystemKWinPlugin PRIVATE QT_STATICPLUGIN) +target_link_libraries(KF6WindowSystemKWinPlugin kwin) diff --git a/local/recipes/kde/kwin/source/src/plugins/windowsystem/kwindowsystem.json b/local/recipes/kde/kwin/source/src/plugins/windowsystem/kwindowsystem.json new file mode 100644 index 0000000000..aaf6fd00dd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowsystem/kwindowsystem.json @@ -0,0 +1,3 @@ +{ + "platforms": ["wayland-org.kde.kwin.qpa"] +} diff --git a/local/recipes/kde/kwin/source/src/plugins/windowsystem/plugin.cpp b/local/recipes/kde/kwin/source/src/plugins/windowsystem/plugin.cpp new file mode 100644 index 0000000000..04c2c793e7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowsystem/plugin.cpp @@ -0,0 +1,40 @@ +/* + SPDX-FileCopyrightText: 2019 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "plugin.h" +#include "windoweffects.h" +#include "windowshadow.h" +#include "windowsystem.h" + +KWindowSystemKWinPlugin::KWindowSystemKWinPlugin(QObject *parent) + : KWindowSystemPluginInterface(parent) +{ +} + +KWindowSystemKWinPlugin::~KWindowSystemKWinPlugin() +{ +} + +KWindowEffectsPrivate *KWindowSystemKWinPlugin::createEffects() +{ + return new KWin::WindowEffects(); +} + +KWindowSystemPrivate *KWindowSystemKWinPlugin::createWindowSystem() +{ + return new KWin::WindowSystem(); +} + +KWindowShadowTilePrivate *KWindowSystemKWinPlugin::createWindowShadowTile() +{ + return new KWin::WindowShadowTile(); +} + +KWindowShadowPrivate *KWindowSystemKWinPlugin::createWindowShadow() +{ + return new KWin::WindowShadow(); +} + +#include "moc_plugin.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/windowsystem/plugin.h b/local/recipes/kde/kwin/source/src/plugins/windowsystem/plugin.h new file mode 100644 index 0000000000..f99e285dd3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowsystem/plugin.h @@ -0,0 +1,24 @@ +/* + SPDX-FileCopyrightText: 2019 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#pragma once + +#include + +class KWindowSystemKWinPlugin : public KWindowSystemPluginInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID KWindowSystemPluginInterface_iid FILE "kwindowsystem.json") + Q_INTERFACES(KWindowSystemPluginInterface) + +public: + explicit KWindowSystemKWinPlugin(QObject *parent = nullptr); + ~KWindowSystemKWinPlugin() override; + + KWindowEffectsPrivate *createEffects() override; + KWindowSystemPrivate *createWindowSystem() override; + KWindowShadowTilePrivate *createWindowShadowTile() override; + KWindowShadowPrivate *createWindowShadow() override; +}; diff --git a/local/recipes/kde/kwin/source/src/plugins/windowsystem/windoweffects.cpp b/local/recipes/kde/kwin/source/src/plugins/windowsystem/windoweffects.cpp new file mode 100644 index 0000000000..2f0bdad672 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowsystem/windoweffects.cpp @@ -0,0 +1,76 @@ +/* + SPDX-FileCopyrightText: 2019 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "windoweffects.h" +#include "core/region.h" +#include "effect/effecthandler.h" + +#include +#include +#include + +Q_DECLARE_METATYPE(KWindowEffects::SlideFromLocation) + +namespace KWin +{ + +WindowEffects::WindowEffects() + : QObject() + , KWindowEffectsPrivate() +{ +} + +WindowEffects::~WindowEffects() +{ +} + +bool WindowEffects::isEffectAvailable(KWindowEffects::Effect effect) +{ + if (!effects) { + return false; + } + switch (effect) { + case KWindowEffects::BackgroundContrast: + return effects->isEffectLoaded(QStringLiteral("contrast")); + case KWindowEffects::BlurBehind: + return effects->isEffectLoaded(QStringLiteral("blur")); + case KWindowEffects::Slide: + return effects->isEffectLoaded(QStringLiteral("slidingpopups")); + default: + // plugin does not provide integration for other effects + return false; + } +} + +void WindowEffects::slideWindow(QWindow *window, KWindowEffects::SlideFromLocation location, int offset) +{ + window->setProperty("kwin_slide", QVariant::fromValue(location)); + window->setProperty("kwin_slide_offset", offset); +} + +void WindowEffects::enableBlurBehind(QWindow *window, bool enable, const QRegion ®ion) +{ + if (enable) { + window->setProperty("kwin_blur", QVariant::fromValue(Region(region))); + } else { + window->setProperty("kwin_blur", {}); + } +} + +void WindowEffects::enableBackgroundContrast(QWindow *window, bool enable, qreal contrast, qreal intensity, qreal saturation, const QRegion ®ion) +{ + if (enable) { + window->setProperty("kwin_background_region", QVariant::fromValue(Region(region))); + window->setProperty("kwin_background_contrast", contrast); + window->setProperty("kwin_background_intensity", intensity); + window->setProperty("kwin_background_saturation", saturation); + } else { + window->setProperty("kwin_background_region", {}); + window->setProperty("kwin_background_contrast", {}); + window->setProperty("kwin_background_intensity", {}); + window->setProperty("kwin_background_saturation", {}); + } +} +} diff --git a/local/recipes/kde/kwin/source/src/plugins/windowsystem/windoweffects.h b/local/recipes/kde/kwin/source/src/plugins/windowsystem/windoweffects.h new file mode 100644 index 0000000000..3c4f2a70b5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowsystem/windoweffects.h @@ -0,0 +1,26 @@ +/* + SPDX-FileCopyrightText: 2019 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#pragma once +#include +#include +#include + +namespace KWin +{ + +class WindowEffects : public QObject, public KWindowEffectsPrivate +{ +public: + WindowEffects(); + ~WindowEffects() override; + + bool isEffectAvailable(KWindowEffects::Effect effect) override; + void slideWindow(QWindow *window, KWindowEffects::SlideFromLocation location, int offset) override; + void enableBlurBehind(QWindow *window, bool enable = true, const QRegion ®ion = QRegion()) override; + void enableBackgroundContrast(QWindow *window, bool enable = true, qreal contrast = 1, qreal intensity = 1, qreal saturation = 1, const QRegion ®ion = QRegion()) override; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/plugins/windowsystem/windowshadow.cpp b/local/recipes/kde/kwin/source/src/plugins/windowsystem/windowshadow.cpp new file mode 100644 index 0000000000..2db94f0064 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowsystem/windowshadow.cpp @@ -0,0 +1,81 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "windowshadow.h" + +#include + +Q_DECLARE_METATYPE(QMargins) + +namespace KWin +{ + +bool WindowShadowTile::create() +{ + return true; +} + +void WindowShadowTile::destroy() +{ +} + +bool WindowShadow::create() +{ + // TODO: Perhaps we set way too many properties here. Alternatively we could put all shadow tiles + // in one big image and attach it rather than 8 separate images. + if (leftTile) { + window->setProperty("kwin_shadow_left_tile", QVariant::fromValue(leftTile->image())); + } + if (topLeftTile) { + window->setProperty("kwin_shadow_top_left_tile", QVariant::fromValue(topLeftTile->image())); + } + if (topTile) { + window->setProperty("kwin_shadow_top_tile", QVariant::fromValue(topTile->image())); + } + if (topRightTile) { + window->setProperty("kwin_shadow_top_right_tile", QVariant::fromValue(topRightTile->image())); + } + if (rightTile) { + window->setProperty("kwin_shadow_right_tile", QVariant::fromValue(rightTile->image())); + } + if (bottomRightTile) { + window->setProperty("kwin_shadow_bottom_right_tile", QVariant::fromValue(bottomRightTile->image())); + } + if (bottomTile) { + window->setProperty("kwin_shadow_bottom_tile", QVariant::fromValue(bottomTile->image())); + } + if (bottomLeftTile) { + window->setProperty("kwin_shadow_bottom_left_tile", QVariant::fromValue(bottomLeftTile->image())); + } + window->setProperty("kwin_shadow_padding", QVariant::fromValue(padding)); + + // Notice that the enabled property must be set last. + window->setProperty("kwin_shadow_enabled", QVariant::fromValue(true)); + + return true; +} + +void WindowShadow::destroy() +{ + // Attempting to uninstall the shadow after the decorated window has been destroyed. It's doomed. + if (!window) { + return; + } + + // Remove relevant shadow properties. + window->setProperty("kwin_shadow_left_tile", {}); + window->setProperty("kwin_shadow_top_left_tile", {}); + window->setProperty("kwin_shadow_top_tile", {}); + window->setProperty("kwin_shadow_top_right_tile", {}); + window->setProperty("kwin_shadow_right_tile", {}); + window->setProperty("kwin_shadow_bottom_right_tile", {}); + window->setProperty("kwin_shadow_bottom_tile", {}); + window->setProperty("kwin_shadow_bottom_left_tile", {}); + window->setProperty("kwin_shadow_padding", {}); + window->setProperty("kwin_shadow_enabled", {}); +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/windowsystem/windowshadow.h b/local/recipes/kde/kwin/source/src/plugins/windowsystem/windowshadow.h new file mode 100644 index 0000000000..8331b05773 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowsystem/windowshadow.h @@ -0,0 +1,28 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#pragma once + +#include + +namespace KWin +{ + +class WindowShadowTile final : public KWindowShadowTilePrivate +{ +public: + bool create() override; + void destroy() override; +}; + +class WindowShadow final : public KWindowShadowPrivate +{ +public: + bool create() override; + void destroy() override; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/windowsystem/windowsystem.cpp b/local/recipes/kde/kwin/source/src/plugins/windowsystem/windowsystem.cpp new file mode 100644 index 0000000000..8ce2329075 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowsystem/windowsystem.cpp @@ -0,0 +1,91 @@ +/* + SPDX-FileCopyrightText: 2019 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#include "windowsystem.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +Q_DECLARE_METATYPE(NET::WindowType) + +namespace KWin +{ + +WindowSystem::WindowSystem() +{ +} + +void WindowSystem::activateWindow(QWindow *win, long int time) +{ + // KWin cannot activate own windows +} + +void WindowSystem::setShowingDesktop(bool showing) +{ + // KWin should not use KWindowSystem to set showing desktop state +} + +bool WindowSystem::showingDesktop() +{ + // KWin should not use KWindowSystem for showing desktop state + return false; +} + +#if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(6, 19) +void WindowSystem::requestToken(QWindow *win, uint32_t serial, const QString &appId) +{ + auto seat = KWin::waylandServer()->seat(); + auto token = KWin::waylandServer()->xdgActivationIntegration()->requestPrivilegedToken(nullptr, seat->display()->serial(), seat, appId); + // Ensure that xdgActivationTokenArrived is always emitted asynchronously + QTimer::singleShot(0, [serial, token] { + Q_EMIT KWaylandExtras::self()->xdgActivationTokenArrived(serial, token); + }); +} +#endif + +void WindowSystem::setCurrentToken(const QString &token) +{ + // KWin cannot activate own windows +} + +quint32 WindowSystem::lastInputSerial(QWindow *window) +{ + auto w = workspace()->findInternal(window); + if (!w) { + return 0; + } + return w->lastUsageSerial(); +} + +void WindowSystem::exportWindow(QWindow *window) +{ +} + +void WindowSystem::unexportWindow(QWindow *window) +{ +} + +void WindowSystem::setMainWindow(QWindow *window, const QString &handle) +{ +} + +QFuture WindowSystem::xdgActivationToken(QWindow *window, uint32_t serial, const QString &appId) +{ + auto seat = KWin::waylandServer()->seat(); + auto token = KWin::waylandServer()->xdgActivationIntegration()->requestPrivilegedToken(nullptr, seat->display()->serial(), seat, appId); + return QtFuture::makeReadyFuture(token); +} +} + +#include "moc_windowsystem.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/windowsystem/windowsystem.h b/local/recipes/kde/kwin/source/src/plugins/windowsystem/windowsystem.h new file mode 100644 index 0000000000..7e428920e0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowsystem/windowsystem.h @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2019 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +#pragma once + +#include + +#include + +namespace KWin +{ + +class WindowSystem : public QObject, public KWindowSystemPrivateV3 +{ + Q_OBJECT +public: + WindowSystem(); + void activateWindow(QWindow *win, long time) override; + bool showingDesktop() override; + void setShowingDesktop(bool showing) override; +#if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(6, 19) + void requestToken(QWindow *win, uint32_t serial, const QString &app_id) override; +#endif + void setCurrentToken(const QString &token) override; + quint32 lastInputSerial(QWindow *window) override; + void exportWindow(QWindow *window) override; + void unexportWindow(QWindow *window) override; + void setMainWindow(QWindow *window, const QString &handle) override; + QFuture xdgActivationToken(QWindow *window, uint32_t serial, const QString &appId) override; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/plugins/windowview/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/windowview/CMakeLists.txt new file mode 100644 index 0000000000..827e599d53 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowview/CMakeLists.txt @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: 2021 Vlad Zahorodnii +# +# SPDX-License-Identifier: BSD-3-Clause + +if (KWIN_BUILD_KCMS) + add_subdirectory(kcm) +endif() + +set(windowview_SOURCES + main.cpp + windowvieweffect.cpp +) + +kconfig_add_kcfg_files(windowview_SOURCES + windowviewconfig.kcfgc +) + +qt_add_dbus_adaptor(windowview_SOURCES org.kde.KWin.Effect.WindowView1.xml windowvieweffect.h KWin::WindowViewEffect) + +kwin_add_builtin_effect(windowview ${windowview_SOURCES}) + +ecm_add_qml_module(windowview + URI "org.kde.kwin.windowview" + QML_FILES + qml/Main.qml + QT_NO_PLUGIN +) + +target_link_libraries(windowview PRIVATE + kwin + + KF6::ConfigGui + KF6::GlobalAccel + KF6::I18n + + Qt::Quick + ) diff --git a/local/recipes/kde/kwin/source/src/plugins/windowview/kcm/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/windowview/kcm/CMakeLists.txt new file mode 100644 index 0000000000..9d635e9863 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowview/kcm/CMakeLists.txt @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2021 Vlad Zahorodnii +# +# SPDX-License-Identifier: BSD-3-Clause + +set(kwin_windowview_config_SOURCES windowvieweffectkcm.cpp) +ki18n_wrap_ui(kwin_windowview_config_SOURCES windowvieweffectkcm.ui) +kconfig_add_kcfg_files(kwin_windowview_config_SOURCES ../windowviewconfig.kcfgc) + +kwin_add_effect_config(kwin_windowview_config ${kwin_windowview_config_SOURCES}) +target_link_libraries(kwin_windowview_config + KF6::KCMUtils + KF6::CoreAddons + KF6::GlobalAccel + KF6::I18n + KF6::XmlGui + KWinEffectsInterface +) diff --git a/local/recipes/kde/kwin/source/src/plugins/windowview/kcm/windowvieweffectkcm.cpp b/local/recipes/kde/kwin/source/src/plugins/windowview/kcm/windowvieweffectkcm.cpp new file mode 100644 index 0000000000..6a861cebda --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowview/kcm/windowvieweffectkcm.cpp @@ -0,0 +1,92 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + SPDX-FileCopyrightText: 2022 ivan tkachenko + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "windowvieweffectkcm.h" + +#include "config-kwin.h" + +#include "windowviewconfig.h" + +#include + +#include +#include +#include +#include + +#include + +K_PLUGIN_CLASS(KWin::WindowViewEffectConfig) + +namespace KWin +{ + +WindowViewEffectConfig::WindowViewEffectConfig(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) +{ + ui.setupUi(widget()); + WindowViewConfig::instance(KWIN_CONFIG); + addConfig(WindowViewConfig::self(), widget()); + + auto actionCollection = new KActionCollection(widget(), QStringLiteral("kwin")); + + actionCollection->setComponentDisplayName(i18n("KWin")); + actionCollection->setConfigGroup(QStringLiteral("windowview")); + actionCollection->setConfigGlobal(true); + + const QKeySequence defaultToggleShortcut = Qt::CTRL | Qt::Key_F9; + QAction *toggleAction = actionCollection->addAction(QStringLiteral("Expose")); + toggleAction->setText(i18n("Toggle Present Windows (Current desktop)")); + toggleAction->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(toggleAction, {defaultToggleShortcut}); + KGlobalAccel::self()->setShortcut(toggleAction, {defaultToggleShortcut}); + + const QKeySequence defaultToggleShortcutAll = Qt::CTRL | Qt::Key_F10; + toggleAction = actionCollection->addAction(QStringLiteral("ExposeAll")); + toggleAction->setText(i18n("Toggle Present Windows (All desktops)")); + toggleAction->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(toggleAction, {defaultToggleShortcutAll}); + KGlobalAccel::self()->setShortcut(toggleAction, {defaultToggleShortcutAll}); + + const QKeySequence defaultToggleShortcutClass = Qt::CTRL | Qt::Key_F7; + toggleAction = actionCollection->addAction(QStringLiteral("ExposeClass")); + toggleAction->setText(i18n("Toggle Present Windows (Window class)")); + toggleAction->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(toggleAction, {defaultToggleShortcutClass}); + KGlobalAccel::self()->setShortcut(toggleAction, {defaultToggleShortcutClass}); + + toggleAction = actionCollection->addAction(QStringLiteral("ExposeClassCurrentDesktop")); + toggleAction->setText(i18n("Toggle Present Windows (Window class on current desktop)")); + toggleAction->setProperty("isConfigurationAction", true); + KGlobalAccel::self()->setDefaultShortcut(toggleAction, QList()); // no default shortcut + KGlobalAccel::self()->setShortcut(toggleAction, QList()); + + ui.shortcutsEditor->addCollection(actionCollection); + connect(ui.shortcutsEditor, &KShortcutsEditor::keyChange, this, &KCModule::markAsChanged); +} + +void WindowViewEffectConfig::save() +{ + KCModule::save(); + ui.shortcutsEditor->save(); + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("windowview")); +} + +void WindowViewEffectConfig::defaults() +{ + ui.shortcutsEditor->allDefault(); + KCModule::defaults(); +} + +} // namespace KWin + +#include "windowvieweffectkcm.moc" + +#include "moc_windowvieweffectkcm.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/windowview/kcm/windowvieweffectkcm.h b/local/recipes/kde/kwin/source/src/plugins/windowview/kcm/windowvieweffectkcm.h new file mode 100644 index 0000000000..87fa341983 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowview/kcm/windowvieweffectkcm.h @@ -0,0 +1,31 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "ui_windowvieweffectkcm.h" + +namespace KWin +{ + +class WindowViewEffectConfig : public KCModule +{ + Q_OBJECT + +public: + explicit WindowViewEffectConfig(QObject *parent, const KPluginMetaData &data); + +public Q_SLOTS: + void save() override; + void defaults() override; + +private: + ::Ui::WindowViewEffectConfig ui; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/windowview/kcm/windowvieweffectkcm.ui b/local/recipes/kde/kwin/source/src/plugins/windowview/kcm/windowvieweffectkcm.ui new file mode 100644 index 0000000000..5f264c5a46 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowview/kcm/windowvieweffectkcm.ui @@ -0,0 +1,51 @@ + + + + WindowViewEffectConfig + + + + 0 + 0 + 455 + 177 + + + + + + + Ignore &minimized windows + + + + + + + + 0 + 0 + + + + KShortcutsEditor::GlobalAction + + + + + + + + KShortcutsEditor + QWidget +
kshortcutseditor.h
+ 1 +
+
+ + +
diff --git a/local/recipes/kde/kwin/source/src/plugins/windowview/main.cpp b/local/recipes/kde/kwin/source/src/plugins/windowview/main.cpp new file mode 100644 index 0000000000..103acac383 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowview/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "windowvieweffect.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(WindowViewEffect, + "metadata.json.stripped", + return WindowViewEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/windowview/metadata.json b/local/recipes/kde/kwin/source/src/plugins/windowview/metadata.json new file mode 100644 index 0000000000..b3a8161962 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowview/metadata.json @@ -0,0 +1,106 @@ +{ + "KPlugin": { + "Category": "Window Management", + "Description": "Zoom out until all opened windows can be displayed side-by-side", + "Description[ar]": "بعِّد إلى أن يصير بالإمكان عرض كل النوافذ المفتوحة جبناً إلى جنب", + "Description[az]": "Bütün açıq pəncərələrin miniatürünü yan-yana göstərir", + "Description[be]": "Маштаб памяншаецца, пакуль усе адкрытыя вокны не будуць адлюстроўвацца побач адно з адным", + "Description[bg]": "Намаляване на мащаба, докато всички отворени прозорци се представят един до друг", + "Description[ca@valencia]": "Reduïx fins que totes les finestres obertes es poden mostrar una al costat de l'altra", + "Description[ca]": "Redueix fins que totes les finestres obertes es poden mostrar una al costat de l'altra", + "Description[cs]": "Oddálí plochu, aby byla vidět všechna okna", + "Description[da]": "Zoom ud indtil alle åbnede vinduer kan vises side om side", + "Description[de]": "Verkleinert Fenster auf der Arbeitsfläche, sodass sie alle nebeneinander sichtbar sind", + "Description[en_GB]": "Zoom out until all opened windows can be displayed side-by-side", + "Description[eo]": "Malzomi ĝis ĉiuj malfermitaj fenestroj povas esti montrataj flank-al-flanke", + "Description[es]": "Reducir hasta que todas las ventanas abiertas se puedan mostrar una al lado de la otra", + "Description[et]": "Vähendamine, kuni kõiki avatud aknaid saab kuvada üksteise kõrval", + "Description[eu]": "Zooma urrundu irekitako leiho guztiak bata bestearen ondoan bistaratu arte", + "Description[fi]": "Loitontaa työpöytää kunnes avoimet ikkunat voi esittää rinnakkain", + "Description[fr]": "Faire un zoom arrière jusqu'à ce que toutes les fenêtres ouvertes puissent être affichées côte à côte", + "Description[gl]": "Reduce ata que poida mostrar lado por lado todas as xanelas.", + "Description[he]": "התרחקות עד שכל החלונות הפתוחים יופיעו זה לצד זה", + "Description[hu]": "Kinagyítja a nézetet, hogy az összes megnyitott ablak áttekinthető legyen", + "Description[ia]": "Zoom foras usque omne fenestras aperite pote esser monstrate flanco a flanco", + "Description[id]": "Zoom keluar hingga semua jendela yang terbuka bisa ditampilkan sisi demi sisi", + "Description[is]": "Minnka aðdrátt þar til allir opnir gluggar birtast hlið við hlið", + "Description[it]": "Arretra per far vedere tutte le finestre aperte fianco a fianco", + "Description[ja]": "ズームアウトしてすべての開かれたウィンドウを並べて表示します", + "Description[ka]": "მასშტაბირება, სანამ ყველა გახსნილი ფანჯარა გვერდიგვერდ არ გამოჩნდება", + "Description[ko]": "모든 열린 창을 축소시켜서 한 화면에 보이도록 합니다", + "Description[lt]": "Sumažinti mastelį, kad visi atverti langai būtų atvaizduojami vienas šalia kito", + "Description[lv]": "Attālina līdz visus atvērtos logus var parādīt vienu otram blakus", + "Description[nb]": "Forminsk skjermflaten helt til alle de åpnede vinduene kan vises side om side", + "Description[nl]": "Zoomt uit totdat alle geopende vensters zij-aan-zij kunnen worden getoond", + "Description[nn]": "Forminsk skjermflata heilt til alle dei opne vindauga kan visast side om side", + "Description[pl]": "Pomniejsza do chwili, aż wszystkie otwarte okna będą widoczne obok siebie", + "Description[pt]": "Reduz até que todas as janelas abertas possam aparecer lado-a-lado", + "Description[pt_BR]": "Reduz até que todas as janelas abertas possam ser apresentadas lado a lado", + "Description[ro]": "Îndepărtează până toate ferestrele deschise se pot afișa una lângă alta", + "Description[ru]": "Просмотр миниатюр всех открытых окон рядом друг с другом", + "Description[sa]": "यावत् सर्वाणि उद्घाटितानि विण्डोः पार्श्वे पार्श्वे प्रदर्शयितुं न शक्यन्ते तावत् यावत् जूम् आउट् कुर्वन्तु", + "Description[sk]": "Oddiali všetky otvorené okná a zobrazí ich vedľa seba", + "Description[sl]": "Oddaljite, dokler se vsa odprta okna ne prikažejo eno poleg drugega", + "Description[sv]": "Zooma ut till alla öppnade fönster kan visas sida vid sida", + "Description[ta]": "அனைத்து சாளரங்களையும் பக்கத்து பக்கத்தில் சிறிதாக்கி காட்டும்", + "Description[tr]": "Tüm açık pencereler yan yana görüntülenebilene dek uzaklaştır", + "Description[uk]": "Зменшення масштабу вікон так, щоб всі відкриті вікна можна було розташувати поряд", + "Description[vi]": "Thu nhỏ cho đến khi tất cả các cửa sổ đang mở hiển thị được cạnh nhau", + "Description[zh_CN]": "所有窗口缩小后平铺显示在一个画面上", + "Description[zh_TW]": "縮小顯示讓所有開啟的視窗都同時顯示", + "EnabledByDefault": true, + "License": "GPL", + "Name": "Present Windows", + "Name[ar]": "النوافذ الحاضرة", + "Name[ast]": "Presentación de ventanes", + "Name[az]": "Hazırkı pəncərələr", + "Name[be]": "Бягучыя вокны", + "Name[bg]": "Представяне на прозорци", + "Name[ca@valencia]": "Presenta les finestres", + "Name[ca]": "Presenta les finestres", + "Name[cs]": "Prezentace oken", + "Name[da]": "Aktuelle vinduer", + "Name[de]": "Fenster zeigen", + "Name[en_GB]": "Present Windows", + "Name[eo]": "Prezenti Fenestrojn", + "Name[es]": "Presentar ventanas", + "Name[et]": "Olemasolevad aknad", + "Name[eu]": "Aurkeztu leihoak", + "Name[fi]": "Ikkunoiden esittäminen", + "Name[fr]": "Fenêtres actuelles", + "Name[gl]": "Dispor as xanelas", + "Name[he]": "הצגת חלונות", + "Name[hu]": "Ablakáttekintő", + "Name[ia]": "Fenestras actual", + "Name[id]": "Present Windows", + "Name[is]": "Opnir gluggar", + "Name[it]": "Presenta le finestre", + "Name[ja]": "ウィンドウを表示", + "Name[ka]": "არსებული ფანჯრები", + "Name[ko]": "창 진열하기", + "Name[lt]": "Pateikti langus", + "Name[lv]": "Parādīt logus", + "Name[nb]": "Presenter vinduer", + "Name[nl]": "Vensters presenteren", + "Name[nn]": "Presenter vindauge", + "Name[pl]": "Prezentacja okien", + "Name[pt]": "Apresentar as Janelas", + "Name[pt_BR]": "Apresentar janelas", + "Name[ro]": "Prezintă ferestrele", + "Name[ru]": "Все окна", + "Name[sa]": "वर्तमान विण्डोज", + "Name[sk]": "Súčasné okná", + "Name[sl]": "Prikaži okna", + "Name[sv]": "Befintliga fönster", + "Name[ta]": "சாளரங்களை முன்வை", + "Name[tr]": "Pencereleri Sun", + "Name[uk]": "Показ вікон", + "Name[vi]": "Các cửa sổ hiện thời", + "Name[zh_CN]": "窗口平铺展示", + "Name[zh_TW]": "展示視窗" + }, + "X-KDE-ConfigModule": "kwin_windowview_config", + "org.kde.kwin.effect": { + "internal": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/windowview/org.kde.KWin.Effect.WindowView1.xml b/local/recipes/kde/kwin/source/src/plugins/windowview/org.kde.KWin.Effect.WindowView1.xml new file mode 100644 index 0000000000..8cd6fb1a57 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowview/org.kde.KWin.Effect.WindowView1.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/windowview/windowviewconfig.kcfg b/local/recipes/kde/kwin/source/src/plugins/windowview/windowviewconfig.kcfg new file mode 100644 index 0000000000..c3108e4e5e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowview/windowviewconfig.kcfg @@ -0,0 +1,27 @@ + + + + + + + false + + + + + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/windowview/windowviewconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/windowview/windowviewconfig.kcfgc new file mode 100644 index 0000000000..1272ae30b4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowview/windowviewconfig.kcfgc @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2021 Vlad Zahorodnii +# +# SPDX-License-Identifier: CC0-1.0 + +File=windowviewconfig.kcfg +ClassName=WindowViewConfig +NameSpace=KWin +Singleton=true +Mutators=true +IncludeFiles=\"effect/globals.h\" diff --git a/local/recipes/kde/kwin/source/src/plugins/windowview/windowvieweffect.cpp b/local/recipes/kde/kwin/source/src/plugins/windowview/windowvieweffect.cpp new file mode 100644 index 0000000000..8cce1d9b5f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowview/windowvieweffect.cpp @@ -0,0 +1,415 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "windowvieweffect.h" +#include "effect/effecthandler.h" +#include "windowview1adaptor.h" +#include "windowviewconfig.h" + +#include +#include +#include + +#include +#include + +using namespace std::chrono_literals; + +namespace KWin +{ + +static const QString s_dbusServiceName = QStringLiteral("org.kde.KWin.Effect.WindowView1"); +static const QString s_dbusObjectPath = QStringLiteral("/org/kde/KWin/Effect/WindowView1"); + +WindowViewEffect::WindowViewEffect() + : m_shutdownTimer(new QTimer(this)) + , m_exposeAction(new QAction(this)) + , m_exposeAllAction(new QAction(this)) + , m_exposeClassAction(new QAction(this)) + , m_exposeClassCurrentDesktopAction(new QAction(this)) +{ + qmlRegisterUncreatableType("org.kde.KWin.Effect.WindowView", 1, 0, "WindowView", QStringLiteral("WindowView cannot be created in QML")); + WindowViewConfig::instance(effects->config()); + new WindowView1Adaptor(this); + + QDBusConnection::sessionBus().registerObject(s_dbusObjectPath, this); + QDBusConnection::sessionBus().registerService(s_dbusServiceName); + + m_shutdownTimer->setSingleShot(true); + connect(m_shutdownTimer, &QTimer::timeout, this, &WindowViewEffect::realDeactivate); + connect(effects, &EffectsHandler::screenAboutToLock, this, &WindowViewEffect::realDeactivate); + + loadFromModule(QStringLiteral("org.kde.kwin.windowview"), QStringLiteral("Main")); + + m_exposeAction->setObjectName(QStringLiteral("Expose")); + m_exposeAction->setText(i18n("Toggle Present Windows (Current desktop)")); + KGlobalAccel::self()->setDefaultShortcut(m_exposeAction, QList() << (Qt::META | Qt::Key_F9) << (Qt::CTRL | Qt::Key_F9)); + KGlobalAccel::self()->setShortcut(m_exposeAction, QList() << (Qt::META | Qt::Key_F9) << (Qt::CTRL | Qt::Key_F9)); + m_shortcut = KGlobalAccel::self()->shortcut(m_exposeAction); + connect(m_exposeAction, &QAction::triggered, this, [this]() { + toggleMode(ModeCurrentDesktop); + }); + + m_exposeAllAction->setObjectName(QStringLiteral("ExposeAll")); + m_exposeAllAction->setText(i18n("Toggle Present Windows (All desktops)")); + KGlobalAccel::self()->setDefaultShortcut(m_exposeAllAction, QList() << (Qt::META | Qt::Key_F10) << Qt::Key_LaunchC << (Qt::CTRL | Qt::Key_F10)); + KGlobalAccel::self()->setShortcut(m_exposeAllAction, QList() << (Qt::META | Qt::Key_F10) << Qt::Key_LaunchC << (Qt::CTRL | Qt::Key_F10)); + m_shortcutAll = KGlobalAccel::self()->shortcut(m_exposeAllAction); + connect(m_exposeAllAction, &QAction::triggered, this, [this]() { + toggleMode(ModeAllDesktops); + }); + + m_exposeClassAction->setObjectName(QStringLiteral("ExposeClass")); + m_exposeClassAction->setText(i18n("Toggle Present Windows (Window class)")); + KGlobalAccel::self()->setDefaultShortcut(m_exposeClassAction, QList() << (Qt::META | Qt::Key_F7) << (Qt::CTRL | Qt::Key_F7)); + KGlobalAccel::self()->setShortcut(m_exposeClassAction, QList() << (Qt::META | Qt::Key_F7) << (Qt::CTRL | Qt::Key_F7)); + m_shortcutClass = KGlobalAccel::self()->shortcut(m_exposeClassAction); + connect(m_exposeClassAction, &QAction::triggered, this, [this]() { + toggleMode(ModeWindowClass); + }); + + m_exposeClassCurrentDesktopAction->setObjectName(QStringLiteral("ExposeClassCurrentDesktop")); + m_exposeClassCurrentDesktopAction->setText(i18n("Toggle Present Windows (Window class on current desktop)")); + KGlobalAccel::self()->setDefaultShortcut(m_exposeClassCurrentDesktopAction, QList()); // no default shortcut + KGlobalAccel::self()->setShortcut(m_exposeClassCurrentDesktopAction, QList()); + m_shortcutClassCurrentDesktop = KGlobalAccel::self()->shortcut(m_exposeClassCurrentDesktopAction); + connect(m_exposeClassCurrentDesktopAction, &QAction::triggered, this, [this]() { + toggleMode(ModeWindowClassCurrentDesktop); + }); + + connect(KGlobalAccel::self(), &KGlobalAccel::globalShortcutChanged, this, [this](QAction *action, const QKeySequence &seq) { + if (action->objectName() == QLatin1StringView("Expose")) { + m_shortcut.clear(); + m_shortcut.append(seq); + } else if (action->objectName() == QLatin1StringView("ExposeAll")) { + m_shortcutAll.clear(); + m_shortcutAll.append(seq); + } else if (action->objectName() == QLatin1StringView("ExposeClass")) { + m_shortcutClass.clear(); + m_shortcutClass.append(seq); + } else if (action->objectName() == QLatin1StringView("ExposeClassCurrentDesktop")) { + m_shortcutClassCurrentDesktop.clear(); + m_shortcutClassCurrentDesktop.append(seq); + } + }); + + m_realtimeToggleAction = new QAction(this); + connect(m_realtimeToggleAction, &QAction::triggered, this, [this]() { + if (m_status == Status::Deactivating) { + if (m_partialActivationFactor < 0.5) { + deactivate(animationDuration()); + } else { + cancelPartialDeactivate(); + } + } else if (m_status == Status::Activating) { + if (m_partialActivationFactor > 0.5) { + activate(); + } else { + cancelPartialActivate(); + } + } + }); + + reconfigure(ReconfigureAll); +} + +WindowViewEffect::~WindowViewEffect() +{ + QDBusConnection::sessionBus().unregisterService(s_dbusServiceName); + QDBusConnection::sessionBus().unregisterObject(s_dbusObjectPath); +} + +int WindowViewEffect::animationDuration() const +{ + return m_animationDuration; +} + +void WindowViewEffect::setAnimationDuration(int duration) +{ + if (m_animationDuration != duration) { + m_animationDuration = duration; + Q_EMIT animationDurationChanged(); + } +} + +bool WindowViewEffect::ignoreMinimized() const +{ + return WindowViewConfig::ignoreMinimized(); +} + +int WindowViewEffect::requestedEffectChainPosition() const +{ + return 70; +} + +void WindowViewEffect::reconfigure(ReconfigureFlags) +{ + WindowViewConfig::self()->read(); + setAnimationDuration(animationTime(300ms)); + + for (ElectricBorder border : std::as_const(m_borderActivate)) { + effects->unreserveElectricBorder(border, this); + } + for (ElectricBorder border : std::as_const(m_borderActivateAll)) { + effects->unreserveElectricBorder(border, this); + } + + m_borderActivate.clear(); + m_borderActivateAll.clear(); + m_borderActivateClass.clear(); + + const auto borderActivate = WindowViewConfig::borderActivate(); + for (int i : borderActivate) { + m_borderActivate.append(ElectricBorder(i)); + effects->reserveElectricBorder(ElectricBorder(i), this); + } + const auto activateAll = WindowViewConfig::borderActivateAll(); + for (int i : activateAll) { + m_borderActivateAll.append(ElectricBorder(i)); + effects->reserveElectricBorder(ElectricBorder(i), this); + } + const auto activateClass = WindowViewConfig::borderActivateClass(); + for (int i : activateClass) { + m_borderActivateClass.append(ElectricBorder(i)); + effects->reserveElectricBorder(ElectricBorder(i), this); + } + const auto activateClassCurrentDesktop = WindowViewConfig::borderActivateClassCurrentDesktop(); + for (int i : activateClassCurrentDesktop) { + m_borderActivateClassCurrentDesktop.append(ElectricBorder(i)); + effects->reserveElectricBorder(ElectricBorder(i), this); + } + + auto touchCallback = [this](ElectricBorder border, const QPointF &deltaProgress, const LogicalOutput *screen) { + if (m_status == Status::Active) { + return; + } + if (m_touchBorderActivate.contains(border)) { + setMode(ModeCurrentDesktop); + } else if (m_touchBorderActivateAll.contains(border)) { + setMode(ModeAllDesktops); + } else if (m_touchBorderActivateClass.contains(border)) { + setMode(ModeWindowClass); + } else if (m_touchBorderActivateClassCurrentDesktop.contains(border)) { + setMode(ModeWindowClassCurrentDesktop); + } + const int maxDelta = 500; // Arbitrary logical pixels value seems to behave better than scaledScreenSize + if (border == ElectricTop || border == ElectricBottom) { + partialActivate(std::min(1.0, std::abs(deltaProgress.y()) / maxDelta)); + } else { + partialActivate(std::min(1.0, std::abs(deltaProgress.x()) / maxDelta)); + } + }; + + QList touchActivateBorders = WindowViewConfig::touchBorderActivate(); + for (const int &border : touchActivateBorders) { + m_touchBorderActivate.append(ElectricBorder(border)); + effects->registerRealtimeTouchBorder(ElectricBorder(border), m_realtimeToggleAction, touchCallback); + } + touchActivateBorders = WindowViewConfig::touchBorderActivateAll(); + for (const int &border : touchActivateBorders) { + m_touchBorderActivateAll.append(ElectricBorder(border)); + effects->registerRealtimeTouchBorder(ElectricBorder(border), m_realtimeToggleAction, touchCallback); + } + touchActivateBorders = WindowViewConfig::touchBorderActivateClass(); + for (const int &border : touchActivateBorders) { + m_touchBorderActivateClass.append(ElectricBorder(border)); + effects->registerRealtimeTouchBorder(ElectricBorder(border), m_realtimeToggleAction, touchCallback); + } + touchActivateBorders = WindowViewConfig::touchBorderActivateClassCurrentDesktop(); + for (const int &border : touchActivateBorders) { + m_touchBorderActivateClassCurrentDesktop.append(ElectricBorder(border)); + effects->registerRealtimeTouchBorder(ElectricBorder(border), m_realtimeToggleAction, touchCallback); + } +} + +qreal WindowViewEffect::partialActivationFactor() const +{ + return m_partialActivationFactor; +} + +void WindowViewEffect::setPartialActivationFactor(qreal factor) +{ + if (m_partialActivationFactor != factor) { + m_partialActivationFactor = factor; + Q_EMIT partialActivationFactorChanged(); + } +} + +bool WindowViewEffect::gestureInProgress() const +{ + return m_gestureInProgress; +} + +void WindowViewEffect::setGestureInProgress(bool gesture) +{ + if (m_gestureInProgress != gesture) { + m_gestureInProgress = gesture; + Q_EMIT gestureInProgressChanged(); + } +} + +void WindowViewEffect::activate(const QStringList &windowIds) +{ + setMode(ModeWindowGroup); + QList internalIds; + internalIds.reserve(windowIds.count()); + for (const QString &windowId : windowIds) { + if (const auto window = effects->findWindow(QUuid(windowId))) { + internalIds.append(window->internalId()); + continue; + } + + // On X11, the task manager can pass a list with X11 ids. + bool ok; + if (const long legacyId = windowId.toLong(&ok); ok) { + if (const auto window = effects->findWindow(legacyId)) { + internalIds.append(window->internalId()); + } + } + } + if (!internalIds.isEmpty()) { + setSelectedIds(internalIds); + m_searchText = QString(); + setRunning(true); + } +} + +void WindowViewEffect::activate() +{ + if (effects->isScreenLocked()) { + return; + } + + m_status = Status::Active; + setSelectedIds(QList()); + + setGestureInProgress(false); + setPartialActivationFactor(0); + + // This one should be the last. + m_searchText = QString(); + setRunning(true); +} + +void WindowViewEffect::partialActivate(qreal factor) +{ + if (effects->isScreenLocked()) { + return; + } + + m_status = Status::Activating; + + setPartialActivationFactor(factor); + setGestureInProgress(true); + + // This one should be the last. + m_searchText = QString(); + setRunning(true); +} + +void WindowViewEffect::cancelPartialActivate() +{ + deactivate(animationDuration()); +} + +void WindowViewEffect::deactivate(int timeout) +{ + const auto screens = effects->screens(); + for (const auto screen : screens) { + if (QuickSceneView *view = viewForScreen(screen)) { + QMetaObject::invokeMethod(view->rootItem(), "stop"); + } + } + m_shutdownTimer->start(timeout); + + setGestureInProgress(false); + setPartialActivationFactor(0.0); +} + +void WindowViewEffect::partialDeactivate(qreal factor) +{ + m_status = Status::Deactivating; + + setPartialActivationFactor(1.0 - factor); + setGestureInProgress(true); +} + +void WindowViewEffect::cancelPartialDeactivate() +{ + activate(); +} + +void WindowViewEffect::realDeactivate() +{ + setRunning(false); + m_status = Status::Inactive; +} + +void WindowViewEffect::setMode(WindowViewEffect::PresentWindowsMode mode) +{ + if (mode == m_mode) { + return; + } + + if (mode != ModeWindowGroup) { + setSelectedIds(QList()); + } + + m_mode = mode; + Q_EMIT modeChanged(); +} + +void WindowViewEffect::toggleMode(PresentWindowsMode mode) +{ + if (!isRunning()) { + setMode(mode); + activate(); + } else { + if (m_mode != mode) { + setMode(mode); + } else { + deactivate(animationDuration()); + } + } +} + +WindowViewEffect::PresentWindowsMode WindowViewEffect::mode() const +{ + return m_mode; +} + +bool WindowViewEffect::borderActivated(ElectricBorder border) +{ + if (effects->activeFullScreenEffect() && effects->activeFullScreenEffect() != this) { + return true; + } + + if (m_borderActivate.contains(border)) { + toggleMode(ModeCurrentDesktop); + } else if (m_borderActivateAll.contains(border)) { + toggleMode(ModeAllDesktops); + } else if (m_borderActivateClass.contains(border)) { + toggleMode(ModeWindowClass); + } else if (m_touchBorderActivateClassCurrentDesktop.contains(border)) { + toggleMode(ModeWindowClassCurrentDesktop); + } else { + return false; + } + + return true; +} + +void WindowViewEffect::setSelectedIds(const QList &ids) +{ + if (m_windowIds != ids) { + m_windowIds = ids; + Q_EMIT selectedIdsChanged(); + } +} + +} // namespace KWin + +#include "moc_windowvieweffect.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/windowview/windowvieweffect.h b/local/recipes/kde/kwin/source/src/plugins/windowview/windowvieweffect.h new file mode 100644 index 0000000000..61450b65b9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/windowview/windowvieweffect.h @@ -0,0 +1,121 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/quickeffect.h" + +#include + +class QAction; + +namespace KWin +{ + +class WindowViewEffect : public QuickSceneEffect +{ + Q_OBJECT + Q_PROPERTY(int animationDuration READ animationDuration NOTIFY animationDurationChanged) + Q_PROPERTY(bool ignoreMinimized READ ignoreMinimized NOTIFY ignoreMinimizedChanged) + Q_PROPERTY(PresentWindowsMode mode READ mode NOTIFY modeChanged) + Q_PROPERTY(qreal partialActivationFactor READ partialActivationFactor NOTIFY partialActivationFactorChanged) + Q_PROPERTY(bool gestureInProgress READ gestureInProgress NOTIFY gestureInProgressChanged) + Q_PROPERTY(QString searchText MEMBER m_searchText NOTIFY searchTextChanged) + Q_PROPERTY(QList selectedIds MEMBER m_windowIds NOTIFY selectedIdsChanged) + +public: + enum PresentWindowsMode { + ModeAllDesktops, // Shows windows of all desktops + ModeCurrentDesktop, // Shows windows on current desktop + ModeWindowGroup, // Shows windows selected via property + ModeWindowClass, // Shows all windows of same class as selected class + ModeWindowClassCurrentDesktop, // Shows windows of same class on current desktop + }; + Q_ENUM(PresentWindowsMode) + + enum class Status { + Inactive, + Activating, + Deactivating, + Active + }; + + WindowViewEffect(); + ~WindowViewEffect() override; + + int animationDuration() const; + void setAnimationDuration(int duration); + + bool ignoreMinimized() const; + + void reconfigure(ReconfigureFlags) override; + int requestedEffectChainPosition() const override; + bool borderActivated(ElectricBorder border) override; + + qreal partialActivationFactor() const; + void setPartialActivationFactor(qreal factor); + + bool gestureInProgress() const; + void setGestureInProgress(bool gesture); + + void setMode(PresentWindowsMode mode); + void toggleMode(PresentWindowsMode mode); + PresentWindowsMode mode() const; + +public Q_SLOTS: + void activate(const QStringList &windowIds); + void activate(); + void deactivate(int timeout); + + void partialActivate(qreal factor); + void cancelPartialActivate(); + void partialDeactivate(qreal factor); + void cancelPartialDeactivate(); + +Q_SIGNALS: + void animationDurationChanged(); + void partialActivationFactorChanged(); + void gestureInProgressChanged(); + void modeChanged(); + void ignoreMinimizedChanged(); + void searchTextChanged(); + void selectedIdsChanged(); + +private: + void realDeactivate(); + void setSelectedIds(const QList &ids); + + QTimer *m_shutdownTimer; + QList m_windowIds; + + // User configuration settings + QAction *m_exposeAction = nullptr; + QAction *m_exposeAllAction = nullptr; + QAction *m_exposeClassAction = nullptr; + QAction *m_exposeClassCurrentDesktopAction = nullptr; + QAction *m_realtimeToggleAction = nullptr; + // Shortcut - needed to toggle the effect + QList m_shortcut; + QList m_shortcutAll; + QList m_shortcutClass; + QList m_shortcutClassCurrentDesktop; + QList m_borderActivate; + QList m_borderActivateAll; + QList m_borderActivateClass; + QList m_borderActivateClassCurrentDesktop; + QList m_touchBorderActivate; + QList m_touchBorderActivateAll; + QList m_touchBorderActivateClass; + QList m_touchBorderActivateClassCurrentDesktop; + QString m_searchText; + Status m_status = Status::Inactive; + qreal m_partialActivationFactor = 0; + PresentWindowsMode m_mode; + int m_animationDuration = 400; + bool m_gestureInProgress = false; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/wobblywindows/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/CMakeLists.txt new file mode 100644 index 0000000000..f37b12f33f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/CMakeLists.txt @@ -0,0 +1,36 @@ +####################################### +# Effect + +set(wobblywindows_SOURCES + main.cpp + wobblywindows.cpp +) + +kconfig_add_kcfg_files(wobblywindows_SOURCES + wobblywindowsconfig.kcfgc +) + +kwin_add_builtin_effect(wobblywindows ${wobblywindows_SOURCES}) +target_link_libraries(wobblywindows PRIVATE + kwin + + KF6::ConfigGui +) + +####################################### +# Config +if (KWIN_BUILD_KCMS) + set(kwin_wobblywindows_config_SRCS wobblywindows_config.cpp) + ki18n_wrap_ui(kwin_wobblywindows_config_SRCS wobblywindows_config.ui) + kconfig_add_kcfg_files(kwin_wobblywindows_config_SRCS wobblywindowsconfig.kcfgc) + + kwin_add_effect_config(kwin_wobblywindows_config ${kwin_wobblywindows_config_SRCS}) + + target_link_libraries(kwin_wobblywindows_config + KF6::KCMUtils + KF6::CoreAddons + KF6::I18n + Qt::DBus + KWinEffectsInterface + ) +endif() diff --git a/local/recipes/kde/kwin/source/src/plugins/wobblywindows/main.cpp b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/main.cpp new file mode 100644 index 0000000000..802bae22a7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "wobblywindows.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(WobblyWindowsEffect, + "metadata.json.stripped", + return WobblyWindowsEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/wobblywindows/metadata.json b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/metadata.json new file mode 100644 index 0000000000..4acb8da71e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/metadata.json @@ -0,0 +1,103 @@ +{ + "KPlugin": { + "Category": "Appearance", + "Description": "Deform windows while they are moving", + "Description[ar]": "تمايل النوافذ أثناء تحركها", + "Description[az]": "Hərəkət etdirildiyində pəncərələr dartılır/yığılır", + "Description[be]": "Дэфармацыя акон падчас перамяшчэння", + "Description[bg]": "Деформиране на прозорците при местене", + "Description[ca@valencia]": "Deforma les finestres quan es mouen", + "Description[ca]": "Deforma les finestres quan es mouen", + "Description[cs]": "Deformace oken při jejich přesouvání", + "Description[da]": "Deformér vinduer mens de flyttes", + "Description[de]": "Lässt Fenster beim Verschieben wackeln", + "Description[en_GB]": "Deform windows while they are moving", + "Description[eo]": "Misformi fenestrojn dum kiam ili moviĝas", + "Description[es]": "Deformar las ventanas mientras se mueven", + "Description[et]": "Akende moonutamine liigutamisel", + "Description[eu]": "Deformatu leihoak mugitzen ari diren bitartean", + "Description[fi]": "Vääristä liikkuvia ikkunoita", + "Description[fr]": "Déformer les fenêtres pendant leur déplacement", + "Description[gl]": "Deforma as xanelas cando se moven.", + "Description[he]": "עיוות החלונות בזמן תזוזתם", + "Description[hu]": "Mozgatás közben deformálja az ablakokat", + "Description[ia]": "Deforma fenestras durante que illes es movente", + "Description[id]": "Lenturkan jendela selagi ia dipindah", + "Description[is]": "Láta glugga verða lina þegar þeir eru á hreyfingu", + "Description[it]": "Deforma le finestre durante il loro spostamento", + "Description[ja]": "ウィンドウを移動時にふにゃふにゃにします", + "Description[ka]": "ფანჯრების დეფორმაცია მათი გადატანისას", + "Description[ko]": "창이 움직일 때 흔듭니다", + "Description[lt]": "Deformuoti langus, kai jie juda", + "Description[lv]": "Transformē logus, kad tos pārvieto", + "Description[nb]": "Deformer vinduer når de flyttes", + "Description[nl]": "Vervormt vensters wanneer ze verplaatst worden", + "Description[nn]": "Deformer vindauga når dei vert flytte", + "Description[pl]": "Odkształca okna przy ich przemieszczaniu", + "Description[pt]": "Deformar as janelas à medida que se movem", + "Description[pt_BR]": "Deforma as janelas enquanto elas são movimentadas", + "Description[ro]": "Deformează ferestrele în timp ce se mută", + "Description[ru]": "Колыхание окна при его перемещении", + "Description[sa]": "गच्छन्तीनां खिडकीनां विकृतीकरणं कुर्वन्तु", + "Description[sk]": "Deformuje okná pri ich presúvaní", + "Description[sl]": "Deformirajte okna, ko se premikajo", + "Description[sv]": "Förvräng fönster medan de flyttas", + "Description[ta]": "சாளரங்களை நகர்த்தும்போது அவற்றை தள்ளாட செய்யும்", + "Description[tr]": "Pencereler taşınırken onları deforme et", + "Description[uk]": "Деформація вікон під час їхнього пересування", + "Description[vi]": "Làm méo các cửa sổ khi chúng di chuyển", + "Description[zh_CN]": "窗口被移动时会呈现惯性晃动特效", + "Description[zh_TW]": "移動視窗時讓它們變形", + "EnabledByDefault": false, + "License": "GPL", + "Name": "Wobbly Windows", + "Name[ar]": "نوافذ متذبذبة", + "Name[ast]": "Ventanes cimblayes", + "Name[az]": "Titrək pəncərələr", + "Name[be]": "Хісткія вокны", + "Name[bg]": "Трептящи прозорци", + "Name[ca@valencia]": "Finestres sacsades", + "Name[ca]": "Finestres sacsejades", + "Name[cs]": "Chvějící se okna", + "Name[da]": "Vaklende vinduer", + "Name[de]": "Wabernde Fenster", + "Name[en_GB]": "Wobbly Windows", + "Name[eo]": "Ŝanceliĝemaj Fenestroj", + "Name[es]": "Ventanas tambaleantes", + "Name[et]": "Võbisevad aknad", + "Name[eu]": "Leiho dardartiak", + "Name[fi]": "Heiluvat ikkunat", + "Name[fr]": "Fenêtres en gélatine", + "Name[gl]": "Xanelas a tremer", + "Name[he]": "חלונות מתנועעים", + "Name[hu]": "Tekergő ablakok", + "Name[ia]": "Fenestras Tremulante", + "Name[id]": "Wobbly Windows", + "Name[is]": "Linir gluggar", + "Name[it]": "Finestre tremolanti", + "Name[ja]": "ふにゃふにゃウィンドウ", + "Name[ka]": "მოკანკალე ფანჯრები", + "Name[ko]": "흔들리는 창", + "Name[lt]": "Svirduliuojantys langai", + "Name[lv]": "Ļodzīgie logi", + "Name[nb]": "Vinglende vinduer", + "Name[nl]": "Wiebelende vensters", + "Name[nn]": "Vaklande vindauge", + "Name[pl]": "Chwiejne okna", + "Name[pt]": "Janelas Trémulas", + "Name[pt_BR]": "Janelas instáveis", + "Name[ro]": "Ferestre tremurătoare", + "Name[ru]": "Колышущиеся окна", + "Name[sa]": "डगमगाती विण्डोज", + "Name[sk]": "Zvlnené okná", + "Name[sl]": "Trepetajoča okna", + "Name[sv]": "Ostadiga fönster", + "Name[ta]": "ஆடும் சாளரங்கள்", + "Name[tr]": "Oynak Pencereler", + "Name[uk]": "Желейні вікна", + "Name[vi]": "Cửa sổ lắc lư", + "Name[zh_CN]": "窗口惯性晃动", + "Name[zh_TW]": "擺動視窗" + }, + "X-KDE-ConfigModule": "kwin_wobblywindows_config" +} diff --git a/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows.cpp b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows.cpp new file mode 100644 index 0000000000..1bd52ff9af --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows.cpp @@ -0,0 +1,1171 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Cédric Borgese + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "wobblywindows.h" +#include "effect/effecthandler.h" +#include "wobblywindowsconfig.h" + +#include + +//#define COMPUTE_STATS + +// if you enable it and run kwin in a terminal from the session it manages, +// be sure to redirect the output of kwin in a file or +// you'll probably get deadlocks. +//#define VERBOSE_MODE + +#if defined COMPUTE_STATS && !defined VERBOSE_MODE +#ifdef __GNUC__ +#warning "You enable COMPUTE_STATS without VERBOSE_MODE, computed stats will not be printed." +#endif +#endif + +Q_LOGGING_CATEGORY(KWIN_WOBBLYWINDOWS, "kwin_effect_wobblywindows", QtWarningMsg) + +namespace KWin +{ + +struct ParameterSet +{ + qreal stiffness; + qreal drag; + qreal move_factor; + + qreal xTessellation; + qreal yTessellation; + + qreal minVelocity; + qreal maxVelocity; + qreal stopVelocity; + qreal minAcceleration; + qreal maxAcceleration; + qreal stopAcceleration; +}; + +static const ParameterSet set_0 = { + 0.15, + 0.80, + 0.10, + 20.0, + 20.0, + 0.0, + 1000.0, + 0.5, + 0.0, + 1000.0, + 0.5, +}; + +static const ParameterSet set_1 = { + 0.10, + 0.85, + 0.10, + 20.0, + 20.0, + 0.0, + 1000.0, + 0.5, + 0.0, + 1000.0, + 0.5, +}; + +static const ParameterSet set_2 = { + 0.06, + 0.90, + 0.10, + 20.0, + 20.0, + 0.0, + 1000.0, + 0.5, + 0.0, + 1000.0, + 0.5, +}; + +static const ParameterSet set_3 = { + 0.03, + 0.92, + 0.20, + 20.0, + 20.0, + 0.0, + 1000.0, + 0.5, + 0.0, + 1000.0, + 0.5, +}; + +static const ParameterSet set_4 = { + 0.01, + 0.97, + 0.25, + 20.0, + 20.0, + 0.0, + 1000.0, + 0.5, + 0.0, + 1000.0, + 0.5, +}; + +static const ParameterSet pset[5] = {set_0, set_1, set_2, set_3, set_4}; + +WobblyWindowsEffect::WobblyWindowsEffect() +{ + WobblyWindowsConfig::instance(effects->config()); + reconfigure(ReconfigureAll); + connect(effects, &EffectsHandler::windowAdded, this, &WobblyWindowsEffect::slotWindowAdded); + + const auto windows = effects->stackingOrder(); + for (EffectWindow *window : windows) { + slotWindowAdded(window); + } +} + +WobblyWindowsEffect::~WobblyWindowsEffect() +{ + if (!windows.empty()) { + // we should be empty at this point... + qCDebug(KWIN_WOBBLYWINDOWS) << "Windows list not empty. Left items : " << windows.count(); + } +} + +void WobblyWindowsEffect::reconfigure(ReconfigureFlags) +{ + WobblyWindowsConfig::self()->read(); + + QString settingsMode = WobblyWindowsConfig::settings(); + if (settingsMode != QStringLiteral("Custom")) { + unsigned int wobblynessLevel = WobblyWindowsConfig::wobblynessLevel(); + if (wobblynessLevel > 4) { + qCDebug(KWIN_WOBBLYWINDOWS) << "Wrong value for \"WobblynessLevel\" : " << wobblynessLevel; + wobblynessLevel = 4; + } + setParameterSet(pset[wobblynessLevel]); + + if (WobblyWindowsConfig::advancedMode()) { + m_stiffness = WobblyWindowsConfig::stiffness() / 100.0; + m_drag = WobblyWindowsConfig::drag() / 100.0; + m_move_factor = WobblyWindowsConfig::moveFactor() / 100.0; + } + } else { // Custom method, read all values from config file. + m_stiffness = WobblyWindowsConfig::stiffness() / 100.0; + m_drag = WobblyWindowsConfig::drag() / 100.0; + m_move_factor = WobblyWindowsConfig::moveFactor() / 100.0; + + m_xTessellation = WobblyWindowsConfig::xTessellation(); + m_yTessellation = WobblyWindowsConfig::yTessellation(); + + m_minVelocity = WobblyWindowsConfig::minVelocity(); + m_maxVelocity = WobblyWindowsConfig::maxVelocity(); + m_stopVelocity = WobblyWindowsConfig::stopVelocity(); + m_minAcceleration = WobblyWindowsConfig::minAcceleration(); + m_maxAcceleration = WobblyWindowsConfig::maxAcceleration(); + m_stopAcceleration = WobblyWindowsConfig::stopAcceleration(); + } + + m_moveWobble = WobblyWindowsConfig::moveWobble(); + m_resizeWobble = WobblyWindowsConfig::resizeWobble(); + +#if defined VERBOSE_MODE + qCDebug(KWIN_WOBBLYWINDOWS) << "Parameters :\n" + << "grid(" << m_stiffness << ", " << m_drag << ", " << m_move_factor << ")\n" + << "velocity(" << m_minVelocity << ", " << m_maxVelocity << ", " << m_stopVelocity << ")\n" + << "acceleration(" << m_minAcceleration << ", " << m_maxAcceleration << ", " << m_stopAcceleration << ")\n" + << "tessellation(" << m_xTessellation << ", " << m_yTessellation << ")"; +#endif +} + +bool WobblyWindowsEffect::supported() +{ + return OffscreenEffect::supported() && effects->animationsSupported(); +} + +void WobblyWindowsEffect::setParameterSet(const ParameterSet &pset) +{ + m_stiffness = pset.stiffness; + m_drag = pset.drag; + m_move_factor = pset.move_factor; + + m_xTessellation = pset.xTessellation; + m_yTessellation = pset.yTessellation; + + m_minVelocity = pset.minVelocity; + m_maxVelocity = pset.maxVelocity; + m_stopVelocity = pset.stopVelocity; + m_minAcceleration = pset.minAcceleration; + m_maxAcceleration = pset.maxAcceleration; + m_stopAcceleration = pset.stopAcceleration; +} + +void WobblyWindowsEffect::setVelocityThreshold(qreal m_minVelocity) +{ + this->m_minVelocity = m_minVelocity; +} + +void WobblyWindowsEffect::setMoveFactor(qreal factor) +{ + m_move_factor = factor; +} + +void WobblyWindowsEffect::setStiffness(qreal stiffness) +{ + m_stiffness = stiffness; +} + +void WobblyWindowsEffect::setDrag(qreal drag) +{ + m_drag = drag; +} + +void WobblyWindowsEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + effects->prePaintScreen(data, presentTime); +} + +static const std::chrono::milliseconds integrationStep(10); + +void WobblyWindowsEffect::prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) +{ + auto infoIt = windows.find(w); + if (infoIt != windows.end()) { + while ((presentTime - infoIt->clock).count() > 0) { + const auto delta = std::min(presentTime - infoIt->clock, integrationStep); + infoIt->clock += delta; + + if (!updateWindowWobblyDatas(w, delta.count())) { + break; + } + } + + if (windows.contains(w)) { + data.setTransformed(); + } + } + + effects->prePaintWindow(view, w, data, presentTime); +} + +void WobblyWindowsEffect::apply(EffectWindow *w, int mask, WindowPaintData &data, WindowQuadList &quads) +{ + if (windows.contains(w)) { + WindowWobblyInfos &wwi = windows[w]; + if (!wwi.wobblying) { + return; + } + + int tx = w->frameGeometry().x(); + int ty = w->frameGeometry().y(); + int width = w->frameGeometry().width(); + int height = w->frameGeometry().height(); + double left = 0.0; + double top = 0.0; + double right = w->width(); + double bottom = w->height(); + + quads = quads.makeRegularGrid(m_xTessellation, m_yTessellation); + for (int i = 0; i < quads.count(); ++i) { + for (int j = 0; j < 4; ++j) { + WindowVertex &v = quads[i][j]; + Pair uv = {v.x() / width, v.y() / height}; + Pair newPos = computeBezierPoint(wwi, uv); + v.move(newPos.x - tx, newPos.y - ty); + } + left = std::min(left, quads[i].left()); + top = std::min(top, quads[i].top()); + right = std::max(right, quads[i].right()); + bottom = std::max(bottom, quads[i].bottom()); + } + QRectF dirtyRect( + left * data.xScale() + w->x() + data.xTranslation(), + top * data.yScale() + w->y() + data.yTranslation(), + (right - left + 1.0) * data.xScale(), + (bottom - top + 1.0) * data.yScale()); + // Expand the dirty region by 1px to fix potential round/floor issues. + dirtyRect.adjust(-1.0, -1.0, 1.0, 1.0); + m_updateRegion = m_updateRegion.united(dirtyRect.toAlignedRect()); + } +} + +void WobblyWindowsEffect::postPaintScreen() +{ + if (!m_updateRegion.isEmpty()) { + effects->addRepaint(m_updateRegion); + m_updateRegion = Region(); + } + + // Call the next effect. + effects->postPaintScreen(); +} + +void WobblyWindowsEffect::slotWindowAdded(EffectWindow *w) +{ + connect(w, &EffectWindow::windowStartUserMovedResized, this, &WobblyWindowsEffect::slotWindowStartUserMovedResized); + connect(w, &EffectWindow::windowStepUserMovedResized, this, &WobblyWindowsEffect::slotWindowStepUserMovedResized); + connect(w, &EffectWindow::windowFinishUserMovedResized, this, &WobblyWindowsEffect::slotWindowFinishUserMovedResized); + connect(w, &EffectWindow::windowMaximizedStateChanged, this, &WobblyWindowsEffect::slotWindowMaximizeStateChanged); +} + +void WobblyWindowsEffect::slotWindowStartUserMovedResized(EffectWindow *w) +{ + if (w->isSpecialWindow()) { + return; + } + + if ((w->isUserMove() && m_moveWobble) || (w->isUserResize() && m_resizeWobble)) { + startMovedResized(w); + } +} + +void WobblyWindowsEffect::slotWindowStepUserMovedResized(EffectWindow *w, const QRectF &geometry) +{ + if (windows.contains(w)) { + WindowWobblyInfos &wwi = windows[w]; + const QRectF rect = w->frameGeometry(); + if (rect.y() != wwi.resize_original_rect.y()) { + wwi.can_wobble_top = true; + } + if (rect.x() != wwi.resize_original_rect.x()) { + wwi.can_wobble_left = true; + } + if (rect.right() != wwi.resize_original_rect.right()) { + wwi.can_wobble_right = true; + } + if (rect.bottom() != wwi.resize_original_rect.bottom()) { + wwi.can_wobble_bottom = true; + } + setVertexSnappingMode(RenderGeometry::VertexSnappingMode::None); + } +} + +void WobblyWindowsEffect::slotWindowFinishUserMovedResized(EffectWindow *w) +{ + if (windows.contains(w)) { + WindowWobblyInfos &wwi = windows[w]; + wwi.status = Free; + const QRectF rect = w->frameGeometry(); + if (rect.y() != wwi.resize_original_rect.y()) { + wwi.can_wobble_top = true; + } + if (rect.x() != wwi.resize_original_rect.x()) { + wwi.can_wobble_left = true; + } + if (rect.right() != wwi.resize_original_rect.right()) { + wwi.can_wobble_right = true; + } + if (rect.bottom() != wwi.resize_original_rect.bottom()) { + wwi.can_wobble_bottom = true; + } + } +} + +void WobblyWindowsEffect::slotWindowMaximizeStateChanged(EffectWindow *w, bool horizontal, bool vertical) +{ + if (w->isUserMove() || w->isSpecialWindow()) { + return; + } + + if (m_moveWobble && m_resizeWobble) { + stepMovedResized(w); + } + + if (windows.contains(w)) { + WindowWobblyInfos &wwi = windows[w]; + const QRectF rect = w->frameGeometry(); + if (rect.y() != wwi.resize_original_rect.y()) { + wwi.can_wobble_top = true; + } + if (rect.x() != wwi.resize_original_rect.x()) { + wwi.can_wobble_left = true; + } + if (rect.right() != wwi.resize_original_rect.right()) { + wwi.can_wobble_right = true; + } + if (rect.bottom() != wwi.resize_original_rect.bottom()) { + wwi.can_wobble_bottom = true; + } + } +} + +void WobblyWindowsEffect::startMovedResized(EffectWindow *w) +{ + if (!windows.contains(w)) { + WindowWobblyInfos new_wwi; + initWobblyInfo(new_wwi, w->frameGeometry()); + windows[w] = new_wwi; + redirect(w); + } + + WindowWobblyInfos &wwi = windows[w]; + wwi.status = Moving; + const QRectF &rect = w->frameGeometry(); + + qreal x_increment = rect.width() / (wwi.width - 1.0); + qreal y_increment = rect.height() / (wwi.height - 1.0); + + Pair picked = {static_cast(cursorPos().x()), static_cast(cursorPos().y())}; + int indx = (picked.x - rect.x()) / x_increment + 0.5; + int indy = (picked.y - rect.y()) / y_increment + 0.5; + int pickedPointIndex = indy * wwi.width + indx; + if (pickedPointIndex < 0) { + qCDebug(KWIN_WOBBLYWINDOWS) << "Picked index == " << pickedPointIndex << " with (" << cursorPos().x() << "," << cursorPos().y() << ")"; + pickedPointIndex = 0; + } else if (static_cast(pickedPointIndex) > wwi.count - 1) { + qCDebug(KWIN_WOBBLYWINDOWS) << "Picked index == " << pickedPointIndex << " with (" << cursorPos().x() << "," << cursorPos().y() << ")"; + pickedPointIndex = wwi.count - 1; + } +#if defined VERBOSE_MODE + qCDebug(KWIN_WOBBLYWINDOWS) << "Original Picked point -- x : " << picked.x << " - y : " << picked.y; +#endif + wwi.constraint[pickedPointIndex] = true; + + if (w->isUserResize()) { + // on a resize, do not allow any edges to wobble until it has been moved from + // its original location + wwi.can_wobble_top = wwi.can_wobble_left = wwi.can_wobble_right = wwi.can_wobble_bottom = false; + wwi.resize_original_rect = w->frameGeometry(); + } else { + wwi.can_wobble_top = wwi.can_wobble_left = wwi.can_wobble_right = wwi.can_wobble_bottom = true; + } +} + +void WobblyWindowsEffect::stepMovedResized(EffectWindow *w) +{ + QRectF new_geometry = w->frameGeometry(); + if (!windows.contains(w)) { + WindowWobblyInfos new_wwi; + initWobblyInfo(new_wwi, new_geometry); + windows[w] = new_wwi; + } + + WindowWobblyInfos &wwi = windows[w]; + wwi.status = Free; + + QRectF maximized_area = effects->clientArea(MaximizeArea, w); + bool throb_direction_out = (new_geometry.top() == maximized_area.top() && new_geometry.bottom() == maximized_area.bottom()) || (new_geometry.left() == maximized_area.left() && new_geometry.right() == maximized_area.right()); + qreal magnitude = throb_direction_out ? 10 : -30; // a small throb out when maximized, a larger throb inwards when restored + for (unsigned int j = 0; j < wwi.height; ++j) { + for (unsigned int i = 0; i < wwi.width; ++i) { + Pair v = {magnitude * (i / qreal(wwi.width - 1) - 0.5), magnitude * (j / qreal(wwi.height - 1) - 0.5)}; + wwi.velocity[j * wwi.width + i] = v; + } + } + + // constrain the middle of the window, so that any asymmetry won't cause it to drift off-center + for (unsigned int j = 1; j < wwi.height - 1; ++j) { + for (unsigned int i = 1; i < wwi.width - 1; ++i) { + wwi.constraint[j * wwi.width + i] = true; + } + } +} + +void WobblyWindowsEffect::initWobblyInfo(WindowWobblyInfos &wwi, QRectF geometry) const +{ + wwi.count = 4 * 4; + wwi.width = 4; + wwi.height = 4; + + wwi.bezierWidth = m_xTessellation; + wwi.bezierHeight = m_yTessellation; + wwi.bezierCount = m_xTessellation * m_yTessellation; + + wwi.origin.resize(wwi.count); + wwi.position.resize(wwi.count); + wwi.velocity.resize(wwi.count); + wwi.acceleration.resize(wwi.count); + wwi.buffer.resize(wwi.count); + wwi.constraint.resize(wwi.count); + + wwi.bezierSurface.resize(wwi.bezierCount); + + wwi.status = Moving; + wwi.clock = std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()); + + qreal x = geometry.x(), y = geometry.y(); + qreal width = geometry.width(), height = geometry.height(); + + Pair initValue = {x, y}; + static const Pair nullPair = {0.0, 0.0}; + + qreal x_increment = width / (wwi.width - 1.0); + qreal y_increment = height / (wwi.height - 1.0); + + for (unsigned int j = 0; j < 4; ++j) { + for (unsigned int i = 0; i < 4; ++i) { + unsigned int idx = j * 4 + i; + wwi.origin[idx] = initValue; + wwi.position[idx] = initValue; + wwi.velocity[idx] = nullPair; + wwi.constraint[idx] = false; + if (i != 4 - 2) { // x grid count - 2, i.e. not the last point + initValue.x += x_increment; + } else { + initValue.x = width + x; + } + initValue.x = initValue.x; + } + initValue.x = x; + initValue.x = initValue.x; + if (j != 4 - 2) { // y grid count - 2, i.e. not the last point + initValue.y += y_increment; + } else { + initValue.y = height + y; + } + initValue.y = initValue.y; + } +} + +WobblyWindowsEffect::Pair WobblyWindowsEffect::computeBezierPoint(const WindowWobblyInfos &wwi, Pair point) const +{ + const qreal tx = point.x; + const qreal ty = point.y; + + // compute polynomial coeff + + qreal px[4]; + px[0] = (1 - tx) * (1 - tx) * (1 - tx); + px[1] = 3 * (1 - tx) * (1 - tx) * tx; + px[2] = 3 * (1 - tx) * tx * tx; + px[3] = tx * tx * tx; + + qreal py[4]; + py[0] = (1 - ty) * (1 - ty) * (1 - ty); + py[1] = 3 * (1 - ty) * (1 - ty) * ty; + py[2] = 3 * (1 - ty) * ty * ty; + py[3] = ty * ty * ty; + + Pair res = {0.0, 0.0}; + + for (unsigned int j = 0; j < 4; ++j) { + for (unsigned int i = 0; i < 4; ++i) { + // this assume the grid is 4*4 + res.x += px[i] * py[j] * wwi.position[i + j * wwi.width].x; + res.y += px[i] * py[j] * wwi.position[i + j * wwi.width].y; + } + } + + return res; +} + +namespace +{ + +static inline void fixVectorBounds(WobblyWindowsEffect::Pair &vec, qreal min, qreal max) +{ + if (fabs(vec.x) < min) { + vec.x = 0.0; + } else if (fabs(vec.x) > max) { + if (vec.x > 0.0) { + vec.x = max; + } else { + vec.x = -max; + } + } + + if (fabs(vec.y) < min) { + vec.y = 0.0; + } else if (fabs(vec.y) > max) { + if (vec.y > 0.0) { + vec.y = max; + } else { + vec.y = -max; + } + } +} + +#if defined COMPUTE_STATS +static inline void computeVectorBounds(WobblyWindowsEffect::Pair &vec, WobblyWindowsEffect::Pair &bound) +{ + if (fabs(vec.x) < bound.x) { + bound.x = fabs(vec.x); + } else if (fabs(vec.x) > bound.y) { + bound.y = fabs(vec.x); + } + if (fabs(vec.y) < bound.x) { + bound.x = fabs(vec.y); + } else if (fabs(vec.y) > bound.y) { + bound.y = fabs(vec.y); + } +} +#endif + +} // close the anonymous namespace + +bool WobblyWindowsEffect::updateWindowWobblyDatas(EffectWindow *w, qreal time) +{ + QRectF rect = w->frameGeometry(); + WindowWobblyInfos &wwi = windows[w]; + + qreal x_length = rect.width() / (wwi.width - 1.0); + qreal y_length = rect.height() / (wwi.height - 1.0); + +#if defined VERBOSE_MODE + qCDebug(KWIN_WOBBLYWINDOWS) << "time " << time; + qCDebug(KWIN_WOBBLYWINDOWS) << "increment x " << x_length << " // y" << y_length; +#endif + + Pair origine = {rect.x(), rect.y()}; + + for (unsigned int j = 0; j < wwi.height; ++j) { + for (unsigned int i = 0; i < wwi.width; ++i) { + wwi.origin[wwi.width * j + i] = origine; + if (i != wwi.width - 2) { + origine.x += x_length; + } else { + origine.x = rect.width() + rect.x(); + } + } + origine.x = rect.x(); + if (j != wwi.height - 2) { + origine.y += y_length; + } else { + origine.y = rect.height() + rect.y(); + } + } + + Pair neighbors[4]; + Pair acceleration; + + qreal acc_sum = 0.0; + qreal vel_sum = 0.0; + + // compute acceleration, velocity and position for each point + + // for corners + + // top-left + + if (wwi.constraint[0]) { + Pair window_pos = wwi.origin[0]; + Pair current_pos = wwi.position[0]; + Pair move = {window_pos.x - current_pos.x, window_pos.y - current_pos.y}; + Pair accel = {move.x * m_stiffness, move.y * m_stiffness}; + wwi.acceleration[0] = accel; + } else { + Pair &pos = wwi.position[0]; + neighbors[0] = wwi.position[1]; + neighbors[1] = wwi.position[wwi.width]; + + acceleration.x = ((neighbors[0].x - pos.x) - x_length) * m_stiffness + (neighbors[1].x - pos.x) * m_stiffness; + acceleration.y = ((neighbors[1].y - pos.y) - y_length) * m_stiffness + (neighbors[0].y - pos.y) * m_stiffness; + + acceleration.x /= 2; + acceleration.y /= 2; + + wwi.acceleration[0] = acceleration; + } + + // top-right + + if (wwi.constraint[wwi.width - 1]) { + Pair window_pos = wwi.origin[wwi.width - 1]; + Pair current_pos = wwi.position[wwi.width - 1]; + Pair move = {window_pos.x - current_pos.x, window_pos.y - current_pos.y}; + Pair accel = {move.x * m_stiffness, move.y * m_stiffness}; + wwi.acceleration[wwi.width - 1] = accel; + } else { + Pair &pos = wwi.position[wwi.width - 1]; + neighbors[0] = wwi.position[wwi.width - 2]; + neighbors[1] = wwi.position[2 * wwi.width - 1]; + + acceleration.x = (x_length - (pos.x - neighbors[0].x)) * m_stiffness + (neighbors[1].x - pos.x) * m_stiffness; + acceleration.y = ((neighbors[1].y - pos.y) - y_length) * m_stiffness + (neighbors[0].y - pos.y) * m_stiffness; + + acceleration.x /= 2; + acceleration.y /= 2; + + wwi.acceleration[wwi.width - 1] = acceleration; + } + + // bottom-left + + if (wwi.constraint[wwi.width * (wwi.height - 1)]) { + Pair window_pos = wwi.origin[wwi.width * (wwi.height - 1)]; + Pair current_pos = wwi.position[wwi.width * (wwi.height - 1)]; + Pair move = {window_pos.x - current_pos.x, window_pos.y - current_pos.y}; + Pair accel = {move.x * m_stiffness, move.y * m_stiffness}; + wwi.acceleration[wwi.width * (wwi.height - 1)] = accel; + } else { + Pair &pos = wwi.position[wwi.width * (wwi.height - 1)]; + neighbors[0] = wwi.position[wwi.width * (wwi.height - 1) + 1]; + neighbors[1] = wwi.position[wwi.width * (wwi.height - 2)]; + + acceleration.x = ((neighbors[0].x - pos.x) - x_length) * m_stiffness + (neighbors[1].x - pos.x) * m_stiffness; + acceleration.y = (y_length - (pos.y - neighbors[1].y)) * m_stiffness + (neighbors[0].y - pos.y) * m_stiffness; + + acceleration.x /= 2; + acceleration.y /= 2; + + wwi.acceleration[wwi.width * (wwi.height - 1)] = acceleration; + } + + // bottom-right + + if (wwi.constraint[wwi.count - 1]) { + Pair window_pos = wwi.origin[wwi.count - 1]; + Pair current_pos = wwi.position[wwi.count - 1]; + Pair move = {window_pos.x - current_pos.x, window_pos.y - current_pos.y}; + Pair accel = {move.x * m_stiffness, move.y * m_stiffness}; + wwi.acceleration[wwi.count - 1] = accel; + } else { + Pair &pos = wwi.position[wwi.count - 1]; + neighbors[0] = wwi.position[wwi.count - 2]; + neighbors[1] = wwi.position[wwi.width * (wwi.height - 1) - 1]; + + acceleration.x = (x_length - (pos.x - neighbors[0].x)) * m_stiffness + (neighbors[1].x - pos.x) * m_stiffness; + acceleration.y = (y_length - (pos.y - neighbors[1].y)) * m_stiffness + (neighbors[0].y - pos.y) * m_stiffness; + + acceleration.x /= 2; + acceleration.y /= 2; + + wwi.acceleration[wwi.count - 1] = acceleration; + } + + // for borders + + // top border + for (unsigned int i = 1; i < wwi.width - 1; ++i) { + if (wwi.constraint[i]) { + Pair window_pos = wwi.origin[i]; + Pair current_pos = wwi.position[i]; + Pair move = {window_pos.x - current_pos.x, window_pos.y - current_pos.y}; + Pair accel = {move.x * m_stiffness, move.y * m_stiffness}; + wwi.acceleration[i] = accel; + } else { + Pair &pos = wwi.position[i]; + neighbors[0] = wwi.position[i - 1]; + neighbors[1] = wwi.position[i + 1]; + neighbors[2] = wwi.position[i + wwi.width]; + + acceleration.x = (x_length - (pos.x - neighbors[0].x)) * m_stiffness + ((neighbors[1].x - pos.x) - x_length) * m_stiffness + (neighbors[2].x - pos.x) * m_stiffness; + acceleration.y = ((neighbors[2].y - pos.y) - y_length) * m_stiffness + (neighbors[0].y - pos.y) * m_stiffness + (neighbors[1].y - pos.y) * m_stiffness; + + acceleration.x /= 3; + acceleration.y /= 3; + + wwi.acceleration[i] = acceleration; + } + } + + // bottom border + for (unsigned int i = wwi.width * (wwi.height - 1) + 1; i < wwi.count - 1; ++i) { + if (wwi.constraint[i]) { + Pair window_pos = wwi.origin[i]; + Pair current_pos = wwi.position[i]; + Pair move = {window_pos.x - current_pos.x, window_pos.y - current_pos.y}; + Pair accel = {move.x * m_stiffness, move.y * m_stiffness}; + wwi.acceleration[i] = accel; + } else { + Pair &pos = wwi.position[i]; + neighbors[0] = wwi.position[i - 1]; + neighbors[1] = wwi.position[i + 1]; + neighbors[2] = wwi.position[i - wwi.width]; + + acceleration.x = (x_length - (pos.x - neighbors[0].x)) * m_stiffness + ((neighbors[1].x - pos.x) - x_length) * m_stiffness + (neighbors[2].x - pos.x) * m_stiffness; + acceleration.y = (y_length - (pos.y - neighbors[2].y)) * m_stiffness + (neighbors[0].y - pos.y) * m_stiffness + (neighbors[1].y - pos.y) * m_stiffness; + + acceleration.x /= 3; + acceleration.y /= 3; + + wwi.acceleration[i] = acceleration; + } + } + + // left border + for (unsigned int i = wwi.width; i < wwi.width * (wwi.height - 1); i += wwi.width) { + if (wwi.constraint[i]) { + Pair window_pos = wwi.origin[i]; + Pair current_pos = wwi.position[i]; + Pair move = {window_pos.x - current_pos.x, window_pos.y - current_pos.y}; + Pair accel = {move.x * m_stiffness, move.y * m_stiffness}; + wwi.acceleration[i] = accel; + } else { + Pair &pos = wwi.position[i]; + neighbors[0] = wwi.position[i + 1]; + neighbors[1] = wwi.position[i - wwi.width]; + neighbors[2] = wwi.position[i + wwi.width]; + + acceleration.x = ((neighbors[0].x - pos.x) - x_length) * m_stiffness + (neighbors[1].x - pos.x) * m_stiffness + (neighbors[2].x - pos.x) * m_stiffness; + acceleration.y = (y_length - (pos.y - neighbors[1].y)) * m_stiffness + ((neighbors[2].y - pos.y) - y_length) * m_stiffness + (neighbors[0].y - pos.y) * m_stiffness; + + acceleration.x /= 3; + acceleration.y /= 3; + + wwi.acceleration[i] = acceleration; + } + } + + // right border + for (unsigned int i = 2 * wwi.width - 1; i < wwi.count - 1; i += wwi.width) { + if (wwi.constraint[i]) { + Pair window_pos = wwi.origin[i]; + Pair current_pos = wwi.position[i]; + Pair move = {window_pos.x - current_pos.x, window_pos.y - current_pos.y}; + Pair accel = {move.x * m_stiffness, move.y * m_stiffness}; + wwi.acceleration[i] = accel; + } else { + Pair &pos = wwi.position[i]; + neighbors[0] = wwi.position[i - 1]; + neighbors[1] = wwi.position[i - wwi.width]; + neighbors[2] = wwi.position[i + wwi.width]; + + acceleration.x = (x_length - (pos.x - neighbors[0].x)) * m_stiffness + (neighbors[1].x - pos.x) * m_stiffness + (neighbors[2].x - pos.x) * m_stiffness; + acceleration.y = (y_length - (pos.y - neighbors[1].y)) * m_stiffness + ((neighbors[2].y - pos.y) - y_length) * m_stiffness + (neighbors[0].y - pos.y) * m_stiffness; + + acceleration.x /= 3; + acceleration.y /= 3; + + wwi.acceleration[i] = acceleration; + } + } + + // for the inner points + for (unsigned int j = 1; j < wwi.height - 1; ++j) { + for (unsigned int i = 1; i < wwi.width - 1; ++i) { + unsigned int index = i + j * wwi.width; + + if (wwi.constraint[index]) { + Pair window_pos = wwi.origin[index]; + Pair current_pos = wwi.position[index]; + Pair move = {window_pos.x - current_pos.x, window_pos.y - current_pos.y}; + Pair accel = {move.x * m_stiffness, move.y * m_stiffness}; + wwi.acceleration[index] = accel; + } else { + Pair &pos = wwi.position[index]; + neighbors[0] = wwi.position[index - 1]; + neighbors[1] = wwi.position[index + 1]; + neighbors[2] = wwi.position[index - wwi.width]; + neighbors[3] = wwi.position[index + wwi.width]; + + acceleration.x = ((neighbors[0].x - pos.x) - x_length) * m_stiffness + (x_length - (pos.x - neighbors[1].x)) * m_stiffness + (neighbors[2].x - pos.x) * m_stiffness + (neighbors[3].x - pos.x) * m_stiffness; + acceleration.y = (y_length - (pos.y - neighbors[2].y)) * m_stiffness + ((neighbors[3].y - pos.y) - y_length) * m_stiffness + (neighbors[0].y - pos.y) * m_stiffness + (neighbors[1].y - pos.y) * m_stiffness; + + acceleration.x /= 4; + acceleration.y /= 4; + + wwi.acceleration[index] = acceleration; + } + } + } + + heightRingLinearMean(wwi.acceleration, wwi); + +#if defined COMPUTE_STATS + Pair accBound = {m_maxAcceleration, m_minAcceleration}; + Pair velBound = {m_maxVelocity, m_minVelocity}; +#endif + + // compute the new velocity of each vertex. + for (unsigned int i = 0; i < wwi.count; ++i) { + Pair acc = wwi.acceleration[i]; + fixVectorBounds(acc, m_minAcceleration, m_maxAcceleration); + +#if defined COMPUTE_STATS + computeVectorBounds(acc, accBound); +#endif + + Pair &vel = wwi.velocity[i]; + vel.x = acc.x * time + vel.x * m_drag; + vel.y = acc.y * time + vel.y * m_drag; + + acc_sum += fabs(acc.x) + fabs(acc.y); + } + + heightRingLinearMean(wwi.velocity, wwi); + + // compute the new pos of each vertex. + for (unsigned int i = 0; i < wwi.count; ++i) { + Pair &pos = wwi.position[i]; + Pair &vel = wwi.velocity[i]; + + fixVectorBounds(vel, m_minVelocity, m_maxVelocity); +#if defined COMPUTE_STATS + computeVectorBounds(vel, velBound); +#endif + + pos.x += vel.x * time * m_move_factor; + pos.y += vel.y * time * m_move_factor; + + vel_sum += fabs(vel.x) + fabs(vel.y); + +#if defined VERBOSE_MODE + if (wwi.constraint[i]) { + qCDebug(KWIN_WOBBLYWINDOWS) << "Constraint point ** vel : " << vel.x << "," << vel.y << " ** move : " << vel.x * time << "," << vel.y * time; + } +#endif + } + + if (!wwi.can_wobble_top) { + for (unsigned int i = 0; i < wwi.width; ++i) { + for (unsigned j = 0; j < wwi.width - 1; ++j) { + wwi.position[i + wwi.width * j].y = wwi.origin[i + wwi.width * j].y; + } + } + } + if (!wwi.can_wobble_bottom) { + for (unsigned int i = wwi.width * (wwi.height - 1); i < wwi.count; ++i) { + for (unsigned j = 0; j < wwi.width - 1; ++j) { + wwi.position[i - wwi.width * j].y = wwi.origin[i - wwi.width * j].y; + } + } + } + if (!wwi.can_wobble_left) { + for (unsigned int i = 0; i < wwi.count; i += wwi.width) { + for (unsigned j = 0; j < wwi.width - 1; ++j) { + wwi.position[i + j].x = wwi.origin[i + j].x; + } + } + } + if (!wwi.can_wobble_right) { + for (unsigned int i = wwi.width - 1; i < wwi.count; i += wwi.width) { + for (unsigned j = 0; j < wwi.width - 1; ++j) { + wwi.position[i - j].x = wwi.origin[i - j].x; + } + } + } + +#if defined VERBOSE_MODE +#if defined COMPUTE_STATS + qCDebug(KWIN_WOBBLYWINDOWS) << "Acceleration bounds (" << accBound.x << ", " << accBound.y << ")"; + qCDebug(KWIN_WOBBLYWINDOWS) << "Velocity bounds (" << velBound.x << ", " << velBound.y << ")"; +#endif + qCDebug(KWIN_WOBBLYWINDOWS) << "sum_acc : " << acc_sum << " *** sum_vel :" << vel_sum; +#endif + + wwi.wobblying = !(acc_sum < m_stopAcceleration && vel_sum < m_stopVelocity); + if (wwi.status != Moving && !wwi.wobblying) { + windows.remove(w); + unredirect(w); + if (windows.isEmpty()) { + effects->addRepaintFull(); + } + return false; + } else if (!wwi.wobblying) { + setVertexSnappingMode(RenderGeometry::VertexSnappingMode::Round); + } + + return true; +} + +void WobblyWindowsEffect::heightRingLinearMean(QList &data, WindowWobblyInfos &wwi) +{ + Pair neighbors[8]; + + // for corners + + // top-left + { + Pair &res = wwi.buffer[0]; + Pair vit = data[0]; + neighbors[0] = data[1]; + neighbors[1] = data[wwi.width]; + neighbors[2] = data[wwi.width + 1]; + + res.x = (neighbors[0].x + neighbors[1].x + neighbors[2].x + 3.0 * vit.x) / 6.0; + res.y = (neighbors[0].y + neighbors[1].y + neighbors[2].y + 3.0 * vit.y) / 6.0; + } + + // top-right + { + Pair &res = wwi.buffer[wwi.width - 1]; + Pair vit = data[wwi.width - 1]; + neighbors[0] = data[wwi.width - 2]; + neighbors[1] = data[2 * wwi.width - 1]; + neighbors[2] = data[2 * wwi.width - 2]; + + res.x = (neighbors[0].x + neighbors[1].x + neighbors[2].x + 3.0 * vit.x) / 6.0; + res.y = (neighbors[0].y + neighbors[1].y + neighbors[2].y + 3.0 * vit.y) / 6.0; + } + + // bottom-left + { + Pair &res = wwi.buffer[wwi.width * (wwi.height - 1)]; + Pair vit = data[wwi.width * (wwi.height - 1)]; + neighbors[0] = data[wwi.width * (wwi.height - 1) + 1]; + neighbors[1] = data[wwi.width * (wwi.height - 2)]; + neighbors[2] = data[wwi.width * (wwi.height - 2) + 1]; + + res.x = (neighbors[0].x + neighbors[1].x + neighbors[2].x + 3.0 * vit.x) / 6.0; + res.y = (neighbors[0].y + neighbors[1].y + neighbors[2].y + 3.0 * vit.y) / 6.0; + } + + // bottom-right + { + Pair &res = wwi.buffer[wwi.count - 1]; + Pair vit = data[wwi.count - 1]; + neighbors[0] = data[wwi.count - 2]; + neighbors[1] = data[wwi.width * (wwi.height - 1) - 1]; + neighbors[2] = data[wwi.width * (wwi.height - 1) - 2]; + + res.x = (neighbors[0].x + neighbors[1].x + neighbors[2].x + 3.0 * vit.x) / 6.0; + res.y = (neighbors[0].y + neighbors[1].y + neighbors[2].y + 3.0 * vit.y) / 6.0; + } + + // for borders + + // top border + for (unsigned int i = 1; i < wwi.width - 1; ++i) { + Pair &res = wwi.buffer[i]; + Pair vit = data[i]; + neighbors[0] = data[i - 1]; + neighbors[1] = data[i + 1]; + neighbors[2] = data[i + wwi.width]; + neighbors[3] = data[i + wwi.width - 1]; + neighbors[4] = data[i + wwi.width + 1]; + + res.x = (neighbors[0].x + neighbors[1].x + neighbors[2].x + neighbors[3].x + neighbors[4].x + 5.0 * vit.x) / 10.0; + res.y = (neighbors[0].y + neighbors[1].y + neighbors[2].y + neighbors[3].y + neighbors[4].y + 5.0 * vit.y) / 10.0; + } + + // bottom border + for (unsigned int i = wwi.width * (wwi.height - 1) + 1; i < wwi.count - 1; ++i) { + Pair &res = wwi.buffer[i]; + Pair vit = data[i]; + neighbors[0] = data[i - 1]; + neighbors[1] = data[i + 1]; + neighbors[2] = data[i - wwi.width]; + neighbors[3] = data[i - wwi.width - 1]; + neighbors[4] = data[i - wwi.width + 1]; + + res.x = (neighbors[0].x + neighbors[1].x + neighbors[2].x + neighbors[3].x + neighbors[4].x + 5.0 * vit.x) / 10.0; + res.y = (neighbors[0].y + neighbors[1].y + neighbors[2].y + neighbors[3].y + neighbors[4].y + 5.0 * vit.y) / 10.0; + } + + // left border + for (unsigned int i = wwi.width; i < wwi.width * (wwi.height - 1); i += wwi.width) { + Pair &res = wwi.buffer[i]; + Pair vit = data[i]; + neighbors[0] = data[i + 1]; + neighbors[1] = data[i - wwi.width]; + neighbors[2] = data[i + wwi.width]; + neighbors[3] = data[i - wwi.width + 1]; + neighbors[4] = data[i + wwi.width + 1]; + + res.x = (neighbors[0].x + neighbors[1].x + neighbors[2].x + neighbors[3].x + neighbors[4].x + 5.0 * vit.x) / 10.0; + res.y = (neighbors[0].y + neighbors[1].y + neighbors[2].y + neighbors[3].y + neighbors[4].y + 5.0 * vit.y) / 10.0; + } + + // right border + for (unsigned int i = 2 * wwi.width - 1; i < wwi.count - 1; i += wwi.width) { + Pair &res = wwi.buffer[i]; + Pair vit = data[i]; + neighbors[0] = data[i - 1]; + neighbors[1] = data[i - wwi.width]; + neighbors[2] = data[i + wwi.width]; + neighbors[3] = data[i - wwi.width - 1]; + neighbors[4] = data[i + wwi.width - 1]; + + res.x = (neighbors[0].x + neighbors[1].x + neighbors[2].x + neighbors[3].x + neighbors[4].x + 5.0 * vit.x) / 10.0; + res.y = (neighbors[0].y + neighbors[1].y + neighbors[2].y + neighbors[3].y + neighbors[4].y + 5.0 * vit.y) / 10.0; + } + + // for the inner points + for (unsigned int j = 1; j < wwi.height - 1; ++j) { + for (unsigned int i = 1; i < wwi.width - 1; ++i) { + unsigned int index = i + j * wwi.width; + + Pair &res = wwi.buffer[index]; + Pair &vit = data[index]; + neighbors[0] = data[index - 1]; + neighbors[1] = data[index + 1]; + neighbors[2] = data[index - wwi.width]; + neighbors[3] = data[index + wwi.width]; + neighbors[4] = data[index - wwi.width - 1]; + neighbors[5] = data[index - wwi.width + 1]; + neighbors[6] = data[index + wwi.width - 1]; + neighbors[7] = data[index + wwi.width + 1]; + + res.x = (neighbors[0].x + neighbors[1].x + neighbors[2].x + neighbors[3].x + neighbors[4].x + neighbors[5].x + neighbors[6].x + neighbors[7].x + 8.0 * vit.x) / 16.0; + res.y = (neighbors[0].y + neighbors[1].y + neighbors[2].y + neighbors[3].y + neighbors[4].y + neighbors[5].y + neighbors[6].y + neighbors[7].y + 8.0 * vit.y) / 16.0; + } + } + + auto tmp = data; + data = wwi.buffer; + wwi.buffer = tmp; +} + +bool WobblyWindowsEffect::isActive() const +{ + return !windows.isEmpty(); +} + +qreal WobblyWindowsEffect::stiffness() const +{ + return m_stiffness; +} + +qreal WobblyWindowsEffect::drag() const +{ + return m_drag; +} + +qreal WobblyWindowsEffect::moveFactor() const +{ + return m_move_factor; +} + +qreal WobblyWindowsEffect::xTessellation() const +{ + return m_xTessellation; +} + +qreal WobblyWindowsEffect::yTessellation() const +{ + return m_yTessellation; +} + +qreal WobblyWindowsEffect::minVelocity() const +{ + return m_minVelocity; +} + +qreal WobblyWindowsEffect::maxVelocity() const +{ + return m_maxVelocity; +} + +qreal WobblyWindowsEffect::stopVelocity() const +{ + return m_stopVelocity; +} + +qreal WobblyWindowsEffect::minAcceleration() const +{ + return m_minAcceleration; +} + +qreal WobblyWindowsEffect::maxAcceleration() const +{ + return m_maxAcceleration; +} + +qreal WobblyWindowsEffect::stopAcceleration() const +{ + return m_stopAcceleration; +} + +bool WobblyWindowsEffect::isMoveWobble() const +{ + return m_moveWobble; +} + +bool WobblyWindowsEffect::isResizeWobble() const +{ + return m_resizeWobble; +} + +} // namespace KWin + +#include "moc_wobblywindows.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows.h b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows.h new file mode 100644 index 0000000000..52ee70af34 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows.h @@ -0,0 +1,170 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Cédric Borgese + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +// Include with base class for effects. +#include "effect/offscreeneffect.h" + +namespace KWin +{ + +struct ParameterSet; + +/** + * Effect which wobble windows + */ +class WobblyWindowsEffect : public OffscreenEffect +{ + Q_OBJECT + Q_PROPERTY(qreal stiffness READ stiffness) + Q_PROPERTY(qreal drag READ drag) + Q_PROPERTY(qreal moveFactor READ moveFactor) + Q_PROPERTY(qreal xTessellation READ xTessellation) + Q_PROPERTY(qreal yTessellation READ yTessellation) + Q_PROPERTY(qreal minVelocity READ minVelocity) + Q_PROPERTY(qreal maxVelocity READ maxVelocity) + Q_PROPERTY(qreal stopVelocity READ stopVelocity) + Q_PROPERTY(qreal minAcceleration READ minAcceleration) + Q_PROPERTY(qreal maxAcceleration READ maxAcceleration) + Q_PROPERTY(qreal stopAcceleration READ stopAcceleration) + Q_PROPERTY(bool moveWobble READ isMoveWobble) + Q_PROPERTY(bool resizeWobble READ isResizeWobble) +public: + WobblyWindowsEffect(); + ~WobblyWindowsEffect() override; + + void reconfigure(ReconfigureFlags) override; + void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override; + void prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) override; + void postPaintScreen() override; + bool isActive() const override; + + int requestedEffectChainPosition() const override + { + // Please notice that the Wobbly Windows effect has to be placed + // after the Maximize effect in the effect chain, otherwise there + // can be visual artifacts when dragging maximized windows. + return 70; + } + + // Wobbly model parameters + void setStiffness(qreal stiffness); + void setDrag(qreal drag); + void setVelocityThreshold(qreal velocityThreshold); + void setMoveFactor(qreal factor); + + struct Pair + { + qreal x; + qreal y; + }; + + enum WindowStatus { + Free, + Moving, + }; + + static bool supported(); + + // for properties + qreal stiffness() const; + qreal drag() const; + qreal moveFactor() const; + qreal xTessellation() const; + qreal yTessellation() const; + qreal minVelocity() const; + qreal maxVelocity() const; + qreal stopVelocity() const; + qreal minAcceleration() const; + qreal maxAcceleration() const; + qreal stopAcceleration() const; + bool isMoveWobble() const; + bool isResizeWobble() const; + +protected: + void apply(EffectWindow *w, int mask, WindowPaintData &data, WindowQuadList &quads) override; + +public Q_SLOTS: + void slotWindowAdded(KWin::EffectWindow *w); + void slotWindowStartUserMovedResized(KWin::EffectWindow *w); + void slotWindowStepUserMovedResized(KWin::EffectWindow *w, const QRectF &geometry); + void slotWindowFinishUserMovedResized(KWin::EffectWindow *w); + void slotWindowMaximizeStateChanged(KWin::EffectWindow *w, bool horizontal, bool vertical); + +private: + void startMovedResized(EffectWindow *w); + void stepMovedResized(EffectWindow *w); + bool updateWindowWobblyDatas(EffectWindow *w, qreal time); + + struct WindowWobblyInfos + { + QList origin; + QList position; + QList velocity; + QList acceleration; + QList buffer; + + // if true, the physics system moves this point based only on it "normal" destination + // given by the window position, ignoring neighbour points. + QList constraint; + + unsigned int width; + unsigned int height; + unsigned int count; + + QList bezierSurface; + unsigned int bezierWidth; + unsigned int bezierHeight; + unsigned int bezierCount; + + WindowStatus status; + bool wobblying = false; + + // for resizing. Only sides that have moved will wobble + bool can_wobble_top, can_wobble_left, can_wobble_right, can_wobble_bottom; + QRectF resize_original_rect; + + std::chrono::milliseconds clock; + }; + + QHash windows; + + Region m_updateRegion; + + qreal m_stiffness; + qreal m_drag; + qreal m_move_factor; + + // the default tessellation for windows + // use qreal instead of int as I really often need + // these values as real to do divisions. + qreal m_xTessellation; + qreal m_yTessellation; + + qreal m_minVelocity; + qreal m_maxVelocity; + qreal m_stopVelocity; + qreal m_minAcceleration; + qreal m_maxAcceleration; + qreal m_stopAcceleration; + + bool m_moveWobble; + bool m_resizeWobble; + + void initWobblyInfo(WindowWobblyInfos &wwi, QRectF geometry) const; + + WobblyWindowsEffect::Pair computeBezierPoint(const WindowWobblyInfos &wwi, Pair point) const; + + static void heightRingLinearMean(QList &data, WindowWobblyInfos &wwi); + + void setParameterSet(const ParameterSet &pset); +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows.kcfg b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows.kcfg new file mode 100644 index 0000000000..7da603f012 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows.kcfg @@ -0,0 +1,57 @@ + + + + + + 0 + + + Auto + + + true + + + true + + + false + + + 15 + + + 80 + + + 10 + + + 20 + + + 20 + + + 0.0 + + + 1000.0 + + + 0.5 + + + 0.0 + + + 1000.0 + + + 5.0 + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows_config.cpp b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows_config.cpp new file mode 100644 index 0000000000..ce61cc0898 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows_config.cpp @@ -0,0 +1,103 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Cédric Borgese + SPDX-FileCopyrightText: 2008 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "wobblywindows_config.h" + +#include "config-kwin.h" + +// KConfigSkeleton +#include "wobblywindowsconfig.h" +#include + +#include +#include +#include + +K_PLUGIN_CLASS(KWin::WobblyWindowsEffectConfig) + +namespace KWin +{ + +//----------------------------------------------------------------------------- +// WARNING: This is (kinda) copied from wobblywindows.cpp + +struct ParameterSet +{ + int stiffness; + int drag; + int move_factor; +}; + +static const ParameterSet set_0 = { + 15, + 80, + 10}; + +static const ParameterSet set_1 = { + 10, + 85, + 10}; + +static const ParameterSet set_2 = { + 6, + 90, + 10}; + +static const ParameterSet set_3 = { + 3, + 92, + 20}; + +static const ParameterSet set_4 = { + 1, + 97, + 25}; + +ParameterSet pset[5] = {set_0, set_1, set_2, set_3, set_4}; + +//----------------------------------------------------------------------------- + +WobblyWindowsEffectConfig::WobblyWindowsEffectConfig(QObject *parent, const KPluginMetaData &data) + : KCModule(parent, data) +{ + WobblyWindowsConfig::instance(KWIN_CONFIG); + m_ui.setupUi(widget()); + + addConfig(WobblyWindowsConfig::self(), widget()); + connect(m_ui.kcfg_WobblynessLevel, &QSlider::valueChanged, this, &WobblyWindowsEffectConfig::wobblinessChanged); +} + +WobblyWindowsEffectConfig::~WobblyWindowsEffectConfig() +{ +} + +void WobblyWindowsEffectConfig::save() +{ + KCModule::save(); + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("wobblywindows")); +} + +void WobblyWindowsEffectConfig::wobblinessChanged() +{ + ParameterSet preset = pset[m_ui.kcfg_WobblynessLevel->value()]; + + m_ui.kcfg_Stiffness->setValue(preset.stiffness); + m_ui.kcfg_Drag->setValue(preset.drag); + m_ui.kcfg_MoveFactor->setValue(preset.move_factor); +} + +} // namespace + +#include "wobblywindows_config.moc" + +#include "moc_wobblywindows_config.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows_config.h b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows_config.h new file mode 100644 index 0000000000..c68c91ce9a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows_config.h @@ -0,0 +1,37 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2008 Cédric Borgese + SPDX-FileCopyrightText: 2008 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "ui_wobblywindows_config.h" + +namespace KWin +{ + +class WobblyWindowsEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit WobblyWindowsEffectConfig(QObject *parent, const KPluginMetaData &data); + ~WobblyWindowsEffectConfig() override; + +public Q_SLOTS: + void save() override; + +private Q_SLOTS: + void wobblinessChanged(); + +private: + ::Ui::WobblyWindowsEffectConfigForm m_ui; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows_config.ui b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows_config.ui new file mode 100644 index 0000000000..0451021e2b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindows_config.ui @@ -0,0 +1,373 @@ + + + WobblyWindowsEffectConfigForm + + + + 0 + 0 + 399 + 229 + + + + + + + false + + + Advanced + + + + + + &Stiffness: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Stiffness + + + + + + + 1 + + + 50 + + + 15 + + + Qt::Horizontal + + + + + + + 1 + + + 50 + + + 15 + + + + + + + Dra&g: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_Drag + + + + + + + &Move factor: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + kcfg_MoveFactor + + + + + + + 50 + + + 100 + + + 85 + + + Qt::Horizontal + + + + + + + 50 + + + 100 + + + 85 + + + + + + + 1 + + + 25 + + + 10 + + + Qt::Horizontal + + + + + + + 1 + + + 25 + + + 10 + + + + + + + + + + Wo&bble when moving + + + + + + + Wobble when &resizing + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + Enable &advanced mode + + + + + + + true + + + &Wobbliness + + + false + + + + + + Less + + + + + + + + 120 + 0 + + + + 4 + + + Qt::Horizontal + + + + + + + More + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + + kcfg_WobblynessLevel + kcfg_MoveWobble + kcfg_ResizeWobble + kcfg_AdvancedMode + kcfg_Stiffness + stiffnessSpin + kcfg_Drag + dragSpin + kcfg_MoveFactor + moveFactorSpin + + + + + kcfg_Stiffness + valueChanged(int) + stiffnessSpin + setValue(int) + + + 304 + 149 + + + 364 + 150 + + + + + stiffnessSpin + valueChanged(int) + kcfg_Stiffness + setValue(int) + + + 364 + 150 + + + 304 + 149 + + + + + kcfg_Drag + valueChanged(int) + dragSpin + setValue(int) + + + 304 + 177 + + + 378 + 180 + + + + + dragSpin + valueChanged(int) + kcfg_Drag + setValue(int) + + + 378 + 180 + + + 304 + 177 + + + + + kcfg_MoveFactor + valueChanged(int) + moveFactorSpin + setValue(int) + + + 304 + 205 + + + 378 + 208 + + + + + moveFactorSpin + valueChanged(int) + kcfg_MoveFactor + setValue(int) + + + 378 + 208 + + + 304 + 205 + + + + + kcfg_AdvancedMode + toggled(bool) + advancedGroup + setEnabled(bool) + + + 249 + 80 + + + 220 + 131 + + + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindowsconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindowsconfig.kcfgc new file mode 100644 index 0000000000..b678137034 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/wobblywindows/wobblywindowsconfig.kcfgc @@ -0,0 +1,5 @@ +File=wobblywindows.kcfg +ClassName=WobblyWindowsConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/local/recipes/kde/kwin/source/src/plugins/zoom/CMakeLists.txt b/local/recipes/kde/kwin/source/src/plugins/zoom/CMakeLists.txt new file mode 100644 index 0000000000..22fcef2bf8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/zoom/CMakeLists.txt @@ -0,0 +1,23 @@ +kwin_add_builtin_effect(zoom) + +target_sources(zoom PRIVATE + focustracker.cpp + main.cpp + textcarettracker.cpp + zoom.cpp + zoom.qrc +) + +kconfig_add_kcfg_files(zoom zoomconfig.kcfgc) + +target_link_libraries(zoom PRIVATE + kwin + + KF6::ConfigGui + KF6::GlobalAccel + KF6::I18n +) + +if (KWIN_BUILD_QACCESSIBILITYCLIENT) + target_link_libraries(zoom PRIVATE QAccessibilityClient6) +endif() diff --git a/local/recipes/kde/kwin/source/src/plugins/zoom/main.cpp b/local/recipes/kde/kwin/source/src/plugins/zoom/main.cpp new file mode 100644 index 0000000000..285175d5eb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/zoom/main.cpp @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "zoom.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY(ZoomEffect, + "metadata.json.stripped") + +} // namespace KWin + +#include "main.moc" diff --git a/local/recipes/kde/kwin/source/src/plugins/zoom/metadata.json b/local/recipes/kde/kwin/source/src/plugins/zoom/metadata.json new file mode 100644 index 0000000000..d0681a0545 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/zoom/metadata.json @@ -0,0 +1,97 @@ +{ + "KPlugin": { + "Category": "Accessibility", + "Description": "Magnify the entire desktop; activated with a keyboard shortcut", + "Description[ar]": "كبّر كلّ سطح المكتب، تفعّل باختصار لوحة المفاتيح", + "Description[bg]": "Увеличаване на целия работен плот, активирано с клавишна комбинация", + "Description[ca@valencia]": "Amplia tot l'escriptori; activat amb una drecera de teclat", + "Description[ca]": "Amplia tot l'escriptori; activat amb una drecera de teclat", + "Description[da]": "Forstører hele skrivebordet; aktiveret med en tastaturgenvej", + "Description[de]": "Vergrößert die gesamte Arbeitsfläche; mit einem Tastatur-Kurzbefehl ausgelöst", + "Description[en_GB]": "Magnify the entire desktop; activated with a keyboard shortcut", + "Description[es]": "Ampliar todo el escritorio; se activa con un atajo de teclado", + "Description[eu]": "Handiagotu mahaigain osoa; teklatuko lasterbide batekin aktibatua", + "Description[fi]": "Suurenna koko työpöytää; käynnistyy pikanäppäimellä", + "Description[fr]": "Agrandir l'ensemble du bureau. Activé grâce à un raccourci clavier.", + "Description[gl]": "Ampliar o escritorio; actívase cun atallo de teclado.", + "Description[he]": "הגדלת המסך כולו, מופעל דרך צירוף מקשי מקלדת", + "Description[hu]": "A teljes asztal nagyítása; gyorsbillentyűvel aktiválható", + "Description[ia]": "Aggrandi le integre scriptorio; activate con un via breve de claviero", + "Description[is]": "Stækka allt skjáborðið; virkjað með flýtilykli", + "Description[it]": "Ingrandisci l'intero desktop; attivato con una scorciatoia da tastiera", + "Description[ka]": "მთელი სამუშაო მაგიდის გადიდება. აქტიურდება კლავიატურის მალსახმობით", + "Description[ko]": "전체 바탕 화면 확대, 키보드 단축키로 활성화", + "Description[lt]": "Padidinti visą darbalaukį; aktyvinama naudojant sparčiuosius klavišus", + "Description[lv]": "Tuvina visu darbvirsmu; ieslēdz ar tastatūras saīsni", + "Description[nb]": "Forstørr hele skrivebordet – slått på/av med snøggtast", + "Description[nl]": "Het gehele bureaublad vergroten; geactiveerd met een sneltoets", + "Description[nn]": "Forstørr heile skrivebordet – slått på/av med snøggtast", + "Description[pl]": "Powiększa cały pulpit; uruchamiane skrótem klawiszowym", + "Description[pt_BR]": "Magnificar toda a área de trabalho; ativado com um atalho de teclado", + "Description[ro]": "Mărește întregul birou; se activează cu o scurtătură de taste", + "Description[ru]": "Увеличение всего рабочего стола; активируется нажатием комбинации клавиш", + "Description[sa]": "सम्पूर्णं डेस्कटॉपं वर्धयन्तु; कीबोर्ड-शॉर्टकट्-सहितं सक्रियम्", + "Description[sk]": "Zväčšiť celú pracovnú plochu; aktivované klávesovou skratkou", + "Description[sl]": "Povečaj celotno namizje; aktivirano z bližnjico tipkovnice", + "Description[sv]": "Förstora hela skrivbordet, aktiveras med en snabbtangent", + "Description[tr]": "Tüm masaüstünü büyüt; klavye kısayoluyla etkinleştirilir", + "Description[uk]": "Збільшення цілої стільниці; активується клавіатурним скороченням", + "Description[zh_CN]": "放大整个桌面;使用键盘快捷键激活", + "Description[zh_TW]": "放大整個桌面——需要用鍵盤快捷鍵觸發", + "EnabledByDefault": true, + "License": "GPL", + "Name": "Zoom", + "Name[ar]": "التكبير", + "Name[ast]": "Zoom", + "Name[az]": "Miqyas", + "Name[be]": "Маштабаванне", + "Name[bg]": "Мащаб", + "Name[ca@valencia]": "Zoom", + "Name[ca]": "Zoom", + "Name[cs]": "Zvětšení", + "Name[da]": "Zoom", + "Name[de]": "Vergrößerung", + "Name[en_GB]": "Zoom", + "Name[eo]": "Zomi", + "Name[es]": "Ampliación", + "Name[et]": "Suurendus", + "Name[eu]": "Handiagotu", + "Name[fi]": "Lähennys", + "Name[fr]": "Zoom", + "Name[ga]": "Zúmáil", + "Name[gl]": "Ampliación", + "Name[he]": "תקריב", + "Name[hu]": "Nagyítás", + "Name[ia]": "Zoom", + "Name[id]": "Zoom", + "Name[is]": "Aðdráttur", + "Name[it]": "Ingrandimento", + "Name[ja]": "ズーム", + "Name[ka]": "გადიდება", + "Name[ko]": "확대/축소", + "Name[lt]": "Didinimas", + "Name[lv]": "Pietuvināt", + "Name[nb]": "Forstørr skrivebordet", + "Name[nl]": "Zoomen", + "Name[nn]": "Forstørr skrivebordet", + "Name[pl]": "Powiększanie", + "Name[pt]": "Ampliação", + "Name[pt_BR]": "Zoom", + "Name[ro]": "Apropiere", + "Name[ru]": "Масштаб", + "Name[sa]": "जूम", + "Name[sk]": "Lupa", + "Name[sl]": "Povečava", + "Name[sv]": "Zooma", + "Name[ta]": "உருப்பெருக்கம்", + "Name[tr]": "Yakınlaştır", + "Name[uk]": "Масштабування", + "Name[vi]": "Thu phóng", + "Name[zh_CN]": "缩放", + "Name[zh_TW]": "縮放" + }, + "org.kde.kwin.effect": { + "exclusiveGroup": "magnifiers", + "internal": true + } +} diff --git a/local/recipes/kde/kwin/source/src/plugins/zoom/shaders/pixelgrid.frag b/local/recipes/kde/kwin/source/src/plugins/zoom/shaders/pixelgrid.frag new file mode 100644 index 0000000000..1e9ee9431f --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/zoom/shaders/pixelgrid.frag @@ -0,0 +1,25 @@ +#include "colormanagement.glsl" + +uniform sampler2D sampler; +uniform int textureWidth; +uniform int textureHeight; + +varying vec2 texcoord0; + +void main() +{ + vec2 texSize = vec2(textureWidth, textureHeight); + vec2 samplePosition = texcoord0 * texSize; + vec2 pixelCenter = floor(samplePosition) + vec2(0.5); + vec2 pixelCenterDistance = abs(samplePosition - pixelCenter); + + vec4 tex; + if (pixelCenterDistance.x > 0.4 || pixelCenterDistance.y > 0.4) { + tex = vec4(0, 0, 0, 1); + } else { + tex = texture2D(sampler, pixelCenter / texSize); + } + + tex = sourceEncodingToNitsInDestinationColorspace(tex); + gl_FragColor = nitsToDestinationEncoding(tex); +} diff --git a/local/recipes/kde/kwin/source/src/plugins/zoom/shaders/pixelgrid_core.frag b/local/recipes/kde/kwin/source/src/plugins/zoom/shaders/pixelgrid_core.frag new file mode 100644 index 0000000000..a1cc7f27f2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/zoom/shaders/pixelgrid_core.frag @@ -0,0 +1,24 @@ +#version 140 + +#include "colormanagement.glsl" + +uniform sampler2D sampler; +uniform int textureWidth; +uniform int textureHeight; + +in vec2 texcoord0; + +out vec4 fragColor; + +void main() +{ + vec2 texSize = vec2(textureWidth, textureHeight); + vec2 samplePosition = texcoord0 * texSize; + vec2 pixelCenter = floor(samplePosition) + vec2(0.5); + vec2 pixelCenterDistance = abs(samplePosition - pixelCenter); + + float t = smoothstep(0.4, 0.5, max(pixelCenterDistance.x, pixelCenterDistance.y)); + vec4 tex = mix(texture(sampler, pixelCenter / texSize), vec4(0, 0, 0, 1), t); + tex = sourceEncodingToNitsInDestinationColorspace(tex); + fragColor = nitsToDestinationEncoding(tex); +} diff --git a/local/recipes/kde/kwin/source/src/plugins/zoom/zoom.cpp b/local/recipes/kde/kwin/source/src/plugins/zoom/zoom.cpp new file mode 100644 index 0000000000..cdfb772f5c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/zoom/zoom.cpp @@ -0,0 +1,725 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2010 Sebastian Sauer + SPDX-FileCopyrightText: 2025 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "zoom.h" +#include "core/rendertarget.h" +#include "core/renderviewport.h" +#include "cursor.h" +#include "effect/effecthandler.h" +#include "focustracker.h" +#include "opengl/glutils.h" +#include "scene/cursoritem.h" +#include "scene/workspacescene.h" +#include "textcarettracker.h" +#include "utils/keys.h" +#include "zoomconfig.h" + +#include +#include +#include +#include + +#include +#include + +using namespace std::chrono_literals; + +static void ensureResources() +{ + // Must initialize resources manually because the effect is a static lib. + Q_INIT_RESOURCE(zoom); +} + +namespace KWin +{ + +ZoomEffect::ZoomEffect() +{ + ensureResources(); + + m_configurationTimer = std::make_unique(); + m_configurationTimer->setInterval(1s); + m_configurationTimer->setSingleShot(true); + connect(m_configurationTimer.get(), &QTimer::timeout, this, &ZoomEffect::saveInitialZoom); + + ZoomConfig::instance(effects->config()); + QAction *a = nullptr; + a = KStandardActions::zoomIn(this, &ZoomEffect::zoomIn, this); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << (Qt::META | Qt::Key_Plus) << (Qt::META | Qt::Key_Equal)); + KGlobalAccel::self()->setShortcut(a, QList() << (Qt::META | Qt::Key_Plus) << (Qt::META | Qt::Key_Equal)); + + a = KStandardActions::zoomOut(this, &ZoomEffect::zoomOut, this); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << (Qt::META | Qt::Key_Minus)); + KGlobalAccel::self()->setShortcut(a, QList() << (Qt::META | Qt::Key_Minus)); + + a = KStandardActions::actualSize(this, &ZoomEffect::actualSize, this); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << (Qt::META | Qt::Key_0)); + KGlobalAccel::self()->setShortcut(a, QList() << (Qt::META | Qt::Key_0)); + + m_touchpadAction = std::make_unique(); + connect(m_touchpadAction.get(), &QAction::triggered, this, [this]() { + const double threshold = 1.15; + if (m_targetZoom < threshold) { + zoomTo(1.0); + } + m_lastPinchProgress = 0; + }); + effects->registerTouchpadPinchShortcut(PinchDirection::Expanding, 3, m_touchpadAction.get(), [this](qreal progress) { + const qreal delta = progress - m_lastPinchProgress; + m_lastPinchProgress = progress; + realtimeZoom(delta); + }); + effects->registerTouchpadPinchShortcut(PinchDirection::Contracting, 3, m_touchpadAction.get(), [this](qreal progress) { + const qreal delta = progress - m_lastPinchProgress; + m_lastPinchProgress = progress; + realtimeZoom(-delta); + }); + + a = new QAction(this); + a->setObjectName(QStringLiteral("MoveZoomLeft")); + a->setText(i18n("Move Zoomed Area to Left")); + KGlobalAccel::self()->setDefaultShortcut(a, QList()); + KGlobalAccel::self()->setShortcut(a, QList()); + connect(a, &QAction::triggered, this, &ZoomEffect::moveZoomLeft); + + a = new QAction(this); + a->setObjectName(QStringLiteral("MoveZoomRight")); + a->setText(i18n("Move Zoomed Area to Right")); + KGlobalAccel::self()->setDefaultShortcut(a, QList()); + KGlobalAccel::self()->setShortcut(a, QList()); + connect(a, &QAction::triggered, this, &ZoomEffect::moveZoomRight); + + a = new QAction(this); + a->setObjectName(QStringLiteral("MoveZoomUp")); + a->setText(i18n("Move Zoomed Area Upwards")); + KGlobalAccel::self()->setDefaultShortcut(a, QList()); + KGlobalAccel::self()->setShortcut(a, QList()); + connect(a, &QAction::triggered, this, &ZoomEffect::moveZoomUp); + + a = new QAction(this); + a->setObjectName(QStringLiteral("MoveZoomDown")); + a->setText(i18n("Move Zoomed Area Downwards")); + KGlobalAccel::self()->setDefaultShortcut(a, QList()); + KGlobalAccel::self()->setShortcut(a, QList()); + connect(a, &QAction::triggered, this, &ZoomEffect::moveZoomDown); + + // TODO: these two actions don't belong into the effect. They need to be moved into KWin core + a = new QAction(this); + a->setObjectName(QStringLiteral("MoveMouseToFocus")); + a->setText(i18n("Move Mouse to Focus")); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << (Qt::META | Qt::Key_F5)); + KGlobalAccel::self()->setShortcut(a, QList() << (Qt::META | Qt::Key_F5)); + connect(a, &QAction::triggered, this, &ZoomEffect::moveMouseToFocus); + + a = new QAction(this); + a->setObjectName(QStringLiteral("MoveMouseToCenter")); + a->setText(i18n("Move Mouse to Center")); + KGlobalAccel::self()->setDefaultShortcut(a, QList() << (Qt::META | Qt::Key_F6)); + KGlobalAccel::self()->setShortcut(a, QList() << (Qt::META | Qt::Key_F6)); + connect(a, &QAction::triggered, this, &ZoomEffect::moveMouseToCenter); + + m_timeline.setDuration(350); + m_timeline.setFrameRange(0, 100); + connect(&m_timeline, &QTimeLine::frameChanged, this, &ZoomEffect::timelineFrameChanged); + connect(effects, &EffectsHandler::windowAdded, this, &ZoomEffect::slotWindowAdded); + connect(effects, &EffectsHandler::screenRemoved, this, &ZoomEffect::slotScreenRemoved); + + const auto windows = effects->stackingOrder(); + for (EffectWindow *w : windows) { + slotWindowAdded(w); + } + + reconfigure(ReconfigureAll); + + const double initialZoom = ZoomConfig::initialZoom(); + if (initialZoom > 1.0) { + zoomTo(initialZoom); + } +} + +ZoomEffect::~ZoomEffect() +{ + // switch off and free resources + showCursor(); + // Save the zoom value. + saveInitialZoom(); +} + +QPointF ZoomEffect::calculateCursorItemPosition() const +{ + return Cursors::self()->mouse()->pos() * m_zoom + QPoint(m_xTranslation, m_yTranslation); +} + +void ZoomEffect::showCursor() +{ + if (m_cursorHidden) { + m_cursorItem.reset(); + m_cursorHidden = false; + effects->showCursor(); + } +} + +void ZoomEffect::hideCursor() +{ + if (m_mouseTracking == MouseTrackingProportional && m_mousePointer == MousePointerKeep) { + return; // don't replace the actual cursor by a static image for no reason. + } + if (!m_cursorHidden) { + effects->hideCursor(); + m_cursorHidden = true; + + if (m_mousePointer == MousePointerKeep || m_mousePointer == MousePointerScale) { + m_cursorItem = std::make_unique(effects->scene()->overlayItem()); + m_cursorItem->setPosition(calculateCursorItemPosition()); + connect(Cursors::self()->mouse(), &Cursor::posChanged, m_cursorItem.get(), [this]() { + m_cursorItem->setPosition(calculateCursorItemPosition()); + }); + } + } +} + +void ZoomEffect::reconfigure(ReconfigureFlags) +{ + ZoomConfig::self()->read(); + // when mouse is set to centered on-screen, turning this on lets the zoom area extend beyond workspace bounds + // On zoom-in and zoom-out change the zoom by the defined zoom-factor. + m_zoomFactor = std::max(0.1, ZoomConfig::zoomFactor()); + m_pixelGridZoom = ZoomConfig::pixelGridZoom(); + // Visibility of the mouse-pointer. + m_mousePointer = MousePointerType(ZoomConfig::mousePointer()); + // Track moving of the mouse. + m_mouseTracking = MouseTrackingType(ZoomConfig::mouseTracking()); + + if (ZoomConfig::enableFocusTracking()) { + if (m_targetZoom > 1) { + trackFocus(); + } + } else { +#if KWIN_BUILD_QACCESSIBILITYCLIENT + m_focusTracker.reset(); +#endif + } + + if (ZoomConfig::enableTextCaretTracking()) { + if (m_targetZoom > 1) { + trackTextCaret(); + } + } else { + m_textCaretTracker.reset(); + } + + if (!ZoomConfig::enableFocusTracking() && !ZoomConfig::enableTextCaretTracking()) { + m_focusPoint.reset(); + } + + // The time in milliseconds to wait before a focus-event takes away a mouse-move. + m_focusDelay = std::max(uint(0), ZoomConfig::focusDelay()); + // The factor the zoom-area will be moved on touching an edge on push-mode or using the navigation KAction's. + m_moveFactor = std::max(0.1, ZoomConfig::moveFactor()); + + const Qt::KeyboardModifiers pointerAxisModifiers = stringToKeyboardModifiers(ZoomConfig::pointerAxisGestureModifiers()); + if (m_axisModifiers != pointerAxisModifiers) { + m_zoomInAxisAction.reset(); + m_zoomOutAxisAction.reset(); + m_axisModifiers = pointerAxisModifiers; + + if (pointerAxisModifiers) { + m_zoomInAxisAction = std::make_unique(); + connect(m_zoomInAxisAction.get(), &QAction::triggered, this, &ZoomEffect::zoomIn); + effects->registerAxisShortcut(pointerAxisModifiers, PointerAxisUp, m_zoomInAxisAction.get()); + + m_zoomOutAxisAction = std::make_unique(); + connect(m_zoomOutAxisAction.get(), &QAction::triggered, this, &ZoomEffect::zoomOut); + effects->registerAxisShortcut(pointerAxisModifiers, PointerAxisDown, m_zoomOutAxisAction.get()); + } + } +} + +void ZoomEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) +{ + data.mask |= PAINT_SCREEN_TRANSFORMED; + if (m_zoom != m_targetZoom) { + int time = 0; + if (m_lastPresentTime.count()) { + time = (presentTime - m_lastPresentTime).count(); + } + m_lastPresentTime = presentTime; + + const float zoomDist = std::abs(m_targetZoom - m_sourceZoom); + if (m_targetZoom > m_zoom) { + m_zoom = std::min(m_zoom + ((zoomDist * time) / animationTime(std::chrono::milliseconds(int(150 * m_zoomFactor)))), m_targetZoom); + } else { + m_zoom = std::max(m_zoom - ((zoomDist * time) / animationTime(std::chrono::milliseconds(int(150 * m_zoomFactor)))), m_targetZoom); + } + } + + if (m_zoom == 1.0) { + m_focusPoint.reset(); + + showCursor(); + } else { + hideCursor(); + if (m_cursorItem && m_mousePointer == MousePointerScale) { + m_cursorItem->setTransform(QTransform::fromScale(m_zoom, m_zoom)); + } + } + + const QSize screenSize = effects->virtualScreenSize(); + + QPoint trackPoint = m_cursorPoint; + + // use the focusPoint if focus tracking is enabled + if (m_focusPoint) { + bool acceptFocus = true; + if (m_mouseTracking != MouseTrackingDisabled && m_focusDelay > 0) { + // Wait some time for the mouse before doing the switch. This serves as threshold + // to prevent the focus from jumping around to much while working with the mouse. + acceptFocus = m_lastMouseEvent.isNull() || m_lastMouseEvent.msecsTo(m_lastFocusEvent) > m_focusDelay; + } + if (acceptFocus) { + trackPoint = *m_focusPoint; + if (m_mouseTracking == MouseTrackingDisabled) { + m_prevPoint = trackPoint; + } + } + } + + // mouse-tracking allows navigation of the zoom-area using the mouse. + switch (m_mouseTracking) { + case MouseTrackingProportional: + m_xTranslation = -int(trackPoint.x() * (m_zoom - 1.0)); + m_yTranslation = -int(trackPoint.y() * (m_zoom - 1.0)); + m_prevPoint = m_cursorPoint; + break; + case MouseTrackingCentered: + m_prevPoint = m_cursorPoint; + m_xTranslation = std::min(0, std::max(int(screenSize.width() - screenSize.width() * m_zoom), int(screenSize.width() / 2 - trackPoint.x() * m_zoom))); + m_yTranslation = std::min(0, std::max(int(screenSize.height() - screenSize.height() * m_zoom), int(screenSize.height() / 2 - trackPoint.y() * m_zoom))); + break; + case MouseTrackingCenteredStrict: + m_prevPoint = m_cursorPoint; + m_xTranslation = int(screenSize.width() / 2 - trackPoint.x() * m_zoom); + m_yTranslation = int(screenSize.height() / 2 - trackPoint.y() * m_zoom); + break; + case MouseTrackingDisabled: + m_xTranslation = std::min(0, std::max(int(screenSize.width() - screenSize.width() * m_zoom), int(screenSize.width() / 2 - m_prevPoint.x() * m_zoom))); + m_yTranslation = std::min(0, std::max(int(screenSize.height() - screenSize.height() * m_zoom), int(screenSize.height() / 2 - m_prevPoint.y() * m_zoom))); + break; + case MouseTrackingPush: { + // touching an edge of the screen moves the zoom-area in that direction. + const int x = trackPoint.x() * m_zoom - m_prevPoint.x() * (m_zoom - 1.0); + const int y = trackPoint.y() * m_zoom - m_prevPoint.y() * (m_zoom - 1.0); + const int threshold = 4; + const RectF currScreen = effects->screenAt(QPoint(x, y))->geometry(); + + // bounds of the screen the cursor's on + const int screenTop = currScreen.top(); + const int screenLeft = currScreen.left(); + const int screenRight = currScreen.right(); + const int screenBottom = currScreen.bottom(); + const int screenCenterX = currScreen.center().x(); + const int screenCenterY = currScreen.center().y(); + + // figure out whether we have adjacent displays in all 4 directions + // We pan within the screen in directions where there are no adjacent screens. + const bool adjacentLeft = screenExistsAt(QPoint(screenLeft - 1, screenCenterY)); + const bool adjacentRight = screenExistsAt(QPoint(screenRight + 1, screenCenterY)); + const bool adjacentTop = screenExistsAt(QPoint(screenCenterX, screenTop - 1)); + const bool adjacentBottom = screenExistsAt(QPoint(screenCenterX, screenBottom + 1)); + + m_xMove = m_yMove = 0; + if (x < screenLeft + threshold && !adjacentLeft) { + m_xMove = (x - threshold - screenLeft) / m_zoom; + } else if (x > screenRight - threshold && !adjacentRight) { + m_xMove = (x + threshold - screenRight) / m_zoom; + } + if (y < screenTop + threshold && !adjacentTop) { + m_yMove = (y - threshold - screenTop) / m_zoom; + } else if (y > screenBottom - threshold && !adjacentBottom) { + m_yMove = (y + threshold - screenBottom) / m_zoom; + } + if (m_xMove) { + m_prevPoint.setX(m_prevPoint.x() + m_xMove); + } + if (m_yMove) { + m_prevPoint.setY(m_prevPoint.y() + m_yMove); + } + m_xTranslation = -int(m_prevPoint.x() * (m_zoom - 1.0)); + m_yTranslation = -int(m_prevPoint.y() * (m_zoom - 1.0)); + break; + } + } + + if (m_cursorItem) { + // x and y translation are changed, update the cursor position + m_cursorItem->setPosition(calculateCursorItemPosition()); + } + + effects->prePaintScreen(data, presentTime); +} + +ZoomEffect::OffscreenData *ZoomEffect::ensureOffscreenData(const RenderTarget &renderTarget, const RenderViewport &viewport, LogicalOutput *screen) +{ + const QSize nativeSize = viewport.deviceSize(); + + // TODO this should be per view, rather than per logical screen. + OffscreenData &data = m_offscreenData[screen]; + data.viewport = viewport.renderRect(); + data.color = renderTarget.colorDescription(); + + const GLenum textureFormat = renderTarget.colorDescription() == ColorDescription::sRGB ? GL_RGBA8 : GL_RGBA16F; + if (!data.texture || data.texture->size() != nativeSize || data.texture->internalFormat() != textureFormat) { + data.texture = GLTexture::allocate(textureFormat, nativeSize); + if (!data.texture) { + return nullptr; + } + data.texture->setFilter(GL_LINEAR); + data.texture->setWrapMode(GL_CLAMP_TO_EDGE); + data.framebuffer = std::make_unique(data.texture.get()); + } + + return &data; +} + +GLShader *ZoomEffect::shaderForZoom(double zoom) +{ + if (zoom < m_pixelGridZoom) { + return ShaderManager::instance()->shader(ShaderTrait::MapTexture | ShaderTrait::TransformColorspace); + } else { + if (!m_pixelGridShader) { + m_pixelGridShader = ShaderManager::instance()->generateShaderFromFile(ShaderTrait::MapTexture, QString(), QStringLiteral(":/effects/zoom/shaders/pixelgrid.frag")); + } + return m_pixelGridShader.get(); + } +} + +void ZoomEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) +{ + OffscreenData *offscreenData = ensureOffscreenData(renderTarget, viewport, screen); + if (!offscreenData) { + return; + } + + // Render the scene in an offscreen texture and then upscale it. + RenderTarget offscreenRenderTarget(offscreenData->framebuffer.get(), renderTarget.colorDescription()); + RenderViewport offscreenViewport(viewport.renderRect(), viewport.scale(), offscreenRenderTarget, QPoint()); + GLFramebuffer::pushFramebuffer(offscreenData->framebuffer.get()); + effects->paintScreen(offscreenRenderTarget, offscreenViewport, mask, deviceRegion, screen); + GLFramebuffer::popFramebuffer(); + + const auto scale = viewport.scale(); + + // Render transformed offscreen texture. + glClearColor(0.0, 0.0, 0.0, 0.0); + glClear(GL_COLOR_BUFFER_BIT); + + GLShader *shader = shaderForZoom(m_zoom); + ShaderManager::instance()->pushShader(shader); + for (auto &[screen, offscreen] : m_offscreenData) { + QMatrix4x4 matrix; + matrix.translate(m_xTranslation * scale, m_yTranslation * scale); + matrix.scale(m_zoom, m_zoom); + matrix.translate(offscreen.viewport.x() * scale, offscreen.viewport.y() * scale); + + shader->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, viewport.projectionMatrix() * matrix); + shader->setUniform(GLShader::IntUniform::TextureWidth, offscreen.texture->width()); + shader->setUniform(GLShader::IntUniform::TextureHeight, offscreen.texture->height()); + shader->setColorspaceUniforms(offscreen.color, renderTarget.colorDescription(), RenderingIntent::Perceptual); + + offscreen.texture->render(offscreen.viewport.size() * scale); + } + ShaderManager::instance()->popShader(); +} + +void ZoomEffect::postPaintScreen() +{ + if (m_zoom == m_targetZoom) { + m_lastPresentTime = std::chrono::milliseconds::zero(); + } + + if (m_zoom == 1.0 || m_zoom != m_targetZoom) { + // Either animation is running or the zoom effect has stopped. + effects->addRepaintFull(); + } + + effects->postPaintScreen(); +} + +void ZoomEffect::zoomIn() +{ + zoomTo(-1.0); +} + +void ZoomEffect::zoomTo(double to) +{ + m_sourceZoom = m_zoom; + if (to < 0.0) { + setTargetZoom(m_targetZoom * m_zoomFactor); + } else { + setTargetZoom(to); + } + m_cursorPoint = effects->cursorPos().toPoint(); + if (m_mouseTracking == MouseTrackingDisabled) { + m_prevPoint = m_cursorPoint; + } +} + +void ZoomEffect::zoomOut() +{ + m_sourceZoom = m_zoom; + setTargetZoom(m_targetZoom / m_zoomFactor); + if ((m_zoomFactor > 1 && m_targetZoom < 1.01) || (m_zoomFactor < 1 && m_targetZoom > 0.99)) { + setTargetZoom(1); + } + if (m_mouseTracking == MouseTrackingDisabled) { + m_prevPoint = effects->cursorPos().toPoint(); + } +} + +void ZoomEffect::actualSize() +{ + m_sourceZoom = m_zoom; + setTargetZoom(1); +} + +void ZoomEffect::timelineFrameChanged(int /* frame */) +{ + const QSize screenSize = effects->virtualScreenSize(); + m_prevPoint.setX(std::max(0, std::min(screenSize.width(), m_prevPoint.x() + m_xMove))); + m_prevPoint.setY(std::max(0, std::min(screenSize.height(), m_prevPoint.y() + m_yMove))); + m_cursorPoint = m_prevPoint; + effects->addRepaintFull(); +} + +void ZoomEffect::moveZoom(int x, int y) +{ + if (m_timeline.state() == QTimeLine::Running) { + m_timeline.stop(); + } + + const QSize screenSize = effects->virtualScreenSize(); + if (x < 0) { + m_xMove = -std::max(1.0, screenSize.width() / m_zoom / m_moveFactor); + } else if (x > 0) { + m_xMove = std::max(1.0, screenSize.width() / m_zoom / m_moveFactor); + } else { + m_xMove = 0; + } + + if (y < 0) { + m_yMove = -std::max(1.0, screenSize.height() / m_zoom / m_moveFactor); + } else if (y > 0) { + m_yMove = std::max(1.0, screenSize.height() / m_zoom / m_moveFactor); + } else { + m_yMove = 0; + } + + m_timeline.start(); +} + +void ZoomEffect::moveZoomLeft() +{ + moveZoom(-1, 0); +} + +void ZoomEffect::moveZoomRight() +{ + moveZoom(1, 0); +} + +void ZoomEffect::moveZoomUp() +{ + moveZoom(0, -1); +} + +void ZoomEffect::moveZoomDown() +{ + moveZoom(0, 1); +} + +void ZoomEffect::moveMouseToFocus() +{ + const auto window = effects->activeWindow(); + if (!window) { + return; + } + const auto center = window->frameGeometry().center(); + QCursor::setPos(center.x(), center.y()); +} + +void ZoomEffect::moveMouseToCenter() +{ + const QRect r = effects->activeScreen()->geometry(); + QCursor::setPos(r.x() + r.width() / 2, r.y() + r.height() / 2); +} + +void ZoomEffect::slotMouseChanged(const QPointF &pos, const QPointF &old) +{ + if (m_zoom == 1.0) { + return; + } + m_cursorPoint = pos.toPoint(); + if (pos != old) { + m_lastMouseEvent = QTime::currentTime(); + effects->addRepaintFull(); + } +} + +void ZoomEffect::slotWindowAdded(EffectWindow *w) +{ + connect(w, &EffectWindow::windowDamaged, this, &ZoomEffect::slotWindowDamaged); +} + +void ZoomEffect::slotWindowDamaged() +{ + if (m_zoom != 1.0) { + effects->addRepaintFull(); + } +} + +void ZoomEffect::slotScreenRemoved(LogicalOutput *screen) +{ + if (auto it = m_offscreenData.find(screen); it != m_offscreenData.end()) { + effects->makeOpenGLContextCurrent(); + m_offscreenData.erase(it); + } +} + +void ZoomEffect::moveFocus(const QPointF &point) +{ + if (m_zoom == 1.0) { + return; + } + m_focusPoint = point.toPoint(); + m_lastFocusEvent = QTime::currentTime(); + effects->addRepaintFull(); +} + +bool ZoomEffect::isActive() const +{ + return m_zoom != 1.0 || m_zoom != m_targetZoom; +} + +int ZoomEffect::requestedEffectChainPosition() const +{ + return 10; +} + +qreal ZoomEffect::configuredZoomFactor() const +{ + return m_zoomFactor; +} + +int ZoomEffect::configuredMousePointer() const +{ + return m_mousePointer; +} + +int ZoomEffect::configuredMouseTracking() const +{ + return m_mouseTracking; +} + +int ZoomEffect::configuredFocusDelay() const +{ + return m_focusDelay; +} + +qreal ZoomEffect::configuredMoveFactor() const +{ + return m_moveFactor; +} + +qreal ZoomEffect::targetZoom() const +{ + return m_targetZoom; +} + +void ZoomEffect::saveInitialZoom() +{ + ZoomConfig::setInitialZoom(m_targetZoom); + ZoomConfig::self()->save(); +} + +bool ZoomEffect::screenExistsAt(const QPoint &point) const +{ + const LogicalOutput *output = effects->screenAt(point); + return output && output->geometry().contains(point); +} + +void ZoomEffect::setTargetZoom(double value) +{ + value = std::clamp(value, 1.0, 100.0); + if (m_targetZoom == value) { + return; + } + const bool newActive = value != 1.0; + const bool oldActive = m_targetZoom != 1.0; + if (newActive && !oldActive) { + if (ZoomConfig::enableTextCaretTracking()) { + trackTextCaret(); + } + if (ZoomConfig::enableFocusTracking()) { + trackFocus(); + } + + connect(effects, &EffectsHandler::mouseChanged, this, &ZoomEffect::slotMouseChanged); + m_cursorPoint = effects->cursorPos().toPoint(); + } else if (!newActive && oldActive) { + m_textCaretTracker.reset(); +#if KWIN_BUILD_QACCESSIBILITYCLIENT + m_focusTracker.reset(); +#endif + disconnect(effects, &EffectsHandler::mouseChanged, this, &ZoomEffect::slotMouseChanged); + } + m_targetZoom = value; + m_configurationTimer->start(); + effects->addRepaintFull(); +} + +void ZoomEffect::realtimeZoom(double delta) +{ + // for the change speed to feel roughly linear, + // we have to increase the delta at higher zoom levels + delta *= m_targetZoom / 2; + setTargetZoom(m_targetZoom + delta); + // skip the animation, we want this to be real time + m_zoom = m_targetZoom; + if (m_zoom == 1.0) { + showCursor(); + } +} + +void ZoomEffect::trackTextCaret() +{ + m_textCaretTracker = std::make_unique(); + connect(m_textCaretTracker.get(), &TextCaretTracker::moved, this, &ZoomEffect::moveFocus); +} + +void ZoomEffect::trackFocus() +{ +#if KWIN_BUILD_QACCESSIBILITYCLIENT + // Dbus-based focus tracking is disabled on wayland because libqaccessibilityclient has + // blocking dbus calls, which can result in kwin_wayland lockups. + + static bool forceFocusTracking = qEnvironmentVariableIntValue("KWIN_WAYLAND_ZOOM_FORCE_LEGACY_FOCUS_TRACKING"); + if (!forceFocusTracking) { + return; + } + + m_focusTracker = std::make_unique(); + connect(m_focusTracker.get(), &FocusTracker::moved, this, &ZoomEffect::moveFocus); +#endif +} + +} // namespace + +#include "moc_zoom.cpp" diff --git a/local/recipes/kde/kwin/source/src/plugins/zoom/zoom.h b/local/recipes/kde/kwin/source/src/plugins/zoom/zoom.h new file mode 100644 index 0000000000..f98630bb2a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/zoom/zoom.h @@ -0,0 +1,156 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2010 Sebastian Sauer + SPDX-FileCopyrightText: 2025 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/colorspace.h" +#include "effect/effect.h" + +#include +#include +#include + +namespace KWin +{ + +class CursorItem; +class GLFramebuffer; +class GLTexture; +class GLVertexBuffer; +class GLShader; +class FocusTracker; +class TextCaretTracker; + +class ZoomEffect : public Effect +{ + Q_OBJECT + Q_PROPERTY(qreal zoomFactor READ configuredZoomFactor) + Q_PROPERTY(int mousePointer READ configuredMousePointer) + Q_PROPERTY(int mouseTracking READ configuredMouseTracking) + Q_PROPERTY(int focusDelay READ configuredFocusDelay) + Q_PROPERTY(qreal moveFactor READ configuredMoveFactor) + Q_PROPERTY(qreal targetZoom READ targetZoom) + +public: + ZoomEffect(); + ~ZoomEffect() override; + + void reconfigure(ReconfigureFlags flags) override; + void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override; + void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) override; + void postPaintScreen() override; + bool isActive() const override; + int requestedEffectChainPosition() const override; + + // for properties + qreal configuredZoomFactor() const; + int configuredMousePointer() const; + int configuredMouseTracking() const; + int configuredFocusDelay() const; + qreal configuredMoveFactor() const; + qreal targetZoom() const; + +private Q_SLOTS: + void saveInitialZoom(); + void zoomIn(); + void zoomTo(double to); + void zoomOut(); + void actualSize(); + void moveZoomLeft(); + void moveZoomRight(); + void moveZoomUp(); + void moveZoomDown(); + void moveMouseToFocus(); + void moveMouseToCenter(); + void timelineFrameChanged(int frame); + void moveFocus(const QPointF &point); + void slotMouseChanged(const QPointF &pos, const QPointF &old); + void slotWindowAdded(EffectWindow *w); + void slotWindowDamaged(); + void slotScreenRemoved(LogicalOutput *screen); + void setTargetZoom(double value); + +private: + enum MouseTrackingType { + MouseTrackingProportional = 0, + MouseTrackingCentered = 1, + MouseTrackingPush = 2, + MouseTrackingDisabled = 3, + MouseTrackingCenteredStrict = 4, + }; + + enum MousePointerType { + MousePointerScale = 0, + MousePointerKeep = 1, + MousePointerHide = 2, + }; + + struct OffscreenData + { + std::unique_ptr texture; + std::unique_ptr framebuffer; + QRectF viewport; + std::shared_ptr color = ColorDescription::sRGB; + }; + + void moveZoom(int x, int y); + bool screenExistsAt(const QPoint &point) const; + void realtimeZoom(double delta); + + QPointF calculateCursorItemPosition() const; + void showCursor(); + void hideCursor(); + GLTexture *ensureCursorTexture(); + OffscreenData *ensureOffscreenData(const RenderTarget &renderTarget, const RenderViewport &viewport, LogicalOutput *screen); + void markCursorTextureDirty(); + + GLShader *shaderForZoom(double zoom); + void trackTextCaret(); + void trackFocus(); + + std::unique_ptr m_configurationTimer; + double m_zoom = 1.0; + double m_targetZoom = 1.0; + double m_sourceZoom = 1.0; + double m_zoomFactor = 1.25; + MouseTrackingType m_mouseTracking = MouseTrackingProportional; + MousePointerType m_mousePointer = MousePointerScale; + QPoint m_cursorPoint; + QPoint m_prevPoint; + QTime m_lastMouseEvent; + std::unique_ptr m_cursorItem; + bool m_cursorHidden = false; + QTimeLine m_timeline; + int m_xMove = 0; + int m_yMove = 0; + int m_xTranslation = 0; + int m_yTranslation = 0; + double m_moveFactor = 20.0; + std::chrono::milliseconds m_lastPresentTime = std::chrono::milliseconds::zero(); + std::map m_offscreenData; + std::unique_ptr m_pixelGridShader; + double m_pixelGridZoom; + std::unique_ptr m_zoomInAxisAction; + std::unique_ptr m_zoomOutAxisAction; + Qt::KeyboardModifiers m_axisModifiers; + std::unique_ptr m_touchpadAction; + double m_lastPinchProgress = 0; + + std::unique_ptr m_textCaretTracker; +#if KWIN_BUILD_QACCESSIBILITYCLIENT + std::unique_ptr m_focusTracker; +#endif + std::optional m_focusPoint = std::nullopt; + QTime m_lastFocusEvent; + int m_focusDelay = 350; // in milliseconds +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/plugins/zoom/zoom.kcfg b/local/recipes/kde/kwin/source/src/plugins/zoom/zoom.kcfg new file mode 100644 index 0000000000..8711e4ca89 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/zoom/zoom.kcfg @@ -0,0 +1,41 @@ + + + + + + + 1.2 + + + 0 + + + 0 + + + false + + + false + + + 350 + + + 20.0 + + + + 1.0 + + + 15.0 + + + Meta+Control + + + diff --git a/local/recipes/kde/kwin/source/src/plugins/zoom/zoom.qrc b/local/recipes/kde/kwin/source/src/plugins/zoom/zoom.qrc new file mode 100644 index 0000000000..56e7369003 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/zoom/zoom.qrc @@ -0,0 +1,6 @@ + + + shaders/pixelgrid.frag + shaders/pixelgrid_core.frag + + diff --git a/local/recipes/kde/kwin/source/src/plugins/zoom/zoomconfig.kcfgc b/local/recipes/kde/kwin/source/src/plugins/zoom/zoomconfig.kcfgc new file mode 100644 index 0000000000..99795831fa --- /dev/null +++ b/local/recipes/kde/kwin/source/src/plugins/zoom/zoomconfig.kcfgc @@ -0,0 +1,5 @@ +File=zoom.kcfg +ClassName=ZoomConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/local/recipes/kde/kwin/source/src/pointer_input.cpp b/local/recipes/kde/kwin/source/src/pointer_input.cpp new file mode 100644 index 0000000000..161cf3d08e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/pointer_input.cpp @@ -0,0 +1,1331 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013, 2016 Martin Gräßlin + SPDX-FileCopyrightText: 2018 Roman Gilg + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + SPDX-FileCopyrightText: 2023 Harald Sitter + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "pointer_input.h" + +#include "config-kwin.h" + +#include "core/output.h" +#include "cursorsource.h" +#include "decorations/decoratedwindow.h" +#include "effect/effecthandler.h" +#include "input_event.h" +#include "input_event_spy.h" +#include "mousebuttons.h" +#include "osd.h" +#include "screenedge.h" +#include "wayland/abstract_data_source.h" +#include "wayland/display.h" +#include "wayland/pointer.h" +#include "wayland/pointerconstraints_v1.h" +#include "wayland/pointerwarp_v1.h" +#include "wayland/seat.h" +#include "wayland/surface.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" +// KDecoration +#include +// screenlocker +#if KWIN_BUILD_SCREENLOCKER +#include +#endif + +#include + +#include +#include +#include + +#include + +#include + +namespace KWin +{ + +static bool screenContainsPos(const QPointF &pos) +{ + const auto outputs = workspace()->outputs(); + for (const LogicalOutput *output : outputs) { + if (output->geometry().contains(flooredPoint(pos))) { + return true; + } + } + return false; +} + +static QPointF confineToBoundingBox(const QPointF &pos, const RectF &boundingBox) +{ + return QPointF( + std::clamp(pos.x(), boundingBox.left(), boundingBox.right() - 1.0), + std::clamp(pos.y(), boundingBox.top(), boundingBox.bottom() - 1.0)); +} + +PointerInputRedirection::PointerInputRedirection(InputRedirection *parent) + : InputDeviceHandler(parent) + , m_cursor(nullptr) +{ +} + +PointerInputRedirection::~PointerInputRedirection() = default; + +CursorTheme PointerInputRedirection::cursorTheme() const +{ + return m_cursor->theme(); +} + +void PointerInputRedirection::init() +{ + Q_ASSERT(!inited()); + waylandServer()->seat()->setHasPointer(input()->hasPointer()); + connect(input(), &InputRedirection::hasPointerChanged, + waylandServer()->seat(), &SeatInterface::setHasPointer); + + m_cursor = new CursorImage(this); + setInited(true); + InputDeviceHandler::init(); + + if (!input()->hasPointer()) { + Cursors::self()->hideCursor(); + } + connect(input(), &InputRedirection::hasPointerChanged, this, []() { + if (input()->hasPointer()) { + Cursors::self()->showCursor(); + } else { + Cursors::self()->hideCursor(); + } + }); + + connect(m_cursor, &CursorImage::changed, Cursors::self()->mouse(), [this] { + Cursors::self()->mouse()->setSource(m_cursor->source()); + m_cursor->updateCursorOutputs(m_pos); + }); + Q_EMIT m_cursor->changed(); + + connect(workspace(), &Workspace::outputsChanged, this, &PointerInputRedirection::updateAfterScreenChange); +#if KWIN_BUILD_SCREENLOCKER + if (kwinApp()->supportsLockScreen()) { + connect(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged, this, [this]() { + if (waylandServer()->seat()->hasPointer()) { + waylandServer()->seat()->cancelPointerPinchGesture(); + waylandServer()->seat()->cancelPointerSwipeGesture(); + } + update(); + }); + } +#endif + connect(workspace(), &QObject::destroyed, this, [this] { + setInited(false); + }); + connect(waylandServer(), &QObject::destroyed, this, [this] { + setInited(false); + }); + connect(waylandServer()->seat(), &SeatInterface::dragEnded, this, [this]() { + // need to force a focused pointer change + setFocus(nullptr); + update(); + }); + // connect the move resize of all window + auto setupMoveResizeConnection = [this](Window *window) { + connect(window, &Window::interactiveMoveResizeStarted, this, &PointerInputRedirection::updateOnStartMoveResize); + connect(window, &Window::interactiveMoveResizeFinished, this, &PointerInputRedirection::update); + }; + const auto clients = workspace()->windows(); + std::for_each(clients.begin(), clients.end(), setupMoveResizeConnection); + connect(workspace(), &Workspace::windowAdded, this, setupMoveResizeConnection); + + // warp the cursor to center of screen containing the workspace center + if (const LogicalOutput *output = workspace()->outputAt(workspace()->geometry().center())) { + warp(output->geometry().center()); + } + updateAfterScreenChange(); + + connect(waylandServer()->pointerWarp(), &PointerWarpV1::warpRequested, this, [](SurfaceInterface *surface, PointerInterface *pointer, const QPointF &point, uint32_t serial) { + if (serial != waylandServer()->seat()->pointer()->focusedSerial()) { + return; + } + if (!surface->boundingRect().contains(point)) { + return; + } + Window *window = waylandServer()->findWindow(surface->mainSurface()); + if (!window) { + return; + } + input()->pointer()->warp(window->mapFromLocal(surface->mapToMainSurface(point))); + }); +} + +void PointerInputRedirection::updateOnStartMoveResize() +{ + breakPointerConstraints(focus() ? focus()->surface() : nullptr); + disconnectPointerConstraintsConnection(); + setFocus(nullptr); +} + +void PointerInputRedirection::updateToReset() +{ + if (decoration()) { + QHoverEvent event(QEvent::HoverLeave, QPointF(), QPointF()); + QCoreApplication::instance()->sendEvent(decoration()->decoration(), &event); + setDecoration(nullptr); + } + if (focus()) { + if (focus()->isClient()) { + focus()->pointerLeaveEvent(); + } + disconnect(m_focusGeometryConnection); + m_focusGeometryConnection = QMetaObject::Connection(); + breakPointerConstraints(focus()->surface()); + disconnectPointerConstraintsConnection(); + setFocus(nullptr); + } +} + +class PositionUpdateBlocker +{ +public: + PositionUpdateBlocker(PointerInputRedirection *pointer) + : m_pointer(pointer) + { + s_counter++; + } + ~PositionUpdateBlocker() + { + s_counter--; + if (s_counter == 0) { + if (!s_scheduledPositions.isEmpty()) { + const auto pos = s_scheduledPositions.takeFirst(); + m_pointer->processMotionInternal(pos.pos, pos.delta, pos.deltaNonAccelerated, pos.time, nullptr, pos.type); + } + } + } + + static bool isPositionBlocked() + { + return s_counter > 0; + } + + static void schedulePosition(const QPointF &pos, const QPointF &delta, const QPointF &deltaNonAccelerated, std::chrono::microseconds time, PointerInputRedirection::MotionType type) + { + s_scheduledPositions.append({pos, delta, deltaNonAccelerated, time, type}); + } + +private: + static int s_counter; + struct ScheduledPosition + { + QPointF pos; + QPointF delta; + QPointF deltaNonAccelerated; + std::chrono::microseconds time; + PointerInputRedirection::MotionType type; + }; + static QList s_scheduledPositions; + + PointerInputRedirection *m_pointer; +}; + +int PositionUpdateBlocker::s_counter = 0; +QList PositionUpdateBlocker::s_scheduledPositions; + +void PointerInputRedirection::processMotionAbsolute(const QPointF &pos, std::chrono::microseconds time, InputDevice *device) +{ + processMotionInternal(pos, QPointF(), QPointF(), time, device, MotionType::Motion); +} + +void PointerInputRedirection::processWarp(const QPointF &pos, std::chrono::microseconds time, InputDevice *device) +{ + processMotionInternal(pos, QPointF(), QPointF(), time, device, MotionType::Warp); +} + +void PointerInputRedirection::processMotion(const QPointF &delta, const QPointF &deltaNonAccelerated, std::chrono::microseconds time, InputDevice *device) +{ + processMotionInternal(m_pos + delta, delta, deltaNonAccelerated, time, device, MotionType::Motion); +} + +void PointerInputRedirection::processMotionInternal(const QPointF &pos, const QPointF &delta, const QPointF &deltaNonAccelerated, std::chrono::microseconds time, InputDevice *device, MotionType type) +{ + input()->setLastInputHandler(this); + if (!inited()) { + return; + } + if (PositionUpdateBlocker::isPositionBlocked()) { + PositionUpdateBlocker::schedulePosition(pos, delta, deltaNonAccelerated, time, type); + return; + } + + PositionUpdateBlocker blocker(this); + updatePosition(pos, delta, time); + + PointerMotionEvent event{ + .device = device, + .position = m_pos, + .delta = delta, + .deltaUnaccelerated = deltaNonAccelerated, + .warp = type == MotionType::Warp, + .buttons = m_qtButtons, + .modifiers = input()->keyboardModifiers(), + .modifiersRelevantForShortcuts = input()->modifiersRelevantForGlobalShortcuts(), + .timestamp = time, + }; + + update(); + input()->processSpies(&InputEventSpy::pointerMotion, &event); + input()->processFilters(&InputEventFilter::pointerMotion, &event); +} + +void PointerInputRedirection::processButton(uint32_t button, PointerButtonState state, std::chrono::microseconds time, InputDevice *device) +{ + input()->setLastInputHandler(this); + if (!inited()) { + return; + } + + if (state == PointerButtonState::Pressed) { + update(); + } + + updateButton(button, state); + + PointerButtonEvent event{ + .device = device, + .position = m_pos, + .state = state, + .button = buttonToQtMouseButton(button), + .nativeButton = button, + .buttons = m_qtButtons, + .modifiers = input()->keyboardModifiers(), + .modifiersRelevantForShortcuts = input()->modifiersRelevantForGlobalShortcuts(), + .timestamp = time, + }; + + input()->processSpies(&InputEventSpy::pointerButton, &event); + input()->processFilters(&InputEventFilter::pointerButton, &event); + if (state == PointerButtonState::Pressed) { + input()->setLastInteractionSerial(waylandServer()->seat()->display()->serial()); + if (auto f = focus()) { + f->setLastUsageSerial(waylandServer()->seat()->display()->serial()); + } + } + + if (state == PointerButtonState::Released) { + update(); + } +} + +void PointerInputRedirection::processAxis(PointerAxis axis, qreal delta, qint32 deltaV120, + PointerAxisSource source, bool inverted, std::chrono::microseconds time, InputDevice *device) +{ + input()->setLastInputHandler(this); + if (!inited()) { + return; + } + + update(); + + Q_EMIT input()->pointerAxisChanged(axis, delta); + + PointerAxisEvent event{ + .device = device, + .position = m_pos, + .delta = delta, + .deltaV120 = deltaV120, + .orientation = (axis == PointerAxis::Horizontal) ? Qt::Horizontal : Qt::Vertical, + .source = source, + .buttons = m_qtButtons, + .modifiers = input()->keyboardModifiers(), + .modifiersRelevantForGlobalShortcuts = input()->modifiersRelevantForGlobalShortcuts(), + .inverted = inverted, + .timestamp = time, + }; + + input()->processSpies(&InputEventSpy::pointerAxis, &event); + input()->processFilters(&InputEventFilter::pointerAxis, &event); +} + +void PointerInputRedirection::processSwipeGestureBegin(int fingerCount, std::chrono::microseconds time, KWin::InputDevice *device) +{ + input()->setLastInputHandler(this); + if (!inited()) { + return; + } + update(); + + PointerSwipeGestureBeginEvent event{ + .fingerCount = fingerCount, + .time = time, + }; + + input()->processSpies(&InputEventSpy::swipeGestureBegin, &event); + input()->processFilters(&InputEventFilter::swipeGestureBegin, &event); +} + +void PointerInputRedirection::processSwipeGestureUpdate(const QPointF &delta, std::chrono::microseconds time, KWin::InputDevice *device) +{ + input()->setLastInputHandler(this); + if (!inited()) { + return; + } + update(); + + PointerSwipeGestureUpdateEvent event{ + .delta = delta, + .time = time, + }; + + input()->processSpies(&InputEventSpy::swipeGestureUpdate, &event); + input()->processFilters(&InputEventFilter::swipeGestureUpdate, &event); +} + +void PointerInputRedirection::processSwipeGestureEnd(std::chrono::microseconds time, KWin::InputDevice *device) +{ + input()->setLastInputHandler(this); + if (!inited()) { + return; + } + update(); + + PointerSwipeGestureEndEvent event{ + .time = time, + }; + + input()->processSpies(&InputEventSpy::swipeGestureEnd, &event); + input()->processFilters(&InputEventFilter::swipeGestureEnd, &event); +} + +void PointerInputRedirection::processSwipeGestureCancelled(std::chrono::microseconds time, KWin::InputDevice *device) +{ + input()->setLastInputHandler(this); + if (!inited()) { + return; + } + update(); + + PointerSwipeGestureCancelEvent event{ + .time = time, + }; + + input()->processSpies(&InputEventSpy::swipeGestureCancelled, &event); + input()->processFilters(&InputEventFilter::swipeGestureCancelled, &event); +} + +void PointerInputRedirection::processPinchGestureBegin(int fingerCount, std::chrono::microseconds time, KWin::InputDevice *device) +{ + input()->setLastInputHandler(this); + if (!inited()) { + return; + } + update(); + + PointerPinchGestureBeginEvent event{ + .fingerCount = fingerCount, + .time = time, + }; + + input()->processSpies(&InputEventSpy::pinchGestureBegin, &event); + input()->processFilters(&InputEventFilter::pinchGestureBegin, &event); +} + +void PointerInputRedirection::processPinchGestureUpdate(qreal scale, qreal angleDelta, const QPointF &delta, std::chrono::microseconds time, KWin::InputDevice *device) +{ + input()->setLastInputHandler(this); + if (!inited()) { + return; + } + update(); + + PointerPinchGestureUpdateEvent event{ + .scale = scale, + .angleDelta = angleDelta, + .delta = delta, + .time = time, + }; + + input()->processSpies(&InputEventSpy::pinchGestureUpdate, &event); + input()->processFilters(&InputEventFilter::pinchGestureUpdate, &event); +} + +void PointerInputRedirection::processPinchGestureEnd(std::chrono::microseconds time, KWin::InputDevice *device) +{ + input()->setLastInputHandler(this); + if (!inited()) { + return; + } + update(); + + PointerPinchGestureEndEvent event{ + .time = time, + }; + + input()->processSpies(&InputEventSpy::pinchGestureEnd, &event); + input()->processFilters(&InputEventFilter::pinchGestureEnd, &event); +} + +void PointerInputRedirection::processPinchGestureCancelled(std::chrono::microseconds time, KWin::InputDevice *device) +{ + input()->setLastInputHandler(this); + if (!inited()) { + return; + } + update(); + + PointerPinchGestureCancelEvent event{ + .time = time, + }; + + input()->processSpies(&InputEventSpy::pinchGestureCancelled, &event); + input()->processFilters(&InputEventFilter::pinchGestureCancelled, &event); +} + +void PointerInputRedirection::processHoldGestureBegin(int fingerCount, std::chrono::microseconds time, KWin::InputDevice *device) +{ + if (!inited()) { + return; + } + update(); + + PointerHoldGestureBeginEvent event{ + .fingerCount = fingerCount, + .time = time, + }; + + input()->processSpies(&InputEventSpy::holdGestureBegin, &event); + input()->processFilters(&InputEventFilter::holdGestureBegin, &event); +} + +void PointerInputRedirection::processHoldGestureEnd(std::chrono::microseconds time, KWin::InputDevice *device) +{ + if (!inited()) { + return; + } + update(); + + PointerHoldGestureEndEvent event{ + .time = time, + }; + + input()->processSpies(&InputEventSpy::holdGestureEnd, &event); + input()->processFilters(&InputEventFilter::holdGestureEnd, &event); +} + +void PointerInputRedirection::processHoldGestureCancelled(std::chrono::microseconds time, KWin::InputDevice *device) +{ + if (!inited()) { + return; + } + update(); + + PointerHoldGestureCancelEvent event{ + .time = time, + }; + + input()->processSpies(&InputEventSpy::holdGestureCancelled, &event); + input()->processFilters(&InputEventFilter::holdGestureCancelled, &event); +} + +void PointerInputRedirection::processFrame(KWin::InputDevice *device) +{ + if (!inited()) { + return; + } + + input()->processFilters(&InputEventFilter::pointerFrame); +} + +bool PointerInputRedirection::areButtonsPressed() const +{ + for (auto state : m_buttons) { + if (state == PointerButtonState::Pressed) { + return true; + } + } + return false; +} + +bool PointerInputRedirection::focusUpdatesBlocked() +{ + if (waylandServer()->seat()->isDragPointer()) { + // ignore during drag and drop + return true; + } + if (waylandServer()->seat()->isTouchSequence()) { + // ignore during touch operations + return true; + } + if (input()->isSelectingWindow()) { + return true; + } + if (areButtonsPressed()) { + return true; + } + return false; +} + +void PointerInputRedirection::cleanupDecoration(Decoration::DecoratedWindowImpl *old, Decoration::DecoratedWindowImpl *now) +{ + disconnect(m_decorationGeometryConnection); + m_decorationGeometryConnection = QMetaObject::Connection(); + + disconnect(m_decorationDestroyedConnection); + m_decorationDestroyedConnection = QMetaObject::Connection(); + + disconnect(m_decorationClosedConnection); + m_decorationClosedConnection = QMetaObject::Connection(); + + if (old) { + // send leave event to old decoration + QHoverEvent event(QEvent::HoverLeave, QPointF(-1, -1), QPointF()); + QCoreApplication::instance()->sendEvent(old->decoration(), &event); + } + if (!now) { + // left decoration + return; + } + + auto pos = m_pos - now->window()->pos(); + QHoverEvent event(QEvent::HoverEnter, pos, QPointF(-1, -1)); + QCoreApplication::instance()->sendEvent(now->decoration(), &event); + now->window()->processDecorationMove(pos, m_pos); + + m_decorationGeometryConnection = connect(decoration()->window(), &Window::frameGeometryChanged, this, [this]() { + // ensure maximize button gets the leave event when maximizing/restore a window, see BUG 385140 + const auto oldDeco = decoration(); + update(); + if (oldDeco && oldDeco == decoration() && !decoration()->window()->isInteractiveMove() && !decoration()->window()->isInteractiveResize() && !areButtonsPressed()) { + // position of window did not change, we need to send HoverMotion manually + const QPointF p = m_pos - decoration()->window()->pos(); + QHoverEvent event(QEvent::HoverMove, p, p); + QCoreApplication::instance()->sendEvent(decoration()->decoration(), &event); + } + }); + + auto resetDecoration = [this]() { + setDecoration(nullptr); // explicitly reset decoration if focus updates are blocked + update(); + }; + + m_decorationClosedConnection = connect(decoration()->window(), &Window::closed, this, resetDecoration); + m_decorationDestroyedConnection = connect(now, &QObject::destroyed, this, resetDecoration); +} + +void PointerInputRedirection::focusUpdate(Window *focusOld, Window *focusNow) +{ + if (focusOld && focusOld->isClient()) { + focusOld->pointerLeaveEvent(); + breakPointerConstraints(focusOld->surface()); + disconnectPointerConstraintsConnection(); + } + disconnect(m_focusGeometryConnection); + m_focusGeometryConnection = QMetaObject::Connection(); + + if (focusNow && focusNow->isClient()) { + focusNow->pointerEnterEvent(m_pos); + } + + auto seat = waylandServer()->seat(); + if (!focusNow || !focusNow->surface()) { + seat->notifyPointerLeave(); + return; + } + + seat->notifyPointerEnter(focusNow->surface(), m_pos, focusNow->inputTransformation()); + + m_focusGeometryConnection = connect(focusNow, &Window::inputTransformationChanged, this, [this]() { + waylandServer()->seat()->setFocusedPointerSurfaceTransformation(focus()->inputTransformation()); + }); + + m_constraintsConnection = connect(focusNow->surface(), &SurfaceInterface::pointerConstraintsChanged, + this, &PointerInputRedirection::updatePointerConstraints); + m_constraintsActivatedConnection = connect(workspace(), &Workspace::windowActivated, + this, &PointerInputRedirection::updatePointerConstraints); + updatePointerConstraints(); +} + +void PointerInputRedirection::breakPointerConstraints(SurfaceInterface *surface) +{ + // cancel pointer constraints + if (surface) { + auto c = surface->confinedPointer(); + if (c && c->isConfined()) { + c->setConfined(false); + } + auto l = surface->lockedPointer(); + if (l && l->isLocked()) { + l->setLocked(false); + } + } + disconnectConfinedPointerRegionConnection(); + m_confined = false; + m_locked = false; +} + +void PointerInputRedirection::disconnectConfinedPointerRegionConnection() +{ + disconnect(m_confinedPointerRegionConnection); + m_confinedPointerRegionConnection = QMetaObject::Connection(); +} + +void PointerInputRedirection::disconnectLockedPointerAboutToBeUnboundConnection() +{ + disconnect(m_lockedPointerAboutToBeUnboundConnection); + m_lockedPointerAboutToBeUnboundConnection = QMetaObject::Connection(); +} + +void PointerInputRedirection::disconnectPointerConstraintsConnection() +{ + disconnect(m_constraintsConnection); + m_constraintsConnection = QMetaObject::Connection(); + + disconnect(m_constraintsActivatedConnection); + m_constraintsActivatedConnection = QMetaObject::Connection(); +} + +void PointerInputRedirection::setEnableConstraints(bool set) +{ + if (m_enableConstraints == set) { + return; + } + m_enableConstraints = set; + updatePointerConstraints(); +} + +void PointerInputRedirection::updatePointerConstraints() +{ + if (!focus()) { + return; + } + const auto s = focus()->surface(); + if (!s) { + return; + } + if (s != waylandServer()->seat()->focusedPointerSurface()) { + return; + } + if (!supportsWarping()) { + return; + } + const bool canConstrain = m_enableConstraints && focus() == workspace()->activeWindow(); + const auto cf = s->confinedPointer(); + if (cf) { + if (cf->isConfined()) { + if (!canConstrain) { + cf->setConfined(false); + m_confined = false; + disconnectConfinedPointerRegionConnection(); + } + return; + } + if (canConstrain && cf->region().contains(flooredPoint(focus()->mapToLocal(m_pos)))) { + cf->setConfined(true); + m_confined = true; + m_confinedPointerRegionConnection = connect(cf, &ConfinedPointerV1Interface::regionChanged, this, [this]() { + if (!focus()) { + return; + } + const auto s = focus()->surface(); + if (!s) { + return; + } + const auto cf = s->confinedPointer(); + if (!cf->region().contains(flooredPoint(focus()->mapToLocal(m_pos)))) { + // pointer no longer in confined region, break the confinement + cf->setConfined(false); + m_confined = false; + } else { + if (!cf->isConfined()) { + cf->setConfined(true); + m_confined = true; + } + } + }); + return; + } + } else { + m_confined = false; + disconnectConfinedPointerRegionConnection(); + } + const auto lock = s->lockedPointer(); + if (lock) { + if (lock->isLocked()) { + if (!canConstrain) { + const auto hint = lock->cursorPositionHint(); + lock->setLocked(false); + m_locked = false; + disconnectLockedPointerAboutToBeUnboundConnection(); + if (hint.x() >= 0 && hint.y() >= 0 && focus() && hint.x() < focus()->width() && hint.y() < focus()->height()) { + processWarp(focus()->mapFromLocal(hint), waylandServer()->seat()->timestamp()); + } + } + return; + } + if (canConstrain && lock->region().contains(flooredPoint(focus()->mapToLocal(m_pos)))) { + lock->setLocked(true); + m_locked = true; + + // The client might cancel pointer locking from its side by unbinding the LockedPointerInterface. + // In this case the cached cursor position hint must be fetched before the resource goes away + m_lockedPointerAboutToBeUnboundConnection = connect(lock, &LockedPointerV1Interface::aboutToBeDestroyed, this, [this, lock]() { + const auto hint = lock->cursorPositionHint(); + if (hint.x() < 0 || hint.y() < 0 || !focus() || hint.x() >= focus()->width() || hint.y() >= focus()->height()) { + return; + } + auto globalHint = focus()->mapFromLocal(hint); + + // When the resource finally goes away, reposition the cursor according to the hint + connect(lock, &LockedPointerV1Interface::destroyed, this, [this, globalHint]() { + processWarp(globalHint, waylandServer()->seat()->timestamp()); + }); + }); + // TODO: connect to region change - is it needed at all? If the pointer is locked it's always in the region + } + } else { + m_locked = false; + disconnectLockedPointerAboutToBeUnboundConnection(); + } +} + +QPointF PointerInputRedirection::applyPointerConfinement(const QPointF &pos) const +{ + if (!focus()) { + return pos; + } + auto s = focus()->surface(); + if (!s) { + return pos; + } + auto cf = s->confinedPointer(); + if (!cf) { + return pos; + } + if (!cf->isConfined()) { + return pos; + } + + const QPointF localPos = focus()->mapToLocal(pos); + if (cf->region().contains(flooredPoint(localPos))) { + return pos; + } + + const QPointF currentPos = focus()->mapToLocal(m_pos); + + // allow either x or y to pass + QPointF p(currentPos.x(), localPos.y()); + if (cf->region().contains(flooredPoint(p))) { + return focus()->mapFromLocal(p); + } + + p = QPointF(localPos.x(), currentPos.y()); + if (cf->region().contains(flooredPoint(p))) { + return focus()->mapFromLocal(p); + } + + return m_pos; +} + +PointerInputRedirection::EdgeBarrierType PointerInputRedirection::edgeBarrierType(const QPointF &pos, const RectF &lastOutputGeometry) const +{ + constexpr qreal cornerThreshold = 15; + const auto moveResizeWindow = workspace()->moveResizeWindow(); + const bool onCorner = (pos - lastOutputGeometry.topLeft()).manhattanLength() <= cornerThreshold + || (pos - lastOutputGeometry.bottomLeft()).manhattanLength() <= cornerThreshold + || (pos - lastOutputGeometry.topRight()).manhattanLength() <= cornerThreshold + || (pos - lastOutputGeometry.bottomRight()).manhattanLength() <= cornerThreshold; + if (moveResizeWindow && moveResizeWindow->isInteractiveMove()) { + return EdgeBarrierType::WindowMoveBarrier; + } else if (moveResizeWindow && moveResizeWindow->isInteractiveResize()) { + return EdgeBarrierType::WindowResizeBarrier; + } else if (options->cornerBarrier() && onCorner) { + return EdgeBarrierType::CornerBarrier; + } else if (workspace()->screenEdges()->inApproachGeometry(pos.toPoint())) { + return EdgeBarrierType::EdgeElementBarrier; + } else { + return EdgeBarrierType::NormalBarrier; + } +} + +qreal PointerInputRedirection::edgeBarrier(EdgeBarrierType type) const +{ + const auto barrierWidth = options->edgeBarrier(); + switch (type) { + case EdgeBarrierType::WindowMoveBarrier: + case EdgeBarrierType::WindowResizeBarrier: + return 1.5 * barrierWidth; + case EdgeBarrierType::EdgeElementBarrier: + return 2 * barrierWidth; + case EdgeBarrierType::CornerBarrier: + return 2000; + case EdgeBarrierType::NormalBarrier: + return barrierWidth; + default: + Q_UNREACHABLE(); + return 0; + } +} + +QPointF PointerInputRedirection::applyEdgeBarrier(const QPointF &pos, const QPointF &relativeMotion, const LogicalOutput *currentOutput, std::chrono::microseconds time) +{ + // edge barriers are counter-productive for absolute motion + if (relativeMotion.isNull()) { + m_movementInEdgeBarrier = QPointF(); + return pos; + } + // optimization to avoid looping over all outputs + if (currentOutput->geometryF().contains(m_pos)) { + m_movementInEdgeBarrier = QPointF(); + return pos; + } + const LogicalOutput *lastOutput = workspace()->outputAt(m_pos); + QPointF newPos = confineToBoundingBox(pos, lastOutput->geometry()); + const auto type = edgeBarrierType(newPos, lastOutput->geometry()); + if (m_lastEdgeBarrierType != type) { + m_movementInEdgeBarrier = QPointF(); + } + m_lastEdgeBarrierType = type; + const auto barrierWidth = edgeBarrier(type); + const qreal returnSpeed = barrierWidth / 10.0 /* px/s */ / 1000'000.0; // px/us + std::chrono::microseconds timeDiff(time - m_lastMoveTime); + qreal returnDistance = returnSpeed * timeDiff.count(); + + const auto euclideanLength = [](const QPointF &point) { + return std::sqrt(point.x() * point.x() + point.y() * point.y()); + }; + const auto shorten = [euclideanLength](const QPointF &point, const qreal distance) { + const qreal length = euclideanLength(point); + if (length <= distance) { + return QPointF(); + } + return point * (1 - distance / length); + }; + + m_movementInEdgeBarrier += (pos - newPos); + m_movementInEdgeBarrier = shorten(m_movementInEdgeBarrier, returnDistance); + + if (euclideanLength(m_movementInEdgeBarrier) > barrierWidth) { + newPos += shorten(m_movementInEdgeBarrier, barrierWidth); + m_movementInEdgeBarrier = QPointF(); + } + return newPos; +} + +void PointerInputRedirection::updatePosition(const QPointF &pos, const QPointF &relativeMotion, std::chrono::microseconds time) +{ + m_lastMoveTime = time; + if (m_locked) { + // locked pointer should not move + return; + } + // verify that at least one screen contains the pointer position + const LogicalOutput *currentOutput = workspace()->outputAt(pos); + QPointF p = confineToBoundingBox(pos, currentOutput->geometry()); + p = applyEdgeBarrier(p, relativeMotion, currentOutput, time); + p = applyPointerConfinement(p); + if (p == m_pos) { + // didn't change due to confinement + return; + } + // verify screen confinement + if (!screenContainsPos(p)) { + return; + } + + m_pos = p; + + workspace()->setActiveOutput(m_pos); + m_cursor->updateCursorOutputs(m_pos); + Cursors::self()->mouse()->setPos(m_pos); + + Q_EMIT input()->globalPointerChanged(m_pos); +} + +void PointerInputRedirection::updateButton(uint32_t button, PointerButtonState state) +{ + m_buttons[button] = state; + + // update Qt buttons + m_qtButtons = Qt::NoButton; + for (auto it = m_buttons.constBegin(); it != m_buttons.constEnd(); ++it) { + if (it.value() == PointerButtonState::Released) { + continue; + } + m_qtButtons |= buttonToQtMouseButton(it.key()); + } + + Q_EMIT input()->pointerButtonStateChanged(button, state); +} + +void PointerInputRedirection::warp(const QPointF &pos) +{ + if (supportsWarping()) { + processWarp(pos, waylandServer()->seat()->timestamp()); + } +} + +bool PointerInputRedirection::supportsWarping() const +{ + return inited(); +} + +void PointerInputRedirection::updateAfterScreenChange() +{ + if (!inited()) { + return; + } + + LogicalOutput *output = nullptr; + if (m_lastOutputWasPlaceholder) { + // previously we've positioned our pointer on a placeholder screen, try + // to get us onto the real "primary" screen instead. + output = workspace()->outputOrder().at(0); + } else { + if (screenContainsPos(m_pos)) { + // pointer still on a screen + return; + } + + // pointer no longer on a screen, reposition to closes screen + output = workspace()->outputAt(m_pos); + } + + m_lastOutputWasPlaceholder = output->isPlaceholder(); + // TODO: better way to get timestamps + processMotionAbsolute(output->geometry().center(), waylandServer()->seat()->timestamp()); +} + +QPointF PointerInputRedirection::position() const +{ + return m_pos; +} + +void PointerInputRedirection::setEffectsOverrideCursor(Qt::CursorShape shape) +{ + if (!inited()) { + return; + } + // current pointer focus window should get a leave event + update(); + m_cursor->setEffectsOverrideCursor(shape); +} + +void PointerInputRedirection::removeEffectsOverrideCursor() +{ + if (!inited()) { + return; + } + // cursor position might have changed while there was an effect in place + update(); + m_cursor->removeEffectsOverrideCursor(); +} + +void PointerInputRedirection::setWindowSelectionCursor(const QByteArray &shape) +{ + if (!inited()) { + return; + } + // send leave to current pointer focus window + updateToReset(); + m_cursor->setWindowSelectionCursor(shape); +} + +void PointerInputRedirection::removeWindowSelectionCursor() +{ + if (!inited()) { + return; + } + update(); + m_cursor->removeWindowSelectionCursor(); +} + +CursorImage::CursorImage(PointerInputRedirection *parent) + : QObject(parent) + , m_pointer(parent) +{ + m_effectsCursor = std::make_unique(); + m_fallbackCursor = std::make_unique(); + m_moveResizeCursor = std::make_unique(); + m_windowSelectionCursor = std::make_unique(); + m_decoration.cursor = std::make_unique(); + m_serverCursor.surface = std::make_unique(); + m_serverCursor.shape = std::make_unique(); + m_dragCursor = std::make_unique(); + +#if KWIN_BUILD_SCREENLOCKER + if (kwinApp()->supportsLockScreen()) { + connect(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged, this, &CursorImage::reevaluteSource); + } +#endif + connect(m_pointer, &PointerInputRedirection::decorationChanged, this, &CursorImage::updateDecoration); + // connect the move resize of all window + auto setupMoveResizeConnection = [this](Window *window) { + connect(window, &Window::moveResizedChanged, this, &CursorImage::updateMoveResize); + connect(window, &Window::moveResizeCursorChanged, this, &CursorImage::updateMoveResize); + }; + const auto clients = workspace()->windows(); + std::for_each(clients.begin(), clients.end(), setupMoveResizeConnection); + connect(workspace(), &Workspace::windowAdded, this, setupMoveResizeConnection); + + m_fallbackCursor->setShape(Qt::ArrowCursor); + + m_effectsCursor->setTheme(m_waylandImage.theme()); + m_fallbackCursor->setTheme(m_waylandImage.theme()); + m_moveResizeCursor->setTheme(m_waylandImage.theme()); + m_windowSelectionCursor->setTheme(m_waylandImage.theme()); + m_decoration.cursor->setTheme(m_waylandImage.theme()); + m_serverCursor.shape->setTheme(m_waylandImage.theme()); + m_dragCursor->setTheme(m_waylandImage.theme()); + + connect(&m_waylandImage, &WaylandCursorImage::themeChanged, this, [this] { + m_effectsCursor->setTheme(m_waylandImage.theme()); + m_fallbackCursor->setTheme(m_waylandImage.theme()); + m_moveResizeCursor->setTheme(m_waylandImage.theme()); + m_windowSelectionCursor->setTheme(m_waylandImage.theme()); + m_decoration.cursor->setTheme(m_waylandImage.theme()); + m_serverCursor.shape->setTheme(m_waylandImage.theme()); + m_dragCursor->setTheme(m_waylandImage.theme()); + }); + + connect(waylandServer()->seat(), &SeatInterface::dragStarted, this, [this]() { + m_dragCursor->setShape(Qt::ForbiddenCursor); + connect(waylandServer()->seat()->dragSource(), &AbstractDataSource::dndActionChanged, this, &CursorImage::updateDragCursor); + connect(waylandServer()->seat()->dragSource(), &AbstractDataSource::acceptedChanged, this, &CursorImage::updateDragCursor); + reevaluteSource(); + }); + + PointerInterface *pointer = waylandServer()->seat()->pointer(); + + connect(pointer, &PointerInterface::focusedSurfaceChanged, + this, &CursorImage::handleFocusedSurfaceChanged); + + reevaluteSource(); +} + +CursorImage::~CursorImage() = default; + +void CursorImage::updateCursorOutputs(const QPointF &pos) +{ + if (m_currentSource == m_serverCursor.surface.get()) { + auto cursorSurface = m_serverCursor.surface->surface(); + if (cursorSurface) { + const RectF cursorGeometry(pos - m_currentSource->hotspot(), m_currentSource->size()); + cursorSurface->setOutputs(waylandServer()->display()->outputsIntersecting(cursorGeometry.toAlignedRect()), + waylandServer()->display()->largestIntersectingOutput(cursorGeometry.toAlignedRect())); + } + } +} + +void CursorImage::handleFocusedSurfaceChanged() +{ + PointerInterface *pointer = waylandServer()->seat()->pointer(); + disconnect(m_serverCursor.connection); + + if (pointer->focusedSurface()) { + m_serverCursor.connection = connect(pointer, &PointerInterface::cursorChanged, + this, &CursorImage::updateServerCursor); + } else { + m_serverCursor.connection = QMetaObject::Connection(); + reevaluteSource(); + } +} + +void CursorImage::updateDecoration() +{ + disconnect(m_decoration.connection); + auto deco = m_pointer->decoration(); + Window *window = deco ? deco->window() : nullptr; + if (window) { + m_decoration.connection = connect(window, &Window::moveResizeCursorChanged, this, &CursorImage::updateDecorationCursor); + } else { + m_decoration.connection = QMetaObject::Connection(); + } + updateDecorationCursor(); +} + +void CursorImage::updateDecorationCursor() +{ + auto deco = m_pointer->decoration(); + if (Window *window = deco ? deco->window() : nullptr) { + m_decoration.cursor->setShape(window->cursor().name()); + } + reevaluteSource(); +} + +void CursorImage::updateMoveResize() +{ + if (Window *window = workspace()->moveResizeWindow()) { + m_moveResizeCursor->setShape(window->cursor().name()); + } + reevaluteSource(); +} + +void CursorImage::updateDragCursor() +{ + AbstractDataSource *dragSource = waylandServer()->seat()->dragSource(); + if (dragSource && dragSource->isAccepted()) { + switch (dragSource->selectedDndAction()) { + case DnDAction::None: + m_dragCursor->setShape(Qt::ClosedHandCursor); + break; + case DnDAction::Copy: + m_dragCursor->setShape(Qt::DragCopyCursor); + break; + case DnDAction::Move: + m_dragCursor->setShape(Qt::DragMoveCursor); + break; + case DnDAction::Ask: + // Cursor themes don't have anything better in the themes yet + // a dnd-drag-ask is proposed + m_dragCursor->setShape(Qt::ClosedHandCursor); + break; + } + } else { + m_dragCursor->setShape(Qt::ForbiddenCursor); + } + reevaluteSource(); +} + +void CursorImage::updateServerCursor(const PointerCursor &cursor) +{ + if (auto surfaceCursor = std::get_if(&cursor)) { + m_serverCursor.surface->update((*surfaceCursor)->surface(), (*surfaceCursor)->hotspot()); + m_serverCursor.cursor = m_serverCursor.surface.get(); + } else if (auto shapeCursor = std::get_if(&cursor)) { + m_serverCursor.shape->setShape(*shapeCursor); + m_serverCursor.cursor = m_serverCursor.shape.get(); + } + reevaluteSource(); +} + +void CursorImage::setEffectsOverrideCursor(Qt::CursorShape shape) +{ + m_effectsCursor->setShape(shape); + reevaluteSource(); +} + +void CursorImage::removeEffectsOverrideCursor() +{ + reevaluteSource(); +} + +void CursorImage::setWindowSelectionCursor(const QByteArray &shape) +{ + if (shape.isEmpty()) { + m_windowSelectionCursor->setShape(Qt::CrossCursor); + } else { + m_windowSelectionCursor->setShape(shape); + } + reevaluteSource(); +} + +void CursorImage::removeWindowSelectionCursor() +{ + reevaluteSource(); +} + +WaylandCursorImage::WaylandCursorImage(QObject *parent) + : QObject(parent) +{ + Cursor *pointerCursor = Cursors::self()->mouse(); + updateCursorTheme(); + + connect(pointerCursor, &Cursor::themeChanged, this, &WaylandCursorImage::updateCursorTheme); + connect(workspace(), &Workspace::outputsChanged, this, &WaylandCursorImage::updateCursorTheme); +} + +CursorTheme WaylandCursorImage::theme() const +{ + return m_cursorTheme; +} + +void WaylandCursorImage::updateCursorTheme() +{ + const Cursor *pointerCursor = Cursors::self()->mouse(); + qreal targetDevicePixelRatio = 1; + + const auto outputs = workspace()->outputs(); + for (const LogicalOutput *output : outputs) { + if (output->scale() > targetDevicePixelRatio) { + targetDevicePixelRatio = output->scale(); + } + } + + m_cursorTheme = CursorTheme(pointerCursor->themeName(), pointerCursor->themeSize(), targetDevicePixelRatio); + if (m_cursorTheme.isEmpty()) { + qCWarning(KWIN_CORE) << "Failed to load cursor theme" << pointerCursor->themeName(); + m_cursorTheme = CursorTheme(Cursor::defaultThemeName(), Cursor::defaultThemeSize(), targetDevicePixelRatio); + + if (m_cursorTheme.isEmpty()) { + qCWarning(KWIN_CORE) << "Failed to load cursor theme" << Cursor::defaultThemeName(); + m_cursorTheme = CursorTheme(Cursor::fallbackThemeName(), Cursor::defaultThemeSize(), targetDevicePixelRatio); + } + } + + if (m_cursorTheme.isEmpty()) { + qCWarning(KWIN_CORE) << "Unable to load any cursor theme"; + } + + Q_EMIT themeChanged(); +} + +void CursorImage::reevaluteSource() +{ + if (waylandServer()->isScreenLocked()) { + setSource(m_serverCursor.cursor); + return; + } + if (waylandServer()->seat()->isDrag()) { + setSource(m_dragCursor.get()); + return; + } + if (input()->isSelectingWindow()) { + setSource(m_windowSelectionCursor.get()); + return; + } + if (effects && effects->isMouseInterception()) { + setSource(m_effectsCursor.get()); + return; + } + if (workspace() && workspace()->moveResizeWindow()) { + setSource(m_moveResizeCursor.get()); + return; + } + if (m_pointer->decoration()) { + setSource(m_decoration.cursor.get()); + return; + } + const PointerInterface *pointer = waylandServer()->seat()->pointer(); + if (pointer && pointer->focusedSurface()) { + setSource(m_serverCursor.cursor); + return; + } + setSource(m_fallbackCursor.get()); +} + +CursorSource *CursorImage::source() const +{ + return m_currentSource; +} + +void CursorImage::setSource(CursorSource *source) +{ + if (m_currentSource == source) { + return; + } + m_currentSource = source; + Q_EMIT changed(); +} + +CursorTheme CursorImage::theme() const +{ + return m_waylandImage.theme(); +} +} + +#include "moc_pointer_input.cpp" diff --git a/local/recipes/kde/kwin/source/src/pointer_input.h b/local/recipes/kde/kwin/source/src/pointer_input.h new file mode 100644 index 0000000000..bd01f9d5e0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/pointer_input.h @@ -0,0 +1,274 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013, 2016 Martin Gräßlin + SPDX-FileCopyrightText: 2018 Roman Gilg + SPDX-FileCopyrightText: 2019 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "cursor.h" +#include "input.h" +#include "utils/cursortheme.h" + +#include +#include +#include +#include + +class QWindow; + +namespace KWin +{ +class Window; +class CursorImage; +class InputDevice; +class InputRedirection; +class CursorShape; +class ShapeCursorSource; +class SurfaceCursorSource; +class PointerSurfaceCursor; +class SurfaceInterface; + +namespace Decoration +{ +class DecoratedWindowImpl; +} + + +class KWIN_EXPORT PointerInputRedirection : public InputDeviceHandler +{ + Q_OBJECT +public: + explicit PointerInputRedirection(InputRedirection *parent); + ~PointerInputRedirection() override; + + void init() override; + + CursorTheme cursorTheme() const; // TODO: Make it a Cursor property + + void updateAfterScreenChange(); + bool supportsWarping() const; + void warp(const QPointF &pos); + + QPointF pos() const + { + return m_pos; + } + Qt::MouseButtons buttons() const + { + return m_qtButtons; + } + bool areButtonsPressed() const; + + void setEffectsOverrideCursor(Qt::CursorShape shape); + void removeEffectsOverrideCursor(); + void setWindowSelectionCursor(const QByteArray &shape); + void removeWindowSelectionCursor(); + + void updatePointerConstraints(); + + void setEnableConstraints(bool set); + + bool isConstrained() const + { + return m_confined || m_locked; + } + + bool focusUpdatesBlocked() override; + + /** + * @internal + */ + void processMotionAbsolute(const QPointF &pos, std::chrono::microseconds time, InputDevice *device = nullptr); + /** + * @internal + */ + void processMotion(const QPointF &delta, const QPointF &deltaNonAccelerated, std::chrono::microseconds time, InputDevice *device); + /** + * @internal + */ + void processButton(uint32_t button, PointerButtonState state, std::chrono::microseconds time, InputDevice *device = nullptr); + /** + * @internal + */ + void processAxis(PointerAxis axis, qreal delta, qint32 deltaV120, PointerAxisSource source, bool inverted, std::chrono::microseconds time, InputDevice *device = nullptr); + /** + * @internal + */ + void processSwipeGestureBegin(int fingerCount, std::chrono::microseconds time, KWin::InputDevice *device = nullptr); + /** + * @internal + */ + void processSwipeGestureUpdate(const QPointF &delta, std::chrono::microseconds time, KWin::InputDevice *device = nullptr); + /** + * @internal + */ + void processSwipeGestureEnd(std::chrono::microseconds time, KWin::InputDevice *device = nullptr); + /** + * @internal + */ + void processSwipeGestureCancelled(std::chrono::microseconds time, KWin::InputDevice *device = nullptr); + /** + * @internal + */ + void processPinchGestureBegin(int fingerCount, std::chrono::microseconds time, KWin::InputDevice *device = nullptr); + /** + * @internal + */ + void processPinchGestureUpdate(qreal scale, qreal angleDelta, const QPointF &delta, std::chrono::microseconds time, KWin::InputDevice *device = nullptr); + /** + * @internal + */ + void processPinchGestureEnd(std::chrono::microseconds time, KWin::InputDevice *device = nullptr); + /** + * @internal + */ + void processPinchGestureCancelled(std::chrono::microseconds time, KWin::InputDevice *device = nullptr); + /** + * @internal + */ + void processHoldGestureBegin(int fingerCount, std::chrono::microseconds time, KWin::InputDevice *device = nullptr); + /** + * @internal + */ + void processHoldGestureEnd(std::chrono::microseconds time, KWin::InputDevice *device = nullptr); + /** + * @internal + */ + void processHoldGestureCancelled(std::chrono::microseconds time, KWin::InputDevice *device = nullptr); + /** + * @internal + */ + void processFrame(KWin::InputDevice *device = nullptr); + +private: + enum class EdgeBarrierType { + NormalBarrier, + WindowMoveBarrier, + // WindowResize is separate from WindowMove since there is edge snapping during resize, so a different resistance might be desirable + WindowResizeBarrier, + EdgeElementBarrier, + CornerBarrier, + }; + void processWarp(const QPointF &pos, std::chrono::microseconds time, InputDevice *device = nullptr); + enum class MotionType { + Motion, + Warp + }; + void processMotionInternal(const QPointF &pos, const QPointF &delta, const QPointF &deltaNonAccelerated, std::chrono::microseconds time, InputDevice *device, MotionType type); + void cleanupDecoration(Decoration::DecoratedWindowImpl *old, Decoration::DecoratedWindowImpl *now) override; + + void focusUpdate(Window *focusOld, Window *focusNow) override; + + QPointF position() const override; + + void updateOnStartMoveResize(); + void updateToReset(); + void updatePosition(const QPointF &pos, const QPointF &relativeMotion, std::chrono::microseconds time); + void updateButton(uint32_t button, PointerButtonState state); + QPointF applyEdgeBarrier(const QPointF &pos, const QPointF &relativeMotion, const LogicalOutput *currentOutput, std::chrono::microseconds time); + EdgeBarrierType edgeBarrierType(const QPointF &pos, const RectF &lastOutputGeometry) const; + qreal edgeBarrier(EdgeBarrierType type) const; + QPointF applyPointerConfinement(const QPointF &pos) const; + void disconnectConfinedPointerRegionConnection(); + void disconnectLockedPointerAboutToBeUnboundConnection(); + void disconnectPointerConstraintsConnection(); + void breakPointerConstraints(SurfaceInterface *surface); + CursorImage *m_cursor; + QPointF m_pos; + QHash m_buttons; + Qt::MouseButtons m_qtButtons; + QMetaObject::Connection m_focusGeometryConnection; + QMetaObject::Connection m_constraintsConnection; + QMetaObject::Connection m_constraintsActivatedConnection; + QMetaObject::Connection m_confinedPointerRegionConnection; + QMetaObject::Connection m_lockedPointerAboutToBeUnboundConnection; + QMetaObject::Connection m_decorationGeometryConnection; + QMetaObject::Connection m_decorationDestroyedConnection; + QMetaObject::Connection m_decorationClosedConnection; + bool m_confined = false; + bool m_locked = false; + bool m_enableConstraints = true; + bool m_lastOutputWasPlaceholder = true; + QPointF m_movementInEdgeBarrier; + std::chrono::microseconds m_lastMoveTime = std::chrono::microseconds::zero(); + friend class PositionUpdateBlocker; + EdgeBarrierType m_lastEdgeBarrierType = EdgeBarrierType::NormalBarrier; +}; + +class WaylandCursorImage : public QObject +{ + Q_OBJECT +public: + explicit WaylandCursorImage(QObject *parent = nullptr); + + CursorTheme theme() const; + +Q_SIGNALS: + void themeChanged(); + +private: + void updateCursorTheme(); + + CursorTheme m_cursorTheme; +}; + +class CursorImage : public QObject +{ + Q_OBJECT +public: + explicit CursorImage(PointerInputRedirection *parent = nullptr); + ~CursorImage() override; + + void setEffectsOverrideCursor(Qt::CursorShape shape); + void removeEffectsOverrideCursor(); + void setWindowSelectionCursor(const QByteArray &shape); + void removeWindowSelectionCursor(); + + CursorTheme theme() const; + CursorSource *source() const; + void setSource(CursorSource *source); + + void updateCursorOutputs(const QPointF &pos); + +Q_SIGNALS: + void changed(); + +private: + void reevaluteSource(); + void updateServerCursor(const std::variant &cursor); + void updateDecoration(); + void updateDecorationCursor(); + void updateMoveResize(); + void updateDragCursor(); + + void handleFocusedSurfaceChanged(); + + PointerInputRedirection *m_pointer; + CursorSource *m_currentSource = nullptr; + WaylandCursorImage m_waylandImage; + + std::unique_ptr m_effectsCursor; + std::unique_ptr m_fallbackCursor; + std::unique_ptr m_moveResizeCursor; + std::unique_ptr m_windowSelectionCursor; + std::unique_ptr m_dragCursor; + + struct + { + std::unique_ptr cursor; + QMetaObject::Connection connection; + } m_decoration; + struct + { + QMetaObject::Connection connection; + std::unique_ptr surface; + std::unique_ptr shape; + CursorSource *cursor = nullptr; + } m_serverCursor; +}; +} diff --git a/local/recipes/kde/kwin/source/src/popup_input_filter.cpp b/local/recipes/kde/kwin/source/src/popup_input_filter.cpp new file mode 100644 index 0000000000..9f958d428c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/popup_input_filter.cpp @@ -0,0 +1,182 @@ +/* + SPDX-FileCopyrightText: 2017 Martin Graesslin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +*/ +#include "popup_input_filter.h" +#include "input_event.h" +#include "internalwindow.h" +#include "keyboard_input.h" +#include "wayland/seat.h" +#include "wayland_server.h" +#include "waylandwindow.h" +#include "window.h" +#include "workspace.h" + +#include + +namespace KWin +{ + +PopupInputFilter::PopupInputFilter() + : QObject() + , InputEventFilter(InputFilterOrder::Popup) +{ + connect(workspace(), &Workspace::windowAdded, this, &PopupInputFilter::handleWindowAdded); + connect(workspace(), &Workspace::windowActivated, this, &PopupInputFilter::handleWindowFocusChanged); +} + +void PopupInputFilter::handleWindowAdded(Window *window) +{ + if (m_popupWindows.contains(window)) { + return; + } + if (window->hasPopupGrab()) { + // TODO: verify that the Window is allowed as a popup + m_popupWindows << window; + focus(window); + + connect(window, &Window::closed, this, [this, window]() { + m_popupWindows.removeOne(window); + // Move focus to the parent popup. If that's the last popup, then move focus back to the parent + if (!m_popupWindows.isEmpty()) { + focus(m_popupWindows.constLast()); + } else { + input()->keyboard()->update(); + } + }); + } +} + +void PopupInputFilter::handleWindowFocusChanged() +{ + // user focussed a window through another mechanism such as a shortcut + cancelPopups(); +} + +bool PopupInputFilter::pointerButton(PointerButtonEvent *event) +{ + if (m_popupWindows.isEmpty()) { + return false; + } + if (event->state == PointerButtonState::Pressed) { + auto pointerFocus = input()->findToplevel(event->position); + if (!pointerFocus || !Window::belongToSameApplication(pointerFocus, m_popupWindows.constLast())) { + // a press on a window (or no window) not belonging to the popup window + cancelPopups(); + // filter out this press + return true; + } + if (pointerFocus && pointerFocus->isDecorated()) { + // test whether it is on the decoration + if (!pointerFocus->clientGeometry().contains(event->position)) { + cancelPopups(); + return true; + } + } + } + return false; +} + +bool PopupInputFilter::keyboardKey(KeyboardKeyEvent *event) +{ + if (m_popupWindows.isEmpty()) { + return false; + } + + Window *last = m_popupWindows.last(); + focus(last); + + if (auto internalWindow = qobject_cast(last)) { + QWindowSystemInterface::handleExtendedKeyEvent(internalWindow->handle(), + event->state != KeyboardKeyState::Released ? QEvent::KeyPress : QEvent::KeyRelease, + event->key, + event->modifiers, + event->nativeScanCode, + event->nativeVirtualKey, + 0, + event->text, + event->state == KeyboardKeyState::Repeated); + } else if (qobject_cast(last)) { + if (!passToInputMethod(event)) { + waylandServer()->seat()->setTimestamp(event->timestamp); + waylandServer()->seat()->notifyKeyboardKey(event->nativeScanCode, event->state, event->serial); + } + } + + return true; +} + +bool PopupInputFilter::touchDown(TouchDownEvent *event) +{ + if (m_popupWindows.isEmpty()) { + return false; + } + auto pointerFocus = input()->findToplevel(event->pos); + if (!pointerFocus || !Window::belongToSameApplication(pointerFocus, m_popupWindows.constLast())) { + // a touch on a window (or no window) not belonging to the popup window + cancelPopups(); + // filter out this touch + return true; + } + if (pointerFocus && pointerFocus->isDecorated()) { + // test whether it is on the decoration + if (!pointerFocus->clientGeometry().contains(event->pos)) { + cancelPopups(); + return true; + } + } + return false; +} + +bool PopupInputFilter::tabletToolTipEvent(TabletToolTipEvent *event) +{ + if (m_popupWindows.isEmpty()) { + return false; + } + if (event->type == TabletToolTipEvent::Type::Press) { + auto tabletFocus = input()->findToplevel(event->position); + if (!tabletFocus || !Window::belongToSameApplication(tabletFocus, m_popupWindows.constLast())) { + // a touch on a window (or no window) not belonging to the popup window + cancelPopups(); + // filter out this touch + return true; + } + if (tabletFocus && tabletFocus->isDecorated()) { + // test whether it is on the decoration + if (!tabletFocus->clientGeometry().contains(event->position)) { + cancelPopups(); + return true; + } + } + } + return false; +} + +void PopupInputFilter::focus(Window *popup) +{ + if (auto internalWindow = qobject_cast(m_popupWindows.constLast())) { + waylandServer()->seat()->setFocusedKeyboardSurface(nullptr); + if (QGuiApplication::focusWindow() != internalWindow->handle()) { + QWindowSystemInterface::handleFocusWindowChanged(internalWindow->handle()); + } + } else if (auto waylandWindow = qobject_cast(m_popupWindows.constLast())) { + if (QGuiApplication::focusWindow()) { + QWindowSystemInterface::handleFocusWindowChanged(nullptr); + } + waylandServer()->seat()->setFocusedKeyboardSurface(waylandWindow->surface(), input()->keyboard()->unfilteredKeys()); + } +} + +void PopupInputFilter::cancelPopups() +{ + while (!m_popupWindows.isEmpty()) { + auto c = m_popupWindows.takeLast(); + c->popupDone(); + } +} + +} + +#include "moc_popup_input_filter.cpp" diff --git a/local/recipes/kde/kwin/source/src/popup_input_filter.h b/local/recipes/kde/kwin/source/src/popup_input_filter.h new file mode 100644 index 0000000000..4d78b7156b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/popup_input_filter.h @@ -0,0 +1,36 @@ +/* + SPDX-FileCopyrightText: 2017 Martin Graesslin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +*/ +#pragma once + +#include "input.h" + +#include +#include + +namespace KWin +{ +class Window; + +class PopupInputFilter : public QObject, public InputEventFilter +{ + Q_OBJECT +public: + explicit PopupInputFilter(); + bool pointerButton(PointerButtonEvent *event) override; + bool keyboardKey(KeyboardKeyEvent *event) override; + bool touchDown(TouchDownEvent *event) override; + bool tabletToolTipEvent(TabletToolTipEvent *event) override; + +private: + void handleWindowAdded(Window *client); + void handleWindowFocusChanged(); + void focus(Window *popup); + void cancelPopups(); + + QList m_popupWindows; +}; +} diff --git a/local/recipes/kde/kwin/source/src/qml/CMakeLists.txt b/local/recipes/kde/kwin/source/src/qml/CMakeLists.txt new file mode 100644 index 0000000000..9001fb8901 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/qml/CMakeLists.txt @@ -0,0 +1,3 @@ +install(DIRECTORY outline/plasma DESTINATION ${KDE_INSTALL_DATADIR}/kwin-wayland/outline) +install(DIRECTORY onscreennotification/plasma DESTINATION ${KDE_INSTALL_DATADIR}/kwin-wayland/onscreennotification) +install(DIRECTORY frames/plasma DESTINATION ${KDE_INSTALL_DATADIR}/kwin-wayland/frames) diff --git a/local/recipes/kde/kwin/source/src/qml/frames/plasma/frame_none.qml b/local/recipes/kde/kwin/source/src/qml/frames/plasma/frame_none.qml new file mode 100644 index 0000000000..a48005f444 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/qml/frames/plasma/frame_none.qml @@ -0,0 +1,39 @@ +/* + SPDX-FileCopyrightText: 2022 MBition GmbH + SPDX-FileContributor: Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 + +import org.kde.kirigami as Kirigami + +ColumnLayout { + id: root + + property QtObject effectFrame: null + + spacing: 5 + + Kirigami.Icon { + id: icon + Layout.preferredWidth: root.effectFrame.iconSize.width + Layout.preferredHeight: root.effectFrame.iconSize.height + Layout.alignment: Qt.AlignHCenter + visible: valid + source: root.effectFrame.icon + } + + QQC2.Label { + id: label + Layout.fillWidth: true + textFormat: Text.PlainText + elide: Text.ElideRight + font: root.effectFrame.font + visible: text !== "" + text: root.effectFrame.text + } +} diff --git a/local/recipes/kde/kwin/source/src/qml/frames/plasma/frame_styled.qml b/local/recipes/kde/kwin/source/src/qml/frames/plasma/frame_styled.qml new file mode 100644 index 0000000000..dfbd49940c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/qml/frames/plasma/frame_styled.qml @@ -0,0 +1,61 @@ +/* + SPDX-FileCopyrightText: 2022 MBition GmbH + SPDX-FileContributor: Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami +import org.kde.ksvg as KSvg +import org.kde.plasma.components as PlasmaComponents + +Item { + id: root + + property QtObject effectFrame: null + + implicitWidth: layout.implicitWidth + layout.anchors.leftMargin + layout.anchors.rightMargin + implicitHeight: layout.implicitHeight + layout.anchors.topMargin + layout.anchors.bottomMargin + + KSvg.FrameSvgItem { + id: frameSvg + imagePath: "widgets/background" + opacity: root.effectFrame.frameOpacity + anchors.fill: parent + } + + RowLayout { + id: layout + anchors { + fill: parent + leftMargin: frameSvg.fixedMargins.left + rightMargin: frameSvg.fixedMargins.right + topMargin: frameSvg.fixedMargins.top + bottomMargin: frameSvg.fixedMargins.bottom + } + spacing: Kirigami.Units.smallSpacing + + Kirigami.Icon { + id: icon + Layout.preferredWidth: root.effectFrame.iconSize.width + Layout.preferredHeight: root.effectFrame.iconSize.height + Layout.alignment: Qt.AlignHCenter + animated: root.effectFrame.crossFadeEnabled + visible: valid + source: root.effectFrame.icon + } + + PlasmaComponents.Label { + id: label + Layout.fillWidth: true + textFormat: Text.PlainText + elide: Text.ElideRight + font: root.effectFrame.font + visible: text !== "" + text: root.effectFrame.text + } + } +} diff --git a/local/recipes/kde/kwin/source/src/qml/frames/plasma/frame_unstyled.qml b/local/recipes/kde/kwin/source/src/qml/frames/plasma/frame_unstyled.qml new file mode 100644 index 0000000000..132675084d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/qml/frames/plasma/frame_unstyled.qml @@ -0,0 +1,53 @@ +/* + SPDX-FileCopyrightText: 2022 MBition GmbH + SPDX-FileContributor: Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls as QQC2 + +import org.kde.kirigami as Kirigami + +Rectangle { + id: root + + property QtObject effectFrame: null + + implicitWidth: layout.implicitWidth + 2 * layout.anchors.margins + implicitHeight: layout.implicitHeight + 2 * layout.anchors.margins + + color: Qt.rgba(0, 0, 0, effectFrame.frameOpacity) + radius: Kirigami.Units.cornerRadius + + RowLayout { + id: layout + anchors { + fill: parent + margins: layout.spacing + } + spacing: 5 + + Kirigami.Icon { + id: icon + Layout.preferredWidth: root.effectFrame.iconSize.width + Layout.preferredHeight: root.effectFrame.iconSize.height + Layout.alignment: Qt.AlignHCenter + visible: valid + source: root.effectFrame.icon + } + + QQC2.Label { + id: label + Layout.fillWidth: true + color: "white" + textFormat: Text.PlainText + elide: Text.ElideRight + font: root.effectFrame.font + visible: text !== "" + text: root.effectFrame.text + } + } +} diff --git a/local/recipes/kde/kwin/source/src/qml/onscreennotification/plasma/dummydata/osd.qml b/local/recipes/kde/kwin/source/src/qml/onscreennotification/plasma/dummydata/osd.qml new file mode 100644 index 0000000000..c18cba89d5 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/qml/onscreennotification/plasma/dummydata/osd.qml @@ -0,0 +1,7 @@ +import QtQuick + +QtObject { + property bool visible: true + property string message: "This is an example message.\nUsing multiple lines" + property string iconName: "kwin" +} diff --git a/local/recipes/kde/kwin/source/src/qml/onscreennotification/plasma/main.qml b/local/recipes/kde/kwin/source/src/qml/onscreennotification/plasma/main.qml new file mode 100644 index 0000000000..54730167d3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/qml/onscreennotification/plasma/main.qml @@ -0,0 +1,35 @@ +/* + SPDX-FileCopyrightText: 2016 Martin Graesslin + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +*/ + +import QtQuick +import QtQuick.Layouts +import QtQuick.Window + +import org.kde.plasma.core as PlasmaCore +import org.kde.kirigami as Kirigami +import org.kde.plasma.components as PlasmaComponents3 + +PlasmaCore.Dialog { + location: PlasmaCore.Types.Floating + visible: osd.visible + flags: Qt.FramelessWindowHint + type: PlasmaCore.Dialog.OnScreenDisplay + outputOnly: true + + mainItem: RowLayout { + spacing: Kirigami.Units.smallSpacing + Kirigami.Icon { + implicitWidth: Kirigami.Units.iconSizes.medium + implicitHeight: implicitWidth + source: osd.iconName + visible: osd.iconName !== "" + } + PlasmaComponents3.Label { + text: osd.message + } + } +} diff --git a/local/recipes/kde/kwin/source/src/qml/outline/plasma/outline.qml b/local/recipes/kde/kwin/source/src/qml/outline/plasma/outline.qml new file mode 100644 index 0000000000..05db089cc3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/qml/outline/plasma/outline.qml @@ -0,0 +1,127 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2017 Kai Uwe Broulik + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ +import QtQuick +import QtQuick.Window +import org.kde.kwin +import org.kde.ksvg as KSvg +import org.kde.kirigami as Kirigami + +Window { + id: window + + readonly property int animationDuration: Kirigami.Units.longDuration + property bool animationEnabled: false + + flags: Qt.BypassWindowManagerHint | Qt.FramelessWindowHint + color: "transparent" + + // outline is a context property + x: outline.unifiedGeometry.x + y: outline.unifiedGeometry.y + width: outline.unifiedGeometry.width + height: outline.unifiedGeometry.height + + visible: outline.active + + onSceneGraphError: (error, message) => { + console.warn("outline: scene graph error:", message); + } + + onVisibleChanged: { + if (visible) { + if (outline.visualParentGeometry.width > 0 && outline.visualParentGeometry.height > 0) { + window.animationEnabled = false + // move our frame to the visual parent geometry + svg.setGeometry(outline.visualParentGeometry) + window.animationEnabled = true + // and then animate it nicely to its destination + svg.setGeometry(outline.geometry) + } else { + // no visual parent? just move it to its destination right away + window.animationEnabled = false + svg.setGeometry(outline.geometry) + window.animationEnabled = true + } + } + } + + Connections { + target: outline + // when unified geometry changes, this means our window position changed and any + // animation will potentially be offset and/or cut off, skip the animation in this case + function onUnifiedGeometryChanged() { + if (window.visible) { + window.animationEnabled = false + svg.setGeometry(outline.geometry) + window.animationEnabled = true + } + } + } + + KSvg.FrameSvgItem { + id: svg + + // takes into account the offset inside unified geometry + function setGeometry(geometry) { + x = geometry.x - outline.unifiedGeometry.x + y = geometry.y - outline.unifiedGeometry.y + width = geometry.width + height = geometry.height + } + + imagePath: "widgets/translucentbackground" + + x: 0 + y: 0 + width: 0 + height: 0 + + enabledBorders: { + var maximizedArea = Workspace.clientArea(Workspace.MaximizeArea, Workspace.screenAt(Qt.point(outline.geometry.x, outline.geometry.y)), Workspace.currentDesktop); + + var left = outline.geometry.x === maximizedArea.x; + var right = outline.geometry.x + outline.geometry.width === maximizedArea.x + maximizedArea.width; + var top = outline.geometry.y === maximizedArea.y; + var bottom = outline.geometry.y + outline.geometry.height === maximizedArea.y + maximizedArea.height; + + var borders = KSvg.FrameSvgItem.AllBorders; + if (left) { + borders = borders & ~KSvg.FrameSvgItem.LeftBorder; + } + if (right) { + borders = borders & ~KSvg.FrameSvgItem.RightBorder; + } + if (top) { + borders = borders & ~KSvg.FrameSvgItem.TopBorder; + } + if (bottom) { + borders = borders & ~KSvg.FrameSvgItem.BottomBorder; + } + if (left && right && bottom && top) { + borders = KSvg.FrameSvgItem.AllBorders; + } + return borders; + } + + Behavior on x { + NumberAnimation { duration: window.animationDuration; easing.type: Easing.InOutCubic; } + enabled: window.animationEnabled + } + Behavior on y { + NumberAnimation { duration: window.animationDuration; easing.type: Easing.InOutCubic; } + enabled: window.animationEnabled + } + Behavior on width { + NumberAnimation { duration: window.animationDuration; easing.type: Easing.InOutCubic; } + enabled: window.animationEnabled + } + Behavior on height { + NumberAnimation { duration: window.animationDuration; easing.type: Easing.InOutCubic; } + enabled: window.animationEnabled + } + } +} diff --git a/local/recipes/kde/kwin/source/src/resources.qrc b/local/recipes/kde/kwin/source/src/resources.qrc new file mode 100644 index 0000000000..deaeb090ee --- /dev/null +++ b/local/recipes/kde/kwin/source/src/resources.qrc @@ -0,0 +1,13 @@ + + + opengl/colormanagement.glsl + opengl/icc.frag + opengl/icc_core.frag + opengl/saturation.glsl + opengl/sdf.glsl + scene/shaders/debug_fractional.frag + scene/shaders/debug_fractional.vert + scene/shaders/debug_fractional_core.frag + scene/shaders/debug_fractional_core.vert + + diff --git a/local/recipes/kde/kwin/source/src/rootinfo_filter.cpp b/local/recipes/kde/kwin/source/src/rootinfo_filter.cpp new file mode 100644 index 0000000000..42b6383e94 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/rootinfo_filter.cpp @@ -0,0 +1,29 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "rootinfo_filter.h" +#include "netinfo.h" + +namespace KWin +{ + +RootInfoFilter::RootInfoFilter(RootInfo *parent) + : X11EventFilter(QList{XCB_CLIENT_MESSAGE}) + , m_rootInfo(parent) +{ +} + +bool RootInfoFilter::event(xcb_generic_event_t *event) +{ + NET::Properties dirtyProtocols; + NET::Properties2 dirtyProtocols2; + m_rootInfo->event(event, &dirtyProtocols, &dirtyProtocols2); + return false; +} + +} diff --git a/local/recipes/kde/kwin/source/src/rootinfo_filter.h b/local/recipes/kde/kwin/source/src/rootinfo_filter.h new file mode 100644 index 0000000000..3428c0ea61 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/rootinfo_filter.h @@ -0,0 +1,33 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2017 Martin Flöser + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once +#include "config-kwin.h" + +#if !KWIN_BUILD_X11 +#error Do not include on non-X11 builds +#endif + +#include "x11eventfilter.h" + +namespace KWin +{ +class RootInfo; + +class RootInfoFilter : public X11EventFilter +{ +public: + explicit RootInfoFilter(RootInfo *parent); + + bool event(xcb_generic_event_t *event) override; + +private: + RootInfo *m_rootInfo; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/rulebooksettings.cpp b/local/recipes/kde/kwin/source/src/rulebooksettings.cpp new file mode 100644 index 0000000000..9d4f2d4ea7 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/rulebooksettings.cpp @@ -0,0 +1,151 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 Henri Chain + SPDX-FileCopyrightText: 2021 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "rulebooksettings.h" +#include "rulesettings.h" + +#include + +namespace KWin +{ +RuleBookSettings::RuleBookSettings(KSharedConfig::Ptr config, QObject *parent) + : RuleBookSettingsBase(config, parent) +{ +} + +RuleBookSettings::RuleBookSettings(QObject *parent) + : RuleBookSettings(KSharedConfig::openConfig(QStringLiteral("kwinrulesrc"), KConfig::NoGlobals), parent) +{ +} + +RuleBookSettings::~RuleBookSettings() +{ + qDeleteAll(m_list); +} + +QList RuleBookSettings::rules() const +{ + QList result; + result.reserve(m_list.count()); + for (const RuleSettings *settings : std::as_const(m_list)) { + if (settings->enabled()) { + result.append(new Rules(settings)); + } + } + return result; +} + +bool RuleBookSettings::usrSave() +{ + bool result = true; + for (const auto &settings : std::as_const(m_list)) { + result &= settings->save(); + } + + // Remove deleted groups from config + for (const QString &groupName : std::as_const(m_storedGroups)) { + if (sharedConfig()->hasGroup(groupName) && !mRuleGroupList.contains(groupName)) { + sharedConfig()->deleteGroup(groupName); + } + } + m_storedGroups = mRuleGroupList; + + return result; +} + +void RuleBookSettings::usrRead() +{ + qDeleteAll(m_list); + m_list.clear(); + + // Legacy path for backwards compatibility with older config files without a rules list + if (mRuleGroupList.isEmpty() && mCount > 0) { + mRuleGroupList.reserve(mCount); + for (int i = 1; i <= count(); i++) { + mRuleGroupList.append(QString::number(i)); + } + save(); // Save the generated ruleGroupList property + } + + mCount = mRuleGroupList.count(); + m_storedGroups = mRuleGroupList; + + m_list.reserve(mRuleGroupList.count()); + for (const QString &groupName : std::as_const(mRuleGroupList)) { + m_list.append(new RuleSettings(sharedConfig(), groupName, this)); + } +} + +bool RuleBookSettings::usrIsSaveNeeded() const +{ + return isSaveNeeded() || std::any_of(m_list.cbegin(), m_list.cend(), [](const auto &settings) { + return settings->isSaveNeeded(); + }); +} + +int RuleBookSettings::ruleCount() const +{ + return m_list.count(); +} + +std::optional RuleBookSettings::indexForId(const QString &id) const +{ + for (int i = 0; i < m_list.count(); i++) { + if (m_list.at(i)->currentGroup() == id) { + return i; + } + } + return std::nullopt; +} + +RuleSettings *RuleBookSettings::ruleSettingsAt(int row) const +{ + Q_ASSERT(row >= 0 && row < m_list.count()); + return m_list.at(row); +} + +RuleSettings *RuleBookSettings::insertRuleSettingsAt(int row) +{ + Q_ASSERT(row >= 0 && row < m_list.count() + 1); + + const QString groupName = generateGroupName(); + RuleSettings *settings = new RuleSettings(sharedConfig(), groupName, this); + settings->setDefaults(); + + m_list.insert(row, settings); + mRuleGroupList.insert(row, groupName); + mCount++; + + return settings; +} + +void RuleBookSettings::removeRuleSettingsAt(int row) +{ + Q_ASSERT(row >= 0 && row < m_list.count()); + + delete m_list.at(row); + m_list.removeAt(row); + mRuleGroupList.removeAt(row); + mCount--; +} + +void RuleBookSettings::moveRuleSettings(int srcRow, int destRow) +{ + Q_ASSERT(srcRow >= 0 && srcRow < m_list.count() && destRow >= 0 && destRow < m_list.count()); + + m_list.insert(destRow, m_list.takeAt(srcRow)); + mRuleGroupList.insert(destRow, mRuleGroupList.takeAt(srcRow)); +} + +QString RuleBookSettings::generateGroupName() +{ + return QUuid::createUuid().toString(QUuid::WithoutBraces); +} +} diff --git a/local/recipes/kde/kwin/source/src/rulebooksettings.h b/local/recipes/kde/kwin/source/src/rulebooksettings.h new file mode 100644 index 0000000000..0ee9c46304 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/rulebooksettings.h @@ -0,0 +1,49 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2020 Henri Chain + SPDX-FileCopyrightText: 2021 Ismael Asensio + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "rulebooksettingsbase.h" +#include + +namespace KWin +{ +class Rules; +class RuleSettings; + +class RuleBookSettings : public RuleBookSettingsBase +{ +public: + RuleBookSettings(KSharedConfig::Ptr config, QObject *parent = nullptr); + RuleBookSettings(QObject *parent = nullptr); + ~RuleBookSettings(); + + QList rules() const; + + bool usrSave() override; + void usrRead() override; + bool usrIsSaveNeeded() const; + + int ruleCount() const; + std::optional indexForId(const QString &id) const; + RuleSettings *ruleSettingsAt(int row) const; + RuleSettings *insertRuleSettingsAt(int row); + void removeRuleSettingsAt(int row); + void moveRuleSettings(int srcRow, int destRow); + +private: + static QString generateGroupName(); + +private: + QList m_list; + QStringList m_storedGroups; +}; + +} diff --git a/local/recipes/kde/kwin/source/src/rulebooksettingsbase.kcfg b/local/recipes/kde/kwin/source/src/rulebooksettingsbase.kcfg new file mode 100644 index 0000000000..5f8f210492 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/rulebooksettingsbase.kcfg @@ -0,0 +1,17 @@ + + + + + + + + 0 + + + + QStringList() + + + diff --git a/local/recipes/kde/kwin/source/src/rulebooksettingsbase.kcfgc b/local/recipes/kde/kwin/source/src/rulebooksettingsbase.kcfgc new file mode 100644 index 0000000000..d2a5b82778 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/rulebooksettingsbase.kcfgc @@ -0,0 +1,5 @@ +File=rulebooksettingsbase.kcfg +NameSpace=KWin +ClassName=RuleBookSettingsBase +Mutators=true +ParentInConstructor=true diff --git a/local/recipes/kde/kwin/source/src/rules.cpp b/local/recipes/kde/kwin/source/src/rules.cpp new file mode 100644 index 0000000000..b99d0424f1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/rules.cpp @@ -0,0 +1,1083 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2004 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "rules.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifndef KCMRULES +#include "client_machine.h" +#include "main.h" +#include "virtualdesktops.h" +#include "window.h" +#endif + +#include "core/output.h" +#include "input.h" +#include "rulebooksettings.h" +#include "rulesettings.h" +#include "wayland_server.h" +#include "workspace.h" +#include "xdgactivationv1.h" + +namespace KWin +{ + +Rules::Rules() + : layerrule(UnusedForceRule) + , wmclassmatch(UnimportantMatch) + , wmclasscomplete(UnimportantMatch) + , windowrolematch(UnimportantMatch) + , titlematch(UnimportantMatch) + , clientmachinematch(UnimportantMatch) + , tagmatch(UnimportantMatch) + , types(NET::AllTypesMask) + , placementrule(UnusedForceRule) + , positionrule(UnusedSetRule) + , sizerule(UnusedSetRule) + , minsizerule(UnusedForceRule) + , maxsizerule(UnusedForceRule) + , opacityactiverule(UnusedForceRule) + , opacityinactiverule(UnusedForceRule) + , ignoregeometryrule(UnusedSetRule) + , desktopsrule(UnusedSetRule) + , screenrule(UnusedSetRule) + , activityrule(UnusedSetRule) + , maximizevertrule(UnusedSetRule) + , maximizehorizrule(UnusedSetRule) + , minimizerule(UnusedSetRule) + , skiptaskbarrule(UnusedSetRule) + , skippagerrule(UnusedSetRule) + , skipswitcherrule(UnusedSetRule) + , aboverule(UnusedSetRule) + , belowrule(UnusedSetRule) + , fullscreenrule(UnusedSetRule) + , noborderrule(UnusedSetRule) + , decocolorrule(UnusedForceRule) + , blockcompositingrule(UnusedForceRule) + , fsplevelrule(UnusedForceRule) + , fpplevelrule(UnusedForceRule) + , acceptfocusrule(UnusedForceRule) + , closeablerule(UnusedForceRule) + , autogrouprule(UnusedForceRule) + , autogroupfgrule(UnusedForceRule) + , autogroupidrule(UnusedForceRule) + , strictgeometryrule(UnusedForceRule) + , shortcutrule(UnusedSetRule) + , disableglobalshortcutsrule(UnusedForceRule) + , desktopfilerule(UnusedSetRule) + , adaptivesyncrule(UnusedForceRule) + , tearingrule(UnusedForceRule) +{ +} + +#define READ_MATCH_STRING(var, func) \ + var = settings->var() func; \ + var##match = static_cast(settings->var##match()) + +#define READ_SET_RULE(var) \ + var = settings->var(); \ + var##rule = static_cast(settings->var##rule()) + +#define READ_FORCE_RULE(var, func) \ + var = func(settings->var()); \ + var##rule = convertForceRule(settings->var##rule()) + +Rules::Rules(const RuleSettings *settings) +{ + readFromSettings(settings); +} + +void Rules::readFromSettings(const RuleSettings *settings) +{ + m_id = settings->currentGroup(); + m_enabled = settings->enabled(); + description = settings->description(); + if (description.isEmpty()) { + description = settings->descriptionLegacy(); + } + READ_MATCH_STRING(wmclass, ); + wmclasscomplete = settings->wmclasscomplete(); + READ_MATCH_STRING(windowrole, ); + READ_MATCH_STRING(title, ); + READ_MATCH_STRING(clientmachine, .toLower()); + READ_MATCH_STRING(tag, ); + types = WindowTypes(settings->types()); + READ_FORCE_RULE(placement, ); + READ_SET_RULE(position); + READ_SET_RULE(size); + if (size.isEmpty() && sizerule != static_cast(Remember)) { + sizerule = UnusedSetRule; + } + READ_FORCE_RULE(minsize, ); + if (!minsize.isValid()) { + minsize = QSize(1, 1); + } + READ_FORCE_RULE(maxsize, ); + if (maxsize.isEmpty()) { + maxsize = QSize(32767, 32767); + } + READ_FORCE_RULE(opacityactive, ); + READ_FORCE_RULE(opacityinactive, ); + READ_SET_RULE(ignoregeometry); + READ_SET_RULE(desktops); + READ_SET_RULE(screen); + READ_SET_RULE(activity); + READ_SET_RULE(maximizevert); + READ_SET_RULE(maximizehoriz); + READ_SET_RULE(minimize); + READ_SET_RULE(skiptaskbar); + READ_SET_RULE(skippager); + READ_SET_RULE(skipswitcher); + READ_SET_RULE(above); + READ_SET_RULE(below); + READ_SET_RULE(fullscreen); + READ_SET_RULE(noborder); + + READ_FORCE_RULE(decocolor, getDecoColor); + if (decocolor.isEmpty()) { + decocolorrule = UnusedForceRule; + } + + READ_FORCE_RULE(blockcompositing, ); + READ_FORCE_RULE(fsplevel, FocusStealingPreventionLevel); + READ_FORCE_RULE(fpplevel, FocusStealingPreventionLevel); + READ_FORCE_RULE(acceptfocus, ); + READ_FORCE_RULE(closeable, ); + READ_FORCE_RULE(autogroup, ); + READ_FORCE_RULE(autogroupfg, ); + READ_FORCE_RULE(autogroupid, ); + READ_FORCE_RULE(strictgeometry, ); + READ_SET_RULE(shortcut); + READ_FORCE_RULE(disableglobalshortcuts, ); + READ_SET_RULE(desktopfile); + READ_FORCE_RULE(layer, ); + READ_FORCE_RULE(adaptivesync, ); + READ_FORCE_RULE(tearing, ); +} + +#undef READ_MATCH_STRING +#undef READ_SET_RULE +#undef READ_FORCE_RULE +#undef READ_FORCE_RULE2 + +QString Rules::id() const +{ + return m_id; +} + +#define WRITE_MATCH_STRING(var, capital, force) \ + settings->set##capital##match(var##match); \ + if (!var.isEmpty() || force) { \ + settings->set##capital(var); \ + } + +#define WRITE_SET_RULE(var, capital, func) \ + settings->set##capital##rule(var##rule); \ + if (var##rule != UnusedSetRule) { \ + settings->set##capital(func(var)); \ + } + +#define WRITE_FORCE_RULE(var, capital, func) \ + settings->set##capital##rule(var##rule); \ + if (var##rule != UnusedForceRule) { \ + settings->set##capital(func(var)); \ + } + +void Rules::write(RuleSettings *settings) const +{ + settings->setDefaults(); + + settings->setEnabled(m_enabled); + settings->setDescription(description); + // always write wmclass + WRITE_MATCH_STRING(wmclass, Wmclass, true); + settings->setWmclasscomplete(wmclasscomplete); + WRITE_MATCH_STRING(windowrole, Windowrole, false); + WRITE_MATCH_STRING(title, Title, false); + WRITE_MATCH_STRING(clientmachine, Clientmachine, false); + WRITE_MATCH_STRING(tag, Tag, false); + settings->setTypes(types); + WRITE_FORCE_RULE(placement, Placement, ); + WRITE_SET_RULE(position, Position, ); + WRITE_SET_RULE(size, Size, ); + WRITE_FORCE_RULE(minsize, Minsize, ); + WRITE_FORCE_RULE(maxsize, Maxsize, ); + WRITE_FORCE_RULE(opacityactive, Opacityactive, ); + WRITE_FORCE_RULE(opacityinactive, Opacityinactive, ); + WRITE_SET_RULE(ignoregeometry, Ignoregeometry, ); + WRITE_SET_RULE(desktops, Desktops, ); + WRITE_SET_RULE(screen, Screen, ); + WRITE_SET_RULE(activity, Activity, ); + WRITE_SET_RULE(maximizevert, Maximizevert, ); + WRITE_SET_RULE(maximizehoriz, Maximizehoriz, ); + WRITE_SET_RULE(minimize, Minimize, ); + WRITE_SET_RULE(skiptaskbar, Skiptaskbar, ); + WRITE_SET_RULE(skippager, Skippager, ); + WRITE_SET_RULE(skipswitcher, Skipswitcher, ); + WRITE_SET_RULE(above, Above, ); + WRITE_SET_RULE(below, Below, ); + WRITE_SET_RULE(fullscreen, Fullscreen, ); + WRITE_SET_RULE(noborder, Noborder, ); + auto colorToString = [](const QString &value) -> QString { + if (value.endsWith(QLatin1String(".colors"))) { + return QFileInfo(value).baseName(); + } else { + return value; + } + }; + WRITE_FORCE_RULE(decocolor, Decocolor, colorToString); + WRITE_FORCE_RULE(blockcompositing, Blockcompositing, ); + const auto focusStealingLevelToInt = [](FocusStealingPreventionLevel fsp) { + return int(fsp); + }; + WRITE_FORCE_RULE(fsplevel, Fsplevel, focusStealingLevelToInt); + WRITE_FORCE_RULE(fpplevel, Fpplevel, focusStealingLevelToInt); + WRITE_FORCE_RULE(acceptfocus, Acceptfocus, ); + WRITE_FORCE_RULE(closeable, Closeable, ); + WRITE_FORCE_RULE(autogroup, Autogroup, ); + WRITE_FORCE_RULE(autogroupfg, Autogroupfg, ); + WRITE_FORCE_RULE(autogroupid, Autogroupid, ); + WRITE_FORCE_RULE(strictgeometry, Strictgeometry, ); + WRITE_SET_RULE(shortcut, Shortcut, ); + WRITE_FORCE_RULE(disableglobalshortcuts, Disableglobalshortcuts, ); + WRITE_SET_RULE(desktopfile, Desktopfile, ); + WRITE_FORCE_RULE(layer, Layer, ); + WRITE_FORCE_RULE(adaptivesync, Adaptivesync, ); +} + +#undef WRITE_MATCH_STRING +#undef WRITE_SET_RULE +#undef WRITE_FORCE_RULE + +// returns true if it doesn't affect anything +bool Rules::isEmpty() const +{ + return placementrule == UnusedForceRule + && positionrule == UnusedSetRule + && sizerule == UnusedSetRule + && minsizerule == UnusedForceRule + && maxsizerule == UnusedForceRule + && opacityactiverule == UnusedForceRule + && opacityinactiverule == UnusedForceRule + && ignoregeometryrule == UnusedSetRule + && desktopsrule == UnusedSetRule + && screenrule == UnusedSetRule + && activityrule == UnusedSetRule + && maximizevertrule == UnusedSetRule + && maximizehorizrule == UnusedSetRule + && minimizerule == UnusedSetRule + && skiptaskbarrule == UnusedSetRule + && skippagerrule == UnusedSetRule + && skipswitcherrule == UnusedSetRule + && aboverule == UnusedSetRule + && belowrule == UnusedSetRule + && fullscreenrule == UnusedSetRule + && noborderrule == UnusedSetRule + && decocolorrule == UnusedForceRule + && blockcompositingrule == UnusedForceRule + && fsplevelrule == UnusedForceRule + && fpplevelrule == UnusedForceRule + && acceptfocusrule == UnusedForceRule + && closeablerule == UnusedForceRule + && autogrouprule == UnusedForceRule + && autogroupfgrule == UnusedForceRule + && autogroupidrule == UnusedForceRule + && strictgeometryrule == UnusedForceRule + && shortcutrule == UnusedSetRule + && disableglobalshortcutsrule == UnusedForceRule + && desktopfilerule == UnusedSetRule + && layerrule == UnusedForceRule + && adaptivesyncrule == UnusedForceRule + && tearingrule == UnusedForceRule; +} + +Rules::ForceRule Rules::convertForceRule(int v) +{ + if (v == DontAffect || v == Force || v == ForceTemporarily) { + return static_cast(v); + } + return UnusedForceRule; +} + +QString Rules::getDecoColor(const QString &themeName) +{ + if (themeName.isEmpty()) { + return QString(); + } + // find the actual scheme file + return QStandardPaths::locate(QStandardPaths::GenericDataLocation, + QLatin1String("color-schemes/") + themeName + QLatin1String(".colors")); +} + +bool typeMatchesMask(WindowType type, WindowTypes mask) +{ + switch (type) { + // clang-format off +#define CHECK_TYPE_MASK( type ) \ +case WindowType:: type: \ + if( int(mask) & int(type##Mask) ) \ + return true; \ + break; + // clang-format on + CHECK_TYPE_MASK(Normal) + CHECK_TYPE_MASK(Desktop) + CHECK_TYPE_MASK(Dock) + CHECK_TYPE_MASK(Toolbar) + CHECK_TYPE_MASK(Menu) + CHECK_TYPE_MASK(Dialog) + CHECK_TYPE_MASK(Override) + CHECK_TYPE_MASK(TopMenu) + CHECK_TYPE_MASK(Utility) + CHECK_TYPE_MASK(Splash) + CHECK_TYPE_MASK(DropdownMenu) + CHECK_TYPE_MASK(PopupMenu) + CHECK_TYPE_MASK(Tooltip) + CHECK_TYPE_MASK(Notification) + CHECK_TYPE_MASK(ComboBox) + CHECK_TYPE_MASK(DNDIcon) + CHECK_TYPE_MASK(OnScreenDisplay) + CHECK_TYPE_MASK(CriticalNotification) + CHECK_TYPE_MASK(AppletPopup) +#undef CHECK_TYPE_MASK + default: + break; + } + return false; +} + +bool Rules::matchType(WindowType match_type) const +{ + if (types != AllTypesMask) { + if (match_type == WindowType::Unknown) { + match_type = WindowType::Normal; // WindowType::Unknown->WindowType::Normal is only here for matching + } + if (!typeMatchesMask(match_type, types)) { + return false; + } + } + return true; +} + +bool Rules::matchWMClass(const QString &match_class, const QString &match_name) const +{ + if (wmclassmatch != UnimportantMatch) { + // TODO optimize? + QString cwmclass = wmclasscomplete + ? match_name + ' ' + match_class + : match_class; + if (wmclassmatch == RegExpMatch && !QRegularExpression(wmclass).match(cwmclass).hasMatch()) { + return false; + } + if (wmclassmatch == ExactMatch && cwmclass != wmclass) { + return false; + } + if (wmclassmatch == SubstringMatch && !cwmclass.contains(wmclass)) { + return false; + } + } + return true; +} + +bool Rules::matchRole(const QString &match_role) const +{ + if (windowrolematch != UnimportantMatch) { + if (windowrolematch == RegExpMatch && !QRegularExpression(windowrole).match(match_role).hasMatch()) { + return false; + } + if (windowrolematch == ExactMatch && match_role != windowrole) { + return false; + } + if (windowrolematch == SubstringMatch && !match_role.contains(windowrole)) { + return false; + } + } + return true; +} + +bool Rules::matchTitle(const QString &match_title) const +{ + if (titlematch != UnimportantMatch) { + if (titlematch == RegExpMatch && !QRegularExpression(title).match(match_title).hasMatch()) { + return false; + } + if (titlematch == ExactMatch && title != match_title) { + return false; + } + if (titlematch == SubstringMatch && !match_title.contains(title)) { + return false; + } + } + return true; +} + +bool Rules::matchClientMachine(const QString &match_machine, bool local) const +{ + if (clientmachinematch != UnimportantMatch) { + // if it's localhost, check also "localhost" before checking hostname + if (match_machine != "localhost" && local + && matchClientMachine("localhost", true)) { + return true; + } + if (clientmachinematch == RegExpMatch + && !QRegularExpression(clientmachine).match(match_machine).hasMatch()) { + return false; + } + if (clientmachinematch == ExactMatch + && clientmachine != match_machine) { + return false; + } + if (clientmachinematch == SubstringMatch + && !match_machine.contains(clientmachine)) { + return false; + } + } + return true; +} + +bool Rules::matchTag(const QString &match_tag) const +{ + if (tagmatch != UnimportantMatch) { + if (tagmatch == RegExpMatch && !QRegularExpression(tag).match(match_tag).hasMatch()) { + return false; + } + if (tagmatch == ExactMatch && tag != match_tag) { + return false; + } + if (tagmatch == SubstringMatch && !match_tag.contains(tag)) { + return false; + } + } + return true; +} + +#ifndef KCMRULES +bool Rules::match(const Window *c) const +{ + if (!m_enabled) { + return false; + } + if (!matchType(c->windowType())) { + return false; + } + if (!matchWMClass(c->resourceClass(), c->resourceName())) { + return false; + } + if (!matchRole(c->windowRole())) { + return false; + } + if (!matchClientMachine(c->clientMachine()->hostName(), c->clientMachine()->isLocal())) { + return false; + } + if (titlematch != UnimportantMatch) { // track title changes to rematch rules + QObject::connect(c, &Window::captionNormalChanged, c, &Window::evaluateWindowRules, Qt::UniqueConnection); + } + if (!matchTitle(c->captionNormal())) { + return false; + } + if (!matchTag(c->tag())) { + return false; + } + return true; +} + +#define NOW_REMEMBER(_T_, _V_) ((selection & _T_) && (_V_##rule == (SetRule)Remember)) + +bool Rules::update(Window *c, int selection) +{ + // TODO check this setting is for this client ? + bool updated = false; + if NOW_REMEMBER (Position, position) { + if (!c->isFullScreen()) { + QPoint new_pos = position; + // don't use the position in the direction which is maximized + if ((c->maximizeMode() & MaximizeHorizontal) == 0) { + new_pos.setX(c->pos().x()); + } + if ((c->maximizeMode() & MaximizeVertical) == 0) { + new_pos.setY(c->pos().y()); + } + updated = updated || position != new_pos; + position = new_pos; + } + } + if NOW_REMEMBER (Size, size) { + if (!c->isFullScreen()) { + QSize new_size = size; + // don't use the position in the direction which is maximized + if ((c->maximizeMode() & MaximizeHorizontal) == 0) { + new_size.setWidth(c->size().width()); + } + if ((c->maximizeMode() & MaximizeVertical) == 0) { + new_size.setHeight(c->size().height()); + } + updated = updated || size != new_size; + size = new_size; + } + } + if NOW_REMEMBER (Desktops, desktops) { + updated = updated || desktops != c->desktopIds(); + desktops = c->desktopIds(); + } + if NOW_REMEMBER (Screen, screen) { + const int index = workspace()->outputs().indexOf(c->output()); + updated = updated || screen != index; + screen = index; + } + if NOW_REMEMBER (Activity, activity) { + updated = updated || activity != c->activities(); + activity = c->activities(); + } + if NOW_REMEMBER (MaximizeVert, maximizevert) { + updated = updated || maximizevert != bool(c->maximizeMode() & MaximizeVertical); + maximizevert = c->maximizeMode() & MaximizeVertical; + } + if NOW_REMEMBER (MaximizeHoriz, maximizehoriz) { + updated = updated || maximizehoriz != bool(c->maximizeMode() & MaximizeHorizontal); + maximizehoriz = c->maximizeMode() & MaximizeHorizontal; + } + if NOW_REMEMBER (Minimize, minimize) { + updated = updated || minimize != c->isMinimized(); + minimize = c->isMinimized(); + } + if NOW_REMEMBER (SkipTaskbar, skiptaskbar) { + updated = updated || skiptaskbar != c->skipTaskbar(); + skiptaskbar = c->skipTaskbar(); + } + if NOW_REMEMBER (SkipPager, skippager) { + updated = updated || skippager != c->skipPager(); + skippager = c->skipPager(); + } + if NOW_REMEMBER (SkipSwitcher, skipswitcher) { + updated = updated || skipswitcher != c->skipSwitcher(); + skipswitcher = c->skipSwitcher(); + } + if NOW_REMEMBER (Above, above) { + updated = updated || above != c->keepAbove(); + above = c->keepAbove(); + } + if NOW_REMEMBER (Below, below) { + updated = updated || below != c->keepBelow(); + below = c->keepBelow(); + } + if NOW_REMEMBER (Fullscreen, fullscreen) { + updated = updated || fullscreen != c->isFullScreen(); + fullscreen = c->isFullScreen(); + } + if NOW_REMEMBER (NoBorder, noborder) { + updated = updated || noborder != c->noBorder(); + noborder = c->noBorder(); + } + if NOW_REMEMBER (DesktopFile, desktopfile) { + updated = updated || desktopfile != c->desktopFileName(); + desktopfile = c->desktopFileName(); + } + return updated; +} + +#undef NOW_REMEMBER + +#define APPLY_RULE(var, name, type) \ + bool Rules::apply##name(type &arg, bool init) const \ + { \ + if (checkSetRule(var##rule, init)) \ + arg = this->var; \ + return checkSetStop(var##rule); \ + } + +#define APPLY_FORCE_RULE(var, name, type) \ + bool Rules::apply##name(type &arg) const \ + { \ + if (checkForceRule(var##rule)) \ + arg = this->var; \ + return checkForceStop(var##rule); \ + } + +APPLY_FORCE_RULE(placement, Placement, PlacementPolicy) + +bool Rules::applyGeometry(RectF &rect, bool init) const +{ + QPointF p = rect.topLeft(); + QSizeF s = rect.size(); + bool ret = false; // no short-circuiting + if (applyPosition(p, init)) { + rect.moveTopLeft(p); + ret = true; + } + if (applySize(s, init)) { + rect.setSize(s); + ret = true; + } + return ret; +} + +bool Rules::applyPosition(QPointF &pos, bool init) const +{ + if (this->position != invalidPoint && checkSetRule(positionrule, init)) { + pos = this->position; + } + return checkSetStop(positionrule); +} + +bool Rules::applySize(QSizeF &s, bool init) const +{ + if (this->size.isValid() && checkSetRule(sizerule, init)) { + s = this->size; + } + return checkSetStop(sizerule); +} + +bool Rules::applyOpacityActive(qreal &s) const +{ + if (checkForceRule(opacityactiverule)) { + s = opacityactive / 100.0; + } else if (checkForceRule(opacityinactiverule)) { + s = 1.0; + } + return checkForceStop(opacityactiverule) || checkForceStop(opacityinactiverule); +} + +bool Rules::applyOpacityInactive(qreal &s) const +{ + if (checkForceRule(opacityinactiverule)) { + s = opacityinactive / 100.0; + } else if (checkForceRule(opacityactiverule)) { + s = 1.0; + } + return checkForceStop(opacityactiverule) || checkForceStop(opacityinactiverule); +} + +APPLY_FORCE_RULE(minsize, MinSize, QSizeF) +APPLY_FORCE_RULE(maxsize, MaxSize, QSizeF) +APPLY_RULE(ignoregeometry, IgnoreGeometry, bool) + +APPLY_RULE(screen, Screen, int) +APPLY_RULE(activity, Activity, QStringList) +APPLY_FORCE_RULE(layer, Layer, enum Layer) + +bool Rules::applyDesktops(QList &vds, bool init) const +{ + if (checkSetRule(desktopsrule, init)) { + vds.clear(); + for (auto id : desktops) { + if (auto vd = VirtualDesktopManager::self()->desktopForId(id)) { + vds << vd; + } + } + } + return checkSetStop(desktopsrule); +} + +bool Rules::applyMaximizeHoriz(MaximizeMode &mode, bool init) const +{ + if (checkSetRule(maximizehorizrule, init)) { + mode = static_cast((maximizehoriz ? MaximizeHorizontal : 0) | (mode & MaximizeVertical)); + } + return checkSetStop(maximizehorizrule); +} + +bool Rules::applyMaximizeVert(MaximizeMode &mode, bool init) const +{ + if (checkSetRule(maximizevertrule, init)) { + mode = static_cast((maximizevert ? MaximizeVertical : 0) | (mode & MaximizeHorizontal)); + } + return checkSetStop(maximizevertrule); +} + +APPLY_RULE(minimize, Minimize, bool) +APPLY_RULE(skiptaskbar, SkipTaskbar, bool) +APPLY_RULE(skippager, SkipPager, bool) +APPLY_RULE(skipswitcher, SkipSwitcher, bool) +APPLY_RULE(above, KeepAbove, bool) +APPLY_RULE(below, KeepBelow, bool) +APPLY_RULE(fullscreen, FullScreen, bool) +APPLY_RULE(noborder, NoBorder, bool) +APPLY_FORCE_RULE(decocolor, DecoColor, QString) +APPLY_FORCE_RULE(blockcompositing, BlockCompositing, bool) +APPLY_FORCE_RULE(fsplevel, FSP, FocusStealingPreventionLevel) +APPLY_FORCE_RULE(fpplevel, FPP, FocusStealingPreventionLevel) +APPLY_FORCE_RULE(acceptfocus, AcceptFocus, bool) +APPLY_FORCE_RULE(closeable, Closeable, bool) +APPLY_FORCE_RULE(autogroup, Autogrouping, bool) +APPLY_FORCE_RULE(autogroupfg, AutogroupInForeground, bool) +APPLY_FORCE_RULE(autogroupid, AutogroupById, QString) +APPLY_FORCE_RULE(strictgeometry, StrictGeometry, bool) +APPLY_RULE(shortcut, Shortcut, QString) +APPLY_FORCE_RULE(disableglobalshortcuts, DisableGlobalShortcuts, bool) +APPLY_RULE(desktopfile, DesktopFile, QString) +APPLY_FORCE_RULE(adaptivesync, AdaptiveSync, bool) +APPLY_FORCE_RULE(tearing, Tearing, bool) + +#undef APPLY_RULE +#undef APPLY_FORCE_RULE + +#define DISCARD_USED_SET_RULE(var) \ + do { \ + if (var##rule == (SetRule)ApplyNow || (withdrawn && var##rule == (SetRule)ForceTemporarily)) { \ + var##rule = UnusedSetRule; \ + changed = true; \ + } \ + } while (false) +#define DISCARD_USED_FORCE_RULE(var) \ + do { \ + if (withdrawn && var##rule == (ForceRule)ForceTemporarily) { \ + var##rule = UnusedForceRule; \ + changed = true; \ + } \ + } while (false) + +bool Rules::discardUsed(bool withdrawn) +{ + bool changed = false; + DISCARD_USED_FORCE_RULE(placement); + DISCARD_USED_SET_RULE(position); + DISCARD_USED_SET_RULE(size); + DISCARD_USED_FORCE_RULE(minsize); + DISCARD_USED_FORCE_RULE(maxsize); + DISCARD_USED_FORCE_RULE(opacityactive); + DISCARD_USED_FORCE_RULE(opacityinactive); + DISCARD_USED_SET_RULE(ignoregeometry); + DISCARD_USED_SET_RULE(desktops); + DISCARD_USED_SET_RULE(screen); + DISCARD_USED_SET_RULE(activity); + DISCARD_USED_SET_RULE(maximizevert); + DISCARD_USED_SET_RULE(maximizehoriz); + DISCARD_USED_SET_RULE(minimize); + DISCARD_USED_SET_RULE(skiptaskbar); + DISCARD_USED_SET_RULE(skippager); + DISCARD_USED_SET_RULE(skipswitcher); + DISCARD_USED_SET_RULE(above); + DISCARD_USED_SET_RULE(below); + DISCARD_USED_SET_RULE(fullscreen); + DISCARD_USED_SET_RULE(noborder); + DISCARD_USED_FORCE_RULE(decocolor); + DISCARD_USED_FORCE_RULE(blockcompositing); + DISCARD_USED_FORCE_RULE(fsplevel); + DISCARD_USED_FORCE_RULE(fpplevel); + DISCARD_USED_FORCE_RULE(acceptfocus); + DISCARD_USED_FORCE_RULE(closeable); + DISCARD_USED_FORCE_RULE(autogroup); + DISCARD_USED_FORCE_RULE(autogroupfg); + DISCARD_USED_FORCE_RULE(autogroupid); + DISCARD_USED_FORCE_RULE(strictgeometry); + DISCARD_USED_SET_RULE(shortcut); + DISCARD_USED_FORCE_RULE(disableglobalshortcuts); + DISCARD_USED_SET_RULE(desktopfile); + DISCARD_USED_FORCE_RULE(layer); + DISCARD_USED_FORCE_RULE(adaptivesync); + DISCARD_USED_FORCE_RULE(tearing); + + return changed; +} +#undef DISCARD_USED_SET_RULE +#undef DISCARD_USED_FORCE_RULE + +#endif + +QDebug &operator<<(QDebug &stream, const Rules *r) +{ + return stream << "[" << r->description << ":" << r->wmclass << "]"; +} + +#ifndef KCMRULES + +void WindowRules::update(Window *c, int selection) +{ + bool updated = false; + for (QList::ConstIterator it = rules.constBegin(); + it != rules.constEnd(); + ++it) { + if ((*it)->update(c, selection)) { // no short-circuiting here + updated = true; + } + } + if (updated) { + workspace()->rulebook()->requestDiskStorage(); + } +} + +#define CHECK_RULE(rule, type) \ + type WindowRules::check##rule(type arg, bool init) const \ + { \ + if (rules.count() == 0) \ + return arg; \ + type ret = arg; \ + for (QList::ConstIterator it = rules.constBegin(); \ + it != rules.constEnd(); \ + ++it) { \ + if ((*it)->apply##rule(ret, init)) \ + break; \ + } \ + return ret; \ + } + +#define CHECK_FORCE_RULE(rule, type) \ + type WindowRules::check##rule(type arg) const \ + { \ + if (rules.count() == 0) \ + return arg; \ + type ret = arg; \ + for (QList::ConstIterator it = rules.begin(); \ + it != rules.end(); \ + ++it) { \ + if ((*it)->apply##rule(ret)) \ + break; \ + } \ + return ret; \ + } + +CHECK_FORCE_RULE(Placement, PlacementPolicy) + +RectF WindowRules::checkGeometry(RectF rect, bool init) const +{ + return RectF(checkPosition(rect.topLeft(), init), checkSize(rect.size(), init)); +} + +RectF WindowRules::checkGeometrySafe(RectF rect, bool init) const +{ + const auto pos = checkPositionSafe(init); + return RectF(pos.value_or(rect.topLeft()), checkSize(rect.size(), init)); +} + +std::optional WindowRules::checkPositionSafe(bool init) const +{ + const auto ret = checkPosition(invalidPoint, init); + if (ret == invalidPoint) { + return std::nullopt; + } + const auto outputs = workspace()->outputs(); + const bool inAnyOutput = std::any_of(outputs.begin(), outputs.end(), [ret](const auto output) { + return output->geometryF().contains(ret); + }); + if (inAnyOutput) { + return ret; + } else { + return std::nullopt; + } +} + +CHECK_RULE(Position, QPointF) +CHECK_RULE(Size, QSizeF) +CHECK_FORCE_RULE(MinSize, QSizeF) +CHECK_FORCE_RULE(MaxSize, QSizeF) +CHECK_FORCE_RULE(OpacityActive, qreal) +CHECK_FORCE_RULE(OpacityInactive, qreal) +CHECK_RULE(IgnoreGeometry, bool) + +CHECK_RULE(Desktops, QList) +CHECK_RULE(Activity, QStringList) +CHECK_RULE(MaximizeVert, MaximizeMode) +CHECK_RULE(MaximizeHoriz, MaximizeMode) + +MaximizeMode WindowRules::checkMaximize(MaximizeMode mode, bool init) const +{ + bool vert = checkMaximizeVert(mode, init) & MaximizeVertical; + bool horiz = checkMaximizeHoriz(mode, init) & MaximizeHorizontal; + return static_cast((vert ? MaximizeVertical : 0) | (horiz ? MaximizeHorizontal : 0)); +} + +LogicalOutput *WindowRules::checkOutput(LogicalOutput *output, bool init) const +{ + if (rules.isEmpty()) { + return output; + } + int ret = workspace()->outputs().indexOf(output); + for (Rules *rule : rules) { + if (rule->applyScreen(ret, init)) { + break; + } + } + LogicalOutput *ruleOutput = workspace()->outputs().value(ret); + return ruleOutput ? ruleOutput : output; +} + +DecorationPolicy WindowRules::checkDecorationPolicy(DecorationPolicy policy, bool init) const +{ + if (checkNoBorder(true, init) == false) { + return DecorationPolicy::Server; + } + if (checkNoBorder(false, init) == true) { + return DecorationPolicy::None; + } + + return policy; +} + +CHECK_RULE(Minimize, bool) +CHECK_RULE(SkipTaskbar, bool) +CHECK_RULE(SkipPager, bool) +CHECK_RULE(SkipSwitcher, bool) +CHECK_RULE(KeepAbove, bool) +CHECK_RULE(KeepBelow, bool) +CHECK_RULE(FullScreen, bool) +CHECK_RULE(NoBorder, bool) +CHECK_FORCE_RULE(DecoColor, QString) +CHECK_FORCE_RULE(BlockCompositing, bool) +CHECK_FORCE_RULE(FSP, FocusStealingPreventionLevel) +CHECK_FORCE_RULE(FPP, FocusStealingPreventionLevel) +CHECK_FORCE_RULE(AcceptFocus, bool) +CHECK_FORCE_RULE(Closeable, bool) +CHECK_FORCE_RULE(Autogrouping, bool) +CHECK_FORCE_RULE(AutogroupInForeground, bool) +CHECK_FORCE_RULE(AutogroupById, QString) +CHECK_FORCE_RULE(StrictGeometry, bool) +CHECK_RULE(Shortcut, QString) +CHECK_FORCE_RULE(DisableGlobalShortcuts, bool) +CHECK_RULE(DesktopFile, QString) +CHECK_FORCE_RULE(Layer, Layer) +CHECK_FORCE_RULE(AdaptiveSync, bool) +CHECK_FORCE_RULE(Tearing, bool) + +#undef CHECK_RULE +#undef CHECK_FORCE_RULE + +RuleBook::RuleBook() + : m_updateTimer(new QTimer(this)) + , m_updatesDisabled(false) +{ + connect(m_updateTimer, &QTimer::timeout, this, &RuleBook::save); + m_updateTimer->setInterval(1000); + m_updateTimer->setSingleShot(true); +} + +RuleBook::~RuleBook() +{ + save(); + deleteAll(); +} + +void RuleBook::deleteAll() +{ + qDeleteAll(m_rules); + m_rules.clear(); +} + +WindowRules RuleBook::find(const Window *window) const +{ + QList ret; + for (Rules *rule : m_rules) { + if (rule->match(window)) { + qCDebug(KWIN_CORE) << "Rule found:" << rule << ":" << window; + ret.append(rule); + } + } + return WindowRules(ret); +} + +void RuleBook::edit(Window *c, bool whole_app) +{ + save(); + QStringList args; + args << QStringLiteral("uuid=%1").arg(c->internalId().toString()); + if (whole_app) { + args << QStringLiteral("whole-app"); + } + QProcess *p = new QProcess(this); + p->setArguments({"kcm_kwinrules", "--args", args.join(QLatin1Char(' '))}); + const QString token = waylandServer()->xdgActivationIntegration()->requestPrivilegedToken(nullptr, input()->lastInteractionSerial(), waylandServer()->seat(), "kcm_kwinrules"); + QProcessEnvironment env = kwinApp()->processStartupEnvironment(); + env.insert(QStringLiteral("XDG_ACTIVATION_TOKEN"), token); + p->setProcessEnvironment(env); + p->setProgram(QStandardPaths::findExecutable("kcmshell6")); + p->setProcessChannelMode(QProcess::MergedChannels); + connect(p, static_cast(&QProcess::finished), p, &QProcess::deleteLater); + connect(p, &QProcess::errorOccurred, this, [p](QProcess::ProcessError e) { + if (e == QProcess::FailedToStart) { + qCDebug(KWIN_CORE) << "Failed to start" << p->program(); + } + }); + p->start(); +} + +void RuleBook::setConfig(const KSharedConfig::Ptr &config) +{ + m_book = std::make_unique(config); +} + +void RuleBook::load() +{ + deleteAll(); + if (!m_book) { + m_book = std::make_unique(); + } else { + m_book->sharedConfig()->reparseConfiguration(); + } + m_book->load(); + m_rules = m_book->rules(); +} + +void RuleBook::save() +{ + m_updateTimer->stop(); + if (!m_book) { + qCWarning(KWIN_CORE) << "RuleBook::save invoked without prior invocation of RuleBook::load"; + return; + } + m_book->save(); +} + +void RuleBook::discardUsed(Window *c, bool withdrawn) +{ + for (QList::Iterator it = m_rules.begin(); + it != m_rules.end();) { + if (c->rules()->contains(*it)) { + const auto index = m_book->indexForId((*it)->id()); + if ((*it)->discardUsed(withdrawn)) { + if (index) { + RuleSettings *setting = m_book->ruleSettingsAt(index.value()); + (*it)->write(setting); + } + } + if ((*it)->isEmpty()) { + c->removeRule(*it); + Rules *r = *it; + it = m_rules.erase(it); + delete r; + if (index) { + m_book->removeRuleSettingsAt(index.value()); + } + continue; + } + } + ++it; + } + if (m_book->usrIsSaveNeeded()) { + requestDiskStorage(); + } +} + +void RuleBook::requestDiskStorage() +{ + m_updateTimer->start(); +} + +void RuleBook::setUpdatesDisabled(bool disable) +{ + m_updatesDisabled = disable; + if (!disable) { + const auto windows = Workspace::self()->windows(); + for (Window *window : windows) { + if (window->supportsWindowRules()) { + window->updateWindowRules(Rules::All); + } + } + } +} + +#endif + +} // namespace + +#include "moc_rules.cpp" diff --git a/local/recipes/kde/kwin/source/src/rules.h b/local/recipes/kde/kwin/source/src/rules.h new file mode 100644 index 0000000000..1bacfe778d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/rules.h @@ -0,0 +1,388 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2004 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "options.h" +#include "utils/common.h" + +class QDebug; +class KConfig; + +namespace KWin +{ + +class Window; +class LogicalOutput; +class Rules; +class RuleSettings; +class RuleBookSettings; +class VirtualDesktop; + +enum class DecorationPolicy; + +#ifndef KCMRULES // only for kwin core + +class WindowRules +{ +public: + explicit WindowRules(const QList &rules); + WindowRules(); + void update(Window *, int selection); + bool contains(const Rules *rule) const; + void remove(Rules *rule); + PlacementPolicy checkPlacement(PlacementPolicy placement) const; + RectF checkGeometry(RectF rect, bool init = false) const; + RectF checkGeometrySafe(RectF rect, bool init = false) const; + std::optional checkPositionSafe(bool init = false) const; + QPointF checkPosition(QPointF pos, bool init = false) const; + QSizeF checkSize(QSizeF s, bool init = false) const; + QSizeF checkMinSize(QSizeF s) const; + QSizeF checkMaxSize(QSizeF s) const; + qreal checkOpacityActive(qreal s) const; + qreal checkOpacityInactive(qreal s) const; + bool checkIgnoreGeometry(bool ignore, bool init = false) const; + QList checkDesktops(QList desktops, bool init = false) const; + LogicalOutput *checkOutput(LogicalOutput *output, bool init = false) const; + QStringList checkActivity(QStringList activity, bool init = false) const; + MaximizeMode checkMaximize(MaximizeMode mode, bool init = false) const; + bool checkMinimize(bool minimized, bool init = false) const; + bool checkSkipTaskbar(bool skip, bool init = false) const; + bool checkSkipPager(bool skip, bool init = false) const; + bool checkSkipSwitcher(bool skip, bool init = false) const; + bool checkKeepAbove(bool above, bool init = false) const; + bool checkKeepBelow(bool below, bool init = false) const; + bool checkFullScreen(bool fs, bool init = false) const; + bool checkNoBorder(bool noborder, bool init = false) const; + DecorationPolicy checkDecorationPolicy(DecorationPolicy policy, bool init = false) const; + QString checkDecoColor(QString schemeFile) const; + bool checkBlockCompositing(bool block) const; + FocusStealingPreventionLevel checkFSP(FocusStealingPreventionLevel fsp) const; + FocusStealingPreventionLevel checkFPP(FocusStealingPreventionLevel fpp) const; + bool checkAcceptFocus(bool focus) const; + bool checkCloseable(bool closeable) const; + bool checkAutogrouping(bool autogroup) const; + bool checkAutogroupInForeground(bool fg) const; + QString checkAutogroupById(QString id) const; + bool checkStrictGeometry(bool strict) const; + QString checkShortcut(QString s, bool init = false) const; + bool checkDisableGlobalShortcuts(bool disable) const; + QString checkDesktopFile(QString desktopFile, bool init = false) const; + Layer checkLayer(Layer layer) const; + bool checkAdaptiveSync(bool adaptivesync) const; + bool checkTearing(bool requestsTearing) const; + +private: + MaximizeMode checkMaximizeVert(MaximizeMode mode, bool init) const; + MaximizeMode checkMaximizeHoriz(MaximizeMode mode, bool init) const; + QList rules; +}; + +#endif + +class Rules +{ +public: + Rules(); + explicit Rules(const RuleSettings *); + enum Type { + Position = 1 << 0, + Size = 1 << 1, + Desktops = 1 << 2, + MaximizeVert = 1 << 3, + MaximizeHoriz = 1 << 4, + Minimize = 1 << 5, + SkipTaskbar = 1 << 7, + SkipPager = 1 << 8, + SkipSwitcher = 1 << 9, + Above = 1 << 10, + Below = 1 << 11, + Fullscreen = 1 << 12, + NoBorder = 1 << 13, + OpacityActive = 1 << 14, + OpacityInactive = 1 << 15, + Activity = 1 << 16, + Screen = 1 << 17, + DesktopFile = 1 << 18, + Layer = 1 << 19, + All = 0xffffffff + }; + Q_DECLARE_FLAGS(Types, Type) + // All these values are saved to the cfg file, and are also used in kstart! + enum { + Unused = 0, + DontAffect, // use the default value + Force, // force the given value + Apply, // apply only after initial mapping + Remember, // like apply, and remember the value when the window is withdrawn + ApplyNow, // apply immediately, then forget the setting + ForceTemporarily // apply and force until the window is withdrawn + }; + enum StringMatch { + FirstStringMatch, + UnimportantMatch = FirstStringMatch, + ExactMatch, + SubstringMatch, + RegExpMatch, + LastStringMatch = RegExpMatch + }; + enum SetRule { + UnusedSetRule = Unused, + SetRuleDummy = 256 // so that it's at least short int + }; + enum ForceRule { + UnusedForceRule = Unused, + ForceRuleDummy = 256 // so that it's at least short int + }; + void write(RuleSettings *) const; + bool isEmpty() const; + QString id() const; + +#ifndef KCMRULES + bool discardUsed(bool withdrawn); + bool match(const Window *c) const; + bool update(Window *, int selection); + bool applyPlacement(PlacementPolicy &placement) const; + bool applyGeometry(RectF &rect, bool init) const; + // use 'invalidPoint' with applyPosition, unlike QSize() and Rect(), QPoint() is a valid point + bool applyPosition(QPointF &pos, bool init) const; + bool applySize(QSizeF &s, bool init) const; + bool applyMinSize(QSizeF &s) const; + bool applyMaxSize(QSizeF &s) const; + bool applyOpacityActive(qreal &s) const; + bool applyOpacityInactive(qreal &s) const; + bool applyIgnoreGeometry(bool &ignore, bool init) const; + bool applyDesktops(QList &desktops, bool init) const; + bool applyScreen(int &desktop, bool init) const; + bool applyActivity(QStringList &activity, bool init) const; + bool applyMaximizeVert(MaximizeMode &mode, bool init) const; + bool applyMaximizeHoriz(MaximizeMode &mode, bool init) const; + bool applyMinimize(bool &minimized, bool init) const; + bool applySkipTaskbar(bool &skip, bool init) const; + bool applySkipPager(bool &skip, bool init) const; + bool applySkipSwitcher(bool &skip, bool init) const; + bool applyKeepAbove(bool &above, bool init) const; + bool applyKeepBelow(bool &below, bool init) const; + bool applyFullScreen(bool &fs, bool init) const; + bool applyNoBorder(bool &noborder, bool init) const; + bool applyDecoColor(QString &schemeFile) const; + bool applyBlockCompositing(bool &block) const; + bool applyFSP(FocusStealingPreventionLevel &fsp) const; + bool applyFPP(FocusStealingPreventionLevel &fpp) const; + bool applyAcceptFocus(bool &focus) const; + bool applyCloseable(bool &closeable) const; + bool applyAutogrouping(bool &autogroup) const; + bool applyAutogroupInForeground(bool &fg) const; + bool applyAutogroupById(QString &id) const; + bool applyStrictGeometry(bool &strict) const; + bool applyShortcut(QString &shortcut, bool init) const; + bool applyDisableGlobalShortcuts(bool &disable) const; + bool applyDesktopFile(QString &desktopFile, bool init) const; + bool applyLayer(enum Layer &layer) const; + bool applyAdaptiveSync(bool &adaptivesync) const; + bool applyTearing(bool &tearing) const; + +private: +#endif + bool matchType(WindowType match_type) const; + bool matchWMClass(const QString &match_class, const QString &match_name) const; + bool matchRole(const QString &match_role) const; + bool matchTitle(const QString &match_title) const; + bool matchClientMachine(const QString &match_machine, bool local) const; + bool matchTag(const QString &match_tag) const; +#ifdef KCMRULES +private: +#endif + void readFromSettings(const RuleSettings *settings); + static ForceRule convertForceRule(int v); + static QString getDecoColor(const QString &themeName); +#ifndef KCMRULES + static bool checkSetRule(SetRule rule, bool init); + static bool checkForceRule(ForceRule rule); + static bool checkSetStop(SetRule rule); + static bool checkForceStop(ForceRule rule); +#endif + enum Layer layer; + ForceRule layerrule; + QString m_id; + bool m_enabled = true; + QString description; + QString wmclass; + StringMatch wmclassmatch; + bool wmclasscomplete; + QString windowrole; + StringMatch windowrolematch; + QString title; + StringMatch titlematch; + QString clientmachine; + StringMatch clientmachinematch; + QString tag; + StringMatch tagmatch; + WindowTypes types; // types for matching + PlacementPolicy placement; + ForceRule placementrule; + QPoint position; + SetRule positionrule; + QSize size; + SetRule sizerule; + QSize minsize; + ForceRule minsizerule; + QSize maxsize; + ForceRule maxsizerule; + int opacityactive; + ForceRule opacityactiverule; + int opacityinactive; + ForceRule opacityinactiverule; + bool ignoregeometry; + SetRule ignoregeometryrule; + QStringList desktops; + SetRule desktopsrule; + int screen; + SetRule screenrule; + QStringList activity; + SetRule activityrule; + bool maximizevert; + SetRule maximizevertrule; + bool maximizehoriz; + SetRule maximizehorizrule; + bool minimize; + SetRule minimizerule; + bool skiptaskbar; + SetRule skiptaskbarrule; + bool skippager; + SetRule skippagerrule; + bool skipswitcher; + SetRule skipswitcherrule; + bool above; + SetRule aboverule; + bool below; + SetRule belowrule; + bool fullscreen; + SetRule fullscreenrule; + bool noborder; + SetRule noborderrule; + QString decocolor; + ForceRule decocolorrule; + bool blockcompositing; + ForceRule blockcompositingrule; + FocusStealingPreventionLevel fsplevel; + FocusStealingPreventionLevel fpplevel; + ForceRule fsplevelrule; + ForceRule fpplevelrule; + bool acceptfocus; + ForceRule acceptfocusrule; + bool closeable; + ForceRule closeablerule; + bool autogroup; + ForceRule autogrouprule; + bool autogroupfg; + ForceRule autogroupfgrule; + QString autogroupid; + ForceRule autogroupidrule; + bool strictgeometry; + ForceRule strictgeometryrule; + QString shortcut; + SetRule shortcutrule; + bool disableglobalshortcuts; + ForceRule disableglobalshortcutsrule; + QString desktopfile; + SetRule desktopfilerule; + bool adaptivesync; + ForceRule adaptivesyncrule; + bool tearing; + ForceRule tearingrule; + friend QDebug &operator<<(QDebug &stream, const Rules *); +}; + +#ifndef KCMRULES +class KWIN_EXPORT RuleBook : public QObject +{ + Q_OBJECT +public: + explicit RuleBook(); + ~RuleBook() override; + WindowRules find(const Window *window) const; + void discardUsed(Window *c, bool withdraw); + void setUpdatesDisabled(bool disable); + bool areUpdatesDisabled() const; + void load(); + void edit(Window *c, bool whole_app); + void requestDiskStorage(); + void setConfig(const KSharedConfig::Ptr &config); + +private Q_SLOTS: + void save(); + +private: + void deleteAll(); + QTimer *m_updateTimer; + bool m_updatesDisabled; + QList m_rules; + std::unique_ptr m_book; +}; + +inline bool RuleBook::areUpdatesDisabled() const +{ + return m_updatesDisabled; +} + +inline bool Rules::checkSetRule(SetRule rule, bool init) +{ + if (rule > (SetRule)DontAffect) { // Unused or DontAffect + if (rule == (SetRule)Force || rule == (SetRule)ApplyNow + || rule == (SetRule)ForceTemporarily || init) { + return true; + } + } + return false; +} + +inline bool Rules::checkForceRule(ForceRule rule) +{ + return rule == (ForceRule)Force || rule == (ForceRule)ForceTemporarily; +} + +inline bool Rules::checkSetStop(SetRule rule) +{ + return rule != UnusedSetRule; +} + +inline bool Rules::checkForceStop(ForceRule rule) +{ + return rule != UnusedForceRule; +} + +inline WindowRules::WindowRules(const QList &r) + : rules(r) +{ +} + +inline WindowRules::WindowRules() +{ +} + +inline bool WindowRules::contains(const Rules *rule) const +{ + return rules.contains(rule); +} + +inline void WindowRules::remove(Rules *rule) +{ + rules.removeOne(rule); +} + +#endif + +QDebug &operator<<(QDebug &stream, const Rules *); + +} // namespace + +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::Rules::Types) diff --git a/local/recipes/kde/kwin/source/src/rulesettings.kcfg b/local/recipes/kde/kwin/source/src/rulesettings.kcfg new file mode 100644 index 0000000000..af49c8cc18 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/rulesettings.kcfg @@ -0,0 +1,466 @@ + + + + + + + + + + + + + + + false + + + + true + + + + + + + Rules::UnimportantMatch + Rules::FirstStringMatch + Rules::LastStringMatch + + + + Rules::UnimportantMatch + + + + + + + + Rules::UnimportantMatch + Rules::FirstStringMatch + Rules::LastStringMatch + + + + + + + + Rules::UnimportantMatch + Rules::FirstStringMatch + Rules::LastStringMatch + + + + + + + + Rules::UnimportantMatch + Rules::FirstStringMatch + Rules::LastStringMatch + + + + + + + + Rules::UnimportantMatch + Rules::FirstStringMatch + Rules::LastStringMatch + + + + + NET::AllTypesMask + + + + + + PlacementCentered + + + + Rules::UnusedForceRule + + + + + invalidPoint + + + + Rules::UnusedSetRule + static_cast<Rules::SetRule>(Rules::ForceTemporarily) + Rules::UnusedSetRule + + + + + + + + Rules::UnusedSetRule + static_cast<Rules::SetRule>(Rules::ForceTemporarily) + Rules::UnusedSetRule + + + + + + + + + Rules::UnusedForceRule + + + + + + + + + Rules::UnusedForceRule + + + + + 0 + 100 + 100 + + + + Rules::UnusedForceRule + + + + + 0 + 100 + 100 + + + + Rules::UnusedForceRule + + + + + false + + + + Rules::UnusedSetRule + static_cast<Rules::SetRule>(Rules::ForceTemporarily) + Rules::UnusedSetRule + + + + + {} + + + + Rules::UnusedSetRule + static_cast<Rules::SetRule>(Rules::ForceTemporarily) + Rules::UnusedSetRule + + + + + 0 + + + + Rules::UnusedSetRule + static_cast<Rules::SetRule>(Rules::ForceTemporarily) + Rules::UnusedSetRule + + + + + + + + Rules::UnusedSetRule + static_cast<Rules::SetRule>(Rules::ForceTemporarily) + Rules::UnusedSetRule + + + + + false + + + + Rules::UnusedSetRule + static_cast<Rules::SetRule>(Rules::ForceTemporarily) + Rules::UnusedSetRule + + + + + false + + + + Rules::UnusedSetRule + static_cast<Rules::SetRule>(Rules::ForceTemporarily) + Rules::UnusedSetRule + + + + + false + + + + Rules::UnusedSetRule + static_cast<Rules::SetRule>(Rules::ForceTemporarily) + Rules::UnusedSetRule + + + + + false + + + + Rules::UnusedSetRule + static_cast<Rules::SetRule>(Rules::ForceTemporarily) + Rules::UnusedSetRule + + + + + false + + + + Rules::UnusedSetRule + static_cast<Rules::SetRule>(Rules::ForceTemporarily) + Rules::UnusedSetRule + + + + + false + + + + Rules::UnusedSetRule + static_cast<Rules::SetRule>(Rules::ForceTemporarily) + Rules::UnusedSetRule + + + + + false + + + + Rules::UnusedSetRule + static_cast<Rules::SetRule>(Rules::ForceTemporarily) + Rules::UnusedSetRule + + + + + false + + + + Rules::UnusedSetRule + static_cast<Rules::SetRule>(Rules::ForceTemporarily) + Rules::UnusedSetRule + + + + + false + + + + Rules::UnusedSetRule + static_cast<Rules::SetRule>(Rules::ForceTemporarily) + Rules::UnusedSetRule + + + + + false + + + + Rules::UnusedSetRule + static_cast<Rules::SetRule>(Rules::ForceTemporarily) + Rules::UnusedSetRule + + + + + + + + Rules::UnusedForceRule + + + + + false + + + + Rules::UnusedForceRule + + + + + 0 + 0 + 4 + + + + Rules::UnusedForceRule + + + + + 0 + 0 + 4 + + + + Rules::UnusedForceRule + + + + + false + + + + Rules::UnusedForceRule + + + + + false + + + + Rules::UnusedForceRule + + + + + false + + + + Rules::UnusedForceRule + + + + + true + + + + Rules::UnusedForceRule + + + + + + + + Rules::UnusedForceRule + + + + + false + + + + Rules::UnusedForceRule + + + + + + + + Rules::UnusedSetRule + static_cast<Rules::SetRule>(Rules::ForceTemporarily) + Rules::UnusedSetRule + + + + + false + + + + Rules::UnusedForceRule + + + + + + + + Rules::UnusedSetRule + static_cast<Rules::SetRule>(Rules::ForceTemporarily) + Rules::UnusedSetRule + + + + + + + + + + + + + + + + + NormalLayer + + + + Rules::UnusedForceRule + + + + + true + + + + Rules::UnusedForceRule + + + + + true + + + + Rules::UnusedForceRule + + + diff --git a/local/recipes/kde/kwin/source/src/rulesettings.kcfgc b/local/recipes/kde/kwin/source/src/rulesettings.kcfgc new file mode 100644 index 0000000000..2a5e26aa19 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/rulesettings.kcfgc @@ -0,0 +1,7 @@ +File=rulesettings.kcfg +IncludeFiles=\"rules.h\",netwm_def.h +NameSpace=KWin +ClassName=RuleSettings +UseEnumTypes=true +Mutators=true +ParentInConstructor=true diff --git a/local/recipes/kde/kwin/source/src/scene/cursoritem.cpp b/local/recipes/kde/kwin/source/src/scene/cursoritem.cpp new file mode 100644 index 0000000000..e3908c4766 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/cursoritem.cpp @@ -0,0 +1,79 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scene/cursoritem.h" +#include "cursor.h" +#include "cursorsource.h" +#include "effect/effect.h" +#include "scene/imageitem.h" +#include "scene/itemrenderer.h" +#include "scene/scene.h" +#include "scene/surfaceitem_wayland.h" + +namespace KWin +{ + +CursorItem::CursorItem(Item *parent) + : Item(parent) +{ + refresh(); + connect(Cursors::self(), &Cursors::currentCursorChanged, this, &CursorItem::refresh); +} + +CursorItem::~CursorItem() +{ +} + +void CursorItem::refresh() +{ + const CursorSource *source = Cursors::self()->currentCursor()->source(); + if (auto surfaceSource = qobject_cast(source)) { + setSurface(surfaceSource->surface(), surfaceSource->hotspot()); + } else if (auto shapeSource = qobject_cast(source)) { + setImage(shapeSource->image(), shapeSource->hotspot()); + } +} + +void CursorItem::setSurface(SurfaceInterface *surface, const QPointF &hotspot) +{ + m_imageItem.reset(); + + if (!surface) { + m_surfaceItem.reset(); + } else if (!m_surfaceItem || m_surfaceItem->surface() != surface) { + m_surfaceItem = std::make_unique(surface, this); + } + if (m_surfaceItem) { + m_surfaceItem->setPosition(-hotspot); + } +} + +void CursorItem::setImage(const QImage &image, const QPointF &hotspot) +{ + m_surfaceItem.reset(); + + if (!m_imageItem) { + m_imageItem = scene()->renderer()->createImageItem(this); + } + m_imageItem->setImage(image); + m_imageItem->setPosition(-hotspot); + m_imageItem->setSize(image.size() / image.devicePixelRatio()); +} + +QPointF CursorItem::hotspot() const +{ + if (m_surfaceItem) { + return -m_surfaceItem->position(); + } else if (m_imageItem) { + return -m_imageItem->position(); + } else { + return QPointF{}; + } +} + +} // namespace KWin + +#include "moc_cursoritem.cpp" diff --git a/local/recipes/kde/kwin/source/src/scene/cursoritem.h b/local/recipes/kde/kwin/source/src/scene/cursoritem.h new file mode 100644 index 0000000000..f47c7ea1b1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/cursoritem.h @@ -0,0 +1,37 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "scene/item.h" + +namespace KWin +{ + +class ImageItem; +class SurfaceInterface; +class SurfaceItemWayland; + +class KWIN_EXPORT CursorItem : public Item +{ + Q_OBJECT + +public: + explicit CursorItem(Item *parent = nullptr); + ~CursorItem() override; + + QPointF hotspot() const; + +private: + void refresh(); + void setSurface(SurfaceInterface *surface, const QPointF &hotspot); + void setImage(const QImage &image, const QPointF &hotspot); + + std::unique_ptr m_imageItem; + std::unique_ptr m_surfaceItem; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/decorationitem.cpp b/local/recipes/kde/kwin/source/src/scene/decorationitem.cpp new file mode 100644 index 0000000000..bca0651cc2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/decorationitem.cpp @@ -0,0 +1,567 @@ +/* + SPDX-FileCopyrightText: 2014 Martin Gräßlin + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scene/decorationitem.h" +#include "compositor.h" +#include "core/output.h" +#include "core/renderbackend.h" +#include "decorations/decoratedwindow.h" +#include "opengl/eglcontext.h" +#include "opengl/gltexture.h" +#include "scene/outlinedborderitem.h" +#include "scene/workspacescene.h" +#include "window.h" + +#include + +#include +#include + +#include + +namespace KWin +{ + +DecorationRenderer::DecorationRenderer(Decoration::DecoratedWindowImpl *client) + : m_client(client) + , m_imageSizesDirty(true) +{ + connect(client->decoration(), &KDecoration3::Decoration::damaged, this, [this](const QRegion ®ion) { + addDamage(Region(region)); + }); + + connect(client->decoration(), &KDecoration3::Decoration::bordersChanged, + this, &DecorationRenderer::invalidate); + connect(client->decoratedWindow(), &KDecoration3::DecoratedWindow::sizeChanged, + this, &DecorationRenderer::invalidate); + + invalidate(); +} + +Decoration::DecoratedWindowImpl *DecorationRenderer::client() const +{ + return m_client; +} + +void DecorationRenderer::invalidate() +{ + if (m_client) { + addDamage(m_client->window()->rect().roundedOut()); + } + m_imageSizesDirty = true; +} + +Region DecorationRenderer::damage() const +{ + return m_damage; +} + +void DecorationRenderer::addDamage(const Region ®ion) +{ + m_damage += region; + Q_EMIT damaged(region); +} + +void DecorationRenderer::resetDamage() +{ + m_damage = Region(); +} + +qreal DecorationRenderer::effectiveDevicePixelRatio() const +{ + // QPainter won't let paint with a device pixel ratio less than 1. + return std::max(qreal(1.0), devicePixelRatio()); +} + +qreal DecorationRenderer::devicePixelRatio() const +{ + return m_devicePixelRatio; +} + +void DecorationRenderer::setDevicePixelRatio(qreal dpr) +{ + if (m_devicePixelRatio != dpr) { + m_devicePixelRatio = dpr; + invalidate(); + } +} + +void DecorationRenderer::renderToPainter(QPainter *painter, const RectF &rect) +{ + client()->decoration()->paint(painter, rect); +} + +SceneOpenGLDecorationRenderer::SceneOpenGLDecorationRenderer(Decoration::DecoratedWindowImpl *client) + : DecorationRenderer(client) + , m_texture() +{ +} + +SceneOpenGLDecorationRenderer::~SceneOpenGLDecorationRenderer() +{ + if (WorkspaceScene *scene = Compositor::self()->scene()) { + scene->openglContext()->makeCurrent(); + } +} + +static void clamp_row(int left, int width, int right, const uint32_t *src, uint32_t *dest) +{ + std::fill_n(dest, left, *src); + std::copy(src, src + width, dest + left); + std::fill_n(dest + left + width, right, *(src + width - 1)); +} + +static void clamp_sides(int left, int width, int right, const uint32_t *src, uint32_t *dest) +{ + std::fill_n(dest, left, *src); + std::fill_n(dest + left + width, right, *(src + width - 1)); +} + +static void clamp(QImage &image, const Rect &viewport) +{ + Q_ASSERT(image.depth() == 32); + if (viewport.isEmpty()) { + image = {}; + return; + } + + const Rect rect = image.rect(); + + const int left = viewport.left() - rect.left(); + const int top = viewport.top() - rect.top(); + const int right = rect.right() - viewport.right(); + const int bottom = rect.bottom() - viewport.bottom(); + + const int width = rect.width() - left - right; + const int height = rect.height() - top - bottom; + + const uint32_t *firstRow = reinterpret_cast(image.scanLine(top)); + const uint32_t *lastRow = reinterpret_cast(image.scanLine(top + height - 1)); + + for (int i = 0; i < top; ++i) { + uint32_t *dest = reinterpret_cast(image.scanLine(i)); + clamp_row(left, width, right, firstRow + left, dest); + } + + for (int i = 0; i < height; ++i) { + uint32_t *dest = reinterpret_cast(image.scanLine(top + i)); + clamp_sides(left, width, right, dest + left, dest); + } + + for (int i = 0; i < bottom; ++i) { + uint32_t *dest = reinterpret_cast(image.scanLine(top + height + i)); + clamp_row(left, width, right, lastRow + left, dest); + } +} + +void SceneOpenGLDecorationRenderer::render(const Region ®ion) +{ + if (areImageSizesDirty()) { + resizeTexture(); + resetImageSizesDirty(); + } + + if (!m_texture) { + // for invalid sizes we get no texture, see BUG 361551 + return; + } + + RectF left, top, right, bottom; + client()->window()->layoutDecorationRects(left, top, right, bottom); + + const qreal devicePixelRatio = effectiveDevicePixelRatio(); + const int topHeight = std::round(top.height() * devicePixelRatio); + const int bottomHeight = std::round(bottom.height() * devicePixelRatio); + const int leftWidth = std::round(left.width() * devicePixelRatio); + + const QPoint topPosition(0, 0); + const QPoint bottomPosition(0, topPosition.y() + topHeight + (2 * TexturePad)); + const QPoint leftPosition(0, bottomPosition.y() + bottomHeight + (2 * TexturePad)); + const QPoint rightPosition(0, leftPosition.y() + leftWidth + (2 * TexturePad)); + + const Rect dirtyRect = region.boundingRect(); + + renderPart(top.intersected(dirtyRect), top, topPosition, devicePixelRatio); + renderPart(bottom.intersected(dirtyRect), bottom, bottomPosition, devicePixelRatio); + renderPart(left.intersected(dirtyRect), left, leftPosition, devicePixelRatio, true); + renderPart(right.intersected(dirtyRect), right, rightPosition, devicePixelRatio, true); +} + +void SceneOpenGLDecorationRenderer::renderPart(const RectF &rect, const RectF &partRect, + const QPoint &textureOffset, + qreal devicePixelRatio, bool rotated) +{ + if (!rect.isValid() || !m_texture) { + return; + } + // We allow partial decoration updates and it might just so happen that the + // dirty region is completely contained inside the decoration part, i.e. + // the dirty region doesn't touch any of the decoration's edges. In that + // case, we should **not** pad the dirty region. + const QMargins padding = texturePadForPart(rect, partRect); + int verticalPadding = padding.top() + padding.bottom(); + int horizontalPadding = padding.left() + padding.right(); + + QSize imageSize(toNativeSize(rect.width()), toNativeSize(rect.height())); + if (rotated) { + imageSize = QSize(imageSize.height(), imageSize.width()); + } + QSize paddedImageSize = imageSize; + paddedImageSize.rheight() += verticalPadding; + paddedImageSize.rwidth() += horizontalPadding; + QImage image(paddedImageSize, QImage::Format_ARGB32_Premultiplied); + image.setDevicePixelRatio(devicePixelRatio); + image.fill(Qt::transparent); + + QRect padClip = QRect(padding.left(), padding.top(), imageSize.width(), imageSize.height()); + QPainter painter(&image); + const qreal inverseScale = 1.0 / devicePixelRatio; + painter.scale(inverseScale, inverseScale); + painter.setRenderHint(QPainter::Antialiasing); + painter.setClipRect(padClip); + painter.translate(padding.left(), padding.top()); + if (rotated) { + painter.translate(0, imageSize.height()); + painter.rotate(-90); + } + painter.scale(devicePixelRatio, devicePixelRatio); + painter.translate(-rect.topLeft()); + renderToPainter(&painter, rect); + painter.end(); + + // fill padding pixels by copying from the neighbour row + clamp(image, padClip); + + QPoint dirtyOffset = ((rect.topLeft() - partRect.topLeft()) * devicePixelRatio).toPoint(); + if (padding.top() == 0) { + dirtyOffset.ry() += TexturePad; + } + if (padding.left() == 0) { + dirtyOffset.rx() += TexturePad; + } + m_texture->update(image, Rect(image.rect()), textureOffset + dirtyOffset); +} + +const QMargins SceneOpenGLDecorationRenderer::texturePadForPart( + const RectF &rect, const RectF &partRect) +{ + QMargins result = QMargins(0, 0, 0, 0); + if (rect.top() == partRect.top()) { + result.setTop(TexturePad); + } + if (rect.bottom() == partRect.bottom()) { + result.setBottom(TexturePad); + } + if (rect.left() == partRect.left()) { + result.setLeft(TexturePad); + } + if (rect.right() == partRect.right()) { + result.setRight(TexturePad); + } + return result; +} + +static int align(int value, int align) +{ + return (value + align - 1) & ~(align - 1); +} + +void SceneOpenGLDecorationRenderer::resizeTexture() +{ + RectF left, top, right, bottom; + client()->window()->layoutDecorationRects(left, top, right, bottom); + QSize size; + + size.rwidth() = toNativeSize(std::max({top.width(), bottom.width(), left.height(), right.height()})); + size.rheight() = toNativeSize(top.height()) + toNativeSize(bottom.height()) + toNativeSize(left.width()) + toNativeSize(right.width()); + + size.rheight() += 4 * (2 * TexturePad); + size.rwidth() += 2 * TexturePad; + size.rwidth() = align(size.width(), 128); + + if (m_texture && m_texture->size() == size) { + return; + } + + if (!size.isEmpty()) { + m_texture = GLTexture::allocate(GL_RGBA8, size); + if (!m_texture) { + return; + } + m_texture->setContentTransform(OutputTransform::FlipY); + m_texture->setFilter(GL_LINEAR); + m_texture->setWrapMode(GL_CLAMP_TO_EDGE); + } else { + m_texture.reset(); + } +} + +int SceneOpenGLDecorationRenderer::toNativeSize(double size) const +{ + return std::round(size * effectiveDevicePixelRatio()); +} + +SceneQPainterDecorationRenderer::SceneQPainterDecorationRenderer(Decoration::DecoratedWindowImpl *client) + : DecorationRenderer(client) +{ +} + +QImage SceneQPainterDecorationRenderer::image(SceneQPainterDecorationRenderer::DecorationPart part) const +{ + Q_ASSERT(part != DecorationPart::Count); + return m_images[int(part)]; +} + +void SceneQPainterDecorationRenderer::render(const Region ®ion) +{ + if (areImageSizesDirty()) { + resizeImages(); + resetImageSizesDirty(); + } + + auto imageSize = [this](DecorationPart part) { + return m_images[int(part)].size() / m_images[int(part)].devicePixelRatio(); + }; + + const Rect top(QPoint(0, 0), imageSize(DecorationPart::Top)); + const Rect left(QPoint(0, top.height()), imageSize(DecorationPart::Left)); + const Rect right(QPoint(top.width() - imageSize(DecorationPart::Right).width(), top.height()), imageSize(DecorationPart::Right)); + const Rect bottom(QPoint(0, left.y() + left.height()), imageSize(DecorationPart::Bottom)); + + const Rect geometry = region.boundingRect(); + auto renderPart = [this](const QRect &rect, const QRect &partRect, int index) { + if (rect.isEmpty()) { + return; + } + QPainter painter(&m_images[index]); + painter.setRenderHint(QPainter::Antialiasing); + painter.setWindow(QRect(partRect.topLeft(), partRect.size() * effectiveDevicePixelRatio())); + painter.setClipRect(rect); + painter.save(); + // clear existing part + painter.setCompositionMode(QPainter::CompositionMode_Source); + painter.fillRect(rect, Qt::transparent); + painter.restore(); + client()->decoration()->paint(&painter, rect); + }; + + renderPart(left.intersected(geometry), left, int(DecorationPart::Left)); + renderPart(top.intersected(geometry), top, int(DecorationPart::Top)); + renderPart(right.intersected(geometry), right, int(DecorationPart::Right)); + renderPart(bottom.intersected(geometry), bottom, int(DecorationPart::Bottom)); +} + +void SceneQPainterDecorationRenderer::resizeImages() +{ + RectF left, top, right, bottom; + client()->window()->layoutDecorationRects(left, top, right, bottom); + + auto checkAndCreate = [this](int index, const QSizeF &size) { + auto dpr = effectiveDevicePixelRatio(); + if (m_images[index].size() != size * dpr || m_images[index].devicePixelRatio() != dpr) { + m_images[index] = QImage(size.toSize() * dpr, QImage::Format_ARGB32_Premultiplied); + m_images[index].setDevicePixelRatio(dpr); + m_images[index].fill(Qt::transparent); + } + }; + checkAndCreate(int(DecorationPart::Left), left.size()); + checkAndCreate(int(DecorationPart::Right), right.size()); + checkAndCreate(int(DecorationPart::Top), top.size()); + checkAndCreate(int(DecorationPart::Bottom), bottom.size()); +} + +DecorationItem::DecorationItem(KDecoration3::Decoration *decoration, Window *window, Item *parent) + : Item(parent) + , m_window(window) + , m_decoration(decoration) +{ + switch (Compositor::self()->backend()->compositingType()) { + case OpenGLCompositing: + m_renderer = std::make_unique(window->decoratedWindow()); + break; + case QPainterCompositing: + m_renderer = std::make_unique(window->decoratedWindow()); + break; + default: + Q_UNREACHABLE(); + } + + connect(window, &Window::targetScaleChanged, this, &DecorationItem::updateScale); + + connect(decoration->window(), &KDecoration3::DecoratedWindow::sizeChanged, + this, &DecorationItem::handleDecorationGeometryChanged); + connect(decoration, &KDecoration3::Decoration::bordersChanged, + this, &DecorationItem::handleDecorationGeometryChanged); + connect(decoration, &KDecoration3::Decoration::borderOutlineChanged, + this, &DecorationItem::updateOutline); + + connect(renderer(), &DecorationRenderer::damaged, + this, qOverload(&Item::scheduleRepaint)); + + setSize(decoration->size()); + updateScale(); + updateOutline(); +} + +DecorationItem::~DecorationItem() +{ +} + +QList DecorationItem::shape() const +{ + RectF left, top, right, bottom; + m_window->layoutDecorationRects(left, top, right, bottom); + return {left, top, right, bottom}; +} + +Region DecorationItem::opaque() const +{ + if (m_window->decorationHasAlpha()) { + return Region(); + } + RectF left, top, right, bottom; + m_window->layoutDecorationRects(left, top, right, bottom); + + // We have to map to integers which has rounding issues + // it's safer for a region to be considered transparent than opaque + // so always align inwards + const QMargins roundingPad = QMargins(1, 1, 1, 1); + Region roundedLeft = left.roundedOut().marginsRemoved(roundingPad); + Region roundedTop = top.roundedOut().marginsRemoved(roundingPad); + Region roundedRight = right.roundedOut().marginsRemoved(roundingPad); + Region roundedBottom = bottom.roundedOut().marginsRemoved(roundingPad); + + return roundedLeft | roundedTop | roundedRight | roundedBottom; +} + +void DecorationItem::preprocess() +{ + const Region damage = m_renderer->damage(); + if (!damage.isEmpty()) { + m_renderer->render(damage); + m_renderer->resetDamage(); + } +} + +void DecorationItem::updateScale() +{ + const double scale = m_window->targetScale(); + if (m_renderer->devicePixelRatio() != scale) { + m_renderer->setDevicePixelRatio(scale); + discardQuads(); + } +} + +void DecorationItem::updateOutline() +{ + if (m_decoration->borderOutline().isNull()) { + m_outlineItem.reset(); + } else { + const auto outline = BorderOutline::from(m_decoration->borderOutline()); + if (!m_outlineItem) { + m_outlineItem = std::make_unique(rect(), outline, this); + } else { + m_outlineItem->setOutline(outline); + } + } +} + +void DecorationItem::handleDecorationGeometryChanged() +{ + setSize(m_decoration->size()); + discardQuads(); + + if (m_outlineItem) { + m_outlineItem->setInnerRect(rect()); + } +} + +DecorationRenderer *DecorationItem::renderer() const +{ + return m_renderer.get(); +} + +Window *DecorationItem::window() const +{ + return m_window; +} + +WindowQuad buildQuad(const RectF &partRect, const QPoint &textureOffset, + const qreal devicePixelRatio, bool rotated) +{ + const int p = DecorationRenderer::TexturePad; + + const double x0 = partRect.left(); + const double y0 = partRect.top(); + const double x1 = partRect.right(); + const double y1 = partRect.bottom(); + + WindowQuad quad; + if (rotated) { + const int u0 = textureOffset.y() + p; + const int v0 = textureOffset.x() + p; + const int u1 = textureOffset.y() + p + std::round(partRect.width() * devicePixelRatio); + const int v1 = textureOffset.x() + p + std::round(partRect.height() * devicePixelRatio); + + quad[0] = WindowVertex(x0, y0, v0, u1); // Top-left + quad[1] = WindowVertex(x1, y0, v0, u0); // Top-right + quad[2] = WindowVertex(x1, y1, v1, u0); // Bottom-right + quad[3] = WindowVertex(x0, y1, v1, u1); // Bottom-left + } else { + const int u0 = textureOffset.x() + p; + const int v0 = textureOffset.y() + p; + const int u1 = textureOffset.x() + p + std::round(partRect.width() * devicePixelRatio); + const int v1 = textureOffset.y() + p + std::round(partRect.height() * devicePixelRatio); + + quad[0] = WindowVertex(x0, y0, u0, v0); // Top-left + quad[1] = WindowVertex(x1, y0, u1, v0); // Top-right + quad[2] = WindowVertex(x1, y1, u1, v1); // Bottom-right + quad[3] = WindowVertex(x0, y1, u0, v1); // Bottom-left + } + return quad; +} + +WindowQuadList DecorationItem::buildQuads() const +{ + if (m_window->frameMargins().isNull()) { + return WindowQuadList(); + } + + RectF left, top, right, bottom; + const qreal devicePixelRatio = m_renderer->effectiveDevicePixelRatio(); + const int texturePad = DecorationRenderer::TexturePad; + + m_window->layoutDecorationRects(left, top, right, bottom); + + const int topHeight = std::round(top.height() * devicePixelRatio); + const int bottomHeight = std::round(bottom.height() * devicePixelRatio); + const int leftWidth = std::round(left.width() * devicePixelRatio); + + const QPoint topPosition(0, 0); + const QPoint bottomPosition(0, topPosition.y() + topHeight + (2 * texturePad)); + const QPoint leftPosition(0, bottomPosition.y() + bottomHeight + (2 * texturePad)); + const QPoint rightPosition(0, leftPosition.y() + leftWidth + (2 * texturePad)); + + WindowQuadList list; + if (left.isValid()) { + list.append(buildQuad(left, leftPosition, devicePixelRatio, true)); + } + if (top.isValid()) { + list.append(buildQuad(top, topPosition, devicePixelRatio, false)); + } + if (right.isValid()) { + list.append(buildQuad(right, rightPosition, devicePixelRatio, true)); + } + if (bottom.isValid()) { + list.append(buildQuad(bottom, bottomPosition, devicePixelRatio, false)); + } + return list; +} + +} // namespace KWin + +#include "moc_decorationitem.cpp" diff --git a/local/recipes/kde/kwin/source/src/scene/decorationitem.h b/local/recipes/kde/kwin/source/src/scene/decorationitem.h new file mode 100644 index 0000000000..0f4da93448 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/decorationitem.h @@ -0,0 +1,163 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "scene/item.h" + +namespace KDecoration3 +{ +class Decoration; +} + +namespace KWin +{ + +class OutlinedBorderItem; +class GLTexture; +class Window; +class LogicalOutput; + +namespace Decoration +{ +class DecoratedWindowImpl; +} + +class KWIN_EXPORT DecorationRenderer : public QObject +{ + Q_OBJECT + +public: + virtual void render(const Region ®ion) = 0; + void invalidate(); + + // TODO: Move damage tracking inside DecorationItem. + Region damage() const; + void addDamage(const Region ®ion); + void resetDamage(); + + qreal effectiveDevicePixelRatio() const; + qreal devicePixelRatio() const; + void setDevicePixelRatio(qreal dpr); + + // Reserve some space for padding. We pad decoration parts to avoid texture bleeding. + static const int TexturePad = 1; + +Q_SIGNALS: + void damaged(const Region ®ion); + +protected: + explicit DecorationRenderer(Decoration::DecoratedWindowImpl *client); + + Decoration::DecoratedWindowImpl *client() const; + + bool areImageSizesDirty() const + { + return m_imageSizesDirty; + } + void resetImageSizesDirty() + { + m_imageSizesDirty = false; + } + void renderToPainter(QPainter *painter, const RectF &rect); + +private: + QPointer m_client; + Region m_damage; + qreal m_devicePixelRatio = 1; + bool m_imageSizesDirty; +}; + +class SceneOpenGLDecorationRenderer : public DecorationRenderer +{ + Q_OBJECT +public: + enum class DecorationPart : int { + Left, + Top, + Right, + Bottom, + Count + }; + explicit SceneOpenGLDecorationRenderer(Decoration::DecoratedWindowImpl *client); + ~SceneOpenGLDecorationRenderer() override; + + void render(const Region ®ion) override; + + GLTexture *texture() + { + return m_texture.get(); + } + GLTexture *texture() const + { + return m_texture.get(); + } + +private: + void renderPart(const RectF &rect, const RectF &partRect, const QPoint &textureOffset, qreal devicePixelRatio, bool rotated = false); + static const QMargins texturePadForPart(const RectF &rect, const RectF &partRect); + void resizeTexture(); + int toNativeSize(double size) const; + std::unique_ptr m_texture; +}; + +class SceneQPainterDecorationRenderer : public DecorationRenderer +{ + Q_OBJECT +public: + enum class DecorationPart : int { + Left, + Top, + Right, + Bottom, + Count + }; + explicit SceneQPainterDecorationRenderer(Decoration::DecoratedWindowImpl *client); + + void render(const Region ®ion) override; + + QImage image(DecorationPart part) const; + +private: + void resizeImages(); + QImage m_images[int(DecorationPart::Count)]; +}; + +/** + * The DecorationItem class represents a server-side decoration. + */ +class KWIN_EXPORT DecorationItem : public Item +{ + Q_OBJECT + +public: + explicit DecorationItem(KDecoration3::Decoration *decoration, Window *window, Item *parent = nullptr); + ~DecorationItem() override; + + DecorationRenderer *renderer() const; + Window *window() const; + + QList shape() const override final; + Region opaque() const override final; + +private Q_SLOTS: + void handleDecorationGeometryChanged(); + void updateScale(); + void updateOutline(); + +protected: + void preprocess() override; + WindowQuadList buildQuads() const override; + +private: + Window *m_window; + QPointer m_output; + QPointer m_decoration; + std::unique_ptr m_renderer; + std::unique_ptr m_outlineItem; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/dndiconitem.cpp b/local/recipes/kde/kwin/source/src/scene/dndiconitem.cpp new file mode 100644 index 0000000000..bd40e59dc9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/dndiconitem.cpp @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scene/dndiconitem.h" +#include "scene/surfaceitem_wayland.h" +#include "wayland/datadevice.h" +#include "wayland/surface.h" + +namespace KWin +{ + +DragAndDropIconItem::DragAndDropIconItem(DragAndDropIcon *icon, Item *parent) + : Item(parent) +{ + m_surfaceItem = std::make_unique(icon->surface(), this); + m_surfaceItem->setPosition(icon->position()); + + connect(icon, &DragAndDropIcon::destroyed, this, [this]() { + m_surfaceItem.reset(); + }); + connect(icon, &DragAndDropIcon::changed, this, [this, icon]() { + m_surfaceItem->setPosition(icon->position()); + }); +} + +DragAndDropIconItem::~DragAndDropIconItem() +{ +} + +SurfaceInterface *DragAndDropIconItem::surface() const +{ + return m_surfaceItem ? m_surfaceItem->surface() : nullptr; +} + +void DragAndDropIconItem::setOutput(LogicalOutput *output) +{ + if (m_surfaceItem && output) { + m_output = output; + m_surfaceItem->surface()->setPreferredBufferScale(output->scale()); + m_surfaceItem->surface()->setPreferredColorDescription(output->blendingColor()); + } +} + +} // namespace KWin + +#include "moc_dndiconitem.cpp" diff --git a/local/recipes/kde/kwin/source/src/scene/dndiconitem.h b/local/recipes/kde/kwin/source/src/scene/dndiconitem.h new file mode 100644 index 0000000000..741779aaac --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/dndiconitem.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/output.h" +#include "effect/globals.h" +#include "scene/item.h" + +namespace KWin +{ + +class DragAndDropIcon; +class SurfaceInterface; +class SurfaceItemWayland; +class PresentationFeedback; + +class DragAndDropIconItem : public Item +{ + Q_OBJECT + +public: + explicit DragAndDropIconItem(DragAndDropIcon *icon, Item *parent = nullptr); + ~DragAndDropIconItem() override; + + SurfaceInterface *surface() const; + + void setOutput(LogicalOutput *output); + +private: + std::unique_ptr m_surfaceItem; + LogicalOutput *m_output = nullptr; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/imageitem.cpp b/local/recipes/kde/kwin/source/src/scene/imageitem.cpp new file mode 100644 index 0000000000..e1be74fb55 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/imageitem.cpp @@ -0,0 +1,88 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scene/imageitem.h" + +#include "opengl/gltexture.h" + +namespace KWin +{ + +ImageItem::ImageItem(Item *parent) + : Item(parent) +{ +} + +QImage ImageItem::image() const +{ + return m_image; +} + +void ImageItem::setImage(const QImage &image) +{ + m_image = image; + discardQuads(); + scheduleRepaint(boundingRect()); +} + +ImageItemOpenGL::ImageItemOpenGL(Item *parent) + : ImageItem(parent) +{ +} + +ImageItemOpenGL::~ImageItemOpenGL() +{ +} + +GLTexture *ImageItemOpenGL::texture() const +{ + return m_texture.get(); +} + +void ImageItemOpenGL::preprocess() +{ + if (m_image.isNull()) { + m_texture.reset(); + m_textureKey = 0; + } else if (m_textureKey != m_image.cacheKey()) { + m_textureKey = m_image.cacheKey(); + + if (!m_texture || m_texture->size() != m_image.size()) { + m_texture = GLTexture::upload(m_image); + if (!m_texture) { + return; + } + m_texture->setFilter(GL_LINEAR); + m_texture->setWrapMode(GL_CLAMP_TO_EDGE); + } else { + m_texture->update(m_image, Rect(m_image.rect())); + } + } +} + +WindowQuadList ImageItemOpenGL::buildQuads() const +{ + const RectF geometry = boundingRect(); + if (geometry.isEmpty()) { + return WindowQuadList{}; + } + + const RectF imageRect = m_image.rect(); + + WindowQuad quad; + quad[0] = WindowVertex(geometry.topLeft(), imageRect.topLeft()); + quad[1] = WindowVertex(geometry.topRight(), imageRect.topRight()); + quad[2] = WindowVertex(geometry.bottomRight(), imageRect.bottomRight()); + quad[3] = WindowVertex(geometry.bottomLeft(), imageRect.bottomLeft()); + + WindowQuadList ret; + ret.append(quad); + return ret; +} + +} // namespace KWin + +#include "moc_imageitem.cpp" diff --git a/local/recipes/kde/kwin/source/src/scene/imageitem.h b/local/recipes/kde/kwin/source/src/scene/imageitem.h new file mode 100644 index 0000000000..53ade42d9b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/imageitem.h @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "scene/item.h" + +#include + +namespace KWin +{ + +class GLTexture; + +class KWIN_EXPORT ImageItem : public Item +{ + Q_OBJECT + +public: + explicit ImageItem(Item *parent = nullptr); + + QImage image() const; + void setImage(const QImage &image); + +protected: + QImage m_image; +}; + +class ImageItemOpenGL : public ImageItem +{ + Q_OBJECT + +public: + explicit ImageItemOpenGL(Item *parent = nullptr); + ~ImageItemOpenGL() override; + + GLTexture *texture() const; + +protected: + void preprocess() override; + WindowQuadList buildQuads() const override; + +private: + std::unique_ptr m_texture; + qint64 m_textureKey = 0; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/item.cpp b/local/recipes/kde/kwin/source/src/scene/item.cpp new file mode 100644 index 0000000000..d85b7a64a4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/item.cpp @@ -0,0 +1,738 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scene/item.h" +#include "core/outputlayer.h" +#include "core/pixelgrid.h" +#include "scene/scene.h" +#include "utils/common.h" +#include "workspace.h" + +namespace KWin +{ + +ItemEffect::ItemEffect(Item *item) + : m_item(item) +{ + item->addEffect(); +} + +ItemEffect::ItemEffect(ItemEffect &&move) + : m_item(std::exchange(move.m_item, nullptr)) +{ +} + +ItemEffect::ItemEffect() +{ +} + +ItemEffect::~ItemEffect() +{ + if (m_item) { + m_item->removeEffect(); + } +} + +ItemEffect &ItemEffect::operator=(ItemEffect &&move) +{ + std::swap(m_item, move.m_item); + return *this; +} + +Item::Item(Item *parent) +{ + setParentItem(parent); +} + +Item::~Item() +{ + setParentItem(nullptr); +} + +Scene *Item::scene() const +{ + return m_scene; +} + +qreal Item::opacity() const +{ + return m_opacity; +} + +void Item::setOpacity(qreal opacity) +{ + if (m_opacity != opacity) { + m_opacity = opacity; + scheduleRepaint(boundingRect()); + } +} + +int Item::z() const +{ + return m_z; +} + +void Item::setZ(int z) +{ + if (m_z == z) { + return; + } + m_z = z; + if (m_parentItem) { + m_parentItem->markSortedChildItemsDirty(); + } + scheduleSceneRepaint(boundingRect()); +} + +Item *Item::parentItem() const +{ + return m_parentItem; +} + +void Item::setParentItem(Item *item) +{ + if (m_parentItem == item) { + return; + } + if (m_parentItem) { + m_parentItem->removeChild(this); + } + m_parentItem = item; + setScene(item ? item->scene() : nullptr); + + if (m_parentItem) { + m_parentItem->addChild(this); + } + updateItemToSceneTransform(); + updateEffectiveVisibility(); +} + +void Item::addChild(Item *item) +{ + Q_ASSERT(!m_childItems.contains(item)); + + m_childItems.append(item); + markSortedChildItemsDirty(); + + updateBoundingRect(); + scheduleRepaint(item->transform().mapRect(item->boundingRect()).translated(item->position())); + + Q_EMIT childAdded(item); +} + +void Item::removeChild(Item *item) +{ + Q_ASSERT(m_childItems.contains(item)); + scheduleRepaint(item->transform().mapRect(item->boundingRect()).translated(item->position())); + + m_childItems.removeOne(item); + markSortedChildItemsDirty(); + + updateBoundingRect(); + + Q_EMIT childRemoved(item); +} + +QList Item::childItems() const +{ + return m_childItems; +} + +void Item::setScene(Scene *scene) +{ + if (m_scene == scene) { + return; + } + if (m_scene) { + for (auto it = m_deviceRepaints.constBegin(); it != m_deviceRepaints.constEnd(); ++it) { + RenderView *view = it.key(); + const Region &dirty = it.value(); + if (!dirty.isEmpty()) { + m_scene->addDeviceRepaint(view, dirty); + } + } + m_deviceRepaints.clear(); + disconnect(m_scene, &Scene::viewRemoved, this, &Item::removeRepaints); + } + if (scene) { + connect(scene, &Scene::viewRemoved, this, &Item::removeRepaints); + } + + m_scene = scene; + + for (Item *childItem : std::as_const(m_childItems)) { + childItem->setScene(scene); + } +} + +QPointF Item::position() const +{ + return m_position; +} + +void Item::setPosition(const QPointF &point) +{ + if (m_position != point) { + scheduleMoveRepaint(this); + m_position = point; + updateItemToSceneTransform(); + if (m_parentItem) { + m_parentItem->updateBoundingRect(); + } + scheduleMoveRepaint(this); + Q_EMIT positionChanged(); + } +} + +QSizeF Item::size() const +{ + return m_size; +} + +void Item::setSize(const QSizeF &size) +{ + if (m_size != size) { + scheduleRepaint(rect()); + m_size = size; + updateBoundingRect(); + scheduleRepaint(rect()); + discardQuads(); + Q_EMIT sizeChanged(); + } +} + +void Item::setGeometry(const RectF &rect) +{ + setPosition(rect.topLeft()); + setSize(rect.size()); +} + +RectF Item::rect() const +{ + return RectF(QPoint(0, 0), size()); +} + +RectF Item::boundingRect() const +{ + return m_boundingRect; +} + +void Item::updateBoundingRect() +{ + RectF boundingRect = rect(); + for (Item *item : std::as_const(m_childItems)) { + boundingRect |= item->transform().mapRect(item->boundingRect()).translated(item->position()); + } + if (m_boundingRect != boundingRect) { + m_boundingRect = boundingRect; + Q_EMIT boundingRectChanged(); + if (m_parentItem) { + m_parentItem->updateBoundingRect(); + } + } +} + +QList Item::shape() const +{ + return QList(); +} + +Region Item::opaque() const +{ + return Region(); +} + +QTransform Item::transform() const +{ + return m_transform; +} + +void Item::setTransform(const QTransform &transform) +{ + if (m_transform == transform) { + return; + } + scheduleRepaint(boundingRect()); + m_transform = transform; + updateItemToSceneTransform(); + if (m_parentItem) { + m_parentItem->updateBoundingRect(); + } + scheduleRepaint(boundingRect()); +} + +void Item::updateItemToSceneTransform() +{ + m_itemToSceneTransform = m_transform; + if (!m_position.isNull()) { + m_itemToSceneTransform *= QTransform::fromTranslate(m_position.x(), m_position.y()); + } + if (m_parentItem) { + m_itemToSceneTransform *= m_parentItem->m_itemToSceneTransform; + } + m_sceneToItemTransform = m_itemToSceneTransform.inverted(); + + for (Item *childItem : std::as_const(m_childItems)) { + childItem->updateItemToSceneTransform(); + } +} + +Region Item::mapToView(const Region ®ion, const RenderView *view) const +{ + Region ret; + for (RectF rect : region.rects()) { + ret |= mapToView(rect, view).toAlignedRect(); + } + return ret; +} + +RectF Item::mapToView(const RectF &rect, const RenderView *view) const +{ + const auto snappedPosition = snapToPixels(m_position, view->scale()); + const RectF ret = rect.translated(snappedPosition); + if (m_parentItem) { + return m_parentItem->mapToView(ret, view); + } else { + return ret; + } +} + +Region Item::mapToScene(const Region ®ion) const +{ + if (region.isEmpty()) { + return Region(); + } + Region ret; + for (const Rect &rect : region.rects()) { + ret |= m_itemToSceneTransform.mapRect(rect); + } + return ret; +} + +RectF Item::mapToScene(const RectF &rect) const +{ + if (rect.isEmpty()) { + return Rect(); + } + return m_itemToSceneTransform.mapRect(rect); +} + +RectF Item::mapFromScene(const RectF &rect) const +{ + if (rect.isEmpty()) { + return Rect(); + } + return m_sceneToItemTransform.mapRect(rect); +} + +Rect Item::paintedDeviceArea(RenderView *view, const RectF &rect) const +{ + const qreal scale = view->scale(); + + RectF snapped = rect.scaled(scale).rounded(); + for (const Item *item = this; item; item = item->parentItem()) { + if (!item->m_transform.isIdentity()) { + snapped = (QTransform::fromScale(1 / scale, 1 / scale) * item->m_transform * QTransform::fromScale(scale, scale)) + .mapRect(snapped); + } + + snapped.translate(snapToPixelGridF(item->position() * scale)); + } + return view->mapToDeviceCoordinatesAligned(scaledRect(snapped, 1.0 / scale)) & view->deviceRect(); +} + +Region Item::paintedDeviceArea(RenderView *view, const Region ®ion) const +{ + Region ret; + for (RectF part : region.rects()) { + ret |= paintedDeviceArea(view, part); + } + return ret; +} + +void Item::stackBefore(Item *sibling) +{ + if (Q_UNLIKELY(!sibling)) { + qCDebug(KWIN_CORE) << Q_FUNC_INFO << "requires a valid sibling"; + return; + } + if (Q_UNLIKELY(!sibling->parentItem() || sibling->parentItem() != parentItem())) { + qCDebug(KWIN_CORE) << Q_FUNC_INFO << "requires items to be siblings"; + return; + } + if (Q_UNLIKELY(sibling == this)) { + return; + } + + const int selfIndex = m_parentItem->m_childItems.indexOf(this); + const int siblingIndex = m_parentItem->m_childItems.indexOf(sibling); + + if (selfIndex == siblingIndex - 1) { + return; + } + + m_parentItem->m_childItems.move(selfIndex, selfIndex > siblingIndex ? siblingIndex : siblingIndex - 1); + m_parentItem->markSortedChildItemsDirty(); + + scheduleSceneRepaint(boundingRect()); + sibling->scheduleSceneRepaint(sibling->boundingRect()); +} + +void Item::stackAfter(Item *sibling) +{ + if (Q_UNLIKELY(!sibling)) { + qCDebug(KWIN_CORE) << Q_FUNC_INFO << "requires a valid sibling"; + return; + } + if (Q_UNLIKELY(!sibling->parentItem() || sibling->parentItem() != parentItem())) { + qCDebug(KWIN_CORE) << Q_FUNC_INFO << "requires items to be siblings"; + return; + } + if (Q_UNLIKELY(sibling == this)) { + return; + } + + const int selfIndex = m_parentItem->m_childItems.indexOf(this); + const int siblingIndex = m_parentItem->m_childItems.indexOf(sibling); + + if (selfIndex == siblingIndex + 1) { + return; + } + + m_parentItem->m_childItems.move(selfIndex, selfIndex > siblingIndex ? siblingIndex + 1 : siblingIndex); + m_parentItem->markSortedChildItemsDirty(); + + scheduleSceneRepaint(boundingRect()); + sibling->scheduleSceneRepaint(sibling->boundingRect()); +} + +void Item::scheduleRepaint(const Region ®ion) +{ + if (isVisible()) { + scheduleRepaintInternal(region); + } +} + +void Item::scheduleRepaint(RenderView *view, const Region ®ion) +{ + if (isVisible()) { + scheduleRepaintInternal(view, region); + } +} + +void Item::scheduleRepaintInternal(const Region ®ion) +{ + if (Q_UNLIKELY(!m_scene)) { + return; + } + const QList views = m_scene->views(); + for (RenderView *view : views) { + if (!view->shouldRenderItem(this)) { + continue; + } + const Region dirtyRegion = paintedDeviceArea(view, region); + if (!dirtyRegion.isEmpty()) { + m_deviceRepaints[view] += dirtyRegion; + view->scheduleRepaint(this); + } + } +} + +void Item::scheduleMoveRepaint(Item *originallyMovedItem) +{ + if (Q_UNLIKELY(!m_scene) || !isVisible()) { + return; + } + const QList views = m_scene->views(); + for (RenderView *view : views) { + if (!view->shouldRenderItem(this)) { + continue; + } + const Region dirtyRegion = paintedDeviceArea(view, rect()); + if (!dirtyRegion.isEmpty()) { + // we can skip the move repaint if the parent item was moved + // and this item was just implicitly moved as a consequence + if (!view->canSkipMoveRepaint(originallyMovedItem)) { + m_deviceRepaints[view] += dirtyRegion; + } + view->scheduleRepaint(this); + } + } + for (Item *child : std::as_const(m_childItems)) { + child->scheduleMoveRepaint(originallyMovedItem); + } +} + +void Item::scheduleRepaintInternal(RenderView *view, const Region ®ion) +{ + if (Q_UNLIKELY(!m_scene) || !view->shouldRenderItem(this)) { + return; + } + const Region dirtyRegion = paintedDeviceArea(view, region); + if (!dirtyRegion.isEmpty()) { + m_deviceRepaints[view] += dirtyRegion; + view->scheduleRepaint(this); + } +} + +void Item::scheduleFrame() +{ + if (!isVisible()) { + return; + } + if (Q_UNLIKELY(!m_scene)) { + return; + } + const QList views = m_scene->views(); + for (RenderView *view : views) { + if (!view->shouldRenderItem(this)) { + continue; + } + const Rect geometry = paintedDeviceArea(view, rect()); + if (!geometry.isEmpty()) { + view->scheduleRepaint(this); + } + } +} + +void Item::scheduleSceneRepaintInternal(const Region ®ion) +{ + if (Q_UNLIKELY(!m_scene)) { + return; + } + const QList views = m_scene->views(); + for (RenderView *view : views) { + if (!view->shouldRenderItem(this) && !view->shouldRenderHole(this)) { + continue; + } + const Region dirtyRegion = paintedDeviceArea(view, region); + if (!dirtyRegion.isEmpty()) { + m_scene->addDeviceRepaint(view, dirtyRegion); + } + } +} + +void Item::preprocess() +{ +} + +WindowQuadList Item::buildQuads() const +{ + return WindowQuadList(); +} + +void Item::discardQuads() +{ + m_quads.reset(); +} + +WindowQuadList Item::quads() const +{ + if (!m_quads.has_value()) { + m_quads = buildQuads(); + } + return m_quads.value(); +} + +bool Item::hasRepaints(RenderView *view) const +{ + const auto it = m_deviceRepaints.find(view); + return it != m_deviceRepaints.end() && !it->isEmpty(); +} + +Region Item::takeDeviceRepaints(RenderView *view) +{ + auto &repaints = m_deviceRepaints[view]; + Region reg; + std::swap(reg, repaints); + return reg; +} + +void Item::resetRepaints(RenderView *view) +{ + m_deviceRepaints.insert(view, Region()); +} + +void Item::removeRepaints(RenderView *view) +{ + m_deviceRepaints.remove(view); +} + +bool Item::explicitVisible() const +{ + return m_explicitVisible; +} + +bool Item::isVisible() const +{ + return m_effectiveVisible; +} + +void Item::setVisible(bool visible) +{ + if (m_explicitVisible != visible) { + m_explicitVisible = visible; + updateEffectiveVisibility(); + } +} + +BorderRadius Item::borderRadius() const +{ + return m_borderRadius; +} + +void Item::setBorderRadius(const BorderRadius &radius) +{ + if (m_borderRadius != radius) { + m_borderRadius = radius; + scheduleRepaint(rect()); + } +} + +void Item::scheduleRepaint(const RectF ®ion) +{ + scheduleRepaint(Region(region.roundedOut())); +} + +void Item::scheduleSceneRepaint(const RectF ®ion) +{ + scheduleSceneRepaint(Region(region.roundedOut())); +} + +void Item::scheduleSceneRepaint(const Region ®ion) +{ + if (isVisible()) { + scheduleSceneRepaintInternal(region); + } +} + +bool Item::computeEffectiveVisibility() const +{ + return m_explicitVisible && (!m_parentItem || m_parentItem->isVisible()); +} + +void Item::updateEffectiveVisibility() +{ + const bool effectiveVisible = computeEffectiveVisibility(); + if (m_effectiveVisible == effectiveVisible) { + return; + } + + m_effectiveVisible = effectiveVisible; + if (!m_effectiveVisible) { + scheduleSceneRepaintInternal(boundingRect().toAlignedRect()); + } else { + scheduleRepaintInternal(boundingRect().toAlignedRect()); + } + + for (Item *childItem : std::as_const(m_childItems)) { + childItem->updateEffectiveVisibility(); + } + Q_EMIT visibleChanged(); +} + +static bool compareZ(const Item *a, const Item *b) +{ + return a->z() < b->z(); +} + +QList Item::sortedChildItems() const +{ + if (!m_sortedChildItems.has_value()) { + QList items = m_childItems; + std::stable_sort(items.begin(), items.end(), compareZ); + m_sortedChildItems = items; + } + return m_sortedChildItems.value(); +} + +void Item::markSortedChildItemsDirty() +{ + m_sortedChildItems.reset(); +} + +const std::shared_ptr &Item::colorDescription() const +{ + return m_colorDescription; +} + +RenderingIntent Item::renderingIntent() const +{ + return m_renderingIntent; +} + +void Item::setColorDescription(const std::shared_ptr &description) +{ + m_colorDescription = description; +} + +void Item::setRenderingIntent(RenderingIntent intent) +{ + m_renderingIntent = intent; +} + +PresentationModeHint Item::presentationHint() const +{ + return m_presentationHint; +} + +void Item::setPresentationHint(PresentationModeHint hint) +{ + m_presentationHint = hint; +} + +bool Item::hasEffects() const +{ + return m_effectCount != 0; +} + +void Item::addEffect() +{ + m_effectCount++; +} + +void Item::removeEffect() +{ + Q_ASSERT(m_effectCount > 0); + m_effectCount--; +} + +void Item::framePainted(RenderView *view, LogicalOutput *output, OutputFrame *frame, std::chrono::milliseconds timestamp) +{ + // The visibility of the item itself is not checked here to be able to paint hidden items for + // things like screncasts or thumbnails + handleFramePainted(output, frame, timestamp); + for (const auto child : std::as_const(m_childItems)) { + if (child->explicitVisible() && workspace()->outputAt(child->mapToScene(child->boundingRect()).center()) == output) { + child->framePainted(view, output, frame, timestamp); + } + } +} + +bool Item::isAncestorOf(const Item *item) const +{ + return std::ranges::any_of(m_childItems, [item](const Item *child) { + return child == item || child->isAncestorOf(item); + }); +} + +void Item::handleFramePainted(LogicalOutput *output, OutputFrame *frame, std::chrono::milliseconds timestamp) +{ +} + +bool Item::hasVisibleContents() const +{ + if (!isVisible()) { + return false; + } + return !m_size.isEmpty() || std::ranges::any_of(m_childItems, [](Item *item) { + return item->hasVisibleContents(); + }); +} + +} // namespace KWin + +#include "moc_item.cpp" diff --git a/local/recipes/kde/kwin/source/src/scene/item.h b/local/recipes/kde/kwin/source/src/scene/item.h new file mode 100644 index 0000000000..545b67e1bd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/item.h @@ -0,0 +1,242 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/colorspace.h" +#include "core/rect.h" +#include "core/region.h" +#include "effect/globals.h" +#include "scene/borderradius.h" +#include "scene/itemgeometry.h" + +#include +#include +#include +#include + +#include + +namespace KWin +{ + +class RenderView; +class Scene; +class SyncReleasePoint; +class DrmDevice; +class Item; +class LogicalOutput; +class OutputFrame; + +class KWIN_EXPORT ItemEffect +{ +public: + explicit ItemEffect(Item *item); + explicit ItemEffect(const ItemEffect ©) = delete; + explicit ItemEffect(ItemEffect &&move); + explicit ItemEffect(); + virtual ~ItemEffect(); + + ItemEffect &operator=(const ItemEffect ©) = delete; + ItemEffect &operator=(ItemEffect &&move); + +private: + QPointer m_item; +}; + +/** + * The Item class is the base class for items in the scene. + */ +class KWIN_EXPORT Item : public QObject +{ + Q_OBJECT + +public: + explicit Item(Item *parent = nullptr); + ~Item() override; + + Scene *scene() const; + + qreal opacity() const; + void setOpacity(qreal opacity); + + QPointF position() const; + void setPosition(const QPointF &point); + + QSizeF size() const; + void setSize(const QSizeF &size); + + void setGeometry(const RectF &rect); + + int z() const; + void setZ(int z); + + /** + * Returns the enclosing rectangle of the item. The rect equals Rect(0, 0, width(), height()). + */ + RectF rect() const; + /** + * Returns the enclosing rectangle of the item and all of its descendants. + */ + RectF boundingRect() const; + + virtual QList shape() const; + virtual Region opaque() const; + + /** + * Returns the visual parent of the item. Note that the visual parent differs from + * the QObject parent. + */ + Item *parentItem() const; + void setParentItem(Item *parent); + QList childItems() const; + QList sortedChildItems() const; + + QTransform transform() const; + void setTransform(const QTransform &transform); + + /** + * Maps the given @a region from the item's coordinate system to the view's coordinate + * system, snapping positions to the view's coordinate grid to match the renderer + */ + Region mapToView(const Region ®ion, const RenderView *view) const; + /** + * Maps the given @a rect from the item's coordinate system to the view's coordinate + * system, snapping positions to the view's coordinate grid to match the renderer + */ + RectF mapToView(const RectF &rect, const RenderView *view) const; + + /** + * Maps the given @a region from the item's coordinate system to the scene's coordinate + * system. + */ + Region mapToScene(const Region ®ion) const; + /** + * Maps the given @a rect from the item's coordinate system to the scene's coordinate + * system. + */ + RectF mapToScene(const RectF &rect) const; + /** + * Maps the given @a rect from the scene's coordinate system to the item's coordinate + * system. + */ + RectF mapFromScene(const RectF &rect) const; + + /** + * Moves this item right before the specified @a sibling in the parent's children list. + */ + void stackBefore(Item *sibling); + /** + * Moves this item right after the specified @a sibling in the parent's children list. + */ + void stackAfter(Item *sibling); + + bool explicitVisible() const; + bool isVisible() const; + void setVisible(bool visible); + + BorderRadius borderRadius() const; + void setBorderRadius(const BorderRadius &radius); + + Rect paintedDeviceArea(RenderView *delegate, const RectF &logicalRect) const; + Region paintedDeviceArea(RenderView *delegate, const Region &logicalRegion) const; + + void scheduleRepaint(const RectF ®ion); + void scheduleSceneRepaint(const RectF ®ion); + void scheduleRepaint(const Region ®ion); + void scheduleSceneRepaint(const Region ®ion); + void scheduleRepaint(RenderView *delegate, const Region ®ion); + void scheduleFrame(); + bool hasRepaints(RenderView *view) const; + Region takeDeviceRepaints(RenderView *delegate); + void resetRepaints(RenderView *delegate); + + WindowQuadList quads() const; + virtual void preprocess(); + const std::shared_ptr &colorDescription() const; + RenderingIntent renderingIntent() const; + PresentationModeHint presentationHint() const; + + bool hasEffects() const; + void addEffect(); + void removeEffect(); + + void framePainted(RenderView *view, LogicalOutput *output, OutputFrame *frame, std::chrono::milliseconds timestamp); + + bool isAncestorOf(const Item *item) const; + /** + * @returns if this Item or any of its children have contents to be rendered + */ + bool hasVisibleContents() const; + +Q_SIGNALS: + void childAdded(Item *item); + void childRemoved(Item *item); + void visibleChanged(); + /** + * This signal is emitted when the position of this item has changed. + */ + void positionChanged(); + /** + * This signal is emitted when the size of this item has changed. + */ + void sizeChanged(); + + /** + * This signal is emitted when the rectangle that encloses this item and all of its children + * has changed. + */ + void boundingRectChanged(); + +protected: + virtual WindowQuadList buildQuads() const; + virtual void handleFramePainted(LogicalOutput *output, OutputFrame *frame, std::chrono::milliseconds timestamp); + void discardQuads(); + void setColorDescription(const std::shared_ptr &description); + void setRenderingIntent(RenderingIntent intent); + void setPresentationHint(PresentationModeHint hint); + void setScene(Scene *scene); + +private: + void addChild(Item *item); + void removeChild(Item *item); + void updateBoundingRect(); + void updateItemToSceneTransform(); + void scheduleRepaintInternal(const Region ®ion); + void scheduleRepaintInternal(RenderView *delegate, const Region ®ion); + void scheduleSceneRepaintInternal(const Region ®ion); + void markSortedChildItemsDirty(); + + bool computeEffectiveVisibility() const; + void updateEffectiveVisibility(); + void removeRepaints(RenderView *delegate); + + void scheduleMoveRepaint(Item *originallyMovedItem); + + Scene *m_scene = nullptr; + QPointer m_parentItem; + QList m_childItems; + QTransform m_transform; + QTransform m_itemToSceneTransform; + QTransform m_sceneToItemTransform; + RectF m_boundingRect; + QPointF m_position; + QSizeF m_size = QSize(0, 0); + BorderRadius m_borderRadius; + qreal m_opacity = 1; + int m_z = 0; + bool m_explicitVisible = true; + bool m_effectiveVisible = true; + QMap m_deviceRepaints; + mutable std::optional m_quads; + mutable std::optional> m_sortedChildItems; + std::shared_ptr m_colorDescription = ColorDescription::sRGB; + RenderingIntent m_renderingIntent = RenderingIntent::Perceptual; + PresentationModeHint m_presentationHint = PresentationModeHint::VSync; + int m_effectCount = 0; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/itemgeometry.cpp b/local/recipes/kde/kwin/source/src/scene/itemgeometry.cpp new file mode 100644 index 0000000000..7f0a9c8168 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/itemgeometry.cpp @@ -0,0 +1,300 @@ +/* + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2022 Arjen Hiemstra + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scene/itemgeometry.h" + +#include + +namespace KWin +{ + +WindowQuad WindowQuad::makeSubQuad(double x1, double y1, double x2, double y2) const +{ + Q_ASSERT(x1 < x2 && y1 < y2 && x1 >= left() && x2 <= right() && y1 >= top() && y2 <= bottom()); + WindowQuad ret(*this); + // vertices are clockwise starting from topleft + ret.verts[0].px = x1; + ret.verts[3].px = x1; + ret.verts[1].px = x2; + ret.verts[2].px = x2; + ret.verts[0].py = y1; + ret.verts[1].py = y1; + ret.verts[2].py = y2; + ret.verts[3].py = y2; + + const double xOrigin = left(); + const double yOrigin = top(); + + const double widthReciprocal = 1 / (right() - xOrigin); + const double heightReciprocal = 1 / (bottom() - yOrigin); + + for (int i = 0; i < 4; ++i) { + const double w1 = (ret.verts[i].px - xOrigin) * widthReciprocal; + const double w2 = (ret.verts[i].py - yOrigin) * heightReciprocal; + + // Use bilinear interpolation to compute the texture coords. + ret.verts[i].tx = (1 - w1) * (1 - w2) * verts[0].tx + w1 * (1 - w2) * verts[1].tx + w1 * w2 * verts[2].tx + (1 - w1) * w2 * verts[3].tx; + ret.verts[i].ty = (1 - w1) * (1 - w2) * verts[0].ty + w1 * (1 - w2) * verts[1].ty + w1 * w2 * verts[2].ty + (1 - w1) * w2 * verts[3].ty; + } + + return ret; +} + +WindowQuadList WindowQuadList::splitAtX(double x) const +{ + WindowQuadList ret; + ret.reserve(count()); + for (const WindowQuad &quad : *this) { + bool wholeleft = true; + bool wholeright = true; + for (int i = 0; i < 4; ++i) { + if (quad[i].x() < x) { + wholeright = false; + } + if (quad[i].x() > x) { + wholeleft = false; + } + } + if (wholeleft || wholeright) { // is whole in one split part + ret.append(quad); + continue; + } + if (quad.top() == quad.bottom() || quad.left() == quad.right()) { // quad has no size + ret.append(quad); + continue; + } + ret.append(quad.makeSubQuad(quad.left(), quad.top(), x, quad.bottom())); + ret.append(quad.makeSubQuad(x, quad.top(), quad.right(), quad.bottom())); + } + return ret; +} + +WindowQuadList WindowQuadList::splitAtY(double y) const +{ + WindowQuadList ret; + ret.reserve(count()); + for (const WindowQuad &quad : *this) { + bool wholetop = true; + bool wholebottom = true; + for (int i = 0; i < 4; ++i) { + if (quad[i].y() < y) { + wholebottom = false; + } + if (quad[i].y() > y) { + wholetop = false; + } + } + if (wholetop || wholebottom) { // is whole in one split part + ret.append(quad); + continue; + } + if (quad.top() == quad.bottom() || quad.left() == quad.right()) { // quad has no size + ret.append(quad); + continue; + } + ret.append(quad.makeSubQuad(quad.left(), quad.top(), quad.right(), y)); + ret.append(quad.makeSubQuad(quad.left(), y, quad.right(), quad.bottom())); + } + return ret; +} + +WindowQuadList WindowQuadList::makeGrid(int maxQuadSize) const +{ + if (empty()) { + return *this; + } + + // Find the bounding rectangle + double left = first().left(); + double right = first().right(); + double top = first().top(); + double bottom = first().bottom(); + + for (const WindowQuad &quad : std::as_const(*this)) { + left = std::min(left, quad.left()); + right = std::max(right, quad.right()); + top = std::min(top, quad.top()); + bottom = std::max(bottom, quad.bottom()); + } + + WindowQuadList ret; + + for (const WindowQuad &quad : std::as_const(*this)) { + const double quadLeft = quad.left(); + const double quadRight = quad.right(); + const double quadTop = quad.top(); + const double quadBottom = quad.bottom(); + + // sanity check, see BUG 390953 + if (quadLeft == quadRight || quadTop == quadBottom) { + ret.append(quad); + continue; + } + + // Compute the top-left corner of the first intersecting grid cell + const double xBegin = left + qFloor((quadLeft - left) / maxQuadSize) * maxQuadSize; + const double yBegin = top + qFloor((quadTop - top) / maxQuadSize) * maxQuadSize; + + // Loop over all intersecting cells and add sub-quads + for (double y = yBegin; y < quadBottom; y += maxQuadSize) { + const double y0 = std::max(y, quadTop); + const double y1 = std::min(quadBottom, y + maxQuadSize); + + for (double x = xBegin; x < quadRight; x += maxQuadSize) { + const double x0 = std::max(x, quadLeft); + const double x1 = std::min(quadRight, x + maxQuadSize); + + ret.append(quad.makeSubQuad(x0, y0, x1, y1)); + } + } + } + + return ret; +} + +WindowQuadList WindowQuadList::makeRegularGrid(int xSubdivisions, int ySubdivisions) const +{ + if (empty()) { + return *this; + } + + // Find the bounding rectangle + double left = first().left(); + double right = first().right(); + double top = first().top(); + double bottom = first().bottom(); + + for (const WindowQuad &quad : *this) { + left = std::min(left, quad.left()); + right = std::max(right, quad.right()); + top = std::min(top, quad.top()); + bottom = std::max(bottom, quad.bottom()); + } + + double xIncrement = (right - left) / xSubdivisions; + double yIncrement = (bottom - top) / ySubdivisions; + + WindowQuadList ret; + + for (const WindowQuad &quad : *this) { + const double quadLeft = quad.left(); + const double quadRight = quad.right(); + const double quadTop = quad.top(); + const double quadBottom = quad.bottom(); + + // sanity check, see BUG 390953 + if (quadLeft == quadRight || quadTop == quadBottom) { + ret.append(quad); + continue; + } + + // Compute the top-left corner of the first intersecting grid cell + const double xBegin = left + qFloor((quadLeft - left) / xIncrement) * xIncrement; + const double yBegin = top + qFloor((quadTop - top) / yIncrement) * yIncrement; + + // Loop over all intersecting cells and add sub-quads + for (double y = yBegin; y < quadBottom; y += yIncrement) { + const double y0 = std::max(y, quadTop); + const double y1 = std::min(quadBottom, y + yIncrement); + + for (double x = xBegin; x < quadRight; x += xIncrement) { + const double x0 = std::max(x, quadLeft); + const double x1 = std::min(quadRight, x + xIncrement); + + ret.append(quad.makeSubQuad(x0, y0, x1, y1)); + } + } + } + + return ret; +} + +void RenderGeometry::copy(std::span destination) +{ + Q_ASSERT(int(destination.size()) >= size()); + std::copy(cbegin(), cend(), destination.begin()); +} + +void RenderGeometry::appendWindowVertex(const WindowVertex &windowVertex, qreal deviceScale) +{ + GLVertex2D glVertex; + switch (m_vertexSnappingMode) { + case VertexSnappingMode::None: + glVertex.position = QVector2D(windowVertex.x(), windowVertex.y()) * deviceScale; + break; + case VertexSnappingMode::Round: + glVertex.position = QVector2D(std::round(windowVertex.x() * deviceScale), std::round(windowVertex.y() * deviceScale)); + break; + } + glVertex.texcoord = QVector2D(windowVertex.u(), windowVertex.v()); + append(glVertex); +} + +void RenderGeometry::appendWindowQuad(const WindowQuad &quad, qreal deviceScale) +{ + // Geometry assumes we're rendering triangles, so add the quad's + // vertices as two triangles. Vertex order is top-left, bottom-left, + // top-right followed by top-right, bottom-left, bottom-right. + appendWindowVertex(quad[0], deviceScale); + appendWindowVertex(quad[3], deviceScale); + appendWindowVertex(quad[1], deviceScale); + + appendWindowVertex(quad[1], deviceScale); + appendWindowVertex(quad[3], deviceScale); + appendWindowVertex(quad[2], deviceScale); +} + +void RenderGeometry::appendSubQuad(const WindowQuad &quad, const RectF &subquad, qreal deviceScale) +{ + std::array vertices; + vertices[0].position = QVector2D(subquad.topLeft()); + vertices[1].position = QVector2D(subquad.topRight()); + vertices[2].position = QVector2D(subquad.bottomRight()); + vertices[3].position = QVector2D(subquad.bottomLeft()); + + const auto deviceQuad = RectF{QPointF(std::round(quad.left() * deviceScale), std::round(quad.top() * deviceScale)), + QPointF(std::round(quad.right() * deviceScale), std::round(quad.bottom() * deviceScale))}; + + const QPointF origin = deviceQuad.topLeft(); + const QSizeF size = deviceQuad.size(); + +#pragma GCC unroll 4 + for (int i = 0; i < 4; ++i) { + const double weight1 = (vertices[i].position.x() - origin.x()) / size.width(); + const double weight2 = (vertices[i].position.y() - origin.y()) / size.height(); + const double oneMinW1 = 1.0 - weight1; + const double oneMinW2 = 1.0 - weight2; + + const float u = oneMinW1 * oneMinW2 * quad[0].u() + weight1 * oneMinW2 * quad[1].u() + + weight1 * weight2 * quad[2].u() + oneMinW1 * weight2 * quad[3].u(); + const float v = oneMinW1 * oneMinW2 * quad[0].v() + weight1 * oneMinW2 * quad[1].v() + + weight1 * weight2 * quad[2].v() + oneMinW1 * weight2 * quad[3].v(); + vertices[i].texcoord = QVector2D(u, v); + } + + append(vertices[0]); + append(vertices[3]); + append(vertices[1]); + + append(vertices[1]); + append(vertices[3]); + append(vertices[2]); +} + +void RenderGeometry::postProcessTextureCoordinates(const QMatrix4x4 &textureMatrix) +{ + if (!textureMatrix.isIdentity()) { + const QVector2D coeff(textureMatrix(0, 0), textureMatrix(1, 1)); + const QVector2D offset(textureMatrix(0, 3), textureMatrix(1, 3)); + + for (auto &vertex : (*this)) { + vertex.texcoord = vertex.texcoord * coeff + offset; + } + } +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/itemgeometry.h b/local/recipes/kde/kwin/source/src/scene/itemgeometry.h new file mode 100644 index 0000000000..a3a2df305c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/itemgeometry.h @@ -0,0 +1,297 @@ +/* + SPDX-FileCopyrightText: 2006 Lubos Lunak + SPDX-FileCopyrightText: 2022 Arjen Hiemstra + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "opengl/glvertexbuffer.h" + +namespace KWin +{ + +/** + * @short Vertex class + * + * A vertex is one position in a window. WindowQuad consists of four WindowVertex objects + * and represents one part of a window. + */ +class KWIN_EXPORT WindowVertex +{ +public: + WindowVertex(); + WindowVertex(const QPointF &position, const QPointF &textureCoordinate); + WindowVertex(double x, double y, double tx, double ty); + + double x() const + { + return px; + } + double y() const + { + return py; + } + double u() const + { + return tx; + } + double v() const + { + return ty; + } + void move(double x, double y); + void setX(double x); + void setY(double y); + +private: + friend class WindowQuad; + friend class WindowQuadList; + double px, py; // position + double tx, ty; // texture coords +}; + +/** + * @short Class representing one area of a window. + * + * WindowQuads consists of four WindowVertex objects and represents one part of a window. + */ +// NOTE: This class expects the (original) vertices to be in the clockwise order starting from topleft. +class KWIN_EXPORT WindowQuad +{ +public: + WindowQuad(); + WindowQuad makeSubQuad(double x1, double y1, double x2, double y2) const; + WindowVertex &operator[](int index); + const WindowVertex &operator[](int index) const; + double left() const; + double right() const; + double top() const; + double bottom() const; + RectF bounds() const; + + static WindowQuad fromRect(const RectF &rect); + +private: + friend class WindowQuadList; + WindowVertex verts[4]; +}; + +class KWIN_EXPORT WindowQuadList + : public QList +{ +public: + WindowQuadList splitAtX(double x) const; + WindowQuadList splitAtY(double y) const; + WindowQuadList makeGrid(int maxquadsize) const; + WindowQuadList makeRegularGrid(int xSubdivisions, int ySubdivisions) const; +}; + +/** + * A helper class for render geometry in device coordinates. + * + * This mostly represents a vector of vertices, with some convenience methods + * for easily converting from WindowQuad and related classes to lists of + * GLVertex2D. This class assumes rendering happens as unindexed triangles. + */ +class KWIN_EXPORT RenderGeometry : public QList +{ +public: + /** + * In what way should vertices snap to integer device coordinates? + * + * Vertices are converted to device coordinates before being sent to the + * rendering system. Depending on scaling factors, this may lead to device + * coordinates with fractional parts. For some cases, this may not be ideal + * as fractional coordinates need to be interpolated and can lead to + * "blurry" rendering. To avoid that, we can snap the vertices to integer + * device coordinates when they are added. + */ + enum class VertexSnappingMode { + None, //< No rounding, device coordinates containing fractional parts + // are passed directly to the rendering system. + Round, //< Perform a simple rounding, device coordinates will not have + // any fractional parts. + }; + + /** + * The vertex snapping mode to use for this geometry. + * + * By default, this is VertexSnappingMode::Round. + */ + inline VertexSnappingMode vertexSnappingMode() const + { + return m_vertexSnappingMode; + } + /** + * Set the vertex snapping mode to use for this geometry. + * + * Note that this doesn't change vertices retroactively, so you should set + * this before adding any vertices, or clear and rebuild the geometry after + * setting it. + * + * @param mode The new rounding mode. + */ + void setVertexSnappingMode(VertexSnappingMode mode) + { + m_vertexSnappingMode = mode; + } + /** + * Copy geometry data into another buffer. + * + * This is primarily intended for copying into a vertex buffer for rendering. + * + * @param destination The destination buffer. This needs to be at least large + * enough to contain all elements. + */ + void copy(std::span destination); + /** + * Append a WindowVertex as a geometry vertex. + * + * WindowVertex is assumed to be in logical coordinates. It will be converted + * to device coordinates using the specified device scale and then rounded + * so it fits correctly on the device pixel grid. + * + * @param windowVertex The WindowVertex instance to append. + * @param deviceScale The scaling factor to use to go from logical to device + * coordinates. + */ + void appendWindowVertex(const WindowVertex &windowVertex, qreal deviceScale); + /** + * Append a WindowQuad as two triangles. + * + * This will append the corners of the specified WindowQuad in the right + * order so they make two triangles that can be rendered by OpenGL. The + * corners are converted to device coordinates and rounded, just like + * `appendWindowVertex()` does. + * + * @param quad The WindowQuad instance to append. + * @param deviceScale The scaling factor to use to go from logical to device + * coordinates. + */ + void appendWindowQuad(const WindowQuad &quad, qreal deviceScale); + /** + * Append a sub-quad of a WindowQuad as two triangles. + * + * This will append the sub-quad specified by `intersection` as two + * triangles. The quad is expected to be in logical coordinates, while the + * intersection is expected to be in device coordinates. The texture + * coordinates of the resulting vertices are based upon those of the quad, + * using bilinear interpolation for interpolating how much of the original + * texture coordinates to use. + * + * @param quad The WindowQuad instance to use a sub-quad of. + * @param subquad The sub-quad to append. + * @param deviceScale The scaling factor used to convert from logical to + * device coordinates. + */ + void appendSubQuad(const WindowQuad &quad, const RectF &subquad, qreal deviceScale); + /** + * Modify this geometry's texture coordinates based on a matrix. + * + * This is primarily intended to convert from non-normalised to normalised + * texture coordinates. + * + * @param textureMatrix The texture matrix to use for modifying the + * texture coordinates. Note that only the 2D scale and + * translation are used. + */ + void postProcessTextureCoordinates(const QMatrix4x4 &textureMatrix); + +private: + VertexSnappingMode m_vertexSnappingMode = VertexSnappingMode::Round; +}; + +inline WindowVertex::WindowVertex() + : px(0) + , py(0) + , tx(0) + , ty(0) +{ +} + +inline WindowVertex::WindowVertex(double _x, double _y, double _tx, double _ty) + : px(_x) + , py(_y) + , tx(_tx) + , ty(_ty) +{ +} + +inline WindowVertex::WindowVertex(const QPointF &position, const QPointF &texturePosition) + : px(position.x()) + , py(position.y()) + , tx(texturePosition.x()) + , ty(texturePosition.y()) +{ +} + +inline void WindowVertex::move(double x, double y) +{ + px = x; + py = y; +} + +inline void WindowVertex::setX(double x) +{ + px = x; +} + +inline void WindowVertex::setY(double y) +{ + py = y; +} + +inline WindowQuad::WindowQuad() +{ +} + +inline WindowVertex &WindowQuad::operator[](int index) +{ + Q_ASSERT(index >= 0 && index < 4); + return verts[index]; +} + +inline const WindowVertex &WindowQuad::operator[](int index) const +{ + Q_ASSERT(index >= 0 && index < 4); + return verts[index]; +} + +inline double WindowQuad::left() const +{ + return std::min(verts[0].px, std::min(verts[1].px, std::min(verts[2].px, verts[3].px))); +} + +inline double WindowQuad::right() const +{ + return std::max(verts[0].px, std::max(verts[1].px, std::max(verts[2].px, verts[3].px))); +} + +inline double WindowQuad::top() const +{ + return std::min(verts[0].py, std::min(verts[1].py, std::min(verts[2].py, verts[3].py))); +} + +inline double WindowQuad::bottom() const +{ + return std::max(verts[0].py, std::max(verts[1].py, std::max(verts[2].py, verts[3].py))); +} + +inline RectF WindowQuad::bounds() const +{ + return RectF(QPointF(left(), top()), QPointF(right(), bottom())); +} + +inline WindowQuad WindowQuad::fromRect(const RectF &rect) +{ + WindowQuad quad; + quad[0] = WindowVertex(rect.topLeft(), QPointF(0, 0)); + quad[1] = WindowVertex(rect.topRight(), QPointF(1, 0)); + quad[2] = WindowVertex(rect.bottomRight(), QPointF(1, 1)); + quad[3] = WindowVertex(rect.bottomLeft(), QPointF(0, 1)); + return quad; +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/itemrenderer.cpp b/local/recipes/kde/kwin/source/src/scene/itemrenderer.cpp new file mode 100644 index 0000000000..f409764d98 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/itemrenderer.cpp @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scene/itemrenderer.h" + +namespace KWin +{ + +ItemRenderer::ItemRenderer() +{ +} + +ItemRenderer::~ItemRenderer() +{ +} + +QPainter *ItemRenderer::painter() const +{ + return nullptr; +} + +void ItemRenderer::beginFrame(const RenderTarget &renderTarget, const RenderViewport &viewport) +{ +} + +void ItemRenderer::endFrame() +{ +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/itemrenderer.h b/local/recipes/kde/kwin/source/src/scene/itemrenderer.h new file mode 100644 index 0000000000..c9ffc828d3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/itemrenderer.h @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "core/region.h" + +#include +#include + +class QPainter; + +namespace KWin +{ + +class ImageItem; +class Item; +class RenderTarget; +class RenderViewport; +class Scene; +class WindowPaintData; + +class KWIN_EXPORT ItemRenderer +{ +public: + ItemRenderer(); + virtual ~ItemRenderer(); + + virtual QPainter *painter() const; + + virtual void beginFrame(const RenderTarget &renderTarget, const RenderViewport &viewport); + virtual void endFrame(); + + virtual void renderBackground(const RenderTarget &renderTarget, const RenderViewport &viewport, const Region &deviceRegion) = 0; + virtual void renderItem(const RenderTarget &renderTarget, const RenderViewport &viewport, Item *item, int mask, const Region &deviceRegion, const WindowPaintData &data, const std::function &filter, const std::function &holeFilter) = 0; + + virtual std::unique_ptr createImageItem(Item *parent = nullptr) = 0; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/itemrenderer_opengl.cpp b/local/recipes/kde/kwin/source/src/scene/itemrenderer_opengl.cpp new file mode 100644 index 0000000000..034f3ae9b2 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/itemrenderer_opengl.cpp @@ -0,0 +1,547 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scene/itemrenderer_opengl.h" +#include "core/colorpipeline.h" +#include "core/pixelgrid.h" +#include "core/rendertarget.h" +#include "core/renderviewport.h" +#include "core/syncobjtimeline.h" +#include "effect/effect.h" +#include "opengl/eglnativefence.h" +#include "scene/decorationitem.h" +#include "scene/imageitem.h" +#include "scene/outlinedborderitem.h" +#include "scene/shadowitem.h" +#include "scene/surfaceitem.h" +#include "scene/workspacescene.h" +#include "utils/common.h" + +namespace KWin +{ + +ItemRendererOpenGL::ItemRendererOpenGL(EglDisplay *eglDisplay) + : m_eglDisplay(eglDisplay) +{ + const QString visualizeOptionsString = qEnvironmentVariable("KWIN_SCENE_VISUALIZE"); + if (!visualizeOptionsString.isEmpty()) { + const QStringList visualtizeOptions = visualizeOptionsString.split(';'); + m_debug.fractionalEnabled = visualtizeOptions.contains(QLatin1StringView("fractional")); + } +} + +std::unique_ptr ItemRendererOpenGL::createImageItem(Item *parent) +{ + return std::make_unique(parent); +} + +void ItemRendererOpenGL::beginFrame(const RenderTarget &renderTarget, const RenderViewport &viewport) +{ + GLFramebuffer *fbo = renderTarget.framebuffer(); + GLFramebuffer::pushFramebuffer(fbo); + + GLVertexBuffer::streamingBuffer()->beginFrame(); +} + +void ItemRendererOpenGL::endFrame() +{ + GLVertexBuffer::streamingBuffer()->endOfFrame(); + GLFramebuffer::popFramebuffer(); + + if (m_eglDisplay) { + EGLNativeFence fence(m_eglDisplay); + if (fence.isValid()) { + for (const auto &releasePoint : m_releasePoints) { + releasePoint->addReleaseFence(fence.fileDescriptor()); + } + m_releasePoints.clear(); + } + } + m_releasePoints.clear(); +} + +QVector4D ItemRendererOpenGL::modulate(float opacity, float brightness) const +{ + const float a = opacity; + const float rgb = opacity * brightness; + + return QVector4D(rgb, rgb, rgb, a); +} + +void ItemRendererOpenGL::setBlendEnabled(bool enabled) +{ + if (enabled && !m_blendingEnabled) { + glEnable(GL_BLEND); + } else if (!enabled && m_blendingEnabled) { + glDisable(GL_BLEND); + } + + m_blendingEnabled = enabled; +} + +static RenderGeometry clipQuads(const Item *item, const ItemRendererOpenGL::RenderContext *context) +{ + const WindowQuadList quads = item->quads(); + + const qreal scale = context->renderTargetScale; + const QPointF itemToDeviceTranslation = context->transformStack.top().map(QPointF(0., 0.)) + - context->viewportOrigin + + context->renderOffset; + + RenderGeometry geometry; + geometry.reserve(quads.count() * 6); + + // split all quads in bounding rect with the actual rects in the region + for (const WindowQuad &quad : std::as_const(quads)) { + if (context->deviceClip != Region::infinite() && !context->hardwareClipping) { + // Scale to device coordinates, rounding as needed. + const RectF deviceBounds = snapToPixelGridF(scaledRect(quad.bounds(), scale)); + + for (const Rect &deviceClipRect : context->deviceClip.rects()) { + const RectF relativeDeviceClipRect = RectF(deviceClipRect).translated(-itemToDeviceTranslation); + const RectF intersected = relativeDeviceClipRect.intersected(deviceBounds); + if (intersected.isValid()) { + if (deviceBounds == intersected) { + // case 1: completely contains, include and do not check other rects + geometry.appendWindowQuad(quad, scale); + break; + } + // case 2: intersection + geometry.appendSubQuad(quad, intersected, scale); + } + } + } else { + geometry.appendWindowQuad(quad, scale); + } + } + + return geometry; +} + +void ItemRendererOpenGL::createRenderNode(Item *item, RenderContext *context, const std::function &filter, const std::function &holeFilter) +{ + bool hole = false; + if (filter && filter(item)) { + if (!holeFilter || !holeFilter(item)) { + return; + } + hole = true; + } + const QList sortedChildItems = item->sortedChildItems(); + + const auto logicalPosition = QVector2D(item->position().x(), item->position().y()); + const auto scale = context->renderTargetScale; + + QMatrix4x4 matrix; + matrix.translate(roundVector(logicalPosition * scale).toVector3D()); + if (context->transformStack.size() == 1) { + matrix *= context->rootTransform; + } + if (!item->transform().isIdentity()) { + matrix.scale(scale, scale); + matrix *= item->transform(); + matrix.scale(1 / scale, 1 / scale); + } + context->transformStack.push(context->transformStack.top() * matrix); + + context->opacityStack.push(context->opacityStack.top() * item->opacity()); + + for (Item *childItem : sortedChildItems) { + if (childItem->z() >= 0) { + break; + } + if (childItem->explicitVisible()) { + createRenderNode(childItem, context, filter, holeFilter); + } + } + + if (const BorderRadius radius = item->borderRadius(); !radius.isNull()) { + const RectF nativeRect = snapToPixelGridF(scaledRect(item->rect(), context->renderTargetScale)); + const BorderRadius nativeRadius = radius.scaled(context->renderTargetScale).rounded(); + context->cornerStack.push({ + .box = nativeRect, + .radius = nativeRadius, + }); + } else if (!context->cornerStack.isEmpty()) { + const auto &top = std::as_const(context->cornerStack).top(); + context->cornerStack.push({ + .box = matrix.inverted().mapRect(top.box), + .radius = top.radius, + }); + } + + item->preprocess(); + + RenderGeometry geometry = clipQuads(item, context); + + if (auto shadowItem = qobject_cast(item)) { + if (!geometry.isEmpty()) { + OpenGLShadowTextureProvider *textureProvider = static_cast(shadowItem->textureProvider()); + if (textureProvider->shadowTexture()) { + RenderNode &renderNode = context->renderNodes.emplace_back(RenderNode{ + .traits = ShaderTrait::MapTexture, + .textures = {textureProvider->shadowTexture()}, + .geometry = geometry, + .transformMatrix = context->transformStack.top(), + .opacity = context->opacityStack.top(), + .hasAlpha = true, + .colorDescription = item->colorDescription(), + .renderingIntent = item->renderingIntent(), + .bufferReleasePoint = nullptr, + .paintHole = hole, + }); + renderNode.geometry.postProcessTextureCoordinates(textureProvider->shadowTexture()->matrix(UnnormalizedCoordinates)); + } + } + } else if (auto decorationItem = qobject_cast(item)) { + if (!geometry.isEmpty()) { + auto renderer = static_cast(decorationItem->renderer()); + if (renderer->texture()) { + RenderNode &renderNode = context->renderNodes.emplace_back(RenderNode{ + .traits = ShaderTrait::MapTexture, + .textures = {renderer->texture()}, + .geometry = geometry, + .transformMatrix = context->transformStack.top(), + .opacity = context->opacityStack.top(), + .hasAlpha = true, + .colorDescription = item->colorDescription(), + .renderingIntent = item->renderingIntent(), + .bufferReleasePoint = nullptr, + .paintHole = hole, + }); + renderNode.geometry.postProcessTextureCoordinates(renderer->texture()->matrix(UnnormalizedCoordinates)); + } + } + } else if (auto surfaceItem = qobject_cast(item)) { + auto texture = static_cast(surfaceItem->texture()); + if (texture && texture->isValid()) { + if (!geometry.isEmpty()) { + RenderNode &renderNode = context->renderNodes.emplace_back(RenderNode{ + .traits = texture->texture().planes.count() == 1 ? ShaderTrait::MapTexture : ShaderTrait::MapMultiPlaneTexture, + .textures = texture->texture().toVarLengthArray(), + .geometry = geometry, + .transformMatrix = context->transformStack.top(), + .opacity = context->opacityStack.top(), + .hasAlpha = surfaceItem->hasAlphaChannel(), + .colorDescription = item->colorDescription(), + .renderingIntent = item->renderingIntent(), + .bufferReleasePoint = surfaceItem->bufferReleasePoint(), + .paintHole = hole, + .hasFloatingPointColor = texture->isFloatingPoint(), + }); + renderNode.geometry.postProcessTextureCoordinates(texture->texture().planes.at(0)->matrix(UnnormalizedCoordinates)); + if (surfaceItem->colorDescription()->yuvCoefficients() != YUVMatrixCoefficients::Identity) { + renderNode.traits |= ShaderTrait::YuvConversion; + } + + if (!context->cornerStack.isEmpty()) { + const auto &top = context->cornerStack.top(); + + renderNode.traits |= ShaderTrait::RoundedCorners; + renderNode.hasAlpha = true; + renderNode.box = QVector4D(top.box.x() + top.box.width() * 0.5, + top.box.y() + top.box.height() * 0.5, + top.box.width() * 0.5, + top.box.height() * 0.5), + renderNode.borderRadius = top.radius.toVector(); + } + } + } + } else if (auto imageItem = qobject_cast(item)) { + if (!geometry.isEmpty()) { + if (imageItem->texture()) { + RenderNode &renderNode = context->renderNodes.emplace_back(RenderNode{ + .traits = ShaderTrait::MapTexture, + .textures = {imageItem->texture()}, + .geometry = geometry, + .transformMatrix = context->transformStack.top(), + .opacity = context->opacityStack.top(), + .hasAlpha = imageItem->image().hasAlphaChannel(), + .colorDescription = item->colorDescription(), + .renderingIntent = item->renderingIntent(), + .bufferReleasePoint = nullptr, + .paintHole = hole, + }); + renderNode.geometry.postProcessTextureCoordinates(imageItem->texture()->matrix(UnnormalizedCoordinates)); + } + } + } else if (auto borderItem = qobject_cast(item)) { + if (!geometry.isEmpty()) { + const BorderOutline outline = borderItem->outline(); + const int thickness = std::round(outline.thickness() * context->renderTargetScale); + const RectF outerRect = snapToPixelGridF(scaledRect(borderItem->rect(), context->renderTargetScale)); + const RectF innerRect = outerRect.adjusted(thickness, thickness, -thickness, -thickness); + context->renderNodes.append(RenderNode{ + .traits = ShaderTrait::Border, + .geometry = geometry, + .transformMatrix = context->transformStack.top(), + .opacity = context->opacityStack.top(), + .hasAlpha = true, + .colorDescription = borderItem->colorDescription(), + .renderingIntent = borderItem->renderingIntent(), + .box = QVector4D(innerRect.x() + innerRect.width() * 0.5, + innerRect.y() + innerRect.height() * 0.5, + innerRect.width() * 0.5, + innerRect.height() * 0.5), + .borderRadius = outline.radius().scaled(context->renderTargetScale).rounded().toVector(), + .borderThickness = thickness, + .borderColor = outline.color(), + .paintHole = hole, + }); + } + } + + for (Item *childItem : sortedChildItems) { + if (childItem->z() < 0) { + continue; + } + if (childItem->explicitVisible()) { + createRenderNode(childItem, context, filter, holeFilter); + } + } + + context->transformStack.pop(); + context->opacityStack.pop(); + if (!context->cornerStack.isEmpty()) { + context->cornerStack.pop(); + } +} + +void ItemRendererOpenGL::renderBackground(const RenderTarget &renderTarget, const RenderViewport &viewport, const Region &deviceRegion) +{ + const auto clipped = deviceRegion & renderTarget.transformedRect(); + if (clipped == renderTarget.transformedRect()) { + glClearColor(0, 0, 0, 0); + glClear(GL_COLOR_BUFFER_BIT); + } else if (!clipped.isEmpty()) { + glClearColor(0, 0, 0, 0); + glEnable(GL_SCISSOR_TEST); + + const auto targetSize = renderTarget.size(); + for (const Rect &deviceRect : clipped.rects()) { + const auto bufferRect = viewport.transform().map(deviceRect, renderTarget.transformedSize()); + glScissor(bufferRect.x(), targetSize.height() - (bufferRect.y() + bufferRect.height()), bufferRect.width(), bufferRect.height()); + glClear(GL_COLOR_BUFFER_BIT); + } + + glDisable(GL_SCISSOR_TEST); + } +} + +void ItemRendererOpenGL::renderItem(const RenderTarget &renderTarget, const RenderViewport &viewport, Item *item, int mask, const Region &deviceRegion, const WindowPaintData &data, const std::function &filter, const std::function &holeFilter) +{ + if (deviceRegion.isEmpty()) { + return; + } + + RenderContext renderContext{ + .projectionMatrix = viewport.projectionMatrix(), + .rootTransform = data.toMatrix(viewport.scale()), // TODO: unify transforms + .deviceClip = (deviceRegion & renderTarget.transformedRect()), + .hardwareClipping = (deviceRegion != Region::infinite() && ((mask & Scene::PAINT_WINDOW_TRANSFORMED) || (mask & Scene::PAINT_SCREEN_TRANSFORMED))) || !viewport.renderOffset().isNull(), + .renderTargetScale = viewport.scale(), + .viewportOrigin = viewport.scaledRenderRect().topLeft(), + .renderOffset = viewport.renderOffset(), + }; + + renderContext.transformStack.push(QMatrix4x4()); + renderContext.opacityStack.push(data.opacity()); + + createRenderNode(item, &renderContext, filter, holeFilter); + + int totalVertexCount = 0; + for (const RenderNode &node : std::as_const(renderContext.renderNodes)) { + totalVertexCount += node.geometry.count(); + } + if (totalVertexCount == 0) { + return; + } + + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + vbo->reset(); + vbo->setAttribLayout(std::span(GLVertexBuffer::GLVertex2DLayout), sizeof(GLVertex2D)); + + const auto map = vbo->map(totalVertexCount); + if (!map) { + return; + } + + for (int i = 0, v = 0; i < renderContext.renderNodes.count(); i++) { + RenderNode &renderNode = renderContext.renderNodes[i]; + renderNode.firstVertex = v; + renderNode.vertexCount = renderNode.geometry.count(); + renderNode.geometry.copy(map->subspan(v)); + v += renderNode.geometry.count(); + } + + vbo->unmap(); + vbo->bindArrays(); + + if (renderContext.hardwareClipping) { + glEnable(GL_SCISSOR_TEST); + } + + // Make sure the blend function is set up correctly in case we will be doing blending + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + // The scissor region must be in the render target local coordinate system. + const QSize bufferOffset = renderTarget.transform().map(QSize(viewport.renderOffset().x(), viewport.renderOffset().y())); + Region scissorRegion = Rect(QPoint(bufferOffset.width(), bufferOffset.height()), renderTarget.size() - 2 * bufferOffset); + if (renderContext.hardwareClipping) { + scissorRegion &= viewport.transform().map(deviceRegion & renderTarget.transformedRect(), renderTarget.transformedSize()); + } + + ShaderTraits lastTraits; + GLShader *shader = nullptr; + for (int i = 0; i < renderContext.renderNodes.count(); i++) { + const RenderNode &renderNode = renderContext.renderNodes[i]; + + ShaderTraits traits = renderNode.traits; + if (renderNode.opacity != 1.0 || data.brightness() != 1.0) { + traits |= ShaderTrait::Modulate; + } + if (data.saturation() != 1.0) { + traits |= ShaderTrait::AdjustSaturation; + } + if (data.brightness() != 1.0 || data.saturation() != 1.0) { + // make sure that brightness and saturation adjustments are always applied in linear space + traits |= ShaderTrait::TransformColorspace; + } else { + const auto colorTransformation = ColorPipeline::create(renderNode.colorDescription, renderTarget.colorDescription(), renderNode.renderingIntent, + renderNode.hasFloatingPointColor ? ColorPipeline::InputType::FloatingPoint : ColorPipeline::InputType::FixedPoint); + if (!colorTransformation.isIdentity()) { + traits |= ShaderTrait::TransformColorspace; + } + } + + if (renderNode.paintHole) { + traits = (traits & ShaderTrait::RoundedCorners) | ShaderTrait::UniformColor; + glBlendFunc(GL_ONE_MINUS_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + setBlendEnabled(true); + } else { + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + setBlendEnabled(renderNode.hasAlpha || renderNode.opacity < 1.0); + } + + if (!shader || traits != lastTraits) { + lastTraits = traits; + if (shader) { + ShaderManager::instance()->popShader(); + } + shader = ShaderManager::instance()->pushShader(traits); + if (traits & ShaderTrait::AdjustSaturation) { + const auto toXYZ = renderTarget.colorDescription()->containerColorimetry().toXYZ(); + shader->setUniform(GLShader::FloatUniform::Saturation, data.saturation()); + shader->setUniform(GLShader::Vec3Uniform::PrimaryBrightness, QVector3D(toXYZ(1, 0), toXYZ(1, 1), toXYZ(1, 2))); + } + + if (traits & ShaderTrait::MapTexture) { + shader->setUniform(GLShader::IntUniform::Sampler, 0); + } else if (traits & ShaderTrait::MapMultiPlaneTexture) { + shader->setUniform(GLShader::IntUniform::Sampler, 0); + shader->setUniform(GLShader::IntUniform::Sampler1, 1); + } + } + shader->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, renderContext.projectionMatrix * renderNode.transformMatrix); + if (traits & ShaderTrait::Modulate) { + shader->setUniform(GLShader::Vec4Uniform::ModulationConstant, modulate(renderNode.opacity, data.brightness())); + } + if (traits & ShaderTrait::TransformColorspace) { + shader->setColorspaceUniforms(renderNode.colorDescription, renderTarget.colorDescription(), renderNode.renderingIntent); + } + if (traits & ShaderTrait::YuvConversion) { + shader->setUniform(GLShader::Mat4Uniform::YuvToRgb, renderNode.colorDescription->yuvMatrix()); + } + if (traits & ShaderTrait::RoundedCorners) { + shader->setUniform(GLShader::Vec4Uniform::Box, renderNode.box); + shader->setUniform(GLShader::Vec4Uniform::CornerRadius, renderNode.borderRadius); + } + if (traits & ShaderTrait::Border) { + shader->setUniform(GLShader::Vec4Uniform::Box, renderNode.box); + shader->setUniform(GLShader::Vec4Uniform::CornerRadius, renderNode.borderRadius); + shader->setUniform(GLShader::IntUniform::Thickness, renderNode.borderThickness); + shader->setUniform(GLShader::ColorUniform::Color, renderNode.borderColor); + } + if (renderNode.paintHole) { + shader->setUniform(GLShader::ColorUniform::Color, QColor(0, 0, 0, 255)); + } + + for (int i = 0; i < renderNode.textures.count() && !renderNode.paintHole; ++i) { + glActiveTexture(GL_TEXTURE0 + i); + renderNode.textures[i]->bind(); + } + + vbo->draw(scissorRegion, GL_TRIANGLES, renderNode.firstVertex, + renderNode.vertexCount, renderContext.hardwareClipping); + + for (int i = 0; i < renderNode.textures.count() && !renderNode.paintHole; ++i) { + glActiveTexture(GL_TEXTURE0 + i); + renderNode.textures[i]->unbind(); + } + + if (renderNode.bufferReleasePoint) { + m_releasePoints.insert(renderNode.bufferReleasePoint); + } + } + if (shader) { + // some other code assumes texture 0 is active + glActiveTexture(GL_TEXTURE0); + ShaderManager::instance()->popShader(); + } + + if (m_debug.fractionalEnabled) { + visualizeFractional(viewport, scissorRegion, renderContext); + } + + vbo->unbindArrays(); + + setBlendEnabled(false); + + if (renderContext.hardwareClipping) { + glDisable(GL_SCISSOR_TEST); + } +} + +void ItemRendererOpenGL::visualizeFractional(const RenderViewport &viewport, const Region &logicalRegion, const RenderContext &renderContext) +{ + if (!m_debug.fractionalShader) { + m_debug.fractionalShader = ShaderManager::instance()->generateShaderFromFile( + ShaderTrait::MapTexture, + QStringLiteral(":/scene/shaders/debug_fractional.vert"), + QStringLiteral(":/scene/shaders/debug_fractional.frag")); + } + + if (!m_debug.fractionalShader) { + return; + } + + ShaderBinder debugShaderBinder(m_debug.fractionalShader.get()); + m_debug.fractionalShader->setUniform("fractionalPrecision", 0.01f); + + auto screenSize = viewport.renderRect().size() * viewport.scale(); + m_debug.fractionalShader->setUniform("screenSize", QVector2D(float(screenSize.width()), float(screenSize.height()))); + + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + + for (int i = 0; i < renderContext.renderNodes.count(); i++) { + const RenderNode &renderNode = renderContext.renderNodes[i]; + + setBlendEnabled(true); + + QVector2D size; + if (!renderNode.textures.isEmpty()) { + size = QVector2D(renderNode.textures[0]->width(), renderNode.textures[0]->height()); + } + + m_debug.fractionalShader->setUniform("geometrySize", size); + m_debug.fractionalShader->setUniform(GLShader::Mat4Uniform::ModelViewProjectionMatrix, renderContext.projectionMatrix * renderNode.transformMatrix); + + vbo->draw(logicalRegion, GL_TRIANGLES, renderNode.firstVertex, + renderNode.vertexCount, renderContext.hardwareClipping); + } +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/itemrenderer_opengl.h b/local/recipes/kde/kwin/source/src/scene/itemrenderer_opengl.h new file mode 100644 index 0000000000..650c6e4e4e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/itemrenderer_opengl.h @@ -0,0 +1,92 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "opengl/glutils.h" +#include "scene/itemrenderer.h" +#include "scene/surfaceitem.h" + +#include + +namespace KWin +{ + +class EglDisplay; + +class KWIN_EXPORT ItemRendererOpenGL : public ItemRenderer +{ +public: + struct RenderNode + { + ShaderTraits traits; + QVarLengthArray textures; + RenderGeometry geometry; + QMatrix4x4 transformMatrix; + int firstVertex = 0; + int vertexCount = 0; + qreal opacity = 1; + bool hasAlpha = false; + std::shared_ptr colorDescription; + RenderingIntent renderingIntent; + std::shared_ptr bufferReleasePoint; + QVector4D box; + QVector4D borderRadius; + int borderThickness = 0; + QColor borderColor; + bool paintHole = false; + bool hasFloatingPointColor = false; + }; + + struct RenderCorner + { + RectF box; + BorderRadius radius; + }; + + struct RenderContext + { + QList renderNodes; + QStack transformStack; + QStack opacityStack; + QStack cornerStack; + const QMatrix4x4 projectionMatrix; + const QMatrix4x4 rootTransform; + const Region deviceClip; + const bool hardwareClipping; + const qreal renderTargetScale; + const QPointF viewportOrigin; + const QPoint renderOffset; + }; + + ItemRendererOpenGL(EglDisplay *eglDisplay); + + void beginFrame(const RenderTarget &renderTarget, const RenderViewport &viewport) override; + void endFrame() override; + + void renderBackground(const RenderTarget &renderTarget, const RenderViewport &viewport, const Region &deviceRegion) override; + void renderItem(const RenderTarget &renderTarget, const RenderViewport &viewport, Item *item, int mask, const Region &deviceRegion, const WindowPaintData &data, const std::function &filter, const std::function &holeFilter) override; + + std::unique_ptr createImageItem(Item *parent = nullptr) override; + +private: + QVector4D modulate(float opacity, float brightness) const; + void setBlendEnabled(bool enabled); + void createRenderNode(Item *item, RenderContext *context, const std::function &filter, const std::function &holeFilter); + void visualizeFractional(const RenderViewport &viewport, const Region &logicalRegion, const RenderContext &renderContext); + + bool m_blendingEnabled = false; + EglDisplay *const m_eglDisplay; + std::unordered_set> m_releasePoints; + + struct + { + bool fractionalEnabled = false; + std::unique_ptr fractionalShader; + } m_debug; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/itemrenderer_qpainter.cpp b/local/recipes/kde/kwin/source/src/scene/itemrenderer_qpainter.cpp new file mode 100644 index 0000000000..0d2e0365e3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/itemrenderer_qpainter.cpp @@ -0,0 +1,213 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scene/itemrenderer_qpainter.h" +#include "core/rendertarget.h" +#include "core/renderviewport.h" +#include "effect/effect.h" +#include "scene/decorationitem.h" +#include "scene/imageitem.h" +#include "scene/surfaceitem.h" +#include "scene/workspacescene.h" +#include "window.h" + +#include + +namespace KWin +{ + +ItemRendererQPainter::ItemRendererQPainter() + : m_painter(std::make_unique()) +{ +} + +ItemRendererQPainter::~ItemRendererQPainter() +{ +} + +std::unique_ptr ItemRendererQPainter::createImageItem(Item *parent) +{ + return std::make_unique(parent); +} + +QPainter *ItemRendererQPainter::painter() const +{ + return m_painter.get(); +} + +void ItemRendererQPainter::beginFrame(const RenderTarget &renderTarget, const RenderViewport &viewport) +{ + QImage *buffer = renderTarget.image(); + m_painter->begin(buffer); + m_painter->setWindow(viewport.renderRect().toRect()); +} + +void ItemRendererQPainter::endFrame() +{ + m_painter->end(); +} + +void ItemRendererQPainter::renderBackground(const RenderTarget &renderTarget, const RenderViewport &viewport, const Region &deviceRegion) +{ + m_painter->setCompositionMode(QPainter::CompositionMode_Source); + const Region clipped = deviceRegion & renderTarget.transformedRect(); + for (const Rect &rect : clipped.rects()) { + m_painter->fillRect(viewport.mapFromDeviceCoordinates(rect), Qt::transparent); + } + m_painter->setCompositionMode(QPainter::CompositionMode_SourceOver); +} + +void ItemRendererQPainter::renderItem(const RenderTarget &renderTarget, const RenderViewport &viewport, Item *item, int mask, const Region &deviceRegion, const WindowPaintData &data, const std::function &filter, const std::function &holeFilter) +{ + Region effectiveRegion = deviceRegion; + + if (!(mask & (Scene::PAINT_WINDOW_TRANSFORMED | Scene::PAINT_SCREEN_TRANSFORMED))) { + const Rect boundingRect = viewport.mapToRenderTarget(item->mapToScene(item->boundingRect())).roundedOut(); + effectiveRegion &= boundingRect; + } + + if (effectiveRegion.isEmpty()) { + return; + } + + m_painter->save(); + m_painter->setClipRegion(QRegion(viewport.mapFromDeviceCoordinatesAligned(effectiveRegion))); + m_painter->setClipping(true); + m_painter->setOpacity(data.opacity()); + + if (mask & Scene::PAINT_WINDOW_TRANSFORMED) { + m_painter->translate(data.xTranslation(), data.yTranslation()); + m_painter->scale(data.xScale(), data.yScale()); + } + + renderItem(m_painter.get(), item, filter); + + m_painter->restore(); +} + +void ItemRendererQPainter::renderItem(QPainter *painter, Item *item, const std::function &filter) const +{ + if (filter && filter(item)) { + return; + } + const QList sortedChildItems = item->sortedChildItems(); + + painter->save(); + painter->translate(item->position()); + painter->setOpacity(painter->opacity() * item->opacity()); + + for (Item *childItem : sortedChildItems) { + if (childItem->z() >= 0) { + break; + } + if (childItem->explicitVisible()) { + renderItem(painter, childItem, filter); + } + } + + item->preprocess(); + if (auto surfaceItem = qobject_cast(item)) { + renderSurfaceItem(painter, surfaceItem); + } else if (auto decorationItem = qobject_cast(item)) { + renderDecorationItem(painter, decorationItem); + } else if (auto imageItem = qobject_cast(item)) { + renderImageItem(painter, imageItem); + } + + for (Item *childItem : sortedChildItems) { + if (childItem->z() < 0) { + continue; + } + if (childItem->explicitVisible()) { + renderItem(painter, childItem, filter); + } + } + + painter->restore(); +} + +void ItemRendererQPainter::renderSurfaceItem(QPainter *painter, SurfaceItem *surfaceItem) const +{ + const auto surfaceTexture = static_cast(surfaceItem->texture()); + if (!surfaceTexture || !surfaceTexture->isValid()) { + return; + } + + const OutputTransform surfaceToBufferTransform = surfaceItem->bufferTransform(); + const QSizeF transformedSize = surfaceToBufferTransform.map(surfaceItem->destinationSize()); + + painter->save(); + switch (surfaceToBufferTransform.kind()) { + case OutputTransform::Normal: + break; + case OutputTransform::Rotate90: + painter->translate(transformedSize.height(), 0); + painter->rotate(90); + break; + case OutputTransform::Rotate180: + painter->translate(transformedSize.width(), transformedSize.height()); + painter->rotate(180); + break; + case OutputTransform::Rotate270: + painter->translate(0, transformedSize.width()); + painter->rotate(270); + break; + case OutputTransform::FlipX: + painter->translate(transformedSize.width(), 0); + painter->scale(-1, 1); + break; + case OutputTransform::FlipX90: + painter->scale(-1, 1); + painter->rotate(90); + break; + case OutputTransform::FlipX180: + painter->translate(0, transformedSize.height()); + painter->scale(-1, 1); + painter->rotate(180); + break; + case OutputTransform::FlipX270: + painter->translate(transformedSize.height(), transformedSize.width()); + painter->scale(-1, 1); + painter->rotate(270); + break; + } + + const RectF sourceBox = surfaceItem->bufferSourceBox(); + const qreal xSourceBoxScale = sourceBox.width() / transformedSize.width(); + const qreal ySourceBoxScale = sourceBox.height() / transformedSize.height(); + + const QList shape = surfaceItem->shape(); + for (const RectF rect : shape) { + const QRectF target = surfaceToBufferTransform.map(rect, surfaceItem->size()); + const QRectF source(sourceBox.x() + target.x() * xSourceBoxScale, + sourceBox.y() + target.y() * ySourceBoxScale, + target.width() * xSourceBoxScale, + target.height() * ySourceBoxScale); + + painter->drawImage(target, surfaceTexture->image(), source); + } + + painter->restore(); +} + +void ItemRendererQPainter::renderDecorationItem(QPainter *painter, DecorationItem *decorationItem) const +{ + const auto renderer = static_cast(decorationItem->renderer()); + RectF dtr, dlr, drr, dbr; + decorationItem->window()->layoutDecorationRects(dlr, dtr, drr, dbr); + + painter->drawImage(dtr, renderer->image(SceneQPainterDecorationRenderer::DecorationPart::Top)); + painter->drawImage(dlr, renderer->image(SceneQPainterDecorationRenderer::DecorationPart::Left)); + painter->drawImage(drr, renderer->image(SceneQPainterDecorationRenderer::DecorationPart::Right)); + painter->drawImage(dbr, renderer->image(SceneQPainterDecorationRenderer::DecorationPart::Bottom)); +} + +void ItemRendererQPainter::renderImageItem(QPainter *painter, ImageItem *imageItem) const +{ + painter->drawImage(imageItem->rect(), imageItem->image()); +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/itemrenderer_qpainter.h b/local/recipes/kde/kwin/source/src/scene/itemrenderer_qpainter.h new file mode 100644 index 0000000000..618c9c1c25 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/itemrenderer_qpainter.h @@ -0,0 +1,44 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "scene/itemrenderer.h" + +class QPainter; + +namespace KWin +{ + +class DecorationItem; +class SurfaceItem; + +class KWIN_EXPORT ItemRendererQPainter : public ItemRenderer +{ +public: + ItemRendererQPainter(); + ~ItemRendererQPainter() override; + + QPainter *painter() const override; + + void beginFrame(const RenderTarget &renderTarget, const RenderViewport &viewport) override; + void endFrame() override; + + void renderBackground(const RenderTarget &renderTarget, const RenderViewport &viewport, const Region &deviceRegion) override; + void renderItem(const RenderTarget &renderTarget, const RenderViewport &viewport, Item *item, int mask, const Region &deviceRegion, const WindowPaintData &data, const std::function &filter, const std::function &holeFilter) override; + + std::unique_ptr createImageItem(Item *parent = nullptr) override; + +private: + void renderSurfaceItem(QPainter *painter, SurfaceItem *surfaceItem) const; + void renderDecorationItem(QPainter *painter, DecorationItem *decorationItem) const; + void renderImageItem(QPainter *painter, ImageItem *imageItem) const; + void renderItem(QPainter *painter, Item *item, const std::function &filter) const; + + std::unique_ptr m_painter; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/rootitem.cpp b/local/recipes/kde/kwin/source/src/scene/rootitem.cpp new file mode 100644 index 0000000000..466a25ce82 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/rootitem.cpp @@ -0,0 +1,17 @@ +/* + SPDX-FileCopyrightText: 2024 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scene/rootitem.h" + +namespace KWin +{ + +RootItem::RootItem(Scene *scene) +{ + setScene(scene); +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/rootitem.h b/local/recipes/kde/kwin/source/src/scene/rootitem.h new file mode 100644 index 0000000000..43b3cdb90e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/rootitem.h @@ -0,0 +1,25 @@ +/* + SPDX-FileCopyrightText: 2024 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "scene/item.h" + +namespace KWin +{ + +/** + * The RootItem type represents the root item in the scene. + */ +class KWIN_EXPORT RootItem : public Item +{ + Q_OBJECT + +public: + explicit RootItem(Scene *scene); +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/scene.cpp b/local/recipes/kde/kwin/source/src/scene/scene.cpp new file mode 100644 index 0000000000..70dc69a574 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/scene.cpp @@ -0,0 +1,701 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scene/scene.h" +#include "core/backendoutput.h" +#include "core/outputlayer.h" +#include "core/pixelgrid.h" +#include "core/renderviewport.h" +#include "effect/effect.h" +#include "scene/cursoritem.h" +#include "scene/item.h" +#include "scene/itemrenderer.h" +#include "scene/surfaceitem.h" + +namespace KWin +{ + +RenderView::RenderView(LogicalOutput *logicalOutput, BackendOutput *backendOutput, OutputLayer *layer) + : m_logicalOutput(logicalOutput) + , m_backendOutput(backendOutput) + , m_layer(layer) +{ +} + +LogicalOutput *RenderView::logicalOutput() const +{ + return m_logicalOutput; +} + +BackendOutput *RenderView::backendOutput() const +{ + return m_backendOutput; +} + +OutputLayer *RenderView::layer() const +{ + return m_layer; +} + +void RenderView::setLayer(OutputLayer *layer) +{ + m_layer = layer; +} + +void RenderView::addDeviceRepaint(const Region &deviceRegion) +{ + if (!m_layer) { + return; + } + m_layer->addDeviceRepaint(deviceRegion); +} + +void RenderView::scheduleRepaint(Item *item) +{ + if (!m_layer) { + return; + } + m_layer->scheduleRepaint(item); +} + +bool RenderView::canSkipMoveRepaint(Item *item) +{ + return false; +} + +bool RenderView::shouldRenderItem(Item *item) const +{ + return true; +} + +void RenderView::setExclusive(bool enable) +{ +} + +QPointF RenderView::hotspot() const +{ + return QPointF{}; +} + +bool RenderView::isVisible() const +{ + return true; +} + +bool RenderView::shouldRenderHole(Item *item) const +{ + return false; +} + +Rect RenderView::deviceRect() const +{ + return Rect(renderOffset(), deviceSize()); +} + +QSize RenderView::deviceSize() const +{ + return (viewport().size() * scale()).toSize(); +} + +RectF RenderView::mapToDeviceCoordinates(const RectF &logicalGeometry) const +{ + return logicalGeometry.translated(-viewport().topLeft()).scaled(scale()).translated(m_renderOffset); +} + +Rect RenderView::mapToDeviceCoordinatesAligned(const Rect &logicalGeometry) const +{ + return mapToDeviceCoordinates(RectF(logicalGeometry)).roundedOut(); +} + +Rect RenderView::mapToDeviceCoordinatesAligned(const RectF &logicalGeometry) const +{ + return mapToDeviceCoordinates(logicalGeometry).roundedOut(); +} + +Rect RenderView::mapToDeviceCoordinatesContained(const Rect &logicalGeometry) const +{ + const RectF ret = RectF(logicalGeometry).translated(-viewport().topLeft()).scaled(scale()); + return Rect(QPoint(std::ceil(ret.left()), std::ceil(ret.top())), + QPoint(std::floor(ret.right()), std::floor(ret.bottom()))) + .translated(m_renderOffset); +} + +Region RenderView::mapToDeviceCoordinatesAligned(const Region &logicalGeometry) const +{ + Region ret; + for (const Rect &logicalRect : logicalGeometry.rects()) { + ret |= mapToDeviceCoordinatesAligned(logicalRect); + } + return ret; +} + +Region RenderView::mapToDeviceCoordinatesContained(const Region &logicalGeometry) const +{ + Region ret; + for (const Rect &logicalRect : logicalGeometry.rects()) { + ret |= mapToDeviceCoordinatesContained(logicalRect); + } + return ret; +} + +RectF RenderView::mapFromDeviceCoordinates(const RectF &deviceGeometry) const +{ + return deviceGeometry.translated(-m_renderOffset).scaled(1.0 / scale()).translated(viewport().topLeft()); +} + +Rect RenderView::mapFromDeviceCoordinatesAligned(const Rect &deviceGeometry) const +{ + return deviceGeometry.translated(-m_renderOffset).scaled(1.0 / scale()).translated(viewport().topLeft()).toAlignedRect(); +} + +Region RenderView::mapFromDeviceCoordinatesAligned(const Region &deviceGeometry) const +{ + Region ret; + for (const Rect &deviceRect : deviceGeometry.rects()) { + ret |= mapFromDeviceCoordinatesAligned(deviceRect); + } + return ret; +} + +QPoint RenderView::renderOffset() const +{ + return m_renderOffset; +} + +void RenderView::setRenderOffset(const QPoint &offset) +{ + if (m_renderOffset == offset) { + return; + } + addDeviceRepaint(deviceRect()); + m_renderOffset = offset; + addDeviceRepaint(deviceRect()); +} + +SceneView::SceneView(Scene *scene, LogicalOutput *logicalOutput, BackendOutput *backendOutput, OutputLayer *layer) + : RenderView(logicalOutput, backendOutput, layer) + , m_scene(scene) +{ + m_scene->addView(this); +} + +SceneView::~SceneView() +{ + m_scene->removeView(this); +} + +QList SceneView::scanoutCandidates(ssize_t maxCount) const +{ + return m_scene->scanoutCandidates(maxCount); +} + +void SceneView::prePaint() +{ + m_scene->prePaint(this); +} + +Region SceneView::collectDamage() +{ + return m_scene->collectDamage(); +} + +void SceneView::postPaint() +{ + m_scene->postPaint(); +} + +void SceneView::paint(const RenderTarget &renderTarget, const QPoint &deviceOffset, const Region &deviceRegion) +{ + m_scene->paint(renderTarget, deviceOffset, deviceRegion); +} + +double SceneView::desiredHdrHeadroom() const +{ + return m_scene->desiredHdrHeadroom(); +} + +void SceneView::setViewport(const RectF &viewport) +{ + if (viewport == m_viewport) { + return; + } + addDeviceRepaint(deviceRect()); + m_viewport = viewport; + addDeviceRepaint(deviceRect()); +} + +void SceneView::setScale(qreal scale) +{ + if (scale == m_scale) { + return; + } + addDeviceRepaint(deviceRect()); + m_scale = scale; + addDeviceRepaint(deviceRect()); +} + +RectF SceneView::viewport() const +{ + return m_viewport; +} + +qreal SceneView::scale() const +{ + return m_scale; +} + +void SceneView::addExclusiveView(RenderView *view) +{ + m_exclusiveViews.push_back(view); +} + +void SceneView::removeExclusiveView(RenderView *view) +{ + m_exclusiveViews.removeOne(view); + m_underlayViews.removeOne(view); +} + +void SceneView::addUnderlay(RenderView *view) +{ + m_underlayViews.push_back(view); +} + +void SceneView::removeUnderlay(RenderView *view) +{ + m_underlayViews.removeOne(view); +} + +bool SceneView::shouldRenderItem(Item *item) const +{ + return std::ranges::none_of(m_exclusiveViews, [item](RenderView *view) { + return view->shouldRenderItem(item); + }); +} + +bool SceneView::shouldRenderHole(Item *item) const +{ + return std::ranges::any_of(m_underlayViews, [item](RenderView *view) { + return view->shouldRenderItem(item); + }); +} + +Scene *SceneView::scene() const +{ + return m_scene; +} + +void SceneView::addWindowFilter(std::function filter) +{ + m_windowFilters.push_back(filter); +} + +bool SceneView::shouldHideWindow(Window *window) const +{ + return std::ranges::any_of(m_windowFilters, [window](const auto filter) { + return filter(window); + }); +} + +ItemView::ItemView(SceneView *parentView, Item *item, LogicalOutput *logicalOutput, BackendOutput *backendOutput, OutputLayer *layer) + : RenderView(logicalOutput, backendOutput, layer) + , m_parentView(parentView) + , m_item(item) +{ + parentView->scene()->addView(this); +} + +ItemView::~ItemView() +{ + m_parentView->scene()->removeView(this); + if (m_exclusive) { + m_parentView->removeExclusiveView(this); + if (m_item) { + m_item->scheduleSceneRepaint(m_item->rect()); + } + } +} + +qreal ItemView::scale() const +{ + return m_parentView->scale(); +} + +QPointF ItemView::hotspot() const +{ + if (auto cursor = qobject_cast(m_item)) { + return cursor->hotspot(); + } else { + return QPointF{}; + } +} + +RectF ItemView::viewport() const +{ + // TODO make the viewport explicit instead? + if (!m_item) { + return RectF(); + } + return calculateViewport(m_item->rect()); +} + +RectF ItemView::calculateViewport(const RectF &itemRect) const +{ + const RectF snapped = snapToPixels(itemRect, scale()); + const auto recommendedSizes = m_layer ? m_layer->recommendedSizes() : QList{}; + if (!recommendedSizes.empty()) { + const auto bufferSize = scaledRect(itemRect, scale()).size(); + auto bigEnough = recommendedSizes | std::views::filter([bufferSize](const auto &size) { + return size.width() >= bufferSize.width() && size.height() >= bufferSize.height(); + }); + const auto it = std::ranges::min_element(bigEnough, [](const auto &left, const auto &right) { + return left.width() * left.height() < right.width() * right.height(); + }); + if (it != bigEnough.end()) { + const auto logicalSize = QSizeF(*it) / scale(); + return m_item->mapToView(RectF(snapped.topLeft(), logicalSize), this); + } + } + return m_item->mapToView(snapped, this); +} + +bool ItemView::isVisible() const +{ + return m_item->isVisible(); +} + +QList ItemView::scanoutCandidates(ssize_t maxCount) const +{ + if (auto item = dynamic_cast(m_item.get())) { + return {item}; + } else { + return {}; + } +} + +void ItemView::prePaint() +{ +} + +Region ItemView::collectDamage() +{ + return m_item->takeDeviceRepaints(this); +} + +void ItemView::postPaint() +{ +} + +void ItemView::paint(const RenderTarget &renderTarget, const QPoint &deviceOffset, const Region ®ion) +{ + const Region globalRegion = region == Region::infinite() ? Region::infinite() : region.translated(viewport().topLeft().toPoint()); + RenderViewport renderViewport(viewport(), m_logicalOutput->scale(), renderTarget, deviceOffset); + auto renderer = m_item->scene()->renderer(); + renderer->beginFrame(renderTarget, renderViewport); + renderer->renderBackground(renderTarget, renderViewport, globalRegion); + WindowPaintData data; + renderer->renderItem(renderTarget, renderViewport, m_item, 0, globalRegion, data, [this](Item *toRender) { + return toRender != m_item; + }, {}); + renderer->endFrame(); +} + +bool ItemView::shouldRenderItem(Item *item) const +{ + return m_item && item == m_item; +} + +void ItemView::setExclusive(bool enable) +{ + if (m_exclusive == enable) { + return; + } + m_exclusive = enable; + if (enable) { + m_item->scheduleSceneRepaint(m_item->rect()); + // also need to add all the Item's pending repaint regions to the scene, + // otherwise some required repaints may be missing + m_parentView->addDeviceRepaint(m_item->takeDeviceRepaints(m_parentView)); + m_parentView->addExclusiveView(this); + if (m_underlay) { + m_parentView->addUnderlay(this); + } + } else { + m_parentView->removeExclusiveView(this); + m_item->scheduleRepaint(m_item->rect()); + } +} + +void ItemView::setUnderlay(bool underlay) +{ + if (m_underlay == underlay) { + return; + } + m_underlay = underlay; + if (!m_exclusive) { + return; + } + if (m_underlay) { + m_parentView->addUnderlay(this); + } else { + m_parentView->removeUnderlay(this); + } + m_item->scheduleSceneRepaint(m_item->rect()); +} + +bool ItemView::needsRepaint() +{ + return m_item->hasRepaints(this); +} + +bool ItemView::canSkipMoveRepaint(Item *item) +{ + return m_layer && item == m_item; +} + +Item *ItemView::item() const +{ + return m_item; +} + +double ItemView::desiredHdrHeadroom() const +{ + const auto &color = m_item->colorDescription(); + const double max = color->maxHdrLuminance().value_or(color->referenceLuminance()); + return max / color->referenceLuminance(); +} + +ItemTreeView::ItemTreeView(SceneView *parentView, Item *item, LogicalOutput *logicalOutput, BackendOutput *backendOutput, OutputLayer *layer) + : ItemView(parentView, item, logicalOutput, backendOutput, layer) +{ +} + +ItemTreeView::~ItemTreeView() +{ + setExclusive(false); +} + +RectF ItemTreeView::viewport() const +{ + // TODO make the viewport explicit instead? + if (!m_item) { + return RectF(); + } + return calculateViewport(m_item->boundingRect()); +} + +QList ItemTreeView::scanoutCandidates(ssize_t maxCount) const +{ + if (dynamic_cast(m_item.get())) { + const bool visibleChildren = std::ranges::any_of(m_item->childItems(), [](Item *child) { + return child->isVisible(); + }); + if (visibleChildren) { + return {}; + } + return {static_cast(m_item.get())}; + } + return {}; +} + +static void accumulateRepaints(Item *item, ItemTreeView *view, Region *repaints) +{ + *repaints += item->takeDeviceRepaints(view); + + const auto childItems = item->childItems(); + for (Item *childItem : childItems) { + accumulateRepaints(childItem, view, repaints); + } +} + +Region ItemTreeView::collectDamage() +{ + Region ret; + accumulateRepaints(m_item, this, &ret); + // FIXME damage tracking for this layer still has some bugs, this effectively disables it + ret = Region::infinite(); + return ret; +} + +void ItemTreeView::paint(const RenderTarget &renderTarget, const QPoint &deviceOffset, const Region &deviceRegion) +{ + RenderViewport renderViewport(viewport(), m_logicalOutput->scale(), renderTarget, deviceOffset); + auto renderer = m_item->scene()->renderer(); + renderer->beginFrame(renderTarget, renderViewport); + renderer->renderBackground(renderTarget, renderViewport, deviceRegion); + WindowPaintData data; + renderer->renderItem(renderTarget, renderViewport, m_item, 0, deviceRegion, data, {}, {}); + renderer->endFrame(); +} + +bool ItemTreeView::shouldRenderItem(Item *item) const +{ + return item == m_item || m_item->isAncestorOf(item); +} + +static void schedulePendingRepaints(RenderView *view, Item *item) +{ + view->addDeviceRepaint(item->takeDeviceRepaints(view)); + const auto children = item->childItems(); + for (Item *child : children) { + schedulePendingRepaints(view, child); + } +} + +void ItemTreeView::setExclusive(bool enable) +{ + if (m_exclusive == enable) { + return; + } + m_exclusive = enable; + if (enable) { + m_item->scheduleSceneRepaint(m_item->boundingRect()); + // also need to add all the Item's pending repaint regions to the scene, + // otherwise some required repaints may be missing + schedulePendingRepaints(m_parentView, m_item); + m_parentView->addExclusiveView(this); + if (m_underlay) { + m_parentView->addUnderlay(this); + } + } else { + m_parentView->removeExclusiveView(this); + m_item->scheduleRepaint(m_item->boundingRect()); + } +} + +static bool recursiveNeedsRepaint(Item *item, RenderView *view) +{ + if (item->hasRepaints(view)) { + return true; + } + const auto children = item->childItems(); + return std::ranges::any_of(children, [view](Item *childItem) { + return recursiveNeedsRepaint(childItem, view); + }); +} + +bool ItemTreeView::needsRepaint() +{ + return recursiveNeedsRepaint(m_item, this); +} + +bool ItemTreeView::isVisible() const +{ + // Item::isVisible isn't enough here, we only want to render the view + // if there's actual contents + return m_item->hasVisibleContents(); +} + +bool ItemTreeView::canSkipMoveRepaint(Item *item) +{ + // this could be more generic, but it's all we need for now + return m_layer && item == m_item; +} + +static double recursiveMaxHdrHeadroom(Item *item) +{ + const auto &color = item->colorDescription(); + const double max = color->maxHdrLuminance().value_or(color->referenceLuminance()); + double headroom = max / color->referenceLuminance(); + const auto children = item->childItems(); + for (Item *child : children) { + headroom = std::max(headroom, recursiveMaxHdrHeadroom(child)); + } + return headroom; +} + +double ItemTreeView::desiredHdrHeadroom() const +{ + return recursiveMaxHdrHeadroom(m_item); +} + +Scene::Scene(std::unique_ptr &&renderer) + : m_renderer(std::move(renderer)) +{ +} + +Scene::~Scene() +{ +} + +ItemRenderer *Scene::renderer() const +{ + return m_renderer.get(); +} + +void Scene::addRepaintFull() +{ + for (const auto &view : std::as_const(m_views)) { + view->addDeviceRepaint(Region::infinite()); + } +} + +void Scene::addLogicalRepaint(int x, int y, int width, int height) +{ + addLogicalRepaint(Region(x, y, width, height)); +} + +void Scene::addLogicalRepaint(const Region &logicalRegion) +{ + for (const auto &view : std::as_const(m_views)) { + addLogicalRepaint(view, logicalRegion); + } +} + +void Scene::addLogicalRepaint(RenderView *view, const Region &logicalRegion) +{ + Region dirtyRegion = view->mapToDeviceCoordinatesAligned(logicalRegion); + if (!dirtyRegion.isEmpty()) { + view->addDeviceRepaint(dirtyRegion); + } +} + +void Scene::addDeviceRepaint(RenderView *view, const Region &deviceRegion) +{ + view->addDeviceRepaint(deviceRegion); +} + +Region Scene::damage() const +{ + return Region(); +} + +Rect Scene::geometry() const +{ + return m_geometry; +} + +void Scene::setGeometry(const Rect &rect) +{ + if (m_geometry != rect) { + m_geometry = rect; + addRepaintFull(); + } +} + +QList Scene::views() const +{ + return m_views; +} + +void Scene::addView(RenderView *view) +{ + m_views.append(view); +} + +void Scene::removeView(RenderView *view) +{ + m_views.removeOne(view); + Q_EMIT viewRemoved(view); +} + +QList Scene::scanoutCandidates(ssize_t maxCount) const +{ + return {}; +} + +} // namespace KWin + +#include "moc_scene.cpp" diff --git a/local/recipes/kde/kwin/source/src/scene/scene.h b/local/recipes/kde/kwin/source/src/scene/scene.h new file mode 100644 index 0000000000..fc8ea970ad --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/scene.h @@ -0,0 +1,260 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/rendertarget.h" +#include "kwin_export.h" + +#include +#include +#include +#include + +namespace KWin +{ + +class ItemRenderer; +class LogicalOutput; +class Scene; +class OutputLayer; +class OutputFrame; +class Item; +class SurfaceItem; +class Window; + +class KWIN_EXPORT RenderView : public QObject +{ + Q_OBJECT +public: + explicit RenderView(LogicalOutput *logicalOutput, BackendOutput *backendOutput, OutputLayer *layer); + + LogicalOutput *logicalOutput() const; + /** + * may be nullptr. + */ + BackendOutput *backendOutput() const; + OutputLayer *layer() const; + + void setLayer(OutputLayer *layer); + + virtual bool isVisible() const; + virtual QPointF hotspot() const; + virtual RectF viewport() const = 0; + virtual qreal scale() const = 0; + virtual QList scanoutCandidates(ssize_t maxCount) const = 0; + virtual void prePaint() = 0; + virtual Region collectDamage() = 0; + virtual void paint(const RenderTarget &renderTarget, const QPoint &deviceOffset, const Region &logicalRegion) = 0; + virtual void postPaint() = 0; + virtual bool shouldRenderItem(Item *item) const; + virtual bool shouldRenderHole(Item *item) const; + virtual double desiredHdrHeadroom() const = 0; + + /** + * add a repaint in layer-local device coordinates + */ + void addDeviceRepaint(const Region &deviceRegion); + void scheduleRepaint(Item *item); + /** + * @returns true if the layer can be moved with the Item + * and thus no repaint is necessary + */ + virtual bool canSkipMoveRepaint(Item *item); + + virtual void setExclusive(bool enable); + + RectF mapToDeviceCoordinates(const RectF &logicalGeometry) const; + Rect mapToDeviceCoordinatesAligned(const Rect &logicalGeometry) const; + Rect mapToDeviceCoordinatesAligned(const RectF &logicalGeometry) const; + Rect mapToDeviceCoordinatesContained(const Rect &logicalGeometry) const; + Region mapToDeviceCoordinatesAligned(const Region &logicalGeometry) const; + Region mapToDeviceCoordinatesContained(const Region &logicalGeometry) const; + + RectF mapFromDeviceCoordinates(const RectF &deviceGeometry) const; + Rect mapFromDeviceCoordinatesAligned(const Rect &deviceGeometry) const; + Region mapFromDeviceCoordinatesAligned(const Region &deviceGeometry) const; + + /** + * @returns Rect(renderOffset(), deviceSize()) + */ + Rect deviceRect() const; + QSize deviceSize() const; + + QPoint renderOffset() const; + void setRenderOffset(const QPoint &offset); + +protected: + LogicalOutput *m_logicalOutput = nullptr; + BackendOutput *m_backendOutput = nullptr; + OutputLayer *m_layer = nullptr; + QPoint m_renderOffset; +}; + +class KWIN_EXPORT SceneView : public RenderView +{ + Q_OBJECT +public: + explicit SceneView(Scene *scene, LogicalOutput *logicalOutput, BackendOutput *backendOutput, OutputLayer *layer); + ~SceneView() override; + + Scene *scene() const; + RectF viewport() const override; + qreal scale() const override; + + void setViewport(const RectF &viewport); + void setScale(qreal scale); + + QList scanoutCandidates(ssize_t maxCount) const override; + void prePaint() override; + Region collectDamage() override; + void paint(const RenderTarget &renderTarget, const QPoint &deviceOffset, const Region &deviceRegion) override; + void postPaint() override; + double desiredHdrHeadroom() const override; + + void addExclusiveView(RenderView *view); + void removeExclusiveView(RenderView *view); + void addUnderlay(RenderView *view); + void removeUnderlay(RenderView *view); + /** + * @returns whether or not the Item should be rendered for this delegate specifically. + */ + bool shouldRenderItem(Item *item) const override; + bool shouldRenderHole(Item *item) const override; + + void addWindowFilter(std::function filter); + bool shouldHideWindow(Window *window) const; + +private: + Scene *m_scene; + LogicalOutput *m_logicalOutput = nullptr; + OutputLayer *m_layer = nullptr; + RectF m_viewport; + qreal m_scale = 1.0; + QList m_exclusiveViews; + QList m_underlayViews; + QList> m_windowFilters; +}; + +class KWIN_EXPORT ItemView : public RenderView +{ +public: + explicit ItemView(SceneView *parentView, Item *item, LogicalOutput *logicalOutput, BackendOutput *backendOutput, OutputLayer *layer); + ~ItemView() override; + + qreal scale() const override; + QPointF hotspot() const override; + RectF viewport() const override; + bool isVisible() const override; + QList scanoutCandidates(ssize_t maxCount) const override; + void prePaint() override; + Region collectDamage() override; + void postPaint() override; + void paint(const RenderTarget &renderTarget, const QPoint &deviceOffset, const Region &logicalRegion) override; + bool shouldRenderItem(Item *item) const override; + void setExclusive(bool enable) override; + void setUnderlay(bool underlay); + + Item *item() const; + + virtual bool needsRepaint(); + bool canSkipMoveRepaint(Item *item) override; + double desiredHdrHeadroom() const override; + +protected: + RectF calculateViewport(const RectF &itemRect) const; + + SceneView *const m_parentView; + const QPointer m_item; + bool m_exclusive = false; + bool m_underlay = false; +}; + +class KWIN_EXPORT ItemTreeView : public ItemView +{ +public: + explicit ItemTreeView(SceneView *parentView, Item *item, LogicalOutput *logicalOutput, BackendOutput *backendOutput, OutputLayer *layer); + ~ItemTreeView() override; + + RectF viewport() const override; + bool isVisible() const override; + QList scanoutCandidates(ssize_t maxCount) const override; + Region collectDamage() override; + void paint(const RenderTarget &renderTarget, const QPoint &deviceOffset, const Region &logicalRegion) override; + bool shouldRenderItem(Item *item) const override; + void setExclusive(bool enable) override; + bool needsRepaint() override; + bool canSkipMoveRepaint(Item *item) override; + double desiredHdrHeadroom() const override; +}; + +class KWIN_EXPORT Scene : public QObject +{ + Q_OBJECT + +public: + // Flags controlling how painting is done. + enum { + // WindowItem (or at least part of it) will be painted opaque. + PAINT_WINDOW_OPAQUE = 1 << 0, + // WindowItem (or at least part of it) will be painted translucent. + PAINT_WINDOW_TRANSLUCENT = 1 << 1, + // WindowItem will be painted with transformed geometry. + PAINT_WINDOW_TRANSFORMED = 1 << 2, + // Paint only a region of the screen (can be optimized, cannot + // be used together with TRANSFORMED flags). + PAINT_SCREEN_REGION = 1 << 3, + // Whole screen will be painted with transformed geometry. + PAINT_SCREEN_TRANSFORMED = 1 << 4, + // At least one window will be painted with transformed geometry. + PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS = 1 << 5, + // Clear whole background as the very first step, without optimizing it + PAINT_SCREEN_BACKGROUND_FIRST = 1 << 6, + }; + + explicit Scene(std::unique_ptr &&renderer); + ~Scene() override; + + ItemRenderer *renderer() const; + + void addLogicalRepaint(const Region &logicalRegion); + void addLogicalRepaint(RenderView *view, const Region &logicalRegion); + void addDeviceRepaint(RenderView *view, const Region &deviceRegion); + void addLogicalRepaint(int x, int y, int width, int height); + void addRepaintFull(); + virtual Region damage() const; + + Rect geometry() const; + void setGeometry(const Rect &rect); + + QList views() const; + void addView(RenderView *view); + void removeView(RenderView *view); + + virtual QList scanoutCandidates(ssize_t maxCount) const; + struct OverlayCandidates + { + QList overlays; + QList underlays; + }; + virtual OverlayCandidates overlayCandidates(ssize_t maxTotalCount, ssize_t maxOverlayCount, ssize_t maxUnderlayCount) const = 0; + virtual void prePaint(SceneView *view) = 0; + virtual Region collectDamage() = 0; + virtual void paint(const RenderTarget &renderTarget, const QPoint &deviceOffset, const Region &deviceRegion) = 0; + virtual void postPaint() = 0; + virtual void frame(SceneView *delegate, OutputFrame *frame) = 0; + virtual double desiredHdrHeadroom() const = 0; + +Q_SIGNALS: + void viewRemoved(RenderView *delegate); + +protected: + std::unique_ptr m_renderer; + QList m_views; + Rect m_geometry; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/shaders/debug_fractional.frag b/local/recipes/kde/kwin/source/src/scene/shaders/debug_fractional.frag new file mode 100644 index 0000000000..92562eb875 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/shaders/debug_fractional.frag @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2022 Arjen Hiemstra + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#version 130 compatibility + +uniform float fractionalPrecision; +uniform vec2 geometrySize; + +varying vec2 texcoord0; +varying float vertexFractional; + +// paint every time we query textures at non-integer alignments +// it implies we're being upscaled in ways that will cause blurryness +// 2x scaling will go through fine +void main() +{ + const float strength = 0.4; + + // Calculate an error correction value based on the minimum precision we + // want to measure. + float errorCorrection = 1.0 / fractionalPrecision; + + // Determine which exact pixel we are reading from the source texture. + // Texture sampling happens in the middle of a pixel so we need to add 0.5. + vec2 sourcePixel = texcoord0 * geometrySize + 0.5; + // Cancel out any precision artifacts below what we actually want to measure. + sourcePixel = round(sourcePixel * errorCorrection) / errorCorrection; + + // The total error is the sum of the fractional parts of the source pixel. + float error = dot(fract(sourcePixel), vec2(1.0)); + + vec4 fragColor = vec4(0.0); + + if (vertexFractional > 0.5) { + fragColor = mix(fragColor, vec4(0.0, 0.0, 1.0, 1.0), strength); + } + + if (error > fractionalPrecision) { + fragColor = mix(fragColor, vec4(1.0, 0.0, 0.0, 1.0), strength); + } + + gl_FragColor = fragColor; +} diff --git a/local/recipes/kde/kwin/source/src/scene/shaders/debug_fractional.vert b/local/recipes/kde/kwin/source/src/scene/shaders/debug_fractional.vert new file mode 100644 index 0000000000..e9b841f2a3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/shaders/debug_fractional.vert @@ -0,0 +1,43 @@ +/* + SPDX-FileCopyrightText: 2022 Arjen Hiemstra + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +uniform mat4 modelViewProjectionMatrix; +uniform float fractionalPrecision; +uniform vec2 screenSize; +uniform vec2 geometrySize; + +attribute vec4 vertex; +attribute vec4 texcoord; + +varying vec2 texcoord0; +varying float vertexFractional; + +// This shader calculates the fractional component of the vertex position and +// passes 1 to the fragment shader if it is larger than the precision we want to +// measure, or 0 if it is not. The fragment shader can then use that information +// to color the pixel based on that value. 0 or 1 is used instead of something +// like vertex coloring because of vertex interpolation and the fragment shader +// having control over the final appearance. +void main(void) +{ + float errorCorrection = 1.0 / fractionalPrecision; + + gl_Position = modelViewProjectionMatrix * vertex; + + vec2 screenPosition = ((gl_Position.xy / gl_Position.w + vec2(1.0)) / vec2(2.0)) * screenSize; + // Cancel out any floating point errors below what we want to measure. + screenPosition = round(screenPosition * errorCorrection) / errorCorrection; + + // Determine how far off the pixel grid this vertex is. + vec2 error = fract(screenPosition); + + vertexFractional = dot(error, vec2(1.0)) > fractionalPrecision ? 1.0 : 0.0; + + // Correct texture sampling for floating-point error on the vertices. + // This currently assumes UV coordinates are always from 0 to 1 over an + // entire triangle. + texcoord0 = texcoord.xy + (error / geometrySize); +} diff --git a/local/recipes/kde/kwin/source/src/scene/shaders/debug_fractional_core.frag b/local/recipes/kde/kwin/source/src/scene/shaders/debug_fractional_core.frag new file mode 100644 index 0000000000..38ceb5beac --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/shaders/debug_fractional_core.frag @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2022 Arjen Hiemstra + SPDX-FileCopyrightText: 2022 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#version 140 + +uniform float fractionalPrecision; +uniform vec2 geometrySize; + +in vec2 texcoord0; +in float vertexFractional; + +out vec4 fragColor; + +// paint every time we query textures at non-integer alignments +// it implies we're being upscaled in ways that will cause blurryness +// 2x scaling will go through fine +void main() +{ + const float strength = 0.4; + + // Calculate an error correction value based on the minimum precision we + // want to measure. + float errorCorrection = 1.0 / fractionalPrecision; + + // Determine which exact pixel we are reading from the source texture. + // Texture sampling happens in the middle of a pixel so we need to add 0.5. + vec2 sourcePixel = texcoord0 * geometrySize + 0.5; + // Cancel out any precision artifacts below what we actually want to measure. + sourcePixel = round(sourcePixel * errorCorrection) / errorCorrection; + + // The total error is the sum of the fractional parts of the source pixel. + float error = dot(fract(sourcePixel), vec2(1.0)); + + fragColor = vec4(0.0); + + if (vertexFractional > 0.5) { + fragColor = mix(fragColor, vec4(0.0, 0.0, 1.0, 1.0), strength); + } + + if (error > fractionalPrecision) { + fragColor = mix(fragColor, vec4(1.0, 0.0, 0.0, 1.0), strength); + } +} + diff --git a/local/recipes/kde/kwin/source/src/scene/shaders/debug_fractional_core.vert b/local/recipes/kde/kwin/source/src/scene/shaders/debug_fractional_core.vert new file mode 100644 index 0000000000..fca66a6259 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/shaders/debug_fractional_core.vert @@ -0,0 +1,45 @@ +/* + SPDX-FileCopyrightText: 2022 Arjen Hiemstra + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#version 140 + +uniform mat4 modelViewProjectionMatrix; +uniform float fractionalPrecision; +uniform vec2 screenSize; +uniform vec2 geometrySize; + +in vec4 vertex; +in vec4 texcoord; + +out vec2 texcoord0; +out float vertexFractional; + +// This shader calculates the fractional component of the vertex position and +// passes 1 to the fragment shader if it is larger than the precision we want to +// measure, or 0 if it is not. The fragment shader can then use that information +// to color the pixel based on that value. 0 or 1 is used instead of something +// like vertex coloring because of vertex interpolation and the fragment shader +// having control over the final appearance. +void main(void) +{ + float errorCorrection = 1.0 / fractionalPrecision; + + gl_Position = modelViewProjectionMatrix * vertex; + + vec2 screenPosition = ((gl_Position.xy / gl_Position.w + vec2(1.0)) / vec2(2.0)) * screenSize; + // Cancel out any floating point errors below what we want to measure. + screenPosition = round(screenPosition * errorCorrection) / errorCorrection; + + // Determine how far off the pixel grid this vertex is. + vec2 error = fract(screenPosition); + + vertexFractional = dot(error, vec2(1.0)) > fractionalPrecision ? 1.0 : 0.0; + + // Correct texture sampling for floating-point error on the vertices. + // This currently assumes UV coordinates are always from 0 to 1 over an + // entire triangle. + texcoord0 = texcoord.xy + (error / geometrySize); +} diff --git a/local/recipes/kde/kwin/source/src/scene/shadowitem.cpp b/local/recipes/kde/kwin/source/src/scene/shadowitem.cpp new file mode 100644 index 0000000000..8d060e9cda --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/shadowitem.cpp @@ -0,0 +1,512 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scene/shadowitem.h" +#include "compositor.h" +#include "core/renderbackend.h" +#include "opengl/eglcontext.h" +#include "opengl/gltexture.h" +#include "scene/workspacescene.h" +#include "shadow.h" +#include "window.h" + +#include + +namespace KWin +{ + +ShadowTextureProvider::ShadowTextureProvider(Shadow *shadow) + : m_shadow(shadow) +{ +} + +ShadowTextureProvider::~ShadowTextureProvider() +{ +} + +ShadowItem::ShadowItem(Shadow *shadow, Window *window, Item *parent) + : Item(parent) + , m_window(window) + , m_shadow(shadow) +{ + switch (Compositor::self()->backend()->compositingType()) { + case OpenGLCompositing: + m_textureProvider = std::make_unique(shadow); + break; + case QPainterCompositing: + m_textureProvider = std::make_unique(shadow); + break; + default: + Q_UNREACHABLE(); + } + + connect(shadow, &Shadow::offsetChanged, this, &ShadowItem::updateGeometry); + connect(shadow, &Shadow::rectChanged, this, &ShadowItem::updateGeometry); + connect(shadow, &Shadow::textureChanged, this, &ShadowItem::handleTextureChanged); + + updateGeometry(); + handleTextureChanged(); +} + +ShadowItem::~ShadowItem() +{ +} + +Shadow *ShadowItem::shadow() const +{ + return m_shadow; +} + +ShadowTextureProvider *ShadowItem::textureProvider() const +{ + return m_textureProvider.get(); +} + +void ShadowItem::updateGeometry() +{ + const RectF rect = m_shadow->rect() + m_shadow->offset(); + + setPosition(rect.topLeft()); + setSize(rect.size()); + discardQuads(); +} + +void ShadowItem::handleTextureChanged() +{ + scheduleRepaint(rect()); + discardQuads(); + m_textureDirty = true; +} + +static inline void distributeHorizontally(RectF &leftRect, RectF &rightRect) +{ + if (leftRect.right() > rightRect.left()) { + const qreal boundedRight = std::min(leftRect.right(), rightRect.right()); + const qreal boundedLeft = std::max(leftRect.left(), rightRect.left()); + const qreal halfOverlap = (boundedRight - boundedLeft) / 2.0; + leftRect.setRight(boundedRight - halfOverlap); + rightRect.setLeft(boundedLeft + halfOverlap); + } +} + +static inline void distributeVertically(RectF &topRect, RectF &bottomRect) +{ + if (topRect.bottom() > bottomRect.top()) { + const qreal boundedBottom = std::min(topRect.bottom(), bottomRect.bottom()); + const qreal boundedTop = std::max(topRect.top(), bottomRect.top()); + const qreal halfOverlap = (boundedBottom - boundedTop) / 2.0; + topRect.setBottom(boundedBottom - halfOverlap); + bottomRect.setTop(boundedTop + halfOverlap); + } +} + +WindowQuadList ShadowItem::buildQuads() const +{ + // Do not draw shadows if window width or window height is less than 5 px. 5 is an arbitrary choice. + if (!m_window->wantsShadowToBeRendered() || m_window->width() < 5 || m_window->height() < 5) { + return WindowQuadList(); + } + + const QSizeF top(m_shadow->elementSize(Shadow::ShadowElementTop)); + const QSizeF topRight(m_shadow->elementSize(Shadow::ShadowElementTopRight)); + const QSizeF right(m_shadow->elementSize(Shadow::ShadowElementRight)); + const QSizeF bottomRight(m_shadow->elementSize(Shadow::ShadowElementBottomRight)); + const QSizeF bottom(m_shadow->elementSize(Shadow::ShadowElementBottom)); + const QSizeF bottomLeft(m_shadow->elementSize(Shadow::ShadowElementBottomLeft)); + const QSizeF left(m_shadow->elementSize(Shadow::ShadowElementLeft)); + const QSizeF topLeft(m_shadow->elementSize(Shadow::ShadowElementTopLeft)); + + const QMarginsF shadowMargins( + std::max({topLeft.width(), left.width(), bottomLeft.width()}), + std::max({topLeft.height(), top.height(), topRight.height()}), + std::max({topRight.width(), right.width(), bottomRight.width()}), + std::max({bottomRight.height(), bottom.height(), bottomLeft.height()})); + + const RectF outerRect = rect(); + + const int width = shadowMargins.left() + std::max(top.width(), bottom.width()) + shadowMargins.right(); + const int height = shadowMargins.top() + std::max(left.height(), right.height()) + shadowMargins.bottom(); + + RectF topLeftRect; + if (!topLeft.isEmpty()) { + topLeftRect = RectF(outerRect.topLeft(), topLeft); + } else { + topLeftRect = RectF(outerRect.left() + shadowMargins.left(), + outerRect.top() + shadowMargins.top(), 0, 0); + } + + RectF topRightRect; + if (!topRight.isEmpty()) { + topRightRect = RectF(outerRect.right() - topRight.width(), outerRect.top(), + topRight.width(), topRight.height()); + } else { + topRightRect = RectF(outerRect.right() - shadowMargins.right(), + outerRect.top() + shadowMargins.top(), 0, 0); + } + + RectF bottomRightRect; + if (!bottomRight.isEmpty()) { + bottomRightRect = RectF(outerRect.right() - bottomRight.width(), + outerRect.bottom() - bottomRight.height(), + bottomRight.width(), bottomRight.height()); + } else { + bottomRightRect = RectF(outerRect.right() - shadowMargins.right(), + outerRect.bottom() - shadowMargins.bottom(), 0, 0); + } + + RectF bottomLeftRect; + if (!bottomLeft.isEmpty()) { + bottomLeftRect = RectF(outerRect.left(), outerRect.bottom() - bottomLeft.height(), + bottomLeft.width(), bottomLeft.height()); + } else { + bottomLeftRect = RectF(outerRect.left() + shadowMargins.left(), + outerRect.bottom() - shadowMargins.bottom(), 0, 0); + } + + // Re-distribute the corner tiles so no one of them is overlapping with others. + // By doing this, we assume that shadow's corner tiles are symmetric + // and it is OK to not draw top/right/bottom/left tile between corners. + // For example, let's say top-left and top-right tiles are overlapping. + // In that case, the right side of the top-left tile will be shifted to left, + // the left side of the top-right tile will shifted to right, and the top + // tile won't be rendered. + distributeHorizontally(topLeftRect, topRightRect); + distributeHorizontally(bottomLeftRect, bottomRightRect); + distributeVertically(topLeftRect, bottomLeftRect); + distributeVertically(topRightRect, bottomRightRect); + + qreal tx1 = 0.0, + tx2 = 0.0, + ty1 = 0.0, + ty2 = 0.0; + + WindowQuadList quads; + quads.reserve(8); + + if (topLeftRect.isValid()) { + tx1 = 0.0; + ty1 = 0.0; + tx2 = topLeftRect.width(); + ty2 = topLeftRect.height(); + WindowQuad topLeftQuad; + topLeftQuad[0] = WindowVertex(topLeftRect.left(), topLeftRect.top(), tx1, ty1); + topLeftQuad[1] = WindowVertex(topLeftRect.right(), topLeftRect.top(), tx2, ty1); + topLeftQuad[2] = WindowVertex(topLeftRect.right(), topLeftRect.bottom(), tx2, ty2); + topLeftQuad[3] = WindowVertex(topLeftRect.left(), topLeftRect.bottom(), tx1, ty2); + quads.append(topLeftQuad); + } + + if (topRightRect.isValid()) { + tx1 = width - topRightRect.width(); + ty1 = 0.0; + tx2 = width; + ty2 = topRightRect.height(); + WindowQuad topRightQuad; + topRightQuad[0] = WindowVertex(topRightRect.left(), topRightRect.top(), tx1, ty1); + topRightQuad[1] = WindowVertex(topRightRect.right(), topRightRect.top(), tx2, ty1); + topRightQuad[2] = WindowVertex(topRightRect.right(), topRightRect.bottom(), tx2, ty2); + topRightQuad[3] = WindowVertex(topRightRect.left(), topRightRect.bottom(), tx1, ty2); + quads.append(topRightQuad); + } + + if (bottomRightRect.isValid()) { + tx1 = width - bottomRightRect.width(); + tx2 = width; + ty1 = height - bottomRightRect.height(); + ty2 = height; + WindowQuad bottomRightQuad; + bottomRightQuad[0] = WindowVertex(bottomRightRect.left(), bottomRightRect.top(), tx1, ty1); + bottomRightQuad[1] = WindowVertex(bottomRightRect.right(), bottomRightRect.top(), tx2, ty1); + bottomRightQuad[2] = WindowVertex(bottomRightRect.right(), bottomRightRect.bottom(), tx2, ty2); + bottomRightQuad[3] = WindowVertex(bottomRightRect.left(), bottomRightRect.bottom(), tx1, ty2); + quads.append(bottomRightQuad); + } + + if (bottomLeftRect.isValid()) { + tx1 = 0.0; + tx2 = bottomLeftRect.width(); + ty1 = height - bottomLeftRect.height(); + ty2 = height; + WindowQuad bottomLeftQuad; + bottomLeftQuad[0] = WindowVertex(bottomLeftRect.left(), bottomLeftRect.top(), tx1, ty1); + bottomLeftQuad[1] = WindowVertex(bottomLeftRect.right(), bottomLeftRect.top(), tx2, ty1); + bottomLeftQuad[2] = WindowVertex(bottomLeftRect.right(), bottomLeftRect.bottom(), tx2, ty2); + bottomLeftQuad[3] = WindowVertex(bottomLeftRect.left(), bottomLeftRect.bottom(), tx1, ty2); + quads.append(bottomLeftQuad); + } + + RectF topRect(QPointF(topLeftRect.right(), outerRect.top()), + QPointF(topRightRect.left(), outerRect.top() + top.height())); + + RectF rightRect(QPointF(outerRect.right() - right.width(), topRightRect.bottom()), + QPointF(outerRect.right(), bottomRightRect.top())); + + RectF bottomRect(QPointF(bottomLeftRect.right(), outerRect.bottom() - bottom.height()), + QPointF(bottomRightRect.left(), outerRect.bottom())); + + RectF leftRect(QPointF(outerRect.left(), topLeftRect.bottom()), + QPointF(outerRect.left() + left.width(), bottomLeftRect.top())); + + // Re-distribute left/right and top/bottom shadow tiles so they don't + // overlap when the window is too small. Please notice that we don't + // fix overlaps between left/top(left/bottom, right/top, and so on) + // corner tiles because corresponding counter parts won't be valid when + // the window is too small, which means they won't be rendered. + distributeHorizontally(leftRect, rightRect); + distributeVertically(topRect, bottomRect); + + if (topRect.isValid()) { + tx1 = shadowMargins.left(); + ty1 = 0.0; + tx2 = tx1 + top.width(); + ty2 = topRect.height(); + WindowQuad topQuad; + topQuad[0] = WindowVertex(topRect.left(), topRect.top(), tx1, ty1); + topQuad[1] = WindowVertex(topRect.right(), topRect.top(), tx2, ty1); + topQuad[2] = WindowVertex(topRect.right(), topRect.bottom(), tx2, ty2); + topQuad[3] = WindowVertex(topRect.left(), topRect.bottom(), tx1, ty2); + quads.append(topQuad); + } + + if (rightRect.isValid()) { + tx1 = width - rightRect.width(); + ty1 = shadowMargins.top(); + tx2 = width; + ty2 = ty1 + right.height(); + WindowQuad rightQuad; + rightQuad[0] = WindowVertex(rightRect.left(), rightRect.top(), tx1, ty1); + rightQuad[1] = WindowVertex(rightRect.right(), rightRect.top(), tx2, ty1); + rightQuad[2] = WindowVertex(rightRect.right(), rightRect.bottom(), tx2, ty2); + rightQuad[3] = WindowVertex(rightRect.left(), rightRect.bottom(), tx1, ty2); + quads.append(rightQuad); + } + + if (bottomRect.isValid()) { + tx1 = shadowMargins.left(); + ty1 = height - bottomRect.height(); + tx2 = tx1 + bottom.width(); + ty2 = height; + WindowQuad bottomQuad; + bottomQuad[0] = WindowVertex(bottomRect.left(), bottomRect.top(), tx1, ty1); + bottomQuad[1] = WindowVertex(bottomRect.right(), bottomRect.top(), tx2, ty1); + bottomQuad[2] = WindowVertex(bottomRect.right(), bottomRect.bottom(), tx2, ty2); + bottomQuad[3] = WindowVertex(bottomRect.left(), bottomRect.bottom(), tx1, ty2); + quads.append(bottomQuad); + } + + if (leftRect.isValid()) { + tx1 = 0.0; + ty1 = shadowMargins.top(); + tx2 = leftRect.width(); + ty2 = ty1 + left.height(); + WindowQuad leftQuad; + leftQuad[0] = WindowVertex(leftRect.left(), leftRect.top(), tx1, ty1); + leftQuad[1] = WindowVertex(leftRect.right(), leftRect.top(), tx2, ty1); + leftQuad[2] = WindowVertex(leftRect.right(), leftRect.bottom(), tx2, ty2); + leftQuad[3] = WindowVertex(leftRect.left(), leftRect.bottom(), tx1, ty2); + quads.append(leftQuad); + } + + return quads; +} + +void ShadowItem::preprocess() +{ + if (m_textureDirty) { + m_textureDirty = false; + m_textureProvider->update(); + } +} + +class DecorationShadowTextureCache +{ +public: + ~DecorationShadowTextureCache(); + DecorationShadowTextureCache(const DecorationShadowTextureCache &) = delete; + static DecorationShadowTextureCache &instance(); + + void unregister(ShadowTextureProvider *provider); + std::shared_ptr getTexture(ShadowTextureProvider *provider); + +private: + DecorationShadowTextureCache() = default; + struct Data + { + std::shared_ptr texture; + QList providers; + }; + QHash m_cache; +}; + +DecorationShadowTextureCache &DecorationShadowTextureCache::instance() +{ + static DecorationShadowTextureCache s_instance; + return s_instance; +} + +DecorationShadowTextureCache::~DecorationShadowTextureCache() +{ + Q_ASSERT(m_cache.isEmpty()); +} + +void DecorationShadowTextureCache::unregister(ShadowTextureProvider *provider) +{ + auto it = m_cache.begin(); + while (it != m_cache.end()) { + auto &d = it.value(); + // check whether the Vector of Shadows contains our shadow and remove all of them + auto glIt = d.providers.begin(); + while (glIt != d.providers.end()) { + if (*glIt == provider) { + glIt = d.providers.erase(glIt); + } else { + glIt++; + } + } + // if there are no shadows any more we can erase the cache entry + if (d.providers.isEmpty()) { + it = m_cache.erase(it); + } else { + it++; + } + } +} + +std::shared_ptr DecorationShadowTextureCache::getTexture(ShadowTextureProvider *provider) +{ + Shadow *shadow = provider->shadow(); + Q_ASSERT(shadow->hasDecorationShadow()); + unregister(provider); + const auto decoShadow = shadow->decorationShadow().lock(); + Q_ASSERT(decoShadow); + auto it = m_cache.find(decoShadow.get()); + if (it != m_cache.end()) { + Q_ASSERT(!it.value().providers.contains(provider)); + it.value().providers << provider; + return it.value().texture; + } + Data d; + d.providers << provider; + d.texture = GLTexture::upload(shadow->decorationShadowImage()); + if (!d.texture) { + return nullptr; + } + d.texture->setFilter(GL_LINEAR); + d.texture->setWrapMode(GL_CLAMP_TO_EDGE); + m_cache.insert(decoShadow.get(), d); + return d.texture; +} + +OpenGLShadowTextureProvider::OpenGLShadowTextureProvider(Shadow *shadow) + : ShadowTextureProvider(shadow) +{ +} + +OpenGLShadowTextureProvider::~OpenGLShadowTextureProvider() +{ + if (m_texture) { + Compositor::self()->scene()->openglContext()->makeCurrent(); + DecorationShadowTextureCache::instance().unregister(this); + m_texture.reset(); + } +} + +void OpenGLShadowTextureProvider::update() +{ + if (m_shadow->hasDecorationShadow()) { + // simplifies a lot by going directly to + m_texture = DecorationShadowTextureCache::instance().getTexture(this); + return; + } + + const QSize top(m_shadow->shadowElement(Shadow::ShadowElementTop).size()); + const QSize topRight(m_shadow->shadowElement(Shadow::ShadowElementTopRight).size()); + const QSize right(m_shadow->shadowElement(Shadow::ShadowElementRight).size()); + const QSize bottom(m_shadow->shadowElement(Shadow::ShadowElementBottom).size()); + const QSize bottomLeft(m_shadow->shadowElement(Shadow::ShadowElementBottomLeft).size()); + const QSize left(m_shadow->shadowElement(Shadow::ShadowElementLeft).size()); + const QSize topLeft(m_shadow->shadowElement(Shadow::ShadowElementTopLeft).size()); + const QSize bottomRight(m_shadow->shadowElement(Shadow::ShadowElementBottomRight).size()); + + const int width = std::max({topLeft.width(), left.width(), bottomLeft.width()}) + std::max(top.width(), bottom.width()) + std::max({topRight.width(), right.width(), bottomRight.width()}); + const int height = std::max({topLeft.height(), top.height(), topRight.height()}) + std::max(left.height(), right.height()) + std::max({bottomLeft.height(), bottom.height(), bottomRight.height()}); + + if (width == 0 || height == 0) { + return; + } + + QImage image(width, height, QImage::Format_ARGB32); + image.fill(Qt::transparent); + + const int innerRectTop = std::max({topLeft.height(), top.height(), topRight.height()}); + const int innerRectLeft = std::max({topLeft.width(), left.width(), bottomLeft.width()}); + + QPainter p; + p.begin(&image); + + p.drawImage(QRectF(0, 0, topLeft.width(), topLeft.height()), m_shadow->shadowElement(Shadow::ShadowElementTopLeft)); + p.drawImage(QRectF(innerRectLeft, 0, top.width(), top.height()), m_shadow->shadowElement(Shadow::ShadowElementTop)); + p.drawImage(QRectF(width - topRight.width(), 0, topRight.width(), topRight.height()), m_shadow->shadowElement(Shadow::ShadowElementTopRight)); + + p.drawImage(QRectF(0, innerRectTop, left.width(), left.height()), m_shadow->shadowElement(Shadow::ShadowElementLeft)); + p.drawImage(QRectF(width - right.width(), innerRectTop, right.width(), right.height()), m_shadow->shadowElement(Shadow::ShadowElementRight)); + + p.drawImage(QRectF(0, height - bottomLeft.height(), bottomLeft.width(), bottomLeft.height()), m_shadow->shadowElement(Shadow::ShadowElementBottomLeft)); + p.drawImage(QRectF(innerRectLeft, height - bottom.height(), bottom.width(), bottom.height()), m_shadow->shadowElement(Shadow::ShadowElementBottom)); + p.drawImage(QRectF(width - bottomRight.width(), height - bottomRight.height(), bottomRight.width(), bottomRight.height()), m_shadow->shadowElement(Shadow::ShadowElementBottomRight)); + + p.end(); + + // Check if the image is alpha-only in practice, and if so convert it to an 8-bpp format + const auto context = EglContext::currentContext(); + if (!context->isOpenGLES() && context->supportsTextureSwizzle() && context->supportsRGTextures()) { + QImage alphaImage(image.size(), QImage::Format_Alpha8); + bool alphaOnly = true; + + for (ptrdiff_t y = 0; alphaOnly && y < image.height(); y++) { + const uint32_t *const src = reinterpret_cast(image.scanLine(y)); + uint8_t *const dst = reinterpret_cast(alphaImage.scanLine(y)); + + for (ptrdiff_t x = 0; x < image.width(); x++) { + if (src[x] & 0x00ffffff) { + alphaOnly = false; + } + + dst[x] = qAlpha(src[x]); + } + } + + if (alphaOnly) { + image = alphaImage; + } + } + + m_texture = GLTexture::upload(image); + if (!m_texture) { + return; + } + m_texture->setFilter(GL_LINEAR); + m_texture->setWrapMode(GL_CLAMP_TO_EDGE); + + if (m_texture->internalFormat() == GL_R8) { + // Swizzle red to alpha and all other channels to zero + m_texture->bind(); + m_texture->setSwizzle(GL_ZERO, GL_ZERO, GL_ZERO, GL_RED); + } +} + +QPainterShadowTextureProvider::QPainterShadowTextureProvider(Shadow *shadow) + : ShadowTextureProvider(shadow) +{ +} + +void QPainterShadowTextureProvider::update() +{ +} + +} // namespace KWin + +#include "moc_shadowitem.cpp" diff --git a/local/recipes/kde/kwin/source/src/scene/shadowitem.h b/local/recipes/kde/kwin/source/src/scene/shadowitem.h new file mode 100644 index 0000000000..4e03fb9265 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/shadowitem.h @@ -0,0 +1,86 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "scene/item.h" + +namespace KWin +{ + +class GLTexture; +class Shadow; +class Window; + +class KWIN_EXPORT ShadowTextureProvider +{ +public: + explicit ShadowTextureProvider(Shadow *shadow); + virtual ~ShadowTextureProvider(); + + Shadow *shadow() const { return m_shadow; } + + virtual void update() = 0; + +protected: + Shadow *m_shadow; +}; + +class OpenGLShadowTextureProvider : public ShadowTextureProvider +{ +public: + explicit OpenGLShadowTextureProvider(Shadow *shadow); + ~OpenGLShadowTextureProvider() override; + + GLTexture *shadowTexture() + { + return m_texture.get(); + } + + void update() override; + +private: + std::shared_ptr m_texture; +}; + +class QPainterShadowTextureProvider : public ShadowTextureProvider +{ +public: + explicit QPainterShadowTextureProvider(Shadow *shadow); + + void update() override; +}; + +/** + * The ShadowItem class represents a nine-tile patch server-side drop-shadow. + */ +class KWIN_EXPORT ShadowItem : public Item +{ + Q_OBJECT + +public: + explicit ShadowItem(Shadow *shadow, Window *window, Item *parent = nullptr); + ~ShadowItem() override; + + Shadow *shadow() const; + ShadowTextureProvider *textureProvider() const; + +protected: + WindowQuadList buildQuads() const override; + void preprocess() override; + +private Q_SLOTS: + void handleTextureChanged(); + void updateGeometry(); + +private: + Window *m_window; + Shadow *m_shadow = nullptr; + std::unique_ptr m_textureProvider; + bool m_textureDirty = true; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/surfaceitem.cpp b/local/recipes/kde/kwin/source/src/scene/surfaceitem.cpp new file mode 100644 index 0000000000..9e2ec596de --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/surfaceitem.cpp @@ -0,0 +1,582 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scene/surfaceitem.h" +#include "compositor.h" +#include "core/graphicsbufferview.h" +#include "core/pixelgrid.h" +#include "core/renderbackend.h" +#include "opengl/eglbackend.h" +#include "opengl/gltexture.h" +#include "qpainter/qpainterbackend.h" +#include "scene/scene.h" +#include "utils/common.h" + +#include + +#include +#include + +using namespace std::chrono_literals; + +namespace KWin +{ + +SurfaceItem::SurfaceItem(Item *parent) + : Item(parent) +{ +} + +QSizeF SurfaceItem::destinationSize() const +{ + return m_destinationSize; +} + +void SurfaceItem::setDestinationSize(const QSizeF &size) +{ + if (m_destinationSize != size) { + m_destinationSize = size; + setSize(size); + discardQuads(); + } +} + +GraphicsBuffer *SurfaceItem::buffer() const +{ + return m_bufferRef.buffer(); +} + +void SurfaceItem::setBuffer(GraphicsBuffer *buffer) +{ + if (buffer) { + m_bufferRef = buffer; + m_hasAlphaChannel = buffer->hasAlphaChannel(); + setBufferSize(buffer->size()); + } else { + m_bufferRef = nullptr; + m_hasAlphaChannel = false; + setBufferSize(QSize(0, 0)); + } +} + +RectF SurfaceItem::bufferSourceBox() const +{ + return m_bufferSourceBox; +} + +void SurfaceItem::setBufferSourceBox(const RectF &box) +{ + if (m_bufferSourceBox != box) { + m_bufferSourceBox = box; + discardQuads(); + } +} + +OutputTransform SurfaceItem::bufferTransform() const +{ + return m_surfaceToBufferTransform; +} + +void SurfaceItem::setBufferTransform(OutputTransform transform) +{ + if (m_surfaceToBufferTransform != transform) { + m_surfaceToBufferTransform = transform; + m_bufferToSurfaceTransform = transform.inverted(); + discardQuads(); + } +} + +QSize SurfaceItem::bufferSize() const +{ + return m_bufferSize; +} + +void SurfaceItem::setBufferSize(const QSize &size) +{ + if (m_bufferSize != size) { + m_bufferSize = size; + discardQuads(); + } +} + +Region SurfaceItem::mapFromBuffer(const Region ®ion) const +{ + const RectF sourceBox = m_bufferToSurfaceTransform.map(m_bufferSourceBox, m_bufferSize); + const qreal xScale = m_destinationSize.width() / sourceBox.width(); + const qreal yScale = m_destinationSize.height() / sourceBox.height(); + + Region result; + for (RectF rect : region.rects()) { + const RectF r = m_bufferToSurfaceTransform.map(rect, m_bufferSize).translated(-sourceBox.topLeft()); + result += RectF(r.x() * xScale, r.y() * yScale, r.width() * xScale, r.height() * yScale).toAlignedRect(); + } + return result; +} + +static Region expandRegion(const Region ®ion, const QMargins &padding) +{ + if (region.isEmpty()) { + return Region(); + } + + Region ret; + for (const Rect &rect : region.rects()) { + ret += rect.marginsAdded(padding); + } + + return ret; +} + +void SurfaceItem::addDamage(const Region ®ion) +{ + if (m_lastDamage) { + const auto diff = std::chrono::steady_clock::now() - *m_lastDamage; + m_lastDamageTimeDiffs.push_back(diff); + if (m_lastDamageTimeDiffs.size() > 100) { + m_lastDamageTimeDiffs.pop_front(); + } + m_frameTimeEstimation = std::accumulate(m_lastDamageTimeDiffs.begin(), m_lastDamageTimeDiffs.end(), 0ns) / m_lastDamageTimeDiffs.size(); + } + m_lastDamage = std::chrono::steady_clock::now(); + m_damage += region; + + const RectF sourceBox = m_bufferToSurfaceTransform.map(m_bufferSourceBox, m_bufferSize); + const qreal xScale = sourceBox.width() / m_destinationSize.width(); + const qreal yScale = sourceBox.height() / m_destinationSize.height(); + const Region logicalDamage = mapFromBuffer(region); + + const auto views = scene()->views(); + for (RenderView *view : views) { + Region viewDamage = logicalDamage; + const qreal viewScale = view->scale(); + if (xScale != viewScale || yScale != viewScale) { + // Simplified version of ceil(ceil(0.5 * output_scale / surface_scale) / output_scale) + const int xPadding = std::ceil(0.5 / xScale); + const int yPadding = std::ceil(0.5 / yScale); + viewDamage = expandRegion(viewDamage, QMargins(xPadding, yPadding, xPadding, yPadding)); + } + scheduleRepaint(view, viewDamage); + } + + Q_EMIT damaged(); +} + +void SurfaceItem::resetDamage() +{ + m_damage = Region(); +} + +Region SurfaceItem::damage() const +{ + return m_damage; +} + +SurfaceTexture *SurfaceItem::texture() const +{ + return m_texture.get(); +} + +void SurfaceItem::destroyTexture() +{ + m_texture.reset(); +} + +void SurfaceItem::preprocess() +{ + if (!m_texture || m_texture->size() != m_bufferSize) { + if (auto backend = qobject_cast(Compositor::self()->backend())) { + m_texture = std::make_unique(backend, this); + } else if (auto backend = qobject_cast(Compositor::self()->backend())) { + m_texture = std::make_unique(backend, this); + } + } + + if (m_texture->isValid()) { + const Region region = damage(); + if (!region.isEmpty()) { + m_texture->update(region); + resetDamage(); + } + } else { + if (m_texture->create()) { + resetDamage(); + } + } +} + +WindowQuadList SurfaceItem::buildQuads() const +{ + const QList region = shape(); + WindowQuadList quads; + quads.reserve(region.count()); + + const RectF sourceBox = m_bufferToSurfaceTransform.map(m_bufferSourceBox, m_bufferSize); + const qreal xScale = sourceBox.width() / m_destinationSize.width(); + const qreal yScale = sourceBox.height() / m_destinationSize.height(); + + for (const RectF rect : region) { + WindowQuad quad; + + const QPointF bufferTopLeft = snapToPixelGridF(m_bufferSourceBox.topLeft() + m_surfaceToBufferTransform.map(QPointF(rect.left() * xScale, rect.top() * yScale), sourceBox.size())); + const QPointF bufferTopRight = snapToPixelGridF(m_bufferSourceBox.topLeft() + m_surfaceToBufferTransform.map(QPointF(rect.right() * xScale, rect.top() * yScale), sourceBox.size())); + const QPointF bufferBottomRight = snapToPixelGridF(m_bufferSourceBox.topLeft() + m_surfaceToBufferTransform.map(QPointF(rect.right() * xScale, rect.bottom() * yScale), sourceBox.size())); + const QPointF bufferBottomLeft = snapToPixelGridF(m_bufferSourceBox.topLeft() + m_surfaceToBufferTransform.map(QPointF(rect.left() * xScale, rect.bottom() * yScale), sourceBox.size())); + + quad[0] = WindowVertex(rect.topLeft(), bufferTopLeft); + quad[1] = WindowVertex(rect.topRight(), bufferTopRight); + quad[2] = WindowVertex(rect.bottomRight(), bufferBottomRight); + quad[3] = WindowVertex(rect.bottomLeft(), bufferBottomLeft); + + quads << quad; + } + + return quads; +} + +ContentType SurfaceItem::contentType() const +{ + return ContentType::None; +} + +void SurfaceItem::setScanoutHint(DrmDevice *device, const QHash> &drmFormats) +{ +} + +void SurfaceItem::freeze() +{ +} + +std::optional SurfaceItem::recursiveFrameTimeEstimation() const +{ + std::optional ret = frameTimeEstimation(); + const auto children = childItems(); + for (Item *child : children) { + const auto other = static_cast(child)->recursiveFrameTimeEstimation(); + if (!other.has_value()) { + continue; + } + if (ret.has_value()) { + ret = std::min(*ret, *other); + } else { + ret = other; + } + } + return ret; +} + +std::optional SurfaceItem::frameTimeEstimation() const +{ + if (m_lastDamage && std::chrono::steady_clock::now() - *m_lastDamage > std::chrono::milliseconds(100)) { + // the surface seems to have stopped rendering entirely + return std::nullopt; + } else { + return m_frameTimeEstimation; + } +} + +std::shared_ptr SurfaceItem::bufferReleasePoint() const +{ + return m_bufferReleasePoint; +} + +bool SurfaceItem::hasAlphaChannel() const +{ + return m_hasAlphaChannel; +} + +SurfaceTexture::~SurfaceTexture() +{ +} + +QSize SurfaceTexture::size() const +{ + return m_size; +} + +OpenGLSurfaceTexture::OpenGLSurfaceTexture(EglBackend *backend, SurfaceItem *item) + : m_backend(backend) + , m_item(item) +{ +} + +OpenGLSurfaceTexture::~OpenGLSurfaceTexture() +{ + destroy(); +} + +bool OpenGLSurfaceTexture::isValid() const +{ + return m_texture.isValid(); +} + +OpenGLSurfaceContents OpenGLSurfaceTexture::texture() const +{ + return m_texture; +} + +bool OpenGLSurfaceTexture::create() +{ + GraphicsBuffer *buffer = m_item->buffer(); + if (buffer->dmabufAttributes()) { + return loadDmabufTexture(buffer); + } else if (buffer->shmAttributes()) { + return loadShmTexture(buffer); + } else if (buffer->singlePixelAttributes()) { + return loadSinglePixelTexture(buffer); + } else { + qCDebug(KWIN_OPENGL) << "Failed to create OpenGLSurfaceTexture for a buffer of unknown type" << buffer; + return false; + } +} + +void OpenGLSurfaceTexture::destroy() +{ + m_texture.reset(); + m_bufferType = BufferType::None; + m_size = QSize(); +} + +void OpenGLSurfaceTexture::update(const Region ®ion) +{ + GraphicsBuffer *buffer = m_item->buffer(); + if (buffer->dmabufAttributes()) { + updateDmabufTexture(buffer); + } else if (buffer->shmAttributes()) { + updateShmTexture(buffer, region); + } else if (buffer->singlePixelAttributes()) { + updateSinglePixelTexture(buffer); + } else { + qCDebug(KWIN_OPENGL) << "Failed to update OpenGLSurfaceTexture for a buffer of unknown type" << buffer; + } +} + +bool OpenGLSurfaceTexture::isFloatingPoint() const +{ + return m_isFloatingPoint; +} + +bool OpenGLSurfaceTexture::loadShmTexture(GraphicsBuffer *buffer) +{ + const GraphicsBufferView view(buffer); + if (Q_UNLIKELY(view.isNull())) { + return false; + } + + std::shared_ptr texture = GLTexture::upload(*view.image()); + if (Q_UNLIKELY(!texture)) { + return false; + } + + texture->setFilter(GL_LINEAR); + texture->setWrapMode(GL_CLAMP_TO_EDGE); + texture->setContentTransform(OutputTransform::FlipY); + + m_texture = {{texture}}; + + m_bufferType = BufferType::Shm; + m_size = buffer->size(); + const auto info = FormatInfo::get(buffer->shmAttributes()->format); + m_isFloatingPoint = info && info->floatingPoint; + + return true; +} + +static Region simplifyDamage(const Region &damage) +{ + if (damage.rects().size() < 3) { + return damage; + } else { + return damage.boundingRect(); + } +} + +void OpenGLSurfaceTexture::updateShmTexture(GraphicsBuffer *buffer, const Region ®ion) +{ + if (Q_UNLIKELY(m_bufferType != BufferType::Shm)) { + destroy(); + create(); + return; + } + + const GraphicsBufferView view(buffer); + if (Q_UNLIKELY(view.isNull())) { + return; + } + + m_texture.planes[0]->update(*view.image(), simplifyDamage(region)); + const auto info = FormatInfo::get(buffer->shmAttributes()->format); + m_isFloatingPoint = info && info->floatingPoint; +} + +bool OpenGLSurfaceTexture::loadDmabufTexture(GraphicsBuffer *buffer) +{ + auto createTexture = [](EGLImageKHR image, const QSize &size, bool isExternalOnly) -> std::shared_ptr { + if (Q_UNLIKELY(image == EGL_NO_IMAGE_KHR)) { + qCritical(KWIN_OPENGL) << "Invalid dmabuf-based wl_buffer"; + return nullptr; + } + + GLint target = isExternalOnly ? GL_TEXTURE_EXTERNAL_OES : GL_TEXTURE_2D; + auto texture = std::make_shared(target); + texture->setSize(size); + if (!texture->create()) { + return nullptr; + } + texture->setWrapMode(GL_CLAMP_TO_EDGE); + texture->setFilter(GL_LINEAR); + texture->bind(); + glEGLImageTargetTexture2DOES(target, static_cast(image)); + texture->unbind(); + texture->setContentTransform(OutputTransform::FlipY); + return texture; + }; + + const auto attribs = buffer->dmabufAttributes(); + if (auto itConv = s_drmConversions.find(buffer->dmabufAttributes()->format); itConv != s_drmConversions.end()) { + QList> textures; + Q_ASSERT(itConv->plane.count() == uint(buffer->dmabufAttributes()->planeCount)); + + for (uint plane = 0; plane < itConv->plane.count(); ++plane) { + const auto ¤tPlane = itConv->plane[plane]; + QSize size = buffer->size(); + size.rwidth() /= currentPlane.widthDivisor; + size.rheight() /= currentPlane.heightDivisor; + + const bool isExternal = m_backend->eglDisplayObject()->isExternalOnly(currentPlane.format, attribs->modifier); + auto t = createTexture(m_backend->importBufferAsImage(buffer, plane, currentPlane.format, size), size, isExternal); + if (!t) { + return false; + } + textures << t; + } + m_texture = {textures}; + } else { + const bool isExternal = m_backend->eglDisplayObject()->isExternalOnly(attribs->format, attribs->modifier); + auto texture = createTexture(m_backend->importBufferAsImage(buffer), buffer->size(), isExternal); + if (!texture) { + return false; + } + m_texture = {{texture}}; + } + m_bufferType = BufferType::DmaBuf; + m_size = buffer->size(); + const auto info = FormatInfo::get(buffer->dmabufAttributes()->format); + m_isFloatingPoint = info && info->floatingPoint; + + return true; +} + +void OpenGLSurfaceTexture::updateDmabufTexture(GraphicsBuffer *buffer) +{ + if (Q_UNLIKELY(m_bufferType != BufferType::DmaBuf)) { + destroy(); + create(); + return; + } + + const GLint target = GL_TEXTURE_2D; + if (auto itConv = s_drmConversions.find(buffer->dmabufAttributes()->format); itConv != s_drmConversions.end()) { + Q_ASSERT(itConv->plane.count() == uint(buffer->dmabufAttributes()->planeCount)); + for (uint plane = 0; plane < itConv->plane.count(); ++plane) { + const auto ¤tPlane = itConv->plane[plane]; + QSize size = buffer->size(); + size.rwidth() /= currentPlane.widthDivisor; + size.rheight() /= currentPlane.heightDivisor; + + m_texture.planes[plane]->bind(); + glEGLImageTargetTexture2DOES(target, static_cast(m_backend->importBufferAsImage(buffer, plane, currentPlane.format, size))); + m_texture.planes[plane]->unbind(); + } + } else { + Q_ASSERT(m_texture.planes.count() == 1); + m_texture.planes[0]->bind(); + glEGLImageTargetTexture2DOES(target, static_cast(m_backend->importBufferAsImage(buffer))); + m_texture.planes[0]->unbind(); + } + const auto info = FormatInfo::get(buffer->dmabufAttributes()->format); + m_isFloatingPoint = info && info->floatingPoint; +} + +bool OpenGLSurfaceTexture::loadSinglePixelTexture(GraphicsBuffer *buffer) +{ + // TODO this shouldn't allocate a texture, + // the renderer should just use a color in the shader + const GraphicsBufferView view(buffer); + std::shared_ptr texture = GLTexture::upload(*view.image()); + if (Q_UNLIKELY(!texture)) { + return false; + } + m_texture = {{texture}}; + m_bufferType = BufferType::SinglePixel; + m_size = QSize(1, 1); + m_isFloatingPoint = false; + return true; +} + +void OpenGLSurfaceTexture::updateSinglePixelTexture(GraphicsBuffer *buffer) +{ + if (Q_UNLIKELY(m_bufferType != BufferType::SinglePixel)) { + destroy(); + create(); + return; + } + const GraphicsBufferView view(buffer); + m_texture.planes[0]->update(*view.image(), Rect(0, 0, 1, 1)); +} + +QPainterSurfaceTexture::QPainterSurfaceTexture(QPainterBackend *backend, SurfaceItem *item) + : m_backend(backend) + , m_item(item) +{ +} + +bool QPainterSurfaceTexture::create() +{ + const GraphicsBufferView view(m_item->buffer()); + if (Q_LIKELY(!view.isNull())) { + // The buffer data is copied as the buffer interface returns a QImage + // which doesn't own the data of the underlying wl_shm_buffer object. + m_image = view.image()->copy(); + } + m_size = m_image.size(); + return !m_image.isNull(); +} + +void QPainterSurfaceTexture::update(const Region ®ion) +{ + const GraphicsBufferView view(m_item->buffer()); + if (Q_UNLIKELY(view.isNull())) { + return; + } + + QPainter painter(&m_image); + painter.setCompositionMode(QPainter::CompositionMode_Source); + + // The buffer data is copied as the buffer interface returns a QImage + // which doesn't own the data of the underlying wl_shm_buffer object. + for (const Rect &rect : region.rects()) { + painter.drawImage(rect, *view.image(), rect); + } +} + +bool QPainterSurfaceTexture::isValid() const +{ + return !m_image.isNull(); +} + +QPainterBackend *QPainterSurfaceTexture::backend() const +{ + return m_backend; +} + +QImage QPainterSurfaceTexture::image() const +{ + return m_image; +} + +} // namespace KWin + +#include "moc_surfaceitem.cpp" diff --git a/local/recipes/kde/kwin/source/src/scene/surfaceitem.h b/local/recipes/kde/kwin/source/src/scene/surfaceitem.h new file mode 100644 index 0000000000..ac70d88cad --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/surfaceitem.h @@ -0,0 +1,205 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/graphicsbuffer.h" +#include "core/output.h" +#include "scene/item.h" + +#include + +namespace KWin +{ + +class EglBackend; +class GLTexture; +class QPainterBackend; +class SurfaceTexture; +class Window; + +/** + * The SurfaceItem class represents a surface with some contents. + */ +class KWIN_EXPORT SurfaceItem : public Item +{ + Q_OBJECT + +public: + QSizeF destinationSize() const; + void setDestinationSize(const QSizeF &size); + + GraphicsBuffer *buffer() const; + void setBuffer(GraphicsBuffer *buffer); + + RectF bufferSourceBox() const; + void setBufferSourceBox(const RectF &box); + + OutputTransform bufferTransform() const; + void setBufferTransform(OutputTransform transform); + + QSize bufferSize() const; + void setBufferSize(const QSize &size); + + bool hasAlphaChannel() const; + + std::shared_ptr bufferReleasePoint() const; + + Region mapFromBuffer(const Region ®ion) const; + + void addDamage(const Region ®ion); + void resetDamage(); + Region damage() const; + + void destroyTexture(); + + SurfaceTexture *texture() const; + + virtual ContentType contentType() const; + virtual void setScanoutHint(DrmDevice *device, const QHash> &drmFormats); + + virtual void freeze(); + + /** + * like frameTimeEstimation, but takes child items into account + */ + std::optional recursiveFrameTimeEstimation() const; + std::optional frameTimeEstimation() const; + +Q_SIGNALS: + void damaged(); + +protected: + explicit SurfaceItem(Item *parent = nullptr); + + void preprocess() override; + WindowQuadList buildQuads() const override; + + Region m_damage; + OutputTransform m_bufferToSurfaceTransform; + OutputTransform m_surfaceToBufferTransform; + GraphicsBufferRef m_bufferRef; + RectF m_bufferSourceBox; + QSize m_bufferSize; + QSizeF m_destinationSize; + bool m_hasAlphaChannel = false; + std::unique_ptr m_texture; + std::deque m_lastDamageTimeDiffs; + std::optional m_lastDamage; + std::optional m_frameTimeEstimation; + std::shared_ptr m_bufferReleasePoint; +}; + +class KWIN_EXPORT SurfaceTexture +{ +public: + virtual ~SurfaceTexture(); + + virtual bool isValid() const = 0; + + virtual bool create() = 0; + virtual void update(const Region ®ion) = 0; + + // TODO: create()/update() steps are unnecessary now, consider removing size(). + QSize size() const; + +protected: + QSize m_size; +}; + +class KWIN_EXPORT OpenGLSurfaceContents +{ +public: + OpenGLSurfaceContents() + { + } + OpenGLSurfaceContents(const std::shared_ptr &contents) + : planes({contents}) + { + } + OpenGLSurfaceContents(const QList> &planes) + : planes(planes) + { + } + + void reset() + { + planes.clear(); + } + bool isValid() const + { + return !planes.isEmpty(); + } + QVarLengthArray toVarLengthArray() const; + + QList> planes; +}; + +class KWIN_EXPORT OpenGLSurfaceTexture : public SurfaceTexture +{ +public: + explicit OpenGLSurfaceTexture(EglBackend *backend, SurfaceItem *item); + ~OpenGLSurfaceTexture() override; + + bool create() override; + void update(const Region ®ion) override; + bool isValid() const override; + bool isFloatingPoint() const; + + OpenGLSurfaceContents texture() const; + +private: + bool loadShmTexture(GraphicsBuffer *buffer); + void updateShmTexture(GraphicsBuffer *buffer, const Region ®ion); + bool loadDmabufTexture(GraphicsBuffer *buffer); + void updateDmabufTexture(GraphicsBuffer *buffer); + bool loadSinglePixelTexture(GraphicsBuffer *buffer); + void updateSinglePixelTexture(GraphicsBuffer *buffer); + void destroy(); + + enum class BufferType { + None, + Shm, + DmaBuf, + SinglePixel, + }; + + BufferType m_bufferType = BufferType::None; + bool m_isFloatingPoint = false; + EglBackend *m_backend; + SurfaceItem *m_item; + OpenGLSurfaceContents m_texture; +}; + +class KWIN_EXPORT QPainterSurfaceTexture : public SurfaceTexture +{ +public: + QPainterSurfaceTexture(QPainterBackend *backend, SurfaceItem *item); + + bool create() override; + void update(const Region ®ion) override; + bool isValid() const override; + + QPainterBackend *backend() const; + QImage image() const; + +protected: + QPainterBackend *m_backend; + SurfaceItem *m_item; + QImage m_image; +}; + +inline QVarLengthArray OpenGLSurfaceContents::toVarLengthArray() const +{ + Q_ASSERT(planes.size() <= 4); + QVarLengthArray ret; + for (const auto &plane : planes) { + ret << plane.get(); + } + return ret; +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/surfaceitem_internal.cpp b/local/recipes/kde/kwin/source/src/scene/surfaceitem_internal.cpp new file mode 100644 index 0000000000..a2c79b39d4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/surfaceitem_internal.cpp @@ -0,0 +1,48 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scene/surfaceitem_internal.h" +#include "internalwindow.h" + +namespace KWin +{ + +SurfaceItemInternal::SurfaceItemInternal(InternalWindow *window, Item *parent) + : SurfaceItem(parent) + , m_window(window) +{ + connect(window, &InternalWindow::presented, + this, &SurfaceItemInternal::handlePresented); + + setDestinationSize(window->bufferGeometry().size()); + setBuffer(m_window->graphicsBuffer()); + setBufferSourceBox(RectF(QPointF(0, 0), window->bufferGeometry().size() * window->bufferScale())); + setBufferTransform(m_window->bufferTransform()); +} + +InternalWindow *SurfaceItemInternal::window() const +{ + return m_window; +} + +QList SurfaceItemInternal::shape() const +{ + return {rect()}; +} + +void SurfaceItemInternal::handlePresented(const InternalWindowFrame &frame) +{ + setDestinationSize(m_window->bufferGeometry().size()); + setBuffer(frame.buffer); + setBufferSourceBox(RectF(QPointF(0, 0), frame.buffer->size())); + setBufferTransform(frame.bufferTransform); + + addDamage(frame.bufferDamage); +} + +} // namespace KWin + +#include "moc_surfaceitem_internal.cpp" diff --git a/local/recipes/kde/kwin/source/src/scene/surfaceitem_internal.h b/local/recipes/kde/kwin/source/src/scene/surfaceitem_internal.h new file mode 100644 index 0000000000..0ee2634de4 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/surfaceitem_internal.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "scene/surfaceitem.h" + +namespace KWin +{ + +class InternalWindow; +struct InternalWindowFrame; + +/** + * The SurfaceItemInternal class represents an internal surface in the scene. + */ +class KWIN_EXPORT SurfaceItemInternal : public SurfaceItem +{ + Q_OBJECT + +public: + explicit SurfaceItemInternal(InternalWindow *window, Item *parent = nullptr); + + InternalWindow *window() const; + + QList shape() const override; + +private Q_SLOTS: + void handlePresented(const InternalWindowFrame &frame); + +private: + InternalWindow *m_window; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/surfaceitem_wayland.cpp b/local/recipes/kde/kwin/source/src/scene/surfaceitem_wayland.cpp new file mode 100644 index 0000000000..572a374f5a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/surfaceitem_wayland.cpp @@ -0,0 +1,316 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scene/surfaceitem_wayland.h" +#include "core/backendoutput.h" +#include "core/drmdevice.h" +#include "core/renderbackend.h" +#include "wayland/linuxdmabufv1clientbuffer.h" +#include "wayland/subcompositor.h" +#include "wayland/surface.h" +#include "window.h" + +#if KWIN_BUILD_X11 +#include "x11window.h" +#endif + +namespace KWin +{ + +SurfaceItemWayland::SurfaceItemWayland(SurfaceInterface *surface, Item *parent) + : SurfaceItem(parent) + , m_surface(surface) +{ + connect(surface, &SurfaceInterface::sizeChanged, + this, &SurfaceItemWayland::handleSurfaceSizeChanged); + connect(surface, &SurfaceInterface::bufferChanged, + this, &SurfaceItemWayland::handleBufferChanged); + connect(surface, &SurfaceInterface::bufferSourceBoxChanged, + this, &SurfaceItemWayland::handleBufferSourceBoxChanged); + connect(surface, &SurfaceInterface::bufferTransformChanged, + this, &SurfaceItemWayland::handleBufferTransformChanged); + + connect(surface, &SurfaceInterface::childSubSurfacesChanged, + this, &SurfaceItemWayland::handleChildSubSurfacesChanged); + connect(surface, &SurfaceInterface::committed, + this, &SurfaceItemWayland::handleSurfaceCommitted); + connect(surface, &SurfaceInterface::damaged, + this, &SurfaceItemWayland::addDamage); + connect(surface, &SurfaceInterface::childSubSurfaceRemoved, + this, &SurfaceItemWayland::handleChildSubSurfaceRemoved); + connect(surface, &SurfaceInterface::colorDescriptionChanged, + this, &SurfaceItemWayland::handleColorDescriptionChanged); + connect(surface, &SurfaceInterface::presentationModeHintChanged, + this, &SurfaceItemWayland::handlePresentationModeHintChanged); + connect(surface, &SurfaceInterface::bufferReleasePointChanged, this, &SurfaceItemWayland::handleReleasePointChanged); + connect(surface, &SurfaceInterface::alphaMultiplierChanged, this, &SurfaceItemWayland::handleAlphaMultiplierChanged); + + connect(surface, &SurfaceInterface::mapped, + this, &SurfaceItemWayland::handleSurfaceMappedChanged); + connect(surface, &SurfaceInterface::unmapped, + this, &SurfaceItemWayland::handleSurfaceMappedChanged); + setVisible(surface->isMapped()); + + SubSurfaceInterface *subsurface = surface->subSurface(); + if (subsurface) { + connect(subsurface, &SubSurfaceInterface::positionChanged, + this, &SurfaceItemWayland::handleSubSurfacePositionChanged); + setPosition(subsurface->position()); + } + + handleChildSubSurfacesChanged(); + setDestinationSize(surface->size()); + setBufferTransform(surface->bufferTransform()); + setBufferSourceBox(surface->bufferSourceBox()); + setBuffer(surface->buffer()); + m_bufferReleasePoint = m_surface->bufferReleasePoint(); + setColorDescription(surface->colorDescription()); + setRenderingIntent(surface->renderingIntent()); + setPresentationHint(surface->presentationModeHint()); + setOpacity(surface->alphaMultiplier()); + + m_fifoFallbackTimer.setInterval(1000 / 20); + m_fifoFallbackTimer.setSingleShot(true); + connect(&m_fifoFallbackTimer, &QTimer::timeout, this, &SurfaceItemWayland::handleFifoFallback); +} + +QList SurfaceItemWayland::shape() const +{ + return {rect()}; +} + +Region SurfaceItemWayland::opaque() const +{ + if (m_surface) { + return m_surface->opaque(); + } + return Region(); +} + +SurfaceInterface *SurfaceItemWayland::surface() const +{ + return m_surface; +} + +void SurfaceItemWayland::handleSurfaceSizeChanged() +{ + setDestinationSize(m_surface->size()); +} + +void SurfaceItemWayland::handleBufferChanged() +{ + setBuffer(m_surface->buffer()); +} + +void SurfaceItemWayland::handleBufferSourceBoxChanged() +{ + setBufferSourceBox(m_surface->bufferSourceBox()); +} + +void SurfaceItemWayland::handleBufferTransformChanged() +{ + setBufferTransform(m_surface->bufferTransform()); +} + +void SurfaceItemWayland::handleSurfaceCommitted() +{ + if (m_surface->hasFifoBarrier()) { + m_fifoFallbackTimer.start(); + } + if (m_surface->hasFrameCallbacks() || m_surface->hasFifoBarrier() || m_surface->hasPresentationFeedback()) { + scheduleFrame(); + } +} + +SurfaceItemWayland *SurfaceItemWayland::getOrCreateSubSurfaceItem(SubSurfaceInterface *child) +{ + auto &item = m_subsurfaces[child]; + if (!item) { + item = std::make_unique(child->surface(), this); + } + return item.get(); +} + +void SurfaceItemWayland::handleChildSubSurfaceRemoved(SubSurfaceInterface *child) +{ + m_subsurfaces.erase(child); +} + +void SurfaceItemWayland::handleChildSubSurfacesChanged() +{ + const QList below = m_surface->below(); + const QList above = m_surface->above(); + + for (int i = 0; i < below.count(); ++i) { + SurfaceItemWayland *subsurfaceItem = getOrCreateSubSurfaceItem(below[i]); + subsurfaceItem->setZ(i - below.count()); + } + + for (int i = 0; i < above.count(); ++i) { + SurfaceItemWayland *subsurfaceItem = getOrCreateSubSurfaceItem(above[i]); + subsurfaceItem->setZ(i); + } +} + +void SurfaceItemWayland::handleSubSurfacePositionChanged() +{ + setPosition(m_surface->subSurface()->position()); +} + +void SurfaceItemWayland::handleSurfaceMappedChanged() +{ + setVisible(m_surface->isMapped()); +} + +ContentType SurfaceItemWayland::contentType() const +{ + return m_surface ? m_surface->contentType() : ContentType::None; +} + +void SurfaceItemWayland::setScanoutHint(DrmDevice *device, const QHash> &drmFormats) +{ + if (!m_surface || !m_surface->dmabufFeedbackV1()) { + return; + } + if (!device && m_scanoutFeedback.has_value()) { + m_surface->dmabufFeedbackV1()->setTranches({}); + m_scanoutFeedback.reset(); + return; + } + if (!m_scanoutFeedback || m_scanoutFeedback->device != device || m_scanoutFeedback->formats != drmFormats) { + m_scanoutFeedback = ScanoutFeedback{ + .device = device, + .formats = drmFormats, + }; + m_surface->dmabufFeedbackV1()->setScanoutTranches(device, drmFormats); + } +} + +void SurfaceItemWayland::freeze() +{ + if (!m_surface) { + return; + } + + m_surface->disconnect(this); + if (auto subsurface = m_surface->subSurface()) { + subsurface->disconnect(this); + } + + for (auto &[subsurface, subsurfaceItem] : m_subsurfaces) { + subsurfaceItem->freeze(); + } + + m_surface = nullptr; + m_fifoFallbackTimer.stop(); +} + +void SurfaceItemWayland::handleColorDescriptionChanged() +{ + auto description = m_surface->colorDescription(); + if (m_surface->colorDescriptionType() == ColorDescriptionType::Windows) { + // TODO also react to config changes after the image description is set? + const auto group = kwinApp()->config()->group("Windows_HDR"); + description = description->withReference(group.readEntry("Reference", 203.0)); + description = description->withHdrMetadata(group.readEntry("MaxFrameAverage", 600), group.readEntry("MaxLuminance", 1'000)); + } + setColorDescription(description); + setRenderingIntent(m_surface->renderingIntent()); +} + +void SurfaceItemWayland::handlePresentationModeHintChanged() +{ + setPresentationHint(m_surface->presentationModeHint()); +} + +void SurfaceItemWayland::handleReleasePointChanged() +{ + m_bufferReleasePoint = m_surface->bufferReleasePoint(); +} + +void SurfaceItemWayland::handleAlphaMultiplierChanged() +{ + setOpacity(m_surface->alphaMultiplier()); +} + +void SurfaceItemWayland::handleFramePainted(LogicalOutput *output, OutputFrame *frame, std::chrono::milliseconds timestamp) +{ + if (!m_surface) { + return; + } + m_surface->frameRendered(timestamp.count()); + if (frame) { + // FIXME make frame always valid + if (auto feedback = m_surface->presentationFeedback(output)) { + frame->addFeedback(std::move(feedback)); + } + } + // TODO only call this once per refresh cycle + m_surface->clearFifoBarrier(); + if (m_fifoFallbackTimer.isActive() && output) { + // TODO once we can rely on frame being not-nullptr, use its refresh duration instead + const auto refreshDuration = std::chrono::nanoseconds(1'000'000'000'000) / output->backendOutput()->refreshRate(); + // some games don't work properly if the refresh rate goes too low with FIFO. 30Hz is assumed to be fine here. + // this must still be slower than the actual screen though, or fifo behavior would be broken! + const auto fallbackRefreshDuration = std::max(refreshDuration * 5 / 4, std::chrono::nanoseconds(1'000'000'000) / 30); + // reset the timer, it should only trigger if we don't present fast enough + m_fifoFallbackTimer.start(std::chrono::duration_cast(fallbackRefreshDuration)); + } +} + +void SurfaceItemWayland::handleFifoFallback() +{ + if (m_surface) { + m_surface->clearFifoBarrier(); + } +} + +#if KWIN_BUILD_X11 +SurfaceItemXwayland::SurfaceItemXwayland(X11Window *window, Item *parent) + : SurfaceItemWayland(window->surface(), parent) + , m_window(window) +{ + connect(window, &X11Window::shapeChanged, this, &SurfaceItemXwayland::handleShapeChange); +} + +void SurfaceItemXwayland::handleShapeChange() +{ + const auto newShape = m_window->shapeRegion(); + Region newBufferShape; + for (const auto &rect : newShape) { + newBufferShape |= rect.toAlignedRect(); + } + scheduleRepaint(newBufferShape.xored(m_previousBufferShape)); + m_previousBufferShape = newBufferShape; + discardQuads(); +} + +QList SurfaceItemXwayland::shape() const +{ + QList shape = m_window->shapeRegion(); + for (RectF &shapePart : shape) { + shapePart = shapePart.intersected(rect()); + } + return shape; +} + +Region SurfaceItemXwayland::opaque() const +{ + Region shapeRegion; + for (const RectF &shapePart : shape()) { + shapeRegion += shapePart.toRect(); + } + if (!m_window->hasAlpha()) { + return shapeRegion; + } else { + return m_window->opaqueRegion() & shapeRegion; + } + return Region(); +} +#endif +} // namespace KWin + +#include "moc_surfaceitem_wayland.cpp" diff --git a/local/recipes/kde/kwin/source/src/scene/surfaceitem_wayland.h b/local/recipes/kde/kwin/source/src/scene/surfaceitem_wayland.h new file mode 100644 index 0000000000..052427662a --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/surfaceitem_wayland.h @@ -0,0 +1,95 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "scene/surfaceitem.h" + +#include +#include + +namespace KWin +{ + +class GraphicsBuffer; +class SubSurfaceInterface; +class SurfaceInterface; +class X11Window; + +/** + * The SurfaceItemWayland class represents a Wayland surface in the scene. + */ +class KWIN_EXPORT SurfaceItemWayland : public SurfaceItem +{ + Q_OBJECT + +public: + explicit SurfaceItemWayland(SurfaceInterface *surface, Item *parent = nullptr); + + QList shape() const override; + Region opaque() const override; + ContentType contentType() const override; + void setScanoutHint(DrmDevice *device, const QHash> &drmFormats) override; + void freeze() override; + + SurfaceInterface *surface() const; + +private Q_SLOTS: + void handleSurfaceCommitted(); + void handleSurfaceSizeChanged(); + void handleBufferChanged(); + void handleBufferSourceBoxChanged(); + void handleBufferTransformChanged(); + + void handleChildSubSurfaceRemoved(SubSurfaceInterface *child); + void handleChildSubSurfacesChanged(); + void handleSubSurfacePositionChanged(); + void handleSurfaceMappedChanged(); + void handleColorDescriptionChanged(); + void handlePresentationModeHintChanged(); + void handleReleasePointChanged(); + void handleAlphaMultiplierChanged(); + + void handleFifoFallback(); + +private: + SurfaceItemWayland *getOrCreateSubSurfaceItem(SubSurfaceInterface *s); + void handleFramePainted(LogicalOutput *output, OutputFrame *frame, std::chrono::milliseconds timestamp) override; + + QPointer m_surface; + struct ScanoutFeedback + { + DrmDevice *device = nullptr; + QHash> formats; + }; + std::optional m_scanoutFeedback; + std::unordered_map> m_subsurfaces; + QTimer m_fifoFallbackTimer; +}; + +#if KWIN_BUILD_X11 +/** + * The SurfaceItemXwayland class represents an Xwayland surface in the scene. + */ +class KWIN_EXPORT SurfaceItemXwayland : public SurfaceItemWayland +{ + Q_OBJECT + +public: + explicit SurfaceItemXwayland(X11Window *window, Item *parent = nullptr); + + Region opaque() const override; + QList shape() const override; + +private: + void handleShapeChange(); + + X11Window *m_window; + Region m_previousBufferShape; +}; +#endif + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/windowitem.cpp b/local/recipes/kde/kwin/source/src/scene/windowitem.cpp new file mode 100644 index 0000000000..f2a48f76cb --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/windowitem.cpp @@ -0,0 +1,340 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scene/windowitem.h" +#include "effect/effecthandler.h" +#include "internalwindow.h" +#include "scene/decorationitem.h" +#include "scene/shadowitem.h" +#include "scene/surfaceitem_internal.h" +#include "scene/surfaceitem_wayland.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" +#if KWIN_BUILD_X11 +#include "x11window.h" +#endif + +#include + +namespace KWin +{ + +WindowItem::WindowItem(Window *window, Item *parent) + : Item(parent) + , m_windowContainer(std::make_unique(this)) + , m_window(window) +{ + connect(window, &Window::decorationChanged, this, &WindowItem::updateDecorationItem); + updateDecorationItem(); + + connect(window, &Window::shadowChanged, this, &WindowItem::updateShadowItem); + updateShadowItem(); + + connect(window, &Window::frameGeometryChanged, this, &WindowItem::updateGeometry); + updateGeometry(); + + if (!window->readyForPainting()) { + connect(window, &Window::readyForPaintingChanged, this, &WindowItem::updateVisibility); + } + connect(window, &Window::lockScreenOverlayChanged, this, &WindowItem::updateVisibility); + connect(window, &Window::minimizedChanged, this, &WindowItem::updateVisibility); + connect(window, &Window::hiddenChanged, this, &WindowItem::updateVisibility); + connect(window, &Window::hiddenByShowDesktopChanged, this, &WindowItem::updateVisibility); + connect(window, &Window::activitiesChanged, this, &WindowItem::updateVisibility); + connect(window, &Window::desktopsChanged, this, &WindowItem::updateVisibility); + connect(window, &Window::offscreenRenderingChanged, this, &WindowItem::updateVisibility); + connect(waylandServer(), &WaylandServer::lockStateChanged, this, &WindowItem::updateVisibility); + connect(workspace(), &Workspace::currentActivityChanged, this, &WindowItem::updateVisibility); + connect(workspace(), &Workspace::currentDesktopChanged, this, &WindowItem::updateVisibility); + updateVisibility(); + + connect(window, &Window::opacityChanged, this, &WindowItem::updateOpacity); + updateOpacity(); + + connect(window, &Window::stackingOrderChanged, this, &WindowItem::updateStackingOrder); + updateStackingOrder(); + + connect(window, &Window::closed, this, &WindowItem::freeze); + + m_effectWindow = std::make_unique(this); +} + +WindowItem::~WindowItem() +{ +} + +SurfaceItem *WindowItem::surfaceItem() const +{ + return m_surfaceItem.get(); +} + +DecorationItem *WindowItem::decorationItem() const +{ + return m_decorationItem.get(); +} + +ShadowItem *WindowItem::shadowItem() const +{ + return m_shadowItem.get(); +} + +Window *WindowItem::window() const +{ + return m_window; +} + +EffectWindow *WindowItem::effectWindow() const +{ + return m_effectWindow.get(); +} + +Item *WindowItem::windowContainer() const +{ + return m_windowContainer.get(); +} + +void WindowItem::refVisible(int reason) +{ + if (reason & PAINT_DISABLED_BY_HIDDEN) { + m_forceVisibleByHiddenCount++; + } + if (reason & PAINT_DISABLED_BY_DESKTOP) { + m_forceVisibleByDesktopCount++; + } + if (reason & PAINT_DISABLED_BY_MINIMIZE) { + m_forceVisibleByMinimizeCount++; + } + if (reason & PAINT_DISABLED_BY_ACTIVITY) { + m_forceVisibleByActivityCount++; + } + updateVisibility(); +} + +void WindowItem::unrefVisible(int reason) +{ + if (reason & PAINT_DISABLED_BY_HIDDEN) { + Q_ASSERT(m_forceVisibleByHiddenCount > 0); + m_forceVisibleByHiddenCount--; + } + if (reason & PAINT_DISABLED_BY_DESKTOP) { + Q_ASSERT(m_forceVisibleByDesktopCount > 0); + m_forceVisibleByDesktopCount--; + } + if (reason & PAINT_DISABLED_BY_MINIMIZE) { + Q_ASSERT(m_forceVisibleByMinimizeCount > 0); + m_forceVisibleByMinimizeCount--; + } + if (reason & PAINT_DISABLED_BY_ACTIVITY) { + Q_ASSERT(m_forceVisibleByActivityCount > 0); + m_forceVisibleByActivityCount--; + } + updateVisibility(); +} + +void WindowItem::elevate() +{ + // Not ideal, but it's also highly unlikely that there are more than 1000 windows. The + // elevation constantly increases so it's possible to force specific stacking order. It + // can potentially overflow, but it's unlikely to happen because windows are elevated + // rarely. + static int elevation = 1000; + + m_elevation = elevation++; + updateStackingOrder(); +} + +void WindowItem::deelevate() +{ + m_elevation.reset(); + updateStackingOrder(); +} + +bool WindowItem::computeVisibility() const +{ + if (!m_window->readyForPainting()) { + return false; + } + if (waylandServer()->isScreenLocked()) { + return m_window->isLockScreen() || m_window->isInputMethod() || m_window->isLockScreenOverlay(); + } + if (!m_window->isOnCurrentDesktop()) { + if (m_forceVisibleByDesktopCount == 0) { + return false; + } + } + if (!m_window->isOnCurrentActivity()) { + if (m_forceVisibleByActivityCount == 0) { + return false; + } + } + if (m_window->isMinimized()) { + if (m_forceVisibleByMinimizeCount == 0) { + return false; + } + } + if (m_window->isHidden() || m_window->isHiddenByShowDesktop()) { + if (m_forceVisibleByHiddenCount == 0) { + return false; + } + } + return true; +} + +void WindowItem::updateVisibility() +{ + const bool visible = computeVisibility(); + setVisible(visible); + + if (m_window->readyForPainting()) { + m_window->setSuspended(!visible && !m_window->isOffscreenRendering()); + } +} + +void WindowItem::updateGeometry() +{ + setPosition(m_window->pos()); + m_windowContainer->setSize(m_window->frameGeometry().size()); +} + +void WindowItem::addSurfaceItemDamageConnects(Item *item) +{ + auto surfaceItem = static_cast(item); + connect(surfaceItem, &SurfaceItem::damaged, this, &WindowItem::markDamaged); + connect(surfaceItem, &SurfaceItem::childAdded, this, &WindowItem::addSurfaceItemDamageConnects); + connect(surfaceItem, &SurfaceItem::childRemoved, this, &WindowItem::markDamaged); + connect(surfaceItem, &SurfaceItem::visibleChanged, this, &WindowItem::markDamaged); + const auto childItems = item->childItems(); + for (const auto &child : childItems) { + addSurfaceItemDamageConnects(child); + } +} + +void WindowItem::updateSurfaceItem(std::unique_ptr &&surfaceItem) +{ + m_surfaceItem = std::move(surfaceItem); + + if (m_surfaceItem) { + connect(m_window, &Window::bufferGeometryChanged, this, &WindowItem::updateSurfacePosition); + connect(m_window, &Window::frameGeometryChanged, this, &WindowItem::updateSurfacePosition); + connect(m_window, &Window::borderRadiusChanged, this, &WindowItem::updateBorderRadius); + addSurfaceItemDamageConnects(m_surfaceItem.get()); + + updateSurfacePosition(); + updateBorderRadius(); + } else { + disconnect(m_window, &Window::bufferGeometryChanged, this, &WindowItem::updateSurfacePosition); + disconnect(m_window, &Window::frameGeometryChanged, this, &WindowItem::updateSurfacePosition); + disconnect(m_window, &Window::borderRadiusChanged, this, &WindowItem::updateBorderRadius); + } +} + +void WindowItem::updateSurfacePosition() +{ + const RectF bufferGeometry = m_window->bufferGeometry(); + const RectF frameGeometry = m_window->frameGeometry(); + + m_surfaceItem->setPosition(bufferGeometry.topLeft() - frameGeometry.topLeft()); +} + +void WindowItem::updateBorderRadius() +{ + m_windowContainer->setBorderRadius(m_window->borderRadius()); +} + +void WindowItem::updateShadowItem() +{ + Shadow *shadow = m_window->shadow(); + if (shadow) { + if (!m_shadowItem || m_shadowItem->shadow() != shadow) { + m_shadowItem = std::make_unique(shadow, m_window, this); + } + m_shadowItem->stackBefore(m_windowContainer.get()); + markDamaged(); + } else { + m_shadowItem.reset(); + } +} + +void WindowItem::updateDecorationItem() +{ + if (m_window->isDeleted()) { + return; + } + if (m_window->decoration()) { + m_decorationItem = std::make_unique(m_window->decoration(), m_window, m_windowContainer.get()); + if (m_surfaceItem) { + m_decorationItem->stackBefore(m_surfaceItem.get()); + } + connect(m_window->decoration(), &KDecoration3::Decoration::damaged, this, &WindowItem::markDamaged); + markDamaged(); + } else { + m_decorationItem.reset(); + } +} + +void WindowItem::updateOpacity() +{ + setOpacity(m_window->opacity()); +} + +void WindowItem::updateStackingOrder() +{ + if (m_elevation.has_value()) { + setZ(m_elevation.value()); + } else { + setZ(m_window->stackingOrder()); + } +} + +void WindowItem::markDamaged() +{ + Q_EMIT m_window->damaged(m_window); +} + +void WindowItem::freeze() +{ + if (m_surfaceItem) { + m_surfaceItem->freeze(); + } +} + +#if KWIN_BUILD_X11 +WindowItemX11::WindowItemX11(X11Window *window, Item *parent) + : WindowItem(window, parent) +{ + initialize(); + + // Xwayland windows and Wayland surfaces are associated asynchronously. + connect(window, &Window::surfaceChanged, this, &WindowItemX11::initialize); +} + +void WindowItemX11::initialize() +{ + if (!window()->surface()) { + updateSurfaceItem(nullptr); + } else { + updateSurfaceItem(std::make_unique(static_cast(window()), m_windowContainer.get())); + } +} +#endif + +WindowItemWayland::WindowItemWayland(Window *window, Item *parent) + : WindowItem(window, parent) +{ + updateSurfaceItem(std::make_unique(window->surface(), m_windowContainer.get())); +} + +WindowItemInternal::WindowItemInternal(InternalWindow *window, Item *parent) + : WindowItem(window, parent) +{ + updateSurfaceItem(std::make_unique(window, m_windowContainer.get())); +} + +} // namespace KWin + +#include "moc_windowitem.cpp" diff --git a/local/recipes/kde/kwin/source/src/scene/windowitem.h b/local/recipes/kde/kwin/source/src/scene/windowitem.h new file mode 100644 index 0000000000..69f578ad67 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/windowitem.h @@ -0,0 +1,138 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "config-kwin.h" + +#include "scene/item.h" + +namespace KDecoration3 +{ +class Decoration; +} + +namespace KWin +{ +class Window; +class DecorationItem; +class EffectWindow; +class InternalWindow; +class Shadow; +class ShadowItem; +class SurfaceItem; +class X11Window; + +/** + * The WindowItem class represents a window in the scene. + * + * A WindowItem is made of a surface with client contents and optionally a server-side frame + * and a drop-shadow. + */ +class KWIN_EXPORT WindowItem : public Item +{ + Q_OBJECT + +public: + enum { + PAINT_DISABLED_BY_HIDDEN = 1 << 0, + PAINT_DISABLED_BY_DESKTOP = 1 << 1, + PAINT_DISABLED_BY_MINIMIZE = 1 << 2, + PAINT_DISABLED_BY_ACTIVITY = 1 << 3, + }; + + ~WindowItem() override; + + SurfaceItem *surfaceItem() const; + DecorationItem *decorationItem() const; + ShadowItem *shadowItem() const; + Window *window() const; + EffectWindow *effectWindow() const; + Item *windowContainer() const; + + void refVisible(int reason); + void unrefVisible(int reason); + + void elevate(); + void deelevate(); + +protected: + explicit WindowItem(Window *window, Item *parent = nullptr); + void updateSurfaceItem(std::unique_ptr &&surfaceItem); + + const std::unique_ptr m_windowContainer; + +private Q_SLOTS: + void updateDecorationItem(); + void updateShadowItem(); + void updateSurfacePosition(); + void updateBorderRadius(); + void updateGeometry(); + void updateOpacity(); + void updateStackingOrder(); + void addSurfaceItemDamageConnects(Item *item); + +private: + bool computeVisibility() const; + void updateVisibility(); + void markDamaged(); + void freeze(); + + Window *m_window; + std::unique_ptr m_surfaceItem; + std::unique_ptr m_decorationItem; + std::unique_ptr m_shadowItem; + std::unique_ptr m_effectWindow; + std::optional m_elevation; + int m_forceVisibleByHiddenCount = 0; + int m_forceVisibleByDesktopCount = 0; + int m_forceVisibleByMinimizeCount = 0; + int m_forceVisibleByActivityCount = 0; +}; + +#if KWIN_BUILD_X11 +/** + * The WindowItemX11 class represents an X11 window (both on X11 and Wayland sessions). + * + * Note that Xwayland windows and Wayland surfaces are associated asynchronously. This means + * that the surfaceItem() function can return @c null until the window is fully initialized. + */ +class KWIN_EXPORT WindowItemX11 : public WindowItem +{ + Q_OBJECT + +public: + explicit WindowItemX11(X11Window *window, Item *parent = nullptr); + +private Q_SLOTS: + void initialize(); +}; +#endif + +/** + * The WindowItemWayland class represents a Wayland window. + */ +class KWIN_EXPORT WindowItemWayland : public WindowItem +{ + Q_OBJECT + +public: + explicit WindowItemWayland(Window *window, Item *parent = nullptr); +}; + +/** + * The WindowItemInternal class represents a window created by the compositor, for + * example, the task switcher, etc. + */ +class KWIN_EXPORT WindowItemInternal : public WindowItem +{ + Q_OBJECT + +public: + explicit WindowItemInternal(InternalWindow *window, Item *parent = nullptr); +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scene/workspacescene.cpp b/local/recipes/kde/kwin/source/src/scene/workspacescene.cpp new file mode 100644 index 0000000000..2e9d5b4abc --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/workspacescene.cpp @@ -0,0 +1,843 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +/* + Design: + + When compositing is turned on, XComposite extension is used to redirect + drawing of windows to pixmaps and XDamage extension is used to get informed + about damage (changes) to window contents. This code is mostly in composite.cpp . + + Compositor::performCompositing() starts one painting pass. Painting is done + by painting the screen, which in turn paints every window. Painting can be affected + using effects, which are chained. E.g. painting a screen means that actually + paintScreen() of the first effect is called, which possibly does modifications + and calls next effect's paintScreen() and so on, until Scene::finalPaintScreen() + is called. + + There are 3 phases of every paint (not necessarily done together): + The pre-paint phase, the paint phase and the post-paint phase. + + The pre-paint phase is used to find out about how the painting will be actually + done (i.e. what the effects will do). For example when only a part of the screen + needs to be updated and no effect will do any transformation it is possible to use + an optimized paint function. How the painting will be done is controlled + by the mask argument, see PAINT_WINDOW_* and PAINT_SCREEN_* flags in scene.h . + For example an effect that decides to paint a normal windows as translucent + will need to modify the mask in its prePaintWindow() to include + the PAINT_WINDOW_TRANSLUCENT flag. The paintWindow() function will then get + the mask with this flag turned on and will also paint using transparency. + + The paint pass does the actual painting, based on the information collected + using the pre-paint pass. After running through the effects' paintScreen() + either paintGenericScreen() or optimized paintSimpleScreen() are called. + Those call paintWindow() on windows (not necessarily all), possibly using + clipping to optimize performance and calling paintWindow() first with only + PAINT_WINDOW_OPAQUE to paint the opaque parts and then later + with PAINT_WINDOW_TRANSLUCENT to paint the transparent parts. Function + paintWindow() again goes through effects' paintWindow() until + finalPaintWindow() is called, which calls the window's performPaint() to + do the actual painting. + + The post-paint can be used for cleanups and is also used for scheduling + repaints during the next painting pass for animations. Effects wanting to + repaint certain parts can manually damage them during post-paint and repaint + of these parts will be done during the next paint pass. + +*/ + +#include "scene/workspacescene.h" +#include "compositor.h" +#include "core/backendoutput.h" +#include "core/graphicsbufferview.h" +#include "core/output.h" +#include "core/pixelgrid.h" +#include "core/renderbackend.h" +#include "core/renderloop.h" +#include "core/renderviewport.h" +#include "cursoritem.h" +#include "effect/effecthandler.h" +#include "opengl/eglbackend.h" +#include "opengl/eglcontext.h" +#include "scene/backgroundeffectitem.h" +#include "scene/decorationitem.h" +#include "scene/dndiconitem.h" +#include "scene/itemrenderer.h" +#include "scene/rootitem.h" +#include "scene/surfaceitem.h" +#include "scene/windowitem.h" +#include "utils/envvar.h" +#include "wayland/seat.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" + +#include + +namespace KWin +{ + +//**************************************** +// Scene +//**************************************** + +WorkspaceScene::WorkspaceScene(std::unique_ptr renderer) + : Scene(std::move(renderer)) + , m_containerItem(std::make_unique(this)) + , m_overlayItem(std::make_unique(this)) + , m_cursorItem(std::make_unique(m_overlayItem.get())) +{ + setGeometry(workspace()->geometry()); + connect(workspace(), &Workspace::geometryChanged, this, [this]() { + setGeometry(workspace()->geometry()); + }); + + connect(waylandServer()->seat(), &SeatInterface::dragStarted, this, &WorkspaceScene::createDndIconItem); + connect(waylandServer()->seat(), &SeatInterface::dragEnded, this, &WorkspaceScene::destroyDndIconItem); + + // make sure it's over the dnd icon + m_cursorItem->setZ(1); + connect(Cursors::self(), &Cursors::hiddenChanged, this, &WorkspaceScene::updateCursor); + connect(Cursors::self(), &Cursors::positionChanged, this, &WorkspaceScene::updateCursor); + updateCursor(); +} + +WorkspaceScene::~WorkspaceScene() +{ +} + +void WorkspaceScene::createDndIconItem() +{ + DragAndDropIcon *dragIcon = waylandServer()->seat()->dragIcon(); + if (!dragIcon) { + return; + } + m_dndIcon = std::make_unique(dragIcon, m_overlayItem.get()); + + auto updatePosition = [this]() { + const auto position = waylandServer()->seat()->dragPosition(); + m_dndIcon->setPosition(position); + m_dndIcon->setOutput(workspace()->outputAt(position)); + }; + + updatePosition(); + connect(waylandServer()->seat(), &SeatInterface::dragMoved, m_dndIcon.get(), updatePosition); +} + +void WorkspaceScene::destroyDndIconItem() +{ + m_dndIcon.reset(); +} + +void WorkspaceScene::updateCursor() +{ + if (Cursors::self()->isCursorHidden()) { + m_cursorItem->setVisible(false); + } else { + m_cursorItem->setVisible(true); + m_cursorItem->setPosition(Cursors::self()->currentCursor()->pos()); + } +} + +Item *WorkspaceScene::containerItem() const +{ + return m_containerItem.get(); +} + +Item *WorkspaceScene::overlayItem() const +{ + return m_overlayItem.get(); +} + +Item *WorkspaceScene::cursorItem() const +{ + return m_cursorItem.get(); +} + +struct ClipCorner +{ + RectF box; + BorderRadius radius; +}; + +static std::optional calculateClipCorner(Item *item, const std::optional &parentClip) +{ + if (!item->borderRadius().isNull()) { + return ClipCorner{ + .box = item->rect(), + .radius = item->borderRadius(), + }; + } else if (parentClip.has_value()) { + return ClipCorner{ + .box = item->transform().inverted().mapRect(parentClip->box.translated(-item->position())), + .radius = parentClip->radius, + }; + } else { + return std::nullopt; + } +} + +static void maybePushCorners(Item *item, QStack &corners) +{ + std::optional top; + if (!corners.isEmpty()) { + top = corners.top(); + } + if (const auto clip = calculateClipCorner(item, top)) { + corners.push(*clip); + } +} + +static bool addCandidates(SceneView *delegate, Item *item, QList &candidates, ssize_t maxCount, Region &occluded, QStack &corners) +{ + if (item->opacity() == 0.0) { + return true; + } + if (item->opacity() != 1.0 || item->hasEffects()) { + return false; + } + const QList children = item->sortedChildItems(); + auto it = children.rbegin(); + for (; it != children.rend(); it++) { + Item *const child = *it; + if (child->z() < 0) { + break; + } + if (!delegate->shouldRenderItem(child)) { + continue; + } + if (child->isVisible() && !occluded.contains(child->mapToView(child->boundingRect(), delegate).rounded())) { + if (!addCandidates(delegate, static_cast(child), candidates, maxCount, occluded, corners)) { + return false; + } + } + } + if (occluded.contains(item->mapToView(item->boundingRect(), delegate).rounded())) { + return true; + } + if (delegate->shouldRenderItem(item)) { + if (auto surfaceItem = qobject_cast(item)) { + candidates.push_back(surfaceItem); + if (candidates.size() > maxCount) { + return false; + } + } else { + return false; + } + } + + maybePushCorners(item, corners); + auto cleanupCorners = qScopeGuard([&corners]() { + if (!corners.isEmpty()) { + corners.pop(); + } + }); + + Region opaque = item->opaque(); + if (!corners.isEmpty()) { + const auto &top = corners.top(); + opaque = top.radius.clip(opaque, top.box); + } + + occluded += item->mapToView(opaque, delegate); + for (; it != children.rend(); it++) { + Item *const child = *it; + if (!delegate->shouldRenderItem(child)) { + continue; + } + if (child->isVisible() && !occluded.contains(child->mapToView(child->boundingRect(), delegate).rounded())) { + if (!addCandidates(delegate, static_cast(child), candidates, maxCount, occluded, corners)) { + return false; + } + } + } + return true; +} + +static bool checkForBlackBackground(SurfaceItem *background) +{ + if (!background->buffer() + || (!background->buffer()->singlePixelAttributes() && !background->buffer()->shmAttributes()) + || background->buffer()->size() != QSize(1, 1)) { + return false; + } + const GraphicsBufferView view(background->buffer()); + if (!view.image()) { + return false; + } + const QRgb rgb = view.image()->pixel(0, 0); + const QVector3D encoded(qRed(rgb) / 255.0, qGreen(rgb) / 255.0, qBlue(rgb) / 255.0); + const QVector3D nits = background->colorDescription()->mapTo(encoded, ColorDescription(Colorimetry::BT709, TransferFunction(TransferFunction::linear), 100, 0, std::nullopt, std::nullopt), background->renderingIntent()); + // below 0.1 nits, it shouldn't be noticeable that we replace it with black + return nits.lengthSquared() <= (0.1 * 0.1); +} + +QList WorkspaceScene::scanoutCandidates(ssize_t maxCount) const +{ + const auto overlayItems = m_overlayItem->childItems(); + const bool needsRendering = std::ranges::any_of(overlayItems, [this](Item *child) { + return child->isVisible() + && child->opacity() != 0.0 + && !child->boundingRect().isEmpty() + && painted_delegate->shouldRenderItem(child) + && painted_delegate->viewport().intersects(child->mapToView(child->boundingRect(), painted_delegate)); + }); + if (needsRendering) { + return {}; + } + QList ret; + if (!effects->blocksDirectScanout()) { + Region occlusion; + QStack corners; + const auto items = m_containerItem->sortedChildItems(); + for (Item *item : items | std::views::reverse) { + if (!item->isVisible() || !painted_delegate->shouldRenderItem(item) || !painted_delegate->viewport().intersects(item->mapToView(item->boundingRect(), painted_delegate))) { + continue; + } + if (!addCandidates(painted_delegate, item, ret, maxCount + 1, occlusion, corners)) { + return {}; + } + if (ret.size() == maxCount + 1 && !checkForBlackBackground(ret.back())) { + return {}; + } + if (occlusion.contains(painted_screen->geometry())) { + break; + } + } + } + if (!ret.empty() && checkForBlackBackground(ret.back())) { + ret.pop_back(); + } + return ret; +} + +static Rect mapToDevice(SceneView *view, Item *item, const RectF &itemLocal) +{ + const RectF localLogical = item->mapToView(itemLocal, view).translated(-view->viewport().topLeft()); + return localLogical.scaled(view->scale()).rounded(); +} + +static Region mapToDevice(SceneView *view, Item *item, const Region &itemLocal) +{ + Region ret; + for (const RectF local : itemLocal.rects()) { + ret |= mapToDevice(view, item, local); + } + return ret; +} + +static bool findOverlayCandidates(SceneView *view, Item *item, ssize_t maxTotalCount, ssize_t maxOverlayCount, ssize_t maxUnderlayCount, Region &occupied, Region &opaque, Region &effected, QList &overlays, QList &underlays, QStack &corners) +{ + if (!item || !item->isVisible() || item->opacity() == 0.0 || item->boundingRect().isEmpty() || !view->viewport().intersects(item->mapToView(item->boundingRect(), view))) { + return true; + } + if (item->hasEffects()) { + // can't put this item, any children on items below this one + // on an overlay, as we don't know what the effect does + effected += mapToDevice(view, item, item->boundingRect()); + return true; + } + maybePushCorners(item, corners); + auto cleanupCorners = qScopeGuard([&corners]() { + if (!corners.isEmpty()) { + corners.pop(); + } + }); + + const QList children = item->sortedChildItems(); + auto it = children.rbegin(); + for (; it != children.rend(); it++) { + Item *const child = *it; + if (child->z() < 0) { + break; + } + if (!findOverlayCandidates(view, child, maxTotalCount, maxOverlayCount, maxUnderlayCount, occupied, opaque, effected, overlays, underlays, corners)) { + return false; + } + } + + // for the Item to be possibly relevant for overlays, it needs to + // - be a SurfaceItem (for now at least) + // - not be empty + // - be the topmost item in the relevant screen area + // - regularly get updates + // - use dmabufs + // - not have any surface-wide opacity (for now) + // - not be entirely covered by other opaque windows + SurfaceItem *surfaceItem = dynamic_cast(item); + const Rect deviceRect = mapToDevice(view, item, item->rect()); + if (surfaceItem + && !surfaceItem->rect().isEmpty() + && surfaceItem->frameTimeEstimation().transform([](const auto t) { + return t < std::chrono::nanoseconds(1'000'000'000) / 20; + }).value_or(false) + && surfaceItem->buffer()->dmabufAttributes() + // TODO make the compositor handle item opacity as well + && surfaceItem->opacity() == 1.0 + && !opaque.contains(deviceRect) + && !effected.intersects(deviceRect)) { + if (occupied.intersects(deviceRect) || (!corners.isEmpty() && corners.top().radius.clips(item->rect(), corners.top().box))) { + const bool isOpaque = surfaceItem->opaque().contains(surfaceItem->rect().roundedOut()); + if (!isOpaque) { + // only fully opaque items can be used as underlays + return false; + } + underlays.push_back(surfaceItem); + } else { + overlays.push_back(surfaceItem); + } + if (overlays.size() + underlays.size() > maxTotalCount + || overlays.size() > maxOverlayCount + || underlays.size() > maxUnderlayCount) { + // If we have to repaint the primary plane anyways, it's not going to provide an efficiency + // or latency improvement to put some but not all quickly updating surfaces on overlays, + // at least not with the current way we use them. + return false; + } + } else { + occupied += deviceRect; + } + opaque += mapToDevice(view, item, item->opaque()); + + for (; it != children.rend(); it++) { + Item *const child = *it; + if (!findOverlayCandidates(view, child, maxTotalCount, maxOverlayCount, maxUnderlayCount, occupied, opaque, effected, overlays, underlays, corners)) { + return false; + } + } + return true; +} + +static const bool s_forceSoftwareCursor = environmentVariableBoolValue("KWIN_FORCE_SW_CURSOR").value_or(false); + +Scene::OverlayCandidates WorkspaceScene::overlayCandidates(ssize_t maxTotalCount, ssize_t maxOverlayCount, ssize_t maxUnderlayCount) const +{ + const auto fallback = [&]() { + if (s_forceSoftwareCursor + || maxOverlayCount == 0 + || !cursorItem()->isVisible() + || !painted_delegate->viewport().intersects(cursorItem()->mapToView(cursorItem()->boundingRect(), painted_delegate))) { + return OverlayCandidates{}; + } + return OverlayCandidates{ + .overlays = {cursorItem()}, + .underlays = {}, + }; + }; + Region occupied; + Region opaque; + Region effected; + QList overlays; + QList underlays; + QStack cornerStack; + const auto overlayItems = m_overlayItem->sortedChildItems(); + for (Item *item : overlayItems | std::views::reverse) { + if (!item->isVisible() || !painted_delegate->viewport().intersects(item->mapToView(item->boundingRect(), painted_delegate))) { + continue; + } + if (item == cursorItem()) { + if (s_forceSoftwareCursor) { + continue; + } + // for the time being, prioritize the cursor above all else, + // even while it's not moving + overlays.push_back(item); + if (overlays.size() > maxOverlayCount || underlays.size() + overlays.size() > maxTotalCount) { + return fallback(); + } + continue; + } + if (!findOverlayCandidates(painted_delegate, item, maxTotalCount, maxOverlayCount, maxUnderlayCount, occupied, opaque, effected, overlays, underlays, cornerStack)) { + return fallback(); + } + } + if (effects->blocksDirectScanout()) { + return fallback(); + } + const auto items = m_containerItem->sortedChildItems(); + for (Item *item : items | std::views::reverse) { + if (!findOverlayCandidates(painted_delegate, item, maxTotalCount, maxOverlayCount, maxUnderlayCount, occupied, opaque, effected, overlays, underlays, cornerStack)) { + return fallback(); + } + } + return OverlayCandidates{ + .overlays = overlays, + .underlays = underlays, + }; +} + +static double getDesiredHdrHeadroom(Item *item) +{ + if (!item->isVisible()) { + return 1; + } + double ret = 1; + const auto children = item->childItems(); + for (const auto &child : children) { + ret = std::max(ret, getDesiredHdrHeadroom(child)); + } + const auto &color = item->colorDescription(); + if (color->maxHdrLuminance() && *color->maxHdrLuminance() > color->referenceLuminance()) { + return std::max(ret, *color->maxHdrLuminance() / color->referenceLuminance()); + } else { + return ret; + } +} + +double WorkspaceScene::desiredHdrHeadroom() const +{ + double maxHeadroom = 1; + for (const auto &item : stacking_order) { + if (!item->window()->frameGeometry().intersects(painted_delegate->viewport())) { + continue; + } + maxHeadroom = std::max(maxHeadroom, getDesiredHdrHeadroom(item)); + } + return maxHeadroom; +} + +void WorkspaceScene::frame(SceneView *delegate, OutputFrame *frame) +{ + LogicalOutput *logicalOutput = delegate->logicalOutput(); + const auto frameTime = std::chrono::duration_cast(logicalOutput->backendOutput()->renderLoop()->lastPresentationTimestamp()); + m_containerItem->framePainted(delegate, logicalOutput, frame, frameTime); + if (m_overlayItem) { + m_overlayItem->framePainted(delegate, logicalOutput, frame, frameTime); + } +} + +void WorkspaceScene::prePaint(SceneView *delegate) +{ + painted_delegate = delegate; + painted_screen = painted_delegate->logicalOutput(); + + createStackingOrder(); + + const RenderLoop *renderLoop = painted_screen->backendOutput()->renderLoop(); + const std::chrono::milliseconds presentTime = + std::chrono::duration_cast(renderLoop->nextPresentationTimestamp()); + + if (presentTime > m_expectedPresentTimestamp) { + m_expectedPresentTimestamp = presentTime; + } + + // preparation step + effects->startPaint(); + + ScreenPrePaintData prePaintData; + prePaintData.mask = 0; + prePaintData.screen = painted_screen; + prePaintData.view = delegate; + + effects->makeOpenGLContextCurrent(); + Q_EMIT preFrameRender(); + + effects->prePaintScreen(prePaintData, m_expectedPresentTimestamp); + m_paintContext.deviceDamage = painted_delegate->mapToDeviceCoordinatesAligned(prePaintData.paint) & painted_delegate->deviceRect(); + m_paintContext.mask = prePaintData.mask; + m_paintContext.phase2Data.clear(); + + if (m_paintContext.mask & (PAINT_SCREEN_TRANSFORMED | PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS)) { + preparePaintGenericScreen(); + } else { + preparePaintSimpleScreen(); + } +} + +static void resetRepaintsHelper(Item *item, SceneView *delegate) +{ + if (delegate->shouldRenderItem(item)) { + item->resetRepaints(delegate); + } + + const auto childItems = item->childItems(); + for (Item *childItem : childItems) { + resetRepaintsHelper(childItem, delegate); + } +} + +static void accumulateRepaints(Item *item, SceneView *view, Region *windowRepaints, Region *accumulatedRepaints, Region *forceTranslucent) +{ + const auto childItems = item->sortedChildItems(); + auto childIt = childItems.begin(); + for (; childIt != childItems.end() && (*childIt)->z() < 0; childIt++) { + accumulateRepaints(*childIt, view, windowRepaints, accumulatedRepaints, forceTranslucent); + } + if (auto background = qobject_cast(item)) { + const Rect viewRect = view->mapToDeviceCoordinates(item->mapToView(item->rect(), view)).rounded(); + if (accumulatedRepaints->intersects(viewRect)) { + *windowRepaints |= viewRect; + *accumulatedRepaints |= viewRect; + if (uint32_t pixels = background->pixelsToExpandRepaintsBelowOpaqueRegions()) { + for (const Rect &rect : accumulatedRepaints->rects()) { + *forceTranslucent |= rect.adjusted(-pixels, -pixels, pixels, pixels) & viewRect; + } + } + } + } else if (view->shouldRenderItem(item)) { + const Region repaints = item->takeDeviceRepaints(view); + *windowRepaints |= repaints; + *accumulatedRepaints |= repaints; + } + + for (; childIt != childItems.end(); childIt++) { + accumulateRepaints(*childIt, view, windowRepaints, accumulatedRepaints, forceTranslucent); + } +} + +void WorkspaceScene::preparePaintGenericScreen() +{ + for (WindowItem *windowItem : std::as_const(stacking_order)) { + resetRepaintsHelper(windowItem, painted_delegate); + + WindowPrePaintData data; + data.mask = m_paintContext.mask; + + effects->prePaintWindow(painted_delegate, windowItem->effectWindow(), data, m_expectedPresentTimestamp); + m_paintContext.phase2Data.append(Phase2Data{ + .item = windowItem, + .deviceRegion = Region::infinite(), + .deviceOpaque = data.deviceOpaque, + .mask = data.mask, + }); + } +} + +static void addOpaqueRegionRecursive(SceneView *view, Item *item, const std::optional &parentCorner, Region &ret) +{ + const std::optional corner = calculateClipCorner(item, parentCorner); + Region opaque = item->opaque(); + if (corner.has_value()) { + opaque = corner->radius.clip(item->opaque(), corner->box); + } + const Rect deviceRect = snapToPixelGrid(view->mapToDeviceCoordinates(item->mapToView(item->rect(), view))); + for (RectF rect : opaque.rects()) { + ret |= snapToPixelGrid(view->mapToDeviceCoordinates(item->mapToView(rect, view))) & deviceRect; + } + const auto children = item->childItems(); + for (Item *child : children) { + addOpaqueRegionRecursive(view, child, corner, ret); + } +} + +void WorkspaceScene::preparePaintSimpleScreen() +{ + for (WindowItem *windowItem : std::as_const(stacking_order)) { + Window *window = windowItem->window(); + WindowPrePaintData data; + data.mask = m_paintContext.mask; + + // Clip out the decoration for opaque windows; the decoration is drawn in the second pass. + if (window->opacity() == 1.0) { + addOpaqueRegionRecursive(painted_delegate, windowItem, std::nullopt, data.deviceOpaque); + } + + effects->prePaintWindow(painted_delegate, windowItem->effectWindow(), data, m_expectedPresentTimestamp); + m_paintContext.phase2Data.append(Phase2Data{ + .item = windowItem, + .deviceRegion = Region{}, + .deviceOpaque = data.deviceOpaque, + .mask = data.mask, + }); + } +} + +Region WorkspaceScene::collectDamage() +{ + if (m_paintContext.mask & (PAINT_SCREEN_TRANSFORMED | PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS)) { + resetRepaintsHelper(m_overlayItem.get(), painted_delegate); + m_paintContext.deviceDamage = painted_delegate->deviceRect(); + return m_paintContext.deviceDamage; + } else { + // collect all damage, from bottom to top + Region accumulatedRepaints; + Region forceTranslucent; + for (auto &data : m_paintContext.phase2Data) { + data.deviceOpaque -= forceTranslucent; + accumulateRepaints(data.item, painted_delegate, &data.deviceRegion, &accumulatedRepaints, &forceTranslucent); + } + accumulateRepaints(m_overlayItem.get(), painted_delegate, &m_paintContext.deviceDamage, &accumulatedRepaints, &forceTranslucent); + + // Perform an occlusion cull pass, to remove surface damage occluded by opaque windows. + Region opaque; + for (auto &paintData : m_paintContext.phase2Data | std::views::reverse) { + m_paintContext.deviceDamage |= paintData.deviceRegion - opaque; + + // TODO make occlusion culling per item, rather than per window + const bool canCover = painted_delegate->shouldRenderItem(paintData.item->surfaceItem()) + || painted_delegate->shouldRenderHole(paintData.item->surfaceItem()); + if (!(paintData.mask & (PAINT_WINDOW_TRANSLUCENT | PAINT_WINDOW_TRANSFORMED)) && canCover) { + opaque += paintData.deviceOpaque; + } + } + + return m_paintContext.deviceDamage & painted_delegate->deviceRect(); + } +} + +void WorkspaceScene::postPaint() +{ + effects->postPaintScreen(); + + painted_delegate = nullptr; + painted_screen = nullptr; + clearStackingOrder(); +} + +void WorkspaceScene::paint(const RenderTarget &renderTarget, const QPoint &deviceOffset, const Region &deviceRegion) +{ + RenderViewport viewport(painted_delegate->viewport(), painted_delegate->scale(), renderTarget, deviceOffset); + + m_renderer->beginFrame(renderTarget, viewport); + + effects->paintScreen(renderTarget, viewport, m_paintContext.mask, deviceRegion, painted_screen); + m_paintScreenCount = 0; + + if (m_overlayItem) { + const Rect bounds = viewport.mapToDeviceCoordinates(m_overlayItem->mapToScene(m_overlayItem->boundingRect())).toRect(); + const Region deviceRepaint = deviceRegion & bounds; + if (!deviceRepaint.isEmpty()) { + m_renderer->renderItem(renderTarget, viewport, m_overlayItem.get(), PAINT_SCREEN_TRANSFORMED, deviceRepaint, WindowPaintData{}, [this](Item *item) { + return !painted_delegate->shouldRenderItem(item); + }, [this](Item *item) { + return painted_delegate->shouldRenderHole(item); + }); + } + } + + Q_EMIT frameRendered(); + m_renderer->endFrame(); +} + +// the function that'll be eventually called by paintScreen() above +void WorkspaceScene::finalPaintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) +{ + m_paintScreenCount++; + if (mask & (PAINT_SCREEN_TRANSFORMED | PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS)) { + paintGenericScreen(renderTarget, viewport, mask, screen); + } else { + paintSimpleScreen(renderTarget, viewport, mask, deviceRegion); + } +} + +// The generic painting code that can handle even transformations. +// It simply paints bottom-to-top. +void WorkspaceScene::paintGenericScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int, LogicalOutput *screen) +{ + if (m_paintContext.mask & PAINT_SCREEN_BACKGROUND_FIRST) { + if (m_paintScreenCount == 1) { + m_renderer->renderBackground(renderTarget, viewport, Region::infinite()); + } + } else { + m_renderer->renderBackground(renderTarget, viewport, Region::infinite()); + } + + for (const Phase2Data &paintData : std::as_const(m_paintContext.phase2Data)) { + paintWindow(renderTarget, viewport, paintData.item, paintData.mask, paintData.deviceRegion); + } +} + +// The optimized case without any transformations at all. +// It can paint only the requested region and can use clipping +// to reduce painting and improve performance. +void WorkspaceScene::paintSimpleScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int, const Region &deviceRegion) +{ + // This is the occlusion culling pass + Region visible = deviceRegion; + for (int i = m_paintContext.phase2Data.size() - 1; i >= 0; --i) { + Phase2Data *data = &m_paintContext.phase2Data[i]; + data->deviceRegion = visible & viewport.deviceRect(); + + if (!(data->mask & PAINT_WINDOW_TRANSFORMED)) { + data->deviceRegion &= viewport.mapToDeviceCoordinatesAligned(data->item->mapToScene(data->item->boundingRect())); + + // TODO change effects API, so occlusion culling is per item, rather than per window + const bool canCover = painted_delegate->shouldRenderItem(data->item->surfaceItem()) + || painted_delegate->shouldRenderHole(data->item->surfaceItem()); + if (!(data->mask & PAINT_WINDOW_TRANSLUCENT) && canCover) { + visible -= data->deviceOpaque; + } + } + } + + m_renderer->renderBackground(renderTarget, viewport, visible); + + for (const Phase2Data &paintData : std::as_const(m_paintContext.phase2Data)) { + paintWindow(renderTarget, viewport, paintData.item, paintData.mask, paintData.deviceRegion); + } +} + +void WorkspaceScene::createStackingOrder() +{ + QList items = m_containerItem->sortedChildItems(); + for (Item *item : std::as_const(items)) { + WindowItem *windowItem = static_cast(item); + if (painted_delegate && painted_delegate->shouldHideWindow(windowItem->window())) { + continue; + } + if (windowItem->isVisible()) { + windowItem->window()->ref(); + stacking_order.append(windowItem); + } + } +} + +void WorkspaceScene::clearStackingOrder() +{ + for (WindowItem *windowItem : std::as_const(stacking_order)) { + windowItem->window()->unref(); + } + stacking_order.clear(); +} + +void WorkspaceScene::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, WindowItem *item, int mask, const Region &deviceRegion) +{ + if (deviceRegion.isEmpty()) { // completely clipped + return; + } + + WindowPaintData data; + effects->paintWindow(renderTarget, viewport, item->effectWindow(), mask, deviceRegion, data); +} + +// the function that'll be eventually called by paintWindow() above +void WorkspaceScene::finalPaintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) +{ + effects->drawWindow(renderTarget, viewport, w, mask, deviceRegion, data); +} + +// will be eventually called from drawWindow() +void WorkspaceScene::finalDrawWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data) +{ + // TODO: Reconsider how the CrossFadeEffect captures the initial window contents to remove + // null pointer delegate checks in "should render item" and "should render hole" checks. + m_renderer->renderItem(renderTarget, viewport, w->windowItem(), mask, deviceRegion, data, [this](Item *item) { + return painted_delegate && !painted_delegate->shouldRenderItem(item); + }, [this](Item *item) { + return painted_delegate && painted_delegate->shouldRenderHole(item); + }); +} + +EglContext *WorkspaceScene::openglContext() const +{ + if (auto eglBackend = qobject_cast(Compositor::self()->backend())) { + return eglBackend->openglContext(); + } + return nullptr; +} + +bool WorkspaceScene::animationsSupported() const +{ + const auto context = openglContext(); + return context && !context->isSoftwareRenderer(); +} + +} // namespace + +#include "moc_workspacescene.cpp" diff --git a/local/recipes/kde/kwin/source/src/scene/workspacescene.h b/local/recipes/kde/kwin/source/src/scene/workspacescene.h new file mode 100644 index 0000000000..26745c83b8 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scene/workspacescene.h @@ -0,0 +1,122 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2006 Lubos Lunak + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "core/renderviewport.h" +#include "scene/scene.h" + +namespace KWin +{ + +class DragAndDropIconItem; +class EffectWindow; +class EglContext; +class Item; +class SurfaceItem; +class WindowItem; +class WindowPaintData; +class CursorItem; + +class KWIN_EXPORT WorkspaceScene : public Scene +{ + Q_OBJECT + +public: + explicit WorkspaceScene(std::unique_ptr renderer); + ~WorkspaceScene() override; + + void initialize(); + + Item *containerItem() const; + Item *overlayItem() const; + Item *cursorItem() const; + + QList scanoutCandidates(ssize_t maxCount) const override; + OverlayCandidates overlayCandidates(ssize_t maxTotalCount, ssize_t maxOverlayCount, ssize_t maxUnderlayCount) const override; + void prePaint(SceneView *delegate) override; + Region collectDamage() override; + void postPaint() override; + void paint(const RenderTarget &renderTarget, const QPoint &deviceOffset, const Region &deviceRegion) override; + void frame(SceneView *delegate, OutputFrame *frame) override; + double desiredHdrHeadroom() const override; + + EglContext *openglContext() const; + + /** + * Whether the Scene is able to drive animations. + * This is used as a hint to the effects system which effects can be supported. + * If the Scene performs software rendering it is supposed to return @c false, + * if rendering is hardware accelerated it should return @c true. + */ + bool animationsSupported() const; + +Q_SIGNALS: + void preFrameRender(); + void frameRendered(); + +protected: + void createStackingOrder(); + void clearStackingOrder(); + friend class EffectsHandler; + // called after all effects had their paintScreen() called + void finalPaintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen); + // shared implementation of painting the screen in the generic + // (unoptimized) way + void preparePaintGenericScreen(); + void paintGenericScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, LogicalOutput *screen); + // shared implementation of painting the screen in an optimized way + void preparePaintSimpleScreen(); + void paintSimpleScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion); + // called after all effects had their paintWindow() called + void finalPaintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data); + // shared implementation, starts painting the window + void paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, WindowItem *w, int mask, const Region &deviceRegion); + // called after all effects had their drawWindow() called + void finalDrawWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceRegion, WindowPaintData &data); + + // saved data for 2nd pass of optimized screen painting + struct Phase2Data + { + WindowItem *item = nullptr; + Region deviceRegion; + Region deviceOpaque; + int mask = 0; + }; + + struct PaintContext + { + Region deviceDamage; + int mask = 0; + QList phase2Data; + }; + + // The screen that is being currently painted + LogicalOutput *painted_screen = nullptr; + SceneView *painted_delegate = nullptr; + + // windows in their stacking order + QList stacking_order; + +private: + void createDndIconItem(); + void destroyDndIconItem(); + void updateCursor(); + + std::chrono::milliseconds m_expectedPresentTimestamp = std::chrono::milliseconds::zero(); + // how many times finalPaintScreen() has been called + int m_paintScreenCount = 0; + PaintContext m_paintContext; + std::unique_ptr m_containerItem; + std::unique_ptr m_overlayItem; + std::unique_ptr m_dndIcon; + std::unique_ptr m_cursorItem; +}; + +} // namespace diff --git a/local/recipes/kde/kwin/source/src/screenedge.cpp b/local/recipes/kde/kwin/source/src/screenedge.cpp new file mode 100644 index 0000000000..6fc4f7e99d --- /dev/null +++ b/local/recipes/kde/kwin/source/src/screenedge.cpp @@ -0,0 +1,1467 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Arthur Arlt + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + Since the functionality provided in this class has been moved from + class Workspace, it is not clear who exactly has written the code. + The list below contains the copyright holders of the class Workspace. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "screenedge.h" + +#include "config-kwin.h" + +#include "core/output.h" +#include "cursor.h" +#include "effect/effecthandler.h" +#include "gestures.h" +#include "main.h" +#include "pointer_input.h" +#include "screenedgegestures.h" +#include "utils/common.h" +#include "virtualdesktops.h" +#include "wayland/seat.h" +#include "wayland_server.h" +#include "window.h" +#include "workspace.h" +// DBus generated +#if KWIN_BUILD_SCREENLOCKER +#include "screenlocker_interface.h" +#endif +// frameworks +#include +// Qt +#include +#include +#include +#include + +using namespace std::chrono_literals; + +namespace KWin +{ + +// Mouse should not move more than this many pixels +static const int DISTANCE_RESET = 30; + +TouchCallback::TouchCallback(QAction *touchUpAction, TouchCallback::CallbackFunction progressCallback) + : m_touchUpAction(touchUpAction) + , m_progressCallback(progressCallback) +{ +} + +TouchCallback::~TouchCallback() +{ +} + +QAction *TouchCallback::touchUpAction() const +{ + return m_touchUpAction; +} + +void TouchCallback::progressCallback(ElectricBorder border, const QPointF &deltaProgress, LogicalOutput *output) const +{ + if (m_progressCallback) { + m_progressCallback(border, deltaProgress, output); + } +} + +bool TouchCallback::hasProgressCallback() const +{ + return m_progressCallback != nullptr; +} + +Edge::Edge(ScreenEdges *parent) + : m_edges(parent) + , m_border(ElectricNone) + , m_action(ElectricActionNone) + , m_reserved(0) + , m_approaching(false) + , m_lastApproachingFactor(0) + , m_blocked(false) + , m_pushBackBlocked(false) + , m_client(nullptr) + , m_output(nullptr) + , m_gesture(std::make_unique(m_edges->gestureRecognizer(), SwipeDirection::Up, RectF())) +{ + connect( + m_gesture.get(), &ScreenEdgeGesture::triggered, this, [this]() { + stopApproaching(); + if (m_client) { + m_client->showOnScreenEdge(); + unreserve(); + return; + } + handleTouchAction(); + handleTouchCallback(); + }, Qt::QueuedConnection); + connect(m_gesture.get(), &ScreenEdgeGesture::started, this, &Edge::startApproaching); + connect(m_gesture.get(), &ScreenEdgeGesture::cancelled, this, &Edge::stopApproaching); + connect(m_gesture.get(), &ScreenEdgeGesture::cancelled, this, [this]() { + if (!m_touchCallbacks.isEmpty() && m_touchCallbacks.constFirst().hasProgressCallback()) { + handleTouchCallback(); + } + }); + connect(m_gesture.get(), &ScreenEdgeGesture::progress, this, [this](const QPointF &delta, qreal progress) { + int factor = progress * 256.0f; + if (m_lastApproachingFactor != factor) { + m_lastApproachingFactor = factor; + Q_EMIT approaching(border(), m_lastApproachingFactor / 256.0f, m_approachGeometry); + } + if (!m_touchCallbacks.isEmpty()) { + m_touchCallbacks.constFirst().progressCallback(border(), delta, m_output); + } + }); + connect(this, &Edge::activatesForTouchGestureChanged, this, [this]() { + if (isReserved()) { + if (activatesForTouchGesture()) { + m_edges->gestureRecognizer()->addGesture(m_gesture.get()); + } else { + m_edges->gestureRecognizer()->removeGesture(m_gesture.get()); + } + } + }); +} + +Edge::~Edge() +{ + stopApproaching(); +} + +void Edge::reserve() +{ + m_reserved++; + if (m_reserved == 1) { + // got activated + activate(); + } +} + +void Edge::reserve(QObject *object, const char *slot) +{ + connect(object, &QObject::destroyed, this, qOverload(&Edge::unreserve)); + m_callBacks.insert(object, QByteArray(slot)); + reserve(); +} + +void Edge::reserveTouchCallBack(QAction *action, TouchCallback::CallbackFunction callback) +{ + if (std::any_of(m_touchCallbacks.constBegin(), m_touchCallbacks.constEnd(), [action](const TouchCallback &c) { + return c.touchUpAction() == action; + })) { + return; + } + reserveTouchCallBack(TouchCallback(action, callback)); +} + +void Edge::reserveTouchCallBack(const TouchCallback &callback) +{ + if (std::any_of(m_touchCallbacks.constBegin(), m_touchCallbacks.constEnd(), [callback](const TouchCallback &c) { + return c.touchUpAction() == callback.touchUpAction(); + })) { + return; + } + const bool wasTouch = activatesForTouchGesture(); + connect(callback.touchUpAction(), &QAction::destroyed, this, [this, callback]() { + unreserveTouchCallBack(callback.touchUpAction()); + }); + m_touchCallbacks << callback; + if (wasTouch != activatesForTouchGesture()) { + Q_EMIT activatesForTouchGestureChanged(); + } + reserve(); +} + +void Edge::unreserveTouchCallBack(QAction *action) +{ + auto it = std::find_if(m_touchCallbacks.begin(), m_touchCallbacks.end(), [action](const TouchCallback &c) { + return c.touchUpAction() == action; + }); + if (it == m_touchCallbacks.end()) { + return; + } + + const bool wasTouch = activatesForTouchGesture(); + m_touchCallbacks.erase(it); + if (wasTouch != activatesForTouchGesture()) { + Q_EMIT activatesForTouchGestureChanged(); + } + unreserve(); +} + +void Edge::unreserve() +{ + m_reserved--; + if (m_reserved == 0) { + // got deactivated + stopApproaching(); + deactivate(); + } +} +void Edge::unreserve(QObject *object) +{ + if (m_callBacks.remove(object) > 0) { + disconnect(object, &QObject::destroyed, this, qOverload(&Edge::unreserve)); + unreserve(); + } +} + +bool Edge::activatesForPointer() const +{ + if (input()->pointer()->isConstrained()) { + return false; + } + + // Most actions do not handle drag and drop properly yet + // but at least allow "show desktop" and "application launcher". + if (waylandServer()->seat()->isDragPointer()) { + if (!m_edges->isDesktopSwitching() && m_action != ElectricActionNone && m_action != ElectricActionShowDesktop && m_action != ElectricActionApplicationLauncher) { + return false; + } + // Don't activate edge when a mouse button is pressed, except when + // moving a window. Dragging a scroll bar all the way to the edge + // shouldn't activate the edge. + } else if (input()->pointer()->areButtonsPressed()) { + auto c = Workspace::self()->moveResizeWindow(); + if (!c || c->isInteractiveResize()) { + return false; + } + } + + if (m_client) { + return true; + } + const bool isMovingWindow = Workspace::self()->moveResizeWindow() && !Workspace::self()->moveResizeWindow()->isInteractiveResize(); + if (m_edges->isDesktopSwitching() || (m_edges->isDesktopSwitchingMovingClients() && isMovingWindow)) { + const bool canSwitch = (isLeft() && VirtualDesktopManager::self()->toLeft(nullptr, options->isRollOverDesktops()) != VirtualDesktopManager::self()->currentDesktop()) + || (isRight() && VirtualDesktopManager::self()->toRight(nullptr, options->isRollOverDesktops()) != VirtualDesktopManager::self()->currentDesktop()) + || (isBottom() && VirtualDesktopManager::self()->below(nullptr, options->isRollOverDesktops()) != VirtualDesktopManager::self()->currentDesktop()) + || (isTop() && VirtualDesktopManager::self()->above(nullptr, options->isRollOverDesktops()) != VirtualDesktopManager::self()->currentDesktop()); + if (canSwitch) { + return true; + } + } + if (!m_callBacks.isEmpty()) { + return true; + } + if (m_action != ElectricActionNone) { + return true; + } + return false; +} + +bool Edge::activatesForTouchGesture() const +{ + if (!isScreenEdge()) { + return false; + } + if (m_blocked) { + return false; + } + if (m_client) { + return true; + } + if (m_touchAction != ElectricActionNone) { + return true; + } + if (!m_touchCallbacks.isEmpty()) { + return true; + } + return false; +} + +bool Edge::triggersFor(const QPoint &cursorPos) const +{ + if (isBlocked()) { + return false; + } + if (!activatesForPointer()) { + return false; + } + if (!m_geometry.contains(cursorPos)) { + return false; + } + if (isLeft() && cursorPos.x() != m_geometry.x()) { + return false; + } + if (isRight() && cursorPos.x() != (m_geometry.x() + m_geometry.width() - 1)) { + return false; + } + if (isTop() && cursorPos.y() != m_geometry.y()) { + return false; + } + if (isBottom() && cursorPos.y() != (m_geometry.y() + m_geometry.height() - 1)) { + return false; + } + return true; +} + +bool Edge::check(const QPoint &cursorPos, const std::chrono::microseconds &triggerTime, bool forceNoPushBack) +{ + if (!triggersFor(cursorPos)) { + if ((cursorPos - m_triggeredPoint).manhattanLength() > DISTANCE_RESET) { + m_lastReset.reset(); + } + return false; + } + if (m_lastTrigger.has_value() && // still in cooldown + (triggerTime - m_lastTrigger.value()) < edges()->reActivationThreshold() - edges()->timeThreshold()) { + // Reset the time, so the user has to actually keep the mouse still for this long to retrigger + m_lastTrigger = triggerTime; + return false; + } + // no pushback so we have to activate at once + bool directActivate = forceNoPushBack || edges()->cursorPushBackDistance().isEmpty(); + if (directActivate || canActivate(cursorPos, triggerTime)) { + markAsTriggered(cursorPos, triggerTime); + handle(cursorPos); + return true; + } else { + pushCursorBack(cursorPos); + m_triggeredPoint = cursorPos; + } + return false; +} + +void Edge::markAsTriggered(const QPoint &cursorPos, const std::chrono::microseconds &triggerTime) +{ + m_lastTrigger = triggerTime; + m_lastReset.reset(); + m_triggeredPoint = cursorPos; +} + +bool Edge::canActivate(const QPoint &cursorPos, const std::chrono::microseconds &triggerTime) +{ + // we check whether either the timer has explicitly been invalidated (successful trigger) or is + // bigger than the reactivation threshold (activation "aborted", usually due to moving away the cursor + // from the corner after successful activation) + // either condition means that "this is the first event in a new attempt" + if (!m_lastReset.has_value() || (triggerTime - m_lastReset.value()) > edges()->reActivationThreshold()) { + m_lastReset = triggerTime; + return false; + } + if (m_lastTrigger.has_value() && (triggerTime - m_lastTrigger.value()) < edges()->reActivationThreshold() - edges()->timeThreshold()) { + return false; + } + if ((triggerTime - m_lastReset.value()) < edges()->timeThreshold()) { + return false; + } + // does the check on position make any sense at all? + if ((cursorPos - m_triggeredPoint).manhattanLength() > DISTANCE_RESET) { + return false; + } + return true; +} + +void Edge::handle(const QPoint &cursorPos) +{ + Window *movingClient = Workspace::self()->moveResizeWindow(); + if ((edges()->isDesktopSwitchingMovingClients() && movingClient && !movingClient->isInteractiveResize()) || (edges()->isDesktopSwitching() && isScreenEdge())) { + // always switch desktops in case: + // moving a Client and option for switch on client move is enabled + // or switch on screen edge is enabled + switchDesktop(cursorPos); + return; + } + if (movingClient) { + // if we are moving a window we don't want to trigger the actions. This just results in + // problems, e.g. Desktop Grid activated or screen locker activated which just cannot + // work as we hold a grab. + return; + } + + if (m_client) { + pushCursorBack(cursorPos); + m_client->showOnScreenEdge(); + unreserve(); + return; + } + + if (handlePointerAction() || handleByCallback()) { + pushCursorBack(cursorPos); + return; + } + if (edges()->isDesktopSwitching() && isCorner()) { + // try again desktop switching for the corner + switchDesktop(cursorPos); + } +} + +bool Edge::handleAction(ElectricBorderAction action) +{ + switch (action) { + case ElectricActionShowDesktop: { + Workspace::self()->setShowingDesktop(!Workspace::self()->showingDesktop()); + return true; + } + case ElectricActionLockScreen: { // Lock the screen +#if KWIN_BUILD_SCREENLOCKER + OrgFreedesktopScreenSaverInterface interface(QStringLiteral("org.freedesktop.ScreenSaver"), + QStringLiteral("/ScreenSaver"), + QDBusConnection::sessionBus()); + if (interface.isValid()) { + interface.Lock(); + } + return true; +#else + return false; +#endif + } + case ElectricActionKRunner: { // open krunner + QDBusConnection::sessionBus().asyncCall( + QDBusMessage::createMethodCall(QStringLiteral("org.kde.krunner"), + QStringLiteral("/App"), + QStringLiteral("org.kde.krunner.App"), + QStringLiteral("display"))); + return true; + } + case ElectricActionActivityManager: { // open activity manager + QDBusConnection::sessionBus().asyncCall( + QDBusMessage::createMethodCall(QStringLiteral("org.kde.plasmashell"), + QStringLiteral("/PlasmaShell"), + QStringLiteral("org.kde.PlasmaShell"), + QStringLiteral("toggleActivityManager"))); + return true; + } + case ElectricActionApplicationLauncher: { + QDBusConnection::sessionBus().asyncCall( + QDBusMessage::createMethodCall(QStringLiteral("org.kde.plasmashell"), + QStringLiteral("/PlasmaShell"), + QStringLiteral("org.kde.PlasmaShell"), + QStringLiteral("activateLauncherMenu"))); + return true; + } + default: + return false; + } +} + +bool Edge::handleByCallback() +{ + if (m_callBacks.isEmpty()) { + return false; + } + for (auto it = m_callBacks.begin(); it != m_callBacks.end(); ++it) { + bool retVal = false; + QMetaObject::invokeMethod(it.key(), it.value().constData(), Q_RETURN_ARG(bool, retVal), Q_ARG(ElectricBorder, m_border)); + if (retVal) { + return true; + } + } + return false; +} + +void Edge::handleTouchCallback() +{ + if (!m_touchCallbacks.isEmpty()) { + m_touchCallbacks.constFirst().touchUpAction()->trigger(); + } +} + +void Edge::switchDesktop(const QPoint &cursorPos) +{ + QPoint pos(cursorPos); + VirtualDesktopManager *vds = VirtualDesktopManager::self(); + VirtualDesktop *oldDesktop = vds->currentDesktop(); + VirtualDesktop *desktop = oldDesktop; + const int OFFSET = 2; + if (isLeft()) { + const VirtualDesktop *interimDesktop = desktop; + desktop = vds->toLeft(desktop, vds->isNavigationWrappingAround()); + if (desktop != interimDesktop) { + pos.setX(workspace()->geometry().width() - 1 - OFFSET); + } + } else if (isRight()) { + const VirtualDesktop *interimDesktop = desktop; + desktop = vds->toRight(desktop, vds->isNavigationWrappingAround()); + if (desktop != interimDesktop) { + pos.setX(OFFSET); + } + } + if (isTop()) { + const VirtualDesktop *interimDesktop = desktop; + desktop = vds->above(desktop, vds->isNavigationWrappingAround()); + if (desktop != interimDesktop) { + pos.setY(workspace()->geometry().height() - 1 - OFFSET); + } + } else if (isBottom()) { + const VirtualDesktop *interimDesktop = desktop; + desktop = vds->below(desktop, vds->isNavigationWrappingAround()); + if (desktop != interimDesktop) { + pos.setY(OFFSET); + } + } + if (Window *c = Workspace::self()->moveResizeWindow()) { + const QList desktops{desktop}; + if (c->rules()->checkDesktops(desktops) != desktops) { + // user attempts to move a client to another desktop where it is ruleforced to not be + return; + } + } + vds->setCurrent(desktop); + if (vds->currentDesktop() != oldDesktop) { + m_pushBackBlocked = true; + input()->pointer()->warp(pos); + auto unblockPush = [this] { + m_pushBackBlocked = false; + }; + QObject::connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::aboutToBlock, this, unblockPush, Qt::SingleShotConnection); + } +} + +void Edge::pushCursorBack(const QPoint &cursorPos) +{ + if (m_pushBackBlocked) { + return; + } + const QSize &distance = edges()->cursorPushBackDistance(); + if (distance.isEmpty()) { + return; + } + + int x = cursorPos.x(); + int y = cursorPos.y(); + if (isLeft()) { + x += distance.width(); + } + if (isRight()) { + x -= distance.width(); + } + if (isTop()) { + y += distance.height(); + } + if (isBottom()) { + y -= distance.height(); + } + input()->pointer()->warp(QPoint(x, y)); +} + +void Edge::setGeometry(const Rect &geometry) +{ + if (m_geometry == geometry) { + return; + } + m_geometry = geometry; + int x = m_geometry.x(); + int y = m_geometry.y(); + int width = m_geometry.width(); + int height = m_geometry.height(); + const int offset = m_edges->cornerOffset(); + if (isCorner()) { + if (isRight()) { + x = x + width - offset; + } + if (isBottom()) { + y = y + height - offset; + } + width = offset; + height = offset; + } else { + if (isLeft()) { + width = offset; + } else if (isRight()) { + x = x + width - offset; + width = offset; + } else if (isTop()) { + height = offset; + } else if (isBottom()) { + y = y + height - offset; + height = offset; + } + } + m_approachGeometry = Rect(x, y, width, height); + + if (isScreenEdge()) { + m_gesture->setGeometry(m_geometry); + } +} + +void Edge::checkBlocking() +{ + Window *client = Workspace::self()->activeWindow(); + const bool newValue = (!m_edges->remainActiveOnFullscreen() || m_client) && client && client->isFullScreen() && client->frameGeometry().contains(m_geometry.center()) && !(effects && effects->hasActiveFullScreenEffect()); + if (newValue == m_blocked) { + return; + } + const bool wasTouch = activatesForTouchGesture(); + m_blocked = newValue; + if (m_blocked && m_approaching) { + stopApproaching(); + } + if (wasTouch != activatesForTouchGesture()) { + Q_EMIT activatesForTouchGestureChanged(); + } +} + +void Edge::activate() +{ + if (activatesForTouchGesture()) { + m_edges->gestureRecognizer()->addGesture(m_gesture.get()); + } +} + +void Edge::deactivate() +{ + m_edges->gestureRecognizer()->removeGesture(m_gesture.get()); +} + +void Edge::startApproaching() +{ + if (m_approaching) { + return; + } + m_approaching = true; + m_lastApproachingFactor = 0; + Q_EMIT approaching(border(), 0.0, m_approachGeometry); +} + +void Edge::stopApproaching() +{ + if (!m_approaching) { + return; + } + m_approaching = false; + m_lastApproachingFactor = 0; + Q_EMIT approaching(border(), 0.0, m_approachGeometry); +} + +void Edge::updateApproaching(const QPointF &point) +{ + if (RectF(approachGeometry()).contains(point)) { + int factor = 0; + const int edgeDistance = m_edges->cornerOffset(); + auto cornerDistance = [=](const QPointF &corner) { + return std::max(std::abs(corner.x() - point.x()), std::abs(corner.y() - point.y())); + }; + constexpr double factorScale = 256; + switch (border()) { + case ElectricTopLeft: + factor = (cornerDistance(approachGeometry().topLeft()) * factorScale) / edgeDistance; + break; + case ElectricTopRight: + factor = (cornerDistance(approachGeometry().topRight()) * factorScale) / edgeDistance; + break; + case ElectricBottomRight: + factor = (cornerDistance(approachGeometry().bottomRight()) * factorScale) / edgeDistance; + break; + case ElectricBottomLeft: + factor = (cornerDistance(approachGeometry().bottomLeft()) * factorScale) / edgeDistance; + break; + case ElectricTop: + factor = (std::abs(point.y() - approachGeometry().y()) * factorScale) / edgeDistance; + break; + case ElectricRight: + factor = (std::abs(point.x() - approachGeometry().right()) * factorScale) / edgeDistance; + break; + case ElectricBottom: + factor = (std::abs(point.y() - approachGeometry().bottom()) * factorScale) / edgeDistance; + break; + case ElectricLeft: + factor = (std::abs(point.x() - approachGeometry().x()) * factorScale) / edgeDistance; + break; + default: + break; + } + factor = factorScale - factor; + if (m_lastApproachingFactor != factor) { + m_lastApproachingFactor = factor; + Q_EMIT approaching(border(), m_lastApproachingFactor / factorScale, m_approachGeometry); + } + } else { + stopApproaching(); + } +} + +void Edge::setBorder(ElectricBorder border) +{ + m_border = border; + switch (m_border) { + case ElectricTop: + m_gesture->setDirection(SwipeDirection::Down); + break; + case ElectricRight: + m_gesture->setDirection(SwipeDirection::Left); + break; + case ElectricBottom: + m_gesture->setDirection(SwipeDirection::Up); + break; + case ElectricLeft: + m_gesture->setDirection(SwipeDirection::Right); + break; + default: + break; + } +} + +void Edge::setTouchAction(ElectricBorderAction action) +{ + const bool wasTouch = activatesForTouchGesture(); + m_touchAction = action; + if (wasTouch != activatesForTouchGesture()) { + Q_EMIT activatesForTouchGestureChanged(); + } +} + +void Edge::setClient(Window *client) +{ + const bool wasTouch = activatesForTouchGesture(); + m_client = client; + if (wasTouch != activatesForTouchGesture()) { + Q_EMIT activatesForTouchGestureChanged(); + } +} + +void Edge::setOutput(LogicalOutput *output) +{ + m_output = output; +} + +LogicalOutput *Edge::output() const +{ + return m_output; +} + +/********************************************************** + * ScreenEdges + *********************************************************/ + +ScreenEdges::ScreenEdges() + : m_desktopSwitching(false) + , m_desktopSwitchingMovingClients(false) + , m_timeThreshold(0) + , m_reactivateThreshold(0) + , m_virtualDesktopLayout({}) + , m_configWatcher(KConfigWatcher::create(kwinApp()->config())) + , m_actionTopLeft(ElectricActionNone) + , m_actionTop(ElectricActionNone) + , m_actionTopRight(ElectricActionNone) + , m_actionRight(ElectricActionNone) + , m_actionBottomRight(ElectricActionNone) + , m_actionBottom(ElectricActionNone) + , m_actionBottomLeft(ElectricActionNone) + , m_actionLeft(ElectricActionNone) + , m_cornerOffset(40) + , m_touchTarget(8) + , m_gestureRecognizer(std::make_unique()) +{ + connect(m_configWatcher.get(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) { + if (group.name() == QLatin1StringView("ScreenEdges") && names.contains(QByteArrayLiteral("TouchTarget"))) { + const int newTouchTarget = group.readEntry("TouchTarget", 8); + if (newTouchTarget != m_touchTarget) { + m_touchTarget = newTouchTarget; + recreateEdges(); + } + } + }); +} + +ScreenEdges::~ScreenEdges() +{ +} + +void ScreenEdges::init() +{ + reconfigure(); + updateLayout(); + recreateEdges(); +} +static ElectricBorderAction electricBorderAction(const QString &name) +{ + QString lowerName = name.toLower(); + if (lowerName == QLatin1StringView("showdesktop")) { + return ElectricActionShowDesktop; + } else if (lowerName == QLatin1StringView("lockscreen")) { + return ElectricActionLockScreen; + } else if (lowerName == QLatin1String("krunner")) { + return ElectricActionKRunner; + } else if (lowerName == QLatin1String("activitymanager")) { + return ElectricActionActivityManager; + } else if (lowerName == QLatin1String("applicationlauncher")) { + return ElectricActionApplicationLauncher; + } + return ElectricActionNone; +} + +void ScreenEdges::reconfigure() +{ + if (!m_config) { + return; + } + KConfigGroup screenEdgesConfig = m_config->group(QStringLiteral("ScreenEdges")); + setRemainActiveOnFullscreen(screenEdgesConfig.readEntry("RemainActiveOnFullscreen", false)); + m_touchTarget = screenEdgesConfig.readEntry("TouchTarget", 8); + + // TODO: migrate settings to a group ScreenEdges + KConfigGroup windowsConfig = m_config->group(QStringLiteral("Windows")); + setTimeThreshold(std::chrono::milliseconds(windowsConfig.readEntry("ElectricBorderDelay", 75))); + setReActivationThreshold(std::max(timeThreshold() + 50ms, std::chrono::milliseconds(windowsConfig.readEntry("ElectricBorderCooldown", 350)))); + int desktopSwitching = windowsConfig.readEntry("ElectricBorders", static_cast(ElectricDisabled)); + if (desktopSwitching == ElectricDisabled) { + setDesktopSwitching(false); + setDesktopSwitchingMovingClients(false); + } else if (desktopSwitching == ElectricMoveOnly) { + setDesktopSwitching(false); + setDesktopSwitchingMovingClients(true); + } else if (desktopSwitching == ElectricAlways) { + setDesktopSwitching(true); + setDesktopSwitchingMovingClients(true); + } + const int pushBack = windowsConfig.readEntry("ElectricBorderPushbackPixels", 1); + m_cursorPushBackDistance = QSize(pushBack, pushBack); + setAllScreenCorners(windowsConfig.readEntry("ElectricBorderAllScreenCorner", true)); + + KConfigGroup borderConfig = m_config->group(QStringLiteral("ElectricBorders")); + setActionForBorder(ElectricTopLeft, &m_actionTopLeft, + electricBorderAction(borderConfig.readEntry("TopLeft", "None"))); + setActionForBorder(ElectricTop, &m_actionTop, + electricBorderAction(borderConfig.readEntry("Top", "None"))); + setActionForBorder(ElectricTopRight, &m_actionTopRight, + electricBorderAction(borderConfig.readEntry("TopRight", "None"))); + setActionForBorder(ElectricRight, &m_actionRight, + electricBorderAction(borderConfig.readEntry("Right", "None"))); + setActionForBorder(ElectricBottomRight, &m_actionBottomRight, + electricBorderAction(borderConfig.readEntry("BottomRight", "None"))); + setActionForBorder(ElectricBottom, &m_actionBottom, + electricBorderAction(borderConfig.readEntry("Bottom", "None"))); + setActionForBorder(ElectricBottomLeft, &m_actionBottomLeft, + electricBorderAction(borderConfig.readEntry("BottomLeft", "None"))); + setActionForBorder(ElectricLeft, &m_actionLeft, + electricBorderAction(borderConfig.readEntry("Left", "None"))); + + borderConfig = m_config->group(QStringLiteral("TouchEdges")); + setActionForTouchBorder(ElectricTop, electricBorderAction(borderConfig.readEntry("Top", "None"))); + setActionForTouchBorder(ElectricRight, electricBorderAction(borderConfig.readEntry("Right", "None"))); + setActionForTouchBorder(ElectricBottom, electricBorderAction(borderConfig.readEntry("Bottom", "None"))); + setActionForTouchBorder(ElectricLeft, electricBorderAction(borderConfig.readEntry("Left", "None"))); +} + +void ScreenEdges::setActionForBorder(ElectricBorder border, ElectricBorderAction *oldValue, ElectricBorderAction newValue) +{ + if (*oldValue == newValue) { + return; + } + if (*oldValue == ElectricActionNone) { + // have to reserve + for (const auto &edge : m_edges) { + if (edge->border() == border) { + edge->reserve(); + } + } + } + if (newValue == ElectricActionNone) { + // have to unreserve + for (const auto &edge : m_edges) { + if (edge->border() == border) { + edge->unreserve(); + } + } + } + *oldValue = newValue; + // update action on all Edges for given border + for (const auto &edge : m_edges) { + if (edge->border() == border) { + edge->setAction(newValue); + } + } +} + +void ScreenEdges::setActionForTouchBorder(ElectricBorder border, ElectricBorderAction newValue) +{ + auto it = m_touchCallbacks.find(border); + ElectricBorderAction oldValue = ElectricActionNone; + if (it != m_touchCallbacks.end()) { + oldValue = it.value(); + } + if (oldValue == newValue) { + return; + } + if (oldValue == ElectricActionNone) { + // have to reserve + for (const auto &edge : m_edges) { + if (edge->border() == border) { + edge->reserve(); + } + } + } + if (newValue == ElectricActionNone) { + // have to unreserve + for (const auto &edge : m_edges) { + if (edge->border() == border) { + edge->unreserve(); + } + } + + m_touchCallbacks.erase(it); + } else { + m_touchCallbacks.insert(border, newValue); + } + // update action on all Edges for given border + for (const auto &edge : m_edges) { + if (edge->border() == border) { + edge->setTouchAction(newValue); + } + } +} + +void ScreenEdges::updateLayout() +{ + const QSize desktopMatrix = VirtualDesktopManager::self()->grid().size(); + Qt::Orientations newLayout = {}; + if (desktopMatrix.width() > 1) { + newLayout |= Qt::Horizontal; + } + if (desktopMatrix.height() > 1) { + newLayout |= Qt::Vertical; + } + if (newLayout == m_virtualDesktopLayout) { + return; + } + if (isDesktopSwitching()) { + reserveDesktopSwitching(false, m_virtualDesktopLayout); + } + m_virtualDesktopLayout = newLayout; + if (isDesktopSwitching()) { + reserveDesktopSwitching(true, m_virtualDesktopLayout); + } +} + +static bool isLeftScreen(const Rect &screen, const Rect &fullArea) +{ + const auto outputs = workspace()->outputs(); + if (outputs.count() == 1) { + return true; + } + if (screen.left() == fullArea.left()) { + return true; + } + // If any other screen has a right edge against our left edge, then this screen is not a left screen + for (const LogicalOutput *output : outputs) { + const Rect otherGeo = output->geometry(); + if (otherGeo == screen) { + // that's our screen to test + continue; + } + if (screen.x() == otherGeo.x() + otherGeo.width() + && screen.y() < otherGeo.y() + otherGeo.height() + && screen.y() + screen.height() > otherGeo.y()) { + // There is a screen to the left + return false; + } + } + // No screen exists to the left, so this is a left screen + return true; +} + +static bool isRightScreen(const Rect &screen, const Rect &fullArea) +{ + const auto outputs = workspace()->outputs(); + if (outputs.count() == 1) { + return true; + } + if (screen.right() == fullArea.right()) { + return true; + } + // If any other screen has any left edge against any of our right edge, then this screen is not a right screen + for (const LogicalOutput *output : outputs) { + const Rect otherGeo = output->geometry(); + if (otherGeo == screen) { + // that's our screen to test + continue; + } + if (screen.x() + screen.width() == otherGeo.x() + && screen.y() < otherGeo.y() + otherGeo.height() + && screen.y() + screen.height() > otherGeo.y()) { + // There is a screen to the right + return false; + } + } + // No screen exists to the right, so this is a right screen + return true; +} + +static bool isTopScreen(const Rect &screen, const Rect &fullArea) +{ + const auto outputs = workspace()->outputs(); + if (outputs.count() == 1) { + return true; + } + if (screen.top() == fullArea.top()) { + return true; + } + // If any other screen has any bottom edge against any of our top edge, then this screen is not a top screen + for (const LogicalOutput *output : outputs) { + const Rect otherGeo = output->geometry(); + if (otherGeo == screen) { + // that's our screen to test + continue; + } + if (screen.y() == otherGeo.y() + otherGeo.height() + && screen.x() < otherGeo.x() + otherGeo.width() + && screen.x() + screen.width() > otherGeo.x()) { + // There is a screen to the top + return false; + } + } + // No screen exists to the top, so this is a top screen + return true; +} + +static bool isBottomScreen(const Rect &screen, const Rect &fullArea) +{ + const auto outputs = workspace()->outputs(); + if (outputs.count() == 1) { + return true; + } + if (screen.bottom() == fullArea.bottom()) { + return true; + } + // If any other screen has any top edge against any of our bottom edge, then this screen is not a bottom screen + for (const LogicalOutput *output : outputs) { + const Rect otherGeo = output->geometry(); + if (otherGeo == screen) { + // that's our screen to test + continue; + } + if (screen.y() + screen.height() == otherGeo.y() + && screen.x() < otherGeo.x() + otherGeo.width() + && screen.x() + screen.width() > otherGeo.x()) { + // There is a screen to the bottom + return false; + } + } + // No screen exists to the bottom, so this is a bottom screen + return true; +} + +bool ScreenEdges::remainActiveOnFullscreen() const +{ + return m_remainActiveOnFullscreen; +} + +void ScreenEdges::recreateEdges() +{ + std::vector> oldEdges = std::move(m_edges); + m_edges.clear(); + const Rect fullArea = workspace()->geometry(); + Region processedRegion; + + const auto outputs = workspace()->outputs(); + for (LogicalOutput *output : outputs) { + const Region screen = Region(output->geometry()).subtracted(processedRegion); + processedRegion += screen; + for (const Rect &screenPart : screen.rects()) { + if (isLeftScreen(screenPart, fullArea)) { + // left most screen + createVerticalEdge(ElectricLeft, screenPart, fullArea, output); + } + if (isRightScreen(screenPart, fullArea)) { + // right most screen + createVerticalEdge(ElectricRight, screenPart, fullArea, output); + } + if (isTopScreen(screenPart, fullArea)) { + // top most screen + createHorizontalEdge(ElectricTop, screenPart, fullArea, output); + } + if (isBottomScreen(screenPart, fullArea)) { + // bottom most screen + createHorizontalEdge(ElectricBottom, screenPart, fullArea, output); + } + } + + if (isAllScreenCorners()) { + const Rect geo = output->geometry(); + m_edges.push_back(createEdge(ElectricTopLeft, geo.x(), geo.y(), m_touchTarget, m_touchTarget, output)); + m_edges.push_back(createEdge(ElectricTopRight, geo.x() + geo.width() - m_touchTarget, geo.y(), m_touchTarget, m_touchTarget, output)); + m_edges.push_back(createEdge(ElectricBottomLeft, geo.x(), geo.y() + geo.height() - m_touchTarget, m_touchTarget, m_touchTarget, output)); + m_edges.push_back(createEdge(ElectricBottomRight, geo.x() + geo.width() - m_touchTarget, geo.y() + geo.height() - m_touchTarget, m_touchTarget, m_touchTarget, output)); + } + } + auto split = std::partition(oldEdges.begin(), oldEdges.end(), [](const auto &edge) { + return !edge->client(); + }); + // copy over the effect/script reservations from the old edges + for (const auto &edge : m_edges) { + for (const auto &oldEdge : std::span(oldEdges.begin(), split)) { + if (oldEdge->border() != edge->border()) { + continue; + } + const QHash &callbacks = oldEdge->callBacks(); + for (auto callback = callbacks.begin(); callback != callbacks.end(); callback++) { + edge->reserve(callback.key(), callback.value().constData()); + } + const auto touchCallBacks = oldEdge->touchCallBacks(); + for (auto c : touchCallBacks) { + edge->reserveTouchCallBack(c); + } + } + } + // copy over the window reservations from the old edges + for (const auto &oldEdge : std::span(split, oldEdges.end())) { + if (!reserve(oldEdge->client(), oldEdge->border())) { + oldEdge->client()->showOnScreenEdge(); + } + } +} + +void ScreenEdges::createVerticalEdge(ElectricBorder border, const Rect &screen, const Rect &fullArea, LogicalOutput *output) +{ + if (border != ElectricRight && border != KWin::ElectricLeft) { + return; + } + int y = screen.y(); + int height = screen.height(); + const int x = (border == ElectricLeft) ? screen.left() : screen.right() - m_touchTarget; + if (!isAllScreenCorners() && isTopScreen(screen, fullArea)) { + // also top most screen + height -= m_cornerOffset; + y += m_cornerOffset; + // create top left/right edge + const ElectricBorder edge = (border == ElectricLeft) ? ElectricTopLeft : ElectricTopRight; + m_edges.push_back(createEdge(edge, x, screen.y(), m_touchTarget, m_touchTarget, output)); + } + if (!isAllScreenCorners() && isBottomScreen(screen, fullArea)) { + // also bottom most screen + height -= m_cornerOffset; + // create bottom left/right edge + const ElectricBorder edge = (border == ElectricLeft) ? ElectricBottomLeft : ElectricBottomRight; + m_edges.push_back(createEdge(edge, x, screen.y() + screen.height() - m_touchTarget, m_touchTarget, m_touchTarget, output)); + } + if (height <= m_cornerOffset) { + // An overlap with another output is near complete. We ignore this border. + return; + } + if (isAllScreenCorners()) { + height -= 2 * m_cornerOffset; + y += m_cornerOffset; + } + m_edges.push_back(createEdge(border, x, y, m_touchTarget, height, output)); +} + +void ScreenEdges::createHorizontalEdge(ElectricBorder border, const Rect &screen, const Rect &fullArea, LogicalOutput *output) +{ + if (border != ElectricTop && border != ElectricBottom) { + return; + } + int x = screen.x(); + int width = screen.width(); + if (!isAllScreenCorners() && isLeftScreen(screen, fullArea)) { + // also left most - adjust only x and width + x += m_cornerOffset; + width -= m_cornerOffset; + } + if (!isAllScreenCorners() && isRightScreen(screen, fullArea)) { + // also right most edge + width -= m_cornerOffset; + } + if (width <= m_cornerOffset) { + // An overlap with another output is near complete. We ignore this border. + return; + } + if (isAllScreenCorners()) { + width -= 2 * m_cornerOffset; + x += m_cornerOffset; + } + const int y = (border == ElectricTop) ? screen.top() : screen.bottom() - m_touchTarget; + m_edges.push_back(createEdge(border, x, y, width, m_touchTarget, output)); +} + +std::unique_ptr ScreenEdges::createEdge(ElectricBorder border, int x, int y, int width, int height, LogicalOutput *output, bool createAction) +{ + std::unique_ptr edge = std::make_unique(this); + // Edges can not have negative size. + Q_ASSERT(width >= 0); + Q_ASSERT(height >= 0); + + edge->setBorder(border); + edge->setGeometry(Rect(x, y, width, height)); + edge->setOutput(output); + if (createAction) { + const ElectricBorderAction action = actionForEdge(edge.get()); + if (action != KWin::ElectricActionNone) { + edge->reserve(); + edge->setAction(action); + } + const ElectricBorderAction touchAction = actionForTouchEdge(edge.get()); + if (touchAction != KWin::ElectricActionNone) { + edge->reserve(); + edge->setTouchAction(touchAction); + } + } + if (isDesktopSwitching()) { + if (edge->isCorner()) { + edge->reserve(); + } else { + if ((m_virtualDesktopLayout & Qt::Horizontal) && (edge->isLeft() || edge->isRight())) { + edge->reserve(); + } + if ((m_virtualDesktopLayout & Qt::Vertical) && (edge->isTop() || edge->isBottom())) { + edge->reserve(); + } + } + } + edge->checkBlocking(); + connect(edge.get(), &Edge::approaching, this, &ScreenEdges::approaching); + return edge; +} + +ElectricBorderAction ScreenEdges::actionForEdge(Edge *edge) const +{ + switch (edge->border()) { + case ElectricTopLeft: + return m_actionTopLeft; + case ElectricTop: + return m_actionTop; + case ElectricTopRight: + return m_actionTopRight; + case ElectricRight: + return m_actionRight; + case ElectricBottomRight: + return m_actionBottomRight; + case ElectricBottom: + return m_actionBottom; + case ElectricBottomLeft: + return m_actionBottomLeft; + case ElectricLeft: + return m_actionLeft; + default: + // fall through + break; + } + return ElectricActionNone; +} + +ElectricBorderAction ScreenEdges::actionForTouchEdge(Edge *edge) const +{ + auto it = m_touchCallbacks.find(edge->border()); + if (it != m_touchCallbacks.end()) { + return it.value(); + } + return ElectricActionNone; +} + +ElectricBorderAction ScreenEdges::actionForTouchBorder(ElectricBorder border) const +{ + return m_touchCallbacks.value(border); +} + +ScreenEdgeGestureRecognizer *ScreenEdges::gestureRecognizer() const +{ + return m_gestureRecognizer.get(); +} + +void ScreenEdges::reserveDesktopSwitching(bool isToReserve, Qt::Orientations o) +{ + if (!o) { + return; + } + for (const auto &edge : m_edges) { + if (edge->isCorner()) { + isToReserve ? edge->reserve() : edge->unreserve(); + } else { + if ((m_virtualDesktopLayout & Qt::Horizontal) && (edge->isLeft() || edge->isRight())) { + isToReserve ? edge->reserve() : edge->unreserve(); + } + if ((m_virtualDesktopLayout & Qt::Vertical) && (edge->isTop() || edge->isBottom())) { + isToReserve ? edge->reserve() : edge->unreserve(); + } + } + } +} + +void ScreenEdges::reserve(ElectricBorder border, QObject *object, const char *slot) +{ + for (const auto &edge : m_edges) { + if (edge->border() == border) { + edge->reserve(object, slot); + } + } +} + +void ScreenEdges::unreserve(ElectricBorder border, QObject *object) +{ + for (const auto &edge : m_edges) { + if (edge->border() == border) { + edge->unreserve(object); + } + } +} + +bool ScreenEdges::reserve(Window *client, ElectricBorder border) +{ + const auto it = std::remove_if(m_edges.begin(), m_edges.end(), [client](const auto &edge) { + return edge->client() == client; + }); + const bool hadBorder = it != m_edges.end(); + m_edges.erase(it, m_edges.end()); + + if (border != ElectricNone) { + return createEdgeForClient(client, border); + } else { + return hadBorder; + } +} + +void ScreenEdges::reserveTouch(ElectricBorder border, QAction *action, TouchCallback::CallbackFunction callback) +{ + for (const auto &edge : m_edges) { + if (edge->border() == border) { + edge->reserveTouchCallBack(action, callback); + } + } +} + +void ScreenEdges::unreserveTouch(ElectricBorder border, QAction *action) +{ + for (const auto &edge : m_edges) { + if (edge->border() == border) { + edge->unreserveTouchCallBack(action); + } + } +} + +bool ScreenEdges::createEdgeForClient(Window *client, ElectricBorder border) +{ + if (client->isDeleted()) { + return false; + } + + int y = 0; + int x = 0; + int width = 0; + int height = 0; + + LogicalOutput *output = client->output(); + const Rect geo = client->frameGeometry().toRect(); + + const Rect screen = output->geometry(); + switch (border) { + case ElectricTop: + y = screen.y(); + x = geo.x(); + height = 1; + width = geo.width(); + break; + case ElectricBottom: + y = screen.y() + screen.height() - 1; + x = geo.x(); + height = 1; + width = geo.width(); + break; + case ElectricLeft: + x = screen.x(); + y = geo.y(); + width = 1; + height = geo.height(); + break; + case ElectricRight: + x = screen.x() + screen.width() - 1; + y = geo.y(); + width = 1; + height = geo.height(); + break; + default: + return false; + } + + const auto &edge = m_edges.emplace_back(createEdge(border, x, y, width, height, output, false)); + edge->setClient(client); + edge->reserve(); + connect(client, &Window::closed, edge.get(), [this, client]() { + deleteEdgeForClient(client); + }); + return true; +} + +void ScreenEdges::deleteEdgeForClient(Window *window) +{ + const auto it = std::remove_if(m_edges.begin(), m_edges.end(), [window](const auto &edge) { + return edge->client() == window; + }); + m_edges.erase(it, m_edges.end()); +} + +bool ScreenEdges::inApproachGeometry(const QPoint &pos) const +{ + for (const auto &edge : m_edges) { + if (edge->approachGeometry().contains(pos)) { + return true; + } + } + return false; +} + +void ScreenEdges::handlePointerMotion(const QPointF &pos, std::chrono::microseconds timestamp) +{ + bool activatedForClient = false; + for (const auto &edge : m_edges) { + if (!edge->isReserved() || edge->isBlocked()) { + continue; + } + if (!edge->activatesForPointer()) { + if (edge->isApproaching()) { + edge->stopApproaching(); + } + continue; + } + if (edge->client() && effects->activeFullScreenEffect()) { + if (edge->isApproaching()) { + edge->stopApproaching(); + } + continue; + } + if (edge->approachGeometry().contains(pos.toPoint())) { + if (!edge->isApproaching()) { + edge->startApproaching(); + } else { + edge->updateApproaching(pos); + } + } else { + if (edge->isApproaching()) { + edge->stopApproaching(); + } + } + // always send event to all edges so that they can update their state + if (edge->check(pos.toPoint(), timestamp)) { + if (edge->client()) { + activatedForClient = true; + } + } + } + if (activatedForClient) { + for (const auto &edge : m_edges) { + if (edge->client()) { + edge->markAsTriggered(pos.toPoint(), timestamp); + } + } + } +} + +void ScreenEdges::setRemainActiveOnFullscreen(bool remainActive) +{ + m_remainActiveOnFullscreen = remainActive; +} + +const std::vector> &ScreenEdges::edges() const +{ + return m_edges; +} + +void ScreenEdges::checkBlocking() +{ + for (const auto &edge : m_edges) { + edge->checkBlocking(); + } +} + +} // namespace + +#include "moc_screenedge.cpp" diff --git a/local/recipes/kde/kwin/source/src/screenedge.h b/local/recipes/kde/kwin/source/src/screenedge.h new file mode 100644 index 0000000000..ea0a8ab24c --- /dev/null +++ b/local/recipes/kde/kwin/source/src/screenedge.h @@ -0,0 +1,604 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2011 Arthur Arlt + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + Since the functionality provided in this class has been moved from + class Workspace, it is not clear who exactly has written the code. + The list below contains the copyright holders of the class Workspace. + + SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich + SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2009 Lucas Murray + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once +// KWin +#include "core/rect.h" +#include "effect/globals.h" +// KDE includes +#include +#include +// Qt +#include +#include + +#include +#include + +class QAction; + +namespace KWin +{ + +class Window; +class LogicalOutput; +class ScreenEdgeGestureRecognizer; +class ScreenEdges; +class ScreenEdgeGesture; + +class TouchCallback +{ +public: + using CallbackFunction = std::function; + explicit TouchCallback(QAction *touchUpAction, TouchCallback::CallbackFunction progressCallback); + ~TouchCallback(); + + QAction *touchUpAction() const; + void progressCallback(ElectricBorder border, const QPointF &deltaProgress, LogicalOutput *output) const; + bool hasProgressCallback() const; + +private: + QAction *m_touchUpAction = nullptr; + TouchCallback::CallbackFunction m_progressCallback; +}; + +class KWIN_EXPORT Edge : public QObject +{ + Q_OBJECT +public: + explicit Edge(ScreenEdges *parent); + ~Edge() override; + bool isLeft() const; + bool isTop() const; + bool isRight() const; + bool isBottom() const; + bool isCorner() const; + bool isScreenEdge() const; + bool triggersFor(const QPoint &cursorPos) const; + bool check(const QPoint &cursorPos, const std::chrono::microseconds &triggerTime, bool forceNoPushBack = false); + void markAsTriggered(const QPoint &cursorPos, const std::chrono::microseconds &triggerTime); + bool isReserved() const; + const Rect &approachGeometry() const; + + ElectricBorder border() const; + void reserve(QObject *object, const char *slot); + const QHash &callBacks() const; + void reserveTouchCallBack(QAction *action, TouchCallback::CallbackFunction callback = nullptr); + void unreserveTouchCallBack(QAction *action); + void startApproaching(); + void stopApproaching(); + bool isApproaching() const; + void setClient(Window *client); + Window *client() const; + void setOutput(LogicalOutput *output); + LogicalOutput *output() const; + const Rect &geometry() const; + void setTouchAction(ElectricBorderAction action); + void checkBlocking(); + + bool activatesForPointer() const; + bool activatesForTouchGesture() const; + +public Q_SLOTS: + void reserve(); + void unreserve(); + void unreserve(QObject *object); + void setBorder(ElectricBorder border); + void setAction(ElectricBorderAction action); + void setGeometry(const Rect &geometry); + void updateApproaching(const QPointF &point); +Q_SIGNALS: + void approaching(ElectricBorder border, qreal factor, const Rect &geometry); + void activatesForTouchGestureChanged(); + +protected: + ScreenEdges *edges(); + const ScreenEdges *edges() const; + bool isBlocked() const; + +private: + void activate(); + void deactivate(); + bool canActivate(const QPoint &cursorPos, const std::chrono::microseconds &triggerTime); + void handle(const QPoint &cursorPos); + bool handleAction(ElectricBorderAction action); + bool handlePointerAction() + { + return handleAction(m_action); + } + bool handleTouchAction() + { + return handleAction(m_touchAction); + } + bool handleByCallback(); + void handleTouchCallback(); + void switchDesktop(const QPoint &cursorPos); + void pushCursorBack(const QPoint &cursorPos); + void reserveTouchCallBack(const TouchCallback &callback); + QList touchCallBacks() const + { + return m_touchCallbacks; + } + ScreenEdges *m_edges; + ElectricBorder m_border; + ElectricBorderAction m_action; + ElectricBorderAction m_touchAction = ElectricActionNone; + int m_reserved; + Rect m_geometry; + Rect m_approachGeometry; + std::optional m_lastTrigger = std::nullopt; + std::optional m_lastReset = std::nullopt; + QPoint m_triggeredPoint; + QHash m_callBacks; + bool m_approaching; + int m_lastApproachingFactor; + bool m_blocked; + bool m_pushBackBlocked; + Window *m_client; + LogicalOutput *m_output; + std::unique_ptr m_gesture; + QList m_touchCallbacks; + friend class ScreenEdges; +}; + +/** + * @short Class for controlling screen edges. + * + * The screen edge functionality is split into three parts: + * @li This manager class ScreenEdges + * @li abstract class @ref Edge + * @li specific implementation of @ref Edge, e.g. WindowBasedEdge + * + * The ScreenEdges creates an @ref Edge for each screen edge which is also an edge in the + * combination of all screens. E.g. if there are two screens, no Edge is created between the screens, + * but at all other edges even if the screens have a different dimension. + * + * In addition at each corner of the overall display geometry an one-pixel large @ref Edge is + * created. No matter how many screens there are, there will only be exactly four of these corner + * edges. This is motivated by Fitts's Law which show that it's easy to trigger such a corner, but + * it would be very difficult to trigger a corner between two screens (one pixel target not visually + * outlined). + * + * The ScreenEdges are used for one of the following functionality: + * @li switch virtual desktop (see property @ref desktopSwitching) + * @li switch virtual desktop when moving a window (see property @ref desktopSwitchingMovingClients) + * @li trigger a pre-defined action (see properties @ref actionTop and similar) + * @li trigger an externally configured action (e.g. Effect, Script, see @ref reserve, @ref unreserve) + * + * An @ref Edge is only active if there is at least one of the possible actions "reserved" for this + * edge. The idea is to not block the screen edge if nothing could be triggered there, so that the + * user e.g. can configure nothing on the top edge, which tends to interfere with full screen apps + * having a hidden panel there. On X11 (currently only supported backend) the @ref Edge is + * represented by a WindowBasedEdge which creates an input only window for the geometry and + * reacts on enter notify events. If the edge gets reserved for the first time a window is created + * and mapped, once the edge gets unreserved again, the window gets destroyed. + * + * When the mouse enters one of the screen edges the following values are used to determine whether + * the action should be triggered or the cursor be pushed back + * @li Time difference between two entering events is not larger than a certain threshold + * @li Time difference between two entering events is larger than @ref timeThreshold + * @li Time difference between two activations is larger than @ref reActivateThreshold + * @li Distance between two enter events is not larger than a defined pixel distance + * These checks are performed in @ref Edge + * + * @todo change way how Effects/Scripts can reserve an edge and are notified. + */ +class KWIN_EXPORT ScreenEdges : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool desktopSwitching READ isDesktopSwitching) + Q_PROPERTY(bool desktopSwitchingMovingClients READ isDesktopSwitchingMovingClients) + Q_PROPERTY(QSize cursorPushBackDistance READ cursorPushBackDistance) + Q_PROPERTY(int actionTopLeft READ actionTopLeft) + Q_PROPERTY(int actionTop READ actionTop) + Q_PROPERTY(int actionTopRight READ actionTopRight) + Q_PROPERTY(int actionRight READ actionRight) + Q_PROPERTY(int actionBottomRight READ actionBottomRight) + Q_PROPERTY(int actionBottom READ actionBottom) + Q_PROPERTY(int actionBottomLeft READ actionBottomLeft) + Q_PROPERTY(int actionLeft READ actionLeft) +public: + explicit ScreenEdges(); + ~ScreenEdges() override; + /** + * @internal + */ + void setConfig(KSharedConfig::Ptr config); + /** + * Initialize the screen edges. + * @internal + */ + void init(); + /** + * Check, if @p pos is in the approach geometry of any edge. + */ + bool inApproachGeometry(const QPoint &pos) const; + /** + * The (dpi dependent) length, reserved for the active corners of each edge - 1/3" + */ + int cornerOffset() const; + /** + * The target size for touch gesture recognition areas + */ + int touchTarget() const; + /** + * Mark the specified screen edge as reserved. This method is provided for external activation + * like effects and scripts. When the effect/script does no longer need the edge it is supposed + * to call @ref unreserve. + * @param border the screen edge to mark as reserved + * @param object The object on which the @p callback needs to be invoked + * @param callback The method name to be invoked - uses QMetaObject::invokeMethod + * @see unreserve + * @todo: add pointer to script/effect + */ + void reserve(ElectricBorder border, QObject *object, const char *callback); + /** + * Mark the specified screen edge as unreserved. This method is provided for external activation + * like effects and scripts. This method is only allowed to be called if @ref reserve had been + * called before for the same @p border. An unbalanced calling of reserve/unreserve leads to the + * edge never being active or never being able to deactivate again. + * @param border the screen edge to mark as unreserved + * @param object the object on which the callback had been invoked + * @see reserve + * @todo: add pointer to script/effect + */ + void unreserve(ElectricBorder border, QObject *object); + /** + * Reserves an edge for the @p client. The idea behind this is to show the @p client if the + * screen edge which the @p client borders gets triggered. + * + * When first called it is tried to create an Edge for the client. This is only done if the + * client borders with a screen edge specified by @p border. If the client doesn't border the + * screen edge, no Edge gets created and the client is shown again. Otherwise there would not + * be a possibility to show the client again. + * + * On subsequent calls for the client no new Edge is created, but the existing one gets reused + * and if the client is already hidden, the Edge gets reserved. + * + * Once the Edge for the client triggers, the client gets shown again and the Edge unreserved. + * The idea is that the Edge can only get activated if the client is currently hidden. + * + * The Edge gets automatically destroyed if the client gets released. + * @param client The Client for which an Edge should be reserved + * @param border The border which the client wants to use, only proper borders are supported (no corners) + */ + bool reserve(KWin::Window *client, ElectricBorder border); + + /** + * Mark the specified screen edge as reserved for touch gestures. This method is provided for + * external activation like effects and scripts. + * When the effect/script does no longer need the edge it is supposed + * to call @ref unreserveTouch. + * @param border the screen edge to mark as reserved + * @param action The action which gets triggered + * @see unreserveTouch + * @since 5.10 + */ + void reserveTouch(ElectricBorder border, QAction *action, TouchCallback::CallbackFunction callback = nullptr); + /** + * Unreserves the specified @p border from activating the @p action for touch gestures. + * @see reserveTouch + * @since 5.10 + */ + void unreserveTouch(ElectricBorder border, QAction *action); + + /** + * Reserve desktop switching for screen edges, if @p isToReserve is @c true. Unreserve otherwise. + * @param isToReserve indicated whether desktop switching should be reserved or unreseved + * @param o Qt orientations + */ + void reserveDesktopSwitching(bool isToReserve, Qt::Orientations o); + void handlePointerMotion(const QPointF &pos, std::chrono::microseconds timestamp); + + bool isDesktopSwitching() const; + bool isDesktopSwitchingMovingClients() const; + bool isAllScreenCorners() const; + const QSize &cursorPushBackDistance() const; + /** + * Minimum time between the push back of the cursor and the activation by re-entering the edge. + */ + std::chrono::milliseconds timeThreshold() const; + /** + * Minimum time between triggers + */ + std::chrono::milliseconds reActivationThreshold() const; + ElectricBorderAction actionTopLeft() const; + ElectricBorderAction actionTop() const; + ElectricBorderAction actionTopRight() const; + ElectricBorderAction actionRight() const; + ElectricBorderAction actionBottomRight() const; + ElectricBorderAction actionBottom() const; + ElectricBorderAction actionBottomLeft() const; + ElectricBorderAction actionLeft() const; + + ElectricBorderAction actionForTouchBorder(ElectricBorder border) const; + + ScreenEdgeGestureRecognizer *gestureRecognizer() const; + + bool remainActiveOnFullscreen() const; + const std::vector> &edges() const; + + void checkBlocking(); + +public Q_SLOTS: + void reconfigure(); + /** + * Updates the layout of virtual desktops and adjust the reserved borders in case of + * virtual desktop switching on edges. + */ + void updateLayout(); + /** + * Recreates all edges e.g. after the screen size changes. + */ + void recreateEdges(); + +Q_SIGNALS: + /** + * Signal emitted during approaching of mouse towards @p border. The @p factor indicates how + * far away the mouse is from the approaching area. The values are clamped into [0.0,1.0] with + * @c 0.0 meaning far away from the border, @c 1.0 in trigger distance. + */ + void approaching(ElectricBorder border, qreal factor, const Rect &geometry); + +private: + enum { + ElectricDisabled = 0, + ElectricMoveOnly = 1, + ElectricAlways = 2, + }; + void setDesktopSwitching(bool enable); + void setDesktopSwitchingMovingClients(bool enable); + void setAllScreenCorners(bool enable); + void setCursorPushBackDistance(const QSize &distance); + void setTimeThreshold(std::chrono::milliseconds threshold); + void setReActivationThreshold(std::chrono::milliseconds threshold); + void createHorizontalEdge(ElectricBorder border, const Rect &screen, const Rect &fullArea, LogicalOutput *output); + void createVerticalEdge(ElectricBorder border, const Rect &screen, const Rect &fullArea, LogicalOutput *output); + std::unique_ptr createEdge(ElectricBorder border, int x, int y, int width, int height, LogicalOutput *output, bool createAction = true); + void setActionForBorder(ElectricBorder border, ElectricBorderAction *oldValue, ElectricBorderAction newValue); + void setActionForTouchBorder(ElectricBorder border, ElectricBorderAction newValue); + void setRemainActiveOnFullscreen(bool remainActive); + ElectricBorderAction actionForEdge(Edge *edge) const; + ElectricBorderAction actionForTouchEdge(Edge *edge) const; + bool createEdgeForClient(Window *client, ElectricBorder border); + void deleteEdgeForClient(Window *client); + bool m_desktopSwitching; + bool m_desktopSwitchingMovingClients; + QSize m_cursorPushBackDistance; + std::chrono::milliseconds m_timeThreshold = std::chrono::milliseconds::zero(); + std::chrono::milliseconds m_reactivateThreshold = std::chrono::milliseconds::zero(); + Qt::Orientations m_virtualDesktopLayout; + std::vector> m_edges; + KSharedConfig::Ptr m_config; + KConfigWatcher::Ptr m_configWatcher; + ElectricBorderAction m_actionTopLeft; + ElectricBorderAction m_actionTop; + ElectricBorderAction m_actionTopRight; + ElectricBorderAction m_actionRight; + ElectricBorderAction m_actionBottomRight; + ElectricBorderAction m_actionBottom; + ElectricBorderAction m_actionBottomLeft; + ElectricBorderAction m_actionLeft; + QMap m_touchCallbacks; + const int m_cornerOffset; + int m_touchTarget; + std::unique_ptr m_gestureRecognizer; + bool m_remainActiveOnFullscreen = false; + bool m_allScreenCorners = true; +}; + +/********************************************************** + * Inlines Edge + *********************************************************/ + +inline bool Edge::isBottom() const +{ + return m_border == ElectricBottom || m_border == ElectricBottomLeft || m_border == ElectricBottomRight; +} + +inline bool Edge::isLeft() const +{ + return m_border == ElectricLeft || m_border == ElectricTopLeft || m_border == ElectricBottomLeft; +} + +inline bool Edge::isRight() const +{ + return m_border == ElectricRight || m_border == ElectricTopRight || m_border == ElectricBottomRight; +} + +inline bool Edge::isTop() const +{ + return m_border == ElectricTop || m_border == ElectricTopLeft || m_border == ElectricTopRight; +} + +inline bool Edge::isCorner() const +{ + return m_border == ElectricTopLeft + || m_border == ElectricTopRight + || m_border == ElectricBottomRight + || m_border == ElectricBottomLeft; +} + +inline bool Edge::isScreenEdge() const +{ + return m_border == ElectricLeft + || m_border == ElectricRight + || m_border == ElectricTop + || m_border == ElectricBottom; +} + +inline bool Edge::isReserved() const +{ + return m_reserved != 0; +} + +inline void Edge::setAction(ElectricBorderAction action) +{ + m_action = action; +} + +inline ScreenEdges *Edge::edges() +{ + return m_edges; +} + +inline const ScreenEdges *Edge::edges() const +{ + return m_edges; +} + +inline const Rect &Edge::geometry() const +{ + return m_geometry; +} + +inline const Rect &Edge::approachGeometry() const +{ + return m_approachGeometry; +} + +inline ElectricBorder Edge::border() const +{ + return m_border; +} + +inline const QHash &Edge::callBacks() const +{ + return m_callBacks; +} + +inline bool Edge::isBlocked() const +{ + return m_blocked; +} + +inline Window *Edge::client() const +{ + return m_client; +} + +inline bool Edge::isApproaching() const +{ + return m_approaching; +} + +/********************************************************** + * Inlines ScreenEdges + *********************************************************/ +inline void ScreenEdges::setConfig(KSharedConfig::Ptr config) +{ + m_config = config; +} + +inline int ScreenEdges::cornerOffset() const +{ + return m_cornerOffset; +} + +inline int ScreenEdges::touchTarget() const +{ + return m_touchTarget; +} + +inline const QSize &ScreenEdges::cursorPushBackDistance() const +{ + return m_cursorPushBackDistance; +} + +inline bool ScreenEdges::isDesktopSwitching() const +{ + return m_desktopSwitching; +} + +inline bool ScreenEdges::isDesktopSwitchingMovingClients() const +{ + return m_desktopSwitchingMovingClients; +} + +inline bool ScreenEdges::isAllScreenCorners() const +{ + return m_allScreenCorners; +} + +inline std::chrono::milliseconds ScreenEdges::reActivationThreshold() const +{ + return m_reactivateThreshold; +} + +inline std::chrono::milliseconds ScreenEdges::timeThreshold() const +{ + return m_timeThreshold; +} + +inline void ScreenEdges::setCursorPushBackDistance(const QSize &distance) +{ + m_cursorPushBackDistance = distance; +} + +inline void ScreenEdges::setDesktopSwitching(bool enable) +{ + if (enable == m_desktopSwitching) { + return; + } + m_desktopSwitching = enable; + reserveDesktopSwitching(enable, m_virtualDesktopLayout); +} + +inline void ScreenEdges::setDesktopSwitchingMovingClients(bool enable) +{ + m_desktopSwitchingMovingClients = enable; +} + +inline void ScreenEdges::setAllScreenCorners(bool enable) +{ + if (enable == m_allScreenCorners) { + return; + } + m_allScreenCorners = enable; + recreateEdges(); +} + +inline void ScreenEdges::setReActivationThreshold(std::chrono::milliseconds threshold) +{ + Q_ASSERT(threshold >= m_timeThreshold); + m_reactivateThreshold = threshold; +} + +inline void ScreenEdges::setTimeThreshold(std::chrono::milliseconds threshold) +{ + m_timeThreshold = threshold; +} + +#define ACTION(name) \ + inline ElectricBorderAction ScreenEdges::name() const \ + { \ + return m_##name; \ + } + +ACTION(actionTopLeft) +ACTION(actionTop) +ACTION(actionTopRight) +ACTION(actionRight) +ACTION(actionBottomRight) +ACTION(actionBottom) +ACTION(actionBottomLeft) +ACTION(actionLeft) + +#undef ACTION + +} diff --git a/local/recipes/kde/kwin/source/src/scripting/dbuscall.cpp b/local/recipes/kde/kwin/source/src/scripting/dbuscall.cpp new file mode 100644 index 0000000000..8fa8428cc9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scripting/dbuscall.cpp @@ -0,0 +1,50 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "dbuscall.h" +#include "scriptingutils.h" + +#include +#include +#include + +namespace KWin +{ + +DBusCall::DBusCall(QObject *parent) + : QObject(parent) +{ +} + +DBusCall::~DBusCall() +{ +} + +void DBusCall::call() +{ + QDBusMessage msg = QDBusMessage::createMethodCall(m_service, m_path, m_interface, m_method); + msg.setArguments(m_arguments); + + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(msg), this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher]() { + watcher->deleteLater(); + if (watcher->isError()) { + Q_EMIT failed(); + return; + } + QVariantList reply = watcher->reply().arguments(); + std::for_each(reply.begin(), reply.end(), [](QVariant &variant) { + variant = dbusToVariant(variant); + }); + Q_EMIT finished(reply); + }); +} + +} // KWin + +#include "moc_dbuscall.cpp" diff --git a/local/recipes/kde/kwin/source/src/scripting/dbuscall.h b/local/recipes/kde/kwin/source/src/scripting/dbuscall.h new file mode 100644 index 0000000000..24f80ee921 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scripting/dbuscall.h @@ -0,0 +1,189 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include +#include + +namespace KWin +{ + +/*! + * \qmltype DBusCall + * \inqmlmodule org.kde.kwin + * + * \brief Qml export for providing a wrapper for sending a message over the DBus + * session bus. + * + * Allows to setup the connection arguments just like in QDBusMessage and supports + * adding arguments to the call. To invoke the message use the slot call(). + * + * If the call succeeds the signal finished() is emitted, if the call fails + * the signal failed() is emitted. + * + * Note: the DBusCall always uses the session bus and performs an async call. + * + * Example on how to use in Qml: + * \code + * DBusCall { + * id: dbus + * service: "org.kde.KWin" + * path: "/KWin" + * method: "nextDesktop" + * Component.onCompleted: dbus.call() + * } + * \endcode + * + * Example with arguments: + * \code + * DBusCall { + * id: dbus + * service: "org.kde.KWin" + * path: "/KWin" + * method: "setCurrentDesktop" + * arguments: [1] + * Component.onCompleted: dbus.call() + * } + * \endcode + * + * Example with a callback: + * \code + * DBusCall { + * id: dbus + * service: "org.kde.KWin" + * path: "/KWin" + * method: "currentDesktop" + * onFinished: console.log(returnValue[0]) + * } + * \endcode + */ +class DBusCall : public QObject +{ + Q_OBJECT + + /*! + * \qmlproperty string DBusCall::service + * + * This property specifies the dbus service name of the remote object. (service, path, dbusInterface, method) + * tuplet forms the destination address. + */ + Q_PROPERTY(QString service READ service WRITE setService NOTIFY serviceChanged) + + /*! + * \qmlproperty string DBusCall::path + * + * This property specifies the dbus object path. (service, path, dbusInterface, method) tuplet + * forms the destination address. + */ + Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) + + /*! + * \qmlproperty string DBusCall::dbusInterface + * + * This property specifies the dbus interface. (service, path, dbusInterface, method) tuplet forms + * the destination address. + */ + Q_PROPERTY(QString dbusInterface READ interface WRITE setInterface NOTIFY interfaceChanged) + + /*! + * \qmlproperty string DBusCall::method + * + * This property specifies the name of the method. (service, path, dbusInterface, method) tuplet + * forms the destination address. + */ + Q_PROPERTY(QString method READ method WRITE setMethod NOTIFY methodChanged) + + /*! + * \qmlproperty string DBusCall::arguments + * + * This property specifies the arguments that will be passed to the request. + */ + Q_PROPERTY(QVariantList arguments READ arguments WRITE setArguments NOTIFY argumentsChanged) +public: + explicit DBusCall(QObject *parent = nullptr); + ~DBusCall() override; + + const QString &service() const; + const QString &path() const; + const QString &interface() const; + const QString &method() const; + const QVariantList &arguments() const; + +public Q_SLOTS: + /*! + * Calls the specified method asynchronously. If the method call succeeds, the finished() signal + * will be emitted. If the method call fails, the failed() signal will be emitted. + * + * \sa finished(), failed() + */ + void call(); + + void setService(const QString &service); + void setPath(const QString &path); + void setInterface(const QString &interface); + void setMethod(const QString &method); + void setArguments(const QVariantList &arguments); + +Q_SIGNALS: + /*! + * \qmlsignal DBusCall::finished(list returnValue) + * + * This signal is emitted if a dbus method call finishes successfully. The \a returnValue + * specifies an optional return value. + */ + void finished(QVariantList returnValue); + + /*! + * \qmlsignal DBusCall::failed() + * + * This signal is emitted if a dbus method call fails. + */ + void failed(); + + void serviceChanged(); + void pathChanged(); + void interfaceChanged(); + void methodChanged(); + void argumentsChanged(); + +private: + QString m_service; + QString m_path; + QString m_interface; + QString m_method; + QVariantList m_arguments; +}; + +#define GENERIC_WRAPPER(type, name, upperName) \ + inline type DBusCall::name() const \ + { \ + return m_##name; \ + } \ + inline void DBusCall::set##upperName(type name) \ + { \ + if (m_##name == name) { \ + return; \ + } \ + m_##name = name; \ + Q_EMIT name##Changed(); \ + } +#define WRAPPER(name, upperName) \ + GENERIC_WRAPPER(const QString &, name, upperName) + +WRAPPER(interface, Interface) +WRAPPER(method, Method) +WRAPPER(path, Path) +WRAPPER(service, Service) + +GENERIC_WRAPPER(const QVariantList &, arguments, Arguments) +#undef WRAPPER +#undef GENERIC_WRAPPER + +} // KWin diff --git a/local/recipes/kde/kwin/source/src/scripting/desktopbackgrounditem.cpp b/local/recipes/kde/kwin/source/src/scripting/desktopbackgrounditem.cpp new file mode 100644 index 0000000000..f0a876bba1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scripting/desktopbackgrounditem.cpp @@ -0,0 +1,126 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "desktopbackgrounditem.h" +#include "core/output.h" +#include "window.h" +#if KWIN_BUILD_ACTIVITIES +#include "activities.h" +#endif +#include "core/outputbackend.h" +#include "main.h" +#include "scripting_logging.h" +#include "virtualdesktops.h" +#include "workspace.h" + +namespace KWin +{ + +DesktopBackgroundItem::DesktopBackgroundItem(QQuickItem *parent) + : WindowThumbnailItem(parent) +{ +} + +void DesktopBackgroundItem::componentComplete() +{ + WindowThumbnailItem::componentComplete(); + updateWindow(); +} + +QString DesktopBackgroundItem::outputName() const +{ + return m_output ? m_output->name() : QString(); +} + +void DesktopBackgroundItem::setOutputName(const QString &name) +{ + setOutput(workspace()->findOutput(name)); +} + +LogicalOutput *DesktopBackgroundItem::output() const +{ + return m_output; +} + +void DesktopBackgroundItem::setOutput(LogicalOutput *output) +{ + if (m_output != output) { + m_output = output; + updateWindow(); + Q_EMIT outputChanged(); + } +} + +VirtualDesktop *DesktopBackgroundItem::desktop() const +{ + return m_desktop; +} + +void DesktopBackgroundItem::setDesktop(VirtualDesktop *desktop) +{ + if (m_desktop != desktop) { + m_desktop = desktop; + updateWindow(); + Q_EMIT desktopChanged(); + } +} + +QString DesktopBackgroundItem::activity() const +{ + return m_activity; +} + +void DesktopBackgroundItem::setActivity(const QString &activity) +{ + if (m_activity != activity) { + m_activity = activity; + updateWindow(); + Q_EMIT activityChanged(); + } +} + +void DesktopBackgroundItem::updateWindow() +{ + if (!isComponentComplete()) { + return; + } + + if (Q_UNLIKELY(!m_output)) { + qCWarning(KWIN_SCRIPTING) << "DesktopBackgroundItem.output is required"; + return; + } + + VirtualDesktop *desktop = m_desktop; + if (!desktop) { + desktop = VirtualDesktopManager::self()->currentDesktop(); + } + + QString activity = m_activity; + if (activity.isEmpty()) { +#if KWIN_BUILD_ACTIVITIES + activity = Workspace::self()->activities()->current(); +#endif + } + + Window *clientCandidate = nullptr; + + const auto clients = workspace()->windows(); + for (Window *client : clients) { + if (client->isDesktop() && client->isOnOutput(m_output) && client->isOnDesktop(desktop) && client->isOnActivity(activity)) { + // In the unlikely event there are multiple desktop windows (e.g. conky's floating panel is of type "desktop") + // choose the one which matches the output size, if possible. + if (!clientCandidate || client->size() == m_output->geometry().size()) { + clientCandidate = client; + } + } + } + + setClient(clientCandidate); +} + +} // namespace KWin + +#include "moc_desktopbackgrounditem.cpp" diff --git a/local/recipes/kde/kwin/source/src/scripting/desktopbackgrounditem.h b/local/recipes/kde/kwin/source/src/scripting/desktopbackgrounditem.h new file mode 100644 index 0000000000..c5cb6f7711 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scripting/desktopbackgrounditem.h @@ -0,0 +1,90 @@ +/* + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "windowthumbnailitem.h" + +namespace KWin +{ + +class LogicalOutput; +class VirtualDesktop; + +/*! + * \qmltype DesktopBackground + * \inqmlmodule org.kde.kwin + * \brief The DesktopBackgroundItem type represents a desktop wallpaper window. + * + * The DesktopBackgroundItem type is a convenience helper that represents the desktop + * background on the specified screen, virtual desktop, and activity. + */ +class DesktopBackgroundItem : public WindowThumbnailItem +{ + Q_OBJECT + /*! + * \qmlproperty string DesktopBackground::outputName + * + * This property specifies the output name of the desktop wallpaper. Either the output or the + * outputName property must be set; otherwise no desktop background will be displayed. + */ + Q_PROPERTY(QString outputName READ outputName WRITE setOutputName NOTIFY outputChanged) + + /*! + * \qmlproperty LogicalOutput DesktopBackground::output + * + * This property specifies the output of the desktop wallpaper. Either the output or the outputName + * property must be set; otherwise no desktop background will be displayed. + */ + Q_PROPERTY(KWin::LogicalOutput *output READ output WRITE setOutput NOTIFY outputChanged) + + /*! + * \qmlproperty string DesktopBackground::activity + * + * This property specifies the activity of the desktop wallpaper. If it's not explicitly set + * to any value, the first desktop background on the specified output will be used. + */ + Q_PROPERTY(QString activity READ activity WRITE setActivity NOTIFY activityChanged) + + /*! + * \qmlproperty VirtualDesktop DesktopBackground::desktop + * + * This property specifies the virtual desktop of the desktop wallpaper. If it's not explicitly + * set to any value, the first desktop background on the specified output will be used. + */ + Q_PROPERTY(KWin::VirtualDesktop *desktop READ desktop WRITE setDesktop NOTIFY desktopChanged) + +public: + explicit DesktopBackgroundItem(QQuickItem *parent = nullptr); + + void componentComplete() override; + + QString outputName() const; + void setOutputName(const QString &name); + + LogicalOutput *output() const; + void setOutput(LogicalOutput *output); + + VirtualDesktop *desktop() const; + void setDesktop(VirtualDesktop *desktop); + + QString activity() const; + void setActivity(const QString &activity); + +Q_SIGNALS: + void outputChanged(); + void desktopChanged(); + void activityChanged(); + +private: + void updateWindow(); + + LogicalOutput *m_output = nullptr; + VirtualDesktop *m_desktop = nullptr; + QString m_activity; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scripting/documentation-effect-global.xml b/local/recipes/kde/kwin/source/src/scripting/documentation-effect-global.xml new file mode 100644 index 0000000000..0339f44768 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scripting/documentation-effect-global.xml @@ -0,0 +1,197 @@ + + + + + + + Global + Methods and properties added to the global JavaScript object. + + + KWin::EffectsHandler + + + effects + + Global property to the core wrapper of KWin Effects + + + KWin::ScriptedEffect + + + effect + + Global property to the actual Effect + + + object + + + Effect + + Provides access to enums defined in KWin::AnimationEffect and KWin::ScriptedEffect + + + object + + + KWin + + Provides access to enums defined in KWin::WorkspaceWrapper + + + object + + + QEasingCurve + + Provides access to enums defined in QEasingCurve + + + + + Q_SCRIPTABLE QList<quint64> + QList<quint64> KWin::ScriptedEffect::animate + (settings) + animate + + +Schedules one or many animations for one window. The animations are defined through the settings object providing +a more declarative way to specify the animations than the animate call on the effect object. The settings object +supports the following attributes: +<syntaxhighlight lang="javascript"> +{ + window: EffectWindow, /* the window to animate, required */ + duration: int, /* duration in msec, required */ + curve: QEasingCurve.Type, /* global easing curve, optional */ + type: Effect.Attribute, /* for first animation, optional */ + from: FPx2, /* for first animation, optional */ + to: FPx2, /* for first animation, optional */ + delay: int, /* for first animation, optional */ + shader: int, /* for first animation, optional */ + animations: [ /* additional animations, optional */ + { + curve: QEasingCurve.Type, /* overrides global */ + type: Effect.Attribute, + from: FPx2, + to: FPx2, + delay: int, + shader: int + } + ] +} +</syntaxhighlight> +At least one animation or attribute setter (see below) needs to be specified either with the top-level properties or in the animations list. + + + + Q_SCRIPTABLE QList<quint64> + QList<quint64> KWin::ScriptedEffect::set + (settings) + set + + +Like animate, just that the manipulation does not implicitly end with the animation. You have to explicitly cancel it. +Until then, the manipulated attribute will remain at animation target value. + + + + Q_SCRIPTABLE bool + bool KWin::ScriptedEffect::cancel + (QList<quint64>) + cancel + + +Cancel one or more present animations caused and returned by KWin::ScriptedEffect::animate or KWin::ScriptedEffect::set. +For convenience you can pass a single quint64 as well. + + + + Q_SCRIPTABLE void + void KWin::ScriptedEffect::print + (QVariant ... values) + print + + Prints all provided values to kDebug and as a D-Bus signal + + + Q_SCRIPTABLE int + int KWin::ScriptedEffect::animationTime + (int duration) + animationTime + + Adjusts the passed in duration to the global animation time factor. + + + Q_SCRIPTABLE int + int KWin::ScriptedEffect::displayWidth + () + displayWidth + + Width of the complete display (all screens). + + + Q_SCRIPTABLE int + int KWin::ScriptedEffect::displayHeight + () + displayHeight + + Height of the complete display (all screens). + + + Q_SCRIPTABLE bool + bool KWin::ScriptedEffect::registerScreenEdge + (ElectricBorder border, QScriptValue callback) + registerScreenEdge + + Registers the callback for the screen edge. When the mouse gets pushed against the given edge the callback will be invoked. + + + Q_SCRIPTABLE bool + bool KWin::ScriptedEffect::registerShortcut + (QString title, QString text, QString keySequence, QScriptValue callback) + registerShortcut + + Registers keySequence as a global shortcut. When the shortcut is invoked the callback will be called. Title and text are used to name the shortcut and make it available to the global shortcut configuration module. + + + Q_SCRIPTABLE uint + uint KWin::ScriptedEffect::addFragmentShader + (ShaderTrait traits, QString fragmentShaderFile) + addFragmentShader + + Creates a shader and returns an identifier which can be used in animate or set. The shader sources must be provided in the shaders sub-directory of the contents package directory. The fragment shader needs to have the file extension frag. Each shader should be provided in a GLSL 1.10 and GLSL 1.40 variant. The 1.40 variant needs to have a suffix _core. E.g. there should be a shader myCustomShader.frag and myCustomShader_core.frag. The vertex shader is generated from the ShaderTrait. The ShaderTrait enum can be used as flags in this method. + + + Q_SCRIPTABLE uint + void KWin::ScriptedEffect::setUniform + (uint shaderId, QString name, QJSValue value) + setUniform + + Updates the uniform value of the uniform identified by @p name for the shader identified by @p shaderId. The @p value can be a floating point numeric value (integer uniform values are not supported), an array with either 2, 3 or 4 numeric values, a string to identify a color or a variant value to identify a color as returned by readConfig. This method can be used to update the state of the shader when the configuration of the effect changed. + + + + + KWin::FPx2 + This class is used to describe the animation end points, that is from which FPx2 values to which FPx2 values an animation goes. This class contains two properties to describe two animation components individually (e.g. width and height). But it's also possible to just have one value (e.g. opacity). In this case the definition of an FPx2 can be replaced by a single value. + + + qreal + + + value1 + + + + + qreal + + + value2 + + + + + + diff --git a/local/recipes/kde/kwin/source/src/scripting/documentation-global.xml b/local/recipes/kde/kwin/source/src/scripting/documentation-global.xml new file mode 100644 index 0000000000..e0a2c3d0af --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scripting/documentation-global.xml @@ -0,0 +1,144 @@ + + + + + + + Global + Methods and properties added to the global JavaScript object. + + + KWin::Options + + + options + + Global property to all configuration values of KWin core. + + + KWin::Workspace + + + workspace + + Global property to the core wrapper of KWin. + + + object + + + KWin + + Provides access to enums defined in KWin::WorkspaceWrapper + + + + + Q_SCRIPTABLE void + void KWin::Scripting::print + (QVariant ... values) + print + + Prints all provided values to kDebug and as a D-Bus signal + + + Q_SCRIPTABLE QVariant + QVariant KWin::Scripting::readConfig + (QString key, QVariant defaultValue = QVariant()) + readConfig + + Reads the config value for key in the Script's configuration with the optional default value. If not providing a default value and no value stored in the configuration an undefined value is returned. + + + Q_SCRIPTABLE bool + bool KWin::Scripting::registerScreenEdge + (ElectricBorder border, QScriptValue callback) + registerScreenEdge + + Registers the callback for the screen edge. When the mouse gets pushed against the given edge the callback will be invoked. + Scripts can also add "X-KWin-Border-Activate" to their metadata file to have the effect listed in the screen edges KCM. This will write an entry BorderConfig= in the script configuration object with a list of ScreenEdges the user has selected. + + + + Q_SCRIPTABLE bool + bool KWin::Scripting::unregisterScreenEdge + (ElectricBorder border) + unregisterScreenEdge + + Unregisters the callback for the screen edge. This will disconnect all callbacks from this script to that edge. + + + Q_SCRIPTABLE bool + bool KWin::Scripting::registerShortcut + (QString title, QString text, QString keySequence, QScriptValue callback) + registerShortcut + + Registers keySequence as a global shortcut. When the shortcut is invoked the callback will be called. Title and text are used to name the shortcut and make it available to the global shortcut configuration module. + + + Q_SCRIPTABLE bool + bool KWin::Scripting::assert + (bool value, QString message = QString()) + assert + + Aborts the execution of the script if value does not evaluate to true. If message is provided an error is thrown with the given message, if not provided an error with default message is thrown. + + + Q_SCRIPTABLE bool + bool KWin::Scripting::assertTrue + (bool value, QString message = QString()) + assertTrue + + Aborts the execution of the script if value does not evaluate to true. If message is provided an error is thrown with the given message, if not provided an error with default message is thrown. + + + Q_SCRIPTABLE bool + bool KWin::Scripting::assertFalse + (bool value, QString message = QString()) + assertFalse + + Aborts the execution of the script if value does not evaluate to false. If message is provided an error is thrown with the given message, if not provided an error with default message is thrown. + + + Q_SCRIPTABLE bool + bool KWin::Scripting::assertEquals + (QVariant expected, QVariant actual, QString message = QString()) + assertEquals + + Aborts the execution of the script if the actual value is not equal to the expected value. If message is provided an error is thrown with the given message, if not provided an error with default message is thrown. + + + Q_SCRIPTABLE bool + bool KWin::Scripting::assertNull + (QVariant value, QString message = QString()) + assertNull + + Aborts the execution of the script if value is not null. If message is provided an error is thrown with the given message, if not provided an error with default message is thrown. + + + Q_SCRIPTABLE bool + bool KWin::Scripting::assertNotNull + (QVariant value, QString message = QString()) + assertNotNull + + Aborts the execution of the script if value is null. If message is provided an error is thrown with the given message, if not provided an error with default message is thrown. + + + Q_SCRIPTABLE void + void KWin::Scripting::callDBus + (QString service, QString path, QString interface, QString method, QVariant arg..., QScriptValue callback = QScriptValue()) + callDBus + + Call a D-Bus method at (service, path, interface and method). A variable number of arguments can be added to the method call. The D-Bus call is always performed in an async way invoking the callback provided as the last (optional) argument. The reply values of the D-Bus method call are passed to the callback. + + + Q_SCRIPTABLE void + void KWin::Scripting::registerUserActionsMenu + (QScriptValue callback) + registerUserActionsMenu + + Registers the passed in callback to be invoked whenever the User actions menu (Alt+F3 or right click on window decoration) is about to be shown. The callback is invoked with a reference to the Client for which the menu is shown. The callback can return either a single menu entry to be added to the menu or its own sub menu with multiple entries. The object for a menu entry should be {title: "My Menu entry", checkable: true, checked: false, triggered: function (action) { // callback with triggered QAction}}, for a menu it should be {title: "My menu", items: [{...}, {...}, ...] /*list with entries as described*/} + + + + diff --git a/local/recipes/kde/kwin/source/src/scripting/gesturehandler.cpp b/local/recipes/kde/kwin/source/src/scripting/gesturehandler.cpp new file mode 100644 index 0000000000..15195604c6 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scripting/gesturehandler.cpp @@ -0,0 +1,182 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "gesturehandler.h" +#include "gestures.h" +#include "globalshortcuts.h" +#include "input.h" + +namespace KWin +{ + +SwipeGestureHandler::SwipeGestureHandler(QObject *parent) + : QObject(parent) +{ +} + +SwipeGestureHandler::~SwipeGestureHandler() +{ +} + +void SwipeGestureHandler::classBegin() +{ +} + +void SwipeGestureHandler::componentComplete() +{ + m_gesture = std::make_unique(m_fingerCount); + m_gesture->setDirection(SwipeDirection(m_direction)); + + connect(m_gesture.get(), &SwipeGesture::triggered, this, &SwipeGestureHandler::activated); + connect(m_gesture.get(), &SwipeGesture::cancelled, this, &SwipeGestureHandler::cancelled); + connect(m_gesture.get(), &SwipeGesture::progress, this, &SwipeGestureHandler::setProgress); + + switch (m_deviceType) { + case Device::Touchpad: + input()->shortcuts()->registerTouchpadSwipe(m_gesture.get()); + break; + case Device::Touchscreen: + input()->shortcuts()->registerTouchscreenSwipe(m_gesture.get()); + break; + } +} + +SwipeGestureHandler::Direction SwipeGestureHandler::direction() const +{ + return m_direction; +} + +void SwipeGestureHandler::setDirection(Direction direction) +{ + if (m_direction != direction) { + m_direction = direction; + Q_EMIT directionChanged(); + } +} + +int SwipeGestureHandler::fingerCount() const +{ + return m_fingerCount; +} + +void SwipeGestureHandler::setFingerCount(int fingerCount) +{ + if (m_fingerCount != fingerCount) { + m_fingerCount = fingerCount; + Q_EMIT fingerCountChanged(); + } +} + +qreal SwipeGestureHandler::progress() const +{ + return m_progress; +} + +void SwipeGestureHandler::setProgress(qreal progress) +{ + if (m_progress != progress) { + m_progress = progress; + Q_EMIT progressChanged(); + } +} + +SwipeGestureHandler::Device SwipeGestureHandler::deviceType() const +{ + return m_deviceType; +} + +void SwipeGestureHandler::setDeviceType(Device device) +{ + if (m_deviceType != device) { + m_deviceType = device; + Q_EMIT deviceTypeChanged(); + } +} + +PinchGestureHandler::PinchGestureHandler(QObject *parent) + : QObject(parent) +{ +} + +PinchGestureHandler::~PinchGestureHandler() +{ +} + +void PinchGestureHandler::classBegin() +{ +} + +void PinchGestureHandler::componentComplete() +{ + m_gesture = std::make_unique(m_fingerCount); + m_gesture->setDirection(PinchDirection(m_direction)); + + connect(m_gesture.get(), &PinchGesture::triggered, this, &PinchGestureHandler::activated); + connect(m_gesture.get(), &PinchGesture::cancelled, this, &PinchGestureHandler::cancelled); + connect(m_gesture.get(), &PinchGesture::progress, this, &PinchGestureHandler::setProgress); + + switch (m_deviceType) { + case Device::Touchpad: + input()->shortcuts()->registerTouchpadPinch(m_gesture.get()); + break; + } +} + +PinchGestureHandler::Direction PinchGestureHandler::direction() const +{ + return m_direction; +} + +void PinchGestureHandler::setDirection(Direction direction) +{ + if (m_direction != direction) { + m_direction = direction; + Q_EMIT directionChanged(); + } +} + +int PinchGestureHandler::fingerCount() const +{ + return m_fingerCount; +} + +void PinchGestureHandler::setFingerCount(int fingerCount) +{ + if (m_fingerCount != fingerCount) { + m_fingerCount = fingerCount; + Q_EMIT fingerCountChanged(); + } +} + +qreal PinchGestureHandler::progress() const +{ + return m_progress; +} + +void PinchGestureHandler::setProgress(qreal progress) +{ + if (m_progress != progress) { + m_progress = progress; + Q_EMIT progressChanged(); + } +} + +PinchGestureHandler::Device PinchGestureHandler::deviceType() const +{ + return m_deviceType; +} + +void PinchGestureHandler::setDeviceType(Device device) +{ + if (m_deviceType != device) { + m_deviceType = device; + Q_EMIT deviceTypeChanged(); + } +} + +} // namespace KWin + +#include "moc_gesturehandler.cpp" diff --git a/local/recipes/kde/kwin/source/src/scripting/gesturehandler.h b/local/recipes/kde/kwin/source/src/scripting/gesturehandler.h new file mode 100644 index 0000000000..f896bde51e --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scripting/gesturehandler.h @@ -0,0 +1,265 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +class QAction; + +namespace KWin +{ + +class PinchGesture; +class SwipeGesture; + +/*! + * \qmltype SwipeGestureHandler + * \inqmlmodule org.kde.kwin + * + * \brief The SwipeGestureHandler type provides a way to handle global swipe gestures. + * + * Example usage: + * \code + * SwipeGestureHandler { + * direction: SwipeGestureHandler.Direction.Up + * fingerCount: 3 + * onActivated: console.log("activated") + * } + * \endcode + */ +class SwipeGestureHandler : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + /*! + * \qmlproperty enumeration SwipeGestureHandler::direction + * \qmlenumeratorsfrom KWin::SwipeGestureHandler::Direction + * + * This property specifies the direction of the swipe gesture. The default value is + * Direction::Invalid. + */ + Q_PROPERTY(Direction direction READ direction WRITE setDirection NOTIFY directionChanged) + + /*! + * \qmlproperty int SwipeGestureHandler::fingerCount + * + * This property specifies the required number of fingers for swipe recognition. + */ + Q_PROPERTY(int fingerCount READ fingerCount WRITE setFingerCount NOTIFY fingerCountChanged) + + /*! + * \qmlproperty real SwipeGestureHandler::progress + * + * This property specifies the progress of the swipe gesture. The progress ranges from + * 0.0 to 1.0. + */ + Q_PROPERTY(qreal progress READ progress NOTIFY progressChanged) + + /*! + * \qmlproperty enumeration SwipeGestureHandler::deviceType + * \qmlenumeratorsfrom KWin::SwipeGestureHandler::Device + * + * This property specifies the input device that can trigger the swipe gesture. + */ + Q_PROPERTY(Device deviceType READ deviceType WRITE setDeviceType NOTIFY deviceTypeChanged) + +public: + explicit SwipeGestureHandler(QObject *parent = nullptr); + ~SwipeGestureHandler() override; + + // Matches SwipeDirection. + /*! + * The Direction type specifies the direction of the swipe gesture. + * + * \value Invalid No direction. + * \value Down Swipe downward. + * \value Left Swipe to the left. + * \value Up Swipe upward. + * \value Right Swipe to the right. + */ + enum class Direction { + Invalid, + Down, + Left, + Up, + Right, + }; + Q_ENUM(Direction) + + /*! + * The Device type specifies the input device that triggers the swipe gesture. + * + * \value Touchpad The gesture is triggered by a touchpad input device. + * \value Touchscreen The gesture is triggered by a touchscreen input device. + */ + enum class Device { + Touchpad, + Touchscreen, + }; + Q_ENUM(Device) + + void classBegin() override; + void componentComplete() override; + + Direction direction() const; + void setDirection(Direction direction); + + int fingerCount() const; + void setFingerCount(int fingerCount); + + qreal progress() const; + void setProgress(qreal progress); + + Device deviceType() const; + void setDeviceType(Device device); + +Q_SIGNALS: + /*! + * \qmlsignal SwipeGestureHandler::activated() + * + * This signal is emitted when the swipe gesture is triggered, i.e. the progress() reaches 1.0. + */ + void activated(); + + /*! + * \qmlsignal SwipeGestureHandler::cancelled() + * + * This signal is emitted when the swipe gesture is cancelled. A swipe gesture can be cancelled + * if the user lifts their fingers or moves the fingers in a different direction, etc. + */ + void cancelled(); + + void progressChanged(); + void directionChanged(); + void fingerCountChanged(); + void deviceTypeChanged(); + +private: + std::unique_ptr m_gesture; + Direction m_direction = Direction::Invalid; + Device m_deviceType = Device::Touchpad; + qreal m_progress = 0; + int m_fingerCount = 3; +}; + +/*! + * \qmltype PinchGestureHandler + * \inqmlmodule org.kde.kwin + * + * \brief The PinchGestureHandler type provides a way to handle global pinch gestures. + * + * Example usage: + * \code + * PinchGestureHandler { + * direction: PinchGestureHandler.Direction.Contracting + * fingerCount: 3 + * onActivated: console.log("activated") + * } + * \endcode + */ +class PinchGestureHandler : public QObject, public QQmlParserStatus +{ + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + + /*! + * \qmlproperty enumeration PinchGestureHandler::direction + * \qmlenumeratorsfrom KWin::PinchGestureHandler::Direction + * + * This property specifies whether the fingers should contract or expand. The default + * value is Direction::Contracting. + */ + Q_PROPERTY(Direction direction READ direction WRITE setDirection NOTIFY directionChanged) + + /*! + * \qmlproperty int PinchGestureHandler::fingerCount + * + * This property specifies the required number of fingers for pinch recognition. + */ + Q_PROPERTY(int fingerCount READ fingerCount WRITE setFingerCount NOTIFY fingerCountChanged) + + /*! + * \qmlproperty real PinchGestureHandler::progress + * + * This property specifies the progress of the pinch gesture. The progress ranges from + * 0.0 to 1.0. + */ + Q_PROPERTY(qreal progress READ progress NOTIFY progressChanged) + +public: + explicit PinchGestureHandler(QObject *parent = nullptr); + ~PinchGestureHandler() override; + + // Matches PinchDirection. + /*! + * The Direction type specifies whether fingers should contract or expand. + * + * \value Expanding Fingers should expand for the pinch gesture. + * \value Contracting Fingers should contract for the pinch gesture. + */ + enum class Direction { + Expanding, + Contracting, + }; + Q_ENUM(Direction) + + /*! + * The Device type specifies the input device that triggers the pinch gesture. + * + * \value Touchpad The pinch gesture is triggered by a touchpad input device. + */ + enum class Device { + Touchpad, + }; + Q_ENUM(Device) + + void classBegin() override; + void componentComplete() override; + + Direction direction() const; + void setDirection(Direction direction); + + int fingerCount() const; + void setFingerCount(int fingerCount); + + qreal progress() const; + void setProgress(qreal progress); + + Device deviceType() const; + void setDeviceType(Device device); + +Q_SIGNALS: + /*! + * \qmlsignal PinchGestureHandler::activated() + * + * This signal is emitted when the pinch gesture is triggered, i.e. the progress() reaches 1.0. + */ + void activated(); + + /*! + * \qmlsignal PinchGestureHandler::cancelled() + * + * This signal is emitted when the pinch gesture is cancelled. A pinch gesture can be cancelled + * if the user lifts their fingers or moves the fingers in a different direction, etc. + */ + void cancelled(); + + void progressChanged(); + void directionChanged(); + void fingerCountChanged(); + void deviceTypeChanged(); + +private: + std::unique_ptr m_gesture; + Direction m_direction = Direction::Contracting; + Device m_deviceType = Device::Touchpad; + qreal m_progress = 0; + int m_fingerCount = 3; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scripting/org.kde.kwin.Script.xml b/local/recipes/kde/kwin/source/src/scripting/org.kde.kwin.Script.xml new file mode 100644 index 0000000000..d22c282149 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scripting/org.kde.kwin.Script.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/local/recipes/kde/kwin/source/src/scripting/screenedgehandler.cpp b/local/recipes/kde/kwin/source/src/scripting/screenedgehandler.cpp new file mode 100644 index 0000000000..16938bfce1 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scripting/screenedgehandler.cpp @@ -0,0 +1,112 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "screenedgehandler.h" + +#include "config-kwin.h" + +#include "screenedge.h" +#include "workspace.h" + +#include + +namespace KWin +{ + +ScreenEdgeHandler::ScreenEdgeHandler(QObject *parent) + : QObject(parent) + , m_enabled(true) + , m_edge(NoEdge) + , m_action(new QAction(this)) +{ + connect(m_action, &QAction::triggered, this, &ScreenEdgeHandler::activated); +} + +ScreenEdgeHandler::~ScreenEdgeHandler() +{ +} + +void ScreenEdgeHandler::setEnabled(bool enabled) +{ + if (m_enabled == enabled) { + return; + } + disableEdge(); + m_enabled = enabled; + enableEdge(); + Q_EMIT enabledChanged(); +} + +void ScreenEdgeHandler::setEdge(Edge edge) +{ + if (m_edge == edge) { + return; + } + disableEdge(); + m_edge = edge; + enableEdge(); + Q_EMIT edgeChanged(); +} + +void ScreenEdgeHandler::enableEdge() +{ + if (!m_enabled || m_edge == NoEdge) { + return; + } + switch (m_mode) { + case Mode::Pointer: + workspace()->screenEdges()->reserve(static_cast(m_edge), this, "borderActivated"); + break; + case Mode::Touch: + workspace()->screenEdges()->reserveTouch(static_cast(m_edge), m_action); + break; + default: + Q_UNREACHABLE(); + } +} + +void ScreenEdgeHandler::disableEdge() +{ + if (!m_enabled || m_edge == NoEdge) { + return; + } + switch (m_mode) { + case Mode::Pointer: + workspace()->screenEdges()->unreserve(static_cast(m_edge), this); + break; + case Mode::Touch: + workspace()->screenEdges()->unreserveTouch(static_cast(m_edge), m_action); + break; + default: + Q_UNREACHABLE(); + } +} + +bool ScreenEdgeHandler::borderActivated(ElectricBorder edge) +{ + if (edge != static_cast(m_edge) || !m_enabled) { + return false; + } + Q_EMIT activated(); + return true; +} + +void ScreenEdgeHandler::setMode(Mode mode) +{ + if (m_mode == mode) { + return; + } + disableEdge(); + m_mode = mode; + enableEdge(); + Q_EMIT modeChanged(); +} + +} // namespace + +#include "moc_screenedgehandler.cpp" diff --git a/local/recipes/kde/kwin/source/src/scripting/screenedgehandler.h b/local/recipes/kde/kwin/source/src/scripting/screenedgehandler.h new file mode 100644 index 0000000000..184756b8f3 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scripting/screenedgehandler.h @@ -0,0 +1,150 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2013 Martin Gräßlin + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include "effect/globals.h" +#include + +class QAction; + +namespace KWin +{ + +/*! + * \qmltype ScreenEdgeHandler + * \inqmlmodule org.kde.kwin + * + * \brief Qml export for reserving a Screen Edge. + * + * The edge is controlled by the enabled property and the edge + * property. If the edge is enabled and gets triggered the activated() + * signal gets emitted. + * + * Example usage: + * \code + * ScreenEdgeHandler { + * edge: ScreenEdgeHandler.LeftEdge + * onActivated: doSomething() + * } + * \endcode + */ +class ScreenEdgeHandler : public QObject +{ + Q_OBJECT + /*! + * \qmlproperty bool ScreenEdgeHandler::enabled + * + * Whether the edge is currently enabled, that is reserved. Default value is \c true. + */ + Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged) + /*! + * \qmlproperty enumeration ScreenEdgeHandler::edge + * \qmlenumeratorsfrom KWin::ScreenEdgeHandler::Edge + * + * Which of the screen edges is to be reserved. Default value is NoEdge. + */ + Q_PROPERTY(Edge edge READ edge WRITE setEdge NOTIFY edgeChanged) + /*! + * \qmlproperty enumeration ScreenEdgeHandler::mode + * \qmlenumeratorsfrom KWin::ScreenEdgeHandler::Mode + * The operation mode for this edge. Default value is Mode::Pointer + */ + Q_PROPERTY(Mode mode READ mode WRITE setMode NOTIFY modeChanged) +public: + /*! + * The Edge type specifies a resesrved screen edge. + * + * \value TopEdge The top edge of the screen. + * \value TopRightEdge The top right corner of the screen. + * \value RightEdge The right edge of the screen. + * \value BottomRightEdge The bottom right corner of the screen. + * \value BottomEdge The bottom edge of the screen. + * \value BottomLeftEdge The bottom left corner of the screen. + * \value LeftEdge The left edge of the screen. + * \value TopLeftEdge The top left corner of the screen. + * \omitvalue EDGE_COUNT + * \value NoEdge No edge. + */ + enum Edge { + TopEdge, + TopRightEdge, + RightEdge, + BottomRightEdge, + BottomEdge, + BottomLeftEdge, + LeftEdge, + TopLeftEdge, + EDGE_COUNT, + NoEdge + }; + Q_ENUM(Edge) + /*! + * Enum describing the operation modes of the edge. + * \value Pointer The screen edge is triggered by a pointer input device. + * \value Touch The screen edge is triggered by a touch input device. + */ + enum class Mode { + Pointer, + Touch + }; + Q_ENUM(Mode) + explicit ScreenEdgeHandler(QObject *parent = nullptr); + ~ScreenEdgeHandler() override; + bool isEnabled() const; + Edge edge() const; + Mode mode() const + { + return m_mode; + } + +public Q_SLOTS: + void setEnabled(bool enabled); + void setEdge(Edge edge); + void setMode(Mode mode); + +Q_SIGNALS: + void enabledChanged(); + void edgeChanged(); + void modeChanged(); + + /*! + * \qmlsignal ScreenEdgeHandler::activated() + * + * This signal is emitted when the screen edge is activated by the user. + * + * The way how the screen edge gets activated depends on the mode(). For example, with + * Mode::Pointer, the screen edge will be activated when the pointer hits the screen edge; + * with Mode::Touch, the screen edge will be activated when user swipes their fingers from + * the corresponding screen border towards the center of the screen. + */ + void activated(); + +private Q_SLOTS: + bool borderActivated(ElectricBorder edge); + +private: + void enableEdge(); + void disableEdge(); + bool m_enabled; + Edge m_edge; + Mode m_mode = Mode::Pointer; + QAction *m_action; +}; + +inline bool ScreenEdgeHandler::isEnabled() const +{ + return m_enabled; +} + +inline ScreenEdgeHandler::Edge ScreenEdgeHandler::edge() const +{ + return m_edge; +} + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scripting/scriptedeffect.cpp b/local/recipes/kde/kwin/source/src/scripting/scriptedeffect.cpp new file mode 100644 index 0000000000..5f4fae55b0 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scripting/scriptedeffect.cpp @@ -0,0 +1,881 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Martin Gräßlin + SPDX-FileCopyrightText: 2018 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scriptedeffect.h" +#include "opengl/glshader.h" +#include "opengl/glshadermanager.h" +#include "scripting_logging.h" +#include "workspace_wrapper.h" + +#include "core/output.h" +#include "effect/effecthandler.h" +#include "input.h" +#include "screenedge.h" +#include "workspace.h" +// KDE +#include +#include +#include +#include +// Qt +#include +#include +#include +#include +#include + +#include + +Q_DECLARE_METATYPE(KSharedConfigPtr) + +namespace KWin +{ + +struct AnimationSettings +{ + enum { + Type = 1 << 0, + Curve = 1 << 1, + Delay = 1 << 2, + Duration = 1 << 3, + FullScreen = 1 << 4, + KeepAlive = 1 << 5, + FrozenTime = 1 << 6 + }; + AnimationEffect::Attribute type; + QEasingCurve::Type curve; + QJSValue from; + QJSValue to; + int delay; + qint64 frozenTime; + uint duration; + uint set; + uint metaData; + bool fullScreenEffect; + bool keepAlive; + std::optional shader; +}; + +AnimationSettings animationSettingsFromObject(const QJSValue &object) +{ + AnimationSettings settings; + settings.set = 0; + settings.metaData = 0; + + settings.to = object.property(QStringLiteral("to")); + settings.from = object.property(QStringLiteral("from")); + + const QJSValue duration = object.property(QStringLiteral("duration")); + if (duration.isNumber()) { + settings.duration = duration.toUInt(); + settings.set |= AnimationSettings::Duration; + } else { + settings.duration = 0; + } + + const QJSValue delay = object.property(QStringLiteral("delay")); + if (delay.isNumber()) { + settings.delay = delay.toInt(); + settings.set |= AnimationSettings::Delay; + } else { + settings.delay = 0; + } + + const QJSValue curve = object.property(QStringLiteral("curve")); + if (curve.isNumber()) { + settings.curve = static_cast(curve.toInt()); + settings.set |= AnimationSettings::Curve; + } else { + settings.curve = QEasingCurve::Linear; + } + + const QJSValue type = object.property(QStringLiteral("type")); + if (type.isNumber()) { + settings.type = static_cast(type.toInt()); + settings.set |= AnimationSettings::Type; + } else { + settings.type = static_cast(-1); + } + + const QJSValue isFullScreen = object.property(QStringLiteral("fullScreen")); + if (isFullScreen.isBool()) { + settings.fullScreenEffect = isFullScreen.toBool(); + settings.set |= AnimationSettings::FullScreen; + } else { + settings.fullScreenEffect = false; + } + + const QJSValue keepAlive = object.property(QStringLiteral("keepAlive")); + if (keepAlive.isBool()) { + settings.keepAlive = keepAlive.toBool(); + settings.set |= AnimationSettings::KeepAlive; + } else { + settings.keepAlive = true; + } + + const QJSValue frozenTime = object.property(QStringLiteral("frozenTime")); + if (frozenTime.isNumber()) { + settings.frozenTime = frozenTime.toInt(); + settings.set |= AnimationSettings::FrozenTime; + } else { + settings.frozenTime = -1; + } + + if (const auto shader = object.property(QStringLiteral("fragmentShader")); shader.isNumber()) { + settings.shader = shader.toUInt(); + } + + typedef QMap MetaTypeMap; + static MetaTypeMap metaTypes({{AnimationEffect::SourceAnchor, QStringLiteral("sourceAnchor")}, + {AnimationEffect::TargetAnchor, QStringLiteral("targetAnchor")}, + {AnimationEffect::RelativeSourceX, QStringLiteral("relativeSourceX")}, + {AnimationEffect::RelativeSourceY, QStringLiteral("relativeSourceY")}, + {AnimationEffect::RelativeTargetX, QStringLiteral("relativeTargetX")}, + {AnimationEffect::RelativeTargetY, QStringLiteral("relativeTargetY")}, + {AnimationEffect::Axis, QStringLiteral("axis")}}); + + for (auto it = metaTypes.constBegin(); it != metaTypes.constEnd(); ++it) { + QJSValue metaVal = object.property(*it); + if (metaVal.isNumber()) { + AnimationEffect::setMetaData(it.key(), metaVal.toInt(), settings.metaData); + } + } + + return settings; +} + +static KWin::FPx2 fpx2FromScriptValue(const QJSValue &value) +{ + if (value.isNull()) { + return FPx2(); + } + if (value.isNumber()) { + return FPx2(value.toNumber()); + } + if (value.isObject()) { + const QJSValue value1 = value.property(QStringLiteral("value1")); + const QJSValue value2 = value.property(QStringLiteral("value2")); + if (!value1.isNumber() || !value2.isNumber()) { + qCDebug(KWIN_SCRIPTING) << "Cannot cast scripted FPx2 to C++"; + return FPx2(); + } + return FPx2(value1.toNumber(), value2.toNumber()); + } + return FPx2(); +} + +ScriptedEffect *ScriptedEffect::create(const KPluginMetaData &effect) +{ + const QString name = effect.pluginId(); + QString scriptFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kwin-wayland/effects/") + name + QLatin1String("/contents/code/main.js")); + if (scriptFile.isEmpty()) { + scriptFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kwin/effects/") + name + QLatin1String("/contents/code/main.js")); + if (scriptFile.isEmpty()) { + qCDebug(KWIN_SCRIPTING) << "Could not locate effect script" << name; + return nullptr; + } + } + + return ScriptedEffect::create(name, scriptFile, effect.value(QStringLiteral("X-KDE-Ordering"), 0), effect.value(QStringLiteral("X-KWin-Exclusive-Category"))); +} + +ScriptedEffect *ScriptedEffect::create(const QString &effectName, const QString &pathToScript, int chainPosition, const QString &exclusiveCategory) +{ + ScriptedEffect *effect = new ScriptedEffect(); + effect->m_exclusiveCategory = exclusiveCategory; + if (!effect->init(effectName, pathToScript)) { + delete effect; + return nullptr; + } + effect->m_chainPosition = chainPosition; + + return effect; +} + +bool ScriptedEffect::supported() +{ + return effects->animationsSupported(); +} + +ScriptedEffect::ScriptedEffect() + : AnimationEffect() + , m_engine(new QJSEngine(this)) + , m_scriptFile(QString()) + , m_config(nullptr) + , m_chainPosition(0) +{ + Q_ASSERT(effects); + connect(effects, &EffectsHandler::activeFullScreenEffectChanged, this, [this]() { + Effect *fullScreenEffect = effects->activeFullScreenEffect(); + if (fullScreenEffect == m_activeFullScreenEffect) { + return; + } + if (m_activeFullScreenEffect == this || fullScreenEffect == this) { + Q_EMIT isActiveFullScreenEffectChanged(); + } + m_activeFullScreenEffect = fullScreenEffect; + }); +} + +ScriptedEffect::~ScriptedEffect() = default; + +bool ScriptedEffect::init(const QString &effectName, const QString &pathToScript) +{ + qRegisterMetaType(); + qRegisterMetaType>(); + + QFile scriptFile(pathToScript); + if (!scriptFile.open(QIODevice::ReadOnly)) { + qCDebug(KWIN_SCRIPTING) << "Could not open script file: " << pathToScript; + return false; + } + m_effectName = effectName; + m_scriptFile = pathToScript; + + // does the effect contain an KConfigXT file? + QString kconfigXTFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kwin-wayland/effects/") + m_effectName + QLatin1String("/contents/config/main.xml")); + if (kconfigXTFile.isNull()) { + kconfigXTFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kwin/effects/") + m_effectName + QLatin1String("/contents/config/main.xml")); + } + if (!kconfigXTFile.isNull()) { + KConfigGroup cg = QCoreApplication::instance()->property("config").value()->group(QStringLiteral("Effect-%1").arg(m_effectName)); + QFile xmlFile(kconfigXTFile); + m_config = new KConfigLoader(cg, &xmlFile, this); + m_config->load(); + } + + m_engine->installExtensions(QJSEngine::ConsoleExtension); + + QJSValue globalObject = m_engine->globalObject(); + + QJSValue effectsObject = m_engine->newQObject(effects); + QJSEngine::setObjectOwnership(effects, QJSEngine::CppOwnership); + globalObject.setProperty(QStringLiteral("effects"), effectsObject); + + QJSValue selfObject = m_engine->newQObject(this); + QJSEngine::setObjectOwnership(this, QJSEngine::CppOwnership); + globalObject.setProperty(QStringLiteral("effect"), selfObject); + + globalObject.setProperty(QStringLiteral("Effect"), + m_engine->newQMetaObject(&ScriptedEffect::staticMetaObject)); + globalObject.setProperty(QStringLiteral("KWin"), + m_engine->newQMetaObject(&QtScriptWorkspaceWrapper::staticMetaObject)); + globalObject.setProperty(QStringLiteral("Globals"), + m_engine->newQMetaObject(&KWin::staticMetaObject)); + globalObject.setProperty(QStringLiteral("QEasingCurve"), + m_engine->newQMetaObject(&QEasingCurve::staticMetaObject)); + + static const QStringList globalProperties{ + QStringLiteral("animationTime"), + QStringLiteral("displayWidth"), + QStringLiteral("displayHeight"), + + QStringLiteral("registerShortcut"), + QStringLiteral("registerScreenEdge"), + QStringLiteral("registerRealtimeScreenEdge"), + QStringLiteral("registerTouchScreenEdge"), + QStringLiteral("unregisterScreenEdge"), + QStringLiteral("unregisterTouchScreenEdge"), + + QStringLiteral("animate"), + QStringLiteral("set"), + QStringLiteral("retarget"), + QStringLiteral("freezeInTime"), + QStringLiteral("redirect"), + QStringLiteral("complete"), + QStringLiteral("cancel"), + QStringLiteral("addShader"), + QStringLiteral("setUniform"), + }; + + for (const QString &propertyName : globalProperties) { + globalObject.setProperty(propertyName, selfObject.property(propertyName)); + } + + const QJSValue result = m_engine->evaluate(QString::fromUtf8(scriptFile.readAll())); + + if (result.isError()) { + qCWarning(KWIN_SCRIPTING, "%s:%d: error: %s", qPrintable(scriptFile.fileName()), + result.property(QStringLiteral("lineNumber")).toInt(), + qPrintable(result.property(QStringLiteral("message")).toString())); + return false; + } + + return true; +} + +void ScriptedEffect::animationEnded(KWin::EffectWindow *w, Attribute a, uint meta) +{ + AnimationEffect::animationEnded(w, a, meta); + Q_EMIT animationEnded(w, 0); +} + +QString ScriptedEffect::pluginId() const +{ + return m_effectName; +} + +bool ScriptedEffect::isActiveFullScreenEffect() const +{ + return effects->activeFullScreenEffect() == this; +} + +QList ScriptedEffect::touchEdgesForAction(const QString &action) const +{ + QList ret; + if (m_exclusiveCategory == QLatin1StringView("show-desktop") && action == QLatin1StringView("show-desktop")) { + for (const auto b : {ElectricTop, ElectricRight, ElectricBottom, ElectricLeft}) { + if (workspace()->screenEdges()->actionForTouchBorder(b) == ElectricActionShowDesktop) { + ret.append(b); + } + } + return ret; + } else { + if (!m_config) { + return ret; + } + return m_config->property(QStringLiteral("TouchBorderActivate") + action).value>(); + } +} + +QJSValue ScriptedEffect::animate_helper(const QJSValue &object, AnimationType animationType) +{ + QJSValue windowProperty = object.property(QStringLiteral("window")); + if (!windowProperty.isObject()) { + m_engine->throwError(QStringLiteral("Window property missing in animation options")); + return QJSValue(); + } + + EffectWindow *window = qobject_cast(windowProperty.toQObject()); + if (!window) { + m_engine->throwError(QStringLiteral("Window property references invalid window")); + return QJSValue(); + } + + QList settings{animationSettingsFromObject(object)}; // global + + QJSValue animations = object.property(QStringLiteral("animations")); // array + if (!animations.isUndefined()) { + if (!animations.isArray()) { + m_engine->throwError(QStringLiteral("Animations provided but not an array")); + return QJSValue(); + } + + const int length = static_cast(animations.property(QStringLiteral("length")).toInt()); + for (int i = 0; i < length; ++i) { + QJSValue value = animations.property(QString::number(i)); + if (value.isObject()) { + AnimationSettings s = animationSettingsFromObject(value); + const uint set = s.set | settings.at(0).set; + // Catch show stoppers (incompletable animation) + if (!(set & AnimationSettings::Type)) { + m_engine->throwError(QStringLiteral("Type property missing in animation options")); + return QJSValue(); + } + if (!(set & AnimationSettings::Duration)) { + m_engine->throwError(QStringLiteral("Duration property missing in animation options")); + return QJSValue(); + } + // Complete local animations from global settings + if (!(s.set & AnimationSettings::Duration)) { + s.duration = settings.at(0).duration; + } + if (!(s.set & AnimationSettings::Curve)) { + s.curve = settings.at(0).curve; + } + if (!(s.set & AnimationSettings::Delay)) { + s.delay = settings.at(0).delay; + } + if (!(s.set & AnimationSettings::FullScreen)) { + s.fullScreenEffect = settings.at(0).fullScreenEffect; + } + if (!(s.set & AnimationSettings::KeepAlive)) { + s.keepAlive = settings.at(0).keepAlive; + } + if (!s.shader.has_value()) { + s.shader = settings.at(0).shader; + } + + if (s.type == ShaderUniform && s.shader) { + auto uniformProperty = value.property(QStringLiteral("uniform")).toString(); + auto shader = findShader(s.shader.value()); + if (!shader) { + m_engine->throwError(QStringLiteral("Shader for given shaderId not found")); + return {}; + } + if (!effects->makeOpenGLContextCurrent()) { + m_engine->throwError(QStringLiteral("Failed to make OpenGL context current")); + return {}; + } + ShaderBinder binder{shader}; + s.metaData = shader->uniformLocation(uniformProperty.toUtf8().constData()); + } + + settings << s; + } + } + } + + if (settings.count() == 1) { + const uint set = settings.at(0).set; + if (!(set & AnimationSettings::Type)) { + m_engine->throwError(QStringLiteral("Type property missing in animation options")); + return QJSValue(); + } + if (!(set & AnimationSettings::Duration)) { + m_engine->throwError(QStringLiteral("Duration property missing in animation options")); + return QJSValue(); + } + } else if (!(settings.at(0).set & AnimationSettings::Type)) { // invalid global + settings.removeAt(0); // -> get rid of it, only used to complete the others + } + + if (settings.isEmpty()) { + m_engine->throwError(QStringLiteral("No animations provided")); + return QJSValue(); + } + + QJSValue array = m_engine->newArray(settings.length()); + for (int i = 0; i < settings.count(); i++) { + const AnimationSettings &setting = settings[i]; + int animationId; + if (animationType == AnimationType::Set) { + animationId = set(window, + setting.type, + setting.duration, + setting.to, + setting.from, + setting.metaData, + setting.curve, + setting.delay, + setting.fullScreenEffect, + setting.keepAlive, + setting.shader ? setting.shader.value() : 0u); + if (setting.frozenTime >= 0) { + freezeInTime(animationId, setting.frozenTime); + } + } else { + animationId = animate(window, + setting.type, + setting.duration, + setting.to, + setting.from, + setting.metaData, + setting.curve, + setting.delay, + setting.fullScreenEffect, + setting.keepAlive, + setting.shader ? setting.shader.value() : 0u); + if (setting.frozenTime >= 0) { + freezeInTime(animationId, setting.frozenTime); + } + } + array.setProperty(i, animationId); + } + + return array; +} + +quint64 ScriptedEffect::animate(KWin::EffectWindow *window, KWin::AnimationEffect::Attribute attribute, + int ms, const QJSValue &to, const QJSValue &from, uint metaData, int curve, + int delay, bool fullScreen, bool keepAlive, uint shaderId) +{ + QEasingCurve qec; + if (curve < QEasingCurve::Custom) { + qec.setType(static_cast(curve)); + } else if (curve == GaussianCurve) { + qec.setCustomType(qecGaussian); + } + return AnimationEffect::animate(window, attribute, metaData, ms, fpx2FromScriptValue(to), qec, + delay, fpx2FromScriptValue(from), fullScreen, keepAlive, findShader(shaderId)); +} + +QJSValue ScriptedEffect::animate(const QJSValue &object) +{ + return animate_helper(object, AnimationType::Animate); +} + +quint64 ScriptedEffect::set(KWin::EffectWindow *window, KWin::AnimationEffect::Attribute attribute, + int ms, const QJSValue &to, const QJSValue &from, uint metaData, int curve, + int delay, bool fullScreen, bool keepAlive, uint shaderId) +{ + QEasingCurve qec; + if (curve < QEasingCurve::Custom) { + qec.setType(static_cast(curve)); + } else if (curve == GaussianCurve) { + qec.setCustomType(qecGaussian); + } + return AnimationEffect::set(window, attribute, metaData, ms, fpx2FromScriptValue(to), qec, + delay, fpx2FromScriptValue(from), fullScreen, keepAlive, findShader(shaderId)); +} + +QJSValue ScriptedEffect::set(const QJSValue &object) +{ + return animate_helper(object, AnimationType::Set); +} + +bool ScriptedEffect::retarget(quint64 animationId, const QJSValue &newTarget, int newRemainingTime) +{ + return AnimationEffect::retarget(animationId, fpx2FromScriptValue(newTarget), newRemainingTime); +} + +bool ScriptedEffect::retarget(const QList &animationIds, const QJSValue &newTarget, int newRemainingTime) +{ + return std::all_of(animationIds.begin(), animationIds.end(), [&](quint64 animationId) { + return retarget(animationId, newTarget, newRemainingTime); + }); +} + +bool ScriptedEffect::freezeInTime(quint64 animationId, qint64 frozenTime) +{ + return AnimationEffect::freezeInTime(animationId, frozenTime); +} + +bool ScriptedEffect::freezeInTime(const QList &animationIds, qint64 frozenTime) +{ + return std::all_of(animationIds.begin(), animationIds.end(), [&](quint64 animationId) { + return AnimationEffect::freezeInTime(animationId, frozenTime); + }); +} + +bool ScriptedEffect::redirect(quint64 animationId, Direction direction, TerminationFlags terminationFlags) +{ + return AnimationEffect::redirect(animationId, direction, terminationFlags); +} + +bool ScriptedEffect::redirect(const QList &animationIds, Direction direction, TerminationFlags terminationFlags) +{ + return std::all_of(animationIds.begin(), animationIds.end(), [&](quint64 animationId) { + return redirect(animationId, direction, terminationFlags); + }); +} + +bool ScriptedEffect::complete(quint64 animationId) +{ + return AnimationEffect::complete(animationId); +} + +bool ScriptedEffect::complete(const QList &animationIds) +{ + return std::all_of(animationIds.begin(), animationIds.end(), [&](quint64 animationId) { + return complete(animationId); + }); +} + +bool ScriptedEffect::cancel(quint64 animationId) +{ + return AnimationEffect::cancel(animationId); +} + +bool ScriptedEffect::cancel(const QList &animationIds) +{ + bool ret = false; + for (const quint64 &animationId : animationIds) { + ret |= cancel(animationId); + } + return ret; +} + +bool ScriptedEffect::isGrabbed(EffectWindow *w, ScriptedEffect::DataRole grabRole) +{ + void *e = w->data(static_cast(grabRole)).value(); + if (e) { + return e != this; + } else { + return false; + } +} + +bool ScriptedEffect::grab(EffectWindow *w, DataRole grabRole, bool force) +{ + void *grabber = w->data(grabRole).value(); + + if (grabber == this) { + return true; + } + + if (grabber != nullptr && grabber != this && !force) { + return false; + } + + w->setData(grabRole, QVariant::fromValue(static_cast(this))); + + return true; +} + +bool ScriptedEffect::ungrab(EffectWindow *w, DataRole grabRole) +{ + void *grabber = w->data(grabRole).value(); + + if (grabber == nullptr) { + return true; + } + + if (grabber != this) { + return false; + } + + w->setData(grabRole, QVariant()); + + return true; +} + +void ScriptedEffect::reconfigure(ReconfigureFlags flags) +{ + AnimationEffect::reconfigure(flags); + if (m_config) { + m_config->read(); + } + Q_EMIT configChanged(); +} + +void ScriptedEffect::registerShortcut(const QString &objectName, const QString &text, + const QString &keySequence, const QJSValue &callback) +{ + if (!callback.isCallable()) { + m_engine->throwError(QStringLiteral("Shortcut handler must be callable")); + return; + } + QAction *action = new QAction(this); + action->setObjectName(objectName); + action->setText(text); + const QKeySequence shortcut = QKeySequence(keySequence); + KGlobalAccel::self()->setShortcut(action, QList() << shortcut); + connect(action, &QAction::triggered, this, [this, action, callback]() { + QJSValue actionObject = m_engine->newQObject(action); + QJSEngine::setObjectOwnership(action, QJSEngine::CppOwnership); + QJSValue(callback).call(QJSValueList{actionObject}); + }); +} + +bool ScriptedEffect::borderActivated(ElectricBorder edge) +{ + auto it = screenEdgeCallbacks().constFind(edge); + if (it != screenEdgeCallbacks().constEnd()) { + for (const QJSValue &callback : it.value()) { + QJSValue(callback).call(); + } + } + return true; +} + +QJSValue ScriptedEffect::readConfig(const QString &key, const QJSValue &defaultValue) +{ + if (!m_config) { + return defaultValue; + } + return m_engine->toScriptValue(m_config->property(key)); +} + +int ScriptedEffect::displayWidth() const +{ + return workspace()->geometry().width(); +} + +int ScriptedEffect::displayHeight() const +{ + return workspace()->geometry().height(); +} + +int ScriptedEffect::animationTime(int defaultTime) const +{ + return Effect::animationTime(std::chrono::milliseconds(defaultTime)); +} + +bool ScriptedEffect::registerScreenEdge(int edge, const QJSValue &callback) +{ + if (!callback.isCallable()) { + m_engine->throwError(QStringLiteral("Screen edge handler must be callable")); + return false; + } + auto it = screenEdgeCallbacks().find(edge); + if (it == screenEdgeCallbacks().end()) { + // not yet registered + workspace()->screenEdges()->reserve(static_cast(edge), this, "borderActivated"); + screenEdgeCallbacks().insert(edge, QJSValueList{callback}); + } else { + it->append(callback); + } + return true; +} + +bool ScriptedEffect::registerRealtimeScreenEdge(int edge, const QJSValue &callback) +{ + if (!callback.isCallable()) { + m_engine->throwError(QStringLiteral("Screen edge handler must be callable")); + return false; + } + auto it = realtimeScreenEdgeCallbacks().find(edge); + if (it == realtimeScreenEdgeCallbacks().end()) { + // not yet registered + realtimeScreenEdgeCallbacks().insert(edge, QJSValueList{callback}); + auto *triggerAction = new QAction(this); + connect(triggerAction, &QAction::triggered, this, [this, edge]() { + auto it = realtimeScreenEdgeCallbacks().constFind(edge); + if (it != realtimeScreenEdgeCallbacks().constEnd()) { + for (const QJSValue &callback : it.value()) { + QJSValue(callback).call({edge}); + } + } + }); + effects->registerRealtimeTouchBorder(static_cast(edge), triggerAction, [this](ElectricBorder border, const QPointF &deltaProgress, LogicalOutput *screen) { + auto it = realtimeScreenEdgeCallbacks().constFind(border); + if (it != realtimeScreenEdgeCallbacks().constEnd()) { + for (const QJSValue &callback : it.value()) { + QJSValue delta = m_engine->newObject(); + delta.setProperty("width", deltaProgress.x()); + delta.setProperty("height", deltaProgress.y()); + + QJSValue(callback).call({border, QJSValue(delta), m_engine->newQObject(screen)}); + } + } + }); + } else { + it->append(callback); + } + return true; +} + +bool ScriptedEffect::unregisterScreenEdge(int edge) +{ + auto it = screenEdgeCallbacks().find(edge); + if (it == screenEdgeCallbacks().end()) { + // not previously registered + return false; + } + workspace()->screenEdges()->unreserve(static_cast(edge), this); + screenEdgeCallbacks().erase(it); + return true; +} + +bool ScriptedEffect::registerTouchScreenEdge(int edge, const QJSValue &callback) +{ + if (m_touchScreenEdgeCallbacks.constFind(edge) != m_touchScreenEdgeCallbacks.constEnd()) { + return false; + } + if (!callback.isCallable()) { + m_engine->throwError(QStringLiteral("Touch screen edge handler must be callable")); + return false; + } + QAction *action = new QAction(this); + connect(action, &QAction::triggered, this, [callback]() { + QJSValue(callback).call(); + }); + workspace()->screenEdges()->reserveTouch(KWin::ElectricBorder(edge), action); + m_touchScreenEdgeCallbacks.insert(edge, action); + return true; +} + +bool ScriptedEffect::unregisterTouchScreenEdge(int edge) +{ + auto it = m_touchScreenEdgeCallbacks.find(edge); + if (it == m_touchScreenEdgeCallbacks.end()) { + return false; + } + delete it.value(); + m_touchScreenEdgeCallbacks.erase(it); + return true; +} + +QJSEngine *ScriptedEffect::engine() const +{ + return m_engine; +} + +uint ScriptedEffect::addFragmentShader(ShaderTrait traits, const QString &fragmentShaderFile) +{ + if (!effects->makeOpenGLContextCurrent()) { + m_engine->throwError(QStringLiteral("Failed to make OpenGL context current")); + return 0; + } + + QString fragment; + if (!fragmentShaderFile.isEmpty()) { + fragment = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kwin-wayland/effects/") + m_effectName + QLatin1String("/contents/shaders/") + fragmentShaderFile); + if (fragment.isEmpty()) { + fragment = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kwin/effects/") + m_effectName + QLatin1String("/contents/shaders/") + fragmentShaderFile); + } + } + + auto shader = ShaderManager::instance()->generateShaderFromFile(static_cast(int(traits)), {}, fragment); + if (!shader->isValid()) { + m_engine->throwError(QStringLiteral("Shader failed to load")); + // 0 is never a valid shader identifier, it's ensured the first shader gets id 1 + return 0; + } + + const uint shaderId{m_nextShaderId}; + m_nextShaderId++; + m_shaders[shaderId] = std::move(shader); + return shaderId; +} + +GLShader *ScriptedEffect::findShader(uint shaderId) const +{ + if (auto it = m_shaders.find(shaderId); it != m_shaders.end()) { + return it->second.get(); + } + return nullptr; +} + +void ScriptedEffect::setUniform(uint shaderId, const QString &name, const QJSValue &value) +{ + auto shader = findShader(shaderId); + if (!shader) { + m_engine->throwError(QStringLiteral("Shader for given shaderId not found")); + return; + } + if (!effects->makeOpenGLContextCurrent()) { + m_engine->throwError(QStringLiteral("Failed to make OpenGL context current")); + return; + } + auto setColorUniform = [this, shader, name](const QColor &color) { + if (!color.isValid()) { + return; + } + if (!shader->setUniform(name.toUtf8().constData(), color)) { + m_engine->throwError(QStringLiteral("Failed to set uniform ") + name); + } + }; + ShaderBinder binder{shader}; + if (value.isString()) { + setColorUniform(value.toString()); + } else if (value.isNumber()) { + if (!shader->setUniform(name.toUtf8().constData(), float(value.toNumber()))) { + m_engine->throwError(QStringLiteral("Failed to set uniform ") + name); + } + } else if (value.isArray()) { + const auto length = value.property(QStringLiteral("length")).toInt(); + if (length == 2) { + if (!shader->setUniform(name.toUtf8().constData(), QVector2D{float(value.property(0).toNumber()), float(value.property(1).toNumber())})) { + m_engine->throwError(QStringLiteral("Failed to set uniform ") + name); + } + } else if (length == 3) { + if (!shader->setUniform(name.toUtf8().constData(), QVector3D{float(value.property(0).toNumber()), float(value.property(1).toNumber()), float(value.property(2).toNumber())})) { + m_engine->throwError(QStringLiteral("Failed to set uniform ") + name); + } + } else if (length == 4) { + if (!shader->setUniform(name.toUtf8().constData(), QVector4D{float(value.property(0).toNumber()), float(value.property(1).toNumber()), float(value.property(2).toNumber()), float(value.property(3).toNumber())})) { + m_engine->throwError(QStringLiteral("Failed to set uniform ") + name); + } + } else { + m_engine->throwError(QStringLiteral("Invalid number of elements in array")); + } + } else if (value.isVariant()) { + const auto variant = value.toVariant(); + setColorUniform(variant.value()); + } else { + m_engine->throwError(QStringLiteral("Invalid value provided for uniform")); + } +} + +} // namespace + +#include "moc_scriptedeffect.cpp" diff --git a/local/recipes/kde/kwin/source/src/scripting/scriptedeffect.h b/local/recipes/kde/kwin/source/src/scripting/scriptedeffect.h new file mode 100644 index 0000000000..cba72399cd --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scripting/scriptedeffect.h @@ -0,0 +1,224 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2012 Martin Gräßlin + SPDX-FileCopyrightText: 2018 David Edmundson + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/animationeffect.h" + +#include +#include + +class KConfigLoader; +class KPluginMetaData; + +class QAction; + +namespace KWin +{ + +class KWIN_EXPORT ScriptedEffect : public KWin::AnimationEffect +{ + Q_OBJECT + + Q_ENUMS(DataRole) + Q_ENUMS(Qt::Axis) + Q_ENUMS(Anchor) + Q_ENUMS(MetaType) + Q_ENUMS(EasingCurve) + Q_ENUMS(SessionState) + Q_ENUMS(ElectricBorder) + Q_ENUMS(ShaderTrait) + /** + * The plugin ID of the effect + */ + Q_PROPERTY(QString pluginId READ pluginId CONSTANT) + /** + * True if we are the active fullscreen effect + */ + Q_PROPERTY(bool isActiveFullScreenEffect READ isActiveFullScreenEffect NOTIFY isActiveFullScreenEffectChanged) + +public: + // copied from effecthandler.h + enum DataRole { + // Grab roles are used to force all other animations to ignore the window. + // The value of the data is set to the Effect's `this` value. + WindowAddedGrabRole = 1, + WindowClosedGrabRole, + WindowMinimizedGrabRole, + WindowUnminimizedGrabRole, + WindowForceBlurRole, ///< For fullscreen effects to enforce blurring of windows, + WindowForceBackgroundContrastRole, ///< For fullscreen effects to enforce the background contrast, + }; + enum EasingCurve { + GaussianCurve = 128 + }; + // copied from kwinglutils.h + enum class ShaderTrait { + MapTexture = (1 << 0), + UniformColor = (1 << 1), + Modulate = (1 << 2), + AdjustSaturation = (1 << 3), + }; + + const QString &scriptFile() const + { + return m_scriptFile; + } + void reconfigure(ReconfigureFlags flags) override; + int requestedEffectChainPosition() const override + { + return m_chainPosition; + } + QString activeConfig() const; + void setActiveConfig(const QString &name); + static ScriptedEffect *create(const QString &effectName, const QString &pathToScript, int chainPosition, const QString &exclusiveCategory); + static ScriptedEffect *create(const KPluginMetaData &effect); + static bool supported(); + ~ScriptedEffect() override; + /** + * Whether another effect has grabbed the @p w with the given @p grabRole. + * @param w The window to check + * @param grabRole The grab role to check + * @returns @c true if another window has grabbed the effect, @c false otherwise + */ + Q_SCRIPTABLE bool isGrabbed(KWin::EffectWindow *w, DataRole grabRole); + + /** + * Grabs the window with the specified role. + * + * @param w The window. + * @param grabRole The grab role. + * @param force By default, if the window is already grabbed by another effect, + * then that window won't be grabbed by effect that called this method. If you + * would like to grab a window even if it's grabbed by another effect, then + * pass @c true. + * @returns @c true if the window was grabbed successfully, otherwise @c false. + */ + Q_SCRIPTABLE bool grab(KWin::EffectWindow *w, DataRole grabRole, bool force = false); + + /** + * Ungrabs the window with the specified role. + * + * @param w The window. + * @param grabRole The grab role. + * @returns @c true if the window was ungrabbed successfully, otherwise @c false. + */ + Q_SCRIPTABLE bool ungrab(KWin::EffectWindow *w, DataRole grabRole); + + /** + * Reads the value from the configuration data for the given key. + * @param key The key to search for + * @param defaultValue The value to return if the key is not found + * @returns The config value if present + */ + Q_SCRIPTABLE QJSValue readConfig(const QString &key, const QJSValue &defaultValue = QJSValue()); + + Q_SCRIPTABLE int displayWidth() const; + Q_SCRIPTABLE int displayHeight() const; + Q_SCRIPTABLE int animationTime(int defaultTime) const; + + Q_SCRIPTABLE void registerShortcut(const QString &objectName, const QString &text, + const QString &keySequence, const QJSValue &callback); + Q_SCRIPTABLE bool registerScreenEdge(int edge, const QJSValue &callback); + Q_SCRIPTABLE bool registerRealtimeScreenEdge(int edge, const QJSValue &callback); + Q_SCRIPTABLE bool unregisterScreenEdge(int edge); + Q_SCRIPTABLE bool registerTouchScreenEdge(int edge, const QJSValue &callback); + Q_SCRIPTABLE bool unregisterTouchScreenEdge(int edge); + + Q_SCRIPTABLE quint64 animate(KWin::EffectWindow *window, Attribute attribute, int ms, + const QJSValue &to, const QJSValue &from = QJSValue(), + uint metaData = 0, int curve = QEasingCurve::Linear, int delay = 0, + bool fullScreen = false, bool keepAlive = true, uint shaderId = 0); + Q_SCRIPTABLE QJSValue animate(const QJSValue &object); + + Q_SCRIPTABLE quint64 set(KWin::EffectWindow *window, Attribute attribute, int ms, + const QJSValue &to, const QJSValue &from = QJSValue(), + uint metaData = 0, int curve = QEasingCurve::Linear, int delay = 0, + bool fullScreen = false, bool keepAlive = true, uint shaderId = 0); + Q_SCRIPTABLE QJSValue set(const QJSValue &object); + + Q_SCRIPTABLE bool retarget(quint64 animationId, const QJSValue &newTarget, + int newRemainingTime = -1); + Q_SCRIPTABLE bool retarget(const QList &animationIds, const QJSValue &newTarget, + int newRemainingTime = -1); + Q_SCRIPTABLE bool freezeInTime(quint64 animationId, qint64 frozenTime); + Q_SCRIPTABLE bool freezeInTime(const QList &animationIds, qint64 frozenTime); + + Q_SCRIPTABLE bool redirect(quint64 animationId, Direction direction, + TerminationFlags terminationFlags = TerminateAtSource); + Q_SCRIPTABLE bool redirect(const QList &animationIds, Direction direction, + TerminationFlags terminationFlags = TerminateAtSource); + + Q_SCRIPTABLE bool complete(quint64 animationId); + Q_SCRIPTABLE bool complete(const QList &animationIds); + + Q_SCRIPTABLE bool cancel(quint64 animationId); + Q_SCRIPTABLE bool cancel(const QList &animationIds); + + Q_SCRIPTABLE QList touchEdgesForAction(const QString &action) const; + + Q_SCRIPTABLE uint addFragmentShader(ShaderTrait traits, const QString &fragmentShaderFile = {}); + + Q_SCRIPTABLE void setUniform(uint shaderId, const QString &name, const QJSValue &value); + + QHash &screenEdgeCallbacks() + { + return m_screenEdgeCallbacks; + } + + QHash &realtimeScreenEdgeCallbacks() + { + return m_realtimeScreenEdgeCallbacks; + } + + QString pluginId() const; + bool isActiveFullScreenEffect() const; + +public Q_SLOTS: + bool borderActivated(ElectricBorder border) override; + +Q_SIGNALS: + /** + * Signal emitted whenever the effect's config changed. + */ + void configChanged(); + void animationEnded(KWin::EffectWindow *w, quint64 animationId); + void isActiveFullScreenEffectChanged(); + +protected: + ScriptedEffect(); + QJSEngine *engine() const; + bool init(const QString &effectName, const QString &pathToScript); + void animationEnded(KWin::EffectWindow *w, Attribute a, uint meta) override; + +private: + enum class AnimationType { + Animate, + Set + }; + + QJSValue animate_helper(const QJSValue &object, AnimationType animationType); + + GLShader *findShader(uint shaderId) const; + + QJSEngine *m_engine; + QString m_effectName; + QString m_scriptFile; + QString m_exclusiveCategory; + QHash m_screenEdgeCallbacks; + QHash m_realtimeScreenEdgeCallbacks; + KConfigLoader *m_config; + int m_chainPosition; + QHash m_touchScreenEdgeCallbacks; + Effect *m_activeFullScreenEffect = nullptr; + std::map> m_shaders; + uint m_nextShaderId{1u}; +}; +} diff --git a/local/recipes/kde/kwin/source/src/scripting/scriptedquicksceneeffect.cpp b/local/recipes/kde/kwin/source/src/scripting/scriptedquicksceneeffect.cpp new file mode 100644 index 0000000000..190429d469 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scripting/scriptedquicksceneeffect.cpp @@ -0,0 +1,135 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scripting/scriptedquicksceneeffect.h" +#include "main.h" + +#include +#include + +#include + +namespace KWin +{ + +ScriptedQuickSceneEffect::ScriptedQuickSceneEffect() +{ + m_visibleTimer.setSingleShot(true); + connect(&m_visibleTimer, &QTimer::timeout, this, [this]() { + setRunning(false); + }); +} + +ScriptedQuickSceneEffect::~ScriptedQuickSceneEffect() +{ +} + +void ScriptedQuickSceneEffect::reconfigure(ReconfigureFlags flags) +{ + m_configLoader->load(); + Q_EMIT m_configLoader->configChanged(); +} + +int ScriptedQuickSceneEffect::requestedEffectChainPosition() const +{ + return m_requestedEffectChainPosition; +} + +void ScriptedQuickSceneEffect::setMetaData(const KPluginMetaData &metaData) +{ + m_requestedEffectChainPosition = metaData.value(QStringLiteral("X-KDE-Ordering"), 50); + + KConfigGroup cg = kwinApp()->config()->group(QStringLiteral("Effect-%1").arg(metaData.pluginId())); + QString configFilePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kwin-wayland/effects/") + metaData.pluginId() + QLatin1String("/contents/config/main.xml")); + if (configFilePath.isNull()) { + configFilePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kwin/effects/") + metaData.pluginId() + QLatin1String("/contents/config/main.xml")); + } + if (configFilePath.isNull()) { + m_configLoader = new KConfigLoader(cg, nullptr, this); + } else { + QFile xmlFile(configFilePath); + m_configLoader = new KConfigLoader(cg, &xmlFile, this); + m_configLoader->load(); + } + + m_configuration = new KConfigPropertyMap(m_configLoader, this); + connect(m_configLoader, &KConfigLoader::configChanged, this, &ScriptedQuickSceneEffect::configurationChanged); +} + +bool ScriptedQuickSceneEffect::isVisible() const +{ + return m_isVisible; +} + +void ScriptedQuickSceneEffect::setVisible(bool visible) +{ + if (m_isVisible == visible) { + return; + } + m_isVisible = visible; + + if (m_isVisible) { + m_visibleTimer.stop(); + setRunning(true); + } else { + // Delay setRunning(false) to avoid destroying views while still executing JS code. + m_visibleTimer.start(); + } + + Q_EMIT visibleChanged(); +} + +KConfigPropertyMap *ScriptedQuickSceneEffect::configuration() const +{ + return m_configuration; +} + +QQmlListProperty ScriptedQuickSceneEffect::data() +{ + return QQmlListProperty(this, nullptr, + data_append, + data_count, + data_at, + data_clear); +} + +void ScriptedQuickSceneEffect::data_append(QQmlListProperty *objects, QObject *object) +{ + if (!object) { + return; + } + + ScriptedQuickSceneEffect *effect = static_cast(objects->object); + if (!effect->m_children.contains(object)) { + object->setParent(effect); + effect->m_children.append(object); + } +} + +qsizetype ScriptedQuickSceneEffect::data_count(QQmlListProperty *objects) +{ + ScriptedQuickSceneEffect *effect = static_cast(objects->object); + return effect->m_children.count(); +} + +QObject *ScriptedQuickSceneEffect::data_at(QQmlListProperty *objects, qsizetype index) +{ + ScriptedQuickSceneEffect *effect = static_cast(objects->object); + return effect->m_children.value(index); +} + +void ScriptedQuickSceneEffect::data_clear(QQmlListProperty *objects) +{ + ScriptedQuickSceneEffect *effect = static_cast(objects->object); + while (!effect->m_children.isEmpty()) { + QObject *child = effect->m_children.takeLast(); + child->setParent(nullptr); + } +} + +} // namespace KWin + +#include "moc_scriptedquicksceneeffect.cpp" diff --git a/local/recipes/kde/kwin/source/src/scripting/scriptedquicksceneeffect.h b/local/recipes/kde/kwin/source/src/scripting/scriptedquicksceneeffect.h new file mode 100644 index 0000000000..93e544060b --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scripting/scriptedquicksceneeffect.h @@ -0,0 +1,105 @@ +/* + SPDX-FileCopyrightText: 2023 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/quickeffect.h" + +#include + +#include + +class KConfigLoader; +class KConfigPropertyMap; + +namespace KWin +{ + +/*! + * \qmltype SceneEffect + * \inqmlmodule org.kde.kwin + * + * \brief The SceneEffect type provides a way to implement effects that replace the default scene with + * a custom one. + * + * Example usage: + * \code + * SceneEffect { + * id: root + * + * delegate: Rectangle { + * color: "blue" + * } + * + * ShortcutHandler { + * name: "Toggle Effect" + * text: i18n("Toggle Effect") + * sequence: "Meta+E" + * onActivated: root.visible = !root.visible; + * } + * } + * \endcode + */ +class ScriptedQuickSceneEffect : public QuickSceneEffect +{ + Q_OBJECT + /*! + * \qmlproperty list SceneEffect::data + * \qmldefault + */ + Q_PROPERTY(QQmlListProperty data READ data) + Q_CLASSINFO("DefaultProperty", "data") + + /*! + * \qmlproperty KConfigPropertyMap SceneEffect::configuration + * + * The key-value store with the effect settings. + */ + Q_PROPERTY(KConfigPropertyMap *configuration READ configuration NOTIFY configurationChanged) + + /*! + * \qmlproperty bool SceneEffect::visible + * + * Whether the effect is shown. Setting this property to \c true activates the effect; setting + * this property to \c false will deactivate the effect and the screen views will be unloaded at + * the next available time. + */ + Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged) + +public: + explicit ScriptedQuickSceneEffect(); + ~ScriptedQuickSceneEffect() override; + + void setMetaData(const KPluginMetaData &metaData); + + void reconfigure(ReconfigureFlags flags) override; + int requestedEffectChainPosition() const override; + + bool isVisible() const; + void setVisible(bool visible); + + QQmlListProperty data(); + KConfigPropertyMap *configuration() const; + + static void data_append(QQmlListProperty *objects, QObject *object); + static qsizetype data_count(QQmlListProperty *objects); + static QObject *data_at(QQmlListProperty *objects, qsizetype index); + static void data_clear(QQmlListProperty *objects); + +Q_SIGNALS: + void visibleChanged(); + void configurationChanged(); + +private: + KConfigLoader *m_configLoader = nullptr; + KConfigPropertyMap *m_configuration = nullptr; + QObjectList m_children; + QTimer m_visibleTimer; + bool m_isVisible = false; + int m_requestedEffectChainPosition = 0; +}; + +} // namespace KWin diff --git a/local/recipes/kde/kwin/source/src/scripting/scripting.cpp b/local/recipes/kde/kwin/source/src/scripting/scripting.cpp new file mode 100644 index 0000000000..62611d5eb9 --- /dev/null +++ b/local/recipes/kde/kwin/source/src/scripting/scripting.cpp @@ -0,0 +1,905 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2010 Rohan Prabhu + SPDX-FileCopyrightText: 2011 Martin Gräßlin + SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "scripting.h" +// own +#include "dbuscall.h" +#include "desktopbackgrounditem.h" +#include "effect/quickeffect.h" +#include "gesturehandler.h" +#include "screenedgehandler.h" +#include "scriptedquicksceneeffect.h" +#include "scripting_logging.h" +#include "scriptingutils.h" +#include "shortcuthandler.h" +#include "virtualdesktopmodel.h" +#include "windowmodel.h" +#include "windowthumbnailitem.h" +#include "workspace_wrapper.h" + +#include "core/output.h" +#include "core/rect.h" +#include "input.h" +#include "options.h" +#include "screenedge.h" +#include "tiles/tilemanager.h" +#include "virtualdesktops.h" +#include "window.h" +#include "workspace.h" +// KDE +#include +#include +#include +#include +#include +#include +// Qt +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "scriptadaptor.h" + +static QRect scriptValueToQRect(const QJSValue &value) +{ + return QRect(value.property(QStringLiteral("x")).toInt(), + value.property(QStringLiteral("y")).toInt(), + value.property(QStringLiteral("width")).toInt(), + value.property(QStringLiteral("height")).toInt()); +} + +static QRectF scriptValueToQRectF(const QJSValue &value) +{ + return QRectF(value.property(QStringLiteral("x")).toNumber(), + value.property(QStringLiteral("y")).toNumber(), + value.property(QStringLiteral("width")).toNumber(), + value.property(QStringLiteral("height")).toNumber()); +} + +static QPoint scriptValueToPoint(const QJSValue &value) +{ + return QPoint(value.property(QStringLiteral("x")).toInt(), + value.property(QStringLiteral("y")).toInt()); +} + +static QPointF scriptValueToPointF(const QJSValue &value) +{ + return QPointF(value.property(QStringLiteral("x")).toNumber(), + value.property(QStringLiteral("y")).toNumber()); +} + +static QSize scriptValueToSize(const QJSValue &value) +{ + return QSize(value.property(QStringLiteral("width")).toInt(), + value.property(QStringLiteral("height")).toInt()); +} + +static QSizeF scriptValueToSizeF(const QJSValue &value) +{ + return QSizeF(value.property(QStringLiteral("width")).toNumber(), + value.property(QStringLiteral("height")).toNumber()); +} + +KWin::AbstractScript::AbstractScript(int id, QString scriptName, QString pluginName, QObject *parent) + : QObject(parent) + , m_scriptId(id) + , m_fileName(scriptName) + , m_pluginName(pluginName) + , m_running(false) +{ + if (m_pluginName.isNull()) { + m_pluginName = scriptName; + } + + new ScriptAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/Scripting/Script") + QString::number(scriptId()), this, QDBusConnection::ExportAdaptors); +} + +KWin::AbstractScript::~AbstractScript() +{ +} + +KConfigGroup KWin::AbstractScript::config() const +{ + return kwinApp()->config()->group(QLatin1String("Script-") + m_pluginName); +} + +void KWin::AbstractScript::stop() +{ + deleteLater(); +} + +KWin::ScriptTimer::ScriptTimer(QObject *parent) + : QTimer(parent) +{ +} + +KWin::Script::Script(int id, QString scriptName, QString pluginName, QObject *parent) + : AbstractScript(id, scriptName, pluginName, parent) + , m_engine(new QJSEngine(this)) + , m_starting(false) +{ + // TODO: Remove in kwin 6. We have these converters only for compatibility reasons. + if (!QMetaType::hasRegisteredConverterFunction()) { + QMetaType::registerConverter(scriptValueToQRect); + } + if (!QMetaType::hasRegisteredConverterFunction()) { + QMetaType::registerConverter(scriptValueToQRectF); + } + + if (!QMetaType::hasRegisteredConverterFunction()) { + QMetaType::registerConverter(scriptValueToPoint); + } + if (!QMetaType::hasRegisteredConverterFunction()) { + QMetaType::registerConverter(scriptValueToPointF); + } + + if (!QMetaType::hasRegisteredConverterFunction()) { + QMetaType::registerConverter(scriptValueToSize); + } + if (!QMetaType::hasRegisteredConverterFunction()) { + QMetaType::registerConverter(scriptValueToSizeF); + } +} + +KWin::Script::~Script() +{ +} + +void KWin::Script::run() +{ + if (running() || m_starting) { + return; + } + + if (calledFromDBus()) { + m_invocationContext = message(); + setDelayedReply(true); + } + + m_starting = true; + QFutureWatcher *watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcherBase::finished, this, &Script::slotScriptLoadedFromFile); + watcher->setFuture(QtConcurrent::run(&KWin::Script::loadScriptFromFile, this, fileName())); +} + +QByteArray KWin::Script::loadScriptFromFile(const QString &fileName) +{ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + return QByteArray(); + } + QByteArray result(file.readAll()); + return result; +} + +void KWin::Script::slotScriptLoadedFromFile() +{ + QFutureWatcher *watcher = dynamic_cast *>(sender()); + if (!watcher) { + // not invoked from a QFutureWatcher + return; + } + if (watcher->result().isNull()) { + // do not load empty script + deleteLater(); + watcher->deleteLater(); + + if (m_invocationContext.type() == QDBusMessage::MethodCallMessage) { + auto reply = m_invocationContext.createErrorReply("org.kde.kwin.Scripting.FileError", QString("Could not open %1").arg(fileName())); + QDBusConnection::sessionBus().send(reply); + m_invocationContext = QDBusMessage(); + } + + return; + } + + // Install console functions (e.g. console.assert(), console.log(), etc). + m_engine->installExtensions(QJSEngine::ConsoleExtension); + + // Make the timer visible to QJSEngine. + QJSValue timerMetaObject = m_engine->newQMetaObject(&ScriptTimer::staticMetaObject); + m_engine->globalObject().setProperty("QTimer", timerMetaObject); + + // Expose enums. + m_engine->globalObject().setProperty(QStringLiteral("KWin"), m_engine->newQMetaObject(&QtScriptWorkspaceWrapper::staticMetaObject)); + + // Make the options object visible to QJSEngine. + QJSValue optionsObject = m_engine->newQObject(options); + QJSEngine::setObjectOwnership(options, QJSEngine::CppOwnership); + m_engine->globalObject().setProperty(QStringLiteral("options"), optionsObject); + + // Make the workspace visible to QJSEngine. + QJSValue workspaceObject = m_engine->newQObject(Scripting::self()->workspaceWrapper()); + QJSEngine::setObjectOwnership(Scripting::self()->workspaceWrapper(), QJSEngine::CppOwnership); + m_engine->globalObject().setProperty(QStringLiteral("workspace"), workspaceObject); + + QJSValue self = m_engine->newQObject(this); + QJSEngine::setObjectOwnership(this, QJSEngine::CppOwnership); + + static const QStringList globalProperties{ + QStringLiteral("readConfig"), + QStringLiteral("callDBus"), + + QStringLiteral("registerShortcut"), + QStringLiteral("registerScreenEdge"), + QStringLiteral("unregisterScreenEdge"), + QStringLiteral("registerTouchScreenEdge"), + QStringLiteral("unregisterTouchScreenEdge"), + QStringLiteral("registerUserActionsMenu"), + }; + + for (const QString &propertyName : globalProperties) { + m_engine->globalObject().setProperty(propertyName, self.property(propertyName)); + } + + // Inject assertion functions. It would be better to create a module with all + // this assert functions or just deprecate them in favor of console.assert(). + QJSValue result = m_engine->evaluate(QStringLiteral(R"( + function assert(condition, message) { + console.assert(condition, message || 'Assertion failed'); + } + function assertTrue(condition, message) { + console.assert(condition, message || 'Assertion failed'); + } + function assertFalse(condition, message) { + console.assert(!condition, message || 'Assertion failed'); + } + function assertNull(value, message) { + console.assert(value === null, message || 'Assertion failed'); + } + function assertNotNull(value, message) { + console.assert(value !== null, message || 'Assertion failed'); + } + function assertEquals(expected, actual, message) { + console.assert(expected === actual, message || 'Assertion failed'); + } + )")); + Q_ASSERT(!result.isError()); + + result = m_engine->evaluate(QString::fromUtf8(watcher->result()), fileName()); + if (result.isError()) { + qCWarning(KWIN_SCRIPTING, "%s:%d: error: %s", qPrintable(fileName()), + result.property(QStringLiteral("lineNumber")).toInt(), + qPrintable(result.property(QStringLiteral("message")).toString())); + deleteLater(); + } + + if (m_invocationContext.type() == QDBusMessage::MethodCallMessage) { + auto reply = m_invocationContext.createReply(); + QDBusConnection::sessionBus().send(reply); + m_invocationContext = QDBusMessage(); + } + + watcher->deleteLater(); + setRunning(true); + m_starting = false; +} + +QVariant KWin::Script::readConfig(const QString &key, const QVariant &defaultValue) +{ + return config().readEntry(key, defaultValue); +} + +void KWin::Script::callDBus(const QString &service, const QString &path, const QString &interface, + const QString &method, const QJSValue &arg1, const QJSValue &arg2, + const QJSValue &arg3, const QJSValue &arg4, const QJSValue &arg5, + const QJSValue &arg6, const QJSValue &arg7, const QJSValue &arg8, + const QJSValue &arg9) +{ + QJSValueList jsArguments; + jsArguments.reserve(9); + + if (!arg1.isUndefined()) { + jsArguments << arg1; + } + if (!arg2.isUndefined()) { + jsArguments << arg2; + } + if (!arg3.isUndefined()) { + jsArguments << arg3; + } + if (!arg4.isUndefined()) { + jsArguments << arg4; + } + if (!arg5.isUndefined()) { + jsArguments << arg5; + } + if (!arg6.isUndefined()) { + jsArguments << arg6; + } + if (!arg7.isUndefined()) { + jsArguments << arg7; + } + if (!arg8.isUndefined()) { + jsArguments << arg8; + } + if (!arg9.isUndefined()) { + jsArguments << arg9; + } + + QJSValue callback; + if (!jsArguments.isEmpty() && jsArguments.last().isCallable()) { + callback = jsArguments.takeLast(); + } + + QVariantList dbusArguments; + dbusArguments.reserve(jsArguments.count()); + for (const QJSValue &jsArgument : std::as_const(jsArguments)) { + dbusArguments << jsArgument.toVariant(); + } + + QDBusMessage message = QDBusMessage::createMethodCall(service, path, interface, method); + message.setArguments(dbusArguments); + + const QDBusPendingCall call = QDBusConnection::sessionBus().asyncCall(message); + if (callback.isUndefined()) { + return; + } + + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, callback](QDBusPendingCallWatcher *self) { + self->deleteLater(); + + if (self->isError()) { + qCWarning(KWIN_SCRIPTING) << "Received D-Bus message is error:" << self->error().message(); + return; + } + + QJSValueList arguments; + const QVariantList reply = self->reply().arguments(); + for (const QVariant &variant : reply) { + arguments << m_engine->toScriptValue(dbusToVariant(variant)); + } + + QJSValue(callback).call(arguments); + }); +} + +bool KWin::Script::registerShortcut(const QString &objectName, const QString &text, const QString &keySequence, const QJSValue &callback) +{ + if (!callback.isCallable()) { + m_engine->throwError(QStringLiteral("Shortcut handler must be callable")); + return false; + } + + QAction *action = new QAction(this); + action->setObjectName(objectName); + action->setText(text); + + const QKeySequence shortcut = keySequence; + KGlobalAccel::self()->setShortcut(action, {shortcut}); + + connect(action, &QAction::triggered, this, [this, action, callback]() { + QJSValue(callback).call({m_engine->toScriptValue(action)}); + }); + + return true; +} + +bool KWin::Script::registerScreenEdge(int edge, const QJSValue &callback) +{ + if (!callback.isCallable()) { + m_engine->throwError(QStringLiteral("Screen edge handler must be callable")); + return false; + } + + QJSValueList &callbacks = m_screenEdgeCallbacks[edge]; + if (callbacks.isEmpty()) { + workspace()->screenEdges()->reserve(static_cast(edge), this, "slotBorderActivated"); + } + + callbacks << callback; + + return true; +} + +bool KWin::Script::unregisterScreenEdge(int edge) +{ + auto it = m_screenEdgeCallbacks.find(edge); + if (it == m_screenEdgeCallbacks.end()) { + return false; + } + + workspace()->screenEdges()->unreserve(static_cast(edge), this); + m_screenEdgeCallbacks.erase(it); + + return true; +} + +bool KWin::Script::registerTouchScreenEdge(int edge, const QJSValue &callback) +{ + if (!callback.isCallable()) { + m_engine->throwError(QStringLiteral("Touch screen edge handler must be callable")); + return false; + } + if (m_touchScreenEdgeCallbacks.contains(edge)) { + return false; + } + + QAction *action = new QAction(this); + workspace()->screenEdges()->reserveTouch(KWin::ElectricBorder(edge), action); + m_touchScreenEdgeCallbacks.insert(edge, action); + + connect(action, &QAction::triggered, this, [callback]() { + QJSValue(callback).call(); + }); + + return true; +} + +bool KWin::Script::unregisterTouchScreenEdge(int edge) +{ + auto it = m_touchScreenEdgeCallbacks.find(edge); + if (it == m_touchScreenEdgeCallbacks.end()) { + return false; + } + + delete it.value(); + m_touchScreenEdgeCallbacks.erase(it); + + return true; +} + +void KWin::Script::registerUserActionsMenu(const QJSValue &callback) +{ + if (!callback.isCallable()) { + m_engine->throwError(QStringLiteral("User action handler must be callable")); + return; + } + m_userActionsMenuCallbacks.append(callback); +} + +QList KWin::Script::actionsForUserActionMenu(KWin::Window *client, QMenu *parent) +{ + QList actions; + actions.reserve(m_userActionsMenuCallbacks.count()); + + for (QJSValue callback : std::as_const(m_userActionsMenuCallbacks)) { + const QJSValue result = callback.call({m_engine->toScriptValue(client)}); + if (result.isError()) { + continue; + } + if (!result.isObject()) { + continue; + } + if (QAction *action = scriptValueToAction(result, parent)) { + actions << action; + } + } + + return actions; +} + +bool KWin::Script::slotBorderActivated(ElectricBorder border) +{ + const QJSValueList callbacks = m_screenEdgeCallbacks.value(border); + if (callbacks.isEmpty()) { + return false; + } + std::for_each(callbacks.begin(), callbacks.end(), [](QJSValue callback) { + callback.call(); + }); + return true; +} + +QAction *KWin::Script::scriptValueToAction(const QJSValue &value, QMenu *parent) +{ + const QString title = value.property(QStringLiteral("text")).toString(); + if (title.isEmpty()) { + return nullptr; + } + + // Either a menu or a menu item. + const QJSValue itemsValue = value.property(QStringLiteral("items")); + if (!itemsValue.isUndefined()) { + return createMenu(title, itemsValue, parent); + } + + return createAction(title, value, parent); +} + +QAction *KWin::Script::createAction(const QString &title, const QJSValue &item, QMenu *parent) +{ + const QJSValue callback = item.property(QStringLiteral("triggered")); + if (!callback.isCallable()) { + return nullptr; + } + + const bool checkable = item.property(QStringLiteral("checkable")).toBool(); + const bool checked = item.property(QStringLiteral("checked")).toBool(); + + QAction *action = new QAction(title, parent); + action->setCheckable(checkable); + action->setChecked(checked); + + connect(action, &QAction::triggered, this, [this, action, callback]() { + QJSValue(callback).call({m_engine->toScriptValue(action)}); + }); + + return action; +} + +QAction *KWin::Script::createMenu(const QString &title, const QJSValue &items, QMenu *parent) +{ + if (!items.isArray()) { + return nullptr; + } + + const int length = items.property(QStringLiteral("length")).toInt(); + if (!length) { + return nullptr; + } + + QMenu *menu = new QMenu(title, parent); + for (int i = 0; i < length; ++i) { + const QJSValue value = items.property(QString::number(i)); + if (!value.isObject()) { + continue; + } + if (QAction *action = scriptValueToAction(value, menu)) { + menu->addAction(action); + } + } + + return menu->menuAction(); +} + +KWin::DeclarativeScript::DeclarativeScript(int id, QString scriptName, QString pluginName, QObject *parent) + : AbstractScript(id, scriptName, pluginName, parent) + , m_context(new QQmlContext(Scripting::self()->declarativeScriptSharedContext(), this)) + , m_component(new QQmlComponent(Scripting::self()->qmlEngine(), this)) +{ + m_context->setContextProperty(QStringLiteral("KWin"), new JSEngineGlobalMethodsWrapper(this)); +} + +KWin::DeclarativeScript::~DeclarativeScript() +{ +} + +void KWin::DeclarativeScript::run() +{ + if (running()) { + return; + } + + m_component->loadUrl(QUrl::fromLocalFile(fileName())); + if (m_component->isLoading()) { + connect(m_component, &QQmlComponent::statusChanged, this, &DeclarativeScript::createComponent); + } else { + createComponent(); + } +} + +void KWin::DeclarativeScript::createComponent() +{ + if (m_component->isError()) { + qCWarning(KWIN_SCRIPTING) << "Component failed to load: " << m_component->errors(); + } else { + if (QObject *object = m_component->create(m_context)) { + object->setParent(this); + } + } + setRunning(true); +} + +KWin::JSEngineGlobalMethodsWrapper::JSEngineGlobalMethodsWrapper(KWin::DeclarativeScript *parent) + : QObject(parent) + , m_script(parent) +{ +} + +KWin::JSEngineGlobalMethodsWrapper::~JSEngineGlobalMethodsWrapper() +{ +} + +QVariant KWin::JSEngineGlobalMethodsWrapper::readConfig(const QString &key, QVariant defaultValue) +{ + return m_script->config().readEntry(key, defaultValue); +} + +KWin::Scripting *KWin::Scripting::s_self = nullptr; + +KWin::Scripting *KWin::Scripting::create(QObject *parent) +{ + Q_ASSERT(!s_self); + s_self = new Scripting(parent); + return s_self; +} + +KWin::Scripting::Scripting(QObject *parent) + : QObject(parent) + , m_scriptsLock(new QRecursiveMutex) + , m_qmlEngine(new QQmlEngine(this)) + , m_declarativeScriptSharedContext(new QQmlContext(m_qmlEngine, this)) + , m_workspaceWrapper(new QtScriptWorkspaceWrapper(this)) +{ + // For plain JavaScript extensions. There's no Rect factory function, we accept any object + // with x, y, width, and height properties as rects. + if (!QMetaType::hasRegisteredConverterFunction()) { + QMetaType::registerConverter([](const QJSValue &value) { + return Rect(value.property(QStringLiteral("x")).toInt(), + value.property(QStringLiteral("y")).toInt(), + value.property(QStringLiteral("width")).toInt(), + value.property(QStringLiteral("height")).toInt()); + }); + } + + if (!QMetaType::hasRegisteredConverterFunction()) { + QMetaType::registerConverter([](const QJSValue &value) { + return RectF(value.property(QStringLiteral("x")).toNumber(), + value.property(QStringLiteral("y")).toNumber(), + value.property(QStringLiteral("width")).toNumber(), + value.property(QStringLiteral("height")).toNumber()); + }); + } + + // For QML extensions. + if (!QMetaType::hasRegisteredConverterFunction()) { + QMetaType::registerConverter([](const QRect &rect) { + return Rect(rect.x(), rect.y(), rect.width(), rect.height()); + }); + } + + if (!QMetaType::hasRegisteredConverterFunction()) { + QMetaType::registerConverter([](const Rect &rect) { + return QRect(rect.x(), rect.y(), rect.width(), rect.height()); + }); + } + + if (!QMetaType::hasRegisteredConverterFunction()) { + QMetaType::registerConverter([](const QRectF &rect) { + return RectF(rect.x(), rect.y(), rect.width(), rect.height()); + }); + } + + if (!QMetaType::hasRegisteredConverterFunction()) { + QMetaType::registerConverter([](const RectF &rect) { + return QRectF(rect.x(), rect.y(), rect.width(), rect.height()); + }); + } + + m_qmlEngine->setProperty("_kirigamiTheme", QStringLiteral("KirigamiPlasmaStyle")); + m_qmlEngine->rootContext()->setContextObject(new KLocalizedQmlContext(m_qmlEngine)); + init(); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/Scripting"), this, QDBusConnection::ExportScriptableContents | QDBusConnection::ExportScriptableInvokables); + connect(Workspace::self(), &Workspace::configChanged, this, &Scripting::start); + connect(Workspace::self(), &Workspace::workspaceInitialized, this, &Scripting::start); +} + +void KWin::Scripting::init() +{ + qRegisterMetaType>(); + qRegisterMetaType>(); + qRegisterMetaType>(); + + qmlRegisterType("org.kde.kwin", 3, 0, "DesktopBackground"); + qmlRegisterType("org.kde.kwin", 3, 0, "WindowThumbnail"); + qmlRegisterType("org.kde.kwin", 3, 0, "DBusCall"); + qmlRegisterType("org.kde.kwin", 3, 0, "ScreenEdgeHandler"); + qmlRegisterType("org.kde.kwin", 3, 0, "ShortcutHandler"); + qmlRegisterType("org.kde.kwin", 3, 0, "SwipeGestureHandler"); + qmlRegisterType("org.kde.kwin", 3, 0, "PinchGestureHandler"); + qmlRegisterType("org.kde.kwin", 3, 0, "WindowModel"); + qmlRegisterType("org.kde.kwin", 3, 0, "WindowFilterModel"); + qmlRegisterType("org.kde.kwin", 3, 0, "VirtualDesktopModel"); + qmlRegisterUncreatableType("org.kde.kwin", 3, 0, "SceneView", QStringLiteral("Can't instantiate an object of type SceneView")); + qmlRegisterType("org.kde.kwin", 3, 0, "SceneEffect"); + + qmlRegisterSingletonType("org.kde.kwin", 3, 0, "Workspace", [](QQmlEngine *qmlEngine, QJSEngine *jsEngine) { + return new DeclarativeScriptWorkspaceWrapper(); + }); + qmlRegisterSingletonInstance("org.kde.kwin", 3, 0, "Options", options); + + qmlRegisterAnonymousType("org.kde.kwin", 3); + qmlRegisterAnonymousType("org.kde.kwin", 3); + qmlRegisterAnonymousType("org.kde.kwin", 3); + qmlRegisterAnonymousType("org.kde.kwin", 3); + qmlRegisterAnonymousType("org.kde.kwin", 3); + qmlRegisterAnonymousType("org.kde.kwin", 3); + // TODO: call the qml types as the C++ types? + qmlRegisterUncreatableType("org.kde.kwin", 3, 0, "CustomTile", QStringLiteral("Cannot create objects of type Tile")); + qmlRegisterUncreatableType("org.kde.kwin", 3, 0, "Tile", QStringLiteral("Cannot create objects of type AbstractTile")); +} + +void KWin::Scripting::start() +{ +#if 0 + // TODO make this threaded again once KConfigGroup is sufficiently thread safe, bug #305361 and friends + // perform querying for the services in a thread + QFutureWatcher *watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, &Scripting::slotScriptsQueried); + watcher->setFuture(QtConcurrent::run(this, &KWin::Scripting::queryScriptsToLoad, pluginStates, offers)); +#else + LoadScriptList scriptsToLoad = queryScriptsToLoad(); + for (LoadScriptList::const_iterator it = scriptsToLoad.constBegin(); + it != scriptsToLoad.constEnd(); + ++it) { + if (it->first) { + loadScript(it->second.first, it->second.second); + } else { + loadDeclarativeScript(it->second.first, it->second.second); + } + } + + runScripts(); +#endif +} + +LoadScriptList KWin::Scripting::queryScriptsToLoad() +{ + KSharedConfig::Ptr _config = kwinApp()->config(); + static bool s_started = false; + if (s_started) { + _config->reparseConfiguration(); + } else { + s_started = true; + } + QMap pluginStates = KConfigGroup(_config, QStringLiteral("Plugins")).entryMap(); + + const QStringList scriptFolders{ + QStringLiteral("kwin-wayland/scripts/"), + QStringLiteral("kwin/scripts/"), + }; + + LoadScriptList scriptsToLoad; + for (const QString &scriptFolder : scriptFolders) { + const auto offers = KPackage::PackageLoader::self()->listPackages(QStringLiteral("KWin/Script"), scriptFolder); + for (const KPluginMetaData &service : offers) { + const QString value = pluginStates.value(service.pluginId() + QLatin1String("Enabled"), QString()); + const bool enabled = value.isNull() ? service.isEnabledByDefault() : QVariant(value).toBool(); + const bool javaScript = service.value(QStringLiteral("X-Plasma-API")) == QLatin1String("javascript"); + const bool declarativeScript = service.value(QStringLiteral("X-Plasma-API")) == QLatin1String("declarativescript"); + if (!javaScript && !declarativeScript) { + continue; + } + + if (!enabled) { + if (isScriptLoaded(service.pluginId())) { + // unload the script + unloadScript(service.pluginId()); + } + continue; + } + const QString pluginName = service.pluginId(); + // The file we want to load depends on the specified API. We could check if one or the other file exists, but that is more error prone and causes IO overhead + const QString relScriptPath = scriptFolder + pluginName + QLatin1String("/contents/") + (javaScript ? QLatin1String("code/main.js") : QLatin1String("ui/main.qml")); + const QString file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, relScriptPath); + if (file.isEmpty()) { + qCDebug(KWIN_SCRIPTING) << "Could not find script file for " << pluginName; + continue; + } + scriptsToLoad << qMakePair(javaScript, qMakePair(file, pluginName)); + } + } + + return scriptsToLoad; +} + +void KWin::Scripting::slotScriptsQueried() +{ + QFutureWatcher *watcher = dynamic_cast *>(sender()); + if (!watcher) { + // slot invoked not from a FutureWatcher + return; + } + + LoadScriptList scriptsToLoad = watcher->result(); + for (LoadScriptList::const_iterator it = scriptsToLoad.constBegin(); + it != scriptsToLoad.constEnd(); + ++it) { + if (it->first) { + loadScript(it->second.first, it->second.second); + } else { + loadDeclarativeScript(it->second.first, it->second.second); + } + } + + runScripts(); + watcher->deleteLater(); +} + +bool KWin::Scripting::isScriptLoaded(const QString &pluginName) const +{ + return findScript(pluginName) != nullptr; +} + +KWin::AbstractScript *KWin::Scripting::findScript(const QString &pluginName) const +{ + QMutexLocker locker(m_scriptsLock.get()); + for (AbstractScript *script : std::as_const(scripts)) { + if (script->pluginName() == pluginName) { + return script; + } + } + return nullptr; +} + +bool KWin::Scripting::unloadScript(const QString &pluginName) +{ + QMutexLocker locker(m_scriptsLock.get()); + for (AbstractScript *script : std::as_const(scripts)) { + if (script->pluginName() == pluginName) { + script->deleteLater(); + return true; + } + } + return false; +} + +void KWin::Scripting::runScripts() +{ + QMutexLocker locker(m_scriptsLock.get()); + for (int i = 0; i < scripts.size(); i++) { + scripts.at(i)->run(); + } +} + +void KWin::Scripting::scriptDestroyed(QObject *object) +{ + QMutexLocker locker(m_scriptsLock.get()); + scripts.removeAll(static_cast(object)); +} + +int KWin::Scripting::loadScript(const QString &filePath, const QString &pluginName) +{ + QMutexLocker locker(m_scriptsLock.get()); + if (isScriptLoaded(pluginName)) { + return -1; + } + const int id = scripts.size(); + KWin::Script *script = new KWin::Script(id, filePath, pluginName, this); + connect(script, &QObject::destroyed, this, &Scripting::scriptDestroyed); + scripts.append(script); + return id; +} + +int KWin::Scripting::loadDeclarativeScript(const QString &filePath, const QString &pluginName) +{ + QMutexLocker locker(m_scriptsLock.get()); + if (isScriptLoaded(pluginName)) { + return -1; + } + const int id = scripts.size(); + KWin::DeclarativeScript *script = new KWin::DeclarativeScript(id, filePath, pluginName, this); + connect(script, &QObject::destroyed, this, &Scripting::scriptDestroyed); + scripts.append(script); + return id; +} + +KWin::Scripting::~Scripting() +{ + QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/Scripting")); + s_self = nullptr; +} + +QList KWin::Scripting::actionsForUserActionMenu(KWin::Window *c, QMenu *parent) +{ + QList actions; + for (AbstractScript *s : std::as_const(scripts)) { + // TODO: Allow declarative scripts to add their own user actions. + if (Script *script = qobject_cast